diff --git a/app/models/absences.py b/app/models/absences.py index e4bbd823d..90c00a354 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 181739ba9..a52d29908 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 f065a1f75..5f4a08a6d 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 4d56ad3cd..eee59ee53 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 12556c2c2..42093ae1b 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 ac9e681c2..a2bd377a2 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 000000000..45e6a559f --- /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