From 2377918b54cd12170b9e3b686150012b14e2a9eb Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 22 Nov 2023 23:31:16 +0100 Subject: [PATCH] API: etudiant/create (WIP), refactoring. --- app/api/etudiants.py | 48 ++++++++++++ app/auth/models.py | 15 ++-- app/entreprises/__init__.py | 5 +- app/entreprises/routes.py | 6 +- app/models/__init__.py | 4 +- app/models/etudiants.py | 16 +++- app/models/events.py | 7 +- app/models/scolar_event.py | 48 ++++++++++++ app/models/validations.py | 46 +----------- app/scodoc/sco_etud.py | 82 ++++----------------- app/scodoc/sco_find_etud.py | 5 +- app/scodoc/sco_formsemestre_inscriptions.py | 16 ++-- app/scodoc/sco_groups.py | 9 ++- app/scodoc/sco_prepajury.py | 4 +- app/scodoc/sco_saisie_notes.py | 2 +- app/scodoc/sco_trombino.py | 24 +++--- app/scodoc/sco_trombino_doc.py | 2 +- app/scodoc/sco_trombino_tours.py | 10 +-- app/scodoc/sco_utils.py | 57 ++++++++++++++ tests/api/test_api_etudiants.py | 20 ++++- 20 files changed, 252 insertions(+), 174 deletions(-) create mode 100644 app/models/scolar_event.py diff --git a/app/api/etudiants.py b/app/api/etudiants.py index 264271cd..03dc217d 100755 --- a/app/api/etudiants.py +++ b/app/api/etudiants.py @@ -18,6 +18,7 @@ from sqlalchemy import desc, func, or_ from sqlalchemy.dialects.postgresql import VARCHAR import app +from app import db from app.api import api_bp as bp, api_web_bp from app.api import tools from app.but import bulletin_but_court @@ -28,10 +29,12 @@ from app.models import ( FormSemestreInscription, FormSemestre, Identite, + ScolarNews, ) from app.scodoc import sco_bulletins from app.scodoc import sco_groups from app.scodoc.sco_bulletins import do_formsemestre_bulletinetud +from app.scodoc import sco_etud from app.scodoc.sco_permissions import Permission from app.scodoc.sco_utils import json_error, suppress_accents @@ -475,3 +478,48 @@ def etudiant_groups(formsemestre_id: int, etudid: int = None): data = sco_groups.get_etud_groups(etud.id, formsemestre.id) return data + + +@bp.route("/etudiant/create", methods=["POST"], defaults={"force": False}) +@bp.route("/etudiant/create/force", methods=["POST"], defaults={"force": True}) +@scodoc +@permission_required(Permission.EtudInscrit) +@as_json +def etudiant_create(force=False): + """Création d'un nouvel étudiant + Si force, crée même si homonymie détectée. + L'étudiant créé n'est pas inscrit à un semestre. + Champs requis: nom, prenom (sauf si config sans prénom), dept (string:acronyme) + """ + args = request.get_json(force=True) # may raise 400 Bad Request + dept = args.get("dept", None) + if not dept: + return scu.json_error(400, "dept requis") + dept_o = Departement.query.filter_by(acronym=dept).first() + if not dept_o: + return scu.json_error(400, "dept invalide") + app.set_sco_dept(dept) + args["dept_id"] = dept_o.id + # vérifie que le département de création est bien autorisé + if not current_user.has_permission(Permission.EtudInscrit, dept): + return json_error(403, "departement non autorisé") + nom = args.get("nom", None) + prenom = args.get("prenom", None) + ok, homonyms = sco_etud.check_nom_prenom_homonyms(nom=nom, prenom=prenom) + if not ok: + return scu.json_error(400, "nom ou prénom invalide") + if len(homonyms) > 0 and not force: + return scu.json_error( + 400, f"{len(homonyms)} homonymes détectés. Vous pouvez utiliser /force." + ) + etud = Identite.create_etud(**args) + # Poste une nouvelle dans le département concerné: + ScolarNews.add( + typ=ScolarNews.NEWS_INSCR, + text=f"Nouvel étudiant {etud.html_link_fiche()}", + url=etud.url_fiche(), + max_frequency=0, + dept_id=dept_o.id, + ) + db.session.commit() + return etud.to_dict_short() diff --git a/app/auth/models.py b/app/auth/models.py index 9f84196d..759c5bd7 100644 --- a/app/auth/models.py +++ b/app/auth/models.py @@ -27,7 +27,6 @@ from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_permissions import Permission from app.scodoc.sco_roles_default import SCO_ROLES_DEFAULTS import app.scodoc.sco_utils as scu -from app.scodoc import sco_etud # a deplacer dans scu VALID_LOGIN_EXP = re.compile(r"^[a-zA-Z0-9@\\\-_\.]+$") @@ -462,8 +461,8 @@ class User(UserMixin, db.Model, ScoDocModel): """nomplogin est le nom en majuscules suivi du prénom et du login e.g. Dupont Pierre (dupont) """ - nom = sco_etud.format_nom(self.nom) if self.nom else self.user_name.upper() - return f"{nom} {sco_etud.format_prenom(self.prenom)} ({self.user_name})" + nom = scu.format_nom(self.nom) if self.nom else self.user_name.upper() + return f"{nom} {scu.format_prenom(self.prenom)} ({self.user_name})" @staticmethod def get_user_id_from_nomplogin(nomplogin: str) -> Optional[int]: @@ -481,29 +480,29 @@ class User(UserMixin, db.Model, ScoDocModel): def get_nom_fmt(self): """Nom formaté: "Martin" """ if self.nom: - return sco_etud.format_nom(self.nom, uppercase=False) + return scu.format_nom(self.nom, uppercase=False) else: return self.user_name def get_prenom_fmt(self): """Prénom formaté (minuscule capitalisées)""" - return sco_etud.format_prenom(self.prenom) + return scu.format_prenom(self.prenom) def get_nomprenom(self): """Nom capitalisé suivi de l'initiale du prénom: Viennet E. """ - prenom_abbrv = scu.abbrev_prenom(sco_etud.format_prenom(self.prenom)) + prenom_abbrv = scu.abbrev_prenom(scu.format_prenom(self.prenom)) return (self.get_nom_fmt() + " " + prenom_abbrv).strip() def get_prenomnom(self): """L'initiale du prénom suivie du nom: "J.-C. Dupont" """ - prenom_abbrv = scu.abbrev_prenom(sco_etud.format_prenom(self.prenom)) + prenom_abbrv = scu.abbrev_prenom(scu.format_prenom(self.prenom)) return (prenom_abbrv + " " + self.get_nom_fmt()).strip() def get_nomcomplet(self): "Prénom et nom complets" - return sco_etud.format_prenom(self.prenom) + " " + self.get_nom_fmt() + return scu.format_prenom(self.prenom) + " " + self.get_nom_fmt() # nomnoacc était le nom en minuscules sans accents (inutile) diff --git a/app/entreprises/__init__.py b/app/entreprises/__init__.py index 121b7e3c..1d41e77d 100644 --- a/app/entreprises/__init__.py +++ b/app/entreprises/__init__.py @@ -6,6 +6,7 @@ from flask import Blueprint from app.scodoc import sco_etud from app.auth.models import User from app.models import Departement +import app.scodoc.sco_utils as scu bp = Blueprint("entreprises", __name__) @@ -15,12 +16,12 @@ SIRET_PROVISOIRE_START = "xx" @bp.app_template_filter() def format_prenom(s): - return sco_etud.format_prenom(s) + return scu.format_prenom(s) @bp.app_template_filter() def format_nom(s): - return sco_etud.format_nom(s) + return scu.format_nom(s) @bp.app_template_filter() diff --git a/app/entreprises/routes.py b/app/entreprises/routes.py index 60d567ab..529706e9 100644 --- a/app/entreprises/routes.py +++ b/app/entreprises/routes.py @@ -1580,8 +1580,8 @@ def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id): ) ) elif request.method == "GET": - form.etudiant.data = f"""{sco_etud.format_nom(etudiant.nom)} { - sco_etud.format_prenom(etudiant.prenom)}""" + form.etudiant.data = f"""{scu.format_nom(etudiant.nom)} { + scu.format_prenom(etudiant.prenom)}""" form.etudid.data = etudiant.id form.type_offre.data = stage_apprentissage.type_offre form.date_debut.data = stage_apprentissage.date_debut @@ -1699,7 +1699,7 @@ def json_etudiants(): list = [] for etudiant in etudiants: content = {} - value = f"{sco_etud.format_nom(etudiant.nom)} {sco_etud.format_prenom(etudiant.prenom)}" + value = f"{scu.format_nom(etudiant.nom)} {scu.format_prenom(etudiant.prenom)}" if etudiant.inscription_courante() is not None: content = { "id": f"{etudiant.id}", diff --git a/app/models/__init__.py b/app/models/__init__.py index ababbdbc..b4eea163 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -71,7 +71,7 @@ class ScoDocModel: @classmethod def convert_dict_fields(cls, args: dict) -> dict: - """Convert fields in the given dict. No side effect. + """Convert fields from the given dict to model's attributes values. No side effect. By default, do nothing, but is overloaded by some subclasses. args: dict with args in application. returns: dict to store in model's db. @@ -133,7 +133,6 @@ from app.models.notes import ( NotesNotesLog, ) from app.models.validations import ( - ScolarEvent, ScolarFormSemestreValidation, ScolarAutorisationInscription, ) @@ -152,3 +151,4 @@ from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE from app.models.config import ScoDocSiteConfig from app.models.assiduites import Assiduite, Justificatif +from app.models.scolar_event import ScolarEvent diff --git a/app/models/etudiants.py b/app/models/etudiants.py index e20a7b6e..192d7ff0 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -15,7 +15,7 @@ from sqlalchemy import desc, text from app import db, log from app import models - +from app.models.scolar_event import ScolarEvent from app.scodoc import notesdb as ndb from app.scodoc.sco_bac import Baccalaureat from app.scodoc.sco_exceptions import ScoInvalidParamError, ScoValueError @@ -170,9 +170,13 @@ class Identite(db.Model, models.ScoDocModel): def html_link_fiche(self) -> str: "lien vers la fiche" - return f"""{self.nomprenom}""" + return f"""{self.nomprenom}""" + + def url_fiche(self) -> str: + "url de la fiche étudiant" + return url_for( + "scolar.ficheEtud", scodoc_dept=self.departement.acronym, etudid=self.id + ) @classmethod def from_request(cls, etudid=None, code_nip=None) -> "Identite": @@ -211,6 +215,10 @@ class Identite(db.Model, models.ScoDocModel): etud.admission = Admission() etud.adresses.append(Adresse(typeadresse="domicile")) db.session.flush() + + event = ScolarEvent(etud=etud, event_type="CREATION") + db.session.add(event) + log(f"Identite.create {etud}") return etud @property diff --git a/app/models/events.py b/app/models/events.py index 74583b8a..06dbe558 100644 --- a/app/models/events.py +++ b/app/models/events.py @@ -133,7 +133,7 @@ class ScolarNews(db.Model): return query.order_by(cls.date.desc()).limit(n).all() @classmethod - def add(cls, typ, obj=None, text="", url=None, max_frequency=600): + def add(cls, typ, obj=None, text="", url=None, max_frequency=600, dept_id=None): """Enregistre une nouvelle Si max_frequency, ne génère pas 2 nouvelles "identiques" à moins de max_frequency secondes d'intervalle (10 minutes par défaut). @@ -141,10 +141,11 @@ class ScolarNews(db.Model): même (obj, typ, user). La nouvelle enregistrée est aussi envoyée par mail. """ + dept_id = dept_id if dept_id is not None else g.scodoc_dept_id if max_frequency: last_news = ( cls.query.filter_by( - dept_id=g.scodoc_dept_id, + dept_id=dept_id, authenticated_user=current_user.user_name, type=typ, object=obj, @@ -163,7 +164,7 @@ class ScolarNews(db.Model): return news = ScolarNews( - dept_id=g.scodoc_dept_id, + dept_id=dept_id, authenticated_user=current_user.user_name, type=typ, object=obj, diff --git a/app/models/scolar_event.py b/app/models/scolar_event.py new file mode 100644 index 00000000..4294efb1 --- /dev/null +++ b/app/models/scolar_event.py @@ -0,0 +1,48 @@ +"""évènements scolaires dans la vie d'un étudiant(inscription, ...) +""" +from app import db +from app.models import SHORT_STR_LEN + + +class ScolarEvent(db.Model): + """Evenement dans le parcours scolaire d'un étudiant""" + + __tablename__ = "scolar_events" + id = db.Column(db.Integer, primary_key=True) + event_id = db.synonym("id") + etudid = db.Column( + db.Integer, + db.ForeignKey("identite.id", ondelete="CASCADE"), + ) + event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) + formsemestre_id = db.Column( + db.Integer, + db.ForeignKey("notes_formsemestre.id", ondelete="SET NULL"), + ) + ue_id = db.Column( + db.Integer, + db.ForeignKey("notes_ue.id", ondelete="SET NULL"), + ) + # 'CREATION', 'INSCRIPTION', 'DEMISSION', + # 'AUT_RED', 'EXCLUS', 'VALID_UE', 'VALID_SEM' + # 'ECHEC_SEM' + # 'UTIL_COMPENSATION' + event_type = db.Column(db.String(SHORT_STR_LEN)) + # Semestre compensé par formsemestre_id: + comp_formsemestre_id = db.Column( + db.Integer, + db.ForeignKey("notes_formsemestre.id"), + ) + etud = db.relationship("Identite", lazy="select", backref="events", uselist=False) + formsemestre = db.relationship( + "FormSemestre", lazy="select", uselist=False, foreign_keys=[formsemestre_id] + ) + + def to_dict(self) -> dict: + "as a dict" + d = dict(self.__dict__) + d.pop("_sa_instance_state", None) + return d + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.event_type}, {self.event_date.isoformat()}, {self.formsemestre})" diff --git a/app/models/validations.py b/app/models/validations.py index 14e7a5b7..17dd12a6 100644 --- a/app/models/validations.py +++ b/app/models/validations.py @@ -1,6 +1,6 @@ # -*- coding: UTF-8 -* -"""Notes, décisions de jury, évènements scolaires +"""Notes, décisions de jury """ from app import db @@ -218,47 +218,3 @@ class ScolarAutorisationInscription(db.Model): msg=f"Passage vers S{autorisation.semestre_id}: effacé", ) db.session.flush() - - -class ScolarEvent(db.Model): - """Evenement dans le parcours scolaire d'un étudiant""" - - __tablename__ = "scolar_events" - id = db.Column(db.Integer, primary_key=True) - event_id = db.synonym("id") - etudid = db.Column( - db.Integer, - db.ForeignKey("identite.id", ondelete="CASCADE"), - ) - event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) - formsemestre_id = db.Column( - db.Integer, - db.ForeignKey("notes_formsemestre.id", ondelete="SET NULL"), - ) - ue_id = db.Column( - db.Integer, - db.ForeignKey("notes_ue.id", ondelete="SET NULL"), - ) - # 'CREATION', 'INSCRIPTION', 'DEMISSION', - # 'AUT_RED', 'EXCLUS', 'VALID_UE', 'VALID_SEM' - # 'ECHEC_SEM' - # 'UTIL_COMPENSATION' - event_type = db.Column(db.String(SHORT_STR_LEN)) - # Semestre compensé par formsemestre_id: - comp_formsemestre_id = db.Column( - db.Integer, - db.ForeignKey("notes_formsemestre.id"), - ) - etud = db.relationship("Identite", lazy="select", backref="events", uselist=False) - formsemestre = db.relationship( - "FormSemestre", lazy="select", uselist=False, foreign_keys=[formsemestre_id] - ) - - def to_dict(self) -> dict: - "as a dict" - d = dict(self.__dict__) - d.pop("_sa_instance_state", None) - return d - - def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.event_type}, {self.event_date.isoformat()}, {self.formsemestre})" diff --git a/app/scodoc/sco_etud.py b/app/scodoc/sco_etud.py index a2872857..9bd09e89 100644 --- a/app/scodoc/sco_etud.py +++ b/app/scodoc/sco_etud.py @@ -45,6 +45,12 @@ from app.models.etudiants import ( pivot_year, ) import app.scodoc.sco_utils as scu +from app.scodoc.sco_utils import ( + format_civilite, + format_nom, + format_nomprenom, + format_prenom, +) import app.scodoc.notesdb as ndb from app.scodoc.sco_exceptions import ScoGenError, ScoValueError from app.scodoc import safehtml @@ -102,60 +108,6 @@ def force_uppercase(s): return s.upper() if s else s -def format_nomprenom(etud, reverse=False): - """Formatte civilité/nom/prenom pour affichages: "M. Pierre Dupont" - Si reverse, "Dupont Pierre", sans civilité. - - DEPRECATED: utiliser Identite.nomprenom - """ - nom = etud.get("nom_disp", "") or etud.get("nom_usuel", "") or etud["nom"] - prenom = format_prenom(etud["prenom"]) - civilite = format_civilite(etud["civilite"]) - if reverse: - fs = [nom, prenom] - else: - fs = [civilite, prenom, nom] - return " ".join([x for x in fs if x]) - - -def format_prenom(s): - """Formatte prenom etudiant pour affichage - DEPRECATED: utiliser Identite.prenom_str - """ - if not s: - return "" - frags = s.split() - r = [] - for frag in frags: - fs = frag.split("-") - r.append("-".join([x.lower().capitalize() for x in fs])) - return " ".join(r) - - -def format_nom(s, uppercase=True): - if not s: - return "" - if uppercase: - return s.upper() - else: - return format_prenom(s) - - -def format_civilite(civilite): - """returns 'M.' ou 'Mme' ou '' (pour le genre neutre, - personne ne souhaitant pas d'affichage). - Raises ScoValueError if conversion fails. - """ - try: - return { - "M": "M.", - "F": "Mme", - "X": "", - }[civilite] - except KeyError as exc: - raise ScoValueError(f"valeur invalide pour la civilité: {civilite}") from exc - - def _format_etat_civil(etud: dict) -> str: "Mme Béatrice DUPONT, en utilisant les données d'état civil si indiquées." if etud["prenom_etat_civil"] or etud["civilite_etat_civil"]: @@ -657,16 +609,6 @@ def create_etud(cnx, args: dict = None): db.session.commit() etudid = etud.id - # event - scolar_events_create( - cnx, - args={ - "etudid": etudid, - "event_date": time.strftime("%d/%m/%Y"), - "formsemestre_id": None, - "event_type": "CREATION", - }, - ) # log logdb( cnx, @@ -674,16 +616,18 @@ def create_etud(cnx, args: dict = None): etudid=etudid, msg="creation initiale", ) - etud = etudident_list(cnx, {"etudid": etudid})[0] - fill_etuds_info([etud]) - etud["url"] = url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid) + etud_dict = etudident_list(cnx, {"etudid": etudid})[0] + fill_etuds_info([etud_dict]) + etud_dict["url"] = url_for( + "scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid + ) ScolarNews.add( typ=ScolarNews.NEWS_INSCR, - text='Nouvel étudiant %(nomprenom)s' % etud, + text=f"Nouvel étudiant {etud.html_link_fiche()}", url=etud["url"], max_frequency=0, ) - return etud + return etud_dict # ---------- "EVENTS" diff --git a/app/scodoc/sco_find_etud.py b/app/scodoc/sco_find_etud.py index 3b9cd130..eade7493 100644 --- a/app/scodoc/sco_find_etud.py +++ b/app/scodoc/sco_find_etud.py @@ -42,6 +42,7 @@ from app.scodoc import sco_groups from app.scodoc.sco_exceptions import ScoException from app.scodoc.sco_permissions import Permission from app.scodoc import sco_preferences +from app.scodoc import sco_utils as scu def form_search_etud( @@ -271,7 +272,7 @@ def search_etud_by_name(term: str) -> list: data = [ { "label": "%s %s %s" - % (x["code_nip"], x["nom"], sco_etud.format_prenom(x["prenom"])), + % (x["code_nip"], x["nom"], scu.format_prenom(x["prenom"])), "value": x["code_nip"], } for x in r @@ -290,7 +291,7 @@ def search_etud_by_name(term: str) -> list: data = [ { - "label": "%s %s" % (x["nom"], sco_etud.format_prenom(x["prenom"])), + "label": "%s %s" % (x["nom"], scu.format_prenom(x["prenom"])), "value": x["etudid"], } for x in r diff --git a/app/scodoc/sco_formsemestre_inscriptions.py b/app/scodoc/sco_formsemestre_inscriptions.py index 7837c299..887a0625 100644 --- a/app/scodoc/sco_formsemestre_inscriptions.py +++ b/app/scodoc/sco_formsemestre_inscriptions.py @@ -39,7 +39,7 @@ from app.comp.res_compat import NotesTableCompat from app.models import Formation, FormSemestre, FormSemestreInscription, Scolog from app.models.etudiants import Identite from app.models.groups import Partition, GroupDescr -from app.models.validations import ScolarEvent +from app.models.scolar_event import ScolarEvent import app.scodoc.sco_utils as scu from app import log from app.scodoc.scolog import logdb @@ -222,10 +222,10 @@ def do_formsemestre_desinscription(etudid, formsemestre_id): cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor.execute( - """SELECT Im.id AS moduleimpl_inscription_id + """SELECT Im.id AS moduleimpl_inscription_id FROM notes_moduleimpl_inscription Im, notes_moduleimpl M WHERE Im.etudid=%(etudid)s - and Im.moduleimpl_id = M.id + and Im.moduleimpl_id = M.id and M.formsemestre_id = %(formsemestre_id)s """, {"etudid": etudid, "formsemestre_id": formsemestre_id}, @@ -253,7 +253,7 @@ def do_formsemestre_desinscription(etudid, formsemestre_id): nbinscrits = len(inscrits) if nbinscrits == 0: log( - f"""do_formsemestre_desinscription: + f"""do_formsemestre_desinscription: suppression du semestre extérieur {formsemestre}""" ) flash("Semestre exterieur supprimé") @@ -436,7 +436,7 @@ def formsemestre_inscription_with_modules( if inscr is not None: H.append( f""" -

{etud.nomprenom} est déjà inscrit +

{etud.nomprenom} est déjà inscrit dans le semestre {formsemestre.titre_mois()}

") H.append( f"""

Continuer quand même l'inscription

""" @@ -644,7 +644,7 @@ function chkbx_select(field_id, state) { """

Voici la liste des modules du semestre choisi.

- Les modules cochés sont ceux dans lesquels l'étudiant est inscrit. + Les modules cochés sont ceux dans lesquels l'étudiant est inscrit. Vous pouvez l'inscrire ou le désincrire d'un ou plusieurs modules.

Attention: cette méthode ne devrait être utilisée que pour les modules diff --git a/app/scodoc/sco_groups.py b/app/scodoc/sco_groups.py index a37e7934..61b2e571 100644 --- a/app/scodoc/sco_groups.py +++ b/app/scodoc/sco_groups.py @@ -53,6 +53,7 @@ from app.scodoc import codes_cursus from app.scodoc import sco_cursus from app.scodoc import sco_etud from app.scodoc.sco_etud import etud_sort_key +import app.scodoc.sco_utils as scu from app.scodoc import sco_xml from app.scodoc.sco_exceptions import ScoException, AccessDenied, ScoValueError from app.scodoc.TrivialFormulator import TrivialFormulator @@ -573,8 +574,8 @@ def XMLgetGroupsInPartition(partition_id): # was XMLgetGroupesTD etudid=str(e["etudid"]), civilite=etud["civilite_str"] or "", sexe=etud["civilite_str"] or "", # compat - nom=sco_etud.format_nom(etud["nom"] or ""), - prenom=sco_etud.format_prenom(etud["prenom"] or ""), + nom=scu.format_nom(etud["nom"] or ""), + prenom=scu.format_prenom(etud["prenom"] or ""), origin=_comp_etud_origin(etud, formsemestre), ) ) @@ -599,8 +600,8 @@ def XMLgetGroupsInPartition(partition_id): # was XMLgetGroupesTD "etud", etudid=str(etud["etudid"]), sexe=etud["civilite_str"] or "", - nom=sco_etud.format_nom(etud["nom"] or ""), - prenom=sco_etud.format_prenom(etud["prenom"] or ""), + nom=scu.format_nom(etud["nom"] or ""), + prenom=scu.format_prenom(etud["prenom"] or ""), origin=_comp_etud_origin(etud, formsemestre), ) ) diff --git a/app/scodoc/sco_prepajury.py b/app/scodoc/sco_prepajury.py index bef32923..ba74b0aa 100644 --- a/app/scodoc/sco_prepajury.py +++ b/app/scodoc/sco_prepajury.py @@ -244,8 +244,8 @@ def feuille_preparation_jury(formsemestre_id): [ etud.id, etud.civilite_str, - sco_etud.format_nom(etud.nom), - sco_etud.format_prenom(etud.prenom), + scu.format_nom(etud.nom), + scu.format_prenom(etud.prenom), etud.date_naissance, etud.admission.bac if etud.admission else "", etud.admission.specialite if etud.admission else "", diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py index e67ebea5..87bc1097 100644 --- a/app/scodoc/sco_saisie_notes.py +++ b/app/scodoc/sco_saisie_notes.py @@ -1244,7 +1244,7 @@ def _form_saisie_notes( '' % classdem + e["civilite_str"] + " " - + sco_etud.format_nomprenom(e, reverse=True) + + scu.format_nomprenom(e, reverse=True) + "" ) diff --git a/app/scodoc/sco_trombino.py b/app/scodoc/sco_trombino.py index 396df98e..d5fbdebb 100644 --- a/app/scodoc/sco_trombino.py +++ b/app/scodoc/sco_trombino.py @@ -129,7 +129,7 @@ def trombino_html(groups_infos): H = [ f""" - """ ] @@ -164,9 +164,9 @@ def trombino_html(groups_infos): H.append("") H.append( '' - + sco_etud.format_prenom(t["prenom"]) + + scu.format_prenom(t["prenom"]) + '' - + sco_etud.format_nom(t["nom"]) + + scu.format_nom(t["nom"]) + (" (dem.)" if t["etat"] == "D" else "") ) H.append("") @@ -175,10 +175,10 @@ def trombino_html(groups_infos): H.append("") H.append( f"""
- Version PDF    - Version doc
""" ) @@ -202,9 +202,9 @@ def check_local_photos_availability(groups_infos, fmt=""): return ( False, scu.confirm_dialog( - f"""

Attention: {nb_missing} photos ne sont pas disponibles + f"""

Attention: {nb_missing} photos ne sont pas disponibles et ne peuvent pas être exportées.

-

Vous pouvez Vous pouvez exporter seulement les photos existantes""", dest_url="trombino", @@ -263,11 +263,11 @@ def trombino_copy_photos(group_ids=[], dialog_confirmed=False): if not dialog_confirmed: return scu.confirm_dialog( f"""

Copier les photos du portail vers ScoDoc ?

-

Les photos du groupe {groups_infos.groups_titles} présentes +

Les photos du groupe {groups_infos.groups_titles} présentes dans ScoDoc seront remplacées par celles du portail (si elles existent).

-

(les photos sont normalement automatiquement copiées - lors de leur première utilisation, l'usage de cette fonction +

(les photos sont normalement automatiquement copiées + lors de leur première utilisation, l'usage de cette fonction n'est nécessaire que si les photos du portail ont été modifiées)

""", @@ -349,7 +349,7 @@ def _trombino_pdf(groups_infos): [img], [ Paragraph( - SU(sco_etud.format_nomprenom(t)), + SU(scu.format_nomprenom(t)), style_sheet["Normal"], ) ], @@ -428,7 +428,7 @@ def _listeappel_photos_pdf(groups_infos): t = groups_infos.members[i] img = _get_etud_platypus_image(t, image_width=PHOTO_WIDTH) txt = Paragraph( - SU(sco_etud.format_nomprenom(t)), + SU(scu.format_nomprenom(t)), style_sheet["Normal"], ) if currow: diff --git a/app/scodoc/sco_trombino_doc.py b/app/scodoc/sco_trombino_doc.py index b40da577..7cdbad97 100644 --- a/app/scodoc/sco_trombino_doc.py +++ b/app/scodoc/sco_trombino_doc.py @@ -55,7 +55,7 @@ def trombino_doc(groups_infos): cell = table.rows[2 * li + 1].cells[co] cell.vertical_alignment = WD_ALIGN_VERTICAL.TOP cell_p, cell_f, cell_r = _paragraph_format_run(cell) - cell_r.add_text(sco_etud.format_nomprenom(t)) + cell_r.add_text(scu.format_nomprenom(t)) cell_f.space_after = Mm(8) return scu.send_docx(document, filename) diff --git a/app/scodoc/sco_trombino_tours.py b/app/scodoc/sco_trombino_tours.py index 821c0e9e..a7096536 100644 --- a/app/scodoc/sco_trombino_tours.py +++ b/app/scodoc/sco_trombino_tours.py @@ -196,9 +196,9 @@ def pdf_trombino_tours( Paragraph( SU( "" - + sco_etud.format_prenom(m["prenom"]) + + scu.format_prenom(m["prenom"]) + " " - + sco_etud.format_nom(m["nom"]) + + scu.format_nom(m["nom"]) + text_group + "" ), @@ -413,11 +413,7 @@ def pdf_feuille_releve_absences( for m in members: currow = [ Paragraph( - SU( - sco_etud.format_nom(m["nom"]) - + " " - + sco_etud.format_prenom(m["prenom"]) - ), + SU(scu.format_nom(m["nom"]) + " " + scu.format_prenom(m["prenom"])), StyleSheet["Normal"], ) ] diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index f93bf052..35fb343e 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -64,6 +64,7 @@ from config import Config from app import log, ScoDocJSONEncoder from app.scodoc.codes_cursus import NOTES_TOLERANCE, CODES_EXPL +from app.scodoc.sco_exceptions import ScoValueError from app.scodoc import sco_xml import sco_version @@ -1139,6 +1140,61 @@ def abbrev_prenom(prenom): return abrv +def format_civilite(civilite): + """returns 'M.' ou 'Mme' ou '' (pour le genre neutre, + personne ne souhaitant pas d'affichage). + Raises ScoValueError if conversion fails. + """ + try: + return { + "M": "M.", + "F": "Mme", + "X": "", + }[civilite] + except KeyError as exc: + raise ScoValueError(f"valeur invalide pour la civilité: {civilite}") from exc + + +def format_nomprenom(etud, reverse=False): + """Formatte civilité/nom/prenom pour affichages: "M. Pierre Dupont" + Si reverse, "Dupont Pierre", sans civilité. + + DEPRECATED: utiliser Identite.nomprenom + """ + nom = etud.get("nom_disp", "") or etud.get("nom_usuel", "") or etud["nom"] + prenom = format_prenom(etud["prenom"]) + civilite = format_civilite(etud["civilite"]) + if reverse: + fs = [nom, prenom] + else: + fs = [civilite, prenom, nom] + return " ".join([x for x in fs if x]) + + +def format_nom(s, uppercase=True): + "Formatte le nom" + if not s: + return "" + if uppercase: + return s.upper() + else: + return format_prenom(s) + + +def format_prenom(s): + """Formatte prenom etudiant pour affichage + DEPRECATED: utiliser Identite.prenom_str + """ + if not s: + return "" + frags = s.split() + r = [] + for frag in frags: + fs = frag.split("-") + r.append("-".join([x.lower().capitalize() for x in fs])) + return " ".join(r) + + # def timedate_human_repr(): "representation du temps courant pour utilisateur" @@ -1480,6 +1536,7 @@ def is_assiduites_module_forced( def get_assiduites_time_config(config_type: str) -> str: from app.models import ScoDocSiteConfig + match config_type: case "matin": return ScoDocSiteConfig.get("assi_morning_time", "08:00:00") diff --git a/tests/api/test_api_etudiants.py b/tests/api/test_api_etudiants.py index e66490fc..bc4563cb 100644 --- a/tests/api/test_api_etudiants.py +++ b/tests/api/test_api_etudiants.py @@ -28,10 +28,11 @@ from tests.api.setup_test_api import ( API_URL, API_USER_ADMIN, CHECK_CERTIFICATE, + DEPT_ACRONYM, POST_JSON, - api_headers, get_auth_headers, ) +from tests.api.setup_test_api import api_headers # pylint: disable=unused-import from tests.api.tools_test_api import ( BULLETIN_ETUDIANT_FIELDS, BULLETIN_FIELDS, @@ -923,3 +924,20 @@ def test_etudiant_groups(api_headers): group = groups[0] fields_ok = verify_fields(group, fields) assert fields_ok is True + + +def test_etudiant_create(api_headers): + """/etudiant/create""" + admin_header = get_auth_headers(API_USER_ADMIN, API_PASSWORD_ADMIN) + args = { + "prenom": "Carl Philipp Emanuel", + "nom": "Bach", + "dept": DEPT_ACRONYM, + "civilite": "M", + } + etud = POST_JSON( + "/etudiant/create", + args, + headers=admin_header, + ) + assert etud["nom"] == args["nom"].upper()
{group_txt}