forked from ScoDoc/ScoDoc
Update opolka/ScoDoc from ScoDoc/ScoDoc #2
@ -126,6 +126,7 @@ class AjoutAssiOrJustForm(FlaskForm):
|
|||||||
|
|
||||||
class AjoutAssiduiteEtudForm(AjoutAssiOrJustForm):
|
class AjoutAssiduiteEtudForm(AjoutAssiOrJustForm):
|
||||||
"Formulaire de saisie d'une assiduité pour un étudiant"
|
"Formulaire de saisie d'une assiduité pour un étudiant"
|
||||||
|
|
||||||
description = TextAreaField(
|
description = TextAreaField(
|
||||||
"Description",
|
"Description",
|
||||||
render_kw={
|
render_kw={
|
||||||
@ -152,6 +153,7 @@ class AjoutAssiduiteEtudForm(AjoutAssiOrJustForm):
|
|||||||
|
|
||||||
class AjoutJustificatifEtudForm(AjoutAssiOrJustForm):
|
class AjoutJustificatifEtudForm(AjoutAssiOrJustForm):
|
||||||
"Formulaire de saisie d'un justificatif pour un étudiant"
|
"Formulaire de saisie d'un justificatif pour un étudiant"
|
||||||
|
|
||||||
raison = TextAreaField(
|
raison = TextAreaField(
|
||||||
"Raison",
|
"Raison",
|
||||||
render_kw={
|
render_kw={
|
||||||
@ -176,6 +178,12 @@ class AjoutJustificatifEtudForm(AjoutAssiOrJustForm):
|
|||||||
|
|
||||||
|
|
||||||
class ChoixDateForm(FlaskForm):
|
class ChoixDateForm(FlaskForm):
|
||||||
|
"""
|
||||||
|
Formulaire de choix de date
|
||||||
|
(utilisé par la page de choix de date
|
||||||
|
si la date courante n'est pas dans le semestre)
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"Init form, adding a filed for our error messages"
|
"Init form, adding a filed for our error messages"
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
@ -5,7 +5,6 @@ from datetime import datetime
|
|||||||
|
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_sqlalchemy.query import Query
|
from flask_sqlalchemy.query import Query
|
||||||
from sqlalchemy.exc import DataError
|
|
||||||
|
|
||||||
from app import db, log, g, set_sco_dept
|
from app import db, log, g, set_sco_dept
|
||||||
from app.models import (
|
from app.models import (
|
||||||
@ -89,6 +88,8 @@ class Assiduite(ScoDocModel):
|
|||||||
lazy="select",
|
lazy="select",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Argument "restrict" obligatoire car on override la fonction "to_dict" de ScoDocModel
|
||||||
|
# pylint: disable-next=unused-argument
|
||||||
def to_dict(self, format_api=True, restrict: bool | None = None) -> dict:
|
def to_dict(self, format_api=True, restrict: bool | None = None) -> dict:
|
||||||
"""Retourne la représentation json de l'assiduité
|
"""Retourne la représentation json de l'assiduité
|
||||||
restrict n'est pas utilisé ici.
|
restrict n'est pas utilisé ici.
|
||||||
@ -307,6 +308,9 @@ class Assiduite(ScoDocModel):
|
|||||||
|
|
||||||
def supprime(self):
|
def supprime(self):
|
||||||
"Supprime l'assiduité. Log et commit."
|
"Supprime l'assiduité. Log et commit."
|
||||||
|
|
||||||
|
# Obligatoire car import circulaire sinon
|
||||||
|
# pylint: disable-next=import-outside-toplevel
|
||||||
from app.scodoc import sco_assiduites as scass
|
from app.scodoc import sco_assiduites as scass
|
||||||
|
|
||||||
if g.scodoc_dept is None and self.etudiant.dept_id is not None:
|
if g.scodoc_dept is None and self.etudiant.dept_id is not None:
|
||||||
@ -356,7 +360,7 @@ class Assiduite(ScoDocModel):
|
|||||||
|
|
||||||
date: str = self.entry_date.strftime("%d/%m/%Y à %H:%M")
|
date: str = self.entry_date.strftime("%d/%m/%Y à %H:%M")
|
||||||
utilisateur: str = ""
|
utilisateur: str = ""
|
||||||
if self.user != None:
|
if self.user is not None:
|
||||||
self.user: User
|
self.user: User
|
||||||
utilisateur = f"par {self.user.get_prenomnom()}"
|
utilisateur = f"par {self.user.get_prenomnom()}"
|
||||||
|
|
||||||
@ -515,6 +519,8 @@ class Justificatif(ScoDocModel):
|
|||||||
def create_justificatif(
|
def create_justificatif(
|
||||||
cls,
|
cls,
|
||||||
etudiant: Identite,
|
etudiant: Identite,
|
||||||
|
# On a besoin des arguments mais on utilise "locals" pour les récupérer
|
||||||
|
# pylint: disable=unused-argument
|
||||||
date_debut: datetime,
|
date_debut: datetime,
|
||||||
date_fin: datetime,
|
date_fin: datetime,
|
||||||
etat: EtatJustificatif,
|
etat: EtatJustificatif,
|
||||||
@ -538,8 +544,10 @@ class Justificatif(ScoDocModel):
|
|||||||
|
|
||||||
def supprime(self):
|
def supprime(self):
|
||||||
"Supprime le justificatif. Log et commit."
|
"Supprime le justificatif. Log et commit."
|
||||||
|
|
||||||
|
# Obligatoire car import circulaire sinon
|
||||||
|
# pylint: disable-next=import-outside-toplevel
|
||||||
from app.scodoc import sco_assiduites as scass
|
from app.scodoc import sco_assiduites as scass
|
||||||
from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
|
|
||||||
|
|
||||||
# Récupération de l'archive du justificatif
|
# Récupération de l'archive du justificatif
|
||||||
archive_name: str = self.fichier
|
archive_name: str = self.fichier
|
||||||
|
@ -20,8 +20,11 @@ class Trace:
|
|||||||
|
|
||||||
Role des fichiers traces :
|
Role des fichiers traces :
|
||||||
- Sauvegarder la date de dépôt du fichier
|
- Sauvegarder la date de dépôt du fichier
|
||||||
- Sauvegarder la date de suppression du fichier (dans le cas de plusieurs fichiers pour un même justif)
|
- Sauvegarder la date de suppression du fichier
|
||||||
- Sauvegarder l'user_id de l'utilisateur ayant déposé le fichier (=> permet de montrer les fichiers qu'aux personnes qui l'on déposé / qui ont le rôle AssiJustifView)
|
(dans le cas de plusieurs fichiers pour un même justif)
|
||||||
|
- Sauvegarder l'user_id de l'utilisateur ayant déposé le fichier
|
||||||
|
(=> permet de montrer les fichiers qu'aux personnes
|
||||||
|
qui l'on déposé / qui ont le rôle AssiJustifView)
|
||||||
|
|
||||||
_trace.csv :
|
_trace.csv :
|
||||||
nom_fichier_srv,datetime_depot,datetime_suppr,user_id
|
nom_fichier_srv,datetime_depot,datetime_suppr,user_id
|
||||||
|
@ -21,6 +21,7 @@ from app.models.assiduites import Assiduite, Justificatif, compute_assiduites_ju
|
|||||||
from app.scodoc import sco_formsemestre_inscriptions
|
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_compute_moy
|
||||||
from app.scodoc import sco_etud
|
from app.scodoc import sco_etud
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
|
|
||||||
@ -37,21 +38,34 @@ class CountCalculator:
|
|||||||
------------
|
------------
|
||||||
1. Initialisation : La classe peut être initialisée avec des horaires personnalisés
|
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.
|
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`.
|
Si non spécifiés, les valeurs par défaut seront
|
||||||
|
chargées depuis la configuration `ScoDocSiteConfig`.
|
||||||
Exemple d'initialisation :
|
Exemple d'initialisation :
|
||||||
calculator = CountCalculator(morning="08:00", noon="13:00", evening="18:00", nb_heures_par_jour=8)
|
calculator = CountCalculator(
|
||||||
|
morning="08:00",
|
||||||
|
noon="13:00",
|
||||||
|
evening="18:00",
|
||||||
|
nb_heures_par_jour=8
|
||||||
|
)
|
||||||
|
|
||||||
2. Ajout d'assiduités :
|
2. Ajout d'assiduités :
|
||||||
Exemple d'ajout d'assiduité :
|
Exemple d'ajout d'assiduité :
|
||||||
- calculator.compute_assiduites(etudiant.assiduites)
|
- calculator.compute_assiduites(etudiant.assiduites)
|
||||||
- calculator.compute_assiduites([<Assiduite>, <Assiduite>, <Assiduite>, <Assiduite>])
|
- 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 :
|
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.
|
le nombre total de jours, de demi-journées et d'heures calculées.
|
||||||
Exemple d'accès aux métriques :
|
Exemple d'accès aux métriques :
|
||||||
metrics = calculator.to_dict()
|
metrics = calculator.to_dict()
|
||||||
|
|
||||||
4.Réinitialisation du comptage: Si besoin on peut réinitialisé le compteur sans perdre la configuration
|
4.Réinitialisation du comptage: Si besoin on peut réinitialiser
|
||||||
|
le compteur sans perdre la configuration
|
||||||
(horaires personnalisés)
|
(horaires personnalisés)
|
||||||
Exemple de réinitialisation :
|
Exemple de réinitialisation :
|
||||||
calculator.reset()
|
calculator.reset()
|
||||||
@ -61,8 +75,10 @@ class CountCalculator:
|
|||||||
- reset() : Réinitialise les compteurs de la classe.
|
- reset() : Réinitialise les compteurs de la classe.
|
||||||
- add_half_day(day: date, is_morning: bool) : Ajoute une demi-journée au comptage.
|
- 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.
|
- 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_long_assiduite(assi: Assiduite) : Traite les assiduités
|
||||||
- compute_assiduites(assiduites: Query | list) : Calcule les métriques pour une collection d'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.
|
- to_dict() : Retourne les métriques sous forme de dictionnaire.
|
||||||
|
|
||||||
Notes :
|
Notes :
|
||||||
@ -85,27 +101,20 @@ class CountCalculator:
|
|||||||
evening: str = None,
|
evening: str = None,
|
||||||
nb_heures_par_jour: int = None,
|
nb_heures_par_jour: int = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
# Transformation d'une heure "HH:MM" en time(h,m)
|
self.morning: time = str_to_time(
|
||||||
STR_TIME = lambda x: time(*list(map(int, x.split(":"))))
|
|
||||||
|
|
||||||
self.morning: time = STR_TIME(
|
|
||||||
morning if morning else ScoDocSiteConfig.get("assi_morning_time", "08:00")
|
morning if morning else ScoDocSiteConfig.get("assi_morning_time", "08:00")
|
||||||
)
|
)
|
||||||
# Date pivot pour déterminer les demi-journées
|
# Date pivot pour déterminer les demi-journées
|
||||||
self.noon: time = STR_TIME(
|
self.noon: time = str_to_time(
|
||||||
noon if noon else ScoDocSiteConfig.get("assi_lunch_time", "13:00")
|
noon if noon else ScoDocSiteConfig.get("assi_lunch_time", "13:00")
|
||||||
)
|
)
|
||||||
self.evening: time = STR_TIME(
|
self.evening: time = str_to_time(
|
||||||
evening if evening else ScoDocSiteConfig.get("assi_afternoon_time", "18:00")
|
evening if evening else ScoDocSiteConfig.get("assi_afternoon_time", "18:00")
|
||||||
)
|
)
|
||||||
|
|
||||||
self.non_work_days: list[scu.NonWorkDays] = (
|
self.non_work_days: list[
|
||||||
scu.NonWorkDays.get_all_non_work_days(dept_id=g.scodoc_dept_id)
|
scu.NonWorkDays
|
||||||
)
|
] = scu.NonWorkDays.get_all_non_work_days(dept_id=g.scodoc_dept_id)
|
||||||
|
|
||||||
delta_total: timedelta = datetime.combine(
|
|
||||||
date.min, self.evening
|
|
||||||
) - datetime.combine(date.min, self.morning)
|
|
||||||
|
|
||||||
# Sera utilisé pour les assiduités longues (> 1 journée)
|
# Sera utilisé pour les assiduités longues (> 1 journée)
|
||||||
self.nb_heures_par_jour = (
|
self.nb_heures_par_jour = (
|
||||||
@ -340,17 +349,27 @@ class CountCalculator:
|
|||||||
|
|
||||||
def setup_data(self):
|
def setup_data(self):
|
||||||
"""Met en forme les données
|
"""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]))
|
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:
|
for value in self.data.values():
|
||||||
self.data[key]["journee"] = len(self.data[key]["journee"])
|
value["journee"] = len(value["journee"])
|
||||||
self.data[key]["demi"] = len(self.data[key]["demi"])
|
value["demi"] = len(value["demi"])
|
||||||
|
|
||||||
def to_dict(self, only_total: bool = True) -> dict[str, int | float]:
|
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 self.data["total"] if only_total else self.data
|
return self.data["total"] if only_total else self.data
|
||||||
|
|
||||||
|
|
||||||
|
def str_to_time(time_str: str) -> time:
|
||||||
|
"""Convertit une chaîne de caractères représentant une heure en objet time
|
||||||
|
exemples :
|
||||||
|
- "08:00" -> time(8, 0)
|
||||||
|
- "18:00:00" -> time(18, 0, 0)
|
||||||
|
"""
|
||||||
|
return time(*list(map(int, time_str.split(":"))))
|
||||||
|
|
||||||
|
|
||||||
def get_assiduites_stats(
|
def get_assiduites_stats(
|
||||||
assiduites: Query, metric: str = "all", filtered: dict[str, object] = None
|
assiduites: Query, metric: str = "all", filtered: dict[str, object] = None
|
||||||
) -> dict[str, int | float]:
|
) -> dict[str, int | float]:
|
||||||
@ -756,7 +775,6 @@ def invalidate_assiduites_etud_date(etudid: int, the_date: datetime):
|
|||||||
pour cet étudiant et cette date.
|
pour cet étudiant et cette date.
|
||||||
Invalide cache absence et caches semestre
|
Invalide cache absence et caches semestre
|
||||||
"""
|
"""
|
||||||
from app.scodoc import sco_compute_moy
|
|
||||||
|
|
||||||
# Semestres a cette date:
|
# Semestres a cette date:
|
||||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)
|
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)
|
||||||
@ -818,4 +836,4 @@ def simple_invalidate_cache(obj: dict, etudid: str | int = None):
|
|||||||
pattern=f"tableau-etud-{etudid}*"
|
pattern=f"tableau-etud-{etudid}*"
|
||||||
)
|
)
|
||||||
# Invalide les tableaux "bilan dept"
|
# Invalide les tableaux "bilan dept"
|
||||||
sco_cache.RequeteTableauAssiduiteCache.delete_pattern(pattern=f"tableau-dept*")
|
sco_cache.RequeteTableauAssiduiteCache.delete_pattern(pattern="tableau-dept*")
|
||||||
|
@ -130,7 +130,8 @@ def print_progress_bar(
|
|||||||
decimals - Optional : nombres de chiffres après la virgule (Int)
|
decimals - Optional : nombres de chiffres après la virgule (Int)
|
||||||
length - Optional : taille de la barre en nombre de caractères (Int)
|
length - Optional : taille de la barre en nombre de caractères (Int)
|
||||||
fill - Optional : charactère de remplissange de la barre (Str)
|
fill - Optional : charactère de remplissange de la barre (Str)
|
||||||
autosize - Optional : Choisir automatiquement la taille de la barre en fonction du terminal (Bool)
|
autosize - Optional : Choisir automatiquement la taille de la barre
|
||||||
|
en fonction du terminal (Bool)
|
||||||
"""
|
"""
|
||||||
percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total)))
|
percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total)))
|
||||||
color = TerminalColor.RED
|
color = TerminalColor.RED
|
||||||
@ -174,11 +175,15 @@ class BiDirectionalEnum(Enum):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def contains(cls, attr: str):
|
def contains(cls, attr: str):
|
||||||
"""Vérifie sur un attribut existe dans l'enum"""
|
"""Vérifie sur un attribut existe dans l'enum"""
|
||||||
|
|
||||||
|
# Existe dans la classe parent de Enum (EnumType)
|
||||||
|
# pylint: disable-next=no-member
|
||||||
return attr.upper() in cls._member_names_
|
return attr.upper() in cls._member_names_
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def all(cls, keys=True):
|
def all(cls, keys=True):
|
||||||
"""Retourne toutes les clés de l'enum"""
|
"""Retourne toutes les clés de l'enum"""
|
||||||
|
# pylint: disable-next=no-member
|
||||||
return cls._member_names_ if keys else list(cls._value2member_map_.keys())
|
return cls._member_names_ if keys else list(cls._value2member_map_.keys())
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -207,6 +212,9 @@ class EtatAssiduite(int, BiDirectionalEnum):
|
|||||||
ABSENT = 2
|
ABSENT = 2
|
||||||
|
|
||||||
def version_lisible(self) -> str:
|
def version_lisible(self) -> str:
|
||||||
|
"""Retourne une version lisible des états d'assiduités
|
||||||
|
Est utilisé pour les vues.
|
||||||
|
"""
|
||||||
return {
|
return {
|
||||||
EtatAssiduite.PRESENT: "Présence",
|
EtatAssiduite.PRESENT: "Présence",
|
||||||
EtatAssiduite.ABSENT: "Absence",
|
EtatAssiduite.ABSENT: "Absence",
|
||||||
@ -225,6 +233,9 @@ class EtatJustificatif(int, BiDirectionalEnum):
|
|||||||
MODIFIE = 3
|
MODIFIE = 3
|
||||||
|
|
||||||
def version_lisible(self) -> str:
|
def version_lisible(self) -> str:
|
||||||
|
"""Retourne une version lisible des états de justificatifs
|
||||||
|
Est utilisé pour les vues.
|
||||||
|
"""
|
||||||
return {
|
return {
|
||||||
EtatJustificatif.VALIDE: "valide",
|
EtatJustificatif.VALIDE: "valide",
|
||||||
EtatJustificatif.ATTENTE: "soumis",
|
EtatJustificatif.ATTENTE: "soumis",
|
||||||
@ -254,11 +265,13 @@ class NonWorkDays(int, BiDirectionalEnum):
|
|||||||
cls, formsemestre_id: int = None, dept_id: int = None
|
cls, formsemestre_id: int = None, dept_id: int = None
|
||||||
) -> list["NonWorkDays"]:
|
) -> list["NonWorkDays"]:
|
||||||
"""
|
"""
|
||||||
get_all_non_work_days Récupère la liste des non workdays (str) depuis les préférences
|
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
|
puis renvoie une liste BiDirectionnalEnum<int> NonWorkDays
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
non_work_days : list[NonWorkDays] = NonWorkDays.get_all_non_work_days(dept_id=g.scodoc_dept_id)
|
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:
|
if datetime.datetime.now().weekday() in non_work_days:
|
||||||
print("Aujourd'hui est un jour non travaillé")
|
print("Aujourd'hui est un jour non travaillé")
|
||||||
|
|
||||||
@ -269,6 +282,8 @@ class NonWorkDays(int, BiDirectionalEnum):
|
|||||||
Returns:
|
Returns:
|
||||||
list[NonWorkDays]: La liste des NonWorkDays en version BiDirectionnalEnum<int>
|
list[NonWorkDays]: La liste des NonWorkDays en version BiDirectionnalEnum<int>
|
||||||
"""
|
"""
|
||||||
|
# Import circulaire
|
||||||
|
# pylint: disable=import-outside-toplevel
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
|
"""
|
||||||
|
Gestion des listes d'assiduités et justificatifs
|
||||||
|
(affichage, pagination, filtrage, options d'affichage, tableaux)
|
||||||
|
"""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from flask import url_for
|
from flask import url_for
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_sqlalchemy.query import Query
|
from flask_sqlalchemy.query import Query
|
||||||
from sqlalchemy import desc, literal, literal_column, union, asc
|
from sqlalchemy import desc, literal, union, asc
|
||||||
|
|
||||||
from app import db, g
|
from app import db, g
|
||||||
from app.auth.models import User
|
from app.auth.models import User
|
||||||
@ -34,9 +39,11 @@ class Pagination:
|
|||||||
On peut ensuite récupérer les éléments de la page courante avec la méthode `items()`
|
On peut ensuite récupérer les éléments de la page courante avec la méthode `items()`
|
||||||
|
|
||||||
Cette classe ne permet pas de changer de page.
|
Cette classe ne permet pas de changer de page.
|
||||||
(Pour cela, il faut créer une nouvelle instance, avec la collection originelle et la nouvelle page)
|
(Pour cela, il faut créer une nouvelle instance,
|
||||||
|
avec la collection originelle et la nouvelle page)
|
||||||
|
|
||||||
l'intéret est de ne pas garder en mémoire toute la collection, mais seulement la page courante
|
l'intéret est de ne pas garder en mémoire toute la collection,
|
||||||
|
mais seulement la page courante
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -45,9 +52,11 @@ class Pagination:
|
|||||||
__init__ Instancie un nouvel objet Pagination
|
__init__ Instancie un nouvel objet Pagination
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
collection (list): La collection à paginer. Il s'agit par exemple d'une requête
|
collection (list): La collection à paginer.
|
||||||
|
Il s'agit par exemple d'une requête
|
||||||
page (int, optional): le numéro de la page à voir. Defaults to 1.
|
page (int, optional): le numéro de la page à voir. Defaults to 1.
|
||||||
per_page (int, optional): le nombre d'éléments par page. Defaults to -1. (-1 = pas de pagination/tout afficher)
|
per_page (int, optional): le nombre d'éléments par page.
|
||||||
|
Defaults to -1. (-1 = pas de pagination/tout afficher)
|
||||||
"""
|
"""
|
||||||
# par défaut le total des pages est 1 (même si la collection est vide)
|
# par défaut le total des pages est 1 (même si la collection est vide)
|
||||||
self.total_pages = 1
|
self.total_pages = 1
|
||||||
@ -231,15 +240,17 @@ class ListeAssiJusti(tb.Table):
|
|||||||
attributs `page` et `NB_PAR_PAGE` de la classe `ListeAssiJusti`.
|
attributs `page` et `NB_PAR_PAGE` de la classe `ListeAssiJusti`.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
collection (list): La collection à paginer. Il s'agit par exemple d'une requête qui a déjà
|
collection (list): La collection à paginer.
|
||||||
|
Il s'agit par exemple d'une requête qui a déjà
|
||||||
été construite et qui est prête à être exécutée.
|
été construite et qui est prête à être exécutée.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Pagination: Un objet Pagination qui encapsule les résultats de la requête paginée.
|
Pagination: Un objet Pagination qui encapsule les résultats de
|
||||||
|
la requête paginée.
|
||||||
|
|
||||||
Note:
|
Note:
|
||||||
Cette méthode ne modifie pas la collection originelle; elle renvoie plutôt un nouvel
|
Cette méthode ne modifie pas la collection originelle;
|
||||||
objet qui contient les résultats paginés.
|
elle renvoie plutôt un nouvel objet qui contient les résultats paginés.
|
||||||
"""
|
"""
|
||||||
return Pagination(
|
return Pagination(
|
||||||
collection,
|
collection,
|
||||||
@ -251,29 +262,35 @@ class ListeAssiJusti(tb.Table):
|
|||||||
"""
|
"""
|
||||||
Combine les requêtes d'assiduités et de justificatifs en une seule requête.
|
Combine les requêtes d'assiduités et de justificatifs en une seule requête.
|
||||||
|
|
||||||
Cette fonction prend en entrée deux requêtes optionnelles, une pour les assiduités
|
Cette fonction prend en entrée deux requêtes optionnelles,
|
||||||
et une pour les justificatifs, et renvoie une requête combinée qui sélectionne
|
une pour les assiduités et une pour les justificatifs,
|
||||||
un ensemble spécifique de colonnes pour chaque type d'objet.
|
et renvoie une requête combinée qui sélectionne un ensemble
|
||||||
|
spécifique de colonnes pour chaque type d'objet.
|
||||||
|
|
||||||
Les colonnes sélectionnées sont:
|
Les colonnes sélectionnées sont:
|
||||||
- obj_id: l'identifiant de l'objet (assiduite_id pour les assiduités, justif_id pour les justificatifs)
|
- obj_id: l'identifiant de l'objet
|
||||||
|
(assiduite_id pour les assiduités, justif_id pour les justificatifs)
|
||||||
- etudid: l'identifiant de l'étudiant
|
- etudid: l'identifiant de l'étudiant
|
||||||
- entry_date: la date de saisie de l'objet
|
- entry_date: la date de saisie de l'objet
|
||||||
- date_debut: la date de début de l'objet
|
- date_debut: la date de début de l'objet
|
||||||
- date_fin: la date de fin de l'objet
|
- date_fin: la date de fin de l'objet
|
||||||
- etat: l'état de l'objet
|
- etat: l'état de l'objet
|
||||||
- type: le type de l'objet ("assiduite" pour les assiduités, "justificatif" pour les justificatifs)
|
- type: le type de l'objet
|
||||||
|
("assiduite" pour les assiduités, "justificatif" pour les justificatifs)
|
||||||
- est_just : si l'assiduité est justifié (booléen) None pour les justificatifs
|
- est_just : si l'assiduité est justifié (booléen) None pour les justificatifs
|
||||||
- user_id : l'identifiant de l'utilisateur qui a signalé l'assiduité ou le justificatif
|
- user_id : l'identifiant de l'utilisateur qui a
|
||||||
|
signalé l'assiduité ou le justificatif
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
query_assiduite (sqlalchemy.orm.Query, optional): Une requête SQLAlchemy
|
query_assiduite (sqlalchemy.orm.Query, optional): Une requête SQLAlchemy
|
||||||
pour les assiduités.
|
pour les assiduités.
|
||||||
Si None (default), aucune assiduité ne sera incluse dans la requête combinée.
|
Si None (default), aucune assiduité ne sera incluse
|
||||||
|
dans la requête combinée.
|
||||||
|
|
||||||
query_justificatif (sqlalchemy.orm.Query, optional): Une requête SQLAlchemy
|
query_justificatif (sqlalchemy.orm.Query, optional): Une requête SQLAlchemy
|
||||||
pour les justificatifs.
|
pour les justificatifs.
|
||||||
Si None (default), aucun justificatif ne sera inclus dans la requête combinée.
|
Si None (default), aucun justificatif ne sera
|
||||||
|
inclus dans la requête combinée.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
sqlalchemy.orm.Query: Une requête combinée qui peut être exécutée pour
|
sqlalchemy.orm.Query: Une requête combinée qui peut être exécutée pour
|
||||||
@ -618,10 +635,15 @@ class AssiFiltre:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
type_obj (int, optional): type d'objet (0:Tout, 1: Assi, 2:Justi). Defaults to 0.
|
type_obj (int, optional): type d'objet (0:Tout, 1: Assi, 2:Justi). Defaults to 0.
|
||||||
entry_date (tuple[int, datetime], optional): (0: egal, 1: avant, 2: après) + datetime(avec TZ). Defaults to None.
|
entry_date (tuple[int, datetime], optional):
|
||||||
date_debut (tuple[int, datetime], optional): (0: egal, 1: avant, 2: après) + datetime(avec TZ). Defaults to None.
|
(0: egal, 1: avant, 2: après) + datetime(avec TZ). Defaults to None.
|
||||||
date_fin (tuple[int, datetime], optional): (0: egal, 1: avant, 2: après) + datetime(avec TZ). Defaults to None.
|
date_debut (tuple[int, datetime], optional):
|
||||||
etats (list[int | EtatJustificatif | EtatAssiduite], optional): liste d'états valides (int | EtatJustificatif | EtatAssiduite). Defaults to None.
|
(0: egal, 1: avant, 2: après) + datetime(avec TZ). Defaults to None.
|
||||||
|
date_fin (tuple[int, datetime], optional):
|
||||||
|
(0: egal, 1: avant, 2: après) + datetime(avec TZ). Defaults to None.
|
||||||
|
etats (list[int | EtatJustificatif | EtatAssiduite], optional):
|
||||||
|
liste d'états valides (int | EtatJustificatif | EtatAssiduite).
|
||||||
|
Defaults to None.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.filtres = {"type_obj": type_obj}
|
self.filtres = {"type_obj": type_obj}
|
||||||
@ -753,6 +775,10 @@ class AssiJustifData:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_etudiants(*etudiants: Identite) -> "AssiJustifData":
|
def from_etudiants(*etudiants: Identite) -> "AssiJustifData":
|
||||||
|
"""
|
||||||
|
Génère un object AssiJustifData à partir d'une liste d'étudiants
|
||||||
|
(Récupère les assiduités et justificatifs des étudiants)
|
||||||
|
"""
|
||||||
data = AssiJustifData()
|
data = AssiJustifData()
|
||||||
data.assiduites_query = Assiduite.query.filter(
|
data.assiduites_query = Assiduite.query.filter(
|
||||||
Assiduite.etudid.in_([e.etudid for e in etudiants])
|
Assiduite.etudid.in_([e.etudid for e in etudiants])
|
||||||
@ -764,4 +790,5 @@ class AssiJustifData:
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
def get(self) -> tuple[Query, Query]:
|
def get(self) -> tuple[Query, Query]:
|
||||||
|
"Renvoi les requêtes d'assiduités et justificatifs"
|
||||||
return self.assiduites_query, self.justificatifs_query
|
return self.assiduites_query, self.justificatifs_query
|
||||||
|
@ -37,7 +37,7 @@ class TableAssi(tb.Table):
|
|||||||
convert_values=False,
|
convert_values=False,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
self.rows: list["RowEtud"] = [] # juste pour que VSCode nous aide sur .rows
|
self.rows: list["RowAssi"] = [] # juste pour que VSCode nous aide sur .rows
|
||||||
classes = ["gt_table"]
|
classes = ["gt_table"]
|
||||||
self.dates = [str(dates[0]) + "T00:00", str(dates[1]) + "T23:59"]
|
self.dates = [str(dates[0]) + "T00:00", str(dates[1]) + "T23:59"]
|
||||||
self.formsemestre = formsemestre
|
self.formsemestre = formsemestre
|
||||||
|
@ -27,7 +27,6 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from flask import g, request, render_template, flash
|
from flask import g, request, render_template, flash
|
||||||
from flask import abort, url_for, redirect, Response
|
from flask import abort, url_for, redirect, Response
|
||||||
@ -122,7 +121,6 @@ def bilan_dept():
|
|||||||
if formsemestre_id:
|
if formsemestre_id:
|
||||||
try:
|
try:
|
||||||
formsemestre: FormSemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
formsemestre: FormSemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||||
annee = formsemestre.annee_scolaire()
|
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
formsemestre_id = ""
|
formsemestre_id = ""
|
||||||
|
|
||||||
@ -533,7 +531,7 @@ def bilan_etud():
|
|||||||
# Récupération des assiduités et justificatifs de l'étudiant
|
# Récupération des assiduités et justificatifs de l'étudiant
|
||||||
data = liste_assi.AssiJustifData(
|
data = liste_assi.AssiJustifData(
|
||||||
etud.assiduites.filter(
|
etud.assiduites.filter(
|
||||||
Assiduite.etat != scu.EtatAssiduite.PRESENT, Assiduite.est_just == False
|
Assiduite.etat != scu.EtatAssiduite.PRESENT, Assiduite.est_just is False
|
||||||
),
|
),
|
||||||
etud.justificatifs.filter(
|
etud.justificatifs.filter(
|
||||||
Justificatif.etat.in_(
|
Justificatif.etat.in_(
|
||||||
@ -860,6 +858,15 @@ def calendrier_assi_etud():
|
|||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.AbsChange)
|
@permission_required(Permission.AbsChange)
|
||||||
def choix_date() -> str:
|
def choix_date() -> str:
|
||||||
|
"""
|
||||||
|
choix_date Choix de la date pour la saisie des assiduités
|
||||||
|
|
||||||
|
Route utilisée uniquement si la date courante n'est pas dans le semestre
|
||||||
|
concerné par la requête vers une des pages suivantes :
|
||||||
|
- saisie_assiduites_group
|
||||||
|
- visu_assiduites_group
|
||||||
|
|
||||||
|
"""
|
||||||
formsemestre_id = request.args.get("formsemestre_id")
|
formsemestre_id = request.args.get("formsemestre_id")
|
||||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||||
|
|
||||||
@ -976,9 +983,6 @@ def signal_assiduites_group():
|
|||||||
if formsemestre.dept_id != g.scodoc_dept_id:
|
if formsemestre.dept_id != g.scodoc_dept_id:
|
||||||
abort(404, "groupes inexistants dans ce département")
|
abort(404, "groupes inexistants dans ce département")
|
||||||
|
|
||||||
# Vérification du forçage du module
|
|
||||||
require_module = sco_preferences.get_preference("forcer_module", formsemestre_id)
|
|
||||||
|
|
||||||
# Récupération des étudiants des groupes
|
# Récupération des étudiants des groupes
|
||||||
etuds = [
|
etuds = [
|
||||||
sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0]
|
sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0]
|
||||||
@ -1110,9 +1114,6 @@ def visu_assiduites_group():
|
|||||||
if formsemestre.dept_id != g.scodoc_dept_id:
|
if formsemestre.dept_id != g.scodoc_dept_id:
|
||||||
abort(404, "groupes inexistants dans ce département")
|
abort(404, "groupes inexistants dans ce département")
|
||||||
|
|
||||||
# Vérfication du forçage du module
|
|
||||||
require_module = sco_preferences.get_preference("forcer_module", formsemestre_id)
|
|
||||||
|
|
||||||
# Récupération des étudiants du/des groupe(s)
|
# Récupération des étudiants du/des groupe(s)
|
||||||
etuds = [
|
etuds = [
|
||||||
sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0]
|
sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0]
|
||||||
@ -1784,22 +1785,6 @@ def signal_assiduites_diff():
|
|||||||
)
|
)
|
||||||
etudiants = list(sorted(etudiants, key=lambda etud: etud.sort_key))
|
etudiants = list(sorted(etudiants, key=lambda etud: etud.sort_key))
|
||||||
|
|
||||||
# Génération de l'HTML
|
|
||||||
|
|
||||||
header: str = html_sco_header.sco_header(
|
|
||||||
page_title="Assiduité: saisie différée",
|
|
||||||
init_qtip=True,
|
|
||||||
cssstyles=[
|
|
||||||
"css/assiduites.css",
|
|
||||||
],
|
|
||||||
javascripts=html_sco_header.BOOTSTRAP_MULTISELECT_JS
|
|
||||||
+ [
|
|
||||||
"js/assiduites.js",
|
|
||||||
"js/date_utils.js",
|
|
||||||
"js/etud_info.js",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
if groups_infos.tous_les_etuds_du_sem:
|
if groups_infos.tous_les_etuds_du_sem:
|
||||||
gr_tit = "en"
|
gr_tit = "en"
|
||||||
else:
|
else:
|
||||||
@ -2078,8 +2063,8 @@ def _differee(
|
|||||||
etudiants (list[dict]): la liste des étudiants (représentés par des dictionnaires)
|
etudiants (list[dict]): la liste des étudiants (représentés par des dictionnaires)
|
||||||
moduleimpl_select (str): l'html représentant le selecteur de module
|
moduleimpl_select (str): l'html représentant le selecteur de module
|
||||||
date (str, optional): la première date à afficher. Defaults to None.
|
date (str, optional): la première date à afficher. Defaults to None.
|
||||||
periode (dict[str, str], optional):La période par défaut de la première colonne. Defaults to None.
|
periode (dict[str, str], optional):La période par défaut de la première colonne.
|
||||||
formsemestre_id (int, optional): l'id du semestre pour le selecteur de module. Defaults to None.
|
formsemestre_id (int, optional): l'id du semestre pour le selecteur de module.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: le widget (html/css/js)
|
str: le widget (html/css/js)
|
||||||
@ -2265,6 +2250,9 @@ def generate_calendar(
|
|||||||
etudiant: Identite,
|
etudiant: Identite,
|
||||||
annee: int = None,
|
annee: int = None,
|
||||||
) -> dict[str, list["Jour"]]:
|
) -> dict[str, list["Jour"]]:
|
||||||
|
"""
|
||||||
|
Génère le calendrier d'assiduité de l'étudiant pour une année scolaire donnée
|
||||||
|
"""
|
||||||
# Si pas d'année alors on prend l'année scolaire en cours
|
# Si pas d'année alors on prend l'année scolaire en cours
|
||||||
if annee is None:
|
if annee is None:
|
||||||
annee = scu.annee_scolaire()
|
annee = scu.annee_scolaire()
|
||||||
@ -2312,13 +2300,26 @@ class Jour:
|
|||||||
self.justificatifs = justificatifs
|
self.justificatifs = justificatifs
|
||||||
|
|
||||||
def get_nom(self, mode_demi: bool = True) -> str:
|
def get_nom(self, mode_demi: bool = True) -> str:
|
||||||
|
"""
|
||||||
|
Renvoie le nom du jour
|
||||||
|
"M19" ou "Mer 19"
|
||||||
|
"""
|
||||||
str_jour: str = scu.DAY_NAMES[self.date.weekday()].capitalize()
|
str_jour: str = scu.DAY_NAMES[self.date.weekday()].capitalize()
|
||||||
return f"{str_jour[0] if mode_demi or self.is_non_work() else str_jour[:3]+' '}{self.date.day}"
|
return (
|
||||||
|
f"{str_jour[0] if mode_demi or self.is_non_work() else str_jour[:3]+' '}"
|
||||||
|
+ f"{self.date.day}"
|
||||||
|
)
|
||||||
|
|
||||||
def get_date(self) -> str:
|
def get_date(self) -> str:
|
||||||
|
"""
|
||||||
|
Renvoie la date du jour au format "dd/mm/yyyy"
|
||||||
|
"""
|
||||||
return self.date.strftime("%d/%m/%Y")
|
return self.date.strftime("%d/%m/%Y")
|
||||||
|
|
||||||
def get_class(self, show_pres: bool = False, show_reta: bool = False) -> str:
|
def get_class(self, show_pres: bool = False, show_reta: bool = False) -> str:
|
||||||
|
"""
|
||||||
|
Retourne la classe css du jour (mode normal)
|
||||||
|
"""
|
||||||
etat = ""
|
etat = ""
|
||||||
est_just = ""
|
est_just = ""
|
||||||
|
|
||||||
@ -2340,13 +2341,16 @@ class Jour:
|
|||||||
def get_demi_class(
|
def get_demi_class(
|
||||||
self, matin: bool, show_pres: bool = False, show_reta: bool = False
|
self, matin: bool, show_pres: bool = False, show_reta: bool = False
|
||||||
) -> str:
|
) -> str:
|
||||||
# Transformation d'une heure "HH:MM" en time(h,m)
|
"""
|
||||||
str2time = lambda x: datetime.time(*list(map(int, x.split(":"))))
|
Renvoie la class css de la demi journée
|
||||||
|
"""
|
||||||
|
|
||||||
heure_midi = str2time(ScoDocSiteConfig.get("assi_lunch_time", "13:00"))
|
heure_midi = scass.str_to_time(ScoDocSiteConfig.get("assi_lunch_time", "13:00"))
|
||||||
|
|
||||||
if matin:
|
if matin:
|
||||||
heure_matin = str2time(ScoDocSiteConfig.get("assi_morning_time", "08:00"))
|
heure_matin = scass.str_to_time(
|
||||||
|
ScoDocSiteConfig.get("assi_morning_time", "08:00")
|
||||||
|
)
|
||||||
matin = (
|
matin = (
|
||||||
# date debut
|
# date debut
|
||||||
scu.localize_datetime(
|
scu.localize_datetime(
|
||||||
@ -2382,7 +2386,9 @@ class Jour:
|
|||||||
|
|
||||||
return f"color {etat} {est_just}"
|
return f"color {etat} {est_just}"
|
||||||
|
|
||||||
heure_soir = str2time(ScoDocSiteConfig.get("assi_afternoon_time", "17:00"))
|
heure_soir = scass.str_to_time(
|
||||||
|
ScoDocSiteConfig.get("assi_afternoon_time", "17:00")
|
||||||
|
)
|
||||||
|
|
||||||
# séparation en demi journées
|
# séparation en demi journées
|
||||||
aprem = (
|
aprem = (
|
||||||
@ -2421,21 +2427,24 @@ class Jour:
|
|||||||
return f"color {etat} {est_just}"
|
return f"color {etat} {est_just}"
|
||||||
|
|
||||||
def has_assiduites(self) -> bool:
|
def has_assiduites(self) -> bool:
|
||||||
|
"""
|
||||||
|
Renverra True si le jour a des assiduités
|
||||||
|
"""
|
||||||
return self.assiduites.count() > 0
|
return self.assiduites.count() > 0
|
||||||
|
|
||||||
def generate_minitimeline(self) -> str:
|
def generate_minitimeline(self) -> str:
|
||||||
|
"""
|
||||||
|
Génère la minitimeline du jour
|
||||||
|
"""
|
||||||
# Récupérer le référenciel de la timeline
|
# Récupérer le référenciel de la timeline
|
||||||
str2time = lambda x: _time_to_timedelta(
|
scass.str_to_time = lambda x: _time_to_timedelta(
|
||||||
datetime.time(*list(map(int, x.split(":"))))
|
datetime.time(*list(map(int, x.split(":"))))
|
||||||
)
|
)
|
||||||
|
|
||||||
heure_matin: datetime.timedelta = str2time(
|
heure_matin: datetime.timedelta = scass.str_to_time(
|
||||||
ScoDocSiteConfig.get("assi_morning_time", "08:00")
|
ScoDocSiteConfig.get("assi_morning_time", "08:00")
|
||||||
)
|
)
|
||||||
heure_midi: datetime.timedelta = str2time(
|
heure_soir: datetime.timedelta = scass.str_to_time(
|
||||||
ScoDocSiteConfig.get("assi_lun_time", "13:00")
|
|
||||||
)
|
|
||||||
heure_soir: datetime.timedelta = str2time(
|
|
||||||
ScoDocSiteConfig.get("assi_afternoon_time", "17:00")
|
ScoDocSiteConfig.get("assi_afternoon_time", "17:00")
|
||||||
)
|
)
|
||||||
# longueur_timeline = heure_soir - heure_matin
|
# longueur_timeline = heure_soir - heure_matin
|
||||||
@ -2484,17 +2493,21 @@ class Jour:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def is_non_work(self):
|
def is_non_work(self):
|
||||||
|
"""
|
||||||
|
Renvoie True si le jour est un jour non travaillé
|
||||||
|
(en fonction de la préférence du département)
|
||||||
|
"""
|
||||||
return self.date.weekday() in scu.NonWorkDays.get_all_non_work_days(
|
return self.date.weekday() in scu.NonWorkDays.get_all_non_work_days(
|
||||||
dept_id=g.scodoc_dept_id
|
dept_id=g.scodoc_dept_id
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_etats_from_assiduites(self, assiduites: Query) -> list[scu.EtatAssiduite]:
|
def _get_etats_from_assiduites(self, assiduites: Query) -> list[scu.EtatAssiduite]:
|
||||||
return list(set([scu.EtatAssiduite(assi.etat) for assi in assiduites]))
|
return list(set(scu.EtatAssiduite(assi.etat) for assi in assiduites))
|
||||||
|
|
||||||
def _get_etats_from_justificatifs(
|
def _get_etats_from_justificatifs(
|
||||||
self, justificatifs: Query
|
self, justificatifs: Query
|
||||||
) -> list[scu.EtatJustificatif]:
|
) -> list[scu.EtatJustificatif]:
|
||||||
return list(set([scu.EtatJustificatif(justi.etat) for justi in justificatifs]))
|
return list(set(scu.EtatJustificatif(justi.etat) for justi in justificatifs))
|
||||||
|
|
||||||
def _get_color_assiduites_cascade(
|
def _get_color_assiduites_cascade(
|
||||||
self,
|
self,
|
||||||
|
Loading…
Reference in New Issue
Block a user