From e634b50d56419372efb929bb1f0f1edf56c12741 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 23 Nov 2023 17:08:18 +0100 Subject: [PATCH] API: create/edit etudiant, admission, adresse --- app/api/etudiants.py | 67 ++++++++++++++++++++++++++++++++- app/models/etudiants.py | 31 ++++++++++----- app/scodoc/sco_etud.py | 2 +- sco_version.py | 2 +- tests/api/test_api_etudiants.py | 62 ++++++++++++++++++++++++++++++ 5 files changed, 152 insertions(+), 12 deletions(-) diff --git a/app/api/etudiants.py b/app/api/etudiants.py index 03dc217d4..e460f29c2 100755 --- a/app/api/etudiants.py +++ b/app/api/etudiants.py @@ -25,6 +25,7 @@ from app.but import bulletin_but_court from app.decorators import scodoc, permission_required from app.models import ( Admission, + Adresse, Departement, FormSemestreInscription, FormSemestre, @@ -513,6 +514,20 @@ def etudiant_create(force=False): 400, f"{len(homonyms)} homonymes détectés. Vous pouvez utiliser /force." ) etud = Identite.create_etud(**args) + db.session.flush() + # --- Données admission + admission_args = args.get("admission", None) + if admission_args: + etud.admission.from_dict(admission_args) + # --- Adresse + adresses = args.get("adresses", []) + if adresses: + # ne prend en compte que la première adresse + # car si la base est concue pour avoir plusieurs adresses par étudiant, + # l'application n'en gère plus qu'une seule. + adresse = etud.adresses.first() + adresse.from_dict(adresses[0]) + # Poste une nouvelle dans le département concerné: ScolarNews.add( typ=ScolarNews.NEWS_INSCR, @@ -522,4 +537,54 @@ def etudiant_create(force=False): dept_id=dept_o.id, ) db.session.commit() - return etud.to_dict_short() + # Note: je ne comprends pas pourquoi un refresh est nécessaire ici + # sans ce refresh, etud.__dict__ est incomplet (pas de 'nom'). + db.session.refresh(etud) + r = etud.to_dict_api() + return r + + +@bp.route("/etudiant///edit", methods=["POST"]) +@scodoc +@permission_required(Permission.EtudInscrit) +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() + # + args = request.get_json(force=True) # may raise 400 Bad Request + etud.from_dict(args) + admission_args = args.get("admission", None) + if admission_args: + etud.admission.from_dict(admission_args) + # --- Adresse + adresses = args.get("adresses", []) + if adresses: + # ne prend en compte que la première adresse + # car si la base est concue pour avoir plusieurs adresses par étudiant, + # l'application n'en gère plus qu'une seule. + adresse = etud.adresses.first() + adresse.from_dict(adresses[0]) + + db.session.commit() + # Note: je ne comprends pas pourquoi un refresh est nécessaire ici + # sans ce refresh, etud.__dict__ est incomplet (pas de 'nom'). + db.session.refresh(etud) + r = etud.to_dict_api() + return r diff --git a/app/models/etudiants.py b/app/models/etudiants.py index 192d7ff0c..d0344166c 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -15,6 +15,7 @@ from sqlalchemy import desc, text from app import db, log from app import models +from app.models.departements import Departement from app.models.scolar_event import ScolarEvent from app.scodoc import notesdb as ndb from app.scodoc.sco_bac import Baccalaureat @@ -204,14 +205,22 @@ class Identite(db.Model, models.ScoDocModel): return cls.create_from_dict(args) @classmethod - def create_from_dict(cls, data) -> "Identite": + def create_from_dict(cls, args) -> "Identite": """Crée un étudiant à partir d'un dict, avec admission et adresse vides. + If required dept_id or dept are not specified, set it to the current dept. + args: dict with args in application. + Les clés adresses et admission ne SONT PAS utilisées. (added to session but not flushed nor commited) """ - etud: Identite = super(cls, cls).create_from_dict(data) - if (data.get("admission_id", None) is None) and ( - data.get("admission", None) is None - ): + if not "dept_id" in args: + if "dept" in args: + departement = Departement.query.filter_by(acronym=args["dept"]).first() + if departement: + args["dept_id"] = departement.id + if not "dept_id" in args: + args["dept_id"] = g.scodoc_dept_id + etud: Identite = super().create_from_dict(args) + if args.get("admission_id", None) is None: etud.admission = Admission() etud.adresses.append(Adresse(typeadresse="domicile")) db.session.flush() @@ -221,6 +230,14 @@ class Identite(db.Model, models.ScoDocModel): log(f"Identite.create {etud}") return etud + @classmethod + def filter_model_attributes(cls, data: dict, excluded: set[str] = None) -> dict: + """Returns a copy of dict with only the keys belonging to the Model and not in excluded.""" + return super().filter_model_attributes( + data, + excluded=(excluded or set()) | {"adresses", "admission", "departement"}, + ) + @property def civilite_str(self) -> str: """returns civilité usuelle: 'M.' ou 'Mme' ou '' (pour le genre neutre, @@ -329,8 +346,6 @@ class Identite(db.Model, models.ScoDocModel): @classmethod def convert_dict_fields(cls, args: dict) -> dict: """Convert fields in the given dict. No other side effect. - If required dept_id is not specified, set it to the current dept. - args: dict with args in application. returns: dict to store in model's db. """ # Les champs qui sont toujours stockés en majuscules: @@ -349,8 +364,6 @@ class Identite(db.Model, models.ScoDocModel): "code_ine", } args_dict = {} - if not "dept_id" in args: - args["dept_id"] = g.scodoc_dept_id for key, value in args.items(): if hasattr(cls, key) and not isinstance(getattr(cls, key, None), property): # compat scodoc7 (mauvaise idée de l'époque) diff --git a/app/scodoc/sco_etud.py b/app/scodoc/sco_etud.py index 9bd09e899..ea7de19c4 100644 --- a/app/scodoc/sco_etud.py +++ b/app/scodoc/sco_etud.py @@ -624,7 +624,7 @@ def create_etud(cnx, args: dict = None): ScolarNews.add( typ=ScolarNews.NEWS_INSCR, text=f"Nouvel étudiant {etud.html_link_fiche()}", - url=etud["url"], + url=etud_dict["url"], max_frequency=0, ) return etud_dict diff --git a/sco_version.py b/sco_version.py index 21b533cd9..f2743e433 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.60" +SCOVERSION = "9.6.61" SCONAME = "ScoDoc" diff --git a/tests/api/test_api_etudiants.py b/tests/api/test_api_etudiants.py index bc4563cbe..ab29e432e 100644 --- a/tests/api/test_api_etudiants.py +++ b/tests/api/test_api_etudiants.py @@ -29,6 +29,7 @@ from tests.api.setup_test_api import ( API_USER_ADMIN, CHECK_CERTIFICATE, DEPT_ACRONYM, + GET, POST_JSON, get_auth_headers, ) @@ -934,6 +935,16 @@ def test_etudiant_create(api_headers): "nom": "Bach", "dept": DEPT_ACRONYM, "civilite": "M", + "admission": { + "commentaire": "test", + "annee_bac": 2024, + }, + "adresses": [ + { + "villedomicile": "Santa Teresa", + "emailperso": "XXX@2666.mx", + } + ], } etud = POST_JSON( "/etudiant/create", @@ -941,3 +952,54 @@ def test_etudiant_create(api_headers): headers=admin_header, ) assert etud["nom"] == args["nom"].upper() + assert etud["admission"]["commentaire"] == args["admission"]["commentaire"] + assert etud["admission"]["annee_bac"] == args["admission"]["annee_bac"] + assert len(etud["adresses"]) == 1 + assert etud["adresses"][0]["villedomicile"] == args["adresses"][0]["villedomicile"] + assert etud["adresses"][0]["emailperso"] == args["adresses"][0]["emailperso"] + etudid = etud["id"] + # On recommence avec une nouvelle requête: + etud = GET(f"/etudiant/etudid/{etudid}", headers=api_headers) + assert etud["nom"] == args["nom"].upper() + assert etud["admission"]["commentaire"] == args["admission"]["commentaire"] + assert etud["admission"]["annee_bac"] == args["admission"]["annee_bac"] + assert len(etud["adresses"]) == 1 + assert etud["adresses"][0]["villedomicile"] == args["adresses"][0]["villedomicile"] + assert etud["adresses"][0]["emailperso"] == args["adresses"][0]["emailperso"] + # Edition + etud = POST_JSON( + f"/etudiant/etudid/{etudid}/edit", + { + "civilite": "F", + }, + headers=admin_header, + ) + assert etud["civilite"] == "F" + assert etud["nom"] == args["nom"].upper() + assert etud["admission"]["commentaire"] == args["admission"]["commentaire"] + assert etud["admission"]["annee_bac"] == args["admission"]["annee_bac"] + assert len(etud["adresses"]) == 1 + assert etud["adresses"][0]["villedomicile"] == args["adresses"][0]["villedomicile"] + assert etud["adresses"][0]["emailperso"] == args["adresses"][0]["emailperso"] + etud = POST_JSON( + f"/etudiant/etudid/{etudid}/edit", + { + "adresses": [ + { + "villedomicile": "Barcelona", + }, + ], + }, + headers=admin_header, + ) + assert etud["adresses"][0]["villedomicile"] == "Barcelona" + etud = POST_JSON( + f"/etudiant/etudid/{etudid}/edit", + { + "admission": { + "commentaire": "un nouveau commentaire", + }, + }, + headers=admin_header, + ) + assert etud["admission"]["commentaire"] == "un nouveau commentaire"