From b13e751e1a9d41b043186b41e09ad4bbff207f81 Mon Sep 17 00:00:00 2001 From: Iziram Date: Fri, 24 Nov 2023 13:58:03 +0100 Subject: [PATCH] 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 a89e89b4..47152e3b 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 f93bf052..309d180c 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 05babeed..3565ea51 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 329b43f4..21c69cbf 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 1f74d7d9..aea76c0d 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 311b24a5..1513226e 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 00000000..436d26b4 --- /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 8b0cf859..00000000 --- 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 cdb6c558..80712eed 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 78a08b22..b85dc5bf 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 00000000..ae43b658 --- /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 00000000..c548e500 --- /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 0a6f12c6..699e0e3a 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: