Update opolka/ScoDoc from ScoDoc/ScoDoc #2

Merged
opolka merged 1272 commits from ScoDoc/ScoDoc:master into master 2024-05-27 09:11:04 +02:00
8 changed files with 189 additions and 97 deletions
Showing only changes of commit c620c3b0e1 - Show all commits

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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