This commit is contained in:
Emmanuel Viennet 2024-02-27 21:40:51 +01:00
commit 741168a065
9 changed files with 590 additions and 446 deletions

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

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

@ -3,9 +3,9 @@
Calendrier de l'assiduité Calendrier de l'assiduité
{% endblock title %} {% endblock title %}
{% block styles %} {% block styles %}
{{ super() }} {{ super() }}
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/minitimeline.css"> <link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/minitimeline.css">
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css"> <link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
{% endblock styles %} {% endblock styles %}
{% block app_content %} {% block app_content %}
@ -15,379 +15,388 @@ 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">
{% for mois,jours in calendrier.items() %} {% for mois,jours in calendrier.items() %}
<div class="month"> <div class="month">
<h3>{{mois}}</h3> <h3>{{mois}}</h3>
<div class="days {{'demi' if mode_demi else ''}}"> <div class="days {{'demi' if mode_demi else ''}}">
{% for jour in jours %} {% for jour in jours %}
{% if jour.is_non_work() %} {% if jour.is_non_work() %}
<div class="day {{jour.get_class()}}"> <div class="day {{jour.get_class()}}">
<span>{{jour.get_nom()}}</span> <span>{{jour.get_nom()}}</span>
{% else %} {% else %}
<div class="day {{jour.get_class(show_pres, show_reta) if not mode_demi else ''}}"> <div class="day {{jour.get_class(show_pres, show_reta) if not mode_demi else ''}}">
{% endif %} {% endif %}
{% if mode_demi %} {% if mode_demi %}
{% if not jour.is_non_work() %} {% if not jour.is_non_work() %}
<span>{{jour.get_nom()}}</span> <span>{{jour.get_nom()}}</span>
<span class="{{jour.get_demi_class(True, show_pres,show_reta)}}"></span> <span class="{{jour.get_demi_class(True, show_pres,show_reta)}}"></span>
<span class="{{jour.get_demi_class(False, show_pres,show_reta)}}"></span> <span class="{{jour.get_demi_class(False, show_pres,show_reta)}}"></span>
{% endif %} {% endif %}
{% else %} {% else %}
{% if not jour.is_non_work() %} {% if not jour.is_non_work() %}
<span>{{jour.get_nom(False)}}</span> <span>{{jour.get_nom(False)}}</span>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if not jour.is_non_work() and jour.has_assiduites()%} {% if not jour.is_non_work() and jour.has_assiduites()%}
<div class="dayline"> <div class="dayline">
<div class="dayline-title"> <div class="dayline-title">
<span>Assiduité du</span> <span>Assiduité du</span>
<br> <br>
<span>{{jour.get_date()}}</span> <span>{{jour.get_date()}}</span>
{{jour.generate_minitimeline() | safe}} {{jour.generate_minitimeline() | safe}}
</div> </div>
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
{% 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>
<span id="label-nom">Assiduité de {{sco.etud.nomprenom}}</span> <span id="label-nom">Assiduité de {{sco.etud.nomprenom}}</span>
</div> </div>
<div class="help"> <div class="help">
<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> &rightarrow; présence de l'étudiant lors de la période <li><span title="Vert" class="present demo"></span> &rightarrow; présence de l'étudiant lors de la
</li> période
<li><span title="Bleu clair" class="nonwork demo"></span> &rightarrow; la période n'est pas travaillée </li>
</li> <li><span title="Bleu clair" class="nonwork demo"></span> &rightarrow; la période n'est pas travaillée
<li><span title="Rouge" class="absent demo"></span> &rightarrow; absence de l'étudiant lors de la période </li>
</li> <li><span title="Rouge" class="absent demo"></span> &rightarrow; absence de l'étudiant lors de la
<li><span title="Rose" class="demo color absent est_just"></span> &rightarrow; absence justifiée période
</li> </li>
<li><span title="Orange" class="retard demo"></span> &rightarrow; retard de l'étudiant lors de la période <li><span title="Rose" class="demo color absent est_just"></span> &rightarrow; absence justifiée
</li> </li>
<li><span title="Jaune clair" class="demo color retard est_just"></span> &rightarrow; retard justifié <li><span title="Orange" class="retard demo"></span> &rightarrow; retard de l'étudiant lors de la
</li> période
</li>
<li><span title="Jaune clair" class="demo color retard est_just"></span> &rightarrow; retard justifié
</li>
<li><span title="Quart Bleu" class="est_just demo"></span> &rightarrow; la période est couverte par un <li><span title="Quart Bleu" class="est_just demo"></span> &rightarrow; la période est couverte par un
justificatif valide</li>
<li><span title="Justif. non valide" class="invalide demo"></span> &rightarrow; la période est
couverte par un justificatif non valide
</li>
<li><span title="Justif. en attente" class="attente demo"></span> &rightarrow; la période
a un justificatif en attente de validation
</li>
</ul>
<p>Vous pouvez passer le curseur sur les jours colorés afin de voir les informations supplémentaires</p>
</div>
<ul class="couleurs print">
<li><span title="Vert" class="present demo"></span> présence
</li>
<li><span title="Bleu clair" class="nonwork demo"></span> non travaillé
</li>
<li><span title="Rouge" class="absent demo"></span> absence
</li>
<li><span title="Rose" class="demo color absent est_just"></span> absence justifiée
</li>
<li><span title="Orange" class="retard demo"></span> retard
</li>
<li><span title="Jaune clair" class="demo color retard est_just"></span>retard justifié
</li>
<li><span title="Quart Bleu" class="est_just demo"></span>
justificatif valide</li> justificatif valide</li>
<li><span title="Justif. non valide" class="invalide demo"></span> &rightarrow; la période est <li><span title="Quart Violet" class="invalide demo"></span> justificatif non valide
couverte par un justificatif non valide
</li>
<li><span title="Justif. en attente" class="attente demo"></span> &rightarrow; la période
a un justificatif en attente de validation
</li> </li>
</ul> </ul>
<p>Vous pouvez passer le curseur sur les jours colorés afin de voir les informations supplémentaires</p>
</div> </div>
<ul class="couleurs print">
<li><span title="Vert" class="present demo"></span> présence
</li>
<li><span title="Bleu clair" class="nonwork demo"></span> non travaillé
</li>
<li><span title="Rouge" class="absent demo"></span> absence
</li>
<li><span title="Rose" class="demo color absent est_just"></span> absence justifiée
</li>
<li><span title="Orange" class="retard demo"></span> retard
</li>
<li><span title="Jaune clair" class="demo color retard est_just"></span>retard justifié
</li>
<li><span title="Quart Bleu" class="est_just demo"></span>
justificatif valide</li>
<li><span title="Quart Violet" class="invalide demo"></span> justificatif non valide
</li>
</ul>
</div>
<style> <style>
.help .couleurs { .help .couleurs {
grid-template-columns: 2; grid-template-columns: 2;
grid-template-rows: auto; grid-template-rows: auto;
display: grid; display: grid;
} }
.couleurs.print {
display: none;
}
.help .couleurs li:nth-child(odd) {
grid-column: 1;
list-style-type: none;
}
.help .couleurs li:nth-child(even) {
grid-column: 2;
list-style-type: none;
}
.color.present {
background-color: var(--color-present) !important;
}
.color.absent {
background-color: var(--color-absent) !important;
}
.color.absent.est_just {
background-color: var(--color-absent-justi) !important;
}
.color.retard {
background-color: var(--color-retard) !important;
}
.color.retard.est_just {
background-color: var(--color-retard-justi) !important;
}
.color.nonwork {
background-color: var(--color-nonwork) !important;
}
.color {
background-color: var(--color-defaut) !important;
}
.pageContent {
margin-top: 1vh;
max-width: var(--sco-content-max-width);
}
.calendrier {
display: flex;
justify-content: space-evenly;
flex-wrap: wrap;
border: 1px solid #444;
border-radius: 12px;
margin-bottom: 12px;
}
.month h3 {
text-align: center;
}
.day,
.demi .day.color.nonwork {
text-align: left;
margin: 2px;
cursor: default;
font-size: 13px;
position: relative;
font-weight: normal;
min-width: 6em;
display: flex;
justify-content: start;
}
.color.est_just.sans_etat::before {
content: "";
position: absolute;
width: 25%;
height: 100%;
background-color: var(--color-justi) !important;
right: 0;
}
.color.invalide::before {
content: "";
position: absolute;
width: 25%;
height: 100%;
right: 0;
background-color: var(--color-justi-invalide) !important;
}
.color.attente::before, .color.modifie::before {
content: "";
position: absolute;
width: 25%;
height: 100%;
right: 0;
background: repeating-linear-gradient(
to bottom,
var(--color-justi-attente-stripe) 0px,
var(--color-justi-attente-stripe) 4px,
var(--color-justi-attente) 4px,
var(--color-justi-attente) 7px
)!important;
}
.demo.invalide {
background-color: var(--color-justi-invalide) !important;
}
.demo.attente {
background: repeating-linear-gradient(
to bottom,
var(--color-justi-attente-stripe) 0px,
var(--color-justi-attente-stripe) 4px,
var(--color-justi-attente) 4px,
var(--color-justi-attente) 7px
)!important;
}
.demo.est_just {
background-color: var(--color-justi) !important;
}
.demi .day.nonwork>span {
flex: none;
border: none;
}
.demi .day {
border-radius: 0;
}
@media print {
.couleurs.print { .couleurs.print {
display: flex;
justify-content: space-evenly;
align-items: center;
}
.couleurs.print li {
list-style-type: none !important;
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
.day,
.demi .day.color.color.nonwork {
min-width: 5em;
font-size: 11px;
}
.demi .day>span:first-of-type {
width: 2.5em;
min-width: 2.5em;
}
.color {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
.day.est_just,
.demi .day span.est_just {
background-image: none;
}
.day.invalide,
.demi .day span.invalide {
background-image: none;
}
.demi .day span.est_just::before {
content: "J";
}
.demi .day span.invalide::before {
content: "JI";
}
#sidebar,
.help,
h2,
#annee,
#label-changer,
.options {
display: none; display: none;
} }
#label-nom, .help .couleurs li:nth-child(odd) {
#label-justi { grid-column: 1;
display: inline; list-style-type: none;
} }
#gtrcontent { .help .couleurs li:nth-child(even) {
margin: 5px; grid-column: 2;
list-style-type: none;
} }
.annee { .color.present {
background-color: var(--color-present) !important;
}
.color.absent {
background-color: var(--color-absent) !important;
}
.color.absent.est_just {
background-color: var(--color-absent-justi) !important;
}
.color.retard {
background-color: var(--color-retard) !important;
}
.color.retard.est_just {
background-color: var(--color-retard-justi) !important;
}
.color.nonwork {
background-color: var(--color-nonwork) !important;
}
.color {
background-color: var(--color-defaut) !important;
}
.pageContent {
margin-top: 1vh;
max-width: var(--sco-content-max-width);
}
.calendrier {
display: flex; display: flex;
justify-content: space-evenly; justify-content: space-evenly;
align-items: center; flex-wrap: wrap;
border: 1px solid #444;
border-radius: 12px;
margin-bottom: 12px;
} }
}
</style>
<script> .month h3 {
function getOptions() { text-align: center;
return {
"show_pres": document.getElementById("show_pres").checked,
"show_reta": document.getElementById("show_reta").checked,
"mode_demi": document.getElementById("mode_demi").checked,
} }
}
.day,
function updatePage(){ .demi .day.color.nonwork {
const url = new URL(location.href); text-align: left;
const options = getOptions(); margin: 2px;
url.searchParams.set("annee", document.getElementById('annee').value); cursor: default;
url.searchParams.set("mode_demi", options.mode_demi); font-size: 13px;
url.searchParams.set("show_pres", options.show_pres); position: relative;
url.searchParams.set("show_reta", options.show_reta); font-weight: normal;
min-width: 6em;
if (location.href != url.href){ display: flex;
location.href = url.href justify-content: start;
} }
}
const defAnnee = {{ annee }}
let annees = {{ annees | safe }}
annees = annees.filter((x, i) => annees.indexOf(x) === i)
const etudid = {{ sco.etud.id }};
const select = document.querySelector('#annee');
annees.forEach((a) => {
const opt = document.createElement("option");
opt.value = a + "",
opt.textContent = `${a} - ${a + 1}`;
if (a === defAnnee) {
opt.selected = true;
document.querySelector('.annee #label-annee').textContent = `Année scolaire ${a}-${a + 1}`
.color.est_just.sans_etat::before {
content: "";
position: absolute;
width: 25%;
height: 100%;
background-color: var(--color-justi) !important;
right: 0;
} }
select.appendChild(opt)
}) .color.invalide::before {
content: "";
document.querySelectorAll('input[type="checkbox"].memo, #annee').forEach(el => { position: absolute;
el.addEventListener('change', function() { width: 25%;
updatePage(); height: 100%;
})}); right: 0;
background-color: var(--color-justi-invalide) !important;
document.querySelectorAll('[assi_id]').forEach((el,i) => { }
el.addEventListener('click', ()=>{
const assi_id = el.getAttribute('assi_id'); .color.attente::before,
window.open(`${SCO_URL}/Assiduites/tableau_assiduite_actions?type=assiduite&action=details&obj_id=${assi_id}`); .color.modifie::before {
content: "";
position: absolute;
width: 25%;
height: 100%;
right: 0;
background: repeating-linear-gradient(to bottom,
var(--color-justi-attente-stripe) 0px,
var(--color-justi-attente-stripe) 4px,
var(--color-justi-attente) 4px,
var(--color-justi-attente) 7px) !important;
}
.demo.invalide {
background-color: var(--color-justi-invalide) !important;
}
.demo.attente {
background: repeating-linear-gradient(to bottom,
var(--color-justi-attente-stripe) 0px,
var(--color-justi-attente-stripe) 4px,
var(--color-justi-attente) 4px,
var(--color-justi-attente) 7px) !important;
}
.demo.est_just {
background-color: var(--color-justi) !important;
}
.demi .day.nonwork>span {
flex: none;
border: none;
}
.demi .day {
border-radius: 0;
}
@media print {
.couleurs.print {
display: flex;
justify-content: space-evenly;
align-items: center;
}
.couleurs.print li {
list-style-type: none !important;
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
.day,
.demi .day.color.color.nonwork {
min-width: 5em;
font-size: 11px;
}
.demi .day>span:first-of-type {
width: 2.5em;
min-width: 2.5em;
}
.color {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
.day.est_just,
.demi .day span.est_just {
background-image: none;
}
.day.invalide,
.demi .day span.invalide {
background-image: none;
}
.demi .day span.est_just::before {
content: "J";
}
.demi .day span.invalide::before {
content: "JI";
}
#sidebar,
.help,
h2,
#annee,
#label-changer,
.options {
display: none;
}
#label-nom,
#label-justi {
display: inline;
}
#gtrcontent {
margin: 5px;
}
.annee {
display: flex;
justify-content: space-evenly;
align-items: center;
}
}
</style>
<script>
function getOptions() {
return {
"show_pres": document.getElementById("show_pres").checked,
"show_reta": document.getElementById("show_reta").checked,
"mode_demi": document.getElementById("mode_demi").checked,
}
}
function updatePage() {
const url = new URL(location.href);
const options = getOptions();
url.searchParams.set("annee", document.getElementById('annee').value);
url.searchParams.set("mode_demi", options.mode_demi);
url.searchParams.set("show_pres", options.show_pres);
url.searchParams.set("show_reta", options.show_reta);
if (location.href != url.href) {
location.href = url.href
}
}
const defAnnee = "{{ annee | safe}}"
let annees = {{ annees | safe }}
annees = annees.filter((x, i) => annees.indexOf(x) === i)
const etudid = {{ sco.etud.id }};
const select = document.querySelector('#annee');
annees.forEach((a) => {
const opt = document.createElement("option");
let a_1 = a.substring(0, 4)
opt.value = a_1 + "",
opt.textContent = a
if (a_1 === defAnnee) {
opt.selected = true;
document.querySelector('.annee #label-annee').textContent = `Année scolaire ${a}`
}
select.appendChild(opt)
}) })
});
</script> document.querySelectorAll('input[type="checkbox"].memo, #annee').forEach(el => {
{% endblock app_content %} el.addEventListener('change', function () {
updatePage();
})
});
document.querySelectorAll('[assi_id]').forEach((el, i) => {
el.addEventListener('click', () => {
const assi_id = el.getAttribute('assi_id');
window.open(`${SCO_URL}/Assiduites/tableau_assiduite_actions?type=assiduite&action=details&obj_id=${assi_id}`);
})
});
</script>
{% endblock app_content %}

View File

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