Assiduite : modification listes WIP

This commit is contained in:
Iziram 2023-10-26 13:12:22 +02:00
parent 20d19a190d
commit ad32e27d7a
5 changed files with 400 additions and 6 deletions

View File

@ -0,0 +1,354 @@
from app.tables import table_builder as tb
from app.models import Identite, Assiduite, Justificatif
from datetime import datetime
from app.scodoc.sco_utils import EtatAssiduite, EtatJustificatif
from flask_sqlalchemy.query import Query, Pagination
from sqlalchemy import union, literal, select, desc
from app import db
from flask import url_for
from app import log
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 = 50
def __init__(
self,
*etudiants: tuple[Identite],
filtre: "Filtre" = None,
page: int = 1,
**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.etudiants = etudiants
# Gestion du filtre, par défaut un filtre vide
self.filtre = filtre if filtre is not None else Filtre()
# Gestion de la pagination (par défaut page 1)
self.page = page
# les lignes du tableau
self.rows: list["RowAssiJusti"] = []
# Instanciation de la classe parent
super().__init__(
row_class=RowAssiJusti,
classes=["gt_table", "gt_left"],
**kwargs,
with_foot_titles=False,
)
self.ajouter_lignes()
def ajouter_lignes(self):
# 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 = Assiduite.query.filter(
Assiduite.etudid.in_([e.etudid for e in self.etudiants])
)
if type_obj in [0, 2]:
justificatifs_query_etudiants = Justificatif.query.filter(
Justificatif.etudid.in_([e.etudid for e in self.etudiants])
)
# 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)
# 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.page, per_page=ListeAssiJusti.NB_PAR_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)
Args:
query_assiduite (sqlalchemy.orm.Query, optional): Une requête SQLAlchemy pour les assiduités.
Si None, aucune assiduité ne sera incluse dans la requête combinée. Defaults to None.
query_justificatif (sqlalchemy.orm.Query, optional): Une requête SQLAlchemy pour les justificatifs.
Si None, aucun justificatif ne sera inclus dans la requête combinée. Defaults to None.
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:
query_assiduite = query_assiduite.with_entities(
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"),
)
queries.append(query_assiduite)
# Définir les colonnes pour la requête de justificatif
if query_justificatif:
query_justificatif = query_justificatif.with_entities(
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"),
)
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):
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):
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"},
)
# Type d'objet
self.add_cell(
"type",
"Type",
self.ligne["type"].capitalize(),
)
# Etat de l'objet
objEnum: EtatAssiduite | EtatJustificatif = (
EtatAssiduite if self.ligne["type"] == "assiduite" else EtatJustificatif
)
self.add_cell(
"etat",
"État",
objEnum.inverse().get(self.ligne["etat"]).name.capitalize(),
)
# 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"]},
)
# Date de fin
self.add_cell(
"date_fin",
"Date de fin",
self.ligne["date_fin"].strftime("%d/%m/%y à %H:%M"),
data={"order": self.ligne["date_fin"]},
)
# 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"]},
)
class Filtre:
"""
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,
etats: list[EtatAssiduite | EtatJustificatif] = 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 = {}
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
if etats is not None:
self.filtres["etats"]: list[int | EtatJustificatif | EtatAssiduite] = etats
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)

View File

@ -17,7 +17,7 @@ from app.scodoc import sco_utils as scu
class TableAssi(tb.Table):
"""Table listant l'assiduité des étudiants
"""Table listant les statistiques d'assiduité des étudiants
L'id de la ligne est etuid, et le row stocke etud.
"""

View File

@ -0,0 +1,13 @@
{% extends "sco_page.j2" %}
{% block scripts %}
{{ super() }}
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
{% endblock %}
{% block app_content %}
{{tableau | safe}}
{% endblock %}

View File

@ -21,7 +21,7 @@
{{tableau | safe}}
<div class=""help">
<div class="help">
Les comptes sont exprimés en {{ assi_metric | lower}}s.
</div>

View File

@ -1014,6 +1014,33 @@ def visu_assi_group():
)
@bp.route("/Test")
@scodoc
@permission_required(Permission.ScoView)
def test():
"""Visualisation de l'assiduité d'un groupe entre deux dates"""
fmt = request.args.get("fmt", "html")
from app.tables.liste_assiduites import ListeAssiJusti
table: ListeAssiJusti = ListeAssiJusti(Identite.get_etud(18114))
if fmt.startswith("xls"):
return scu.send_file(
table.excel(),
filename=f"assiduite-{groups_infos.groups_filename}",
mime=scu.XLSX_MIMETYPE,
suffix=scu.XLSX_SUFFIX,
)
return render_template(
"assiduites/pages/test_assi.j2",
sco=ScoData(),
tableau=table.html(),
title=f"Test tableau",
)
@bp.route("/SignalAssiduiteDifferee")
@scodoc
@permission_required(Permission.AbsChange)