From 095eb6ce202a7809e1b44d93b624b0155f6d2dc3 Mon Sep 17 00:00:00 2001 From: iziram Date: Tue, 7 Feb 2023 15:28:55 +0100 Subject: [PATCH 1/5] =?UTF-8?q?module=20assiduites=20:=20rework=20dates=20?= =?UTF-8?q?+=20rev=20(tests=20unit=20=E2=9C=85test=20api=20=E2=9C=85)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit module assiduites : rework dates + rev (tests unit ✅test api ✅) oubli fichier --- app/api/assiduites.py | 13 +- app/api/justificatifs.py | 17 +-- app/scodoc/sco_archives_justificatifs.py | 5 +- app/scodoc/sco_assiduites.py | 119 +++++++++++---- app/scodoc/sco_utils.py | 20 +-- tests/api/test_api_justificatifs.py | 8 +- tests/unit/test_assiduites.py | 181 +++++++++++++---------- 7 files changed, 214 insertions(+), 149 deletions(-) diff --git a/app/api/assiduites.py b/app/api/assiduites.py index 44dae142..fe21e606 100644 --- a/app/api/assiduites.py +++ b/app/api/assiduites.py @@ -526,7 +526,7 @@ def _count_manager(requested) -> tuple[str, dict]: return (metric, filtered) -def _filter_manager(requested, assiduites_query): +def _filter_manager(requested, assiduites_query: Assiduite): """ Retourne les assiduites entrées filtrées en fonction de la request """ @@ -538,19 +538,14 @@ def _filter_manager(requested, assiduites_query): # cas 2 : date de début deb = requested.args.get("date_debut") deb: datetime = scu.is_iso_formated(deb, True) - if deb is not None: - - assiduites_query = scass.filter_assiduites_by_date( - assiduites_query, deb, sup=True - ) # cas 3 : date de fin fin = requested.args.get("date_fin") fin = scu.is_iso_formated(fin, True) - if fin is not None: - assiduites_query = scass.filter_assiduites_by_date( - assiduites_query, fin, sup=False + if (deb, fin) != (None, None): + assiduites_query: Assiduite = scass.filter_by_date( + assiduites_query, Assiduite, deb, fin ) # cas 4 : moduleimpl_id diff --git a/app/api/justificatifs.py b/app/api/justificatifs.py index 9af0c955..3cebc4d8 100644 --- a/app/api/justificatifs.py +++ b/app/api/justificatifs.py @@ -384,7 +384,8 @@ def justif_import(justif_id: int = None): archiver: JustificatifArchiver = JustificatifArchiver() try: - archive_name: str = archiver.save_justificatif( + fname: str + archive_name, fname = archiver.save_justificatif( etudid=justificatif_unique.etudid, filename=file.filename, data=file.stream.read(), @@ -396,7 +397,7 @@ def justif_import(justif_id: int = None): db.session.add(justificatif_unique) db.session.commit() - return jsonify({"response": "imported"}) + return jsonify({"filename": fname}) except ScoValueError as err: return json_error(404, err.args[0]) @@ -527,7 +528,6 @@ def justif_list(justif_id: int = None): # Partie justification -# TODO: justificatif-justified @bp.route("/justificatif/justified/", methods=["GET"]) @api_web_bp.route("/justificatif/justified/", methods=["GET"]) @scodoc @@ -567,19 +567,14 @@ def _filter_manager(requested, justificatifs_query): # cas 2 : date de début deb = requested.args.get("date_debut") deb: datetime = scu.is_iso_formated(deb, True) - if deb is not None: - - justificatifs_query = scass.filter_justificatifs_by_date( - justificatifs_query, deb, sup=True - ) # cas 3 : date de fin fin = requested.args.get("date_fin") fin = scu.is_iso_formated(fin, True) - if fin is not None: - justificatifs_query = scass.filter_justificatifs_by_date( - justificatifs_query, fin, sup=False + if (deb, fin) != (None, None): + justificatifs_query: Justificatif = scass.filter_by_date( + justificatifs_query, Justificatif, deb, fin ) return justificatifs_query diff --git a/app/scodoc/sco_archives_justificatifs.py b/app/scodoc/sco_archives_justificatifs.py index 7202f5e1..2f5b6a5f 100644 --- a/app/scodoc/sco_archives_justificatifs.py +++ b/app/scodoc/sco_archives_justificatifs.py @@ -35,7 +35,6 @@ class JustificatifArchiver(BaseArchiver): """ Ajoute un fichier dans une archive "justificatif" pour l'etudid donné Retourne l'archive_name utilisé - TODO: renvoie archive_name + filename """ self._set_dept(etudid) if archive_name is None: @@ -45,9 +44,9 @@ class JustificatifArchiver(BaseArchiver): else: archive_id: str = self.get_id_from_name(etudid, archive_name) - self.store(archive_id, filename, data) + fname: str = self.store(archive_id, filename, data) - return self.get_archive_name(archive_id) + return self.get_archive_name(archive_id), fname def delete_justificatif(self, etudid: int, archive_name: str, filename: str = None): """ diff --git a/app/scodoc/sco_assiduites.py b/app/scodoc/sco_assiduites.py index 0bdbfbaa..8d31def5 100644 --- a/app/scodoc/sco_assiduites.py +++ b/app/scodoc/sco_assiduites.py @@ -13,22 +13,20 @@ def get_assiduites_stats( ) -> Assiduite: if filtered is not None: + deb, fin = None, None for key in filtered: if key == "etat": assiduites = filter_assiduites_by_etat(assiduites, filtered[key]) elif key == "date_fin": - assiduites = filter_assiduites_by_date( - assiduites, filtered[key], sup=False - ) + fin = filtered[key] elif key == "date_debut": - assiduites = filter_assiduites_by_date( - assiduites, filtered[key], sup=True - ) + deb = filtered[key] elif key == "moduleimpl_id": assiduites = filter_by_module_impl(assiduites, filtered[key]) elif key == "formsemestre": assiduites = filter_by_formsemestre(assiduites, filtered[key]) - + if (deb, fin) != (None, None): + assiduites = filter_by_date(assiduites, Assiduite, deb, fin) count: dict = get_count(assiduites) metrics: list[str] = metric.split(",") @@ -41,8 +39,56 @@ def get_assiduites_stats( return output if output else count -def get_count(assiduites: Assiduite) -> dict[str, int or float]: +def big_counter( + interval: tuple[datetime], + pref_time: time = time(12, 0), +): + curr_date: datetime + if interval[0].time() >= pref_time: + curr_date = scu.localize_datetime( + datetime.combine(interval[0].date(), pref_time) + ) + else: + curr_date = scu.localize_datetime( + datetime.combine(interval[0].date(), time(0, 0)) + ) + + def next_(curr: datetime, journee): + if curr.time() != pref_time: + next_time = scu.localize_datetime(datetime.combine(curr.date(), pref_time)) + else: + next_time = scu.localize_datetime( + datetime.combine(curr.date() + timedelta(days=1), time(0, 0)) + ) + journee += 1 + return next_time, journee + + demi: int = 0 + j: int = 0 + while curr_date <= interval[1]: + next_time: datetime + next_time, j = next_(curr_date, j) + if scu.is_period_overlapping((curr_date, next_time), interval, True): + demi += 1 + curr_date = next_time + + delta: timedelta = interval[1] - interval[0] + heures: float = delta.total_seconds() / 3600 + + if delta.days >= 1: + heures -= delta.days * 16 + + return (demi, j, heures) + + +def get_count( + assiduites: Assiduite, noon: time = time(hour=12) +) -> dict[str, int or float]: + """Fonction permettant de compter les assiduites + -> seul "compte" est correcte lorsque les assiduites viennent de plusieurs étudiants + """ + # TODO: Comptage demi journée / journée d'assiduité longue output: dict[str, int or float] = {} compte: int = assiduites.count() heure: float = 0.0 @@ -55,13 +101,26 @@ def get_count(assiduites: Assiduite) -> dict[str, int or float]: current_time: str = None midnight: time = time(hour=0) - noon: time = time(hour=12) def time_check(dtime): return midnight <= dtime.time() <= noon for ass in all_assiduites: delta: timedelta = ass.date_fin - ass.date_debut + + if delta.days > 0: + + computed_values: tuple[int, int, float] = big_counter( + (ass.date_debut, ass.date_fin), noon + ) + + demi += computed_values[0] - 1 + journee += computed_values[1] - 1 + heure += computed_values[2] + + current_day = ass.date_fin.date() + continue + heure += delta.total_seconds() / 3600 ass_time: str = time_check(ass.date_debut) @@ -89,25 +148,30 @@ def filter_assiduites_by_etat(assiduites: Assiduite, etat: str) -> Assiduite: return assiduites.filter(Assiduite.etat.in_(etats)) -def filter_assiduites_by_date( - assiduites: Assiduite, date_: datetime, sup: bool = True -) -> Assiduite: +def filter_by_date( + collection: Assiduite or Justificatif, + collection_cls: Assiduite or Justificatif, + date_deb: datetime = None, + date_fin: datetime = None, + strict: bool = False, +): """ Filtrage d'une collection d'assiduites en fonction d'une date - - Sup == True -> les assiduites doivent débuter après 'date'\n - Sup == False -> les assiduites doivent finir avant 'date' """ + if date_deb is None: + date_deb = datetime.min + if date_fin is None: + date_fin = datetime.max - if date_.tzinfo is None: - first_assiduite: Assiduite = assiduites.first() - if first_assiduite is not None: - date_: datetime = date_.replace(tzinfo=first_assiduite.date_debut.tzinfo) - - if sup: - return assiduites.filter(Assiduite.date_debut >= date_) - - return assiduites.filter(Assiduite.date_fin <= date_) + date_deb = scu.localize_datetime(date_deb) + date_fin = scu.localize_datetime(date_fin) + if not strict: + return collection.filter( + collection_cls.date_debut <= date_fin, collection_cls.date_fin >= date_deb + ) + return collection.filter( + collection_cls.date_debut < date_fin, collection_cls.date_fin > date_deb + ) def filter_justificatifs_by_etat( @@ -190,11 +254,8 @@ def justifies(justi: Justificatif) -> list[int]: Justificatif, Assiduite.etudid == Justificatif.etudid ).filter(Assiduite.etat != scu.EtatAssiduite.PRESENT) - assiduites_query = filter_assiduites_by_date( - assiduites_query, justi.date_debut, True - ) - assiduites_query = filter_assiduites_by_date( - assiduites_query, justi.date_fin, False + assiduites_query = filter_by_date( + assiduites_query, Assiduite, justi.date_debut, justi.date_fin ) justified = [assi.id for assi in assiduites_query.all()] diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 7d7ec436..4d56ad3c 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -158,7 +158,7 @@ def localize_datetime(date: datetime.datetime or str) -> datetime.datetime: date = is_iso_formated(date, convert=True) new_date: datetime.datetime = date - if date.tzinfo is None: + if date is not None and date.tzinfo is None: from app.models.assiduites import Assiduite first_assiduite = Assiduite.query.first() @@ -174,27 +174,19 @@ def localize_datetime(date: datetime.datetime or str) -> datetime.datetime: def is_period_overlapping( periode: tuple[datetime.datetime, datetime.datetime], interval: tuple[datetime.datetime, datetime.datetime], + strict: bool = True, ) -> bool: """ Vérifie si la période et l'interval s'intersectent - + si strict == True : les extrémitées ne comptes pas Retourne Vrai si c'est le cas, faux sinon """ p_deb, p_fin = periode i_deb, i_fin = interval - # i = intervalmap() - # p = intervalmap() - # i[:] = 0 - # p[:] = 0 - # i[i_deb:i_fin] = 1 - # p[p_deb:p_fin] = 1 - - # # TOTALK: Vérification des bornes de la période dans l'interval et inversement - # res: int = sum((i[p_deb], i[p_fin], p[i_deb], p[i_fin])) - - # return res > 0 - return p_deb <= i_fin and p_fin >= i_deb + if not strict: + return p_deb <= i_fin and p_fin >= i_deb + return p_deb < i_fin and p_fin > i_deb # Types de modules diff --git a/tests/api/test_api_justificatifs.py b/tests/api/test_api_justificatifs.py index 8e5b16e3..48867c38 100644 --- a/tests/api/test_api_justificatifs.py +++ b/tests/api/test_api_justificatifs.py @@ -310,13 +310,13 @@ def test_import_justificatif(api_headers): filename: str = "tests/api/test_api_justificatif.txt" resp: dict = send_file(1, filename, api_headers) - assert "response" in resp - assert resp["response"] == "imported" + assert "filename" in resp + assert resp["filename"] == "test_api_justificatif.txt" filename: str = "tests/api/test_api_justificatif2.txt" resp: dict = send_file(1, filename, api_headers) - assert "response" in resp - assert resp["response"] == "imported" + assert "filename" in resp + assert resp["filename"] == "test_api_justificatif2.txt" # Mauvais fonctionnement diff --git a/tests/unit/test_assiduites.py b/tests/unit/test_assiduites.py index bdbf930e..3c84fa8a 100644 --- a/tests/unit/test_assiduites.py +++ b/tests/unit/test_assiduites.py @@ -258,54 +258,59 @@ def verifier_filtrage_justificatifs(etud: Identite, justificatifs: list[Justific scass.filter_justificatifs_by_etat(etud.justificatifs, "autre").count() == 0 ), "Filtrage de l'état 'autre' mauvais" - # Date début - date = scu.localize_datetime("2022-09-01T10:00+01:00") - assert ( - scass.filter_justificatifs_by_date(etud.justificatifs, date, sup=True).count() - == 5 - ), "Filtrage 'Date début' mauvais 1" - date = scu.localize_datetime("2022-09-03T08:00:00+01:00") - assert ( - scass.filter_justificatifs_by_date(etud.justificatifs, date, sup=True).count() - == 5 - ), "Filtrage 'Date début' mauvais 2" - date = scu.localize_datetime("2022-09-03T09:00:00+01:00") - assert ( - scass.filter_justificatifs_by_date(etud.justificatifs, date, sup=True).count() - == 4 - ), "Filtrage 'Date début' mauvais 3" - date = scu.localize_datetime("2022-09-03T09:00:02+01:00") - assert ( - scass.filter_justificatifs_by_date(etud.justificatifs, date, sup=True).count() - == 4 - ), "Filtrage 'Date début' mauvais 4" + # Dates + + assert ( + scass.filter_by_date(etud.justificatifs, Justificatif).count() == 5 + ), "Filtrage 'Toute Date' mauvais 1" - # Date fin date = scu.localize_datetime("2022-09-01T10:00+01:00") assert ( - scass.filter_justificatifs_by_date(etud.justificatifs, date, sup=False).count() - == 0 - ), "Filtrage 'Date fin' mauvais 1" - date = scu.localize_datetime("2022-09-03T10:00:00+01:00") + scass.filter_by_date(etud.justificatifs, Justificatif, date_deb=date).count() + == 5 + ), "Filtrage 'Toute Date' mauvais 2" + + date = scu.localize_datetime("2022-09-03T08:00+01:00") assert ( - scass.filter_justificatifs_by_date(etud.justificatifs, date, sup=False).count() + scass.filter_by_date(etud.justificatifs, Justificatif, date_deb=date).count() + == 5 + ), "Filtrage 'date début' mauvais 3" + + date = scu.localize_datetime("2022-09-03T08:00:01+01:00") + assert ( + scass.filter_by_date(etud.justificatifs, Justificatif, date_deb=date).count() + == 5 + ), "Filtrage 'date début' mauvais 4" + + date = scu.localize_datetime("2022-09-03T10:00+01:00") + assert ( + scass.filter_by_date(etud.justificatifs, Justificatif, date_deb=date).count() + == 4 + ), "Filtrage 'date début' mauvais 5" + + date = scu.localize_datetime("2022-09-01T10:00+01:00") + assert ( + scass.filter_by_date(etud.justificatifs, Justificatif, date_fin=date).count() + == 0 + ), "Filtrage 'Toute Date' mauvais 6" + + date = scu.localize_datetime("2022-09-03T08:00+01:00") + assert ( + scass.filter_by_date(etud.justificatifs, Justificatif, date_fin=date).count() == 1 - ), "Filtrage 'Date fin' mauvais 2" + ), "Filtrage 'date début' mauvais 7" + date = scu.localize_datetime("2022-09-03T10:00:01+01:00") assert ( - scass.filter_justificatifs_by_date(etud.justificatifs, date, sup=False).count() - == 1 - ), "Filtrage 'Date fin' mauvais 3" - date = scu.localize_datetime("2023-01-04T13:00:01+01:00") + scass.filter_by_date(etud.justificatifs, Justificatif, date_fin=date).count() + == 2 + ), "Filtrage 'date début' mauvais 8" + + date = scu.localize_datetime("2023-01-03T12:00+01:00") assert ( - scass.filter_justificatifs_by_date(etud.justificatifs, date, sup=False).count() + scass.filter_by_date(etud.justificatifs, Justificatif, date_fin=date).count() == 5 - ), "Filtrage 'Date fin' mauvais 4" - date = scu.localize_datetime("2023-01-03T11:00:01+01:00") - assert ( - scass.filter_justificatifs_by_date(etud.justificatifs, date, sup=False).count() - == 4 - ), "Filtrage 'Date fin' mauvais 5" + ), "Filtrage 'date début' mauvais 9" # Justifications des assiduites @@ -371,7 +376,7 @@ def editer_supprimer_assiduites(etuds: list[Identite], moduleimpls: list[int]): # Vérification du changement assert ( - scass.filter_assiduites_by_etat(etuds[0].assiduites, "retard").count() == 3 + scass.filter_assiduites_by_etat(etuds[0].assiduites, "retard").count() == 4 ), "Edition d'assiduité mauvais" assert ( scass.filter_by_module_impl(etuds[1].assiduites, moduleimpls[0].id).count() == 2 @@ -382,7 +387,7 @@ def editer_supprimer_assiduites(etuds: list[Identite], moduleimpls: list[int]): db.session.delete(ass3) db.session.commit() - assert etuds[2].assiduites.count() == 5, "Supression d'assiduité mauvais" + assert etuds[2].assiduites.count() == 6, "Supression d'assiduité mauvais" def ajouter_assiduites( @@ -441,6 +446,13 @@ def ajouter_assiduites( "moduleimpl": moduleimpls[3], "desc": "Description", }, + { + "etat": scu.EtatAssiduite.RETARD, + "deb": "2022-11-04T11:00:01+01:00", + "fin": "2022-12-04T12:00+01:00", + "moduleimpl": None, + "desc": "Description", + }, ] assiduites = [ @@ -514,10 +526,14 @@ def verifier_comptage_et_filtrage_assiduites( # Vérification du comptage classique comptage = scass.get_assiduites_stats(etu1.assiduites) - assert comptage["compte"] == 6, "la métrique 'Comptage' n'est pas bien calculée" - assert comptage["journee"] == 3, "la métrique 'Journée' n'est pas bien calculée" - assert comptage["demi"] == 4, "la métrique 'Demi-Journée' n'est pas bien calculée" - assert comptage["heure"] == 8, "la métrique 'Heure' n'est pas bien calculée" + assert comptage["compte"] == 6 + 1, "la métrique 'Comptage' n'est pas bien calculée" + assert ( + comptage["journee"] == 3 + 30 + ), "la métrique 'Journée' n'est pas bien calculée" + assert ( + comptage["demi"] == 4 + 60 + ), "la métrique 'Demi-Journée' n'est pas bien calculée" + assert comptage["heure"] == 8 + 241, "la métrique 'Heure' n'est pas bien calculée" # Vérification du filtrage classique @@ -526,19 +542,19 @@ def verifier_comptage_et_filtrage_assiduites( scass.filter_assiduites_by_etat(etu2.assiduites, "present").count() == 2 ), "Filtrage de l'état 'présent' mauvais" assert ( - scass.filter_assiduites_by_etat(etu2.assiduites, "retard").count() == 2 + scass.filter_assiduites_by_etat(etu2.assiduites, "retard").count() == 3 ), "Filtrage de l'état 'retard' mauvais" assert ( scass.filter_assiduites_by_etat(etu2.assiduites, "absent").count() == 2 ), "Filtrage de l'état 'absent' mauvais" assert ( - scass.filter_assiduites_by_etat(etu2.assiduites, "absent,retard").count() == 4 + scass.filter_assiduites_by_etat(etu2.assiduites, "absent,retard").count() == 5 ), "Filtrage de l'état 'absent,retard' mauvais" assert ( scass.filter_assiduites_by_etat( etu2.assiduites, "absent,retard,present" ).count() - == 6 + == 7 ), "Filtrage de l'état 'absent,retard,present' mauvais" assert ( scass.filter_assiduites_by_etat(etu2.assiduites, "autre").count() == 0 @@ -558,7 +574,7 @@ def verifier_comptage_et_filtrage_assiduites( scass.filter_by_module_impl(etu3.assiduites, mod22.id).count() == 2 ), "Filtrage par 'Moduleimpl' mauvais" assert ( - scass.filter_by_module_impl(etu3.assiduites, None).count() == 1 + scass.filter_by_module_impl(etu3.assiduites, None).count() == 2 ), "Filtrage par 'Moduleimpl' mauvais" assert ( scass.filter_by_module_impl(etu3.assiduites, 152).count() == 0 @@ -569,7 +585,7 @@ def verifier_comptage_et_filtrage_assiduites( FormSemestre.query.filter_by(id=fms["id"]).first() for fms in formsemestres ] assert ( - scass.filter_by_formsemestre(etu1.assiduites, formsemestres[0]).count() == 3 + scass.filter_by_formsemestre(etu1.assiduites, formsemestres[0]).count() == 4 ), "Filtrage 'Formsemestre' mauvais" assert ( scass.filter_by_formsemestre(etu1.assiduites, formsemestres[1]).count() == 3 @@ -579,41 +595,48 @@ def verifier_comptage_et_filtrage_assiduites( ), "Filtrage 'Formsemestre' mauvais" # Date début - date = scu.localize_datetime("2022-09-01T10:00+01:00") assert ( - scass.filter_assiduites_by_date(etu2.assiduites, date, sup=True).count() == 6 - ), "Filtrage 'Date début' mauvais" - date = scu.localize_datetime("2022-09-03T10:00:00+01:00") - assert ( - scass.filter_assiduites_by_date(etu2.assiduites, date, sup=True).count() == 5 - ), "Filtrage 'Date début' mauvais" - date = scu.localize_datetime("2022-09-03T10:00:01+01:00") - assert ( - scass.filter_assiduites_by_date(etu2.assiduites, date, sup=True).count() == 5 - ), "Filtrage 'Date début' mauvais" - date = scu.localize_datetime("2022-09-03T10:00:02+01:00") - assert ( - scass.filter_assiduites_by_date(etu2.assiduites, date, sup=True).count() == 4 - ), "Filtrage 'Date début' mauvais" + scass.filter_by_date(etu2.assiduites, Assiduite).count() == 7 + ), "Filtrage 'Date début' mauvais 1" - # Date fin date = scu.localize_datetime("2022-09-01T10:00+01:00") assert ( - scass.filter_assiduites_by_date(etu2.assiduites, date, sup=False).count() == 0 - ), "Filtrage 'Date fin' mauvais" - date = scu.localize_datetime("2022-09-03T10:00:00+01:00") + scass.filter_by_date(etu2.assiduites, Assiduite, date_deb=date).count() == 7 + ), "Filtrage 'Date début' mauvais 2" + + date = scu.localize_datetime("2022-09-03T10:00+01:00") assert ( - scass.filter_assiduites_by_date(etu2.assiduites, date, sup=False).count() == 1 - ), "Filtrage 'Date fin' mauvais" + scass.filter_by_date(etu2.assiduites, Assiduite, date_deb=date).count() == 7 + ), "Filtrage 'Date début' mauvais 3" + + date = scu.localize_datetime("2022-09-03T16:00+01:00") + assert ( + scass.filter_by_date(etu2.assiduites, Assiduite, date_deb=date).count() == 4 + ), "Filtrage 'Date début' mauvais 4" + + # Date Fin + + date = scu.localize_datetime("2022-09-01T10:00+01:00") + assert ( + scass.filter_by_date(etu2.assiduites, Assiduite, date_fin=date).count() == 0 + ), "Filtrage 'Date fin' mauvais 1" + + date = scu.localize_datetime("2022-09-03T10:00+01:00") + assert ( + scass.filter_by_date(etu2.assiduites, Assiduite, date_fin=date).count() == 1 + ), "Filtrage 'Date fin' mauvais 2" + date = scu.localize_datetime("2022-09-03T10:00:01+01:00") assert ( - scass.filter_assiduites_by_date(etu2.assiduites, date, sup=False).count() == 1 - ), "Filtrage 'Date fin' mauvais" - date = scu.localize_datetime("2023-01-04T13:00:01+01:00") + scass.filter_by_date(etu2.assiduites, Assiduite, date_fin=date).count() == 2 + ), "Filtrage 'Date fin' mauvais 3" + + date = scu.localize_datetime("2022-09-03T16:00+01:00") assert ( - scass.filter_assiduites_by_date(etu2.assiduites, date, sup=False).count() == 6 - ), "Filtrage 'Date fin' mauvais" - date = scu.localize_datetime("2023-01-03T11:00:01+01:00") + scass.filter_by_date(etu2.assiduites, Assiduite, date_fin=date).count() == 3 + ), "Filtrage 'Date fin' mauvais 4" + + date = scu.localize_datetime("2023-01-04T16:00+01:00") assert ( - scass.filter_assiduites_by_date(etu2.assiduites, date, sup=False).count() == 4 - ), "Filtrage 'Date fin' mauvais" + scass.filter_by_date(etu2.assiduites, Assiduite, date_fin=date).count() == 7 + ), "Filtrage 'Date fin' mauvais 5" From c11599b64f77a48ce22389973cac399125d889f6 Mon Sep 17 00:00:00 2001 From: iziram Date: Tue, 7 Feb 2023 18:49:51 +0100 Subject: [PATCH 2/5] script migration abs -> assiduites (WIP) --- app/models/absences.py | 2 + app/models/assiduites.py | 8 +- app/scodoc/sco_abs.py | 34 +++++- app/scodoc/sco_utils.py | 4 +- scodoc.py | 13 +++ tools/__init__.py | 1 + tools/migrate_abs_to_assiduites.py | 173 +++++++++++++++++++++++++++++ 7 files changed, 230 insertions(+), 5 deletions(-) create mode 100644 tools/migrate_abs_to_assiduites.py diff --git a/app/models/absences.py b/app/models/absences.py index e4bbd823..90c00a35 100644 --- a/app/models/absences.py +++ b/app/models/absences.py @@ -15,8 +15,10 @@ class Absence(db.Model): db.Integer, db.ForeignKey("identite.id", ondelete="CASCADE"), index=True ) jour = db.Column(db.Date) + # absent / justifié / absent+ justifié estabs = db.Column(db.Boolean()) estjust = db.Column(db.Boolean()) + matin = db.Column(db.Boolean()) # motif de l'absence: description = db.Column(db.Text()) diff --git a/app/models/assiduites.py b/app/models/assiduites.py index 181739ba..a52d2990 100644 --- a/app/models/assiduites.py +++ b/app/models/assiduites.py @@ -77,6 +77,7 @@ class Assiduite(db.Model): etat: EtatAssiduite, moduleimpl: ModuleImpl = None, description: str = None, + entry_date: datetime = None, ) -> object or int: """Créer une nouvelle assiduité pour l'étudiant""" # Vérification de non duplication des périodes @@ -97,6 +98,7 @@ class Assiduite(db.Model): etudiant=etud, moduleimpl_id=moduleimpl.id, desc=description, + entry_date=entry_date, ) else: raise ScoValueError("L'étudiant n'est pas inscrit au moduleimpl") @@ -107,6 +109,7 @@ class Assiduite(db.Model): etat=etat, etudiant=etud, desc=description, + entry_date=entry_date, ) return nouv_assiduite @@ -178,6 +181,7 @@ class Justificatif(db.Model): date_fin: datetime, etat: EtatJustificatif, raison: str = None, + entry_date: datetime = None, ) -> object or int: """Créer un nouveau justificatif pour l'étudiant""" # Vérification de non duplication des périodes @@ -193,6 +197,7 @@ class Justificatif(db.Model): etat=etat, etudiant=etud, raison=raison, + entry_date=entry_date, ) return nouv_justificatif @@ -214,8 +219,7 @@ def is_period_conflicting( uni for uni in collection if is_period_overlapping( - (date_debut, date_fin), - (uni.date_debut, uni.date_fin), + (date_debut, date_fin), (uni.date_debut, uni.date_fin), bornes=False ) ] diff --git a/app/scodoc/sco_abs.py b/app/scodoc/sco_abs.py index f065a1f7..5f4a08a6 100644 --- a/app/scodoc/sco_abs.py +++ b/app/scodoc/sco_abs.py @@ -42,6 +42,8 @@ from app.scodoc import sco_cache from app.scodoc import sco_etud from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_preferences +from app.models import Assiduite +import app.scodoc.sco_assiduites as scass import app.scodoc.sco_utils as scu # --- Misc tools.... ------------------ @@ -1026,7 +1028,7 @@ def get_abs_count(etudid, sem): """ return get_abs_count_in_interval(etudid, sem["date_debut_iso"], sem["date_fin_iso"]) -# TODO: relier avec module assiduites + 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, nb abs justifiées) @@ -1052,6 +1054,36 @@ def get_abs_count_in_interval(etudid, date_debut_iso, date_fin_iso): return r +def get_assiduites_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, 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: + + date_debut: datetime.datetime = scu.is_iso_formated(date_debut_iso, True) + date_fin: datetime.datetime = scu.is_iso_formated(date_debut_iso, True) + + assiduites: Assiduite = Assiduite.query.filter_by(etudid=etudid) + + assiduites = scass.filter_assiduites_by_date(assiduites, date_debut, sup=True) + assiduites = scass.filter_assiduites_by_date(assiduites, date_fin, sup=False) + + nb_abs = scass.get_count(assiduites)["demi"] + nb_abs_just = count_abs_just( + etudid=etudid, + debut=date_debut_iso, + fin=date_fin_iso, + ) + r = (nb_abs, nb_abs_just) + ans = sco_cache.AbsSemEtudCache.set(key, r) + if not ans: + log("warning: get_abs_count failed to cache") + return r + + def invalidate_abs_count(etudid, sem): """Invalidate (clear) cached counts""" date_debut = sem["date_debut_iso"] diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 4d56ad3c..eee59ee5 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -174,7 +174,7 @@ def localize_datetime(date: datetime.datetime or str) -> datetime.datetime: def is_period_overlapping( periode: tuple[datetime.datetime, datetime.datetime], interval: tuple[datetime.datetime, datetime.datetime], - strict: bool = True, + bornes: bool = True, ) -> bool: """ Vérifie si la période et l'interval s'intersectent @@ -184,7 +184,7 @@ def is_period_overlapping( p_deb, p_fin = periode i_deb, i_fin = interval - if not strict: + if bornes: return p_deb <= i_fin and p_fin >= i_deb return p_deb < i_fin and p_fin > i_deb diff --git a/scodoc.py b/scodoc.py index 12556c2c..42093ae1 100755 --- a/scodoc.py +++ b/scodoc.py @@ -470,6 +470,19 @@ def migrate_scodoc7_dept_archives(dept: str): # migrate-scodoc7-dept-archives tools.migrate_scodoc7_dept_archives(dept) +@app.cli.command() +@click.argument("dept", default="") +@click.argument("morning", default="") +@click.argument("noon", default="") +@click.argument("evening", default="") +@with_appcontext +def migrate_abs_to_assiduites( + dept: str = "", morning: str = "", noon: str = "", evening: str = "" +): # migrate-scodoc7-dept-archives + """Post-migration: renomme les archives en fonction des id de ScoDoc 9""" + tools.migrate_abs_to_assiduites(dept, morning, noon, evening) + + @app.cli.command() @click.argument("dept", default="") @with_appcontext diff --git a/tools/__init__.py b/tools/__init__.py index ac9e681c..a2bd377a 100644 --- a/tools/__init__.py +++ b/tools/__init__.py @@ -8,3 +8,4 @@ from tools.import_scodoc7_user_db import import_scodoc7_user_db from tools.import_scodoc7_dept import import_scodoc7_dept from tools.migrate_scodoc7_archives import migrate_scodoc7_dept_archives from tools.migrate_scodoc7_logos import migrate_scodoc7_dept_logos +from tools.migrate_abs_to_assiduites import migrate_abs_to_assiduites diff --git a/tools/migrate_abs_to_assiduites.py b/tools/migrate_abs_to_assiduites.py new file mode 100644 index 00000000..45e6a559 --- /dev/null +++ b/tools/migrate_abs_to_assiduites.py @@ -0,0 +1,173 @@ +# Script de migration des données de la base "absences" -> "assiduites"/"justificatifs" + +from app import db + +from app.models import ( + Assiduite, + Justificatif, + Absence, + Identite, + ModuleImpl, + Departement, +) +from app.scodoc.sco_utils import EtatAssiduite, EtatJustificatif, localize_datetime +from datetime import time, datetime, date + + +class glob: + DUPLICATIONS_ASSIDUITES: dict[tuple[date, bool, int], Assiduite] = {} + DUPLICATED: list[Justificatif] = [] + + +def migrate_abs_to_assiduites( + dept: str = "", morning: str = None, noon: str = None, evening: str = None +): + """ + une absence à 3 états: + + |.estabs|.estjust| + |1|0| -> absence non justifiée + |1|1| -> absence justifiée + |0|1| -> justifié + + dualité des temps : + + .matin: bool (0:00 -> time_pref | time_pref->23:59:59) + .jour : date (jour de l'absence/justificatif) + .moduleimpl_id: relation -> moduleimpl_id + description:str -> motif abs / raision justif + + .entry_date: datetime -> timestamp d'entrée de l'abs + .etudid: relation -> Identite + """ + if morning == "": + pref_time_morning = time(8, 0) + else: + morning: list[str] = morning.split("h") + pref_time_morning = time(int(morning[0]), int(morning[1])) + + if noon == "": + pref_time_noon = time(12, 0) + else: + noon: list[str] = noon.split("h") + pref_time_noon = time(int(noon[0]), int(noon[1])) + + if evening == "": + pref_time_evening = time(18, 0) + else: + evening: list[str] = evening.split("h") + pref_time_evening = time(int(evening[0]), int(evening[1])) + + absences_query = Absence.query + if dept != "": + depts_id = [dep.id for dep in Departement.query.filter_by(acronym=dept).all()] + absences_query = absences_query.filter(Absence.etudid.in_(depts_id)) + absences: list[Absence] = absences_query.order_by(Absence.jour).all() + + glob.DUPLICATED = [] + glob.DUPLICATIONS_ASSIDUITES = {} + + for abs in absences: + print(f"\n== {abs.jour}:{abs.etudid}:{abs.matin} ==") + if abs.estabs: + generated = _from_abs_to_assiduite( + abs, pref_time_morning, pref_time_noon, pref_time_evening + ) + if not isinstance(generated, str): + db.session.add(generated) + print( + f"{abs.jour}:absence:{abs.etudid}:{abs.matin} -> {generated.date_debut}:{generated.date_fin}" + ) + if abs.estjust: + generated = _from_abs_to_justificatif( + abs, pref_time_morning, pref_time_noon, pref_time_evening + ) + if not isinstance(generated, str): + db.session.add(generated) + print( + f"{abs.jour}:justif:{abs.etudid}:{abs.matin} -> {generated.date_debut}:{generated.date_fin}" + ) + + dup_assi = glob.DUPLICATED + assi: Assiduite + for assi in dup_assi: + assi.moduleimpl_id = None + db.session.add(assi) + + db.session.commit() + + +def _from_abs_to_assiduite( + _abs: Absence, morning: time, noon: time, evening: time +) -> Assiduite: + etat = EtatAssiduite.ABSENT + date_deb: datetime = None + date_fin: datetime = None + if _abs.matin: + date_deb = datetime.combine(_abs.jour, morning) + date_fin = datetime.combine(_abs.jour, noon) + else: + date_deb = datetime.combine(_abs.jour, noon) + date_fin = datetime.combine(_abs.jour, evening) + + date_deb = localize_datetime(date_deb) + date_fin = localize_datetime(date_fin) + duplicata: Assiduite = glob.DUPLICATIONS_ASSIDUITES.get( + (_abs.jour, _abs.matin, _abs.etudid) + ) + if duplicata is not None: + glob.DUPLICATED.append(duplicata) + return "Duplicated" + + desc: str = _abs.description + entry_date: datetime = _abs.entry_date + + etud: Identite = Identite.query.filter_by(id=_abs.etudid).first() + moduleimpl: ModuleImpl = ModuleImpl.query.filter_by(id=_abs.moduleimpl_id).first() + + retour = Assiduite.create_assiduite( + etud=etud, + date_debut=date_deb, + date_fin=date_fin, + etat=etat, + moduleimpl=moduleimpl, + description=desc, + entry_date=entry_date, + ) + + glob.DUPLICATIONS_ASSIDUITES[(_abs.jour, _abs.matin, _abs.etudid)] = retour + + return retour + + +def _from_abs_to_justificatif( + _abs: Absence, morning: time, noon: time, evening: time +) -> Justificatif: + etat = EtatJustificatif.VALIDE + date_deb: datetime = None + date_fin: datetime = None + if _abs.matin: + date_deb = datetime.combine(_abs.jour, morning) + date_fin = datetime.combine(_abs.jour, noon) + else: + date_deb = datetime.combine(_abs.jour, noon) + date_fin = datetime.combine(_abs.jour, evening) + + date_deb = localize_datetime(date_deb) + date_fin = localize_datetime(date_fin) + + desc: str = _abs.description + entry_date: datetime = _abs.entry_date + + etud: Identite = Identite.query.filter_by(id=_abs.etudid).first() + + retour = Justificatif.create_justificatif( + etud=etud, + date_debut=date_deb, + date_fin=date_fin, + etat=etat, + raison=desc, + entry_date=entry_date, + ) + + return retour From e18990d804b021c3eb784fded7685508006cc2bd Mon Sep 17 00:00:00 2001 From: iziram Date: Wed, 8 Feb 2023 19:48:34 +0100 Subject: [PATCH 3/5] assiduites : Nouveau comptage + script migration (ajout progresse bar + options) --- app/scodoc/sco_assiduites.py | 294 +++++++++++++++++++++-------- app/scodoc/sco_utils.py | 14 +- scodoc.py | 34 +++- tests/unit/test_assiduites.py | 10 +- tools/migrate_abs_to_assiduites.py | 95 ++++++++-- 5 files changed, 337 insertions(+), 110 deletions(-) diff --git a/app/scodoc/sco_assiduites.py b/app/scodoc/sco_assiduites.py index 8d31def5..0617fc76 100644 --- a/app/scodoc/sco_assiduites.py +++ b/app/scodoc/sco_assiduites.py @@ -5,7 +5,142 @@ from app.models.assiduites import Assiduite, Justificatif from app.models.etudiants import Identite from app.models.formsemestre import FormSemestre, FormSemestreInscription -# TOTALK: Réfléchir sur le fractionnement d'une assiduite prolongée + +class CountCalculator: + def __init__( + self, + morning: time = time(8, 0), + noon: time = time(12, 0), + after_noon: time = time(14, 00), + evening: time = time(18, 0), + skip_saturday: bool = True, + ) -> None: + + self.morning: time = morning + self.noon: time = noon + self.after_noon: time = after_noon + self.evening: time = evening + self.skip_saturday: bool = skip_saturday + + delta_total: timedelta = datetime.combine(date.min, evening) - datetime.combine( + date.min, morning + ) + delta_lunch: timedelta = datetime.combine( + date.min, after_noon + ) - datetime.combine(date.min, noon) + + self.hour_per_day: float = (delta_total - delta_lunch).total_seconds() / 3600 + + self.days: list[date] = [] + self.half_days: list[tuple[date, bool]] = [] # tuple -> (date, morning:bool) + self.hours: float = 0.0 + + self.count: int = 0 + + def add_half_day(self, day: date, is_morning: bool = True): + key: tuple[date, bool] = (day, is_morning) + if key not in self.half_days: + self.half_days.append(key) + + def add_day(self, day: date): + if day not in self.days: + self.days.append(day) + + def check_in_morning(self, period: tuple[datetime, datetime]) -> bool: + + interval_morning: tuple[datetime, datetime] = ( + scu.localize_datetime(datetime.combine(period[0].date(), self.morning)), + scu.localize_datetime(datetime.combine(period[0].date(), self.noon)), + ) + + in_morning: bool = scu.is_period_overlapping(period, interval_morning) + return in_morning + + def check_in_evening(self, period: tuple[datetime, datetime]) -> bool: + + interval_evening: tuple[datetime, datetime] = ( + scu.localize_datetime(datetime.combine(period[0].date(), self.after_noon)), + scu.localize_datetime(datetime.combine(period[0].date(), self.evening)), + ) + + in_evening: bool = scu.is_period_overlapping(period, interval_evening) + + return in_evening + + def compute_long_assiduite(self, assi: Assiduite): + + pointer_date: date = assi.date_debut.date() + timedelta(days=1) + start_hours: timedelta = assi.date_debut - scu.localize_datetime( + datetime.combine(assi.date_debut, self.morning) + ) + finish_hours: timedelta = assi.date_fin - scu.localize_datetime( + datetime.combine(assi.date_fin, self.morning) + ) + + self.add_day(assi.date_debut.date()) + self.add_day(assi.date_fin.date()) + + start_period: tuple[datetime, datetime] = ( + assi.date_debut, + scu.localize_datetime( + datetime.combine(assi.date_debut.date(), self.evening) + ), + ) + + finish_period: tuple[datetime, datetime] = ( + scu.localize_datetime(datetime.combine(assi.date_fin.date(), self.morning)), + assi.date_fin, + ) + hours = 0.0 + for period in (start_period, finish_period): + if self.check_in_evening(period): + self.add_half_day(period[0].date(), False) + if self.check_in_morning(period): + self.add_half_day(period[0].date()) + + while pointer_date < assi.date_fin.date(): + if pointer_date.weekday() < (6 - self.skip_saturday): + self.add_day(pointer_date) + self.add_half_day(pointer_date) + self.add_half_day(pointer_date, False) + self.hours += self.hour_per_day + hours += self.hour_per_day + + pointer_date += timedelta(days=1) + + self.hours += finish_hours.total_seconds() / 3600 + self.hours += self.hour_per_day - (start_hours.total_seconds() / 3600) + + def compute_assiduites(self, assiduites: Assiduite): + assi: Assiduite + for assi in assiduites.all(): + self.count += 1 + delta: timedelta = assi.date_fin - assi.date_debut + + if delta.days > 0: + # raise Exception(self.hours) + self.compute_long_assiduite(assi) + + continue + + period: tuple[datetime, datetime] = (assi.date_debut, assi.date_fin) + deb_date: date = assi.date_debut.date() + if self.check_in_morning(period): + self.add_half_day(deb_date) + if self.check_in_evening(period): + self.add_half_day(deb_date, False) + + self.add_day(deb_date) + + self.hours += delta.total_seconds() / 3600 + + def to_dict(self) -> dict[str, object]: + return { + "compte": self.count, + "journee": len(self.days), + "demi": len(self.half_days), + "heure": round(self.hours, 2), + } def get_assiduites_stats( @@ -27,7 +162,10 @@ def get_assiduites_stats( assiduites = filter_by_formsemestre(assiduites, filtered[key]) if (deb, fin) != (None, None): assiduites = filter_by_date(assiduites, Assiduite, deb, fin) - count: dict = get_count(assiduites) + + calculator: CountCalculator = CountCalculator() + calculator.compute_assiduites(assiduites) + count: dict = calculator.to_dict() metrics: list[str] = metric.split(",") @@ -39,104 +177,104 @@ def get_assiduites_stats( return output if output else count -def big_counter( - interval: tuple[datetime], - pref_time: time = time(12, 0), -): - curr_date: datetime +# def big_counter( +# interval: tuple[datetime], +# pref_time: time = time(12, 0), +# ): +# curr_date: datetime - if interval[0].time() >= pref_time: - curr_date = scu.localize_datetime( - datetime.combine(interval[0].date(), pref_time) - ) - else: - curr_date = scu.localize_datetime( - datetime.combine(interval[0].date(), time(0, 0)) - ) +# if interval[0].time() >= pref_time: +# curr_date = scu.localize_datetime( +# datetime.combine(interval[0].date(), pref_time) +# ) +# else: +# curr_date = scu.localize_datetime( +# datetime.combine(interval[0].date(), time(0, 0)) +# ) - def next_(curr: datetime, journee): - if curr.time() != pref_time: - next_time = scu.localize_datetime(datetime.combine(curr.date(), pref_time)) - else: - next_time = scu.localize_datetime( - datetime.combine(curr.date() + timedelta(days=1), time(0, 0)) - ) - journee += 1 - return next_time, journee +# def next_(curr: datetime, journee): +# if curr.time() != pref_time: +# next_time = scu.localize_datetime(datetime.combine(curr.date(), pref_time)) +# else: +# next_time = scu.localize_datetime( +# datetime.combine(curr.date() + timedelta(days=1), time(0, 0)) +# ) +# journee += 1 +# return next_time, journee - demi: int = 0 - j: int = 0 - while curr_date <= interval[1]: - next_time: datetime - next_time, j = next_(curr_date, j) - if scu.is_period_overlapping((curr_date, next_time), interval, True): - demi += 1 - curr_date = next_time +# demi: int = 0 +# j: int = 0 +# while curr_date <= interval[1]: +# next_time: datetime +# next_time, j = next_(curr_date, j) +# if scu.is_period_overlapping((curr_date, next_time), interval, True): +# demi += 1 +# curr_date = next_time - delta: timedelta = interval[1] - interval[0] - heures: float = delta.total_seconds() / 3600 +# delta: timedelta = interval[1] - interval[0] +# heures: float = delta.total_seconds() / 3600 - if delta.days >= 1: - heures -= delta.days * 16 +# if delta.days >= 1: +# heures -= delta.days * 16 - return (demi, j, heures) +# return (demi, j, heures) -def get_count( - assiduites: Assiduite, noon: time = time(hour=12) -) -> dict[str, int or float]: - """Fonction permettant de compter les assiduites - -> seul "compte" est correcte lorsque les assiduites viennent de plusieurs étudiants - """ - # TODO: Comptage demi journée / journée d'assiduité longue - output: dict[str, int or float] = {} - compte: int = assiduites.count() - heure: float = 0.0 - journee: int = 0 - demi: int = 0 +# def get_count( +# assiduites: Assiduite, noon: time = time(hour=12) +# ) -> dict[str, int or float]: +# """Fonction permettant de compter les assiduites +# -> seul "compte" est correcte lorsque les assiduites viennent de plusieurs étudiants +# """ +# # TODO: Comptage demi journée / journée d'assiduité longue +# output: dict[str, int or float] = {} +# compte: int = assiduites.count() +# heure: float = 0.0 +# journee: int = 0 +# demi: int = 0 - all_assiduites: list[Assiduite] = assiduites.order_by(Assiduite.date_debut).all() +# all_assiduites: list[Assiduite] = assiduites.order_by(Assiduite.date_debut).all() - current_day: date = None - current_time: str = None +# current_day: date = None +# current_time: str = None - midnight: time = time(hour=0) +# midnight: time = time(hour=0) - def time_check(dtime): - return midnight <= dtime.time() <= noon +# def time_check(dtime): +# return midnight <= dtime.time() <= noon - for ass in all_assiduites: - delta: timedelta = ass.date_fin - ass.date_debut +# for ass in all_assiduites: +# delta: timedelta = ass.date_fin - ass.date_debut - if delta.days > 0: +# if delta.days > 0: - computed_values: tuple[int, int, float] = big_counter( - (ass.date_debut, ass.date_fin), noon - ) +# computed_values: tuple[int, int, float] = big_counter( +# (ass.date_debut, ass.date_fin), noon +# ) - demi += computed_values[0] - 1 - journee += computed_values[1] - 1 - heure += computed_values[2] +# demi += computed_values[0] - 1 +# journee += computed_values[1] - 1 +# heure += computed_values[2] - current_day = ass.date_fin.date() - continue +# current_day = ass.date_fin.date() +# continue - heure += delta.total_seconds() / 3600 +# heure += delta.total_seconds() / 3600 - ass_time: str = time_check(ass.date_debut) +# ass_time: str = time_check(ass.date_debut) - if current_day != ass.date_debut.date(): - current_day = ass.date_debut.date() - current_time = ass_time - demi += 1 - journee += 1 +# if current_day != ass.date_debut.date(): +# current_day = ass.date_debut.date() +# current_time = ass_time +# demi += 1 +# journee += 1 - if current_time != ass_time: - current_time = ass_time - demi += 1 +# if current_time != ass_time: +# current_time = ass_time +# demi += 1 - heure = round(heure, 2) - return {"compte": compte, "journee": journee, "heure": heure, "demi": demi} +# heure = round(heure, 2) +# return {"compte": compte, "journee": journee, "heure": heure, "demi": demi} def filter_assiduites_by_etat(assiduites: Assiduite, etat: str) -> Assiduite: diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index eee59ee5..9f78fd2f 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -159,15 +159,11 @@ def localize_datetime(date: datetime.datetime or str) -> datetime.datetime: new_date: datetime.datetime = date if date is not None and date.tzinfo is None: - from app.models.assiduites import Assiduite - - first_assiduite = Assiduite.query.first() - if first_assiduite is not None: - new_date = date.replace(tzinfo=first_assiduite.date_debut.tzinfo) - else: - # TOTALK: Paramètre permettant d'avoir l'UTC par défaut - tmp = is_iso_formated("2022-01-01T08:00:00+01:00", True) - new_date = date.replace(tzinfo=tmp.tzinfo) + # TOTALK: Paramètre scodoc pour avoir la timezone du serveur/ timezone paramétrée + time_zone: datetime.timezone = datetime.timezone( + datetime.timedelta(seconds=3600), "default" + ) + new_date = date.replace(tzinfo=time_zone) return new_date diff --git a/scodoc.py b/scodoc.py index 42093ae1..f3736772 100755 --- a/scodoc.py +++ b/scodoc.py @@ -471,15 +471,35 @@ def migrate_scodoc7_dept_archives(dept: str): # migrate-scodoc7-dept-archives @app.cli.command() -@click.argument("dept", default="") -@click.argument("morning", default="") -@click.argument("noon", default="") -@click.argument("evening", default="") +@click.option( + "-d", "--dept", help="Restreint la migration au dept sélectionné (ACRONYME)" +) +@click.option( + "-m", + "--morning", + help="Spécifie l'heure de début des cours format `hh:mm`", + default="08h00", + show_default=True, +) +@click.option( + "-n", + "--noon", + help="Spécifie l'heure de fin du matin (et donc début de l'après-midi) format `hh:mm`", + default="12h00", + show_default=True, +) +@click.option( + "-e", + "--evening", + help="Spécifie l'heure de fin des cours format `hh:mm`", + default="18h00", + show_default=True, +) @with_appcontext def migrate_abs_to_assiduites( - dept: str = "", morning: str = "", noon: str = "", evening: str = "" -): # migrate-scodoc7-dept-archives - """Post-migration: renomme les archives en fonction des id de ScoDoc 9""" + dept: str = None, morning: str = None, noon: str = None, evening: str = None +): # migrate-abs-to-assiduites + """Permet de migrer les absences vers le nouveau module d'assiduités""" tools.migrate_abs_to_assiduites(dept, morning, noon, evening) diff --git a/tests/unit/test_assiduites.py b/tests/unit/test_assiduites.py index 3c84fa8a..604be477 100644 --- a/tests/unit/test_assiduites.py +++ b/tests/unit/test_assiduites.py @@ -449,7 +449,7 @@ def ajouter_assiduites( { "etat": scu.EtatAssiduite.RETARD, "deb": "2022-11-04T11:00:01+01:00", - "fin": "2022-12-04T12:00+01:00", + "fin": "2022-12-05T12:00+01:00", "moduleimpl": None, "desc": "Description", }, @@ -528,12 +528,14 @@ def verifier_comptage_et_filtrage_assiduites( assert comptage["compte"] == 6 + 1, "la métrique 'Comptage' n'est pas bien calculée" assert ( - comptage["journee"] == 3 + 30 + comptage["journee"] == 3 + 22 ), "la métrique 'Journée' n'est pas bien calculée" assert ( - comptage["demi"] == 4 + 60 + comptage["demi"] == 4 + 43 ), "la métrique 'Demi-Journée' n'est pas bien calculée" - assert comptage["heure"] == 8 + 241, "la métrique 'Heure' n'est pas bien calculée" + assert comptage["heure"] == float( + 8 + 169 + ), "la métrique 'Heure' n'est pas bien calculée" # Vérification du filtrage classique diff --git a/tools/migrate_abs_to_assiduites.py b/tools/migrate_abs_to_assiduites.py index 45e6a559..a8cb967c 100644 --- a/tools/migrate_abs_to_assiduites.py +++ b/tools/migrate_abs_to_assiduites.py @@ -1,4 +1,5 @@ # Script de migration des données de la base "absences" -> "assiduites"/"justificatifs" +import shutil from app import db @@ -19,6 +20,60 @@ class glob: DUPLICATED: list[Justificatif] = [] +class bcolors: + BLUE = "\033[94m" + CYAN = "\033[96m" + GREEN = "\033[92m" + MAGENTA = "\033[95m" + RED = "\033[91m" + RESET = "\033[0m" + + +def printProgressBar( + iteration, + total, + prefix="", + suffix="", + finish_msg="", + decimals=1, + length=100, + fill="█", + autosize=False, +): + """ + Affiche une progress bar à un point donné (mettre dans une boucle pour rendre dynamique) + @params: + iteration - Required : index du point donné (Int) + total - Required : nombre total avant complétion (eg: len(List)) + prefix - Optional : Préfix -> écrit à gauche de la barre (Str) + suffix - Optional : Suffix -> écrit à droite de la barre (Str) + decimals - Optional : nombres de chiffres après la virgule (Int) + length - Optional : taille de la barre en nombre de caractères (Int) + fill - Optional : charactère de remplissange de la barre (Str) + autosize - Optional : Choisir automatiquement la taille de la barre en fonction du terminal (Bool) + """ + percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total))) + color = bcolors.RED + if 50 > float(percent) > 25: + color = bcolors.MAGENTA + if 75 > float(percent) > 50: + color = bcolors.BLUE + if 90 > float(percent) > 75: + color = bcolors.CYAN + if 100 >= float(percent) > 90: + color = bcolors.GREEN + styling = f"{prefix} |{fill}| {percent}% {suffix}" + if autosize: + cols, _ = shutil.get_terminal_size(fallback=(length, 1)) + length = cols - len(styling) + filledLength = int(length * iteration // total) + bar = fill * filledLength + "-" * (length - filledLength) + print(f"\r{color}{styling.replace(fill, bar)}{bcolors.RESET}", end="\r") + # Affiche une nouvelle ligne vide + if iteration == total: + print(f"\n{finish_msg}") + + def migrate_abs_to_assiduites( dept: str = "", morning: str = None, noon: str = None, evening: str = None ): @@ -40,26 +95,26 @@ def migrate_abs_to_assiduites( .entry_date: datetime -> timestamp d'entrée de l'abs .etudid: relation -> Identite """ - if morning == "": + if morning is None: pref_time_morning = time(8, 0) else: morning: list[str] = morning.split("h") pref_time_morning = time(int(morning[0]), int(morning[1])) - if noon == "": + if noon is None: pref_time_noon = time(12, 0) else: noon: list[str] = noon.split("h") pref_time_noon = time(int(noon[0]), int(noon[1])) - if evening == "": + if evening is None: pref_time_evening = time(18, 0) else: evening: list[str] = evening.split("h") pref_time_evening = time(int(evening[0]), int(evening[1])) absences_query = Absence.query - if dept != "": + if dept is not None: depts_id = [dep.id for dep in Departement.query.filter_by(acronym=dept).all()] absences_query = absences_query.filter(Absence.etudid.in_(depts_id)) absences: list[Absence] = absences_query.order_by(Absence.jour).all() @@ -67,26 +122,33 @@ def migrate_abs_to_assiduites( glob.DUPLICATED = [] glob.DUPLICATIONS_ASSIDUITES = {} - for abs in absences: - print(f"\n== {abs.jour}:{abs.etudid}:{abs.matin} ==") + absences_len: int = len(absences) + + printProgressBar(0, absences_len, "Progression", "effectué", autosize=True) + + for i, abs in enumerate(absences): + if abs.estabs: generated = _from_abs_to_assiduite( abs, pref_time_morning, pref_time_noon, pref_time_evening ) if not isinstance(generated, str): db.session.add(generated) - print( - f"{abs.jour}:absence:{abs.etudid}:{abs.matin} -> {generated.date_debut}:{generated.date_fin}" - ) + if abs.estjust: generated = _from_abs_to_justificatif( abs, pref_time_morning, pref_time_noon, pref_time_evening ) if not isinstance(generated, str): db.session.add(generated) - print( - f"{abs.jour}:justif:{abs.etudid}:{abs.matin} -> {generated.date_debut}:{generated.date_fin}" - ) + + printProgressBar( + i, + absences_len, + "Progression", + "effectué", + autosize=True, + ) dup_assi = glob.DUPLICATED assi: Assiduite @@ -96,6 +158,15 @@ def migrate_abs_to_assiduites( db.session.commit() + printProgressBar( + absences_len, + absences_len, + "Progression", + "effectué", + autosize=True, + finish_msg=f"{bcolors.GREEN}Les absences ont bien été migrées.{bcolors.RESET}", + ) + def _from_abs_to_assiduite( _abs: Absence, morning: time, noon: time, evening: time From 53c9658ce1478d42542db83a00619e20ff46ac31 Mon Sep 17 00:00:00 2001 From: iziram Date: Thu, 9 Feb 2023 21:04:53 +0100 Subject: [PATCH 4/5] optimisation migration abs to assiduites (WIP) --- app/models/assiduites.py | 79 ++++++++-- app/profiler.py | 43 ++++++ app/scodoc/sco_utils.py | 55 +++++++ tools/migrate_abs_to_assiduites.py | 240 +++++++++++++++++------------ 4 files changed, 305 insertions(+), 112 deletions(-) create mode 100644 app/profiler.py diff --git a/app/models/assiduites.py b/app/models/assiduites.py index a52d2990..81173d4c 100644 --- a/app/models/assiduites.py +++ b/app/models/assiduites.py @@ -81,13 +81,11 @@ class Assiduite(db.Model): ) -> object or int: """Créer une nouvelle assiduité pour l'étudiant""" # Vérification de non duplication des périodes - assiduites: list[Assiduite] = etud.assiduites.all() - - if is_period_conflicting(date_debut, date_fin, assiduites): + assiduites: list[Assiduite] = etud.assiduites + if is_period_conflicting(date_debut, date_fin, assiduites, Assiduite): raise ScoValueError( "Duplication des assiduités (la période rentrée rentre en conflit avec une assiduité enregistrée)" ) - if moduleimpl is not None: # Vérification de l'existence du module pour l'étudiant if moduleimpl.est_inscrit(etud): @@ -114,6 +112,32 @@ class Assiduite(db.Model): return nouv_assiduite + @classmethod + def fast_create_assiduite( + cls, + etudid: int, + date_debut: datetime, + date_fin: datetime, + etat: EtatAssiduite, + moduleimpl_id: int = None, + description: str = None, + entry_date: datetime = None, + ) -> object or int: + """Créer une nouvelle assiduité pour l'étudiant""" + # Vérification de non duplication des périodes + + nouv_assiduite = Assiduite( + date_debut=date_debut, + date_fin=date_fin, + etat=etat, + etudid=etudid, + moduleimpl_id=moduleimpl_id, + desc=description, + entry_date=entry_date, + ) + + return nouv_assiduite + class Justificatif(db.Model): """ @@ -185,8 +209,8 @@ class Justificatif(db.Model): ) -> object or int: """Créer un nouveau justificatif pour l'étudiant""" # Vérification de non duplication des périodes - justificatifs: list[Justificatif] = etud.justificatifs.all() - if is_period_conflicting(date_debut, date_fin, justificatifs): + justificatifs: list[Justificatif] = etud.justificatifs + if is_period_conflicting(date_debut, date_fin, justificatifs, Justificatif): raise ScoValueError( "Duplication des justificatifs (la période rentrée rentre en conflit avec un justificatif enregistré)" ) @@ -202,11 +226,35 @@ class Justificatif(db.Model): return nouv_justificatif + @classmethod + def fast_create_justificatif( + cls, + etudid: int, + date_debut: datetime, + date_fin: datetime, + etat: EtatJustificatif, + raison: str = None, + entry_date: datetime = None, + ) -> object or int: + """Créer un nouveau justificatif pour l'étudiant""" + + nouv_justificatif = Justificatif( + date_debut=date_debut, + date_fin=date_fin, + etat=etat, + etudid=etudid, + raison=raison, + entry_date=entry_date, + ) + + return nouv_justificatif + def is_period_conflicting( date_debut: datetime, date_fin: datetime, collection: list[Assiduite or Justificatif], + collection_cls: Assiduite or Justificatif, ) -> bool: """ Vérifie si une date n'entre pas en collision @@ -215,12 +263,15 @@ def is_period_conflicting( date_debut = localize_datetime(date_debut) date_fin = localize_datetime(date_fin) - unified = [ - uni - for uni in collection - if is_period_overlapping( - (date_debut, date_fin), (uni.date_debut, uni.date_fin), bornes=False - ) - ] - return len(unified) != 0 + if ( + collection.filter_by(date_debut=date_debut, date_fin=date_fin).first() + is not None + ): + return True + + count: int = collection.filter( + collection_cls.date_debut < date_fin, collection_cls.date_fin > date_debut + ).count() + + return count > 0 diff --git a/app/profiler.py b/app/profiler.py new file mode 100644 index 00000000..0e61d385 --- /dev/null +++ b/app/profiler.py @@ -0,0 +1,43 @@ +from time import time +from datetime import datetime + + +class Profiler: + OUTPUT: str = "/tmp/scodoc.profiler.csv" + + def __init__(self, tag: str) -> None: + self.tag: str = tag + self.start_time: time = None + self.stop_time: time = None + + def start(self): + self.start_time = time() + return self + + def stop(self): + self.stop_time = time() + return self + + def elapsed(self) -> float: + return self.stop_time - self.start_time + + def dates(self) -> tuple[datetime, datetime]: + return datetime.fromtimestamp(self.start_time), datetime.fromtimestamp( + self.stop_time + ) + + def write(self): + with open(Profiler.OUTPUT, "a") as file: + dates: tuple = self.dates() + date_str = (dates[0].isoformat(), dates[1].isoformat()) + file.write(f"\n{self.tag},{self.elapsed() : .2}") + + @classmethod + def write_in(cls, msg: str): + with open(cls.OUTPUT, "a") as file: + file.write(f"\n# {msg}") + + @classmethod + def clear(cls): + with open(cls.OUTPUT, "w") as file: + file.write("") diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 9f78fd2f..55be9425 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -39,6 +39,7 @@ from hashlib import md5 import numbers import os import re +from shutil import get_terminal_size import _thread import time import unicodedata @@ -88,6 +89,60 @@ ETATS_INSCRIPTION = { } +def printProgressBar( + iteration, + total, + prefix="", + suffix="", + finish_msg="", + decimals=1, + length=100, + fill="█", + autosize=False, +): + """ + Affiche une progress bar à un point donné (mettre dans une boucle pour rendre dynamique) + @params: + iteration - Required : index du point donné (Int) + total - Required : nombre total avant complétion (eg: len(List)) + prefix - Optional : Préfix -> écrit à gauche de la barre (Str) + suffix - Optional : Suffix -> écrit à droite de la barre (Str) + decimals - Optional : nombres de chiffres après la virgule (Int) + length - Optional : taille de la barre en nombre de caractères (Int) + fill - Optional : charactère de remplissange de la barre (Str) + autosize - Optional : Choisir automatiquement la taille de la barre en fonction du terminal (Bool) + """ + percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total))) + color = ProgressBarColors.RED + if 50 >= float(percent) > 25: + color = ProgressBarColors.MAGENTA + if 75 >= float(percent) > 50: + color = ProgressBarColors.BLUE + if 90 >= float(percent) > 75: + color = ProgressBarColors.CYAN + if 100 >= float(percent) > 90: + color = ProgressBarColors.GREEN + styling = f"{prefix} |{fill}| {percent}% {suffix}" + if autosize: + cols, _ = get_terminal_size(fallback=(length, 1)) + length = cols - len(styling) + filledLength = int(length * iteration // total) + bar = fill * filledLength + "-" * (length - filledLength) + print(f"\r{color}{styling.replace(fill, bar)}{ProgressBarColors.RESET}", end="\r") + # Affiche une nouvelle ligne vide + if iteration == total: + print(f"\n{finish_msg}") + + +class ProgressBarColors: + BLUE = "\033[94m" + CYAN = "\033[96m" + GREEN = "\033[92m" + MAGENTA = "\033[95m" + RED = "\033[91m" + RESET = "\033[0m" + + class BiDirectionalEnum(Enum): """Permet la recherche inverse d'un enum Condition : les clés et les valeurs doivent être uniques diff --git a/tools/migrate_abs_to_assiduites.py b/tools/migrate_abs_to_assiduites.py index a8cb967c..e89db945 100644 --- a/tools/migrate_abs_to_assiduites.py +++ b/tools/migrate_abs_to_assiduites.py @@ -1,77 +1,33 @@ # Script de migration des données de la base "absences" -> "assiduites"/"justificatifs" -import shutil - from app import db - +from app.profiler import Profiler from app.models import ( Assiduite, Justificatif, Absence, Identite, - ModuleImpl, + ModuleImplInscription, Departement, ) -from app.scodoc.sco_utils import EtatAssiduite, EtatJustificatif, localize_datetime +from app.scodoc.sco_utils import ( + EtatAssiduite, + EtatJustificatif, + localize_datetime, + ProgressBarColors, + printProgressBar, +) from datetime import time, datetime, date +from json import dump -class glob: +class _glob: DUPLICATIONS_ASSIDUITES: dict[tuple[date, bool, int], Assiduite] = {} + DUPLICATIONS_JUSTIFICATIFS: dict[tuple[date, bool, int], Justificatif] = {} DUPLICATED: list[Justificatif] = [] - - -class bcolors: - BLUE = "\033[94m" - CYAN = "\033[96m" - GREEN = "\033[92m" - MAGENTA = "\033[95m" - RED = "\033[91m" - RESET = "\033[0m" - - -def printProgressBar( - iteration, - total, - prefix="", - suffix="", - finish_msg="", - decimals=1, - length=100, - fill="█", - autosize=False, -): - """ - Affiche une progress bar à un point donné (mettre dans une boucle pour rendre dynamique) - @params: - iteration - Required : index du point donné (Int) - total - Required : nombre total avant complétion (eg: len(List)) - prefix - Optional : Préfix -> écrit à gauche de la barre (Str) - suffix - Optional : Suffix -> écrit à droite de la barre (Str) - decimals - Optional : nombres de chiffres après la virgule (Int) - length - Optional : taille de la barre en nombre de caractères (Int) - fill - Optional : charactère de remplissange de la barre (Str) - autosize - Optional : Choisir automatiquement la taille de la barre en fonction du terminal (Bool) - """ - percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total))) - color = bcolors.RED - if 50 > float(percent) > 25: - color = bcolors.MAGENTA - if 75 > float(percent) > 50: - color = bcolors.BLUE - if 90 > float(percent) > 75: - color = bcolors.CYAN - if 100 >= float(percent) > 90: - color = bcolors.GREEN - styling = f"{prefix} |{fill}| {percent}% {suffix}" - if autosize: - cols, _ = shutil.get_terminal_size(fallback=(length, 1)) - length = cols - len(styling) - filledLength = int(length * iteration // total) - bar = fill * filledLength + "-" * (length - filledLength) - print(f"\r{color}{styling.replace(fill, bar)}{bcolors.RESET}", end="\r") - # Affiche une nouvelle ligne vide - if iteration == total: - print(f"\n{finish_msg}") + PROBLEMS: dict[int, list[str]] = {} + CURRENT_ETU: list = [] + MODULES: list[tuple[int, int]] = [] + COMPTE: list[int, int] = [] def migrate_abs_to_assiduites( @@ -95,6 +51,11 @@ def migrate_abs_to_assiduites( .entry_date: datetime -> timestamp d'entrée de l'abs .etudid: relation -> Identite """ + Profiler.clear() + + time_elapsed: Profiler = Profiler("migration") + time_elapsed.start() + if morning is None: pref_time_morning = time(8, 0) else: @@ -115,42 +76,77 @@ def migrate_abs_to_assiduites( absences_query = Absence.query if dept is not None: - depts_id = [dep.id for dep in Departement.query.filter_by(acronym=dept).all()] - absences_query = absences_query.filter(Absence.etudid.in_(depts_id)) - absences: list[Absence] = absences_query.order_by(Absence.jour).all() - glob.DUPLICATED = [] - glob.DUPLICATIONS_ASSIDUITES = {} + dept: Departement = Departement.query.filter_by(acronym=dept).first() + if dept is not None: + etuds_id: list[int] = [etud.id for etud in dept.etudiants] + absences_query = absences_query.filter(Absence.etudid.in_(etuds_id)) + absences: Absence = absences_query.order_by(Absence.etudid) - absences_len: int = len(absences) + _glob.DUPLICATED = [] + _glob.DUPLICATIONS_ASSIDUITES = {} + _glob.DUPLICATIONS_JUSTIFICATIFS = {} + _glob.PROBLEMS = {} + _glob.CURRENT_ETU = [] + _glob.MODULES = [] + _glob.COMPTE = [0, 0] + + absences_len: int = absences.count() + + print( + f"{ProgressBarColors.BLUE}{absences_len} absences vont être migrées{ProgressBarColors.RESET}" + ) printProgressBar(0, absences_len, "Progression", "effectué", autosize=True) for i, abs in enumerate(absences): - - if abs.estabs: - generated = _from_abs_to_assiduite( - abs, pref_time_morning, pref_time_noon, pref_time_evening - ) + try: + if abs.estabs: + generated = _from_abs_to_assiduite( + abs, pref_time_morning, pref_time_noon, pref_time_evening + ) if not isinstance(generated, str): db.session.add(generated) + _glob.COMPTE[0] += 1 + except Exception as e: + if abs.id not in _glob.PROBLEMS: + _glob.PROBLEMS[abs.id] = [] + _glob.PROBLEMS[abs.id].append(e.args[0]) - if abs.estjust: - generated = _from_abs_to_justificatif( - abs, pref_time_morning, pref_time_noon, pref_time_evening + try: + if abs.estjust: + generated = _from_abs_to_justificatif( + abs, pref_time_morning, pref_time_noon, pref_time_evening + ) + if not isinstance(generated, str): + db.session.add(generated) + _glob.COMPTE[1] += 1 + + except Exception as e: + if abs.id not in _glob.PROBLEMS: + _glob.PROBLEMS[abs.id] = [] + _glob.PROBLEMS[abs.id].append(e.args[0]) + + if i % 10 == 0: + printProgressBar( + i, + absences_len, + "Progression", + "effectué", + autosize=True, ) - if not isinstance(generated, str): - db.session.add(generated) - printProgressBar( - i, - absences_len, - "Progression", - "effectué", - autosize=True, - ) + if i % 1000 == 0: + printProgressBar( + i, + absences_len, + "Progression", + "effectué", + autosize=True, + ) + db.session.commit() - dup_assi = glob.DUPLICATED + dup_assi = _glob.DUPLICATED assi: Assiduite for assi in dup_assi: assi.moduleimpl_id = None @@ -164,9 +160,28 @@ def migrate_abs_to_assiduites( "Progression", "effectué", autosize=True, - finish_msg=f"{bcolors.GREEN}Les absences ont bien été migrées.{bcolors.RESET}", + finish_msg=f"{ProgressBarColors.GREEN}Les absences ont bien été migrées.{ProgressBarColors.RESET}", ) + time_elapsed.stop() + print( + f"{ProgressBarColors.GREEN}La migration a pris {time_elapsed.elapsed():.2f} secondes {ProgressBarColors.RESET}" + ) + + print( + f"{ProgressBarColors.RED}Il y a eu {len(_glob.PROBLEMS)} absences qui n'ont pas pu être migrée." + ) + print( + f"Vous retrouverez un fichier json {ProgressBarColors.GREEN}/tmp/scodoc_migration_abs.json{ProgressBarColors.RED} contenant les ids des absences ainsi que les erreurs liées." + ) + with open("/tmp/scodoc_migration_abs.json", "w", encoding="utf-8") as file: + dump(_glob.PROBLEMS, file) + + print( + f"{ProgressBarColors.CYAN}{_glob.COMPTE[0]} assiduités et {_glob.COMPTE[1]} justificatifs ont été générés.{ProgressBarColors.RESET}" + ) + # afficher nombre justificatifs généré par rapport au nombre de justificatifs + def _from_abs_to_assiduite( _abs: Absence, morning: time, noon: time, evening: time @@ -174,6 +189,7 @@ def _from_abs_to_assiduite( etat = EtatAssiduite.ABSENT date_deb: datetime = None date_fin: datetime = None + if _abs.matin: date_deb = datetime.combine(_abs.jour, morning) date_fin = datetime.combine(_abs.jour, noon) @@ -183,37 +199,54 @@ def _from_abs_to_assiduite( date_deb = localize_datetime(date_deb) date_fin = localize_datetime(date_fin) - duplicata: Assiduite = glob.DUPLICATIONS_ASSIDUITES.get( + + duplicata: Assiduite = _glob.DUPLICATIONS_ASSIDUITES.get( (_abs.jour, _abs.matin, _abs.etudid) ) if duplicata is not None: - glob.DUPLICATED.append(duplicata) + _glob.DUPLICATED.append(duplicata) return "Duplicated" desc: str = _abs.description entry_date: datetime = _abs.entry_date - etud: Identite = Identite.query.filter_by(id=_abs.etudid).first() - moduleimpl: ModuleImpl = ModuleImpl.query.filter_by(id=_abs.moduleimpl_id).first() + if _abs.etudid not in _glob.CURRENT_ETU: + etud: Identite = Identite.query.filter_by(id=_abs.etudid).first() + if etud is None: + return "No Etud" + _glob.CURRENT_ETU.append(_abs.etudid) - retour = Assiduite.create_assiduite( - etud=etud, + moduleimpl_id: int = _abs.moduleimpl_id + + if ( + moduleimpl_id is not None + and (_abs.etudid, _abs.moduleimpl_id) not in _glob.MODULES + ): + moduleimpl_inscription: ModuleImplInscription = ( + ModuleImplInscription.query.filter_by( + moduleimpl_id=_abs.moduleimpl_id, etudid=_abs.etudid + ).first() + ) + if moduleimpl_inscription is None: + raise Exception("Moduleimpl_id incorrect ou étudiant non inscrit") + + retour = Assiduite.fast_create_assiduite( + etudid=_abs.etudid, date_debut=date_deb, date_fin=date_fin, etat=etat, - moduleimpl=moduleimpl, + moduleimpl_id=moduleimpl_id, description=desc, entry_date=entry_date, ) - - glob.DUPLICATIONS_ASSIDUITES[(_abs.jour, _abs.matin, _abs.etudid)] = retour - + _glob.DUPLICATIONS_ASSIDUITES[(_abs.jour, _abs.matin, _abs.etudid)] = retour return retour def _from_abs_to_justificatif( _abs: Absence, morning: time, noon: time, evening: time ) -> Justificatif: + etat = EtatJustificatif.VALIDE date_deb: datetime = None date_fin: datetime = None @@ -227,13 +260,23 @@ def _from_abs_to_justificatif( date_deb = localize_datetime(date_deb) date_fin = localize_datetime(date_fin) + duplicata: Justificatif = _glob.DUPLICATIONS_JUSTIFICATIFS.get( + (_abs.jour, _abs.matin, _abs.etudid) + ) + if duplicata is not None: + return "Duplicated" + desc: str = _abs.description entry_date: datetime = _abs.entry_date - etud: Identite = Identite.query.filter_by(id=_abs.etudid).first() + if _abs.etudid not in _glob.CURRENT_ETU: + etud: Identite = Identite.query.filter_by(id=_abs.etudid).first() + if etud is None: + return "No Etud" + _glob.CURRENT_ETU.append(_abs.etudid) - retour = Justificatif.create_justificatif( - etud=etud, + retour = Justificatif.fast_create_justificatif( + etudid=_abs.etudid, date_debut=date_deb, date_fin=date_fin, etat=etat, @@ -241,4 +284,5 @@ def _from_abs_to_justificatif( entry_date=entry_date, ) + _glob.DUPLICATIONS_JUSTIFICATIFS[(_abs.jour, _abs.matin, _abs.etudid)] = retour return retour From 21f57aab8f6273122e48e59da8a88b51fbb9211c Mon Sep 17 00:00:00 2001 From: iziram Date: Fri, 10 Feb 2023 14:08:31 +0100 Subject: [PATCH 5/5] migration abs ->assiduites : statistiques --- tools/migrate_abs_to_assiduites.py | 89 ++++++++++++++++++++++++------ 1 file changed, 73 insertions(+), 16 deletions(-) diff --git a/tools/migrate_abs_to_assiduites.py b/tools/migrate_abs_to_assiduites.py index e89db945..a2c763a6 100644 --- a/tools/migrate_abs_to_assiduites.py +++ b/tools/migrate_abs_to_assiduites.py @@ -17,7 +17,7 @@ from app.scodoc.sco_utils import ( printProgressBar, ) from datetime import time, datetime, date -from json import dump +from json import dump, dumps class _glob: @@ -28,6 +28,64 @@ class _glob: CURRENT_ETU: list = [] MODULES: list[tuple[int, int]] = [] COMPTE: list[int, int] = [] + ERR_ETU: list[int] = [] + + +class _Statistics: + def __init__(self) -> None: + self.object: dict = {"total": 0} + self.year: int = None + + def __set_year(self, year: int): + if year not in self.object: + self.object[year] = { + "etuds_inexistant": [], + "abs_invalide": {}, + } + self.year = year + return self + + def __add_etud(self, etudid: int): + if etudid not in self.object[self.year]["etuds_inexistant"]: + self.object[self.year]["etuds_inexistant"].append(etudid) + return self + + def __add_abs(self, abs: int, err: str): + if abs not in self.object[self.year]["abs_invalide"]: + self.object[self.year]["abs_invalide"][abs] = [err] + else: + self.object[self.year]["abs_invalide"][abs].append(err) + + return self + + def add_problem(self, abs: Absence, err: str): + abs.jour: date + pivot: date = date(abs.jour.year, 9, 15) + year: int = abs.jour.year + if pivot < abs.jour: + year += 1 + self.__set_year(year) + + if err == "Etudiant inexistant": + self.__add_etud(abs.etudid) + else: + self.__add_abs(abs.id, err) + + self.object["total"] += 1 + + def compute_stats(self) -> dict: + stats: dict = {"total": self.object["total"]} + for year in self.object: + if year == "total": + continue + stats[year] = {} + stats[year]["etuds_inexistant"] = len(self.object[year]["etuds_inexistant"]) + stats[year]["abs_invalide"] = len(self.object[year]["abs_invalide"]) + + return stats + + def export(self, file): + dump(self.object, file, indent=2) def migrate_abs_to_assiduites( @@ -52,6 +110,7 @@ def migrate_abs_to_assiduites( .etudid: relation -> Identite """ Profiler.clear() + stats: _Statistics = _Statistics() time_elapsed: Profiler = Profiler("migration") time_elapsed.start() @@ -86,10 +145,10 @@ def migrate_abs_to_assiduites( _glob.DUPLICATED = [] _glob.DUPLICATIONS_ASSIDUITES = {} _glob.DUPLICATIONS_JUSTIFICATIFS = {} - _glob.PROBLEMS = {} _glob.CURRENT_ETU = [] _glob.MODULES = [] _glob.COMPTE = [0, 0] + _glob.ERR_ETU = [] absences_len: int = absences.count() @@ -109,9 +168,7 @@ def migrate_abs_to_assiduites( db.session.add(generated) _glob.COMPTE[0] += 1 except Exception as e: - if abs.id not in _glob.PROBLEMS: - _glob.PROBLEMS[abs.id] = [] - _glob.PROBLEMS[abs.id].append(e.args[0]) + stats.add_problem(abs, e.args[0]) try: if abs.estjust: @@ -123,9 +180,7 @@ def migrate_abs_to_assiduites( _glob.COMPTE[1] += 1 except Exception as e: - if abs.id not in _glob.PROBLEMS: - _glob.PROBLEMS[abs.id] = [] - _glob.PROBLEMS[abs.id].append(e.args[0]) + stats.add_problem(abs, e.args[0]) if i % 10 == 0: printProgressBar( @@ -160,27 +215,29 @@ def migrate_abs_to_assiduites( "Progression", "effectué", autosize=True, - finish_msg=f"{ProgressBarColors.GREEN}Les absences ont bien été migrées.{ProgressBarColors.RESET}", ) time_elapsed.stop() + + statistiques: dict = stats.compute_stats() print( f"{ProgressBarColors.GREEN}La migration a pris {time_elapsed.elapsed():.2f} secondes {ProgressBarColors.RESET}" ) print( - f"{ProgressBarColors.RED}Il y a eu {len(_glob.PROBLEMS)} absences qui n'ont pas pu être migrée." + f"{ProgressBarColors.RED}{statistiques['total']} absences qui n'ont pas pu être migrée." ) print( - f"Vous retrouverez un fichier json {ProgressBarColors.GREEN}/tmp/scodoc_migration_abs.json{ProgressBarColors.RED} contenant les ids des absences ainsi que les erreurs liées." + f"Vous retrouverez un fichier json {ProgressBarColors.GREEN}/tmp/scodoc_migration_abs.json{ProgressBarColors.RED} contenant les problèmes de migrations" ) with open("/tmp/scodoc_migration_abs.json", "w", encoding="utf-8") as file: - dump(_glob.PROBLEMS, file) + stats.export(file) print( f"{ProgressBarColors.CYAN}{_glob.COMPTE[0]} assiduités et {_glob.COMPTE[1]} justificatifs ont été générés.{ProgressBarColors.RESET}" ) - # afficher nombre justificatifs généré par rapport au nombre de justificatifs + + print(dumps(statistiques, indent=2)) def _from_abs_to_assiduite( @@ -205,7 +262,7 @@ def _from_abs_to_assiduite( ) if duplicata is not None: _glob.DUPLICATED.append(duplicata) - return "Duplicated" + return "Duplicata" desc: str = _abs.description entry_date: datetime = _abs.entry_date @@ -213,7 +270,7 @@ def _from_abs_to_assiduite( if _abs.etudid not in _glob.CURRENT_ETU: etud: Identite = Identite.query.filter_by(id=_abs.etudid).first() if etud is None: - return "No Etud" + raise Exception("Etudiant inexistant") _glob.CURRENT_ETU.append(_abs.etudid) moduleimpl_id: int = _abs.moduleimpl_id @@ -272,7 +329,7 @@ def _from_abs_to_justificatif( if _abs.etudid not in _glob.CURRENT_ETU: etud: Identite = Identite.query.filter_by(id=_abs.etudid).first() if etud is None: - return "No Etud" + raise Exception("Etudiant inexistant") _glob.CURRENT_ETU.append(_abs.etudid) retour = Justificatif.fast_create_justificatif(