diff --git a/app/api/assiduites.py b/app/api/assiduites.py index ec47f518a..bbd6142f9 100644 --- a/app/api/assiduites.py +++ b/app/api/assiduites.py @@ -601,9 +601,12 @@ def _create_singular( moduleimpl_id = data.get("moduleimpl_id", False) moduleimpl: ModuleImpl = None - if moduleimpl_id not in [False, None]: + if moduleimpl_id not in [False, None, "", "-1"]: if moduleimpl_id != "autre": - moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first() + try: + moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first() + except ValueError: + moduleimpl = None if moduleimpl is None: errors.append("param 'moduleimpl_id': invalide") else: @@ -810,7 +813,7 @@ def _edit_singular(assiduite_unique, data): moduleimpl: ModuleImpl = None if moduleimpl_id is not False: - if moduleimpl_id is not None: + if moduleimpl_id not in [None, "", "-1"]: if moduleimpl_id == "autre": assiduite_unique.moduleimpl_id = None external_data = ( @@ -823,7 +826,13 @@ def _edit_singular(assiduite_unique, data): assiduite_unique.external_data = external_data else: - moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first() + try: + moduleimpl = ModuleImpl.query.filter_by( + id=int(moduleimpl_id) + ).first() + except ValueError: + moduleimpl = None + if moduleimpl is None: errors.append("param 'moduleimpl_id': invalide") else: @@ -834,7 +843,7 @@ def _edit_singular(assiduite_unique, data): else: assiduite_unique.moduleimpl_id = moduleimpl_id else: - assiduite_unique.moduleimpl_id = moduleimpl_id + assiduite_unique.moduleimpl_id = None # Cas 3 : desc desc = data.get("desc", False) diff --git a/app/models/assiduites.py b/app/models/assiduites.py index 7f5520df7..0f8933e73 100644 --- a/app/models/assiduites.py +++ b/app/models/assiduites.py @@ -350,11 +350,15 @@ def compute_assiduites_justified( if justificatifs is None: justificatifs: Justificatif = Justificatif.query.filter_by(etudid=etudid).all() + justificatifs = [j for j in justificatifs if j.etat == EtatJustificatif.VALIDE] + assiduites: Assiduite = Assiduite.query.filter_by(etudid=etudid) assiduites_justifiees: list[int] = [] for assi in assiduites: + if assi.etat == EtatAssiduite.PRESENT: + continue if any( assi.date_debut >= j.date_debut and assi.date_fin <= j.date_fin for j in justificatifs diff --git a/app/models/etudiants.py b/app/models/etudiants.py index e00f84650..a451526f7 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -74,9 +74,11 @@ class Identite(db.Model): ) # Relations avec les assiduites et les justificatifs - assiduites = db.relationship("Assiduite", back_populates="etudiant", lazy="dynamic") + assiduites = db.relationship( + "Assiduite", back_populates="etudiant", lazy="dynamic", cascade="all, delete" + ) justificatifs = db.relationship( - "Justificatif", back_populates="etudiant", lazy="dynamic" + "Justificatif", back_populates="etudiant", lazy="dynamic", cascade="all, delete" ) def __repr__(self): diff --git a/app/scodoc/sco_assiduites.py b/app/scodoc/sco_assiduites.py index 2a478843b..07e778cf3 100644 --- a/app/scodoc/sco_assiduites.py +++ b/app/scodoc/sco_assiduites.py @@ -212,7 +212,7 @@ def get_assiduites_stats( output: dict = {} calculator: CountCalculator = CountCalculator() - if "split" not in filtered: + if filtered is None or "split" not in filtered: calculator.compute_assiduites(assiduites) count: dict = calculator.to_dict() @@ -382,7 +382,10 @@ def justifies(justi: Justificatif, obj: bool = False) -> list[int] or Query: def get_all_justified( - etudid: int, date_deb: datetime = None, date_fin: datetime = None + etudid: int, + date_deb: datetime = None, + date_fin: datetime = None, + moduleimpl_id: int = None, ) -> Query: """Retourne toutes les assiduités justifiées sur une période""" @@ -393,7 +396,9 @@ def get_all_justified( date_deb = scu.localize_datetime(date_deb) date_fin = scu.localize_datetime(date_fin) - justified = Assiduite.query.filter_by(est_just=True, etudid=etudid) + justified: Query = Assiduite.query.filter_by(est_just=True, etudid=etudid) + if moduleimpl_id is not None: + justified = justified.filter_by(moduleimpl_id=moduleimpl_id) after = filter_by_date( justified, Assiduite, @@ -419,7 +424,7 @@ def get_assiduites_count(etudid: int, sem: dict) -> tuple[int, int]: def formsemestre_get_assiduites_count( - etudid: int, formsemestre: FormSemestre + 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) @@ -428,9 +433,14 @@ def formsemestre_get_assiduites_count( metrique = sco_preferences.get_preference("assi_metrique", formsemestre.id) return get_assiduites_count_in_interval( etudid, - date_debut=formsemestre.date_debut, - date_fin=formsemestre.date_fin, + date_debut=scu.localize_datetime( + datetime.combine(formsemestre.date_debut, time(8, 0)) + ), + date_fin=scu.localize_datetime( + datetime.combine(formsemestre.date_fin, time(18, 0)) + ), metrique=scu.translate_assiduites_metric(metrique), + moduleimpl_id=moduleimpl_id, ) @@ -441,6 +451,7 @@ def get_assiduites_count_in_interval( metrique="demi", date_debut: datetime = None, date_fin: datetime = None, + moduleimpl_id: int = None, ): """Les comptes d'absences de cet étudiant entre ces deux dates, incluses: tuple (nb abs, nb abs justifiées) @@ -452,33 +463,39 @@ def get_assiduites_count_in_interval( key = f"{etudid}_{date_debut_iso}_{date_fin_iso}{metrique}_assiduites" r = sco_cache.AbsSemEtudCache.get(key) - if not r: + if not r or moduleimpl_id is not None: date_debut: datetime = date_debut or datetime.fromisoformat(date_debut_iso) date_fin: datetime = date_fin or datetime.fromisoformat(date_fin_iso) - assiduites: Assiduite = Assiduite.query.filter_by(etudid=etudid) + assiduites: Query = Assiduite.query.filter_by(etudid=etudid) assiduites = assiduites.filter(Assiduite.etat == scu.EtatAssiduite.ABSENT) justificatifs: Justificatif = Justificatif.query.filter_by(etudid=etudid) assiduites = filter_by_date(assiduites, Assiduite, date_debut, date_fin) + + if moduleimpl_id is not None: + assiduites = assiduites.filter_by(moduleimpl_id=moduleimpl_id) + justificatifs = filter_by_date( justificatifs, Justificatif, date_debut, date_fin ) - calculator: CountCalculator = CountCalculator() calculator.compute_assiduites(assiduites) nb_abs: dict = calculator.to_dict()[metrique] - abs_just: list[Assiduite] = get_all_justified(etudid, date_debut, date_fin) + abs_just: list[Assiduite] = get_all_justified( + etudid, date_debut, date_fin, moduleimpl_id + ) calculator.reset() calculator.compute_assiduites(abs_just) nb_abs_just: dict = calculator.to_dict()[metrique] r = (nb_abs, nb_abs_just) - ans = sco_cache.AbsSemEtudCache.set(key, r) - if not ans: - log("warning: get_assiduites_count failed to cache") + if moduleimpl_id is None: + ans = sco_cache.AbsSemEtudCache.set(key, r) + if not ans: + log("warning: get_assiduites_count failed to cache") return r diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index ee8417f3b..00f0604ef 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -237,7 +237,7 @@ def localize_datetime(date: datetime.datetime or str) -> datetime.datetime: new_date: datetime.datetime = date if new_date.tzinfo is None: try: - new_date = timezone("Europe/Paris").localize(date) + new_date = TIME_ZONE.localize(date) except OverflowError: new_date = timezone("UTC").localize(date) return new_date diff --git a/app/static/js/assiduites.js b/app/static/js/assiduites.js index 2ee935efc..73d872ec6 100644 --- a/app/static/js/assiduites.js +++ b/app/static/js/assiduites.js @@ -811,11 +811,8 @@ function numberTimeToDate(nb) { * @param {boolean} clear vidage de l'objet "assiduites" ou non * @returns {object} l'objets Assiduités { : [,]} */ -function getAssiduitesFromEtuds(clear, has_formsemestre = true, deb, fin) { +function getAssiduitesFromEtuds(clear, deb, fin) { const etudIds = Object.keys(etuds).join(","); - const formsemestre_id = has_formsemestre - ? `formsemestre_id=${getFormSemestreId()}&` - : ""; const date_debut = deb ? deb : toIsoString(getPrevDate()); const date_fin = fin ? fin : toIsoString(getNextDate()); @@ -826,7 +823,7 @@ function getAssiduitesFromEtuds(clear, has_formsemestre = true, deb, fin) { const url_api = getUrl() + - `/api/assiduites/group/query?date_debut=${date_debut}&${formsemestre_id}&date_fin=${date_fin}&etudids=${etudIds}`; + `/api/assiduites/group/query?date_debut=${date_debut}&date_fin=${date_fin}&etudids=${etudIds}`; sync_get(url_api, (data, status) => { if (status === "success") { const dataKeys = Object.keys(data); @@ -924,14 +921,11 @@ function deleteAssiduite(assiduite_id) { function hasModuleImpl(assiduite) { if (assiduite.moduleimpl_id != null) return true; - if ( - "external_data" in assiduite && - assiduite.external_data instanceof Object && - "module" in assiduite.external_data - ) - return true; - - return false; + return ( + assiduite.hasOwnProperty("external_data") && + assiduite.external_data != null && + assiduite.external_data.hasOwnProperty("module") + ); } /** @@ -1057,16 +1051,13 @@ function getAssiduiteValue(field) { * Mise à jour des assiduités d'un étudiant * @param {String | Number} etudid identifiant de l'étudiant */ -function actualizeEtudAssiduite(etudid, has_formsemestre = true) { - const formsemestre_id = has_formsemestre - ? `formsemestre_id=${getFormSemestreId()}&` - : ""; +function actualizeEtudAssiduite(etudid) { const date_debut = toIsoString(getPrevDate()); const date_fin = toIsoString(getNextDate()); const url_api = getUrl() + - `/api/assiduites/${etudid}/query?${formsemestre_id}date_debut=${date_debut}&date_fin=${date_fin}`; + `/api/assiduites/${etudid}/query?date_debut=${date_debut}&date_fin=${date_fin}`; sync_get(url_api, (data, status) => { if (status === "success") { assiduites[etudid] = data; @@ -1331,7 +1322,7 @@ function insertEtudRow(etud, index, output = false) { * @param {String | Number} etudid l'identifiant de l'étudiant */ function actualizeEtud(etudid) { - actualizeEtudAssiduite(etudid, !isSingleEtud()); + actualizeEtudAssiduite(etudid); //Actualize row const etudHolder = document.querySelector(".etud_holder"); const ancient_row = document.getElementById(`etud_row_${etudid}`); @@ -1412,10 +1403,10 @@ function setModuleImplId(assiduite, module = null) { const moduleimpl = module == null ? getModuleImplId() : module; if (moduleimpl === "autre") { if ( - "external_data" in assiduite && - assiduite.external_data instanceof Object + assiduite.hasOwnProperty("external_data") && + assiduite.external_data != null ) { - if ("module" in assiduite.external_data) { + if (assiduite.external_data.hasOwnProperty("module")) { assiduite.external_data.module = "Autre"; } else { assiduite["external_data"] = { module: "Autre" }; @@ -1427,10 +1418,10 @@ function setModuleImplId(assiduite, module = null) { } else { assiduite["moduleimpl_id"] = moduleimpl; if ( - "external_data" in assiduite && - assiduite.external_data instanceof Object + assiduite.hasOwnProperty("external_data") && + assiduite.external_data != null ) { - if ("module" in assiduite.external_data) { + if (assiduite.external_data.hasOwnProperty("module")) { delete assiduite.external_data.module; } } @@ -1482,9 +1473,9 @@ function getCurrentAssiduiteModuleImplId() { let mod = currentAssiduites[0].moduleimpl_id; if ( mod == null && - "external_data" in currentAssiduites[0] && - currentAssiduites[0].external_data instanceof Object && - "module" in currentAssiduites[0].external_data + currentAssiduites[0].hasOwnProperty("external_data") && + currentAssiduites[0].external_data != null && + currentAssiduites[0].external_data.hasOwnProperty("module") ) { mod = currentAssiduites[0].external_data.module; } @@ -1696,9 +1687,9 @@ function getModuleImpl(assiduite) { if (id == null || id == undefined) { if ( - "external_data" in assiduite && - assiduite.external_data instanceof Object && - "module" in assiduite.external_data + assiduite.hasOwnProperty("external_data") && + assiduite.external_data != null && + assiduite.external_data.hasOwnProperty("module") ) { return assiduite.external_data.module; } else { @@ -1724,10 +1715,12 @@ function getModuleImpl(assiduite) { } function getUser(obj) { - if ("external_data" in obj && obj.external_data != null) { - if ("enseignant" in obj.external_data) { - return obj.external_data.enseignant; - } + if ( + obj.hasOwnProperty("external_data") && + obj.external_data != null && + obj.external_data.hasOwnProperty("enseignant") + ) { + return obj.external_data.enseignant; } return obj.user_id; diff --git a/app/templates/assiduites/widgets/differee.j2 b/app/templates/assiduites/widgets/differee.j2 index aa42a3e5a..8d96c5bc4 100644 --- a/app/templates/assiduites/widgets/differee.j2 +++ b/app/templates/assiduites/widgets/differee.j2 @@ -533,7 +533,7 @@ } if (get) { - getAssiduitesFromEtuds(false, false, d_debut.format(), d_fin.format()) + getAssiduitesFromEtuds(false, d_debut.format(), d_fin.format()) return 0x0; } diff --git a/app/templates/assiduites/widgets/tableau_assi.j2 b/app/templates/assiduites/widgets/tableau_assi.j2 index c40d211b0..78c34cf00 100644 --- a/app/templates/assiduites/widgets/tableau_assi.j2 +++ b/app/templates/assiduites/widgets/tableau_assi.j2 @@ -184,8 +184,11 @@ path, (data) => { let module = data.moduleimpl_id; - - if (module == null && "external_data" in data && "module" in data.external_data) { + if ( + module == null && data.hasOwnProperty("external_data") && + data.external_data != null && + data.external_data.hasOwnProperty('module') + ) { module = data.external_data.module.toLowerCase(); } diff --git a/app/views/notes.py b/app/views/notes.py index 9f69cbd97..f3ad9e440 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -1174,22 +1174,9 @@ def view_module_abs(moduleimpl_id, fmt="html"): rows = [] for etud in inscrits: - # TODO-ASSIDUITE ne va pas car ne filtre pas sur le moduleimpl - # nb_abs, nb_abs_just = sco_assiduites.formsemestre_get_assiduites_count(etud.id, modimpl.formsemestre) - nb_abs, nb_abs_just = 0, 0 # XXX TODO-ASSIDUITE - # nb_abs = sco_abs.count_abs( - # etudid=etud.id, - # debut=debut_sem, - # fin=fin_sem, - # moduleimpl_id=moduleimpl_id, - # ) - # if nb_abs: - # nb_abs_just = sco_abs.count_abs_just( - # etudid=etud.id, - # debut=debut_sem, - # fin=fin_sem, - # moduleimpl_id=moduleimpl_id, - # ) + nb_abs, nb_abs_just = sco_assiduites.formsemestre_get_assiduites_count( + etud.id, modimpl.formsemestre, moduleimpl_id=modimpl.id + ) rows.append( { "nomprenom": etud.nomprenom, diff --git a/tests/unit/test_assiduites.py b/tests/unit/test_assiduites.py index 725be9491..50f92c649 100644 --- a/tests/unit/test_assiduites.py +++ b/tests/unit/test_assiduites.py @@ -13,8 +13,7 @@ import app.scodoc.sco_assiduites as scass import app.scodoc.sco_utils as scu from app import db from app.models import Assiduite, FormSemestre, Identite, Justificatif, ModuleImpl - -# from app.scodoc import sco_abs_views, sco_formsemestre TODO-ASSIDUITE +from app.models.assiduites import compute_assiduites_justified from app.scodoc.sco_exceptions import ScoValueError from tests.unit import sco_fake_gen @@ -39,7 +38,6 @@ def test_bi_directional_enum(test_client): assert BiInt.inverse()[1] == BiInt.A and BiInt.inverse()[2] == BiInt.B -@pytest.mark.skip # XXX TODO-ASSIDUITE (issue #690) def test_general(test_client): """tests général du modèle assiduite""" @@ -80,11 +78,9 @@ def test_general(test_client): date_fin="31/07/2024", ) - formsemestre_1 = sco_formsemestre.get_formsemestre( - formsemestre_id_1 - ) # Utiliser plutot FormSemestre de nos jours TODO-ASSIDUITE - formsemestre_2 = sco_formsemestre.get_formsemestre(formsemestre_id_2) - formsemestre_3 = sco_formsemestre.get_formsemestre(formsemestre_id_3) + formsemestre_1 = FormSemestre.get_formsemestre(formsemestre_id_1) + formsemestre_2 = FormSemestre.get_formsemestre(formsemestre_id_2) + formsemestre_3 = FormSemestre.get_formsemestre(formsemestre_id_3) # Création des modulesimpls (4, 2 par semestre) @@ -137,7 +133,7 @@ def test_general(test_client): etud_faux_dict = g_fake.create_etud(code_nip=None, prenom="etudfaux") etud_faux = Identite.query.filter_by(id=etud_faux_dict["id"]).first() - # verif_migration_abs_assiduites() // Test à revoir TODO-ASSIDUITE + # verif_migration_abs_assiduites() // Test à revoir TODO-ASSIDUITE (issue #696) ajouter_assiduites(etuds, moduleimpls, etud_faux) justificatifs: list[Justificatif] = ajouter_justificatifs(etuds[0]) @@ -145,11 +141,14 @@ def test_general(test_client): etuds, moduleimpls, (formsemestre_1, formsemestre_2, formsemestre_3) ) verifier_filtrage_justificatifs(etuds[0], justificatifs) + + essais_cache(etuds[0].etudid, (formsemestre_1, formsemestre_2), moduleimpls) + editer_supprimer_assiduites(etuds, moduleimpls) editer_supprimer_justificatif(etuds[0]) -@pytest.mark.skip # XXX TODO-ASSIDUITE (issue #696) +# XXX TODO-ASSIDUITE (issue #696) def verif_migration_abs_assiduites(): """Vérification que le script de migration fonctionne correctement""" downgrade_module(assiduites=True, justificatifs=True) @@ -476,11 +475,11 @@ def _get_justi( ).first() -def essais_cache(etudid): +def essais_cache(etudid, sems: tuple[FormSemestre], moduleimpls: list[ModuleImpl]): """Vérification des fonctionnalités du cache""" - date_deb: str = "2023-01-01T07:00" - date_fin: str = "2023-03-31T19:00" + 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 @@ -490,8 +489,36 @@ def essais_cache(etudid): ) assert ( - assiduites_count_cache == assiduites_count_no_cache == (34, 15) - ), "Erreur cache" + 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): @@ -543,6 +570,8 @@ def ajouter_justificatifs(etud): db.session.commit() 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) @@ -858,7 +887,7 @@ def ajouter_assiduites( def verifier_comptage_et_filtrage_assiduites( - etuds: list[Identite], moduleimpls: list[int], formsemestres: tuple[int] + etuds: list[Identite], moduleimpls: list[int], formsemestres: tuple[FormSemestre] ): """ Deuxième partie: @@ -931,9 +960,6 @@ def verifier_comptage_et_filtrage_assiduites( ), "Filtrage par 'Moduleimpl' mauvais" # Formsemestre - formsemestres = [ - FormSemestre.query.filter_by(id=fms["id"]).first() for fms in formsemestres - ] assert ( scass.filter_by_formsemestre( etu1.assiduites, Assiduite, formsemestres[0] diff --git a/tests/unit/test_sco_basic.py b/tests/unit/test_sco_basic.py index 030207e5e..be078c921 100644 --- a/tests/unit/test_sco_basic.py +++ b/tests/unit/test_sco_basic.py @@ -23,15 +23,18 @@ import app from app import db from app.comp import res_sem from app.comp.res_compat import NotesTableCompat -from app.models import FormSemestre +from app.models import FormSemestre, Assiduite, Justificatif from app.scodoc import sco_formsemestre from app.scodoc import sco_bulletins from app.scodoc import codes_cursus +from app.scodoc import sco_assiduites as scass from app.scodoc import sco_evaluations from app.scodoc import sco_evaluation_db 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 @@ -186,21 +189,10 @@ def run_sco_basic(verbose=False) -> FormSemestre: # ----------------------- etudid = etuds[0]["etudid"] - # XXX TODO-ASSIDUITE - # _ = sco_abs_views.doSignaleAbsence( - # "15/01/2020", "18/01/2020", demijournee=2, etudid=etudid - # ) - - # _ = sco_abs_views.doJustifAbsence( - # "17/01/2020", - # "18/01/2020", - # demijournee=2, - # etudid=etudid, - # ) - - # nbabs, nbabsjust = sco_abs.get_abs_count(etudid, sem) - # assert nbabs == 6, f"incorrect nbabs ({nbabs})" - # assert nbabsjust == 2, f"incorrect nbabsjust ({nbabsjust})" + _signal_absences_justificatifs(etudid) + nbabs, nbabsjust = scass.get_assiduites_count(etudid, sem) + assert nbabs == 6, f"incorrect nbabs ({nbabs})" + assert nbabsjust == 2, f"incorrect nbabsjust ({nbabsjust})" # --- Permission saisie notes et décisions de jury, avec ou sans démission ou défaillance # on n'a pas encore saisi de décisions @@ -251,3 +243,30 @@ def run_sco_basic(verbose=False) -> FormSemestre: ) assert q.count() == 0 return formsemestre + + +def _signal_absences_justificatifs(etudid: int): + etud: Identite = Identite.query.get(etudid) + db.session.commit() + for i in range(15, 18): + db.session.add( + Assiduite.create_assiduite( + etud=etud, + date_debut=localize_datetime(datetime.datetime(2020, 1, i, 8, 0)), + date_fin=localize_datetime(datetime.datetime(2020, 1, i, 18, 0)), + etat=EtatAssiduite.ABSENT, + ) + ) + db.session.commit() + justif: Justificatif = Justificatif.create_justificatif( + etud=etud, + date_debut=localize_datetime(datetime.datetime(2020, 1, 17, 8, 0)), + date_fin=localize_datetime(datetime.datetime(2020, 1, 17, 18, 0)), + etat=EtatJustificatif.VALIDE, + ) + db.session.add(justif) + compute_assiduites_justified( + etud.etudid, + [justif], + ) + db.session.commit()