diff --git a/app/api/justificatifs.py b/app/api/justificatifs.py index f048d1a67..b3528362a 100644 --- a/app/api/justificatifs.py +++ b/app/api/justificatifs.py @@ -22,7 +22,6 @@ from app.api import get_model_api_object, tools from app.decorators import permission_required, scodoc from app.models import Identite, Justificatif, Departement, FormSemestre, Scolog from app.models.assiduites import ( - compute_assiduites_justified, get_formsemestre_from_data, ) from app.scodoc.sco_archives_justificatifs import JustificatifArchiver @@ -310,7 +309,6 @@ def justif_create(etudid: int = None, nip=None, ine=None): errors: list[dict] = [] success: list[dict] = [] - justifs: list[Justificatif] = [] # énumération des justificatifs for i, data in enumerate(create_list): @@ -322,11 +320,9 @@ def justif_create(etudid: int = None, nip=None, ine=None): errors.append({"indice": i, "message": obj}) else: success.append({"indice": i, "message": obj}) - justifs.append(justi) + justi.justifier_assiduites() scass.simple_invalidate_cache(data, etud.id) - # Actualisation des assiduités justifiées en fonction de tous les nouveaux justificatifs - compute_assiduites_justified(etud.etudid, justifs) return {"errors": errors, "success": success} @@ -495,6 +491,7 @@ def justif_edit(justif_id: int): return json_error(404, err) # Mise à jour du justificatif + justificatif_unique.dejustifier_assiduites() db.session.add(justificatif_unique) db.session.commit() @@ -511,11 +508,7 @@ def justif_edit(justif_id: int): retour = { "couverture": { "avant": avant_ids, - "apres": compute_assiduites_justified( - justificatif_unique.etudid, - [justificatif_unique], - True, - ), + "apres": justificatif_unique.justifier_assiduites(), } } # Invalide le cache @@ -592,14 +585,10 @@ def _delete_one(justif_id: int) -> tuple[int, str]: # On invalide le cache scass.simple_invalidate_cache(justificatif_unique.to_dict()) + # On actualise les assiduités justifiées de l'étudiant concerné + justificatif_unique.dejustifier_assiduites() # On supprime le justificatif db.session.delete(justificatif_unique) - # On actualise les assiduités justifiées de l'étudiant concerné - compute_assiduites_justified( - justificatif_unique.etudid, - Justificatif.query.filter_by(etudid=justificatif_unique.etudid).all(), - True, - ) return (200, "OK") @@ -700,7 +689,6 @@ def justif_export(justif_id: int | None = None, filename: str | None = None): @as_json @permission_required(Permission.AbsChange) def justif_remove(justif_id: int = None): - # XXX TODO pas de test unitaire """ Supression d'un fichier ou d'une archive { diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index 8eec9ba6a..ab4227b0d 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -393,7 +393,7 @@ class BulletinBUT: else: etud_ues_ids = res.etud_ues_ids(etud.id) - nbabs, nbabsjust = formsemestre.get_abs_count(etud.id) + nbabsnj, nbabsjust, nbabs = formsemestre.get_abs_count(etud.id) etud_groups = sco_groups.get_etud_formsemestre_groups( etud, formsemestre, only_to_show=True ) @@ -408,7 +408,7 @@ class BulletinBUT: } if self.prefs["bul_show_abs"]: semestre_infos["absences"] = { - "injustifie": nbabs - nbabsjust, + "injustifie": nbabsnj, "total": nbabs, "metrique": { "H.": "Heure(s)", @@ -525,7 +525,7 @@ class BulletinBUT: d["demission"] = "" # --- Absences - d["nbabs"], d["nbabsjust"] = self.res.formsemestre.get_abs_count(etud.id) + _, d["nbabsjust"], d["nbabs"] = self.res.formsemestre.get_abs_count(etud.id) # --- Decision Jury infos, _ = sco_bulletins.etud_descr_situation_semestre( @@ -540,9 +540,9 @@ class BulletinBUT: d.update(infos) # --- Rangs - d["rang_nt"] = ( - f"{d['semestre']['rang']['value']} / {d['semestre']['rang']['total']}" - ) + d[ + "rang_nt" + ] = f"{d['semestre']['rang']['value']} / {d['semestre']['rang']['total']}" d["rang_txt"] = "Rang " + d["rang_nt"] d.update(sco_bulletins.make_context_dict(self.res.formsemestre, d["etud"])) diff --git a/app/but/bulletin_but_xml_compat.py b/app/but/bulletin_but_xml_compat.py index 07522f80c..fb9af2056 100644 --- a/app/but/bulletin_but_xml_compat.py +++ b/app/but/bulletin_but_xml_compat.py @@ -241,7 +241,7 @@ def bulletin_but_xml_compat( # --- Absences if sco_preferences.get_preference("bul_show_abs", formsemestre_id): - nbabs, nbabsjust = formsemestre.get_abs_count(etud.id) + _, nbabsjust, nbabs = formsemestre.get_abs_count(etud.id) doc.append(Element("absences", nbabs=str(nbabs), nbabsjust=str(nbabsjust))) # -------- LA SUITE EST COPIEE SANS MODIF DE sco_bulletins_xml.py --------- diff --git a/app/models/assiduites.py b/app/models/assiduites.py index 8580179bd..db7a25872 100644 --- a/app/models/assiduites.py +++ b/app/models/assiduites.py @@ -574,11 +574,7 @@ class Justificatif(ScoDocModel): db.session.delete(self) db.session.commit() # On actualise les assiduités justifiées de l'étudiant concerné - compute_assiduites_justified( - self.etudid, - Justificatif.query.filter_by(etudid=self.etudid).all(), - True, - ) + self.dejustifier_assiduites() def get_fichiers(self) -> tuple[list[str], int]: """Renvoie la liste des noms de fichiers justicatifs @@ -600,6 +596,64 @@ class Justificatif(ScoDocModel): accessible_filenames.append(filename[0]) return accessible_filenames, len(filenames) + def justifier_assiduites( + self, + ) -> list[int]: + """Justifie les assiduités sur la période de validité du justificatif""" + log(f"justifier_assiduites: {self}") + assiduites_justifiees: list[int] = [] + if self.etat != EtatJustificatif.VALIDE: + return [] + # On récupère les assiduités de l'étudiant sur la période donnée + assiduites: Query = self.etudiant.assiduites.filter( + Assiduite.date_debut >= self.date_debut, + Assiduite.date_fin <= self.date_fin, + Assiduite.etat != EtatAssiduite.PRESENT, + ) + # Pour chaque assiduité, on la justifie + for assi in assiduites: + assi.est_just = True + assiduites_justifiees.append(assi.assiduite_id) + db.session.add(assi) + + db.session.commit() + + return assiduites_justifiees + + def dejustifier_assiduites(self) -> list[int]: + """ + Déjustifie les assiduités sur la période du justificatif + """ + assiduites_dejustifiees: list[int] = [] + + # On récupère les assiduités de l'étudiant sur la période donnée + assiduites: Query = self.etudiant.assiduites.filter( + Assiduite.date_debut >= self.date_debut, + Assiduite.date_fin <= self.date_fin, + Assiduite.etat != EtatAssiduite.PRESENT, + ) + assi: Assiduite + for assi in assiduites: + # On récupère les justificatifs qui justifient l'assiduité `assi` + assi_justifs: list[int] = get_justifs_from_date( + self.etudiant.etudid, + assi.date_debut, + assi.date_fin, + long=False, + valid=True, + ) + # Si il n'y a pas d'autre justificatif valide, on déjustifie l'assiduité + if len(assi_justifs) == 0 or ( + len(assi_justifs) == 1 and assi_justifs[0] == self.justif_id + ): + assi.est_just = False + assiduites_dejustifiees.append(assi.assiduite_id) + db.session.add(assi) + + db.session.commit() + + return assiduites_dejustifiees + def is_period_conflicting( date_debut: datetime, @@ -623,72 +677,6 @@ def is_period_conflicting( return count > 0 -def compute_assiduites_justified( - etudid: int, justificatifs: list[Justificatif] = None, reset: bool = False -) -> list[int]: - """ - Args: - etudid (int): l'identifiant de l'étudiant - justificatifs (list[Justificatif]): La liste des justificatifs qui seront utilisés - reset (bool, optional): remet à false les assiduites non justifiés. Defaults to False. - - Returns: - list[int]: la liste des assiduités qui ont été justifiées. - """ - # TODO à optimiser (car très long avec 40000 assiduités) - # On devrait : - # - récupérer uniquement les assiduités qui sont sur la période des justificatifs donnés - # - Pour chaque assiduité trouvée, il faut récupérer les justificatifs qui la justifie - # - Si au moins un justificatif valide couvre la période de l'assiduité alors on la justifie - - # Si on ne donne pas de justificatifs on prendra par défaut tous les justificatifs de l'étudiant - if justificatifs is None: - justificatifs: list[Justificatif] = Justificatif.query.filter_by( - etudid=etudid - ).all() - - # On ne prend que les justificatifs valides - justificatifs = [j for j in justificatifs if j.etat == EtatJustificatif.VALIDE] - - # On récupère les assiduités de l'étudiant - assiduites: Assiduite = Assiduite.query.filter_by(etudid=etudid) - - assiduites_justifiees: list[int] = [] - - for assi in assiduites: - # On ne justifie pas les Présences - if assi.etat == EtatAssiduite.PRESENT: - continue - - # On récupère les justificatifs qui justifient l'assiduité `assi` - assi_justificatifs = Justificatif.query.filter( - Justificatif.etudid == assi.etudid, - Justificatif.date_debut <= assi.date_debut, - Justificatif.date_fin >= assi.date_fin, - Justificatif.etat == EtatJustificatif.VALIDE, - ).all() - - # Si au moins un justificatif possède une période qui couvre l'assiduité - if any( - assi.date_debut >= j.date_debut and assi.date_fin <= j.date_fin - for j in justificatifs + assi_justificatifs - ): - # On justifie l'assiduité - # On ajoute l'id de l'assiduité à la liste des assiduités justifiées - assi.est_just = True - assiduites_justifiees.append(assi.assiduite_id) - db.session.add(assi) - elif reset: - # Si le paramètre reset est Vrai alors les assiduités non justifiées - # sont remise en "non justifiée" - assi.est_just = False - db.session.add(assi) - # On valide la session - db.session.commit() - # On renvoie la liste des assiduite_id des assiduités justifiées - return assiduites_justifiees - - def get_assiduites_justif(assiduite_id: int, long: bool) -> list[int | dict]: """ get_assiduites_justif Récupération des justificatifs d'une assiduité diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 09c1d3056..5541d1780 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -875,7 +875,7 @@ class FormSemestre(db.Model): def get_abs_count(self, etudid): """Les comptes d'absences de cet étudiant dans ce semestre: - tuple (nb abs, nb abs justifiées) + tuple (nb abs non just, nb abs justifiées, nb abs total) Utilise un cache. """ from app.scodoc import sco_assiduites diff --git a/app/scodoc/html_sidebar.py b/app/scodoc/html_sidebar.py index f1c8f8356..2d351e628 100755 --- a/app/scodoc/html_sidebar.py +++ b/app/scodoc/html_sidebar.py @@ -175,10 +175,9 @@ def sidebar(etudid: int = None): inscription = etud.inscription_courante() if inscription: formsemestre = inscription.formsemestre - nbabs, nbabsjust = sco_assiduites.formsemestre_get_assiduites_count( + nbabsnj, nbabsjust, _ = sco_assiduites.formsemestre_get_assiduites_count( etudid, formsemestre ) - nbabsnj = nbabs - nbabsjust H.append( f""" 1 journée) self.nb_heures_par_jour = ( @@ -661,7 +661,7 @@ def create_absence_billet( db.session.add(justi) db.session.commit() - compute_assiduites_justified(etud.id, [justi]) + justi.justifier_assiduites() calculator: CountCalculator = CountCalculator() calculator.compute_assiduites([assiduite_unique]) @@ -671,7 +671,7 @@ def create_absence_billet( # Gestion du cache def get_assiduites_count(etudid: int, sem: dict) -> tuple[int, int]: """Les comptes d'absences de cet étudiant dans ce semestre: - tuple (nb abs non justifiées, nb abs justifiées) + tuple (nb abs non justifiées, nb abs justifiées, nb abs total) Utilise un cache. """ metrique = sco_preferences.get_preference("assi_metrique", sem["formsemestre_id"]) @@ -687,17 +687,17 @@ def formsemestre_get_assiduites_count( etudid: int, formsemestre: FormSemestre, moduleimpl_id: int = None ) -> tuple[int, int]: """Les comptes d'absences de cet étudiant dans ce semestre: - tuple (nb abs non justifiées, nb abs justifiées) + tuple (nb abs non justifiées, nb abs justifiées, nb abs total) Utilise un cache. """ metrique = sco_preferences.get_preference("assi_metrique", formsemestre.id) return get_assiduites_count_in_interval( etudid, date_debut=scu.localize_datetime( - datetime.combine(formsemestre.date_debut, time(8, 0)) + datetime.combine(formsemestre.date_debut, time(0, 0)) ), date_fin=scu.localize_datetime( - datetime.combine(formsemestre.date_fin, time(18, 0)) + datetime.combine(formsemestre.date_fin, time(23, 0)) ), metrique=scu.translate_assiduites_metric(metrique), moduleimpl_id=moduleimpl_id, @@ -714,12 +714,12 @@ def get_assiduites_count_in_interval( moduleimpl_id: int = None, ): """Les comptes d'absences de cet étudiant entre ces deux dates, incluses: - tuple (nb abs, nb abs justifiées) + tuple (nb abs non justifiées, nb abs justifiées, nb abs total) On peut spécifier les dates comme datetime ou iso. Utilise un cache. """ - date_debut_iso = date_debut_iso or date_debut.isoformat() - date_fin_iso = date_fin_iso or date_fin.isoformat() + date_debut_iso = date_debut_iso or date_debut.strftime("%Y-%m-%d") + date_fin_iso = date_fin_iso or date_fin.strftime("%Y-%m-%d") key = f"{etudid}_{date_debut_iso}_{date_fin_iso}_assiduites" r = sco_cache.AbsSemEtudCache.get(key) @@ -744,9 +744,10 @@ def get_assiduites_count_in_interval( if not ans: log("warning: get_assiduites_count failed to cache") - nb_abs: dict = r["absent"][metrique] - nb_abs_just: dict = r["absent_just"][metrique] - return (nb_abs, nb_abs_just) + nb_abs: int = r["absent"][metrique] + nb_abs_nj: int = r["absent_non_just"][metrique] + nb_abs_just: int = r["absent_just"][metrique] + return (nb_abs_nj, nb_abs_just, nb_abs) def invalidate_assiduites_count(etudid: int, sem: dict): diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index 1b70d3858..d1c32795d 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -196,7 +196,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"): pid = partition["partition_id"] partitions_etud_groups[pid] = sco_groups.get_etud_groups_in_partition(pid) # --- Absences - I["nbabs"], I["nbabsjust"] = sco_assiduites.get_assiduites_count(etudid, nt.sem) + _, I["nbabsjust"], I["nbabs"] = sco_assiduites.get_assiduites_count(etudid, nt.sem) # --- Decision Jury infos, dpv = etud_descr_situation_semestre( @@ -471,7 +471,7 @@ def _ue_mod_bulletin( ) # peut etre 'NI' is_malus = mod["module"]["module_type"] == ModuleType.MALUS if bul_show_abs_modules: - nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, sem) + _, nbabsjust, nbabs = sco_assiduites.get_assiduites_count(etudid, sem) mod_abs = [nbabs, nbabsjust] mod["mod_abs_txt"] = scu.fmt_abs(mod_abs) else: diff --git a/app/scodoc/sco_bulletins_json.py b/app/scodoc/sco_bulletins_json.py index 0481e6f9c..a7848b39e 100644 --- a/app/scodoc/sco_bulletins_json.py +++ b/app/scodoc/sco_bulletins_json.py @@ -296,7 +296,7 @@ def formsemestre_bulletinetud_published_dict( # --- Absences if prefs["bul_show_abs"]: - nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, sem) + _, nbabsjust, nbabs = sco_assiduites.get_assiduites_count(etudid, sem) d["absences"] = dict(nbabs=nbabs, nbabsjust=nbabsjust) # --- Décision Jury diff --git a/app/scodoc/sco_bulletins_xml.py b/app/scodoc/sco_bulletins_xml.py index 25f2cfa64..77f95ac28 100644 --- a/app/scodoc/sco_bulletins_xml.py +++ b/app/scodoc/sco_bulletins_xml.py @@ -260,7 +260,7 @@ def make_xml_formsemestre_bulletinetud( numero=str(mod["numero"]), titre=quote_xml_attr(mod["titre"]), abbrev=quote_xml_attr(mod["abbrev"]), - code_apogee=quote_xml_attr(mod["code_apogee"]) + code_apogee=quote_xml_attr(mod["code_apogee"]), # ects=ects ects des modules maintenant inutilisés ) x_ue.append(x_mod) @@ -347,7 +347,7 @@ def make_xml_formsemestre_bulletinetud( # --- Absences if sco_preferences.get_preference("bul_show_abs", formsemestre_id): - nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, sem) + _, nbabsjust, nbabs = sco_assiduites.get_assiduites_count(etudid, sem) doc.append(Element("absences", nbabs=str(nbabs), nbabsjust=str(nbabsjust))) # --- Decision Jury if ( diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py index df4770fa3..27d99fffd 100644 --- a/app/scodoc/sco_formsemestre_validation.py +++ b/app/scodoc/sco_formsemestre_validation.py @@ -722,8 +722,8 @@ def formsemestre_recap_parcours_table( f"""{scu.fmt_note(nt.get_etud_moy_gen(etudid))}""" ) # Absences (nb d'abs non just. dans ce semestre) - nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, sem) - H.append(f"""{nbabs - nbabsjust}""") + nbabsnj = sco_assiduites.get_assiduites_count(etudid, sem)[0] + H.append(f"""{nbabsnj}""") # UEs for ue in ues: diff --git a/app/scodoc/sco_poursuite_dut.py b/app/scodoc/sco_poursuite_dut.py index c271628a2..475d59808 100644 --- a/app/scodoc/sco_poursuite_dut.py +++ b/app/scodoc/sco_poursuite_dut.py @@ -105,7 +105,9 @@ def etud_get_poursuite_info(sem: dict, etud: dict) -> dict: rangs.append(["rang_" + code_module, rang_module]) # Absences - nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, nt.sem) + nbabsnj, nbabsjust, _ = sco_assiduites.get_assiduites_count( + etudid, nt.sem + ) # En BUT, prend tout, sinon ne prend que les semestre validés par le jury if nt.is_apc or ( dec @@ -125,7 +127,7 @@ def etud_get_poursuite_info(sem: dict, etud: dict) -> dict: ("date_debut", s["date_debut"]), ("date_fin", s["date_fin"]), ("periode", "%s - %s" % (s["mois_debut"], s["mois_fin"])), - ("AbsNonJust", nbabs - nbabsjust), + ("AbsNonJust", nbabsnj), ("AbsJust", nbabsjust), ] # ajout des 2 champs notes des modules et classement dans chaque module diff --git a/app/tables/recap.py b/app/tables/recap.py index f26535edc..0c853c352 100644 --- a/app/tables/recap.py +++ b/app/tables/recap.py @@ -620,7 +620,7 @@ class RowRecap(tb.Row): def add_abs(self): "Ajoute les colonnes absences" # Absences (nb d'abs non just. dans ce semestre) - nbabs, nbabsjust = self.table.res.formsemestre.get_abs_count(self.etud.id) + _, nbabsjust, nbabs = self.table.res.formsemestre.get_abs_count(self.etud.id) self.add_cell("nbabs", "Abs", f"{nbabs:1.0f}", "abs", raw_content=nbabs) self.add_cell( "nbabsjust", "Just.", f"{nbabsjust:1.0f}", "abs", raw_content=nbabsjust @@ -691,9 +691,9 @@ class RowRecap(tb.Row): self.add_ue_modimpls_cols(ue, ue_status["is_capitalized"]) self.nb_ues_etud_parcours = len(res.etud_parcours_ues_ids(etud.id)) - ue_valid_txt = ue_valid_txt_html = ( - f"{self.nb_ues_validables}/{self.nb_ues_etud_parcours}" - ) + ue_valid_txt = ( + ue_valid_txt_html + ) = f"{self.nb_ues_validables}/{self.nb_ues_etud_parcours}" if self.nb_ues_warning: ue_valid_txt_html += " " + scu.EMO_WARNING cell_class = "" @@ -717,9 +717,9 @@ class RowRecap(tb.Row): # sous-classé par JuryRow pour ajouter les codes table: TableRecap = self.table formsemestre: FormSemestre = table.res.formsemestre - table.group_titles["col_ue"] = ( - f"UEs du S{formsemestre.semestre_id} {formsemestre.annee_scolaire()}" - ) + table.group_titles[ + "col_ue" + ] = f"UEs du S{formsemestre.semestre_id} {formsemestre.annee_scolaire()}" col_id = f"moy_ue_{ue.id}" val = ( ue_status["moy"] diff --git a/app/templates/assiduites/pages/bilan_etud.j2 b/app/templates/assiduites/pages/bilan_etud.j2 index 15fed8aad..ae494bdbe 100644 --- a/app/templates/assiduites/pages/bilan_etud.j2 +++ b/app/templates/assiduites/pages/bilan_etud.j2 @@ -66,7 +66,6 @@ Bilan assiduité de {{sco.etud.nomprenom}} {% endblock styles %} {% block app_content %} -{% include "assiduites/widgets/tableau_base.j2" %}

Bilan de l'assiduité de {{sco.etud.html_link_fiche()|safe}}

diff --git a/app/templates/assiduites/pages/signal_assiduites_group.j2 b/app/templates/assiduites/pages/signal_assiduites_group.j2 index 12380afaa..4eb21e413 100644 --- a/app/templates/assiduites/pages/signal_assiduites_group.j2 +++ b/app/templates/assiduites/pages/signal_assiduites_group.j2 @@ -1,23 +1,23 @@ {% extends "sco_page.j2" %} {% block title %} - {{title}} +{{title}} {% endblock title %} {% block scripts %} - {{ super() }} - - - - - - +{{ super() }} + + + + + + - + {% endblock scripts %} {% block styles %} - {{ super() }} - - - - - +{{ super() }} + + + + + {% endblock styles %} @@ -80,7 +80,11 @@ {% include "assiduites/widgets/toast.j2" %} {{ minitimeline|safe }} - +
@@ -102,8 +106,7 @@
Groupes : {{grp|safe}}
Date : - +
@@ -162,4 +165,4 @@
-{% endblock app_content %} +{% endblock app_content %} \ No newline at end of file diff --git a/app/templates/assiduites/widgets/tableau.j2 b/app/templates/assiduites/widgets/tableau.j2 index 39bbb2f45..1ab881c66 100644 --- a/app/templates/assiduites/widgets/tableau.j2 +++ b/app/templates/assiduites/widgets/tableau.j2 @@ -174,7 +174,7 @@ } window.addEventListener('load', ()=>{ - const table_columns = [...document.querySelectorAll('.external-sort')]; + const table_columns = [...document.querySelectorAll('th.external-sort')]; table_columns.forEach((e)=>e.addEventListener('click', ()=>{ // récupération de l'ordre "ascending" / "descending" diff --git a/app/templates/assiduites/widgets/tableau_base.j2 b/app/templates/assiduites/widgets/tableau_base.j2 deleted file mode 100644 index 71580a14e..000000000 --- a/app/templates/assiduites/widgets/tableau_base.j2 +++ /dev/null @@ -1,624 +0,0 @@ - - -{% include "assiduites/widgets/alert.j2" %} -{% include "assiduites/widgets/prompt.j2" %} - - - - \ No newline at end of file diff --git a/app/templates/assiduites/widgets/tableau_justi.j2 b/app/templates/assiduites/widgets/tableau_justi.j2 deleted file mode 100644 index 06e5c6e51..000000000 --- a/app/templates/assiduites/widgets/tableau_justi.j2 +++ /dev/null @@ -1,679 +0,0 @@ - - - - - - - - - - - - -
-
- Début - -
-
-
- Fin - -
-
-
- État - -
-
-
- Raison - -
-
-
- Fichier - -
-
-
-
- - - diff --git a/app/templates/sidebar.j2 b/app/templates/sidebar.j2 index f07e68084..00c612301 100755 --- a/app/templates/sidebar.j2 +++ b/app/templates/sidebar.j2 @@ -56,8 +56,8 @@ Absences {% if sco.etud_cur_sem %} - ({{sco.prefs["assi_metrique"]}}) + ({{sco.prefs["assi_metrique"]}})
{{'%1.0f'|format(sco.nbabsjust)}} J., {{'%1.0f'|format(sco.nbabsnj)}} N.J.
{% endif %}
    diff --git a/app/views/__init__.py b/app/views/__init__.py index 890fb63eb..b28a4e57c 100644 --- a/app/views/__init__.py +++ b/app/views/__init__.py @@ -74,8 +74,9 @@ class ScoData: if ins: self.etud_cur_sem = ins.formsemestre ( - self.nbabs, + self.nbabsnj, self.nbabsjust, + self.nbabs, ) = sco_assiduites.get_assiduites_count_in_interval( etud.id, self.etud_cur_sem.date_debut.isoformat(), @@ -84,7 +85,6 @@ class ScoData: sco_preferences.get_preference("assi_metrique") ), ) - self.nbabsnj = self.nbabs - self.nbabsjust else: self.etud_cur_sem = None else: diff --git a/app/views/assiduites.py b/app/views/assiduites.py index f892edc6d..6b2e931a2 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -64,7 +64,7 @@ from app.models import ( ) from app.scodoc.codes_cursus import UE_STANDARD from app.auth.models import User -from app.models.assiduites import get_assiduites_justif, compute_assiduites_justified +from app.models.assiduites import get_assiduites_justif from app.tables.list_etuds import RowEtud, TableEtud import app.tables.liste_assiduites as liste_assi @@ -468,7 +468,7 @@ def _record_assiduite_etud( ) # On met à jour les assiduités en fonction du nouveau justificatif - compute_assiduites_justified(etud.id, [justi]) + justi.justifier_assiduites() # Invalider cache scass.simple_invalidate_cache(ass.to_dict(), etud.id) @@ -778,6 +778,7 @@ def _record_justificatif_etud( form.date_debut.data = dt_debut_tz_server form.date_fin.data = dt_fin_tz_server form.entry_date.data = dt_entry_date_tz_server + justif.dejustifier_assiduites() if justif.edit_from_form(form): message = "Justificatif modifié" @@ -792,7 +793,6 @@ def _record_justificatif_etud( ) else: message = "Pas de modification" - else: justif = Justificatif.create_justificatif( etud, @@ -816,7 +816,7 @@ def _record_justificatif_etud( # pour utiliser le "reset" (remise en "non_just") des assiduités # (à terme, il faudrait ne recalculer que les assiduités impactées) # VOIR TODO dans compute_assiduites_justified - compute_assiduites_justified(etud.id, reset=True) + justif.justifier_assiduites() scass.simple_invalidate_cache(justif.to_dict(), etud.id) flash(message) return True @@ -1595,7 +1595,7 @@ def tableau_assiduite_actions(): user_id=current_user.id, ) - compute_assiduites_justified(objet.etudiant.id, [justificatif_correspondant]) + justificatif_correspondant.justifier_assiduites() scass.simple_invalidate_cache( justificatif_correspondant.to_dict(), objet.etudiant.id ) @@ -1707,9 +1707,10 @@ def _action_modifier_justificatif(justi: Justificatif): justi.fichier = archive_name + justi.dejustifier_assiduites() db.session.add(justi) db.session.commit() - scass.compute_assiduites_justified(justi.etudid, reset=True) + justi.justifier_assiduites() scass.simple_invalidate_cache(justi.to_dict(True), justi.etudid) @@ -2181,12 +2182,8 @@ def _module_selector(formsemestre: FormSemestre, moduleimpl_id: int = None) -> s """ # récupération des ues du semestre ntc: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) - ues = ntc.get_ues_stat_dict() - modimpls_list: list[dict] = [] - for ue in ues: - # Ajout des moduleimpl de chaque ue dans la liste des moduleimpls - modimpls_list += ntc.get_modimpls_dict(ue_id=ue["ue_id"]) + modimpls_list: list[dict] = ntc.get_modimpls_dict() # prévoie la sélection par défaut d'un moduleimpl s'il a été passé en paramètre selected = "" if moduleimpl_id is not None else "selected" diff --git a/app/views/notes.py b/app/views/notes.py index 717131db3..570b64f52 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -1187,14 +1187,18 @@ def view_module_abs(moduleimpl_id, fmt="html"): rows = [] for etud in inscrits: - nb_abs, nb_abs_just = sco_assiduites.formsemestre_get_assiduites_count( + ( + nb_abs_nj, + nb_abs_just, + nb_abs, + ) = sco_assiduites.formsemestre_get_assiduites_count( etud.id, modimpl.formsemestre, moduleimpl_id=modimpl.id ) rows.append( { "nomprenom": etud.nomprenom, "just": nb_abs_just, - "nojust": nb_abs - nb_abs_just, + "nojust": nb_abs_nj, "total": nb_abs, "_nomprenom_target": url_for( "scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id diff --git a/tests/api/test_api_justificatifs.py b/tests/api/test_api_justificatifs.py index 5e73d2c71..5ae86528b 100644 --- a/tests/api/test_api_justificatifs.py +++ b/tests/api/test_api_justificatifs.py @@ -38,6 +38,8 @@ JUSTIFICATIFS_FIELDS = { "external_data": dict, } +DEPT_JUSTIFICATIFS_FIELDS = JUSTIFICATIFS_FIELDS | {"formsemestre": dict | None} + CREATE_FIELD = {"justif_id": int, "couverture": list} BATCH_FIELD = {"errors": list, "success": list} @@ -169,6 +171,32 @@ def test_route_justificatifs(api_headers): check_failure_get(f"/justificatifs/{FAUX}/query?", api_headers) +def test_route_justificatifs_formsemestre(api_headers): + """test de la route /justificatifs/formsemestre/""" + # Bon fonctionnement + + data = GET(path="/justificatifs/formsemestre/1", headers=api_headers) + assert isinstance(data, list) + for just in data: + check_fields(just, JUSTIFICATIFS_FIELDS) + + # Mauvais fonctionnement + check_failure_get(path="/justificatifs/formsemestre/42069", headers=api_headers) + + +def test_justificatifs_dept(api_headers): + """test de la route /justificatifs/dept/""" + # Bon fonctionnement + + data = GET(path="/justificatifs/dept/1", headers=api_headers) + assert isinstance(data, list) + for just in data: + check_fields(just, DEPT_JUSTIFICATIFS_FIELDS) + + # Mauvais fonctionnement + check_failure_get(path="/justificatifs/dept/42069", headers=api_headers) + + def test_route_create(api_admin_headers): """test de la route /justificatif//create""" # -== Unique ==- diff --git a/tests/unit/test_assiduites.py b/tests/unit/test_assiduites.py index 0cc7ce594..3063c9730 100644 --- a/tests/unit/test_assiduites.py +++ b/tests/unit/test_assiduites.py @@ -20,7 +20,6 @@ from app.models import ( ModuleImpl, Absence, ) -from app.models.assiduites import compute_assiduites_justified from app.scodoc.sco_exceptions import ScoValueError from tests.unit import sco_fake_gen @@ -73,8 +72,6 @@ def test_general(test_client): verifier_comptage_et_filtrage_assiduites(etuds, moduleimpls[:4], formsemestres) verifier_filtrage_justificatifs(etuds[0], justificatifs) - essais_cache(etuds[0].etudid, formsemestres[:2], moduleimpls) - editer_supprimer_assiduites(etuds, moduleimpls) editer_supprimer_justificatif(etuds[0]) @@ -403,54 +400,6 @@ def _get_justi( ).first() -def essais_cache(etudid, sems: tuple[FormSemestre], moduleimpls: list[ModuleImpl]): - """Vérification des fonctionnalités du cache""" - # TODO faire un test séparé du test_general - # voir test_calcul_assiduites pour faire - - date_deb: str = "2022-09-01T07:00" - date_fin: str = "2023-01-31T19:00" - - assiduites_count_no_cache = scass.get_assiduites_count_in_interval( - etudid, date_deb, date_fin - ) - assiduites_count_cache = scass.get_assiduites_count_in_interval( - etudid, date_deb, date_fin - ) - - assert ( - assiduites_count_cache == assiduites_count_no_cache == (2, 1) - ), "Erreur cache classique" - - assert scass.formsemestre_get_assiduites_count(etudid, sems[0]) == ( - 2, - 1, - ), "Erreur formsemestre_get_assiduites_count (sans module) A" - assert scass.formsemestre_get_assiduites_count(etudid, sems[1]) == ( - 0, - 0, - ), "Erreur formsemestre_get_assiduites_count (sans module) B" - - assert scass.formsemestre_get_assiduites_count( - etudid, sems[0], moduleimpl_id=moduleimpls[0].id - ) == ( - 1, - 1, - ), "Erreur formsemestre_get_assiduites_count (avec module) A" - assert scass.formsemestre_get_assiduites_count( - etudid, sems[0], moduleimpl_id=moduleimpls[1].id - ) == ( - 1, - 0, - ), "Erreur formsemestre_get_assiduites_count (avec module) A" - assert scass.formsemestre_get_assiduites_count( - etudid, sems[0], moduleimpl_id=moduleimpls[2].id - ) == ( - 0, - 0, - ), "Erreur formsemestre_get_assiduites_count (avec module) A" - - def ajouter_justificatifs(etud): """test de l'ajout des justificatifs""" @@ -498,10 +447,9 @@ def ajouter_justificatifs(etud): ) db.session.add(just_obj) db.session.commit() + just_obj.justifier_assiduites() justificatifs.append(just_obj) - compute_assiduites_justified(etud.etudid, justificatifs) - # Vérification de la création des justificatifs assert [ justi for justi in justificatifs if not isinstance(justi, Justificatif) @@ -1416,6 +1364,7 @@ def test_cas_justificatifs(test_client): Tests de certains cas particuliers des justificatifs - Création du justificatif avant ou après assiduité - Assiduité complétement couverte ou non + - Modification de la couverture (edition du justificatif) """ data = _setup_fake_db( @@ -1462,7 +1411,7 @@ def test_cas_justificatifs(test_client): etat=scu.EtatJustificatif.VALIDE, ) - compute_assiduites_justified(etud_1.etudid, [justif_2]) + justif_2.justifier_assiduites() assert len(scass.justifies(justif_2)) == 1, "Justification non prise en compte (b1)" @@ -1496,7 +1445,8 @@ def test_cas_justificatifs(test_client): ) # Mise à jour de l'assiduité - compute_assiduites_justified(etud_1.etudid, [justif_3, justif_4]) + justif_3.justifier_assiduites() + justif_4.justifier_assiduites() assert ( len(scass.justifies(justif_3)) == 1 @@ -1504,3 +1454,279 @@ def test_cas_justificatifs(test_client): assert ( len(scass.justifies(justif_4)) == 0 ), "Justification complète non prise en compte (c2)" + + # <- Vérification modification de la couverture -> + + # Deux assiduités, 8/01/2024 de 8h à 10h et 14h à 16h + + assi_2: Assiduite = Assiduite.create_assiduite( + etud=etud_1, + date_debut=scu.is_iso_formated("2024-01-08T08:00", True), + date_fin=scu.is_iso_formated("2024-01-08T10:00", True), + etat=scu.EtatAssiduite.ABSENT, + ) + assi_3: Assiduite = Assiduite.create_assiduite( + etud=etud_1, + date_debut=scu.is_iso_formated("2024-01-08T14:00", True), + date_fin=scu.is_iso_formated("2024-01-08T16:00", True), + etat=scu.EtatAssiduite.ABSENT, + ) + + # <=>Justification complète<=> + # les deux assiduités sont couvertes + + justif_5: Justificatif = Justificatif.create_justificatif( + etudiant=etud_1, + date_debut=scu.is_iso_formated("2024-01-08T00:00:00", True), + date_fin=scu.is_iso_formated("2024-01-08T23:59:59", True), + etat=scu.EtatJustificatif.VALIDE, + ) + + # Justification des assiduités + assi_ids: list[int] = justif_5.justifier_assiduites() + assert len(assi_ids) == 2, "Vérification Modification couverture (d1)" + assert assi_2.assiduite_id in assi_ids, "Vérification Modification couverture (d2)" + assert assi_3.assiduite_id in assi_ids, "Vérification Modification couverture (d3)" + assert assi_2.est_just is True, "Vérification Modification couverture (d4)" + assert assi_3.est_just is True, "Vérification Modification couverture (d5)" + + # Déjustification des assiduités + justif_5.dejustifier_assiduites() + assi_ids: list[int] = justif_5.dejustifier_assiduites() + assert len(assi_ids) == 2, "Vérification Modification couverture (d6)" + assert assi_2.assiduite_id in assi_ids, "Vérification Modification couverture (d7)" + assert assi_3.assiduite_id in assi_ids, "Vérification Modification couverture (d8)" + assert assi_2.est_just is False, "Vérification Modification couverture (d9)" + assert assi_3.est_just is False, "Vérification Modification couverture (d10)" + + # <=>Justification Partielle<=> + # Seule la première assiduité est couverte + + justif_5.date_fin = scu.is_iso_formated("2024-01-08T11:00", True) + db.session.add(justif_5) + db.session.commit() + + assi_ids: list[int] = justif_5.justifier_assiduites() + assert len(assi_ids) == 1, "Vérification Modification couverture (e1)" + assert assi_2.assiduite_id in assi_ids, "Vérification Modification couverture (e2)" + assert ( + assi_3.assiduite_id not in assi_ids + ), "Vérification Modification couverture (e3)" + assert assi_2.est_just is True, "Vérification Modification couverture (e4)" + assert assi_3.est_just is False, "Vérification Modification couverture (e5)" + + assi_ids: list[int] = justif_5.dejustifier_assiduites() + assert len(assi_ids) == 1, "Vérification Modification couverture (e6)" + assert assi_2.assiduite_id in assi_ids, "Vérification Modification couverture (e7)" + assert ( + assi_3.assiduite_id not in assi_ids + ), "Vérification Modification couverture (e3)" + assert assi_2.est_just is False, "Vérification Modification couverture (e8)" + assert assi_3.est_just is False, "Vérification Modification couverture (e9)" + + # <=>Justification Multiple<=> + # Deux justificatifs couvrent une même assiduité + + # on justifie la première assiduité avec le premier justificatif + justif_5.justifier_assiduites() + + # deuxième justificatif + justif_6: Justificatif = Justificatif.create_justificatif( + etudiant=etud_1, + date_debut=scu.is_iso_formated("2024-01-08T08:00", True), + date_fin=scu.is_iso_formated("2024-01-08T10:00", True), + etat=scu.EtatJustificatif.VALIDE, + ) + + assi_ids: list[int] = justif_6.justifier_assiduites() + assert len(assi_ids) == 1, "Vérification Modification couverture (f1)" + assert assi_2.assiduite_id in assi_ids, "Vérification Modification couverture (f2)" + assert ( + assi_3.assiduite_id not in assi_ids + ), "Vérification Modification couverture (f3)" + assert assi_2.est_just is True, "Vérification Modification couverture (f4)" + assert assi_3.est_just is False, "Vérification Modification couverture (f5)" + + # on déjustifie le justificatif 5 + justif_5.etat = scu.EtatJustificatif.NON_VALIDE + db.session.add(justif_5) + db.session.commit() + + assi_ids: list[int] = justif_5.dejustifier_assiduites() + assert len(assi_ids) == 0, "Vérification Modification couverture (f6)" + assert ( + assi_2.assiduite_id not in assi_ids + ), "Vérification Modification couverture (f7)" + assert assi_2.est_just is True, "Vérification Modification couverture (f8)" + + # on déjustifie le justificatif 6 + justif_6.etat = scu.EtatJustificatif.NON_VALIDE + db.session.add(justif_6) + db.session.commit() + assi_ids: list[int] = justif_6.dejustifier_assiduites() + assert len(assi_ids) == 1, "Vérification Modification couverture (f9)" + assert assi_2.assiduite_id in assi_ids, "Vérification Modification couverture (f10)" + assert assi_2.est_just is False, "Vérification Modification couverture (f11)" + + # <=>Justification Chevauchée<=> + # 1 justificatif chevauche une assiduité (8h -> 10h) (9h -> 11h) + + justif_7: Justificatif = Justificatif.create_justificatif( + etudiant=etud_1, + date_debut=scu.is_iso_formated("2024-01-08T09:00", True), + date_fin=scu.is_iso_formated("2024-01-08T11:00", True), + etat=scu.EtatJustificatif.VALIDE, + ) + + assi_ids: list[int] = justif_7.justifier_assiduites() + assert len(assi_ids) == 0, "Vérification Modification couverture (g1)" + assert ( + assi_2.assiduite_id not in assi_ids + ), "Vérification Modification couverture (g2)" + assert assi_2.est_just is False, "Vérification Modification couverture (g3)" + + # Modification pour correspondre à l'assiduité + justif_7.date_debut = scu.is_iso_formated("2024-01-08T08:00", True) + db.session.add(justif_7) + db.session.commit() + + assi_ids: list[int] = justif_7.justifier_assiduites() + assert len(assi_ids) == 1, "Vérification Modification couverture (g4)" + assert assi_2.assiduite_id in assi_ids, "Vérification Modification couverture (g5)" + assert assi_2.est_just is True, "Vérification Modification couverture (g6)" + + +def test_cache_assiduites(test_client): + """Vérification du bon fonctionnement du cache des assiduités""" + + data = _setup_fake_db( + [("2024-01-01", "2024-06-30"), ("2024-07-01", "2024-12-31")], + 1, + 1, + ) + + formsemestre1: FormSemestre = data["formsemestres"][0] + formsemestre2: FormSemestre = data["formsemestres"][1] + + moduleimpl: ModuleImpl = data["moduleimpls"][0] + etud: Identite = data["etuds"][0] + + # Création des assiduités + assiduites: list[dict] = [ + # Semestre 1 + { + "date_debut": "2024-01-08T08:00", + "date_fin": "2024-01-08T10:00", + "moduleimpl": moduleimpl, + }, + { + "date_debut": "2024-01-08T14:00", + "date_fin": "2024-01-08T16:00", + "moduleimpl": moduleimpl, + }, + { + "date_debut": "2024-01-09T08:00", + "date_fin": "2024-01-09T10:00", + "moduleimpl": None, + }, + { + "date_debut": "2024-01-09T14:00", + "date_fin": "2024-01-09T16:00", + "moduleimpl": None, + }, + { + "date_debut": "2024-01-10T08:00", + "date_fin": "2024-01-10T10:00", + "moduleimpl": None, + }, + { + "date_debut": "2024-01-10T14:00", + "date_fin": "2024-01-10T16:00", + "moduleimpl": moduleimpl, + }, + # Semestre 2 + { + "date_debut": "2024-07-09T14:00", + "date_fin": "2024-07-09T16:00", + "moduleimpl": None, + }, + { + "date_debut": "2024-07-10T08:00", + "date_fin": "2024-07-10T10:00", + "moduleimpl": None, + }, + { + "date_debut": "2024-07-10T14:00", + "date_fin": "2024-07-10T16:00", + "moduleimpl": None, + }, + ] + + justificatifs: list[dict] = [ + { + "date_debut": "2024-01-10T00:00", + "date_fin": "2024-01-10T23:59", + }, + { + "date_debut": "2024-07-09T00:00", + "date_fin": "2024-07-09T23:59", + }, + ] + + # On ajoute les assiduités et les justificatifs + + for assi in assiduites: + Assiduite.create_assiduite( + etud=etud, + date_debut=scu.is_iso_formated(assi["date_debut"], True), + date_fin=scu.is_iso_formated(assi["date_fin"], True), + moduleimpl=assi["moduleimpl"], + etat=scu.EtatAssiduite.ABSENT, + ) + + for justi in justificatifs: + Justificatif.create_justificatif( + etudiant=etud, + date_debut=scu.is_iso_formated(justi["date_debut"], True), + date_fin=scu.is_iso_formated(justi["date_fin"], True), + etat=scu.EtatJustificatif.VALIDE, + ).justifier_assiduites() + + # Premier semestre 4nj / 2j / 6t + assert scass.get_assiduites_count(etud.id, formsemestre1.to_dict()) == (4, 2, 6) + assert scass.formsemestre_get_assiduites_count(etud.id, formsemestre1) == (4, 2, 6) + + # ModuleImpl 2nj / 1j / 3t + assert scass.formsemestre_get_assiduites_count( + etud.id, formsemestre1, moduleimpl.id + ) == (2, 1, 3) + # Deuxième semestre 2nj / 1j / 3t + assert scass.get_assiduites_count(etud.id, formsemestre2.to_dict()) == (2, 1, 3) + + # On supprime la première assiduité (sans invalider le cache) + assi: Assiduite = Assiduite.query.filter_by(etudid=etud.id).first() + db.session.delete(assi) + db.session.commit() + + # Premier semestre 4nj / 2j / 6t (Identique car cache) + assert scass.get_assiduites_count(etud.id, formsemestre1.to_dict()) == (4, 2, 6) + assert scass.formsemestre_get_assiduites_count(etud.id, formsemestre1) == (4, 2, 6) + # ModuleImpl 1nj / 1j / 2t (Change car non cache) + assert scass.formsemestre_get_assiduites_count( + etud.id, formsemestre1, moduleimpl.id + ) == (1, 1, 2) + # Deuxième semestre 2nj / 1j / 3t (Identique car cache et non modifié) + assert scass.get_assiduites_count(etud.id, formsemestre2.to_dict()) == (2, 1, 3) + + # On invalide maintenant le cache + scass.invalidate_assiduites_count(etud.id, formsemestre1.to_dict()) + + # Premier semestre 3nj / 2j / 5t (Change car cache invalidé) + assert scass.get_assiduites_count(etud.id, formsemestre1.to_dict()) == (3, 2, 5) + assert scass.formsemestre_get_assiduites_count(etud.id, formsemestre1) == (3, 2, 5) + # ModuleImpl 1nj / 1j / 2t (Ne change pas car pas de changement) + assert scass.formsemestre_get_assiduites_count( + etud.id, formsemestre1, moduleimpl.id + ) == (1, 1, 2) + # Deuxième semestre 2nj / 1j / 3t (Identique car cache et non modifié) + assert scass.get_assiduites_count(etud.id, formsemestre2.to_dict()) == (2, 1, 3) diff --git a/tests/unit/test_sco_basic.py b/tests/unit/test_sco_basic.py index d4cb7a729..b3bba2e20 100644 --- a/tests/unit/test_sco_basic.py +++ b/tests/unit/test_sco_basic.py @@ -33,7 +33,6 @@ from app.scodoc import sco_formsemestre_validation from app.scodoc import sco_cursus_dut from app.scodoc import sco_saisie_notes from app.scodoc.sco_utils import EtatAssiduite, EtatJustificatif, localize_datetime -from app.models.assiduites import compute_assiduites_justified DEPT = TestConfig.DEPT_TEST @@ -192,7 +191,7 @@ def run_sco_basic(verbose=False, dept=None) -> FormSemestre: etudid = etuds[0]["etudid"] _signal_absences_justificatifs(etudid) - nbabs, nbabsjust = scass.get_assiduites_count(etudid, sem) + _, nbabsjust, nbabs = scass.get_assiduites_count(etudid, sem) assert nbabs == 6, f"incorrect nbabs ({nbabs})" assert nbabsjust == 2, f"incorrect nbabsjust ({nbabsjust})" @@ -267,8 +266,5 @@ def _signal_absences_justificatifs(etudid: int): etat=EtatJustificatif.VALIDE, ) db.session.add(justif) - compute_assiduites_justified( - etud.etudid, - [justif], - ) db.session.commit() + justif.justifier_assiduites() diff --git a/tools/fakedatabase/create_test_api_database.py b/tools/fakedatabase/create_test_api_database.py index 5b3e96a2c..cd5182bee 100644 --- a/tools/fakedatabase/create_test_api_database.py +++ b/tools/fakedatabase/create_test_api_database.py @@ -396,7 +396,7 @@ def ajouter_assiduites_justificatifs(formsemestre: FormSemestre): for etud in formsemestre.etuds: base_date = datetime.datetime( - 2022, 9, [5, 12, 19, 26][random.randint(0, 3)], 8, 0, 0 + 2021, 9, [6, 13, 20, 27][random.randint(0, 3)], 8, 0, 0 ) base_date = localize_datetime(base_date)