From 5a41f9c1c31c147ffed89b6807556ee8c32e4d4e Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 26 Oct 2023 11:09:22 +0200 Subject: [PATCH 1/9] 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 04a630ca..a2ac2475 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 9f945769..4d7ab146 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 97e75e9ac996fac8e949ae0aa3ddc3025f74da73 Mon Sep 17 00:00:00 2001 From: Iziram Date: Thu, 26 Oct 2023 15:52:53 +0200 Subject: [PATCH 2/9] 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 a9451ae7..2e0c06c6 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 a78e709d..57b7dfb7 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 bbf785a7..6faad0fb 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 ecc2ce51..74c0c4b9 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 7a55c7a9..78f45d78 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 045546d0..ffd25464 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 ad010e2c..78a08b22 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 @@ -1380,7 +1375,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 c723cef66c340c0265a7b517d15d6c430775148c Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 26 Oct 2023 16:15:29 +0200 Subject: [PATCH 3/9] 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 189a8715..eacfe043 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 eb82de4c..cab6feae 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 02cd2984..0388c4c2 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 9faa5866814b7182f5b8a44ebd75c023e22d9d80 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 26 Oct 2023 16:44:09 +0200 Subject: [PATCH 4/9] 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 2e0c06c6..6c2b63df 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 d5149f75db4c0aa6ee3f916e219d4db6ac8cf801 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 26 Oct 2023 17:28:36 +0200 Subject: [PATCH 5/9] 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 6c2b63df..f5195f52 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 70e7355fa218e78ce2e7043d7f87f67060ea9f43 Mon Sep 17 00:00:00 2001 From: Iziram Date: Fri, 27 Oct 2023 10:33:37 +0200 Subject: [PATCH 6/9] Assiduite : fix date saisie + edit assiduite --- app/api/assiduites.py | 8 +++++++- app/static/js/assiduites.js | 8 ++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/api/assiduites.py b/app/api/assiduites.py index f5195f52..33a24136 100644 --- a/app/api/assiduites.py +++ b/app/api/assiduites.py @@ -873,7 +873,13 @@ def _edit_singular(assiduite_unique, data): else: force = scu.is_assiduites_module_forced(dept_id=etud.dept_id) - if force: + external_data = ( + external_data + if external_data is not None and isinstance(external_data, dict) + else assiduite_unique.external_data + ) + + if force and not external_data.get("module", False): errors.append( "param 'moduleimpl_id' : le moduleimpl_id ne peut pas être nul" ) diff --git a/app/static/js/assiduites.js b/app/static/js/assiduites.js index 74c0c4b9..21b5c1d0 100644 --- a/app/static/js/assiduites.js +++ b/app/static/js/assiduites.js @@ -755,9 +755,7 @@ function isConflictSameAsPeriod(conflict, period = undefined) { * @returns {Date} la date sélectionnée */ function getDate() { - const date = new Date( - document.querySelector("#tl_date").getAttribute("value") - ); + const date = new Date(document.querySelector("#tl_date").value); date.setHours(0, 0, 0, 0); return date; } @@ -1804,7 +1802,9 @@ function getModuleImpl(assiduite) { assiduite.external_data != null && assiduite.external_data.hasOwnProperty("module") ) { - return assiduite.external_data.module; + return assiduite.external_data.module == "Autre" + ? "Tout module" + : assiduite.external_data.module; } else { return "Pas de module"; } From 943055b32855487d2637f24c0913e989f1450aef Mon Sep 17 00:00:00 2001 From: Iziram Date: Fri, 27 Oct 2023 16:05:40 +0200 Subject: [PATCH 7/9] Assiduites : documentation code python --- app/api/assiduites.py | 280 +++++++++++++++++++++------ app/api/justificatifs.py | 240 ++++++++++++++++------- app/forms/main/config_assiduites.py | 4 +- app/models/assiduites.py | 76 +++++++- app/views/assiduites.py | 286 +++++++++++++++++++--------- tools/downgrade_assiduites.py | 32 +++- 6 files changed, 682 insertions(+), 236 deletions(-) diff --git a/app/api/assiduites.py b/app/api/assiduites.py index 33a24136..ae0993a7 100644 --- a/app/api/assiduites.py +++ b/app/api/assiduites.py @@ -163,23 +163,22 @@ def count_assiduites( """ - # query = Identite.query.filter_by(id=etudid) - # if g.scodoc_dept: - # query = query.filter_by(dept_id=g.scodoc_dept_id) - - # etud: Identite = query.first_or_404(etudid) + # Récupération de l'étudiant etud: Identite = tools.get_etud(etudid, nip, ine) - + # Vérification que l'étudiant existe if etud is None: return json_error( 404, message="étudiant inconnu", ) + # Les filtres qui seront appliqués au comptage (type, date, etudid...) filtered: dict[str, object] = {} + # la métrique du comptage (all, demi, heure, journee) metric: str = "all" + # Si la requête a des paramètres if with_query: metric, filtered = _count_manager(request) @@ -254,11 +253,7 @@ def assiduites(etudid: int = None, nip=None, ine=None, with_query: bool = False) """ - # query = Identite.query.filter_by(id=etudid) - # if g.scodoc_dept: - # query = query.filter_by(dept_id=g.scodoc_dept_id) - - # etud: Identite = query.first_or_404(etudid) + # Récupération de l'étudiant etud: Identite = tools.get_etud(etudid, nip, ine) if etud is None: @@ -266,15 +261,23 @@ def assiduites(etudid: int = None, nip=None, ine=None, with_query: bool = False) 404, message="étudiant inconnu", ) + # Récupération des assiduités de l'étudiant assiduites_query: Query = etud.assiduites + # Filtrage des assiduités en fonction des paramètres de la requête if with_query: assiduites_query = _filter_manager(request, assiduites_query) + # Préparation de la réponse json + data_set: list[dict] = [] + for ass in assiduites_query.all(): + # conversion Assiduite -> Dict data = ass.to_dict(format_api=True) + # Ajout des justificatifs (ou non dépendamment de la requête) data = _with_justifs(data) + # Ajout de l'assiduité dans la liste de retour data_set.append(data) return data_set @@ -326,6 +329,7 @@ def assiduites_group(with_query: bool = False): """ + # Récupération des étudiants dans la requête etuds = request.args.get("etudids", "") etuds = etuds.split(",") try: @@ -333,6 +337,7 @@ def assiduites_group(with_query: bool = False): except ValueError: return json_error(404, "Le champs etudids n'est pas correctement formé") + # Vérification que tous les étudiants sont du même département query = Identite.query.filter(Identite.id.in_(etuds)) if g.scodoc_dept: query = query.filter_by(dept_id=g.scodoc_dept_id) @@ -342,15 +347,21 @@ def assiduites_group(with_query: bool = False): 404, "Tous les étudiants ne sont pas dans le même département et/ou n'existe pas.", ) + + # Récupération de toutes les assiduités liés aux étudiants assiduites_query = Assiduite.query.filter(Assiduite.etudid.in_(etuds)) + # Filtrage des assiduités en fonction des filtres passés dans la requête if with_query: assiduites_query = _filter_manager(request, assiduites_query) + # Préparation de retour json + # Dict représentant chaque étudiant avec sa liste d'assiduité data_set: dict[list[dict]] = {str(key): [] for key in etuds} for ass in assiduites_query.all(): data = ass.to_dict(format_api=True) data = _with_justifs(data) + # Ajout de l'assiduité dans la liste du bon étudiant data_set.get(str(data["etudid"])).append(data) return data_set @@ -375,20 +386,23 @@ def assiduites_group(with_query: bool = False): @permission_required(Permission.ScoView) def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False): """Retourne toutes les assiduités du formsemestre""" + + # Récupération du formsemestre à partir du formsemestre_id formsemestre: FormSemestre = None formsemestre_id = int(formsemestre_id) formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first() - if formsemestre is None: return json_error(404, "le paramètre 'formsemestre_id' n'existe pas") + # Récupération des assiduités du formsemestre assiduites_query = scass.filter_by_formsemestre( Assiduite.query, Assiduite, formsemestre ) - + # Filtrage en fonction des paramètres de la requête if with_query: assiduites_query = _filter_manager(request, assiduites_query) + # Préparation du retour JSON data_set: list[dict] = [] for ass in assiduites_query.all(): data = ass.to_dict(format_api=True) @@ -422,21 +436,28 @@ def count_assiduites_formsemestre( formsemestre_id: int = None, with_query: bool = False ): """Comptage des assiduités du formsemestre""" + + # Récupération du formsemestre à partir du formsemestre_id formsemestre: FormSemestre = None formsemestre_id = int(formsemestre_id) formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first() - if formsemestre is None: return json_error(404, "le paramètre 'formsemestre_id' n'existe pas") + # Récupération des étudiants du formsemestre etuds = formsemestre.etuds.all() etuds_id = [etud.id for etud in etuds] + # Récupération des assiduités des étudiants du semestre assiduites_query = Assiduite.query.filter(Assiduite.etudid.in_(etuds_id)) + # Filtrage des assiduités en fonction des dates du semestre assiduites_query = scass.filter_by_formsemestre( assiduites_query, Assiduite, formsemestre ) + + # Gestion de la métrique de comptage (all,demi,heure,journee) metric: str = "all" + # Gestion du filtre (en fonction des paramètres de la requête) filtered: dict = {} if with_query: metric, filtered = _count_manager(request) @@ -481,23 +502,36 @@ def assiduite_create(etudid: int = None, nip=None, ine=None): ] """ + # Récupération de l'étudiant etud: Identite = tools.get_etud(etudid, nip, ine) if etud is None: return json_error( 404, message="étudiant inconnu", ) + # Mise à jour du "g.scodoc_dept" si route sans dept if g.scodoc_dept is None and etud.dept_id is not None: # route sans département set_sco_dept(etud.departement.acronym) + + # Récupération de la liste des assiduités à créer create_list: list[object] = request.get_json(force=True) + # Vérification que c'est bien une liste if not isinstance(create_list, list): return json_error(404, "Le contenu envoyé n'est pas une liste") - errors: list = [] - success: list = [] + # Préparation du retour + + errors: list[dict[str, object]] = [] + success: list[dict[str, object]] = [] + + # Pour chaque objet de la liste, + # on récupère son indice et l'objet for i, data in enumerate(create_list): + # On créé l'assiduité + # 200 + obj si réussi + # 404 + message d'erreur si non réussi code, obj = _create_one(data, etud) if code == 404: errors.append({"indice": i, "message": obj}) @@ -570,53 +604,77 @@ def _create_one( data: dict, etud: Identite, ) -> tuple[int, object]: - """TODO @iziram: documenter""" + """ + _create_one Création d'une assiduité à partir d'une représentation JSON + + Cette fonction vérifie la représentation JSON + + Puis crée l'assiduité si la représentation est valide. + + Args: + data (dict): représentation json d'une assiduité + etud (Identite): l'étudiant concerné par l'assiduité + + Returns: + tuple[int, object]: code, objet + code : 200 si réussi 404 sinon + objet : dict{assiduite_id:?} si réussi str"message d'erreur" sinon + """ + errors: list[str] = [] # -- vérifications de l'objet json -- # cas 1 : ETAT - etat = data.get("etat", None) + etat: str = data.get("etat", None) if etat is None: errors.append("param 'etat': manquant") elif not scu.EtatAssiduite.contains(etat): errors.append("param 'etat': invalide") - etat = scu.EtatAssiduite.get(etat) + etat: scu.EtatAssiduite = scu.EtatAssiduite.get(etat) # cas 2 : date_debut - date_debut = data.get("date_debut", None) + date_debut: str = data.get("date_debut", None) if date_debut is None: errors.append("param 'date_debut': manquant") - deb = scu.is_iso_formated(date_debut, convert=True) + # Conversion de la chaine de caractère en datetime (tz ou non) + deb: datetime = scu.is_iso_formated(date_debut, convert=True) + # si chaine invalide if deb is None: errors.append("param 'date_debut': format invalide") + # Si datetime sans timezone elif deb.tzinfo is None: - deb = scu.localize_datetime(deb) + # Mise à jour de la timezone avec celle du serveur + deb: datetime = scu.localize_datetime(deb) - # cas 3 : date_fin - date_fin = data.get("date_fin", None) + # cas 3 : date_fin (Même fonctionnement ^ ) + date_fin: str = data.get("date_fin", None) if date_fin is None: errors.append("param 'date_fin': manquant") - fin = scu.is_iso_formated(date_fin, convert=True) + fin: datetime = scu.is_iso_formated(date_fin, convert=True) if fin is None: errors.append("param 'date_fin': format invalide") elif fin.tzinfo is None: - fin = scu.localize_datetime(fin) + fin: datetime = scu.localize_datetime(fin) - # cas 5 : desc + # cas 4 : desc desc: str = data.get("desc", None) - external_data = data.get("external_data", None) + # cas 5 : external data + external_data: dict = data.get("external_data", None) if external_data is not None: if not isinstance(external_data, dict): errors.append("param 'external_data' : n'est pas un objet JSON") - # cas 4 : moduleimpl_id + # cas 6 : moduleimpl_id + # On récupère le moduleimpl moduleimpl_id = data.get("moduleimpl_id", False) moduleimpl: ModuleImpl = None + # On vérifie si le moduleimpl existe (uniquement s'il a été renseigné) if moduleimpl_id not in [False, None, "", "-1"]: + # Si le moduleimpl n'est pas "autre" alors on vérifie si l'id est valide if moduleimpl_id != "autre": try: moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first() @@ -625,16 +683,23 @@ def _create_one( if moduleimpl is None: errors.append("param 'moduleimpl_id': invalide") else: + # Sinon on met à none le moduleimpl + # et on ajoute dans external data + # le module "autre" moduleimpl_id = None - external_data = external_data if external_data is not None else {} + external_data: dict = external_data if external_data is not None else {} external_data["module"] = "Autre" + # Si il y a des erreurs alors on ne crée pas l'assiduité et on renvoie les erreurs if errors: + # Construit une chaine de caractère avec les erreurs séparées par `,` err: str = ", ".join(errors) + # 404 représente le code d'erreur et err la chaine nouvellement créée return (404, err) - # TOUT EST OK + # SI TOUT EST OK try: + # On essaye de créer l'assiduité nouv_assiduite: Assiduite = Assiduite.create_assiduite( date_debut=deb, date_fin=fin, @@ -647,12 +712,23 @@ def _create_one( notify_mail=True, ) + # create_assiduite générera des ScoValueError si jamais il y a un autre problème + # - Etudiant non inscrit dans le module + # - module obligatoire + # - Assiduité conflictuelles + + # Si tout s'est bien passé on ajoute l'assiduité à la session + # et on retourne un code 200 avec un objet possèdant l'assiduite_id db.session.add(nouv_assiduite) db.session.commit() - return (200, {"assiduite_id": nouv_assiduite.id}) except ScoValueError as excp: # ici on utilise pas json_error car on doit renvoyer status, message + # Ici json_error ne peut être utilisé car il terminerai le processus de création + # Cela voudrait dire qu'une seule erreur dans une assiduité imposerait de + # tout refaire à partir de l'erreur. + + # renvoit un code 404 et le message d'erreur de la ScoValueError return 404, excp.args[0] @@ -675,14 +751,20 @@ def assiduite_delete(): """ + # Récupération des ids envoyés dans la liste assiduites_list: list[int] = request.get_json(force=True) if not isinstance(assiduites_list, list): return json_error(404, "Le contenu envoyé n'est pas une liste") + # Préparation du retour json output = {"errors": [], "success": []} - for i, ass in enumerate(assiduites_list): - code, msg = _delete_singular(ass, db) + # Pour chaque assiduite_id on essaye de supprimer l'assiduité + for i, assiduite_id in enumerate(assiduites_list): + # De la même façon que "_create_one" + # Ici le code est soit 200 si réussi ou 404 si raté + # Le message est le message d'erreur si erreur + code, msg = _delete_one(assiduite_id) if code == 404: output["errors"].append({"indice": i, "message": msg}) else: @@ -692,24 +774,43 @@ def assiduite_delete(): return output -def _delete_singular(assiduite_id: int, database) -> tuple[int, str]: - """@iziram PLEASE COMMENT THIS F*CKING CODE""" +def _delete_one(assiduite_id: int) -> tuple[int, str]: + """ + _delete_singular Supprime une assiduité à partir de son id + + Args: + assiduite_id (int): l'identifiant de l'assiduité + Returns: + tuple[int, str]: code, message + code : 200 si réussi, 404 sinon + message : OK si réussi, le message d'erreur sinon + """ assiduite_unique: Assiduite = Assiduite.query.filter_by(id=assiduite_id).first() if assiduite_unique is None: - # on ne peut pas utiliser json_error ici car on est déclaré (int, str) + # Ici json_error ne peut être utilisé car il terminerai le processus de création + # Cela voudrait dire qu'une seule erreur d'id imposerait de + # tout refaire à partir de l'erreur. return 404, "Assiduite non existante" + + # Mise à jour du g.scodoc_dept si la route est sans département 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() + + # Récupération de la version dict de l'assiduité + # Pour invalider le cache + assi_dict = assiduite_unique.to_dict() + + # Suppression de l'assiduité et LOG log(f"delete_assiduite: {assiduite_unique.etudiant.id} {assiduite_unique}") Scolog.logdb( method="delete_assiduite", etudid=assiduite_unique.etudiant.id, msg=f"assiduité: {assiduite_unique}", ) - database.session.delete(assiduite_unique) - scass.simple_invalidate_cache(ass_dict) + db.session.delete(assiduite_unique) + # Invalidation du cache + scass.simple_invalidate_cache(assi_dict) return 200, "OK" @@ -730,17 +831,25 @@ def assiduite_edit(assiduite_id: int): "est_just"?: bool } """ + + # Récupération de l'assiduité à modifier assiduite_unique: Assiduite = Assiduite.query.filter_by( id=assiduite_id ).first_or_404() - errors: list[str] = [] + # Récupération des valeurs à modifier data = request.get_json(force=True) - code, obj = _edit_singular(assiduite_unique, data) + # Préparation du retour + errors: list[str] = [] + + # Code 200 si modification réussie + # Code 404 si raté + message d'erreur + code, obj = _edit_one(assiduite_unique, data) if code == 404: return json_error(404, obj) + # Mise à jour de l'assiduité et LOG log(f"assiduite_edit: {assiduite_unique.etudiant.id} {assiduite_unique}") Scolog.logdb( "assiduite_edit", @@ -791,7 +900,7 @@ def assiduites_edit(): ) continue - code, obj = _edit_singular(assi, data) + code, obj = _edit_one(assi, data) obj_retour = { "indice": i, "message": obj, @@ -806,46 +915,69 @@ def assiduites_edit(): return {"errors": errors, "success": success} -def _edit_singular(assiduite_unique, data): +def _edit_one(assiduite_unique: Assiduite, data: dict) -> tuple[int, str]: + """ + _edit_singular Modifie une assiduité à partir de données JSON + + Args: + assiduite_unique (Assiduite): l'assiduité à modifier + data (dict): les nouvelles données + + Returns: + tuple[int,str]: code, message + code : 200 si réussi, 404 sinon + message : OK si réussi, message d'erreur sinon + """ + + # Mise à jour du g.scodoc_dept en cas de route sans département 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 # Cas 1 : Etat if data.get("etat") is not None: - etat = scu.EtatAssiduite.get(data.get("etat")) + etat: scu.EtatAssiduite = scu.EtatAssiduite.get(data.get("etat")) if etat is None: errors.append("param 'etat': invalide") else: + # Mise à jour de l'état assiduite_unique.etat = etat - external_data = data.get("external_data") + # Cas 2 : external_data + external_data: dict = data.get("external_data") if external_data is not None: if not isinstance(external_data, dict): errors.append("param 'external_data' : n'est pas un objet JSON") else: + # Mise à jour de l'external data assiduite_unique.external_data = external_data - # Cas 2 : Moduleimpl_id + # Cas 3 : Moduleimpl_id moduleimpl_id = data.get("moduleimpl_id", False) moduleimpl: ModuleImpl = None + # False si on modifie pas le moduleimpl if moduleimpl_id is not False: + # Si le module n'est pas nul if moduleimpl_id not in [None, "", "-1"]: + # Gestion du module Autre if moduleimpl_id == "autre": + # module autre = moduleimpl_id:None + external_data["module"]:"Autre" assiduite_unique.moduleimpl_id = None - external_data = ( + external_data: dict = ( external_data if external_data is not None and isinstance(external_data, dict) else assiduite_unique.external_data ) - external_data = external_data if external_data is not None else {} + external_data: dict = external_data if external_data is not None else {} external_data["module"] = "Autre" assiduite_unique.external_data = external_data else: + # Vérification de l'id et récupération de l'objet ModuleImpl try: moduleimpl = ModuleImpl.query.filter_by( id=int(moduleimpl_id) @@ -861,8 +993,11 @@ def _edit_singular(assiduite_unique, data): ): errors.append("param 'moduleimpl_id': etud non inscrit") else: + # Mise à jour du moduleimpl assiduite_unique.moduleimpl_id = moduleimpl_id else: + # Vérification du force module en cas de modification du moduleimpl en moduleimpl nul + # Récupération du formsemestre lié à l'assiduité formsemestre: FormSemestre = get_formsemestre_from_data( assiduite_unique.to_dict() ) @@ -884,12 +1019,12 @@ def _edit_singular(assiduite_unique, data): "param 'moduleimpl_id' : le moduleimpl_id ne peut pas être nul" ) - # Cas 3 : desc - desc = data.get("desc", False) + # Cas 4 : desc + desc: str = data.get("desc", False) if desc is not False: assiduite_unique.description = desc - # Cas 4 : est_just + # Cas 5 : est_just if assiduite_unique.etat == scu.EtatAssiduite.PRESENT: assiduite_unique.est_just = False else: @@ -906,9 +1041,11 @@ def _edit_singular(assiduite_unique, data): ) if errors: + # Retour des erreurs en une seule chaîne séparée par des `,` err: str = ", ".join(errors) return (404, err) + # Mise à jour de l'assiduité et LOG log(f"_edit_singular: {assiduite_unique.etudiant.id} {assiduite_unique}") Scolog.logdb( "assiduite_edit", @@ -1003,21 +1140,34 @@ def _count_manager(requested) -> tuple[str, dict]: def _filter_manager(requested, assiduites_query: Query) -> Query: """ - Retourne les assiduites entrées filtrées en fonction de la request + _filter_manager Retourne les assiduites entrées filtrées en fonction de la request + + Args: + requested (request): La requête http + assiduites_query (Query): la query d'assiduités à filtrer + + Returns: + Query: La query filtrée """ # cas 1 : etat assiduite - etat = requested.args.get("etat") + etat: str = requested.args.get("etat") if etat is not None: assiduites_query = scass.filter_assiduites_by_etat(assiduites_query, etat) # cas 2 : date de début - deb = requested.args.get("date_debut", "").replace(" ", "+") - deb: datetime = scu.is_iso_formated(deb, True) + deb: str = requested.args.get("date_debut", "").replace(" ", "+") + deb: datetime = scu.is_iso_formated( + deb, True + ) # transformation de la chaine en datetime # cas 3 : date de fin - fin = requested.args.get("date_fin", "").replace(" ", "+") - fin = scu.is_iso_formated(fin, True) + fin: str = requested.args.get("date_fin", "").replace(" ", "+") + fin: datetime = scu.is_iso_formated( + fin, True + ) # transformation de la chaine en datetime + # Pour filtrer les dates il faut forcement avoir les deux bornes + # [date_debut : date_fin] if (deb, fin) != (None, None): assiduites_query: Query = scass.filter_by_date( assiduites_query, Assiduite, deb, fin @@ -1071,10 +1221,12 @@ def _filter_manager(requested, assiduites_query: Query) -> Query: if user_id is not False: assiduites_query: Query = scass.filter_by_user_id(assiduites_query, user_id) + # cas 9 : order (renvoie la query ordonnée en "date début Décroissante") order = requested.args.get("order", None) if order is not None: assiduites_query: Query = assiduites_query.order_by(Assiduite.date_debut.desc()) + # cas 10 : courant (Ne renvoie que les assiduités de l'année courante) courant = requested.args.get("courant", None) if courant is not None: annee: int = scu.annee_scolaire() @@ -1087,7 +1239,17 @@ def _filter_manager(requested, assiduites_query: Query) -> Query: return assiduites_query -def _with_justifs(assi): +def _with_justifs(assi: dict): + """ + _with_justifs ajoute la liste des justificatifs à l'assiduité + + Condition : `with_justifs` doit se trouver dans les paramètres de la requête + Args: + assi (dict): un dictionnaire représentant une assiduité + + Returns: + dict: l'assiduité avec les justificatifs ajoutés + """ if request.args.get("with_justifs") is None: return assi assi["justificatifs"] = get_assiduites_justif(assi["assiduite_id"], True) diff --git a/app/api/justificatifs.py b/app/api/justificatifs.py index e8893388..b398b83c 100644 --- a/app/api/justificatifs.py +++ b/app/api/justificatifs.py @@ -3,7 +3,7 @@ # Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## -"""ScoDoc 9 API : Assiduités +"""ScoDoc 9 API : Justificatifs """ from datetime import datetime @@ -28,6 +28,7 @@ from app.models import ( ) from app.models.assiduites import ( compute_assiduites_justified, + get_formsemestre_from_data, ) from app.scodoc.sco_archives_justificatifs import JustificatifArchiver from app.scodoc.sco_exceptions import ScoValueError @@ -114,7 +115,7 @@ def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = Fal query?user_id=[int] ex query?user_id=3 """ - + # Récupération de l'étudiant etud: Identite = tools.get_etud(etudid, nip, ine) if etud is None: @@ -122,11 +123,15 @@ def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = Fal 404, message="étudiant inconnu", ) + + # Récupération des justificatifs de l'étudiant justificatifs_query = etud.justificatifs + # Filtrage des justificatifs en fonction de la requête if with_query: justificatifs_query = _filter_manager(request, justificatifs_query) + # Mise en forme des données puis retour en JSON data_set: list[dict] = [] for just in justificatifs_query.all(): data = just.to_dict(format_api=True) @@ -147,44 +152,51 @@ def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = Fal @permission_required(Permission.ScoView) def justificatifs_dept(dept_id: int = None, with_query: bool = False): """ """ - dept = Departement.query.get_or_404(dept_id) - etuds = [etud.id for etud in dept.etudiants] - justificatifs_query = Justificatif.query.filter(Justificatif.etudid.in_(etuds)) + # Récupération du département et des étudiants du département + dept: Departement = Departement.query.get_or_404(dept_id) + etuds: list[int] = [etud.id for etud in dept.etudiants] + # Récupération des justificatifs des étudiants du département + justificatifs_query: Query = Justificatif.query.filter( + Justificatif.etudid.in_(etuds) + ) + + # Filtrage des justificatifs if with_query: - justificatifs_query = _filter_manager(request, justificatifs_query) + justificatifs_query: Query = _filter_manager(request, justificatifs_query) + # Mise en forme des données et retour JSON data_set: list[dict] = [] for just in justificatifs_query: - data_set.append(_set_sems_and_groupe(just)) + data_set.append(_set_sems(just)) return data_set -def _set_sems_and_groupe(justi: Justificatif) -> dict: - from app.scodoc.sco_groups import get_etud_groups +def _set_sems(justi: Justificatif) -> dict: + """ + _set_sems Ajoute le formsemestre associé au justificatif s'il existe + Si le formsemestre n'existe pas, renvoie la simple représentation du justificatif + + Args: + justi (Justificatif): Le justificatif + + Returns: + dict: La représentation de l'assiduité en dictionnaire + """ + # Conversion du justificatif en dictionnaire data = justi.to_dict(format_api=True) - formsemestre: FormSemestre = ( - FormSemestre.query.join( - FormSemestreInscription, - FormSemestre.id == FormSemestreInscription.formsemestre_id, - ) - .filter( - justi.date_debut <= FormSemestre.date_fin, - justi.date_fin >= FormSemestre.date_debut, - FormSemestreInscription.etudid == justi.etudid, - ) - .first() - ) + # Récupération du formsemestre de l'assiduité + formsemestre: FormSemestre = get_formsemestre_from_data(justi.to_dict()) + # Si le formsemestre existe on l'ajoute au dictionnaire if formsemestre: data["formsemestre"] = { "id": formsemestre.id, "title": formsemestre.session_id(), } - return data @@ -208,20 +220,27 @@ def _set_sems_and_groupe(justi: Justificatif) -> dict: @permission_required(Permission.ScoView) def justificatifs_formsemestre(formsemestre_id: int, with_query: bool = False): """Retourne tous les justificatifs du formsemestre""" + + # Récupération du formsemestre formsemestre: FormSemestre = None formsemestre_id = int(formsemestre_id) - formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first() + formsemestre: FormSemestre = FormSemestre.query.filter_by( + id=formsemestre_id + ).first() if formsemestre is None: return json_error(404, "le paramètre 'formsemestre_id' n'existe pas") - justificatifs_query = scass.filter_by_formsemestre( + # Récupération des justificatifs du semestre + justificatifs_query: Query = scass.filter_by_formsemestre( Justificatif.query, Justificatif, formsemestre ) + # Filtrage des justificatifs if with_query: - justificatifs_query = _filter_manager(request, justificatifs_query) + justificatifs_query: Query = _filter_manager(request, justificatifs_query) + # Retour des justificatifs en JSON data_set: list[dict] = [] for justi in justificatifs_query.all(): data = justi.to_dict(format_api=True) @@ -264,6 +283,8 @@ def justif_create(etudid: int = None, nip=None, ine=None): ] """ + + # Récupération de l'étudiant etud: Identite = tools.get_etud(etudid, nip, ine) if etud is None: @@ -272,16 +293,22 @@ def justif_create(etudid: int = None, nip=None, ine=None): message="étudiant inconnu", ) + # Récupération des justificatifs à créer create_list: list[object] = request.get_json(force=True) if not isinstance(create_list, list): return json_error(404, "Le contenu envoyé n'est pas une liste") - errors: list = [] - success: list = [] - justifs: list = [] + errors: list[dict] = [] + success: list[dict] = [] + justifs: list[Justificatif] = [] + + # énumération des justificatifs for i, data in enumerate(create_list): code, obj, justi = _create_one(data, etud) + code: int + obj: str | dict + justi: Justificatif | None if code == 404: errors.append({"indice": i, "message": obj}) else: @@ -289,6 +316,7 @@ def justif_create(etudid: int = None, nip=None, ine=None): justifs.append(justi) scass.simple_invalidate_cache(data, etud.id) + # Actualisation des assiduités justifiées en fonction de tous les nouveaux justificatifs compute_assiduites_justified(etud.etudid, justifs) return {"errors": errors, "success": success} @@ -296,32 +324,32 @@ def justif_create(etudid: int = None, nip=None, ine=None): def _create_one( data: dict, etud: Identite, -) -> tuple[int, object]: +) -> tuple[int, object, Justificatif]: errors: list[str] = [] # -- vérifications de l'objet json -- # cas 1 : ETAT - etat = data.get("etat", None) + etat: str = data.get("etat", None) if etat is None: errors.append("param 'etat': manquant") elif not scu.EtatJustificatif.contains(etat): errors.append("param 'etat': invalide") - etat = scu.EtatJustificatif.get(etat) + etat: scu.EtatJustificatif = scu.EtatJustificatif.get(etat) # cas 2 : date_debut - date_debut = data.get("date_debut", None) + date_debut: str = data.get("date_debut", None) if date_debut is None: errors.append("param 'date_debut': manquant") - deb = scu.is_iso_formated(date_debut, convert=True) + deb: datetime = scu.is_iso_formated(date_debut, convert=True) if deb is None: errors.append("param 'date_debut': format invalide") # cas 3 : date_fin - date_fin = data.get("date_fin", None) + date_fin: str = data.get("date_fin", None) if date_fin is None: errors.append("param 'date_fin': manquant") - fin = scu.is_iso_formated(date_fin, convert=True) + fin: datetime = scu.is_iso_formated(date_fin, convert=True) if fin is None: errors.append("param 'date_fin': format invalide") @@ -329,7 +357,7 @@ def _create_one( raison: str = data.get("raison", None) - external_data = data.get("external_data") + external_data: dict = data.get("external_data") if external_data is not None: if not isinstance(external_data, dict): errors.append("param 'external_data' : n'est pas un objet JSON") @@ -341,6 +369,7 @@ def _create_one( # TOUT EST OK try: + # On essaye de créer le justificatif nouv_justificatif: Query = Justificatif.create_justificatif( date_debut=deb, date_fin=fin, @@ -351,6 +380,11 @@ def _create_one( external_data=external_data, ) + # Si tout s'est bien passé on ajoute l'assiduité à la session + # et on retourne un code 200 avec un objet possèdant le justif_id + # ainsi que les assiduités justifiées par le dit justificatif + + # On renvoie aussi le justificatif créé pour pour le calcul total de fin db.session.add(nouv_justificatif) db.session.commit() @@ -363,10 +397,7 @@ def _create_one( nouv_justificatif, ) except ScoValueError as excp: - return ( - 404, - excp.args[0], - ) + return (404, excp.args[0], None) @bp.route("/justificatif//edit", methods=["POST"]) @@ -387,53 +418,58 @@ def justif_edit(justif_id: int): "date_fin"?: str } """ + + # Récupération du justificatif à modifier justificatif_unique: Query = Justificatif.query.filter_by( id=justif_id ).first_or_404() errors: list[str] = [] data = request.get_json(force=True) + + # Récupération des assiduités (id) précédemment justifiée par le justificatif avant_ids: list[int] = scass.justifies(justificatif_unique) # Vérifications de data # Cas 1 : Etat if data.get("etat") is not None: - etat = scu.EtatJustificatif.get(data.get("etat")) + etat: scu.EtatJustificatif = scu.EtatJustificatif.get(data.get("etat")) if etat is None: errors.append("param 'etat': invalide") else: justificatif_unique.etat = etat # Cas 2 : raison - raison = data.get("raison", False) + raison: str = data.get("raison", False) if raison is not False: justificatif_unique.raison = raison deb, fin = None, None # cas 3 : date_debut - date_debut = data.get("date_debut", False) + date_debut: str = data.get("date_debut", False) if date_debut is not False: if date_debut is None: errors.append("param 'date_debut': manquant") - deb = scu.is_iso_formated(date_debut.replace(" ", "+"), convert=True) + deb: datetime = scu.is_iso_formated(date_debut.replace(" ", "+"), convert=True) if deb is None: errors.append("param 'date_debut': format invalide") # cas 4 : date_fin - date_fin = data.get("date_fin", False) + date_fin: str = data.get("date_fin", False) if date_fin is not False: if date_fin is None: errors.append("param 'date_fin': manquant") - fin = scu.is_iso_formated(date_fin.replace(" ", "+"), convert=True) + fin: datetime = scu.is_iso_formated(date_fin.replace(" ", "+"), convert=True) if fin is None: errors.append("param 'date_fin': format invalide") - # Mise à jour des dates + # Récupération des dates précédentes si deb ou fin est None deb = deb if deb is not None else justificatif_unique.date_debut fin = fin if fin is not None else justificatif_unique.date_fin - external_data = data.get("external_data") + # Mise à jour de l'external data + external_data: dict = data.get("external_data") if external_data is not None: if not isinstance(external_data, dict): errors.append("param 'external_data' : n'est pas un objet JSON") @@ -443,6 +479,7 @@ def justif_edit(justif_id: int): if fin <= deb: errors.append("param 'dates' : Date de début après date de fin") + # Mise à jour des dates du justificatif justificatif_unique.date_debut = deb justificatif_unique.date_fin = fin @@ -450,9 +487,14 @@ def justif_edit(justif_id: int): err: str = ", ".join(errors) return json_error(404, err) + # Mise à jour du justificatif db.session.add(justificatif_unique) db.session.commit() + # Génération du dictionnaire de retour + # La couverture correspond + # - aux assiduités précédemment justifiées par le justificatif + # - aux assiduités qui sont justifiées par le justificatif modifié retour = { "couverture": { "avant": avant_ids, @@ -463,7 +505,7 @@ def justif_edit(justif_id: int): ), } } - + # Invalide le cache scass.simple_invalidate_cache(justificatif_unique.to_dict()) return retour @@ -487,6 +529,8 @@ def justif_delete(): """ + + # Récupération des justif_ids justificatifs_list: list[int] = request.get_json(force=True) if not isinstance(justificatifs_list, list): return json_error(404, "Le contenu envoyé n'est pas une liste") @@ -494,7 +538,7 @@ def justif_delete(): output = {"errors": [], "success": []} for i, ass in enumerate(justificatifs_list): - code, msg = _delete_singular(ass, db) + code, msg = _delete_one(ass) if code == 404: output["errors"].append({"indice": i, "message": msg}) else: @@ -505,22 +549,41 @@ def justif_delete(): return output -def _delete_singular(justif_id: int, database): - justificatif_unique: Query = Justificatif.query.filter_by(id=justif_id).first() +def _delete_one(justif_id: int) -> tuple[int, str]: + """ + _delete_one Supprime un justificatif + + Args: + justif_id (int): l'identifiant du justificatif + + Returns: + tuple[int, str]: code, message + code : 200 si réussi, 404 sinon + message : OK si réussi, message d'erreur sinon + """ + # Récupération du justificatif à supprimer + justificatif_unique: Justificatif = Justificatif.query.filter_by( + id=justif_id + ).first() if justificatif_unique is None: return (404, "Justificatif non existant") + # Récupération de l'archive du justificatif archive_name: str = justificatif_unique.fichier if archive_name is not None: + # Si elle existe : on essaye de la supprimer archiver: JustificatifArchiver = JustificatifArchiver() try: archiver.delete_justificatif(justificatif_unique.etudiant, archive_name) except ValueError: pass + # On invalide le cache scass.simple_invalidate_cache(justificatif_unique.to_dict()) - database.session.delete(justificatif_unique) + # On supprime le justificatif + db.session.delete(justificatif_unique) + # On actualise les assiduités justifiées de l'étudiant concerné compute_assiduites_justified( justificatif_unique.etudid, Justificatif.query.filter_by(etudid=justificatif_unique.etudid).all(), @@ -541,23 +604,27 @@ def justif_import(justif_id: int = None): """ Importation d'un fichier (création d'archive) """ + + # On vérifie qu'un fichier a bien été envoyé if len(request.files) == 0: return json_error(404, "Il n'y a pas de fichier joint") - file = list(request.files.values())[0] if file.filename == "": return json_error(404, "Il n'y a pas de fichier joint") + # On récupère le justificatif auquel on va importer le fichier query: Query = Justificatif.query.filter_by(id=justif_id) if g.scodoc_dept: query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id) - justificatif_unique: Justificatif = query.first_or_404() + # Récupération de l'archive si elle existe archive_name: str = justificatif_unique.fichier + # Utilisation de l'archiver de justificatifs archiver: JustificatifArchiver = JustificatifArchiver() try: + # On essaye de sauvegarder le fichier fname: str archive_name, fname = archiver.save_justificatif( justificatif_unique.etudiant, @@ -567,6 +634,7 @@ def justif_import(justif_id: int = None): user_id=current_user.id, ) + # On actualise l'archive du justificatif justificatif_unique.fichier = archive_name db.session.add(justificatif_unique) @@ -574,6 +642,7 @@ def justif_import(justif_id: int = None): return {"filename": fname} except ScoValueError as err: + # Si cela ne fonctionne pas on renvoie une erreur return json_error(404, err.args[0]) @@ -587,23 +656,26 @@ def justif_export(justif_id: int = None, filename: str = None): Retourne un fichier d'une archive d'un justificatif """ + # On récupère le justificatif concerné query: Query = Justificatif.query.filter_by(id=justif_id) if g.scodoc_dept: query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id) - justificatif_unique: Justificatif = query.first_or_404() + # On récupère l'archive concernée archive_name: str = justificatif_unique.fichier if archive_name is None: + # On retourne une erreur si le justificatif n'a pas de fichiers return json_error(404, "le justificatif ne possède pas de fichier") + # On récupère le fichier et le renvoie en une réponse déjà formée archiver: JustificatifArchiver = JustificatifArchiver() - try: return archiver.get_justificatif_file( archive_name, justificatif_unique.etudiant, filename ) except ScoValueError as err: + # On retourne une erreur json si jamais il y a un problème return json_error(404, err.args[0]) @@ -616,7 +688,6 @@ def justif_export(justif_id: int = None, filename: str = None): def justif_remove(justif_id: int = None): """ Supression d'un fichier ou d'une archive - # TOTALK: Doc, expliquer les noms coté server { "remove": <"all"/"list"> @@ -627,31 +698,41 @@ def justif_remove(justif_id: int = None): } """ + # On récupère le dictionnaire data: dict = request.get_json(force=True) + # On récupère le justificatif concerné query: Query = Justificatif.query.filter_by(id=justif_id) if g.scodoc_dept: query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id) - justificatif_unique: Justificatif = query.first_or_404() + # On récupère l'archive archive_name: str = justificatif_unique.fichier if archive_name is None: + # On retourne une erreur si le justificatif n'a pas de fichiers return json_error(404, "le justificatif ne possède pas de fichier") + # On regarde le type de suppression (all ou list) + # Si all : on supprime tous les fichiers + # Si list : on supprime les fichiers dont le nom est dans la liste remove: str = data.get("remove") if remove is None or remove not in ("all", "list"): return json_error(404, "param 'remove': Valeur invalide") + + # On récupère l'archiver et l'étudiant archiver: JustificatifArchiver = JustificatifArchiver() etud = justificatif_unique.etudiant try: if remove == "all": + # Suppression de toute l'archive du justificatif archiver.delete_justificatif(etud, archive_name=archive_name) justificatif_unique.fichier = None db.session.add(justificatif_unique) db.session.commit() else: + # Suppression des fichiers dont le nom se trouve dans la liste "filenames" for fname in data.get("filenames", []): archiver.delete_justificatif( etud, @@ -659,6 +740,7 @@ def justif_remove(justif_id: int = None): filename=fname, ) + # Si il n'y a plus de fichiers dans l'archive, on la supprime if len(archiver.list_justificatifs(archive_name, etud)) == 0: archiver.delete_justificatif(etud, archive_name) justificatif_unique.fichier = None @@ -666,8 +748,10 @@ def justif_remove(justif_id: int = None): db.session.commit() except ScoValueError as err: + # On retourne une erreur json si jamais il y a eu un problème return json_error(404, err.args[0]) + # On retourne une réponse "removed" si tout s'est bien passé return {"response": "removed"} @@ -682,29 +766,36 @@ def justif_list(justif_id: int = None): Liste les fichiers du justificatif """ + # Récupération du justificatif concerné query: Query = Justificatif.query.filter_by(id=justif_id) if g.scodoc_dept: query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id) - justificatif_unique: Justificatif = query.first_or_404() + # Récupération de l'archive avec l'archiver archive_name: str = justificatif_unique.fichier - filenames: list[str] = [] - archiver: JustificatifArchiver = JustificatifArchiver() if archive_name is not None: filenames = archiver.list_justificatifs( archive_name, justificatif_unique.etudiant ) - + # Préparation du retour + # - total : le nombre total de fichier du justificatif + # - filenames : le nom des fichiers visible par l'utilisateur retour = {"total": len(filenames), "filenames": []} + # Pour chaque nom de fichier on vérifie + # - Si l'utilisateur qui a importé le fichier est le même que + # l'utilisateur qui a demandé la liste des fichiers + # - Ou si l'utilisateur qui a demandé la liste possède la permission AbsJustifView + # Si c'est le cas alors on ajoute à la liste des fichiers visibles for filename in filenames: if int(filename[1]) == current_user.id or current_user.has_permission( Permission.AbsJustifView ): retour["filenames"].append(filename[0]) + # On renvoie le total et la liste des fichiers visibles return retour @@ -720,44 +811,45 @@ def justif_justifies(justif_id: int = None): Liste assiduite_id justifiées par le justificatif """ + # On récupère le justificatif concerné query: Query = Justificatif.query.filter_by(id=justif_id) if g.scodoc_dept: query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id) - justificatif_unique: Justificatif = query.first_or_404() + # On récupère la liste des assiduités justifiées par le justificatif assiduites_list: list[int] = scass.justifies(justificatif_unique) - + # On la renvoie return assiduites_list # -- Utils -- -def _filter_manager(requested, justificatifs_query): +def _filter_manager(requested, justificatifs_query: Query): """ Retourne les justificatifs entrés filtrés en fonction de la request """ # cas 1 : etat justificatif - etat = requested.args.get("etat") + etat: str = requested.args.get("etat") if etat is not None: - justificatifs_query = scass.filter_justificatifs_by_etat( + justificatifs_query: Query = scass.filter_justificatifs_by_etat( justificatifs_query, etat ) # cas 2 : date de début - deb = requested.args.get("date_debut", "").replace(" ", "+") + deb: str = requested.args.get("date_debut", "").replace(" ", "+") deb: datetime = scu.is_iso_formated(deb, True) # cas 3 : date de fin - fin = requested.args.get("date_fin", "").replace(" ", "+") - fin = scu.is_iso_formated(fin, True) + fin: str = requested.args.get("date_fin", "").replace(" ", "+") + fin: datetime = scu.is_iso_formated(fin, True) if (deb, fin) != (None, None): justificatifs_query: Query = scass.filter_by_date( justificatifs_query, Justificatif, deb, fin ) - + # cas 4 : user_id user_id = requested.args.get("user_id", False) if user_id is not False: justificatifs_query: Query = scass.filter_by_user_id( @@ -778,12 +870,13 @@ def _filter_manager(requested, justificatifs_query): except ValueError: formsemestre = None + # cas 6 : order (retourne les justificatifs par ordre décroissant de date_debut) order = requested.args.get("order", None) if order is not None: justificatifs_query: Query = justificatifs_query.order_by( Justificatif.date_debut.desc() ) - + # cas 7 : courant (retourne uniquement les justificatifs de l'année scolaire courante) courant = requested.args.get("courant", None) if courant is not None: annee: int = scu.annee_scolaire() @@ -793,6 +886,7 @@ def _filter_manager(requested, justificatifs_query): Justificatif.date_fin <= scu.date_fin_anne_scolaire(annee), ) + # cas 8 : group_id filtre les justificatifs d'un groupe d'étudiant group_id = requested.args.get("group_id", None) if group_id is not None: try: diff --git a/app/forms/main/config_assiduites.py b/app/forms/main/config_assiduites.py index 7f2cdaa2..ba7198b8 100644 --- a/app/forms/main/config_assiduites.py +++ b/app/forms/main/config_assiduites.py @@ -37,7 +37,9 @@ import datetime class TimeField(StringField): - """HTML5 time input.""" + """HTML5 time input. + tiré de : https://gist.github.com/tachyondecay/6016d32f65a996d0d94f + """ widget = TimeInput() diff --git a/app/models/assiduites.py b/app/models/assiduites.py index 57b7dfb7..a89e89b4 100644 --- a/app/models/assiduites.py +++ b/app/models/assiduites.py @@ -82,6 +82,7 @@ class Assiduite(db.Model): etat = self.etat user: User = None if format_api: + # format api utilise les noms "present,absent,retard" au lieu des int etat = EtatAssiduite.inverse().get(self.etat).name if self.user_id is not None: user = db.session.get(User, self.user_id) @@ -345,15 +346,10 @@ def is_period_conflicting( avec les justificatifs ou assiduites déjà présentes """ + # On s'assure que les dates soient avec TimeZone date_debut = localize_datetime(date_debut) date_fin = localize_datetime(date_fin) - if ( - collection.filter_by(date_debut=date_debut, date_fin=date_fin).first() - is not None - ): - return True - count: int = collection.filter( collection_cls.date_debut < date_fin, collection_cls.date_fin > date_debut ).count() @@ -375,19 +371,26 @@ def compute_assiduites_justified( Returns: list[int]: la liste des assiduités qui ont été justifiées. """ + # Si on ne donne pas de justificatifs on prendra par défaut tous les justificatifs de l'étudiant if justificatifs is None: - justificatifs: Justificatif = Justificatif.query.filter_by(etudid=etudid).all() + justificatifs: list[Justificatif] = Justificatif.query.filter_by( + etudid=etudid + ).all() + # On ne prend que les justificatifs valides justificatifs = [j for j in justificatifs if j.etat == EtatJustificatif.VALIDE] + # On récupère les assiduités de l'étudiant assiduites: Assiduite = Assiduite.query.filter_by(etudid=etudid) assiduites_justifiees: list[int] = [] for assi in assiduites: + # On ne justifie pas les Présences if assi.etat == EtatAssiduite.PRESENT: continue + # On récupère les justificatifs qui justifient l'assiduité `assi` assi_justificatifs = Justificatif.query.filter( Justificatif.etudid == assi.etudid, Justificatif.date_debut <= assi.date_debut, @@ -395,21 +398,39 @@ def compute_assiduites_justified( Justificatif.etat == EtatJustificatif.VALIDE, ).all() + # Si au moins un justificatif possède une période qui couvre l'assiduité if any( assi.date_debut >= j.date_debut and assi.date_fin <= j.date_fin for j in justificatifs + assi_justificatifs ): + # On justifie l'assiduité + # On ajoute l'id de l'assiduité à la liste des assiduités justifiées assi.est_just = True assiduites_justifiees.append(assi.assiduite_id) db.session.add(assi) elif reset: + # Si le paramètre reset est Vrai alors les assiduités non justifiées + # sont remise en "non justifiée" assi.est_just = False db.session.add(assi) + # On valide la session db.session.commit() + # On renvoie la liste des assiduite_id des assiduités justifiées return assiduites_justifiees -def get_assiduites_justif(assiduite_id: int, long: bool): +def get_assiduites_justif(assiduite_id: int, long: bool) -> list[int | dict]: + """ + get_assiduites_justif Récupération des justificatifs d'une assiduité + + Args: + assiduite_id (int): l'identifiant de l'assiduité + long (bool): Retourner des dictionnaires à la place + des identifiants des justificatifs + + Returns: + list[int | dict]: La liste des justificatifs (par défaut uniquement les identifiants, sinon les Dict si long est vrai) + """ assi: Assiduite = Assiduite.query.get_or_404(assiduite_id) return get_justifs_from_date(assi.etudid, assi.date_debut, assi.date_fin, long) @@ -420,20 +441,57 @@ def get_justifs_from_date( date_fin: datetime, long: bool = False, valid: bool = False, -): +) -> list[int | dict]: + """ + get_justifs_from_date Récupération des justificatifs couvrant une période pour un étudiant donné + + Args: + etudid (int): l'identifiant de l'étudiant + date_debut (datetime): la date de début (datetime avec timezone) + date_fin (datetime): la date de fin (datetime avec timezone) + long (bool, optional): Définition de la sortie. + Vrai pour avoir les dictionnaires des justificatifs. + Faux pour avoir uniquement les identifiants + Defaults to False. + valid (bool, optional): Filtre pour n'avoir que les justificatifs valide. + Si vrai : le retour ne contiendra que des justificatifs valides + Sinon le retour contiendra tout type de justificatifs + Defaults to False. + + Returns: + list[int | dict]: La liste des justificatifs (par défaut uniquement les identifiants, sinon les Dict si long est vrai) + """ + # On récupère les justificatifs d'un étudiant couvrant la période donnée justifs: Query = Justificatif.query.filter( Justificatif.etudid == etudid, Justificatif.date_debut <= date_debut, Justificatif.date_fin >= date_fin, ) + # si valide est vrai alors on filtre pour n'avoir que les justificatifs valide if valid: justifs = justifs.filter(Justificatif.etat == EtatJustificatif.VALIDE) + # On renvoie la liste des id des justificatifs si long est Faux, sinon on renvoie les dicts des justificatifs 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: + """ + get_formsemestre_from_data récupère un formsemestre en fonction des données passées + + Args: + data (dict[str, datetime | int]): Une réprésentation simplifiée d'une assiduité ou d'un justificatif + + data = { + "etudid" : int, + "date_debut": datetime (tz), + "date_fin": datetime (tz), + } + + Returns: + FormSemestre: Le formsemestre trouvé ou None + """ return ( FormSemestre.query.join( FormSemestreInscription, diff --git a/app/views/assiduites.py b/app/views/assiduites.py index 2033476a..da56a2e6 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -160,6 +160,8 @@ class HTMLBuilder: @permission_required(Permission.AbsChange) def bilan_dept(): """Gestionnaire assiduités, page principale""" + + # Préparation de la page H = [ html_sco_header.sco_header( page_title="Saisie de l'assiduité", @@ -183,7 +185,10 @@ def bilan_dept(): """

Pour signaler, annuler ou justifier l'assiduité d'un seul étudiant, choisissez d'abord la personne concernée :

""" ) + # Ajout de la barre de recherche d'étudiant (redirection vers bilan etud) H.append(sco_find_etud.form_search_etud(dest_url="assiduites.bilan_etud")) + + # Gestion des billets d'absences if current_user.has_permission( Permission.AbsChange ) and sco_preferences.get_preference("handle_billets_abs"): @@ -195,19 +200,23 @@ def bilan_dept(): """ ) + + # Récupération des années d'étude du département + # (afin de sélectionner une année) dept: Departement = Departement.query.filter_by(id=g.scodoc_dept_id).first() annees: list[int] = sorted( [f.date_debut.year for f in dept.formsemestres], reverse=True, ) - - annee = scu.annee_scolaire() - + annee = scu.annee_scolaire() # Année courante, sera utilisée par défaut + # Génération d'une liste "json" d'années annees_str: str = "[" for ann in annees: annees_str += f"{ann}," annees_str += "]" + # Récupération d'un formsemestre + # (pour n'afficher que les assiduites/justificatifs liés au formsemestre) formsemestre_id = request.args.get("formsemestre_id", "") if formsemestre_id: try: @@ -216,6 +225,7 @@ def bilan_dept(): except AttributeError: formsemestre_id = "" + # Peuplement du template jinja H.append( render_template( "assiduites/pages/bilan_dept.j2", @@ -230,49 +240,6 @@ def bilan_dept(): return "\n".join(H) -# @bp.route("/ListeSemestre") -# @scodoc -# @permission_required(Permission.ScoView) -# def liste_assiduites_formsemestre(): -# """ -# liste_assiduites_etud Affichage de toutes les assiduites et justificatifs d'un etudiant -# Args: -# etudid (int): l'identifiant de l'étudiant - -# Returns: -# str: l'html généré -# """ - -# formsemestre_id = request.args.get("formsemestre_id", -1) -# formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) -# if formsemestre.dept_id != g.scodoc_dept_id: -# abort(404, "FormSemestre inexistant dans ce département") - -# header: str = html_sco_header.sco_header( -# page_title="Liste des assiduités du semestre", -# init_qtip=True, -# javascripts=[ -# "js/assiduites.js", -# "libjs/moment-2.29.4.min.js", -# "libjs/moment-timezone.js", -# ], -# cssstyles=CSSSTYLES -# + [ -# "css/assiduites.css", -# ], -# ) - -# return HTMLBuilder( -# header, -# render_template( -# "assiduites/pages/liste_semestre.j2", -# sco=ScoData(formsemestre=formsemestre), -# sem=formsemestre.titre_annee(), -# formsemestre_id=formsemestre.id, -# ), -# ).build() - - @bp.route("/SignaleAssiduiteEtud") @scodoc @permission_required(Permission.AbsChange) @@ -287,22 +254,22 @@ def signal_assiduites_etud(): str: l'html généré """ + # Récupération de l'étudiant concerné etudid = request.args.get("etudid", -1) etud: Identite = Identite.query.get_or_404(etudid) 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()) - # gestion évaluations + # gestion évaluations (Appel à la page depuis les évaluations) saisie_eval: bool = request.args.get("saisie_eval") is not None - date_deb: str = request.args.get("date_deb") date_fin: str = request.args.get("date_fin") moduleimpl_id: int = request.args.get("moduleimpl_id", "") evaluation_id: int = request.args.get("evaluation_id") - redirect_url: str = ( "#" if not saisie_eval @@ -313,6 +280,7 @@ def signal_assiduites_etud(): ) ) + # Préparation de la page (Header) header: str = html_sco_header.sco_header( page_title="Saisie assiduité", init_qtip=True, @@ -335,11 +303,14 @@ def signal_assiduites_etud(): "assi_afternoon_time", "18:00:00" ) + # Gestion du selecteur de moduleimpl (pour le tableau différé) select = f""" """ + + # Génération de la page return HTMLBuilder( header, _mini_timeline(), @@ -381,13 +352,16 @@ def liste_assiduites_etud(): str: l'html généré """ + # Récupération de l'étudiant concerné etudid = request.args.get("etudid", -1) etud: Identite = Identite.query.get_or_404(etudid) 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) assiduite_id: int = request.args.get("assiduite_id", -1) + # Préparation de la page header: str = html_sco_header.sco_header( page_title=f"Assiduité de {etud.nomprenom}", init_qtip=True, @@ -401,7 +375,7 @@ def liste_assiduites_etud(): "css/assiduites.css", ], ) - + # Peuplement du template jinja return HTMLBuilder( header, render_template( @@ -429,12 +403,13 @@ def bilan_etud(): Returns: str: l'html généré """ - + # Récupération de l'étudiant etudid = request.args.get("etudid", -1) etud: Identite = Identite.query.get_or_404(etudid) if etud.dept_id != g.scodoc_dept_id: abort(404, "étudiant inexistant dans ce département") + # Préparation de la page (header) header: str = html_sco_header.sco_header( page_title=f"Bilan de l'assiduité de {etud.nomprenom}", init_qtip=True, @@ -449,13 +424,16 @@ def bilan_etud(): ], ) + # Gestion des dates du bilan (par défaut l'année scolaire) date_debut: str = f"{scu.annee_scolaire()}-09-01" date_fin: str = f"{scu.annee_scolaire()+1}-06-30" + # Récupération de la métrique d'assiduité assi_metric = scu.translate_assiduites_metric( sco_preferences.get_preference("assi_metrique", dept_id=g.scodoc_dept_id), ) + # Génération de la page return HTMLBuilder( header, render_template( @@ -486,11 +464,13 @@ def ajout_justificatif_etud(): str: l'html généré """ + # Récupération de l'étudiant concerné etudid = request.args.get("etudid", -1) etud: Identite = Identite.query.get_or_404(etudid) if etud.dept_id != g.scodoc_dept_id: abort(404, "étudiant inexistant dans ce département") + # Préparation de la page (header) header: str = html_sco_header.sco_header( page_title="Justificatifs", init_qtip=True, @@ -505,6 +485,7 @@ def ajout_justificatif_etud(): ], ) + # Peuplement du template jinja return HTMLBuilder( header, render_template( @@ -533,11 +514,13 @@ def calendrier_etud(): str: l'html généré """ + # Récupération de l'étudiant etudid = request.args.get("etudid", -1) etud: Identite = Identite.query.get_or_404(etudid) if etud.dept_id != g.scodoc_dept_id: abort(404, "étudiant inexistant dans ce département") + # Préparation de la page header: str = html_sco_header.sco_header( page_title="Calendrier de l'assiduité", init_qtip=True, @@ -552,16 +535,20 @@ def calendrier_etud(): ], ) + # Récupération des années d'étude de l'étudiant annees: list[int] = sorted( [ins.formsemestre.date_debut.year for ins in etud.formsemestre_inscriptions], reverse=True, ) + # Transformation en une liste "json" + # (sera utilisé pour générer le selecteur d'année) annees_str: str = "[" for ann in annees: annees_str += f"{ann}," annees_str += "]" + # Peuplement du template jinja return HTMLBuilder( header, render_template( @@ -585,11 +572,11 @@ def signal_assiduites_group(): Returns: str: l'html généré """ + # Récupération des paramètres de l'url formsemestre_id: int = request.args.get("formsemestre_id", -1) moduleimpl_id: int = request.args.get("moduleimpl_id") date: str = request.args.get("jour", datetime.date.today().isoformat()) group_ids: list[int] = request.args.get("group_ids", None) - if group_ids is None: group_ids = [] else: @@ -601,12 +588,14 @@ def signal_assiduites_group(): moduleimpl_id = int(moduleimpl_id) except (TypeError, ValueError): moduleimpl_id = None + # Vérification du formsemestre_id try: formsemestre_id = int(formsemestre_id) except (TypeError, ValueError): formsemestre_id = None + # Gestion des groupes groups_infos = sco_groups_view.DisplayedGroupsInfos( group_ids, moduleimpl_id=moduleimpl_id, formsemestre_id=formsemestre_id ) @@ -617,10 +606,6 @@ def signal_assiduites_group(): + html_sco_header.sco_footer() ) - # --- URL DEFAULT --- - - base_url: str = f"SignalAssiduiteGr?date={date}&{groups_infos.groups_query_args}" - # --- Filtrage par formsemestre --- formsemestre_id = groups_infos.formsemestre_id @@ -628,17 +613,22 @@ def signal_assiduites_group(): if formsemestre.dept_id != g.scodoc_dept_id: abort(404, "groupes inexistants dans ce département") + # Vérification du forçage du module require_module = sco_preferences.get_preference("forcer_module", formsemestre_id) + + # Récupération des étudiants des groupes etuds = [ sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0] for m in groups_infos.members ] # --- Vérification de la date --- - real_date = scu.is_iso_formated(date, True).date() if real_date < formsemestre.date_debut or real_date > formsemestre.date_fin: + # Si le jour est hors semestre, indiquer une erreur + + # Formatage des dates pour le message d'erreur real_str = real_date.strftime("%d/%m/%Y") form_deb = formsemestre.date_debut.strftime("%d/%m/%Y") form_fin = formsemestre.date_fin.strftime("%d/%m/%Y") @@ -662,8 +652,7 @@ def signal_assiduites_group(): # Si aucun etudiant n'est inscrit au module choisi... moduleimpl_id = None - # --- Génération de l'HTML --- - sem = formsemestre.to_dict() + # Récupération du nom des/du groupe(s) if groups_infos.tous_les_etuds_du_sem: gr_tit = "en" @@ -676,12 +665,15 @@ def signal_assiduites_group(): grp + ' ' + groups_infos.groups_titles + "" ) + # --- Génération de l'HTML --- + header: str = html_sco_header.sco_header( page_title="Saisie journalière des assiduités", init_qtip=True, javascripts=html_sco_header.BOOTSTRAP_MULTISELECT_JS + [ # Voir fonctionnement JS + # XXX Retirer moment "js/etud_info.js", "js/groups_view.js", "js/assiduites.js", @@ -694,6 +686,10 @@ def signal_assiduites_group(): ], ) + # Récupération du semestre en dictionnaire + sem = formsemestre.to_dict() + + # Peuplement du template jinja return HTMLBuilder( header, _mini_timeline(), @@ -731,11 +727,12 @@ def visu_assiduites_group(): Returns: str: l'html généré """ + + # Récupération des paramètres de la requête formsemestre_id: int = request.args.get("formsemestre_id", -1) moduleimpl_id: int = request.args.get("moduleimpl_id") date: str = request.args.get("jour", datetime.date.today().isoformat()) group_ids: list[int] = request.args.get("group_ids", None) - if group_ids is None: group_ids = [] else: @@ -755,6 +752,7 @@ def visu_assiduites_group(): except (TypeError, ValueError) as exc: raise ScoValueError("identifiant de formsemestre invalide") from exc + # Récupérations des/du groupe(s) groups_infos = sco_groups_view.DisplayedGroupsInfos( group_ids, moduleimpl_id=moduleimpl_id, formsemestre_id=formsemestre_id ) @@ -765,10 +763,6 @@ def visu_assiduites_group(): + html_sco_header.sco_footer() ) - # --- URL DEFAULT --- - - base_url: str = f"SignalAssiduiteGr?date={date}&{groups_infos.groups_query_args}" - # --- Filtrage par formsemestre --- formsemestre_id = groups_infos.formsemestre_id @@ -776,10 +770,10 @@ def visu_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 - ) + # Vérfication du forçage du module + require_module = sco_preferences.get_preference("forcer_module", formsemestre_id) + # Récupération des étudiants du/des groupe(s) etuds = [ sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0] for m in groups_infos.members @@ -810,7 +804,6 @@ def visu_assiduites_group(): moduleimpl_id = None # --- Génération de l'HTML --- - sem = formsemestre.to_dict() if groups_infos.tous_les_etuds_du_sem: gr_tit = "en" @@ -841,6 +834,9 @@ def visu_assiduites_group(): ], ) + # Récupération du semestre en dictionnaire + sem = formsemestre.to_dict() + return HTMLBuilder( header, _mini_timeline(), @@ -873,10 +869,14 @@ def visu_assiduites_group(): @permission_required(Permission.ScoView) def etat_abs_date(): """date_debut, date_fin en ISO""" + + # Récupération des paramètre de la requête date_debut_str = request.args.get("date_debut") date_fin_str = request.args.get("date_fin") title = request.args.get("desc") group_ids: list[int] = request.args.get("group_ids", None) + + # Vérification des dates try: date_debut = datetime.datetime.fromisoformat(date_debut_str) except ValueError as exc: @@ -885,6 +885,8 @@ def etat_abs_date(): date_fin = datetime.datetime.fromisoformat(date_fin_str) except ValueError as exc: raise ScoValueError("date_fin invalide") from exc + + # Vérification des groupes if group_ids is None: group_ids = [] else: @@ -893,25 +895,30 @@ def etat_abs_date(): groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids) + # Récupération des étudiants des groupes etuds = [ sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0] for m in groups_infos.members ] + # Récupération des assiduites des étudiants assiduites: Assiduite = Assiduite.query.filter( Assiduite.etudid.in_([e["etudid"] for e in etuds]) ) + # Filtrage des assiduités en fonction des dates données assiduites = scass.filter_by_date( assiduites, Assiduite, date_debut, date_fin, False ) + # Génération d'objet étudiant simplifié (nom+lien cal, etat_assiduite) etudiants: list[dict] = [] for etud in etuds: + # On récupère l'état de la première assiduité sur la période assi = assiduites.filter_by(etudid=etud["etudid"]).first() - etat = "" if assi is not None and assi.etat != 0: etat = scu.EtatAssiduite.inverse().get(assi.etat).name + # On génère l'objet simplifié etudiant = { "nom": f"""' + groups_infos.groups_titles + "" ) + # Génération de la page return render_template( "assiduites/pages/visu_assi.j2", assi_metric=scu.translate_assiduites_metric( @@ -1013,26 +1030,27 @@ def visu_assi_group(): @scodoc @permission_required(Permission.AbsChange) def signal_assiduites_diff(): + # Récupération des paramètres de la requête group_ids: list[int] = request.args.get("group_ids", None) formsemestre_id: int = request.args.get("formsemestre_id", -1) date: str = request.args.get("jour", datetime.date.today().isoformat()) - date_deb: str = request.args.get("date_deb") date_fin: str = request.args.get("date_fin") - semaine: str = request.args.get("semaine") + # Dans le cas où on donne une semaine plutot qu'un jour if semaine is not None: + # On génère la semaine iso à partir de l'anne scolaire. semaine = ( f"{scu.annee_scolaire()}-W{semaine}" if "W" not in semaine else semaine ) + # On met à jour les dates avec le date de debut et fin de semaine date_deb: datetime.date = datetime.datetime.strptime( semaine + "-1", "%Y-W%W-%w" ) date_fin: datetime.date = date_deb + datetime.timedelta(days=6) etudiants: list[dict] = [] - titre = None # Vérification du formsemestre_id @@ -1052,14 +1070,13 @@ def signal_assiduites_diff(): elif real_date > formsemestre.date_fin: date = formsemestre.date_fin.isoformat() + # Vérification des groupes if group_ids is None: group_ids = [] else: group_ids = group_ids.split(",") map(str, group_ids) - groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids) - if not groups_infos.members: return ( html_sco_header.sco_header(page_title="Assiduité: saisie différée") @@ -1067,6 +1084,7 @@ def signal_assiduites_diff(): + html_sco_header.sco_footer() ) + # Récupération des étudiants etudiants.extend( [ sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0] @@ -1076,6 +1094,8 @@ def signal_assiduites_diff(): etudiants = list(sorted(etudiants, key=lambda x: x["nom"])) + # Génération de l'HTML + header: str = html_sco_header.sco_header( page_title="Assiduité: saisie différée", init_qtip=True, @@ -1142,13 +1162,17 @@ def signal_evaluation_abs(etudid: int = None, evaluation_id: int = None): Alors l'absence sera sur la période de l'évaluation Sinon L'utilisateur sera redirigé vers la page de saisie des absences de l'étudiant """ + + # Récupération de l'étudiant concerné etud: Identite = Identite.query.get_or_404(etudid) if etud.dept_id != g.scodoc_dept_id: abort(404, "étudiant inexistant dans ce département") + # Récupération de l'évaluation concernée evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id) delta: datetime.timedelta = evaluation.date_fin - evaluation.date_debut + # Si l'évaluation dure plus qu'un jour alors on redirige vers la page de saisie etudiant if delta > datetime.timedelta(days=1): # rediriger vers page saisie return redirect( @@ -1164,7 +1188,7 @@ def signal_evaluation_abs(etudid: int = None, evaluation_id: int = None): ) ) - # créer l'assiduité + # Sinon on créé l'assiduité try: assiduite_unique: Assiduite = Assiduite.create_assiduite( @@ -1175,8 +1199,8 @@ def signal_evaluation_abs(etudid: int = None, evaluation_id: int = None): moduleimpl=evaluation.moduleimpl, ) except ScoValueError as see: + # En cas d'erreur msg: str = see.args[0] - if "Duplication" in msg: msg = "Une autre assiduité concerne déjà cette période. En cliquant sur continuer vous serez redirigé vers la page de saisie des assiduités de l'étudiant." dest: str = url_for( @@ -1195,6 +1219,7 @@ def signal_evaluation_abs(etudid: int = None, evaluation_id: int = None): db.session.add(assiduite_unique) db.session.commit() + # on flash pour indiquer que l'absence a bien été créée puis on revient sur la page de l'évaluation flash("L'absence a bien été créée") # rediriger vers la page d'évaluation return redirect( @@ -1208,16 +1233,20 @@ def signal_evaluation_abs(etudid: int = None, evaluation_id: int = None): def generate_bul_list(etud: Identite, semestre: FormSemestre) -> str: """Génère la liste des assiduités d'un étudiant pour le bulletin mail""" + + # On récupère la métrique d'assiduité metrique: str = scu.translate_assiduites_metric( sco_preferences.get_preference("assi_metrique", formsemestre_id=semestre.id), ) + # On récupère le nombre maximum de ligne d'assiduité max_nb: int = int( sco_preferences.get_preference( "bul_mail_list_abs_nb", formsemestre_id=semestre.id ) ) + # On récupère les assiduités et les justificatifs de l'étudiant assiduites = scass.filter_by_formsemestre( etud.assiduites, Assiduite, semestre ).order_by(Assiduite.entry_date.desc()) @@ -1225,10 +1254,17 @@ def generate_bul_list(etud: Identite, semestre: FormSemestre) -> str: etud.justificatifs, Justificatif, semestre ).order_by(Justificatif.entry_date.desc()) + # On calcule les statistiques stats: dict = scass.get_assiduites_stats( assiduites, metric=metrique, filtered={"split": True} ) + # 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 @@ -1275,13 +1311,36 @@ def generate_bul_list(etud: Identite, semestre: FormSemestre) -> str: def _get_date_str(deb: datetime.datetime, fin: datetime.datetime) -> str: + """ + _get_date_str transforme une période en chaîne lisible + + Args: + deb (datetime.datetime): date de début + fin (datetime.datetime): date de fin + + Returns: + str: + "le dd/mm/yyyy de hh:MM à hh:MM" si les deux date sont sur le même jour + "du dd/mm/yyyy hh:MM audd/mm/yyyy hh:MM" sinon + """ if deb.date() == fin.date(): temps = deb.strftime("%d/%m/%Y %H:%M").split(" ") + [fin.strftime("%H:%M")] return f"le {temps[0]} de {temps[1]} à {temps[2]}" return f'du {deb.strftime("%d/%m/%Y %H:%M")} au {fin.strftime("%d/%m/%Y %H:%M")}' -def _get_days_between_dates(deb: str, fin: str): +def _get_days_between_dates(deb: str, fin: str) -> str: + """ + _get_days_between_dates récupère tous les jours entre deux dates + + Args: + deb (str): date de début + fin (str): date de fin + + Returns: + str: une chaine json représentant une liste des jours + ['date_iso','date_iso2', ...] + """ if deb is None or fin is None: return "null" try: @@ -1301,8 +1360,25 @@ def _get_days_between_dates(deb: str, fin: str): def _differee( - etudiants, moduleimpl_select, date=None, periode=None, formsemestre_id=None -): + etudiants: list[dict], + moduleimpl_select: str, + date: str = None, + periode: dict[str, str] = None, + formsemestre_id: int = None, +) -> str: + """ + _differee Génère un tableau de saisie différé + + Args: + etudiants (list[dict]): la liste des étudiants (représentés par des dictionnaires) + moduleimpl_select (str): l'html représentant le selecteur de module + date (str, optional): la première date à afficher. Defaults to None. + periode (dict[str, str], optional):La période par défaut de la première colonne. Defaults to None. + formsemestre_id (int, optional): l'id du semestre pour le selecteur de module. Defaults to None. + + Returns: + str: le widget (html/css/js) + """ if date is None: date = datetime.date.today().isoformat() @@ -1329,9 +1405,7 @@ def _differee( ) -def _module_selector( - formsemestre: FormSemestre, moduleimpl_id: int = None -) -> HTMLElement: +def _module_selector(formsemestre: FormSemestre, moduleimpl_id: int = None) -> str: """ _module_selector Génère un HTMLSelectElement à partir des moduleimpl du formsemestre @@ -1341,18 +1415,26 @@ def _module_selector( Returns: str: La représentation str d'un HTMLSelectElement """ - + # récupération des ues du semestre ntc: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) + ues = ntc.get_ues_stat_dict() modimpls_list: list[dict] = [] - ues = ntc.get_ues_stat_dict() for ue in ues: + # Ajout des moduleimpl de chaque ue dans la liste des moduleimpls modimpls_list += ntc.get_modimpls_dict(ue_id=ue["ue_id"]) + # 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" - modules = [] + # 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: modname: str = ( (modimpl["module"]["code"] or "") @@ -1361,11 +1443,6 @@ def _module_selector( ) modules.append({"moduleimpl_id": modimpl["moduleimpl_id"], "name": modname}) - try: - moduleimpl_id = int(moduleimpl_id) - except (ValueError, TypeError): - moduleimpl_id = None - return render_template( "assiduites/widgets/moduleimpl_selector.j2", selected=selected, @@ -1374,13 +1451,30 @@ def _module_selector( ) -def _dynamic_module_selector(): +def _dynamic_module_selector() -> str: + """ + _dynamic_module_selector retourne l'html/css/javascript du selecteur de module dynamique + + Returns: + str: l'html/css/javascript du selecteur de module dynamique + """ return render_template( "assiduites/widgets/moduleimpl_dynamic_selector.j2", ) -def _timeline(formsemestre_id=None) -> HTMLElement: +def _timeline(formsemestre_id: int = None) -> str: + """ + _timeline retourne l'html de la timeline + + Args: + formsemestre_id (int, optional): un formsemestre. Defaults to None. + Le formsemestre sert à obtenir la période par défaut de la timeline + sinon ce sera de 2 heure dès le début de la timeline + + Returns: + str: l'html en chaîne de caractères + """ return render_template( "assiduites/widgets/timeline.j2", t_start=ScoDocSiteConfig.assi_get_rounded_time("assi_morning_time", "08:00:00"), @@ -1392,7 +1486,13 @@ def _timeline(formsemestre_id=None) -> HTMLElement: ) -def _mini_timeline() -> HTMLElement: +def _mini_timeline() -> str: + """ + _mini_timeline Retourne l'html lié au mini timeline d'assiduités + + Returns: + str: l'html en chaîne de caractères + """ return render_template( "assiduites/widgets/minitimeline.j2", t_start=ScoDocSiteConfig.assi_get_rounded_time("assi_morning_time", "08:00:00"), diff --git a/tools/downgrade_assiduites.py b/tools/downgrade_assiduites.py index dd87981a..2a6ddd08 100644 --- a/tools/downgrade_assiduites.py +++ b/tools/downgrade_assiduites.py @@ -26,6 +26,7 @@ def downgrade_module( dept_etudid: list[int] = None dept_id: int = None + # Récupération du département si spécifié if dept is not None: departement: Departement = Departement.query.filter_by(acronym=dept).first() @@ -34,13 +35,16 @@ def downgrade_module( dept_etudid = [etud.id for etud in departement.etudiants] dept_id = departement.id + # Suppression des assiduités if assiduites: _remove_assiduites(dept_etudid) - + # Suppression des justificatifs if justificatifs: _remove_justificatifs(dept_etudid) _remove_justificatifs_archive(dept_id) + # Si on supprime tout le module assiduité/justificatif alors on remet à zero + # les séquences postgres if dept is None: if assiduites: db.session.execute( @@ -51,26 +55,52 @@ def downgrade_module( sa.text("ALTER SEQUENCE justificatifs_id_seq RESTART WITH 1") ) + # On valide l'opération sur la bdd db.session.commit() + # On affiche un message pour l'utilisateur print( f"{TerminalColor.GREEN}Le module assiduité a bien été remis à zero.{TerminalColor.RESET}" ) def _remove_assiduites(dept_etudid: str = None): + """ + _remove_assiduites Supprime les assiduités + + Args: + dept_etudid (str, optional): la liste des etudid d'un département. Defaults to None. + """ if dept_etudid is None: + # Si pas d'étudids alors on supprime toutes les assiduités Assiduite.query.delete() else: + # Sinon on supprime que les assiduités des étudiants donnés Assiduite.query.filter(Assiduite.etudid.in_(dept_etudid)).delete() def _remove_justificatifs(dept_etudid: str = None): + """ + _remove_justificatifs Supprime les justificatifs + + Args: + dept_etudid (str, optional): la liste des etudid d'un département. Defaults to None. + """ if dept_etudid is None: + # Si pas d'étudids alors on supprime tous les justificatifs Justificatif.query.delete() else: + # Sinon on supprime que les justificatifs des étudiants donnés Justificatif.query.filter(Justificatif.etudid.in_(dept_etudid)).delete() def _remove_justificatifs_archive(dept_id: int = None): + """ + _remove_justificatifs_archive On supprime les archives des fichiers justificatifs + + Args: + dept_id (int, optional): l'id du département à supprimer . Defaults to None. + Si none : supprime tous les département + Sinon uniquement le département sélectionné + """ JustificatifArchiver().remove_dept_archive(dept_id) From 4aebece03e50a1b5eec7ddbf38bc4a2fa0a7a44e Mon Sep 17 00:00:00 2001 From: Iziram Date: Fri, 27 Oct 2023 16:51:06 +0200 Subject: [PATCH 8/9] Assiduite : bug fix liste justifier --- app/static/js/assiduites.js | 62 +++++++++---------- .../assiduites/widgets/tableau_base.j2 | 4 +- 2 files changed, 31 insertions(+), 35 deletions(-) diff --git a/app/static/js/assiduites.js b/app/static/js/assiduites.js index 21b5c1d0..90ed21d6 100644 --- a/app/static/js/assiduites.js +++ b/app/static/js/assiduites.js @@ -1650,36 +1650,33 @@ function fastJustify(assiduite) { fin: new moment.tz(assiduite.date_fin, TIMEZONE), }; const action = (justifs) => { - if (justifs.length > 0) { - justifyAssiduite(assiduite.assiduite_id, !assiduite.est_just); - } else { - //créer un nouveau justificatif - // Afficher prompt -> demander raison et état + //créer un nouveau justificatif + // Afficher prompt -> demander raison et état - const success = () => { - const raison = document.getElementById("promptText").value; - const etat = document.getElementById("promptSelect").value; + const success = () => { + const raison = document.getElementById("promptText").value; + const etat = document.getElementById("promptSelect").value; - //créer justificatif + //créer justificatif - const justif = { - date_debut: new moment.tz(assiduite.date_debut, TIMEZONE).format(), - date_fin: new moment.tz(assiduite.date_fin, TIMEZONE).format(), - raison: raison, - etat: etat, - }; - - createJustificatif(justif); - - generateAllEtudRow(); - try { - loadAll(); - } catch {} + const justif = { + date_debut: new moment.tz(assiduite.date_debut, TIMEZONE).format(), + date_fin: new moment.tz(assiduite.date_fin, TIMEZONE).format(), + raison: raison, + etat: etat, }; - const content = document.createElement("fieldset"); + createJustificatif(justif); - const htmlPrompt = `Entrez l'état du justificatif : + generateAllEtudRow(); + try { + loadAll(); + } catch {} + }; + + const content = document.createElement("fieldset"); + + const htmlPrompt = `Entrez l'état du justificatif : `; - content.innerHTML = htmlPrompt; + content.innerHTML = htmlPrompt; - openPromptModal( - "Nouveau justificatif (Rapide)", - content, - success, - () => {}, - "#7059FF" - ); - } + openPromptModal( + "Nouveau justificatif (Rapide)", + content, + success, + () => {}, + "#7059FF" + ); }; if (assiduite.etudid) { getJustificatifFromPeriod(period, assiduite.etudid, action); diff --git a/app/templates/assiduites/widgets/tableau_base.j2 b/app/templates/assiduites/widgets/tableau_base.j2 index 99d724d3..272335d0 100644 --- a/app/templates/assiduites/widgets/tableau_base.j2 +++ b/app/templates/assiduites/widgets/tableau_base.j2 @@ -345,8 +345,8 @@ let assi = Object.values(assiduites).flat().filter((a) => { return a.assiduite_id == obj_id })[0] li.addEventListener('click', () => { - if (assiduite && !assiduite[0].est_just && assiduite[0].etat != "PRESENT") { - fastJustify(assiduite[0]) + if (assi && !assi.est_just && assi.etat != "PRESENT") { + fastJustify(assi) } else { openAlertModal("Erreur", document.createTextNode("L'assiduité est déjà justifiée.")) } From 5824b7fb59200960f9f6f29252425cbcaf693de6 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 27 Oct 2023 17:30:53 +0200 Subject: [PATCH 9/9] version --- sco_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sco_version.py b/sco_version.py index 4d7ab146..3fa1a685 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.49" +SCOVERSION = "9.6.50" SCONAME = "ScoDoc"