diff --git a/app/models/etudiants.py b/app/models/etudiants.py index 0c153b8a4..3bb115988 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -49,6 +49,32 @@ class Identite(db.Model): def __repr__(self): return f"" + def civilite_str(self): + """returns 'M.' ou 'Mme' ou '' (pour le genre neutre, + personnes ne souhaitant pas d'affichage). + """ + return {"M": "M.", "F": "Mme", "X": ""}[self.civilite] + + def nom_disp(self): + "nom à afficher" + if self.nom_usuel: + return ( + (self.nom_usuel + " (" + self.nom + ")") if self.nom else self.nom_usuel + ) + else: + return self.nom + + def inscription_courante(self): + """La première inscription à un formsemestre _actuellement_ en cours. + None s'il n'y en a pas (ou plus, ou pas encore). + """ + r = [ + ins + for ins in self.formsemestre_inscriptions + if ins.formsemestre.est_courant() + ] + return r[0] if r else None + class Adresse(db.Model): """Adresse d'un étudiant diff --git a/app/models/formations.py b/app/models/formations.py index a49ce655c..e1f02bea9 100644 --- a/app/models/formations.py +++ b/app/models/formations.py @@ -241,7 +241,7 @@ class Module(db.Model): def is_apc(self): "True si module SAÉ ou Ressource" - return scu.ModuleType(self.module_type) in { + return self.module_type and scu.ModuleType(self.module_type) in { scu.ModuleType.RESSOURCE, scu.ModuleType.SAE, } diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 3ffcec5b1..fa30fffc0 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -2,6 +2,7 @@ """ScoDoc models: formsemestre """ +import datetime from typing import Any import flask_sqlalchemy @@ -13,11 +14,14 @@ from app.models import CODE_STR_LEN from app.models import UniteEns import app.scodoc.notesdb as ndb +import app.scodoc.sco_utils as scu from app.scodoc import sco_evaluation_db from app.models.formations import UniteEns, Module from app.models.moduleimpls import ModuleImpl from app.models.etudiants import Identite from app.scodoc import sco_codes_parcours +from app.scodoc import sco_preferences +from app.scodoc.sco_vdi import ApoEtapeVDI class FormSemestre(db.Model): @@ -40,7 +44,7 @@ class FormSemestre(db.Model): ) # False si verrouillé modalite = db.Column( db.String(SHORT_STR_LEN), db.ForeignKey("notes_form_modalites.modalite") - ) + ) # "FI", "FAP", "FC", ... # gestion compensation sem DUT: gestion_compensation = db.Column( db.Boolean(), nullable=False, default=False, server_default="false" @@ -89,7 +93,12 @@ class FormSemestre(db.Model): viewonly=True, lazy="dynamic", ) - + responsables = db.relationship( + "User", + secondary="notes_formsemestre_responsables", + lazy=True, + backref=db.backref("formsemestres", lazy=True), + ) # Ancien id ScoDoc7 pour les migrations de bases anciennes # ne pas utiliser après migrate_scodoc7_dept_archives scodoc7_id = db.Column(db.Text(), nullable=True) @@ -99,6 +108,18 @@ class FormSemestre(db.Model): if self.modalite is None: self.modalite = FormationModalite.DEFAULT_MODALITE + def to_dict(self): + d = dict(self.__dict__) + d.pop("_sa_instance_state", None) + # ScoDoc7 output_formators: (backward compat) + d["formsemestre_id"] = self.id + d["date_debut"] = ( + self.date_debut.strftime("%d/%m/%Y") if self.date_debut else "" + ) + d["date_fin"] = self.date_fin.strftime("%d/%m/%Y") if self.date_fin else "" + d["responsables"] = [u.id for u in self.responsables] + return d + def query_ues(self, with_sport=False) -> flask_sqlalchemy.BaseQuery: """UE des modules de ce semestre. - Formations classiques: les UEs auxquelles appartiennent @@ -120,6 +141,76 @@ class FormSemestre(db.Model): sem_ues = sem_ues.filter(UniteEns.type != sco_codes_parcours.UE_SPORT) return sem_ues + def est_courant(self) -> bool: + """Vrai si la date actuelle (now) est dans le semestre + (les dates de début et fin sont incluses) + """ + today = datetime.date.today() + return (self.date_debut <= today) and (today <= self.date_fin) + + def est_decale(self): + """Vrai si semestre "décalé" + c'est à dire semestres impairs commençant entre janvier et juin + et les pairs entre juillet et decembre + """ + if self.semestre_id <= 0: + return False # formations sans semestres + return (self.semestre_id % 2 and self.date_debut.month <= 6) or ( + not self.semestre_id % 2 and self.date_debut.month > 6 + ) + + def etapes_apo_str(self) -> str: + """Chaine décrivant les étapes de ce semestre + ex: "V1RT, V1RT3, V1RT4" + """ + if not self.etapes: + return "" + return ", ".join([str(x.etape_apo) for x in self.etapes]) + + def responsables_str(self, abbrev_prenom=True) -> str: + """chaîne "J. Dupond, X. Martin" + ou "Jacques Dupond, Xavier Martin" + """ + if not self.responsables: + return "" + if abbrev_prenom: + return ", ".join([u.get_prenomnom() for u in self.responsables]) + else: + return ", ".join([u.get_nomcomplet() for u in self.responsables]) + + def session_id(self) -> str: + """identifiant externe de semestre de formation + Exemple: RT-DUT-FI-S1-ANNEE + + DEPT-TYPE-MODALITE+-S?|SPECIALITE + + TYPE=DUT|LP*|M* + MODALITE=FC|FI|FA (si plusieurs, en inverse alpha) + + SPECIALITE=[A-Z]+ EON,ASSUR, ... (si pas Sn ou SnD) + + ANNEE=annee universitaire de debut (exemple: un S2 de 2013-2014 sera S2-2013) + """ + imputation_dept = sco_preferences.get_preference("ImputationDept", self.id) + if not imputation_dept: + imputation_dept = sco_preferences.get_preference("DeptName") + imputation_dept = imputation_dept.upper() + parcours_name = self.formation.get_parcours().NAME + modalite = self.modalite + # exception pour code Apprentissage: + modalite = (modalite or "").replace("FAP", "FA").replace("APP", "FA") + if self.semestre_id > 0: + decale = "D" if self.est_decale() else "" + semestre_id = f"S{self.semestre_id}{decale}" + else: + semestre_id = self.formation.code_specialite or "" + annee_sco = str( + scu.annee_scolaire_debut(self.date_debut.year, self.date_debut.month) + ) + return scu.sanitize_string( + "-".join((imputation_dept, parcours_name, modalite, semestre_id, annee_sco)) + ) + # Association id des utilisateurs responsables (aka directeurs des etudes) du semestre notes_formsemestre_responsables = db.Table( @@ -144,6 +235,12 @@ class FormsemestreEtape(db.Model): ) etape_apo = db.Column(db.String(APO_CODE_STR_LEN)) + def __repr__(self): + return f"" + + def as_apovdi(self): + return ApoEtapeVDI(self.etape_apo) + class FormationModalite(db.Model): """Modalités de formation, utilisées pour la présentation diff --git a/app/scodoc/sco_abs.py b/app/scodoc/sco_abs.py index 7d5e7976e..28bfa9885 100644 --- a/app/scodoc/sco_abs.py +++ b/app/scodoc/sco_abs.py @@ -1028,20 +1028,26 @@ def get_abs_count(etudid, sem): tuple (nb abs non justifiées, nb abs justifiées) Utilise un cache. """ - date_debut = sem["date_debut_iso"] - date_fin = sem["date_fin_iso"] - key = str(etudid) + "_" + date_debut + "_" + date_fin + return get_abs_count_in_interval(etudid, sem["date_debut_iso"], sem["date_fin_iso"]) + + +def get_abs_count_in_interval(etudid, date_debut_iso, date_fin_iso): + """Les comptes d'absences de cet étudiant entre ces deux dates, incluses: + tuple (nb abs non justifiées, nb abs justifiées) + Utilise un cache. + """ + key = str(etudid) + "_" + date_debut_iso + "_" + date_fin_iso r = sco_cache.AbsSemEtudCache.get(key) if not r: - nb_abs = count_abs( # was CountAbs XXX + nb_abs = count_abs( etudid=etudid, - debut=date_debut, - fin=date_fin, + debut=date_debut_iso, + fin=date_fin_iso, ) - nb_abs_just = count_abs_just( # XXX was CountAbsJust + nb_abs_just = count_abs_just( etudid=etudid, - debut=date_debut, - fin=date_fin, + debut=date_debut_iso, + fin=date_fin_iso, ) r = (nb_abs, nb_abs_just) ans = sco_cache.AbsSemEtudCache.set(key, r) diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index 23d63bff7..9b48fad5f 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -584,7 +584,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours); H.append( f"""