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 datetime import datetime
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
|
||||||
from flask import g, request
|
from flask import g, request, Response
|
||||||
from flask_json import as_json
|
from flask_json import as_json
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
@ -18,7 +18,7 @@ from sqlalchemy import desc, func, or_
|
|||||||
from sqlalchemy.dialects.postgresql import VARCHAR
|
from sqlalchemy.dialects.postgresql import VARCHAR
|
||||||
|
|
||||||
import app
|
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 api_bp as bp, api_web_bp
|
||||||
from app.api import tools
|
from app.api import tools
|
||||||
from app.but import bulletin_but_court
|
from app.but import bulletin_but_court
|
||||||
@ -26,6 +26,7 @@ from app.decorators import scodoc, permission_required
|
|||||||
from app.models import (
|
from app.models import (
|
||||||
Admission,
|
Admission,
|
||||||
Departement,
|
Departement,
|
||||||
|
EtudAnnotation,
|
||||||
FormSemestreInscription,
|
FormSemestreInscription,
|
||||||
FormSemestre,
|
FormSemestre,
|
||||||
Identite,
|
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", defaults={"long": False})
|
||||||
@bp.route("/etudiants/courants/long", defaults={"long": True})
|
@bp.route("/etudiants/courants/long", defaults={"long": True})
|
||||||
@api_web_bp.route("/etudiants/courants", defaults={"long": False})
|
@api_web_bp.route("/etudiants/courants", defaults={"long": False})
|
||||||
@ -389,28 +416,15 @@ def bulletin(
|
|||||||
pdf = True
|
pdf = True
|
||||||
if version not in scu.BULLETINS_VERSIONS_BUT:
|
if version not in scu.BULLETINS_VERSIONS_BUT:
|
||||||
return json_error(404, "version invalide")
|
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()
|
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first_or_404()
|
||||||
dept = Departement.query.filter_by(id=formsemestre.dept_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:
|
if g.scodoc_dept and dept.acronym != g.scodoc_dept:
|
||||||
return json_error(404, "formsemestre inexistant")
|
return json_error(404, "formsemestre inexistant")
|
||||||
app.set_sco_dept(dept.acronym)
|
app.set_sco_dept(dept.acronym)
|
||||||
|
|
||||||
if code_type == "nip":
|
ok, etud = _get_etud_by_code(code_type, code, dept)
|
||||||
query = Identite.query.filter_by(code_nip=code, dept_id=dept.id)
|
if not ok:
|
||||||
elif code_type == "etudid":
|
return etud # json error
|
||||||
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")
|
|
||||||
|
|
||||||
if version == "butcourt":
|
if version == "butcourt":
|
||||||
if pdf:
|
if pdf:
|
||||||
@ -562,26 +576,15 @@ def etudiant_create(force=False):
|
|||||||
@api_web_bp.route("/etudiant/<string:code_type>/<string:code>/edit", methods=["POST"])
|
@api_web_bp.route("/etudiant/<string:code_type>/<string:code>/edit", methods=["POST"])
|
||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.EtudInscrit)
|
@permission_required(Permission.EtudInscrit)
|
||||||
|
@as_json
|
||||||
def etudiant_edit(
|
def etudiant_edit(
|
||||||
code_type: str = "etudid",
|
code_type: str = "etudid",
|
||||||
code: str = None,
|
code: str = None,
|
||||||
):
|
):
|
||||||
"""Edition des données étudiant (identité, admission, adresses)"""
|
"""Edition des données étudiant (identité, admission, adresses)"""
|
||||||
if code_type == "nip":
|
ok, etud = _get_etud_by_code(code_type, code, g.scodoc_dept)
|
||||||
query = Identite.query.filter_by(code_nip=code)
|
if not ok:
|
||||||
elif code_type == "etudid":
|
return etud # json error
|
||||||
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()
|
|
||||||
#
|
#
|
||||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
args = request.get_json(force=True) # may raise 400 Bad Request
|
||||||
etud.from_dict(args)
|
etud.from_dict(args)
|
||||||
@ -604,3 +607,67 @@ def etudiant_edit(
|
|||||||
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
||||||
r = etud.to_dict_api(restrict=restrict)
|
r = etud.to_dict_api(restrict=restrict)
|
||||||
return r
|
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(
|
adresses = db.relationship(
|
||||||
"Adresse", back_populates="etud", cascade="all,delete", lazy="dynamic"
|
"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")
|
billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic")
|
||||||
#
|
#
|
||||||
dispense_ues = db.relationship(
|
dispense_ues = db.relationship(
|
||||||
@ -477,9 +482,9 @@ class Identite(models.ScoDocModel):
|
|||||||
"civilite": self.civilite,
|
"civilite": self.civilite,
|
||||||
"code_ine": self.code_ine or "",
|
"code_ine": self.code_ine or "",
|
||||||
"code_nip": self.code_nip or "",
|
"code_nip": self.code_nip or "",
|
||||||
"date_naissance": self.date_naissance.strftime("%d/%m/%Y")
|
"date_naissance": (
|
||||||
if self.date_naissance
|
self.date_naissance.strftime("%d/%m/%Y") if self.date_naissance else ""
|
||||||
else "",
|
),
|
||||||
"dept_acronym": self.departement.acronym,
|
"dept_acronym": self.departement.acronym,
|
||||||
"dept_id": self.dept_id,
|
"dept_id": self.dept_id,
|
||||||
"dept_naissance": self.dept_naissance or "",
|
"dept_naissance": self.dept_naissance or "",
|
||||||
@ -519,12 +524,16 @@ class Identite(models.ScoDocModel):
|
|||||||
e.pop("departement", None)
|
e.pop("departement", None)
|
||||||
e["sort_key"] = self.sort_key
|
e["sort_key"] = self.sort_key
|
||||||
if with_annotations:
|
if with_annotations:
|
||||||
e["annotations"] = [
|
e["annotations"] = (
|
||||||
annot.to_dict(restrict=restrict)
|
[
|
||||||
for annot in EtudAnnotation.query.filter_by(etudid=self.id).order_by(
|
annot.to_dict()
|
||||||
desc(EtudAnnotation.date)
|
for annot in EtudAnnotation.query.filter_by(
|
||||||
)
|
etudid=self.id
|
||||||
|
).order_by(desc(EtudAnnotation.date))
|
||||||
]
|
]
|
||||||
|
if not restrict
|
||||||
|
else []
|
||||||
|
)
|
||||||
if restrict:
|
if restrict:
|
||||||
# Met à None les attributs protégés:
|
# Met à None les attributs protégés:
|
||||||
for attr in self.protected_attrs:
|
for attr in self.protected_attrs:
|
||||||
@ -1079,18 +1088,14 @@ class EtudAnnotation(db.Model):
|
|||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
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
|
author = db.Column(db.Text) # le pseudo (user_name), was zope_authenticated_user
|
||||||
comment = db.Column(db.Text)
|
comment = db.Column(db.Text)
|
||||||
|
|
||||||
protected_attrs = {"comment"}
|
def to_dict(self):
|
||||||
|
"""Représentation dictionnaire."""
|
||||||
def to_dict(self, restrict=False):
|
|
||||||
"""Représentation dictionnaire. Si restrict, filtre les champs protégés (RGPD)."""
|
|
||||||
e = dict(self.__dict__)
|
e = dict(self.__dict__)
|
||||||
e.pop("_sa_instance_state", None)
|
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
|
return e
|
||||||
|
|
||||||
|
|
||||||
|
@ -204,8 +204,8 @@ def test_etudiants(api_headers):
|
|||||||
|
|
||||||
all_unique = True
|
all_unique = True
|
||||||
list_ids = [etud["id"] for etud in etud_nip]
|
list_ids = [etud["id"] for etud in etud_nip]
|
||||||
for id in list_ids:
|
for etudid in list_ids:
|
||||||
if list_ids.count(id) > 1:
|
if list_ids.count(etudid) > 1:
|
||||||
all_unique = False
|
all_unique = False
|
||||||
assert all_unique is True
|
assert all_unique is True
|
||||||
|
|
||||||
@ -226,8 +226,8 @@ def test_etudiants(api_headers):
|
|||||||
|
|
||||||
all_unique = True
|
all_unique = True
|
||||||
list_ids = [etud["id"] for etud in etud_ine]
|
list_ids = [etud["id"] for etud in etud_ine]
|
||||||
for id in list_ids:
|
for etudid in list_ids:
|
||||||
if list_ids.count(id) > 1:
|
if list_ids.count(etudid) > 1:
|
||||||
all_unique = False
|
all_unique = False
|
||||||
assert all_unique is True
|
assert all_unique is True
|
||||||
|
|
||||||
@ -267,6 +267,53 @@ def test_etudiants_by_name(api_headers):
|
|||||||
assert etuds[0]["nom"] == "RÉGNIER"
|
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):
|
def test_etudiant_photo(api_headers):
|
||||||
"""
|
"""
|
||||||
Routes : /etudiant/etudid/<int:etudid>/photo en GET et en POST
|
Routes : /etudiant/etudid/<int:etudid>/photo en GET et en POST
|
||||||
|
@ -15,7 +15,14 @@ from flask import current_app
|
|||||||
|
|
||||||
import app
|
import app
|
||||||
from app import db
|
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_etud
|
||||||
from app.scodoc import sco_find_etud, sco_import_etuds
|
from app.scodoc import sco_find_etud, sco_import_etuds
|
||||||
from config import TestConfig
|
from config import TestConfig
|
||||||
@ -116,6 +123,32 @@ def test_etat_civil(test_client):
|
|||||||
assert e_d["ne"] == "(e)"
|
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):
|
def test_etud_legacy(test_client):
|
||||||
"Test certaines fonctions scodoc7 (sco_etud)"
|
"Test certaines fonctions scodoc7 (sco_etud)"
|
||||||
dept = Departement.query.first()
|
dept = Departement.query.first()
|
||||||
|
Loading…
Reference in New Issue
Block a user