diff --git a/app/models/assiduites.py b/app/models/assiduites.py index a89e89b4f6..47152e3b6e 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 f93bf05269..309d180cfc 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 05babeedec..3565ea5151 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 329b43f4a6..21c69cbf7a 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 1f74d7d997..aea76c0d5e 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 311b24a55c..1513226e38 100644 --- a/app/templates/assiduites/pages/ajout_assiduites.j2 +++ b/app/templates/assiduites/pages/ajout_assiduites.j2 @@ -2,7 +2,6 @@ {% block pageContent %}