From e0ca0100d0f564d207a4866a92c4ee8a9ddc20c1 Mon Sep 17 00:00:00 2001 From: Iziram Date: Fri, 5 Jan 2024 10:06:16 +0100 Subject: [PATCH 1/2] Assiduites : maj CountCalculator fixes #820 --- app/forms/main/config_assiduites.py | 8 +- app/models/assiduites.py | 28 ++ app/scodoc/sco_assiduites.py | 345 +++++++++++++----- app/scodoc/sco_preferences.py | 11 + app/scodoc/sco_utils.py | 41 +++ .../assiduites/pages/config_assiduites.j2 | 8 +- app/views/assiduites.py | 4 +- app/views/scodoc.py | 32 +- 8 files changed, 355 insertions(+), 122 deletions(-) diff --git a/app/forms/main/config_assiduites.py b/app/forms/main/config_assiduites.py index 16b6f1279..56fd0534f 100644 --- a/app/forms/main/config_assiduites.py +++ b/app/forms/main/config_assiduites.py @@ -119,15 +119,15 @@ def check_ics_regexp(form, field): class ConfigAssiduitesForm(FlaskForm): "Formulaire paramétrage Module Assiduité" - morning_time = TimeField( + assi_morning_time = TimeField( "Début de la journée" ) # TODO utiliser TextField + timepicker voir AjoutAssiOrJustForm - lunch_time = TimeField( + assi_lunch_time = TimeField( "Heure de midi (date pivot entre matin et après-midi)" ) # TODO - afternoon_time = TimeField("Fin de la journée") # TODO + assi_afternoon_time = TimeField("Fin de la journée") # TODO - tick_time = DecimalField( + assi_tick_time = DecimalField( "Granularité de la timeline (temps en minutes)", places=0, validators=[check_tick_time], diff --git a/app/models/assiduites.py b/app/models/assiduites.py index e58e6aff2..5dc18a489 100644 --- a/app/models/assiduites.py +++ b/app/models/assiduites.py @@ -25,6 +25,7 @@ from app.scodoc.sco_utils import ( EtatJustificatif, localize_datetime, is_assiduites_module_forced, + NonWorkDays, ) @@ -154,6 +155,33 @@ class Assiduite(ScoDocModel): ) if date_fin.tzinfo is None: log(f"Warning: create_assiduite: date_fin without timezone ({date_fin})") + + # Vérification jours non travaillés + # -> vérifie si la date de début ou la date de fin est sur un jour non travaillé + # On récupère les formsemestres des dates de début et de fin + formsemetre_date_debut: FormSemestre = get_formsemestre_from_data( + { + "etudid": etud.id, + "date_debut": date_debut, + "date_fin": date_debut, + } + ) + formsemetre_date_fin: FormSemestre = get_formsemestre_from_data( + { + "etudid": etud.id, + "date_debut": date_fin, + "date_fin": date_fin, + } + ) + if date_debut.weekday() in NonWorkDays.get_all_non_work_days( + formsemestre_id=formsemetre_date_debut + ): + raise ScoValueError("La date de début n'est pas un jour travaillé") + if date_fin.weekday() in NonWorkDays.get_all_non_work_days( + formsemestre_id=formsemetre_date_fin + ): + raise ScoValueError("La date de fin n'est pas un jour travaillé") + # Vérification de non duplication des périodes assiduites: Query = etud.assiduites if is_period_conflicting(date_debut, date_fin, assiduites, Assiduite): diff --git a/app/scodoc/sco_assiduites.py b/app/scodoc/sco_assiduites.py index 00268dfed..f46205adb 100644 --- a/app/scodoc/sco_assiduites.py +++ b/app/scodoc/sco_assiduites.py @@ -15,59 +15,222 @@ from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_preferences from app.scodoc import sco_cache from app.scodoc import sco_etud +from app.models import ScoDocSiteConfig +from flask import g class CountCalculator: - """Classe qui gére le comptage des assiduités""" + """ + La classe CountCalculator est conçue pour gérer le comptage des assiduités, + en calculant le nombre total de jours complets, + de demi-journées, et d'heures passées sur une période donnée. + Elle prend en compte les jours non travaillés, + les horaires de travail standard et les assiduités s'étendant sur plusieurs jours. - # TODO documenter + Utilisation : + ------------ + 1. Initialisation : La classe peut être initialisée avec des horaires personnalisés + pour le matin, le midi et le soir, ainsi qu'une durée de pause déjeuner. + Si non spécifiés, les valeurs par défaut seront chargées depuis la configuration `ScoDocSiteConfig`. + Exemple d'initialisation : + calculator = CountCalculator(morning="08:00", noon="13:00", evening="18:00", nb_heures_par_jour=8) + + 2. Ajout d'assiduités : + Exemple d'ajout d'assiduité : + - calculator.compute_assiduites(etudiant.assiduites) + - calculator.compute_assiduites([, , , ]) + + 3. Accès aux métriques : Après l'ajout des assiduités, on peut accéder aux métriques telles que : + le nombre total de jours, de demi-journées et d'heures calculées. + Exemple d'accès aux métriques : + metrics = calculator.to_dict() + + 4.Réinitialisation du comptage: Si besoin on peut réinitialisé le compteur sans perdre la configuration + (horaires personnalisés) + Exemple de réinitialisation : + calculator.reset() + + Méthodes Principales : + --------------------- + - reset() : Réinitialise les compteurs de la classe. + - add_half_day(day: date, is_morning: bool) : Ajoute une demi-journée au comptage. + - add_day(day: date) : Ajoute un jour complet au comptage. + - compute_long_assiduite(assi: Assiduite) : Traite les assiduités s'étendant sur plus d'un jour. + - compute_assiduites(assiduites: Query | list) : Calcule les métriques pour une collection d'assiduités. + - to_dict() : Retourne les métriques sous forme de dictionnaire. + + Notes : + ------ + + Détails des calculs des heures: + Pour les assiduités courtes (<= 1 jour): + heures = assi.deb - assi.fin + Pour les assiduités longues (> 1 jour): + heures = + heures(assi.deb => fin_journee) + nb_heure_par_jour * (nb_jours-2) + + heures(assi.fin => fin_journee) + """ def __init__( self, - morning: time = time(8, 0), # TODO utiliser ScoDocSiteConfig - noon: time = time(12, 0), - after_noon: time = time(14, 00), - evening: time = time(18, 0), - skip_saturday: bool = True, # TODO préférence workdays + morning: str = None, + noon: str = None, + evening: str = None, + nb_heures_par_jour: int = None, ) -> 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 + # Transformation d'une heure "HH:MM" en time(h,m) + STR_TIME = lambda x: time(*list(map(int, x.split(":")))) - delta_total: timedelta = datetime.combine(date.min, evening) - datetime.combine( - date.min, morning + self.morning: time = STR_TIME( + morning if morning else ScoDocSiteConfig.get("assi_morning_time", "08:00") + ) + # Date pivot pour déterminer les demi-journées + self.noon: time = STR_TIME( + noon if noon else ScoDocSiteConfig.get("assi_lunch_time", "13:00") + ) + self.evening: time = STR_TIME( + evening if evening else ScoDocSiteConfig.get("assi_afternoon_time", "18:00") ) - 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.non_work_days: list[ + scu.NonWorkDays + ] = scu.NonWorkDays.get_all_non_work_days(dept_id=g.scodoc_dept_id) - self.days: list[date] = [] - self.half_days: list[tuple[date, bool]] = [] # tuple -> (date, morning:bool) - self.hours: float = 0.0 + delta_total: timedelta = datetime.combine( + date.min, self.evening + ) - datetime.combine(date.min, self.morning) - self.count: int = 0 + # Sera utilisé pour les assiduités longues (> 1 journée) + self.nb_heures_par_jour = ( + nb_heures_par_jour + if nb_heures_par_jour + else sco_preferences.get_preference( + "nb_heures_par_jour", dept_id=g.scodoc_dept_id + ) + ) + + self.data = {} + self.reset() def reset(self): """Remet à zero les compteurs""" - self.days = [] - self.half_days = [] - self.hours = 0.0 - self.count = 0 + self.data = { + "total": { + "journee": [], + "demi": [], + "heure": 0, + "compte": 0, + }, + "absent": { + "journee": [], + "demi": [], + "heure": 0, + "compte": 0, + }, + "absent_just": { + "journee": [], + "demi": [], + "heure": 0, + "compte": 0, + }, + "absent_non_just": { + "journee": [], + "demi": [], + "heure": 0, + "compte": 0, + }, + "retard": { + "journee": [], + "demi": [], + "heure": 0, + "compte": 0, + }, + "retard_just": { + "journee": [], + "demi": [], + "heure": 0, + "compte": 0, + }, + "retard_non_just": { + "journee": [], + "demi": [], + "heure": 0, + "compte": 0, + }, + "present": { + "journee": [], + "demi": [], + "heure": 0, + "compte": 0, + }, + } - def add_half_day(self, day: date, is_morning: bool = True): + def get_count_key(self, etat: scu.EtatAssiduite, justi: bool = False) -> str: + """Récupère une clé de dictionnaire en fonction de l'état de l'assiduité + et si elle est justifié + """ + keys: dict[EtatAssiduite, str] = { + scu.EtatAssiduite.ABSENT: "absent", + scu.EtatAssiduite.RETARD: "retard", + scu.EtatAssiduite.PRESENT: "present", + } + count_key: str = keys.get(etat) + if etat != scu.EtatAssiduite.PRESENT: + count_key += "_just" if justi else "_non_just" + return count_key + + def add_half_day(self, day: date, assi: Assiduite, is_morning: bool = True): """Ajoute une demi-journée dans le comptage""" 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): + count_key: str = self.get_count_key(assi.etat, assi.est_just) + if assi.etat != scu.EtatAssiduite.PRESENT: + _key: str = scu.EtatAssiduite.inverse().get(assi.etat).name.lower() + if key not in self.data[_key]["demi"]: + self.data[_key]["demi"].append(day) + + if key not in self.data["total"]["demi"]: + self.data["total"]["demi"].append(key) + if key not in self.data[count_key]["demi"]: + self.data[count_key]["demi"].append(key) + + def add_day(self, day: date, assi: Assiduite): """Ajoute un jour dans le comptage""" - if day not in self.days: - self.days.append(day) + count_key: str = self.get_count_key(assi.etat, assi.est_just) + if assi.etat != scu.EtatAssiduite.PRESENT: + key: str = scu.EtatAssiduite.inverse().get(assi.etat).name.lower() + if day not in self.data[key]["journee"]: + self.data[key]["journee"].append(day) + + if day not in self.data["total"]["journee"]: + self.data["total"]["journee"].append(day) + if day not in self.data[count_key]["journee"]: + self.data[count_key]["journee"].append(day) + + def add_hours(self, hours: float, assi: Assiduite): + """Ajoute des heures dans le comptage""" + + count_key: str = self.get_count_key(assi.etat, assi.est_just) + if assi.etat != scu.EtatAssiduite.PRESENT: + self.data[scu.EtatAssiduite.inverse().get(assi.etat).name.lower()][ + "heure" + ] += hours + + self.data[count_key]["heure"] += hours + self.data["total"]["heure"] += hours + + def add_count(self, assi: Assiduite): + """Ajoute 1 count dans le comptage""" + + count_key: str = self.get_count_key(assi.etat, assi.est_just) + if assi.etat != scu.EtatAssiduite.PRESENT: + self.data[scu.EtatAssiduite.inverse().get(assi.etat).name.lower()][ + "compte" + ] += 1 + + self.data[count_key]["compte"] += 1 + self.data["total"]["compte"] += 1 def is_in_morning(self, period: tuple[datetime, datetime]) -> bool: """Vérifiée si la période donnée fait partie du matin @@ -90,7 +253,9 @@ class CountCalculator: """ 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.noon) + timedelta(seconds=1) + ), scu.localize_datetime(datetime.combine(period[0].date(), self.evening)), ) @@ -102,15 +267,9 @@ class CountCalculator: """Calcule les métriques sur une assiduité longue (plus d'un jour)""" 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()) + self.add_day(assi.date_debut.date(), assi) + self.add_day(assi.date_fin.date(), assi) start_period: tuple[datetime, datetime] = ( assi.date_debut, @@ -123,58 +282,67 @@ class CountCalculator: 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.is_in_evening(period): - self.add_half_day(period[0].date(), False) + self.add_half_day(period[0].date(), assi, False) if self.is_in_morning(period): - self.add_half_day(period[0].date()) + self.add_half_day(period[0].date(), assi) while pointer_date < assi.date_fin.date(): - # TODO : Utiliser la préférence de département : workdays - 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 + if pointer_date.weekday() not in self.non_work_days: + self.add_day(pointer_date, assi) + self.add_half_day(pointer_date, assi) + self.add_half_day(pointer_date, assi, False) + self.add_hours(self.nb_heures_par_jour, assi) pointer_date += timedelta(days=1) - self.hours += finish_hours.total_seconds() / 3600 - self.hours += self.hour_per_day - (start_hours.total_seconds() / 3600) + # Gestion des heures des dates de début et des dates de fin + deb_hours = (start_period[1] - start_period[0]).total_seconds() / 3600 + fin_hours = (finish_period[1] - finish_period[0]).total_seconds() / 3600 + + self.add_hours(deb_hours + fin_hours, assi) def compute_assiduites(self, assiduites: Query | list): """Calcule les métriques pour la collection d'assiduité donnée""" assi: Assiduite for assi in assiduites: - self.count += 1 + # Ajout vérification workday + # (Si préférence mise après avoir déjà noté des assiduités) + if assi.date_debut.weekday() in self.non_work_days: + continue + + self.add_count(assi) + delta: timedelta = assi.date_fin - assi.date_debut if delta.days > 0: 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.is_in_morning(period): - self.add_half_day(deb_date) + self.add_half_day(deb_date, assi) if self.is_in_evening(period): - self.add_half_day(deb_date, False) + self.add_half_day(deb_date, assi, False) - self.add_day(deb_date) + self.add_day(deb_date, assi) - self.hours += delta.total_seconds() / 3600 + self.add_hours(delta.total_seconds() / 3600, assi) + self.setup_data() - def to_dict(self) -> dict[str, int | float]: + def setup_data(self): + """Met en forme les données + pour les journées et les demi-journées : au lieu d'avoir list[str] on a le nombre (len(list[str])) + """ + for key in self.data: + self.data[key]["journee"] = len(self.data[key]["journee"]) + self.data[key]["demi"] = len(self.data[key]["demi"]) + + def to_dict(self, only_total: bool = True) -> dict[str, int | float]: """Retourne les métriques sous la forme d'un dictionnaire""" - return { - "compte": self.count, - "journee": len(self.days), - "demi": len(self.half_days), - "heure": round(self.hours, 2), - } + return self.data["total"] if only_total else self.data def get_assiduites_stats( @@ -211,55 +379,34 @@ def get_assiduites_stats( metrics: list[str] = metric.split(",") output: dict = {} calculator: CountCalculator = CountCalculator() + calculator.compute_assiduites(assiduites) if filtered is None or "split" not in filtered: - calculator.compute_assiduites(assiduites) - count: dict = calculator.to_dict() - + count: dict = calculator.to_dict(only_total=True) for key, val in count.items(): if key in metrics: output[key] = val return output if output else count + # Récupération des états etats: list[str] = ( filtered["etat"].split(",") if "etat" in filtered else ["absent", "present", "retard"] ) - + # Préparation du dictionnaire de retour avec les valeurs du calcul + count: dict = calculator.to_dict(only_total=False) for etat in etats: - output[etat] = _count_assiduites_etat(etat, assiduites, calculator, metrics) - if "est_just" not in filtered: - output[etat]["justifie"] = _count_assiduites_etat( - etat, assiduites, calculator, metrics, justifie=True - ) - + if etat != "present": + output[etat] = count[etat] + output[etat]["justifie"] = count[etat + "_just"] + output[etat]["non_justifie"] = count[etat + "_non_just"] + else: + output[etat] = count[etat] + output["total"] = count["total"] return output -def _count_assiduites_etat( - etat: str, - assiduites: Query, - calculator: CountCalculator, - metrics: list[str], - justifie: bool = False, -): # TODO type retour ? - # TODO documenter - calculator.reset() - etat_num: int = scu.EtatAssiduite.get(etat, -1) - assiduites_etat: Query = assiduites.filter(Assiduite.etat == etat_num) - if justifie: - assiduites_etat = assiduites_etat.filter(Assiduite.est_just == True) - - calculator.compute_assiduites(assiduites_etat) - count_etat: dict = calculator.to_dict() - output_etat: dict = {} - for key, val in count_etat.items(): - if key in metrics: - output_etat[key] = val - return output_etat if output_etat else count_etat - - def filter_assiduites_by_etat(assiduites: Assiduite, etat: str) -> Query: """ Filtrage d'une collection d'assiduites en fonction de leur état diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py index ed126bce7..ede0698a8 100644 --- a/app/scodoc/sco_preferences.py +++ b/app/scodoc/sco_preferences.py @@ -655,6 +655,17 @@ class BasePreferences: "explanation": "Durée d'un créneau en heure. Utilisé dans les pages de saisie", }, ), + ( + "nb_heures_par_jour", + { + "initvalue": 8, + "size": 10, + "title": "Nombre d'heures de travail dans une journée", + "type": "int", + "explanation": "Est utilisé dans le calcul de la métrique 'heure'. ", + "category": "assi", + }, + ), ( "assi_etat_defaut", { diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 7f23f8465..5e2dae80f 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -238,6 +238,47 @@ class EtatJustificatif(int, BiDirectionalEnum): return etat in cls._value2member_map_ +class NonWorkDays(int, BiDirectionalEnum): + """Correspondance entre les jours et les numéros de jours""" + + LUN = 0 + MAR = 1 + MER = 2 + JEU = 3 + VEN = 4 + SAM = 5 + DIM = 6 + + @classmethod + def get_all_non_work_days( + cls, formsemestre_id: int = None, dept_id: int = None + ) -> list["NonWorkDays"]: + """ + get_all_non_work_days Récupère la liste des non workdays (str) depuis les préférences + puis renvoie une liste BiDirectionnalEnum NonWorkDays + + Example: + non_work_days : list[NonWorkDays] = NonWorkDays.get_all_non_work_days(dept_id=g.scodoc_dept_id) + if datetime.datetime.now().weekday() in non_work_days: + print("Aujourd'hui est un jour non travaillé") + + Args: + formsemestre_id (int, optional): id d'un formsemestre . Defaults to None. + dept_id (int, optional): id d'un départment. Defaults to None. + + Returns: + list[NonWorkDays]: La liste des NonWorkDays en version BiDirectionnalEnum + """ + from app.scodoc import sco_preferences + + return [ + cls.get(day.strip()) + for day in sco_preferences.get_preference( + "non_travail", formsemestre_id=formsemestre_id, dept_id=dept_id + ).split(",") + ] + + def is_iso_formated(date: str, convert=False) -> bool or datetime.datetime or None: """ Vérifie si une date est au format iso diff --git a/app/templates/assiduites/pages/config_assiduites.j2 b/app/templates/assiduites/pages/config_assiduites.j2 index c3b86b8b6..a0203d70d 100644 --- a/app/templates/assiduites/pages/config_assiduites.j2 +++ b/app/templates/assiduites/pages/config_assiduites.j2 @@ -79,10 +79,10 @@ c'est à dire à la montre des étudiants. {{ form.hidden_tag() }} {{ wtf.form_errors(form, hiddens="only") }} - {{ wtf.form_field(form.morning_time) }} - {{ wtf.form_field(form.lunch_time) }} - {{ wtf.form_field(form.afternoon_time) }} - {{ wtf.form_field(form.tick_time) }} + {{ wtf.form_field(form.assi_morning_time) }} + {{ wtf.form_field(form.assi_lunch_time) }} + {{ wtf.form_field(form.assi_afternoon_time) }} + {{ wtf.form_field(form.assi_tick_time) }}
diff --git a/app/views/assiduites.py b/app/views/assiduites.py index 44ca2ba8a..243399f92 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -348,8 +348,8 @@ def _get_dates_from_assi_form( Ramène ok=True si ok. Met des messages d'erreur dans le form. """ - debut_jour = "00:00" - fin_jour = "23:59:59" + debut_jour = ScoDocSiteConfig.get("assi_morning_time", "08:00") + fin_jour = ScoDocSiteConfig.get("assi_afternoon_time", "17:00") date_fin = None # On commence par convertir individuellement tous les champs try: diff --git a/app/views/scodoc.py b/app/views/scodoc.py index 619492e7f..720007eb1 100644 --- a/app/views/scodoc.py +++ b/app/views/scodoc.py @@ -337,15 +337,19 @@ def config_assiduites(): ("edt_ics_user_path", "Chemin vers les ics des enseignants"), ) + assi_options = ( + ("assi_morning_time", "Heure du début de la journée"), + ("assi_lunch_time", "Heure du midi"), + ("assi_afternoon_time", "Heure du fin de la journée"), + ("assi_tick_time", "Granularité de la timeline"), + ) + if form.validate_on_submit(): - if ScoDocSiteConfig.set("assi_morning_time", form.data["morning_time"]): - flash("Heure du début de la journée enregistrée") - if ScoDocSiteConfig.set("assi_lunch_time", form.data["lunch_time"]): - flash("Heure de midi enregistrée") - if ScoDocSiteConfig.set("assi_afternoon_time", form.data["afternoon_time"]): - flash("Heure de fin de la journée enregistrée") - if ScoDocSiteConfig.set("assi_tick_time", float(form.data["tick_time"])): - flash("Granularité de la timeline enregistrée") + # --- Options assiduités + for opt_name, message in assi_options: + if ScoDocSiteConfig.set(opt_name, form.data[opt_name]): + flash(f"{message} enregistrée") + # --- Calendriers emploi du temps for opt_name, message in edt_options: if ScoDocSiteConfig.set(opt_name, form.data[opt_name]): @@ -354,19 +358,21 @@ def config_assiduites(): return redirect(url_for("scodoc.configuration")) if request.method == "GET": - form.morning_time.data = ScoDocSiteConfig.get( + form.assi_morning_time.data = ScoDocSiteConfig.get( "assi_morning_time", datetime.time(8, 0, 0) ) - form.lunch_time.data = ScoDocSiteConfig.get( + form.assi_lunch_time.data = ScoDocSiteConfig.get( "assi_lunch_time", datetime.time(13, 0, 0) ) - form.afternoon_time.data = ScoDocSiteConfig.get( + form.assi_afternoon_time.data = ScoDocSiteConfig.get( "assi_afternoon_time", datetime.time(18, 0, 0) ) try: - form.tick_time.data = float(ScoDocSiteConfig.get("assi_tick_time", 15.0)) + form.assi_tick_time.data = float( + ScoDocSiteConfig.get("assi_tick_time", 15.0) + ) except ValueError: - form.tick_time.data = 15.0 + form.assi_tick_time.data = 15.0 ScoDocSiteConfig.set("assi_tick_time", 15.0) # --- Emplois du temps for opt_name, _ in edt_options: From 5af3e8d14d90f8dc69306bd9a5f25a8c1fd90ae5 Mon Sep 17 00:00:00 2001 From: Iziram Date: Fri, 5 Jan 2024 13:42:55 +0100 Subject: [PATCH 2/2] Assiduites : ajout test unitaire countCalculator + maj autres tests --- app/api/__init__.py | 8 +- app/api/assiduites.py | 10 +- app/api/justificatifs.py | 4 +- tests/api/setup_test_api.py | 7 +- tests/api/test_api_assiduites.py | 86 ++- tests/unit/test_assiduites.py | 525 +++++++++++++----- .../fakedatabase/create_test_api_database.py | 4 +- 7 files changed, 472 insertions(+), 172 deletions(-) diff --git a/app/api/__init__.py b/app/api/__init__.py index a46d08896..a6f2b680b 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -59,7 +59,13 @@ def get_model_api_object(model_cls: db.Model, model_id: int, join_cls: db.Model query = model_cls.query.filter_by(id=model_id) if g.scodoc_dept and join_cls is not None: query = query.join(join_cls).filter_by(dept_id=g.scodoc_dept_id) - unique: model_cls = query.first_or_404() + unique: model_cls = query.first() + + if unique is None: + return scu.json_error( + 404, + message=f"{model_cls.__name__} inexistant(e)", + ) return unique.to_dict(format_api=True) diff --git a/app/api/assiduites.py b/app/api/assiduites.py index d75b796c3..4ca6fcbc7 100644 --- a/app/api/assiduites.py +++ b/app/api/assiduites.py @@ -39,6 +39,7 @@ from app.scodoc.sco_utils import json_error @api_web_bp.route("/assiduite/") @scodoc @permission_required(Permission.ScoView) +@as_json def assiduite(assiduite_id: int = None): """Retourne un objet assiduité à partir de son id @@ -172,6 +173,7 @@ def count_assiduites( 404, message="étudiant inconnu", ) + g.scodoc_dept_id = etud.dept_id # Les filtres qui seront appliqués au comptage (type, date, etudid...) filtered: dict[str, object] = {} @@ -444,6 +446,8 @@ def count_assiduites_formsemestre( if formsemestre is None: return json_error(404, "le paramètre 'formsemestre_id' n'existe pas") + g.scodoc_dept_id = formsemestre.dept_id + # Récupération des étudiants du formsemestre etuds = formsemestre.etuds.all() etuds_id = [etud.id for etud in etuds] @@ -833,9 +837,9 @@ def assiduite_edit(assiduite_id: int): """ # Récupération de l'assiduité à modifier - assiduite_unique: Assiduite = Assiduite.query.filter_by( - id=assiduite_id - ).first_or_404() + assiduite_unique: Assiduite = Assiduite.query.filter_by(id=assiduite_id).first() + if assiduite_unique is None: + return json_error(404, "Assiduité non existante") # Récupération des valeurs à modifier data = request.get_json(force=True) diff --git a/app/api/justificatifs.py b/app/api/justificatifs.py index 3c98ffbfd..fd9b8cc64 100644 --- a/app/api/justificatifs.py +++ b/app/api/justificatifs.py @@ -154,7 +154,9 @@ def justificatifs_dept(dept_id: int = None, with_query: bool = False): """XXX TODO missing doc""" # Récupération du département et des étudiants du département - dept: Departement = Departement.query.get_or_404(dept_id) + dept: Departement = Departement.query.get(dept_id) + if dept is None: + json_error(404, "Assiduité non existante") etuds: list[int] = [etud.id for etud in dept.etudiants] # Récupération des justificatifs des étudiants du département diff --git a/tests/api/setup_test_api.py b/tests/api/setup_test_api.py index adcd2e0a3..c34867d1d 100644 --- a/tests/api/setup_test_api.py +++ b/tests/api/setup_test_api.py @@ -81,6 +81,9 @@ def GET(path: str, headers: dict = None, errmsg=None, dept=None): timeout=SCO_TEST_API_TIMEOUT, ) if reply.status_code != 200: + print("url", SCODOC_URL) + print("url", url) + print("reply", reply.text) raise APIError( errmsg or f"""erreur status={reply.status_code} !""", reply.json() ) @@ -153,7 +156,7 @@ def check_failure_get(path: str, headers: dict, err: str = None): """ try: - GET(path=path, headers=headers) + GET(path=path, headers=headers, dept=DEPT_ACRONYM) # ^ Renvoi un 404 except APIError as api_err: if err is not None: @@ -177,7 +180,7 @@ def check_failure_post(path: str, headers: dict, data: dict, err: str = None): """ try: - data = POST_JSON(path=path, headers=headers, data=data) + data = POST_JSON(path=path, headers=headers, data=data, dept=DEPT_ACRONYM) # ^ Renvoie un 404 except APIError as api_err: if err is not None: diff --git a/tests/api/test_api_assiduites.py b/tests/api/test_api_assiduites.py index 9c361c231..824fb51a7 100644 --- a/tests/api/test_api_assiduites.py +++ b/tests/api/test_api_assiduites.py @@ -11,6 +11,7 @@ from types import NoneType from tests.api.setup_test_api import ( GET, POST_JSON, + DEPT_ACRONYM, APIError, api_headers, api_admin_headers, @@ -45,7 +46,7 @@ ASSIDUITES_FIELDS = { CREATE_FIELD = {"assiduite_id": int} BATCH_FIELD = {"errors": list, "success": list} -COUNT_FIELDS = {"compte": int, "journee": int, "demi": int, "heure": float} +COUNT_FIELDS = {"compte": int, "journee": int, "demi": int, "heure": int | float} TO_REMOVE = [] @@ -81,7 +82,7 @@ def test_route_assiduite(api_headers): """test de la route /assiduite/""" # Bon fonctionnement == id connu - data = GET(path="/assiduite/1", headers=api_headers) + data = GET(path="/assiduite/1", headers=api_headers, dept=DEPT_ACRONYM) check_fields(data, fields=ASSIDUITES_FIELDS) # Mauvais Fonctionnement == id inconnu @@ -97,13 +98,16 @@ def test_route_count_assiduites(api_headers): # Bon fonctionnement - data = GET(path=f"/assiduites/{ETUDID}/count", headers=api_headers) + data = GET( + path=f"/assiduites/{ETUDID}/count", headers=api_headers, dept=DEPT_ACRONYM + ) check_fields(data, COUNT_FIELDS) metrics = {"heure", "compte"} data = GET( path=f"/assiduites/{ETUDID}/count/query?metric={','.join(metrics)}", headers=api_headers, + dept=DEPT_ACRONYM, ) assert set(data.keys()) == metrics @@ -118,12 +122,14 @@ def test_route_assiduites(api_headers): # Bon fonctionnement - data = GET(path=f"/assiduites/{ETUDID}", headers=api_headers) + data = GET(path=f"/assiduites/{ETUDID}", headers=api_headers, dept=DEPT_ACRONYM) assert isinstance(data, list) for ass in data: check_fields(ass, ASSIDUITES_FIELDS) - data = GET(path=f"/assiduites/{ETUDID}/query?", headers=api_headers) + data = GET( + path=f"/assiduites/{ETUDID}/query?", headers=api_headers, dept=DEPT_ACRONYM + ) assert isinstance(data, list) for ass in data: check_fields(ass, ASSIDUITES_FIELDS) @@ -138,13 +144,19 @@ def test_route_formsemestre_assiduites(api_headers): # Bon fonctionnement - data = GET(path=f"/assiduites/formsemestre/{FORMSEMESTREID}", headers=api_headers) + data = GET( + path=f"/assiduites/formsemestre/{FORMSEMESTREID}", + headers=api_headers, + dept=DEPT_ACRONYM, + ) assert isinstance(data, list) for ass in data: check_fields(ass, ASSIDUITES_FIELDS) data = GET( - path=f"/assiduites/formsemestre/{FORMSEMESTREID}/query?", headers=api_headers + path=f"/assiduites/formsemestre/{FORMSEMESTREID}/query?", + headers=api_headers, + dept=DEPT_ACRONYM, ) assert isinstance(data, list) for ass in data: @@ -169,13 +181,19 @@ def test_route_count_formsemestre_assiduites(api_headers): # Bon fonctionnement data = GET( - path=f"/assiduites/formsemestre/{FORMSEMESTREID}/count", headers=api_headers + path=f"/assiduites/formsemestre/{FORMSEMESTREID}/count", + headers=api_headers, + dept=DEPT_ACRONYM, ) + + print("data: ", data) + check_fields(data, COUNT_FIELDS) metrics = {"heure", "compte"} data = GET( path=f"/assiduites/formsemestre/{FORMSEMESTREID}/count/query?metric={','.join(metrics)}", headers=api_headers, + dept=DEPT_ACRONYM, ) assert set(data.keys()) == metrics @@ -198,9 +216,11 @@ def test_route_create(api_admin_headers): # -== Unique ==- # Bon fonctionnement - data = create_data("present", "01") + data = create_data("present", "03") - res = POST_JSON(f"/assiduite/{ETUDID}/create", [data], api_admin_headers) + res = POST_JSON( + f"/assiduite/{ETUDID}/create", [data], api_admin_headers, dept=DEPT_ACRONYM + ) check_fields(res, BATCH_FIELD) assert len(res["success"]) == 1 @@ -208,11 +228,14 @@ def test_route_create(api_admin_headers): data = GET( path=f'/assiduite/{res["success"][0]["message"]["assiduite_id"]}', headers=api_admin_headers, + dept=DEPT_ACRONYM, ) check_fields(data, fields=ASSIDUITES_FIELDS) - data2 = create_data("absent", "02", MODULE, "desc") - res = POST_JSON(f"/assiduite/{ETUDID}/create", [data2], api_admin_headers) + data2 = create_data("absent", "04", MODULE, "desc") + res = POST_JSON( + f"/assiduite/{ETUDID}/create", [data2], api_admin_headers, dept=DEPT_ACRONYM + ) check_fields(res, BATCH_FIELD) assert len(res["success"]) == 1 @@ -221,7 +244,9 @@ def test_route_create(api_admin_headers): # Mauvais fonctionnement check_failure_post(f"/assiduite/{FAUX}/create", api_admin_headers, [data]) - res = POST_JSON(f"/assiduite/{ETUDID}/create", [data], api_admin_headers) + res = POST_JSON( + f"/assiduite/{ETUDID}/create", [data], api_admin_headers, dept=DEPT_ACRONYM + ) check_fields(res, BATCH_FIELD) assert len(res["errors"]) == 1 assert ( @@ -231,8 +256,9 @@ def test_route_create(api_admin_headers): res = POST_JSON( f"/assiduite/{ETUDID}/create", - [create_data("absent", "03", FAUX)], + [create_data("absent", "05", FAUX)], api_admin_headers, + dept=DEPT_ACRONYM, ) check_fields(res, BATCH_FIELD) assert len(res["errors"]) == 1 @@ -245,10 +271,12 @@ def test_route_create(api_admin_headers): etats = ["present", "absent", "retard"] data = [ create_data(etats[d % 3], 10 + d, MODULE if d % 2 else None) - for d in range(randint(3, 5)) + for d in range(randint(2, 4)) ] - res = POST_JSON(f"/assiduite/{ETUDID}/create", data, api_admin_headers) + res = POST_JSON( + f"/assiduite/{ETUDID}/create", data, api_admin_headers, dept=DEPT_ACRONYM + ) check_fields(res, BATCH_FIELD) for dat in res["success"]: check_fields(dat["message"], CREATE_FIELD) @@ -257,15 +285,18 @@ def test_route_create(api_admin_headers): # Mauvais Fonctionnement data2 = [ - create_data("present", "01"), + create_data("present", "03"), create_data("present", "25", FAUX), create_data("blabla", 26), create_data("absent", 32), + create_data("absent", "01"), ] - res = POST_JSON(f"/assiduite/{ETUDID}/create", data2, api_admin_headers) + res = POST_JSON( + f"/assiduite/{ETUDID}/create", data2, api_admin_headers, dept=DEPT_ACRONYM + ) check_fields(res, BATCH_FIELD) - assert len(res["errors"]) == 4 + assert len(res["errors"]) == 5 assert ( res["errors"][0]["message"] @@ -277,6 +308,7 @@ def test_route_create(api_admin_headers): res["errors"][3]["message"] == "param 'date_debut': format invalide, param 'date_fin': format invalide" ) + assert res["errors"][4]["message"] == "La date de début n'est pas un jour travaillé" def test_route_edit(api_admin_headers): @@ -285,11 +317,15 @@ def test_route_edit(api_admin_headers): # Bon fonctionnement data = {"etat": "retard", "moduleimpl_id": MODULE} - res = POST_JSON(f"/assiduite/{TO_REMOVE[0]}/edit", data, api_admin_headers) + res = POST_JSON( + f"/assiduite/{TO_REMOVE[0]}/edit", data, api_admin_headers, dept=DEPT_ACRONYM + ) assert res == {"OK": True} data["moduleimpl_id"] = None - res = POST_JSON(f"/assiduite/{TO_REMOVE[1]}/edit", data, api_admin_headers) + res = POST_JSON( + f"/assiduite/{TO_REMOVE[1]}/edit", data, api_admin_headers, dept=DEPT_ACRONYM + ) assert res == {"OK": True} # Mauvais fonctionnement @@ -311,13 +347,13 @@ def test_route_delete(api_admin_headers): # Bon fonctionnement data = TO_REMOVE[0] - res = POST_JSON("/assiduite/delete", [data], api_admin_headers) + res = POST_JSON("/assiduite/delete", [data], api_admin_headers, dept=DEPT_ACRONYM) check_fields(res, BATCH_FIELD) for dat in res["success"]: assert dat["message"] == "OK" # Mauvais fonctionnement - res = POST_JSON("/assiduite/delete", [data], api_admin_headers) + res = POST_JSON("/assiduite/delete", [data], api_admin_headers, dept=DEPT_ACRONYM) check_fields(res, BATCH_FIELD) assert len(res["errors"]) == 1 @@ -327,7 +363,7 @@ def test_route_delete(api_admin_headers): data = TO_REMOVE[1:] - res = POST_JSON("/assiduite/delete", data, api_admin_headers) + res = POST_JSON("/assiduite/delete", data, api_admin_headers, dept=DEPT_ACRONYM) check_fields(res, BATCH_FIELD) for dat in res["success"]: assert dat["message"] == "OK" @@ -340,7 +376,7 @@ def test_route_delete(api_admin_headers): FAUX + 2, ] - res = POST_JSON("/assiduite/delete", data2, api_admin_headers) + res = POST_JSON("/assiduite/delete", data2, api_admin_headers, dept=DEPT_ACRONYM) check_fields(res, BATCH_FIELD) assert len(res["errors"]) == 3 diff --git a/tests/unit/test_assiduites.py b/tests/unit/test_assiduites.py index e763597aa..7ea636a24 100644 --- a/tests/unit/test_assiduites.py +++ b/tests/unit/test_assiduites.py @@ -50,106 +50,30 @@ def test_bi_directional_enum(test_client): def test_general(test_client): """tests général du modèle assiduite""" - g_fake = sco_fake_gen.ScoFake(verbose=False) - - # Création d'une formation (1) - - formation_id = g_fake.create_formation() - ue_id = g_fake.create_ue( - formation_id=formation_id, acronyme="T1", titre="UE TEST 1" + data: dict = _setup_fake_db( + dates_formsemestre=[ + ("01/09/2022", "31/12/2022"), + ("01/01/2023", "31/07/2023"), + ("01/01/2024", "31/07/2024"), + ], + nb_modules=2, + nb_etuds=3, ) - matiere_id = g_fake.create_matiere(ue_id=ue_id, titre="test matière") - module_id_1 = g_fake.create_module( - matiere_id=matiere_id, code="Mo1", coefficient=1.0, titre="test module" + etuds, moduleimpls, etud_faux, formsemestres = ( + data["etuds"], + data["moduleimpls"], + data["etud_faux"], + data["formsemestres"], ) - module_id_2 = g_fake.create_module( - matiere_id=matiere_id, code="Mo2", coefficient=1.0, titre="test module2" - ) - - # Création semestre (2) - - formsemestre_id_1 = g_fake.create_formsemestre( - formation_id=formation_id, - semestre_id=1, - date_debut="01/09/2022", - date_fin="31/12/2022", - ) - formsemestre_id_2 = g_fake.create_formsemestre( - formation_id=formation_id, - semestre_id=2, - date_debut="01/01/2023", - date_fin="31/07/2023", - ) - formsemestre_id_3 = g_fake.create_formsemestre( - formation_id=formation_id, - semestre_id=3, - date_debut="01/01/2024", - date_fin="31/07/2024", - ) - formsemestre_1 = FormSemestre.get_formsemestre(formsemestre_id_1) - formsemestre_2 = FormSemestre.get_formsemestre(formsemestre_id_2) - formsemestre_3 = FormSemestre.get_formsemestre(formsemestre_id_3) - - # Création des modulesimpls (4, 2 par semestre) - - moduleimpl_1_1 = g_fake.create_moduleimpl( - module_id=module_id_1, - formsemestre_id=formsemestre_id_1, - ) - moduleimpl_1_2 = g_fake.create_moduleimpl( - module_id=module_id_2, - formsemestre_id=formsemestre_id_1, - ) - - moduleimpl_2_1 = g_fake.create_moduleimpl( - module_id=module_id_1, - formsemestre_id=formsemestre_id_2, - ) - moduleimpl_2_2 = g_fake.create_moduleimpl( - module_id=module_id_2, - formsemestre_id=formsemestre_id_2, - ) - moduleimpls = [ - moduleimpl_1_1, - moduleimpl_1_2, - moduleimpl_2_1, - moduleimpl_2_2, - ] - moduleimpls = [ - ModuleImpl.query.filter_by(id=mi_id).first() for mi_id in moduleimpls - ] - - # Création de 3 étudiants - etud_0 = g_fake.create_etud(prenom="etud0") - etud_1 = g_fake.create_etud(prenom="etud1") - etud_2 = g_fake.create_etud(prenom="etud2") - etuds_dict = [etud_0, etud_1, etud_2] - # etuds_dict = [g_fake.create_etud(prenom=f"etud{i}") for i in range(3)] - - etuds = [] - for etud in etuds_dict: - g_fake.inscrit_etudiant(formsemestre_id=formsemestre_id_1, etud=etud) - g_fake.inscrit_etudiant(formsemestre_id=formsemestre_id_2, etud=etud) - - etuds.append(Identite.query.filter_by(id=etud["etudid"]).first()) - - assert None not in etuds, "Problème avec la conversion en Identite" - - # Etudiant faux - - etud_faux_dict = g_fake.create_etud(prenom="etudfaux") - etud_faux = Identite.query.filter_by(id=etud_faux_dict["etudid"]).first() verif_migration_abs_assiduites() ajouter_assiduites(etuds, moduleimpls, etud_faux) justificatifs: list[Justificatif] = ajouter_justificatifs(etuds[0]) - verifier_comptage_et_filtrage_assiduites( - etuds, moduleimpls, (formsemestre_1, formsemestre_2, formsemestre_3) - ) + verifier_comptage_et_filtrage_assiduites(etuds, moduleimpls[:4], formsemestres) verifier_filtrage_justificatifs(etuds[0], justificatifs) - essais_cache(etuds[0].etudid, (formsemestre_1, formsemestre_2), moduleimpls) + essais_cache(etuds[0].etudid, formsemestres[:2], moduleimpls) editer_supprimer_assiduites(etuds, moduleimpls) editer_supprimer_justificatif(etuds[0]) @@ -531,8 +455,8 @@ def ajouter_justificatifs(etud): obj_justificatifs = [ { "etat": scu.EtatJustificatif.ATTENTE, - "deb": "2022-09-03T08:00+01:00", - "fin": "2022-09-03T09:59:59+01:00", + "deb": "2022-09-05T08:00+01:00", + "fin": "2022-09-05T09:59:59+01:00", "raison": None, }, { @@ -543,14 +467,14 @@ def ajouter_justificatifs(etud): }, { "etat": scu.EtatJustificatif.VALIDE, - "deb": "2022-09-03T10:00:00+01:00", - "fin": "2022-09-03T12:00+01:00", + "deb": "2022-09-05T10:00:00+01:00", + "fin": "2022-09-05T12:00+01:00", "raison": None, }, { "etat": scu.EtatJustificatif.NON_VALIDE, - "deb": "2022-09-03T14:00:00+01:00", - "fin": "2022-09-03T15:00+01:00", + "deb": "2022-09-05T14:00:00+01:00", + "fin": "2022-09-05T15:00+01:00", "raison": "Description", }, { @@ -581,14 +505,6 @@ def ajouter_justificatifs(etud): justi for justi in justificatifs if not isinstance(justi, Justificatif) ] == [], "La création des justificatifs de base n'est pas OK" - # Vérification de la gestion des erreurs - - test_assiduite = { - "etat": scu.EtatJustificatif.ATTENTE, - "deb": "2023-01-03T11:00:01+01:00", - "fin": "2023-01-03T12:00+01:00", - "raison": "Description", - } return justificatifs @@ -646,19 +562,19 @@ def verifier_filtrage_justificatifs(etud: Identite, justificatifs: list[Justific == 5 ), "Filtrage 'Toute Date' mauvais 2" - date = scu.localize_datetime("2022-09-03T08:00+01:00") + date = scu.localize_datetime("2022-09-05T08:00+01:00") assert ( 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") + date = scu.localize_datetime("2022-09-05T08: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") + date = scu.localize_datetime("2022-09-05T10:00+01:00") assert ( scass.filter_by_date(etud.justificatifs, Justificatif, date_deb=date).count() == 4 @@ -668,25 +584,25 @@ def verifier_filtrage_justificatifs(etud: Identite, justificatifs: list[Justific assert ( scass.filter_by_date(etud.justificatifs, Justificatif, date_fin=date).count() == 0 - ), "Filtrage 'Toute Date' mauvais 6" + ), "Filtrage 'date fin' mauvais 6" - date = scu.localize_datetime("2022-09-03T08:00+01:00") + date = scu.localize_datetime("2022-09-05T08:00+01:00") assert ( scass.filter_by_date(etud.justificatifs, Justificatif, date_fin=date).count() == 1 - ), "Filtrage 'date début' mauvais 7" + ), "Filtrage 'date fin' mauvais 7" - date = scu.localize_datetime("2022-09-03T10:00:01+01:00") + date = scu.localize_datetime("2022-09-05T10:00:01+01:00") assert ( scass.filter_by_date(etud.justificatifs, Justificatif, date_fin=date).count() == 2 - ), "Filtrage 'date début' mauvais 8" + ), "Filtrage 'date fin' mauvais 8" date = scu.localize_datetime("2023-01-03T12:00+01:00") assert ( scass.filter_by_date(etud.justificatifs, Justificatif, date_fin=date).count() == 5 - ), "Filtrage 'date début' mauvais 9" + ), "Filtrage 'date fin' mauvais 9" # Justifications des assiduites @@ -785,8 +701,8 @@ def ajouter_assiduites( obj_assiduites = [ { "etat": scu.EtatAssiduite.PRESENT, - "deb": "2022-09-03T08:00+01:00", - "fin": "2022-09-03T10:00+01:00", + "deb": "2022-09-05T08:00+01:00", + "fin": "2022-09-05T10:00+01:00", "moduleimpl": None, "desc": None, }, @@ -799,15 +715,15 @@ def ajouter_assiduites( }, { "etat": scu.EtatAssiduite.ABSENT, - "deb": "2022-09-03T10:00:01+01:00", - "fin": "2022-09-03T11:00+01:00", + "deb": "2022-09-05T10:00:01+01:00", + "fin": "2022-09-05T11:00+01:00", "moduleimpl": moduleimpls[0], "desc": None, }, { "etat": scu.EtatAssiduite.ABSENT, - "deb": "2022-09-03T14:00:00+01:00", - "fin": "2022-09-03T15:00+01:00", + "deb": "2022-09-05T14:00:00+01:00", + "fin": "2022-09-05T15:00+01:00", "moduleimpl": moduleimpls[1], "desc": "Description", }, @@ -877,6 +793,44 @@ def ajouter_assiduites( excp.args[0] == "Duplication: la période rentre en conflit avec une plage enregistrée" ) + + try: + test_assiduite2 = { + "etat": scu.EtatAssiduite.RETARD, + "deb": "2022-09-03T11:00:01+01:00", + "fin": "2022-09-03T12:00+01:00", + "moduleimpl": moduleimpls[3], + "desc": "Description", + } + Assiduite.create_assiduite( + etuds[0], + scu.is_iso_formated(test_assiduite2["deb"], True), + scu.is_iso_formated(test_assiduite2["fin"], True), + test_assiduite2["etat"], + test_assiduite2["moduleimpl"], + test_assiduite2["desc"], + ) + except ScoValueError as excp: + assert excp.args[0] == "La date de début n'est pas un jour travaillé" + try: + test_assiduite2 = { + "etat": scu.EtatAssiduite.RETARD, + "deb": "2022-09-02T11:00:01+01:00", + "fin": "2022-09-03T12:00+01:00", + "moduleimpl": moduleimpls[3], + "desc": "Description", + } + Assiduite.create_assiduite( + etuds[0], + scu.is_iso_formated(test_assiduite2["deb"], True), + scu.is_iso_formated(test_assiduite2["fin"], True), + test_assiduite2["etat"], + test_assiduite2["moduleimpl"], + test_assiduite2["desc"], + ) + except ScoValueError as excp: + assert excp.args[0] == "La date de fin n'est pas un jour travaillé" + try: Assiduite.create_assiduite( etud_faux, @@ -904,20 +858,6 @@ def verifier_comptage_et_filtrage_assiduites( mod11, mod12, mod21, mod22 = moduleimpls - # Vérification du comptage classique - comptage = scass.get_assiduites_stats(etu1.assiduites) - - assert comptage["compte"] == 6 + 1, "la métrique 'Comptage' n'est pas bien calculée" - assert ( - comptage["journee"] == 3 + 22 - ), "la métrique 'Journée' n'est pas bien calculée" - assert ( - comptage["demi"] == 4 + 43 - ), "la métrique 'Demi-Journée' 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 # Etat @@ -993,12 +933,12 @@ def verifier_comptage_et_filtrage_assiduites( 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") + date = scu.localize_datetime("2022-09-05T10:00+01:00") assert ( 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") + date = scu.localize_datetime("2022-09-05T16:00+01:00") assert ( scass.filter_by_date(etu2.assiduites, Assiduite, date_deb=date).count() == 4 ), "Filtrage 'Date début' mauvais 4" @@ -1010,17 +950,17 @@ def verifier_comptage_et_filtrage_assiduites( 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") + date = scu.localize_datetime("2022-09-05T10: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") + date = scu.localize_datetime("2022-09-05T10:00:01+01:00") assert ( 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") + date = scu.localize_datetime("2022-09-05T16:00+01:00") assert ( scass.filter_by_date(etu2.assiduites, Assiduite, date_fin=date).count() == 3 ), "Filtrage 'Date fin' mauvais 4" @@ -1112,3 +1052,310 @@ def _create_abs( db.session.add_all(abs_list) db.session.commit() + + +def _setup_fake_db( + dates_formsemestre: list[tuple[str, str]], + nb_modules: int = 0, + nb_etuds: int = 1, +) -> dict: + g_fake = sco_fake_gen.ScoFake(verbose=False) + + # Création d'une formation + formation_id = g_fake.create_formation() + ue_id = g_fake.create_ue( + formation_id=formation_id, acronyme="T1", titre="UE TEST 1" + ) + matiere_id = g_fake.create_matiere(ue_id=ue_id, titre="test matière") + + module_ids: list[int] = [ + g_fake.create_module( + matiere_id=matiere_id, + code=f"Mo{i}", + coefficient=1.0, + titre=f"test module{i}", + ) + for i in range(nb_modules) + ] + + # Création semestre + + formsemestre_ids: list[int] = [ + g_fake.create_formsemestre( + formation_id=formation_id, + semestre_id=1, + date_debut=deb, + date_fin=fin, + ) + for deb, fin in dates_formsemestre + ] + + formsemestres: list[FormSemestre] = list( + map(FormSemestre.get_formsemestre, formsemestre_ids) + ) + + # Création des modulesimpls (2 par semestre) + + moduleimpls: list[int] = [] + + for i in range(len(dates_formsemestre)): + for j in range(nb_modules): + mod, form = module_ids[j], formsemestres[i] + + moduleimpl_id: int = g_fake.create_moduleimpl( + module_id=mod, + formsemestre_id=form.id, + ) + + moduleimpls.append(ModuleImpl.query.filter_by(id=moduleimpl_id).first()) + + # Création de 3 étudiants + etud_0 = g_fake.create_etud(prenom="etud0") + etud_1 = g_fake.create_etud(prenom="etud1") + etud_2 = g_fake.create_etud(prenom="etud2") + etuds_dict = [etud_0, etud_1, etud_2] + + etud_dicts: list[dict] = [ + g_fake.create_etud(prenom=f"etud{i}") for i in range(nb_etuds) + ] + + etuds = [] + for etud in etuds_dict: + for form_id in formsemestre_ids: + g_fake.inscrit_etudiant(formsemestre_id=form_id, etud=etud) + etuds.append(Identite.query.filter_by(id=etud["etudid"]).first()) + # Etudiant faux + etud_faux_dict = g_fake.create_etud(prenom="etudfaux") + etud_faux = Identite.query.filter_by(id=etud_faux_dict["etudid"]).first() + + return { + "moduleimpls": moduleimpls, + "formsemestres": formsemestres, + "etuds": etuds, + "etud_faux": etud_faux, + } + + +def test_calcul_assiduites(test_client): + """Vérification du bon calcul des assiduités""" + + data: dict = _setup_fake_db([("01/12/2023", "31/12/2023")]) + + formsemestre: FormSemestre = data["formsemestres"][0] + etud: Identite = data["etuds"][0] + + """ + Exemple tuple: + ( + "12-04T08:00", # Date de début + "12-04T09:00", # Date de fin + scu.EtatAssiduite.ABSENT, # Etat + False # est_just + ) + """ + + assiduites: list[tuple] = [ + # Journée du 04/12 + ( + "12-04T08:00", + "12-04T10:00", + scu.EtatAssiduite.ABSENT, + False, + ), + ( + "12-04T10:15", + "12-04T12:15", + scu.EtatAssiduite.RETARD, + False, + ), + ( + "12-04T13:15", + "12-04T15:15", + scu.EtatAssiduite.PRESENT, + False, + ), + ( + "12-04T15:15", + "12-04T17:00", + scu.EtatAssiduite.ABSENT, + True, + ), + # 05/12 + ( + "12-05T08:00", + "12-05T09:00", + scu.EtatAssiduite.RETARD, + True, + ), + ( + "12-05T09:00", + "12-05T10:00", + scu.EtatAssiduite.PRESENT, + False, + ), + ( + "12-05T10:15", + "12-05T12:15", + scu.EtatAssiduite.PRESENT, + False, + ), + ( + "12-05T13:15", + "12-05T14:30", + scu.EtatAssiduite.ABSENT, + False, + ), + ( + "12-05T14:30", + "12-05T16:30", + scu.EtatAssiduite.RETARD, + False, + ), + ( + "12-05T16:30", + "12-05T17:00", + scu.EtatAssiduite.PRESENT, + False, + ), + # 06/12 + ( + "12-06T08:00", + "12-06T10:00", + scu.EtatAssiduite.PRESENT, + False, + ), + ( + "12-06T10:15", + "12-06T12:15", + scu.EtatAssiduite.RETARD, + False, + ), + ( + "12-06T13:15", + "12-06T13:45", + scu.EtatAssiduite.ABSENT, + True, + ), + ( + "12-06T13:45", + "12-06T15:00", + scu.EtatAssiduite.PRESENT, + False, + ), + ( + "12-06T15:00", + "12-06T17:00", + scu.EtatAssiduite.RETARD, + False, + ), + # 07/12 + ( + "12-07T08:00", + "12-07T08:30", + scu.EtatAssiduite.RETARD, + True, + ), + ( + "12-07T08:30", + "12-07T10:00", + scu.EtatAssiduite.PRESENT, + False, + ), + ( + "12-07T10:15", + "12-07T12:15", + scu.EtatAssiduite.ABSENT, + True, + ), + # 08/12 + ( + "12-08T08:00", + "12-08T10:00", + scu.EtatAssiduite.PRESENT, + False, + ), + ( + "12-08T10:15", + "12-08T12:15", + scu.EtatAssiduite.ABSENT, + False, + ), + ( + "12-08T13:15", + "12-08T14:15", + scu.EtatAssiduite.RETARD, + True, + ), + ( + "12-08T14:15", + "12-08T15:15", + scu.EtatAssiduite.PRESENT, + False, + ), + ( + "12-08T15:15", + "12-08T17:00", + scu.EtatAssiduite.ABSENT, + False, + ), + # 11/12 -> 15/12 + ( + "12-11T08:00", + "12-15T17:00", + scu.EtatAssiduite.ABSENT, + False, + ), + ] + + for ass in assiduites: + ass_obj = Assiduite.create_assiduite( + etud=etud, + date_debut=scu.is_iso_formated("2023-" + ass[0], True), + date_fin=scu.is_iso_formated("2023-" + ass[1], True), + etat=ass[2], + est_just=ass[3], + ) + db.session.add(ass_obj) + db.session.commit() + + calculator = scass.CountCalculator( + morning="08:00", noon="12:15", evening="17:00", nb_heures_par_jour=8 + ) + + calculator.compute_assiduites(etud.assiduites) + + result: dict = calculator.to_dict(only_total=False) + + # Résultat attendu : + # (les additions dans les absences corresponde à (compte_assiduite + compte_assiduite_longue)) + resultat_attendu: dict = { + "present": {"journee": 5, "demi": 8, "heure": 13.25, "compte": 9}, + "absent": { + "journee": 5 + 5, + "demi": 7 + 10, + "heure": 11.25 + 42, + "compte": 7 + 1, + }, + "absent_just": {"journee": 3, "demi": 3, "heure": 4.25, "compte": 3}, + "absent_non_just": { + "journee": 3 + 5, + "demi": 4 + 10, + "heure": 7 + 42, + "compte": 4 + 1, + }, + "retard": { + "journee": 5, + "demi": 7, + "heure": 10.5, + "compte": 7, + }, + "retard_just": {"journee": 3, "demi": 3, "heure": 2.5, "compte": 3}, + "retard_non_just": {"journee": 3, "demi": 4, "heure": 8.0, "compte": 4}, + "total": {"journee": 10, "demi": 19, "heure": 77.0, "compte": 24}, + } + + for key in resultat_attendu: + for key2 in resultat_attendu[key]: + assert ( + result[key][key2] == resultat_attendu[key][key2] + ), f"Le calcul [{key}][{key2}] est faux (attendu > {resultat_attendu[key][key2]} ≠ {result[key][key2]} < obtenu)" diff --git a/tools/fakedatabase/create_test_api_database.py b/tools/fakedatabase/create_test_api_database.py index cdbf6590b..79997e1f3 100644 --- a/tools/fakedatabase/create_test_api_database.py +++ b/tools/fakedatabase/create_test_api_database.py @@ -388,7 +388,9 @@ def ajouter_assiduites_justificatifs(formsemestre: FormSemestre): MODS.append(None) for etud in formsemestre.etuds: - base_date = datetime.datetime(2022, 9, random.randint(1, 30), 8, 0, 0) + base_date = datetime.datetime( + 2022, 9, [5, 12, 19, 26][random.randint(0, 3)], 8, 0, 0 + ) base_date = localize_datetime(base_date) for i in range(random.randint(1, 5)):