From ad32e27d7ac264bbb5ec43ae605a055a4c0fd01b Mon Sep 17 00:00:00 2001 From: Iziram Date: Thu, 26 Oct 2023 13:12:22 +0200 Subject: [PATCH 01/10] Assiduite : modification listes WIP --- app/tables/liste_assiduites.py | 354 ++++++++++++++++++++ app/tables/visu_assiduites.py | 2 +- app/templates/assiduites/pages/test_assi.j2 | 13 + app/templates/assiduites/pages/visu_assi.j2 | 10 +- app/views/assiduites.py | 27 ++ 5 files changed, 400 insertions(+), 6 deletions(-) create mode 100644 app/tables/liste_assiduites.py create mode 100644 app/templates/assiduites/pages/test_assi.j2 diff --git a/app/tables/liste_assiduites.py b/app/tables/liste_assiduites.py new file mode 100644 index 000000000..7492a4712 --- /dev/null +++ b/app/tables/liste_assiduites.py @@ -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) diff --git a/app/tables/visu_assiduites.py b/app/tables/visu_assiduites.py index 4ccbae8f4..4a48728fe 100644 --- a/app/tables/visu_assiduites.py +++ b/app/tables/visu_assiduites.py @@ -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. """ diff --git a/app/templates/assiduites/pages/test_assi.j2 b/app/templates/assiduites/pages/test_assi.j2 new file mode 100644 index 000000000..3291c1895 --- /dev/null +++ b/app/templates/assiduites/pages/test_assi.j2 @@ -0,0 +1,13 @@ +{% extends "sco_page.j2" %} + +{% block scripts %} +{{ super() }} + +{% endblock %} + +{% block app_content %} + + +{{tableau | safe}} + +{% endblock %} \ No newline at end of file diff --git a/app/templates/assiduites/pages/visu_assi.j2 b/app/templates/assiduites/pages/visu_assi.j2 index f947928f3..10af77956 100644 --- a/app/templates/assiduites/pages/visu_assi.j2 +++ b/app/templates/assiduites/pages/visu_assi.j2 @@ -1,8 +1,8 @@ {% extends "sco_page.j2" %} {% block scripts %} - {{ super() }} - +{{ super() }} + {% endblock %} {% block app_content %} @@ -21,8 +21,8 @@ {{tableau | safe}} -
-Les comptes sont exprimés en {{ assi_metric | lower}}s. +
+ Les comptes sont exprimés en {{ assi_metric | lower}}s.
-{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/app/views/assiduites.py b/app/views/assiduites.py index 81e734a9e..96f2b7802 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -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) From 990749486ac6f8bc83345f7d1b2771a7a9f86027 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 26 Oct 2023 11:09:22 +0200 Subject: [PATCH 02/10] Fix typo (bulletins legacy) --- app/scodoc/sco_bulletins_legacy.py | 9 +++++---- sco_version.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/scodoc/sco_bulletins_legacy.py b/app/scodoc/sco_bulletins_legacy.py index 04a630ca0..a2ac24752 100644 --- a/app/scodoc/sco_bulletins_legacy.py +++ b/app/scodoc/sco_bulletins_legacy.py @@ -32,7 +32,7 @@ Voir sco_bulletins_standard pour une version plus récente. CE FORMAT N'EVOLUERA PLUS ET EST CONSIDERE COMME OBSOLETE. - + """ from flask import g, url_for @@ -316,7 +316,8 @@ class BulletinGeneratorLegacy(sco_bulletins_generator.BulletinGenerator): # --- Absences H.append( f"""

- + Absences :{I['nbabs']} demi-journées, dont {I['nbabsjust']} justifiées (pendant ce semestre).

@@ -340,10 +341,10 @@ class BulletinGeneratorLegacy(sco_bulletins_generator.BulletinGenerator): for appreciation in appreciations: if can_edit_app: mlink = f"""modifier supprimer""" else: mlink = "" diff --git a/sco_version.py b/sco_version.py index 9f9457694..4d7ab1462 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.48" +SCOVERSION = "9.6.49" SCONAME = "ScoDoc" From 903a03dbd6ea33be48d205249233f8bb4523b082 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 26 Oct 2023 16:15:29 +0200 Subject: [PATCH 03/10] Fix delete etud and delete dept --- app/scodoc/sco_dept.py | 2 ++ app/scodoc/sco_preferences.py | 49 ++++++++++++++++++----------------- app/views/scolar.py | 48 ++++++++++++++++------------------ 3 files changed, 50 insertions(+), 49 deletions(-) diff --git a/app/scodoc/sco_dept.py b/app/scodoc/sco_dept.py index 189a87157..eacfe0439 100644 --- a/app/scodoc/sco_dept.py +++ b/app/scodoc/sco_dept.py @@ -405,6 +405,8 @@ def delete_dept(dept_id: int) -> str: "delete from scolar_news where dept_id = %(dept_id)s", "delete from notes_semset where dept_id = %(dept_id)s", "delete from notes_formations where dept_id = %(dept_id)s", + "delete from itemsuivi_tags where dept_id = %(dept_id)s", + "delete from identite where dept_id = %(dept_id)s", "delete from departement where id = %(dept_id)s", "drop table tags_temp", "drop table formations_temp", diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py index eb82de4c3..cab6feaee 100644 --- a/app/scodoc/sco_preferences.py +++ b/app/scodoc/sco_preferences.py @@ -385,11 +385,11 @@ class BasePreferences: "size": 40, "explanation": f"""adresse expéditeur pour tous les envois par mail (bulletins, notifications, etc.). Si vide, utilise la config globale. - Pour les comptes (mot de passe), voir la config globale accessible + Pour les comptes (mot de passe), voir la config globale accessible en tant qu'administrateur depuis la page d'accueil. - + """, "category": "misc", "only_global": True, @@ -419,8 +419,8 @@ class BasePreferences: { "initvalue": 0, "title": "BUT: moyenne générale sans les UE sans notes", - "explanation": """La moyenne générale indicative BUT est basée sur les moyennes d'UE pondérées par leurs ECTS. - Si cette option est cochée, ne prend pas en compte les UEs sans notes. Attention: changer ce réglage va modifier toutes + "explanation": """La moyenne générale indicative BUT est basée sur les moyennes d'UE pondérées par leurs ECTS. + Si cette option est cochée, ne prend pas en compte les UEs sans notes. Attention: changer ce réglage va modifier toutes les moyennes du semestre !. Aucun effet dans les formations non BUT.""", "input_type": "boolcheckbox", "category": "apc", @@ -583,16 +583,16 @@ class BasePreferences: "initvalue": """ --- Ceci est un message de notification automatique issu de ScoDoc --- - L'étudiant %(nomprenom)s - L'étudiant %(nomprenom)s - L'étudiant %(nomprenom)s - inscrit en %(inscription)s) - inscrit en %(inscription)s) - inscrit en %(inscription)s) + L'étudiant %(nomprenom)s + L'étudiant %(nomprenom)s + L'étudiant %(nomprenom)s + inscrit en %(inscription)s) + inscrit en %(inscription)s) + inscrit en %(inscription)s) - a cumulé %(nbabsjust)s absences justifiées - a cumulé %(nbabsjust)s absences justifiées - a cumulé %(nbabsjust)s absences justifiées + a cumulé %(nbabsjust)s absences justifiées + a cumulé %(nbabsjust)s absences justifiées + a cumulé %(nbabsjust)s absences justifiées et %(nbabsnonjust)s absences NON justifiées. Le compte a pu changer depuis cet envoi, voir la fiche sur %(url_ficheetud)s. @@ -626,10 +626,11 @@ class BasePreferences: "forcer_module", { "initvalue": 0, - "title": "Forcer la déclaration du module.", + "title": "Imposer la déclaration du module", "input_type": "boolcheckbox", "labels": ["non", "oui"], "category": "assi", + "explanation": "toute saisie d'absence doit indiquer le module concerné", }, ), # ( @@ -1047,17 +1048,17 @@ class BasePreferences: ( "PV_INTRO", { - "initvalue": """- + "initvalue": """- Vu l'arrêté du 3 août 2005 relatif au diplôme universitaire de technologie et notamment son article 4 et 6; - - - - - - + - + - + - vu l'arrêté n° %(Decnum)s du Président de l'%(UnivName)s; - - - - - - + - + - + - vu la délibération de la commission %(Type)s en date du %(Date)s présidée par le Chef du département; """, "title": """Paragraphe d'introduction sur le PV""", @@ -1206,9 +1207,9 @@ class BasePreferences: Le jury de %(type_jury_abbrv)s du département %(DeptName)s - s'est réuni le %(date_jury)s. - s'est réuni le %(date_jury)s. - s'est réuni le %(date_jury)s. + s'est réuni le %(date_jury)s. + s'est réuni le %(date_jury)s. + s'est réuni le %(date_jury)s. Les décisions vous concernant sont : diff --git a/app/views/scolar.py b/app/views/scolar.py index 02cd29840..0388c4c2e 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -42,6 +42,7 @@ from flask_json import as_json from flask_login import current_user from flask_wtf import FlaskForm from flask_wtf.file import FileField, FileAllowed +import sqlalchemy as sa from wtforms import SubmitField import app @@ -1853,22 +1854,16 @@ def etud_copy_in_other_dept(etudid: int): ) -@bp.route("/etudident_delete", methods=["GET", "POST"]) +@bp.route("/etudident_delete/", methods=["GET", "POST"]) @scodoc @permission_required(Permission.EtudInscrit) @scodoc7func -def etudident_delete(etudid, dialog_confirmed=False): +def etudident_delete(etudid: int = -1, dialog_confirmed=False): "Delete a student" - cnx = ndb.GetDBConnexion() - etuds = sco_etud.etudident_list(cnx, {"etudid": etudid}) - if not etuds: - raise ScoValueError("Étudiant inexistant !") - else: - etud = etuds[0] - sco_etud.fill_etuds_info([etud]) + etud = Identite.get_etud(etudid) if not dialog_confirmed: return scu.confirm_dialog( - """

Confirmer la suppression de l'étudiant {e[nomprenom]} ?

+ f"""

Confirmer la suppression de l'étudiant {etud.nomprenom} ?

Prenez le temps de vérifier que vous devez vraiment supprimer cet étudiant ! @@ -1877,16 +1872,13 @@ def etudident_delete(etudid, dialog_confirmed=False): efface toute trace de l'étudiant: inscriptions, notes, absences... dans tous les semestres qu'il a fréquenté.

-

Dans la plupart des cas, vous avez seulement besoin de le

    désinscrire
- d'un semestre ? (dans ce cas passez par sa fiche, menu associé au semestre)

+

Dans la plupart des cas, vous avez seulement besoin de le désinscrire + d'un semestre ! (pour cela, passez par sa fiche, menu associé au semestre)

-

Vérifier la fiche de {e[nomprenom]} -

""".format( - e=etud, - fiche_url=url_for( +

Vérifier la fiche de {etud.nomprenom} +

""", dest_url="", cancel_url=url_for( "scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid @@ -1894,13 +1886,19 @@ def etudident_delete(etudid, dialog_confirmed=False): OK="Supprimer définitivement cet étudiant", parameters={"etudid": etudid}, ) - log("etudident_delete: etudid=%(etudid)s nomprenom=%(nomprenom)s" % etud) + log(f"etudident_delete: {etud}") + formsemestre_ids_to_inval = [ + ins.formsemestre_id for ins in etud.formsemestre_inscriptions + ] + # delete in all tables ! # c'est l'ancienne façon de gérer les cascades dans notre pseudo-ORM :) tables = [ "notes_appreciations", "scolar_autorisation_inscription", "scolar_formsemestre_validation", + "apc_validation_rcue", + "apc_validation_annee", "scolar_events", "notes_notes_log", "notes_notes", @@ -1914,14 +1912,14 @@ def etudident_delete(etudid, dialog_confirmed=False): "absences_notifications", "billet_absence", ] - cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) for table in tables: - cursor.execute("delete from %s where etudid=%%(etudid)s" % table, etud) - cursor.execute("delete from identite where id=%(etudid)s", etud) - cnx.commit() + db.session.execute( + sa.text(f"""delete from {table} where etudid=:etudid"""), {"etudid": etudid} + ) + db.session.delete(etud) + db.session.commit() # Inval semestres où il était inscrit: - to_inval = [s["formsemestre_id"] for s in etud["sems"]] - for formsemestre_id in to_inval: + for formsemestre_id in formsemestre_ids_to_inval: sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id) flash("Étudiant supprimé !") return flask.redirect(scu.ScoURL()) From 5e0b9f95cc77b08f6a6bd16c576d619091fda2c1 Mon Sep 17 00:00:00 2001 From: Iziram Date: Thu, 26 Oct 2023 15:52:53 +0200 Subject: [PATCH 04/10] Assiduite : fixe forcer module Fix #798 --- app/api/assiduites.py | 28 +++++++- app/models/assiduites.py | 35 ++++++++- app/scodoc/sco_utils.py | 19 +++++ app/static/js/assiduites.js | 72 +++++++++++++++---- .../pages/signal_assiduites_etud.j2 | 2 +- .../widgets/moduleimpl_dynamic_selector.j2 | 9 ++- .../assiduites/widgets/moduleimpl_selector.j2 | 3 +- .../widgets/simplemoduleimpl_select.j2 | 6 ++ app/views/assiduites.py | 15 ++-- 9 files changed, 157 insertions(+), 32 deletions(-) create mode 100644 app/templates/assiduites/widgets/simplemoduleimpl_select.j2 diff --git a/app/api/assiduites.py b/app/api/assiduites.py index a9451ae73..2e0c06c6c 100644 --- a/app/api/assiduites.py +++ b/app/api/assiduites.py @@ -25,7 +25,11 @@ from app.models import ( Scolog, ) from flask_sqlalchemy.query import Query -from app.models.assiduites import get_assiduites_justif, get_justifs_from_date +from app.models.assiduites import ( + get_assiduites_justif, + get_justifs_from_date, + get_formsemestre_from_data, +) from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_permissions import Permission from app.scodoc.sco_utils import json_error @@ -694,6 +698,9 @@ def _delete_singular(assiduite_id: int, database): assiduite_unique: Assiduite = Assiduite.query.filter_by(id=assiduite_id).first() if assiduite_unique is None: return (404, "Assiduite non existante") + if g.scodoc_dept is None and assiduite_unique.etudiant.dept_id is not None: + # route sans département + set_sco_dept(assiduite_unique.etudiant.departement.acronym) ass_dict = assiduite_unique.to_dict() log(f"delete_assiduite: {assiduite_unique.etudiant.id} {assiduite_unique}") Scolog.logdb( @@ -800,6 +807,9 @@ def assiduites_edit(): def _edit_singular(assiduite_unique, data): + if g.scodoc_dept is None and assiduite_unique.etudiant.dept_id is not None: + # route sans département + set_sco_dept(assiduite_unique.etudiant.departement.acronym) errors: list[str] = [] # Vérifications de data @@ -835,7 +845,6 @@ def _edit_singular(assiduite_unique, data): external_data = external_data if external_data is not None else {} external_data["module"] = "Autre" assiduite_unique.external_data = external_data - else: try: moduleimpl = ModuleImpl.query.filter_by( @@ -854,7 +863,20 @@ def _edit_singular(assiduite_unique, data): else: assiduite_unique.moduleimpl_id = moduleimpl_id else: - assiduite_unique.moduleimpl_id = None + formsemestre: FormSemestre = get_formsemestre_from_data( + assiduite_unique.to_dict() + ) + force: bool + + if formsemestre: + force = scu.is_assiduites_module_forced(formsemestre_id=formsemestre.id) + else: + force = scu.is_assiduites_module_forced(dept_id=etud.dept_id) + + if force: + errors.append( + "param 'moduleimpl_id' : le moduleimpl_id ne peut pas être nul" + ) # Cas 3 : desc desc = data.get("desc", False) diff --git a/app/models/assiduites.py b/app/models/assiduites.py index a78e709df..57b7dfb77 100644 --- a/app/models/assiduites.py +++ b/app/models/assiduites.py @@ -4,7 +4,7 @@ from datetime import datetime from app import db, log -from app.models import ModuleImpl, Scolog +from app.models import ModuleImpl, Scolog, FormSemestre, FormSemestreInscription from app.models.etudiants import Identite from app.auth.models import User from app.scodoc import sco_abs_notification @@ -13,6 +13,7 @@ from app.scodoc.sco_utils import ( EtatAssiduite, EtatJustificatif, localize_datetime, + is_assiduites_module_forced, ) from flask_sqlalchemy.query import Query @@ -162,6 +163,23 @@ class Assiduite(db.Model): moduleimpl_id = moduleimpl.id else: raise ScoValueError("L'étudiant n'est pas inscrit au module") + elif not ( + external_data is not None and external_data.get("module") is not None + ): + # Vérification si module forcé + formsemestre: FormSemestre = get_formsemestre_from_data( + {"etudid": etud.id, "date_debut": date_debut, "date_fin": date_fin} + ) + force: bool + + if formsemestre: + force = is_assiduites_module_forced(formsemestre_id=formsemestre.id) + else: + force = is_assiduites_module_forced(dept_id=etud.dept_id) + + if force: + raise ScoValueError("Module non renseigné") + nouv_assiduite = Assiduite( date_debut=date_debut, date_fin=date_fin, @@ -413,3 +431,18 @@ def get_justifs_from_date( justifs = justifs.filter(Justificatif.etat == EtatJustificatif.VALIDE) return [j.justif_id if not long else j.to_dict(True) for j in justifs] + + +def get_formsemestre_from_data(data: dict[str, datetime | int]) -> FormSemestre: + return ( + FormSemestre.query.join( + FormSemestreInscription, + FormSemestre.id == FormSemestreInscription.formsemestre_id, + ) + .filter( + data["date_debut"] <= FormSemestre.date_fin, + data["date_fin"] >= FormSemestre.date_debut, + FormSemestreInscription.etudid == data["etudid"], + ) + .first() + ) diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index bbf785a7c..6faad0fbd 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -1448,3 +1448,22 @@ def is_entreprises_enabled(): from app.models import ScoDocSiteConfig return ScoDocSiteConfig.is_entreprises_enabled() + + +def is_assiduites_module_forced( + formsemestre_id: int = None, dept_id: int = None +) -> bool: + from app.scodoc import sco_preferences + + retour: bool + + if dept_id is None: + dept_id = g.scodoc_dept_id + + try: + retour = sco_preferences.get_preference( + "forcer_module", formsemestre_id=int(formsemestre_id) + ) + except (TypeError, ValueError): + retour = sco_preferences.get_preference("forcer_module", dept_id=dept_id) + return retour diff --git a/app/static/js/assiduites.js b/app/static/js/assiduites.js index ecc2ce516..74c0c4b9c 100644 --- a/app/static/js/assiduites.js +++ b/app/static/js/assiduites.js @@ -84,19 +84,19 @@ function validateSelectors(btn) { ); }); - // if (getModuleImplId() == null && window.forceModule) { - // const HTML = ` - //

Attention, le module doit obligatoirement être renseigné.

- //

Cela vient de la configuration du semestre ou plus largement du département.

- //

Si c'est une erreur, veuillez voir avec le ou les responsables de votre scodoc.

- // `; + if (getModuleImplId() == null && window.forceModule) { + const HTML = ` +

Attention, le module doit obligatoirement être renseigné.

+

Cela vient de la configuration du semestre ou plus largement du département.

+

Si c'est une erreur, veuillez voir avec le ou les responsables de votre scodoc.

+ `; - // const content = document.createElement("div"); - // content.innerHTML = HTML; + const content = document.createElement("div"); + content.innerHTML = HTML; - // openAlertModal("Sélection du module", content); - // return; - // } + openAlertModal("Sélection du module", content); + return; + } getAssiduitesFromEtuds(true); @@ -905,6 +905,9 @@ function createAssiduite(etat, etudid) { } const path = getUrl() + `/api/assiduite/${etudid}/create`; + + let with_errors = false; + sync_post( path, [assiduite], @@ -913,14 +916,31 @@ function createAssiduite(etat, etudid) { if (data.success.length > 0) { let obj = data.success["0"].message.assiduite_id; } + if (data.errors.length > 0) { + console.error(data.errors["0"].message); + if (data.errors["0"].message == "Module non renseigné") { + const HTML = ` +

Attention, le module doit obligatoirement être renseigné.

+

Cela vient de la configuration du semestre ou plus largement du département.

+

Si c'est une erreur, veuillez voir avec le ou les responsables de votre scodoc.

+ `; + + const content = document.createElement("div"); + content.innerHTML = HTML; + + openAlertModal("Sélection du module", content); + } + with_errors = true; + } }, (data, status) => { //error console.error(data, status); errorAlert(); + with_errors = true; } ); - return true; + return !with_errors; } /** @@ -1000,7 +1020,33 @@ function editAssiduite(assiduite_id, etat, assi) { (data, status) => { //error console.error(data, status); - errorAlert(); + try { + errorJson = data.responseJSON; + if (errorJson.message == "param 'moduleimpl_id': etud non inscrit") { + const html = ` +

L'étudiant n'est pas inscrit à ce module

+ `; + const div = document.createElement("div"); + div.innerHTML = html; + openAlertModal("Erreur Module", div); + return; + } + if ( + errorJson.message == + "param 'moduleimpl_id' : le moduleimpl_id ne peut pas être nul" + ) { + const html = ` +

Un module doit être spécifié

+ `; + const div = document.createElement("div"); + div.innerHTML = html; + openAlertModal("Erreur Module", div); + return; + } + } catch (e) { + console.error(e); + //errorAlert(); + } } ); diff --git a/app/templates/assiduites/pages/signal_assiduites_etud.j2 b/app/templates/assiduites/pages/signal_assiduites_etud.j2 index 7a55c7a9e..78f45d789 100644 --- a/app/templates/assiduites/pages/signal_assiduites_etud.j2 +++ b/app/templates/assiduites/pages/signal_assiduites_etud.j2 @@ -16,7 +16,7 @@
- {{moduleimpl_select | safe }} + {% include "assiduites/widgets/moduleimpl_dynamic_selector.j2" %}
diff --git a/app/templates/assiduites/widgets/moduleimpl_dynamic_selector.j2 b/app/templates/assiduites/widgets/moduleimpl_dynamic_selector.j2 index 045546d01..ffd25464c 100644 --- a/app/templates/assiduites/widgets/moduleimpl_dynamic_selector.j2 +++ b/app/templates/assiduites/widgets/moduleimpl_dynamic_selector.j2 @@ -1,9 +1,12 @@ @@ -70,7 +73,7 @@ function populateSelect(sems, selected, query) { const select = document.querySelector(query); - select.innerHTML = `` + select.innerHTML = document.getElementById('saved').innerHTML sems.forEach((mods, label) => { const optGrp = document.createElement('optgroup'); optGrp.label = label diff --git a/app/templates/assiduites/widgets/moduleimpl_selector.j2 b/app/templates/assiduites/widgets/moduleimpl_selector.j2 index ad010e2cd..78a08b226 100644 --- a/app/templates/assiduites/widgets/moduleimpl_selector.j2 +++ b/app/templates/assiduites/widgets/moduleimpl_selector.j2 @@ -1,7 +1,6 @@ - + {render_template("assiduites/widgets/simplemoduleimpl_select.j2")} """ - return HTMLBuilder( header, _mini_timeline(), @@ -356,7 +355,6 @@ def signal_assiduites_etud(): forcer_module=sco_preferences.get_preference( "forcer_module", dept_id=g.scodoc_dept_id ), - moduleimpl_select=_dynamic_module_selector(), diff=_differee( etudiants=[sco_etud.get_etud_info(etudid=etud.etudid, filled=True)[0]], moduleimpl_select=select, @@ -630,10 +628,7 @@ def signal_assiduites_group(): if formsemestre.dept_id != g.scodoc_dept_id: abort(404, "groupes inexistants dans ce département") - require_module = sco_preferences.get_preference( - "abs_require_module", formsemestre_id - ) - + require_module = sco_preferences.get_preference("forcer_module", formsemestre_id) etuds = [ sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0] for m in groups_infos.members @@ -1407,7 +1402,9 @@ def _module_selector( def _dynamic_module_selector(): - return render_template("assiduites/widgets/moduleimpl_dynamic_selector.j2") + return render_template( + "assiduites/widgets/moduleimpl_dynamic_selector.j2", + ) def _timeline(formsemestre_id=None) -> HTMLElement: From 1d64680c5566f0fd6929e2374b3ff4bc69b3a340 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 26 Oct 2023 16:44:09 +0200 Subject: [PATCH 05/10] WIP: reponses API assiduite avec json_error partout sauf pour _edit_singular --- app/api/assiduites.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/api/assiduites.py b/app/api/assiduites.py index 2e0c06c6c..6c2b63dfc 100644 --- a/app/api/assiduites.py +++ b/app/api/assiduites.py @@ -652,10 +652,7 @@ def _create_one( return (200, {"assiduite_id": nouv_assiduite.id}) except ScoValueError as excp: - return ( - 404, - excp.args[0], - ) + return json_error(404, message=excp.args[0]) @bp.route("/assiduite/delete", methods=["POST"]) @@ -697,7 +694,7 @@ def assiduite_delete(): def _delete_singular(assiduite_id: int, database): assiduite_unique: Assiduite = Assiduite.query.filter_by(id=assiduite_id).first() if assiduite_unique is None: - return (404, "Assiduite non existante") + return json_error(404, "Assiduite non existante") if g.scodoc_dept is None and assiduite_unique.etudiant.dept_id is not None: # route sans département set_sco_dept(assiduite_unique.etudiant.departement.acronym) From b28a257129f553686816ec35f8fe3a5dac2c2c0d Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 26 Oct 2023 17:28:36 +0200 Subject: [PATCH 06/10] Fix API (mostly revert previous commit) --- app/api/assiduites.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/api/assiduites.py b/app/api/assiduites.py index 6c2b63dfc..f5195f529 100644 --- a/app/api/assiduites.py +++ b/app/api/assiduites.py @@ -652,7 +652,8 @@ def _create_one( return (200, {"assiduite_id": nouv_assiduite.id}) except ScoValueError as excp: - return json_error(404, message=excp.args[0]) + # ici on utilise pas json_error car on doit renvoyer status, message + return 404, excp.args[0] @bp.route("/assiduite/delete", methods=["POST"]) @@ -691,10 +692,12 @@ def assiduite_delete(): return output -def _delete_singular(assiduite_id: int, database): +def _delete_singular(assiduite_id: int, database) -> tuple[int, str]: + """@iziram PLEASE COMMENT THIS F*CKING CODE""" assiduite_unique: Assiduite = Assiduite.query.filter_by(id=assiduite_id).first() if assiduite_unique is None: - return json_error(404, "Assiduite non existante") + # on ne peut pas utiliser json_error ici car on est déclaré (int, str) + return 404, "Assiduite non existante" if g.scodoc_dept is None and assiduite_unique.etudiant.dept_id is not None: # route sans département set_sco_dept(assiduite_unique.etudiant.departement.acronym) @@ -707,7 +710,7 @@ def _delete_singular(assiduite_id: int, database): ) database.session.delete(assiduite_unique) scass.simple_invalidate_cache(ass_dict) - return (200, "OK") + return 200, "OK" @bp.route("/assiduite//edit", methods=["POST"]) From 69e25952e3e1509ac2060c8d39f20ddfa2fe6c91 Mon Sep 17 00:00:00 2001 From: Iziram Date: Fri, 3 Nov 2023 14:57:22 +0100 Subject: [PATCH 07/10] fusion col type/etat + col utilisateur + etudiant_seul --- app/tables/liste_assiduites.py | 117 +++++++++++++++++++++++---------- app/views/assiduites.py | 8 ++- 2 files changed, 87 insertions(+), 38 deletions(-) diff --git a/app/tables/liste_assiduites.py b/app/tables/liste_assiduites.py index 7492a4712..89c030f42 100644 --- a/app/tables/liste_assiduites.py +++ b/app/tables/liste_assiduites.py @@ -1,5 +1,6 @@ from app.tables import table_builder as tb from app.models import Identite, Assiduite, Justificatif +from app.auth.models import User from datetime import datetime from app.scodoc.sco_utils import EtatAssiduite, EtatJustificatif from flask_sqlalchemy.query import Query, Pagination @@ -50,9 +51,11 @@ class ListeAssiJusti(tb.Table): self.ajouter_lignes() + def etudiant_seul(self) -> bool: + return len(self.etudiants) == 1 + def ajouter_lignes(self): # Générer les query assiduités et justificatifs - assiduites_query_etudiants: Query = None justificatifs_query_etudiants: Query = None @@ -121,6 +124,8 @@ class ListeAssiJusti(tb.Table): - 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. @@ -146,6 +151,8 @@ class ListeAssiJusti(tb.Table): 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"), ) queries.append(query_assiduite) @@ -159,6 +166,10 @@ class ListeAssiJusti(tb.Table): 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"), ) queries.append(query_justificatif) @@ -187,6 +198,67 @@ class RowAssiJusti(tb.Row): ) def ajouter_colonnes(self, lien_redirection: str = None): + # Ajout de l'étudiant + self.table: ListeAssiJusti + if not self.table.etudiant_seul(): + self._etud() + + # 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"]}, + ) + # 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"]}, + ) + + # Ajout de l'utilisateur ayant saisie l'objet + self._utilisateur() + + # Ajout colonne actions + self._actions() + + def _type(self) -> None: + obj_type: str = "" + is_assiduite: bool = self.ligne["type"] == "assiduite" + if is_assiduite: + 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: + 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) + + def _etud(self) -> None: etud = self.etud self.table.group_titles.update( { @@ -218,44 +290,19 @@ class RowAssiJusti(tb.Row): 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 - ) + + def _utilisateur(self) -> None: + utilisateur: User = User.query.get(self.ligne["user_id"]) self.add_cell( - "etat", - "État", - objEnum.inverse().get(self.ligne["etat"]).name.capitalize(), + "user", + "Saisie par", + "Inconnu" if utilisateur is None else utilisateur.get_nomprenom(), ) - # 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"]}, - ) + def _actions(self) -> None: + # XXX Ajouter une colonne avec les liens d'action (supprimer, modifier) + pass class Filtre: diff --git a/app/views/assiduites.py b/app/views/assiduites.py index 1c9a6f1b2..333117c25 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -1033,9 +1033,11 @@ def test(): """Visualisation de l'assiduité d'un groupe entre deux dates""" fmt = request.args.get("fmt", "html") + etudid = request.args.get("etudid", 18114) + from app.tables.liste_assiduites import ListeAssiJusti - table: ListeAssiJusti = ListeAssiJusti(Identite.get_etud(18114)) + table: ListeAssiJusti = ListeAssiJusti(Identite.get_etud(etudid)) if fmt.startswith("xls"): return scu.send_file( @@ -1286,12 +1288,12 @@ def generate_bul_list(etud: Identite, semestre: FormSemestre) -> str: assiduites, metric=metrique, filtered={"split": True} ) - # On sépare : + # On sépare : # - abs_j = absences justifiées # - abs_nj = absences non justifiées # - retards = les retards # - justifs = les justificatifs - + abs_j: list[str] = [ {"date": _get_date_str(assi.date_debut, assi.date_fin)} for assi in assiduites From c0253bd05d324fcba1f461592dc1975bdf2c3ab8 Mon Sep 17 00:00:00 2001 From: Iziram Date: Wed, 22 Nov 2023 16:49:13 +0100 Subject: [PATCH 08/10] Assiduites : WIP tableaux options --- app/tables/liste_assiduites.py | 14 +++++-- app/templates/assiduites/pages/test_assi.j2 | 31 ++++++++++++++++ app/views/assiduites.py | 41 +++++++++++++++++---- 3 files changed, 74 insertions(+), 12 deletions(-) diff --git a/app/tables/liste_assiduites.py b/app/tables/liste_assiduites.py index 89c030f42..329b43f4a 100644 --- a/app/tables/liste_assiduites.py +++ b/app/tables/liste_assiduites.py @@ -16,13 +16,14 @@ class ListeAssiJusti(tb.Table): L'affichage par défaut se fait par ordre de date de fin décroissante. """ - NB_PAR_PAGE: int = 50 + NB_PAR_PAGE: int = 2 def __init__( self, *etudiants: tuple[Identite], filtre: "Filtre" = None, page: int = 1, + nb_par_page: int = None, **kwargs, ) -> None: """ @@ -36,7 +37,12 @@ class ListeAssiJusti(tb.Table): # 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 + self.page: int = page + self.nb_par_page: int = ( + nb_par_page if nb_par_page is not None else ListeAssiJusti.NB_PAR_PAGE + ) + + self.total_page: int = None # les lignes du tableau self.rows: list["RowAssiJusti"] = [] @@ -80,7 +86,7 @@ class ListeAssiJusti(tb.Table): # 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()) @@ -106,7 +112,7 @@ class ListeAssiJusti(tb.Table): résultats paginés. """ return query.paginate( - page=self.page, per_page=ListeAssiJusti.NB_PAR_PAGE, error_out=False + page=self.page, per_page=self.nb_par_page, error_out=False ) def joindre(self, query_assiduite: Query = None, query_justificatif: Query = None): diff --git a/app/templates/assiduites/pages/test_assi.j2 b/app/templates/assiduites/pages/test_assi.j2 index 3291c1895..8b0cf8597 100644 --- a/app/templates/assiduites/pages/test_assi.j2 +++ b/app/templates/assiduites/pages/test_assi.j2 @@ -7,6 +7,37 @@ {% block app_content %} + + Options +
+ + {% if show_pres %} + + {% else %} + + {% endif %} + + + {% if show_reta %} + + {% else %} + + {% endif %} +
+ + + + + + +
+ +
+
{{tableau | safe}} diff --git a/app/views/assiduites.py b/app/views/assiduites.py index 74215069b..0a6f12c64 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -327,7 +327,6 @@ def signal_assiduites_etud(): ), assi_morning=ScoDocSiteConfig.get("assi_morning_time", "08:00"), assi_evening=ScoDocSiteConfig.get("assi_afternoon_time", "18:00"), - saisie_eval=saisie_eval, date_deb=date_deb, date_fin=date_fin, @@ -1045,18 +1044,39 @@ def visu_assi_group(): ) -@bp.route("/Test") +@bp.route("/testTableau") @scodoc @permission_required(Permission.ScoView) -def test(): +def testTableau(): """Visualisation de l'assiduité d'un groupe entre deux dates""" - fmt = request.args.get("fmt", "html") - etudid = request.args.get("etudid", 18114) + etudid = request.args.get( + "etudid", 18114 + ) # TODO retirer la valeur par défaut de test + + fmt = request.args.get("fmt", "html") + show_pres: bool | str = request.args.get("show_pres", False) + show_reta: bool | str = request.args.get("show_reta", False) + + nb_ligne_page: int = request.args.get("nb_ligne_page") + # Vérification de nb_ligne_page + try: + nb_ligne_page: int = int(nb_ligne_page) + except (ValueError, TypeError): + nb_ligne_page = None + + page_number: int = request.args.get("n_page", 1) + # Vérification de page_number + try: + page_number: int = int(page_number) + except (ValueError, TypeError): + page_number = 1 from app.tables.liste_assiduites import ListeAssiJusti - table: ListeAssiJusti = ListeAssiJusti(Identite.get_etud(etudid)) + table: ListeAssiJusti = ListeAssiJusti( + Identite.get_etud(etudid), page=page_number, nb_par_page=nb_ligne_page + ) if fmt.startswith("xls"): return scu.send_file( @@ -1071,6 +1091,11 @@ def test(): sco=ScoData(), tableau=table.html(), title=f"Test tableau", + total_pages=table.total_pages, + page_number=page_number, + show_pres=show_pres, + show_reta=show_reta, + nb_ligne_page=nb_ligne_page, ) @@ -1354,10 +1379,10 @@ def generate_bul_list(etud: Identite, semestre: FormSemestre) -> str: ) -@bp.route("/test", methods=["GET", "POST"]) +@bp.route("/testDate", methods=["GET", "POST"]) @scodoc @permission_required(Permission.ScoView) -def test(): +def testDateutils(): """XXX fonction de test a retirer""" if request.method == "POST": print("test date_utils : ", request.form) From b13e751e1a9d41b043186b41e09ad4bbff207f81 Mon Sep 17 00:00:00 2001 From: Iziram Date: Fri, 24 Nov 2023 13:58:03 +0100 Subject: [PATCH 09/10] Assiduites : WIP tableaux actions (sauf modifier) --- app/models/assiduites.py | 74 +++++- app/scodoc/sco_utils.py | 16 ++ app/static/js/date_utils.js | 7 + app/tables/liste_assiduites.py | 200 ++++++++++++--- app/tables/table_builder.py | 19 +- .../assiduites/pages/ajout_assiduites.j2 | 7 +- .../assiduites/pages/tableau_actions.j2 | 27 +++ app/templates/assiduites/pages/test_assi.j2 | 44 ---- .../widgets/moduleimpl_dynamic_selector.j2 | 8 +- .../assiduites/widgets/moduleimpl_selector.j2 | 2 + .../widgets/simplemoduleimpl_select.j2 | 10 +- app/templates/assiduites/widgets/tableau.j2 | 69 ++++++ .../widgets/tableau_actions/details.j2 | 107 ++++++++ .../widgets/tableau_actions/modifier.j2 | 107 ++++++++ app/views/assiduites.py | 229 +++++++++++++++--- 15 files changed, 801 insertions(+), 125 deletions(-) create mode 100644 app/templates/assiduites/pages/tableau_actions.j2 delete mode 100644 app/templates/assiduites/pages/test_assi.j2 create mode 100644 app/templates/assiduites/widgets/tableau.j2 create mode 100644 app/templates/assiduites/widgets/tableau_actions/details.j2 create mode 100644 app/templates/assiduites/widgets/tableau_actions/modifier.j2 diff --git a/app/models/assiduites.py b/app/models/assiduites.py index a89e89b4f..47152e3b6 100644 --- a/app/models/assiduites.py +++ b/app/models/assiduites.py @@ -3,8 +3,8 @@ """ from datetime import datetime -from app import db, log -from app.models import ModuleImpl, Scolog, FormSemestre, FormSemestreInscription +from app import db, log, g +from app.models import ModuleImpl, Module, Scolog, FormSemestre, FormSemestreInscription from app.models.etudiants import Identite from app.auth.models import User from app.scodoc import sco_abs_notification @@ -204,6 +204,43 @@ class Assiduite(db.Model): sco_abs_notification.abs_notify(etud.id, nouv_assiduite.date_debut) return nouv_assiduite + def supprimer(self): + from app.scodoc import sco_assiduites as scass + + if g.scodoc_dept is None and self.etudiant.dept_id is not None: + # route sans département + set_sco_dept(self.etudiant.departement.acronym) + obj_dict: dict = self.to_dict() + # Suppression de l'objet et LOG + log(f"delete_assidutite: {self.etudiant.id} {self}") + Scolog.logdb( + method=f"delete_assiduite", + etudid=self.etudiant.id, + msg=f"Assiduité: {self}", + ) + db.session.delete(self) + # Invalidation du cache + scass.simple_invalidate_cache(obj_dict) + + def get_formsemestre(self) -> FormSemestre: + return get_formsemestre_from_data(self.to_dict()) + + def get_module(self, traduire: bool = False) -> int | str: + if self.moduleimpl_id is not None: + if traduire: + modimpl: ModuleImpl = ModuleImpl.query.get(self.moduleimpl_id) + mod: Module = Module.query.get(modimpl.module_id) + return f"{mod.code} {mod.titre}" + + elif self.external_data is not None and "module" in self.external_data: + return ( + "Tout module" + if self.external_data["module"] == "Autre" + else self.external_data["module"] + ) + + return "Non spécifié" if traduire else None + class Justificatif(db.Model): """ @@ -334,6 +371,39 @@ class Justificatif(db.Model): ) return nouv_justificatif + def supprimer(self): + from app.scodoc import sco_assiduites as scass + + # Récupération de l'archive du justificatif + archive_name: str = self.fichier + + if archive_name is not None: + # Si elle existe : on essaye de la supprimer + archiver: JustificatifArchiver = JustificatifArchiver() + try: + archiver.delete_justificatif(self.etudiant, archive_name) + except ValueError: + pass + if g.scodoc_dept is None and self.etudiant.dept_id is not None: + # route sans département + set_sco_dept(self.etudiant.departement.acronym) + # On invalide le cache + scass.simple_invalidate_cache(self.to_dict()) + # Suppression de l'objet et LOG + log(f"delete_justificatif: {self.etudiant.id} {self}") + Scolog.logdb( + method=f"delete_justificatif", + etudid=self.etudiant.id, + msg=f"Justificatif: {self}", + ) + db.session.delete(self) + # On actualise les assiduités justifiées de l'étudiant concerné + compute_assiduites_justified( + self.etudid, + Justificatif.query.filter_by(etudid=self.etudid).all(), + True, + ) + def is_period_conflicting( date_debut: datetime, diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index f93bf0526..309d180cf 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -204,6 +204,13 @@ class EtatAssiduite(int, BiDirectionalEnum): RETARD = 1 ABSENT = 2 + def version_lisible(self) -> str: + return { + EtatAssiduite.PRESENT: "Présence", + EtatAssiduite.ABSENT: "Absence", + EtatAssiduite.RETARD: "Retard", + }.get(self, "") + class EtatJustificatif(int, BiDirectionalEnum): """Code des états des justificatifs""" @@ -215,6 +222,14 @@ class EtatJustificatif(int, BiDirectionalEnum): ATTENTE = 2 MODIFIE = 3 + def version_lisible(self) -> str: + return { + EtatJustificatif.VALIDE: "valide", + EtatJustificatif.ATTENTE: "soumis", + EtatJustificatif.MODIFIE: "modifié", + EtatJustificatif.NON_VALIDE: "invalide", + }.get(self, "") + def is_iso_formated(date: str, convert=False) -> bool or datetime.datetime or None: """ @@ -1480,6 +1495,7 @@ def is_assiduites_module_forced( def get_assiduites_time_config(config_type: str) -> str: from app.models import ScoDocSiteConfig + match config_type: case "matin": return ScoDocSiteConfig.get("assi_morning_time", "08:00:00") diff --git a/app/static/js/date_utils.js b/app/static/js/date_utils.js index 05babeede..3565ea515 100644 --- a/app/static/js/date_utils.js +++ b/app/static/js/date_utils.js @@ -448,6 +448,13 @@ class ScoDocDateTimePicker extends HTMLElement { // Ajouter le style au shadow DOM shadow.appendChild(style); + + //Si une value est donnée + + let value = this.getAttribute("value"); + if (value != null) { + this.value = value; + } } static get observedAttributes() { diff --git a/app/tables/liste_assiduites.py b/app/tables/liste_assiduites.py index 329b43f4a..21c69cbf7 100644 --- a/app/tables/liste_assiduites.py +++ b/app/tables/liste_assiduites.py @@ -5,7 +5,7 @@ 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 app import db, g from flask import url_for from app import log @@ -16,14 +16,13 @@ class ListeAssiJusti(tb.Table): L'affichage par défaut se fait par ordre de date de fin décroissante. """ - NB_PAR_PAGE: int = 2 + NB_PAR_PAGE: int = 25 def __init__( self, - *etudiants: tuple[Identite], + table_data: "Data", filtre: "Filtre" = None, - page: int = 1, - nb_par_page: int = None, + options: "Options" = None, **kwargs, ) -> None: """ @@ -33,14 +32,12 @@ class ListeAssiJusti(tb.Table): 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 + self.table_data: "Data" = table_data # 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: int = page - self.nb_par_page: int = ( - nb_par_page if nb_par_page is not None else ListeAssiJusti.NB_PAR_PAGE - ) + + # Gestion des options, par défaut un objet Options vide + self.options = options if options is not None else Options() self.total_page: int = None @@ -57,9 +54,6 @@ class ListeAssiJusti(tb.Table): self.ajouter_lignes() - def etudiant_seul(self) -> bool: - return len(self.etudiants) == 1 - def ajouter_lignes(self): # Générer les query assiduités et justificatifs assiduites_query_etudiants: Query = None @@ -69,13 +63,21 @@ class ListeAssiJusti(tb.Table): 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]) - ) + 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 = Justificatif.query.filter( - Justificatif.etudid.in_([e.etudid for e in self.etudiants]) - ) + justificatifs_query_etudiants = self.table_data.justificatifs_query # Combinaison des requêtes @@ -112,7 +114,7 @@ class ListeAssiJusti(tb.Table): résultats paginés. """ return query.paginate( - page=self.page, per_page=self.nb_par_page, error_out=False + 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): @@ -149,7 +151,7 @@ class ListeAssiJusti(tb.Table): # Définir les colonnes pour la requête d'assiduité if query_assiduite: - query_assiduite = query_assiduite.with_entities( + assiduites_entities: list = [ Assiduite.assiduite_id.label("obj_id"), Assiduite.etudid.label("etudid"), Assiduite.entry_date.label("entry_date"), @@ -159,12 +161,17 @@ class ListeAssiJusti(tb.Table): 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: - query_justificatif = query_justificatif.with_entities( + justificatifs_entities: list = [ Justificatif.justif_id.label("obj_id"), Justificatif.etudid.label("etudid"), Justificatif.entry_date.label("entry_date"), @@ -176,6 +183,13 @@ class ListeAssiJusti(tb.Table): # 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) @@ -206,7 +220,7 @@ class RowAssiJusti(tb.Row): def ajouter_colonnes(self, lien_redirection: str = None): # Ajout de l'étudiant self.table: ListeAssiJusti - if not self.table.etudiant_seul(): + if self.table.options.show_etu: self._etud() # Type d'objet @@ -218,28 +232,37 @@ class RowAssiJusti(tb.Row): "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"], ) # 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"]}, ) + + # Ajout des colonnes optionnelles + self._optionnelles() + + # Ajout colonne actions + if self.table.options.show_actions: + self._actions() + + # Ajout de l'utilisateur ayant saisie 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"], ) - # Ajout de l'utilisateur ayant saisie l'objet - self._utilisateur() - - # Ajout colonne actions - self._actions() - def _type(self) -> None: obj_type: str = "" is_assiduite: bool = self.ligne["type"] == "assiduite" @@ -297,6 +320,21 @@ class RowAssiJusti(tb.Row): 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"]) @@ -304,11 +342,46 @@ class RowAssiJusti(tb.Row): "user", "Saisie par", "Inconnu" if utilisateur is None else utilisateur.get_nomprenom(), + classes=["small-font"], ) def _actions(self) -> None: - # XXX Ajouter une colonne avec les liens d'action (supprimer, modifier) - pass + 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'Détails') # utiliser url_for + + # 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'Modifier') # utiliser url_for + + # 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'Supprimer') # utiliser url_for + + self.add_cell( + "actions", "Actions", " ".join(html), raw_content="test", no_excel=True + ) class Filtre: @@ -323,7 +396,6 @@ class Filtre: 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. @@ -336,7 +408,7 @@ class Filtre: etats (list[int | EtatJustificatif | EtatAssiduite], optional): liste d'états valides (int | EtatJustificatif | EtatAssiduite). Defaults to None. """ - self.filtres = {} + self.filtres = {"type_obj": type_obj} if entry_date is not None: self.filtres["entry_date"]: tuple[int, datetime] = entry_date @@ -347,9 +419,6 @@ class Filtre: 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é @@ -405,3 +474,58 @@ class Filtre: int: le/les types d'objets à afficher """ return self.filtres.get("type_obj", 0) + + +class Options: + VRAI = ["on", "true", "t", "v", "vrai", True, 1] + + 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 + + self.show_pres: bool = show_pres in Options.VRAI + self.show_reta: bool = show_reta in Options.VRAI + self.show_desc: bool = show_desc in Options.VRAI + self.show_etu: bool = show_etu in Options.VRAI + self.show_actions: bool = show_actions in Options.VRAI + self.show_module: bool = show_module in Options.VRAI + + def remplacer(self, **kwargs): + for k, v in kwargs.items(): + if k.startswith("show_"): + self.__setattr__(k, v in Options.VRAI) + elif k in ["page", "nb_ligne_page"]: + self.__setattr__(k, int(v)) + + +class Data: + 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) -> "Data": + data = Data() + 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 diff --git a/app/tables/table_builder.py b/app/tables/table_builder.py index 1f74d7d99..aea76c0d5 100644 --- a/app/tables/table_builder.py +++ b/app/tables/table_builder.py @@ -84,6 +84,8 @@ class Table(Element): self.row_by_id: dict[str, "Row"] = {} self.column_ids = [] "ordered list of columns ids" + self.raw_column_ids = [] + "ordered list of columns ids for excel" self.groups = [] "ordered list of column groups names" self.group_titles = {} @@ -360,6 +362,7 @@ class Row(Element): target_attrs: dict = None, target: str = None, column_classes: set[str] = None, + no_excel: bool = False, ) -> "Cell": """Create cell and add it to the row. group: groupe de colonnes @@ -380,10 +383,17 @@ class Row(Element): target=target, target_attrs=target_attrs, ) - return self.add_cell_instance(col_id, cell, column_group=group, title=title) + return self.add_cell_instance( + col_id, cell, column_group=group, title=title, no_excel=no_excel + ) def add_cell_instance( - self, col_id: str, cell: "Cell", column_group: str = None, title: str = None + self, + col_id: str, + cell: "Cell", + column_group: str = None, + title: str = None, + no_excel: bool = False, ) -> "Cell": """Add a cell to the row. Si title est None, il doit avoir été ajouté avec table.add_title(). @@ -392,6 +402,9 @@ class Row(Element): self.cells[col_id] = cell if col_id not in self.table.column_ids: self.table.column_ids.append(col_id) + if not no_excel: + self.table.raw_column_ids.append(col_id) + self.table.insert_group(column_group) if column_group is not None: self.table.column_group[col_id] = column_group @@ -422,7 +435,7 @@ class Row(Element): """row as a dict, with only cell contents""" return { col_id: self.cells.get(col_id, self.table.empty_cell).raw_content - for col_id in self.table.column_ids + for col_id in self.table.raw_column_ids } def to_excel(self, sheet, style=None) -> list: diff --git a/app/templates/assiduites/pages/ajout_assiduites.j2 b/app/templates/assiduites/pages/ajout_assiduites.j2 index 311b24a55..1513226e3 100644 --- a/app/templates/assiduites/pages/ajout_assiduites.j2 +++ b/app/templates/assiduites/pages/ajout_assiduites.j2 @@ -2,7 +2,6 @@ {% block pageContent %}

Ajouter une assiduité

- {% include "assiduites/widgets/tableau_base.j2" %} {% if saisie_eval %}

@@ -63,8 +62,7 @@
- - {% include "assiduites/widgets/tableau_assi.j2" %} + {{tableau | safe }}
@@ -141,7 +139,7 @@ let assiduite_id = null; createAssiduiteComplete(assiduite, etudid); - loadAll(); + updateTableau(); btn.disabled = true; setTimeout(() => { btn.disabled = false; @@ -208,7 +206,6 @@ {% endif %} window.addEventListener("load", () => { - loadAll(); document.getElementById('assi_journee').addEventListener('click', () => { dayOnly() }); dayOnly() diff --git a/app/templates/assiduites/pages/tableau_actions.j2 b/app/templates/assiduites/pages/tableau_actions.j2 new file mode 100644 index 000000000..436d26b4d --- /dev/null +++ b/app/templates/assiduites/pages/tableau_actions.j2 @@ -0,0 +1,27 @@ +{% extends "sco_page.j2" %} + +{% block scripts %} +{{ super() }} + + +{% endblock %} + +{% block app_content %} + +{% if action == "modifier" %} +{% include "assiduites/widgets/tableau_actions/modifier.j2" %} +{% else%} +{% include "assiduites/widgets/tableau_actions/details.j2" %} +{% endif %} +
+
+
+Retour + + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/assiduites/pages/test_assi.j2 b/app/templates/assiduites/pages/test_assi.j2 deleted file mode 100644 index 8b0cf8597..000000000 --- a/app/templates/assiduites/pages/test_assi.j2 +++ /dev/null @@ -1,44 +0,0 @@ -{% extends "sco_page.j2" %} - -{% block scripts %} -{{ super() }} - -{% endblock %} - -{% block app_content %} - - - Options -
- - {% if show_pres %} - - {% else %} - - {% endif %} - - - {% if show_reta %} - - {% else %} - - {% endif %} -
- - - - - - -
- -
-
- -{{tableau | safe}} - -{% endblock %} \ No newline at end of file diff --git a/app/templates/assiduites/widgets/moduleimpl_dynamic_selector.j2 b/app/templates/assiduites/widgets/moduleimpl_dynamic_selector.j2 index cdb6c5585..80712eed9 100644 --- a/app/templates/assiduites/widgets/moduleimpl_dynamic_selector.j2 +++ b/app/templates/assiduites/widgets/moduleimpl_dynamic_selector.j2 @@ -119,11 +119,17 @@ } + {% if moduleid %} + const moduleimpl_dynamic_selector_id = "{{moduleid}}" + {% else %} + const moduleimpl_dynamic_selector_id = "moduleimpl_select" + + {% endif %} window.addEventListener("load", () => { - document.getElementById('moduleimpl_select').addEventListener('change', (el) => { + document.getElementById(moduleimpl_dynamic_selector_id).addEventListener('change', (el) => { const assi = getCurrentAssiduite(etudid); if (assi) { editAssiduite(assi.assiduite_id, assi.etat, [assi]); diff --git a/app/templates/assiduites/widgets/moduleimpl_selector.j2 b/app/templates/assiduites/widgets/moduleimpl_selector.j2 index 78a08b226..b85dc5bff 100644 --- a/app/templates/assiduites/widgets/moduleimpl_selector.j2 +++ b/app/templates/assiduites/widgets/moduleimpl_selector.j2 @@ -1,6 +1,8 @@ + {% else %} + + {% endif %} + + + {% if options.show_reta %} + + {% else %} + + {% endif %} + + {% if options.show_desc %} + + {% else %} + + {% endif %} +
+ + + + + + +
+ +
+
+ +{{tableau | safe}} + + + \ No newline at end of file diff --git a/app/templates/assiduites/widgets/tableau_actions/details.j2 b/app/templates/assiduites/widgets/tableau_actions/details.j2 new file mode 100644 index 000000000..ae43b6585 --- /dev/null +++ b/app/templates/assiduites/widgets/tableau_actions/details.j2 @@ -0,0 +1,107 @@ +

Détails {{type}}

+ +
+
+ Étudiant.e concerné.e: {{objet.etud_nom}} +
+ +
+ Période concernée : {{objet.date_debut}} au {{objet.date_fin}} +
+ + {% if type == "Assiduité" %} +
+ Module concernée : {{objet.module}} +
+ {% else %} + {% endif %} + +
+ {% if type == "Justificatif" %} + État du justificatif : + {% else %} + État de l'assiduité : + {% endif %} + {{objet.etat}} + +
+ +
+ {% if type == "Justificatif" %} +
Raison:
+ {% if objet.raison != None %} +
{{objet.raison}}
+ {% else %} +
/div> + {% endif %} + {% else %} +
Description:
+ {% if objet.description != None %} +
{{objet.description}}
+ {% else %} +
+ {% endif %} + {% endif %} +
+
+ + {# Affichage des justificatifs si assiduité justifiée #} + {% if type == "Assiduité" and objet.etat != "Présence" %} +
+ Justifiée: + {% if objet.justification.est_just %} + Oui +
+ {% for justi in objet.justification.justificatifs %} + Justificatif du {{justi.date_debut}} au {{justi.date_fin}} + {% endfor %} +
+ {% else %} + Non + {% endif %} +
+ {% endif %} + + {# Affichage des assiduités justifiées si justificatif valide #} + {% if type == "Justificatif" and objet.etat == "Valide" %} +
+ Assiduités concernées: + {% if objet.justification.assiduites %} +
+ {% for assi in objet.justification.assiduites %} + Assiduité {{assi.etat}} du {{assi.date_debut}} au + {{assi.date_fin}} + {% endfor %} +
+ {% else %} + Aucune + {% endif %} +
+ {% endif %} + + {# Affichage des fichiers des justificatifs #} + {% if type == "Justificatif"%} +
+ Fichiers enregistrés: + {% if objet.justification.fichiers.total != 0 %} +
Total : {{objet.justification.fichiers.total}}
+
+ {% for filename in objet.justification.fichiers.filenames %} +
+ +
+ {% endfor %} +
+ {% else %} + Aucun + {% endif %} +
+ {% endif %} + +
+ Saisie par {{objet.saisie_par}} le {{objet.entry_date}} +
\ No newline at end of file diff --git a/app/templates/assiduites/widgets/tableau_actions/modifier.j2 b/app/templates/assiduites/widgets/tableau_actions/modifier.j2 new file mode 100644 index 000000000..c548e5009 --- /dev/null +++ b/app/templates/assiduites/widgets/tableau_actions/modifier.j2 @@ -0,0 +1,107 @@ +

Modifier {{type}}

+ +
+ + + {% if type == "Assiduité" %} + + État + + + Module + {{moduleimpl | safe}} + + Description + + + + + {% else %} + + + Date de début + + Date de fin + + + État + + + Raison + + + Fichiers + +
+ + {% if objet.justification.fichiers.total != 0 %} +
Total : {{objet.justification.fichiers.total}}
+
    + {% for filename in objet.justification.fichiers.filenames %} +
  • + + +
  • + {% endfor %} +
+ {% else %} + Aucun + {% endif %} +
+
+ + + + + {% endif %} +
+
+ +
+ + \ No newline at end of file diff --git a/app/views/assiduites.py b/app/views/assiduites.py index 0a6f12c64..699e0e3a5 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -32,6 +32,7 @@ from flask import abort, url_for, redirect from flask_login import current_user from app import db + from app.comp import res_sem from app.comp.res_compat import NotesTableCompat from app.decorators import ( @@ -47,6 +48,10 @@ from app.models import ( Departement, Evaluation, ) +from app.auth.models import User +from app.models.assiduites import get_assiduites_justif, compute_assiduites_justified +import app.tables.liste_assiduites as liste_assi + from app.views import assiduites_bp as bp from app.views import ScoData @@ -65,6 +70,7 @@ from app.scodoc.sco_exceptions import ScoValueError from app.tables.visu_assiduites import TableAssi, etuds_sorted_from_ids +from app.scodoc.sco_archives_justificatifs import JustificatifArchiver CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS @@ -314,6 +320,15 @@ def signal_assiduites_etud(): """ + tableau = _preparer_tableau( + etud, + filename=f"assiduite-{etudid}", + afficher_etu=False, + filtre=liste_assi.Filtre(type_obj=0), + options=liste_assi.Options(show_module=True), + ) + if not tableau[0]: + return tableau[1] # Génération de la page return HTMLBuilder( header, @@ -332,6 +347,7 @@ def signal_assiduites_etud(): date_fin=date_fin, redirect_url=redirect_url, moduleimpl_id=moduleimpl_id, + tableau=tableau[1], ), # render_template( # "assiduites/pages/signal_assiduites_etud.j2", @@ -1044,26 +1060,43 @@ def visu_assi_group(): ) -@bp.route("/testTableau") -@scodoc -@permission_required(Permission.ScoView) -def testTableau(): - """Visualisation de l'assiduité d'un groupe entre deux dates""" +def _preparer_tableau( + *etudiants: Identite, + filename: str = "tableau-assiduites", + afficher_etu: bool = True, + filtre: liste_assi.Filtre = None, + options: liste_assi.Options = None, +) -> tuple[bool, "Response"]: + """ + _preparer_tableau prépare un tableau d'assiduités / justificatifs - etudid = request.args.get( - "etudid", 18114 - ) # TODO retirer la valeur par défaut de test + Cette fontion récupère dans la requête les arguments : + + valeurs possibles des booléens vrais ["on", "true", "t", "v", "vrai", True, 1] + toute autre valeur est considérée comme fausse. + + show_pres : bool -> Affiche les présences, par défaut False + show_reta : bool -> Affiche les retard, par défaut False + show_desc : bool -> Affiche les descriptions, par défaut False + + + + Returns: + tuple[bool | "Reponse" ]: + - bool : Vrai si la réponse est du Text/HTML + - Reponse : du Text/HTML ou Une Reponse (téléchargement fichier) + """ - fmt = request.args.get("fmt", "html") show_pres: bool | str = request.args.get("show_pres", False) show_reta: bool | str = request.args.get("show_reta", False) + show_desc: bool | str = request.args.get("show_desc", False) nb_ligne_page: int = request.args.get("nb_ligne_page") # Vérification de nb_ligne_page try: nb_ligne_page: int = int(nb_ligne_page) except (ValueError, TypeError): - nb_ligne_page = None + nb_ligne_page = liste_assi.ListeAssiJusti.NB_PAR_PAGE page_number: int = request.args.get("n_page", 1) # Vérification de page_number @@ -1072,33 +1105,177 @@ def testTableau(): except (ValueError, TypeError): page_number = 1 - from app.tables.liste_assiduites import ListeAssiJusti + fmt = request.args.get("fmt", "html") - table: ListeAssiJusti = ListeAssiJusti( - Identite.get_etud(etudid), page=page_number, nb_par_page=nb_ligne_page + if options is None: + options: liste_assi.Options = liste_assi.Options() + + options.remplacer( + page=page_number, + nb_ligne_page=nb_ligne_page, + show_pres=show_pres, + show_reta=show_reta, + show_desc=show_desc, + show_etu=afficher_etu, + ) + + table: liste_assi.ListeAssiJusti = liste_assi.ListeAssiJusti( + table_data=liste_assi.Data.from_etudiants(*etudiants), + options=options, + filtre=filtre, ) if fmt.startswith("xls"): - return scu.send_file( + return False, scu.send_file( table.excel(), - filename=f"assiduite-{groups_infos.groups_filename}", + filename=filename, mime=scu.XLSX_MIMETYPE, suffix=scu.XLSX_SUFFIX, ) - return render_template( - "assiduites/pages/test_assi.j2", - sco=ScoData(), + return True, render_template( + "assiduites/widgets/tableau.j2", tableau=table.html(), - title=f"Test tableau", total_pages=table.total_pages, - page_number=page_number, - show_pres=show_pres, - show_reta=show_reta, - nb_ligne_page=nb_ligne_page, + options=options, ) +@bp.route("/TableauAssiduiteActions", methods=["GET", "POST"]) +@scodoc +@permission_required(Permission.AbsChange) +def tableau_assiduite_actions(): + obj_type: str = request.args.get("type", "assiduite") + action: str = request.args.get("action", "details") + obj_id: str = int(request.args.get("obj_id", -1)) + + objet: Assiduite | Justificatif + + if obj_type == "assiduite": + objet: Assiduite = Assiduite.query.get_or_404(obj_id) + else: + objet: Justificatif = Justificatif.query.get_or_404(obj_id) + + if action == "supprimer": + objet.supprimer() + if obj_type == "assiduite": + flash("L'assiduité a bien été supprimée") + else: + flash("Le justificatif a bien été supprimé") + + return redirect(request.referrer) + + if request.method == "GET": + module = "" + + if obj_type == "assiduite": + formsemestre = objet.get_formsemestre() + if objet.moduleimpl_id is not None: + module = objet.moduleimpl_id + elif objet.external_data is not None: + module = objet.external_data.get("module", "") + module = module.lower() if isinstance(module, str) else module + module = _module_selector(formsemestre, module) + + return render_template( + "assiduites/pages/tableau_actions.j2", + sco=ScoData(etud=objet.etudiant), + type="Justificatif" if obj_type == "justificatif" else "Assiduité", + action=action, + objet=_preparer_objet(obj_type, objet), + obj_id=obj_id, + moduleimpl=module, + ) + + +def _preparer_objet( + obj_type: str, objet: Assiduite | Justificatif, sans_gros_objet: bool = False +) -> dict: + # Préparation d'un objet pour simplifier l'affichage jinja + objet_prepare: dict = objet.to_dict() + if obj_type == "assiduite": + objet_prepare["etat"] = ( + scu.EtatAssiduite(objet.etat).version_lisible().capitalize() + ) + objet_prepare["real_etat"] = scu.EtatAssiduite(objet.etat).name.lower() + objet_prepare["description"] = ( + "" if objet.description is None else objet.description + ) + objet_prepare["description"] = objet_prepare["description"].strip() + + # Gestion du module + objet_prepare["module"] = objet.get_module(True) + + # Gestion justification + + if not objet.est_just: + objet_prepare["justification"] = {"est_just": False} + else: + objet_prepare["justification"] = {"est_just": True, "justificatifs": []} + + if not sans_gros_objet: + justificatifs: list[int] = get_assiduites_justif( + objet.assiduite_id, False + ) + for justi_id in justificatifs: + justi: Justificatif = Justificatif.query.get(justi_id) + objet_prepare["justification"]["justificatifs"].append( + _preparer_objet("justificatif", justi, sans_gros_objet=True) + ) + + else: + objet_prepare["etat"] = ( + scu.EtatJustificatif(objet.etat).version_lisible().capitalize() + ) + objet_prepare["real_etat"] = scu.EtatJustificatif(objet.etat).name.lower() + objet_prepare["raison"] = "" if objet.raison is None else objet.raison + objet_prepare["raison"] = objet_prepare["raison"].strip() + + objet_prepare["justification"] = {"assiduites": [], "fichiers": {}} + if not sans_gros_objet: + assiduites: list[int] = scass.justifies(objet) + for assi_id in assiduites: + assi: Assiduite = Assiduite.query.get(assi_id) + objet_prepare["justification"]["assiduites"].append( + _preparer_objet("assiduite", assi, sans_gros_objet=True) + ) + + # Récupération de l'archive avec l'archiver + archive_name: str = objet.fichier + filenames: list[str] = [] + archiver: JustificatifArchiver = JustificatifArchiver() + if archive_name is not None: + filenames = archiver.list_justificatifs(archive_name, objet.etudiant) + objet_prepare["justification"]["fichiers"] = { + "total": len(filenames), + "filenames": [], + } + for filename in filenames: + if int(filename[1]) == current_user.id or current_user.has_permission( + Permission.AbsJustifView + ): + objet_prepare["justification"]["fichiers"]["filenames"].append( + filename[0] + ) + + objet_prepare["date_fin"] = objet.date_fin.strftime("%d/%m/%y à %H:%M") + objet_prepare["real_date_fin"] = objet.date_fin.isoformat() + objet_prepare["date_debut"] = objet.date_debut.strftime("%d/%m/%y à %H:%M") + objet_prepare["real_date_debut"] = objet.date_debut.isoformat() + + objet_prepare["entry_date"] = objet.entry_date.strftime("%d/%m/%y à %H:%M") + + objet_prepare["etud_nom"] = objet.etudiant.nomprenom + + if objet.user_id != None: + user: User = User.query.get(objet.user_id) + objet_prepare["saisie_par"] = user.get_nomprenom() + else: + objet_prepare["saisie_par"] = "Inconnu" + + return objet_prepare + + @bp.route("/SignalAssiduiteDifferee") @scodoc @permission_required(Permission.AbsChange) @@ -1534,12 +1711,6 @@ def _module_selector(formsemestre: FormSemestre, moduleimpl_id: int = None) -> s # prévoie la sélection par défaut d'un moduleimpl s'il a été passé en paramètre selected = "" if moduleimpl_id is not None else "selected" - # Vérification que le moduleimpl_id passé en paramètre est bien un entier - try: - moduleimpl_id = int(moduleimpl_id) - except (ValueError, TypeError): - moduleimpl_id = None - modules: list[dict[str, str | int]] = [] # Récupération de l'id et d'un nom lisible pour chaque moduleimpl for modimpl in modimpls_list: From 7a80ec3ce59e02c22b16819061350d48467e2a74 Mon Sep 17 00:00:00 2001 From: Iziram Date: Fri, 24 Nov 2023 18:07:30 +0100 Subject: [PATCH 10/10] Assiduites : Tableaux (sans QOL) --- app/api/justificatifs.py | 4 +- app/models/assiduites.py | 34 ++++ app/static/js/date_utils.js | 2 +- app/tables/liste_assiduites.py | 25 +-- .../assiduites/pages/ajout_justificatif.j2 | 32 +--- .../assiduites/pages/liste_assiduites.j2 | 93 +--------- app/templates/assiduites/widgets/tableau.j2 | 6 +- .../widgets/tableau_actions/details.j2 | 13 +- .../widgets/tableau_actions/modifier.j2 | 54 +++--- app/views/assiduites.py | 161 ++++++++++++++---- 10 files changed, 225 insertions(+), 199 deletions(-) diff --git a/app/api/justificatifs.py b/app/api/justificatifs.py index b398b83cb..e90098e5e 100644 --- a/app/api/justificatifs.py +++ b/app/api/justificatifs.py @@ -646,8 +646,8 @@ def justif_import(justif_id: int = None): return json_error(404, err.args[0]) -@bp.route("/justificatif//export/", methods=["POST"]) -@api_web_bp.route("/justificatif//export/", methods=["POST"]) +@bp.route("/justificatif//export/", methods=["GET", "POST"]) +@api_web_bp.route("/justificatif//export/", methods=["GET", "POST"]) @scodoc @login_required @permission_required(Permission.AbsChange) diff --git a/app/models/assiduites.py b/app/models/assiduites.py index 47152e3b6..937e11c43 100644 --- a/app/models/assiduites.py +++ b/app/models/assiduites.py @@ -204,6 +204,40 @@ class Assiduite(db.Model): sco_abs_notification.abs_notify(etud.id, nouv_assiduite.date_debut) return nouv_assiduite + def set_moduleimpl(self, moduleimpl_id: int | str) -> bool: + moduleimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id) + if moduleimpl is not None: + # Vérification de l'inscription de l'étudiant + if moduleimpl.est_inscrit(self.etudiant): + self.moduleimpl_id = moduleimpl.id + else: + raise ScoValueError("L'étudiant n'est pas inscrit au module") + elif isinstance(moduleimpl_id, str): + if self.external_data is None: + self.external_data = {"module": moduleimpl_id} + else: + self.external_data["module"] = moduleimpl_id + self.moduleimpl_id = None + else: + # Vérification si module forcé + formsemestre: FormSemestre = get_formsemestre_from_data( + { + "etudid": self.etudid, + "date_debut": self.date_debut, + "date_fin": self.date_fin, + } + ) + force: bool + + if formsemestre: + force = is_assiduites_module_forced(formsemestre_id=formsemestre.id) + else: + force = is_assiduites_module_forced(dept_id=etud.dept_id) + + if force: + raise ScoValueError("Module non renseigné") + return True + def supprimer(self): from app.scodoc import sco_assiduites as scass diff --git a/app/static/js/date_utils.js b/app/static/js/date_utils.js index 3565ea515..298967438 100644 --- a/app/static/js/date_utils.js +++ b/app/static/js/date_utils.js @@ -481,7 +481,7 @@ class ScoDocDateTimePicker extends HTMLElement { } else { // Mettre à jour la valeur de l'input caché avant la soumission this.hiddenInput.value = this.isValid() - ? this.valueAsDate.toIsoUtcString() + ? this.valueAsDate.toFakeIso() : ""; } }); diff --git a/app/tables/liste_assiduites.py b/app/tables/liste_assiduites.py index 21c69cbf7..6a61de53c 100644 --- a/app/tables/liste_assiduites.py +++ b/app/tables/liste_assiduites.py @@ -17,6 +17,7 @@ class ListeAssiJusti(tb.Table): """ NB_PAR_PAGE: int = 25 + MAX_PAR_PAGE: int = 200 def __init__( self, @@ -221,7 +222,7 @@ class RowAssiJusti(tb.Row): # Ajout de l'étudiant self.table: ListeAssiJusti if self.table.options.show_etu: - self._etud() + self._etud(lien_redirection) # Type d'objet self._type() @@ -287,7 +288,7 @@ class RowAssiJusti(tb.Row): self.add_cell("obj_type", "Type", obj_type) - def _etud(self) -> None: + def _etud(self, lien_redirection) -> None: etud = self.etud self.table.group_titles.update( { @@ -357,7 +358,7 @@ class RowAssiJusti(tb.Row): obj_id=self.ligne["obj_id"], scodoc_dept=g.scodoc_dept, ) - html.append(f'Détails') # utiliser url_for + html.append(f'ℹ️') # utiliser url_for # Modifier url = url_for( @@ -367,7 +368,7 @@ class RowAssiJusti(tb.Row): obj_id=self.ligne["obj_id"], scodoc_dept=g.scodoc_dept, ) - html.append(f'Modifier') # utiliser url_for + html.append(f'📝') # utiliser url_for # Supprimer url = url_for( @@ -377,11 +378,9 @@ class RowAssiJusti(tb.Row): obj_id=self.ligne["obj_id"], scodoc_dept=g.scodoc_dept, ) - html.append(f'Supprimer') # utiliser url_for + html.append(f'') # utiliser url_for - self.add_cell( - "actions", "Actions", " ".join(html), raw_content="test", no_excel=True - ) + self.add_cell("actions", "Actions", " ".join(html), no_excel=True) class Filtre: @@ -492,6 +491,8 @@ class Options: ): 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: bool = show_pres in Options.VRAI self.show_reta: bool = show_reta in Options.VRAI @@ -503,9 +504,13 @@ class Options: def remplacer(self, **kwargs): for k, v in kwargs.items(): if k.startswith("show_"): - self.__setattr__(k, v in Options.VRAI) + setattr(self, k, v in Options.VRAI) elif k in ["page", "nb_ligne_page"]: - self.__setattr__(k, int(v)) + setattr(self, k, int(v)) + if k == "nb_ligne_page": + self.nb_ligne_page = min( + self.nb_ligne_page, ListeAssiJusti.MAX_PAR_PAGE + ) class Data: diff --git a/app/templates/assiduites/pages/ajout_justificatif.j2 b/app/templates/assiduites/pages/ajout_justificatif.j2 index d74737146..c295fc9ec 100644 --- a/app/templates/assiduites/pages/ajout_justificatif.j2 +++ b/app/templates/assiduites/pages/ajout_justificatif.j2 @@ -2,8 +2,6 @@ {% block pageContent %}

Justifier des absences ou retards

- {% include "assiduites/widgets/tableau_base.j2" %} -
@@ -58,28 +56,9 @@
- - {% include "assiduites/widgets/tableau_justi.j2" %} + {{tableau | safe }}
-
- -

Gestion des justificatifs

-

- Faites - clic droit sur une ligne du tableau pour afficher le menu - contextuel : -

    -
  • Détails : Affiche les détails du justificatif sélectionné
  • -
  • Editer : Permet de modifier le justificatif (dates, etat, ajouter/supprimer fichier etc)
  • -
  • Supprimer : Permet de supprimer le justificatif (Action Irréversible)
  • -
-

- -

Cliquer sur l'icone d'entonoir afin de filtrer le tableau des justificatifs

- -
-
\ No newline at end of file diff --git a/app/views/assiduites.py b/app/views/assiduites.py index 699e0e3a5..6427e84a5 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -266,13 +266,6 @@ def signal_assiduites_etud(): if etud.dept_id != g.scodoc_dept_id: abort(404, "étudiant inexistant dans ce département") - # Récupération de la date (par défaut la date du jour) - date = request.args.get("date", datetime.date.today().isoformat()) - heures: list[str] = [ - request.args.get("heure_deb", ""), - request.args.get("heure_fin", ""), - ] - # gestion évaluations (Appel à la page depuis les évaluations) saisie_eval: bool = request.args.get("saisie_eval") is not None @@ -305,26 +298,13 @@ def signal_assiduites_etud(): ], ) - # Gestion des horaires (journée, matin, soir) - - morning = ScoDocSiteConfig.assi_get_rounded_time("assi_morning_time", "08:00:00") - lunch = ScoDocSiteConfig.assi_get_rounded_time("assi_lunch_time", "13:00:00") - afternoon = ScoDocSiteConfig.assi_get_rounded_time( - "assi_afternoon_time", "18:00:00" - ) - - # Gestion du selecteur de moduleimpl (pour le tableau différé) - select = f""" - - """ - tableau = _preparer_tableau( - etud, + liste_assi.Data.from_etudiants( + etud, + ), filename=f"assiduite-{etudid}", afficher_etu=False, - filtre=liste_assi.Filtre(type_obj=0), + filtre=liste_assi.Filtre(type_obj=1), options=liste_assi.Options(show_module=True), ) if not tableau[0]: @@ -393,7 +373,7 @@ def liste_assiduites_etud(): if etud.dept_id != g.scodoc_dept_id: abort(404, "étudiant inexistant dans ce département") - # Gestion d'une assiduité unique (redirigé depuis le calendrier) + # Gestion d'une assiduité unique (redirigé depuis le calendrier) TODO-Assiduites assiduite_id: int = request.args.get("assiduite_id", -1) # Préparation de la page @@ -409,18 +389,25 @@ def liste_assiduites_etud(): "css/assiduites.css", ], ) + tableau = _preparer_tableau( + liste_assi.Data.from_etudiants( + etud, + ), + filename=f"assiduites-justificatifs-{etudid}", + afficher_etu=False, + filtre=liste_assi.Filtre(type_obj=0), + options=liste_assi.Options(show_module=True), + ) + if not tableau[0]: + return tableau[1] # Peuplement du template jinja return HTMLBuilder( header, render_template( "assiduites/pages/liste_assiduites.j2", sco=ScoData(etud), - date=datetime.date.today().isoformat(), assi_id=assiduite_id, - assi_limit_annee=sco_preferences.get_preference( - "assi_limit_annee", - dept_id=g.scodoc_dept_id, - ), + tableau=tableau[1], ), ).build() @@ -517,6 +504,19 @@ def ajout_justificatif_etud(): ], ) + tableau = _preparer_tableau( + liste_assi.Data.from_etudiants( + etud, + ), + filename=f"justificatifs-{etudid}", + afficher_etu=False, + filtre=liste_assi.Filtre(type_obj=2), + options=liste_assi.Options(show_module=False, show_desc=True), + afficher_options=False, + ) + if not tableau[0]: + return tableau[1] + # Peuplement du template jinja return HTMLBuilder( header, @@ -529,6 +529,7 @@ def ajout_justificatif_etud(): ), assi_morning=ScoDocSiteConfig.get("assi_morning_time", "08:00"), assi_evening=ScoDocSiteConfig.get("assi_afternoon_time", "18:00"), + tableau=tableau[1], ), ).build() @@ -1061,11 +1062,12 @@ def visu_assi_group(): def _preparer_tableau( - *etudiants: Identite, + data: liste_assi.Data, filename: str = "tableau-assiduites", afficher_etu: bool = True, filtre: liste_assi.Filtre = None, options: liste_assi.Options = None, + afficher_options: bool = True, ) -> tuple[bool, "Response"]: """ _preparer_tableau prépare un tableau d'assiduités / justificatifs @@ -1120,7 +1122,7 @@ def _preparer_tableau( ) table: liste_assi.ListeAssiJusti = liste_assi.ListeAssiJusti( - table_data=liste_assi.Data.from_etudiants(*etudiants), + table_data=data, options=options, filtre=filtre, ) @@ -1138,6 +1140,7 @@ def _preparer_tableau( tableau=table.html(), total_pages=table.total_pages, options=options, + afficher_options=afficher_options, ) @@ -1186,6 +1189,100 @@ def tableau_assiduite_actions(): obj_id=obj_id, moduleimpl=module, ) + # Cas des POSTS + if obj_type == "assiduite": + try: + _action_modifier_assiduite(objet) + except ScoValueError as error: + raise ScoValueError(error.args[0], request.referrer) from error + flash("L'assiduité a bien été modifiée.") + else: + try: + _action_modifier_justificatif(objet) + except ScoValueError as error: + raise ScoValueError(error.args[0], request.referrer) from error + flash("Le justificatif a bien été modifié.") + return redirect(request.form["table_url"]) + + +def _action_modifier_assiduite(assi: Assiduite): + form = request.form + + # Gestion de l'état + etat = scu.EtatAssiduite.get(form["etat"]) + if etat is not None: + assi.etat = etat + if etat == scu.EtatAssiduite.PRESENT: + assi.est_just = False + else: + assi.est_just = len(get_assiduites_justif(assi.assiduite_id, False)) > 0 + + # Gestion de la description + assi.description = form["description"] + + module: str = form["moduleimpl_select"] + + if module == "": + module = None + else: + try: + module = int(module) + except ValueError: + pass + + assi.set_moduleimpl(module) + + db.session.add(assi) + db.session.commit() + scass.simple_invalidate_cache(assi.to_dict(True), assi.etudid) + + +def _action_modifier_justificatif(justi: Justificatif): + form = request.form + + # Gestion des Dates + + date_debut: datetime = scu.is_iso_formated(form["date_debut"], True) + date_fin: datetime = scu.is_iso_formated(form["date_fin"], True) + if date_debut is None or date_fin is None or date_fin < date_debut: + raise ScoValueError("Dates invalides", request.referrer) + justi.date_debut = date_debut + justi.date_fin = date_fin + + # Gestion de l'état + etat = scu.EtatJustificatif.get(form["etat"]) + if etat is not None: + justi.etat = etat + else: + raise ScoValueError("État invalide", request.referrer) + + # Gestion de la raison + justi.raison = form["raison"] + + # Gestion des fichiers + files = request.files.getlist("justi_fich") + if len(files) != 0: + files = request.files.values() + + archive_name: str = justi.fichier + # Utilisation de l'archiver de justificatifs + archiver: JustificatifArchiver = JustificatifArchiver() + + for fich in files: + archive_name, _ = archiver.save_justificatif( + justi.etudiant, + filename=fich.filename, + data=fich.stream.read(), + archive_name=archive_name, + user_id=current_user.id, + ) + + justi.fichier = archive_name + + db.session.add(justi) + db.session.commit() + scass.compute_assiduites_justified(justi.etudid, reset=True) + scass.simple_invalidate_cache(justi.to_dict(True), justi.etudid) def _preparer_objet(