forked from ScoDoc/ScoDoc
Annotations étudiants: API et tests
This commit is contained in:
parent
052fb3c7b9
commit
b30ea5f5fd
@ -10,7 +10,7 @@
|
||||
from datetime import datetime
|
||||
from operator import attrgetter
|
||||
|
||||
from flask import g, request
|
||||
from flask import g, request, Response
|
||||
from flask_json import as_json
|
||||
from flask_login import current_user
|
||||
from flask_login import login_required
|
||||
@ -18,7 +18,7 @@ from sqlalchemy import desc, func, or_
|
||||
from sqlalchemy.dialects.postgresql import VARCHAR
|
||||
|
||||
import app
|
||||
from app import db
|
||||
from app import db, log
|
||||
from app.api import api_bp as bp, api_web_bp
|
||||
from app.api import tools
|
||||
from app.but import bulletin_but_court
|
||||
@ -26,6 +26,7 @@ from app.decorators import scodoc, permission_required
|
||||
from app.models import (
|
||||
Admission,
|
||||
Departement,
|
||||
EtudAnnotation,
|
||||
FormSemestreInscription,
|
||||
FormSemestre,
|
||||
Identite,
|
||||
@ -54,6 +55,32 @@ import app.scodoc.sco_utils as scu
|
||||
#
|
||||
|
||||
|
||||
def _get_etud_by_code(
|
||||
code_type: str, code: str, dept: Departement
|
||||
) -> tuple[bool, Response | Identite]:
|
||||
"""Get etud, using etudid, NIP or INE
|
||||
Returns True, etud if ok, or False, error response.
|
||||
"""
|
||||
if code_type == "nip":
|
||||
query = Identite.query.filter_by(code_nip=code)
|
||||
elif code_type == "etudid":
|
||||
try:
|
||||
etudid = int(code)
|
||||
except ValueError:
|
||||
return False, json_error(404, "invalid etudid type")
|
||||
query = Identite.query.filter_by(id=etudid)
|
||||
elif code_type == "ine":
|
||||
query = Identite.query.filter_by(code_ine=code)
|
||||
else:
|
||||
return False, json_error(404, "invalid code_type")
|
||||
if dept:
|
||||
query = query.filter_by(dept_id=dept.id)
|
||||
etud = query.first()
|
||||
if etud is None:
|
||||
return False, json_error(404, message="etudiant inexistant")
|
||||
return True, etud
|
||||
|
||||
|
||||
@bp.route("/etudiants/courants", defaults={"long": False})
|
||||
@bp.route("/etudiants/courants/long", defaults={"long": True})
|
||||
@api_web_bp.route("/etudiants/courants", defaults={"long": False})
|
||||
@ -389,28 +416,15 @@ def bulletin(
|
||||
pdf = True
|
||||
if version not in scu.BULLETINS_VERSIONS_BUT:
|
||||
return json_error(404, "version invalide")
|
||||
# return f"{code_type}={code}, version={version}, pdf={pdf}"
|
||||
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first_or_404()
|
||||
dept = Departement.query.filter_by(id=formsemestre.dept_id).first_or_404()
|
||||
if g.scodoc_dept and dept.acronym != g.scodoc_dept:
|
||||
return json_error(404, "formsemestre inexistant")
|
||||
app.set_sco_dept(dept.acronym)
|
||||
|
||||
if code_type == "nip":
|
||||
query = Identite.query.filter_by(code_nip=code, dept_id=dept.id)
|
||||
elif code_type == "etudid":
|
||||
try:
|
||||
etudid = int(code)
|
||||
except ValueError:
|
||||
return json_error(404, "invalid etudid type")
|
||||
query = Identite.query.filter_by(id=etudid)
|
||||
elif code_type == "ine":
|
||||
query = Identite.query.filter_by(code_ine=code, dept_id=dept.id)
|
||||
else:
|
||||
return json_error(404, "invalid code_type")
|
||||
etud = query.first()
|
||||
if etud is None:
|
||||
return json_error(404, message="etudiant inexistant")
|
||||
ok, etud = _get_etud_by_code(code_type, code, dept)
|
||||
if not ok:
|
||||
return etud # json error
|
||||
|
||||
if version == "butcourt":
|
||||
if pdf:
|
||||
@ -562,26 +576,15 @@ def etudiant_create(force=False):
|
||||
@api_web_bp.route("/etudiant/<string:code_type>/<string:code>/edit", methods=["POST"])
|
||||
@scodoc
|
||||
@permission_required(Permission.EtudInscrit)
|
||||
@as_json
|
||||
def etudiant_edit(
|
||||
code_type: str = "etudid",
|
||||
code: str = None,
|
||||
):
|
||||
"""Edition des données étudiant (identité, admission, adresses)"""
|
||||
if code_type == "nip":
|
||||
query = Identite.query.filter_by(code_nip=code)
|
||||
elif code_type == "etudid":
|
||||
try:
|
||||
etudid = int(code)
|
||||
except ValueError:
|
||||
return json_error(404, "invalid etudid type")
|
||||
query = Identite.query.filter_by(id=etudid)
|
||||
elif code_type == "ine":
|
||||
query = Identite.query.filter_by(code_ine=code)
|
||||
else:
|
||||
return json_error(404, "invalid code_type")
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
etud: Identite = query.first()
|
||||
ok, etud = _get_etud_by_code(code_type, code, g.scodoc_dept)
|
||||
if not ok:
|
||||
return etud # json error
|
||||
#
|
||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
||||
etud.from_dict(args)
|
||||
@ -604,3 +607,67 @@ def etudiant_edit(
|
||||
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
||||
r = etud.to_dict_api(restrict=restrict)
|
||||
return r
|
||||
|
||||
|
||||
@bp.route("/etudiant/<string:code_type>/<string:code>/annotation", methods=["POST"])
|
||||
@api_web_bp.route(
|
||||
"/etudiant/<string:code_type>/<string:code>/annotation", methods=["POST"]
|
||||
)
|
||||
@scodoc
|
||||
@permission_required(Permission.EtudInscrit) # il faut en plus ViewEtudData
|
||||
@as_json
|
||||
def etudiant_annotation(
|
||||
code_type: str = "etudid",
|
||||
code: str = None,
|
||||
):
|
||||
"""Ajout d'une annotation sur un étudiant"""
|
||||
if not current_user.has_permission(Permission.ViewEtudData):
|
||||
return json_error(403, "non autorisé (manque ViewEtudData)")
|
||||
ok, etud = _get_etud_by_code(code_type, code, g.scodoc_dept)
|
||||
if not ok:
|
||||
return etud # json error
|
||||
#
|
||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
||||
comment = args.get("comment", None)
|
||||
if not isinstance(comment, str):
|
||||
return json_error(404, "invalid comment (expected string)")
|
||||
if len(comment) > scu.MAX_TEXT_LEN:
|
||||
return json_error(404, "invalid comment (too large)")
|
||||
annotation = EtudAnnotation(comment=comment, author=current_user.user_name)
|
||||
etud.annotations.append(annotation)
|
||||
db.session.add(etud)
|
||||
db.session.commit()
|
||||
log(f"etudiant_annotation/{etud.id}/{annotation.id}")
|
||||
return annotation.to_dict()
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/etudiant/<string:code_type>/<string:code>/annotation/<int:annotation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/<string:code_type>/<string:code>/annotation/<int:annotation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@as_json
|
||||
@permission_required(Permission.EtudInscrit)
|
||||
def etudiant_annotation_delete(
|
||||
code_type: str = "etudid", code: str = None, annotation_id: int = None
|
||||
):
|
||||
"""
|
||||
Suppression d'une annotation
|
||||
"""
|
||||
ok, etud = _get_etud_by_code(code_type, code, g.scodoc_dept)
|
||||
if not ok:
|
||||
return etud # json error
|
||||
annotation = EtudAnnotation.query.filter_by(
|
||||
etudid=etud.id, id=annotation_id
|
||||
).first()
|
||||
if annotation is None:
|
||||
return json_error(404, "annotation not found")
|
||||
log(f"etudiant_annotation_delete/{etud.id}/{annotation.id}")
|
||||
db.session.delete(annotation)
|
||||
db.session.commit()
|
||||
return "ok"
|
||||
|
@ -101,7 +101,12 @@ class Identite(models.ScoDocModel):
|
||||
adresses = db.relationship(
|
||||
"Adresse", back_populates="etud", cascade="all,delete", lazy="dynamic"
|
||||
)
|
||||
|
||||
annotations = db.relationship(
|
||||
"EtudAnnotation",
|
||||
backref="etudiant",
|
||||
cascade="all, delete-orphan",
|
||||
lazy="dynamic",
|
||||
)
|
||||
billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic")
|
||||
#
|
||||
dispense_ues = db.relationship(
|
||||
@ -477,9 +482,9 @@ class Identite(models.ScoDocModel):
|
||||
"civilite": self.civilite,
|
||||
"code_ine": self.code_ine or "",
|
||||
"code_nip": self.code_nip or "",
|
||||
"date_naissance": self.date_naissance.strftime("%d/%m/%Y")
|
||||
if self.date_naissance
|
||||
else "",
|
||||
"date_naissance": (
|
||||
self.date_naissance.strftime("%d/%m/%Y") if self.date_naissance else ""
|
||||
),
|
||||
"dept_acronym": self.departement.acronym,
|
||||
"dept_id": self.dept_id,
|
||||
"dept_naissance": self.dept_naissance or "",
|
||||
@ -519,12 +524,16 @@ class Identite(models.ScoDocModel):
|
||||
e.pop("departement", None)
|
||||
e["sort_key"] = self.sort_key
|
||||
if with_annotations:
|
||||
e["annotations"] = [
|
||||
annot.to_dict(restrict=restrict)
|
||||
for annot in EtudAnnotation.query.filter_by(etudid=self.id).order_by(
|
||||
desc(EtudAnnotation.date)
|
||||
)
|
||||
]
|
||||
e["annotations"] = (
|
||||
[
|
||||
annot.to_dict()
|
||||
for annot in EtudAnnotation.query.filter_by(
|
||||
etudid=self.id
|
||||
).order_by(desc(EtudAnnotation.date))
|
||||
]
|
||||
if not restrict
|
||||
else []
|
||||
)
|
||||
if restrict:
|
||||
# Met à None les attributs protégés:
|
||||
for attr in self.protected_attrs:
|
||||
@ -1079,18 +1088,14 @@ class EtudAnnotation(db.Model):
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
etudid = db.Column(db.Integer) # sans contrainte (compat ScoDoc 7))
|
||||
etudid = db.Column(db.Integer, db.ForeignKey(Identite.id))
|
||||
author = db.Column(db.Text) # le pseudo (user_name), was zope_authenticated_user
|
||||
comment = db.Column(db.Text)
|
||||
|
||||
protected_attrs = {"comment"}
|
||||
|
||||
def to_dict(self, restrict=False):
|
||||
"""Représentation dictionnaire. Si restrict, filtre les champs protégés (RGPD)."""
|
||||
def to_dict(self):
|
||||
"""Représentation dictionnaire."""
|
||||
e = dict(self.__dict__)
|
||||
e.pop("_sa_instance_state", None)
|
||||
if restrict:
|
||||
e = {k: v for (k, v) in e.items() if k not in self.protected_attrs}
|
||||
return e
|
||||
|
||||
|
||||
|
@ -204,8 +204,8 @@ def test_etudiants(api_headers):
|
||||
|
||||
all_unique = True
|
||||
list_ids = [etud["id"] for etud in etud_nip]
|
||||
for id in list_ids:
|
||||
if list_ids.count(id) > 1:
|
||||
for etudid in list_ids:
|
||||
if list_ids.count(etudid) > 1:
|
||||
all_unique = False
|
||||
assert all_unique is True
|
||||
|
||||
@ -226,8 +226,8 @@ def test_etudiants(api_headers):
|
||||
|
||||
all_unique = True
|
||||
list_ids = [etud["id"] for etud in etud_ine]
|
||||
for id in list_ids:
|
||||
if list_ids.count(id) > 1:
|
||||
for etudid in list_ids:
|
||||
if list_ids.count(etudid) > 1:
|
||||
all_unique = False
|
||||
assert all_unique is True
|
||||
|
||||
@ -267,6 +267,53 @@ def test_etudiants_by_name(api_headers):
|
||||
assert etuds[0]["nom"] == "RÉGNIER"
|
||||
|
||||
|
||||
def test_etudiant_annotations(api_headers):
|
||||
"""
|
||||
Routes:
|
||||
POST /etudiant/etudid/<int:etudid>/annotation
|
||||
GET /etudiant/etudid/<int:etudid>[/long]
|
||||
"""
|
||||
admin_header = get_auth_headers(API_USER_ADMIN, API_PASSWORD_ADMIN)
|
||||
args = {
|
||||
"prenom": "Annoté",
|
||||
"nom": "Bach A",
|
||||
"dept": DEPT_ACRONYM,
|
||||
"civilite": "M",
|
||||
}
|
||||
etud = POST_JSON(
|
||||
"/etudiant/create",
|
||||
args,
|
||||
headers=admin_header,
|
||||
)
|
||||
assert etud["nom"] == args["nom"].upper()
|
||||
etudid = etud["id"]
|
||||
# récupère annotation (liste vide)
|
||||
etud = GET(f"/etudiant/etudid/{etudid}", headers=api_headers)
|
||||
assert etud["nom"]
|
||||
assert etud["annotations"] == []
|
||||
# ajoute annotation
|
||||
annotation = POST_JSON(
|
||||
f"/etudiant/etudid/{etudid}/annotation",
|
||||
{"comment": "annotation 1"},
|
||||
headers=admin_header,
|
||||
)
|
||||
assert annotation
|
||||
annotation_id = annotation["id"]
|
||||
etud = GET(f"/etudiant/etudid/{etudid}", headers=api_headers)
|
||||
assert len(etud["annotations"]) == 0 # pas le droit => cachée
|
||||
etud = GET(f"/etudiant/etudid/{etudid}", headers=admin_header)
|
||||
assert len(etud["annotations"]) == 1 # ok avec admin
|
||||
assert etud["annotations"][0]["comment"] == "annotation 1"
|
||||
assert etud["annotations"][0]["id"] == annotation_id
|
||||
# Supprime annotation
|
||||
POST_JSON(
|
||||
f"/etudiant/etudid/{etudid}/annotation/{annotation_id}/delete",
|
||||
headers=admin_header,
|
||||
)
|
||||
etud = GET(f"/etudiant/etudid/{etudid}", headers=api_headers)
|
||||
assert len(etud["annotations"]) == 0
|
||||
|
||||
|
||||
def test_etudiant_photo(api_headers):
|
||||
"""
|
||||
Routes : /etudiant/etudid/<int:etudid>/photo en GET et en POST
|
||||
|
@ -15,7 +15,14 @@ from flask import current_app
|
||||
|
||||
import app
|
||||
from app import db
|
||||
from app.models import Admission, Adresse, Departement, FormSemestre, Identite
|
||||
from app.models import (
|
||||
Admission,
|
||||
Adresse,
|
||||
Departement,
|
||||
EtudAnnotation,
|
||||
FormSemestre,
|
||||
Identite,
|
||||
)
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_find_etud, sco_import_etuds
|
||||
from config import TestConfig
|
||||
@ -116,6 +123,32 @@ def test_etat_civil(test_client):
|
||||
assert e_d["ne"] == "(e)"
|
||||
|
||||
|
||||
def test_etud_annotations(test_client):
|
||||
"Test ajout/suppression annotations"
|
||||
dept = Departement.query.first()
|
||||
args = {"nom": "nom_a", "prenom": "prénom_a", "civilite": "M", "dept_id": dept.id}
|
||||
etud = Identite.create_etud(**args)
|
||||
db.session.flush()
|
||||
# Ajout annotations
|
||||
etud.annotations.append(EtudAnnotation(comment="annotation 1"))
|
||||
etud.annotations.append(EtudAnnotation(comment="annotation 2"))
|
||||
db.session.add(etud)
|
||||
db.session.commit()
|
||||
assert etud.annotations.count() == 2
|
||||
# Suppression de la première
|
||||
a = etud.annotations.first()
|
||||
assert a.comment == "annotation 1"
|
||||
db.session.delete(a)
|
||||
db.session.commit()
|
||||
assert db.session.get(Identite, etud.id)
|
||||
assert etud.annotations.count() == 1
|
||||
# Suppression de l'étudiant (cascade)
|
||||
a2_id = etud.annotations.first().id
|
||||
db.session.delete(etud)
|
||||
db.session.commit()
|
||||
assert db.session.get(EtudAnnotation, a2_id) is None
|
||||
|
||||
|
||||
def test_etud_legacy(test_client):
|
||||
"Test certaines fonctions scodoc7 (sco_etud)"
|
||||
dept = Departement.query.first()
|
||||
|
Loading…
Reference in New Issue
Block a user