forked from ScoDoc/ScoDoc
Merge branch 'main96' of https://scodoc.org/git/iziram/ScoDoc
This commit is contained in:
commit
741168a065
@ -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,3 +1,8 @@
|
|||||||
|
"""
|
||||||
|
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
|
||||||
@ -8,10 +13,18 @@ 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
|
||||||
from app.models import Assiduite, Identite, Justificatif
|
from app.models import Assiduite, Identite, Justificatif
|
||||||
from app.scodoc.sco_utils import EtatAssiduite, EtatJustificatif, to_bool
|
from app.scodoc.sco_utils import (
|
||||||
|
EtatAssiduite,
|
||||||
|
EtatJustificatif,
|
||||||
|
to_bool,
|
||||||
|
date_debut_annee_scolaire,
|
||||||
|
date_fin_annee_scolaire,
|
||||||
|
localize_datetime,
|
||||||
|
)
|
||||||
from app.tables import table_builder as tb
|
from app.tables import table_builder as tb
|
||||||
from app.scodoc.sco_cache import RequeteTableauAssiduiteCache
|
from app.scodoc.sco_cache import RequeteTableauAssiduiteCache
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
|
from app.scodoc.sco_preferences import get_preference
|
||||||
|
|
||||||
|
|
||||||
class Pagination:
|
class Pagination:
|
||||||
@ -26,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
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -37,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
|
||||||
@ -195,6 +212,17 @@ class ListeAssiJusti(tb.Table):
|
|||||||
r = query_finale.all()
|
r = query_finale.all()
|
||||||
RequeteTableauAssiduiteCache.set(cle_cache, r)
|
RequeteTableauAssiduiteCache.set(cle_cache, r)
|
||||||
|
|
||||||
|
# Filtrer Si préférence "Limiter les assiduités à l'année courante"
|
||||||
|
if get_preference("assi_limit_annee"):
|
||||||
|
annee_debut = localize_datetime(date_debut_annee_scolaire())
|
||||||
|
annee_fin = localize_datetime(date_fin_annee_scolaire())
|
||||||
|
r = [
|
||||||
|
obj
|
||||||
|
for obj in r
|
||||||
|
if obj._asdict()["date_debut"] >= annee_debut
|
||||||
|
and obj._asdict()["date_fin"] <= annee_fin
|
||||||
|
]
|
||||||
|
|
||||||
# Paginer la requête pour ne pas envoyer trop d'informations au client
|
# Paginer la requête pour ne pas envoyer trop d'informations au client
|
||||||
pagination: Pagination = self.paginer(r, no_pagination=self.no_pagination)
|
pagination: Pagination = self.paginer(r, no_pagination=self.no_pagination)
|
||||||
self.total_pages = pagination.total_pages
|
self.total_pages = pagination.total_pages
|
||||||
@ -212,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,
|
||||||
@ -232,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
|
||||||
@ -599,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}
|
||||||
@ -637,7 +678,7 @@ class AssiFiltre:
|
|||||||
|
|
||||||
type_filtrage, date = val_filtre
|
type_filtrage, date = val_filtre
|
||||||
|
|
||||||
match (type_filtrage):
|
match type_filtrage:
|
||||||
# On garde uniquement les dates supérieures au filtre
|
# On garde uniquement les dates supérieures au filtre
|
||||||
case 2:
|
case 2:
|
||||||
query_filtree = query_filtree.filter(
|
query_filtree = query_filtree.filter(
|
||||||
@ -734,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])
|
||||||
@ -745,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
|
||||||
|
@ -15,9 +15,12 @@ Calendrier de l'assiduité
|
|||||||
<h2>Assiduité de {{sco.etud.html_link_fiche()|safe}}</h2>
|
<h2>Assiduité de {{sco.etud.html_link_fiche()|safe}}</h2>
|
||||||
|
|
||||||
<div class="options">
|
<div class="options">
|
||||||
<input type="checkbox" id="show_pres" name="show_pres" class="memo" {{'checked' if show_pres else ''}}><label for="show_pres">afficher les présences</label>
|
<input type="checkbox" id="show_pres" name="show_pres" class="memo" {{'checked' if show_pres else '' }}><label
|
||||||
<input type="checkbox" name="show_reta" id="show_reta" class="memo" {{'checked' if show_reta else ''}}><label for="show_reta">afficher les retards</label>
|
for="show_pres">afficher les présences</label>
|
||||||
<input type="checkbox" name="mode_demi" id="mode_demi" class="memo" {{'checked' if mode_demi else ''}}><label for="mode_demi">mode demi journée</label>
|
<input type="checkbox" name="show_reta" id="show_reta" class="memo" {{'checked' if show_reta else '' }}><label
|
||||||
|
for="show_reta">afficher les retards</label>
|
||||||
|
<input type="checkbox" name="mode_demi" id="mode_demi" class="memo" {{'checked' if mode_demi else '' }}><label
|
||||||
|
for="mode_demi">mode demi journée</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="calendrier">
|
<div class="calendrier">
|
||||||
@ -64,7 +67,7 @@ Calendrier de l'assiduité
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<div class="annee">
|
<div class="annee">
|
||||||
<span id="label-annee">Année scolaire 2022-2023</span><span id="label-changer" style="margin-left: 5px;">Changer
|
<span id="label-annee">Année scolaire</span><span id="label-changer" style="margin-left: 5px;">Changer
|
||||||
année: </span>
|
année: </span>
|
||||||
<select name="" id="annee">
|
<select name="" id="annee">
|
||||||
</select>
|
</select>
|
||||||
@ -76,15 +79,18 @@ Calendrier de l'assiduité
|
|||||||
<h3>Calendrier</h3>
|
<h3>Calendrier</h3>
|
||||||
<p>Code couleur</p>
|
<p>Code couleur</p>
|
||||||
<ul class="couleurs">
|
<ul class="couleurs">
|
||||||
<li><span title="Vert" class="present demo"></span> → présence de l'étudiant lors de la période
|
<li><span title="Vert" class="present demo"></span> → présence de l'étudiant lors de la
|
||||||
|
période
|
||||||
</li>
|
</li>
|
||||||
<li><span title="Bleu clair" class="nonwork demo"></span> → la période n'est pas travaillée
|
<li><span title="Bleu clair" class="nonwork demo"></span> → la période n'est pas travaillée
|
||||||
</li>
|
</li>
|
||||||
<li><span title="Rouge" class="absent demo"></span> → absence de l'étudiant lors de la période
|
<li><span title="Rouge" class="absent demo"></span> → absence de l'étudiant lors de la
|
||||||
|
période
|
||||||
</li>
|
</li>
|
||||||
<li><span title="Rose" class="demo color absent est_just"></span> → absence justifiée
|
<li><span title="Rose" class="demo color absent est_just"></span> → absence justifiée
|
||||||
</li>
|
</li>
|
||||||
<li><span title="Orange" class="retard demo"></span> → retard de l'étudiant lors de la période
|
<li><span title="Orange" class="retard demo"></span> → retard de l'étudiant lors de la
|
||||||
|
période
|
||||||
</li>
|
</li>
|
||||||
<li><span title="Jaune clair" class="demo color retard est_just"></span> → retard justifié
|
<li><span title="Jaune clair" class="demo color retard est_just"></span> → retard justifié
|
||||||
</li>
|
</li>
|
||||||
@ -154,6 +160,7 @@ Calendrier de l'assiduité
|
|||||||
.color.absent.est_just {
|
.color.absent.est_just {
|
||||||
background-color: var(--color-absent-justi) !important;
|
background-color: var(--color-absent-justi) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.color.retard {
|
.color.retard {
|
||||||
background-color: var(--color-retard) !important;
|
background-color: var(--color-retard) !important;
|
||||||
}
|
}
|
||||||
@ -218,31 +225,31 @@ Calendrier de l'assiduité
|
|||||||
right: 0;
|
right: 0;
|
||||||
background-color: var(--color-justi-invalide) !important;
|
background-color: var(--color-justi-invalide) !important;
|
||||||
}
|
}
|
||||||
.color.attente::before, .color.modifie::before {
|
|
||||||
|
.color.attente::before,
|
||||||
|
.color.modifie::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 25%;
|
width: 25%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
right: 0;
|
right: 0;
|
||||||
background: repeating-linear-gradient(
|
background: repeating-linear-gradient(to bottom,
|
||||||
to bottom,
|
|
||||||
var(--color-justi-attente-stripe) 0px,
|
var(--color-justi-attente-stripe) 0px,
|
||||||
var(--color-justi-attente-stripe) 4px,
|
var(--color-justi-attente-stripe) 4px,
|
||||||
var(--color-justi-attente) 4px,
|
var(--color-justi-attente) 4px,
|
||||||
var(--color-justi-attente) 7px
|
var(--color-justi-attente) 7px) !important;
|
||||||
)!important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo.invalide {
|
.demo.invalide {
|
||||||
background-color: var(--color-justi-invalide) !important;
|
background-color: var(--color-justi-invalide) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo.attente {
|
.demo.attente {
|
||||||
background: repeating-linear-gradient(
|
background: repeating-linear-gradient(to bottom,
|
||||||
to bottom,
|
|
||||||
var(--color-justi-attente-stripe) 0px,
|
var(--color-justi-attente-stripe) 0px,
|
||||||
var(--color-justi-attente-stripe) 4px,
|
var(--color-justi-attente-stripe) 4px,
|
||||||
var(--color-justi-attente) 4px,
|
var(--color-justi-attente) 4px,
|
||||||
var(--color-justi-attente) 7px
|
var(--color-justi-attente) 7px) !important;
|
||||||
)!important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo.est_just {
|
.demo.est_just {
|
||||||
@ -358,7 +365,7 @@ Calendrier de l'assiduité
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const defAnnee = {{ annee }}
|
const defAnnee = "{{ annee | safe}}"
|
||||||
let annees = {{ annees | safe }}
|
let annees = {{ annees | safe }}
|
||||||
annees = annees.filter((x, i) => annees.indexOf(x) === i)
|
annees = annees.filter((x, i) => annees.indexOf(x) === i)
|
||||||
const etudid = {{ sco.etud.id }};
|
const etudid = {{ sco.etud.id }};
|
||||||
@ -366,11 +373,12 @@ Calendrier de l'assiduité
|
|||||||
const select = document.querySelector('#annee');
|
const select = document.querySelector('#annee');
|
||||||
annees.forEach((a) => {
|
annees.forEach((a) => {
|
||||||
const opt = document.createElement("option");
|
const opt = document.createElement("option");
|
||||||
opt.value = a + "",
|
let a_1 = a.substring(0, 4)
|
||||||
opt.textContent = `${a} - ${a + 1}`;
|
opt.value = a_1 + "",
|
||||||
if (a === defAnnee) {
|
opt.textContent = a
|
||||||
|
if (a_1 === defAnnee) {
|
||||||
opt.selected = true;
|
opt.selected = true;
|
||||||
document.querySelector('.annee #label-annee').textContent = `Année scolaire ${a}-${a + 1}`
|
document.querySelector('.annee #label-annee').textContent = `Année scolaire ${a}`
|
||||||
|
|
||||||
}
|
}
|
||||||
select.appendChild(opt)
|
select.appendChild(opt)
|
||||||
@ -379,7 +387,8 @@ Calendrier de l'assiduité
|
|||||||
document.querySelectorAll('input[type="checkbox"].memo, #annee').forEach(el => {
|
document.querySelectorAll('input[type="checkbox"].memo, #annee').forEach(el => {
|
||||||
el.addEventListener('change', function () {
|
el.addEventListener('change', function () {
|
||||||
updatePage();
|
updatePage();
|
||||||
})});
|
})
|
||||||
|
});
|
||||||
|
|
||||||
document.querySelectorAll('[assi_id]').forEach((el, i) => {
|
document.querySelectorAll('[assi_id]').forEach((el, i) => {
|
||||||
el.addEventListener('click', () => {
|
el.addEventListener('click', () => {
|
||||||
|
@ -25,8 +25,10 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
import json
|
||||||
import re
|
import re
|
||||||
from typing import Any
|
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
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
|
||||||
@ -121,7 +123,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 = ""
|
||||||
|
|
||||||
@ -230,17 +231,22 @@ def ajout_assiduite_etud() -> str | Response:
|
|||||||
# On dresse la liste des modules de l'année scolaire en cours
|
# On dresse la liste des modules de l'année scolaire en cours
|
||||||
# auxquels est inscrit l'étudiant pour peupler le menu "module"
|
# auxquels est inscrit l'étudiant pour peupler le menu "module"
|
||||||
modimpls_by_formsemestre = etud.get_modimpls_by_formsemestre(scu.annee_scolaire())
|
modimpls_by_formsemestre = etud.get_modimpls_by_formsemestre(scu.annee_scolaire())
|
||||||
choices = {
|
choices: OrderedDict = OrderedDict()
|
||||||
"": [("", "Non spécifié"), ("autre", "Autre module (pas dans la liste)")]
|
choices[""] = [("", "Non spécifié"), ("autre", "Autre module (pas dans la liste)")]
|
||||||
}
|
|
||||||
for formsemestre_id in modimpls_by_formsemestre:
|
for formsemestre_id in modimpls_by_formsemestre:
|
||||||
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
||||||
|
|
||||||
# indique le nom du semestre dans le menu (optgroup)
|
# indique le nom du semestre dans le menu (optgroup)
|
||||||
choices[formsemestre.titre_annee()] = [
|
group_name: str = formsemestre.titre_annee()
|
||||||
|
choices[group_name] = [
|
||||||
(m.id, f"{m.module.code} {m.module.abbrev or m.module.titre or ''}")
|
(m.id, f"{m.module.code} {m.module.abbrev or m.module.titre or ''}")
|
||||||
for m in modimpls_by_formsemestre[formsemestre_id]
|
for m in modimpls_by_formsemestre[formsemestre_id]
|
||||||
if m.module.ue.type == UE_STANDARD
|
if m.module.ue.type == UE_STANDARD
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if formsemestre.est_courant():
|
||||||
|
choices.move_to_end(group_name, last=False)
|
||||||
|
choices.move_to_end("", last=False)
|
||||||
form.modimpl.choices = choices
|
form.modimpl.choices = choices
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
@ -532,7 +538,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_(
|
||||||
@ -825,17 +831,19 @@ def calendrier_assi_etud():
|
|||||||
# Récupération des années d'étude de l'étudiant
|
# Récupération des années d'étude de l'étudiant
|
||||||
annees: list[int] = []
|
annees: list[int] = []
|
||||||
for ins in etud.formsemestre_inscriptions:
|
for ins in etud.formsemestre_inscriptions:
|
||||||
|
date_deb = ins.formsemestre.date_debut
|
||||||
|
date_fin = ins.formsemestre.date_fin
|
||||||
annees.extend(
|
annees.extend(
|
||||||
(ins.formsemestre.date_debut.year, ins.formsemestre.date_fin.year)
|
[
|
||||||
|
scu.annee_scolaire_repr(date_deb.year, date_deb.month),
|
||||||
|
scu.annee_scolaire_repr(date_fin.year, date_fin.month),
|
||||||
|
]
|
||||||
)
|
)
|
||||||
annees = sorted(annees, reverse=True)
|
annees = sorted(annees, reverse=True)
|
||||||
|
|
||||||
# Transformation en une liste "json"
|
# Transformation en une liste "json"
|
||||||
# (sera utilisé pour générer le selecteur d'année)
|
# (sera utilisé pour générer le selecteur d'année)
|
||||||
annees_str: str = "["
|
annees_str: str = json.dumps(annees)
|
||||||
for ann in annees:
|
|
||||||
annees_str += f"{ann},"
|
|
||||||
annees_str += "]"
|
|
||||||
|
|
||||||
calendrier: dict[str, list["Jour"]] = generate_calendar(etud, annee)
|
calendrier: dict[str, list["Jour"]] = generate_calendar(etud, annee)
|
||||||
|
|
||||||
@ -857,6 +865,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)
|
||||||
|
|
||||||
@ -973,9 +990,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]
|
||||||
@ -1107,9 +1121,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]
|
||||||
@ -1781,22 +1792,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:
|
||||||
@ -2075,8 +2070,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)
|
||||||
@ -2162,7 +2157,7 @@ def _module_selector_multiple(
|
|||||||
Prend les semestres de l'année, sauf si only_form est indiqué.
|
Prend les semestres de l'année, sauf si only_form est indiqué.
|
||||||
"""
|
"""
|
||||||
modimpls_by_formsemestre = etud.get_modimpls_by_formsemestre(scu.annee_scolaire())
|
modimpls_by_formsemestre = etud.get_modimpls_by_formsemestre(scu.annee_scolaire())
|
||||||
choices = {}
|
choices = OrderedDict()
|
||||||
for formsemestre_id in modimpls_by_formsemestre:
|
for formsemestre_id in modimpls_by_formsemestre:
|
||||||
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
||||||
if only_form is not None and formsemestre != only_form:
|
if only_form is not None and formsemestre != only_form:
|
||||||
@ -2177,6 +2172,9 @@ def _module_selector_multiple(
|
|||||||
if m.module.ue.type == UE_STANDARD
|
if m.module.ue.type == UE_STANDARD
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if formsemestre.est_courant():
|
||||||
|
choices.move_to_end(formsemestre.titre_annee(), last=False)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"assiduites/widgets/moduleimpl_selector_multiple.j2",
|
"assiduites/widgets/moduleimpl_selector_multiple.j2",
|
||||||
choices=choices,
|
choices=choices,
|
||||||
@ -2262,6 +2260,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()
|
||||||
@ -2309,13 +2310,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 = ""
|
||||||
|
|
||||||
@ -2337,13 +2351,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(
|
||||||
@ -2355,12 +2372,16 @@ class Jour:
|
|||||||
assiduites_matin = [
|
assiduites_matin = [
|
||||||
assi
|
assi
|
||||||
for assi in self.assiduites
|
for assi in self.assiduites
|
||||||
if scu.is_period_overlapping((assi.date_debut, assi.date_fin), matin)
|
if scu.is_period_overlapping(
|
||||||
|
(assi.date_debut, assi.date_fin), matin, bornes=False
|
||||||
|
)
|
||||||
]
|
]
|
||||||
justificatifs_matin = [
|
justificatifs_matin = [
|
||||||
justi
|
justi
|
||||||
for justi in self.justificatifs
|
for justi in self.justificatifs
|
||||||
if scu.is_period_overlapping((justi.date_debut, justi.date_fin), matin)
|
if scu.is_period_overlapping(
|
||||||
|
(justi.date_debut, justi.date_fin), matin, bornes=False
|
||||||
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
etat = self._get_color_assiduites_cascade(
|
etat = self._get_color_assiduites_cascade(
|
||||||
@ -2375,7 +2396,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 = (
|
||||||
@ -2388,13 +2411,17 @@ class Jour:
|
|||||||
assiduites_aprem = [
|
assiduites_aprem = [
|
||||||
assi
|
assi
|
||||||
for assi in self.assiduites
|
for assi in self.assiduites
|
||||||
if scu.is_period_overlapping((assi.date_debut, assi.date_fin), aprem)
|
if scu.is_period_overlapping(
|
||||||
|
(assi.date_debut, assi.date_fin), aprem, bornes=False
|
||||||
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
justificatifs_aprem = [
|
justificatifs_aprem = [
|
||||||
justi
|
justi
|
||||||
for justi in self.justificatifs
|
for justi in self.justificatifs
|
||||||
if scu.is_period_overlapping((justi.date_debut, justi.date_fin), aprem)
|
if scu.is_period_overlapping(
|
||||||
|
(justi.date_debut, justi.date_fin), aprem, bornes=False
|
||||||
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
etat = self._get_color_assiduites_cascade(
|
etat = self._get_color_assiduites_cascade(
|
||||||
@ -2410,21 +2437,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
|
||||||
@ -2433,6 +2463,7 @@ class Jour:
|
|||||||
# chaque block d'assiduité est défini par:
|
# chaque block d'assiduité est défini par:
|
||||||
# longueur = ( (fin-deb) / longueur_timeline ) * 100
|
# longueur = ( (fin-deb) / longueur_timeline ) * 100
|
||||||
# emplacement = ( (deb - heure_matin) / longueur_timeline ) * 100
|
# emplacement = ( (deb - heure_matin) / longueur_timeline ) * 100
|
||||||
|
# longueur + emplacement = 100% sinon on réduit longueur
|
||||||
|
|
||||||
assiduite_blocks: list[dict[str, float | str]] = []
|
assiduite_blocks: list[dict[str, float | str]] = []
|
||||||
|
|
||||||
@ -2448,8 +2479,10 @@ class Jour:
|
|||||||
else heure_soir
|
else heure_soir
|
||||||
)
|
)
|
||||||
|
|
||||||
longueur: float = ((fin - deb) / longueur_timeline) * 100
|
|
||||||
emplacement: float = ((deb - heure_matin) / longueur_timeline) * 100
|
emplacement: float = ((deb - heure_matin) / longueur_timeline) * 100
|
||||||
|
longueur: float = ((fin - deb) / longueur_timeline) * 100
|
||||||
|
if longueur + emplacement > 100:
|
||||||
|
longueur = 100 - emplacement
|
||||||
etat: str = scu.EtatAssiduite(assi.etat).name.lower()
|
etat: str = scu.EtatAssiduite(assi.etat).name.lower()
|
||||||
est_just: str = "est_just" if assi.est_just else ""
|
est_just: str = "est_just" if assi.est_just else ""
|
||||||
|
|
||||||
@ -2470,17 +2503,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