Merge branch 'main96' of https://scodoc.org/git/iziram/ScoDoc into iziram-main96

This commit is contained in:
Emmanuel Viennet 2024-01-05 22:50:21 +01:00
commit 564d766087
15 changed files with 827 additions and 294 deletions

View File

@ -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) query = model_cls.query.filter_by(id=model_id)
if g.scodoc_dept and join_cls is not None: if g.scodoc_dept and join_cls is not None:
query = query.join(join_cls).filter_by(dept_id=g.scodoc_dept_id) 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) return unique.to_dict(format_api=True)

View File

@ -39,6 +39,7 @@ from app.scodoc.sco_utils import json_error
@api_web_bp.route("/assiduite/<int:assiduite_id>") @api_web_bp.route("/assiduite/<int:assiduite_id>")
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json
def assiduite(assiduite_id: int = None): def assiduite(assiduite_id: int = None):
"""Retourne un objet assiduité à partir de son id """Retourne un objet assiduité à partir de son id
@ -172,6 +173,7 @@ def count_assiduites(
404, 404,
message="étudiant inconnu", message="étudiant inconnu",
) )
g.scodoc_dept_id = etud.dept_id
# Les filtres qui seront appliqués au comptage (type, date, etudid...) # Les filtres qui seront appliqués au comptage (type, date, etudid...)
filtered: dict[str, object] = {} filtered: dict[str, object] = {}
@ -444,6 +446,8 @@ def count_assiduites_formsemestre(
if formsemestre is None: if formsemestre is None:
return json_error(404, "le paramètre 'formsemestre_id' n'existe pas") 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 # Récupération des étudiants du formsemestre
etuds = formsemestre.etuds.all() etuds = formsemestre.etuds.all()
etuds_id = [etud.id for etud in etuds] 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 # Récupération de l'assiduité à modifier
assiduite_unique: Assiduite = Assiduite.query.filter_by( assiduite_unique: Assiduite = Assiduite.query.filter_by(id=assiduite_id).first()
id=assiduite_id if assiduite_unique is None:
).first_or_404() return json_error(404, "Assiduité non existante")
# Récupération des valeurs à modifier # Récupération des valeurs à modifier
data = request.get_json(force=True) data = request.get_json(force=True)

View File

@ -154,7 +154,9 @@ def justificatifs_dept(dept_id: int = None, with_query: bool = False):
"""XXX TODO missing doc""" """XXX TODO missing doc"""
# Récupération du département et des étudiants du département # 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] etuds: list[int] = [etud.id for etud in dept.etudiants]
# Récupération des justificatifs des étudiants du département # Récupération des justificatifs des étudiants du département

View File

@ -119,15 +119,15 @@ def check_ics_regexp(form, field):
class ConfigAssiduitesForm(FlaskForm): class ConfigAssiduitesForm(FlaskForm):
"Formulaire paramétrage Module Assiduité" "Formulaire paramétrage Module Assiduité"
morning_time = TimeField( assi_morning_time = TimeField(
"Début de la journée" "Début de la journée"
) # TODO utiliser TextField + timepicker voir AjoutAssiOrJustForm ) # TODO utiliser TextField + timepicker voir AjoutAssiOrJustForm
lunch_time = TimeField( assi_lunch_time = TimeField(
"Heure de midi (date pivot entre matin et après-midi)" "Heure de midi (date pivot entre matin et après-midi)"
) # TODO ) # 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)", "Granularité de la timeline (temps en minutes)",
places=0, places=0,
validators=[check_tick_time], validators=[check_tick_time],

View File

@ -25,6 +25,7 @@ from app.scodoc.sco_utils import (
EtatJustificatif, EtatJustificatif,
localize_datetime, localize_datetime,
is_assiduites_module_forced, is_assiduites_module_forced,
NonWorkDays,
) )
@ -154,6 +155,33 @@ class Assiduite(ScoDocModel):
) )
if date_fin.tzinfo is None: if date_fin.tzinfo is None:
log(f"Warning: create_assiduite: date_fin without timezone ({date_fin})") 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 # Vérification de non duplication des périodes
assiduites: Query = etud.assiduites assiduites: Query = etud.assiduites
if is_period_conflicting(date_debut, date_fin, assiduites, Assiduite): if is_period_conflicting(date_debut, date_fin, assiduites, Assiduite):

View File

@ -15,59 +15,222 @@ from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.models import ScoDocSiteConfig
from flask import g
class CountCalculator: 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([<Assiduite>, <Assiduite>, <Assiduite>, <Assiduite>])
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__( def __init__(
self, self,
morning: time = time(8, 0), # TODO utiliser ScoDocSiteConfig morning: str = None,
noon: time = time(12, 0), noon: str = None,
after_noon: time = time(14, 00), evening: str = None,
evening: time = time(18, 0), nb_heures_par_jour: int = None,
skip_saturday: bool = True, # TODO préférence workdays
) -> None: ) -> None:
self.morning: time = morning # Transformation d'une heure "HH:MM" en time(h,m)
self.noon: time = noon STR_TIME = lambda x: time(*list(map(int, x.split(":"))))
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( self.morning: time = STR_TIME(
date.min, morning 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] = [] delta_total: timedelta = datetime.combine(
self.half_days: list[tuple[date, bool]] = [] # tuple -> (date, morning:bool) date.min, self.evening
self.hours: float = 0.0 ) - 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): def reset(self):
"""Remet à zero les compteurs""" """Remet à zero les compteurs"""
self.days = [] self.data = {
self.half_days = [] "total": {
self.hours = 0.0 "journee": [],
self.count = 0 "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""" """Ajoute une demi-journée dans le comptage"""
key: tuple[date, bool] = (day, is_morning) 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""" """Ajoute un jour dans le comptage"""
if day not in self.days: count_key: str = self.get_count_key(assi.etat, assi.est_just)
self.days.append(day) 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: def is_in_morning(self, period: tuple[datetime, datetime]) -> bool:
"""Vérifiée si la période donnée fait partie du matin """Vérifiée si la période donnée fait partie du matin
@ -90,7 +253,9 @@ class CountCalculator:
""" """
interval_evening: tuple[datetime, datetime] = ( 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)), 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)""" """Calcule les métriques sur une assiduité longue (plus d'un jour)"""
pointer_date: date = assi.date_debut.date() + timedelta(days=1) 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_debut.date(), assi)
self.add_day(assi.date_fin.date()) self.add_day(assi.date_fin.date(), assi)
start_period: tuple[datetime, datetime] = ( start_period: tuple[datetime, datetime] = (
assi.date_debut, assi.date_debut,
@ -123,58 +282,67 @@ class CountCalculator:
scu.localize_datetime(datetime.combine(assi.date_fin.date(), self.morning)), scu.localize_datetime(datetime.combine(assi.date_fin.date(), self.morning)),
assi.date_fin, assi.date_fin,
) )
hours = 0.0
for period in (start_period, finish_period): for period in (start_period, finish_period):
if self.is_in_evening(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): 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(): while pointer_date < assi.date_fin.date():
# TODO : Utiliser la préférence de département : workdays if pointer_date.weekday() not in self.non_work_days:
if pointer_date.weekday() < (6 - self.skip_saturday): self.add_day(pointer_date, assi)
self.add_day(pointer_date) self.add_half_day(pointer_date, assi)
self.add_half_day(pointer_date) self.add_half_day(pointer_date, assi, False)
self.add_half_day(pointer_date, False) self.add_hours(self.nb_heures_par_jour, assi)
self.hours += self.hour_per_day
hours += self.hour_per_day
pointer_date += timedelta(days=1) pointer_date += timedelta(days=1)
self.hours += finish_hours.total_seconds() / 3600 # Gestion des heures des dates de début et des dates de fin
self.hours += self.hour_per_day - (start_hours.total_seconds() / 3600) 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): def compute_assiduites(self, assiduites: Query | list):
"""Calcule les métriques pour la collection d'assiduité donnée""" """Calcule les métriques pour la collection d'assiduité donnée"""
assi: Assiduite assi: Assiduite
for assi in assiduites: 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 delta: timedelta = assi.date_fin - assi.date_debut
if delta.days > 0: if delta.days > 0:
self.compute_long_assiduite(assi) self.compute_long_assiduite(assi)
continue continue
period: tuple[datetime, datetime] = (assi.date_debut, assi.date_fin) period: tuple[datetime, datetime] = (assi.date_debut, assi.date_fin)
deb_date: date = assi.date_debut.date() deb_date: date = assi.date_debut.date()
if self.is_in_morning(period): 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): 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""" """Retourne les métriques sous la forme d'un dictionnaire"""
return { return self.data["total"] if only_total else self.data
"compte": self.count,
"journee": len(self.days),
"demi": len(self.half_days),
"heure": round(self.hours, 2),
}
def get_assiduites_stats( def get_assiduites_stats(
@ -211,55 +379,34 @@ def get_assiduites_stats(
metrics: list[str] = metric.split(",") metrics: list[str] = metric.split(",")
output: dict = {} output: dict = {}
calculator: CountCalculator = CountCalculator() calculator: CountCalculator = CountCalculator()
calculator.compute_assiduites(assiduites)
if filtered is None or "split" not in filtered: if filtered is None or "split" not in filtered:
calculator.compute_assiduites(assiduites) count: dict = calculator.to_dict(only_total=True)
count: dict = calculator.to_dict()
for key, val in count.items(): for key, val in count.items():
if key in metrics: if key in metrics:
output[key] = val output[key] = val
return output if output else count return output if output else count
# Récupération des états
etats: list[str] = ( etats: list[str] = (
filtered["etat"].split(",") filtered["etat"].split(",")
if "etat" in filtered if "etat" in filtered
else ["absent", "present", "retard"] 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: for etat in etats:
output[etat] = _count_assiduites_etat(etat, assiduites, calculator, metrics) if etat != "present":
if "est_just" not in filtered: output[etat] = count[etat]
output[etat]["justifie"] = _count_assiduites_etat( output[etat]["justifie"] = count[etat + "_just"]
etat, assiduites, calculator, metrics, justifie=True output[etat]["non_justifie"] = count[etat + "_non_just"]
) else:
output[etat] = count[etat]
output["total"] = count["total"]
return output 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: def filter_assiduites_by_etat(assiduites: Assiduite, etat: str) -> Query:
""" """
Filtrage d'une collection d'assiduites en fonction de leur état Filtrage d'une collection d'assiduites en fonction de leur état

View File

@ -655,6 +655,17 @@ class BasePreferences:
"explanation": "Durée d'un créneau en heure. Utilisé dans les pages de saisie", "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", "assi_etat_defaut",
{ {

View File

@ -238,6 +238,47 @@ class EtatJustificatif(int, BiDirectionalEnum):
return etat in cls._value2member_map_ 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<int> 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<int>
"""
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: def is_iso_formated(date: str, convert=False) -> bool or datetime.datetime or None:
""" """
Vérifie si une date est au format iso Vérifie si une date est au format iso

View File

@ -79,10 +79,10 @@ c'est à dire à la montre des étudiants.
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{{ wtf.form_errors(form, hiddens="only") }} {{ wtf.form_errors(form, hiddens="only") }}
{{ wtf.form_field(form.morning_time) }} {{ wtf.form_field(form.assi_morning_time) }}
{{ wtf.form_field(form.lunch_time) }} {{ wtf.form_field(form.assi_lunch_time) }}
{{ wtf.form_field(form.afternoon_time) }} {{ wtf.form_field(form.assi_afternoon_time) }}
{{ wtf.form_field(form.tick_time) }} {{ wtf.form_field(form.assi_tick_time) }}
</div> </div>
</div> </div>
<div class="row"> <div class="row">

View File

@ -348,8 +348,8 @@ def _get_dates_from_assi_form(
Ramène ok=True si ok. Ramène ok=True si ok.
Met des messages d'erreur dans le form. Met des messages d'erreur dans le form.
""" """
debut_jour = "00:00" debut_jour = ScoDocSiteConfig.get("assi_morning_time", "08:00")
fin_jour = "23:59:59" fin_jour = ScoDocSiteConfig.get("assi_afternoon_time", "17:00")
date_fin = None date_fin = None
# On commence par convertir individuellement tous les champs # On commence par convertir individuellement tous les champs
try: try:

View File

@ -337,15 +337,19 @@ def config_assiduites():
("edt_ics_user_path", "Chemin vers les ics des enseignants"), ("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 form.validate_on_submit():
if ScoDocSiteConfig.set("assi_morning_time", form.data["morning_time"]): # --- Options assiduités
flash("Heure du début de la journée enregistrée") for opt_name, message in assi_options:
if ScoDocSiteConfig.set("assi_lunch_time", form.data["lunch_time"]): if ScoDocSiteConfig.set(opt_name, form.data[opt_name]):
flash("Heure de midi enregistrée") flash(f"{message} 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")
# --- Calendriers emploi du temps # --- Calendriers emploi du temps
for opt_name, message in edt_options: for opt_name, message in edt_options:
if ScoDocSiteConfig.set(opt_name, form.data[opt_name]): if ScoDocSiteConfig.set(opt_name, form.data[opt_name]):
@ -354,19 +358,21 @@ def config_assiduites():
return redirect(url_for("scodoc.configuration")) return redirect(url_for("scodoc.configuration"))
if request.method == "GET": 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) "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) "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) "assi_afternoon_time", datetime.time(18, 0, 0)
) )
try: 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: except ValueError:
form.tick_time.data = 15.0 form.assi_tick_time.data = 15.0
ScoDocSiteConfig.set("assi_tick_time", 15.0) ScoDocSiteConfig.set("assi_tick_time", 15.0)
# --- Emplois du temps # --- Emplois du temps
for opt_name, _ in edt_options: for opt_name, _ in edt_options:

View File

@ -81,6 +81,9 @@ def GET(path: str, headers: dict = None, errmsg=None, dept=None):
timeout=SCO_TEST_API_TIMEOUT, timeout=SCO_TEST_API_TIMEOUT,
) )
if reply.status_code != 200: if reply.status_code != 200:
print("url", SCODOC_URL)
print("url", url)
print("reply", reply.text)
raise APIError( raise APIError(
errmsg or f"""erreur status={reply.status_code} !""", reply.json() 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: try:
GET(path=path, headers=headers) GET(path=path, headers=headers, dept=DEPT_ACRONYM)
# ^ Renvoi un 404 # ^ Renvoi un 404
except APIError as api_err: except APIError as api_err:
if err is not None: if err is not None:
@ -177,7 +180,7 @@ def check_failure_post(path: str, headers: dict, data: dict, err: str = None):
""" """
try: 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 # ^ Renvoie un 404
except APIError as api_err: except APIError as api_err:
if err is not None: if err is not None:

View File

@ -11,6 +11,7 @@ from types import NoneType
from tests.api.setup_test_api import ( from tests.api.setup_test_api import (
GET, GET,
POST_JSON, POST_JSON,
DEPT_ACRONYM,
APIError, APIError,
api_headers, api_headers,
api_admin_headers, api_admin_headers,
@ -45,7 +46,7 @@ ASSIDUITES_FIELDS = {
CREATE_FIELD = {"assiduite_id": int} CREATE_FIELD = {"assiduite_id": int}
BATCH_FIELD = {"errors": list, "success": list} 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 = [] TO_REMOVE = []
@ -81,7 +82,7 @@ def test_route_assiduite(api_headers):
"""test de la route /assiduite/<assiduite_id:int>""" """test de la route /assiduite/<assiduite_id:int>"""
# Bon fonctionnement == id connu # 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) check_fields(data, fields=ASSIDUITES_FIELDS)
# Mauvais Fonctionnement == id inconnu # Mauvais Fonctionnement == id inconnu
@ -97,13 +98,16 @@ def test_route_count_assiduites(api_headers):
# Bon fonctionnement # 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) check_fields(data, COUNT_FIELDS)
metrics = {"heure", "compte"} metrics = {"heure", "compte"}
data = GET( data = GET(
path=f"/assiduites/{ETUDID}/count/query?metric={','.join(metrics)}", path=f"/assiduites/{ETUDID}/count/query?metric={','.join(metrics)}",
headers=api_headers, headers=api_headers,
dept=DEPT_ACRONYM,
) )
assert set(data.keys()) == metrics assert set(data.keys()) == metrics
@ -118,12 +122,14 @@ def test_route_assiduites(api_headers):
# Bon fonctionnement # 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) assert isinstance(data, list)
for ass in data: for ass in data:
check_fields(ass, ASSIDUITES_FIELDS) 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) assert isinstance(data, list)
for ass in data: for ass in data:
check_fields(ass, ASSIDUITES_FIELDS) check_fields(ass, ASSIDUITES_FIELDS)
@ -138,13 +144,19 @@ def test_route_formsemestre_assiduites(api_headers):
# Bon fonctionnement # 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) assert isinstance(data, list)
for ass in data: for ass in data:
check_fields(ass, ASSIDUITES_FIELDS) check_fields(ass, ASSIDUITES_FIELDS)
data = GET( 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) assert isinstance(data, list)
for ass in data: for ass in data:
@ -169,13 +181,19 @@ def test_route_count_formsemestre_assiduites(api_headers):
# Bon fonctionnement # Bon fonctionnement
data = GET( 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) check_fields(data, COUNT_FIELDS)
metrics = {"heure", "compte"} metrics = {"heure", "compte"}
data = GET( data = GET(
path=f"/assiduites/formsemestre/{FORMSEMESTREID}/count/query?metric={','.join(metrics)}", path=f"/assiduites/formsemestre/{FORMSEMESTREID}/count/query?metric={','.join(metrics)}",
headers=api_headers, headers=api_headers,
dept=DEPT_ACRONYM,
) )
assert set(data.keys()) == metrics assert set(data.keys()) == metrics
@ -198,9 +216,11 @@ def test_route_create(api_admin_headers):
# -== Unique ==- # -== Unique ==-
# Bon fonctionnement # 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) check_fields(res, BATCH_FIELD)
assert len(res["success"]) == 1 assert len(res["success"]) == 1
@ -208,11 +228,14 @@ def test_route_create(api_admin_headers):
data = GET( data = GET(
path=f'/assiduite/{res["success"][0]["message"]["assiduite_id"]}', path=f'/assiduite/{res["success"][0]["message"]["assiduite_id"]}',
headers=api_admin_headers, headers=api_admin_headers,
dept=DEPT_ACRONYM,
) )
check_fields(data, fields=ASSIDUITES_FIELDS) check_fields(data, fields=ASSIDUITES_FIELDS)
data2 = create_data("absent", "02", MODULE, "desc") data2 = create_data("absent", "04", MODULE, "desc")
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) check_fields(res, BATCH_FIELD)
assert len(res["success"]) == 1 assert len(res["success"]) == 1
@ -221,7 +244,9 @@ def test_route_create(api_admin_headers):
# Mauvais fonctionnement # Mauvais fonctionnement
check_failure_post(f"/assiduite/{FAUX}/create", api_admin_headers, [data]) 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) check_fields(res, BATCH_FIELD)
assert len(res["errors"]) == 1 assert len(res["errors"]) == 1
assert ( assert (
@ -231,8 +256,9 @@ def test_route_create(api_admin_headers):
res = POST_JSON( res = POST_JSON(
f"/assiduite/{ETUDID}/create", f"/assiduite/{ETUDID}/create",
[create_data("absent", "03", FAUX)], [create_data("absent", "05", FAUX)],
api_admin_headers, api_admin_headers,
dept=DEPT_ACRONYM,
) )
check_fields(res, BATCH_FIELD) check_fields(res, BATCH_FIELD)
assert len(res["errors"]) == 1 assert len(res["errors"]) == 1
@ -245,10 +271,12 @@ def test_route_create(api_admin_headers):
etats = ["present", "absent", "retard"] etats = ["present", "absent", "retard"]
data = [ data = [
create_data(etats[d % 3], 10 + d, MODULE if d % 2 else None) 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) check_fields(res, BATCH_FIELD)
for dat in res["success"]: for dat in res["success"]:
check_fields(dat["message"], CREATE_FIELD) check_fields(dat["message"], CREATE_FIELD)
@ -257,15 +285,18 @@ def test_route_create(api_admin_headers):
# Mauvais Fonctionnement # Mauvais Fonctionnement
data2 = [ data2 = [
create_data("present", "01"), create_data("present", "03"),
create_data("present", "25", FAUX), create_data("present", "25", FAUX),
create_data("blabla", 26), create_data("blabla", 26),
create_data("absent", 32), 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) check_fields(res, BATCH_FIELD)
assert len(res["errors"]) == 4 assert len(res["errors"]) == 5
assert ( assert (
res["errors"][0]["message"] res["errors"][0]["message"]
@ -277,6 +308,7 @@ def test_route_create(api_admin_headers):
res["errors"][3]["message"] res["errors"][3]["message"]
== "param 'date_debut': format invalide, param 'date_fin': format invalide" == "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): def test_route_edit(api_admin_headers):
@ -285,11 +317,15 @@ def test_route_edit(api_admin_headers):
# Bon fonctionnement # Bon fonctionnement
data = {"etat": "retard", "moduleimpl_id": MODULE} 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} assert res == {"OK": True}
data["moduleimpl_id"] = None 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} assert res == {"OK": True}
# Mauvais fonctionnement # Mauvais fonctionnement
@ -311,13 +347,13 @@ def test_route_delete(api_admin_headers):
# Bon fonctionnement # Bon fonctionnement
data = TO_REMOVE[0] 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) check_fields(res, BATCH_FIELD)
for dat in res["success"]: for dat in res["success"]:
assert dat["message"] == "OK" assert dat["message"] == "OK"
# Mauvais fonctionnement # 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) check_fields(res, BATCH_FIELD)
assert len(res["errors"]) == 1 assert len(res["errors"]) == 1
@ -327,7 +363,7 @@ def test_route_delete(api_admin_headers):
data = TO_REMOVE[1:] 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) check_fields(res, BATCH_FIELD)
for dat in res["success"]: for dat in res["success"]:
assert dat["message"] == "OK" assert dat["message"] == "OK"
@ -340,7 +376,7 @@ def test_route_delete(api_admin_headers):
FAUX + 2, 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) check_fields(res, BATCH_FIELD)
assert len(res["errors"]) == 3 assert len(res["errors"]) == 3

View File

@ -50,106 +50,30 @@ def test_bi_directional_enum(test_client):
def test_general(test_client): def test_general(test_client):
"""tests général du modèle assiduite""" """tests général du modèle assiduite"""
g_fake = sco_fake_gen.ScoFake(verbose=False) data: dict = _setup_fake_db(
dates_formsemestre=[
# Création d'une formation (1) ("01/09/2022", "31/12/2022"),
("01/01/2023", "31/07/2023"),
formation_id = g_fake.create_formation() ("01/01/2024", "31/07/2024"),
ue_id = g_fake.create_ue( ],
formation_id=formation_id, acronyme="T1", titre="UE TEST 1" nb_modules=2,
nb_etuds=3,
) )
matiere_id = g_fake.create_matiere(ue_id=ue_id, titre="test matière") etuds, moduleimpls, etud_faux, formsemestres = (
module_id_1 = g_fake.create_module( data["etuds"],
matiere_id=matiere_id, code="Mo1", coefficient=1.0, titre="test module" 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() verif_migration_abs_assiduites()
ajouter_assiduites(etuds, moduleimpls, etud_faux) ajouter_assiduites(etuds, moduleimpls, etud_faux)
justificatifs: list[Justificatif] = ajouter_justificatifs(etuds[0]) justificatifs: list[Justificatif] = ajouter_justificatifs(etuds[0])
verifier_comptage_et_filtrage_assiduites( verifier_comptage_et_filtrage_assiduites(etuds, moduleimpls[:4], formsemestres)
etuds, moduleimpls, (formsemestre_1, formsemestre_2, formsemestre_3)
)
verifier_filtrage_justificatifs(etuds[0], justificatifs) 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_assiduites(etuds, moduleimpls)
editer_supprimer_justificatif(etuds[0]) editer_supprimer_justificatif(etuds[0])
@ -531,8 +455,8 @@ def ajouter_justificatifs(etud):
obj_justificatifs = [ obj_justificatifs = [
{ {
"etat": scu.EtatJustificatif.ATTENTE, "etat": scu.EtatJustificatif.ATTENTE,
"deb": "2022-09-03T08:00+01:00", "deb": "2022-09-05T08:00+01:00",
"fin": "2022-09-03T09:59:59+01:00", "fin": "2022-09-05T09:59:59+01:00",
"raison": None, "raison": None,
}, },
{ {
@ -543,14 +467,14 @@ def ajouter_justificatifs(etud):
}, },
{ {
"etat": scu.EtatJustificatif.VALIDE, "etat": scu.EtatJustificatif.VALIDE,
"deb": "2022-09-03T10:00:00+01:00", "deb": "2022-09-05T10:00:00+01:00",
"fin": "2022-09-03T12:00+01:00", "fin": "2022-09-05T12:00+01:00",
"raison": None, "raison": None,
}, },
{ {
"etat": scu.EtatJustificatif.NON_VALIDE, "etat": scu.EtatJustificatif.NON_VALIDE,
"deb": "2022-09-03T14:00:00+01:00", "deb": "2022-09-05T14:00:00+01:00",
"fin": "2022-09-03T15:00+01:00", "fin": "2022-09-05T15:00+01:00",
"raison": "Description", "raison": "Description",
}, },
{ {
@ -581,14 +505,6 @@ def ajouter_justificatifs(etud):
justi for justi in justificatifs if not isinstance(justi, Justificatif) justi for justi in justificatifs if not isinstance(justi, Justificatif)
] == [], "La création des justificatifs de base n'est pas OK" ] == [], "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 return justificatifs
@ -646,19 +562,19 @@ def verifier_filtrage_justificatifs(etud: Identite, justificatifs: list[Justific
== 5 == 5
), "Filtrage 'Toute Date' mauvais 2" ), "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 ( assert (
scass.filter_by_date(etud.justificatifs, Justificatif, date_deb=date).count() scass.filter_by_date(etud.justificatifs, Justificatif, date_deb=date).count()
== 5 == 5
), "Filtrage 'date début' mauvais 3" ), "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 ( assert (
scass.filter_by_date(etud.justificatifs, Justificatif, date_deb=date).count() scass.filter_by_date(etud.justificatifs, Justificatif, date_deb=date).count()
== 5 == 5
), "Filtrage 'date début' mauvais 4" ), "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 ( assert (
scass.filter_by_date(etud.justificatifs, Justificatif, date_deb=date).count() scass.filter_by_date(etud.justificatifs, Justificatif, date_deb=date).count()
== 4 == 4
@ -668,25 +584,25 @@ def verifier_filtrage_justificatifs(etud: Identite, justificatifs: list[Justific
assert ( assert (
scass.filter_by_date(etud.justificatifs, Justificatif, date_fin=date).count() scass.filter_by_date(etud.justificatifs, Justificatif, date_fin=date).count()
== 0 == 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 ( assert (
scass.filter_by_date(etud.justificatifs, Justificatif, date_fin=date).count() scass.filter_by_date(etud.justificatifs, Justificatif, date_fin=date).count()
== 1 == 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 ( assert (
scass.filter_by_date(etud.justificatifs, Justificatif, date_fin=date).count() scass.filter_by_date(etud.justificatifs, Justificatif, date_fin=date).count()
== 2 == 2
), "Filtrage 'date début' mauvais 8" ), "Filtrage 'date fin' mauvais 8"
date = scu.localize_datetime("2023-01-03T12:00+01:00") date = scu.localize_datetime("2023-01-03T12:00+01:00")
assert ( assert (
scass.filter_by_date(etud.justificatifs, Justificatif, date_fin=date).count() scass.filter_by_date(etud.justificatifs, Justificatif, date_fin=date).count()
== 5 == 5
), "Filtrage 'date début' mauvais 9" ), "Filtrage 'date fin' mauvais 9"
# Justifications des assiduites # Justifications des assiduites
@ -785,8 +701,8 @@ def ajouter_assiduites(
obj_assiduites = [ obj_assiduites = [
{ {
"etat": scu.EtatAssiduite.PRESENT, "etat": scu.EtatAssiduite.PRESENT,
"deb": "2022-09-03T08:00+01:00", "deb": "2022-09-05T08:00+01:00",
"fin": "2022-09-03T10:00+01:00", "fin": "2022-09-05T10:00+01:00",
"moduleimpl": None, "moduleimpl": None,
"desc": None, "desc": None,
}, },
@ -799,15 +715,15 @@ def ajouter_assiduites(
}, },
{ {
"etat": scu.EtatAssiduite.ABSENT, "etat": scu.EtatAssiduite.ABSENT,
"deb": "2022-09-03T10:00:01+01:00", "deb": "2022-09-05T10:00:01+01:00",
"fin": "2022-09-03T11:00+01:00", "fin": "2022-09-05T11:00+01:00",
"moduleimpl": moduleimpls[0], "moduleimpl": moduleimpls[0],
"desc": None, "desc": None,
}, },
{ {
"etat": scu.EtatAssiduite.ABSENT, "etat": scu.EtatAssiduite.ABSENT,
"deb": "2022-09-03T14:00:00+01:00", "deb": "2022-09-05T14:00:00+01:00",
"fin": "2022-09-03T15:00+01:00", "fin": "2022-09-05T15:00+01:00",
"moduleimpl": moduleimpls[1], "moduleimpl": moduleimpls[1],
"desc": "Description", "desc": "Description",
}, },
@ -877,6 +793,44 @@ def ajouter_assiduites(
excp.args[0] excp.args[0]
== "Duplication: la période rentre en conflit avec une plage enregistrée" == "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: try:
Assiduite.create_assiduite( Assiduite.create_assiduite(
etud_faux, etud_faux,
@ -904,20 +858,6 @@ def verifier_comptage_et_filtrage_assiduites(
mod11, mod12, mod21, mod22 = moduleimpls 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 # Vérification du filtrage classique
# Etat # Etat
@ -993,12 +933,12 @@ def verifier_comptage_et_filtrage_assiduites(
scass.filter_by_date(etu2.assiduites, Assiduite, date_deb=date).count() == 7 scass.filter_by_date(etu2.assiduites, Assiduite, date_deb=date).count() == 7
), "Filtrage 'Date début' mauvais 2" ), "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 ( assert (
scass.filter_by_date(etu2.assiduites, Assiduite, date_deb=date).count() == 7 scass.filter_by_date(etu2.assiduites, Assiduite, date_deb=date).count() == 7
), "Filtrage 'Date début' mauvais 3" ), "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 ( assert (
scass.filter_by_date(etu2.assiduites, Assiduite, date_deb=date).count() == 4 scass.filter_by_date(etu2.assiduites, Assiduite, date_deb=date).count() == 4
), "Filtrage 'Date début' mauvais 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 scass.filter_by_date(etu2.assiduites, Assiduite, date_fin=date).count() == 0
), "Filtrage 'Date fin' mauvais 1" ), "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 ( assert (
scass.filter_by_date(etu2.assiduites, Assiduite, date_fin=date).count() == 1 scass.filter_by_date(etu2.assiduites, Assiduite, date_fin=date).count() == 1
), "Filtrage 'Date fin' mauvais 2" ), "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 ( assert (
scass.filter_by_date(etu2.assiduites, Assiduite, date_fin=date).count() == 2 scass.filter_by_date(etu2.assiduites, Assiduite, date_fin=date).count() == 2
), "Filtrage 'Date fin' mauvais 3" ), "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 ( assert (
scass.filter_by_date(etu2.assiduites, Assiduite, date_fin=date).count() == 3 scass.filter_by_date(etu2.assiduites, Assiduite, date_fin=date).count() == 3
), "Filtrage 'Date fin' mauvais 4" ), "Filtrage 'Date fin' mauvais 4"
@ -1112,3 +1052,310 @@ def _create_abs(
db.session.add_all(abs_list) db.session.add_all(abs_list)
db.session.commit() 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)"

View File

@ -388,7 +388,9 @@ def ajouter_assiduites_justificatifs(formsemestre: FormSemestre):
MODS.append(None) MODS.append(None)
for etud in formsemestre.etuds: 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) base_date = localize_datetime(base_date)
for i in range(random.randint(1, 5)): for i in range(random.randint(1, 5)):