forked from ScoDoc/ScoDoc
Update opolka/ScoDoc from ScoDoc/ScoDoc #2
354
app/tables/liste_assiduites.py
Normal file
354
app/tables/liste_assiduites.py
Normal 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)
|
@ -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.
|
||||
"""
|
||||
|
||||
|
13
app/templates/assiduites/pages/test_assi.j2
Normal file
13
app/templates/assiduites/pages/test_assi.j2
Normal 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 %}
|
@ -21,7 +21,7 @@
|
||||
|
||||
{{tableau | safe}}
|
||||
|
||||
<div class=""help">
|
||||
<div class="help">
|
||||
Les comptes sont exprimés en {{ assi_metric | lower}}s.
|
||||
</div>
|
||||
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user