diff --git a/app/models/assiduites.py b/app/models/assiduites.py index db7a2587..745dfdbb 100644 --- a/app/models/assiduites.py +++ b/app/models/assiduites.py @@ -1,6 +1,6 @@ # -*- coding: UTF-8 -* -"""Gestion de l'assiduité (assiduités + justificatifs) -""" +"""Gestion de l'assiduité (assiduités + justificatifs)""" + from datetime import datetime from flask_login import current_user @@ -336,13 +336,19 @@ class Assiduite(ScoDocModel): """ return get_formsemestre_from_data(self.to_dict()) - def get_module(self, traduire: bool = False) -> int | str: - "TODO documenter" + def get_module(self, traduire: bool = False) -> Module | str: + """ + Retourne le module associé à l'assiduité + Si traduire est vrai, retourne le titre du module précédé du code + Sinon rentourne l'objet Module ou None + """ + if self.moduleimpl_id is not None: + modimpl: ModuleImpl = ModuleImpl.query.get(self.moduleimpl_id) + mod: Module = Module.query.get(modimpl.module_id) if traduire: - modimpl: ModuleImpl = ModuleImpl.query.get(self.moduleimpl_id) - mod: Module = Module.query.get(modimpl.module_id) return f"{mod.code} {mod.titre}" + return mod elif self.external_data is not None and "module" in self.external_data: return ( diff --git a/app/tables/liste_assiduites.py b/app/tables/liste_assiduites.py index 2441f101..819c3342 100644 --- a/app/tables/liste_assiduites.py +++ b/app/tables/liste_assiduites.py @@ -12,7 +12,7 @@ from sqlalchemy import desc, literal, union, asc from app import db, g from app.auth.models import User -from app.models import Assiduite, Identite, Justificatif +from app.models import Assiduite, Identite, Justificatif, Module from app.scodoc.sco_utils import ( EtatAssiduite, EtatJustificatif, @@ -534,10 +534,45 @@ class RowAssiJusti(tb.Row): 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}) + if self.table.no_pagination: + mod: Module = assi.get_module(False) + code = mod.code if isinstance(mod, Module) else "" + titre = "" + if isinstance(mod, Module): + titre = mod.titre + elif isinstance(mod, str): + titre = mod + else: + titre = "Non Spécifié" + + self.add_cell( + "code_module", "Code Module", code, data={"order": code} + ) + self.add_cell( + "titre_module", + "Titre Module", + titre, + data={"order": titre}, + ) + else: + mod: Module = assi.get_module(True) + self.add_cell( + "module", + "Module", + mod, + data={"order": mod}, + ) else: - self.add_cell("module", "Module", "", data={"order": ""}) + if self.table.no_pagination: + self.add_cell("module", "Module", "", data={"order": ""}) + else: + self.add_cell("code_module", "Code Module", "", data={"order": ""}) + self.add_cell( + "titre_module", + "Titre Module", + "", + data={"order": ""}, + ) def _utilisateur(self) -> None: utilisateur: User = ( diff --git a/app/templates/assiduites/pages/bilan_dept.j2 b/app/templates/assiduites/pages/bilan_dept.j2 index f4a20efb..4fa342eb 100644 --- a/app/templates/assiduites/pages/bilan_dept.j2 +++ b/app/templates/assiduites/pages/bilan_dept.j2 @@ -20,6 +20,39 @@ le semestre concerné (saisie par jour ou saisie différée).
{{billets | safe}} +
+ +
+

Télécharger l'assiduité

+ +
+ +
+ +
+ + +
+ +
+ +
+
{{tableau | safe }} diff --git a/app/templates/assiduites/pages/signal_assiduites_diff.j2 b/app/templates/assiduites/pages/signal_assiduites_diff.j2 index f19e59ac..120369fe 100644 --- a/app/templates/assiduites/pages/signal_assiduites_diff.j2 +++ b/app/templates/assiduites/pages/signal_assiduites_diff.j2 @@ -17,6 +17,15 @@ gap: 0.5em; } + #actions { + flex-direction: row; + align-items: center; + margin-bottom: 5px; + } + #actions label{ + margin: 0; + } + #fix { display: flex; flex-direction: row; @@ -49,21 +58,24 @@ } #tableau-periode { + display: flex; + flex-direction: column; overflow-x: scroll; max-width: var(--sco-content-max-width); } #tableau-periode .pdp { - width: 2em; - height: 2em; - border-radius: 50%; + width: 5em; + border-radius: 8px; } - .grid-table { - display: grid; - grid-template-columns: 200px repeat({{ etudiants|length }}, 1fr); - width: var(--sco-content-max-width); + .header { + background-color: #f9f9f9; + padding: 10px; + text-align: center; + border: 1px solid #ddd; } + .cell, .header { border: 1px solid #ddd; padding: 10px; @@ -74,19 +86,23 @@ } .header{ justify-content: space-between; - } - .cell{ + + .cell { + display: flex; + align-items: center; justify-content: center; - + border: 1px solid #ddd; + padding: 10px; + text-align: center; + width: 256px; } - .cell p{ text-align: center; } .sticky { position: sticky; - left: 0; + top: 0; background-color: #f9f9f9; z-index: 2; } @@ -95,12 +111,24 @@ display: block; top: 0; z-index: 0; + width: 100% !important; + min-width: inherit !important; + } + + .assi-btns { + display: flex; + gap: 4px; } .pointer{ cursor: pointer; } + .ligne{ + display: flex; + gap: 1px; + } + {% endblock styles %} @@ -124,6 +152,10 @@ function afficherPDP(checked) { } else { gtrcontent.removeAttribute("data-pdp"); } + + // On sauvegarde le choix dans le localStorage + localStorage.setItem("scodoc-signal_assiduites_diff-pdp", `${checked}`); + pdp.checked = checked; } /** @@ -156,7 +188,7 @@ async function nouvellePeriode(period = null) { moduleimpl_id = period.moduleimpl_id; }else{ //Sinon on vérifie qu'on a bien des valeurs - const text = document.createTextNode("Veuillez remplir tous les champs pour ajouter une période.") + const text = document.createTextNode("Veuillez remplir tous les champs pour ajouter une plage.") if (date == "" || debut == "" || fin == "" || moduleimpl_id == "") { openAlertModal( "Erreur", @@ -168,10 +200,11 @@ async function nouvellePeriode(period = null) { // On ajoute la nouvelle période au tableau let periodeDiv = document.createElement("div"); - periodeDiv.classList.add("cell", "sticky"); + periodeDiv.classList.add("cell", "header"); periodeDiv.id = `periode-${periodId}`; + const periodP = document.createElement("p"); - periodP.textContent = `Période du ${date} de ${debut} à ${fin}`; + periodP.textContent = `Plage du ${date} de ${debut} à ${fin}`; // On ajoute le moduleimpl const modP = document.createElement("p"); @@ -184,7 +217,7 @@ async function nouvellePeriode(period = null) { // On supprime toutes les cases du tableau correspondant à cette période document .querySelectorAll( - `.cell[data-periodeid="${periodeDiv.getAttribute("data-periodeid")}"]` + `[data-periodeid="${periodeDiv.getAttribute("data-periodeid")}"]` ) .forEach((e) => e.remove()); // On supprime la période de la Map periodes @@ -195,11 +228,11 @@ async function nouvellePeriode(period = null) { periodeDiv.appendChild(modP); periodeDiv.appendChild(close); periodeDiv.setAttribute("data-periodeid", periodId); - document.getElementById("tableau-periode").appendChild(periodeDiv); + document.getElementById("tete-table").appendChild(periodeDiv); // On récupère les étudiants (etudids) let etudids = [ - ...document.querySelectorAll("#tableau-periode .header[data-etudid]"), + ...document.querySelectorAll(".ligne[data-etudid]"), ].map((e) => e.getAttribute("data-etudid")); // On génère une date de début et de fin de la période @@ -249,7 +282,7 @@ async function nouvellePeriode(period = null) { cell.setAttribute("data-etudid", etudid); cell.setAttribute("data-periodeid", periodId); cell.id = `cell-${etudid}-${periodId}`; - document.getElementById("tableau-periode").appendChild(cell); + document.querySelector(`.ligne[data-etudid="${etudid}"]`).appendChild(cell); //Vérification inscription au module // Si l'étudiant n'est pas inscrit, on le notifie et on passe à l'étudiant suivant @@ -265,6 +298,10 @@ async function nouvellePeriode(period = null) { const assiduites = data[etudid]; // Si l'étudiant n'a pas d'assiduité, on crée les boutons assiduité if (assiduites.length == 0) { + + const assi_btns = document.createElement('div'); + assi_btns.classList.add('assi-btns'); + ["present", "retard", "absent"].forEach((value) => { const cbox = document.createElement("input"); cbox.type = "checkbox"; @@ -284,8 +321,9 @@ async function nouvellePeriode(period = null) { // Si une valeur par défaut est donnée alors on l'applique cbox.checked = etatDef.value == value; - cell.appendChild(cbox); + assi_btns.appendChild(cbox); }); + cell.appendChild(assi_btns); } else { // Si une (ou plus) assiduité sont trouvée pour la période // alors on affiche les informations de la première assiduité @@ -297,6 +335,8 @@ async function nouvellePeriode(period = null) { .catch((error) => { console.error("Error:", error); }); + + document.getElementById("tableau-periode").classList.remove("hidden"); } /** * Permet de récupérer la saisie puis créer les assiduités grâce à l'api @@ -337,6 +377,7 @@ function sauvegarderAssiduites() { } }); } + // Une fois les assiduités générées, on les envoie à l'api async_post( "../../api/assiduites/create", @@ -344,7 +385,7 @@ function sauvegarderAssiduites() { // Si la requête passe async (data) => { // On supprime toutes les cases du tableau pour le mettre à jour - document.querySelectorAll(".cell").forEach((e) => e.remove()); + document.querySelectorAll("[data-periodeid]").forEach((e)=>e.remove()) // On recrée les périodes // (cela permet de redemander les assiduités, donc mettre à jour les cases) @@ -402,7 +443,7 @@ function sauvegarderAssiduites() { const period = periodes.get(periodeId); const li = document.createElement("li"); // On affiche la période - li.textContent = `Période du ${period.date_debut.format( + li.textContent = `Plage du ${period.date_debut.format( "DD/MM/YYYY HH:mm" )} à ${period.date_fin.format("HH:mm")}`; @@ -474,7 +515,8 @@ if (window.forceModule) { * - On vérifie si la date est un jour travaillé */ async function main() { - afficherPDP(pdp.checked); + const checked = localStorage.getItem("scodoc-signal_assiduites_diff-pdp") == "true"; + afficherPDP(checked); $("#date").on("change", async function (d) { // On vérifie si la date est un jour travaillé dateCouranteEstTravaillee(); @@ -497,8 +539,8 @@ main();
- +
+
+ + -
- - - - - -
+ +
-
-
- -
Période
- + +
+
Étudiants
+ {#
Plage X
#} +
+ {# ... #} + + + {% for etud in etudiants %} -
- {{etud.nomprenom}} - {{ etud.nomprenom }} +
+
+ {{etud.nomprenom}} + {{ etud.nomprenom }} +
+ {#
Assiduité Plage 1
#} +
{% endfor %} - +
diff --git a/app/views/assiduites.py b/app/views/assiduites.py index 347b140c..cb0f8f92 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -186,6 +186,12 @@ def bilan_dept(): if not table[0]: return table[1] + # Récupération des formsemestres (pour le menu déroulant) + formsemestres: Query = FormSemestre.get_dept_formsemestres_courants(dept) + formsemestres_choices: dict[int, str] = { + fs.id: fs.titre_annee() for fs in formsemestres + } + # Peuplement du template jinja return render_template( "assiduites/pages/bilan_dept.j2", @@ -193,6 +199,8 @@ def bilan_dept(): search_etud=sco_find_etud.form_search_etud(dest_url="assiduites.bilan_etud"), billets=billets, sco=ScoData(formsemestre=formsemestre), + formsemestres=formsemestres_choices, + formsemestre_id=None if not formsemestre else formsemestre.id, ) @@ -1565,6 +1573,85 @@ def _prepare_tableau( ) +@bp.route("/recup_assiduites_plage", methods=["POST"]) +@scodoc +@permission_required(Permission.AbsChange) +def recup_assiduites_plage(): + """ + Renvoie un fichier excel contenant toutes les assiduités d'une plage + La plage est définie par les valeurs "datedeb" et "datefin" du formulaire + Par défaut tous les étudiants du département sont concernés + Si le champs "formsemestre_id" est présent dans le formulaire et est non vide, + seuls les étudiants inscrits dans ce semestre sont concernés. + """ + + date_deb: datetime.datetime = request.form.get("datedeb") + date_fin: datetime.datetime = request.form.get("datefin") + + # Vérification des dates + try: + date_deb = datetime.datetime.strptime(date_deb, "%d/%m/%Y") + except ValueError as exc: + raise ScoValueError("date_debut invalide", dest_url=request.referrer) from exc + try: + date_fin = datetime.datetime.strptime(date_fin, "%d/%m/%Y") + except ValueError as exc: + raise ScoValueError("date_fin invalide", dest_url=request.referrer) from exc + + # Récupération des étudiants + etuds: Query = [] + formsemestre_id: str | None = request.form.get("formsemestre_id") + + name: str = "" + + if formsemestre_id is not None and formsemestre_id != "": + formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) + etuds = formsemestre.etuds + name = formsemestre.session_id() + else: + dept: Departement = Departement.query.get_or_404(g.scodoc_dept_id) + etuds = dept.etudiants + name = dept.acronym + + # Récupération des assiduités + assiduites: Query = Assiduite.query.filter( + Assiduite.etudid.in_([etud.id for etud in etuds]) + ) + + # Filtrage des assiduités en fonction des dates données + assiduites = scass.filter_by_date(assiduites, Assiduite, date_deb, date_fin) + + table_data: liste_assi.AssiJustifData = liste_assi.AssiJustifData( + assiduites_query=assiduites, + ) + + options: liste_assi.AssiDisplayOptions = liste_assi.AssiDisplayOptions( + show_pres=True, + show_reta=True, + show_module=True, + show_etu=True, + ) + + date_deb_str: str = date_deb.strftime("%d-%m-%Y") + date_fin_str: str = date_fin.strftime("%d-%m-%Y") + + filename: str = f"assiduites_{name}_{date_deb_str}_{date_fin_str}" + + tableau: liste_assi.ListeAssiJusti = liste_assi.ListeAssiJusti( + table_data, + options=options, + titre="tableau-dept-" + filename, + no_pagination=True, + ) + + return scu.send_file( + tableau.excel(), + filename=filename, + mime=scu.XLSX_MIMETYPE, + suffix=scu.XLSX_SUFFIX, + ) + + @bp.route("/tableau_assiduite_actions", methods=["GET", "POST"]) @scodoc @permission_required(Permission.AbsChange)