diff --git a/app/models/assiduites.py b/app/models/assiduites.py index a52d29908..81173d4c1 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 000000000..0e61d3856 --- /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 9f78fd2f5..55be94255 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 a8cb967cc..e89db9459 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