From 7d3811113baa60a3980bccbe8c660ed8882c38d1 Mon Sep 17 00:00:00 2001 From: lehmann Date: Mon, 31 Jul 2023 19:57:47 +0200 Subject: [PATCH 1/5] Partition editor : progress bar --- app/static/css/partition_editor.css | 18 ++++++++++++++++++ app/templates/scolar/partition_editor.j2 | 14 +++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/app/static/css/partition_editor.css b/app/static/css/partition_editor.css index 777b37e4b4..09f042fefc 100644 --- a/app/static/css/partition_editor.css +++ b/app/static/css/partition_editor.css @@ -394,6 +394,7 @@ body.editionActivated .filtres .nonEditable .move { padding: 4px 8px; margin-bottom: 16px; border-radius: 4px; + position: relative; } #zoneChoix .autoAffectation>select { @@ -415,6 +416,23 @@ body.editionActivated .filtres .nonEditable .move { margin-bottom: 4px; width: fit-content; } +#zoneChoix .autoAffectation .progress { + position: absolute; + top: 100%; + left: 0; + right: 0; + height: 4px; + background: #717171; +} + +#zoneChoix .autoAffectation .progress>div { + position: absolute; + top: 0; + left: 0; + width: calc(100% * var(--nombre) / var(--reference)); + bottom: 0; + background: #0c9; +} #zoneChoix .etudiants>div { background: #FFF; diff --git a/app/templates/scolar/partition_editor.j2 b/app/templates/scolar/partition_editor.j2 index b9a95eb1f7..76e8a69c1a 100644 --- a/app/templates/scolar/partition_editor.j2 +++ b/app/templates/scolar/partition_editor.j2 @@ -47,6 +47,10 @@ vers le groupe
Valider
+ +
+
+
@@ -430,6 +434,7 @@ /****************************/ /* Affectation à un groupe */ /****************************/ + var progressNb = 0; function affectationGo() { let from = document.querySelector("#affectationFrom").value; let to = document.querySelector("#affectationTo").value; @@ -450,7 +455,12 @@ }) } - console.log(elements); + let progress = document.querySelector("#zoneChoix .autoAffectation .progress"); + if(elements.length > 1){ + progress.style.setProperty('--reference', elements.length); + progress.style.setProperty('--nombre', 0); + progressNb = 0; + } elements.forEach(groupeSelected => { if (to[0] != "n") { @@ -502,6 +512,8 @@ this.classList.remove("saving"); this.classList.add("saved"); setTimeout(() => { this.classList.remove("saved") }, 800); + let progress = document.querySelector("#zoneChoix .autoAffectation .progress"); + progress.style.setProperty('--nombre', ++progressNb); return; } throw 'Les données retournées ne sont pas valides'; From 2cad6560c4681d6cc7bec1dfe97b17c8e066d9a2 Mon Sep 17 00:00:00 2001 From: lehmann Date: Mon, 31 Jul 2023 20:10:42 +0200 Subject: [PATCH 2/5] Progress fin message --- app/templates/scolar/partition_editor.j2 | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/app/templates/scolar/partition_editor.j2 b/app/templates/scolar/partition_editor.j2 index 76e8a69c1a..bdd900d929 100644 --- a/app/templates/scolar/partition_editor.j2 +++ b/app/templates/scolar/partition_editor.j2 @@ -51,9 +51,9 @@
- + - +
@@ -435,6 +435,7 @@ /* Affectation à un groupe */ /****************************/ var progressNb = 0; + var progressRef = 0; function affectationGo() { let from = document.querySelector("#affectationFrom").value; let to = document.querySelector("#affectationTo").value; @@ -456,9 +457,10 @@ } let progress = document.querySelector("#zoneChoix .autoAffectation .progress"); - if(elements.length > 1){ + if (elements.length > 1) { progress.style.setProperty('--reference', elements.length); progress.style.setProperty('--nombre', 0); + progressRef = elements.length; progressNb = 0; } @@ -512,8 +514,13 @@ this.classList.remove("saving"); this.classList.add("saved"); setTimeout(() => { this.classList.remove("saved") }, 800); + let progress = document.querySelector("#zoneChoix .autoAffectation .progress"); progress.style.setProperty('--nombre', ++progressNb); + + if (progressNb == progressRef) { + sco_message("Tous les étudiants sont affectés"); + } return; } throw 'Les données retournées ne sont pas valides'; From 720ca7222c0ddaf2a79a9c34c45ecdc36f3cea0e Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 31 Jul 2023 20:37:58 +0200 Subject: [PATCH 3/5] Fix #666: affichage malus sur tableau bord modimpl. --- app/scodoc/sco_evaluations.py | 19 +++++++++---------- app/scodoc/sco_moduleimpl_status.py | 5 ++++- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/app/scodoc/sco_evaluations.py b/app/scodoc/sco_evaluations.py index 4a9dfb6d44..12440d1126 100644 --- a/app/scodoc/sco_evaluations.py +++ b/app/scodoc/sco_evaluations.py @@ -40,7 +40,6 @@ from flask import request from app.comp import res_sem from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre -from app.models import ScolarNews import app.scodoc.sco_utils as scu from app.scodoc.sco_utils import ModuleType @@ -217,19 +216,19 @@ def do_evaluation_etat( (TotalNbMissing > 0) and (E["evaluation_type"] != scu.EVALUATION_RATTRAPAGE) and (E["evaluation_type"] != scu.EVALUATION_SESSION2) - and not is_malus ): complete = False else: complete = True - if ( - TotalNbMissing > 0 - and ((TotalNbMissing == TotalNbAtt) or E["publish_incomplete"]) - and not is_malus - ): - evalattente = True - else: - evalattente = False + + complete = ( + (TotalNbMissing == 0) + or (E["evaluation_type"] == scu.EVALUATION_RATTRAPAGE) + or (E["evaluation_type"] == scu.EVALUATION_SESSION2) + ) + evalattente = (TotalNbMissing > 0) and ( + (TotalNbMissing == TotalNbAtt) or E["publish_incomplete"] + ) # mais ne met pas en attente les evals immediates sans aucune notes: if E["publish_incomplete"] and nb_notes == 0: evalattente = False diff --git a/app/scodoc/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py index 170bfdcae0..84e5766ae7 100644 --- a/app/scodoc/sco_moduleimpl_status.py +++ b/app/scodoc/sco_moduleimpl_status.py @@ -564,7 +564,10 @@ def _ligne_evaluation( if modimpl.module.ue.type != UE_SPORT: # Avertissement si coefs x poids nuls if coef < scu.NOTES_PRECISION: - H.append("""coef. nul !""") + if modimpl.module.module_type == scu.ModuleType.MALUS: + H.append("""malus""") + else: + H.append("""coef. nul !""") elif is_apc: # visualisation des poids (Hinton map) H.append(_evaluation_poids_html(evaluation, max_poids)) From 243268a0c39382424cc92e6e64a480dca96d72bc Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 1 Aug 2023 22:07:23 +0200 Subject: [PATCH 4/5] API: exporte les date et datetime en ISO et non pas en RFC 822. --- app/__init__.py | 10 ++++++++-- tests/api/test_api_etudiants.py | 2 ++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 86646eb407..f1fba060a4 100755 --- a/app/__init__.py +++ b/app/__init__.py @@ -148,7 +148,7 @@ def handle_invalid_usage(error): # JSON ENCODING # used by some internal finctions -# the API is now using flask_son, NOT THIS ENCODER +# the API is now using flask_json, NOT THIS ENCODER class ScoDocJSONEncoder(json.JSONEncoder): def default(self, o): # pylint: disable=E0202 if isinstance(o, (datetime.date, datetime.datetime)): @@ -260,7 +260,13 @@ def create_app(config_class=DevConfig): CAS(app, url_prefix="/cas", configuration_function=cas.set_cas_configuration) app.wsgi_app = ReverseProxied(app.wsgi_app) - FlaskJSON(app) + app_json = FlaskJSON(app) + + @app_json.encoder + def scodoc_json_encoder(o): + "Overide default date encoding (RFC 822) and use ISO" + if isinstance(o, (datetime.date, datetime.datetime)): + return o.isoformat() # Pour conserver l'ordre des objets dans les JSON: # e.g. l'ordre des UE dans les bulletins diff --git a/tests/api/test_api_etudiants.py b/tests/api/test_api_etudiants.py index 56f3193d33..c7d948ef2a 100644 --- a/tests/api/test_api_etudiants.py +++ b/tests/api/test_api_etudiants.py @@ -17,6 +17,7 @@ Utilisation : pytest tests/api/test_api_etudiants.py """ +import re import requests from app.scodoc import sco_utils as scu @@ -100,6 +101,7 @@ def test_etudiants_courant(api_headers): etud = etudiants[-1] assert verify_fields(etud, ETUD_FIELDS) is True + assert re.match(r"^\d{4}-\d\d-\d\d$", etud["date_naissance"]) def test_etudiant(api_headers): From cae3511f3c8c9bdbc4103bd7a02a17bd2e492424 Mon Sep 17 00:00:00 2001 From: iziram Date: Wed, 9 Aug 2023 09:57:47 +0200 Subject: [PATCH 5/5] Assiduites : bug fix multiple + modifs routes api --- app/api/assiduites.py | 27 +++++++------------ app/api/etudiants.py | 3 +++ app/api/justificatifs.py | 17 +++++++++--- app/models/assiduites.py | 23 ++++++++++++++++ app/scodoc/sco_assiduites.py | 24 +++++++++++------ app/static/js/assiduites.js | 10 +++++++ .../assiduites/widgets/tableau_assi.j2 | 2 +- .../assiduites/widgets/tableau_base.j2 | 4 +-- .../assiduites/widgets/tableau_justi.j2 | 4 +-- tests/unit/test_assiduites.py | 15 ++++++++--- 10 files changed, 93 insertions(+), 36 deletions(-) diff --git a/app/api/assiduites.py b/app/api/assiduites.py index f67e582ae0..48f54db674 100644 --- a/app/api/assiduites.py +++ b/app/api/assiduites.py @@ -25,6 +25,7 @@ from app.models import ( Scolog, Justificatif, ) +from app.models.assiduites import get_assiduites_justif from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_permissions import Permission from app.scodoc.sco_utils import json_error @@ -77,7 +78,7 @@ def assiduite_justificatifs(assiduite_id: int = None, long: bool = False): ] """ - return _get_assiduites_justif(assiduite_id, True) + return get_assiduites_justif(assiduite_id, True) # etudid @@ -371,7 +372,7 @@ def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False): if formsemestre is None: return json_error(404, "le paramètre 'formsemestre_id' n'existe pas") - assiduites_query = scass.filter_by_formsemestre(Assiduite.query, formsemestre) + assiduites_query = scass.filter_by_formsemestre(Assiduite.query,Assiduite, formsemestre) if with_query: assiduites_query = _filter_manager(request, assiduites_query) @@ -420,7 +421,9 @@ def count_assiduites_formsemestre( etuds_id = [etud.id for etud in etuds] assiduites_query = Assiduite.query.filter(Assiduite.etudid.in_(etuds_id)) - assiduites_query = scass.filter_by_formsemestre(assiduites_query, formsemestre) + assiduites_query = scass.filter_by_formsemestre( + assiduites_query, Assiduite, formsemestre + ) metric: str = "all" filtered: dict = {} if with_query: @@ -1000,7 +1003,9 @@ def _filter_manager(requested, assiduites_query: Assiduite): formsemestre: FormSemestre = None formsemestre_id = int(formsemestre_id) formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first() - assiduites_query = scass.filter_by_formsemestre(assiduites_query, formsemestre) + assiduites_query = scass.filter_by_formsemestre( + assiduites_query, Assiduite, formsemestre + ) # cas 6 : est_just @@ -1027,20 +1032,8 @@ def _filter_manager(requested, assiduites_query: Assiduite): return assiduites_query -def _get_assiduites_justif(assiduite_id: int, long: bool): - assi: Assiduite = Assiduite.query.get_or_404(assiduite_id) - - justifs: Justificatif = Justificatif.query.filter( - Justificatif.etudid == assi.etudid, - Justificatif.date_debut <= assi.date_debut, - Justificatif.date_fin >= assi.date_fin, - ) - - return [j.justif_id if not long else j.to_dict(True) for j in justifs] - - def _with_justifs(assi): if request.args.get("with_justifs") is None: return assi - assi["justificatifs"] = _get_assiduites_justif(assi["assiduite_id"], True) + assi["justificatifs"] = get_assiduites_justif(assi["assiduite_id"], True) return assi diff --git a/app/api/etudiants.py b/app/api/etudiants.py index a5d63e4dca..fd1acdf6d5 100755 --- a/app/api/etudiants.py +++ b/app/api/etudiants.py @@ -137,6 +137,9 @@ def etudiant(etudid: int = None, nip: str = None, ine: str = None): return etud.to_dict_api() +@bp.route("/etudiant/etudid//photo") +@bp.route("/etudiant/nip//photo") +@bp.route("/etudiant/ine//photo") @api_web_bp.route("/etudiant/etudid//photo") @api_web_bp.route("/etudiant/nip//photo") @api_web_bp.route("/etudiant/ine//photo") diff --git a/app/api/justificatifs.py b/app/api/justificatifs.py index 2ec632929d..30357d81ca 100644 --- a/app/api/justificatifs.py +++ b/app/api/justificatifs.py @@ -18,7 +18,7 @@ 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 +from app.models import Identite, Justificatif, Departement, FormSemestre from app.models.assiduites import ( compute_assiduites_justified, ) @@ -682,8 +682,19 @@ def _filter_manager(requested, justificatifs_query): user_id = requested.args.get("user_id", False) if user_id is not False: - justificatif_query: Justificatif = scass.filter_by_user_id( - justificatif_query, user_id + justificatifs_query: Justificatif = scass.filter_by_user_id( + justificatifs_query, user_id + ) + + # cas 5 : formsemestre_id + formsemestre_id = requested.args.get("formsemestre_id") + + if formsemestre_id is not None: + 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 ) return justificatifs_query diff --git a/app/models/assiduites.py b/app/models/assiduites.py index 251bde7de4..0cea4781e9 100644 --- a/app/models/assiduites.py +++ b/app/models/assiduites.py @@ -129,6 +129,12 @@ class Assiduite(db.Model): raise ScoValueError( "Duplication des assiduités (la période rentrée rentre en conflit avec une assiduité enregistrée)" ) + + if not est_just: + est_just = ( + len(_get_assiduites_justif(etud.etudid, date_debut, date_fin)) > 0 + ) + if moduleimpl is not None: # Vérification de l'existence du module pour l'étudiant if moduleimpl.est_inscrit(etud): @@ -359,3 +365,20 @@ def compute_assiduites_justified( db.session.add(assi) db.session.commit() return assiduites_justifiees + + +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) + + +def _get_assiduites_justif( + etudid: int, date_debut: datetime, date_fin: datetime, long: bool = False +): + justifs: Justificatif = Justificatif.query.filter( + Justificatif.etudid == etudid, + Justificatif.date_debut <= date_debut, + Justificatif.date_fin >= date_fin, + ) + + return [j.justif_id if not long else j.to_dict(True) for j in justifs] diff --git a/app/scodoc/sco_assiduites.py b/app/scodoc/sco_assiduites.py index 5bd814a9f5..c78d1b8f89 100644 --- a/app/scodoc/sco_assiduites.py +++ b/app/scodoc/sco_assiduites.py @@ -194,7 +194,9 @@ def get_assiduites_stats( elif key == "moduleimpl_id": assiduites = filter_by_module_impl(assiduites, filtered[key]) elif key == "formsemestre": - assiduites = filter_by_formsemestre(assiduites, filtered[key]) + assiduites = filter_by_formsemestre( + assiduites, Assiduite, filtered[key] + ) elif key == "est_just": assiduites = filter_assiduites_by_est_just(assiduites, filtered[key]) elif key == "user_id": @@ -290,16 +292,20 @@ def filter_by_module_impl( return assiduites.filter(Assiduite.moduleimpl_id == module_impl_id) -def filter_by_formsemestre(assiduites_query: Assiduite, formsemestre: FormSemestre): +def filter_by_formsemestre( + collection_query: Assiduite or Justificatif, + collection_class: Assiduite or Justificatif, + formsemestre: FormSemestre, +): """ - Filtrage d'une collection d'assiduites en fonction d'un formsemestre + Filtrage d'une collection en fonction d'un formsemestre """ if formsemestre is None: - return assiduites_query.filter(False) + return collection_query.filter(False) - assiduites_query = ( - assiduites_query.join(Identite, Assiduite.etudid == Identite.id) + collection_result = ( + collection_query.join(Identite, collection_class.etudid == Identite.id) .join( FormSemestreInscription, Identite.id == FormSemestreInscription.etudid, @@ -310,9 +316,11 @@ def filter_by_formsemestre(assiduites_query: Assiduite, formsemestre: FormSemest form_date_debut = formsemestre.date_debut + timedelta(days=1) form_date_fin = formsemestre.date_fin + timedelta(days=1) - assiduites_query = assiduites_query.filter(Assiduite.date_debut >= form_date_debut) + collection_result = collection_result.filter( + collection_class.date_debut >= form_date_debut + ) - return assiduites_query.filter(Assiduite.date_fin <= form_date_fin) + return collection_result.filter(collection_class.date_fin <= form_date_fin) def justifies(justi: Justificatif, obj: bool = False) -> list[int]: diff --git a/app/static/js/assiduites.js b/app/static/js/assiduites.js index f8779cb0e3..5fe6bba82b 100644 --- a/app/static/js/assiduites.js +++ b/app/static/js/assiduites.js @@ -1716,3 +1716,13 @@ function getModuleImpl(assiduite) { return moduleimpls[id]; } + +function getUser(obj) { + if ("external_data" in obj && obj.external_data != null) { + if ("enseignant" in obj.external_data) { + 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 596336e478..c40d211b00 100644 --- a/app/templates/assiduites/widgets/tableau_assi.j2 +++ b/app/templates/assiduites/widgets/tableau_assi.j2 @@ -108,7 +108,7 @@ async_get( path, (data) => { - const user = data.user_id; + const user = getUser(data); const module = getModuleImpl(data); const date_debut = moment.tz(data.date_debut, TIMEZONE).format("DD/MM/YYYY HH:mm"); diff --git a/app/templates/assiduites/widgets/tableau_base.j2 b/app/templates/assiduites/widgets/tableau_base.j2 index a05a6d0614..5a795af1a9 100644 --- a/app/templates/assiduites/widgets/tableau_base.j2 +++ b/app/templates/assiduites/widgets/tableau_base.j2 @@ -246,8 +246,8 @@ } if (keyword.indexOf("module") != -1) { - keyValueA = getModuleImpl(keyValueA); - keyValueB = getModuleImpl(keyValueB); + keyValueA = getModuleImpl(a); + keyValueB = getModuleImpl(b); } let orderDertermined = keyValueA > keyValueB; diff --git a/app/templates/assiduites/widgets/tableau_justi.j2 b/app/templates/assiduites/widgets/tableau_justi.j2 index 1cbeea8acb..c4b677733c 100644 --- a/app/templates/assiduites/widgets/tableau_justi.j2 +++ b/app/templates/assiduites/widgets/tableau_justi.j2 @@ -124,7 +124,7 @@ async_get( path, (data) => { - const user = data.user_id; + const user = getUser(data); const date_debut = moment.tz(data.date_debut, TIMEZONE).format("DD/MM/YYYY HH:mm"); const date_fin = moment.tz(data.date_fin, TIMEZONE).format("DD/MM/YYYY HH:mm"); const entry_date = moment.tz(data.entry_date, TIMEZONE).format("DD/MM/YYYY HH:mm"); @@ -336,7 +336,7 @@ a.textContent = name a.classList.add("fich-file") - a.onclick = () => { downloadFile(id, name) }; + a.onclick = () => { downloadFile(justif_id, name) }; const input = document.createElement('input') input.type = "checkbox" diff --git a/tests/unit/test_assiduites.py b/tests/unit/test_assiduites.py index a9205659e6..a9657278f9 100644 --- a/tests/unit/test_assiduites.py +++ b/tests/unit/test_assiduites.py @@ -926,13 +926,22 @@ def verifier_comptage_et_filtrage_assiduites( FormSemestre.query.filter_by(id=fms["id"]).first() for fms in formsemestres ] assert ( - scass.filter_by_formsemestre(etu1.assiduites, formsemestres[0]).count() == 4 + scass.filter_by_formsemestre( + etu1.assiduites, Assiduite, formsemestres[0] + ).count() + == 4 ), "Filtrage 'Formsemestre' mauvais" assert ( - scass.filter_by_formsemestre(etu1.assiduites, formsemestres[1]).count() == 3 + scass.filter_by_formsemestre( + etu1.assiduites, Assiduite, formsemestres[1] + ).count() + == 3 ), "Filtrage 'Formsemestre' mauvais" assert ( - scass.filter_by_formsemestre(etu1.assiduites, formsemestres[2]).count() == 0 + scass.filter_by_formsemestre( + etu1.assiduites, Assiduite, formsemestres[2] + ).count() + == 0 ), "Filtrage 'Formsemestre' mauvais" # Date début