From ee86fba3d3d17048013a33eef1bf8d3191947acf Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet
Date: Tue, 22 Nov 2022 13:13:16 +0100
Subject: [PATCH] min/max evals sur bul. json classic. + Tests unitaires
bulletin.
---
README.md | 2 +-
app/__init__.py | 2 -
app/scodoc/sco_bulletins_json.py | 88 +++++++++++++-------------
app/scodoc/sco_bulletins_standard.py | 19 +++---
app/scodoc/sco_preferences.py | 5 +-
app/scodoc/sco_saisie_notes.py | 69 +++++++++++---------
app/views/notes.py | 4 +-
scodoc.py | 1 +
tests/conftest.py | 2 +-
tests/unit/sco_fake_gen.py | 56 ++++++++++------
tests/unit/setup.py | 6 +-
tests/unit/test_bulletin.py | 95 ++++++++++++++++++++++++++++
tests/unit/test_departements.py | 1 -
tests/unit/test_notes_modules.py | 78 +++++++++++++++--------
tests/unit/test_notes_rattrapage.py | 30 +++++----
tests/unit/test_sco_basic.py | 44 +++++++------
16 files changed, 331 insertions(+), 171 deletions(-)
create mode 100644 tests/unit/test_bulletin.py
diff --git a/README.md b/README.md
index 115ef229..3cfbf39d 100644
--- a/README.md
+++ b/README.md
@@ -101,7 +101,7 @@ Certains tests ont besoin d'un département déjà créé, qui n'est pas créé
scripts de tests:
Lancer au préalable:
- flask delete-dept TEST00 && flask create-dept TEST00
+ flask delete-dept -fy TEST00 && flask create-dept TEST00
Puis dérouler les tests unitaires:
diff --git a/app/__init__.py b/app/__init__.py
index 428ba164..4431ff69 100644
--- a/app/__init__.py
+++ b/app/__init__.py
@@ -435,8 +435,6 @@ def initialize_scodoc_database(erase=False, create_all=False):
SQL tables and functions.
If erase is True, _erase_ all database content.
"""
- from app import models
-
# - ERASE (the truncation sql function has been defined above)
if erase:
truncate_database()
diff --git a/app/scodoc/sco_bulletins_json.py b/app/scodoc/sco_bulletins_json.py
index 5caba24a..fb0be065 100644
--- a/app/scodoc/sco_bulletins_json.py
+++ b/app/scodoc/sco_bulletins_json.py
@@ -43,6 +43,7 @@ import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app.scodoc import sco_abs
from app.scodoc import sco_edit_ue
+from app.scodoc import sco_evaluations
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups
@@ -84,13 +85,14 @@ def formsemestre_bulletinetud_published_dict(
xml_nodate=False,
xml_with_decisions=False, # inclue les decisions même si non publiées
version="long",
-):
+) -> dict:
"""Dictionnaire representant les informations _publiees_ du bulletin de notes
Utilisé pour JSON, devrait l'être aussi pour XML. (todo)
"""
from app.scodoc import sco_bulletins
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
+ prefs = sco_preferences.SemPreferences(formsemestre_id)
etud = Identite.query.get(etudid)
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
@@ -158,11 +160,8 @@ def formsemestre_bulletinetud_published_dict(
ues = nt.get_ues_stat_dict()
modimpls = nt.get_modimpls_dict()
nbetuds = len(nt.etud_moy_gen_ranks)
- mg = scu.fmt_note(nt.get_etud_moy_gen(etudid))
- if (
- nt.get_moduleimpls_attente()
- or sco_preferences.get_preference("bul_show_rangs", formsemestre_id) == 0
- ):
+ moy_gen = scu.fmt_note(nt.get_etud_moy_gen(etudid))
+ if nt.get_moduleimpls_attente() or not prefs["bul_show_rangs"]:
# n'affiche pas le rang sur le bulletin s'il y a des
# notes en attente dans ce semestre
rang = ""
@@ -175,7 +174,7 @@ def formsemestre_bulletinetud_published_dict(
)
d["note"] = dict(
- value=mg,
+ value=moy_gen,
min=scu.fmt_note(nt.moy_min),
max=scu.fmt_note(nt.moy_max),
moy=scu.fmt_note(nt.moy_moy),
@@ -217,9 +216,7 @@ def formsemestre_bulletinetud_published_dict(
value=scu.fmt_note(ue_status["cur_moy_ue"] if ue_status else ""),
min=scu.fmt_note(ue["min"]),
max=scu.fmt_note(ue["max"]),
- moy=scu.fmt_note(
- ue["moy"]
- ), # CM : ajout pour faire apparaitre la moyenne des UE
+ moy=scu.fmt_note(ue["moy"]),
),
rang=rang,
effectif=effectif,
@@ -259,10 +256,7 @@ def formsemestre_bulletinetud_published_dict(
m["note"][k] = scu.fmt_note(m["note"][k])
u["module"].append(m)
- if (
- sco_preferences.get_preference("bul_show_mod_rangs", formsemestre_id)
- and nt.mod_rangs is not None
- ):
+ if prefs["bul_show_mod_rangs"] and nt.mod_rangs is not None:
m["rang"] = dict(
value=nt.mod_rangs[modimpl["moduleimpl_id"]][0][etudid]
)
@@ -274,33 +268,40 @@ def formsemestre_bulletinetud_published_dict(
if version != "short":
for e in evals:
if e["visibulletin"] or version == "long":
- val = e["notes"].get(etudid, {"value": "NP"})[
- "value"
- ] # NA si etud demissionnaire
+ val = e["notes"].get(etudid, {"value": "NP"})["value"]
+ # nb: val est NA si etud démissionnaire
val = scu.fmt_note(val, note_max=e["note_max"])
- m["evaluation"].append(
- dict(
- jour=ndb.DateDMYtoISO(e["jour"], null_is_empty=True),
- heure_debut=ndb.TimetoISO8601(
- e["heure_debut"], null_is_empty=True
- ),
- heure_fin=ndb.TimetoISO8601(
- e["heure_fin"], null_is_empty=True
- ),
- coefficient=e["coefficient"],
- evaluation_type=e["evaluation_type"],
- evaluation_id=e[
- "evaluation_id"
- ], # CM : ajout pour permettre de faire le lien sur les bulletins en ligne avec l'évaluation
- description=quote_xml_attr(e["description"]),
- note=val,
- )
+ eval_dict = dict(
+ jour=ndb.DateDMYtoISO(e["jour"], null_is_empty=True),
+ heure_debut=ndb.TimetoISO8601(
+ e["heure_debut"], null_is_empty=True
+ ),
+ heure_fin=ndb.TimetoISO8601(
+ e["heure_fin"], null_is_empty=True
+ ),
+ coefficient=e["coefficient"],
+ evaluation_type=e["evaluation_type"],
+ # CM : ajout pour permettre de faire le lien sur
+ # les bulletins en ligne avec l'évaluation:
+ evaluation_id=e["evaluation_id"],
+ description=quote_xml_attr(e["description"]),
+ note=val,
)
+ if prefs["bul_show_minmax_eval"] or prefs["bul_show_moypromo"]:
+ etat = sco_evaluations.do_evaluation_etat(
+ e["evaluation_id"]
+ )
+ if prefs["bul_show_minmax_eval"]:
+ eval_dict["min"] = scu.fmt_note(etat["mini"])
+ eval_dict["max"] = scu.fmt_note(etat["maxi"])
+ if prefs["bul_show_moypromo"]:
+ eval_dict["moy"] = scu.fmt_note(etat["moy"])
+
+ m["evaluation"].append(eval_dict)
+
# Evaluations incomplètes ou futures:
complete_eval_ids = set([e["evaluation_id"] for e in evals])
- if sco_preferences.get_preference(
- "bul_show_all_evals", formsemestre_id
- ):
+ if prefs["bul_show_all_evals"]:
all_evals = sco_evaluation_db.do_evaluation_list(
args={"moduleimpl_id": modimpl["moduleimpl_id"]}
)
@@ -344,7 +345,7 @@ def formsemestre_bulletinetud_published_dict(
)
# --- Absences
- if sco_preferences.get_preference("bul_show_abs", formsemestre_id):
+ if prefs["bul_show_abs"]:
nbabs, nbabsjust = sco_abs.get_abs_count(etudid, sem)
d["absences"] = dict(nbabs=nbabs, nbabsjust=nbabsjust)
@@ -403,17 +404,14 @@ def dict_decision_jury(
"""
from app.scodoc import sco_bulletins
+ prefs = sco_preferences.SemPreferences(formsemestre.id)
+
d = {}
- if (
- sco_preferences.get_preference("bul_show_decision", formsemestre.id)
- or with_decisions
- ):
+ if prefs["bul_show_decision"] or with_decisions:
infos, dpv = sco_bulletins.etud_descr_situation_semestre(
etud.id,
formsemestre.id,
- show_uevalid=sco_preferences.get_preference(
- "bul_show_uevalid", formsemestre.id
- ),
+ show_uevalid=prefs["bul_show_uevalid"],
)
d["situation"] = infos["situation"]
if dpv:
diff --git a/app/scodoc/sco_bulletins_standard.py b/app/scodoc/sco_bulletins_standard.py
index 968bbdd9..b326ef7b 100644
--- a/app/scodoc/sco_bulletins_standard.py
+++ b/app/scodoc/sco_bulletins_standard.py
@@ -46,11 +46,12 @@ de la forme %(XXX)s sont remplacées par la valeur de XXX, pour XXX dans:
Balises img: actuellement interdites.
"""
+from flask import url_for, g
+
from reportlab.platypus import KeepTogether, Paragraph, Spacer, Table
from reportlab.lib.units import cm, mm
from reportlab.lib.colors import Color, blue
-
import app.scodoc.sco_utils as scu
from app.scodoc.sco_pdf import SU, make_paras
from app.scodoc import sco_preferences
@@ -434,7 +435,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
plusminus = pluslink
try:
ects_txt = str(int(ue["ects"]))
- except:
+ except (ValueError, KeyError):
ects_txt = "-"
t = {
@@ -602,12 +603,14 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
"rang": mod["mod_rang_txt"], # vide si pas option rang
"note": mod["mod_moy_txt"],
"coef": mod["mod_coef_txt"] if prefs["bul_show_coef"] else "",
- "abs": mod.get(
- "mod_abs_txt", ""
- ), # absent si pas option show abs module
- "_css_row_class": "notes_bulletin_row_mod%s" % rowstyle,
- "_titre_target": "moduleimpl_status?moduleimpl_id=%s"
- % mod["moduleimpl_id"],
+ # vide si pas option show abs module:
+ "abs": mod.get("mod_abs_txt", ""),
+ "_css_row_class": f"notes_bulletin_row_mod{rowstyle}",
+ "_titre_target": url_for(
+ "notes.moduleimpl_status",
+ scodoc_dept=g.scodoc_dept,
+ moduleimpl_id=mod["moduleimpl_id"],
+ ),
"_titre_help": mod["mod_descr_txt"],
"_hidden": hidden,
"_pdf_style": pdf_style,
diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py
index 0223c7b6..92378db7 100644
--- a/app/scodoc/sco_preferences.py
+++ b/app/scodoc/sco_preferences.py
@@ -2034,7 +2034,10 @@ class BasePreferences(object):
if modif:
sco_cache.invalidate_formsemestre()
- def set(self, formsemestre_id, name, value):
+ def set(self, formsemestre_id: int, name: str, value: str):
+ """Set and save a preference value.
+ If formsemestre_id is None, global pref.
+ """
if not name or name[0] == "_" or name not in self.prefs_name:
raise ValueError(f"invalid preference name: {name}")
if formsemestre_id and name in self.prefs_only_global:
diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py
index 5e114677..4e33d5e7 100644
--- a/app/scodoc/sco_saisie_notes.py
+++ b/app/scodoc/sco_saisie_notes.py
@@ -39,7 +39,7 @@ from flask_login import current_user
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.models import Evaluation, FormSemestre
-from app.models import ScolarNews
+from app.models import ModuleImpl, ScolarNews
from app.models.etudiants import Identite
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType
@@ -216,7 +216,8 @@ def do_evaluation_upload_xls():
eval_id = None
if eval_id != evaluation_id:
diag.append(
- f"Erreur: fichier invalide: le code d'évaluation de correspond pas ! ('{eval_id_str}' != '{evaluation_id}')"
+ f"""Erreur: fichier invalide: le code d'évaluation de correspond pas ! ('{
+ eval_id_str}' != '{evaluation_id}')"""
)
raise InvalidNoteValue()
# --- get notes -> list (etudid, value)
@@ -238,15 +239,14 @@ def do_evaluation_upload_xls():
ni += 1
except:
diag.append(
- 'Erreur: Ligne invalide ! (erreur ligne %d)
"%s"'
- % (ni, str(lines[ni]))
+ f"""Erreur: Ligne invalide ! (erreur ligne {ni})
{lines[ni]}"""
)
raise InvalidNoteValue()
# -- check values
L, invalids, withoutnotes, absents, _ = _check_notes(notes, E, M["module"])
if len(invalids):
diag.append(
- "Erreur: la feuille contient %d notes invalides
" % len(invalids)
+ f"Erreur: la feuille contient {len(invalids)} notes invalides"
)
if len(invalids) < 25:
etudsnames = [
@@ -424,25 +424,34 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
evaluation_id, by_uid=current_user.id
)
else:
- raise AccessDenied("Modification des notes impossible pour %s" % current_user)
+ raise AccessDenied(f"Modification des notes impossible pour {current_user}")
notes = [(etudid, scu.NOTES_SUPPRESS) for etudid in notes_db.keys()]
+ status_url = url_for(
+ "notes.moduleimpl_status",
+ scodoc_dept=g.scodoc_dept,
+ moduleimpl_id=E["moduleimpl_id"],
+ )
+
if not dialog_confirmed:
nb_changed, nb_suppress, existing_decisions = notes_add(
current_user, evaluation_id, notes, do_it=False, check_inscription=False
)
- msg = (
- "Confirmer la suppression des %d notes ? (peut affecter plusieurs groupes)
"
- % nb_suppress
- )
+ msg = f"""Confirmer la suppression des {nb_suppress} notes ?
+ (peut affecter plusieurs groupes)
+
+ """
+
if existing_decisions:
- msg += """Important: il y a déjà des décisions de jury enregistrées, qui seront potentiellement à revoir suite à cette modification !
"""
+ msg += """Important: il y a déjà des décisions de
+ jury enregistrées, qui seront potentiellement à revoir suite à
+ cette modification !
"""
return scu.confirm_dialog(
msg,
dest_url="",
OK="Supprimer les notes",
- cancel_url="moduleimpl_status?moduleimpl_id=%s" % E["moduleimpl_id"],
+ cancel_url=status_url,
parameters={"evaluation_id": evaluation_id},
)
@@ -455,26 +464,28 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
check_inscription=False,
)
assert nb_changed == nb_suppress
- H = ["%s notes supprimées
" % nb_suppress]
+ H = [f"""{nb_suppress} notes supprimées
"""]
if existing_decisions:
H.append(
- """Important: il y avait déjà des décisions de jury enregistrées, qui sont potentiellement à revoir suite à cette modification !
"""
+ """Important: il y avait déjà des décisions
+ de jury enregistrées, qui sont potentiellement à revoir suite
+ à cette modification !
+
"""
)
H += [
- 'continuer'
- % E["moduleimpl_id"]
+ f"""
continuer
+ """
]
# news
- M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
- mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
- mod["moduleimpl_id"] = M["moduleimpl_id"]
- mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
+ modimpl = ModuleImpl.query.get(E["moduleimpl_id"])
ScolarNews.add(
typ=ScolarNews.NEWS_NOTE,
- obj=M["moduleimpl_id"],
- text='Suppression des notes d\'une évaluation dans %(titre)s'
- % mod,
- url=mod["url"],
+ obj=modimpl.id,
+ text=f"""Suppression des notes d'une évaluation dans
+ {modimpl.module.titre or 'module sans titre'}
+ """,
+ url=status_url,
)
return html_sco_header.sco_header() + "\n".join(H) + html_sco_header.sco_footer()
@@ -555,7 +566,7 @@ def notes_add(
oldval = notes_db[etudid]["value"]
if type(value) != type(oldval):
changed = True
- elif type(value) == type(1.0) and (
+ elif type(value) == float and (
abs(value - oldval) > scu.NOTES_PRECISION
):
changed = True
@@ -566,10 +577,10 @@ def notes_add(
if do_it:
cursor.execute(
"""INSERT INTO notes_notes_log
- (etudid,evaluation_id,value,comment,date,uid)
+ (etudid,evaluation_id,value,comment,date,uid)
SELECT etudid, evaluation_id, value, comment, date, uid
FROM notes_notes
- WHERE etudid=%(etudid)s
+ WHERE etudid=%(etudid)s
and evaluation_id=%(evaluation_id)s
""",
{"etudid": etudid, "evaluation_id": evaluation_id},
@@ -588,7 +599,7 @@ def notes_add(
cursor.execute(
"""UPDATE notes_notes
SET value=%(value)s, comment=%(comment)s, date=%(date)s, uid=%(uid)s
- WHERE etudid = %(etudid)s
+ WHERE etudid = %(etudid)s
and evaluation_id = %(evaluation_id)s
""",
aa,
@@ -609,7 +620,7 @@ def notes_add(
# garde trace de la suppression dans l'historique:
aa["value"] = scu.NOTES_SUPPRESS
cursor.execute(
- """INSERT INTO notes_notes_log (etudid,evaluation_id,value,comment,date,uid)
+ """INSERT INTO notes_notes_log (etudid,evaluation_id,value,comment,date,uid)
VALUES (%(etudid)s, %(evaluation_id)s, %(value)s, %(comment)s, %(date)s, %(uid)s)
""",
aa,
diff --git a/app/views/notes.py b/app/views/notes.py
index 9b2c5dca..34067a8d 100644
--- a/app/views/notes.py
+++ b/app/views/notes.py
@@ -651,7 +651,9 @@ def index_html():
Référentiels de compétences
diff --git a/scodoc.py b/scodoc.py
index e8839f78..52c49302 100755
--- a/scodoc.py
+++ b/scodoc.py
@@ -349,6 +349,7 @@ def abort_if_false(ctx, param, value):
@app.cli.command()
@click.option(
+ "-y",
"--yes",
is_flag=True,
callback=abort_if_false,
diff --git a/tests/conftest.py b/tests/conftest.py
index f164da16..eb1a494f 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -38,7 +38,7 @@ def test_client():
u.add_role(admin_role, TestConfig.DEPT_TEST)
db.session.add(u)
db.session.commit()
- # Creation département de Test
+ # Création département de Test
d = models.Departement(acronym=TestConfig.DEPT_TEST)
db.session.add(d)
db.session.commit()
diff --git a/tests/unit/sco_fake_gen.py b/tests/unit/sco_fake_gen.py
index 26c24402..7e03f152 100644
--- a/tests/unit/sco_fake_gen.py
+++ b/tests/unit/sco_fake_gen.py
@@ -38,14 +38,28 @@ from app.scodoc import sco_utils as scu
from app import log
from app.scodoc.sco_exceptions import ScoValueError
+from tests.unit.setup import NOTES_T
+
random.seed(12345) # tests reproductibles
NOMS_DIR = Config.SCODOC_DIR + "/tools/fakeportal/nomsprenoms"
-NOMS = [x.strip() for x in open(NOMS_DIR + "/noms.txt").readlines()]
-PRENOMS_H = [x.strip() for x in open(NOMS_DIR + "/prenoms-h.txt").readlines()]
-PRENOMS_F = [x.strip() for x in open(NOMS_DIR + "/prenoms-f.txt").readlines()]
-PRENOMS_X = [x.strip() for x in open(NOMS_DIR + "/prenoms-x.txt").readlines()]
+NOMS = [
+ x.strip()
+ for x in open(NOMS_DIR + "/noms.txt", encoding=scu.SCO_ENCODING).readlines()
+]
+PRENOMS_H = [
+ x.strip()
+ for x in open(NOMS_DIR + "/prenoms-h.txt", encoding=scu.SCO_ENCODING).readlines()
+]
+PRENOMS_F = [
+ x.strip()
+ for x in open(NOMS_DIR + "/prenoms-f.txt", encoding=scu.SCO_ENCODING).readlines()
+]
+PRENOMS_X = [
+ x.strip()
+ for x in open(NOMS_DIR + "/prenoms-x.txt", encoding=scu.SCO_ENCODING).readlines()
+]
def id_generator(size=6, chars=string.ascii_uppercase + string.digits):
@@ -297,18 +311,18 @@ class ScoFake(object):
@logging_meth
def create_note(
self,
- evaluation=None,
- etud=None,
+ evaluation_id: int = None,
+ etudid: int = None,
note=None,
- comment=None,
- user=None, # User instance
+ comment: str = None,
+ user: User = None, # User instance
):
if user is None:
user = self.default_user
return sco_saisie_notes.notes_add(
user,
- evaluation["evaluation_id"],
- [(etud["etudid"], note)],
+ evaluation_id,
+ [(etudid, note)],
comment=comment,
)
@@ -393,24 +407,24 @@ class ScoFake(object):
eval_list.append(e)
return formsemestre_id, eval_list
- def set_etud_notes_sem(
- self, sem, eval_list, etuds, notes=None, random_min=0, random_max=20
+ def set_etud_notes_evals(
+ self, eval_list: list[dict], etuds: list[dict], notes=None
):
"""Met des notes aux étudiants indiqués des evals indiquées.
Args:
- sem: dict
- eval_list: list of dicts
- etuds: list of dicts
notes: liste des notes (float).
- Si non spécifié, tire au hasard dans `[random_min, random_max]`
+ Si non spécifié, utilise la liste NOTES_T
"""
- set_random = notes is None
+ if notes is None:
+ notes = NOTES_T
for e in eval_list:
- if set_random:
- notes = [float(random.randint(random_min, random_max)) for _ in etuds]
- for etud, note in zip(etuds, notes):
- self.create_note(evaluation=e, etud=etud, note=note)
+ for idx, etud in enumerate(etuds):
+ self.create_note(
+ evaluation_id=e["id"],
+ etudid=etud["id"],
+ note=notes[idx % len(notes)],
+ )
def set_code_jury(
self,
diff --git a/tests/unit/setup.py b/tests/unit/setup.py
index f9798f19..de7f2643 100644
--- a/tests/unit/setup.py
+++ b/tests/unit/setup.py
@@ -3,12 +3,16 @@ Quelques fonctions d'initialisation pour tests unitaires
"""
from tests.unit import sco_fake_gen
-from app import db
from app import models
import app.scodoc.sco_utils as scu
from app.scodoc import sco_codes_parcours
+# Valeurs des notes saisies par les tests:
+NOTES_T = [
+ float(x) for x in (20, 0, 10, 13 / 7.0, 12.5, 24.0 / 11) + tuple(range(1, 19))
+]
+
def build_formation_test(
nb_mods=1, parcours=sco_codes_parcours.ParcoursBUT, with_ue_sport=False
diff --git a/tests/unit/test_bulletin.py b/tests/unit/test_bulletin.py
new file mode 100644
index 00000000..68a050d7
--- /dev/null
+++ b/tests/unit/test_bulletin.py
@@ -0,0 +1,95 @@
+"""Tests unitaires : bulletins de notes
+
+Utiliser comme:
+ pytest tests/unit/test_sco_basic.py
+
+Au besoin, créer un base de test neuve:
+ ./tools/create_database.sh SCODOC_TEST
+
+"""
+
+from app.models import FormSemestre, Identite
+
+from config import TestConfig
+
+import app
+from app.scodoc import sco_bulletins_json
+from app.scodoc import sco_preferences
+from tests.unit import sco_fake_gen
+from tests.unit import test_sco_basic
+
+
+DEPT = TestConfig.DEPT_TEST
+
+
+def test_bulletin(test_client):
+ """Vérifications sur les bulletins de notes"""
+ G = sco_fake_gen.ScoFake(verbose=False)
+ app.set_sco_dept(DEPT)
+ formsemestre = test_sco_basic.run_sco_basic()
+ modimpl = formsemestre.modimpls.first()
+ evaluation = modimpl.evaluations.first()
+ # S'assure qu'on a bien une formation classique:
+ assert formsemestre.formation.is_apc() is False
+ etud: Identite = formsemestre.etuds.first()
+ assert etud
+ # Ici on a un modimpl avec 9 inscrits, 2 evals ayant toutes leurs notes
+ # Vérification des min/max évaluation sur le bulletin
+ bul = sco_bulletins_json.formsemestre_bulletinetud_published_dict(
+ formsemestre.id,
+ etud.id,
+ force_publishing=True,
+ xml_with_decisions=True,
+ )
+ assert isinstance(bul, dict)
+ assert bul["type"] == "classic"
+ modules_res = bul["ue"][0]["module"]
+ assert len(modules_res) == 1 # 1 seul module complet
+ module_res = modules_res[0]
+ assert modimpl.module.code == module_res["code"]
+ assert len(module_res["evaluation"]) == 2
+ note_eval_1 = module_res["evaluation"][0]
+ assert "note" in note_eval_1
+ assert "min" not in note_eval_1
+ assert not sco_preferences.get_preference("bul_show_minmax_eval")
+ # Change préférence pour avoir min/max évaluation
+ prefs = sco_preferences.get_base_preferences()
+ prefs.set(None, "bul_show_minmax_eval", True)
+ assert sco_preferences.get_preference("bul_show_minmax_eval")
+ # Redemande le bulletin
+ bul = sco_bulletins_json.formsemestre_bulletinetud_published_dict(
+ formsemestre.id,
+ etud.id,
+ force_publishing=True,
+ xml_with_decisions=True,
+ )
+ note_eval_1 = bul["ue"][0]["module"][0]["evaluation"][0]
+ assert "min" in note_eval_1
+ assert "max" in note_eval_1
+ min_eval_1 = float(note_eval_1["min"])
+ max_eval_1 = float(note_eval_1["max"])
+ # la valeur actuelle est 12.34, on s'assure qu'elle n'est pas extrême:
+ assert min_eval_1 > 0
+ assert max_eval_1 < 20
+
+ # Saisie note pour changer min/max:
+ # Met le max à 20:
+ G.create_note(evaluation_id=evaluation.id, etudid=etud.id, note=20.0)
+ bul = sco_bulletins_json.formsemestre_bulletinetud_published_dict(
+ formsemestre.id,
+ etud.id,
+ force_publishing=True,
+ xml_with_decisions=True,
+ )
+ note_eval_1 = bul["ue"][0]["module"][0]["evaluation"][0]
+ assert note_eval_1["max"] == "20.00"
+ # Met le min à zero:
+ G.create_note(evaluation_id=evaluation.id, etudid=etud.id, note=0.0)
+ bul = sco_bulletins_json.formsemestre_bulletinetud_published_dict(
+ formsemestre.id,
+ etud.id,
+ force_publishing=True,
+ xml_with_decisions=True,
+ )
+ note_eval_1 = bul["ue"][0]["module"][0]["evaluation"][0]
+ assert note_eval_1["min"] == "00.00"
diff --git a/tests/unit/test_departements.py b/tests/unit/test_departements.py
index e8ca47f6..9f99b30e 100644
--- a/tests/unit/test_departements.py
+++ b/tests/unit/test_departements.py
@@ -11,7 +11,6 @@ from flask import g
import app
from app import db
from app.models import Departement, ScoPreference, FormSemestre, formsemestre
-from app.scodoc import notesdb as ndb
from app.scodoc import sco_formsemestre
from app.scodoc import sco_preferences
from tests.unit import test_sco_basic
diff --git a/tests/unit/test_notes_modules.py b/tests/unit/test_notes_modules.py
index 98f6f640..c04a6c77 100644
--- a/tests/unit/test_notes_modules.py
+++ b/tests/unit/test_notes_modules.py
@@ -108,10 +108,14 @@ def test_notes_modules(test_client):
# --- Notes ordinaires
note_1 = 12.0
note_2 = 13.0
- _, _, _ = G.create_note(evaluation=e1, etud=etuds[0], note=note_1)
- _, _, _ = G.create_note(evaluation=e2, etud=etuds[0], note=note_2)
- _, _, _ = G.create_note(evaluation=e1, etud=etuds[1], note=note_1 / 2)
- _, _, _ = G.create_note(evaluation=e2, etud=etuds[1], note=note_2 / 3)
+ _, _, _ = G.create_note(evaluation_id=e1["id"], etudid=etuds[0]["id"], note=note_1)
+ _, _, _ = G.create_note(evaluation_id=e2["id"], etudid=etuds[0]["id"], note=note_2)
+ _, _, _ = G.create_note(
+ evaluation_id=e1["id"], etudid=etuds[1]["id"], note=note_1 / 2
+ )
+ _, _, _ = G.create_note(
+ evaluation_id=e2["id"], etudid=etuds[1]["id"], note=note_2 / 3
+ )
b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"]
)
@@ -132,16 +136,16 @@ def test_notes_modules(test_client):
)
# Absence à une évaluation
- _, _, _ = G.create_note(evaluation=e1, etud=etud, note=None) # abs
- _, _, _ = G.create_note(evaluation=e2, etud=etud, note=note_2)
+ _, _, _ = G.create_note(evaluation_id=e1["id"], etudid=etudid, note=None) # abs
+ _, _, _ = G.create_note(evaluation_id=e2["id"], etudid=etudid, note=note_2)
b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"]
)
note_th = (coef_1 * 0.0 + coef_2 * note_2) / (coef_1 + coef_2)
assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(note_th)
# Absences aux deux évaluations
- _, _, _ = G.create_note(evaluation=e1, etud=etud, note=None) # abs
- _, _, _ = G.create_note(evaluation=e2, etud=etud, note=None) # abs
+ _, _, _ = G.create_note(evaluation_id=e1["id"], etudid=etudid, note=None) # abs
+ _, _, _ = G.create_note(evaluation_id=e2["id"], etudid=etudid, note=None) # abs
b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"]
)
@@ -156,8 +160,10 @@ def test_notes_modules(test_client):
)
# Note excusée EXC <-> scu.NOTES_NEUTRALISE
- _, _, _ = G.create_note(evaluation=e1, etud=etud, note=note_1)
- _, _, _ = G.create_note(evaluation=e2, etud=etud, note=scu.NOTES_NEUTRALISE) # EXC
+ _, _, _ = G.create_note(evaluation_id=e1["id"], etudid=etudid, note=note_1)
+ _, _, _ = G.create_note(
+ evaluation_id=e2["id"], etudid=etudid, note=scu.NOTES_NEUTRALISE
+ ) # EXC
b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"]
)
@@ -171,8 +177,10 @@ def test_notes_modules(test_client):
expected_moy_ue=note_1,
)
# Note en attente ATT <-> scu.NOTES_ATTENTE
- _, _, _ = G.create_note(evaluation=e1, etud=etud, note=note_1)
- _, _, _ = G.create_note(evaluation=e2, etud=etud, note=scu.NOTES_ATTENTE) # ATT
+ _, _, _ = G.create_note(evaluation_id=e1["id"], etudid=etudid, note=note_1)
+ _, _, _ = G.create_note(
+ evaluation_id=e2["id"], etudid=etudid, note=scu.NOTES_ATTENTE
+ ) # ATT
b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"]
)
@@ -186,8 +194,12 @@ def test_notes_modules(test_client):
expected_moy_ue=note_1,
)
# Neutralisation (EXC) des 2 évals
- _, _, _ = G.create_note(evaluation=e1, etud=etud, note=scu.NOTES_NEUTRALISE) # EXC
- _, _, _ = G.create_note(evaluation=e2, etud=etud, note=scu.NOTES_NEUTRALISE) # EXC
+ _, _, _ = G.create_note(
+ evaluation_id=e1["id"], etudid=etudid, note=scu.NOTES_NEUTRALISE
+ ) # EXC
+ _, _, _ = G.create_note(
+ evaluation_id=e2["id"], etudid=etudid, note=scu.NOTES_NEUTRALISE
+ ) # EXC
b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"]
)
@@ -201,8 +213,12 @@ def test_notes_modules(test_client):
expected_moy_ue=np.nan,
)
# Attente (ATT) sur les 2 evals
- _, _, _ = G.create_note(evaluation=e1, etud=etud, note=scu.NOTES_ATTENTE) # ATT
- _, _, _ = G.create_note(evaluation=e2, etud=etud, note=scu.NOTES_ATTENTE) # ATT
+ _, _, _ = G.create_note(
+ evaluation_id=e1["id"], etudid=etudid, note=scu.NOTES_ATTENTE
+ ) # ATT
+ _, _, _ = G.create_note(
+ evaluation_id=e2["id"], etudid=etudid, note=scu.NOTES_ATTENTE
+ ) # ATT
b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"]
)
@@ -261,7 +277,7 @@ def test_notes_modules(test_client):
{"etudid": etudid, "moduleimpl_id": moduleimpl_id},
formsemestre_id=formsemestre_id,
)
- _, _, _ = G.create_note(evaluation=e1, etud=etud, note=12.5)
+ _, _, _ = G.create_note(evaluation_id=e1["id"], etudid=etudid, note=12.5)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
mod_stats = nt.get_mod_stats(moduleimpl_id)
@@ -289,7 +305,7 @@ def test_notes_modules(test_client):
description="evaluation mod 2",
coefficient=1.0,
)
- _, _, _ = G.create_note(evaluation=e_m2, etud=etud, note=19.5)
+ _, _, _ = G.create_note(evaluation_id=e_m2["id"], etudid=etudid, note=19.5)
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
ue_status = nt.get_etud_ue_status(etudid, ue_id)
@@ -297,12 +313,16 @@ def test_notes_modules(test_client):
# Moyenne d'UE si l'un des modules est EXC ("NA")
# 2 modules, notes EXC dans le premier, note valide n dans le second
# la moyenne de l'UE doit être n
- _, _, _ = G.create_note(evaluation=e1, etud=etud, note=scu.NOTES_NEUTRALISE) # EXC
- _, _, _ = G.create_note(evaluation=e2, etud=etud, note=scu.NOTES_NEUTRALISE) # EXC
- _, _, _ = G.create_note(evaluation=e_m2, etud=etud, note=12.5)
- _, _, _ = G.create_note(evaluation=e1, etud=etuds[1], note=11.0)
- _, _, _ = G.create_note(evaluation=e2, etud=etuds[1], note=11.0)
- _, _, _ = G.create_note(evaluation=e_m2, etud=etuds[1], note=11.0)
+ _, _, _ = G.create_note(
+ evaluation_id=e1["id"], etudid=etudid, note=scu.NOTES_NEUTRALISE
+ ) # EXC
+ _, _, _ = G.create_note(
+ evaluation_id=e2["id"], etudid=etudid, note=scu.NOTES_NEUTRALISE
+ ) # EXC
+ _, _, _ = G.create_note(evaluation_id=e_m2["id"], etudid=etudid, note=12.5)
+ _, _, _ = G.create_note(evaluation_id=e1["id"], etudid=etuds[1]["id"], note=11.0)
+ _, _, _ = G.create_note(evaluation_id=e2["id"], etudid=etuds[1]["id"], note=11.0)
+ _, _, _ = G.create_note(evaluation_id=e_m2["id"], etudid=etuds[1]["id"], note=11.0)
b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"]
@@ -366,8 +386,12 @@ def test_notes_modules_att_dem(test_client):
coefficient=coef_1,
)
# Attente (ATT) sur les 2 evals
- _, _, _ = G.create_note(evaluation=e1, etud=etuds[0], note=scu.NOTES_ATTENTE) # ATT
- _, _, _ = G.create_note(evaluation=e1, etud=etuds[1], note=scu.NOTES_ATTENTE) # ATT
+ _, _, _ = G.create_note(
+ evaluation_id=e1["id"], etudid=etuds[0]["id"], note=scu.NOTES_ATTENTE
+ ) # ATT
+ _, _, _ = G.create_note(
+ evaluation_id=e1["id"], etudid=etuds[1]["id"], note=scu.NOTES_ATTENTE
+ ) # ATT
# Démission du premier étudiant
sco_formsemestre_inscriptions.do_formsemestre_demission(
etuds[0]["etudid"],
@@ -406,7 +430,7 @@ def test_notes_modules_att_dem(test_client):
assert note_e1 == scu.NOTES_ATTENTE # XXXX un peu contestable
# Saisie note ABS pour le deuxième etud
- _, _, _ = G.create_note(evaluation=e1, etud=etuds[1], note=None) # ABS
+ _, _, _ = G.create_note(evaluation_id=e1["id"], etudid=etuds[1]["id"], note=None)
nt = check_nt(
etuds[1]["etudid"],
sem["formsemestre_id"],
diff --git a/tests/unit/test_notes_rattrapage.py b/tests/unit/test_notes_rattrapage.py
index 07a6763f..e804b5e6 100644
--- a/tests/unit/test_notes_rattrapage.py
+++ b/tests/unit/test_notes_rattrapage.py
@@ -1,13 +1,11 @@
"""Test calculs rattrapages
"""
-from flask import g
-
import app
-from app.but.bulletin_but import *
+
from app.comp import res_sem
from app.comp.res_but import ResultatsSemestreBUT
-from app.models import ModuleImpl
+from app.models import FormSemestre, ModuleImpl
from app.scodoc import (
sco_bulletins,
sco_evaluation_db,
@@ -75,8 +73,8 @@ def test_notes_rattrapage(test_client):
evaluation_type=scu.EVALUATION_RATTRAPAGE,
)
etud = etuds[0]
- _, _, _ = G.create_note(evaluation=e, etud=etud, note=12.0)
- _, _, _ = G.create_note(evaluation=e_rat, etud=etud, note=11.0)
+ _, _, _ = G.create_note(evaluation_id=e["id"], etudid=etud["id"], note=12.0)
+ _, _, _ = G.create_note(evaluation_id=e_rat["id"], etudid=etud["id"], note=11.0)
# --- Vérifications internes structures ScoDoc
formsemestre = FormSemestre.query.get(formsemestre_id)
@@ -100,21 +98,21 @@ def test_notes_rattrapage(test_client):
# Note moyenne: ici le ratrapage est inférieur à la note:
assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(12.0)
# rattrapage > moyenne:
- _, _, _ = G.create_note(evaluation=e_rat, etud=etud, note=18.0)
+ _, _, _ = G.create_note(evaluation_id=e_rat["id"], etudid=etud["id"], note=18.0)
b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"]
)
assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(18.0)
# rattrapage vs absences
- _, _, _ = G.create_note(evaluation=e, etud=etud, note=None) # abs
- _, _, _ = G.create_note(evaluation=e_rat, etud=etud, note=17.0)
+ _, _, _ = G.create_note(evaluation_id=e["id"], etudid=etud["id"], note=None) # abs
+ _, _, _ = G.create_note(evaluation_id=e_rat["id"], etudid=etud["id"], note=17.0)
b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"]
)
assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(17.0)
# et sans note de rattrapage
- _, _, _ = G.create_note(evaluation=e, etud=etud, note=10.0) # abs
- _, _, _ = G.create_note(evaluation=e_rat, etud=etud, note=None)
+ _, _, _ = G.create_note(evaluation_id=e["id"], etudid=etud["id"], note=10.0)
+ _, _, _ = G.create_note(evaluation_id=e_rat["id"], etudid=etud["id"], note=None)
b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"]
)
@@ -156,21 +154,25 @@ def test_notes_rattrapage(test_client):
assert len(mod_res.get_evaluations_completes(moduleimpl)) == 2
# Saisie note session 2:
- _, _, _ = G.create_note(evaluation=e_session2, etud=etud, note=5.0)
+ _, _, _ = G.create_note(evaluation_id=e_session2["id"], etudid=etud["id"], note=5.0)
b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"]
)
# Note moyenne: utilise session 2 même si inférieure
assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(5.0)
- _, _, _ = G.create_note(evaluation=e_session2, etud=etud, note=20.0)
+ _, _, _ = G.create_note(
+ evaluation_id=e_session2["id"], etudid=etud["id"], note=20.0
+ )
b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"]
)
# Note moyenne: utilise session 2 même si inférieure
assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(20.0)
- _, _, _ = G.create_note(evaluation=e_session2, etud=etud, note=None)
+ _, _, _ = G.create_note(
+ evaluation_id=e_session2["id"], etudid=etud["id"], note=None
+ )
b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"]
)
diff --git a/tests/unit/test_sco_basic.py b/tests/unit/test_sco_basic.py
index 89d023ef..f3d46758 100644
--- a/tests/unit/test_sco_basic.py
+++ b/tests/unit/test_sco_basic.py
@@ -11,12 +11,11 @@ Au besoin, créer un base de test neuve:
./tools/create_database.sh SCODOC_TEST
"""
-import random
-
from app.models import FormSemestreInscription, Identite
from config import TestConfig
from tests.unit import sco_fake_gen
+from tests.unit.setup import NOTES_T
import app
from app import db
@@ -33,7 +32,6 @@ from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formsemestre_validation
from app.scodoc import sco_cursus_dut
from app.scodoc import sco_saisie_notes
-from app.scodoc import sco_utils as scu
DEPT = TestConfig.DEPT_TEST
@@ -47,9 +45,10 @@ def test_sco_basic(test_client):
run_sco_basic()
-def run_sco_basic(verbose=False):
+def run_sco_basic(verbose=False) -> FormSemestre:
"""Scénario de base: création formation, semestre, étudiants, notes,
décisions jury
+ Renvoie le formsemestre créé.
"""
G = sco_fake_gen.ScoFake(verbose=verbose)
@@ -91,11 +90,11 @@ def run_sco_basic(verbose=False):
)
assert q.count() == 1
ins = q.first()
- assert ins.etape == None
+ assert ins.etape is None
assert ins.etat == "I"
- assert ins.parcour == None
+ assert ins.parcour is None
- # --- Creation évaluation
+ # --- Création évaluation
e = G.create_evaluation(
moduleimpl_id=moduleimpl_id,
jour="01/01/2020",
@@ -104,10 +103,13 @@ def run_sco_basic(verbose=False):
)
# --- Saisie toutes les notes de l'évaluation
- for etud in etuds:
+ for idx, etud in enumerate(etuds):
nb_changed, nb_suppress, existing_decisions = G.create_note(
- evaluation=e, etud=etud, note=float(random.randint(0, 20))
+ evaluation_id=e["id"], etudid=etud["id"], note=NOTES_T[idx % len(NOTES_T)]
)
+ assert not existing_decisions
+ assert nb_suppress == 0
+ assert nb_changed == 1
# --- Vérifie que les notes sont prises en compte:
b = sco_bulletins.formsemestre_bulletinetud_dict(formsemestre_id, etud["etudid"])
@@ -132,13 +134,13 @@ def run_sco_basic(verbose=False):
coefficient=1.0,
)
# Saisie les notes des 5 premiers étudiants:
- for etud in etuds[:5]:
+ for idx, etud in enumerate(etuds[:5]):
nb_changed, nb_suppress, existing_decisions = G.create_note(
- evaluation=e2, etud=etud, note=float(random.randint(0, 20))
+ evaluation_id=e2["id"], etudid=etud["id"], note=NOTES_T[idx % len(NOTES_T)]
)
# Cette éval n'est pas complète
etat = sco_evaluations.do_evaluation_etat(e2["evaluation_id"])
- assert etat["evalcomplete"] == False
+ assert etat["evalcomplete"] is False
# la première éval est toujours complète:
etat = sco_evaluations.do_evaluation_etat(e["evaluation_id"])
assert etat["evalcomplete"]
@@ -147,14 +149,14 @@ def run_sco_basic(verbose=False):
e2["publish_incomplete"] = True
sco_evaluation_db.do_evaluation_edit(e2)
etat = sco_evaluations.do_evaluation_etat(e2["evaluation_id"])
- assert etat["evalcomplete"] == False
+ assert etat["evalcomplete"] is False
assert etat["nb_att"] == 0 # il n'y a pas de notes (explicitement) en attente
assert etat["evalattente"] # mais l'eval est en attente (prise en compte immédiate)
# Saisie des notes qui manquent:
- for etud in etuds[5:]:
+ for idx, etud in enumerate(etuds[5:]):
nb_changed, nb_suppress, existing_decisions = G.create_note(
- evaluation=e2, etud=etud, note=float(random.randint(0, 20))
+ evaluation_id=e2["id"], etudid=etud["id"], note=NOTES_T[idx % len(NOTES_T)]
)
etat = sco_evaluations.do_evaluation_etat(e2["evaluation_id"])
assert etat["evalcomplete"]
@@ -173,7 +175,10 @@ def run_sco_basic(verbose=False):
assert f'{etat["nb_inscrits"]} notes changées' in ans
etat = sco_evaluations.do_evaluation_etat(e["evaluation_id"])
assert etat["evalcomplete"]
- # --- Saisie absences
+
+ # -----------------------
+ # --- Saisie absences ---
+ # -----------------------
etudid = etuds[0]["etudid"]
_ = sco_abs_views.doSignaleAbsence(
@@ -188,8 +193,8 @@ def run_sco_basic(verbose=False):
)
nbabs, nbabsjust = sco_abs.get_abs_count(etudid, sem)
- assert nbabs == 6, "incorrect nbabs (%d)" % nbabs
- assert nbabsjust == 2, "incorrect nbabsjust (%s)" % nbabsjust
+ assert nbabs == 6, f"incorrect nbabs ({nbabs})"
+ assert nbabsjust == 2, f"incorrect nbabsjust ({nbabsjust})"
# --- Permission saisie notes et décisions de jury, avec ou sans démission ou défaillance
# on n'a pas encore saisi de décisions
@@ -227,7 +232,7 @@ def run_sco_basic(verbose=False):
for ue_id in dec_ues:
assert dec_ues[ue_id]["code"] in {"ADM", "CMP"}
- # ---- Suppression étudiant, vérification inscription
+ # ---- Suppression d'un étudiant, vérification inscription
# (permet de tester les cascades)
etud = Identite.query.get(etuds[0]["id"])
assert etud is not None
@@ -239,3 +244,4 @@ def run_sco_basic(verbose=False):
etudid=etudid, formsemestre_id=formsemestre_id
)
assert q.count() == 0
+ return formsemestre