From 80dd4b13440460adc60299251c3daf19062e4b3f Mon Sep 17 00:00:00 2001 From: "pascal.bouron" Date: Sun, 3 Sep 2023 21:07:13 +0200 Subject: [PATCH 01/97] Actualiser app/views/assiduites.py typo --- app/views/assiduites.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/assiduites.py b/app/views/assiduites.py index ec81393988..956ed5ebf6 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -151,7 +151,7 @@ def index_html(): ), """

Traitement des assiduités

- Pour saisir des assiduités ou consulter les états, il est recommandé par passer par + Pour saisir des assiduités ou consulter les états, il est recommandé de passer par le semestre concerné (saisie par jour ou saisie différée).

""", From 36f7d0396a83e772c1268aff61d01f9623cde1d6 Mon Sep 17 00:00:00 2001 From: iziram Date: Tue, 5 Sep 2023 08:41:40 +0200 Subject: [PATCH 02/97] =?UTF-8?q?Assiduit=C3=A9s:=20test=5Fsco=5Fbasic=20#?= =?UTF-8?q?692?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/etudiants.py | 6 +++-- tests/unit/test_sco_basic.py | 51 +++++++++++++++++++++++++----------- 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/app/models/etudiants.py b/app/models/etudiants.py index e00f846507..a451526f7d 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -74,9 +74,11 @@ class Identite(db.Model): ) # Relations avec les assiduites et les justificatifs - assiduites = db.relationship("Assiduite", back_populates="etudiant", lazy="dynamic") + assiduites = db.relationship( + "Assiduite", back_populates="etudiant", lazy="dynamic", cascade="all, delete" + ) justificatifs = db.relationship( - "Justificatif", back_populates="etudiant", lazy="dynamic" + "Justificatif", back_populates="etudiant", lazy="dynamic", cascade="all, delete" ) def __repr__(self): diff --git a/tests/unit/test_sco_basic.py b/tests/unit/test_sco_basic.py index 030207e5ec..be078c9211 100644 --- a/tests/unit/test_sco_basic.py +++ b/tests/unit/test_sco_basic.py @@ -23,15 +23,18 @@ import app from app import db from app.comp import res_sem from app.comp.res_compat import NotesTableCompat -from app.models import FormSemestre +from app.models import FormSemestre, Assiduite, Justificatif from app.scodoc import sco_formsemestre from app.scodoc import sco_bulletins from app.scodoc import codes_cursus +from app.scodoc import sco_assiduites as scass from app.scodoc import sco_evaluations from app.scodoc import sco_evaluation_db from app.scodoc import sco_formsemestre_validation from app.scodoc import sco_cursus_dut from app.scodoc import sco_saisie_notes +from app.scodoc.sco_utils import EtatAssiduite, EtatJustificatif, localize_datetime +from app.models.assiduites import compute_assiduites_justified DEPT = TestConfig.DEPT_TEST @@ -186,21 +189,10 @@ def run_sco_basic(verbose=False) -> FormSemestre: # ----------------------- etudid = etuds[0]["etudid"] - # XXX TODO-ASSIDUITE - # _ = sco_abs_views.doSignaleAbsence( - # "15/01/2020", "18/01/2020", demijournee=2, etudid=etudid - # ) - - # _ = sco_abs_views.doJustifAbsence( - # "17/01/2020", - # "18/01/2020", - # demijournee=2, - # etudid=etudid, - # ) - - # nbabs, nbabsjust = sco_abs.get_abs_count(etudid, sem) - # assert nbabs == 6, f"incorrect nbabs ({nbabs})" - # assert nbabsjust == 2, f"incorrect nbabsjust ({nbabsjust})" + _signal_absences_justificatifs(etudid) + nbabs, nbabsjust = scass.get_assiduites_count(etudid, sem) + assert nbabs == 6, f"incorrect nbabs ({nbabs})" + assert nbabsjust == 2, f"incorrect nbabsjust ({nbabsjust})" # --- Permission saisie notes et décisions de jury, avec ou sans démission ou défaillance # on n'a pas encore saisi de décisions @@ -251,3 +243,30 @@ def run_sco_basic(verbose=False) -> FormSemestre: ) assert q.count() == 0 return formsemestre + + +def _signal_absences_justificatifs(etudid: int): + etud: Identite = Identite.query.get(etudid) + db.session.commit() + for i in range(15, 18): + db.session.add( + Assiduite.create_assiduite( + etud=etud, + date_debut=localize_datetime(datetime.datetime(2020, 1, i, 8, 0)), + date_fin=localize_datetime(datetime.datetime(2020, 1, i, 18, 0)), + etat=EtatAssiduite.ABSENT, + ) + ) + db.session.commit() + justif: Justificatif = Justificatif.create_justificatif( + etud=etud, + date_debut=localize_datetime(datetime.datetime(2020, 1, 17, 8, 0)), + date_fin=localize_datetime(datetime.datetime(2020, 1, 17, 18, 0)), + etat=EtatJustificatif.VALIDE, + ) + db.session.add(justif) + compute_assiduites_justified( + etud.etudid, + [justif], + ) + db.session.commit() From d3e3b48d17ff0daa22248bee23b126e9b656e33e Mon Sep 17 00:00:00 2001 From: iziram Date: Tue, 5 Sep 2023 08:59:21 +0200 Subject: [PATCH 03/97] Assiduites : correction bug saisie bornes semestre --- app/static/js/assiduites.js | 16 +++++----------- app/templates/assiduites/widgets/differee.j2 | 2 +- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/app/static/js/assiduites.js b/app/static/js/assiduites.js index 2ee935efcb..a28c0eca85 100644 --- a/app/static/js/assiduites.js +++ b/app/static/js/assiduites.js @@ -811,11 +811,8 @@ function numberTimeToDate(nb) { * @param {boolean} clear vidage de l'objet "assiduites" ou non * @returns {object} l'objets Assiduités { : [,]} */ -function getAssiduitesFromEtuds(clear, has_formsemestre = true, deb, fin) { +function getAssiduitesFromEtuds(clear, deb, fin) { const etudIds = Object.keys(etuds).join(","); - const formsemestre_id = has_formsemestre - ? `formsemestre_id=${getFormSemestreId()}&` - : ""; const date_debut = deb ? deb : toIsoString(getPrevDate()); const date_fin = fin ? fin : toIsoString(getNextDate()); @@ -826,7 +823,7 @@ function getAssiduitesFromEtuds(clear, has_formsemestre = true, deb, fin) { const url_api = getUrl() + - `/api/assiduites/group/query?date_debut=${date_debut}&${formsemestre_id}&date_fin=${date_fin}&etudids=${etudIds}`; + `/api/assiduites/group/query?date_debut=${date_debut}&date_fin=${date_fin}&etudids=${etudIds}`; sync_get(url_api, (data, status) => { if (status === "success") { const dataKeys = Object.keys(data); @@ -1057,16 +1054,13 @@ function getAssiduiteValue(field) { * Mise à jour des assiduités d'un étudiant * @param {String | Number} etudid identifiant de l'étudiant */ -function actualizeEtudAssiduite(etudid, has_formsemestre = true) { - const formsemestre_id = has_formsemestre - ? `formsemestre_id=${getFormSemestreId()}&` - : ""; +function actualizeEtudAssiduite(etudid) { const date_debut = toIsoString(getPrevDate()); const date_fin = toIsoString(getNextDate()); const url_api = getUrl() + - `/api/assiduites/${etudid}/query?${formsemestre_id}date_debut=${date_debut}&date_fin=${date_fin}`; + `/api/assiduites/${etudid}/query?date_debut=${date_debut}&date_fin=${date_fin}`; sync_get(url_api, (data, status) => { if (status === "success") { assiduites[etudid] = data; @@ -1331,7 +1325,7 @@ function insertEtudRow(etud, index, output = false) { * @param {String | Number} etudid l'identifiant de l'étudiant */ function actualizeEtud(etudid) { - actualizeEtudAssiduite(etudid, !isSingleEtud()); + actualizeEtudAssiduite(etudid); //Actualize row const etudHolder = document.querySelector(".etud_holder"); const ancient_row = document.getElementById(`etud_row_${etudid}`); diff --git a/app/templates/assiduites/widgets/differee.j2 b/app/templates/assiduites/widgets/differee.j2 index aa42a3e5ac..8d96c5bc41 100644 --- a/app/templates/assiduites/widgets/differee.j2 +++ b/app/templates/assiduites/widgets/differee.j2 @@ -533,7 +533,7 @@ } if (get) { - getAssiduitesFromEtuds(false, false, d_debut.format(), d_fin.format()) + getAssiduitesFromEtuds(false, d_debut.format(), d_fin.format()) return 0x0; } From 4387727d2f6c5c821bba1ec3141ed1f997c9496b Mon Sep 17 00:00:00 2001 From: iziram Date: Tue, 5 Sep 2023 09:25:51 +0200 Subject: [PATCH 04/97] =?UTF-8?q?Assiduit=C3=A9=20:=20Erreur=20module=20et?= =?UTF-8?q?=20external=5Fdata=20#711?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/assiduites.py | 19 ++++++-- app/static/js/assiduites.js | 47 +++++++++---------- .../assiduites/widgets/tableau_assi.j2 | 7 ++- 3 files changed, 42 insertions(+), 31 deletions(-) diff --git a/app/api/assiduites.py b/app/api/assiduites.py index ec47f518a2..bbd6142f91 100644 --- a/app/api/assiduites.py +++ b/app/api/assiduites.py @@ -601,9 +601,12 @@ def _create_singular( moduleimpl_id = data.get("moduleimpl_id", False) moduleimpl: ModuleImpl = None - if moduleimpl_id not in [False, None]: + if moduleimpl_id not in [False, None, "", "-1"]: if moduleimpl_id != "autre": - moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first() + try: + moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first() + except ValueError: + moduleimpl = None if moduleimpl is None: errors.append("param 'moduleimpl_id': invalide") else: @@ -810,7 +813,7 @@ def _edit_singular(assiduite_unique, data): moduleimpl: ModuleImpl = None if moduleimpl_id is not False: - if moduleimpl_id is not None: + if moduleimpl_id not in [None, "", "-1"]: if moduleimpl_id == "autre": assiduite_unique.moduleimpl_id = None external_data = ( @@ -823,7 +826,13 @@ def _edit_singular(assiduite_unique, data): assiduite_unique.external_data = external_data else: - moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first() + try: + moduleimpl = ModuleImpl.query.filter_by( + id=int(moduleimpl_id) + ).first() + except ValueError: + moduleimpl = None + if moduleimpl is None: errors.append("param 'moduleimpl_id': invalide") else: @@ -834,7 +843,7 @@ def _edit_singular(assiduite_unique, data): else: assiduite_unique.moduleimpl_id = moduleimpl_id else: - assiduite_unique.moduleimpl_id = moduleimpl_id + assiduite_unique.moduleimpl_id = None # Cas 3 : desc desc = data.get("desc", False) diff --git a/app/static/js/assiduites.js b/app/static/js/assiduites.js index a28c0eca85..73d872ec65 100644 --- a/app/static/js/assiduites.js +++ b/app/static/js/assiduites.js @@ -921,14 +921,11 @@ function deleteAssiduite(assiduite_id) { function hasModuleImpl(assiduite) { if (assiduite.moduleimpl_id != null) return true; - if ( - "external_data" in assiduite && - assiduite.external_data instanceof Object && - "module" in assiduite.external_data - ) - return true; - - return false; + return ( + assiduite.hasOwnProperty("external_data") && + assiduite.external_data != null && + assiduite.external_data.hasOwnProperty("module") + ); } /** @@ -1406,10 +1403,10 @@ function setModuleImplId(assiduite, module = null) { const moduleimpl = module == null ? getModuleImplId() : module; if (moduleimpl === "autre") { if ( - "external_data" in assiduite && - assiduite.external_data instanceof Object + assiduite.hasOwnProperty("external_data") && + assiduite.external_data != null ) { - if ("module" in assiduite.external_data) { + if (assiduite.external_data.hasOwnProperty("module")) { assiduite.external_data.module = "Autre"; } else { assiduite["external_data"] = { module: "Autre" }; @@ -1421,10 +1418,10 @@ function setModuleImplId(assiduite, module = null) { } else { assiduite["moduleimpl_id"] = moduleimpl; if ( - "external_data" in assiduite && - assiduite.external_data instanceof Object + assiduite.hasOwnProperty("external_data") && + assiduite.external_data != null ) { - if ("module" in assiduite.external_data) { + if (assiduite.external_data.hasOwnProperty("module")) { delete assiduite.external_data.module; } } @@ -1476,9 +1473,9 @@ function getCurrentAssiduiteModuleImplId() { let mod = currentAssiduites[0].moduleimpl_id; if ( mod == null && - "external_data" in currentAssiduites[0] && - currentAssiduites[0].external_data instanceof Object && - "module" in currentAssiduites[0].external_data + currentAssiduites[0].hasOwnProperty("external_data") && + currentAssiduites[0].external_data != null && + currentAssiduites[0].external_data.hasOwnProperty("module") ) { mod = currentAssiduites[0].external_data.module; } @@ -1690,9 +1687,9 @@ function getModuleImpl(assiduite) { if (id == null || id == undefined) { if ( - "external_data" in assiduite && - assiduite.external_data instanceof Object && - "module" in assiduite.external_data + assiduite.hasOwnProperty("external_data") && + assiduite.external_data != null && + assiduite.external_data.hasOwnProperty("module") ) { return assiduite.external_data.module; } else { @@ -1718,10 +1715,12 @@ function getModuleImpl(assiduite) { } function getUser(obj) { - if ("external_data" in obj && obj.external_data != null) { - if ("enseignant" in obj.external_data) { - return obj.external_data.enseignant; - } + if ( + obj.hasOwnProperty("external_data") && + obj.external_data != null && + obj.external_data.hasOwnProperty("enseignant") + ) { + return obj.external_data.enseignant; } return obj.user_id; diff --git a/app/templates/assiduites/widgets/tableau_assi.j2 b/app/templates/assiduites/widgets/tableau_assi.j2 index c40d211b00..78c34cf007 100644 --- a/app/templates/assiduites/widgets/tableau_assi.j2 +++ b/app/templates/assiduites/widgets/tableau_assi.j2 @@ -184,8 +184,11 @@ path, (data) => { let module = data.moduleimpl_id; - - if (module == null && "external_data" in data && "module" in data.external_data) { + if ( + module == null && data.hasOwnProperty("external_data") && + data.external_data != null && + data.external_data.hasOwnProperty('module') + ) { module = data.external_data.module.toLowerCase(); } From 6c77f7faca3e9d0e15f116b8991e121b76b7a6d2 Mon Sep 17 00:00:00 2001 From: iziram Date: Tue, 5 Sep 2023 14:25:38 +0200 Subject: [PATCH 05/97] =?UTF-8?q?Assiduit=C3=A9s=20:=20fix=20view=5Fmodule?= =?UTF-8?q?=5Fabs=20#688?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/assiduites.py | 4 +++ app/scodoc/sco_assiduites.py | 48 +++++++++++++++++++-------- app/scodoc/sco_utils.py | 2 +- app/views/notes.py | 19 ++--------- tests/unit/test_assiduites.py | 61 ++++++++++++++++++++++++++--------- 5 files changed, 88 insertions(+), 46 deletions(-) diff --git a/app/models/assiduites.py b/app/models/assiduites.py index 7f5520df74..0f8933e734 100644 --- a/app/models/assiduites.py +++ b/app/models/assiduites.py @@ -350,11 +350,15 @@ def compute_assiduites_justified( if justificatifs is None: justificatifs: Justificatif = Justificatif.query.filter_by(etudid=etudid).all() + justificatifs = [j for j in justificatifs if j.etat == EtatJustificatif.VALIDE] + assiduites: Assiduite = Assiduite.query.filter_by(etudid=etudid) assiduites_justifiees: list[int] = [] for assi in assiduites: + if assi.etat == EtatAssiduite.PRESENT: + continue if any( assi.date_debut >= j.date_debut and assi.date_fin <= j.date_fin for j in justificatifs diff --git a/app/scodoc/sco_assiduites.py b/app/scodoc/sco_assiduites.py index 2a478843b6..c7f5fac8a2 100644 --- a/app/scodoc/sco_assiduites.py +++ b/app/scodoc/sco_assiduites.py @@ -212,7 +212,7 @@ def get_assiduites_stats( output: dict = {} calculator: CountCalculator = CountCalculator() - if "split" not in filtered: + if filtered is None or "split" not in filtered: calculator.compute_assiduites(assiduites) count: dict = calculator.to_dict() @@ -382,7 +382,10 @@ def justifies(justi: Justificatif, obj: bool = False) -> list[int] or Query: def get_all_justified( - etudid: int, date_deb: datetime = None, date_fin: datetime = None + etudid: int, + date_deb: datetime = None, + date_fin: datetime = None, + moduleimpl_id: int = None, ) -> Query: """Retourne toutes les assiduités justifiées sur une période""" @@ -393,7 +396,9 @@ def get_all_justified( date_deb = scu.localize_datetime(date_deb) date_fin = scu.localize_datetime(date_fin) - justified = Assiduite.query.filter_by(est_just=True, etudid=etudid) + justified: Query = Assiduite.query.filter_by(est_just=True, etudid=etudid) + if moduleimpl_id is not None: + justified = justified.filter_by(moduleimpl_id=moduleimpl_id) after = filter_by_date( justified, Assiduite, @@ -419,7 +424,7 @@ def get_assiduites_count(etudid: int, sem: dict) -> tuple[int, int]: def formsemestre_get_assiduites_count( - etudid: int, formsemestre: FormSemestre + etudid: int, formsemestre: FormSemestre, moduleimpl_id: int = None ) -> tuple[int, int]: """Les comptes d'absences de cet étudiant dans ce semestre: tuple (nb abs non justifiées, nb abs justifiées) @@ -428,9 +433,14 @@ def formsemestre_get_assiduites_count( metrique = sco_preferences.get_preference("assi_metrique", formsemestre.id) return get_assiduites_count_in_interval( etudid, - date_debut=formsemestre.date_debut, - date_fin=formsemestre.date_fin, + date_debut=scu.localize_datetime( + datetime.combine(formsemestre.date_debut, time(8, 0)) + ), + date_fin=scu.localize_datetime( + datetime.combine(formsemestre.date_fin, time(18, 0)) + ), metrique=scu.translate_assiduites_metric(metrique), + moduleimpl_id=moduleimpl_id, ) @@ -441,6 +451,7 @@ def get_assiduites_count_in_interval( metrique="demi", date_debut: datetime = None, date_fin: datetime = None, + moduleimpl_id: int = None, ): """Les comptes d'absences de cet étudiant entre ces deux dates, incluses: tuple (nb abs, nb abs justifiées) @@ -452,33 +463,44 @@ def get_assiduites_count_in_interval( key = f"{etudid}_{date_debut_iso}_{date_fin_iso}{metrique}_assiduites" r = sco_cache.AbsSemEtudCache.get(key) - if not r: + if not r or moduleimpl_id is not None: date_debut: datetime = date_debut or datetime.fromisoformat(date_debut_iso) date_fin: datetime = date_fin or datetime.fromisoformat(date_fin_iso) - assiduites: Assiduite = Assiduite.query.filter_by(etudid=etudid) + log(f"deb : {date_debut.isoformat()} fin: {date_fin.isoformat()}") + + assiduites: Query = Assiduite.query.filter_by(etudid=etudid) assiduites = assiduites.filter(Assiduite.etat == scu.EtatAssiduite.ABSENT) justificatifs: Justificatif = Justificatif.query.filter_by(etudid=etudid) assiduites = filter_by_date(assiduites, Assiduite, date_debut, date_fin) + + if moduleimpl_id is not None: + assiduites = assiduites.filter_by(moduleimpl_id=moduleimpl_id) + justificatifs = filter_by_date( justificatifs, Justificatif, date_debut, date_fin ) - + log(f"---assi---\n{[a.assiduite_id for a in assiduites]}") calculator: CountCalculator = CountCalculator() calculator.compute_assiduites(assiduites) nb_abs: dict = calculator.to_dict()[metrique] - abs_just: list[Assiduite] = get_all_justified(etudid, date_debut, date_fin) + abs_just: list[Assiduite] = get_all_justified( + etudid, date_debut, date_fin, moduleimpl_id + ) + + log(f"---justi---\n{[a.assiduite_id for a in abs_just]}") calculator.reset() calculator.compute_assiduites(abs_just) nb_abs_just: dict = calculator.to_dict()[metrique] r = (nb_abs, nb_abs_just) - ans = sco_cache.AbsSemEtudCache.set(key, r) - if not ans: - log("warning: get_assiduites_count failed to cache") + if moduleimpl_id is None: + ans = sco_cache.AbsSemEtudCache.set(key, r) + if not ans: + log("warning: get_assiduites_count failed to cache") return r diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index ee8417f3b5..00f0604efb 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -237,7 +237,7 @@ def localize_datetime(date: datetime.datetime or str) -> datetime.datetime: new_date: datetime.datetime = date if new_date.tzinfo is None: try: - new_date = timezone("Europe/Paris").localize(date) + new_date = TIME_ZONE.localize(date) except OverflowError: new_date = timezone("UTC").localize(date) return new_date diff --git a/app/views/notes.py b/app/views/notes.py index 9f69cbd97f..f3ad9e4402 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -1174,22 +1174,9 @@ def view_module_abs(moduleimpl_id, fmt="html"): rows = [] for etud in inscrits: - # TODO-ASSIDUITE ne va pas car ne filtre pas sur le moduleimpl - # nb_abs, nb_abs_just = sco_assiduites.formsemestre_get_assiduites_count(etud.id, modimpl.formsemestre) - nb_abs, nb_abs_just = 0, 0 # XXX TODO-ASSIDUITE - # nb_abs = sco_abs.count_abs( - # etudid=etud.id, - # debut=debut_sem, - # fin=fin_sem, - # moduleimpl_id=moduleimpl_id, - # ) - # if nb_abs: - # nb_abs_just = sco_abs.count_abs_just( - # etudid=etud.id, - # debut=debut_sem, - # fin=fin_sem, - # moduleimpl_id=moduleimpl_id, - # ) + nb_abs, nb_abs_just = sco_assiduites.formsemestre_get_assiduites_count( + etud.id, modimpl.formsemestre, moduleimpl_id=modimpl.id + ) rows.append( { "nomprenom": etud.nomprenom, diff --git a/tests/unit/test_assiduites.py b/tests/unit/test_assiduites.py index 725be9491e..7077f7e776 100644 --- a/tests/unit/test_assiduites.py +++ b/tests/unit/test_assiduites.py @@ -13,8 +13,7 @@ import app.scodoc.sco_assiduites as scass import app.scodoc.sco_utils as scu from app import db from app.models import Assiduite, FormSemestre, Identite, Justificatif, ModuleImpl - -# from app.scodoc import sco_abs_views, sco_formsemestre TODO-ASSIDUITE +from app.models.assiduites import compute_assiduites_justified from app.scodoc.sco_exceptions import ScoValueError from tests.unit import sco_fake_gen @@ -39,7 +38,7 @@ def test_bi_directional_enum(test_client): assert BiInt.inverse()[1] == BiInt.A and BiInt.inverse()[2] == BiInt.B -@pytest.mark.skip # XXX TODO-ASSIDUITE (issue #690) +# @pytest.mark.skip # XXX TODO-ASSIDUITE (issue #690) def test_general(test_client): """tests général du modèle assiduite""" @@ -80,11 +79,11 @@ def test_general(test_client): date_fin="31/07/2024", ) - formsemestre_1 = sco_formsemestre.get_formsemestre( + formsemestre_1 = FormSemestre.get_formsemestre( formsemestre_id_1 ) # Utiliser plutot FormSemestre de nos jours TODO-ASSIDUITE - formsemestre_2 = sco_formsemestre.get_formsemestre(formsemestre_id_2) - formsemestre_3 = sco_formsemestre.get_formsemestre(formsemestre_id_3) + formsemestre_2 = FormSemestre.get_formsemestre(formsemestre_id_2) + formsemestre_3 = FormSemestre.get_formsemestre(formsemestre_id_3) # Création des modulesimpls (4, 2 par semestre) @@ -145,11 +144,14 @@ def test_general(test_client): etuds, moduleimpls, (formsemestre_1, formsemestre_2, formsemestre_3) ) verifier_filtrage_justificatifs(etuds[0], justificatifs) + + essais_cache(etuds[0].etudid, (formsemestre_1, formsemestre_2), moduleimpls) + editer_supprimer_assiduites(etuds, moduleimpls) editer_supprimer_justificatif(etuds[0]) -@pytest.mark.skip # XXX TODO-ASSIDUITE (issue #696) +# XXX TODO-ASSIDUITE (issue #696) def verif_migration_abs_assiduites(): """Vérification que le script de migration fonctionne correctement""" downgrade_module(assiduites=True, justificatifs=True) @@ -476,11 +478,11 @@ def _get_justi( ).first() -def essais_cache(etudid): +def essais_cache(etudid, sems: tuple[FormSemestre], moduleimpls: list[ModuleImpl]): """Vérification des fonctionnalités du cache""" - date_deb: str = "2023-01-01T07:00" - date_fin: str = "2023-03-31T19:00" + date_deb: str = "2022-09-01T07:00" + date_fin: str = "2023-01-31T19:00" assiduites_count_no_cache = scass.get_assiduites_count_in_interval( etudid, date_deb, date_fin @@ -490,8 +492,36 @@ def essais_cache(etudid): ) assert ( - assiduites_count_cache == assiduites_count_no_cache == (34, 15) - ), "Erreur cache" + assiduites_count_cache == assiduites_count_no_cache == (2, 1) + ), "Erreur cache classique" + + assert scass.formsemestre_get_assiduites_count(etudid, sems[0]) == ( + 2, + 1, + ), "Erreur formsemestre_get_assiduites_count (sans module) A" + assert scass.formsemestre_get_assiduites_count(etudid, sems[1]) == ( + 0, + 0, + ), "Erreur formsemestre_get_assiduites_count (sans module) B" + + assert scass.formsemestre_get_assiduites_count( + etudid, sems[0], moduleimpl_id=moduleimpls[0].id + ) == ( + 1, + 1, + ), "Erreur formsemestre_get_assiduites_count (avec module) A" + assert scass.formsemestre_get_assiduites_count( + etudid, sems[0], moduleimpl_id=moduleimpls[1].id + ) == ( + 1, + 0, + ), "Erreur formsemestre_get_assiduites_count (avec module) A" + assert scass.formsemestre_get_assiduites_count( + etudid, sems[0], moduleimpl_id=moduleimpls[2].id + ) == ( + 0, + 0, + ), "Erreur formsemestre_get_assiduites_count (avec module) A" def ajouter_justificatifs(etud): @@ -543,6 +573,8 @@ def ajouter_justificatifs(etud): db.session.commit() justificatifs.append(just_obj) + compute_assiduites_justified(etud.etudid, justificatifs) + # Vérification de la création des justificatifs assert [ justi for justi in justificatifs if not isinstance(justi, Justificatif) @@ -858,7 +890,7 @@ def ajouter_assiduites( def verifier_comptage_et_filtrage_assiduites( - etuds: list[Identite], moduleimpls: list[int], formsemestres: tuple[int] + etuds: list[Identite], moduleimpls: list[int], formsemestres: tuple[FormSemestre] ): """ Deuxième partie: @@ -931,9 +963,6 @@ def verifier_comptage_et_filtrage_assiduites( ), "Filtrage par 'Moduleimpl' mauvais" # Formsemestre - formsemestres = [ - FormSemestre.query.filter_by(id=fms["id"]).first() for fms in formsemestres - ] assert ( scass.filter_by_formsemestre( etu1.assiduites, Assiduite, formsemestres[0] From 3ecb7afdb5fecbd2185d0a3a88a77d63f3bf40cc Mon Sep 17 00:00:00 2001 From: iziram Date: Tue, 5 Sep 2023 14:42:51 +0200 Subject: [PATCH 06/97] =?UTF-8?q?Assiduit=C3=A9:=20test=5Fgeneral=20utilis?= =?UTF-8?q?e=20ancien=20module=20absence=20#690?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_assiduites.py | 5 ----- tests/unit/test_assiduites.py | 7 ++----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/app/scodoc/sco_assiduites.py b/app/scodoc/sco_assiduites.py index c7f5fac8a2..07e778cf33 100644 --- a/app/scodoc/sco_assiduites.py +++ b/app/scodoc/sco_assiduites.py @@ -467,8 +467,6 @@ def get_assiduites_count_in_interval( date_debut: datetime = date_debut or datetime.fromisoformat(date_debut_iso) date_fin: datetime = date_fin or datetime.fromisoformat(date_fin_iso) - log(f"deb : {date_debut.isoformat()} fin: {date_fin.isoformat()}") - assiduites: Query = Assiduite.query.filter_by(etudid=etudid) assiduites = assiduites.filter(Assiduite.etat == scu.EtatAssiduite.ABSENT) justificatifs: Justificatif = Justificatif.query.filter_by(etudid=etudid) @@ -481,7 +479,6 @@ def get_assiduites_count_in_interval( justificatifs = filter_by_date( justificatifs, Justificatif, date_debut, date_fin ) - log(f"---assi---\n{[a.assiduite_id for a in assiduites]}") calculator: CountCalculator = CountCalculator() calculator.compute_assiduites(assiduites) nb_abs: dict = calculator.to_dict()[metrique] @@ -490,8 +487,6 @@ def get_assiduites_count_in_interval( etudid, date_debut, date_fin, moduleimpl_id ) - log(f"---justi---\n{[a.assiduite_id for a in abs_just]}") - calculator.reset() calculator.compute_assiduites(abs_just) nb_abs_just: dict = calculator.to_dict()[metrique] diff --git a/tests/unit/test_assiduites.py b/tests/unit/test_assiduites.py index 7077f7e776..50f92c649e 100644 --- a/tests/unit/test_assiduites.py +++ b/tests/unit/test_assiduites.py @@ -38,7 +38,6 @@ def test_bi_directional_enum(test_client): assert BiInt.inverse()[1] == BiInt.A and BiInt.inverse()[2] == BiInt.B -# @pytest.mark.skip # XXX TODO-ASSIDUITE (issue #690) def test_general(test_client): """tests général du modèle assiduite""" @@ -79,9 +78,7 @@ def test_general(test_client): date_fin="31/07/2024", ) - formsemestre_1 = FormSemestre.get_formsemestre( - formsemestre_id_1 - ) # Utiliser plutot FormSemestre de nos jours TODO-ASSIDUITE + formsemestre_1 = FormSemestre.get_formsemestre(formsemestre_id_1) formsemestre_2 = FormSemestre.get_formsemestre(formsemestre_id_2) formsemestre_3 = FormSemestre.get_formsemestre(formsemestre_id_3) @@ -136,7 +133,7 @@ def test_general(test_client): etud_faux_dict = g_fake.create_etud(code_nip=None, prenom="etudfaux") etud_faux = Identite.query.filter_by(id=etud_faux_dict["id"]).first() - # verif_migration_abs_assiduites() // Test à revoir TODO-ASSIDUITE + # verif_migration_abs_assiduites() // Test à revoir TODO-ASSIDUITE (issue #696) ajouter_assiduites(etuds, moduleimpls, etud_faux) justificatifs: list[Justificatif] = ajouter_justificatifs(etuds[0]) From d21388edb8687c1b169de19547461b52941686c3 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 5 Sep 2023 15:12:39 +0200 Subject: [PATCH 07/97] Fix: lien suppression expression n'existe plus --- app/scodoc/sco_moduleimpl_status.py | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/app/scodoc/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py index 43b66230b3..8d438d79b5 100644 --- a/app/scodoc/sco_moduleimpl_status.py +++ b/app/scodoc/sco_moduleimpl_status.py @@ -299,27 +299,15 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None): has_expression = sco_compute_moy.moduleimpl_has_expression(modimpl) if has_expression: H.append( - f""" - Règle de calcul: - moyenne={modimpl.computation_expr} - """ + """ + Règle de calcul: + inutilisée dans cette version de ScoDoc + + + """ ) - H.append("""inutilisée dans cette version de ScoDoc""") - if sco_moduleimpl.can_change_ens(moduleimpl_id, raise_exc=False): - H.append( - f""" supprimer""" - ) - H.append("""""") - H.append("") else: - H.append( - '' - # règle de calcul standard' - ) + H.append('') H.append("") H.append( f""" Date: Tue, 5 Sep 2023 15:13:50 +0200 Subject: [PATCH 08/97] Fix: bul BUT pdf / autorisation_inscription --- app/but/bulletin_but_court_pdf.py | 4 ++-- sco_version.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/but/bulletin_but_court_pdf.py b/app/but/bulletin_but_court_pdf.py index 9642de833f..edd8c0a351 100644 --- a/app/but/bulletin_but_court_pdf.py +++ b/app/but/bulletin_but_court_pdf.py @@ -499,14 +499,14 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard): def boite_decisions_jury(self): """La boite en bas à droite avec jury""" txt = f"""ECTS acquis en BUT : {self.ects_total:g}
""" - if self.bul["semestre"]["decision_annee"]: + if self.bul["semestre"].get("decision_annee", None): txt += f""" Jury tenu le { datetime.datetime.fromisoformat(self.bul["semestre"]["decision_annee"]["date"]).strftime("%d/%m/%Y") }, année BUT {self.bul["semestre"]["decision_annee"]["code"]}.
""" - if self.bul["semestre"]["autorisation_inscription"]: + if self.bul["semestre"].get("autorisation_inscription", None): txt += ( "
Autorisé à s'inscrire en " + ", ".join( diff --git a/sco_version.py b/sco_version.py index 4610d0b5aa..43b8cef6b2 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.19" +SCOVERSION = "9.6.20" SCONAME = "ScoDoc" From 221e96b917191d6b3ace3b7f8b749bab5e5d6d48 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 5 Sep 2023 15:36:10 +0200 Subject: [PATCH 09/97] =?UTF-8?q?Fix:=20bul=20but=20court:=20'aucune=20UE'?= =?UTF-8?q?=20quand=20pas=20publi=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/bulletin_but_court.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/but/bulletin_but_court.py b/app/but/bulletin_but_court.py index 922e26de52..f88b319f5b 100644 --- a/app/but/bulletin_but_court.py +++ b/app/but/bulletin_but_court.py @@ -69,13 +69,13 @@ def bulletin_but(formsemestre_id: int, etudid: int = None, fmt="html"): if fmt == "pdf": bul: dict = bulletins_sem.bulletin_etud_complet(etud) else: # la même chose avec un peu moins d'infos - bul: dict = bulletins_sem.bulletin_etud(etud) + bul: dict = bulletins_sem.bulletin_etud(etud, force_publishing=True) decision_ues = ( {x["acronyme"]: x for x in bul["semestre"]["decision_ue"]} if "semestre" in bul and "decision_ue" in bul["semestre"] else {} ) - if not "ues" in bul: + if "ues" not in bul: raise ScoValueError("Aucune UE à afficher") cursus = cursus_but.EtudCursusBUT(etud, formsemestre.formation) refcomp = formsemestre.formation.referentiel_competence From 5b76654b7b7408f1115b02d6dc47ab9347a75f8d Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 5 Sep 2023 21:49:10 +0200 Subject: [PATCH 10/97] Enhance error handling in scodoc7 decorator. --- app/decorators.py | 5 ++++- app/templates/sco_value_error.j2 | 7 +++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/decorators.py b/app/decorators.py index 08aea4f736..d8816d6180 100644 --- a/app/decorators.py +++ b/app/decorators.py @@ -186,7 +186,10 @@ def scodoc7func(func): arg_names = argspec.args for arg_name in arg_names: # pour chaque arg de la fonction vue # peut produire une KeyError s'il manque un argument attendu: - v = req_args[arg_name] + try: + v = req_args[arg_name] + except KeyError as exc: + raise ScoValueError(f"argument {arg_name} manquant") from exc # try to convert all arguments to INTEGERS # necessary for db ids and boolean values try: diff --git a/app/templates/sco_value_error.j2 b/app/templates/sco_value_error.j2 index 94b6031019..e9dbd86f90 100644 --- a/app/templates/sco_value_error.j2 +++ b/app/templates/sco_value_error.j2 @@ -15,4 +15,11 @@ {% endif %}

+

+Si le problème persiste, merci de contacter le support ScoDoc via + +{{scu.SCO_DISCORD_ASSISTANCE}} + +

+ {% endblock %} \ No newline at end of file From 03b9a92ba8cb59a3d085bc9711b4b769c8a5f362 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 5 Sep 2023 22:50:01 +0200 Subject: [PATCH 11/97] =?UTF-8?q?Ajoute=20liens=20sur=20bulletins=20BUT,?= =?UTF-8?q?=20pr=C3=A9cise=20ann=C3=A9e=20de=20validation,=20corrige=20bug?= =?UTF-8?q?=20affichage=20decision=20ann=C3=A9ee=20BUT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/bulletin_but_court_pdf.py | 3 ++- app/models/but_validations.py | 4 ++- app/scodoc/sco_bulletins_json.py | 1 + app/static/css/bulletin_court.css | 10 +++++-- app/static/css/releve-but.css | 2 +- app/static/css/scodoc.css | 2 +- app/templates/bul_head.j2 | 25 +++++++++++------- app/templates/but/bulletin_court_page.j2 | 33 ++++++++++++++---------- 8 files changed, 51 insertions(+), 29 deletions(-) diff --git a/app/but/bulletin_but_court_pdf.py b/app/but/bulletin_but_court_pdf.py index edd8c0a351..abd90015b5 100644 --- a/app/but/bulletin_but_court_pdf.py +++ b/app/but/bulletin_but_court_pdf.py @@ -503,7 +503,8 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard): txt += f""" Jury tenu le { datetime.datetime.fromisoformat(self.bul["semestre"]["decision_annee"]["date"]).strftime("%d/%m/%Y") - }, année BUT {self.bul["semestre"]["decision_annee"]["code"]}. + }, année BUT{self.bul["semestre"]["decision_annee"]["ordre"]} + {self.bul["semestre"]["decision_annee"]["code"]}.
""" if self.bul["semestre"].get("autorisation_inscription", None): diff --git a/app/models/but_validations.py b/app/models/but_validations.py index 6a3939a642..539c1239da 100644 --- a/app/models/but_validations.py +++ b/app/models/but_validations.py @@ -214,10 +214,12 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict: decisions["decision_rcue"] = [] decisions["descr_decisions_rcue"] = "" decisions["descr_decisions_niveaux"] = "" - # --- Année: prend la validation pour l'année scolaire de ce semestre + # --- Année: prend la validation pour l'année scolaire et l'ordre de ce semestre + annee_but = (formsemestre.semestre_id + 1) // 2 validation = ApcValidationAnnee.query.filter_by( etudid=etud.id, annee_scolaire=formsemestre.annee_scolaire(), + ordre=annee_but, referentiel_competence_id=formsemestre.formation.referentiel_competence_id, ).first() if validation: diff --git a/app/scodoc/sco_bulletins_json.py b/app/scodoc/sco_bulletins_json.py index e5e7693d55..df54776949 100644 --- a/app/scodoc/sco_bulletins_json.py +++ b/app/scodoc/sco_bulletins_json.py @@ -405,6 +405,7 @@ def dict_decision_jury( """dict avec decision pour bulletins json - autorisation_inscription - decision : décision semestre + - decision_annee : annee BUT - decision_ue : list des décisions UE - situation diff --git a/app/static/css/bulletin_court.css b/app/static/css/bulletin_court.css index 6e16b6b703..614a332115 100644 --- a/app/static/css/bulletin_court.css +++ b/app/static/css/bulletin_court.css @@ -5,11 +5,17 @@ } } +div.but_bul_court_links { + margin-left: 16px; + margin-bottom: 16px; +} + div.but_bul_court { - width: 17cm; + /* width: 17cm; */ display: grid; - grid-template-columns: 6cm 11cm; font-size: 11pt; + grid-template-columns: 6cm 11cm; + margin-left: 16px; } #infos_etudiant { diff --git a/app/static/css/releve-but.css b/app/static/css/releve-but.css index 1f7a492c96..25a31c972e 100644 --- a/app/static/css/releve-but.css +++ b/app/static/css/releve-but.css @@ -28,7 +28,7 @@ main { ; --couleurSurlignage: rgba(255, 253, 110, 0.49); max-width: 1000px; - margin: auto; + margin-left: 16px; display: none; } diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 9312a5d650..05107dc4bf 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -3077,7 +3077,7 @@ div.bul_foot { border-radius: 16px; border: 1px solid #AAA; padding: 16px 32px; - margin: auto; + margin-left: 16px; } div.bull_appreciations { diff --git a/app/templates/bul_head.j2 b/app/templates/bul_head.j2 index 89fa768998..c851fa5775 100644 --- a/app/templates/bul_head.j2 +++ b/app/templates/bul_head.j2 @@ -42,17 +42,24 @@ format='pdf', version=version, )}}">{{scu.ICON_PDF|safe}} - {% if formsemestre.formation.is_apc() %} - version courte spéciale BUT - {% endif %}
+ {% if formsemestre.formation.is_apc() %} + + {% endif %} {% if not is_apc %} diff --git a/app/templates/but/bulletin_court_page.j2 b/app/templates/but/bulletin_court_page.j2 index 1068e01671..d73c6f389a 100644 --- a/app/templates/but/bulletin_court_page.j2 +++ b/app/templates/but/bulletin_court_page.j2 @@ -39,19 +39,23 @@ {%- endmacro %} {% block app_content %} -

-version pdf {{scu.ICON_PDF|safe}} -version complète -

- +
{{etud.nomprenom}}
@@ -157,7 +161,8 @@ Jury tenu le {{ datetime.datetime.fromisoformat(bul.semestre.decision_annee.date).strftime("%d/%m/%Y") }}, - année BUT {{bul.semestre.decision_annee.code}}. + année BUT{{bul.semestre.decision_annee.ordre}} + {{bul.semestre.decision_annee.code}}. {% endif %} {% set virg = joiner(", ") %} {% for aut in bul.semestre.autorisation_inscription -%} From c587c8b7d2f2bcfee04c41680e5bb21f1f20bb31 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 6 Sep 2023 08:48:00 +0200 Subject: [PATCH 12/97] Petites modifs des imports et routes --- app/scodoc/sco_evaluation_check_abs.py | 7 +++-- app/scodoc/sco_groups.py | 14 +++++----- app/templates/scolar/partition_editor.j2 | 3 +- app/views/notes.py | 35 ++++++++++++++---------- app/views/scolar.py | 30 +++++++++++--------- sco_version.py | 2 +- 6 files changed, 52 insertions(+), 39 deletions(-) diff --git a/app/scodoc/sco_evaluation_check_abs.py b/app/scodoc/sco_evaluation_check_abs.py index d2dea80962..957ba963c8 100644 --- a/app/scodoc/sco_evaluation_check_abs.py +++ b/app/scodoc/sco_evaluation_check_abs.py @@ -108,9 +108,10 @@ def evaluation_check_absences(evaluation: Evaluation): return ValButAbs, AbsNonSignalee, ExcNonSignalee, ExcNonJust, AbsButExc -def evaluation_check_absences_html(evaluation_id, with_header=True, show_ok=True): +def evaluation_check_absences_html( + evaluation: Evaluation, with_header=True, show_ok=True +): """Affiche état vérification absences d'une évaluation""" - evaluation: Evaluation = db.session.get(Evaluation, evaluation_id) am, pm = evaluation.is_matin(), evaluation.is_apresmidi() # 1 si matin, 0 si apres midi, 2 si toute la journee: match am, pm: @@ -249,7 +250,7 @@ def formsemestre_check_absences_html(formsemestre_id): ): H.append( evaluation_check_absences_html( - evaluation.id, # XXX TODO-ASSIDUITE remplacer par evaluation ... + evaluation, with_header=False, show_ok=False, ) diff --git a/app/scodoc/sco_groups.py b/app/scodoc/sco_groups.py index 6a7b686d85..d2df8a21c3 100644 --- a/app/scodoc/sco_groups.py +++ b/app/scodoc/sco_groups.py @@ -1377,20 +1377,18 @@ def group_rename(group_id): return group_set_name(group, tf[2]["group_name"]) -def groups_auto_repartition(partition_id=None): - """Reparti les etudiants dans des groupes dans une partition, en respectant le niveau +def groups_auto_repartition(partition: Partition): + """Réparti les etudiants dans des groupes dans une partition, en respectant le niveau et la mixité. """ - partition: Partition = Partition.query.get_or_404(partition_id) if not partition.groups_editable: raise AccessDenied("Partition non éditable") - formsemestre_id = partition.formsemestre_id formsemestre = partition.formsemestre # renvoie sur page édition partitions et groupes dest_url = url_for( "scolar.partition_editor", scodoc_dept=g.scodoc_dept, - formsemestre_id=formsemestre_id, + formsemestre_id=formsemestre.id, ) if not formsemestre.can_change_groups(): raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !") @@ -1409,7 +1407,9 @@ def groups_auto_repartition(partition_id=None): ] H = [ - html_sco_header.sco_header(page_title="Répartition des groupes"), + html_sco_header.sco_header( + page_title="Répartition des groupes", formsemestre_id=formsemestre.id + ), f"""

Répartition des groupes de {partition.partition_name}

Semestre {formsemestre.titre_annee()}

Les groupes existants seront effacés et remplacés par @@ -1455,7 +1455,7 @@ def groups_auto_repartition(partition_id=None): listes = {} for civilite in civilites: listes[civilite] = [ - (_get_prev_moy(x["etudid"], formsemestre_id), x["etudid"]) + (_get_prev_moy(x["etudid"], formsemestre.id), x["etudid"]) for x in identdict.values() if x["civilite"] == civilite ] diff --git a/app/templates/scolar/partition_editor.j2 b/app/templates/scolar/partition_editor.j2 index 97744f4314..0abd990fdc 100644 --- a/app/templates/scolar/partition_editor.j2 +++ b/app/templates/scolar/partition_editor.j2 @@ -224,7 +224,8 @@ Afficher sur bulletins et tableaux

`; diff --git a/app/views/notes.py b/app/views/notes.py index f3ad9e4402..a7fd0d212c 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -44,7 +44,6 @@ from app import models from app.auth.models import User from app.but import ( apc_edit_ue, - bulletin_but_court, cursus_but, jury_edit_manual, jury_but, @@ -58,21 +57,23 @@ from app.but.forms import jury_but_forms from app.comp import jury, res_sem from app.comp.res_compat import NotesTableCompat from app.models import ( + ApcNiveau, BulAppreciations, + DispenseUE, Evaluation, Formation, + FormSemestre, + FormSemestreInscription, + FormSemestreUEComputationExpr, + Identite, + Module, + ModuleImpl, ScolarAutorisationInscription, ScolarNews, Scolog, + ScoDocSiteConfig, + UniteEns, ) -from app.models.but_refcomp import ApcNiveau -from app.models.config import ScoDocSiteConfig -from app.models.etudiants import Identite -from app.models.formsemestre import FormSemestre, FormSemestreInscription -from app.models.formsemestre import FormSemestreUEComputationExpr -from app.models.moduleimpls import ModuleImpl -from app.models.modules import Module -from app.models.ues import DispenseUE, UniteEns from app.scodoc.sco_exceptions import ScoFormationConflict, ScoPermissionDenied from app.views import notes_bp as bp @@ -1790,11 +1791,17 @@ sco_publish( sco_undo_notes.evaluation_list_operations, Permission.ScoView, ) -sco_publish( - "/evaluation_check_absences_html", - sco_evaluation_check_abs.evaluation_check_absences_html, - Permission.ScoView, -) + + +@bp.route("/evaluation_check_absences_html/") +@scodoc +@permission_required(Permission.ScoView) +def evaluation_check_absences_html(evaluation_id: int): + "Check absences sur une évaluation" + evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id) + return sco_evaluation_check_abs.evaluation_check_absences_html(evaluation) + + sco_publish( "/formsemestre_check_absences_html", sco_evaluation_check_abs.formsemestre_check_absences_html, diff --git a/app/views/scolar.py b/app/views/scolar.py index b8a4466e71..ee36c79e3b 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -50,14 +50,16 @@ from app.decorators import ( scodoc7func, permission_required, permission_required_compat_scodoc7, - admin_required, - login_required, ) -from app.models.etudiants import Identite +from app.models import ( + FormSemestre, + Identite, + Partition, + ScolarEvent, + ScolarNews, + Scolog, +) from app.models.etudiants import make_etud_args -from app.models.events import ScolarNews, Scolog -from app.models.formsemestre import FormSemestre -from app.models.validations import ScolarEvent from app.views import scolar_bp as bp from app.views import ScoData @@ -67,7 +69,6 @@ from app.scodoc.scolog import logdb from app.scodoc.sco_permissions import Permission from app.scodoc.sco_exceptions import ( AccessDenied, - ScoException, ScoValueError, ) @@ -845,12 +846,15 @@ sco_publish( methods=["GET", "POST"], ) -sco_publish( - "/groups_auto_repartition", - sco_groups.groups_auto_repartition, - Permission.ScoView, - methods=["GET", "POST"], -) + +@bp.route("/groups_auto_repartition/", methods=["GET", "POST"]) +@scodoc +@permission_required(Permission.ScoView) +def groups_auto_repartition(partition_id: int): + "Réparti les etudiants dans des groupes dans une partition" + partition: Partition = Partition.query.get_or_404(partition_id) + return sco_groups.groups_auto_repartition(partition) + sco_publish( "/edit_partition_form", diff --git a/sco_version.py b/sco_version.py index 43b8cef6b2..a9d45b757d 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.20" +SCOVERSION = "9.6.21" SCONAME = "ScoDoc" From cf74708f83d2f4be467fa35eb8dcf0de1cf3890c Mon Sep 17 00:00:00 2001 From: iziram Date: Wed, 6 Sep 2023 11:21:53 +0200 Subject: [PATCH 13/97] Assiduites : evaluation_check_absences #685 --- app/scodoc/sco_evaluation_check_abs.py | 57 ++++++------ .../pages/signal_assiduites_etud.j2 | 25 +++++ app/templates/assiduites/widgets/differee.j2 | 6 +- app/views/assiduites.py | 93 ++++++++++++++++++- 4 files changed, 148 insertions(+), 33 deletions(-) diff --git a/app/scodoc/sco_evaluation_check_abs.py b/app/scodoc/sco_evaluation_check_abs.py index 957ba963c8..a59cc44c81 100644 --- a/app/scodoc/sco_evaluation_check_abs.py +++ b/app/scodoc/sco_evaluation_check_abs.py @@ -30,16 +30,17 @@ from flask import url_for, g from app import db -from app.models import Evaluation, FormSemestre, Identite +from app.models import Evaluation, FormSemestre, Identite, Assiduite import app.scodoc.sco_utils as scu from app.scodoc import html_sco_header from app.scodoc import sco_evaluations from app.scodoc import sco_evaluation_db -from app.scodoc.sco_exceptions import ScoValueError from app.scodoc import sco_groups +from flask_sqlalchemy.query import Query +from sqlalchemy import or_, and_ + -# XXX TODO-ASSIDUITE https://scodoc.org/git/ScoDoc/ScoDoc/issues/685 def evaluation_check_absences(evaluation: Evaluation): """Vérifie les absences au moment de cette évaluation. Cas incohérents que l'on peut rencontrer pour chaque étudiant: @@ -50,28 +51,32 @@ def evaluation_check_absences(evaluation: Evaluation): EXC et pas justifie Ramene 5 listes d'etudid """ - raise ScoValueError("Fonction non disponible, patience !") # XXX TODO-ASSIDUITE - if not evaluation.date_debut: return [], [], [], [], [] # evaluation sans date - am, pm = evaluation.is_matin(), evaluation.is_apresmidi() + etudids = [ + etudid + for etudid, _ in sco_groups.do_evaluation_listeetuds_groups( + evaluation.id, getallstudents=True + ) + ] - # Liste les absences à ce moment: - absences = sco_abs.list_abs_jour(evaluation.date_debut, am=am, pm=pm) - abs_etudids = set([x["etudid"] for x in absences]) # ensemble des etudiants absents - abs_non_just = sco_abs.list_abs_non_just_jour( - evaluation.date_debut.date(), am=am, pm=pm + deb, fin = scu.localize_datetime(evaluation.date_debut), scu.localize_datetime( + evaluation.date_fin ) - abs_nj_etudids = set( - [x["etudid"] for x in abs_non_just] - ) # ensemble des etudiants absents non justifies - justifs = sco_abs.list_abs_jour( - evaluation.date_debut.date(), am=am, pm=pm, is_abs=None, is_just=True + + assiduites: Query = Assiduite.query.filter( + Assiduite.etudid.in_(etudids), + Assiduite.etat == scu.EtatAssiduite.ABSENT, + or_( + and_(Assiduite.date_debut >= deb, Assiduite.date_debut <= fin), + and_(Assiduite.date_fin >= deb, Assiduite.date_fin <= fin), + ), ) - just_etudids = set( - [x["etudid"] for x in justifs] - ) # ensemble des etudiants avec justif + + abs_etudids = set(assi.etudid for assi in assiduites) + abs_nj_etudids = set(assi.etudid for assi in assiduites if assi.est_just is False) + just_etudids = set(assi.etudid for assi in assiduites if assi.est_just is True) # Les notes: notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation.id) @@ -80,9 +85,7 @@ def evaluation_check_absences(evaluation: Evaluation): ExcNonSignalee = [] # note EXC mais pas noté absent ExcNonJust = [] # note EXC mais absent non justifie AbsButExc = [] # note ABS mais justifié - for etudid, _ in sco_groups.do_evaluation_listeetuds_groups( - evaluation.id, getallstudents=True - ): + for etudid in etudids: if etudid in notes_db: val = notes_db[etudid]["value"] if ( @@ -170,14 +173,10 @@ def evaluation_check_absences_html( ) if linkabs: url = url_for( - "absences.doSignaleAbsence", # XXX TODO-ASSIDUITE - scodoc_dept=g.scodoc_dept, + "assiduites.signal_evaluation_abs", etudid=etudid, - # par defaut signale le jour du début de l'éval - datedebut=evaluation.date_debut.strftime("%d/%m/%Y"), - datefin=evaluation.date_debut.strftime("%d/%m/%Y"), - demijournee=demijournee, - moduleimpl_id=evaluation.moduleimpl_id, + evaluation_id=evaluation.id, + scodoc_dept=g.scodoc_dept, ) H.append( f"""signaler cette absence""" diff --git a/app/templates/assiduites/pages/signal_assiduites_etud.j2 b/app/templates/assiduites/pages/signal_assiduites_etud.j2 index 2222cbe425..71dd199624 100644 --- a/app/templates/assiduites/pages/signal_assiduites_etud.j2 +++ b/app/templates/assiduites/pages/signal_assiduites_etud.j2 @@ -32,6 +32,18 @@

+ + {% if saisie_eval %} +
+
+

+ La saisie de l'assiduité a été préconfigurée en fonction de l'évaluation.
+ Une fois la saisie finie, cliquez sur le lien si dessous pour revenir sur la gestion de l'évaluation +

+ retourner sur la page de l'évaluation +
+ {% endif %} + {{diff | safe}}
@@ -118,7 +130,20 @@ window.forceModule = "{{ forcer_module }}" window.forceModule = window.forceModule == "True" ? true : false + const date_deb = "{{date_deb}}"; + const date_fin = "{{date_fin}}"; + + {% if saisie_eval %} + createColumn( + date_deb, + date_fin, + {{ moduleimpl_id }} + ); + window.location.href = "#saisie_eval" + getAndUpdateCol(1) + {% else %} createColumn(); + {% endif %} diff --git a/app/templates/assiduites/widgets/differee.j2 b/app/templates/assiduites/widgets/differee.j2 index 8d96c5bc41..523c4a8270 100644 --- a/app/templates/assiduites/widgets/differee.j2 +++ b/app/templates/assiduites/widgets/differee.j2 @@ -278,7 +278,7 @@ currentDate = moment(currentDate).tz(TIMEZONE).format("YYYY-MM-DDTHH:mm"); } - function createColumn(dateStart = "", dateEnd = "") { + function createColumn(dateStart = "", dateEnd = "", moduleimpl_id = "") { let table = document.getElementById("studentTable"); let th = document.createElement("div"); th.classList.add("th", "error"); @@ -343,6 +343,10 @@ editModuleImpl(sl); }) + if (moduleimpl_id != "") { + sl.value = moduleimpl_id; + } + let rows = table.querySelector(".tbody").querySelectorAll(".tr"); for (let i = 0; i < rows.length; i++) { let td = document.createElement("div"); diff --git a/app/views/assiduites.py b/app/views/assiduites.py index ebf9dbd318..4e13e8076f 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -1,7 +1,7 @@ import datetime -from flask import g, request, render_template -from flask import abort, url_for +from flask import g, request, render_template, flash +from flask import abort, url_for, redirect from flask_login import current_user from app import db @@ -18,6 +18,7 @@ from app.models import ( Assiduite, Departement, FormSemestreInscription, + Evaluation, ) from app.views import assiduites_bp as bp from app.views import ScoData @@ -203,6 +204,27 @@ def signal_assiduites_etud(): if etud.dept_id != g.scodoc_dept_id: abort(404, "étudiant inexistant dans ce département") + date = request.args.get("date", datetime.date.today().isoformat()) + + # gestion é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 + else url_for( + "notes.evaluation_check_absences_html", + evaluation_id=evaluation_id, + scodoc_dept=g.scodoc_dept, + ) + ) + header: str = html_sco_header.sco_header( page_title="Saisie Assiduités", init_qtip=True, @@ -235,7 +257,7 @@ def signal_assiduites_etud(): render_template( "assiduites/pages/signal_assiduites_etud.j2", sco=ScoData(etud), - date=datetime.date.today().isoformat(), + date=date, morning=morning, lunch=lunch, timeline=_timeline(), @@ -249,6 +271,11 @@ def signal_assiduites_etud(): etudiants=[sco_etud.get_etud_info(etudid=etud.etudid, filled=True)[0]], moduleimpl_select=select, ), + saisie_eval=saisie_eval, + date_deb=date_deb, + date_fin=date_fin, + redirect_url=redirect_url, + moduleimpl_id=moduleimpl_id, ), ).build() @@ -995,6 +1022,66 @@ def signal_assiduites_diff(): ).build() +@bp.route("/SignalEvaluationAbs//") +@scodoc +@permission_required(Permission.ScoView) +def signal_evaluation_abs(etudid: int = None, evaluation_id: int = None): + """ + Signale l'absence d'un étudiant à une évaluation + Si la durée de l'évaluation est inférieur à 1 jour + 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 + """ + etud: Identite = Identite.query.get_or_404(etudid) + if etud.dept_id != g.scodoc_dept_id: + abort(404, "étudiant inexistant dans ce département") + + evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id) + + delta: datetime.timedelta = evaluation.date_fin - evaluation.date_debut + if delta > datetime.timedelta(days=1): + # rediriger vers page saisie + flash("Redirection pour saisie abs") + return redirect( + url_for( + "assiduites.signal_assiduites_etud", + etudid=etudid, + evaluation_id=evaluation.id, + date_deb=evaluation.date_debut.strftime("%Y-%m-%dT%H:%M:%S"), + date_fin=evaluation.date_fin.strftime("%Y-%m-%dT%H:%M:%S"), + moduleimpl_id=evaluation.moduleimpl.id, + saisie_eval="true", + scodoc_dept=g.scodoc_dept, + ) + ) + + # créer l'assiduité + + assiduite_unique: Assiduite = Assiduite.create_assiduite( + etud=etud, + date_debut=scu.localize_datetime(evaluation.date_debut), + date_fin=scu.localize_datetime(evaluation.date_fin), + etat=scu.EtatAssiduite.ABSENT, + moduleimpl=evaluation.moduleimpl, + ) + + db.session.add(assiduite_unique) + db.session.commit() + + flash("L'absence a bien été créée") + # rediriger vers la page d'évaluation + return redirect( + url_for( + "notes.evaluation_check_absences_html", + evaluation_id=evaluation.id, + scodoc_dept=g.scodoc_dept, + ) + ) + + +# --- Fonctions internes --- + + def _get_days_between_dates(deb: str, fin: str): if deb is None or fin is None: return "null" From 483c86d904078f52d9de0bcca7a114f8835daf4f Mon Sep 17 00:00:00 2001 From: iziram Date: Wed, 6 Sep 2023 15:09:43 +0200 Subject: [PATCH 14/97] Assiduites : fix #698 et #697 --- app/scodoc/sco_groups_view.py | 90 +++++++++-------------------- app/scodoc/sco_moduleimpl_status.py | 15 +++++ app/views/assiduites.py | 9 ++- 3 files changed, 49 insertions(+), 65 deletions(-) diff --git a/app/scodoc/sco_groups_view.py b/app/scodoc/sco_groups_view.py index b27098ef26..728ccb3635 100644 --- a/app/scodoc/sco_groups_view.py +++ b/app/scodoc/sco_groups_view.py @@ -833,7 +833,7 @@ def tab_absences_html(groups_infos, etat=None): "
  • ", form_choix_jour_saisie_hebdo(groups_infos), "
  • ", - f"""
  • État des assiduités du groupe
  • """, "", @@ -890,76 +890,38 @@ def form_choix_jour_saisie_hebdo(groups_infos, moduleimpl_id=None): authuser = current_user if not authuser.has_permission(Permission.ScoAbsChange): return "" - sem = groups_infos.formsemestre - first_monday = sco_cal.ddmmyyyy(sem["date_debut"]).prev_monday() - today_idx = datetime.date.today().weekday() - - FA = [] # formulaire avec menu saisi absences - FA.append( - # TODO-ASSIDUITE et utiliser url_for... (was Absences/SignaleAbsenceGrSemestre) - '
    ' - ) - FA.append('' % sem) - FA.append(groups_infos.get_form_elem()) - if moduleimpl_id: - FA.append( - '' % moduleimpl_id - ) - FA.append('') - - FA.append( - """""" - ) - FA.append("""") - FA.append("
    ") - return "\n".join(FA) + return f""" + + """ -# Ajout Le Havre -# Formulaire saisie absences semaine +# Saisie de l'assiduité par semaine def form_choix_saisie_semaine(groups_infos): authuser = current_user if not authuser.has_permission(Permission.ScoAbsChange): return "" - # construit l'URL "destination" - # (a laquelle on revient apres saisie absences) query_args = parse_qs(request.query_string) - moduleimpl_id = query_args.get("moduleimpl_id", [""])[0] - if "head_message" in query_args: - del query_args["head_message"] - destination = "%s?%s" % ( - request.base_url, - urllib.parse.urlencode(query_args, True), - ) - destination = destination.replace( - "%", "%%" - ) # car ici utilisee dans un format string ! - - DateJour = time.strftime("%d/%m/%Y") - datelundi = sco_cal.ddmmyyyy(DateJour).prev_monday() - FA = [] # formulaire avec menu saisie hebdo des absences - # XXX TODO-ASSIDUITE et utiliser un POST - FA.append('
    ') - FA.append('' % datelundi) - FA.append('' % moduleimpl_id) - FA.append('' % destination) - FA.append(groups_infos.get_form_elem()) - FA.append( - '' - ) # XXX - FA.append("
    ") - return "\n".join(FA) + moduleimpl_id = query_args.get("moduleimpl_id", [None])[0] + semaine = datetime.date.today().isocalendar().week + return f""" + + """ def export_groups_as_moodle_csv(formsemestre_id=None): diff --git a/app/scodoc/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py index 8d438d79b5..68c04f0129 100644 --- a/app/scodoc/sco_moduleimpl_status.py +++ b/app/scodoc/sco_moduleimpl_status.py @@ -331,6 +331,21 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None): }&formsemestre_id={formsemestre.id} &moduleimpl_id={moduleimpl_id} " + >Saisie Absences journée + """ + ) + + H.append( + f""" + Saisie Absences hebdo """ ) diff --git a/app/views/assiduites.py b/app/views/assiduites.py index 4e13e8076f..958a3ae5c4 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -1002,7 +1002,9 @@ def signal_assiduites_diff(): "assiduites/pages/signal_assiduites_diff.j2", diff=_differee( etudiants=etudiants, - moduleimpl_select=_module_selector(formsemestre), + moduleimpl_select=_module_selector( + formsemestre, request.args.get("moduleimpl_id", None) + ), date=date, periode={ "deb": formsemestre.date_debut.isoformat(), @@ -1162,6 +1164,11 @@ 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, From be6cf7a62df02ed70bcf7a3b9094e2f377492b04 Mon Sep 17 00:00:00 2001 From: iziram Date: Wed, 6 Sep 2023 16:41:46 +0200 Subject: [PATCH 15/97] Assiduites: traitement billets absences #699 --- app/scodoc/sco_assiduites.py | 44 ++++++++++++++++++++++++--- app/views/absences.py | 59 +++++++----------------------------- 2 files changed, 51 insertions(+), 52 deletions(-) diff --git a/app/scodoc/sco_assiduites.py b/app/scodoc/sco_assiduites.py index 07e778cf33..3990cd82fd 100644 --- a/app/scodoc/sco_assiduites.py +++ b/app/scodoc/sco_assiduites.py @@ -4,9 +4,9 @@ Ecrit par Matthias Hartmann. from datetime import date, datetime, time, timedelta from pytz import UTC -from app import log +from app import log, db import app.scodoc.sco_utils as scu -from app.models.assiduites import Assiduite, Justificatif +from app.models.assiduites import Assiduite, Justificatif, compute_assiduites_justified from app.models.etudiants import Identite from app.models.formsemestre import FormSemestre, FormSemestreInscription from app.scodoc import sco_formsemestre_inscriptions @@ -141,11 +141,11 @@ class CountCalculator: self.hours += finish_hours.total_seconds() / 3600 self.hours += self.hour_per_day - (start_hours.total_seconds() / 3600) - def compute_assiduites(self, assiduites: Assiduite): + def compute_assiduites(self, assiduites: Query or list): """Calcule les métriques pour la collection d'assiduité donnée""" assi: Assiduite assiduites: list[Assiduite] = ( - assiduites.all() if isinstance(assiduites, Assiduite) else assiduites + assiduites.all() if isinstance(assiduites, Query) else assiduites ) for assi in assiduites: self.count += 1 @@ -408,6 +408,42 @@ def get_all_justified( return after +def create_absence( + date_debut: datetime, + date_fin: datetime, + etudid: int, + description: str = None, + est_just: bool = False, +) -> int: + etud: Identite = Identite.query.filter_by(etudid=etudid).first_or_404() + assiduite_unique: Assiduite = Assiduite.create_assiduite( + etud=etud, + date_debut=date_debut, + date_fin=date_fin, + etat=scu.EtatAssiduite.ABSENT, + description=description, + ) + db.session.add(assiduite_unique) + + db.session.commit() + if est_just: + justi = Justificatif.create_justificatif( + etud=etud, + date_debut=date_debut, + date_fin=date_fin, + etat=scu.EtatJustificatif.VALIDE, + raison=description, + ) + db.session.add(justi) + db.session.commit() + + compute_assiduites_justified(etud.id, [justi]) + + calculator: CountCalculator = CountCalculator() + calculator.compute_assiduites([assiduite_unique]) + return calculator.to_dict()["demi"] + + # Gestion du cache def get_assiduites_count(etudid: int, sem: dict) -> tuple[int, int]: """Les comptes d'absences de cet étudiant dans ce semestre: diff --git a/app/views/absences.py b/app/views/absences.py index 3268bb4ccc..fd8a1443e0 100644 --- a/app/views/absences.py +++ b/app/views/absences.py @@ -56,6 +56,7 @@ from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc import html_sco_header from app.scodoc import sco_cal +from app.scodoc import sco_assiduites as scass from app.scodoc import sco_abs_billets from app.scodoc import sco_etud from app.scodoc import sco_preferences @@ -312,54 +313,16 @@ def _ProcessBilletAbsence( n = 0 # nombre de demi-journées d'absence ajoutées # 1-- Ajout des absences (et justifs) - datedebut = billet.abs_begin.strftime("%d/%m/%Y") - datefin = billet.abs_end.strftime("%d/%m/%Y") - dates = sco_cal.DateRangeISO(datedebut, datefin) - # commence après-midi ? - if dates and billet.abs_begin.hour > 11: - # XXX TODO-ASSIDUITE - raise ScoValueError("Fonction non disponible") - sco_abs.add_absence( - billet.etudid, - dates[0], - 0, - estjust, - description=description, - ) - n += 1 - dates = dates[1:] - # termine matin ? - if dates and billet.abs_end.hour < 12: - # XXX TODO-ASSIDUITE - raise ScoValueError("Fonction non disponible") - sco_abs.add_absence( - billet.etudid, - dates[-1], - 1, - estjust, - description=description, - ) - n += 1 - dates = dates[:-1] - - for jour in dates: - raise ScoValueError("Fonction non disponible") - sco_abs.add_absence( - billet.etudid, - jour, - 0, - estjust, - description=description, - ) - # XXX TODO-ASSIDUITE - sco_abs.add_absence( - billet.etudid, - jour, - 1, - estjust, - description=description, - ) - n += 2 + datedebut = billet.abs_begin + datefin = billet.abs_end + log(f"Gestion du billet n°{billet.id}") + n = scass.create_absence( + date_debut=datedebut, + date_fin=datefin, + etudid=billet.etudid, + description=description, + est_just=estjust, + ) # 2- Change état du billet billet.etat = True From 96d6e5d1f5d45a131135139f25f743cc54dea7ac Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 6 Sep 2023 21:34:12 +0200 Subject: [PATCH 16/97] Fix: evaluation_check_absences_html access --- app/views/notes.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/views/notes.py b/app/views/notes.py index a7fd0d212c..f81b5e1b54 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -1798,7 +1798,13 @@ sco_publish( @permission_required(Permission.ScoView) def evaluation_check_absences_html(evaluation_id: int): "Check absences sur une évaluation" - evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id) + evaluation: Evaluation = ( + Evaluation.query.filter_by(id=evaluation_id) + .join(ModuleImpl) + .join(FormSemestre) + .filter_by(dept_id=g.scodoc_dept_id) + .first_or_404() + ) return sco_evaluation_check_abs.evaluation_check_absences_html(evaluation) From 9105a9a5f044dc5853838050eb3f003a8e2ff447 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 6 Sep 2023 22:07:32 +0200 Subject: [PATCH 17/97] Version 9.6.23 (oups) --- sco_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sco_version.py b/sco_version.py index a9d45b757d..a17429ed27 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.21" +SCOVERSION = "9.6.23" SCONAME = "ScoDoc" From 578a6144931d30c0858cd1a8fbd47df18cd4b9d8 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 7 Sep 2023 08:53:37 +0200 Subject: [PATCH 18/97] Fix: import bulletins but courts --- app/views/notes.py | 1 + sco_version.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/notes.py b/app/views/notes.py index f81b5e1b54..51a24caee2 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -44,6 +44,7 @@ from app import models from app.auth.models import User from app.but import ( apc_edit_ue, + bulletin_but_court, cursus_but, jury_edit_manual, jury_but, diff --git a/sco_version.py b/sco_version.py index a17429ed27..34eef316d5 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.23" +SCOVERSION = "9.6.24" SCONAME = "ScoDoc" From d2bd88ba320b751cf14213b1c97c770290004929 Mon Sep 17 00:00:00 2001 From: iziram Date: Thu, 7 Sep 2023 08:59:06 +0200 Subject: [PATCH 19/97] Assiduites : fix evaluation check abs + partition editor --- app/scodoc/sco_evaluation_check_abs.py | 8 +++----- app/views/scolar.py | 1 + 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/scodoc/sco_evaluation_check_abs.py b/app/scodoc/sco_evaluation_check_abs.py index a59cc44c81..41299056aa 100644 --- a/app/scodoc/sco_evaluation_check_abs.py +++ b/app/scodoc/sco_evaluation_check_abs.py @@ -51,7 +51,7 @@ def evaluation_check_absences(evaluation: Evaluation): EXC et pas justifie Ramene 5 listes d'etudid """ - if not evaluation.date_debut: + if not evaluation.date_debut or not evaluation.date_fin: return [], [], [], [], [] # evaluation sans date etudids = [ @@ -68,10 +68,8 @@ def evaluation_check_absences(evaluation: Evaluation): assiduites: Query = Assiduite.query.filter( Assiduite.etudid.in_(etudids), Assiduite.etat == scu.EtatAssiduite.ABSENT, - or_( - and_(Assiduite.date_debut >= deb, Assiduite.date_debut <= fin), - and_(Assiduite.date_fin >= deb, Assiduite.date_fin <= fin), - ), + fin >= Assiduite.date_debut, + deb <= Assiduite.date_fin, ) abs_etudids = set(assi.etudid for assi in assiduites) diff --git a/app/views/scolar.py b/app/views/scolar.py index ee36c79e3b..3d8b6f296a 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -933,6 +933,7 @@ def partition_editor(formsemestre_id: int, edit_partition=False): formsemestre=formsemestre, read_only=not formsemestre.can_change_groups(), edit_partition=edit_partition, + scu=scu, ), html_sco_header.sco_footer(), ] From ee2e2fe5a0df68cdc0d76c3efc7847a90120c46b Mon Sep 17 00:00:00 2001 From: iziram Date: Thu, 7 Sep 2023 09:17:25 +0200 Subject: [PATCH 20/97] Assiduites : ajout gestion erreur eval check abs --- app/views/assiduites.py | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/app/views/assiduites.py b/app/views/assiduites.py index 958a3ae5c4..b037b10ac6 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -1043,7 +1043,6 @@ def signal_evaluation_abs(etudid: int = None, evaluation_id: int = None): delta: datetime.timedelta = evaluation.date_fin - evaluation.date_debut if delta > datetime.timedelta(days=1): # rediriger vers page saisie - flash("Redirection pour saisie abs") return redirect( url_for( "assiduites.signal_assiduites_etud", @@ -1059,13 +1058,31 @@ def signal_evaluation_abs(etudid: int = None, evaluation_id: int = None): # créer l'assiduité - assiduite_unique: Assiduite = Assiduite.create_assiduite( - etud=etud, - date_debut=scu.localize_datetime(evaluation.date_debut), - date_fin=scu.localize_datetime(evaluation.date_fin), - etat=scu.EtatAssiduite.ABSENT, - moduleimpl=evaluation.moduleimpl, - ) + try: + assiduite_unique: Assiduite = Assiduite.create_assiduite( + etud=etud, + date_debut=scu.localize_datetime(evaluation.date_debut), + date_fin=scu.localize_datetime(evaluation.date_fin), + etat=scu.EtatAssiduite.ABSENT, + moduleimpl=evaluation.moduleimpl, + ) + except ScoValueError as see: + 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( + "assiduites.signal_assiduites_etud", + etudid=etudid, + evaluation_id=evaluation.id, + date_deb=evaluation.date_debut.strftime("%Y-%m-%dT%H:%M:%S"), + date_fin=evaluation.date_fin.strftime("%Y-%m-%dT%H:%M:%S"), + moduleimpl_id=evaluation.moduleimpl.id, + saisie_eval="true", + scodoc_dept=g.scodoc_dept, + duplication="oui", + ) + raise ScoValueError(msg, dest) db.session.add(assiduite_unique) db.session.commit() From 7fad9e0a71930ea4f438889327c8be3c0d49b1c3 Mon Sep 17 00:00:00 2001 From: iziram Date: Thu, 7 Sep 2023 10:34:51 +0200 Subject: [PATCH 21/97] =?UTF-8?q?Assiduites=20:=20remise=20en=20=C3=A9tat?= =?UTF-8?q?=20test=20unitaire=20migration=20#696?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/test_assiduites.py | 130 ++++++++++++++++++++++++++++------ 1 file changed, 110 insertions(+), 20 deletions(-) diff --git a/tests/unit/test_assiduites.py b/tests/unit/test_assiduites.py index 50f92c649e..0ed1bb506c 100644 --- a/tests/unit/test_assiduites.py +++ b/tests/unit/test_assiduites.py @@ -11,14 +11,23 @@ import pytest import app.scodoc.sco_assiduites as scass import app.scodoc.sco_utils as scu -from app import db -from app.models import Assiduite, FormSemestre, Identite, Justificatif, ModuleImpl +from app import db, log +from app.models import ( + Assiduite, + FormSemestre, + Identite, + Justificatif, + ModuleImpl, + Absence, +) from app.models.assiduites import compute_assiduites_justified from app.scodoc.sco_exceptions import ScoValueError from tests.unit import sco_fake_gen from tools import downgrade_module, migrate_abs_to_assiduites +import datetime as dt + class BiInt(int, scu.BiDirectionalEnum): """Classe pour tester la classe BiDirectionalEnum""" @@ -133,7 +142,7 @@ def test_general(test_client): etud_faux_dict = g_fake.create_etud(code_nip=None, prenom="etudfaux") etud_faux = Identite.query.filter_by(id=etud_faux_dict["id"]).first() - # verif_migration_abs_assiduites() // Test à revoir TODO-ASSIDUITE (issue #696) + verif_migration_abs_assiduites() ajouter_assiduites(etuds, moduleimpls, etud_faux) justificatifs: list[Justificatif] = ajouter_justificatifs(etuds[0]) @@ -307,14 +316,13 @@ def verif_migration_abs_assiduites(): False, ), # 3 assi 22-23-24/02/2023 08h > 13h (3dj) JUSTI(ext) ]: - continue - # sco_abs_views.doSignaleAbsence( # TODO-ASSIDUITE - # datedebut=debut, - # datefin=fin, - # demijournee=demijournee, - # etudid=etudid, - # estjust=justifiee, - # ) + _create_abs( + date_debut=debut, + date_fin=fin, + demijournee=demijournee, + etudid=etudid, + estjust=justifiee, + ) # --- Justification de certaines absences @@ -330,13 +338,14 @@ def verif_migration_abs_assiduites(): 2, ), ]: - continue - # sco_abs_views.doJustifAbsence( - # datedebut=debut, - # datefin=fin, - # demijournee=demijournee, - # etudid=etudid, - # ) + _create_abs( + date_debut=debut, + date_fin=fin, + demijournee=demijournee, + etudid=etudid, + estjust=True, + estabs=False, + ) migrate_abs_to_assiduites() @@ -442,8 +451,6 @@ def verif_migration_abs_assiduites(): _get_assi("2023-02-24T08:00", "2023-02-24T13:00", True) is not None ), "Migration : Abs n°27 mal migrée" - essais_cache(etudid) - downgrade_module(assiduites=True, justificatifs=True) @@ -1025,3 +1032,86 @@ def verifier_comptage_et_filtrage_assiduites( assert ( scass.filter_by_date(etu2.assiduites, Assiduite, date_fin=date).count() == 7 ), "Filtrage 'Date fin' mauvais 5" + + +def _create_abs( + date_debut, date_fin, demijournee, estjust=False, etudid=False, estabs=True +): + etud = Identite.from_request(etudid) + deb: dt.date = dt.datetime.strptime(date_debut, "%d/%m/%Y").date() + fin: dt.date = dt.datetime.strptime(date_fin, "%d/%m/%Y").date() + abs_list: list[Absence] = [] + while deb < fin: + if deb.weekday() in [5, 6]: + deb += dt.timedelta(days=1) + continue + if demijournee == 2: + abs_list.append( + Absence( + etudid=etud.id, + jour=deb.isoformat(), + estabs=estabs, + estjust=estjust, + matin=True, + ) + ) + abs_list.append( + Absence( + etudid=etud.id, + jour=deb.isoformat(), + estabs=estabs, + estjust=estjust, + matin=False, + ) + ) + else: + abs_list.append( + Absence( + etudid=etud.id, + jour=deb.isoformat(), + estabs=estabs, + estjust=estjust, + matin=demijournee, + ) + ) + log( + f"create_abs [{etudid}, {deb.isoformat()}, {estabs}, {estjust}, {['aprem', 'matin', 'journee'][demijournee]}]" + ) + deb += dt.timedelta(days=1) + + if deb == fin and deb.weekday() not in [5, 6]: + if demijournee == 2: + abs_list.append( + Absence( + etudid=etud.id, + jour=deb.isoformat(), + estabs=estabs, + estjust=estjust, + matin=True, + ) + ) + abs_list.append( + Absence( + etudid=etud.id, + jour=deb.isoformat(), + estabs=estabs, + estjust=estjust, + matin=False, + ) + ) + else: + abs_list.append( + Absence( + etudid=etud.id, + jour=deb.isoformat(), + estabs=estabs, + estjust=estjust, + matin=demijournee, + ) + ) + log( + f"create_abs [{etudid}, {deb.isoformat()}, {estabs}, {estjust}, {['aprem', 'matin', 'journee'][demijournee]}]" + ) + + db.session.add_all(abs_list) + db.session.commit() From e4b603aa9e3b24cd810e56c102d4734ad062a4b0 Mon Sep 17 00:00:00 2001 From: "pascal.bouron" Date: Thu, 7 Sep 2023 15:35:45 +0200 Subject: [PATCH 22/97] Actualiser app/scodoc/sco_permissions.py typo --- app/scodoc/sco_permissions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scodoc/sco_permissions.py b/app/scodoc/sco_permissions.py index 11497367a5..dd402548ab 100644 --- a/app/scodoc/sco_permissions.py +++ b/app/scodoc/sco_permissions.py @@ -27,7 +27,7 @@ _SCO_PERMISSIONS = ( (1 << 13, "ScoAbsChange", "Saisir des absences"), (1 << 14, "ScoAbsAddBillet", "Saisir des billets d'absences"), # changer adresse/photo ou pour envoyer bulletins par mail ou pour debouche - (1 << 15, "ScoEtudChangeAdr", "Changer les addresses d'étudiants"), + (1 << 15, "ScoEtudChangeAdr", "Changer les adresses d'étudiants"), ( 1 << 16, "APIEditGroups", From 32b57839c5ca91a0c25fa4bfbef9a79c79f92ee1 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 7 Sep 2023 23:09:39 +0200 Subject: [PATCH 23/97] =?UTF-8?q?Fix:=20gestion=20des=20archives=20(confus?= =?UTF-8?q?ion=20de=20d=C3=A9partements)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/justificatifs.py | 30 ++--- app/scodoc/sco_archives.py | 141 +++++++++++++++-------- app/scodoc/sco_archives_etud.py | 54 +++++---- app/scodoc/sco_archives_justificatifs.py | 84 +++++++------- app/scodoc/sco_etape_apogee.py | 18 +-- app/scodoc/sco_formsemestre_status.py | 2 +- app/scodoc/sco_page_etud.py | 2 +- app/views/notes.py | 2 +- sco_version.py | 2 +- 9 files changed, 193 insertions(+), 142 deletions(-) diff --git a/app/api/justificatifs.py b/app/api/justificatifs.py index 0fd59b975a..5f66fa6030 100644 --- a/app/api/justificatifs.py +++ b/app/api/justificatifs.py @@ -8,8 +8,9 @@ from datetime import datetime from flask_json import as_json -from flask import g, jsonify, request +from flask import g, request from flask_login import login_required, current_user +from flask_sqlalchemy.query import Query import app.scodoc.sco_assiduites as scass import app.scodoc.sco_utils as scu @@ -26,7 +27,6 @@ from app.scodoc.sco_archives_justificatifs import JustificatifArchiver from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_permissions import Permission from app.scodoc.sco_utils import json_error -from flask_sqlalchemy.query import Query # Partie Modèle @@ -436,7 +436,7 @@ def _delete_singular(justif_id: int, database): if archive_name is not None: archiver: JustificatifArchiver = JustificatifArchiver() try: - archiver.delete_justificatif(justificatif_unique.etudid, archive_name) + archiver.delete_justificatif(justificatif_unique.etudiant, archive_name) except ValueError: pass @@ -481,7 +481,7 @@ def justif_import(justif_id: int = None): try: fname: str archive_name, fname = archiver.save_justificatif( - etudid=justificatif_unique.etudid, + justificatif_unique.etudiant, filename=file.filename, data=file.stream.read(), archive_name=archive_name, @@ -512,7 +512,7 @@ def justif_export(justif_id: int = None, filename: str = None): if g.scodoc_dept: query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id) - justificatif_unique: Justificaitf = query.first_or_404() + justificatif_unique: Justificatif = query.first_or_404() archive_name: str = justificatif_unique.fichier if archive_name is None: @@ -522,7 +522,7 @@ def justif_export(justif_id: int = None, filename: str = None): try: return archiver.get_justificatif_file( - archive_name, justificatif_unique.etudid, filename + archive_name, justificatif_unique.etudiant, filename ) except ScoValueError as err: return json_error(404, err.args[0]) @@ -564,10 +564,10 @@ def justif_remove(justif_id: int = None): if remove is None or remove not in ("all", "list"): return json_error(404, "param 'remove': Valeur invalide") archiver: JustificatifArchiver = JustificatifArchiver() - etudid: int = justificatif_unique.etudid + etud = justificatif_unique.etudiant try: if remove == "all": - archiver.delete_justificatif(etudid=etudid, archive_name=archive_name) + archiver.delete_justificatif(etud, archive_name=archive_name) justificatif_unique.fichier = None db.session.add(justificatif_unique) db.session.commit() @@ -575,13 +575,13 @@ def justif_remove(justif_id: int = None): else: for fname in data.get("filenames", []): archiver.delete_justificatif( - etudid=etudid, + etud, archive_name=archive_name, filename=fname, ) - if len(archiver.list_justificatifs(archive_name, etudid)) == 0: - archiver.delete_justificatif(etudid, archive_name) + if len(archiver.list_justificatifs(archive_name, etud)) == 0: + archiver.delete_justificatif(etud, archive_name) justificatif_unique.fichier = None db.session.add(justificatif_unique) db.session.commit() @@ -616,16 +616,16 @@ def justif_list(justif_id: int = None): archiver: JustificatifArchiver = JustificatifArchiver() if archive_name is not None: filenames = archiver.list_justificatifs( - archive_name, justificatif_unique.etudid + archive_name, justificatif_unique.etudiant ) retour = {"total": len(filenames), "filenames": []} - for fi in filenames: - if int(fi[1]) == current_user.id or current_user.has_permission( + for filename in filenames: + if int(filename[1]) == current_user.id or current_user.has_permission( Permission.ScoJustifView ): - retour["filenames"].append(fi[0]) + retour["filenames"].append(filename[0]) return retour diff --git a/app/scodoc/sco_archives.py b/app/scodoc/sco_archives.py index c6a646ee50..51bba4b208 100644 --- a/app/scodoc/sco_archives.py +++ b/app/scodoc/sco_archives.py @@ -81,7 +81,7 @@ from app.scodoc import sco_pv_pdf from app.scodoc.sco_exceptions import ScoValueError -class BaseArchiver(object): +class BaseArchiver: def __init__(self, archive_type=""): self.archive_type = archive_type self.initialized = False @@ -92,14 +92,17 @@ class BaseArchiver(object): "set dept" self.dept_id = dept_id - def initialize(self): + def initialize(self, dept_id: int = None): + """Fixe le département et initialise les répertoires au besoin.""" + # Set departement (à chaque fois car peut changer d'une utilisation à l'autre) + self.dept_id = getattr(g, "scodoc_dept_id") if dept_id is None else dept_id if self.initialized: return dirs = [Config.SCODOC_VAR_DIR, "archives"] if self.archive_type: dirs.append(self.archive_type) - self.root = os.path.join(*dirs) + self.root = os.path.join(*dirs) # /opt/scodoc-data/archives/ log("initialized archiver, path=" + self.root) path = dirs[0] for directory in dirs[1:]: @@ -112,15 +115,13 @@ class BaseArchiver(object): finally: scu.GSL.release() self.initialized = True - if self.dept_id is None: - self.dept_id = getattr(g, "scodoc_dept_id") - def get_obj_dir(self, oid: int): + def get_obj_dir(self, oid: int, dept_id: int = None): """ :return: path to directory of archives for this object (eg formsemestre_id or etudid). If directory does not yet exist, create it. """ - self.initialize() + self.initialize(dept_id) dept_dir = os.path.join(self.root, str(self.dept_id)) try: scu.GSL.acquire() @@ -141,21 +142,21 @@ class BaseArchiver(object): scu.GSL.release() return obj_dir - def list_oids(self): + def list_oids(self, dept_id: int = None): """ :return: list of archive oids """ - self.initialize() + self.initialize(dept_id) base = os.path.join(self.root, str(self.dept_id)) + os.path.sep dirs = glob.glob(base + "*") return [os.path.split(x)[1] for x in dirs] - def list_obj_archives(self, oid: int): + def list_obj_archives(self, oid: int, dept_id: int = None): """Returns :return: list of archive identifiers for this object (paths to non empty dirs) """ - self.initialize() - base = self.get_obj_dir(oid) + os.path.sep + self.initialize(dept_id) + base = self.get_obj_dir(oid, dept_id=dept_id) + os.path.sep dirs = glob.glob( base + "[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]" @@ -165,9 +166,9 @@ class BaseArchiver(object): dirs.sort() return dirs - def delete_archive(self, archive_id: str): + def delete_archive(self, archive_id: str, dept_id: int = None): """Delete (forever) this archive""" - self.initialize() + self.initialize(dept_id) try: scu.GSL.acquire() shutil.rmtree(archive_id, ignore_errors=True) @@ -180,9 +181,9 @@ class BaseArchiver(object): *[int(x) for x in os.path.split(archive_id)[1].split("-")] ) - def list_archive(self, archive_id: str) -> str: + def list_archive(self, archive_id: str, dept_id: int = None) -> str: """Return list of filenames (without path) in archive""" - self.initialize() + self.initialize(dept_id) try: scu.GSL.acquire() files = os.listdir(archive_id) @@ -201,12 +202,12 @@ class BaseArchiver(object): "^[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}$", archive_name ) - def get_id_from_name(self, oid, archive_name: str): + def get_id_from_name(self, oid, archive_name: str, dept_id: int = None): """returns archive id (check that name is valid)""" - self.initialize() + self.initialize(dept_id) if not self.is_valid_archive_name(archive_name): raise ScoValueError(f"Archive {archive_name} introuvable") - archive_id = os.path.join(self.get_obj_dir(oid), archive_name) + archive_id = os.path.join(self.get_obj_dir(oid, dept_id=dept_id), archive_name) if not os.path.isdir(archive_id): log( f"invalid archive name: {archive_name}, oid={oid}, archive_id={archive_id}" @@ -214,9 +215,9 @@ class BaseArchiver(object): raise ScoValueError(f"Archive {archive_name} introuvable") return archive_id - def get_archive_description(self, archive_id: str) -> str: + def get_archive_description(self, archive_id: str, dept_id: int = None) -> str: """Return description of archive""" - self.initialize() + self.initialize(dept_id) filename = os.path.join(archive_id, "_description.txt") try: with open(filename, encoding=scu.SCO_ENCODING) as f: @@ -229,11 +230,11 @@ class BaseArchiver(object): return descr - def create_obj_archive(self, oid: int, description: str): + def create_obj_archive(self, oid: int, description: str, dept_id: int = None): """Creates a new archive for this object and returns its id.""" # id suffixé par YYYY-MM-DD-hh-mm-ss archive_id = ( - self.get_obj_dir(oid) + self.get_obj_dir(oid, dept_id=dept_id) + os.path.sep + "-".join([f"{x:02d}" for x in time.localtime()[:6]]) ) @@ -248,7 +249,13 @@ class BaseArchiver(object): self.store(archive_id, "_description.txt", description) return archive_id - def store(self, archive_id: str, filename: str, data: Union[str, bytes]): + def store( + self, + archive_id: str, + filename: str, + data: Union[str, bytes], + dept_id: int = None, + ): """Store data in archive, under given filename. Filename may be modified (sanitized): return used filename The file is created or replaced. @@ -256,7 +263,7 @@ class BaseArchiver(object): """ if isinstance(data, str): data = data.encode(scu.SCO_ENCODING) - self.initialize() + self.initialize(dept_id) filename = scu.sanitize_filename(filename) log(f"storing {filename} ({len(data)} bytes) in {archive_id}") try: @@ -268,9 +275,9 @@ class BaseArchiver(object): scu.GSL.release() return filename - def get(self, archive_id: str, filename: str): + def get(self, archive_id: str, filename: str, dept_id: int = None): """Retreive data""" - self.initialize() + self.initialize(dept_id) if not scu.is_valid_filename(filename): log(f"""Archiver.get: invalid filename '{filename}'""") raise ScoValueError("archive introuvable (déjà supprimée ?)") @@ -280,11 +287,11 @@ class BaseArchiver(object): data = f.read() return data - def get_archived_file(self, oid, archive_name, filename): + def get_archived_file(self, oid, archive_name, filename, dept_id: int = None): """Recupère les donnees du fichier indiqué et envoie au client. Returns: Response """ - archive_id = self.get_id_from_name(oid, archive_name) + archive_id = self.get_id_from_name(oid, archive_name, dept_id=dept_id) data = self.get(archive_id, filename) mime = mimetypes.guess_type(filename)[0] if mime is None: @@ -298,7 +305,7 @@ class SemsArchiver(BaseArchiver): BaseArchiver.__init__(self, archive_type="") -PVArchive = SemsArchiver() +PV_ARCHIVER = SemsArchiver() # ---------------------------------------------------------------------------- @@ -332,8 +339,10 @@ def do_formsemestre_archive( formsemestre = FormSemestre.get_formsemestre(formsemestre_id) res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) sem_archive_id = formsemestre_id - archive_id = PVArchive.create_obj_archive(sem_archive_id, description) - date = PVArchive.get_archive_date(archive_id).strftime("%d/%m/%Y à %H:%M") + archive_id = PV_ARCHIVER.create_obj_archive( + sem_archive_id, description, formsemestre.dept_id + ) + date = PV_ARCHIVER.get_archive_date(archive_id).strftime("%d/%m/%Y à %H:%M") if not group_ids: # tous les inscrits du semestre @@ -347,7 +356,12 @@ def do_formsemestre_archive( # Tableau recap notes en XLS (pour tous les etudiants, n'utilise pas les groupes) data, _ = gen_formsemestre_recapcomplet_excel(res, include_evaluations=True) if data: - PVArchive.store(archive_id, "Tableau_moyennes" + scu.XLSX_SUFFIX, data) + PV_ARCHIVER.store( + archive_id, + "Tableau_moyennes" + scu.XLSX_SUFFIX, + data, + dept_id=formsemestre.dept_id, + ) # Tableau recap notes en HTML (pour tous les etudiants, n'utilise pas les groupes) table_html, _, _ = gen_formsemestre_recapcomplet_html_table( formsemestre, res, include_evaluations=True @@ -367,13 +381,17 @@ def do_formsemestre_archive( html_sco_header.sco_footer(), ] ) - PVArchive.store(archive_id, "Tableau_moyennes.html", data) + PV_ARCHIVER.store( + archive_id, "Tableau_moyennes.html", data, dept_id=formsemestre.dept_id + ) # Bulletins en JSON data = gen_formsemestre_recapcomplet_json(formsemestre_id, xml_with_decisions=True) data_js = json.dumps(data, indent=1, cls=ScoDocJSONEncoder) if data: - PVArchive.store(archive_id, "Bulletins.json", data_js) + PV_ARCHIVER.store( + archive_id, "Bulletins.json", data_js, dept_id=formsemestre.dept_id + ) # Décisions de jury, en XLS if formsemestre.formation.is_apc(): response = jury_but_pv.pvjury_page_but(formsemestre_id, fmt="xls") @@ -383,17 +401,23 @@ def do_formsemestre_archive( formsemestre_id, format="xls", publish=False ) if data: - PVArchive.store( + PV_ARCHIVER.store( archive_id, "Decisions_Jury" + scu.XLSX_SUFFIX, data, + dept_id=formsemestre.dept_id, ) # Classeur bulletins (PDF) data, _ = sco_bulletins_pdf.get_formsemestre_bulletins_pdf( formsemestre_id, version=bul_version ) if data: - PVArchive.store(archive_id, "Bulletins.pdf", data) + PV_ARCHIVER.store( + archive_id, + "Bulletins.pdf", + data, + dept_id=formsemestre.dept_id, + ) # Lettres individuelles (PDF): data = sco_pv_lettres_inviduelles.pdf_lettres_individuelles( formsemestre_id, @@ -403,7 +427,12 @@ def do_formsemestre_archive( signature=signature, ) if data: - PVArchive.store(archive_id, f"CourriersDecisions{groups_filename}.pdf", data) + PV_ARCHIVER.store( + archive_id, + f"CourriersDecisions{groups_filename}.pdf", + data, + dept_id=formsemestre.dept_id, + ) # PV de jury (PDF): data = sco_pv_pdf.pvjury_pdf( @@ -419,7 +448,12 @@ def do_formsemestre_archive( anonymous=anonymous, ) if data: - PVArchive.store(archive_id, f"PV_Jury{groups_filename}.pdf", data) + PV_ARCHIVER.store( + archive_id, + f"PV_Jury{groups_filename}.pdf", + data, + dept_id=formsemestre.dept_id, + ) def formsemestre_archive(formsemestre_id, group_ids: list[int] = None): @@ -558,14 +592,21 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement. def formsemestre_list_archives(formsemestre_id): """Page listing archives""" + formsemestre = FormSemestre.get_formsemestre(formsemestre_id) sem_archive_id = formsemestre_id L = [] - for archive_id in PVArchive.list_obj_archives(sem_archive_id): + for archive_id in PV_ARCHIVER.list_obj_archives( + sem_archive_id, dept_id=formsemestre.dept_id + ): a = { "archive_id": archive_id, - "description": PVArchive.get_archive_description(archive_id), - "date": PVArchive.get_archive_date(archive_id), - "content": PVArchive.list_archive(archive_id), + "description": PV_ARCHIVER.get_archive_description( + archive_id, dept_id=formsemestre.dept_id + ), + "date": PV_ARCHIVER.get_archive_date(archive_id), + "content": PV_ARCHIVER.list_archive( + archive_id, dept_id=formsemestre.dept_id + ), } L.append(a) @@ -575,7 +616,7 @@ def formsemestre_list_archives(formsemestre_id): else: H.append("
      ") for a in L: - archive_name = PVArchive.get_archive_name(a["archive_id"]) + archive_name = PV_ARCHIVER.get_archive_name(a["archive_id"]) H.append( '
    • %s : %s (supprimer)
        ' % ( @@ -602,7 +643,9 @@ def formsemestre_get_archived_file(formsemestre_id, archive_name, filename): """Send file to client.""" formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) sem_archive_id = formsemestre.id - return PVArchive.get_archived_file(sem_archive_id, archive_name, filename) + return PV_ARCHIVER.get_archived_file( + sem_archive_id, archive_name, filename, dept_id=formsemestre.dept_id + ) def formsemestre_delete_archive(formsemestre_id, archive_name, dialog_confirmed=False): @@ -617,7 +660,9 @@ def formsemestre_delete_archive(formsemestre_id, archive_name, dialog_confirmed= ) ) sem_archive_id = formsemestre_id - archive_id = PVArchive.get_id_from_name(sem_archive_id, archive_name) + archive_id = PV_ARCHIVER.get_id_from_name( + sem_archive_id, archive_name, dept_id=formsemestre.dept_id + ) dest_url = url_for( "notes.formsemestre_list_archives", @@ -628,7 +673,7 @@ def formsemestre_delete_archive(formsemestre_id, archive_name, dialog_confirmed= if not dialog_confirmed: return scu.confirm_dialog( f"""

        Confirmer la suppression de l'archive du { - PVArchive.get_archive_date(archive_id).strftime("%d/%m/%Y %H:%M") + PV_ARCHIVER.get_archive_date(archive_id).strftime("%d/%m/%Y %H:%M") } ?

        La suppression sera définitive.

        """, @@ -640,6 +685,6 @@ def formsemestre_delete_archive(formsemestre_id, archive_name, dialog_confirmed= }, ) - PVArchive.delete_archive(archive_id) + PV_ARCHIVER.delete_archive(archive_id, dept_id=formsemestre.dept_id) flash("Archive supprimée") return flask.redirect(dest_url) diff --git a/app/scodoc/sco_archives_etud.py b/app/scodoc/sco_archives_etud.py index b538d0f5e7..4bc76a10f8 100644 --- a/app/scodoc/sco_archives_etud.py +++ b/app/scodoc/sco_archives_etud.py @@ -52,7 +52,8 @@ class EtudsArchiver(sco_archives.BaseArchiver): sco_archives.BaseArchiver.__init__(self, archive_type="docetuds") -EtudsArchive = EtudsArchiver() +# Global au processus, attention ! +ETUDS_ARCHIVER = EtudsArchiver() def can_edit_etud_archive(authuser): @@ -60,21 +61,21 @@ def can_edit_etud_archive(authuser): return authuser.has_permission(Permission.ScoEtudAddAnnotations) -def etud_list_archives_html(etudid): +def etud_list_archives_html(etud: Identite): """HTML snippet listing archives""" can_edit = can_edit_etud_archive(current_user) - etuds = sco_etud.get_etud_info(etudid=etudid) - if not etuds: - raise ScoValueError("étudiant inexistant") - etud = etuds[0] - etud_archive_id = etudid + etud_archive_id = etud.id L = [] - for archive_id in EtudsArchive.list_obj_archives(etud_archive_id): + for archive_id in ETUDS_ARCHIVER.list_obj_archives( + etud_archive_id, dept_id=etud.dept_id + ): a = { "archive_id": archive_id, - "description": EtudsArchive.get_archive_description(archive_id), - "date": EtudsArchive.get_archive_date(archive_id), - "content": EtudsArchive.list_archive(archive_id), + "description": ETUDS_ARCHIVER.get_archive_description( + archive_id, dept_id=etud.dept_id + ), + "date": ETUDS_ARCHIVER.get_archive_date(archive_id), + "content": ETUDS_ARCHIVER.list_archive(archive_id, dept_id=etud.dept_id), } L.append(a) delete_icon = scu.icontag( @@ -85,7 +86,7 @@ def etud_list_archives_html(etudid): ) H = ['
          '] for a in L: - archive_name = EtudsArchive.get_archive_name(a["archive_id"]) + archive_name = ETUDS_ARCHIVER.get_archive_name(a["archive_id"]) H.append( """
        • %s""" % (a["date"].strftime("%d/%m/%Y %H:%M"), a["description"]) @@ -93,14 +94,14 @@ def etud_list_archives_html(etudid): for filename in a["content"]: H.append( """%s""" - % (etudid, archive_name, filename, filename) + % (etud.id, archive_name, filename, filename) ) if not a["content"]: H.append("aucun fichier !") if can_edit: H.append( '%s' - % (etudid, archive_name, delete_icon) + % (etud.id, archive_name, delete_icon) ) else: H.append('' + delete_disabled_icon + "") @@ -108,7 +109,7 @@ def etud_list_archives_html(etudid): if can_edit: H.append( '
        • ajouter un fichier
        • ' - % etudid + % etud.id ) H.append("
        ") return "".join(H) @@ -121,12 +122,13 @@ def add_archives_info_to_etud_list(etuds): for etud in etuds: l = [] etud_archive_id = etud["etudid"] - for archive_id in EtudsArchive.list_obj_archives(etud_archive_id): + # Here, ETUDS_ARCHIVER will use g.dept_id + for archive_id in ETUDS_ARCHIVER.list_obj_archives(etud_archive_id): l.append( "%s (%s)" % ( - EtudsArchive.get_archive_description(archive_id), - EtudsArchive.list_archive(archive_id)[0], + ETUDS_ARCHIVER.get_archive_description(archive_id), + ETUDS_ARCHIVER.list_archive(archive_id)[0], ) ) etud["etudarchive"] = ", ".join(l) @@ -197,8 +199,8 @@ def _store_etud_file_to_new_archive( filesize = len(data) if filesize < 10 or filesize > scu.CONFIG.ETUD_MAX_FILE_SIZE: return False, f"Fichier archive '{filename}' de taille invalide ! ({filesize})" - archive_id = EtudsArchive.create_obj_archive(etud_archive_id, description) - EtudsArchive.store(archive_id, filename, data) + archive_id = ETUDS_ARCHIVER.create_obj_archive(etud_archive_id, description) + ETUDS_ARCHIVER.store(archive_id, filename, data) return True, "ok" @@ -212,14 +214,16 @@ def etud_delete_archive(etudid, archive_name, dialog_confirmed=False): raise ScoValueError("étudiant inexistant") etud = etuds[0] etud_archive_id = etud["etudid"] - archive_id = EtudsArchive.get_id_from_name(etud_archive_id, archive_name) + archive_id = ETUDS_ARCHIVER.get_id_from_name( + etud_archive_id, archive_name, dept_id=etud["dept_id"] + ) if not dialog_confirmed: return scu.confirm_dialog( """

        Confirmer la suppression des fichiers ?

        Fichier associé le %s à l'étudiant %s

        La suppression sera définitive.

        """ % ( - EtudsArchive.get_archive_date(archive_id).strftime("%d/%m/%Y %H:%M"), + ETUDS_ARCHIVER.get_archive_date(archive_id).strftime("%d/%m/%Y %H:%M"), etud["nomprenom"], ), dest_url="", @@ -232,7 +236,7 @@ def etud_delete_archive(etudid, archive_name, dialog_confirmed=False): parameters={"etudid": etudid, "archive_name": archive_name}, ) - EtudsArchive.delete_archive(archive_id) + ETUDS_ARCHIVER.delete_archive(archive_id, dept_id=etud["dept_id"]) flash("Archive supprimée") return flask.redirect( url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid) @@ -246,7 +250,9 @@ def etud_get_archived_file(etudid, archive_name, filename): raise ScoValueError("étudiant inexistant") etud = etuds[0] etud_archive_id = etud["etudid"] - return EtudsArchive.get_archived_file(etud_archive_id, archive_name, filename) + return ETUDS_ARCHIVER.get_archived_file( + etud_archive_id, archive_name, filename, dept_id=etud["dept_id"] + ) # --- Upload d'un ensemble de fichiers (pour un groupe d'étudiants) diff --git a/app/scodoc/sco_archives_justificatifs.py b/app/scodoc/sco_archives_justificatifs.py index 0030d203a6..dc99c33915 100644 --- a/app/scodoc/sco_archives_justificatifs.py +++ b/app/scodoc/sco_archives_justificatifs.py @@ -102,7 +102,7 @@ class JustificatifArchiver(BaseArchiver): def save_justificatif( self, - etudid: int, + etud: Identite, filename: str, data: bytes or str, archive_name: str = None, @@ -113,17 +113,18 @@ class JustificatifArchiver(BaseArchiver): Ajoute un fichier dans une archive "justificatif" pour l'etudid donné Retourne l'archive_name utilisé """ - self._set_dept(etudid) if archive_name is None: archive_id: str = self.create_obj_archive( - oid=etudid, description=description + oid=etud.id, description=description, dept_id=etud.dept_id ) else: - archive_id: str = self.get_id_from_name(etudid, archive_name) + archive_id: str = self.get_id_from_name( + etud.id, archive_name, dept_id=etud.dept_id + ) - fname: str = self.store(archive_id, filename, data) + fname: str = self.store(archive_id, filename, data, dept_id=etud.dept_id) - trace = Trace(self.get_obj_dir(etudid)) + trace = Trace(self.get_obj_dir(etud.id, dept_id=etud.dept_id)) trace.set_trace(fname, mode="entry") if user_id is not None: trace.set_trace(fname, mode="user_id", current_user=user_id) @@ -132,7 +133,7 @@ class JustificatifArchiver(BaseArchiver): def delete_justificatif( self, - etudid: int, + etud: Identite, archive_name: str, filename: str = None, has_trace: bool = True, @@ -140,92 +141,91 @@ class JustificatifArchiver(BaseArchiver): """ Supprime une archive ou un fichier particulier de l'archivage de l'étudiant donné - Si trace == True : sauvegarde le nom du/des fichier(s) supprimé(s) dans la trace de l'étudiant + Si trace == True : sauvegarde le nom du/des fichier(s) supprimé(s) + dans la trace de l'étudiant """ - self._set_dept(etudid) - if str(etudid) not in self.list_oids(): - raise ValueError(f"Aucune archive pour etudid[{etudid}]") + if str(etud.id) not in self.list_oids(): + raise ValueError(f"Aucune archive pour etudid[{etud.id}]") - archive_id = self.get_id_from_name(etudid, archive_name) + archive_id = self.get_id_from_name(etud.id, archive_name, dept_id=etud.dept_id) if filename is not None: - if filename not in self.list_archive(archive_id): + if filename not in self.list_archive(archive_id, dept_id=etud.dept_id): raise ValueError( - f"filename {filename} inconnu dans l'archive archive_id[{archive_id}] -> etudid[{etudid}]" + f"""filename {filename} inconnu dans l'archive archive_id[{ + archive_id}] -> etudid[{etud.id}]""" ) - path: str = os.path.join(self.get_obj_dir(etudid), archive_id, filename) + path: str = os.path.join( + self.get_obj_dir(etud.id, dept_id=etud.dept_id), archive_id, filename + ) if os.path.isfile(path): if has_trace: - trace = Trace(self.get_obj_dir(etudid)) + trace = Trace(self.get_obj_dir(etud.id, dept_id=etud.dept_id)) trace.set_trace(filename, mode="delete") os.remove(path) else: if has_trace: - trace = Trace(self.get_obj_dir(etudid)) - trace.set_trace(*self.list_archive(archive_id), mode="delete") + trace = Trace(self.get_obj_dir(etud.id, dept_id=etud.dept_id)) + trace.set_trace( + *self.list_archive(archive_id, dept_id=etud.dept_id), mode="delete" + ) self.delete_archive( os.path.join( - self.get_obj_dir(etudid), + self.get_obj_dir(etud.id, dept_id=etud.dept_id), archive_id, ) ) def list_justificatifs( - self, archive_name: str, etudid: int + self, archive_name: str, etud: Identite ) -> list[tuple[str, int]]: """ Retourne la liste des noms de fichiers dans l'archive donnée """ - self._set_dept(etudid) filenames: list[str] = [] - archive_id = self.get_id_from_name(etudid, archive_name) + archive_id = self.get_id_from_name(etud.id, archive_name, dept_id=etud.dept_id) - filenames = self.list_archive(archive_id) - trace: Trace = Trace(self.get_obj_dir(etudid)) + filenames = self.list_archive(archive_id, dept_id=etud.dept_id) + trace: Trace = Trace(self.get_obj_dir(etud.id, dept_id=etud.dept_id)) traced = trace.get_trace(filenames) retour = [(key, value[2]) for key, value in traced.items()] return retour - def get_justificatif_file(self, archive_name: str, etudid: int, filename: str): + def get_justificatif_file(self, archive_name: str, etud: Identite, filename: str): """ Retourne une réponse de téléchargement de fichier si le fichier existe """ - self._set_dept(etudid) - archive_id: str = self.get_id_from_name(etudid, archive_name) - if filename in self.list_archive(archive_id): - return self.get_archived_file(etudid, archive_name, filename) + archive_id: str = self.get_id_from_name( + etud.id, archive_name, dept_id=etud.dept_id + ) + if filename in self.list_archive(archive_id, dept_id=etud.dept_id): + return self.get_archived_file( + etud.id, archive_name, filename, dept_id=etud.dept_id + ) raise ScoValueError( f"Fichier {filename} introuvable dans l'archive {archive_name}" ) - def _set_dept(self, etudid: int): - """ - Mets à jour le dept_id de l'archiver en fonction du département de l'étudiant - """ - etud: Identite = Identite.query.filter_by(id=etudid).first() - self.set_dept_id(etud.dept_id) - def remove_dept_archive(self, dept_id: int = None): """ Supprime toutes les archives d'un département (ou de tous les départements) ⚠ Supprime aussi les fichiers de trace ⚠ """ - self.set_dept_id(1) - self.initialize() - + # juste pour récupérer .root, dept_id n'a pas d'importance + self.initialize(dept_id=1) if dept_id is None: rmtree(self.root, ignore_errors=True) else: rmtree(os.path.join(self.root, str(dept_id)), ignore_errors=True) - def get_trace( - self, etudid: int, *fnames: str + def get_trace( # XXX inutilisée ? + self, etud: Identite, *fnames: str ) -> dict[str, list[datetime, datetime]]: """Récupère la trace des justificatifs de l'étudiant""" - trace = Trace(self.get_obj_dir(etudid)) + trace = Trace(self.get_obj_dir(etud.id, etud.dept_id)) return trace.get_trace(fnames) diff --git a/app/scodoc/sco_etape_apogee.py b/app/scodoc/sco_etape_apogee.py index 01081021e8..ce0f5457af 100644 --- a/app/scodoc/sco_etape_apogee.py +++ b/app/scodoc/sco_etape_apogee.py @@ -85,7 +85,7 @@ class ApoCSVArchiver(sco_archives.BaseArchiver): sco_archives.BaseArchiver.__init__(self, archive_type="apo_csv") -ApoCSVArchive = ApoCSVArchiver() +APO_CSV_ARCHIVER = ApoCSVArchiver() # def get_sem_apo_archive(formsemestre_id): @@ -126,9 +126,9 @@ def apo_csv_store(csv_data: str, annee_scolaire, sem_id): oid = f"{annee_scolaire}-{sem_id}" description = f"""{str(apo_data.etape)};{annee_scolaire};{sem_id}""" - archive_id = ApoCSVArchive.create_obj_archive(oid, description) + archive_id = APO_CSV_ARCHIVER.create_obj_archive(oid, description) csv_data_bytes = csv_data.encode(sco_apogee_reader.APO_OUTPUT_ENCODING) - ApoCSVArchive.store(archive_id, filename, csv_data_bytes) + APO_CSV_ARCHIVER.store(archive_id, filename, csv_data_bytes) return apo_data.etape @@ -138,7 +138,7 @@ def apo_csv_list_stored_archives(annee_scolaire=None, sem_id=None, etapes=None): :return: list of informations about stored CSV [ { } ] """ - oids = ApoCSVArchive.list_oids() # [ '2016-1', ... ] + oids = APO_CSV_ARCHIVER.list_oids() # [ '2016-1', ... ] # filter if annee_scolaire: e = re.compile(str(annee_scolaire) + "-.+") @@ -149,9 +149,9 @@ def apo_csv_list_stored_archives(annee_scolaire=None, sem_id=None, etapes=None): infos = [] # liste d'infos for oid in oids: - archive_ids = ApoCSVArchive.list_obj_archives(oid) + archive_ids = APO_CSV_ARCHIVER.list_obj_archives(oid) for archive_id in archive_ids: - description = ApoCSVArchive.get_archive_description(archive_id) + description = APO_CSV_ARCHIVER.get_archive_description(archive_id) fs = tuple(description.split(";")) if len(fs) == 3: arch_etape_apo, arch_annee_scolaire, arch_sem_id = fs @@ -165,7 +165,7 @@ def apo_csv_list_stored_archives(annee_scolaire=None, sem_id=None, etapes=None): "annee_scolaire": int(arch_annee_scolaire), "sem_id": int(arch_sem_id), "etape_apo": arch_etape_apo, # qui contient éventuellement le VDI - "date": ApoCSVArchive.get_archive_date(archive_id), + "date": APO_CSV_ARCHIVER.get_archive_date(archive_id), } ) infos.sort(key=lambda x: x["etape_apo"]) @@ -185,7 +185,7 @@ def apo_csv_list_stored_etapes(annee_scolaire, sem_id=None, etapes=None): def apo_csv_delete(archive_id): """Delete archived CSV""" - ApoCSVArchive.delete_archive(archive_id) + APO_CSV_ARCHIVER.delete_archive(archive_id) def apo_csv_get_archive(etape_apo, annee_scolaire="", sem_id=""): @@ -209,7 +209,7 @@ def apo_csv_get(etape_apo="", annee_scolaire="", sem_id="") -> str: "Etape %s non enregistree (%s, %s)" % (etape_apo, annee_scolaire, sem_id) ) archive_id = info["archive_id"] - data = ApoCSVArchive.get(archive_id, etape_apo + ".csv") + data = APO_CSV_ARCHIVER.get(archive_id, etape_apo + ".csv") # ce fichier a été archivé donc généré par ScoDoc # son encodage est donc APO_OUTPUT_ENCODING return data.decode(sco_apogee_reader.APO_OUTPUT_ENCODING) diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 286bcf113c..7e67e4af8c 100755 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -448,7 +448,7 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str: "title": "Documents archivés", "endpoint": "notes.formsemestre_list_archives", "args": {"formsemestre_id": formsemestre_id}, - "enabled": sco_archives.PVArchive.list_obj_archives(formsemestre_id), + "enabled": sco_archives.PV_ARCHIVER.list_obj_archives(formsemestre_id), }, ] diff --git a/app/scodoc/sco_page_etud.py b/app/scodoc/sco_page_etud.py index 403b894722..554ce78046 100644 --- a/app/scodoc/sco_page_etud.py +++ b/app/scodoc/sco_page_etud.py @@ -441,7 +441,7 @@ def ficheEtud(etudid=None): # Fichiers archivés: info["fichiers_archive_htm"] = ( '
        Fichiers associés
        ' - + sco_archives_etud.etud_list_archives_html(etudid) + + sco_archives_etud.etud_list_archives_html(etud) ) # Devenir de l'étudiant: diff --git a/app/views/notes.py b/app/views/notes.py index 51a24caee2..8d18445185 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -44,7 +44,7 @@ from app import models from app.auth.models import User from app.but import ( apc_edit_ue, - bulletin_but_court, + bulletin_but_court, # ne pas enlever: ajoute des routes ! cursus_but, jury_edit_manual, jury_but, diff --git a/sco_version.py b/sco_version.py index 34eef316d5..8343cb8263 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.24" +SCOVERSION = "9.6.25" SCONAME = "ScoDoc" From 0f2579dc0f846b5d3bca31c5d2ffacda2aa5e394 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 7 Sep 2023 23:09:39 +0200 Subject: [PATCH 24/97] =?UTF-8?q?Fix:=20gestion=20des=20archives=20(confus?= =?UTF-8?q?ion=20de=20d=C3=A9partements)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/justificatifs.py | 30 ++--- app/scodoc/sco_archives.py | 141 +++++++++++++++-------- app/scodoc/sco_archives_etud.py | 54 +++++---- app/scodoc/sco_archives_justificatifs.py | 84 +++++++------- app/scodoc/sco_etape_apogee.py | 18 +-- app/scodoc/sco_formsemestre_status.py | 2 +- app/scodoc/sco_page_etud.py | 2 +- app/views/notes.py | 2 +- sco_version.py | 2 +- 9 files changed, 193 insertions(+), 142 deletions(-) diff --git a/app/api/justificatifs.py b/app/api/justificatifs.py index 0fd59b975a..5f66fa6030 100644 --- a/app/api/justificatifs.py +++ b/app/api/justificatifs.py @@ -8,8 +8,9 @@ from datetime import datetime from flask_json import as_json -from flask import g, jsonify, request +from flask import g, request from flask_login import login_required, current_user +from flask_sqlalchemy.query import Query import app.scodoc.sco_assiduites as scass import app.scodoc.sco_utils as scu @@ -26,7 +27,6 @@ from app.scodoc.sco_archives_justificatifs import JustificatifArchiver from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_permissions import Permission from app.scodoc.sco_utils import json_error -from flask_sqlalchemy.query import Query # Partie Modèle @@ -436,7 +436,7 @@ def _delete_singular(justif_id: int, database): if archive_name is not None: archiver: JustificatifArchiver = JustificatifArchiver() try: - archiver.delete_justificatif(justificatif_unique.etudid, archive_name) + archiver.delete_justificatif(justificatif_unique.etudiant, archive_name) except ValueError: pass @@ -481,7 +481,7 @@ def justif_import(justif_id: int = None): try: fname: str archive_name, fname = archiver.save_justificatif( - etudid=justificatif_unique.etudid, + justificatif_unique.etudiant, filename=file.filename, data=file.stream.read(), archive_name=archive_name, @@ -512,7 +512,7 @@ def justif_export(justif_id: int = None, filename: str = None): if g.scodoc_dept: query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id) - justificatif_unique: Justificaitf = query.first_or_404() + justificatif_unique: Justificatif = query.first_or_404() archive_name: str = justificatif_unique.fichier if archive_name is None: @@ -522,7 +522,7 @@ def justif_export(justif_id: int = None, filename: str = None): try: return archiver.get_justificatif_file( - archive_name, justificatif_unique.etudid, filename + archive_name, justificatif_unique.etudiant, filename ) except ScoValueError as err: return json_error(404, err.args[0]) @@ -564,10 +564,10 @@ def justif_remove(justif_id: int = None): if remove is None or remove not in ("all", "list"): return json_error(404, "param 'remove': Valeur invalide") archiver: JustificatifArchiver = JustificatifArchiver() - etudid: int = justificatif_unique.etudid + etud = justificatif_unique.etudiant try: if remove == "all": - archiver.delete_justificatif(etudid=etudid, archive_name=archive_name) + archiver.delete_justificatif(etud, archive_name=archive_name) justificatif_unique.fichier = None db.session.add(justificatif_unique) db.session.commit() @@ -575,13 +575,13 @@ def justif_remove(justif_id: int = None): else: for fname in data.get("filenames", []): archiver.delete_justificatif( - etudid=etudid, + etud, archive_name=archive_name, filename=fname, ) - if len(archiver.list_justificatifs(archive_name, etudid)) == 0: - archiver.delete_justificatif(etudid, archive_name) + if len(archiver.list_justificatifs(archive_name, etud)) == 0: + archiver.delete_justificatif(etud, archive_name) justificatif_unique.fichier = None db.session.add(justificatif_unique) db.session.commit() @@ -616,16 +616,16 @@ def justif_list(justif_id: int = None): archiver: JustificatifArchiver = JustificatifArchiver() if archive_name is not None: filenames = archiver.list_justificatifs( - archive_name, justificatif_unique.etudid + archive_name, justificatif_unique.etudiant ) retour = {"total": len(filenames), "filenames": []} - for fi in filenames: - if int(fi[1]) == current_user.id or current_user.has_permission( + for filename in filenames: + if int(filename[1]) == current_user.id or current_user.has_permission( Permission.ScoJustifView ): - retour["filenames"].append(fi[0]) + retour["filenames"].append(filename[0]) return retour diff --git a/app/scodoc/sco_archives.py b/app/scodoc/sco_archives.py index c6a646ee50..51bba4b208 100644 --- a/app/scodoc/sco_archives.py +++ b/app/scodoc/sco_archives.py @@ -81,7 +81,7 @@ from app.scodoc import sco_pv_pdf from app.scodoc.sco_exceptions import ScoValueError -class BaseArchiver(object): +class BaseArchiver: def __init__(self, archive_type=""): self.archive_type = archive_type self.initialized = False @@ -92,14 +92,17 @@ class BaseArchiver(object): "set dept" self.dept_id = dept_id - def initialize(self): + def initialize(self, dept_id: int = None): + """Fixe le département et initialise les répertoires au besoin.""" + # Set departement (à chaque fois car peut changer d'une utilisation à l'autre) + self.dept_id = getattr(g, "scodoc_dept_id") if dept_id is None else dept_id if self.initialized: return dirs = [Config.SCODOC_VAR_DIR, "archives"] if self.archive_type: dirs.append(self.archive_type) - self.root = os.path.join(*dirs) + self.root = os.path.join(*dirs) # /opt/scodoc-data/archives/ log("initialized archiver, path=" + self.root) path = dirs[0] for directory in dirs[1:]: @@ -112,15 +115,13 @@ class BaseArchiver(object): finally: scu.GSL.release() self.initialized = True - if self.dept_id is None: - self.dept_id = getattr(g, "scodoc_dept_id") - def get_obj_dir(self, oid: int): + def get_obj_dir(self, oid: int, dept_id: int = None): """ :return: path to directory of archives for this object (eg formsemestre_id or etudid). If directory does not yet exist, create it. """ - self.initialize() + self.initialize(dept_id) dept_dir = os.path.join(self.root, str(self.dept_id)) try: scu.GSL.acquire() @@ -141,21 +142,21 @@ class BaseArchiver(object): scu.GSL.release() return obj_dir - def list_oids(self): + def list_oids(self, dept_id: int = None): """ :return: list of archive oids """ - self.initialize() + self.initialize(dept_id) base = os.path.join(self.root, str(self.dept_id)) + os.path.sep dirs = glob.glob(base + "*") return [os.path.split(x)[1] for x in dirs] - def list_obj_archives(self, oid: int): + def list_obj_archives(self, oid: int, dept_id: int = None): """Returns :return: list of archive identifiers for this object (paths to non empty dirs) """ - self.initialize() - base = self.get_obj_dir(oid) + os.path.sep + self.initialize(dept_id) + base = self.get_obj_dir(oid, dept_id=dept_id) + os.path.sep dirs = glob.glob( base + "[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]" @@ -165,9 +166,9 @@ class BaseArchiver(object): dirs.sort() return dirs - def delete_archive(self, archive_id: str): + def delete_archive(self, archive_id: str, dept_id: int = None): """Delete (forever) this archive""" - self.initialize() + self.initialize(dept_id) try: scu.GSL.acquire() shutil.rmtree(archive_id, ignore_errors=True) @@ -180,9 +181,9 @@ class BaseArchiver(object): *[int(x) for x in os.path.split(archive_id)[1].split("-")] ) - def list_archive(self, archive_id: str) -> str: + def list_archive(self, archive_id: str, dept_id: int = None) -> str: """Return list of filenames (without path) in archive""" - self.initialize() + self.initialize(dept_id) try: scu.GSL.acquire() files = os.listdir(archive_id) @@ -201,12 +202,12 @@ class BaseArchiver(object): "^[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}$", archive_name ) - def get_id_from_name(self, oid, archive_name: str): + def get_id_from_name(self, oid, archive_name: str, dept_id: int = None): """returns archive id (check that name is valid)""" - self.initialize() + self.initialize(dept_id) if not self.is_valid_archive_name(archive_name): raise ScoValueError(f"Archive {archive_name} introuvable") - archive_id = os.path.join(self.get_obj_dir(oid), archive_name) + archive_id = os.path.join(self.get_obj_dir(oid, dept_id=dept_id), archive_name) if not os.path.isdir(archive_id): log( f"invalid archive name: {archive_name}, oid={oid}, archive_id={archive_id}" @@ -214,9 +215,9 @@ class BaseArchiver(object): raise ScoValueError(f"Archive {archive_name} introuvable") return archive_id - def get_archive_description(self, archive_id: str) -> str: + def get_archive_description(self, archive_id: str, dept_id: int = None) -> str: """Return description of archive""" - self.initialize() + self.initialize(dept_id) filename = os.path.join(archive_id, "_description.txt") try: with open(filename, encoding=scu.SCO_ENCODING) as f: @@ -229,11 +230,11 @@ class BaseArchiver(object): return descr - def create_obj_archive(self, oid: int, description: str): + def create_obj_archive(self, oid: int, description: str, dept_id: int = None): """Creates a new archive for this object and returns its id.""" # id suffixé par YYYY-MM-DD-hh-mm-ss archive_id = ( - self.get_obj_dir(oid) + self.get_obj_dir(oid, dept_id=dept_id) + os.path.sep + "-".join([f"{x:02d}" for x in time.localtime()[:6]]) ) @@ -248,7 +249,13 @@ class BaseArchiver(object): self.store(archive_id, "_description.txt", description) return archive_id - def store(self, archive_id: str, filename: str, data: Union[str, bytes]): + def store( + self, + archive_id: str, + filename: str, + data: Union[str, bytes], + dept_id: int = None, + ): """Store data in archive, under given filename. Filename may be modified (sanitized): return used filename The file is created or replaced. @@ -256,7 +263,7 @@ class BaseArchiver(object): """ if isinstance(data, str): data = data.encode(scu.SCO_ENCODING) - self.initialize() + self.initialize(dept_id) filename = scu.sanitize_filename(filename) log(f"storing {filename} ({len(data)} bytes) in {archive_id}") try: @@ -268,9 +275,9 @@ class BaseArchiver(object): scu.GSL.release() return filename - def get(self, archive_id: str, filename: str): + def get(self, archive_id: str, filename: str, dept_id: int = None): """Retreive data""" - self.initialize() + self.initialize(dept_id) if not scu.is_valid_filename(filename): log(f"""Archiver.get: invalid filename '{filename}'""") raise ScoValueError("archive introuvable (déjà supprimée ?)") @@ -280,11 +287,11 @@ class BaseArchiver(object): data = f.read() return data - def get_archived_file(self, oid, archive_name, filename): + def get_archived_file(self, oid, archive_name, filename, dept_id: int = None): """Recupère les donnees du fichier indiqué et envoie au client. Returns: Response """ - archive_id = self.get_id_from_name(oid, archive_name) + archive_id = self.get_id_from_name(oid, archive_name, dept_id=dept_id) data = self.get(archive_id, filename) mime = mimetypes.guess_type(filename)[0] if mime is None: @@ -298,7 +305,7 @@ class SemsArchiver(BaseArchiver): BaseArchiver.__init__(self, archive_type="") -PVArchive = SemsArchiver() +PV_ARCHIVER = SemsArchiver() # ---------------------------------------------------------------------------- @@ -332,8 +339,10 @@ def do_formsemestre_archive( formsemestre = FormSemestre.get_formsemestre(formsemestre_id) res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) sem_archive_id = formsemestre_id - archive_id = PVArchive.create_obj_archive(sem_archive_id, description) - date = PVArchive.get_archive_date(archive_id).strftime("%d/%m/%Y à %H:%M") + archive_id = PV_ARCHIVER.create_obj_archive( + sem_archive_id, description, formsemestre.dept_id + ) + date = PV_ARCHIVER.get_archive_date(archive_id).strftime("%d/%m/%Y à %H:%M") if not group_ids: # tous les inscrits du semestre @@ -347,7 +356,12 @@ def do_formsemestre_archive( # Tableau recap notes en XLS (pour tous les etudiants, n'utilise pas les groupes) data, _ = gen_formsemestre_recapcomplet_excel(res, include_evaluations=True) if data: - PVArchive.store(archive_id, "Tableau_moyennes" + scu.XLSX_SUFFIX, data) + PV_ARCHIVER.store( + archive_id, + "Tableau_moyennes" + scu.XLSX_SUFFIX, + data, + dept_id=formsemestre.dept_id, + ) # Tableau recap notes en HTML (pour tous les etudiants, n'utilise pas les groupes) table_html, _, _ = gen_formsemestre_recapcomplet_html_table( formsemestre, res, include_evaluations=True @@ -367,13 +381,17 @@ def do_formsemestre_archive( html_sco_header.sco_footer(), ] ) - PVArchive.store(archive_id, "Tableau_moyennes.html", data) + PV_ARCHIVER.store( + archive_id, "Tableau_moyennes.html", data, dept_id=formsemestre.dept_id + ) # Bulletins en JSON data = gen_formsemestre_recapcomplet_json(formsemestre_id, xml_with_decisions=True) data_js = json.dumps(data, indent=1, cls=ScoDocJSONEncoder) if data: - PVArchive.store(archive_id, "Bulletins.json", data_js) + PV_ARCHIVER.store( + archive_id, "Bulletins.json", data_js, dept_id=formsemestre.dept_id + ) # Décisions de jury, en XLS if formsemestre.formation.is_apc(): response = jury_but_pv.pvjury_page_but(formsemestre_id, fmt="xls") @@ -383,17 +401,23 @@ def do_formsemestre_archive( formsemestre_id, format="xls", publish=False ) if data: - PVArchive.store( + PV_ARCHIVER.store( archive_id, "Decisions_Jury" + scu.XLSX_SUFFIX, data, + dept_id=formsemestre.dept_id, ) # Classeur bulletins (PDF) data, _ = sco_bulletins_pdf.get_formsemestre_bulletins_pdf( formsemestre_id, version=bul_version ) if data: - PVArchive.store(archive_id, "Bulletins.pdf", data) + PV_ARCHIVER.store( + archive_id, + "Bulletins.pdf", + data, + dept_id=formsemestre.dept_id, + ) # Lettres individuelles (PDF): data = sco_pv_lettres_inviduelles.pdf_lettres_individuelles( formsemestre_id, @@ -403,7 +427,12 @@ def do_formsemestre_archive( signature=signature, ) if data: - PVArchive.store(archive_id, f"CourriersDecisions{groups_filename}.pdf", data) + PV_ARCHIVER.store( + archive_id, + f"CourriersDecisions{groups_filename}.pdf", + data, + dept_id=formsemestre.dept_id, + ) # PV de jury (PDF): data = sco_pv_pdf.pvjury_pdf( @@ -419,7 +448,12 @@ def do_formsemestre_archive( anonymous=anonymous, ) if data: - PVArchive.store(archive_id, f"PV_Jury{groups_filename}.pdf", data) + PV_ARCHIVER.store( + archive_id, + f"PV_Jury{groups_filename}.pdf", + data, + dept_id=formsemestre.dept_id, + ) def formsemestre_archive(formsemestre_id, group_ids: list[int] = None): @@ -558,14 +592,21 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement. def formsemestre_list_archives(formsemestre_id): """Page listing archives""" + formsemestre = FormSemestre.get_formsemestre(formsemestre_id) sem_archive_id = formsemestre_id L = [] - for archive_id in PVArchive.list_obj_archives(sem_archive_id): + for archive_id in PV_ARCHIVER.list_obj_archives( + sem_archive_id, dept_id=formsemestre.dept_id + ): a = { "archive_id": archive_id, - "description": PVArchive.get_archive_description(archive_id), - "date": PVArchive.get_archive_date(archive_id), - "content": PVArchive.list_archive(archive_id), + "description": PV_ARCHIVER.get_archive_description( + archive_id, dept_id=formsemestre.dept_id + ), + "date": PV_ARCHIVER.get_archive_date(archive_id), + "content": PV_ARCHIVER.list_archive( + archive_id, dept_id=formsemestre.dept_id + ), } L.append(a) @@ -575,7 +616,7 @@ def formsemestre_list_archives(formsemestre_id): else: H.append("
          ") for a in L: - archive_name = PVArchive.get_archive_name(a["archive_id"]) + archive_name = PV_ARCHIVER.get_archive_name(a["archive_id"]) H.append( '
        • %s : %s (supprimer)
            ' % ( @@ -602,7 +643,9 @@ def formsemestre_get_archived_file(formsemestre_id, archive_name, filename): """Send file to client.""" formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) sem_archive_id = formsemestre.id - return PVArchive.get_archived_file(sem_archive_id, archive_name, filename) + return PV_ARCHIVER.get_archived_file( + sem_archive_id, archive_name, filename, dept_id=formsemestre.dept_id + ) def formsemestre_delete_archive(formsemestre_id, archive_name, dialog_confirmed=False): @@ -617,7 +660,9 @@ def formsemestre_delete_archive(formsemestre_id, archive_name, dialog_confirmed= ) ) sem_archive_id = formsemestre_id - archive_id = PVArchive.get_id_from_name(sem_archive_id, archive_name) + archive_id = PV_ARCHIVER.get_id_from_name( + sem_archive_id, archive_name, dept_id=formsemestre.dept_id + ) dest_url = url_for( "notes.formsemestre_list_archives", @@ -628,7 +673,7 @@ def formsemestre_delete_archive(formsemestre_id, archive_name, dialog_confirmed= if not dialog_confirmed: return scu.confirm_dialog( f"""

            Confirmer la suppression de l'archive du { - PVArchive.get_archive_date(archive_id).strftime("%d/%m/%Y %H:%M") + PV_ARCHIVER.get_archive_date(archive_id).strftime("%d/%m/%Y %H:%M") } ?

            La suppression sera définitive.

            """, @@ -640,6 +685,6 @@ def formsemestre_delete_archive(formsemestre_id, archive_name, dialog_confirmed= }, ) - PVArchive.delete_archive(archive_id) + PV_ARCHIVER.delete_archive(archive_id, dept_id=formsemestre.dept_id) flash("Archive supprimée") return flask.redirect(dest_url) diff --git a/app/scodoc/sco_archives_etud.py b/app/scodoc/sco_archives_etud.py index b538d0f5e7..4bc76a10f8 100644 --- a/app/scodoc/sco_archives_etud.py +++ b/app/scodoc/sco_archives_etud.py @@ -52,7 +52,8 @@ class EtudsArchiver(sco_archives.BaseArchiver): sco_archives.BaseArchiver.__init__(self, archive_type="docetuds") -EtudsArchive = EtudsArchiver() +# Global au processus, attention ! +ETUDS_ARCHIVER = EtudsArchiver() def can_edit_etud_archive(authuser): @@ -60,21 +61,21 @@ def can_edit_etud_archive(authuser): return authuser.has_permission(Permission.ScoEtudAddAnnotations) -def etud_list_archives_html(etudid): +def etud_list_archives_html(etud: Identite): """HTML snippet listing archives""" can_edit = can_edit_etud_archive(current_user) - etuds = sco_etud.get_etud_info(etudid=etudid) - if not etuds: - raise ScoValueError("étudiant inexistant") - etud = etuds[0] - etud_archive_id = etudid + etud_archive_id = etud.id L = [] - for archive_id in EtudsArchive.list_obj_archives(etud_archive_id): + for archive_id in ETUDS_ARCHIVER.list_obj_archives( + etud_archive_id, dept_id=etud.dept_id + ): a = { "archive_id": archive_id, - "description": EtudsArchive.get_archive_description(archive_id), - "date": EtudsArchive.get_archive_date(archive_id), - "content": EtudsArchive.list_archive(archive_id), + "description": ETUDS_ARCHIVER.get_archive_description( + archive_id, dept_id=etud.dept_id + ), + "date": ETUDS_ARCHIVER.get_archive_date(archive_id), + "content": ETUDS_ARCHIVER.list_archive(archive_id, dept_id=etud.dept_id), } L.append(a) delete_icon = scu.icontag( @@ -85,7 +86,7 @@ def etud_list_archives_html(etudid): ) H = ['
              '] for a in L: - archive_name = EtudsArchive.get_archive_name(a["archive_id"]) + archive_name = ETUDS_ARCHIVER.get_archive_name(a["archive_id"]) H.append( """
            • %s""" % (a["date"].strftime("%d/%m/%Y %H:%M"), a["description"]) @@ -93,14 +94,14 @@ def etud_list_archives_html(etudid): for filename in a["content"]: H.append( """%s""" - % (etudid, archive_name, filename, filename) + % (etud.id, archive_name, filename, filename) ) if not a["content"]: H.append("aucun fichier !") if can_edit: H.append( '%s' - % (etudid, archive_name, delete_icon) + % (etud.id, archive_name, delete_icon) ) else: H.append('' + delete_disabled_icon + "") @@ -108,7 +109,7 @@ def etud_list_archives_html(etudid): if can_edit: H.append( '
            • ajouter un fichier
            • ' - % etudid + % etud.id ) H.append("
            ") return "".join(H) @@ -121,12 +122,13 @@ def add_archives_info_to_etud_list(etuds): for etud in etuds: l = [] etud_archive_id = etud["etudid"] - for archive_id in EtudsArchive.list_obj_archives(etud_archive_id): + # Here, ETUDS_ARCHIVER will use g.dept_id + for archive_id in ETUDS_ARCHIVER.list_obj_archives(etud_archive_id): l.append( "%s (%s)" % ( - EtudsArchive.get_archive_description(archive_id), - EtudsArchive.list_archive(archive_id)[0], + ETUDS_ARCHIVER.get_archive_description(archive_id), + ETUDS_ARCHIVER.list_archive(archive_id)[0], ) ) etud["etudarchive"] = ", ".join(l) @@ -197,8 +199,8 @@ def _store_etud_file_to_new_archive( filesize = len(data) if filesize < 10 or filesize > scu.CONFIG.ETUD_MAX_FILE_SIZE: return False, f"Fichier archive '{filename}' de taille invalide ! ({filesize})" - archive_id = EtudsArchive.create_obj_archive(etud_archive_id, description) - EtudsArchive.store(archive_id, filename, data) + archive_id = ETUDS_ARCHIVER.create_obj_archive(etud_archive_id, description) + ETUDS_ARCHIVER.store(archive_id, filename, data) return True, "ok" @@ -212,14 +214,16 @@ def etud_delete_archive(etudid, archive_name, dialog_confirmed=False): raise ScoValueError("étudiant inexistant") etud = etuds[0] etud_archive_id = etud["etudid"] - archive_id = EtudsArchive.get_id_from_name(etud_archive_id, archive_name) + archive_id = ETUDS_ARCHIVER.get_id_from_name( + etud_archive_id, archive_name, dept_id=etud["dept_id"] + ) if not dialog_confirmed: return scu.confirm_dialog( """

            Confirmer la suppression des fichiers ?

            Fichier associé le %s à l'étudiant %s

            La suppression sera définitive.

            """ % ( - EtudsArchive.get_archive_date(archive_id).strftime("%d/%m/%Y %H:%M"), + ETUDS_ARCHIVER.get_archive_date(archive_id).strftime("%d/%m/%Y %H:%M"), etud["nomprenom"], ), dest_url="", @@ -232,7 +236,7 @@ def etud_delete_archive(etudid, archive_name, dialog_confirmed=False): parameters={"etudid": etudid, "archive_name": archive_name}, ) - EtudsArchive.delete_archive(archive_id) + ETUDS_ARCHIVER.delete_archive(archive_id, dept_id=etud["dept_id"]) flash("Archive supprimée") return flask.redirect( url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid) @@ -246,7 +250,9 @@ def etud_get_archived_file(etudid, archive_name, filename): raise ScoValueError("étudiant inexistant") etud = etuds[0] etud_archive_id = etud["etudid"] - return EtudsArchive.get_archived_file(etud_archive_id, archive_name, filename) + return ETUDS_ARCHIVER.get_archived_file( + etud_archive_id, archive_name, filename, dept_id=etud["dept_id"] + ) # --- Upload d'un ensemble de fichiers (pour un groupe d'étudiants) diff --git a/app/scodoc/sco_archives_justificatifs.py b/app/scodoc/sco_archives_justificatifs.py index 0030d203a6..dc99c33915 100644 --- a/app/scodoc/sco_archives_justificatifs.py +++ b/app/scodoc/sco_archives_justificatifs.py @@ -102,7 +102,7 @@ class JustificatifArchiver(BaseArchiver): def save_justificatif( self, - etudid: int, + etud: Identite, filename: str, data: bytes or str, archive_name: str = None, @@ -113,17 +113,18 @@ class JustificatifArchiver(BaseArchiver): Ajoute un fichier dans une archive "justificatif" pour l'etudid donné Retourne l'archive_name utilisé """ - self._set_dept(etudid) if archive_name is None: archive_id: str = self.create_obj_archive( - oid=etudid, description=description + oid=etud.id, description=description, dept_id=etud.dept_id ) else: - archive_id: str = self.get_id_from_name(etudid, archive_name) + archive_id: str = self.get_id_from_name( + etud.id, archive_name, dept_id=etud.dept_id + ) - fname: str = self.store(archive_id, filename, data) + fname: str = self.store(archive_id, filename, data, dept_id=etud.dept_id) - trace = Trace(self.get_obj_dir(etudid)) + trace = Trace(self.get_obj_dir(etud.id, dept_id=etud.dept_id)) trace.set_trace(fname, mode="entry") if user_id is not None: trace.set_trace(fname, mode="user_id", current_user=user_id) @@ -132,7 +133,7 @@ class JustificatifArchiver(BaseArchiver): def delete_justificatif( self, - etudid: int, + etud: Identite, archive_name: str, filename: str = None, has_trace: bool = True, @@ -140,92 +141,91 @@ class JustificatifArchiver(BaseArchiver): """ Supprime une archive ou un fichier particulier de l'archivage de l'étudiant donné - Si trace == True : sauvegarde le nom du/des fichier(s) supprimé(s) dans la trace de l'étudiant + Si trace == True : sauvegarde le nom du/des fichier(s) supprimé(s) + dans la trace de l'étudiant """ - self._set_dept(etudid) - if str(etudid) not in self.list_oids(): - raise ValueError(f"Aucune archive pour etudid[{etudid}]") + if str(etud.id) not in self.list_oids(): + raise ValueError(f"Aucune archive pour etudid[{etud.id}]") - archive_id = self.get_id_from_name(etudid, archive_name) + archive_id = self.get_id_from_name(etud.id, archive_name, dept_id=etud.dept_id) if filename is not None: - if filename not in self.list_archive(archive_id): + if filename not in self.list_archive(archive_id, dept_id=etud.dept_id): raise ValueError( - f"filename {filename} inconnu dans l'archive archive_id[{archive_id}] -> etudid[{etudid}]" + f"""filename {filename} inconnu dans l'archive archive_id[{ + archive_id}] -> etudid[{etud.id}]""" ) - path: str = os.path.join(self.get_obj_dir(etudid), archive_id, filename) + path: str = os.path.join( + self.get_obj_dir(etud.id, dept_id=etud.dept_id), archive_id, filename + ) if os.path.isfile(path): if has_trace: - trace = Trace(self.get_obj_dir(etudid)) + trace = Trace(self.get_obj_dir(etud.id, dept_id=etud.dept_id)) trace.set_trace(filename, mode="delete") os.remove(path) else: if has_trace: - trace = Trace(self.get_obj_dir(etudid)) - trace.set_trace(*self.list_archive(archive_id), mode="delete") + trace = Trace(self.get_obj_dir(etud.id, dept_id=etud.dept_id)) + trace.set_trace( + *self.list_archive(archive_id, dept_id=etud.dept_id), mode="delete" + ) self.delete_archive( os.path.join( - self.get_obj_dir(etudid), + self.get_obj_dir(etud.id, dept_id=etud.dept_id), archive_id, ) ) def list_justificatifs( - self, archive_name: str, etudid: int + self, archive_name: str, etud: Identite ) -> list[tuple[str, int]]: """ Retourne la liste des noms de fichiers dans l'archive donnée """ - self._set_dept(etudid) filenames: list[str] = [] - archive_id = self.get_id_from_name(etudid, archive_name) + archive_id = self.get_id_from_name(etud.id, archive_name, dept_id=etud.dept_id) - filenames = self.list_archive(archive_id) - trace: Trace = Trace(self.get_obj_dir(etudid)) + filenames = self.list_archive(archive_id, dept_id=etud.dept_id) + trace: Trace = Trace(self.get_obj_dir(etud.id, dept_id=etud.dept_id)) traced = trace.get_trace(filenames) retour = [(key, value[2]) for key, value in traced.items()] return retour - def get_justificatif_file(self, archive_name: str, etudid: int, filename: str): + def get_justificatif_file(self, archive_name: str, etud: Identite, filename: str): """ Retourne une réponse de téléchargement de fichier si le fichier existe """ - self._set_dept(etudid) - archive_id: str = self.get_id_from_name(etudid, archive_name) - if filename in self.list_archive(archive_id): - return self.get_archived_file(etudid, archive_name, filename) + archive_id: str = self.get_id_from_name( + etud.id, archive_name, dept_id=etud.dept_id + ) + if filename in self.list_archive(archive_id, dept_id=etud.dept_id): + return self.get_archived_file( + etud.id, archive_name, filename, dept_id=etud.dept_id + ) raise ScoValueError( f"Fichier {filename} introuvable dans l'archive {archive_name}" ) - def _set_dept(self, etudid: int): - """ - Mets à jour le dept_id de l'archiver en fonction du département de l'étudiant - """ - etud: Identite = Identite.query.filter_by(id=etudid).first() - self.set_dept_id(etud.dept_id) - def remove_dept_archive(self, dept_id: int = None): """ Supprime toutes les archives d'un département (ou de tous les départements) ⚠ Supprime aussi les fichiers de trace ⚠ """ - self.set_dept_id(1) - self.initialize() - + # juste pour récupérer .root, dept_id n'a pas d'importance + self.initialize(dept_id=1) if dept_id is None: rmtree(self.root, ignore_errors=True) else: rmtree(os.path.join(self.root, str(dept_id)), ignore_errors=True) - def get_trace( - self, etudid: int, *fnames: str + def get_trace( # XXX inutilisée ? + self, etud: Identite, *fnames: str ) -> dict[str, list[datetime, datetime]]: """Récupère la trace des justificatifs de l'étudiant""" - trace = Trace(self.get_obj_dir(etudid)) + trace = Trace(self.get_obj_dir(etud.id, etud.dept_id)) return trace.get_trace(fnames) diff --git a/app/scodoc/sco_etape_apogee.py b/app/scodoc/sco_etape_apogee.py index 01081021e8..ce0f5457af 100644 --- a/app/scodoc/sco_etape_apogee.py +++ b/app/scodoc/sco_etape_apogee.py @@ -85,7 +85,7 @@ class ApoCSVArchiver(sco_archives.BaseArchiver): sco_archives.BaseArchiver.__init__(self, archive_type="apo_csv") -ApoCSVArchive = ApoCSVArchiver() +APO_CSV_ARCHIVER = ApoCSVArchiver() # def get_sem_apo_archive(formsemestre_id): @@ -126,9 +126,9 @@ def apo_csv_store(csv_data: str, annee_scolaire, sem_id): oid = f"{annee_scolaire}-{sem_id}" description = f"""{str(apo_data.etape)};{annee_scolaire};{sem_id}""" - archive_id = ApoCSVArchive.create_obj_archive(oid, description) + archive_id = APO_CSV_ARCHIVER.create_obj_archive(oid, description) csv_data_bytes = csv_data.encode(sco_apogee_reader.APO_OUTPUT_ENCODING) - ApoCSVArchive.store(archive_id, filename, csv_data_bytes) + APO_CSV_ARCHIVER.store(archive_id, filename, csv_data_bytes) return apo_data.etape @@ -138,7 +138,7 @@ def apo_csv_list_stored_archives(annee_scolaire=None, sem_id=None, etapes=None): :return: list of informations about stored CSV [ { } ] """ - oids = ApoCSVArchive.list_oids() # [ '2016-1', ... ] + oids = APO_CSV_ARCHIVER.list_oids() # [ '2016-1', ... ] # filter if annee_scolaire: e = re.compile(str(annee_scolaire) + "-.+") @@ -149,9 +149,9 @@ def apo_csv_list_stored_archives(annee_scolaire=None, sem_id=None, etapes=None): infos = [] # liste d'infos for oid in oids: - archive_ids = ApoCSVArchive.list_obj_archives(oid) + archive_ids = APO_CSV_ARCHIVER.list_obj_archives(oid) for archive_id in archive_ids: - description = ApoCSVArchive.get_archive_description(archive_id) + description = APO_CSV_ARCHIVER.get_archive_description(archive_id) fs = tuple(description.split(";")) if len(fs) == 3: arch_etape_apo, arch_annee_scolaire, arch_sem_id = fs @@ -165,7 +165,7 @@ def apo_csv_list_stored_archives(annee_scolaire=None, sem_id=None, etapes=None): "annee_scolaire": int(arch_annee_scolaire), "sem_id": int(arch_sem_id), "etape_apo": arch_etape_apo, # qui contient éventuellement le VDI - "date": ApoCSVArchive.get_archive_date(archive_id), + "date": APO_CSV_ARCHIVER.get_archive_date(archive_id), } ) infos.sort(key=lambda x: x["etape_apo"]) @@ -185,7 +185,7 @@ def apo_csv_list_stored_etapes(annee_scolaire, sem_id=None, etapes=None): def apo_csv_delete(archive_id): """Delete archived CSV""" - ApoCSVArchive.delete_archive(archive_id) + APO_CSV_ARCHIVER.delete_archive(archive_id) def apo_csv_get_archive(etape_apo, annee_scolaire="", sem_id=""): @@ -209,7 +209,7 @@ def apo_csv_get(etape_apo="", annee_scolaire="", sem_id="") -> str: "Etape %s non enregistree (%s, %s)" % (etape_apo, annee_scolaire, sem_id) ) archive_id = info["archive_id"] - data = ApoCSVArchive.get(archive_id, etape_apo + ".csv") + data = APO_CSV_ARCHIVER.get(archive_id, etape_apo + ".csv") # ce fichier a été archivé donc généré par ScoDoc # son encodage est donc APO_OUTPUT_ENCODING return data.decode(sco_apogee_reader.APO_OUTPUT_ENCODING) diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 286bcf113c..7e67e4af8c 100755 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -448,7 +448,7 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str: "title": "Documents archivés", "endpoint": "notes.formsemestre_list_archives", "args": {"formsemestre_id": formsemestre_id}, - "enabled": sco_archives.PVArchive.list_obj_archives(formsemestre_id), + "enabled": sco_archives.PV_ARCHIVER.list_obj_archives(formsemestre_id), }, ] diff --git a/app/scodoc/sco_page_etud.py b/app/scodoc/sco_page_etud.py index 403b894722..554ce78046 100644 --- a/app/scodoc/sco_page_etud.py +++ b/app/scodoc/sco_page_etud.py @@ -441,7 +441,7 @@ def ficheEtud(etudid=None): # Fichiers archivés: info["fichiers_archive_htm"] = ( '
            Fichiers associés
            ' - + sco_archives_etud.etud_list_archives_html(etudid) + + sco_archives_etud.etud_list_archives_html(etud) ) # Devenir de l'étudiant: diff --git a/app/views/notes.py b/app/views/notes.py index 51a24caee2..8d18445185 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -44,7 +44,7 @@ from app import models from app.auth.models import User from app.but import ( apc_edit_ue, - bulletin_but_court, + bulletin_but_court, # ne pas enlever: ajoute des routes ! cursus_but, jury_edit_manual, jury_but, diff --git a/sco_version.py b/sco_version.py index 34eef316d5..8343cb8263 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.24" +SCOVERSION = "9.6.25" SCONAME = "ScoDoc" From dbb18e7557aefccb730ce2b27378839da28315f9 Mon Sep 17 00:00:00 2001 From: "pascal.bouron" Date: Thu, 7 Sep 2023 15:35:45 +0200 Subject: [PATCH 25/97] Actualiser app/scodoc/sco_permissions.py typo --- app/scodoc/sco_permissions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scodoc/sco_permissions.py b/app/scodoc/sco_permissions.py index 11497367a5..dd402548ab 100644 --- a/app/scodoc/sco_permissions.py +++ b/app/scodoc/sco_permissions.py @@ -27,7 +27,7 @@ _SCO_PERMISSIONS = ( (1 << 13, "ScoAbsChange", "Saisir des absences"), (1 << 14, "ScoAbsAddBillet", "Saisir des billets d'absences"), # changer adresse/photo ou pour envoyer bulletins par mail ou pour debouche - (1 << 15, "ScoEtudChangeAdr", "Changer les addresses d'étudiants"), + (1 << 15, "ScoEtudChangeAdr", "Changer les adresses d'étudiants"), ( 1 << 16, "APIEditGroups", From 56276cc5b901d96200a1f3c2bfa3d991feb7b228 Mon Sep 17 00:00:00 2001 From: iziram Date: Fri, 8 Sep 2023 11:59:10 +0200 Subject: [PATCH 26/97] Assiduites : Liste des abs dans mail bul #691 --- app/scodoc/sco_bulletins.py | 10 ++- app/scodoc/sco_preferences.py | 11 +++ .../widgets/liste_assiduites_mail.j2 | 16 ++++ app/views/assiduites.py | 75 ++++++++++++++++++- 4 files changed, 107 insertions(+), 5 deletions(-) create mode 100644 app/templates/assiduites/widgets/liste_assiduites_mail.j2 diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index a1e67d3382..0fe70efa3d 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -1097,10 +1097,12 @@ def mail_bulletin(formsemestre_id, infos, pdfdata, filename, recipient_addr): hea = "" if sco_preferences.get_preference("bul_mail_list_abs"): - hea += "\n\n" + "(LISTE D'ABSENCES NON DISPONIBLE)" # XXX TODO-ASSIDUITE - # sco_abs_views.ListeAbsEtud( - # etud["etudid"], with_evals=False, format="text" - # ) + from app.views.assiduites import generate_bul_list + + etud_identite: Identite = Identite.get_etud(etud["etudid"]) + form_semestre: FormSemestre = FormSemestre.get_formsemestre(formsemestre_id) + hea += "\n\n" + hea += generate_bul_list(etud_identite, form_semestre) subject = f"""Relevé de notes de {etud["nomprenom"]}""" recipients = [recipient_addr] diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py index 1acfc5dab0..e7514ed80d 100644 --- a/app/scodoc/sco_preferences.py +++ b/app/scodoc/sco_preferences.py @@ -1750,6 +1750,17 @@ class BasePreferences: "category": "bul_mail", }, ), + ( + "bul_mail_list_abs_nb", + { + "initvalue": 10, + "title": "Nombre maximum de dates par catégorie", + "explanation": "dans la liste des absences dans le mail envoyant le bulletin de notes (catégories : abs,abs_just, retard,justificatifs)", + "type": "int", + "size": 3, + "category": "bul_mail", + }, + ), ( "bul_mail_contact_addr", { diff --git a/app/templates/assiduites/widgets/liste_assiduites_mail.j2 b/app/templates/assiduites/widgets/liste_assiduites_mail.j2 new file mode 100644 index 0000000000..33254d6525 --- /dev/null +++ b/app/templates/assiduites/widgets/liste_assiduites_mail.j2 @@ -0,0 +1,16 @@ +<=== Assiduité ===> +--- Absences non justifiées {{stats.absent[metric] - stats.absent.justifie[metric]}} ({{metrique}}) --- +{% for assi in abs_nj %}- Absence non just. {{assi.date}} +{% endfor %} + +--- Absences justifiées {{stats.absent.justifie[metric]}} ({{metrique}}) --- +{% for assi in abs_j %}- Absence just. {{assi.date}} +{% endfor %} + +--- Retard {{stats.retard[metric]}} ({{metrique}}) --- +{% for assi in retards %}- Retard {{assi.date}} +{% endfor %} + +--- Justificatif --- +{% for justi in justifs %}- Justificatif {{justi.date}} {{justi.raison}} : {{justi.etat}} +{% endfor %} \ No newline at end of file diff --git a/app/views/assiduites.py b/app/views/assiduites.py index b037b10ac6..654873b0a7 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -16,6 +16,7 @@ from app.models import ( Identite, ScoDocSiteConfig, Assiduite, + Justificatif, Departement, FormSemestreInscription, Evaluation, @@ -1082,7 +1083,7 @@ def signal_evaluation_abs(etudid: int = None, evaluation_id: int = None): scodoc_dept=g.scodoc_dept, duplication="oui", ) - raise ScoValueError(msg, dest) + raise ScoValueError(msg, dest) from see db.session.add(assiduite_unique) db.session.commit() @@ -1098,9 +1099,81 @@ 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""" + metrique: str = scu.translate_assiduites_metric( + sco_preferences.get_preference("assi_metrique", formsemestre_id=semestre.id), + ) + + max_nb: int = int( + sco_preferences.get_preference( + "bul_mail_list_abs_nb", formsemestre_id=semestre.id + ) + ) + + assiduites = scass.filter_by_formsemestre( + etud.assiduites, Assiduite, semestre + ).order_by(Assiduite.entry_date.desc()) + justificatifs = scass.filter_by_formsemestre( + etud.justificatifs, Justificatif, semestre + ).order_by(Justificatif.entry_date.desc()) + + stats: dict = scass.get_assiduites_stats( + assiduites, metric=metrique, filtered={"split": True} + ) + + abs_j: list[str] = [ + {"date": _get_date_str(assi.date_debut, assi.date_fin)} + for assi in assiduites + if assi.etat == scu.EtatAssiduite.ABSENT and assi.est_just is True + ] + abs_nj: list[str] = [ + {"date": _get_date_str(assi.date_debut, assi.date_fin)} + for assi in assiduites + if assi.etat == scu.EtatAssiduite.ABSENT and assi.est_just is False + ] + retards: list[str] = [ + {"date": _get_date_str(assi.date_debut, assi.date_fin)} + for assi in assiduites + if assi.etat == scu.EtatAssiduite.RETARD + ] + + justifs: list[dict[str, str]] = [ + { + "date": _get_date_str(justi.date_debut, justi.date_fin), + "raison": "" if justi.raison is None else justi.raison, + "etat": { + scu.EtatJustificatif.VALIDE: "justificatif valide", + scu.EtatJustificatif.NON_VALIDE: "justificatif invalide", + scu.EtatJustificatif.ATTENTE: "justificatif en attente de validation", + scu.EtatJustificatif.MODIFIE: "justificatif ayant été modifié", + }.get(justi.etat), + } + for justi in justificatifs + ] + + return render_template( + "assiduites/widgets/liste_assiduites_mail.j2", + abs_j=abs_j[:max_nb], + abs_nj=abs_nj[:max_nb], + retards=retards[:max_nb], + justifs=justifs[:max_nb], + stats=stats, + metrique=scu.translate_assiduites_metric(metrique, short=True, inverse=False), + metric=metrique, + ) + + # --- Fonctions internes --- +def _get_date_str(deb: datetime.datetime, fin: datetime.datetime) -> str: + 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): if deb is None or fin is None: return "null" From 4c2ea340589b451f088d6d4610e078f2d86bd73c Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 10 Sep 2023 19:40:18 +0200 Subject: [PATCH 27/97] correction affichage export apogee --- app/scodoc/sco_semset.py | 4 ++-- sco_version.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/scodoc/sco_semset.py b/app/scodoc/sco_semset.py index 49ec35e08a..c1ed85c5ef 100644 --- a/app/scodoc/sco_semset.py +++ b/app/scodoc/sco_semset.py @@ -306,9 +306,9 @@ class SemSet(dict): H.append("

            ") if self["sem_id"] == 1: - periode = "1re période (S1, S3)" + periode = "1re période (S1, S3, S5)" elif self["sem_id"] == 2: - periode = "2de période (S2, S4)" + periode = "2de période (S2, S4, S6)" else: periode = "non semestrialisée (LP, ...). Incompatible avec BUT." diff --git a/sco_version.py b/sco_version.py index 8343cb8263..f10bb0ce64 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.25" +SCOVERSION = "9.6.26" SCONAME = "ScoDoc" From d193610b3737bfde11b5b0b95781a1fffc33094c Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 10 Sep 2023 19:46:50 +0200 Subject: [PATCH 28/97] =?UTF-8?q?API=20formsemestre:=20n'exporte=20pas=20g?= =?UTF-8?q?roups=5Fauto=5Fassignment=5Fdata=20sauf=20via=20la=20route=20d?= =?UTF-8?q?=C3=A9di=C3=A9e.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/formsemestre.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 20a1cf359e..e29ffbfc31 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -195,6 +195,7 @@ class FormSemestre(db.Model): """ d = dict(self.__dict__) d.pop("_sa_instance_state", None) + d.pop("groups_auto_assignment_data", None) # ScoDoc7 output_formators: (backward compat) d["formsemestre_id"] = self.id d["titre_num"] = self.titre_num() @@ -226,6 +227,7 @@ class FormSemestre(db.Model): """ d = dict(self.__dict__) d.pop("_sa_instance_state", None) + d.pop("groups_auto_assignment_data", None) d["annee_scolaire"] = self.annee_scolaire() if self.date_debut: d["date_debut"] = self.date_debut.strftime("%d/%m/%Y") From 08c8a06965fbb66e14697cbbc53deec81bc5919a Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 10 Sep 2023 19:50:29 +0200 Subject: [PATCH 29/97] =?UTF-8?q?Modifie=20formulation=20'D=C3=A9cision=20?= =?UTF-8?q?saisie=20le'=20sur=20les=20bul.=20court=20BUT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/bulletin_but_court_pdf.py | 2 +- app/templates/but/bulletin_court_page.j2 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/but/bulletin_but_court_pdf.py b/app/but/bulletin_but_court_pdf.py index abd90015b5..a657cf43a0 100644 --- a/app/but/bulletin_but_court_pdf.py +++ b/app/but/bulletin_but_court_pdf.py @@ -501,7 +501,7 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard): txt = f"""ECTS acquis en BUT : {self.ects_total:g}
            """ if self.bul["semestre"].get("decision_annee", None): txt += f""" - Jury tenu le { + Décision saisie le { datetime.datetime.fromisoformat(self.bul["semestre"]["decision_annee"]["date"]).strftime("%d/%m/%Y") }, année BUT{self.bul["semestre"]["decision_annee"]["ordre"]} {self.bul["semestre"]["decision_annee"]["code"]}. diff --git a/app/templates/but/bulletin_court_page.j2 b/app/templates/but/bulletin_court_page.j2 index d73c6f389a..2533684756 100644 --- a/app/templates/but/bulletin_court_page.j2 +++ b/app/templates/but/bulletin_court_page.j2 @@ -158,7 +158,7 @@
            ECTS acquis en BUT : {{"%g"|format(ects_total)}}
            {% if bul.semestre.decision_annee %} - Jury tenu le {{ + Décision saisie le {{ datetime.datetime.fromisoformat(bul.semestre.decision_annee.date).strftime("%d/%m/%Y") }}, année BUT{{bul.semestre.decision_annee.ordre}} From ef96277365221caaf242008425896b9cf7725b84 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 10 Sep 2023 21:16:31 +0200 Subject: [PATCH 30/97] Option de config globale pour interdire l'export des bulletins PDF. Implements #715 --- app/api/etudiants.py | 5 +++-- app/but/bulletin_but_pdf.py | 3 +++ app/forms/main/config_main.py | 9 +++++++++ app/models/config.py | 23 +++++++++++++++++++++++ app/scodoc/gen_tables.py | 3 +-- app/scodoc/sco_bulletins_generator.py | 8 +++++++- app/scodoc/sco_utils.py | 4 +--- 7 files changed, 47 insertions(+), 8 deletions(-) diff --git a/app/api/etudiants.py b/app/api/etudiants.py index 572f77ca49..5c0c981bbd 100755 --- a/app/api/etudiants.py +++ b/app/api/etudiants.py @@ -27,6 +27,7 @@ from app.models import ( FormSemestreInscription, FormSemestre, Identite, + ScoDocSiteConfig, ) from app.scodoc import sco_bulletins from app.scodoc import sco_groups @@ -359,7 +360,7 @@ def bulletin( with_img_signatures_pdf: bool = True, ): """ - Retourne le bulletin d'un étudiant en fonction de son id et d'un semestre donné + Retourne le bulletin d'un étudiant dans un formsemestre. formsemestre_id : l'id d'un formsemestre code_type : "etudid", "nip" ou "ine" @@ -376,7 +377,7 @@ def bulletin( formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first_or_404() dept = Departement.query.filter_by(id=formsemestre.dept_id).first_or_404() if g.scodoc_dept and dept.acronym != g.scodoc_dept: - return json_error(404, "formsemestre inexistant", as_response=True) + return json_error(404, "formsemestre inexistant") app.set_sco_dept(dept.acronym) if code_type == "nip": diff --git a/app/but/bulletin_but_pdf.py b/app/but/bulletin_but_pdf.py index c52c443115..82750d2064 100644 --- a/app/but/bulletin_but_pdf.py +++ b/app/but/bulletin_but_pdf.py @@ -24,6 +24,7 @@ from reportlab.lib.colors import blue from reportlab.lib.units import cm, mm from reportlab.platypus import Paragraph, Spacer +from app.models import ScoDocSiteConfig from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard from app.scodoc import gen_tables from app.scodoc.codes_cursus import UE_SPORT @@ -48,6 +49,8 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard): - en HTML: une chaine - en PDF: une liste d'objets PLATYPUS (eg instance de Table). """ + if fmt == "pdf" and ScoDocSiteConfig.is_bul_pdf_disabled(): + return [Paragraph("

            Export des PDF interdit par l'administrateur

            ")] tables_infos = [ # ---- TABLE SYNTHESE UES self.but_table_synthese_ues(), diff --git a/app/forms/main/config_main.py b/app/forms/main/config_main.py index f9eff362df..cb6b0ba225 100644 --- a/app/forms/main/config_main.py +++ b/app/forms/main/config_main.py @@ -76,6 +76,7 @@ class ScoDocConfigurationForm(FlaskForm): Attention: si ce champ peut aussi être défini dans chaque département.""", validators=[Optional(), Email()], ) + disable_bul_pdf = BooleanField("empêcher tous les exports PDF (!)") submit_scodoc = SubmitField("Valider") cancel_scodoc = SubmitField("Annuler", render_kw={"formnovalidate": True}) @@ -94,6 +95,7 @@ def configuration(): "month_debut_annee_scolaire": ScoDocSiteConfig.get_month_debut_annee_scolaire(), "month_debut_periode2": ScoDocSiteConfig.get_month_debut_periode2(), "email_from_addr": ScoDocSiteConfig.get("email_from_addr"), + "disable_bul_pdf": ScoDocSiteConfig.is_bul_pdf_disabled(), } ) if request.method == "POST" and ( @@ -139,6 +141,13 @@ def configuration(): ) if ScoDocSiteConfig.set("email_from_addr", form_scodoc.data["email_from_addr"]): flash("Adresse email origine enregistrée") + if ScoDocSiteConfig.disable_bul_pdf( + enabled=form_scodoc.data["disable_bul_pdf"] + ): + flash( + "Exports PDF " + + ("désactivés" if form_scodoc.data["disable_bul_pdf"] else "réactivés") + ) return redirect(url_for("scodoc.index")) return render_template( diff --git a/app/models/config.py b/app/models/config.py index c436248fc5..e54cda7819 100644 --- a/app/models/config.py +++ b/app/models/config.py @@ -95,6 +95,7 @@ class ScoDocSiteConfig(db.Model): "enable_entreprises": bool, "month_debut_annee_scolaire": int, "month_debut_periode2": int, + "disable_bul_pdf": bool, # CAS "cas_enable": bool, "cas_server": str, @@ -235,6 +236,12 @@ class ScoDocSiteConfig(db.Model): cfg = ScoDocSiteConfig.query.filter_by(name="enable_entreprises").first() return cfg is not None and cfg.value + @classmethod + def is_bul_pdf_disabled(cls) -> bool: + """True si on interdit les exports PDF des bulltins""" + cfg = ScoDocSiteConfig.query.filter_by(name="disable_bul_pdf").first() + return cfg is not None and cfg.value + @classmethod def enable_entreprises(cls, enabled=True) -> bool: """Active (ou déactive) le module entreprises. True si changement.""" @@ -251,6 +258,22 @@ class ScoDocSiteConfig(db.Model): return True return False + @classmethod + def disable_bul_pdf(cls, enabled=True) -> bool: + """Interedit (ou autorise) les exports PDF. True si changement.""" + if enabled != ScoDocSiteConfig.is_bul_pdf_disabled(): + cfg = ScoDocSiteConfig.query.filter_by(name="disable_bul_pdf").first() + if cfg is None: + cfg = ScoDocSiteConfig( + name="disable_bul_pdf", value="on" if enabled else "" + ) + else: + cfg.value = "on" if enabled else "" + db.session.add(cfg) + db.session.commit() + return True + return False + @classmethod def get(cls, name: str, default: str = "") -> str: "Get configuration param; empty string or specified default if unset" diff --git a/app/scodoc/gen_tables.py b/app/scodoc/gen_tables.py index 0920b81f5c..7647f3e4c9 100644 --- a/app/scodoc/gen_tables.py +++ b/app/scodoc/gen_tables.py @@ -778,8 +778,7 @@ if __name__ == "__main__": print(table.gen(format="json")) # Test pdf: import io - from reportlab.platypus import KeepInFrame - from app.scodoc import sco_preferences, sco_pdf + from app.scodoc import sco_preferences preferences = sco_preferences.SemPreferences() table.preferences = preferences diff --git a/app/scodoc/sco_bulletins_generator.py b/app/scodoc/sco_bulletins_generator.py index bf6660d8fd..2229a37da0 100644 --- a/app/scodoc/sco_bulletins_generator.py +++ b/app/scodoc/sco_bulletins_generator.py @@ -62,7 +62,7 @@ from reportlab.platypus import Table, TableStyle, Image, KeepInFrame from flask import request from flask_login import current_user -from app.models import FormSemestre, Identite +from app.models import FormSemestre, Identite, ScoDocSiteConfig from app.scodoc import sco_utils as scu from app.scodoc.sco_exceptions import NoteProcessError from app import log @@ -197,6 +197,10 @@ class BulletinGenerator: else: # Insere notre marqueur qui permet de générer les bookmarks et filigrannes: story.insert(index_obj_debut, marque_debut_bulletin) + + if ScoDocSiteConfig.is_bul_pdf_disabled(): + story = [Paragraph("

            Export des PDF interdit par l'administrateur

            ")] + # # objects.append(sco_pdf.FinBulletin()) if not stand_alone: @@ -290,6 +294,8 @@ def make_formsemestre_bulletin_etud( gen_class = bulletin_get_class(bul_class_name + "BUT") if gen_class is None: gen_class = bulletin_get_class(bul_class_name) + if gen_class is not None: + break if gen_class is None: raise ValueError(f"Type de bulletin PDF invalide (paramètre: {bul_class_name})") diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 00f0604efb..7cbc3e0d20 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -1035,9 +1035,7 @@ def get_request_args(): def json_error(status_code, message=None) -> Response: - """Simple JSON for errors. - If as-response, returns Flask's Response. Otherwise returns a dict. - """ + """Simple JSON for errors.""" payload = { "error": HTTP_STATUS_CODES.get(status_code, "Unknown error"), "status": status_code, From e1ea5a33d24c04a0b4d23dd5e320c58b78ea898d Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 10 Sep 2023 21:22:52 +0200 Subject: [PATCH 31/97] =?UTF-8?q?modif=20intitul=C3=A9=20option?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/forms/main/config_main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/forms/main/config_main.py b/app/forms/main/config_main.py index cb6b0ba225..8c955c9cb1 100644 --- a/app/forms/main/config_main.py +++ b/app/forms/main/config_main.py @@ -76,7 +76,7 @@ class ScoDocConfigurationForm(FlaskForm): Attention: si ce champ peut aussi être défini dans chaque département.""", validators=[Optional(), Email()], ) - disable_bul_pdf = BooleanField("empêcher tous les exports PDF (!)") + disable_bul_pdf = BooleanField("empêcher les exports des bulletins en PDF") submit_scodoc = SubmitField("Valider") cancel_scodoc = SubmitField("Annuler", render_kw={"formnovalidate": True}) From d55f90469c6d6674ed670bafec28943309361fec Mon Sep 17 00:00:00 2001 From: iziram Date: Sun, 10 Sep 2023 22:11:10 +0200 Subject: [PATCH 32/97] =?UTF-8?q?Assiduit=C3=A9s=20:=20correction=20delete?= =?UTF-8?q?=5Fjustificatif=20sco=5Farchives=5Fjustificatifs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_archives_justificatifs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/scodoc/sco_archives_justificatifs.py b/app/scodoc/sco_archives_justificatifs.py index dc99c33915..1ca486b375 100644 --- a/app/scodoc/sco_archives_justificatifs.py +++ b/app/scodoc/sco_archives_justificatifs.py @@ -144,7 +144,8 @@ class JustificatifArchiver(BaseArchiver): Si trace == True : sauvegarde le nom du/des fichier(s) supprimé(s) dans la trace de l'étudiant """ - if str(etud.id) not in self.list_oids(): + + if str(etud.id) not in self.list_oids(etud.dept_id): raise ValueError(f"Aucune archive pour etudid[{etud.id}]") archive_id = self.get_id_from_name(etud.id, archive_name, dept_id=etud.dept_id) From 638ff758cd150ae52154a7e2898fbfeee52b0e6b Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 11 Sep 2023 07:11:52 +0200 Subject: [PATCH 33/97] Fix sco_archives_justificatifs --- app/scodoc/sco_archives_justificatifs.py | 2 +- sco_version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/scodoc/sco_archives_justificatifs.py b/app/scodoc/sco_archives_justificatifs.py index dc99c33915..14a779bf8b 100644 --- a/app/scodoc/sco_archives_justificatifs.py +++ b/app/scodoc/sco_archives_justificatifs.py @@ -144,7 +144,7 @@ class JustificatifArchiver(BaseArchiver): Si trace == True : sauvegarde le nom du/des fichier(s) supprimé(s) dans la trace de l'étudiant """ - if str(etud.id) not in self.list_oids(): + if str(etud.id) not in self.list_oids(etud.dept_id): raise ValueError(f"Aucune archive pour etudid[{etud.id}]") archive_id = self.get_id_from_name(etud.id, archive_name, dept_id=etud.dept_id) diff --git a/sco_version.py b/sco_version.py index f10bb0ce64..a44e5b0a14 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.26" +SCOVERSION = "9.6.27" SCONAME = "ScoDoc" From 864a5a9405ae42424be769546c5565323442e8af Mon Sep 17 00:00:00 2001 From: iziram Date: Mon, 11 Sep 2023 08:31:09 +0200 Subject: [PATCH 34/97] Assiduites : fix liste num page #714 --- app/api/assiduites.py | 4 ++++ app/api/justificatifs.py | 6 ++++++ app/scodoc/sco_saisie_notes.py | 1 + app/static/js/assiduites.js | 10 ++++++---- app/templates/assiduites/widgets/tableau_base.j2 | 12 ++++++++---- tests/unit/test_assiduites.py | 1 - 6 files changed, 25 insertions(+), 9 deletions(-) diff --git a/app/api/assiduites.py b/app/api/assiduites.py index bbd6142f91..ab568ae6c3 100644 --- a/app/api/assiduites.py +++ b/app/api/assiduites.py @@ -1024,6 +1024,10 @@ 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) + order = requested.args.get("order", None) + if order is not None: + assiduites_query: Query = assiduites_query.order_by(Assiduite.date_debut.desc()) + return assiduites_query diff --git a/app/api/justificatifs.py b/app/api/justificatifs.py index 5f66fa6030..3a2977d02e 100644 --- a/app/api/justificatifs.py +++ b/app/api/justificatifs.py @@ -696,4 +696,10 @@ def _filter_manager(requested, justificatifs_query): justificatifs_query, Justificatif, formsemestre ) + order = requested.args.get("order", None) + if order is not None: + justificatifs_query: Query = justificatifs_query.order_by( + Justificatif.date_debut.desc() + ) + return justificatifs_query diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py index a2c951480d..058c53efb6 100644 --- a/app/scodoc/sco_saisie_notes.py +++ b/app/scodoc/sco_saisie_notes.py @@ -1107,6 +1107,7 @@ def _get_sorted_etuds(evaluation: Evaluation, etudids: list, formsemestre_id: in evaluation.date_debut.date().isoformat() if evaluation.date_debut else "" ) warn_abs_lst = [] + # XXX TODO-ASSIDUITE (issue #686) if evaluation.is_matin(): nbabs = 0 # TODO-ASSIDUITE sco_abs.count_abs(etudid, jour_iso, jour_iso, matin=True) nbabsjust = 0 # TODO-ASSIDUITE sco_abs.count_abs_just(etudid, jour_iso, jour_iso, matin=True) diff --git a/app/static/js/assiduites.js b/app/static/js/assiduites.js index 73d872ec65..a62ebe20a5 100644 --- a/app/static/js/assiduites.js +++ b/app/static/js/assiduites.js @@ -1065,8 +1065,9 @@ function actualizeEtudAssiduite(etudid) { }); } -function getAllAssiduitesFromEtud(etudid, action) { - const url_api = getUrl() + `/api/assiduites/${etudid}`; +function getAllAssiduitesFromEtud(etudid, action, order = false) { + const url_api = + getUrl() + `/api/assiduites/${etudid}${order ? "/query?order" : ""}`; $.ajax({ async: true, @@ -1634,8 +1635,9 @@ function createJustificatif(justif, success = () => {}) { }); } -function getAllJustificatifsFromEtud(etudid, action) { - const url_api = getUrl() + `/api/justificatifs/${etudid}`; +function getAllJustificatifsFromEtud(etudid, action, order = false) { + const url_api = + getUrl() + `/api/justificatifs/${etudid}${order ? "/query?order" : ""}`; $.ajax({ async: true, type: "GET", diff --git a/app/templates/assiduites/widgets/tableau_base.j2 b/app/templates/assiduites/widgets/tableau_base.j2 index 5a795af1a9..f1180c6cee 100644 --- a/app/templates/assiduites/widgets/tableau_base.j2 +++ b/app/templates/assiduites/widgets/tableau_base.j2 @@ -150,7 +150,7 @@ paginationContainerAssiduites.querySelector('.pagination_moins').addEventListener('click', () => { if (currentPageAssiduites > 1) { currentPageAssiduites--; - paginationContainerAssiduites.querySelector('#paginationAssi').value = currentPageAssiduites + paginationContainerAssiduites.querySelector('#paginationAssi').value = currentPageAssiduites + "" assiduiteCallBack(array); } @@ -159,7 +159,7 @@ paginationContainerAssiduites.querySelector('.pagination_plus').addEventListener('click', () => { if (currentPageAssiduites < totalPages) { currentPageAssiduites++; - paginationContainerAssiduites.querySelector('#paginationAssi').value = currentPageAssiduites + paginationContainerAssiduites.querySelector('#paginationAssi').value = currentPageAssiduites + "" assiduiteCallBack(array); } }) @@ -199,8 +199,12 @@ if (assi) { paginationContainerAssiduites.querySelector('#paginationAssi').appendChild(paginationButton) + if (i == currentPageAssiduites) + paginationContainerAssiduites.querySelector('#paginationAssi').value = i + ""; } else { paginationContainerJustificatifs.querySelector('#paginationJusti').appendChild(paginationButton) + if (i == currentPageJustificatifs) + paginationContainerJustificatifs.querySelector('#paginationJusti').value = i + ""; } } updateActivePaginationButton(assi); @@ -230,8 +234,8 @@ } function loadAll() { - try { getAllAssiduitesFromEtud(etudid, assiduiteCallBack) } catch (_) { } - try { getAllJustificatifsFromEtud(etudid, justificatifCallBack) } catch (_) { } + try { getAllAssiduitesFromEtud(etudid, assiduiteCallBack, true) } catch (_) { } + try { getAllJustificatifsFromEtud(etudid, justificatifCallBack, true) } catch (_) { } } function order(keyword, callback = () => { }, el, assi = true) { diff --git a/tests/unit/test_assiduites.py b/tests/unit/test_assiduites.py index 0ed1bb506c..46016f267a 100644 --- a/tests/unit/test_assiduites.py +++ b/tests/unit/test_assiduites.py @@ -157,7 +157,6 @@ def test_general(test_client): editer_supprimer_justificatif(etuds[0]) -# XXX TODO-ASSIDUITE (issue #696) def verif_migration_abs_assiduites(): """Vérification que le script de migration fonctionne correctement""" downgrade_module(assiduites=True, justificatifs=True) From 6a5c594e683ac84c7a1dd6df74719592c215e346 Mon Sep 17 00:00:00 2001 From: iziram Date: Mon, 11 Sep 2023 08:34:56 +0200 Subject: [PATCH 35/97] Assiduites : fast justify plus court que assi fix #716 --- app/static/js/assiduites.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/app/static/js/assiduites.js b/app/static/js/assiduites.js index a62ebe20a5..20902c4a95 100644 --- a/app/static/js/assiduites.js +++ b/app/static/js/assiduites.js @@ -1559,12 +1559,8 @@ function fastJustify(assiduite) { //créer justificatif const justif = { - date_debut: new moment.tz(assiduite.date_debut, TIMEZONE) - .add(1, "s") - .format(), - date_fin: new moment.tz(assiduite.date_fin, TIMEZONE) - .subtract(1, "s") - .format(), + date_debut: new moment.tz(assiduite.date_debut, TIMEZONE).format(), + date_fin: new moment.tz(assiduite.date_fin, TIMEZONE).format(), raison: raison, etat: etat, }; From d0159df66526e0d57e0e069a14fd023f9801c9b9 Mon Sep 17 00:00:00 2001 From: iziram Date: Mon, 11 Sep 2023 09:05:29 +0200 Subject: [PATCH 36/97] Assiduites : edit assi fix est_just #717 --- app/api/assiduites.py | 22 +++++++++++++------ app/models/assiduites.py | 20 ++++++++++++----- .../assiduites/widgets/tableau_assi.j2 | 2 +- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/app/api/assiduites.py b/app/api/assiduites.py index ab568ae6c3..72bbbeda71 100644 --- a/app/api/assiduites.py +++ b/app/api/assiduites.py @@ -27,7 +27,7 @@ from app.models import ( Justificatif, ) from flask_sqlalchemy.query import Query -from app.models.assiduites import get_assiduites_justif +from app.models.assiduites import get_assiduites_justif, get_justifs_from_date from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_permissions import Permission from app.scodoc.sco_utils import json_error @@ -851,12 +851,20 @@ def _edit_singular(assiduite_unique, data): assiduite_unique.desc = desc # Cas 4 : est_just - est_just = data.get("est_just") - if est_just is not None: - if not isinstance(est_just, bool): - errors.append("param 'est_just' : booléen non reconnu") - else: - assiduite_unique.est_just = est_just + if assiduite_unique.etat == scu.EtatAssiduite.PRESENT: + assiduite_unique.est_just = False + else: + assiduite_unique.est_just = ( + len( + get_justifs_from_date( + assiduite_unique.etudiant.id, + assiduite_unique.date_debut, + assiduite_unique.date_fin, + valid=True, + ) + ) + > 0 + ) if errors: err: str = ", ".join(errors) diff --git a/app/models/assiduites.py b/app/models/assiduites.py index 0f8933e734..994a88e07b 100644 --- a/app/models/assiduites.py +++ b/app/models/assiduites.py @@ -134,7 +134,10 @@ class Assiduite(db.Model): if not est_just: est_just = ( - len(_get_assiduites_justif(etud.etudid, date_debut, date_fin)) > 0 + len( + get_justifs_from_date(etud.etudid, date_debut, date_fin, valid=True) + ) + > 0 ) if moduleimpl is not None: @@ -375,16 +378,23 @@ def compute_assiduites_justified( def get_assiduites_justif(assiduite_id: int, long: bool): assi: Assiduite = Assiduite.query.get_or_404(assiduite_id) - return _get_assiduites_justif(assi.etudid, assi.date_debut, assi.date_fin, long) + return get_justifs_from_date(assi.etudid, assi.date_debut, assi.date_fin, long) -def _get_assiduites_justif( - etudid: int, date_debut: datetime, date_fin: datetime, long: bool = False +def get_justifs_from_date( + etudid: int, + date_debut: datetime, + date_fin: datetime, + long: bool = False, + valid: bool = False, ): - justifs: Justificatif = Justificatif.query.filter( + justifs: Query = Justificatif.query.filter( Justificatif.etudid == etudid, Justificatif.date_debut <= date_debut, Justificatif.date_fin >= date_fin, ) + if valid: + justifs = justifs.filter(Justificatif.etat == EtatJustificatif.VALIDE) + return [j.justif_id if not long else j.to_dict(True) for j in justifs] diff --git a/app/templates/assiduites/widgets/tableau_assi.j2 b/app/templates/assiduites/widgets/tableau_assi.j2 index 78c34cf007..8ef6848a52 100644 --- a/app/templates/assiduites/widgets/tableau_assi.j2 +++ b/app/templates/assiduites/widgets/tableau_assi.j2 @@ -239,7 +239,7 @@ edit = setModuleImplId(edit, module); fullEditAssiduites(data.assiduite_id, edit, () => { - try { getAllAssiduitesFromEtud(etudid, assiduiteCallBack) } catch (_) { } + loadAll(); }) From c4c7e0ef806e04d6c904dd3a9925ffe0e34cd17f Mon Sep 17 00:00:00 2001 From: iziram Date: Mon, 11 Sep 2023 09:16:50 +0200 Subject: [PATCH 37/97] Assiduites : fix description assi #719 --- app/api/assiduites.py | 3 +-- app/templates/assiduites/widgets/tableau_assi.j2 | 2 +- app/templates/assiduites/widgets/tableau_justi.j2 | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/api/assiduites.py b/app/api/assiduites.py index 72bbbeda71..041bb9a0f5 100644 --- a/app/api/assiduites.py +++ b/app/api/assiduites.py @@ -728,7 +728,6 @@ def assiduite_edit(assiduite_id: int): assiduite_unique.etudiant.id, msg=f"assiduite: modif {assiduite_unique}", ) - db.session.add(assiduite_unique) db.session.commit() scass.simple_invalidate_cache(assiduite_unique.to_dict()) @@ -848,7 +847,7 @@ def _edit_singular(assiduite_unique, data): # Cas 3 : desc desc = data.get("desc", False) if desc is not False: - assiduite_unique.desc = desc + assiduite_unique.description = desc # Cas 4 : est_just if assiduite_unique.etat == scu.EtatAssiduite.PRESENT: diff --git a/app/templates/assiduites/widgets/tableau_assi.j2 b/app/templates/assiduites/widgets/tableau_assi.j2 index 8ef6848a52..fd1ee7df7e 100644 --- a/app/templates/assiduites/widgets/tableau_assi.j2 +++ b/app/templates/assiduites/widgets/tableau_assi.j2 @@ -147,7 +147,7 @@ ${etat}
            - Créer par + Créée par ${user}
    diff --git a/app/templates/assiduites/widgets/tableau_justi.j2 b/app/templates/assiduites/widgets/tableau_justi.j2 index c4b677733c..4b3a502289 100644 --- a/app/templates/assiduites/widgets/tableau_justi.j2 +++ b/app/templates/assiduites/widgets/tableau_justi.j2 @@ -169,7 +169,7 @@ ${etat}
    - Créer par + Créé par ${user}
    From 044926fd62819d818cc0de68bdcaa8db172a36f6 Mon Sep 17 00:00:00 2001 From: iziram Date: Mon, 11 Sep 2023 10:45:21 +0200 Subject: [PATCH 38/97] Assiduites : saisie sur mobile #723 --- app/templates/assiduites/widgets/timeline.j2 | 138 +++++++++++-------- 1 file changed, 80 insertions(+), 58 deletions(-) diff --git a/app/templates/assiduites/widgets/timeline.j2 b/app/templates/assiduites/widgets/timeline.j2 index 706c5f504b..576e30218b 100644 --- a/app/templates/assiduites/widgets/timeline.j2 +++ b/app/templates/assiduites/widgets/timeline.j2 @@ -18,6 +18,8 @@ const period_default = {{ periode_defaut }}; + let handleMoving = false; + function createTicks() { let i = t_start; @@ -87,72 +89,92 @@ } - function setupTimeLine(callback) { + function timelineMainEvent(event, callback) { const func_call = callback ? callback : () => { }; - timelineContainer.addEventListener("mousedown", (event) => { - const startX = event.clientX; - if (event.target === periodTimeLine) { - const startLeft = parseFloat(periodTimeLine.style.left); + const startX = (event.clientX || event.changedTouches[0].clientX); - const onMouseMove = (moveEvent) => { - const deltaX = moveEvent.clientX - startX; - const containerWidth = timelineContainer.clientWidth; + if (event.target.classList.contains("period-handle")) { + const startWidth = parseFloat(periodTimeLine.style.width); + const startLeft = parseFloat(periodTimeLine.style.left); + const isLeftHandle = event.target.classList.contains("left"); + handleMoving = true + const onMouseMove = (moveEvent) => { + + if (!handleMoving) return; + + const deltaX = (moveEvent.clientX || moveEvent.changedTouches[0].clientX) - startX; + const containerWidth = timelineContainer.clientWidth; + const newWidth = + startWidth + ((isLeftHandle ? -deltaX : deltaX) / containerWidth) * 100; + + if (isLeftHandle) { const newLeft = startLeft + (deltaX / containerWidth) * 100; + adjustPeriodPosition(newLeft, newWidth); + } else { + adjustPeriodPosition(parseFloat(periodTimeLine.style.left), newWidth); + } - adjustPeriodPosition(newLeft, parseFloat(periodTimeLine.style.width)); + updatePeriodTimeLabel(); + }; + const mouseUp = () => { + snapHandlesToQuarters(); + generateAllEtudRow(); - updatePeriodTimeLabel(); - }; + timelineContainer.removeEventListener("mousemove", onMouseMove); + handleMoving = false; + func_call(); - document.addEventListener("mousemove", onMouseMove); - document.addEventListener( - "mouseup", - () => { - generateAllEtudRow(); - snapHandlesToQuarters(); - document.removeEventListener("mousemove", onMouseMove); - func_call(); - }, - { once: true } - ); - } else if (event.target.classList.contains("period-handle")) { - const startWidth = parseFloat(periodTimeLine.style.width); - const startLeft = parseFloat(periodTimeLine.style.left); - const isLeftHandle = event.target.classList.contains("left"); - - const onMouseMove = (moveEvent) => { - const deltaX = moveEvent.clientX - startX; - const containerWidth = timelineContainer.clientWidth; - const newWidth = - startWidth + ((isLeftHandle ? -deltaX : deltaX) / containerWidth) * 100; - - if (isLeftHandle) { - const newLeft = startLeft + (deltaX / containerWidth) * 100; - adjustPeriodPosition(newLeft, newWidth); - } else { - adjustPeriodPosition(parseFloat(periodTimeLine.style.left), newWidth); - } - - updatePeriodTimeLabel(); - }; - - document.addEventListener("mousemove", onMouseMove); - document.addEventListener( - "mouseup", - () => { - snapHandlesToQuarters(); - generateAllEtudRow(); - - document.removeEventListener("mousemove", onMouseMove); - - func_call(); - - }, - { once: true } - ); } - }); + timelineContainer.addEventListener("mousemove", onMouseMove); + timelineContainer.addEventListener("touchmove", onMouseMove); + document.addEventListener( + "mouseup", + mouseUp, + ); + document.addEventListener( + "touchend", + mouseUp, + ); + } else if (event.target === periodTimeLine) { + + const startLeft = parseFloat(periodTimeLine.style.left); + + const onMouseMove = (moveEvent) => { + console.warn("move Period") + if (handleMoving) return; + const deltaX = (moveEvent.clientX || moveEvent.changedTouches[0].clientX) - startX; + const containerWidth = timelineContainer.clientWidth; + const newLeft = startLeft + (deltaX / containerWidth) * 100; + + adjustPeriodPosition(newLeft, parseFloat(periodTimeLine.style.width)); + + updatePeriodTimeLabel(); + }; + const mouseUp = () => { + generateAllEtudRow(); + snapHandlesToQuarters(); + timelineContainer.removeEventListener("mousemove", onMouseMove); + func_call(); + } + timelineContainer.addEventListener("mousemove", onMouseMove); + timelineContainer.addEventListener("touchmove", onMouseMove); + document.addEventListener( + "mouseup", + mouseUp, + { once: true } + ); + document.addEventListener( + "touchend", + mouseUp, + { once: true } + ); + } + } + + function setupTimeLine(callback) { + timelineContainer.addEventListener("mousedown", (e) => { timelineMainEvent(e, callback) }); + timelineContainer.addEventListener("touchstart", (e) => { timelineMainEvent(e, callback) }); } function adjustPeriodPosition(newLeft, newWidth) { From ba113f9cccd62ed10853776817a0538f2295b309 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 11 Sep 2023 07:11:52 +0200 Subject: [PATCH 39/97] Fix sco_archives_justificatifs --- app/scodoc/sco_archives_justificatifs.py | 1 - sco_version.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/scodoc/sco_archives_justificatifs.py b/app/scodoc/sco_archives_justificatifs.py index 1ca486b375..14a779bf8b 100644 --- a/app/scodoc/sco_archives_justificatifs.py +++ b/app/scodoc/sco_archives_justificatifs.py @@ -144,7 +144,6 @@ class JustificatifArchiver(BaseArchiver): Si trace == True : sauvegarde le nom du/des fichier(s) supprimé(s) dans la trace de l'étudiant """ - if str(etud.id) not in self.list_oids(etud.dept_id): raise ValueError(f"Aucune archive pour etudid[{etud.id}]") diff --git a/sco_version.py b/sco_version.py index f10bb0ce64..a44e5b0a14 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.26" +SCOVERSION = "9.6.27" SCONAME = "ScoDoc" From 740a7defacc14cbd8869c90cf254797a292a245c Mon Sep 17 00:00:00 2001 From: iziram Date: Mon, 11 Sep 2023 11:11:00 +0200 Subject: [PATCH 40/97] Assiduites : index find etud renvoi sur bilan etud #709 --- app/views/assiduites.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/views/assiduites.py b/app/views/assiduites.py index 654873b0a7..3736d45f3f 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -162,18 +162,18 @@ def index_html(): """

    Pour signaler, annuler ou justifier une assiduité pour un seul étudiant, choisissez d'abord le concerné:

    """ ) - H.append(sco_find_etud.form_search_etud()) - # if current_user.has_permission( - # Permission.ScoAbsChange - # ) and sco_preferences.get_preference("handle_billets_abs"): - # H.append( - # f""" - #

    Billets d'absence

    - # - # """ - # ) + H.append(sco_find_etud.form_search_etud(dest_url="assiduites.bilan_etud")) + if current_user.has_permission( + Permission.ScoAbsChange + ) and sco_preferences.get_preference("handle_billets_abs"): + H.append( + f""" +

    Billets d'absence

    + + """ + ) H.append( render_template( From b27cf5198f3864abd01ff76c265e02bbd2e96bed Mon Sep 17 00:00:00 2001 From: iziram Date: Mon, 11 Sep 2023 11:30:43 +0200 Subject: [PATCH 41/97] =?UTF-8?q?Assiduites=20:=20Am=C3=A9lioration=20cont?= =?UTF-8?q?raste=20bouton=20=C3=A9tat=20#708?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/static/css/assiduites.css | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/static/css/assiduites.css b/app/static/css/assiduites.css index 10031d7d5c..206cdc56e7 100644 --- a/app/static/css/assiduites.css +++ b/app/static/css/assiduites.css @@ -268,6 +268,7 @@ background-size: cover; } + .rbtn.present::before { background-image: url(../icons/present.svg); } @@ -285,8 +286,8 @@ } .rbtn:checked:before { - outline: 3px solid #7059FF; - border-radius: 5px; + outline: 5px solid #7059FF; + border-radius: 50%; } .rbtn:focus { From 673179c8f7cc8879ec6a36e1e93567e13e0ed0cf Mon Sep 17 00:00:00 2001 From: iziram Date: Mon, 11 Sep 2023 15:55:18 +0200 Subject: [PATCH 42/97] =?UTF-8?q?Assiduit=C3=A9s=20:=20issue=20712=20WIP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_assiduites.py | 2 +- app/static/css/assiduites.css | 2 ++ app/static/js/assiduites.js | 22 +++++++++---- app/templates/assiduites/pages/calendrier.j2 | 2 ++ .../assiduites/pages/liste_assiduites.j2 | 32 ++++++++++++++++++ .../assiduites/widgets/minitimeline.j2 | 5 +++ .../assiduites/widgets/tableau_base.j2 | 33 +++++++++++++++++-- app/views/assiduites.py | 3 ++ 8 files changed, 91 insertions(+), 10 deletions(-) diff --git a/app/scodoc/sco_assiduites.py b/app/scodoc/sco_assiduites.py index 3990cd82fd..1ad5332d40 100644 --- a/app/scodoc/sco_assiduites.py +++ b/app/scodoc/sco_assiduites.py @@ -311,7 +311,7 @@ def filter_by_date( ) -def filter_justificatifs_by_etat(justificatifs: Justificatif, etat: str) -> Query: +def filter_justificatifs_by_etat(justificatifs: Query, etat: str) -> Query: """ Filtrage d'une collection de justificatifs en fonction de leur état """ diff --git a/app/static/css/assiduites.css b/app/static/css/assiduites.css index 206cdc56e7..2d07244633 100644 --- a/app/static/css/assiduites.css +++ b/app/static/css/assiduites.css @@ -136,6 +136,8 @@ flex-direction: column; align-items: flex-start; margin: 0 5%; + + cursor: pointer; } .etud_row.def .nom::after, diff --git a/app/static/js/assiduites.js b/app/static/js/assiduites.js index 20902c4a95..1456f67142 100644 --- a/app/static/js/assiduites.js +++ b/app/static/js/assiduites.js @@ -1065,9 +1065,17 @@ function actualizeEtudAssiduite(etudid) { }); } -function getAllAssiduitesFromEtud(etudid, action, order = false) { +function getAllAssiduitesFromEtud( + etudid, + action, + order = false, + justifs = false +) { const url_api = - getUrl() + `/api/assiduites/${etudid}${order ? "/query?order" : ""}`; + getUrl() + + `/api/assiduites/${etudid}${ + order ? "/query?order%".replace("%", justifs ? "&with_justifs" : "") : "" + }`; $.ajax({ async: true, @@ -1241,12 +1249,10 @@ function generateEtudRow( - +
    @@ -1567,8 +1573,10 @@ function fastJustify(assiduite) { createJustificatif(justif); - // justifyAssiduite(assiduite.assiduite_id, true); generateAllEtudRow(); + try { + loadAll(); + } catch {} }; const content = document.createElement("fieldset"); diff --git a/app/templates/assiduites/pages/calendrier.j2 b/app/templates/assiduites/pages/calendrier.j2 index ce8e9e0f1f..3f8099342e 100644 --- a/app/templates/assiduites/pages/calendrier.j2 +++ b/app/templates/assiduites/pages/calendrier.j2 @@ -354,5 +354,7 @@ setterAnnee(defAnnee) }; + + function isCalendrier() { return true } {% endblock pageContent %} \ No newline at end of file diff --git a/app/templates/assiduites/pages/liste_assiduites.j2 b/app/templates/assiduites/pages/liste_assiduites.j2 index d18021806a..80e14a6d67 100644 --- a/app/templates/assiduites/pages/liste_assiduites.j2 +++ b/app/templates/assiduites/pages/liste_assiduites.j2 @@ -48,8 +48,40 @@ \ No newline at end of file diff --git a/app/templates/assiduites/widgets/minitimeline.j2 b/app/templates/assiduites/widgets/minitimeline.j2 index f3febd833b..835cb30842 100644 --- a/app/templates/assiduites/widgets/minitimeline.j2 +++ b/app/templates/assiduites/widgets/minitimeline.j2 @@ -71,6 +71,11 @@ updateSelectedSelect(getCurrentAssiduiteModuleImplId()); updateJustifyBtn(); } + try { + if (isCalendrier()) { + window.location = `ListeAssiduitesEtud?etudid=${etudid}&assiduite_id=${assiduité.assiduite_id}` + } + } catch { } }); //ajouter affichage assiduites on over setupAssiduiteBuble(block, assiduité); diff --git a/app/templates/assiduites/widgets/tableau_base.j2 b/app/templates/assiduites/widgets/tableau_base.j2 index f1180c6cee..2dec4a2cea 100644 --- a/app/templates/assiduites/widgets/tableau_base.j2 +++ b/app/templates/assiduites/widgets/tableau_base.j2 @@ -18,6 +18,9 @@ document.addEventListener("click", () => { contextMenu.style.display = "none"; + if (contextMenu.childElementCount > 3) { + contextMenu.removeChild(contextMenu.lastElementChild) + } }); editOption.addEventListener("click", () => { @@ -94,6 +97,11 @@ } } + if (k == "obj_id") { + const obj_id = el.assiduite_id || el.justif_id; + return f.obj_id.includes(obj_id) + } + return true; }) @@ -234,8 +242,8 @@ } function loadAll() { - try { getAllAssiduitesFromEtud(etudid, assiduiteCallBack, true) } catch (_) { } - try { getAllJustificatifsFromEtud(etudid, justificatifCallBack, true) } catch (_) { } + try { getAllAssiduitesFromEtud(etudid, assiduiteCallBack, true, true) } catch (_) { } + try { getAllJustificatifsFromEtud(etudid, justificatifCallBack, true, true) } catch (_) { } } function order(keyword, callback = () => { }, el, assi = true) { @@ -645,6 +653,27 @@ contextMenu.style.top = `${e.clientY - contextMenu.offsetHeight}px`; contextMenu.style.left = `${e.clientX}px`; contextMenu.style.display = "block"; + if (contextMenu.childElementCount > 3) { + contextMenu.removeChild(contextMenu.lastElementChild) + } + if (selectedRow.getAttribute('type') == "assiduite") { + + const li = document.createElement('li') + li.textContent = "Justifier" + + li.addEventListener('click', () => { + let obj_id = selectedRow.getAttribute('obj_id'); + assiduite = Object.values(assiduites).flat().filter((a) => { return a.assiduite_id == obj_id }) + console.log(assiduite[0]) + if (assiduite && !assiduite[0].est_just && assiduite[0].etat != "PRESENT") { + fastJustify(assiduite[0]) + } else { + openAlertModal("Erreur", document.createTextNode("L'assiduité est déjà justifiée ou ne peut pas l'être.")) + } + }) + + contextMenu.appendChild(li) + } } diff --git a/app/views/assiduites.py b/app/views/assiduites.py index 3736d45f3f..b119705415 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -299,6 +299,8 @@ def liste_assiduites_etud(): if etud.dept_id != g.scodoc_dept_id: abort(404, "étudiant inexistant dans ce département") + assiduite_id: int = request.args.get("assiduite_id", -1) + header: str = html_sco_header.sco_header( page_title="Liste des assiduités", init_qtip=True, @@ -319,6 +321,7 @@ def liste_assiduites_etud(): "assiduites/pages/liste_assiduites.j2", sco=ScoData(etud), date=datetime.date.today().isoformat(), + assi_id=assiduite_id, ), ).build() From 190f1de97435848cfc6fc4c094ee213e2b42799a Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 11 Sep 2023 17:58:12 +0200 Subject: [PATCH 43/97] Tri responsable formsemestre par nom. --- app/models/formsemestre.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index e29ffbfc31..8edc88a320 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -18,6 +18,7 @@ from flask_login import current_user from flask import flash, g, url_for from sqlalchemy.sql import text +from sqlalchemy import func import app.scodoc.sco_utils as scu from app import db, log @@ -138,6 +139,7 @@ class FormSemestre(db.Model): secondary="notes_formsemestre_responsables", lazy=True, backref=db.backref("formsemestres", lazy=True), + order_by=func.upper(User.nom), ) partitions = db.relationship( "Partition", From 306edaf63d1181cfac555149d40b38de4f09aacd Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 11 Sep 2023 17:58:12 +0200 Subject: [PATCH 44/97] Tri responsable formsemestre par nom. --- app/models/formsemestre.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index e29ffbfc31..8edc88a320 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -18,6 +18,7 @@ from flask_login import current_user from flask import flash, g, url_for from sqlalchemy.sql import text +from sqlalchemy import func import app.scodoc.sco_utils as scu from app import db, log @@ -138,6 +139,7 @@ class FormSemestre(db.Model): secondary="notes_formsemestre_responsables", lazy=True, backref=db.backref("formsemestres", lazy=True), + order_by=func.upper(User.nom), ) partitions = db.relationship( "Partition", From 1ec07a9329966bdbeb12070fe03932743c4e3b28 Mon Sep 17 00:00:00 2001 From: iziram Date: Tue, 12 Sep 2023 09:37:03 +0200 Subject: [PATCH 45/97] Assiduites : fin issue #712 --- app/api/assiduites.py | 9 ++++++++ app/api/justificatifs.py | 9 ++++++++ app/scodoc/sco_preferences.py | 11 +++++++++ app/static/js/assiduites.js | 21 +++++++++++++---- .../assiduites/pages/ajout_justificatif.j2 | 3 +++ app/templates/assiduites/pages/bilan_dept.j2 | 20 ++++++++++------ app/templates/assiduites/pages/bilan_etud.j2 | 3 +++ .../assiduites/pages/liste_assiduites.j2 | 3 +++ .../assiduites/widgets/tableau_base.j2 | 4 ++-- app/views/assiduites.py | 23 +++++++++++++++++++ 10 files changed, 93 insertions(+), 13 deletions(-) diff --git a/app/api/assiduites.py b/app/api/assiduites.py index 041bb9a0f5..dd2be3c9ca 100644 --- a/app/api/assiduites.py +++ b/app/api/assiduites.py @@ -1035,6 +1035,15 @@ def _filter_manager(requested, assiduites_query: Query) -> Query: if order is not None: assiduites_query: Query = assiduites_query.order_by(Assiduite.date_debut.desc()) + courant = requested.args.get("courant", None) + if courant is not None: + annee: int = scu.annee_scolaire() + + assiduites_query: Query = assiduites_query.filter( + Assiduite.date_debut >= scu.date_debut_anne_scolaire(annee), + Assiduite.date_fin <= scu.date_fin_anne_scolaire(annee), + ) + return assiduites_query diff --git a/app/api/justificatifs.py b/app/api/justificatifs.py index 3a2977d02e..9c1fd00105 100644 --- a/app/api/justificatifs.py +++ b/app/api/justificatifs.py @@ -702,4 +702,13 @@ def _filter_manager(requested, justificatifs_query): Justificatif.date_debut.desc() ) + courant = requested.args.get("courant", None) + if courant is not None: + annee: int = scu.annee_scolaire() + + justificatifs_query: Query = justificatifs_query.filter( + Justificatif.date_debut >= scu.date_debut_anne_scolaire(annee), + Justificatif.date_fin <= scu.date_fin_anne_scolaire(annee), + ) + return justificatifs_query diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py index e7514ed80d..d3cd1d6bff 100644 --- a/app/scodoc/sco_preferences.py +++ b/app/scodoc/sco_preferences.py @@ -610,6 +610,17 @@ class BasePreferences: }, ), # Assiduités + ( + "assi_limit_annee", + { + "initvalue": 1, + "title": "Ne lister que les assiduités de l'année", + "explanation": "Limite l'affichage des listes d'assiduités et de justificatifs à l'année en cours", + "input_type": "boolcheckbox", + "labels": ["non", "oui"], + "category": "assi", + }, + ), ( "forcer_module", { diff --git a/app/static/js/assiduites.js b/app/static/js/assiduites.js index 1456f67142..019cb894a2 100644 --- a/app/static/js/assiduites.js +++ b/app/static/js/assiduites.js @@ -1069,12 +1069,17 @@ function getAllAssiduitesFromEtud( etudid, action, order = false, - justifs = false + justifs = false, + courant = false ) { const url_api = getUrl() + `/api/assiduites/${etudid}${ - order ? "/query?order%".replace("%", justifs ? "&with_justifs" : "") : "" + order + ? "/query?order%°" + .replace("%", justifs ? "&with_justifs" : "") + .replace("°", courant ? "&courant" : "") + : "" }`; $.ajax({ @@ -1639,9 +1644,17 @@ function createJustificatif(justif, success = () => {}) { }); } -function getAllJustificatifsFromEtud(etudid, action, order = false) { +function getAllJustificatifsFromEtud( + etudid, + action, + order = false, + courant = false +) { const url_api = - getUrl() + `/api/justificatifs/${etudid}${order ? "/query?order" : ""}`; + getUrl() + + `/api/justificatifs/${etudid}${ + order ? "/query?order°".replace("°", courant ? "&courant" : "") : "" + }`; $.ajax({ async: true, type: "GET", diff --git a/app/templates/assiduites/pages/ajout_justificatif.j2 b/app/templates/assiduites/pages/ajout_justificatif.j2 index 4cbc8f590a..878d2fa064 100644 --- a/app/templates/assiduites/pages/ajout_justificatif.j2 +++ b/app/templates/assiduites/pages/ajout_justificatif.j2 @@ -221,6 +221,9 @@ const etudid = {{ sco.etud.id }}; + + const assi_limit_annee = "{{ assi_limit_annee }}" == "True" ? true : false; + window.onload = () => { loadAll(); } diff --git a/app/templates/assiduites/pages/bilan_dept.j2 b/app/templates/assiduites/pages/bilan_dept.j2 index aea0bb8b06..2e8ca5be96 100644 --- a/app/templates/assiduites/pages/bilan_dept.j2 +++ b/app/templates/assiduites/pages/bilan_dept.j2 @@ -29,6 +29,7 @@
    {% endblock pageContent %} \ No newline at end of file diff --git a/app/views/assiduites.py b/app/views/assiduites.py index 55b96e4703..c40345c9bb 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -174,7 +174,7 @@ def index_html(): """ ) - dept : Departement = Departement.query.filter_by(id=g.scodoc_dept_id).first() + 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, @@ -438,6 +438,8 @@ def ajout_justificatif_etud(): "assi_limit_annee", dept_id=g.scodoc_dept_id, ), + assi_morning=ScoDocSiteConfig.get("assi_morning_time", "08:00"), + assi_evening=ScoDocSiteConfig.get("assi_evening_time", "18:00"), ), ).build() From 602b5084679c02fbf2df0aea49d1a8edb92e0e26 Mon Sep 17 00:00:00 2001 From: iziram Date: Tue, 12 Sep 2023 13:40:03 +0200 Subject: [PATCH 47/97] Assiduites : route formsemestre justificatifs --- app/api/justificatifs.py | 42 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/app/api/justificatifs.py b/app/api/justificatifs.py index 9c1fd00105..099d9e6c53 100644 --- a/app/api/justificatifs.py +++ b/app/api/justificatifs.py @@ -151,6 +151,48 @@ def justificatifs_dept(dept_id: int = None, with_query: bool = False): return data_set +@bp.route( + "/justificatifs/formsemestre/", defaults={"with_query": False} +) +@api_web_bp.route( + "/justificatifs/formsemestre/", defaults={"with_query": False} +) +@bp.route( + "/justificatifs/formsemestre//query", + defaults={"with_query": True}, +) +@api_web_bp.route( + "/justificatifs/formsemestre//query", + defaults={"with_query": True}, +) +@login_required +@scodoc +@as_json +@permission_required(Permission.ScoView) +def justificatifs_formsemestre(formsemestre_id: int, with_query: bool = False): + """Retourne tous les justificatifs du formsemestre""" + 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") + + justificatifs_query = scass.filter_by_formsemestre( + Justificatif.query, Justificatif, formsemestre + ) + + if with_query: + justificatifs_query = _filter_manager(request, justificatifs_query) + + data_set: list[dict] = [] + for justi in justificatifs_query.all(): + data = justi.to_dict(format_api=True) + data_set.append(data) + + return data_set + + @bp.route("/justificatif//create", methods=["POST"]) @api_web_bp.route("/justificatif//create", methods=["POST"]) @bp.route("/justificatif/etudid//create", methods=["POST"]) From 996e83f3b3ed55028c86b2df3db928b8a607e344 Mon Sep 17 00:00:00 2001 From: iziram Date: Tue, 12 Sep 2023 14:58:01 +0200 Subject: [PATCH 48/97] =?UTF-8?q?Assiduit=C3=A9s=20:=20publication=20api?= =?UTF-8?q?=20justificatif=20route=20dept?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/justificatifs.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/api/justificatifs.py b/app/api/justificatifs.py index 099d9e6c53..e68b397ac3 100644 --- a/app/api/justificatifs.py +++ b/app/api/justificatifs.py @@ -130,6 +130,8 @@ def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = Fal @api_web_bp.route( "/justificatifs/dept//query", defaults={"with_query": True} ) +@bp.route("/justificatifs/dept/", defaults={"with_query": False}) +@bp.route("/justificatifs/dept//query", defaults={"with_query": True}) @login_required @scodoc @as_json From 23e189c21a8db0f2811b85cd9d62844ae12c114a Mon Sep 17 00:00:00 2001 From: iziram Date: Sun, 10 Sep 2023 22:11:10 +0200 Subject: [PATCH 49/97] =?UTF-8?q?Assiduit=C3=A9s=20:=20correction=20delete?= =?UTF-8?q?=5Fjustificatif=20sco=5Farchives=5Fjustificatifs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_archives_justificatifs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/scodoc/sco_archives_justificatifs.py b/app/scodoc/sco_archives_justificatifs.py index 14a779bf8b..1ca486b375 100644 --- a/app/scodoc/sco_archives_justificatifs.py +++ b/app/scodoc/sco_archives_justificatifs.py @@ -144,6 +144,7 @@ class JustificatifArchiver(BaseArchiver): Si trace == True : sauvegarde le nom du/des fichier(s) supprimé(s) dans la trace de l'étudiant """ + if str(etud.id) not in self.list_oids(etud.dept_id): raise ValueError(f"Aucune archive pour etudid[{etud.id}]") From e4cf023e464579fc783d39b342c0c083ace70dfd Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 11 Sep 2023 07:11:52 +0200 Subject: [PATCH 50/97] Fix sco_archives_justificatifs --- app/scodoc/sco_archives_justificatifs.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app/scodoc/sco_archives_justificatifs.py b/app/scodoc/sco_archives_justificatifs.py index 1ca486b375..14a779bf8b 100644 --- a/app/scodoc/sco_archives_justificatifs.py +++ b/app/scodoc/sco_archives_justificatifs.py @@ -144,7 +144,6 @@ class JustificatifArchiver(BaseArchiver): Si trace == True : sauvegarde le nom du/des fichier(s) supprimé(s) dans la trace de l'étudiant """ - if str(etud.id) not in self.list_oids(etud.dept_id): raise ValueError(f"Aucune archive pour etudid[{etud.id}]") From dc4eb63afe3cfef4ecfd4321d027e15f65621d44 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 12 Sep 2023 19:57:39 +0200 Subject: [PATCH 51/97] Fix some type annotations --- app/but/jury_but.py | 3 +-- app/but/rcue.py | 3 +-- app/models/assiduites.py | 6 +++--- app/scodoc/sco_archives.py | 3 +-- app/scodoc/sco_assiduites.py | 25 +++++++++++-------------- app/views/assiduites.py | 8 ++++---- tools/migrate_abs_to_assiduites.py | 3 +-- 7 files changed, 22 insertions(+), 29 deletions(-) diff --git a/app/but/jury_but.py b/app/but/jury_but.py index 4eb7525f15..22cbc070a4 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -61,7 +61,6 @@ DecisionsProposeesUE: décisions de jury sur une UE du BUT from datetime import datetime import html import re -from typing import Union import numpy as np from flask import flash, g, url_for @@ -150,7 +149,7 @@ class DecisionsProposees: def __init__( self, etud: Identite = None, - code: Union[str, list[str]] = None, + code: str | list[str] | None = None, explanation="", code_valide=None, include_communs=True, diff --git a/app/but/rcue.py b/app/but/rcue.py index b7cf2711e5..5a5fce3ea9 100644 --- a/app/but/rcue.py +++ b/app/but/rcue.py @@ -6,7 +6,6 @@ """Jury BUT: un RCUE, ou Regroupe Cohérent d'UEs """ -from typing import Union from flask_sqlalchemy.query import Query from app.comp.res_but import ResultatsSemestreBUT @@ -205,7 +204,7 @@ class RegroupementCoherentUE: self.moy_rcue > codes_cursus.BUT_BARRE_RCUE ) - def code_valide(self) -> Union[ApcValidationRCUE, None]: + def code_valide(self) -> ApcValidationRCUE | None: "Si ce RCUE est ADM, CMP ou ADJ, la validation. Sinon, None" validation = self.query_validations().first() if (validation is not None) and ( diff --git a/app/models/assiduites.py b/app/models/assiduites.py index 994a88e07b..ca787e980d 100644 --- a/app/models/assiduites.py +++ b/app/models/assiduites.py @@ -123,7 +123,7 @@ class Assiduite(db.Model): user_id: int = None, est_just: bool = False, external_data: dict = None, - ) -> object or int: + ) -> "Assiduite": """Créer une nouvelle assiduité pour l'étudiant""" # Vérification de non duplication des périodes assiduites: Query = etud.assiduites @@ -285,7 +285,7 @@ class Justificatif(db.Model): entry_date: datetime = None, user_id: int = None, external_data: dict = None, - ) -> object or int: + ) -> "Justificatif": """Créer un nouveau justificatif pour l'étudiant""" nouv_justificatif = Justificatif( date_debut=date_debut, @@ -313,7 +313,7 @@ def is_period_conflicting( date_debut: datetime, date_fin: datetime, collection: Query, - collection_cls: Assiduite or Justificatif, + collection_cls: Assiduite | Justificatif, ) -> bool: """ Vérifie si une date n'entre pas en collision diff --git a/app/scodoc/sco_archives.py b/app/scodoc/sco_archives.py index 51bba4b208..9c05c6535b 100644 --- a/app/scodoc/sco_archives.py +++ b/app/scodoc/sco_archives.py @@ -47,7 +47,6 @@ nommé _description.txt qui est une description (humaine, format libre) de l'archive. """ -from typing import Union import datetime import glob import json @@ -253,7 +252,7 @@ class BaseArchiver: self, archive_id: str, filename: str, - data: Union[str, bytes], + data: str | bytes, dept_id: int = None, ): """Store data in archive, under given filename. diff --git a/app/scodoc/sco_assiduites.py b/app/scodoc/sco_assiduites.py index 1ad5332d40..c80a5f55e9 100644 --- a/app/scodoc/sco_assiduites.py +++ b/app/scodoc/sco_assiduites.py @@ -141,12 +141,9 @@ class CountCalculator: self.hours += finish_hours.total_seconds() / 3600 self.hours += self.hour_per_day - (start_hours.total_seconds() / 3600) - def compute_assiduites(self, assiduites: Query or list): + def compute_assiduites(self, assiduites: Query | list): """Calcule les métriques pour la collection d'assiduité donnée""" assi: Assiduite - assiduites: list[Assiduite] = ( - assiduites.all() if isinstance(assiduites, Query) else assiduites - ) for assi in assiduites: self.count += 1 delta: timedelta = assi.date_fin - assi.date_debut @@ -167,7 +164,7 @@ class CountCalculator: self.hours += delta.total_seconds() / 3600 - def to_dict(self) -> dict[str, int or float]: + def to_dict(self) -> dict[str, int | float]: """Retourne les métriques sous la forme d'un dictionnaire""" return { "compte": self.count, @@ -179,7 +176,7 @@ class CountCalculator: def get_assiduites_stats( assiduites: Query, metric: str = "all", filtered: dict[str, object] = None -) -> dict[str, int or float]: +) -> dict[str, int | float]: """Compte les assiduités en fonction des filtres""" if filtered is not None: @@ -276,7 +273,7 @@ def filter_assiduites_by_est_just(assiduites: Assiduite, est_just: bool) -> Quer def filter_by_user_id( - collection: Assiduite or Justificatif, + collection: Assiduite | Justificatif, user_id: int, ) -> Query: """ @@ -286,8 +283,8 @@ def filter_by_user_id( def filter_by_date( - collection: Assiduite or Justificatif, - collection_cls: Assiduite or Justificatif, + collection: Assiduite | Justificatif, + collection_cls: Assiduite | Justificatif, date_deb: datetime = None, date_fin: datetime = None, strict: bool = False, @@ -320,7 +317,7 @@ def filter_justificatifs_by_etat(justificatifs: Query, etat: str) -> Query: return justificatifs.filter(Justificatif.etat.in_(etats)) -def filter_by_module_impl(assiduites: Assiduite, module_impl_id: int or None) -> Query: +def filter_by_module_impl(assiduites: Assiduite, module_impl_id: int | None) -> Query: """ Filtrage d'une collection d'assiduites en fonction de l'ID du module_impl """ @@ -328,8 +325,8 @@ def filter_by_module_impl(assiduites: Assiduite, module_impl_id: int or None) -> def filter_by_formsemestre( - collection_query: Assiduite or Justificatif, - collection_class: Assiduite or Justificatif, + collection_query: Assiduite | Justificatif, + collection_class: Assiduite | Justificatif, formsemestre: FormSemestre, ) -> Query: """ @@ -358,7 +355,7 @@ def filter_by_formsemestre( return collection_result.filter(collection_class.date_fin <= form_date_fin) -def justifies(justi: Justificatif, obj: bool = False) -> list[int] or Query: +def justifies(justi: Justificatif, obj: bool = False) -> list[int] | Query: """ Retourne la liste des assiduite_id qui sont justifié par la justification Une assiduité est justifiée si elle est COMPLETEMENT ou PARTIELLEMENT @@ -597,7 +594,7 @@ def invalidate_assiduites_etud_date(etudid, date: datetime): invalidate_assiduites_count(etudid, sem) -def simple_invalidate_cache(obj: dict, etudid: str or int = None): +def simple_invalidate_cache(obj: dict, etudid: str | int = None): """Invalide le cache de l'étudiant et du / des semestres""" date_debut = ( obj["date_debut"] diff --git a/app/views/assiduites.py b/app/views/assiduites.py index c40345c9bb..a2e5a3b1d7 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -109,13 +109,13 @@ class HTMLStringElement(HTMLElement): class HTMLBuilder: - def __init__(self, *content: HTMLElement or str) -> None: - self.content: list[HTMLElement or str] = list(content) + def __init__(self, *content: HTMLElement | str) -> None: + self.content: list[HTMLElement | str] = list(content) - def add(self, *element: HTMLElement or str): + def add(self, *element: HTMLElement | str): self.content.extend(element) - def remove(self, element: HTMLElement or str): + def remove(self, element: HTMLElement | str): if element in self.content: self.content.remove(element) diff --git a/tools/migrate_abs_to_assiduites.py b/tools/migrate_abs_to_assiduites.py index 68e2cab98e..87c799dfff 100644 --- a/tools/migrate_abs_to_assiduites.py +++ b/tools/migrate_abs_to_assiduites.py @@ -152,7 +152,6 @@ class _Merger: def export(self): """Génère un nouvel objet Assiduité ou Justificatif""" - obj: Assiduite or Justificatif = None if self.est_abs: _glob.COMPTE[0] += 1 self._to_assi() @@ -167,7 +166,7 @@ def _assi_in_justifs(deb, fin, etudid): class _Statistics: def __init__(self) -> None: - self.object: dict[str, dict or int] = {"total": 0} + self.object: dict[str, dict | int] = {"total": 0} self.year: int = None def __set_year(self, year: int): From 1cbdc97680541f93ae31018fd0457490f378d49f Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 12 Sep 2023 20:09:51 +0200 Subject: [PATCH 52/97] Upgrade pip packages in venv --- requirements-3.11.txt | 59 +++++++++++++++---------- requirements-3.9.txt | 100 ------------------------------------------ 2 files changed, 37 insertions(+), 122 deletions(-) delete mode 100755 requirements-3.9.txt diff --git a/requirements-3.11.txt b/requirements-3.11.txt index 75b898bf50..4a071323e9 100644 --- a/requirements-3.11.txt +++ b/requirements-3.11.txt @@ -1,27 +1,31 @@ -alembic==1.11.1 +alembic==1.12.0 astroid==2.15.6 -async-timeout==4.0.2 +async-timeout==4.0.3 attrs==23.1.0 Babel==2.12.1 -black==23.7.0 +black==23.9.1 blinker==1.6.2 +Brotli==1.0.9 cachelib==0.9.0 certifi==2023.7.22 cffi==1.15.1 -chardet==5.1.0 +chardet==5.2.0 charset-normalizer==3.2.0 -click==8.1.6 +click==8.1.7 cracklib==2.9.6 -cryptography==41.0.2 +cryptography==41.0.3 +cssselect2==0.7.0 Deprecated==1.2.14 dill==0.3.7 -dnspython==2.4.1 +dnspython==2.4.2 dominate==2.8.0 email-validator==2.0.0.post2 ERAlchemy==1.2.10 et-xmlfile==1.1.0 -exceptiongroup==1.1.2 -Flask==2.3.2 +exceptiongroup==1.1.3 +execnet==2.0.2 +flake8==6.1.0 +Flask==2.3.3 flask-babel==3.1.0 Flask-Bootstrap==3.3.7.1 Flask-Caching==2.0.2 @@ -29,13 +33,15 @@ Flask-HTTPAuth==4.8.0 Flask-JSON==0.4.0 Flask-Login==0.6.2 Flask-Mail==0.9.1 -Flask-Migrate==4.0.4 +Flask-Migrate==4.0.5 Flask-Moment==1.0.5 -Flask-SQLAlchemy==3.0.5 +Flask-SQLAlchemy==3.1.1 Flask-WTF==1.1.1 +fonttools==4.41.1 gprof2dot==2022.7.29 greenlet==2.0.2 gunicorn==21.2.0 +html5lib==1.1 icalendar==5.0.7 idna==3.4 importlib-metadata==6.8.0 @@ -48,21 +54,24 @@ lxml==4.9.3 Mako==1.2.4 MarkupSafe==2.1.3 mccabe==0.7.0 -mypy==1.4.1 +mypy==1.5.1 mypy-extensions==1.0.0 -numpy==1.25.1 +numpy==1.25.2 openpyxl==3.1.2 packaging==23.1 -pandas==2.0.3 +pandas==2.1.0 pathspec==0.11.2 Pillow==10.0.0 platformdirs==3.10.0 -pluggy==1.2.0 -psycopg2==2.9.6 +pluggy==1.3.0 +psycopg2==2.9.7 puremagic==1.15 py==1.11.0 +pycodestyle==2.11.0 pycparser==2.21 pydot==1.4.2 +pydyf==0.7.0 +pyflakes==3.1.0 pygraphviz==1.11 PyJWT==2.8.0 pylint==2.17.5 @@ -71,31 +80,37 @@ pylint-flask-sqlalchemy==0.2.0 pylint-plugin-utils==0.8.2 pyOpenSSL==23.2.0 pyparsing==3.1.1 -pytest==7.4.0 +pyphen==0.14.0 +pytest==7.4.2 +pytest-xdist==3.3.1 python-dateutil==2.8.2 python-docx==0.8.11 python-dotenv==1.0.0 python-editor==1.0.4 -pytz==2023.3 +pytz==2023.3.post1 PyYAML==6.0.1 -redis==4.6.0 +redis==5.0.0 reportlab==4.0.4 requests==2.31.0 rq==1.15.1 six==1.16.0 snakeviz==2.2.0 -SQLAlchemy==2.0.19 +SQLAlchemy==2.0.20 +tinycss2==1.2.1 toml==0.10.2 tomli==2.0.1 tomlkit==0.12.1 -tornado==6.3.2 +tornado==6.3.3 tuna==0.5.11 typing_extensions==4.7.1 tzdata==2023.3 urllib3==2.0.4 visitor==0.1.3 -Werkzeug==2.3.6 +weasyprint==59.0 +webencodings==0.5.1 +Werkzeug==2.3.7 wrapt==1.15.0 WTForms==3.0.1 xmltodict==0.13.0 zipp==3.16.2 +zopfli==0.2.2 diff --git a/requirements-3.9.txt b/requirements-3.9.txt deleted file mode 100755 index 43a9383cfe..0000000000 --- a/requirements-3.9.txt +++ /dev/null @@ -1,100 +0,0 @@ -alembic==1.10.2 -astroid==2.15.2 -async-timeout==4.0.2 -attrs==22.2.0 -Babel==2.12.1 -black==23.3.0 -blinker==1.6 -cachelib==0.9.0 -certifi==2022.12.7 -cffi==1.15.1 -chardet==5.1.0 -charset-normalizer==3.1.0 -click==8.1.3 -cracklib==2.9.3 -cryptography==40.0.1 -Deprecated==1.2.13 -dill==0.3.6 -dnspython==2.3.0 -dominate==2.7.0 -email-validator==1.3.1 -ERAlchemy==1.2.10 -et-xmlfile==1.1.0 -exceptiongroup==1.1.1 -Flask==2.2.3 -flask-babel==3.0.1 -Flask-Bootstrap==3.3.7.1 -Flask-Caching==2.0.2 -Flask-HTTPAuth==4.7.0 -Flask-JSON==0.3.5 -Flask-Login==0.6.2 -Flask-Mail==0.9.1 -Flask-Migrate==4.0.4 -Flask-Moment==1.0.5 -Flask-SQLAlchemy==3.0.3 -Flask-WTF==1.1.1 -gprof2dot==2022.7.29 -greenlet==2.0.2 -gunicorn==20.1.0 -icalendar==5.0.4 -idna==3.4 -importlib-metadata==6.1.0 -iniconfig==2.0.0 -isort==5.12.0 -itsdangerous==2.1.2 -Jinja2==3.1.2 -lazy-object-proxy==1.9.0 -lxml==4.9.2 -Mako==1.2.4 -MarkupSafe==2.1.2 -mccabe==0.7.0 -mypy==1.1.1 -mypy-extensions==1.0.0 -numpy==1.24.2 -openpyxl==3.1.2 -packaging==23.0 -pandas==1.5.3 -pathspec==0.11.1 -Pillow==9.5.0 -pkg_resources==0.0.0 -platformdirs==3.2.0 -pluggy==1.0.0 -psycopg2==2.9.6 -py==1.11.0 -pycparser==2.21 -pydot==1.4.2 -pygraphviz==1.10 -PyJWT==2.6.0 -pylint==2.17.2 -pylint-flask==0.6 -pylint-flask-sqlalchemy==0.2.0 -pylint-plugin-utils==0.7 -pyOpenSSL==23.1.1 -pyparsing==3.0.9 -pytest==7.2.2 -python-dateutil==2.8.2 -python-docx==0.8.11 -python-dotenv==1.0.0 -python-editor==1.0.4 -pytz==2022.7.1 -PyYAML==6.0 -redis==4.5.4 -reportlab==3.6.12 -requests==2.28.2 -rq==1.13.0 -six==1.16.0 -snakeviz==2.1.1 -SQLAlchemy==1.4.47 -toml==0.10.2 -tomli==2.0.1 -tomlkit==0.11.7 -tornado==6.2 -tuna==0.5.11 -typing_extensions==4.5.0 -urllib3==1.26.15 -visitor==0.1.3 -Werkzeug==2.2.3 -wrapt==1.15.0 -WTForms==3.0.1 -xmltodict==0.13.0 -zipp==3.15.0 From 1f5422f621f38ccaded6c817f3ea02c9c9021e9f Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 12 Sep 2023 20:54:09 +0200 Subject: [PATCH 53/97] Format affichage date page user --- app/templates/auth/user_info_page.j2 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/templates/auth/user_info_page.j2 b/app/templates/auth/user_info_page.j2 index cb5a552683..a7bb02f4ec 100644 --- a/app/templates/auth/user_info_page.j2 +++ b/app/templates/auth/user_info_page.j2 @@ -27,15 +27,15 @@ {% if current_user.is_administrator() %} {% endif %}
    Dernière modif mot de passe: - {{user.date_modif_passwd.isoformat() if user.date_modif_passwd else ""}}
    + {{user.date_modif_passwd.strftime("%d/%m/%Y") if user.date_modif_passwd else ""}}
    Date d'expiration: - {{user.date_expiration.isoformat() if user.date_expiration else "(sans limite)"}} + {{user.date_expiration.strftime("%d/%m/%Y") if user.date_expiration else "(sans limite)"}}
    From 3eb8869349b7742adaf68b48a37f3755db13b0b1 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 12 Sep 2023 21:23:05 +0200 Subject: [PATCH 54/97] =?UTF-8?q?Fix:=20choix=20format=20bulletin=20PDF=20?= =?UTF-8?q?BUT=20si=20pr=C3=A9f=C3=A9rence=20modifi=C3=A9e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_bulletins_generator.py | 2 +- sco_version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/scodoc/sco_bulletins_generator.py b/app/scodoc/sco_bulletins_generator.py index 2229a37da0..34e38f871e 100644 --- a/app/scodoc/sco_bulletins_generator.py +++ b/app/scodoc/sco_bulletins_generator.py @@ -292,7 +292,7 @@ def make_formsemestre_bulletin_etud( ): if bul_dict.get("type") == "BUT" and fmt.startswith("pdf"): gen_class = bulletin_get_class(bul_class_name + "BUT") - if gen_class is None: + if gen_class is None and bul_dict.get("type") != "BUT": gen_class = bulletin_get_class(bul_class_name) if gen_class is not None: break diff --git a/sco_version.py b/sco_version.py index a44e5b0a14..3c2292d5b1 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.27" +SCOVERSION = "9.6.28" SCONAME = "ScoDoc" From 6cbfd97031e86c21105fecbb5c37a376dc526d81 Mon Sep 17 00:00:00 2001 From: iziram Date: Wed, 13 Sep 2023 08:24:08 +0200 Subject: [PATCH 55/97] Assiduites : fix #686 --- app/scodoc/sco_saisie_notes.py | 40 +++++++++++++++------------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py index 058c53efb6..489165bd01 100644 --- a/app/scodoc/sco_saisie_notes.py +++ b/app/scodoc/sco_saisie_notes.py @@ -46,6 +46,7 @@ from app.models import ( Module, ModuleImpl, ScolarNews, + Assiduite, ) from app.models.etudiants import Identite @@ -75,6 +76,8 @@ import app.scodoc.sco_utils as scu from app.scodoc.sco_utils import json_error from app.scodoc.sco_utils import ModuleType +from flask_sqlalchemy.query import Query + def convert_note_from_string( note: str, @@ -1102,30 +1105,21 @@ def _get_sorted_etuds(evaluation: Evaluation, etudids: list, formsemestre_id: in # Groupes auxquels appartient cet étudiant: e["groups"] = sco_groups.get_etud_groups(etudid, formsemestre_id) - # Information sur absence (tenant compte de la demi-journée) - jour_iso = ( - evaluation.date_debut.date().isoformat() if evaluation.date_debut else "" - ) - warn_abs_lst = [] - # XXX TODO-ASSIDUITE (issue #686) - if evaluation.is_matin(): - nbabs = 0 # TODO-ASSIDUITE sco_abs.count_abs(etudid, jour_iso, jour_iso, matin=True) - nbabsjust = 0 # TODO-ASSIDUITE sco_abs.count_abs_just(etudid, jour_iso, jour_iso, matin=True) - if nbabs: - if nbabsjust: - warn_abs_lst.append("absent justifié le matin !") - else: - warn_abs_lst.append("absent le matin !") - if evaluation.is_apresmidi(): - nbabs = 0 # TODO-ASSIDUITE sco_abs.count_abs(etudid, jour_iso, jour_iso, matin=0) - nbabsjust = 0 # TODO-ASSIDUITE sco_abs.count_abs_just(etudid, jour_iso, jour_iso, matin=0) - if nbabs: - if nbabsjust: - warn_abs_lst.append("absent justifié l'après-midi !") - else: - warn_abs_lst.append("absent l'après-midi !") + # Information sur absence + warn_abs_lst: str = "" + if evaluation.date_debut is not None and evaluation.date_fin is not None: + assiduites_etud: Query = etud.assiduites.filter( + Assiduite.etat == scu.EtatAssiduite.ABSENT, + Assiduite.date_debut <= evaluation.date_fin, + Assiduite.date_fin >= evaluation.date_debut, + ) + premiere_assi: Assiduite = assiduites_etud.first() + if premiere_assi is not None: + warn_abs_lst: str = ( + f"absent {'justifié' if premiere_assi.est_just else ''}" + ) - e["absinfo"] = '' + " ".join(warn_abs_lst) + " " + e["absinfo"] = '' + warn_abs_lst + " " # Note actuelle de l'étudiant: if etudid in notes_db: From a7058fb86b37f68c30319b95f53b2371d32739cd Mon Sep 17 00:00:00 2001 From: iziram Date: Wed, 13 Sep 2023 08:59:54 +0200 Subject: [PATCH 56/97] Assiduites : fix #728 --- app/scodoc/sco_groups_view.py | 2 +- app/static/js/assiduites.js | 2 +- app/views/assiduites.py | 12 ++++++++---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/scodoc/sco_groups_view.py b/app/scodoc/sco_groups_view.py index 728ccb3635..0eefe05805 100644 --- a/app/scodoc/sco_groups_view.py +++ b/app/scodoc/sco_groups_view.py @@ -900,7 +900,7 @@ def form_choix_jour_saisie_hebdo(groups_infos, moduleimpl_id=None): formsemestre_id=groups_infos.formsemestre_id, moduleimpl_id="" if moduleimpl_id is None else moduleimpl_id ) - }';">Saisie du jour + }';">Saisie du jour ({datetime.date.today().strftime('%d/%m/%Y')}) """ diff --git a/app/static/js/assiduites.js b/app/static/js/assiduites.js index 019cb894a2..dbfa3736e1 100644 --- a/app/static/js/assiduites.js +++ b/app/static/js/assiduites.js @@ -577,7 +577,7 @@ function updateDate() { return true; } else { const att = document.createTextNode( - "Le jour sélectionné n'est pas un jour travaillé." + `Le jour sélectionné (${formatDate(date)}) n'est pas un jour travaillé.` ); openAlertModal("Erreur", att, "", "crimson"); dateInput.value = dateInput.getAttribute("value"); diff --git a/app/views/assiduites.py b/app/views/assiduites.py index a2e5a3b1d7..db3cb9005c 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -565,10 +565,14 @@ def signal_assiduites_group(): real_date = scu.is_iso_formated(date, True).date() - if real_date < formsemestre.date_debut: - date = formsemestre.date_debut.isoformat() - elif real_date > formsemestre.date_fin: - date = formsemestre.date_fin.isoformat() + if real_date < formsemestre.date_debut or real_date > formsemestre.date_fin: + 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") + raise ScoValueError( + f"Impossible de saisir les assiduités pour le {real_str}" + + f" : Jour en dehors du semestre ( {form_deb} → {form_fin}) " + ) # --- Restriction en fonction du moduleimpl_id --- if moduleimpl_id: From 18908594828fc060746b659eb0becb1e671b2f02 Mon Sep 17 00:00:00 2001 From: iziram Date: Wed, 13 Sep 2023 15:19:21 +0200 Subject: [PATCH 57/97] Assiduites : fix Justificatifs en attentes #724 --- app/api/justificatifs.py | 67 +++- app/scodoc/sco_formsemestre_status.py | 5 + app/scodoc/sco_utils.py | 4 +- app/templates/assiduites/pages/bilan_dept.j2 | 13 +- .../assiduites/pages/liste_assiduites.j2 | 4 +- .../assiduites/widgets/tableau_assi.j2 | 181 +++++++++ .../assiduites/widgets/tableau_base.j2 | 343 +----------------- .../assiduites/widgets/tableau_justi.j2 | 192 +++++++++- app/templates/sidebar.j2 | 5 +- app/views/assiduites.py | 18 +- 10 files changed, 470 insertions(+), 362 deletions(-) diff --git a/app/api/justificatifs.py b/app/api/justificatifs.py index e68b397ac3..f753fc0e0f 100644 --- a/app/api/justificatifs.py +++ b/app/api/justificatifs.py @@ -19,7 +19,13 @@ from app.api import api_bp as bp from app.api import api_web_bp from app.api import get_model_api_object, tools from app.decorators import permission_required, scodoc -from app.models import Identite, Justificatif, Departement, FormSemestre +from app.models import ( + Identite, + Justificatif, + Departement, + FormSemestre, + FormSemestreInscription, +) from app.models.assiduites import ( compute_assiduites_justified, ) @@ -27,6 +33,7 @@ from app.scodoc.sco_archives_justificatifs import JustificatifArchiver from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_permissions import Permission from app.scodoc.sco_utils import json_error +from app.scodoc.sco_groups import get_group_members # Partie Modèle @@ -145,14 +152,40 @@ def justificatifs_dept(dept_id: int = None, with_query: bool = False): if with_query: justificatifs_query = _filter_manager(request, justificatifs_query) + data_set: list[dict] = [] - for just in justificatifs_query.all(): - data = just.to_dict(format_api=True) - data_set.append(data) + for just in justificatifs_query: + data_set.append(_set_sems_and_groupe(just)) return data_set +def _set_sems_and_groupe(justi: Justificatif) -> dict: + from app.scodoc.sco_groups import get_etud_groups + + 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() + ) + if formsemestre: + data["formsemestre"] = { + "id": formsemestre.id, + "title": formsemestre.session_id(), + } + + return data + + @bp.route( "/justificatifs/formsemestre/", defaults={"with_query": False} ) @@ -732,13 +765,16 @@ def _filter_manager(requested, justificatifs_query): # cas 5 : formsemestre_id formsemestre_id = requested.args.get("formsemestre_id") - if formsemestre_id is not None: + if formsemestre_id not in [None, "", -1]: formsemestre: FormSemestre = None - formsemestre_id = int(formsemestre_id) - formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first() - justificatifs_query = scass.filter_by_formsemestre( - justificatifs_query, Justificatif, formsemestre - ) + try: + formsemestre_id = int(formsemestre_id) + formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first() + justificatifs_query = scass.filter_by_formsemestre( + justificatifs_query, Justificatif, formsemestre + ) + except ValueError: + formsemestre = None order = requested.args.get("order", None) if order is not None: @@ -755,4 +791,15 @@ def _filter_manager(requested, justificatifs_query): Justificatif.date_fin <= scu.date_fin_anne_scolaire(annee), ) + group_id = requested.args.get("group_id", None) + if group_id is not None: + try: + group_id = int(group_id) + etudids: list[int] = [etu["etudid"] for etu in get_group_members(group_id)] + justificatifs_query = justificatifs_query.filter( + Justificatif.etudid.in_(etudids) + ) + except ValueError: + group_id = None + return justificatifs_query diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 7e67e4af8c..c21e8774dc 100755 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -852,6 +852,11 @@ def _make_listes_sem(formsemestre: FormSemestre, with_absences=True): }?group_ids=%(group_id)s&formsemestre_id={ formsemestre.formsemestre_id }"> + """ else: diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 7cbc3e0d20..f92ab38e38 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -670,8 +670,8 @@ def AbsencesURL(): def AssiduitesURL(): """URL of Assiduités""" - return url_for("assiduites.index_html", scodoc_dept=g.scodoc_dept)[ - : -len("/index_html") + return url_for("assiduites.bilan_dept", scodoc_dept=g.scodoc_dept)[ + : -len("/BilanDept") ] diff --git a/app/templates/assiduites/pages/bilan_dept.j2 b/app/templates/assiduites/pages/bilan_dept.j2 index 2e8ca5be96..3301c85f6d 100644 --- a/app/templates/assiduites/pages/bilan_dept.j2 +++ b/app/templates/assiduites/pages/bilan_dept.j2 @@ -6,6 +6,7 @@

    Justificatifs en attente (ou modifiés)

    + {% include "assiduites/widgets/tableau_justi.j2" %}
    @@ -34,14 +35,17 @@ generate(defAnnee) } + let formsemestre_id = "{{formsemestre_id}}" + let group_id = "{{group_id}}" + function getDeptJustificatifsFromPeriod(action) { - const path = getUrl() + `/api/justificatifs/dept/${dept_id}/query?date_debut=${bornes.deb}&date_fin=${bornes.fin}&etat=attente,modifie` + const formsemestre = formsemestre_id ? `&formsemestre_id=${formsemestre_id}` : "" + const group = group_id ? `&group_id=${group_id}` : "" + const path = getUrl() + `/api/justificatifs/dept/${dept_id}/query?date_debut=${bornes.deb}&date_fin=${bornes.fin}&etat=attente,modifie${formsemestre}${group}` async_get( path, (data, status) => { - console.log(data); justificatifCallBack(data); - }, (data, status) => { console.error(data, status) @@ -88,6 +92,7 @@ filterJustificatifs = { "columns": [ + "formsemestre", "etudid", "entry_date", "date_debut", @@ -100,7 +105,7 @@ "etat": [ "attente", "modifie" - ] + ], } } const select = document.querySelector('#annee'); diff --git a/app/templates/assiduites/pages/liste_assiduites.j2 b/app/templates/assiduites/pages/liste_assiduites.j2 index eb1f9ade32..aefc644bdf 100644 --- a/app/templates/assiduites/pages/liste_assiduites.j2 +++ b/app/templates/assiduites/pages/liste_assiduites.j2 @@ -4,10 +4,10 @@

    Liste de l'assiduité et des justificatifs de {{sco.etud.nomprenom}}

    {% include "assiduites/widgets/tableau_base.j2" %}

    Assiduités :

    - + {% include "assiduites/widgets/tableau_assi.j2" %}

    Justificatifs :

    - + {% include "assiduites/widgets/tableau_justi.j2" %}
    • Detail
    • diff --git a/app/templates/assiduites/widgets/tableau_assi.j2 b/app/templates/assiduites/widgets/tableau_assi.j2 index fd1ee7df7e..b6f5349b49 100644 --- a/app/templates/assiduites/widgets/tableau_assi.j2 +++ b/app/templates/assiduites/widgets/tableau_assi.j2 @@ -261,4 +261,185 @@ } ); } + + function filterAssi() { + let html = ` +
      +

      Affichage des colonnes:

      +
      + + + + + + +
      +
      +

      Filtrage des colonnes:

      + + Date de saisie + + + + + Date de début + + + + + Date de fin + + + + + Etat + + + + + + Module + + + + Est Justifiée + + +
      + `; + const span = document.createElement('span'); + span.innerHTML = html + html = span.firstElementChild + + const filterHead = html.querySelector('.filter-head'); + filterHead.innerHTML = "" + let cols = ["entry_date", "date_debut", "date_fin", "etat", "moduleimpl_id", "est_just"]; + + cols.forEach((k) => { + const label = document.createElement('label') + label.classList.add('f-label') + const s = document.createElement('span'); + s.textContent = columnTranslator(k); + + + const input = document.createElement('input'); + input.classList.add('chk') + input.type = "checkbox" + input.name = k + input.id = k; + input.checked = filterAssiduites.columns.includes(k) + + label.appendChild(s) + label.appendChild(input) + filterHead.appendChild(label) + }) + + const sl = html.querySelector('.filter-line #moduleimpl_id'); + let opts = [] + Object.keys(moduleimpls).forEach((k) => { + const opt = document.createElement('option'); + opt.value = k == null ? "null" : k; + opt.textContent = moduleimpls[k]; + opts.push(opt); + }) + + opts = opts.sort((a, b) => { + return a.value < b.value + }) + + sl.append(...opts); + + // Mise à jour des filtres + + Object.keys(filterAssiduites.filters).forEach((key) => { + const l = html.querySelector(`.filter-title[for="${key}"]`).parentElement; + if (key.indexOf('date') != -1) { + l.querySelector(`#${key}_pref`).value = filterAssiduites.filters[key].pref; + l.querySelector(`#${key}_time`).value = filterAssiduites.filters[key].time.format("YYYY-MM-DDTHH:mm"); + + } else if (key.indexOf('etat') != -1) { + l.querySelectorAll('input').forEach((e) => { + e.checked = filterAssiduites.filters[key].includes(e.value) + }) + } else if (key.indexOf("module") != -1) { + l.querySelector('#moduleimpl_id').value = filterAssiduites.filters[key]; + } else if (key.indexOf("est_just") != -1) { + l.querySelector('#est_just').value = filterAssiduites.filters[key]; + } + }) + + openPromptModal("Filtrage des assiduités", html, () => { + + const columns = [...document.querySelectorAll('.chk')] + .map((el) => { if (el.checked) return el.id }) + .filter((el) => el) + + filterAssiduites.columns = columns + filterAssiduites.filters = {} + //reste des filtres + + const lines = [...document.querySelectorAll('.filter-line')]; + + lines.forEach((l) => { + const key = l.querySelector('.filter-title').getAttribute('for'); + + if (key.indexOf('date') != -1) { + const pref = l.querySelector(`#${key}_pref`).value; + const time = l.querySelector(`#${key}_time`).value; + if (l.querySelector(`#${key}_time`).value != "") { + filterAssiduites.filters[key] = { + pref: pref, + time: new moment.tz(time, TIMEZONE) + } + } + } else if (key.indexOf('etat') != -1) { + filterAssiduites.filters[key] = [...l.querySelectorAll("input:checked")].map((e) => e.value); + } else if (key.indexOf("module") != -1) { + filterAssiduites.filters[key] = l.querySelector('#moduleimpl_id').value; + } else if (key.indexOf("est_just") != -1) { + filterAssiduites.filters[key] = l.querySelector('#est_just').value; + } + }) + + + getAllAssiduitesFromEtud(etudid, assiduiteCallBack) + + }, () => { }, "#7059FF"); + } \ No newline at end of file diff --git a/app/templates/assiduites/widgets/tableau_base.j2 b/app/templates/assiduites/widgets/tableau_base.j2 index 2b468e6a4d..e337ebedad 100644 --- a/app/templates/assiduites/widgets/tableau_base.j2 +++ b/app/templates/assiduites/widgets/tableau_base.j2 @@ -102,6 +102,10 @@ return f.obj_id.includes(obj_id) } + if (k == "formsemestre") { + return f.formsemestre === "" || (el.hasOwnProperty("formsemestre") && el.formsemestre.title.replaceAll('-', ' ').indexOf(f.formsemestre) != -1); + } + return true; }) @@ -286,343 +290,6 @@ } - function filter(assi = true) { - if (assi) { - let html = ` -
      -

      Affichage des colonnes:

      -
      - - - - - - -
      -
      -

      Filtrage des colonnes:

      - - Date de saisie - - - - - Date de début - - - - - Date de fin - - - - - Etat - - - - - - Module - - - - Est Justifiée - - -
      - `; - const span = document.createElement('span'); - span.innerHTML = html - html = span.firstElementChild - - const filterHead = html.querySelector('.filter-head'); - filterHead.innerHTML = "" - let cols = ["entry_date", "date_debut", "date_fin", "etat", "moduleimpl_id", "est_just"]; - - cols.forEach((k) => { - const label = document.createElement('label') - label.classList.add('f-label') - const s = document.createElement('span'); - s.textContent = columnTranslator(k); - - - const input = document.createElement('input'); - input.classList.add('chk') - input.type = "checkbox" - input.name = k - input.id = k; - input.checked = filterAssiduites.columns.includes(k) - - label.appendChild(s) - label.appendChild(input) - filterHead.appendChild(label) - }) - - const sl = html.querySelector('.filter-line #moduleimpl_id'); - let opts = [] - Object.keys(moduleimpls).forEach((k) => { - const opt = document.createElement('option'); - opt.value = k == null ? "null" : k; - opt.textContent = moduleimpls[k]; - opts.push(opt); - }) - - opts = opts.sort((a, b) => { - return a.value < b.value - }) - - sl.append(...opts); - - // Mise à jour des filtres - - Object.keys(filterAssiduites.filters).forEach((key) => { - const l = html.querySelector(`.filter-title[for="${key}"]`).parentElement; - if (key.indexOf('date') != -1) { - l.querySelector(`#${key}_pref`).value = filterAssiduites.filters[key].pref; - l.querySelector(`#${key}_time`).value = filterAssiduites.filters[key].time.format("YYYY-MM-DDTHH:mm"); - - } else if (key.indexOf('etat') != -1) { - l.querySelectorAll('input').forEach((e) => { - e.checked = filterAssiduites.filters[key].includes(e.value) - }) - } else if (key.indexOf("module") != -1) { - l.querySelector('#moduleimpl_id').value = filterAssiduites.filters[key]; - } else if (key.indexOf("est_just") != -1) { - l.querySelector('#est_just').value = filterAssiduites.filters[key]; - } - }) - - openPromptModal("Filtrage des assiduités", html, () => { - - const columns = [...document.querySelectorAll('.chk')] - .map((el) => { if (el.checked) return el.id }) - .filter((el) => el) - - filterAssiduites.columns = columns - filterAssiduites.filters = {} - //reste des filtres - - const lines = [...document.querySelectorAll('.filter-line')]; - - lines.forEach((l) => { - const key = l.querySelector('.filter-title').getAttribute('for'); - - if (key.indexOf('date') != -1) { - const pref = l.querySelector(`#${key}_pref`).value; - const time = l.querySelector(`#${key}_time`).value; - if (l.querySelector(`#${key}_time`).value != "") { - filterAssiduites.filters[key] = { - pref: pref, - time: new moment.tz(time, TIMEZONE) - } - } - } else if (key.indexOf('etat') != -1) { - filterAssiduites.filters[key] = [...l.querySelectorAll("input:checked")].map((e) => e.value); - } else if (key.indexOf("module") != -1) { - filterAssiduites.filters[key] = l.querySelector('#moduleimpl_id').value; - } else if (key.indexOf("est_just") != -1) { - filterAssiduites.filters[key] = l.querySelector('#est_just').value; - } - }) - - - getAllAssiduitesFromEtud(etudid, assiduiteCallBack) - - }, () => { }, "#7059FF"); - } else { - let html = ` -
      -

      Affichage des colonnes:

      -
      - - - - - - -
      -
      -

      Filtrage des colonnes:

      - - Date de saisie - - - - - Date de début - - - - - Date de fin - - - - - Etat - - - - - -
      - `; - const span = document.createElement('span'); - span.innerHTML = html - html = span.firstElementChild - - const filterHead = html.querySelector('.filter-head'); - filterHead.innerHTML = "" - let cols = ["entry_date", "date_debut", "date_fin", "etat", "raison", "fichier"]; - - cols.forEach((k) => { - const label = document.createElement('label') - label.classList.add('f-label') - const s = document.createElement('span'); - s.textContent = columnTranslator(k); - - - const input = document.createElement('input'); - input.classList.add('chk') - input.type = "checkbox" - input.name = k - input.id = k; - input.checked = filterJustificatifs.columns.includes(k) - - label.appendChild(s) - label.appendChild(input) - filterHead.appendChild(label) - }) - - // Mise à jour des filtres - - Object.keys(filterJustificatifs.filters).forEach((key) => { - const l = html.querySelector(`.filter-title[for="${key}"]`).parentElement; - if (key.indexOf('date') != -1) { - l.querySelector(`#${key}_pref`).value = filterJustificatifs.filters[key].pref; - l.querySelector(`#${key}_time`).value = filterJustificatifs.filters[key].time.format("YYYY-MM-DDTHH:mm"); - - } else if (key.indexOf('etat') != -1) { - l.querySelectorAll('input').forEach((e) => { - e.checked = filterJustificatifs.filters[key].includes(e.value) - }) - } - }) - - openPromptModal("Filtrage des Justificatifs", html, () => { - - const columns = [...document.querySelectorAll('.chk')] - .map((el) => { if (el.checked) return el.id }) - .filter((el) => el) - - filterJustificatifs.columns = columns - filterJustificatifs.filters = {} - //reste des filtres - - const lines = [...document.querySelectorAll('.filter-line')]; - - lines.forEach((l) => { - const key = l.querySelector('.filter-title').getAttribute('for'); - - if (key.indexOf('date') != -1) { - const pref = l.querySelector(`#${key}_pref`).value; - const time = l.querySelector(`#${key}_time`).value; - if (l.querySelector(`#${key}_time`).value != "") { - filterJustificatifs.filters[key] = { - pref: pref, - time: new moment.tz(time, TIMEZONE) - } - } - } else if (key.indexOf('etat') != -1) { - filterJustificatifs.filters[key] = [...l.querySelectorAll("input:checked")].map((e) => e.value); - } - }) - - - getAllJustificatifsFromEtud(etudid, justificatifCallBack) - - }, () => { }, "#7059FF"); - } - } function columnTranslator(colName) { switch (colName) { @@ -644,6 +311,8 @@ return "Fichier"; case "etudid": return "Etudiant"; + case "formsemestre": + return "Semestre"; } } diff --git a/app/templates/assiduites/widgets/tableau_justi.j2 b/app/templates/assiduites/widgets/tableau_justi.j2 index 4b3a502289..636f9ede51 100644 --- a/app/templates/assiduites/widgets/tableau_justi.j2 +++ b/app/templates/assiduites/widgets/tableau_justi.j2 @@ -99,7 +99,13 @@ } else if (k.indexOf('etudid') != -1) { const e = getEtudiant(justificatif.etudid); - td.textContent = `${e.prenom.capitalize()} ${e.nom.toUpperCase()}`; + td.innerHTML = `${e.prenom.capitalize()} ${e.nom.toUpperCase()}`; + } else if (k == "formsemestre") { + if (justificatif.hasOwnProperty("formsemestre")) { + td.textContent = justificatif.formsemestre.title.replaceAll('-', ' '); + } else { + td.textContent = `Pas de Semestre`; + } } else { if (justificatif[k] != null) { @@ -458,7 +464,191 @@ }) } + function filterJusti(dept = false) { + let dept_html_head = ` + + + ` + + let dept_html_body = ` + + Recherche dans les semestre + + + ` + + let html = ` +
      +

      Affichage des colonnes:

      +
      + ${dept ? dept_html_head : ""} + + + + + + +
      +
      +

      Filtrage des colonnes:

      + + Date de saisie + + + + + Date de début + + + + + Date de fin + + + + + Etat + + + + + + ${dept ? dept_html_body : ""} +
      + `; + const span = document.createElement('span'); + span.innerHTML = html + html = span.firstElementChild + + const filterHead = html.querySelector('.filter-head'); + filterHead.innerHTML = "" + let cols = ["formsemestre", "etudid", "entry_date", "date_debut", "date_fin", "etat", "raison", "fichier"]; + + cols.forEach((k) => { + const label = document.createElement('label') + label.classList.add('f-label') + const s = document.createElement('span'); + s.textContent = columnTranslator(k); + + + const input = document.createElement('input'); + input.classList.add('chk') + input.type = "checkbox" + input.name = k + input.id = k; + input.checked = filterJustificatifs.columns.includes(k) + + label.appendChild(s) + label.appendChild(input) + filterHead.appendChild(label) + }) + + // Mise à jour des filtres + + Object.keys(filterJustificatifs.filters).forEach((key) => { + const l = html.querySelector(`.filter-title[for="${key}"]`).parentElement; + if (key.indexOf('date') != -1) { + l.querySelector(`#${key}_pref`).value = filterJustificatifs.filters[key].pref; + l.querySelector(`#${key}_time`).value = filterJustificatifs.filters[key].time.format("YYYY-MM-DDTHH:mm"); + + } else if (key.indexOf('etat') != -1) { + l.querySelectorAll('input').forEach((e) => { + e.checked = filterJustificatifs.filters[key].includes(e.value) + }) + } else if (key == "formsemestre") { + l.querySelector('#formsemestre').value = filterJustificatifs.filters["formsemestre"]; + } + }) + + openPromptModal("Filtrage des Justificatifs", html, () => { + + const columns = [...document.querySelectorAll('.chk')] + .map((el) => { if (el.checked) return el.id }) + .filter((el) => el) + + filterJustificatifs.columns = columns + filterJustificatifs.filters = {} + //reste des filtres + + const lines = [...document.querySelectorAll('.filter-line')]; + + lines.forEach((l) => { + const key = l.querySelector('.filter-title').getAttribute('for'); + + if (key.indexOf('date') != -1) { + const pref = l.querySelector(`#${key}_pref`).value; + const time = l.querySelector(`#${key}_time`).value; + if (l.querySelector(`#${key}_time`).value != "") { + filterJustificatifs.filters[key] = { + pref: pref, + time: new moment.tz(time, TIMEZONE) + } + } + } else if (key.indexOf('etat') != -1) { + filterJustificatifs.filters[key] = [...l.querySelectorAll("input:checked")].map((e) => e.value); + } else if (key == "formsemestre") { + filterJustificatifs.filters["formsemestre"] = l.querySelector('#formsemestre').value; + } + }) + + + if (dept) { + loadAll(); + } else { + getAllJustificatifsFromEtud(etudid, justificatifCallBack) + } + + }, () => { }, "#7059FF"); + }