forked from ScoDoc/ScoDoc
562 lines
20 KiB
Python
562 lines
20 KiB
Python
from datetime import datetime
|
||
|
||
from flask import url_for
|
||
from flask_sqlalchemy.query import Pagination, Query
|
||
from sqlalchemy import desc, literal, union
|
||
|
||
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.tables import table_builder as tb
|
||
|
||
|
||
class ListeAssiJusti(tb.Table):
|
||
"""
|
||
Table listant les Assiduites et Justificatifs d'une collection d'étudiants
|
||
L'affichage par défaut se fait par ordre de date de fin décroissante.
|
||
"""
|
||
|
||
NB_PAR_PAGE: int = 25
|
||
MAX_PAR_PAGE: int = 200
|
||
|
||
def __init__(
|
||
self,
|
||
table_data: "AssiJustifData",
|
||
filtre: "AssiFiltre" = None,
|
||
options: "AssiDisplayOptions" = None,
|
||
**kwargs,
|
||
) -> None:
|
||
"""
|
||
__init__ Instancie un nouveau table de liste d'assiduités/justificaitifs
|
||
|
||
Args:
|
||
filtre (Filtre, optional): Filtrage des objets à afficher. Defaults to None.
|
||
page (int, optional): numéro de page de la pagination. Defaults to 1.
|
||
"""
|
||
self.table_data: "AssiJustifData" = table_data
|
||
# Gestion du filtre, par défaut un filtre vide
|
||
self.filtre = filtre if filtre is not None else AssiFiltre()
|
||
|
||
# Gestion des options, par défaut un objet Options vide
|
||
self.options = options if options is not None else AssiDisplayOptions()
|
||
|
||
self.total_page: int = None
|
||
|
||
# les lignes du tableau
|
||
self.rows: list["RowAssiJusti"] = []
|
||
|
||
# Instanciation de la classe parent
|
||
super().__init__(
|
||
row_class=RowAssiJusti,
|
||
classes=["liste_assi", "gt_table", "gt_left"],
|
||
**kwargs,
|
||
with_foot_titles=False,
|
||
)
|
||
|
||
self.add_assiduites()
|
||
|
||
def add_assiduites(self):
|
||
"Ajoute le contenu de la table, avec assiduités et justificatif réunis"
|
||
# Générer les query assiduités et justificatifs
|
||
assiduites_query_etudiants: Query = None
|
||
justificatifs_query_etudiants: Query = None
|
||
|
||
# Récupération du filtrage des objets -> 0 : tout, 1 : Assi, 2: Justi
|
||
type_obj = self.filtre.type_obj()
|
||
|
||
if type_obj in [0, 1]:
|
||
assiduites_query_etudiants = self.table_data.assiduites_query
|
||
|
||
# Non affichage des présences
|
||
if not self.options.show_pres:
|
||
assiduites_query_etudiants = assiduites_query_etudiants.filter(
|
||
Assiduite.etat != EtatAssiduite.PRESENT
|
||
)
|
||
# Non affichage des retards
|
||
if not self.options.show_reta:
|
||
assiduites_query_etudiants = assiduites_query_etudiants.filter(
|
||
Assiduite.etat != EtatAssiduite.RETARD
|
||
)
|
||
|
||
if type_obj in [0, 2]:
|
||
justificatifs_query_etudiants = self.table_data.justificatifs_query
|
||
|
||
# Combinaison des requêtes
|
||
|
||
query_finale: Query = self.joindre(
|
||
query_assiduite=assiduites_query_etudiants,
|
||
query_justificatif=justificatifs_query_etudiants,
|
||
)
|
||
|
||
# Paginer la requête pour ne pas envoyer trop d'informations au client
|
||
pagination: Pagination = self.paginer(query_finale)
|
||
self.total_pages: int = pagination.pages
|
||
# Générer les lignes de la page
|
||
for ligne in pagination.items:
|
||
row: RowAssiJusti = self.row_class(self, ligne._asdict())
|
||
row.ajouter_colonnes()
|
||
self.add_row(row)
|
||
|
||
def paginer(self, query: Query) -> Pagination:
|
||
"""
|
||
Applique la pagination à une requête SQLAlchemy en fonction des paramètres de la classe.
|
||
|
||
Cette méthode prend une requête SQLAlchemy et applique la pagination en utilisant les
|
||
attributs `page` et `NB_PAR_PAGE` de la classe `ListeAssiJusti`.
|
||
|
||
Args:
|
||
query (Query): La requête SQLAlchemy à paginer. Il s'agit 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.
|
||
|
||
Note:
|
||
Cette méthode ne modifie pas la requête originale; elle renvoie plutôt un nouvel
|
||
objet qui contient les résultats paginés.
|
||
"""
|
||
return query.paginate(
|
||
page=self.options.page, per_page=self.options.nb_ligne_page, error_out=False
|
||
)
|
||
|
||
def joindre(self, query_assiduite: Query = None, query_justificatif: Query = None):
|
||
"""
|
||
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.
|
||
|
||
Les colonnes sélectionnées sont:
|
||
- 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)
|
||
- 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
|
||
|
||
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.
|
||
|
||
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.
|
||
|
||
Returns:
|
||
sqlalchemy.orm.Query: Une requête combinée qui peut être exécutée pour
|
||
obtenir les résultats.
|
||
|
||
Raises:
|
||
ValueError: Si aucune requête n'est fournie (les deux paramètres sont None).
|
||
"""
|
||
queries = []
|
||
|
||
# Définir les colonnes pour la requête d'assiduité
|
||
if query_assiduite:
|
||
assiduites_entities: list = [
|
||
Assiduite.assiduite_id.label("obj_id"),
|
||
Assiduite.etudid.label("etudid"),
|
||
Assiduite.entry_date.label("entry_date"),
|
||
Assiduite.date_debut.label("date_debut"),
|
||
Assiduite.date_fin.label("date_fin"),
|
||
Assiduite.etat.label("etat"),
|
||
literal("assiduite").label("type"),
|
||
Assiduite.est_just.label("est_just"),
|
||
Assiduite.user_id.label("user_id"),
|
||
]
|
||
|
||
if self.options.show_desc:
|
||
assiduites_entities.append(Assiduite.description.label("description"))
|
||
|
||
query_assiduite = query_assiduite.with_entities(*assiduites_entities)
|
||
queries.append(query_assiduite)
|
||
|
||
# Définir les colonnes pour la requête de justificatif
|
||
if query_justificatif:
|
||
justificatifs_entities: list = [
|
||
Justificatif.justif_id.label("obj_id"),
|
||
Justificatif.etudid.label("etudid"),
|
||
Justificatif.entry_date.label("entry_date"),
|
||
Justificatif.date_debut.label("date_debut"),
|
||
Justificatif.date_fin.label("date_fin"),
|
||
Justificatif.etat.label("etat"),
|
||
literal("justificatif").label("type"),
|
||
# On doit avoir les mêmes colonnes sur les deux requêtes,
|
||
# donc on la met en nul car un justifcatif ne peut être justifié
|
||
literal(None).label("est_just"),
|
||
Justificatif.user_id.label("user_id"),
|
||
]
|
||
|
||
if self.options.show_desc:
|
||
justificatifs_entities.append(Justificatif.raison.label("description"))
|
||
|
||
query_justificatif = query_justificatif.with_entities(
|
||
*justificatifs_entities
|
||
)
|
||
queries.append(query_justificatif)
|
||
|
||
# S'assurer qu'au moins une requête est fournie
|
||
if not queries:
|
||
raise ValueError(
|
||
"Au moins une query (assiduité ou justificatif) doit être fournie"
|
||
)
|
||
|
||
# Combiner les requêtes avec une union
|
||
query_combinee = union(*queries).alias("combinee")
|
||
|
||
query_combinee = db.session.query(query_combinee).order_by(desc("date_debut"))
|
||
|
||
return query_combinee
|
||
|
||
|
||
class RowAssiJusti(tb.Row):
|
||
"Ligne de table pour une assiduité"
|
||
|
||
def __init__(self, table: ListeAssiJusti, ligne: dict):
|
||
self.ligne: dict = ligne
|
||
self.etud: Identite = Identite.get_etud(ligne["etudid"])
|
||
|
||
super().__init__(
|
||
table=table,
|
||
row_id=f'{ligne["etudid"]}_{ligne["type"]}_{ligne["obj_id"]}',
|
||
)
|
||
|
||
def ajouter_colonnes(self, lien_redirection: str = None):
|
||
# Ajout colonne actions
|
||
if self.table.options.show_actions:
|
||
self._actions()
|
||
|
||
# Ajout de l'étudiant
|
||
self.table: ListeAssiJusti
|
||
if self.table.options.show_etu:
|
||
self._etud(lien_redirection)
|
||
|
||
# Type d'objet
|
||
self._type()
|
||
|
||
# Date de début
|
||
self.add_cell(
|
||
"date_debut",
|
||
"Date de début",
|
||
self.ligne["date_debut"].strftime("%d/%m/%y à %H:%M"),
|
||
data={"order": self.ligne["date_debut"]},
|
||
raw_content=self.ligne["date_debut"],
|
||
column_classes={"date"},
|
||
)
|
||
# Date de fin
|
||
self.add_cell(
|
||
"date_fin",
|
||
"Date de fin",
|
||
self.ligne["date_fin"].strftime("%d/%m/%y à %H:%M"),
|
||
raw_content=self.ligne["date_fin"],
|
||
data={"order": self.ligne["date_fin"]},
|
||
column_classes={"date"},
|
||
)
|
||
|
||
# Ajout des colonnes optionnelles
|
||
self._optionnelles()
|
||
|
||
# Ajout de l'utilisateur ayant saisi l'objet
|
||
self._utilisateur()
|
||
|
||
# Date de saisie
|
||
self.add_cell(
|
||
"entry_date",
|
||
"Saisie le",
|
||
self.ligne["entry_date"].strftime("%d/%m/%y à %H:%M"),
|
||
data={"order": self.ligne["entry_date"]},
|
||
raw_content=self.ligne["entry_date"],
|
||
classes=["small-font"],
|
||
column_classes={"entry_date"},
|
||
)
|
||
|
||
def _type(self) -> None:
|
||
obj_type: str = ""
|
||
is_assiduite: bool = self.ligne["type"] == "assiduite"
|
||
if is_assiduite:
|
||
self.classes.append("row-assiduite")
|
||
self.classes.append(EtatAssiduite(self.ligne["etat"]).name.lower())
|
||
etat: str = {
|
||
EtatAssiduite.PRESENT: "Présence",
|
||
EtatAssiduite.ABSENT: "Absence",
|
||
EtatAssiduite.RETARD: "Retard",
|
||
}.get(self.ligne["etat"])
|
||
justifiee: str = "Justifiée" if self.ligne["est_just"] else ""
|
||
obj_type = f"{etat} {justifiee}"
|
||
else:
|
||
self.classes.append("row-justificatif")
|
||
self.classes.append(EtatJustificatif(self.ligne["etat"]).name.lower())
|
||
etat: str = {
|
||
EtatJustificatif.VALIDE: "valide",
|
||
EtatJustificatif.ATTENTE: "soumis",
|
||
EtatJustificatif.MODIFIE: "modifié",
|
||
EtatJustificatif.NON_VALIDE: "invalide",
|
||
}.get(self.ligne["etat"])
|
||
obj_type = f"Justificatif {etat}"
|
||
|
||
self.add_cell("obj_type", "Type", obj_type, classes=["assi-type"])
|
||
|
||
def _etud(self, lien_redirection) -> None:
|
||
etud = self.etud
|
||
self.table.group_titles.update(
|
||
{
|
||
"etud_codes": "Codes",
|
||
"identite_detail": "",
|
||
"identite_court": "",
|
||
}
|
||
)
|
||
|
||
# Ajout des informations de l'étudiant
|
||
|
||
self.add_cell(
|
||
"nom_disp",
|
||
"Nom",
|
||
etud.nom_disp(),
|
||
"etudinfo",
|
||
attrs={"id": str(etud.id)},
|
||
data={"order": etud.sort_key},
|
||
target=lien_redirection,
|
||
target_attrs={"class": "discretelink"},
|
||
)
|
||
self.add_cell(
|
||
"prenom",
|
||
"Prénom",
|
||
etud.prenom_str,
|
||
"etudinfo",
|
||
attrs={"id": str(etud.id)},
|
||
data={"order": etud.sort_key},
|
||
target=lien_redirection,
|
||
target_attrs={"class": "discretelink"},
|
||
)
|
||
|
||
def _optionnelles(self) -> None:
|
||
if self.table.options.show_desc:
|
||
self.add_cell(
|
||
"description",
|
||
"Description",
|
||
self.ligne["description"] if self.ligne["description"] else "",
|
||
)
|
||
if self.table.options.show_module:
|
||
if self.ligne["type"] == "assiduite":
|
||
assi: Assiduite = Assiduite.query.get(self.ligne["obj_id"])
|
||
mod: str = assi.get_module(True)
|
||
self.add_cell("module", "Module", mod, data={"order": mod})
|
||
else:
|
||
self.add_cell("module", "Module", "", data={"order": ""})
|
||
|
||
def _utilisateur(self) -> None:
|
||
utilisateur: User = (
|
||
User.query.get(self.ligne["user_id"]) if self.ligne["user_id"] else None
|
||
)
|
||
|
||
self.add_cell(
|
||
"user",
|
||
"Saisie par",
|
||
"Inconnu" if utilisateur is None else utilisateur.get_nomprenom(),
|
||
classes=["small-font"],
|
||
)
|
||
|
||
def _actions(self) -> None:
|
||
url: str
|
||
html: list[str] = []
|
||
|
||
# Détails
|
||
url = url_for(
|
||
"assiduites.tableau_assiduite_actions",
|
||
type=self.ligne["type"],
|
||
action="details",
|
||
obj_id=self.ligne["obj_id"],
|
||
scodoc_dept=g.scodoc_dept,
|
||
)
|
||
html.append(f'<a title="Détails" href="{url}">ℹ️</a>')
|
||
|
||
# Modifier
|
||
url = url_for(
|
||
"assiduites.tableau_assiduite_actions",
|
||
type=self.ligne["type"],
|
||
action="modifier",
|
||
obj_id=self.ligne["obj_id"],
|
||
scodoc_dept=g.scodoc_dept,
|
||
)
|
||
html.append(f'<a title="Modifier" href="{url}">📝</a>')
|
||
|
||
# Supprimer
|
||
url = url_for(
|
||
"assiduites.tableau_assiduite_actions",
|
||
type=self.ligne["type"],
|
||
action="supprimer",
|
||
obj_id=self.ligne["obj_id"],
|
||
scodoc_dept=g.scodoc_dept,
|
||
)
|
||
html.append(f'<a title="Supprimer" href="{url}">❌</a>') # utiliser url_for
|
||
|
||
self.add_cell(
|
||
"actions",
|
||
"",
|
||
" ".join(html),
|
||
no_excel=True,
|
||
column_classes={"actions"},
|
||
)
|
||
|
||
|
||
class AssiFiltre:
|
||
"""
|
||
Classe représentant le filtrage qui sera appliqué aux objets
|
||
du Tableau `ListeAssiJusti`
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
type_obj: int = 0,
|
||
entry_date: tuple[int, datetime] = None,
|
||
date_debut: tuple[int, datetime] = None,
|
||
date_fin: tuple[int, datetime] = None,
|
||
) -> None:
|
||
"""
|
||
__init__ Instancie un nouvel objet filtre.
|
||
|
||
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.
|
||
"""
|
||
|
||
self.filtres = {"type_obj": type_obj}
|
||
|
||
if entry_date is not None:
|
||
self.filtres["entry_date"]: tuple[int, datetime] = entry_date
|
||
|
||
if date_debut is not None:
|
||
self.filtres["date_debut"]: tuple[int, datetime] = date_debut
|
||
|
||
if date_fin is not None:
|
||
self.filtres["date_fin"]: tuple[int, datetime] = date_fin
|
||
|
||
def filtrage(self, query: Query, obj_class: db.Model) -> Query:
|
||
"""
|
||
filtrage Filtre la query passée en paramètre et retourne l'objet filtré
|
||
|
||
Args:
|
||
query (Query): La query à filtrer
|
||
|
||
Returns:
|
||
Query: La query filtrée
|
||
"""
|
||
|
||
query_filtree: Query = query
|
||
|
||
cle_filtre: str
|
||
for cle_filtre, val_filtre in self.filtres.items():
|
||
if "date" in cle_filtre:
|
||
type_filtrage: int
|
||
date: datetime
|
||
|
||
type_filtrage, date = val_filtre
|
||
|
||
match (type_filtrage):
|
||
# On garde uniquement les dates supérieur au filtre
|
||
case 2:
|
||
query_filtree = query_filtree.filter(
|
||
getattr(obj_class, cle_filtre) > date
|
||
)
|
||
# On garde uniquement les dates inférieur au filtre
|
||
case 1:
|
||
query_filtree = query_filtree.filter(
|
||
getattr(obj_class, cle_filtre) < date
|
||
)
|
||
# Par défaut on garde uniquement les dates égales au filtre
|
||
case _:
|
||
query_filtree = query_filtree.filter(
|
||
getattr(obj_class, cle_filtre) == date
|
||
)
|
||
|
||
if cle_filtre == "etats":
|
||
etats: list[int | EtatJustificatif | EtatAssiduite] = val_filtre
|
||
# On garde uniquement les objets ayant un état compris dans le filtre
|
||
query_filtree = query_filtree.filter(obj_class.etat.in_(etats))
|
||
|
||
return query_filtree
|
||
|
||
def type_obj(self) -> int:
|
||
"""
|
||
type_obj Renvoi le/les types d'objets à représenter
|
||
|
||
(0:Tout, 1: Assi, 2:Justi)
|
||
|
||
Returns:
|
||
int: le/les types d'objets à afficher
|
||
"""
|
||
return self.filtres.get("type_obj", 0)
|
||
|
||
|
||
class AssiDisplayOptions:
|
||
"Options pour affichage tableau"
|
||
|
||
def __init__(
|
||
self,
|
||
page: int = 1,
|
||
nb_ligne_page: int = None,
|
||
show_pres: str | bool = False,
|
||
show_reta: str | bool = False,
|
||
show_desc: str | bool = False,
|
||
show_etu: str | bool = True,
|
||
show_actions: str | bool = True,
|
||
show_module: str | bool = False,
|
||
):
|
||
self.page: int = page
|
||
self.nb_ligne_page: int = nb_ligne_page
|
||
if self.nb_ligne_page is not None:
|
||
self.nb_ligne_page = min(nb_ligne_page, ListeAssiJusti.MAX_PAR_PAGE)
|
||
|
||
self.show_pres = to_bool(show_pres)
|
||
self.show_reta = to_bool(show_reta)
|
||
self.show_desc = to_bool(show_desc)
|
||
self.show_etu = to_bool(show_etu)
|
||
self.show_actions = to_bool(show_actions)
|
||
self.show_module = to_bool(show_module)
|
||
|
||
def remplacer(self, **kwargs):
|
||
"Positionnne options booléennes selon arguments"
|
||
for k, v in kwargs.items():
|
||
if k.startswith("show_"):
|
||
setattr(self, k, to_bool(v))
|
||
elif k in ["page", "nb_ligne_page"]:
|
||
setattr(self, k, int(v))
|
||
if k == "nb_ligne_page":
|
||
self.nb_ligne_page = min(
|
||
self.nb_ligne_page, ListeAssiJusti.MAX_PAR_PAGE
|
||
)
|
||
|
||
|
||
class AssiJustifData:
|
||
"Les assiduités et justificatifs"
|
||
|
||
def __init__(
|
||
self, assiduites_query: Query = None, justificatifs_query: Query = None
|
||
):
|
||
self.assiduites_query: Query = assiduites_query
|
||
self.justificatifs_query: Query = justificatifs_query
|
||
|
||
@staticmethod
|
||
def from_etudiants(*etudiants: Identite) -> "AssiJustifData":
|
||
data = AssiJustifData()
|
||
data.assiduites_query = Assiduite.query.filter(
|
||
Assiduite.etudid.in_([e.etudid for e in etudiants])
|
||
)
|
||
data.justificatifs_query = Justificatif.query.filter(
|
||
Justificatif.etudid.in_([e.etudid for e in etudiants])
|
||
)
|
||
|
||
return data
|
||
|
||
def get(self) -> tuple[Query, Query]:
|
||
return self.assiduites_query, self.justificatifs_query
|