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"),
)