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)
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)

View File

@ -39,6 +39,7 @@ from app.scodoc.sco_utils import json_error
@api_web_bp.route("/assiduite/<int:assiduite_id>")
@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)

View File

@ -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

View File

@ -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],

View File

@ -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):

View File

@ -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([<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__(
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

View File

@ -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",
{

View File

@ -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<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:
"""
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() }}
{{ 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) }}
</div>
</div>
<div class="row">

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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/<assiduite_id:int>"""
# 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

View File

@ -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)"

View File

@ -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)):