forked from ScoDoc/ScoDoc
Merge branch 'main96' of https://scodoc.org/git/iziram/ScoDoc
This commit is contained in:
commit
741168a065
@ -126,6 +126,7 @@ class AjoutAssiOrJustForm(FlaskForm):
|
||||
|
||||
class AjoutAssiduiteEtudForm(AjoutAssiOrJustForm):
|
||||
"Formulaire de saisie d'une assiduité pour un étudiant"
|
||||
|
||||
description = TextAreaField(
|
||||
"Description",
|
||||
render_kw={
|
||||
@ -152,6 +153,7 @@ class AjoutAssiduiteEtudForm(AjoutAssiOrJustForm):
|
||||
|
||||
class AjoutJustificatifEtudForm(AjoutAssiOrJustForm):
|
||||
"Formulaire de saisie d'un justificatif pour un étudiant"
|
||||
|
||||
raison = TextAreaField(
|
||||
"Raison",
|
||||
render_kw={
|
||||
@ -176,6 +178,12 @@ class AjoutJustificatifEtudForm(AjoutAssiOrJustForm):
|
||||
|
||||
|
||||
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):
|
||||
"Init form, adding a filed for our error messages"
|
||||
super().__init__(*args, **kwargs)
|
||||
|
@ -5,7 +5,6 @@ from datetime import datetime
|
||||
|
||||
from flask_login import current_user
|
||||
from flask_sqlalchemy.query import Query
|
||||
from sqlalchemy.exc import DataError
|
||||
|
||||
from app import db, log, g, set_sco_dept
|
||||
from app.models import (
|
||||
@ -89,6 +88,8 @@ class Assiduite(ScoDocModel):
|
||||
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:
|
||||
"""Retourne la représentation json de l'assiduité
|
||||
restrict n'est pas utilisé ici.
|
||||
@ -307,6 +308,9 @@ class Assiduite(ScoDocModel):
|
||||
|
||||
def supprime(self):
|
||||
"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
|
||||
|
||||
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")
|
||||
utilisateur: str = ""
|
||||
if self.user != None:
|
||||
if self.user is not None:
|
||||
self.user: User
|
||||
utilisateur = f"par {self.user.get_prenomnom()}"
|
||||
|
||||
@ -515,6 +519,8 @@ class Justificatif(ScoDocModel):
|
||||
def create_justificatif(
|
||||
cls,
|
||||
etudiant: Identite,
|
||||
# On a besoin des arguments mais on utilise "locals" pour les récupérer
|
||||
# pylint: disable=unused-argument
|
||||
date_debut: datetime,
|
||||
date_fin: datetime,
|
||||
etat: EtatJustificatif,
|
||||
@ -538,8 +544,10 @@ class Justificatif(ScoDocModel):
|
||||
|
||||
def supprime(self):
|
||||
"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.sco_archives_justificatifs import JustificatifArchiver
|
||||
|
||||
# Récupération de l'archive du justificatif
|
||||
archive_name: str = self.fichier
|
||||
|
@ -20,8 +20,11 @@ class Trace:
|
||||
|
||||
Role des fichiers traces :
|
||||
- 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 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)
|
||||
- Sauvegarder la date de suppression du fichier
|
||||
(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 :
|
||||
nom_fichier_srv,datetime_depot,datetime_suppr,user_id
|
||||
|
@ -21,6 +21,7 @@ from app.models.assiduites import Assiduite, Justificatif, compute_assiduites_ju
|
||||
from app.scodoc import sco_formsemestre_inscriptions
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_compute_moy
|
||||
from app.scodoc import sco_etud
|
||||
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
|
||||
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 :
|
||||
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 :
|
||||
Exemple d'ajout d'assiduité :
|
||||
- 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.
|
||||
Exemple d'accès aux métriques :
|
||||
metrics = calculator.to_dict()
|
||||
|
||||
4.Réinitialisation du comptage: Si besoin on peut réinitialisé le compteur sans perdre la configuration
|
||||
4.Réinitialisation du comptage: Si besoin on peut réinitialiser
|
||||
le compteur sans perdre la configuration
|
||||
(horaires personnalisés)
|
||||
Exemple de réinitialisation :
|
||||
calculator.reset()
|
||||
@ -61,8 +75,10 @@ class CountCalculator:
|
||||
- reset() : Réinitialise les compteurs de la classe.
|
||||
- add_half_day(day: date, is_morning: bool) : Ajoute une demi-journée au comptage.
|
||||
- add_day(day: date) : Ajoute un jour complet au comptage.
|
||||
- compute_long_assiduite(assi: Assiduite) : Traite les assiduités s'étendant sur plus d'un jour.
|
||||
- compute_assiduites(assiduites: Query | list) : Calcule les métriques pour une collection d'assiduités.
|
||||
- compute_long_assiduite(assi: Assiduite) : Traite les assiduités
|
||||
s'étendant sur plus d'un jour.
|
||||
- compute_assiduites(assiduites: Query | list) : Calcule les métriques pour
|
||||
une collection d'assiduités.
|
||||
- to_dict() : Retourne les métriques sous forme de dictionnaire.
|
||||
|
||||
Notes :
|
||||
@ -85,27 +101,20 @@ class CountCalculator:
|
||||
evening: str = None,
|
||||
nb_heures_par_jour: int = None,
|
||||
) -> None:
|
||||
# Transformation d'une heure "HH:MM" en time(h,m)
|
||||
STR_TIME = lambda x: time(*list(map(int, x.split(":"))))
|
||||
|
||||
self.morning: time = STR_TIME(
|
||||
self.morning: time = str_to_time(
|
||||
morning if morning else ScoDocSiteConfig.get("assi_morning_time", "08:00")
|
||||
)
|
||||
# Date pivot pour déterminer les demi-journées
|
||||
self.noon: time = STR_TIME(
|
||||
self.noon: time = str_to_time(
|
||||
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")
|
||||
)
|
||||
|
||||
self.non_work_days: list[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)
|
||||
self.non_work_days: list[
|
||||
scu.NonWorkDays
|
||||
] = scu.NonWorkDays.get_all_non_work_days(dept_id=g.scodoc_dept_id)
|
||||
|
||||
# Sera utilisé pour les assiduités longues (> 1 journée)
|
||||
self.nb_heures_par_jour = (
|
||||
@ -340,17 +349,27 @@ class CountCalculator:
|
||||
|
||||
def setup_data(self):
|
||||
"""Met en forme les données
|
||||
pour les journées et les demi-journées : au lieu d'avoir list[str] on a le nombre (len(list[str]))
|
||||
pour les journées et les demi-journées :
|
||||
au lieu d'avoir list[str] on a le nombre (len(list[str]))
|
||||
"""
|
||||
for key in self.data:
|
||||
self.data[key]["journee"] = len(self.data[key]["journee"])
|
||||
self.data[key]["demi"] = len(self.data[key]["demi"])
|
||||
for value in self.data.values():
|
||||
value["journee"] = len(value["journee"])
|
||||
value["demi"] = len(value["demi"])
|
||||
|
||||
def to_dict(self, only_total: bool = True) -> dict[str, int | float]:
|
||||
"""Retourne les métriques sous la forme d'un dictionnaire"""
|
||||
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(
|
||||
assiduites: Query, metric: str = "all", filtered: dict[str, object] = None
|
||||
) -> dict[str, int | float]:
|
||||
@ -756,7 +775,6 @@ def invalidate_assiduites_etud_date(etudid: int, the_date: datetime):
|
||||
pour cet étudiant et cette date.
|
||||
Invalide cache absence et caches semestre
|
||||
"""
|
||||
from app.scodoc import sco_compute_moy
|
||||
|
||||
# Semestres a cette date:
|
||||
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}*"
|
||||
)
|
||||
# Invalide les tableaux "bilan dept"
|
||||
sco_cache.RequeteTableauAssiduiteCache.delete_pattern(pattern=f"tableau-dept*")
|
||||
sco_cache.RequeteTableauAssiduiteCache.delete_pattern(pattern="tableau-dept*")
|
||||
|
@ -130,7 +130,8 @@ def print_progress_bar(
|
||||
decimals - Optional : nombres de chiffres après la virgule (Int)
|
||||
length - Optional : taille de la barre en nombre de caractères (Int)
|
||||
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)))
|
||||
color = TerminalColor.RED
|
||||
@ -174,11 +175,15 @@ class BiDirectionalEnum(Enum):
|
||||
@classmethod
|
||||
def contains(cls, attr: str):
|
||||
"""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_
|
||||
|
||||
@classmethod
|
||||
def all(cls, keys=True):
|
||||
"""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())
|
||||
|
||||
@classmethod
|
||||
@ -207,6 +212,9 @@ class EtatAssiduite(int, BiDirectionalEnum):
|
||||
ABSENT = 2
|
||||
|
||||
def version_lisible(self) -> str:
|
||||
"""Retourne une version lisible des états d'assiduités
|
||||
Est utilisé pour les vues.
|
||||
"""
|
||||
return {
|
||||
EtatAssiduite.PRESENT: "Présence",
|
||||
EtatAssiduite.ABSENT: "Absence",
|
||||
@ -225,6 +233,9 @@ class EtatJustificatif(int, BiDirectionalEnum):
|
||||
MODIFIE = 3
|
||||
|
||||
def version_lisible(self) -> str:
|
||||
"""Retourne une version lisible des états de justificatifs
|
||||
Est utilisé pour les vues.
|
||||
"""
|
||||
return {
|
||||
EtatJustificatif.VALIDE: "valide",
|
||||
EtatJustificatif.ATTENTE: "soumis",
|
||||
@ -254,11 +265,13 @@ class NonWorkDays(int, BiDirectionalEnum):
|
||||
cls, formsemestre_id: int = None, dept_id: int = None
|
||||
) -> list["NonWorkDays"]:
|
||||
"""
|
||||
get_all_non_work_days Récupère la liste des non workdays (str) depuis les préférences
|
||||
get_all_non_work_days Récupère la liste des non workdays
|
||||
(str) depuis les préférences
|
||||
puis renvoie une liste BiDirectionnalEnum<int> NonWorkDays
|
||||
|
||||
Example:
|
||||
non_work_days : list[NonWorkDays] = NonWorkDays.get_all_non_work_days(dept_id=g.scodoc_dept_id)
|
||||
non_work_days : list[NonWorkDays] =
|
||||
NonWorkDays.get_all_non_work_days(dept_id=g.scodoc_dept_id)
|
||||
if datetime.datetime.now().weekday() in non_work_days:
|
||||
print("Aujourd'hui est un jour non travaillé")
|
||||
|
||||
@ -269,6 +282,8 @@ class NonWorkDays(int, BiDirectionalEnum):
|
||||
Returns:
|
||||
list[NonWorkDays]: La liste des NonWorkDays en version BiDirectionnalEnum<int>
|
||||
"""
|
||||
# Import circulaire
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from app.scodoc import sco_preferences
|
||||
|
||||
return [
|
||||
|
@ -1,3 +1,8 @@
|
||||
"""
|
||||
Gestion des listes d'assiduités et justificatifs
|
||||
(affichage, pagination, filtrage, options d'affichage, tableaux)
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from flask import url_for
|
||||
@ -8,10 +13,18 @@ from sqlalchemy import desc, literal, union, asc
|
||||
from app import db, g
|
||||
from app.auth.models import User
|
||||
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.scodoc.sco_cache import RequeteTableauAssiduiteCache
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_preferences import get_preference
|
||||
|
||||
|
||||
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()`
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
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)
|
||||
self.total_pages = 1
|
||||
@ -195,6 +212,17 @@ class ListeAssiJusti(tb.Table):
|
||||
r = query_finale.all()
|
||||
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
|
||||
pagination: Pagination = self.paginer(r, no_pagination=self.no_pagination)
|
||||
self.total_pages = pagination.total_pages
|
||||
@ -212,15 +240,17 @@ class ListeAssiJusti(tb.Table):
|
||||
attributs `page` et `NB_PAR_PAGE` de la classe `ListeAssiJusti`.
|
||||
|
||||
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.
|
||||
|
||||
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:
|
||||
Cette méthode ne modifie pas la collection originelle; elle renvoie plutôt un nouvel
|
||||
objet qui contient les résultats paginés.
|
||||
Cette méthode ne modifie pas la collection originelle;
|
||||
elle renvoie plutôt un nouvel objet qui contient les résultats paginés.
|
||||
"""
|
||||
return Pagination(
|
||||
collection,
|
||||
@ -232,29 +262,35 @@ class ListeAssiJusti(tb.Table):
|
||||
"""
|
||||
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
|
||||
et une pour les justificatifs, et renvoie une requête combinée qui sélectionne
|
||||
un ensemble spécifique de colonnes pour chaque type d'objet.
|
||||
Cette fonction prend en entrée deux requêtes optionnelles,
|
||||
une pour les assiduités et une pour les justificatifs,
|
||||
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:
|
||||
- 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
|
||||
- entry_date: la date de saisie de l'objet
|
||||
- date_debut: la date de début de l'objet
|
||||
- date_fin: la date de fin 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
|
||||
- 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:
|
||||
query_assiduite (sqlalchemy.orm.Query, optional): Une requête SQLAlchemy
|
||||
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
|
||||
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:
|
||||
sqlalchemy.orm.Query: Une requête combinée qui peut être exécutée pour
|
||||
@ -599,10 +635,15 @@ class AssiFiltre:
|
||||
|
||||
Args:
|
||||
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.
|
||||
date_debut (tuple[int, datetime], optional): (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.
|
||||
entry_date (tuple[int, datetime], optional):
|
||||
(0: egal, 1: avant, 2: après) + datetime(avec TZ). Defaults to None.
|
||||
date_debut (tuple[int, datetime], optional):
|
||||
(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}
|
||||
@ -637,7 +678,7 @@ class AssiFiltre:
|
||||
|
||||
type_filtrage, date = val_filtre
|
||||
|
||||
match (type_filtrage):
|
||||
match type_filtrage:
|
||||
# On garde uniquement les dates supérieures au filtre
|
||||
case 2:
|
||||
query_filtree = query_filtree.filter(
|
||||
@ -734,6 +775,10 @@ class AssiJustifData:
|
||||
|
||||
@staticmethod
|
||||
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.assiduites_query = Assiduite.query.filter(
|
||||
Assiduite.etudid.in_([e.etudid for e in etudiants])
|
||||
@ -745,4 +790,5 @@ class AssiJustifData:
|
||||
return data
|
||||
|
||||
def get(self) -> tuple[Query, Query]:
|
||||
"Renvoi les requêtes d'assiduités et justificatifs"
|
||||
return self.assiduites_query, self.justificatifs_query
|
||||
|
@ -37,7 +37,7 @@ class TableAssi(tb.Table):
|
||||
convert_values=False,
|
||||
**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"]
|
||||
self.dates = [str(dates[0]) + "T00:00", str(dates[1]) + "T23:59"]
|
||||
self.formsemestre = formsemestre
|
||||
|
@ -3,9 +3,9 @@
|
||||
Calendrier de l'assiduité
|
||||
{% endblock title %}
|
||||
{% block styles %}
|
||||
{{ super() }}
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/minitimeline.css">
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
|
||||
{{ super() }}
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/minitimeline.css">
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
|
||||
{% endblock styles %}
|
||||
|
||||
{% block app_content %}
|
||||
@ -15,379 +15,388 @@ Calendrier de l'assiduité
|
||||
<h2>Assiduité de {{sco.etud.html_link_fiche()|safe}}</h2>
|
||||
|
||||
<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" 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>
|
||||
<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" 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 class="calendrier">
|
||||
{% for mois,jours in calendrier.items() %}
|
||||
<div class="month">
|
||||
<h3>{{mois}}</h3>
|
||||
<div class="days {{'demi' if mode_demi else ''}}">
|
||||
{% for jour in jours %}
|
||||
{% if jour.is_non_work() %}
|
||||
<div class="day {{jour.get_class()}}">
|
||||
<span>{{jour.get_nom()}}</span>
|
||||
{% else %}
|
||||
<div class="day {{jour.get_class(show_pres, show_reta) if not mode_demi else ''}}">
|
||||
<div class="calendrier">
|
||||
{% for mois,jours in calendrier.items() %}
|
||||
<div class="month">
|
||||
<h3>{{mois}}</h3>
|
||||
<div class="days {{'demi' if mode_demi else ''}}">
|
||||
{% for jour in jours %}
|
||||
{% if jour.is_non_work() %}
|
||||
<div class="day {{jour.get_class()}}">
|
||||
<span>{{jour.get_nom()}}</span>
|
||||
{% else %}
|
||||
<div class="day {{jour.get_class(show_pres, show_reta) if not mode_demi else ''}}">
|
||||
{% endif %}
|
||||
{% if mode_demi %}
|
||||
{% if not jour.is_non_work() %}
|
||||
<span>{{jour.get_nom()}}</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>
|
||||
{% endif %}
|
||||
{% if not jour.is_non_work() %}
|
||||
<span>{{jour.get_nom()}}</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>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if not jour.is_non_work() %}
|
||||
<span>{{jour.get_nom(False)}}</span>
|
||||
{% endif %}
|
||||
{% if not jour.is_non_work() %}
|
||||
<span>{{jour.get_nom(False)}}</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if not jour.is_non_work() and jour.has_assiduites()%}
|
||||
|
||||
|
||||
<div class="dayline">
|
||||
<div class="dayline-title">
|
||||
<span>Assiduité du</span>
|
||||
<br>
|
||||
<span>{{jour.get_date()}}</span>
|
||||
{{jour.generate_minitimeline() | safe}}
|
||||
<span>Assiduité du</span>
|
||||
<br>
|
||||
<span>{{jour.get_date()}}</span>
|
||||
{{jour.generate_minitimeline() | safe}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="annee">
|
||||
<span id="label-annee">Année scolaire 2022-2023</span><span id="label-changer" style="margin-left: 5px;">Changer
|
||||
année: </span>
|
||||
<select name="" id="annee">
|
||||
</select>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="annee">
|
||||
<span id="label-annee">Année scolaire</span><span id="label-changer" style="margin-left: 5px;">Changer
|
||||
année: </span>
|
||||
<select name="" id="annee">
|
||||
</select>
|
||||
|
||||
<span id="label-nom">Assiduité de {{sco.etud.nomprenom}}</span>
|
||||
</div>
|
||||
<span id="label-nom">Assiduité de {{sco.etud.nomprenom}}</span>
|
||||
</div>
|
||||
|
||||
<div class="help">
|
||||
<h3>Calendrier</h3>
|
||||
<p>Code couleur</p>
|
||||
<ul class="couleurs">
|
||||
<li><span title="Vert" class="present demo"></span> → présence de l'étudiant lors de la période
|
||||
</li>
|
||||
<li><span title="Bleu clair" class="nonwork demo"></span> → la période n'est pas travaillée
|
||||
</li>
|
||||
<li><span title="Rouge" class="absent demo"></span> → absence de l'étudiant lors de la période
|
||||
</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 de l'étudiant lors de la période
|
||||
</li>
|
||||
<li><span title="Jaune clair" class="demo color retard est_just"></span> → retard justifié
|
||||
</li>
|
||||
<div class="help">
|
||||
<h3>Calendrier</h3>
|
||||
<p>Code couleur</p>
|
||||
<ul class="couleurs">
|
||||
<li><span title="Vert" class="present demo"></span> → présence de l'étudiant lors de la
|
||||
période
|
||||
</li>
|
||||
<li><span title="Bleu clair" class="nonwork demo"></span> → la période n'est pas travaillée
|
||||
</li>
|
||||
<li><span title="Rouge" class="absent demo"></span> → absence de l'étudiant lors de la
|
||||
période
|
||||
</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 de l'étudiant lors de la
|
||||
période
|
||||
</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> → la période est couverte par un
|
||||
<li><span title="Quart Bleu" class="est_just demo"></span> → la période est couverte par un
|
||||
justificatif valide</li>
|
||||
<li><span title="Justif. non valide" class="invalide demo"></span> → la période est
|
||||
couverte par un justificatif non valide
|
||||
</li>
|
||||
<li><span title="Justif. en attente" class="attente demo"></span> → 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>
|
||||
<li><span title="Justif. non valide" class="invalide demo"></span> → la période est
|
||||
couverte par un justificatif non valide
|
||||
</li>
|
||||
<li><span title="Justif. en attente" class="attente demo"></span> → la période
|
||||
a un justificatif en attente de validation
|
||||
<li><span title="Quart Violet" class="invalide demo"></span> justificatif non valide
|
||||
</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>
|
||||
<li><span title="Quart Violet" class="invalide demo"></span> justificatif non valide
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.help .couleurs {
|
||||
grid-template-columns: 2;
|
||||
grid-template-rows: auto;
|
||||
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 {
|
||||
<style>
|
||||
.help .couleurs {
|
||||
grid-template-columns: 2;
|
||||
grid-template-rows: auto;
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.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;
|
||||
.help .couleurs li:nth-child(odd) {
|
||||
grid-column: 1;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
#gtrcontent {
|
||||
margin: 5px;
|
||||
.help .couleurs li:nth-child(even) {
|
||||
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;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
border: 1px solid #444;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
</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,
|
||||
.month h3 {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
document.querySelectorAll('input[type="checkbox"].memo, #annee').forEach(el => {
|
||||
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}`);
|
||||
|
||||
.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 {
|
||||
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>
|
||||
{% endblock app_content %}
|
||||
document.querySelectorAll('input[type="checkbox"].memo, #annee').forEach(el => {
|
||||
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 %}
|
@ -25,8 +25,10 @@
|
||||
##############################################################################
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from flask import g, request, render_template, flash
|
||||
from flask import abort, url_for, redirect, Response
|
||||
@ -121,7 +123,6 @@ def bilan_dept():
|
||||
if formsemestre_id:
|
||||
try:
|
||||
formsemestre: FormSemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
annee = formsemestre.annee_scolaire()
|
||||
except AttributeError:
|
||||
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
|
||||
# auxquels est inscrit l'étudiant pour peupler le menu "module"
|
||||
modimpls_by_formsemestre = etud.get_modimpls_by_formsemestre(scu.annee_scolaire())
|
||||
choices = {
|
||||
"": [("", "Non spécifié"), ("autre", "Autre module (pas dans la liste)")]
|
||||
}
|
||||
choices: OrderedDict = OrderedDict()
|
||||
choices[""] = [("", "Non spécifié"), ("autre", "Autre module (pas dans la liste)")]
|
||||
for formsemestre_id in modimpls_by_formsemestre:
|
||||
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
||||
|
||||
# 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 ''}")
|
||||
for m in modimpls_by_formsemestre[formsemestre_id]
|
||||
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
|
||||
|
||||
if form.validate_on_submit():
|
||||
@ -532,7 +538,7 @@ def bilan_etud():
|
||||
# Récupération des assiduités et justificatifs de l'étudiant
|
||||
data = liste_assi.AssiJustifData(
|
||||
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(
|
||||
Justificatif.etat.in_(
|
||||
@ -825,17 +831,19 @@ def calendrier_assi_etud():
|
||||
# Récupération des années d'étude de l'étudiant
|
||||
annees: list[int] = []
|
||||
for ins in etud.formsemestre_inscriptions:
|
||||
date_deb = ins.formsemestre.date_debut
|
||||
date_fin = ins.formsemestre.date_fin
|
||||
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)
|
||||
|
||||
# Transformation en une liste "json"
|
||||
# (sera utilisé pour générer le selecteur d'année)
|
||||
annees_str: str = "["
|
||||
for ann in annees:
|
||||
annees_str += f"{ann},"
|
||||
annees_str += "]"
|
||||
annees_str: str = json.dumps(annees)
|
||||
|
||||
calendrier: dict[str, list["Jour"]] = generate_calendar(etud, annee)
|
||||
|
||||
@ -857,6 +865,15 @@ def calendrier_assi_etud():
|
||||
@scodoc
|
||||
@permission_required(Permission.AbsChange)
|
||||
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: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
|
||||
@ -973,9 +990,6 @@ def signal_assiduites_group():
|
||||
if formsemestre.dept_id != g.scodoc_dept_id:
|
||||
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
|
||||
etuds = [
|
||||
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:
|
||||
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)
|
||||
etuds = [
|
||||
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))
|
||||
|
||||
# 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:
|
||||
gr_tit = "en"
|
||||
else:
|
||||
@ -2075,8 +2070,8 @@ def _differee(
|
||||
etudiants (list[dict]): la liste des étudiants (représentés par des dictionnaires)
|
||||
moduleimpl_select (str): l'html représentant le selecteur de module
|
||||
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.
|
||||
formsemestre_id (int, optional): l'id du semestre pour le selecteur de module. 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.
|
||||
|
||||
Returns:
|
||||
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é.
|
||||
"""
|
||||
modimpls_by_formsemestre = etud.get_modimpls_by_formsemestre(scu.annee_scolaire())
|
||||
choices = {}
|
||||
choices = OrderedDict()
|
||||
for formsemestre_id in modimpls_by_formsemestre:
|
||||
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
||||
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 formsemestre.est_courant():
|
||||
choices.move_to_end(formsemestre.titre_annee(), last=False)
|
||||
|
||||
return render_template(
|
||||
"assiduites/widgets/moduleimpl_selector_multiple.j2",
|
||||
choices=choices,
|
||||
@ -2262,6 +2260,9 @@ def generate_calendar(
|
||||
etudiant: Identite,
|
||||
annee: int = None,
|
||||
) -> 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
|
||||
if annee is None:
|
||||
annee = scu.annee_scolaire()
|
||||
@ -2309,13 +2310,26 @@ class Jour:
|
||||
self.justificatifs = justificatifs
|
||||
|
||||
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()
|
||||
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:
|
||||
"""
|
||||
Renvoie la date du jour au format "dd/mm/yyyy"
|
||||
"""
|
||||
return self.date.strftime("%d/%m/%Y")
|
||||
|
||||
def get_class(self, show_pres: bool = False, show_reta: bool = False) -> str:
|
||||
"""
|
||||
Retourne la classe css du jour (mode normal)
|
||||
"""
|
||||
etat = ""
|
||||
est_just = ""
|
||||
|
||||
@ -2337,13 +2351,16 @@ class Jour:
|
||||
def get_demi_class(
|
||||
self, matin: bool, show_pres: bool = False, show_reta: bool = False
|
||||
) -> 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:
|
||||
heure_matin = str2time(ScoDocSiteConfig.get("assi_morning_time", "08:00"))
|
||||
heure_matin = scass.str_to_time(
|
||||
ScoDocSiteConfig.get("assi_morning_time", "08:00")
|
||||
)
|
||||
matin = (
|
||||
# date debut
|
||||
scu.localize_datetime(
|
||||
@ -2355,12 +2372,16 @@ class Jour:
|
||||
assiduites_matin = [
|
||||
assi
|
||||
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 = [
|
||||
justi
|
||||
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(
|
||||
@ -2375,7 +2396,9 @@ class Jour:
|
||||
|
||||
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
|
||||
aprem = (
|
||||
@ -2388,13 +2411,17 @@ class Jour:
|
||||
assiduites_aprem = [
|
||||
assi
|
||||
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 = [
|
||||
justi
|
||||
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(
|
||||
@ -2410,21 +2437,24 @@ class Jour:
|
||||
return f"color {etat} {est_just}"
|
||||
|
||||
def has_assiduites(self) -> bool:
|
||||
"""
|
||||
Renverra True si le jour a des assiduités
|
||||
"""
|
||||
return self.assiduites.count() > 0
|
||||
|
||||
def generate_minitimeline(self) -> str:
|
||||
"""
|
||||
Génère la minitimeline du jour
|
||||
"""
|
||||
# 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(":"))))
|
||||
)
|
||||
|
||||
heure_matin: datetime.timedelta = str2time(
|
||||
heure_matin: datetime.timedelta = scass.str_to_time(
|
||||
ScoDocSiteConfig.get("assi_morning_time", "08:00")
|
||||
)
|
||||
heure_midi: datetime.timedelta = str2time(
|
||||
ScoDocSiteConfig.get("assi_lun_time", "13:00")
|
||||
)
|
||||
heure_soir: datetime.timedelta = str2time(
|
||||
heure_soir: datetime.timedelta = scass.str_to_time(
|
||||
ScoDocSiteConfig.get("assi_afternoon_time", "17:00")
|
||||
)
|
||||
# longueur_timeline = heure_soir - heure_matin
|
||||
@ -2433,6 +2463,7 @@ class Jour:
|
||||
# chaque block d'assiduité est défini par:
|
||||
# longueur = ( (fin-deb) / 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]] = []
|
||||
|
||||
@ -2448,8 +2479,10 @@ class Jour:
|
||||
else heure_soir
|
||||
)
|
||||
|
||||
longueur: float = ((fin - deb) / 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()
|
||||
est_just: str = "est_just" if assi.est_just else ""
|
||||
|
||||
@ -2470,17 +2503,21 @@ class Jour:
|
||||
)
|
||||
|
||||
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(
|
||||
dept_id=g.scodoc_dept_id
|
||||
)
|
||||
|
||||
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(
|
||||
self, justificatifs: Query
|
||||
) -> 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(
|
||||
self,
|
||||
|
Loading…
Reference in New Issue
Block a user