From 61061d4905dc13d434f20e879434efdb08b1041d Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 10 May 2022 10:06:51 +0200 Subject: [PATCH 01/23] Table recap.: export evaluations en excel --- app/models/formsemestre.py | 9 +++++++-- app/scodoc/sco_recapcomplet.py | 5 +++-- app/static/js/table_recap.js | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index dee03510e..e243c3983 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -287,7 +287,7 @@ class FormSemestre(db.Model): """ if not self.etapes: return "" - return ", ".join(sorted([str(x.etape_apo) for x in self.etapes])) + return ", ".join(sorted([etape.etape_apo for etape in self.etapes if etape])) def responsables_str(self, abbrev_prenom=True) -> str: """chaîne "J. Dupond, X. Martin" @@ -449,10 +449,15 @@ class FormSemestreEtape(db.Model): db.Integer, db.ForeignKey("notes_formsemestre.id"), ) + # etape_apo aurait du etre not null, mais oublié etape_apo = db.Column(db.String(APO_CODE_STR_LEN), index=True) + def __bool__(self): + "Etape False if code empty" + return self.etape_apo is not None and (len(self.etape_apo) > 0) + def __repr__(self): - return f"" + return f"" def as_apovdi(self): return ApoEtapeVDI(self.etape_apo) diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py index 6a22c1953..96d576bf3 100644 --- a/app/scodoc/sco_recapcomplet.py +++ b/app/scodoc/sco_recapcomplet.py @@ -85,7 +85,7 @@ def formsemestre_recapcomplet( """ formsemestre = FormSemestre.query.get_or_404(formsemestre_id) file_formats = {"csv", "json", "xls", "xlsx", "xlsall", "xml"} - supported_formats = file_formats | {"html"} + supported_formats = file_formats | {"html", "evals"} if tabformat not in supported_formats: raise ScoValueError(f"Format non supporté: {tabformat}") is_file = tabformat in file_formats @@ -131,7 +131,8 @@ def formsemestre_recapcomplet( for (format, label) in ( ("html", "Tableau"), ("evals", "Avec toutes les évaluations"), - ("xlsx", "Excel non formatté"), + ("xlsx", "Excel (non formatté)"), + ("xlsall", "Excel avec évaluations"), ("xml", "Bulletins XML (obsolète)"), ("json", "Bulletins JSON"), ): diff --git a/app/static/js/table_recap.js b/app/static/js/table_recap.js index 4dcd0c23a..36426b508 100644 --- a/app/static/js/table_recap.js +++ b/app/static/js/table_recap.js @@ -100,7 +100,7 @@ $(function () { }, { // Elimine les 0 à gauche pour les exports excel et les "copy" - targets: ["col_mod", "col_moy_gen", "col_ue", "col_res", "col_sae"], + targets: ["col_mod", "col_moy_gen", "col_ue", "col_res", "col_sae", "evaluation"], render: function (data, type, row) { return type === 'export' ? data.replace(/0(\d\..*)/, '$1') : data; } From 272a57d1c88bd3f4bafccfef322b311cafa7a7b7 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 10 May 2022 11:04:24 +0200 Subject: [PATCH 02/23] Fixes #380 --- app/scodoc/sco_edit_module.py | 6 +++--- sco_version.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py index ece30a345..68166788e 100644 --- a/app/scodoc/sco_edit_module.py +++ b/app/scodoc/sco_edit_module.py @@ -359,7 +359,6 @@ def can_delete_module(module): def do_module_delete(oid): "delete module" - from app.scodoc import sco_formations module = Module.query.get_or_404(oid) mod = module_list({"module_id": oid})[0] # sco7 @@ -422,13 +421,14 @@ def module_delete(module_id=None): H = [ html_sco_header.sco_header(page_title="Suppression d'un module"), - """

Suppression du module %(titre)s (%(code)s)

""" % mod, + f"""

Suppression du module {module.titre} ({module.code})

""", ] dest_url = url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, - formation_id=str(mod["formation_id"]), + formation_id=module.formation_id, + semestre_idx=module.ue.semestre_idx, ) tf = TrivialFormulator( request.base_url, diff --git a/sco_version.py b/sco_version.py index 8c3e5f8bd..027f1d59a 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.2.18" +SCOVERSION = "9.2.19" SCONAME = "ScoDoc" From 0d638de2090ea229e6ade3bf5c110623140c819d Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 10 May 2022 17:56:21 +0200 Subject: [PATCH 03/23] Fix: UE delete --- app/scodoc/sco_edit_ue.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index fd25e5da3..4c0a8ef59 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -156,6 +156,8 @@ def do_ue_delete(ue_id, delete_validations=False, force=False): from app.scodoc import sco_parcours_dut ue = UniteEns.query.get_or_404(ue_id) + formation_id = ue.formation_id + semestre_idx = ue.semestre_idx if not can_delete_ue(ue): raise ScoNonEmptyFormationObject( "UE", @@ -163,8 +165,8 @@ def do_ue_delete(ue_id, delete_validations=False, force=False): dest_url=url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, - formation_id=ue.formation_id, - semestre_idx=ue.semestre_idx, + formation_id=formation_id, + semestre_idx=semestre_idx, ), ) @@ -187,13 +189,13 @@ def do_ue_delete(ue_id, delete_validations=False, force=False): cancel_url=url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, - formation_id=ue.formation_id, - semestre_idx=ue.semestre_idx, + formation_id=formation_id, + semestre_idx=semestre_idx, ), parameters={"ue_id": ue.id, "dialog_confirmed": 1}, ) if delete_validations: - log("deleting all validations of UE %s" % ue.id) + log(f"deleting all validations of UE {ue.id}") ndb.SimpleQuery( "DELETE FROM scolar_formsemestre_validation WHERE ue_id=%(ue_id)s", {"ue_id": ue.id}, @@ -215,10 +217,10 @@ def do_ue_delete(ue_id, delete_validations=False, force=False): # utilisé: acceptable de tout invalider): sco_cache.invalidate_formsemestre() # news - F = sco_formations.formation_list(args={"formation_id": ue.formation_id})[0] + F = sco_formations.formation_list(args={"formation_id": formation_id})[0] ScolarNews.add( typ=ScolarNews.NEWS_FORM, - obj=ue.formation_id, + obj=formation_id, text=f"Modification de la formation {F['acronyme']}", max_frequency=10 * 60, ) @@ -228,8 +230,8 @@ def do_ue_delete(ue_id, delete_validations=False, force=False): url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, - formation_id=ue.formation_id, - semestre_idx=ue.semestre_idx, + formation_id=formation_id, + semestre_idx=semestre_idx, ) ) return None From a40cea67f08d6d550d2208fecf184301c77c766b Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 10 May 2022 18:03:33 +0200 Subject: [PATCH 04/23] =?UTF-8?q?Compl=C3=A8te=20#377:=20bulles=20aide=20s?= =?UTF-8?q?ur=20liste=20abs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_abs_views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/scodoc/sco_abs_views.py b/app/scodoc/sco_abs_views.py index 8427777f2..c01588df6 100644 --- a/app/scodoc/sco_abs_views.py +++ b/app/scodoc/sco_abs_views.py @@ -983,7 +983,8 @@ def _tables_abs_etud( )[0] if format == "html": ex.append( - f"""{mod["module"]["code"] or '(module sans code)'}""" ) From 04bda84228a27f29a9f6b1b9e05d8082626517ca Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 10 May 2022 18:09:42 +0200 Subject: [PATCH 05/23] formsemestre_bulletinetud: 404 if invalid formsemestre_id --- app/views/notes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/notes.py b/app/views/notes.py index e66a42f7b..8c1ef42df 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -292,7 +292,7 @@ def formsemestre_bulletinetud( format = format or "html" if not isinstance(formsemestre_id, int): - raise ValueError("formsemestre_id must be an integer !") + abort(404) formsemestre = FormSemestre.query.get_or_404(formsemestre_id) if etudid: etud = models.Identite.query.get_or_404(etudid) From 4eb08ec1d429e827cf4a89070c4fca6151e7aab5 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 10 May 2022 18:18:44 +0200 Subject: [PATCH 06/23] formsemestre_recapcomplet: 404 if invalid formsemestre_id --- app/scodoc/sco_recapcomplet.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py index 96d576bf3..cc4fb2bc1 100644 --- a/app/scodoc/sco_recapcomplet.py +++ b/app/scodoc/sco_recapcomplet.py @@ -32,7 +32,7 @@ import time from xml.etree import ElementTree from flask import g, request -from flask import url_for +from flask import abort, url_for from app import log from app.but import bulletin_but @@ -83,6 +83,8 @@ def formsemestre_recapcomplet( force_publishing: publie les xml et json même si bulletins non publiés selected_etudid: etudid sélectionné (pour scroller au bon endroit) """ + if not isinstance(formsemestre_id, int): + abort(404) formsemestre = FormSemestre.query.get_or_404(formsemestre_id) file_formats = {"csv", "json", "xls", "xlsx", "xlsall", "xml"} supported_formats = file_formats | {"html", "evals"} From 55ffc80400c31f51986f7ac1566d7de89018d40c Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 10 May 2022 18:21:28 +0200 Subject: [PATCH 07/23] Fix: affichages malus --- app/scodoc/sco_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index ff91c1495..c1e7dca27 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -191,7 +191,7 @@ def fmt_note(val, note_max=None, keep_numeric=False): return "EXC" # excuse, note neutralise if val == NOTES_ATTENTE: return "ATT" # attente, note neutralisee - if isinstance(val, float) or isinstance(val, int): + if not isinstance(val, str): if np.isnan(val): return "~" if (note_max is not None) and note_max > 0: From 52d0499c9b73cfd4ef0eda4d6f6fe1d0d033000a Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 10 May 2022 20:29:30 +0200 Subject: [PATCH 08/23] Fix: scodoc_table_results --- app/scodoc/sco_export_results.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/scodoc/sco_export_results.py b/app/scodoc/sco_export_results.py index 30750e3e5..6c0053069 100644 --- a/app/scodoc/sco_export_results.py +++ b/app/scodoc/sco_export_results.py @@ -208,25 +208,29 @@ def _build_results_list(dpv_by_sem, etuds_infos): return rows, titles, columns_ids -def get_set_formsemestre_id_dates(start_date, end_date): +def get_set_formsemestre_id_dates(start_date, end_date) -> set: """Ensemble des formsemestre_id entre ces dates""" s = ndb.SimpleDictFetch( """SELECT id FROM notes_formsemestre - WHERE date_debut >= %(start_date)s AND date_fin <= %(end_date)s + WHERE date_debut >= %(start_date)s + AND date_fin <= %(end_date)s + AND dept_id = %(dept_id)s """, - {"start_date": start_date, "end_date": end_date}, + {"start_date": start_date, "end_date": end_date, "dept_id": g.scodoc_dept_id}, ) return {x["id"] for x in s} -def scodoc_table_results(start_date="", end_date="", types_parcours=[], format="html"): +def scodoc_table_results( + start_date="", end_date="", types_parcours: list = None, format="html" +): """Page affichant la table des résultats Les dates sont en dd/mm/yyyy (datepicker javascript) types_parcours est la liste des types de parcours à afficher (liste de chaines, eg ['100', '210'] ) """ - log("scodoc_table_results: start_date=%s" % (start_date,)) # XXX + log(f"scodoc_table_results: start_date={start_date!r}") if not types_parcours: types_parcours = [] if not isinstance(types_parcours, list): From 9e494d39cb5ef0aaed04b4d713efcc3ef43b09b0 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 10 May 2022 20:32:25 +0200 Subject: [PATCH 09/23] typo --- app/auth/models.py | 2 +- app/scodoc/sco_recapcomplet.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/auth/models.py b/app/auth/models.py index 4ed1e41d5..ad56c5063 100644 --- a/app/auth/models.py +++ b/app/auth/models.py @@ -348,7 +348,7 @@ class User(UserMixin, db.Model): return None def get_nom_fmt(self): - """Nom formatté: "Martin" """ + """Nom formaté: "Martin" """ if self.nom: return sco_etud.format_nom(self.nom, uppercase=False) else: diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py index 96d576bf3..bbb1aee0c 100644 --- a/app/scodoc/sco_recapcomplet.py +++ b/app/scodoc/sco_recapcomplet.py @@ -131,7 +131,7 @@ def formsemestre_recapcomplet( for (format, label) in ( ("html", "Tableau"), ("evals", "Avec toutes les évaluations"), - ("xlsx", "Excel (non formatté)"), + ("xlsx", "Excel (non formaté)"), ("xlsall", "Excel avec évaluations"), ("xml", "Bulletins XML (obsolète)"), ("json", "Bulletins JSON"), From 01af698c6c6b7c741858d08ec7e5282fb71a2bfe Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 10 May 2022 21:16:07 +0200 Subject: [PATCH 10/23] Import XML: remove useless route and set default module type --- app/scodoc/sco_formations.py | 5 +++++ app/views/notes.py | 11 ----------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/app/scodoc/sco_formations.py b/app/scodoc/sco_formations.py index 178138b22..6c6e70194 100644 --- a/app/scodoc/sco_formations.py +++ b/app/scodoc/sco_formations.py @@ -256,6 +256,11 @@ def formation_import_xml(doc: str, import_tags=True): mod_info[1]["formation_id"] = formation_id mod_info[1]["matiere_id"] = mat_id mod_info[1]["ue_id"] = ue_id + mod_info[1]["module_type"] = ( + scu.ModuleType.STANDARD + if mod_info[1]["module_type"] is None + else mod_info[1]["module_type"] + ) mod_id = sco_edit_module.do_module_create(mod_info[1]) if xml_module_id: modules_old2new[int(xml_module_id)] = mod_id diff --git a/app/views/notes.py b/app/views/notes.py index e66a42f7b..86c4f6f73 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -648,17 +648,6 @@ def formation_export(formation_id, export_ids=False, format=None): ) -@bp.route("/formation_import_xml") -@scodoc -@permission_required(Permission.ScoChangeFormation) -@scodoc7func -def formation_import_xml(file): - "import d'une formation en XML" - log("formation_import_xml") - doc = file.read() - return sco_formations.formation_import_xml(doc) - - @bp.route("/formation_import_xml_form", methods=["GET", "POST"]) @scodoc @permission_required(Permission.ScoChangeFormation) From 29bdfb5cb0e44e4e39a2ac6ada13a0a31e27696c Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 10 May 2022 21:19:12 +0200 Subject: [PATCH 11/23] oups --- app/scodoc/sco_formations.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/scodoc/sco_formations.py b/app/scodoc/sco_formations.py index 6c6e70194..ae6ecc0b6 100644 --- a/app/scodoc/sco_formations.py +++ b/app/scodoc/sco_formations.py @@ -256,11 +256,8 @@ def formation_import_xml(doc: str, import_tags=True): mod_info[1]["formation_id"] = formation_id mod_info[1]["matiere_id"] = mat_id mod_info[1]["ue_id"] = ue_id - mod_info[1]["module_type"] = ( - scu.ModuleType.STANDARD - if mod_info[1]["module_type"] is None - else mod_info[1]["module_type"] - ) + if not "module_type" in mod_info[1]: + mod_info[1]["module_type"] = scu.ModuleType.STANDARD mod_id = sco_edit_module.do_module_create(mod_info[1]) if xml_module_id: modules_old2new[int(xml_module_id)] = mod_id From 7b4451918279950f9e8695525e34ce27db8d0df9 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 10 May 2022 21:41:46 +0200 Subject: [PATCH 12/23] Fix: pe_settag --- app/pe/pe_settag.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/pe/pe_settag.py b/app/pe/pe_settag.py index f4ada213b..be5028f3c 100644 --- a/app/pe/pe_settag.py +++ b/app/pe/pe_settag.py @@ -97,7 +97,7 @@ class SetTag(pe_tagtable.TableTag): """Mémorise les semtag nécessaires au jury.""" self.SemTagDict = {fid: SemTagDict[fid] for fid in self.get_Fids_in_settag()} if PE_DEBUG >= 1: - pe_print(u" => %d semestres fusionnés" % len(self.SemTagDict)) + pe_print(" => %d semestres fusionnés" % len(self.SemTagDict)) # ------------------------------------------------------------------------------------------------------------------- def comp_data_settag(self): @@ -210,7 +210,7 @@ class SetTagInterClasse(pe_tagtable.TableTag): # ------------------------------------------------------------------------------------------------------------------- def __init__(self, nom_combinaison, diplome): - pe_tagtable.TableTag.__init__(self, nom=nom_combinaison + "_%d" % diplome) + pe_tagtable.TableTag.__init__(self, nom=f"{nom_combinaison}_{diplome or ''}") self.combinaison = nom_combinaison self.parcoursDict = {} @@ -243,7 +243,7 @@ class SetTagInterClasse(pe_tagtable.TableTag): fid: SetTagDict[fid] for fid in self.get_Fids_in_settag() if fid != None } if PE_DEBUG >= 1: - pe_print(u" => %d semestres utilisés" % len(self.SetTagDict)) + pe_print(" => %d semestres utilisés" % len(self.SetTagDict)) # ------------------------------------------------------------------------------------------------------------------- def comp_data_settag(self): From 9935aabf3be5f3c0f7b90f68ed0f7f82054e87a4 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 10 May 2022 22:02:21 +0200 Subject: [PATCH 13/23] Bonus IUT de Blagnac --- app/comp/bonus_spo.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index 86d444c31..8d426b682 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -481,6 +481,19 @@ class BonusBezier(BonusSportAdditif): proportion_point = 0.03 +class BonusBlagnac(BonusSportAdditif): + """Calcul bonus modules optionnels (sport, culture), règle IUT de Blagnac. + + Le bonus est égal à 5% des points au dessus de 10 à appliquer sur toutes + les UE du semestre, applicable dans toutes les formations (DUT, BUT, ...). + """ + + name = "bonus_iutblagnac" + displayed_name = "IUT de Blagnac" + proportion_point = 0.05 + classic_use_bonus_ues = True # toujours sur les UE + + class BonusBordeaux1(BonusSportMultiplicatif): """Calcul bonus modules optionnels (sport, culture), règle IUT Bordeaux 1, sur moyenne générale et UEs. From 1209cf4ef3a8475fd5c82ceadbc86a409dcf48de Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 10 May 2022 22:40:00 +0200 Subject: [PATCH 14/23] formatage --- app/scodoc/sco_formsemestre_exterieurs.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/app/scodoc/sco_formsemestre_exterieurs.py b/app/scodoc/sco_formsemestre_exterieurs.py index 9dfcbbb67..0da85f2c3 100644 --- a/app/scodoc/sco_formsemestre_exterieurs.py +++ b/app/scodoc/sco_formsemestre_exterieurs.py @@ -262,7 +262,7 @@ def formsemestre_ext_edit_ue_validations(formsemestre_id, etudid): ) -def _make_page(etud, sem, tf, message=""): +def _make_page(etud: dict, sem, tf, message="") -> list: formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"]) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) moy_gen = nt.get_etud_moy_gen(etud["etudid"]) @@ -277,21 +277,20 @@ def _make_page(etud, sem, tf, message=""):

""" % etud, - """

La moyenne de ce semestre serait: - %s / 20 + f"""

La moyenne de ce semestre serait: + {moy_gen} / 20

- """ - % moy_gen, + """, '
', tf[1], "
", - """ - """ - % (sem["formsemestre_id"], etud["etudid"]), + f""" + """, html_sco_header.sco_footer(), ] return H From f9ec454da5a038b4c364e854deafc985283f955b Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 11 May 2022 00:59:51 +0200 Subject: [PATCH 15/23] =?UTF-8?q?API:=20revision=20pour=20multi-d=C3=A9par?= =?UTF-8?q?tements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/absences.py | 133 ++++--- app/api/etudiants.py | 544 ++++++++++++++++------------- app/models/etudiants.py | 1 + tests/api/exemple-api-basic.py | 25 +- tests/api/test_api_absences.py | 33 +- tests/api/test_api_etudiants.py | 2 +- tests/api/test_api_formsemestre.py | 9 +- 7 files changed, 396 insertions(+), 351 deletions(-) diff --git a/app/api/absences.py b/app/api/absences.py index 44b1c3500..51c4673e9 100644 --- a/app/api/absences.py +++ b/app/api/absences.py @@ -5,73 +5,62 @@ from flask import jsonify from app.api import bp from app.api.errors import error_response from app.api.auth import token_auth, token_permission_required -from app.api.tools import get_etud_from_etudid_or_nip_or_ine -from app.scodoc import notesdb as ndb +from app.models import Identite +from app.scodoc import notesdb as ndb from app.scodoc import sco_abs -from app.scodoc.sco_groups import get_group_members from app.scodoc.sco_permissions import Permission @bp.route("/absences/etudid/", methods=["GET"]) -@bp.route("/absences/nip/", methods=["GET"]) -@bp.route("/absences/ine/", methods=["GET"]) @token_auth.login_required @token_permission_required(Permission.APIView) -def absences(etudid: int = None, nip: int = None, ine: int = None): +def absences(etudid: int = None): """ Retourne la liste des absences d'un étudiant donné etudid : l'etudid d'un étudiant - nip: le code nip d'un étudiant - ine : le code ine d'un étudiant Exemple de résultat: - [ - { - "jour": "2022-04-15", - "matin": true, - "estabs": true, - "estjust": true, - "description": "", - "begin": "2022-04-15 08:00:00", - "end": "2022-04-15 11:59:59" - }, - { - "jour": "2022-04-15", - "matin": false, - "estabs": true, - "estjust": false, - "description": "", - "begin": "2022-04-15 12:00:00", - "end": "2022-04-15 17:59:59" - } - ] + [ + { + "jour": "2022-04-15", + "matin": true, + "estabs": true, + "estjust": true, + "description": "", + "begin": "2022-04-15 08:00:00", + "end": "2022-04-15 11:59:59" + }, + { + "jour": "2022-04-15", + "matin": false, + "estabs": true, + "estjust": false, + "description": "", + "begin": "2022-04-15 12:00:00", + "end": "2022-04-15 17:59:59" + } + ] """ - if etudid is None: - # Récupération de l'étudiant - etud = get_etud_from_etudid_or_nip_or_ine(etudid, nip, ine) - if etud is None: - return error_response( - 404, - message="id de l'étudiant (etudid, nip, ine) inconnu", - ) - etudid = etud.etudid - - # Récupération des absences de l'étudiant + etud = Identite.query.get(etudid) + if etud is None: + return error_response( + 404, + message="id de l'étudiant (etudid, nip, ine) inconnu", + ) + # Absences de l'étudiant ndb.open_db_connection() - absences = sco_abs.list_abs_date(etudid) + absences = sco_abs.list_abs_date(etud.id) for absence in absences: absence["jour"] = absence["jour"].isoformat() return jsonify(absences) @bp.route("/absences/etudid//just", methods=["GET"]) -@bp.route("/absences/nip//just", methods=["GET"]) -@bp.route("/absences/ine//just", methods=["GET"]) @token_auth.login_required @token_permission_required(Permission.APIView) -def absences_just(etudid: int = None, nip: int = None, ine: int = None): +def absences_just(etudid: int = None): """ Retourne la liste des absences justifiées d'un étudiant donné @@ -80,39 +69,37 @@ def absences_just(etudid: int = None, nip: int = None, ine: int = None): ine : le code ine d'un étudiant Exemple de résultat : - [ - { - "jour": "2022-04-15", - "matin": true, - "estabs": true, - "estjust": true, - "description": "", - "begin": "2022-04-15 08:00:00", - "end": "2022-04-15 11:59:59" - }, - { - "jour": "Fri, 15 Apr 2022 00:00:00 GMT", - "matin": false, - "estabs": true, - "estjust": true, - "description": "", - "begin": "2022-04-15 12:00:00", - "end": "2022-04-15 17:59:59" - } - ] + [ + { + "jour": "2022-04-15", + "matin": true, + "estabs": true, + "estjust": true, + "description": "", + "begin": "2022-04-15 08:00:00", + "end": "2022-04-15 11:59:59" + }, + { + "jour": "Fri, 15 Apr 2022 00:00:00 GMT", + "matin": false, + "estabs": true, + "estjust": true, + "description": "", + "begin": "2022-04-15 12:00:00", + "end": "2022-04-15 17:59:59" + } + ] """ - if etudid is None: - etud = get_etud_from_etudid_or_nip_or_ine(etudid, nip, ine) - if etud is None: - return error_response( - 404, - message="id de l'étudiant (etudid, nip, ine) inconnu", - ) - etudid = etud.etudid + etud = Identite.query.get(etudid) + if etud is None: + return error_response( + 404, + message="id de l'étudiant (etudid, nip, ine) inconnu", + ) - # Récupération des absences justifiées de l'étudiant + # Absences justifiées de l'étudiant abs_just = [ - absence for absence in sco_abs.list_abs_date(etudid) if absence["estjust"] + absence for absence in sco_abs.list_abs_date(etud.id) if absence["estjust"] ] for absence in abs_just: absence["jour"] = absence["jour"].isoformat() diff --git a/app/api/etudiants.py b/app/api/etudiants.py index a41634f95..a193553fa 100644 --- a/app/api/etudiants.py +++ b/app/api/etudiants.py @@ -1,14 +1,20 @@ -#################################################### Etudiants ######################################################## +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +""" + API : accès aux étudiants +""" from flask import jsonify import app -from app import models from app.api import bp from app.api.errors import error_response from app.api.auth import token_auth, token_permission_required -from app.api.tools import get_etud_from_etudid_or_nip_or_ine -from app.models import FormSemestreInscription, FormSemestre, Identite +from app.models import Departement, FormSemestreInscription, FormSemestre, Identite from app.scodoc import sco_bulletins from app.scodoc import sco_groups from app.scodoc.sco_permissions import Permission @@ -59,53 +65,102 @@ def etudiants_courant(long=False): @bp.route("/etudiant/ine/", methods=["GET"]) @token_auth.login_required @token_permission_required(Permission.APIView) -def etudiant(etudid: int = None, nip: int = None, ine: int = None): +def etudiant(etudid: int = None, nip: str = None, ine: str = None): """ - Retourne les informations de l'étudiant correspondant à l'id passé en paramètres. + Retourne les informations de l'étudiant correspondant, ou 404 si non trouvé. - etudid : l'etudid d'un étudiant - nip : le code nip d'un étudiant - ine : le code ine d'un étudiant + etudid : l'etudid de l'étudiant + nip : le code nip de l'étudiant + ine : le code ine de l'étudiant + + Les codes INE et NIP sont uniques au sein d'un département. + Si plusieurs objets ont le même code, on ramène le plus récemment inscrit. Exemple de résultat : - { - "civilite": "X", - "code_ine": "1", - "code_nip": "1", - "date_naissance": "", - "email": "SACHA.COSTA@example.com", - "emailperso": "", - "etudid": 1, - "nom": "COSTA", - "prenom": "SACHA", - "nomprenom": "Sacha COSTA", - "lieu_naissance": "", - "dept_naissance": "", - "nationalite": "", - "boursier": "", - "id": 1, - "codepostaldomicile": "", - "paysdomicile": "", - "telephonemobile": "", - "typeadresse": "domicile", - "domicile": "", - "villedomicile": "", - "telephone": "", - "fax": "", - "description": "" - } + { + "civilite": "X", + "code_ine": "1", + "code_nip": "1", + "date_naissance": "", + "email": "SACHA.COSTA@example.com", + "emailperso": "", + "etudid": 1, + "nom": "COSTA", + "prenom": "SACHA", + "nomprenom": "Sacha COSTA", + "lieu_naissance": "", + "dept_naissance": "", + "nationalite": "", + "boursier": "", + "id": 1, + "codepostaldomicile": "", + "paysdomicile": "", + "telephonemobile": "", + "typeadresse": "domicile", + "domicile": "", + "villedomicile": "", + "telephone": "", + "fax": "", + "description": "" + } """ - # Récupération de l'étudiant - etud = get_etud_from_etudid_or_nip_or_ine(etudid, nip, ine) + if etudid is not None: + etud = Identite.query.get(etudid) + else: + if nip is not None: + query = Identite.query.filter_by(code_nip=nip) + elif ine is not None: + query = Identite.query.filter_by(code_ine=ine) + else: + return error_response( + 404, + message="parametre manquant", + ) + if query.count() > 1: # cas rare d'un étudiant présent dans plusieurs depts + etuds = [] + for e in query: + admission = e.admission.first() + etuds.append((((admission.annee or 0) if admission else 0), e)) + etuds.sort() + etud = etuds[-1][1] + else: + etud = query.first() + if etud is None: return error_response( 404, - message="id de l'étudiant (etudid, nip, ine) inconnu", + message="étudiant inconnu", ) - # Mise en forme des données - data = etud.to_dict_bul(include_urls=False) - return jsonify(data) + return jsonify(etud.to_dict_bul(include_urls=False)) + + +@bp.route("/etudiants/etudid/", methods=["GET"]) +@bp.route("/etudiants/nip/", methods=["GET"]) +@bp.route("/etudiants/ine/", methods=["GET"]) +@token_auth.login_required +@token_permission_required(Permission.APIView) +def etudiants(etudid: int = None, nip: str = None, ine: str = None): + """ + Info sur le ou les étudiants correspondant. Comme /etudiant mais renvoie + toujours une liste. + Si non trouvé, liste vide, pas d'erreur. + Dans 99% des cas, la liste contient un seul étudiant, mais si l'étudiant a + été inscrit dans plusieurs départements, on a plusieurs objets (1 par dept.). + """ + if etudid is not None: + query = Identite.query.filter_by(id=etudid) + elif nip is not None: + query = Identite.query.filter_by(code_nip=nip) + elif ine is not None: + query = Identite.query.filter_by(code_ine=ine) + else: + return error_response( + 404, + message="parametre manquant", + ) + + return jsonify([etud.to_dict_bul(include_urls=False) for etud in query]) @bp.route("/etudiant/etudid//formsemestres") @@ -115,56 +170,65 @@ def etudiant(etudid: int = None, nip: int = None, ine: int = None): @token_permission_required(Permission.APIView) def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None): """ - Retourne la liste des semestres qu'un étudiant a suivis, triés par ordre chronologique. - - etudid : l'etudid d'un étudiant - nip : le code nip d'un étudiant - ine : le code ine d'un étudiant + Liste des semestres qu'un étudiant a suivi, triés par ordre chronologique. + Accès par etudid, nip ou ine Exemple de résultat : - [ - { - "date_fin": "31/08/2022", - "resp_can_edit": false, - "dept_id": 1, - "etat": true, - "resp_can_change_ens": true, - "id": 1, - "modalite": "FI", - "ens_can_edit_eval": false, - "formation_id": 1, - "gestion_compensation": false, - "elt_sem_apo": null, - "semestre_id": 1, - "bul_hide_xml": false, - "elt_annee_apo": null, - "titre": "Semestre test", - "block_moyennes": false, - "scodoc7_id": null, - "date_debut": "01/09/2021", - "gestion_semestrielle": false, - "bul_bgcolor": "white", - "formsemestre_id": 1, - "titre_num": "Semestre test semestre 1", - "date_debut_iso": "2021-09-01", - "date_fin_iso": "2022-08-31", - "responsables": [] - }, - ... - ] + [ + { + "date_fin": "31/08/2022", + "resp_can_edit": false, + "dept_id": 1, + "etat": true, + "resp_can_change_ens": true, + "id": 1, + "modalite": "FI", + "ens_can_edit_eval": false, + "formation_id": 1, + "gestion_compensation": false, + "elt_sem_apo": null, + "semestre_id": 1, + "bul_hide_xml": false, + "elt_annee_apo": null, + "titre": "Semestre test", + "block_moyennes": false, + "scodoc7_id": null, + "date_debut": "01/09/2021", + "gestion_semestrielle": false, + "bul_bgcolor": "white", + "formsemestre_id": 1, + "titre_num": "Semestre test semestre 1", + "date_debut_iso": "2021-09-01", + "date_fin_iso": "2022-08-31", + "responsables": [] + }, + ... + ] """ - # Récupération de l'étudiant - etud = get_etud_from_etudid_or_nip_or_ine(etudid, nip, ine) - if etud is None: + if etudid is not None: + query = FormSemestre.query.filter( + FormSemestreInscription.etudid == etudid, + FormSemestreInscription.formsemestre_id == FormSemestre.id, + ) + elif nip is not None: + query = FormSemestre.query.filter( + Identite.code_nip == nip, + FormSemestreInscription.etudid == Identite.id, + FormSemestreInscription.formsemestre_id == FormSemestre.id, + ) + elif ine is not None: + query = FormSemestre.query.filter( + Identite.code_ine == ine, + FormSemestreInscription.etudid == Identite.id, + FormSemestreInscription.formsemestre_id == FormSemestre.id, + ) + else: return error_response( 404, - message="id de l'étudiant (etudid, nip, ine) inconnu", + message="parametre manquant", ) - formsemestres = models.FormSemestre.query.filter( - models.FormSemestreInscription.etudid == etud.id, - models.FormSemestreInscription.formsemestre_id == models.FormSemestre.id, - ).order_by(models.FormSemestre.date_debut) + formsemestres = query.order_by(FormSemestre.date_debut) return jsonify([formsemestre.to_dict() for formsemestre in formsemestres]) @@ -204,8 +268,8 @@ def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None) def etudiant_bulletin_semestre( formsemestre_id, etudid: int = None, - nip: int = None, - ine: int = None, + nip: str = None, + ine: str = None, version="long", ): """ @@ -216,12 +280,12 @@ def etudiant_bulletin_semestre( nip : le code nip d'un étudiant ine : le code ine d'un étudiant Exemple de résultat : - { - "version": "0", - "type": "BUT", - "date": "2022-04-27T07:18:16.450634Z", - "publie": true, - "etudiant": { + { + "version": "0", + "type": "BUT", + "date": "2022-04-27T07:18:16.450634Z", + "publie": true, + "etudiant": { "civilite": "X", "code_ine": "1", "code_nip": "1", @@ -247,17 +311,17 @@ def etudiant_bulletin_semestre( "villedomicile": "", "telephone": "", "fax": "", - "description": "" - }, - "formation": { + "description": "", + }, + "formation": { "id": 1, "acronyme": "BUT R&T", "titre_officiel": "Bachelor technologique r\u00e9seaux et t\u00e9l\u00e9communications", - "titre": "BUT R&T" - }, - "formsemestre_id": 1, - "etat_inscription": "I", - "options": { + "titre": "BUT R&T", + }, + "formsemestre_id": 1, + "etat_inscription": "I", + "options": { "show_abs": true, "show_abs_modules": false, "show_ects": true, @@ -276,128 +340,113 @@ def etudiant_bulletin_semestre( "show_temporary": true, "temporary_txt": "Provisoire", "show_uevalid": true, - "show_date_inscr": true - }, - "ressources": { + "show_date_inscr": true, + }, + "ressources": { "R101": { - "id": 1, - "titre": "Initiation aux r\u00e9seaux informatiques", - "code_apogee": null, - "url": "/ScoDoc/TAPI/Scolarite/Notes/moduleimpl_status?moduleimpl_id=1", - "moyenne": {}, - "evaluations": [ - { - "id": 1, - "description": "eval1", - "date": "2022-04-20", - "heure_debut": "08:00", - "heure_fin": "09:00", - "coef": "01.00", - "poids": { - "RT1.1": 1.0, - }, - "note": { - "value": "12.00", - "min": "00.00", - "max": "18.00", - "moy": "10.88" - }, - "url": "/ScoDoc/TAPI/Scolarite/Notes/evaluation_listenotes?evaluation_id=1" - } - ] + "id": 1, + "titre": "Initiation aux r\u00e9seaux informatiques", + "code_apogee": null, + "url": "/ScoDoc/TAPI/Scolarite/Notes/moduleimpl_status?moduleimpl_id=1", + "moyenne": {}, + "evaluations": [ + { + "id": 1, + "description": "eval1", + "date": "2022-04-20", + "heure_debut": "08:00", + "heure_fin": "09:00", + "coef": "01.00", + "poids": { + "RT1.1": 1.0, + }, + "note": { + "value": "12.00", + "min": "00.00", + "max": "18.00", + "moy": "10.88", + }, + "url": "/ScoDoc/TAPI/Scolarite/Notes/evaluation_listenotes?evaluation_id=1", + } + ], }, - }, - "saes": { + }, + "saes": { "SAE11": { - "id": 2, - "titre": "Se sensibiliser \u00e0 l'hygi\u00e8ne informatique et \u00e0 la cybers\u00e9curit\u00e9", - "code_apogee": null, - "url": "/ScoDoc/TAPI/Scolarite/Notes/moduleimpl_status?moduleimpl_id=2", - "moyenne": {}, - "evaluations": [] + "id": 2, + "titre": "Se sensibiliser \u00e0 l'hygi\u00e8ne informatique et \u00e0 la cybers\u00e9curit\u00e9", + "code_apogee": null, + "url": "/ScoDoc/TAPI/Scolarite/Notes/moduleimpl_status?moduleimpl_id=2", + "moyenne": {}, + "evaluations": [], }, - }, - "ues": { + }, + "ues": { "RT1.1": { - "id": 1, - "titre": "Administrer les r\u00e9seaux et l\u2019Internet", - "numero": 1, - "type": 0, - "color": "#B80004", - "competence": null, - "moyenne": { - "value": "08.50", - "min": "06.00", - "max": "16.50", - "moy": "11.31", - "rang": "12", - "total": 16 - }, - "bonus": "00.00", - "malus": "00.00", - "capitalise": null, - "ressources": { - "R101": { - "id": 1, - "coef": 12.0, - "moyenne": "12.00" + "id": 1, + "titre": "Administrer les r\u00e9seaux et l\u2019Internet", + "numero": 1, + "type": 0, + "color": "#B80004", + "competence": null, + "moyenne": { + "value": "08.50", + "min": "06.00", + "max": "16.50", + "moy": "11.31", + "rang": "12", + "total": 16, }, - }, - "saes": { - "SAE11": { - "id": 2, - "coef": 16.0, - "moyenne": "~" + "bonus": "00.00", + "malus": "00.00", + "capitalise": null, + "ressources": { + "R101": {"id": 1, "coef": 12.0, "moyenne": "12.00"}, }, - }, - "ECTS": { - "acquis": 0.0, - "total": 12.0 - } + "saes": { + "SAE11": {"id": 2, "coef": 16.0, "moyenne": "~"}, + }, + "ECTS": {"acquis": 0.0, "total": 12.0}, }, - "semestre": { - "etapes": [], - "date_debut": "2021-09-01", - "date_fin": "2022-08-31", - "annee_universitaire": "2021 - 2022", - "numero": 1, - "inscription": "", - "groupes": [], - "absences": { - "injustifie": 1, - "total": 2 + "semestre": { + "etapes": [], + "date_debut": "2021-09-01", + "date_fin": "2022-08-31", + "annee_universitaire": "2021 - 2022", + "numero": 1, + "inscription": "", + "groupes": [], + "absences": {"injustifie": 1, "total": 2}, + "ECTS": {"acquis": 0, "total": 30.0}, + "notes": {"value": "10.60", "min": "02.40", "moy": "11.05", "max": "17.40"}, + "rang": {"value": "10", "total": 16}, }, - "ECTS": { - "acquis": 0, - "total": 30.0 - }, - "notes": { - "value": "10.60", - "min": "02.40", - "moy": "11.05", - "max": "17.40" - }, - "rang": { - "value": "10", - "total": 16 - } - } - } + }, + } """ - formsemestre = models.FormSemestre.query.filter_by( - id=formsemestre_id - ).first_or_404() + formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first_or_404() + dept = Departement.query.filter_by(id=formsemestre.dept_id).first_or_404() - dept = models.Departement.query.filter_by(id=formsemestre.dept_id).first_or_404() + if etudid is not None: + query = Identite.query.filter_by(id=etudid) + elif nip is not None: + query = Identite.query.filter_by(code_nip=nip, dept_id=dept.id) + elif ine is not None: + query = Identite.query.filter_by(code_ine=ine, dept_id=dept.id) + else: + return error_response( + 404, + message="parametre manquant", + ) - app.set_sco_dept(dept.acronym) - - etud = get_etud_from_etudid_or_nip_or_ine(etudid, nip, ine) + etud = query.first() if etud is None: return error_response( 404, message="id de l'étudiant (etudid, nip, ine) inconnu", ) + + app.set_sco_dept(dept.acronym) return sco_bulletins.get_formsemestre_bulletin_etud_json( formsemestre, etud, version ) @@ -429,44 +478,57 @@ def etudiant_groups( ine : le code ine d'un étudiant Exemple de résultat : - [ - { - "partition_id": 1, - "id": 1, - "formsemestre_id": 1, - "partition_name": null, - "numero": 0, - "bul_show_rank": false, - "show_in_lists": true, - "group_id": 1, - "group_name": null - }, - { - "partition_id": 2, - "id": 2, - "formsemestre_id": 1, - "partition_name": "TD", - "numero": 1, - "bul_show_rank": false, - "show_in_lists": true, - "group_id": 2, - "group_name": "A" - } - ] + [ + { + "partition_id": 1, + "id": 1, + "formsemestre_id": 1, + "partition_name": null, + "numero": 0, + "bul_show_rank": false, + "show_in_lists": true, + "group_id": 1, + "group_name": null + }, + { + "partition_id": 2, + "id": 2, + "formsemestre_id": 1, + "partition_name": "TD", + "numero": 1, + "bul_show_rank": false, + "show_in_lists": true, + "group_id": 2, + "group_name": "A" + } + ] """ - if etudid is None: - etud = get_etud_from_etudid_or_nip_or_ine(etudid, nip, ine) - if etud is None: - return error_response( - 404, - message="id de l'étudiant (etudid, nip, ine) inconnu", - ) - etudid = etud.etudid - # Récupération du formsemestre - sem = models.FormSemestre.query.filter_by(id=formsemestre_id).first_or_404() - dept = models.Departement.query.get(sem.dept_id) + formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first() + if formsemestre is None: + return error_response( + 404, + message="formsemestre inconnu", + ) + dept = Departement.query.get(formsemestre.dept_id) + if etudid is not None: + query = Identite.query.filter_by(id=etudid) + elif nip is not None: + query = Identite.query.filter_by(code_nip=nip, dept_id=dept.id) + elif ine is not None: + query = Identite.query.filter_by(code_ine=ine, dept_id=dept.id) + else: + return error_response( + 404, + message="parametre manquant", + ) + etud = query.first() + if etud is None: + return error_response( + 404, + message="etudiant inconnu", + ) app.set_sco_dept(dept.acronym) - data = sco_groups.get_etud_groups(etudid, sem.id) + data = sco_groups.get_etud_groups(etud.id, formsemestre.id) return jsonify(data) diff --git a/app/models/etudiants.py b/app/models/etudiants.py index 912136e61..44b7ec360 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -178,6 +178,7 @@ class Identite(db.Model): "date_naissance": self.date_naissance.strftime("%d/%m/%Y") if self.date_naissance else "", + "dept_id": self.dept_id, "email": self.get_first_email() or "", "emailperso": self.get_first_email("emailperso"), "etudid": self.id, diff --git a/tests/api/exemple-api-basic.py b/tests/api/exemple-api-basic.py index a9f109289..1c7d61b8a 100644 --- a/tests/api/exemple-api-basic.py +++ b/tests/api/exemple-api-basic.py @@ -26,13 +26,17 @@ import urllib3 from pprint import pprint as pp # --- Lecture configuration (variables d'env ou .env) -BASEDIR = os.path.abspath(os.path.dirname(__file__)) +try: + BASEDIR = os.path.abspath(os.path.dirname(__file__)) +except NameError: + BASEDIR = "." + load_dotenv(os.path.join(BASEDIR, ".env")) CHK_CERT = bool(int(os.environ.get("CHECK_CERTIFICATE", False))) SCODOC_URL = os.environ["SCODOC_URL"] API_URL = SCODOC_URL + "/ScoDoc/api" SCODOC_USER = os.environ["SCODOC_USER"] -SCODOC_PASSWORD = os.environ["SCODOC_PASSWD"] +SCODOC_PASSWORD = os.environ["SCODOC_PASSWORD"] print(f"SCODOC_URL={SCODOC_URL}") print(f"API URL={API_URL}") @@ -90,6 +94,23 @@ formsemestre_id = 1028 # A adapter etudid = 14721 bul_dut = GET(f"/etudiant/etudid/{etudid}/formsemestre/{formsemestre_id}/bulletin") + +# Infos sur un étudiant +etudid = 3561 +code_nip = "11303314" +etud = GET(f"/etudiant/etudid/{etudid}") +print(etud) + +etud = GET(f"/etudiant/nip/{code_nip}") +print(etud) + +sems = GET(f"/etudiant/etudid/{etudid}/formsemestres") +print("\n".join([s["titre_num"] for s in sems])) + +sems = GET(f"/etudiant/nip/{code_nip}/formsemestres") +print("\n".join([s["titre_num"] for s in sems])) + + # # --- Recupere la liste de tous les semestres: # sems = GET(s, "Notes/formsemestre_list?format=json", "Aucun semestre !") diff --git a/tests/api/test_api_absences.py b/tests/api/test_api_absences.py index a0a90c576..687af7d62 100644 --- a/tests/api/test_api_absences.py +++ b/tests/api/test_api_absences.py @@ -22,8 +22,7 @@ from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers # Etudiant pour les tests ETUDID = 1 -INE = "1" -NIP = "1" + # absences def test_absences(api_headers): @@ -37,20 +36,6 @@ def test_absences(api_headers): ) assert r.status_code == 200 - r = requests.get( - f"{API_URL}/absences/nip/{NIP}", - headers=api_headers, - verify=CHECK_CERTIFICATE, - ) - assert r.status_code == 200 - - r = requests.get( - f"{API_URL}/absences/ine/{INE}", - headers=api_headers, - verify=CHECK_CERTIFICATE, - ) - assert r.status_code == 200 - # absences_justify def test_absences_justify(api_headers): @@ -65,22 +50,6 @@ def test_absences_justify(api_headers): assert r.status_code == 200 # TODO vérifier résultat - r = requests.get( - API_URL + f"/absences/nip/{NIP}/just", - headers=api_headers, - verify=CHECK_CERTIFICATE, - ) - assert r.status_code == 200 - # TODO vérifier résultat - - r = requests.get( - API_URL + f"/absences/ine/{INE}/just", - headers=api_headers, - verify=CHECK_CERTIFICATE, - ) - assert r.status_code == 200 - # TODO vérifier résultat - # XXX TODO # def test_abs_groupe_etat(api_headers): diff --git a/tests/api/test_api_etudiants.py b/tests/api/test_api_etudiants.py index c0ccfc9e7..4c88d8619 100644 --- a/tests/api/test_api_etudiants.py +++ b/tests/api/test_api_etudiants.py @@ -93,7 +93,7 @@ def test_etudiant(api_headers): ) assert r.status_code == 200 etud = r.json() - assert len(etud) == 24 + assert len(etud) == 25 fields_ok = verify_fields(etud, ETUD_FIELDS) assert fields_ok is True diff --git a/tests/api/test_api_formsemestre.py b/tests/api/test_api_formsemestre.py index 57d797721..bdcf7e93e 100644 --- a/tests/api/test_api_formsemestre.py +++ b/tests/api/test_api_formsemestre.py @@ -24,6 +24,11 @@ from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers from tests.api.tools_test_api import MODIMPL_FIELDS, verify_fields from tests.api.tools_test_api import FSEM_FIELDS, UE_FIELDS, MODULE_FIELDS +# Etudiant pour les tests +ETUDID = 1 +NIP = "1" +INE = "INE1" + def test_formsemestre(api_headers): """ @@ -53,7 +58,7 @@ def test_etudiant_bulletin(api_headers): bull_a = r.json() r = requests.get( - f"{API_URL}/etudiant/nip/1/formsemestre/{formsemestre_id}/bulletin", + f"{API_URL}/etudiant/nip/{NIP}/formsemestre/{formsemestre_id}/bulletin", headers=api_headers, verify=CHECK_CERTIFICATE, ) @@ -61,7 +66,7 @@ def test_etudiant_bulletin(api_headers): bull_b = r.json() r = requests.get( - f"{API_URL}/etudiant/ine/1/formsemestre/{formsemestre_id}/bulletin", + f"{API_URL}/etudiant/ine/{INE}/formsemestre/{formsemestre_id}/bulletin", headers=api_headers, verify=CHECK_CERTIFICATE, ) From 54d65c01ae3825af33904f8b16597369efd5583e Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 11 May 2022 01:00:52 +0200 Subject: [PATCH 16/23] 9.2.20 (API) --- sco_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sco_version.py b/sco_version.py index 027f1d59a..aaeef2963 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.2.19" +SCOVERSION = "9.2.20" SCONAME = "ScoDoc" From 457928522b64253b0648d8f9f8cc3e8d9680a62b Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 11 May 2022 04:14:42 +0200 Subject: [PATCH 17/23] =?UTF-8?q?API:=20evaluations,=20dept=5Facronym=20de?= =?UTF-8?q?s=20=C3=A9tudiants,=20tests=20associ=C3=A9s.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/evaluations.py | 3 ++- app/models/etudiants.py | 1 + app/models/evaluations.py | 9 ++++++--- app/scodoc/sco_recapcomplet.py | 4 +++- app/views/notes.py | 2 +- sco_version.py | 2 +- tests/api/exemple-api-basic.py | 4 +++- tests/api/setup_test_api.py | 1 + tests/api/test_api_departements.py | 9 +++++++-- tests/api/test_api_etudiants.py | 9 +++++++-- tests/api/tools_test_api.py | 2 ++ 11 files changed, 34 insertions(+), 12 deletions(-) diff --git a/app/api/evaluations.py b/app/api/evaluations.py index cef58a426..7d2776d29 100644 --- a/app/api/evaluations.py +++ b/app/api/evaluations.py @@ -4,6 +4,7 @@ from flask import jsonify import app from app import models +from app.models import Evaluation from app.api import bp from app.api.auth import token_auth, token_permission_required from app.api.errors import error_response @@ -46,7 +47,7 @@ def evaluations(moduleimpl_id: int): ] """ # Récupération de toutes les évaluations - evals = models.Evaluation.query.filter_by(id=moduleimpl_id) + evals = Evaluation.query.filter_by(id=moduleimpl_id) # Mise en forme des données data = [d.to_dict() for d in evals] diff --git a/app/models/etudiants.py b/app/models/etudiants.py index 44b7ec360..2e9292c1d 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -179,6 +179,7 @@ class Identite(db.Model): if self.date_naissance else "", "dept_id": self.dept_id, + "dept_acronym": self.departement.acronym, "email": self.get_first_email() or "", "emailperso": self.get_first_email("emailperso"), "etudid": self.id, diff --git a/app/models/evaluations.py b/app/models/evaluations.py index 84383adc5..ef2a62887 100644 --- a/app/models/evaluations.py +++ b/app/models/evaluations.py @@ -5,8 +5,6 @@ import datetime from app import db -from app.models import formsemestre -from app.models.formsemestre import FormSemestre from app.models.moduleimpls import ModuleImpl from app.models.ues import UniteEns @@ -54,7 +52,12 @@ class Evaluation(db.Model): # ScoDoc7 output_formators e["evaluation_id"] = self.id e["jour"] = ndb.DateISOtoDMY(e["jour"]) + e["date_debut"] = datetime.datetime.combine( + self.jour, self.heure_debut + ).isoformat() + e["date_fin"] = datetime.datetime.combine(self.jour, self.heure_fin).isoformat() e["numero"] = ndb.int_null_is_zero(e["numero"]) + e["poids"] = self.get_ue_poids_dict() # { ue_id : poids } return evaluation_enrich_dict(e) def from_dict(self, data): @@ -153,7 +156,7 @@ class EvaluationUEPoids(db.Model): # Fonction héritée de ScoDoc7 à refactorer def evaluation_enrich_dict(e): - """add or convert some fileds in an evaluation dict""" + """add or convert some fields in an evaluation dict""" # For ScoDoc7 compat heure_debut_dt = e["heure_debut"] or datetime.time( 8, 00 diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py index bbb1aee0c..1c5c19eb4 100644 --- a/app/scodoc/sco_recapcomplet.py +++ b/app/scodoc/sco_recapcomplet.py @@ -32,7 +32,7 @@ import time from xml.etree import ElementTree from flask import g, request -from flask import url_for +from flask import abort, url_for from app import log from app.but import bulletin_but @@ -83,6 +83,8 @@ def formsemestre_recapcomplet( force_publishing: publie les xml et json même si bulletins non publiés selected_etudid: etudid sélectionné (pour scroller au bon endroit) """ + if not isinstance(formsemestre_id, int): + abort(404) formsemestre = FormSemestre.query.get_or_404(formsemestre_id) file_formats = {"csv", "json", "xls", "xlsx", "xlsall", "xml"} supported_formats = file_formats | {"html", "evals"} diff --git a/app/views/notes.py b/app/views/notes.py index 86c4f6f73..f924acb92 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -292,7 +292,7 @@ def formsemestre_bulletinetud( format = format or "html" if not isinstance(formsemestre_id, int): - raise ValueError("formsemestre_id must be an integer !") + abort(404, description="formsemestre_id must be an integer !") formsemestre = FormSemestre.query.get_or_404(formsemestre_id) if etudid: etud = models.Identite.query.get_or_404(etudid) diff --git a/sco_version.py b/sco_version.py index aaeef2963..120447d32 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.2.20" +SCOVERSION = "9.2.21" SCONAME = "ScoDoc" diff --git a/tests/api/exemple-api-basic.py b/tests/api/exemple-api-basic.py index 1c7d61b8a..a581a8783 100644 --- a/tests/api/exemple-api-basic.py +++ b/tests/api/exemple-api-basic.py @@ -33,7 +33,7 @@ except NameError: load_dotenv(os.path.join(BASEDIR, ".env")) CHK_CERT = bool(int(os.environ.get("CHECK_CERTIFICATE", False))) -SCODOC_URL = os.environ["SCODOC_URL"] +SCODOC_URL = os.environ["SCODOC_URL"] or "http://localhost:5000" API_URL = SCODOC_URL + "/ScoDoc/api" SCODOC_USER = os.environ["SCODOC_USER"] SCODOC_PASSWORD = os.environ["SCODOC_PASSWORD"] @@ -110,6 +110,8 @@ print("\n".join([s["titre_num"] for s in sems])) sems = GET(f"/etudiant/nip/{code_nip}/formsemestres") print("\n".join([s["titre_num"] for s in sems])) +# Evaluation +evals = GET("/evaluations/1") # # --- Recupere la liste de tous les semestres: # sems = GET(s, "Notes/formsemestre_list?format=json", "Aucun semestre !") diff --git a/tests/api/setup_test_api.py b/tests/api/setup_test_api.py index 4a9d45393..9791a975e 100644 --- a/tests/api/setup_test_api.py +++ b/tests/api/setup_test_api.py @@ -25,6 +25,7 @@ SCODOC_URL = os.environ["SCODOC_URL"] API_URL = SCODOC_URL + "/ScoDoc/api" API_USER = os.environ.get("API_USER", "test") API_PASSWORD = os.environ.get("API_PASSWD", "test") +DEPT_ACRONYM = "TAPI" print(f"SCODOC_URL={SCODOC_URL}") print(f"API URL={API_URL}") diff --git a/tests/api/test_api_departements.py b/tests/api/test_api_departements.py index 436cab2b5..fe83da783 100644 --- a/tests/api/test_api_departements.py +++ b/tests/api/test_api_departements.py @@ -19,7 +19,12 @@ Utilisation : import requests -from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers +from tests.api.setup_test_api import ( + API_URL, + CHECK_CERTIFICATE, + DEPT_ACRONYM, + api_headers, +) from tests.api.tools_test_api import verify_fields DEPARTEMENT_FIELDS = [ @@ -86,7 +91,7 @@ def test_list_etudiants(api_headers): fields = {"id", "nip", "ine", "nom", "nom_usuel", "prenom", "civilite"} r = requests.get( - API_URL + "/departement/TAPI/etudiants", + f"{API_URL}/departement/{DEPT_ACRONYM}/etudiants", headers=api_headers, verify=CHECK_CERTIFICATE, ) diff --git a/tests/api/test_api_etudiants.py b/tests/api/test_api_etudiants.py index 4c88d8619..001d27525 100644 --- a/tests/api/test_api_etudiants.py +++ b/tests/api/test_api_etudiants.py @@ -19,7 +19,12 @@ Utilisation : import requests -from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers +from tests.api.setup_test_api import ( + API_URL, + CHECK_CERTIFICATE, + DEPT_ACRONYM, + api_headers, +) from tests.api.tools_test_api import verify_fields from tests.api.tools_test_api import ETUD_FIELDS, FSEM_FIELDS @@ -83,7 +88,7 @@ def test_etudiant(api_headers): etud = r.json() fields_ok = verify_fields(etud, ETUD_FIELDS) assert fields_ok is True - + assert etud["dept_acronym"] == DEPT_ACRONYM ######### Test code ine ######### r = requests.get( diff --git a/tests/api/tools_test_api.py b/tests/api/tools_test_api.py index 582b03fbc..d1d14402b 100644 --- a/tests/api/tools_test_api.py +++ b/tests/api/tools_test_api.py @@ -21,6 +21,8 @@ ETUD_FIELDS = { "code_nip", "codepostaldomicile", "date_naissance", + "dept_acronym", + "dept_id", "dept_naissance", "description", "domicile", From e140bd73722ec256cf0c68fb6f051baab305823c Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 12 May 2022 17:46:34 +0200 Subject: [PATCH 18/23] API: formsemetre: ajoute annee_scolaire --- app/api/formsemestres.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/api/formsemestres.py b/app/api/formsemestres.py index 577b2dbc4..725454b18 100644 --- a/app/api/formsemestres.py +++ b/app/api/formsemestres.py @@ -59,6 +59,7 @@ def formsemestre(formsemestre_id: int): # pour accéder aux préferences dept = Departement.query.get(formsemestre.dept_id) app.set_sco_dept(dept.acronym) + data["annee_scolaire"] = formsemestre.annee_scolaire_str() data["session_id"] = formsemestre.session_id() return jsonify(data) From b6af7d45c9e2082889a9565f3cc626b0102b1185 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 18 May 2022 20:40:50 +0200 Subject: [PATCH 19/23] Bonus Rennes 1 (Lannion, St Malo) --- app/comp/bonus_spo.py | 45 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index 8d426b682..06a7bde76 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -703,6 +703,51 @@ class BonusGrenobleIUT1(BonusSportMultiplicatif): super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan) +class BonusIUTRennes1(BonusSportAdditif): + """Calcul bonus optionnels (sport, langue vivante, engagement étudiant), + règle IUT de l'Université de Rennes 1 (Lannion, St Malo). + +
    +
  • Les étudiants peuvent suivre un ou plusieurs activités optionnelles notées. + La meilleure des notes obtenue est prise en compte, si elle est supérieure à 10/20. +
  • +
  • Le vingtième des points au dessus de 10 est ajouté à la moyenne des UE. +
  • +
  • Exemple: un étudiant ayant 16/20 bénéficiera d'un bonus de (16-10)/20 = 0,3 points + sur chaque UE. +
  • +
+ """ + + name = "bonus_iut_rennes1" + displayed_name = "IUTs de Rennes 1 (Lannion, St Malo)" + seuil_moy_gen = 10.0 + proportion_point = 1 / 20.0 + classic_use_bonus_ues = True + # Adapté de BonusTarbes, mais s'applique aussi en classic + def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan): + """calcul du bonus""" + # Prend la note de chaque modimpl, sans considération d'UE + if len(sem_modimpl_moys_inscrits.shape) > 2: # apc + sem_modimpl_moys_inscrits = sem_modimpl_moys_inscrits[:, :, 0] + # ici sem_modimpl_moys_inscrits est nb_etuds x nb_mods_bonus, en APC et en classic + note_bonus_max = np.max(sem_modimpl_moys_inscrits, axis=1) # 1d, nb_etuds + nb_ues = self.formsemestre.query_ues(with_sport=False).count() + + bonus_moy_arr = np.where( + note_bonus_max > self.seuil_moy_gen, + (note_bonus_max - self.seuil_moy_gen) * self.proportion_point, + 0.0, + ) + # Seuil: bonus dans [min, max] (défaut [0,20]) + bonus_max = self.bonus_max or 20.0 + np.clip(bonus_moy_arr, self.bonus_min, bonus_max, out=bonus_moy_arr) + if self.formsemestre.formation.is_apc(): + bonus_moy_arr = np.stack([bonus_moy_arr] * nb_ues).T + + self.bonus_additif(bonus_moy_arr) + + class BonusLaRochelle(BonusSportAdditif): """Calcul bonus modules optionnels (sport, culture), règle IUT de La Rochelle. From abffc00570b2709d407e9122e96a629f662dc887 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 18 May 2022 20:41:55 +0200 Subject: [PATCH 20/23] =?UTF-8?q?Config:=20message=20flash=20si=20inchang?= =?UTF-8?q?=C3=A9e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/forms/main/config_main.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/forms/main/config_main.py b/app/forms/main/config_main.py index 2c2aa3d5e..d08719375 100644 --- a/app/forms/main/config_main.py +++ b/app/forms/main/config_main.py @@ -82,7 +82,9 @@ def configuration(): form_bonus.data["bonus_sport_func_name"] ) app.clear_scodoc_cache() - flash(f"Fonction bonus sport&culture configurée.") + flash("""Fonction bonus sport&culture configurée.""") + else: + flash("Fonction bonus inchangée.") return redirect(url_for("scodoc.index")) elif form_scodoc.submit_scodoc.data and form_scodoc.validate(): if ScoDocSiteConfig.enable_entreprises( From 878ea41933d7e6d3eb76252f6f6792184825a935 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 18 May 2022 20:43:01 +0200 Subject: [PATCH 21/23] Ajout groupes et rangs/groupes aux bulletins BUT --- app/but/bulletin_but.py | 49 ++++++++++++++++--- app/comp/res_common.py | 3 +- app/comp/res_compat.py | 80 +++++++++++++++++++++++++------- app/models/etudiants.py | 4 +- app/models/groups.py | 33 +++++++++++++ app/scodoc/sco_bulletins.py | 4 +- app/scodoc/sco_bulletins_json.py | 2 +- app/scodoc/sco_bulletins_xml.py | 2 +- app/scodoc/sco_groups.py | 36 ++++++++++++-- 9 files changed, 179 insertions(+), 34 deletions(-) diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index 689ea9ed9..f9dbb8706 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -14,10 +14,12 @@ from flask import url_for, g from app.comp.res_but import ResultatsSemestreBUT from app.models import FormSemestre, Identite +from app.models.groups import GroupDescr from app.models.ues import UniteEns from app.scodoc import sco_bulletins, sco_utils as scu from app.scodoc import sco_bulletins_json from app.scodoc import sco_bulletins_pdf +from app.scodoc import sco_groups from app.scodoc import sco_preferences from app.scodoc.sco_codes_parcours import UE_SPORT, DEF from app.scodoc.sco_utils import fmt_note @@ -64,8 +66,16 @@ class BulletinBUT: # } return d - def etud_ue_results(self, etud: Identite, ue: UniteEns, decision_ue: dict) -> dict: - "dict synthèse résultats UE" + def etud_ue_results( + self, + etud: Identite, + ue: UniteEns, + decision_ue: dict, + etud_groups: list[GroupDescr] = None, + ) -> dict: + """dict synthèse résultats UE + etud_groups : liste des groupes, pour affichage du rang. + """ res = self.res d = { @@ -81,7 +91,7 @@ class BulletinBUT: if res.bonus_ues is not None and ue.id in res.bonus_ues else fmt_note(0.0), "malus": fmt_note(res.malus[ue.id][etud.id]), - "capitalise": None, # "AAAA-MM-JJ" TODO #sco92 + "capitalise": None, # "AAAA-MM-JJ" TODO #sco93 "ressources": self.etud_ue_mod_results(etud, ue, res.ressources), "saes": self.etud_ue_mod_results(etud, ue, res.saes), } @@ -103,7 +113,18 @@ class BulletinBUT: "moy": fmt_note(res.etud_moy_ue[ue.id].mean()), "rang": rang, "total": effectif, # nb etud avec note dans cette UE + "groupes": {}, } + if self.prefs["bul_show_ue_rangs"]: + for group in etud_groups: + if group.partition.bul_show_rank: + rang, effectif = self.res.get_etud_ue_rang( + ue.id, etud.id, group.id + ) + d["moyenne"]["groupes"][group.id] = { + "value": rang, + "total": effectif, + } else: # ceci suppose que l'on a une seule UE bonus, # en tous cas elles auront la même description @@ -275,6 +296,9 @@ class BulletinBUT: return d nbabs, nbabsjust = formsemestre.get_abs_count(etud.id) + etud_groups = sco_groups.get_etud_formsemestre_groups( + etud, formsemestre, only_to_show=True + ) semestre_infos = { "etapes": [str(x.etape_apo) for x in formsemestre.etapes if x.etape_apo], "date_debut": formsemestre.date_debut.isoformat(), @@ -282,7 +306,7 @@ class BulletinBUT: "annee_universitaire": formsemestre.annee_scolaire_str(), "numero": formsemestre.semestre_id, "inscription": "", # inutilisé mais nécessaire pour le js de Seb. - "groupes": [], # XXX TODO + "groupes": [group.to_dict() for group in etud_groups], } if self.prefs["bul_show_abs"]: semestre_infos["absences"] = { @@ -306,15 +330,25 @@ class BulletinBUT: "max": fmt_note(res.etud_moy_gen.max()), } if self.prefs["bul_show_rangs"] and not np.isnan(res.etud_moy_gen[etud.id]): - # classement wrt moyenne général, indicatif + # classement wrt moyenne générale, indicatif semestre_infos["rang"] = { "value": res.etud_moy_gen_ranks[etud.id], "total": nb_inscrits, + "groupes": {}, } + # Rangs par groupes + for group in etud_groups: + if group.partition.bul_show_rank: + rang, effectif = self.res.get_etud_rang_group(etud.id, group.id) + semestre_infos["rang"]["groupes"][group.id] = { + "value": rang, + "total": effectif, + } else: semestre_infos["rang"] = { "value": "-", "total": nb_inscrits, + "groupes": {}, } d.update( { @@ -324,7 +358,10 @@ class BulletinBUT: "saes": self.etud_mods_results(etud, res.saes, version=version), "ues": { ue.acronyme: self.etud_ue_results( - etud, ue, decision_ue=decisions_ues.get(ue.id, {}) + etud, + ue, + decision_ue=decisions_ues.get(ue.id, {}), + etud_groups=etud_groups, ) for ue in res.ues # si l'UE comporte des modules auxquels on est inscrit: diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 7bc199ead..5170875d7 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -18,7 +18,7 @@ from app.auth.models import User from app.comp.res_cache import ResultatsCache from app.comp import res_sem from app.comp.moy_mod import ModuleImplResults -from app.models import FormSemestre, FormSemestreUECoef, formsemestre +from app.models import FormSemestre, FormSemestreUECoef from app.models import Identite from app.models import ModuleImpl, ModuleImplInscription from app.models.ues import UniteEns @@ -151,6 +151,7 @@ class ResultatsSemestre(ResultatsCache): if m.module.module_type == scu.ModuleType.SAE ] + # --- JURY... def get_etud_ue_validables(self, etudid: int) -> list[UniteEns]: """Liste des UEs du semestre qui doivent être validées diff --git a/app/comp/res_compat.py b/app/comp/res_compat.py index 8bbed0904..5ac18ff4e 100644 --- a/app/comp/res_compat.py +++ b/app/comp/res_compat.py @@ -35,7 +35,9 @@ class NotesTableCompat(ResultatsSemestre): "malus", "etud_moy_gen_ranks", "etud_moy_gen_ranks_int", + "moy_gen_rangs_by_group", "ue_rangs", + "ue_rangs_by_group", ) def __init__(self, formsemestre: FormSemestre): @@ -48,6 +50,8 @@ class NotesTableCompat(ResultatsSemestre): self.moy_min = "NA" self.moy_max = "NA" self.moy_moy = "NA" + self.moy_gen_rangs_by_group = {} # { group_id : (Series, Series) } + self.ue_rangs_by_group = {} # { ue_id : {group_id : (Series, Series)}} self.expr_diagnostics = "" self.parcours = self.formsemestre.formation.get_parcours() @@ -153,31 +157,83 @@ class NotesTableCompat(ResultatsSemestre): def compute_rangs(self): """Calcule les classements Moyenne générale: etud_moy_gen_ranks - Par UE (sauf ue bonus) + Par UE (sauf ue bonus): ue_rangs[ue.id] + Par groupe: classements selon moy_gen et UE: + moy_gen_rangs_by_group[group_id] + ue_rangs_by_group[group_id] """ ( self.etud_moy_gen_ranks, self.etud_moy_gen_ranks_int, ) = moy_sem.comp_ranks_series(self.etud_moy_gen) - for ue in self.formsemestre.query_ues(): + ues = self.formsemestre.query_ues() + for ue in ues: moy_ue = self.etud_moy_ue[ue.id] self.ue_rangs[ue.id] = ( moy_sem.comp_ranks_series(moy_ue)[0], # juste en chaine int(moy_ue.count()), ) # .count() -> nb of non NaN values + # Rangs dans les groupes (moy. gen et par UE) + self.moy_gen_rangs_by_group = {} # { group_id : (Series, Series) } + self.ue_rangs_by_group = {} + partitions_avec_rang = self.formsemestre.partitions.filter_by( + bul_show_rank=True + ) + for partition in partitions_avec_rang: + for group in partition.groups: + # on prend l'intersection car les groupes peuvent inclure des étudiants désinscrits + group_members = list( + {etud.id for etud in group.etuds}.intersection( + self.etud_moy_gen.index + ) + ) + # list() car pandas veut une sequence pour take() + # Rangs / moyenne générale: + group_moys_gen = self.etud_moy_gen[group_members] + self.moy_gen_rangs_by_group[group.id] = moy_sem.comp_ranks_series( + group_moys_gen + ) + # Rangs / UEs: + for ue in ues: + group_moys_ue = self.etud_moy_ue[ue.id][group_members] + self.ue_rangs_by_group.setdefault(ue.id, {})[ + group.id + ] = moy_sem.comp_ranks_series(group_moys_ue) - def get_etud_ue_rang(self, ue_id, etudid) -> tuple[str, int]: + def get_etud_rang(self, etudid: int) -> str: + """Le rang (classement) de l'étudiant dans le semestre. + Result: "13" ou "12 ex" + """ + return self.etud_moy_gen_ranks.get(etudid, 99999) + + def get_etud_ue_rang(self, ue_id, etudid, group_id=None) -> tuple[str, int]: """Le rang de l'étudiant dans cette ue + Si le group_id est spécifié, rang au sein de ce groupe, sinon global. Result: rang:str, effectif:str """ - rangs, effectif = self.ue_rangs[ue_id] - if rangs is not None: - rang = rangs[etudid] + if group_id is None: + rangs, effectif = self.ue_rangs[ue_id] + if rangs is not None: + rang = rangs[etudid] + else: + return "", "" else: - return "", "" + rangs = self.ue_rangs_by_group[ue_id][group_id][0] + rang = rangs[etudid] + effectif = len(rangs) return rang, effectif + def get_etud_rang_group(self, etudid: int, group_id: int) -> tuple[str, int]: + """Rang de l'étudiant (selon moy gen) et effectif dans ce groupe. + Si le groupe n'a pas de rang (partition avec bul_show_rank faux), ramène "", 0 + """ + if group_id in self.moy_gen_rangs_by_group: + r = self.moy_gen_rangs_by_group[group_id][0] # version en str + return (r[etudid], len(r)) + else: + return "", 0 + def etud_check_conditions_ues(self, etudid): """Vrai si les conditions sur les UE sont remplies. Ne considère que les UE ayant des notes (moyenne calculée). @@ -298,16 +354,6 @@ class NotesTableCompat(ResultatsSemestre): "ects_pot_fond": 0.0, # not implemented (anciennemment pour école ingé) } - def get_etud_rang(self, etudid: int) -> str: - """Le rang (classement) de l'étudiant dans le semestre. - Result: "13" ou "12 ex" - """ - return self.etud_moy_gen_ranks.get(etudid, 99999) - - def get_etud_rang_group(self, etudid: int, group_id: int): - "Le rang de l'étudiant dans ce groupe (NON IMPLEMENTE)" - return (None, 0) # XXX unimplemented TODO - def get_evals_in_mod(self, moduleimpl_id: int) -> list[dict]: """Liste d'informations (compat NotesTable) sur évaluations completes de ce module. diff --git a/app/models/etudiants.py b/app/models/etudiants.py index 2e9292c1d..0bce6d47e 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -56,11 +56,11 @@ class Identite(db.Model): # adresses = db.relationship("Adresse", lazy="dynamic", backref="etud") billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic") - # one-to-one relation: + # admission = db.relationship("Admission", backref="identite", lazy="dynamic") def __repr__(self): - return f"" + return f"" @classmethod def from_request(cls, etudid=None, code_nip=None): diff --git a/app/models/groups.py b/app/models/groups.py index 9cf5f2364..4c64ad543 100644 --- a/app/models/groups.py +++ b/app/models/groups.py @@ -25,9 +25,11 @@ class Partition(db.Model): partition_name = db.Column(db.String(SHORT_STR_LEN)) # numero = ordre de presentation) numero = db.Column(db.Integer) + # Calculer le rang ? bul_show_rank = db.Column( db.Boolean(), nullable=False, default=False, server_default="false" ) + # Montrer quand on indique les groupes de l'étudiant ? show_in_lists = db.Column( db.Boolean(), nullable=False, default=True, server_default="true" ) @@ -50,6 +52,18 @@ class Partition(db.Model): def __repr__(self): return f"""<{self.__class__.__name__} {self.id} "{self.partition_name or '(default)'}">""" + def to_dict(self, with_groups=False) -> dict: + """as a dict, with or without groups""" + d = { + "id": self.id, + "formsemestre_id": self.partition_id, + "name": self.partition_name, + "numero": self.numero, + } + if with_groups: + d["groups"] = [group.to_dict(with_partition=False) for group in self.groups] + return d + class GroupDescr(db.Model): """Description d'un groupe d'une partition""" @@ -78,6 +92,17 @@ class GroupDescr(db.Model): "Nom avec partition: 'TD A'" return f"{self.partition.partition_name or ''} {self.group_name or '-'}" + def to_dict(self, with_partition=True) -> dict: + """as a dict, with or without partition""" + d = { + "id": self.id, + "partition_id": self.partition_id, + "name": self.group_name, + } + if with_partition: + d["partition"] = self.partition.to_dict(with_groups=False) + return d + group_membership = db.Table( "group_membership", @@ -85,3 +110,11 @@ group_membership = db.Table( db.Column("group_id", db.Integer, db.ForeignKey("group_descr.id")), db.UniqueConstraint("etudid", "group_id"), ) +# class GroupMembership(db.Model): +# """Association groupe / étudiant""" + +# __tablename__ = "group_membership" +# __table_args__ = (db.UniqueConstraint("etudid", "group_id"),) +# id = db.Column(db.Integer, primary_key=True) +# etudid = db.Column(db.Integer, db.ForeignKey("identite.id")) +# group_id = db.Column(db.Integer, db.ForeignKey("group_descr.id")) diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index efc0b5341..df790649a 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -251,7 +251,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"): rang = "" rang_gr, ninscrits_gr, gr_name = get_etud_rangs_groups( - etudid, formsemestre_id, partitions, partitions_etud_groups, nt + etudid, partitions, partitions_etud_groups, nt ) if nt.get_moduleimpls_attente(): @@ -651,7 +651,7 @@ def _ue_mod_bulletin( def get_etud_rangs_groups( - etudid, formsemestre_id, partitions, partitions_etud_groups, nt + etudid: int, partitions, partitions_etud_groups, nt: NotesTableCompat ): """Ramene rang et nb inscrits dans chaque partition""" rang_gr, ninscrits_gr, gr_name = {}, {}, {} diff --git a/app/scodoc/sco_bulletins_json.py b/app/scodoc/sco_bulletins_json.py index 7a6bbd49e..78425028d 100644 --- a/app/scodoc/sco_bulletins_json.py +++ b/app/scodoc/sco_bulletins_json.py @@ -165,7 +165,7 @@ def formsemestre_bulletinetud_published_dict( else: rang = str(nt.get_etud_rang(etudid)) rang_gr, ninscrits_gr, gr_name = sco_bulletins.get_etud_rangs_groups( - etudid, formsemestre_id, partitions, partitions_etud_groups, nt + etudid, partitions, partitions_etud_groups, nt ) d["note"] = dict( diff --git a/app/scodoc/sco_bulletins_xml.py b/app/scodoc/sco_bulletins_xml.py index d6925d8c6..f173b56ba 100644 --- a/app/scodoc/sco_bulletins_xml.py +++ b/app/scodoc/sco_bulletins_xml.py @@ -172,7 +172,7 @@ def make_xml_formsemestre_bulletinetud( else: rang = str(nt.get_etud_rang(etudid)) rang_gr, ninscrits_gr, gr_name = sco_bulletins.get_etud_rangs_groups( - etudid, formsemestre_id, partitions, partitions_etud_groups, nt + etudid, partitions, partitions_etud_groups, nt ) doc.append( diff --git a/app/scodoc/sco_groups.py b/app/scodoc/sco_groups.py index 80d8e02c6..d316640fc 100644 --- a/app/scodoc/sco_groups.py +++ b/app/scodoc/sco_groups.py @@ -43,13 +43,14 @@ from xml.etree.ElementTree import Element import flask from flask import g, request from flask import url_for, make_response +from sqlalchemy.sql import text from app import db from app.comp import res_sem from app.comp.res_compat import NotesTableCompat -from app.models import FormSemestre, formsemestre +from app.models import FormSemestre, Identite from app.models import GROUPNAME_STR_LEN, SHORT_STR_LEN -from app.models.groups import Partition +from app.models.groups import GroupDescr, Partition import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb from app import log, cache @@ -61,7 +62,6 @@ from app.scodoc import sco_etud from app.scodoc import sco_permissions_check from app.scodoc import sco_xml from app.scodoc.sco_exceptions import ScoException, AccessDenied, ScoValueError -from app.scodoc.sco_permissions import Permission from app.scodoc.TrivialFormulator import TrivialFormulator @@ -413,6 +413,34 @@ def formsemestre_get_etud_groupnames(formsemestre_id, attr="group_name"): return R +def get_etud_formsemestre_groups( + etud: Identite, formsemestre: FormSemestre, only_to_show=True +) -> list[GroupDescr]: + """Liste les groupes auxquels est inscrit""" + # Note: je n'ai pas réussi à cosntruire une requete SQLAlechemy avec + # la Table d'association group_membership + cursor = db.session.execute( + text( + """ + SELECT g.id + FROM group_descr g, group_membership gm, partition p + WHERE gm.etudid = :etudid + AND gm.group_id = g.id + AND g.partition_id = p.id + AND p.formsemestre_id = :formsemestre_id + AND p.partition_name is not NULL + """ + + (" and (p.show_in_lists is True) " if only_to_show else "") + + """ + ORDER BY p.numero + """ + ), + {"etudid": etud.id, "formsemestre_id": formsemestre.id}, + ) + return [GroupDescr.query.get(group_id) for group_id in cursor] + + +# Ancienne fonction: def etud_add_group_infos(etud, formsemestre_id, sep=" ", only_to_show=False): """Add informations on partitions and group memberships to etud (a dict with an etudid) @@ -453,7 +481,7 @@ def etud_add_group_infos(etud, formsemestre_id, sep=" ", only_to_show=False): ) etud["partitionsgroupes"] = sep.join( [ - gr["partition_name"] + ":" + gr["group_name"] + (gr["partition_name"] or "") + ":" + gr["group_name"] for gr in infos if gr["group_name"] is not None ] From 5e46d2fc3581653401d562b16b54c178add14688 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 19 May 2022 04:15:26 +0200 Subject: [PATCH 22/23] Fix: json evaluations sans dates --- app/models/evaluations.py | 19 +++++++++++++------ sco_version.py | 2 +- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/app/models/evaluations.py b/app/models/evaluations.py index ef2a62887..5b0960ba0 100644 --- a/app/models/evaluations.py +++ b/app/models/evaluations.py @@ -46,16 +46,23 @@ class Evaluation(db.Model): def __repr__(self): return f"""""" - def to_dict(self): + def to_dict(self) -> dict: + "Représentation dict, pour json" e = dict(self.__dict__) e.pop("_sa_instance_state", None) # ScoDoc7 output_formators e["evaluation_id"] = self.id - e["jour"] = ndb.DateISOtoDMY(e["jour"]) - e["date_debut"] = datetime.datetime.combine( - self.jour, self.heure_debut - ).isoformat() - e["date_fin"] = datetime.datetime.combine(self.jour, self.heure_fin).isoformat() + e["jour"] = e["jour"].strftime("%d/%m/%Y") if e["jour"] else "" + if self.jour is None: + e["date_debut"] = None + e["date_fin"] = None + else: + e["date_debut"] = datetime.datetime.combine( + self.jour, self.heure_debut or datetime.time(0, 0) + ).isoformat() + e["date_fin"] = datetime.datetime.combine( + self.jour, self.heure_fin or datetime.time(0, 0) + ).isoformat() e["numero"] = ndb.int_null_is_zero(e["numero"]) e["poids"] = self.get_ue_poids_dict() # { ue_id : poids } return evaluation_enrich_dict(e) diff --git a/sco_version.py b/sco_version.py index 120447d32..3222274fa 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.2.21" +SCOVERSION = "9.2.22" SCONAME = "ScoDoc" From 8a1569ac54b5b3434b1acfac5b0393c1055089e3 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 19 May 2022 10:51:43 +0200 Subject: [PATCH 23/23] API: nom des permissions --- app/scodoc/sco_permissions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/scodoc/sco_permissions.py b/app/scodoc/sco_permissions.py index ccf46e8e8..fd04e5105 100644 --- a/app/scodoc/sco_permissions.py +++ b/app/scodoc/sco_permissions.py @@ -50,10 +50,10 @@ _SCO_PERMISSIONS = ( (1 << 27, "RelationsEntreprisesCorrespondants", "Voir les correspondants"), # 27 à 39 ... réservé pour "entreprises" # Api scodoc9 - (1 << 40, "APIView", "Voir"), - (1 << 41, "APIEtudChangeGroups", "Modifier les groupes"), - (1 << 42, "APIEditAllNotes", "Modifier toutes les notes"), - (1 << 43, "APIAbsChange", "Saisir des absences"), + (1 << 40, "APIView", "API: Lecture"), + (1 << 41, "APIEtudChangeGroups", "API: Modifier les groupes"), + (1 << 42, "APIEditAllNotes", "API: Modifier toutes les notes"), + (1 << 43, "APIAbsChange", "API: Saisir des absences"), )