Compare commits

...

7 Commits

39 changed files with 953 additions and 1078 deletions

View File

@ -30,7 +30,7 @@ from app.models import (
Module, Module,
UniteEns, UniteEns,
) )
from app.scodoc import sco_formations from app.formations import formation_io
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
@ -141,7 +141,7 @@ def formation_export_by_formation_id(formation_id: int, export_ids=False):
formation = query.first_or_404(formation_id) formation = query.first_or_404(formation_id)
app.set_sco_dept(formation.departement.acronym) app.set_sco_dept(formation.departement.acronym)
try: try:
data = sco_formations.formation_export(formation_id, export_ids) data = formation_io.formation_export(formation_id, export_ids)
except ValueError: except ValueError:
return json_error(500, message="Erreur inconnue") return json_error(500, message="Erreur inconnue")

View File

@ -0,0 +1 @@
# Fonctions et vues sur les formations ScoDoc

View File

@ -33,6 +33,7 @@ from flask import flash, g, url_for, render_template, request
import sqlalchemy import sqlalchemy
from app import db from app import db
from app.formations import edit_ue
from app.models import SHORT_STR_LEN from app.models import SHORT_STR_LEN
from app.models.formations import Formation from app.models.formations import Formation
from app.models.modules import Module from app.models.modules import Module
@ -45,7 +46,6 @@ from app.scodoc.sco_exceptions import ScoValueError, ScoNonEmptyFormationObject
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc import codes_cursus from app.scodoc import codes_cursus
from app.scodoc import sco_edit_ue
def formation_delete(formation_id=None, dialog_confirmed=False): def formation_delete(formation_id=None, dialog_confirmed=False):
@ -123,7 +123,7 @@ def do_formation_delete(formation_id):
db.session.flush() db.session.flush()
# Suppression des UEs # Suppression des UEs
for ue in formation.ues: for ue in formation.ues:
sco_edit_ue.do_ue_delete(ue, force=True) edit_ue.do_ue_delete(ue, force=True)
db.session.delete(formation) db.session.delete(formation)

View File

@ -25,16 +25,14 @@
# #
############################################################################## ##############################################################################
"""Ajout/Modification/Supression matieres """Ajout/Modification/Suppression matieres
(portage from DTML)
""" """
import flask import flask
from flask import g, render_template, request, url_for from flask import flash, g, render_template, request, url_for
from app import db, log from app import db, log
from app.models import Formation, Matiere, UniteEns, ScolarNews from app.models import Matiere, UniteEns
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
@ -44,59 +42,9 @@ from app.scodoc.sco_exceptions import (
ScoNonEmptyFormationObject, ScoNonEmptyFormationObject,
) )
_matiereEditor = ndb.EditableTable(
"notes_matieres",
"matiere_id",
("matiere_id", "ue_id", "numero", "titre"),
sortkey="numero",
output_formators={"numero": ndb.int_null_is_zero},
)
def matiere_list(*args, **kw):
"list matieres"
cnx = ndb.GetDBConnexion()
return _matiereEditor.list(cnx, *args, **kw)
def do_matiere_edit(*args, **kw):
"edit a matiere"
from app.scodoc import sco_edit_ue
cnx = ndb.GetDBConnexion()
# check
mat = matiere_list({"matiere_id": args[0]["matiere_id"]})[0]
if matiere_is_locked(mat["matiere_id"]):
raise ScoLockedFormError()
# edit
_matiereEditor.edit(cnx, *args, **kw)
formation_id = sco_edit_ue.ue_list({"ue_id": mat["ue_id"]})[0]["formation_id"]
db.session.get(Formation, formation_id).invalidate_cached_sems()
def do_matiere_create(args):
"create a matiere"
from app.scodoc import sco_edit_ue
cnx = ndb.GetDBConnexion()
# check
ue = sco_edit_ue.ue_list({"ue_id": args["ue_id"]})[0]
# create matiere
r = _matiereEditor.create(cnx, args)
# news
formation = db.session.get(Formation, ue["formation_id"])
ScolarNews.add(
typ=ScolarNews.NEWS_FORM,
obj=ue["formation_id"],
text=f"Modification de la formation {formation.acronyme}",
)
formation.invalidate_cached_sems()
return r
def matiere_create(ue_id=None): def matiere_create(ue_id=None):
"""Creation d'une matiere""" """Formulaire création d'une matiere"""
ue: UniteEns = UniteEns.query.get_or_404(ue_id) ue: UniteEns = UniteEns.query.get_or_404(ue_id)
default_numero = max([mat.numero for mat in ue.matieres] or [9]) + 1 default_numero = max([mat.numero for mat in ue.matieres] or [9]) + 1
H = [ H = [
@ -153,8 +101,8 @@ associé.
if tf[0] == -1: if tf[0] == -1:
return flask.redirect(dest_url) return flask.redirect(dest_url)
# check unicity # check unicity
mats = matiere_list(args={"ue_id": ue_id, "titre": tf[2]["titre"]}) nb_mats = Matiere.query.filter_by(ue_id=ue_id, titre=tf[2]["titre"]).count()
if mats: if nb_mats:
return render_template( return render_template(
"sco_page.j2", "sco_page.j2",
title="Création d'une matière", title="Création d'une matière",
@ -164,57 +112,14 @@ associé.
+ tf[1] + tf[1]
), ),
) )
_ = do_matiere_create(tf[2]) Matiere.create_from_dict(tf[2])
return flask.redirect(dest_url) return flask.redirect(dest_url)
def can_delete_matiere(matiere: Matiere) -> tuple[bool, str]:
"True si la matiere n'est pas utilisée dans des formsemestre"
locked = matiere_is_locked(matiere.id)
if locked:
return False
if any(m.modimpls.all() for m in matiere.modules):
return False
return True
def do_matiere_delete(oid):
"delete matiere and attached modules"
from app.scodoc import sco_edit_ue
from app.scodoc import sco_edit_module
cnx = ndb.GetDBConnexion()
# check
matiere = Matiere.query.get_or_404(oid)
mat = matiere_list({"matiere_id": oid})[0] # compat sco7
ue = sco_edit_ue.ue_list({"ue_id": mat["ue_id"]})[0]
if not can_delete_matiere(matiere):
# il y a au moins un modimpl dans un module de cette matière
raise ScoNonEmptyFormationObject("Matière", matiere.titre)
log("do_matiere_delete: matiere_id=%s" % matiere.id)
# delete all modules in this matiere
mods = sco_edit_module.module_list({"matiere_id": matiere.id})
for mod in mods:
sco_edit_module.do_module_delete(mod["module_id"])
_matiereEditor.delete(cnx, oid)
# news
formation = db.session.get(Formation, ue["formation_id"])
ScolarNews.add(
typ=ScolarNews.NEWS_FORM,
obj=ue["formation_id"],
text=f"Modification de la formation {formation.acronyme}",
)
formation.invalidate_cached_sems()
def matiere_delete(matiere_id=None): def matiere_delete(matiere_id=None):
"""Delete matière""" """Form delete matière"""
from app.scodoc import sco_edit_ue matiere = Matiere.get_instance(matiere_id)
if not matiere.can_be_deleted():
matiere = Matiere.query.get_or_404(matiere_id)
if not can_delete_matiere(matiere):
# il y a au moins un modimpl dans un module de cette matière # il y a au moins un modimpl dans un module de cette matière
raise ScoNonEmptyFormationObject( raise ScoNonEmptyFormationObject(
"Matière", "Matière",
@ -227,22 +132,20 @@ def matiere_delete(matiere_id=None):
), ),
) )
mat = matiere_list(args={"matiere_id": matiere_id})[0]
UE = sco_edit_ue.ue_list(args={"ue_id": mat["ue_id"]})[0]
H = [ H = [
"<h2>Suppression de la matière %(titre)s" % mat, f"""<h2>Suppression de la matière {matiere.titre}
" dans l'UE (%(acronyme)s))</h2>" % UE, dans l'UE {matiere.ue.acronyme}</h2>""",
] ]
dest_url = url_for( dest_url = url_for(
"notes.ue_table", "notes.ue_table",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formation_id=str(UE["formation_id"]), formation_id=matiere.ue.formation_id,
) )
tf = TrivialFormulator( tf = TrivialFormulator(
request.base_url, request.base_url,
scu.get_request_args(), scu.get_request_args(),
(("matiere_id", {"input_type": "hidden"}),), (("matiere_id", {"input_type": "hidden"}),),
initvalues=mat, initvalues=matiere.to_dict(),
submitlabel="Confirmer la suppression", submitlabel="Confirmer la suppression",
cancelbutton="Annuler", cancelbutton="Annuler",
) )
@ -255,29 +158,23 @@ def matiere_delete(matiere_id=None):
if tf[0] == -1: if tf[0] == -1:
return flask.redirect(dest_url) return flask.redirect(dest_url)
do_matiere_delete(matiere_id) matiere.delete()
return flask.redirect(dest_url) return flask.redirect(dest_url)
def matiere_edit(matiere_id=None): def matiere_edit(matiere_id=None):
"""Edit matiere""" """Form edit matiere"""
from app.scodoc import sco_edit_ue matiere: Matiere = Matiere.get_instance(matiere_id)
if matiere.is_locked():
F = matiere_list(args={"matiere_id": matiere_id}) raise ScoLockedFormError()
if not F: ue = matiere.ue
raise ScoValueError("Matière inexistante !") formation = ue.formation
F = F[0] ues = matiere.ue.formation.ues
ues = sco_edit_ue.ue_list(args={"ue_id": F["ue_id"]}) ue_names = [f"{u.acronyme} ({u.titre or ''})" for u in ues]
if not ues: ue_ids = [u.id for u in ues]
raise ScoValueError("UE inexistante !")
ue = ues[0]
formation: Formation = Formation.query.get_or_404(ue["formation_id"])
ues = sco_edit_ue.ue_list(args={"formation_id": ue["formation_id"]})
ue_names = ["%(acronyme)s (%(titre)s)" % u for u in ues]
ue_ids = [u["ue_id"] for u in ues]
H = [ H = [
"""<h2>Modification de la matière %(titre)s""" % F, f"""<h2>Modification de la matière {matiere.titre or 'sans titre'}
f"""(formation ({formation.acronyme}, version {formation.version})</h2>""", (formation ({formation.acronyme}, version {formation.version})</h2>""",
] ]
help_msg = """<p class="help">Les matières sont des groupes de modules dans une UE help_msg = """<p class="help">Les matières sont des groupes de modules dans une UE
d'une formation donnée. Les matières servent surtout pour la d'une formation donnée. Les matières servent surtout pour la
@ -317,14 +214,14 @@ associé.
}, },
), ),
), ),
initvalues=F, initvalues=matiere.to_dict(),
submitlabel="Modifier les valeurs", submitlabel="Modifier les valeurs",
) )
dest_url = url_for( dest_url = url_for(
"notes.ue_table", "notes.ue_table",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formation_id=str(ue["formation_id"]), formation_id=formation.id,
) )
if tf[0] == 0: if tf[0] == 0:
return render_template( return render_template(
@ -336,8 +233,8 @@ associé.
return flask.redirect(dest_url) return flask.redirect(dest_url)
else: else:
# check unicity # check unicity
mats = matiere_list(args={"ue_id": tf[2]["ue_id"], "titre": tf[2]["titre"]}) mats = Matiere.query.filter_by(ue_id=tf[2]["ue_id"], titre=tf[2]["titre"]).all()
if len(mats) > 1 or (len(mats) == 1 and mats[0]["matiere_id"] != matiere_id): if len(mats) > 1 or (len(mats) == 1 and mats[0].id != matiere_id):
return render_template( return render_template(
"sco_page.j2", "sco_page.j2",
title="Modification d'une matière", title="Modification d'une matière",
@ -348,32 +245,18 @@ associé.
), ),
) )
modif = False
# changement d'UE ? # changement d'UE ?
if tf[2]["ue_id"] != F["ue_id"]: if tf[2]["ue_id"] != ue.id:
log("attaching mat %s to new UE %s" % (matiere_id, tf[2]["ue_id"])) log(f"attaching mat {matiere_id} to new UE id={tf[2]['ue_id']}")
ndb.SimpleQuery( new_ue = UniteEns.get_ue(tf[2]["ue_id"])
"UPDATE notes_modules SET ue_id = %(ue_id)s WHERE matiere_id=%(matiere_id)s", if new_ue.formation_id != formation.id:
{"ue_id": tf[2]["ue_id"], "matiere_id": matiere_id}, raise ScoValueError("UE does not belong to the same formation")
) matiere.ue = new_ue
modif = True
do_matiere_edit(tf[2]) modif |= matiere.from_dict(tf[2])
if modif:
db.session.commit()
matiere.ue.formation.invalidate_cached_sems()
flash("Matière modifiée", "info")
return flask.redirect(dest_url) return flask.redirect(dest_url)
def matiere_is_locked(matiere_id):
"""True if matiere should not be modified
(contains modules used in a locked formsemestre)
"""
r = ndb.SimpleDictFetch(
"""SELECT ma.id
FROM notes_matieres ma, notes_modules mod, notes_formsemestre sem, notes_moduleimpl mi
WHERE ma.id = mod.matiere_id
AND mi.module_id = mod.id
AND mi.formsemestre_id = sem.id
AND ma.id = %(matiere_id)s
AND sem.etat = false
""",
{"matiere_id": matiere_id},
)
return len(r) > 0

View File

@ -33,168 +33,28 @@ from flask import flash, url_for, render_template
from flask import g, request from flask import g, request
from flask_login import current_user from flask_login import current_user
from app import db, log from app import db
from app import models from app import models
from app.models import APO_CODE_STR_LEN from app.models import APO_CODE_STR_LEN
from app.models import Formation, Matiere, Module, UniteEns from app.models import Formation, Matiere, Module, UniteEns
from app.models import FormSemestre, ModuleImpl from app.models import FormSemestre, ModuleImpl
from app.models import ScolarNews
from app.models.but_refcomp import ApcAppCritique, ApcParcours from app.models.but_refcomp import ApcAppCritique, ApcParcours
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_exceptions import ( from app.scodoc.sco_exceptions import (
ScoValueError, ScoValueError,
ScoLockedFormError,
ScoGenError,
ScoNonEmptyFormationObject, ScoNonEmptyFormationObject,
) )
from app.scodoc import codes_cursus from app.scodoc import codes_cursus
from app.scodoc import sco_edit_matiere
from app.scodoc import sco_moduleimpl
_moduleEditor = ndb.EditableTable(
"notes_modules",
"module_id",
(
"module_id",
"titre",
"code",
"abbrev",
"heures_cours",
"heures_td",
"heures_tp",
"coefficient",
"ue_id",
"matiere_id",
"formation_id",
"semestre_id",
"numero",
"code_apogee",
"module_type",
"edt_id",
#'ects'
),
sortkey="numero, code, titre",
output_formators={
"heures_cours": ndb.float_null_is_zero,
"heures_td": ndb.float_null_is_zero,
"heures_tp": ndb.float_null_is_zero,
"numero": ndb.int_null_is_zero,
"coefficient": ndb.float_null_is_zero,
"module_type": ndb.int_null_is_zero,
#'ects' : ndb.float_null_is_null
},
)
def module_list(*args, **kw):
"list modules"
cnx = ndb.GetDBConnexion()
return _moduleEditor.list(cnx, *args, **kw)
def do_module_create(args) -> int:
"Create a module. Returns id of new object."
formation = db.session.get(Formation, args["formation_id"])
# refuse de créer un module APC avec semestres incohérents:
if formation.is_apc():
ue = db.session.get(UniteEns, args["ue_id"])
if int(args.get("semestre_id", 0)) != ue.semestre_idx:
raise ScoValueError("Formation incompatible: contacter le support ScoDoc")
# create
module = Module.create_from_dict(args)
db.session.commit()
log(f"do_module_create: created {module.id} with {args}")
# news
ScolarNews.add(
typ=ScolarNews.NEWS_FORM,
obj=formation.id,
text=f"Modification de la formation {formation.acronyme}",
)
formation.invalidate_cached_sems()
return module.id
def module_create(
matiere_id=None, module_type=None, semestre_id=None, formation_id=None
):
"""Formulaire de création d'un module
Si matiere_id est spécifié, le module sera créé dans cette matière (cas normal).
Sinon, donne le choix de l'UE de rattachement et utilise la première
matière de cette UE (si elle n'existe pas, la crée).
"""
return module_edit(
create=True,
matiere_id=matiere_id,
module_type=module_type,
semestre_id=semestre_id,
formation_id=formation_id,
)
def can_delete_module(module):
"True si le module n'est pas utilisée dans des formsemestre"
return len(module.modimpls.all()) == 0
def do_module_delete(oid):
"delete module"
module = Module.query.get_or_404(oid)
mod = module_list({"module_id": oid})[0] # sco7
if module_is_locked(module.id):
raise ScoLockedFormError()
if not can_delete_module(module):
raise ScoNonEmptyFormationObject(
"Module",
msg=module.titre,
dest_url=url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=module.formation_id,
semestre_idx=module.ue.semestre_idx,
),
)
# S'il y a des moduleimpls, on ne peut pas detruire le module !
mods = sco_moduleimpl.moduleimpl_list(module_id=oid)
if mods:
err_page = f"""
<h3>Destruction du module impossible car il est utilisé dans des
semestres existants !</h3>
<p class="help">Il faut d'abord supprimer le semestre (ou en retirer
ce module).
Mais il est peut être préférable de laisser ce programme intact et
d'en créer une nouvelle version pour la modifier sans affecter
les semestres déjà en place.
</p>
<a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
formation_id=mod["formation_id"])}">reprendre</a>
"""
raise ScoGenError(err_page)
# delete
cnx = ndb.GetDBConnexion()
_moduleEditor.delete(cnx, oid)
# news
formation = module.formation
ScolarNews.add(
typ=ScolarNews.NEWS_FORM,
obj=mod["formation_id"],
text=f"Modification de la formation {formation.acronyme}",
)
formation.invalidate_cached_sems()
def module_delete(module_id=None): def module_delete(module_id=None):
"""Delete a module""" """Formulaire suppression d'un module"""
module = Module.query.get_or_404(module_id) module = Module.query.get_or_404(module_id)
mod = module_list(args={"module_id": module_id})[0] # sco7
if not can_delete_module(module): if not module.can_be_deleted():
raise ScoNonEmptyFormationObject( raise ScoNonEmptyFormationObject(
"Module", "Module",
msg=module.titre, msg=module.titre,
@ -221,7 +81,7 @@ def module_delete(module_id=None):
request.base_url, request.base_url,
scu.get_request_args(), scu.get_request_args(),
(("module_id", {"input_type": "hidden"}),), (("module_id", {"input_type": "hidden"}),),
initvalues=mod, initvalues=module.to_dict(),
submitlabel="Confirmer la suppression", submitlabel="Confirmer la suppression",
cancelbutton="Annuler", cancelbutton="Annuler",
) )
@ -231,37 +91,38 @@ def module_delete(module_id=None):
title="Suppression d'un module", title="Suppression d'un module",
content="\n".join(H) + tf[1], content="\n".join(H) + tf[1],
) )
elif tf[0] == -1: if tf[0] == -1: # cancel
return flask.redirect(dest_url) return flask.redirect(dest_url)
else:
do_module_delete(module_id) module.delete()
return flask.redirect(dest_url) return flask.redirect(dest_url)
def do_module_edit(vals: dict) -> None: def do_module_edit(vals: dict) -> None:
"edit a module" "edit a module"
# check # check
mod = module_list({"module_id": vals["module_id"]})[0] module = Module.get_instance(vals["module_id"])
if module_is_locked(mod["module_id"]):
# formation verrouillée: empeche de modifier certains champs:
vals = vals.copy()
protected_fields = ("coefficient", "ue_id", "matiere_id", "semestre_id")
for f in protected_fields:
if f in vals:
del vals[f]
# edit # edit
cnx = ndb.GetDBConnexion() modif = module.from_dict(vals)
_moduleEditor.edit(cnx, vals) if modif:
db.session.get(Formation, mod["formation_id"]).invalidate_cached_sems() module.formation.invalidate_cached_sems()
def check_module_code_unicity(code, field, formation_id, module_id=None): def module_create(
"true si code module unique dans la formation" matiere_id=None, module_type=None, semestre_id=None, formation_id=None
modules = module_list(args={"code": code, "formation_id": formation_id}) ):
if module_id: # edition: supprime le module en cours """Formulaire de création d'un module
modules = [m for m in modules if m["module_id"] != module_id] Si matiere_id est spécifié, le module sera créé dans cette matière (cas normal).
Sinon, donne le choix de l'UE de rattachement et utilise la première
return len(modules) == 0 matière de cette UE (si elle n'existe pas, la crée).
"""
return module_edit(
create=True,
matiere_id=matiere_id,
module_type=module_type,
semestre_id=semestre_id,
formation_id=formation_id,
)
def module_edit( def module_edit(
@ -278,14 +139,12 @@ def module_edit(
Sinon, donne le choix de l'UE de rattachement et utilise la première matière Sinon, donne le choix de l'UE de rattachement et utilise la première matière
de cette UE (si elle n'existe pas, la crée). de cette UE (si elle n'existe pas, la crée).
""" """
from app.scodoc import sco_tag_module
# --- Détermination de la formation # --- Détermination de la formation
orig_semestre_idx = semestre_id orig_semestre_idx = semestre_id
ue = None ue = None
if create: if create:
if matiere_id: if matiere_id:
matiere = Matiere.query.get_or_404(matiere_id) matiere = Matiere.get_instance(matiere_id)
ue = matiere.ue ue = matiere.ue
formation = ue.formation formation = ue.formation
orig_semestre_idx = ue.semestre_idx if semestre_id is None else semestre_id orig_semestre_idx = ue.semestre_idx if semestre_id is None else semestre_id
@ -300,7 +159,7 @@ def module_edit(
ue = module.ue ue = module.ue
module_dict = module.to_dict() module_dict = module.to_dict()
formation = module.formation formation = module.formation
unlocked = not module_is_locked(module_id) unlocked = not module.is_locked()
parcours = codes_cursus.get_cursus_from_code(formation.type_parcours) parcours = codes_cursus.get_cursus_from_code(formation.type_parcours)
is_apc = parcours.APC_SAE # BUT is_apc = parcours.APC_SAE # BUT
@ -326,17 +185,14 @@ def module_edit(
if (module and module.matiere and (module.matiere.id == mat.id)) if (module and module.matiere and (module.matiere.id == mat.id))
or (mat.id == mat.ue.matieres.first().id) or (mat.id == mat.ue.matieres.first().id)
] ]
mat_names = [ mat_names = [f"S{mat.ue.semestre_idx} / {mat.ue.acronyme}" for mat in matieres]
"S%s / %s" % (mat.ue.semestre_idx, mat.ue.acronyme) for mat in matieres
]
else: else:
mat_names = ["%s / %s" % (mat.ue.acronyme, mat.titre or "") for mat in matieres] mat_names = ["{mat.ue.acronyme} / {mat.titre or ''}" for mat in matieres]
if module: # edition if module: # edition
ue_mat_ids = ["%s!%s" % (mat.ue.id, mat.id) for mat in matieres] ue_mat_ids = [f"{mat.ue.id}!{mat.id}" for mat in matieres]
module_dict["ue_matiere_id"] = "%s!%s" % ( module_dict["ue_matiere_id"] = (
module_dict["ue_id"], f"{module_dict['ue_id']}!{module_dict['matiere_id']}"
module_dict["matiere_id"],
) )
semestres_indices = list(range(1, parcours.NB_SEM + 1)) semestres_indices = list(range(1, parcours.NB_SEM + 1))
@ -433,8 +289,8 @@ def module_edit(
"explanation": """code du module (issu du programme, exemple M1203, "explanation": """code du module (issu du programme, exemple M1203,
R2.01, ou SAÉ 3.4. Doit être unique dans la formation)""", R2.01, ou SAÉ 3.4. Doit être unique dans la formation)""",
"allow_null": False, "allow_null": False,
"validator": lambda val, field, formation_id=formation.id: check_module_code_unicity( "validator": lambda val, _, formation_id=formation.id: Module.check_module_code_unicity(
val, field, formation_id, module_id=module.id if module else None val, formation_id, module_id=module.id if module else None
), ),
}, },
), ),
@ -602,7 +458,8 @@ def module_edit(
"title": "UE de rattachement", "title": "UE de rattachement",
"explanation": "utilisée notamment pour les malus", "explanation": "utilisée notamment pour les malus",
"labels": [ "labels": [
f"S{u.semestre_idx if u.semestre_idx is not None else '.'} / {u.acronyme} {u.titre}" f"""S{u.semestre_idx if u.semestre_idx is not None else '.'
} / {u.acronyme} {u.titre}"""
for u in ues for u in ues
], ],
"allowed_values": [u.id for u in ues], "allowed_values": [u.id for u in ues],
@ -631,7 +488,8 @@ def module_edit(
"input_type": "menu", "input_type": "menu",
"type": "int", "type": "int",
"title": parcours.SESSION_NAME.capitalize(), "title": parcours.SESSION_NAME.capitalize(),
"explanation": f"{parcours.SESSION_NAME} de début du module dans la formation standard", "explanation": f"""{parcours.SESSION_NAME
} de début du module dans la formation standard""",
"labels": [str(x) for x in semestres_indices], "labels": [str(x) for x in semestres_indices],
"allowed_values": semestres_indices, "allowed_values": semestres_indices,
"enabled": unlocked, "enabled": unlocked,
@ -767,6 +625,7 @@ def module_edit(
module_dict["semestre_id"] = 1 module_dict["semestre_id"] = 1
else: else:
module_dict["semestre_id"] = module.ue.semestre_idx module_dict["semestre_id"] = module.ue.semestre_idx
tags = module.tags if module else []
tf = TrivialFormulator( tf = TrivialFormulator(
request.base_url, request.base_url,
scu.get_request_args(), scu.get_request_args(),
@ -774,7 +633,9 @@ def module_edit(
html_foot_markup=( html_foot_markup=(
f"""<div class="scobox sco_tag_module_edit"><span f"""<div class="scobox sco_tag_module_edit"><span
class="sco_tag_edit"><textarea data-module_id="{module_id}" class="module_tag_editor" class="sco_tag_edit"><textarea data-module_id="{module_id}" class="module_tag_editor"
>{','.join(sco_tag_module.module_tag_list(module_id))}</textarea></span></div> >{
','.join(t.title for t in tags)
}</textarea></span></div>
""" """
if not create if not create
else "" else ""
@ -833,14 +694,17 @@ def module_edit(
if matiere: if matiere:
tf[2]["matiere_id"] = matiere.id tf[2]["matiere_id"] = matiere.id
else: else:
matiere_id = sco_edit_matiere.do_matiere_create( matiere = Matiere.create_from_dict(
{"ue_id": ue.id, "titre": ue.titre or "", "numero": 1}, {
"ue_id": ue.id,
"titre": ue.titre or "",
"numero": 1,
}
) )
tf[2]["matiere_id"] = matiere_id tf[2]["matiere_id"] = matiere.id
tf[2]["semestre_id"] = ue.semestre_idx tf[2]["semestre_id"] = ue.semestre_idx
module_id = do_module_create(tf[2]) module = Module.create_from_dict(tf[2], news=True)
module = db.session.get(Module, module_id)
else: # EDITION MODULE else: # EDITION MODULE
# l'UE de rattachement peut changer # l'UE de rattachement peut changer
tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!") tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!")
@ -885,6 +749,7 @@ def module_edit(
] ]
db.session.add(module) db.session.add(module)
db.session.commit() db.session.commit()
module.formation.invalidate_cached_sems()
return flask.redirect( return flask.redirect(
url_for( url_for(
"notes.ue_table", "notes.ue_table",
@ -897,30 +762,36 @@ def module_edit(
def module_table(formation_id): def module_table(formation_id):
"""Liste des modules de la formation """Liste des modules de la formation
(XXX inutile ou a revoir) (affichage debug)
""" """
from app.scodoc import sco_formations formation = Formation.get_formation(formation_id)
editable = current_user.has_permission(Permission.EditFormation)
if not formation_id:
raise ScoValueError("invalid formation !")
formation: Formation = Formation.query.get_or_404(formation_id)
H = [ H = [
f"""<h2>Listes des modules dans la formation {formation.titre} ({formation.acronyme}</h2> f"""<h2>Listes des modules dans la formation
{formation.titre} ({formation.acronyme} (debug)
</h2>
<ul class="notes_module_list"> <ul class="notes_module_list">
""", """,
] ]
editable = current_user.has_permission(Permission.EditFormation)
for module_dict in module_list(args={"formation_id": formation_id}): for module in formation.modules:
H.append('<li class="notes_module_list">%s' % module_dict) m_dict = module.to_dict()
m_dict["parcours"] = [p.code for p in module.parcours]
str_module = str(m_dict).replace(",", ",\n")
H.append(
f'<li class="notes_module_list"><pre style="margin-bottom: 1px;">{str_module}</pre>'
)
if editable: if editable:
H.append( H.append(
'<a href="module_edit?module_id=%(module_id)s">modifier</a>' f"""
% module_dict <a class="stdlink" href="{
) url_for('notes.module_edit', scodoc_dept=g.scodoc_dept, module_id=module.id)
H.append( }">modifier</a>
'<a href="module_delete?module_id=%(module_id)s">supprimer</a>' <a class="stdlink" href="{
% module_dict url_for('notes.module_delete', scodoc_dept=g.scodoc_dept, module_id=module.id)
}">supprimer</a>
"""
) )
H.append("</li>") H.append("</li>")
H.append("</ul>") H.append("</ul>")
@ -931,29 +802,6 @@ def module_table(formation_id):
) )
def module_is_locked(module_id):
"""True if module should not be modified
(used in a locked formsemestre)
"""
r = ndb.SimpleDictFetch(
"""SELECT mi.id
FROM notes_modules mod, notes_formsemestre sem, notes_moduleimpl mi
WHERE mi.module_id = mod.id
AND mi.formsemestre_id = sem.id
AND mi.module_id = %(module_id)s
AND sem.etat = false
""",
{"module_id": module_id},
)
return len(r) > 0
def module_count_moduleimpls(module_id):
"Number of moduleimpls using this module"
mods = sco_moduleimpl.moduleimpl_list(module_id=module_id)
return len(mods)
def formation_add_malus_modules( def formation_add_malus_modules(
formation_id: int, semestre_id: int = None, titre=None, redirect=True formation_id: int, semestre_id: int = None, titre=None, redirect=True
): ):
@ -967,7 +815,7 @@ def formation_add_malus_modules(
ues = ues.filter_by(semestre_idx=semestre_id) ues = ues.filter_by(semestre_idx=semestre_id)
for ue in ues: for ue in ues:
if ue.type == codes_cursus.UE_STANDARD: if ue.type == codes_cursus.UE_STANDARD:
if ue_add_malus_module(ue, titre=titre) != None: if ue_add_malus_module(ue, titre=titre) is not None:
nb += 1 nb += 1
flash(f"Modules de malus ajoutés dans {nb} UEs du S{semestre_id}") flash(f"Modules de malus ajoutés dans {nb} UEs du S{semestre_id}")
@ -1003,7 +851,8 @@ def ue_add_malus_module(ue: UniteEns, titre=None, code=None) -> int:
# c'est ennuyeux: dans ce cas, on pourrait demander à indiquer explicitement # c'est ennuyeux: dans ce cas, on pourrait demander à indiquer explicitement
# le semestre ? ou affecter le malus au semestre 1 ??? # le semestre ? ou affecter le malus au semestre 1 ???
raise ScoValueError( raise ScoValueError(
"Impossible d'ajouter un malus si l'UE n'a pas de numéro de semestre et ne comporte pas d'autres modules" """Impossible d'ajouter un malus si l'UE n'a pas de numéro de semestre
et ne comporte pas d'autres modules"""
) )
else: else:
semestre_id = ue.semestre_idx semestre_id = ue.semestre_idx

View File

@ -63,11 +63,8 @@ from app.scodoc.sco_exceptions import (
from app.scodoc import codes_cursus from app.scodoc import codes_cursus
from app.scodoc import sco_edit_apc from app.scodoc import sco_edit_apc
from app.scodoc import sco_edit_matiere
from app.scodoc import sco_edit_module
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
from app.scodoc import sco_tag_module
_ueEditor = ndb.EditableTable( _ueEditor = ndb.EditableTable(
"notes_ue", "notes_ue",
@ -376,7 +373,8 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
"type": "float", "type": "float",
"min_value": 0, "min_value": 0,
"title": "Coef. RCUE", "title": "Coef. RCUE",
"explanation": """pondération utilisée pour le calcul de la moyenne du RCUE. Laisser à 1, sauf si votre établissement a explicitement décidé de pondérations. "explanation": """pondération utilisée pour le calcul de la moyenne du RCUE.
Laisser à 1, sauf si votre établissement a explicitement décidé de pondérations.
""", """,
"defaut": 1.0, "defaut": 1.0,
"allow_null": False, "allow_null": False,
@ -422,7 +420,8 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
{ {
"title": "Code Apogée", "title": "Code Apogée",
"size": 25, "size": 25,
"explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules", "explanation": """(optionnel) code élément pédagogique Apogée
ou liste de codes ELP séparés par des virgules""",
"max_length": APO_CODE_STR_LEN, "max_length": APO_CODE_STR_LEN,
}, },
), ),
@ -446,7 +445,8 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
"input_type": "boolcheckbox", "input_type": "boolcheckbox",
"title": "UE externe", "title": "UE externe",
"readonly": not create, # ne permet pas de transformer une UE existante en externe "readonly": not create, # ne permet pas de transformer une UE existante en externe
"explanation": "réservé pour les capitalisations d'UE effectuées à l'extérieur de l'établissement", "explanation": """réservé pour les capitalisations d'UEs
effectuées à l'extérieur de l'établissement""",
}, },
), ),
( (
@ -466,7 +466,8 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
"input_type": "boolcheckbox", "input_type": "boolcheckbox",
"default": True, "default": True,
"title": "Créer matière identique", "title": "Créer matière identique",
"explanation": "créer immédiatement une matière dans cette UE (utile si on n'utilise pas de matières)", "explanation": """créer immédiatement une matière dans cette UE
(utile si on n'utilise pas de matières)""",
}, },
) )
) )
@ -548,12 +549,13 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
if is_apc or cursus.UE_IS_MODULE or tf[2]["create_matiere"]: if is_apc or cursus.UE_IS_MODULE or tf[2]["create_matiere"]:
# rappel: en APC, toutes les UE ont une matière, créée ici # rappel: en APC, toutes les UE ont une matière, créée ici
# (inutilisée mais à laquelle les modules sont rattachés) # (inutilisée mais à laquelle les modules sont rattachés)
matiere_id = sco_edit_matiere.do_matiere_create( matiere = Matiere.create_from_dict(
{"ue_id": ue_id, "titre": tf[2]["titre"], "numero": 1}, {"ue_id": ue_id, "titre": tf[2]["titre"], "numero": 1}
) )
matiere_id = matiere.id
if cursus.UE_IS_MODULE: if cursus.UE_IS_MODULE:
# dans ce mode, crée un (unique) module dans l'UE: # dans ce mode, crée un (unique) module dans l'UE:
_ = sco_edit_module.do_module_create( _ = Module.create_from_dict(
{ {
"titre": tf[2]["titre"], "titre": tf[2]["titre"],
"code": tf[2]["acronyme"], "code": tf[2]["acronyme"],
@ -565,6 +567,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
"semestre_id": tf[2]["semestre_idx"], "semestre_id": tf[2]["semestre_idx"],
}, },
) )
db.session.commit()
ue = db.session.get(UniteEns, ue_id) ue = db.session.get(UniteEns, ue_id)
flash(f"UE créée (code {ue.ue_code})") flash(f"UE créée (code {ue.ue_code})")
else: else:
@ -608,9 +611,10 @@ def _add_ue_semestre_id(ues: list[dict], is_apc):
ue["semestre_id"] = codes_cursus.UE_SEM_DEFAULT ue["semestre_id"] = codes_cursus.UE_SEM_DEFAULT
else: else:
# était le comportement ScoDoc7 # était le comportement ScoDoc7
modules = sco_edit_module.module_list(args={"ue_id": ue["ue_id"]}) ue = UniteEns.get_ue(ue["ue_id"])
if modules: module = ue.modules.first()
ue["semestre_id"] = modules[0]["semestre_id"] if module:
ue["semestre_id"] = module.semestre_id
else: else:
ue["semestre_id"] = codes_cursus.UE_SEM_DEFAULT ue["semestre_id"] = codes_cursus.UE_SEM_DEFAULT
@ -677,9 +681,10 @@ def ue_delete(ue_id=None, delete_validations=False, dialog_confirmed=False):
return do_ue_delete(ue, delete_validations=delete_validations) return do_ue_delete(ue, delete_validations=delete_validations)
def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list def ue_table(formation_id=None, semestre_idx=1, msg=""):
"""Liste des matières et modules d'une formation, avec liens pour """Page affiochage ou édition d'une formation
éditer (si non verrouillée). avec UEs, matières et module,
et liens pour éditer si non verrouillée et permission.
""" """
from app.scodoc import sco_formsemestre_validation from app.scodoc import sco_formsemestre_validation
@ -1241,7 +1246,7 @@ def _ue_table_ues(
def _ue_table_matieres( def _ue_table_matieres(
parcours, parcours,
ue, ue_dict: dict,
editable, editable,
tag_editable, tag_editable,
arrow_up, arrow_up,
@ -1251,26 +1256,27 @@ def _ue_table_matieres(
delete_disabled_icon, delete_disabled_icon,
): ):
"""Édition de programme: liste des matières (et leurs modules) d'une UE.""" """Édition de programme: liste des matières (et leurs modules) d'une UE."""
ue = UniteEns.get_ue(ue_dict["ue_id"])
H = [] H = []
if not parcours.UE_IS_MODULE: if not parcours.UE_IS_MODULE:
H.append('<ul class="notes_matiere_list">') H.append('<ul class="notes_matiere_list">')
matieres = sco_edit_matiere.matiere_list(args={"ue_id": ue["ue_id"]}) matieres = ue.matieres.all()
for mat in matieres: for mat in matieres:
if not parcours.UE_IS_MODULE: if not parcours.UE_IS_MODULE:
H.append('<li class="notes_matiere_list">') H.append('<li class="notes_matiere_list">')
if editable and not sco_edit_matiere.matiere_is_locked(mat["matiere_id"]): if editable and not mat.is_locked():
H.append( H.append(
f"""<a class="stdlink" href="{ f"""<a class="stdlink" href="{
url_for("notes.matiere_edit", url_for("notes.matiere_edit",
scodoc_dept=g.scodoc_dept, matiere_id=mat["matiere_id"]) scodoc_dept=g.scodoc_dept, matiere_id=mat.id)
}"> }">
""" """
) )
H.append("%(titre)s" % mat) H.append(f"{mat.titre or 'sans titre'}")
if editable and not sco_edit_matiere.matiere_is_locked(mat["matiere_id"]): if editable and not mat.is_locked():
H.append("</a>") H.append("</a>")
modules = sco_edit_module.module_list(args={"matiere_id": mat["matiere_id"]}) modules = mat.modules.all()
H.append( H.append(
_ue_table_modules( _ue_table_modules(
parcours, parcours,
@ -1292,14 +1298,17 @@ def _ue_table_matieres(
H.append("<li>Aucune matière dans cette UE ! ") H.append("<li>Aucune matière dans cette UE ! ")
if editable: if editable:
H.append( H.append(
"""<a class="stdlink" href="ue_delete?ue_id=%(ue_id)s">supprimer l'UE</a>""" f"""<a class="stdlink" href="{
% ue url_for('notes.ue_delete', scodoc_dept=g.scodoc_dept, ue_id=ue.id)
}">supprimer l'UE</a>"""
) )
H.append("</li>") H.append("</li>")
if editable and not parcours.UE_IS_MODULE: if editable and not parcours.UE_IS_MODULE:
H.append( H.append(
'<li><a class="stdlink" href="matiere_create?ue_id=%(ue_id)s">créer une matière</a> </li>' f"""<li><a class="stdlink" href="{
% ue url_for("notes.matiere_create", scodoc_dept=g.scodoc_dept, ue_id=ue.id)
}">créer une matière</a>
</li>"""
) )
if not parcours.UE_IS_MODULE: if not parcours.UE_IS_MODULE:
H.append("</ul>") H.append("</ul>")
@ -1308,9 +1317,9 @@ def _ue_table_matieres(
def _ue_table_modules( def _ue_table_modules(
parcours, parcours,
ue, ue: UniteEns,
mat, mat: Matiere,
modules, modules: list[Module],
editable, editable,
tag_editable, tag_editable,
arrow_up, arrow_up,
@ -1318,7 +1327,6 @@ def _ue_table_modules(
arrow_none, arrow_none,
delete_icon, delete_icon,
delete_disabled_icon, delete_disabled_icon,
unit_name="matière",
add_suppress_link=True, # lien "supprimer cette matière" add_suppress_link=True, # lien "supprimer cette matière"
empty_list_msg="Aucun élément dans cette matière", empty_list_msg="Aucun élément dans cette matière",
create_element_msg="créer un module", create_element_msg="créer un module",
@ -1327,91 +1335,83 @@ def _ue_table_modules(
H = ['<ul class="notes_module_list">'] H = ['<ul class="notes_module_list">']
im = 0 im = 0
for mod in modules: for mod in modules:
mod["nb_moduleimpls"] = sco_edit_module.module_count_moduleimpls( nb_moduleimpls = mod.modimpls.count()
mod["module_id"]
)
klass = "notes_module_list" klass = "notes_module_list"
if mod["module_type"] == ModuleType.MALUS: if mod.module_type == ModuleType.MALUS:
klass += " module_malus" klass += " module_malus"
H.append('<li class="%s">' % klass) H.append(f'<li class="{klass}">')
H.append('<span class="notes_module_list_buts">') H.append('<span class="notes_module_list_buts">')
if im != 0 and editable: if im != 0 and editable:
H.append( H.append(
'<a href="module_move?module_id=%s&after=0" class="aud">%s</a>' f"""<a href="module_move?module_id={mod.id}&after=0" class="aud">{arrow_up}</a>"""
% (mod["module_id"], arrow_up)
) )
else: else:
H.append(arrow_none) H.append(arrow_none)
if im < len(modules) - 1 and editable: if im < len(modules) - 1 and editable:
H.append( H.append(
'<a href="module_move?module_id=%s&after=1" class="aud">%s</a>' f"""<a href="module_move?module_id={mod.id}&after=1" class="aud">{arrow_down}</a>"""
% (mod["module_id"], arrow_down)
) )
else: else:
H.append(arrow_none) H.append(arrow_none)
im += 1 im += 1
if mod["nb_moduleimpls"] == 0 and editable: icon = delete_icon if nb_moduleimpls == 0 and editable else delete_disabled_icon
icon = delete_icon
else:
icon = delete_disabled_icon
H.append( H.append(
'<a class="smallbutton" href="module_delete?module_id=%s">%s</a>' f"""<a class="smallbutton" href="{
% (mod["module_id"], icon) url_for("notes.module_delete", scodoc_dept=g.scodoc_dept, module_id=mod.id)
}">{icon}</a>"""
) )
H.append("</span>") H.append("</span>")
mod_editable = ( mod_editable = editable
editable # and not sco_edit_module.module_is_locked( Mod['module_id'])
)
if mod_editable: if mod_editable:
H.append( H.append(
'<a class="discretelink" title="Modifier le module numéro %(numero)s, utilisé par %(nb_moduleimpls)d sessions" href="module_edit?module_id=%(module_id)s">' f"""<a class="discretelink" title="Modifier le module numéro %(numero)s, utilisé par
% mod {nb_moduleimpls} sessions" href="{
url_for("notes.module_edit", scodoc_dept=g.scodoc_dept, module_id=mod.id)
}">"""
) )
if mod["module_type"] not in (scu.ModuleType.STANDARD, scu.ModuleType.MALUS): if mod.module_type not in (scu.ModuleType.STANDARD, scu.ModuleType.MALUS):
H.append( H.append(
f"""<span class="invalid-module-type">{scu.EMO_WARNING} type incompatible </span>""" f"""<span class="invalid-module-type">{scu.EMO_WARNING} type incompatible </span>"""
) )
H.append( H.append(
'<span class="formation_module_tit">%s</span>' f"""<span class="formation_module_tit">{scu.join_words(mod.code, mod.titre)}</span>"""
% scu.join_words(mod["code"], mod["titre"])
) )
if mod_editable: if mod_editable:
H.append("</a>") H.append("</a>")
heurescoef = ( heures = (
"%(heures_cours)s/%(heures_td)s/%(heures_tp)s, coef. %(coefficient)s" % mod f"""{mod.heures_cours or 0}/{mod.heures_td or 0}/{mod.heures_tp or 0}, """
if (mod.heures_cours or mod.heures_td or mod.heures_tp)
else ""
) )
heurescoef = f"""{heures}coef. {mod.coefficient}"""
edit_url = url_for( edit_url = url_for(
"apiweb.formation_module_set_code_apogee", "apiweb.formation_module_set_code_apogee",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
module_id=mod["module_id"], module_id=mod.id,
) )
heurescoef += f""", Apo: <span heurescoef += f""", Apo: <span
class="{'span_apo_edit' if editable else ''}" class="{'span_apo_edit' if editable else ''}"
data-url="{edit_url}" id="{mod["module_id"]}" data-url="{edit_url}" id="{mod.id}"
data-placeholder="{scu.APO_MISSING_CODE_STR}">{ data-placeholder="{scu.APO_MISSING_CODE_STR}">{
mod["code_apogee"] or "" mod.code_apogee or ""
}</span>""" }</span>"""
if tag_editable: tag_cls = "module_tag_editor" if tag_editable else "module_tag_editor_ro"
tag_cls = "module_tag_editor" tag_edit = f"""<span class="sco_tag_edit">
else: <form>
tag_cls = "module_tag_editor_ro" <textarea data-module_id="{mod.id}" class="{tag_cls}">{
tag_mk = """<span class="sco_tag_edit"><form><textarea data-module_id="{}" class="{}">{}</textarea></form></span>""" ",".join([ tag.title for tag in mod.tags ])
tag_edit = tag_mk.format( }</textarea>
mod["module_id"], </form>
tag_cls, </span>"""
",".join(sco_tag_module.module_tag_list(mod["module_id"])), if ue.semestre_idx is not None and mod.semestre_id != ue.semestre_idx:
)
if ue["semestre_idx"] is not None and mod["semestre_id"] != ue["semestre_idx"]:
warning_semestre = ' <span class="red">incohérent ?</span>' warning_semestre = ' <span class="red">incohérent ?</span>'
else: else:
warning_semestre = "" warning_semestre = ""
H.append( H.append(
" %s %s%s" % (parcours.SESSION_NAME, mod["semestre_id"], warning_semestre) f""" {parcours.SESSION_NAME} {mod.semestre_id}{warning_semestre}
+ " (%s)" % heurescoef {heurescoef}{tag_edit}"""
+ tag_edit
) )
H.append("</li>") H.append("</li>")
if not modules: if not modules:
@ -1420,7 +1420,7 @@ def _ue_table_modules(
H.append( H.append(
f"""<a class="stdlink" href="{ f"""<a class="stdlink" href="{
url_for("notes.matiere_delete", url_for("notes.matiere_delete",
scodoc_dept=g.scodoc_dept, matiere_id=mat["matiere_id"])}" scodoc_dept=g.scodoc_dept, matiere_id=mat.id)}"
>la supprimer</a> >la supprimer</a>
""" """
) )
@ -1429,7 +1429,7 @@ def _ue_table_modules(
H.append( H.append(
f"""<li> <a class="stdlink" href="{ f"""<li> <a class="stdlink" href="{
url_for("notes.module_create", url_for("notes.module_create",
scodoc_dept=g.scodoc_dept, matiere_id=mat["matiere_id"])}" scodoc_dept=g.scodoc_dept, matiere_id=mat.id)}"
>{create_element_msg}</a></li> >{create_element_msg}</a></li>
""" """
) )

View File

@ -34,10 +34,10 @@ from flask import flash, g, request, url_for
from flask_login import current_user from flask_login import current_user
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app import db from app import db
from app import log from app import log
from app.models import Formation, FormSemestre, Module, UniteEns from app.formations import edit_ue
from app.models import Formation, FormSemestre, Matiere, Module, UniteEns
from app.models import ScolarNews from app.models import ScolarNews
from app.models.but_refcomp import ( from app.models.but_refcomp import (
ApcAppCritique, ApcAppCritique,
@ -48,49 +48,12 @@ from app.models.but_refcomp import (
) )
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc import codes_cursus from app.scodoc import codes_cursus
from app.scodoc import sco_edit_matiere
from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_tag_module
from app.scodoc import sco_xml from app.scodoc import sco_xml
import sco_version
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_exceptions import ScoValueError, ScoFormatError from app.scodoc.sco_exceptions import ScoValueError, ScoFormatError
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
import sco_version
_formationEditor = ndb.EditableTable(
"notes_formations",
"formation_id",
(
"formation_id",
"acronyme",
"titre",
"titre_officiel",
"version",
"formation_code",
"type_parcours",
"code_specialite",
"referentiel_competence_id",
"commentaire",
),
filter_dept=True,
sortkey="acronyme",
)
def formation_list(formation_id=None, args={}): ### XXX obsolete, à supprimer
"""List formation(s) with given id, or matching args
(when args is given, formation_id is ignored).
"""
if not args:
if formation_id is None:
args = {}
else:
args = {"formation_id": formation_id}
cnx = ndb.GetDBConnexion()
r = _formationEditor.list(cnx, args=args)
return r
def formation_export_dict( def formation_export_dict(
@ -149,35 +112,35 @@ def formation_export_dict(
ue_dict.pop("code_apogee_rcue", None) ue_dict.pop("code_apogee_rcue", None)
if ue_dict.get("ects") is None: if ue_dict.get("ects") is None:
ue_dict.pop("ects", None) ue_dict.pop("ects", None)
mats = sco_edit_matiere.matiere_list({"ue_id": ue.id}) mats = ue.matieres.all()
mats.sort(key=lambda m: m["numero"] or 0) mats.sort(key=lambda m: m.numero)
ue_dict["matiere"] = mats mats_dict = [mat.to_dict() for mat in mats]
for mat in mats: ue_dict["matiere"] = mats_dict
matiere_id = mat["matiere_id"] for mat_d in mats_dict:
matiere_id = mat_d["matiere_id"]
if not export_ids: if not export_ids:
del mat["id"] del mat_d["id"]
del mat["matiere_id"] del mat_d["matiere_id"]
del mat["ue_id"] del mat_d["ue_id"]
mods = sco_edit_module.module_list({"matiere_id": matiere_id}) mat = db.session.get(Matiere, matiere_id)
mods.sort(key=lambda m: (m["numero"] or 0, m["code"])) mods = mat.modules.all()
mat["module"] = mods mods.sort(key=lambda m: (m.numero, m.code))
for mod in mods: mat_d["module"] = [mod.to_dict() for mod in mods]
module_id = mod["module_id"] for module, mod_d in zip(mods, mat_d["module"]):
if export_tags: if export_tags:
tags = sco_tag_module.module_tag_list(module_id=mod["module_id"]) tags = [t.title for t in module.tags]
if tags: if tags:
mod["tags"] = [{"name": x} for x in tags] mod_d["tags"] = [{"name": x} for x in tags]
# #
module: Module = db.session.get(Module, module_id)
if module.is_apc(): if module.is_apc():
# Exporte les coefficients # Exporte les coefficients
if ue_reference_style == "id": if ue_reference_style == "id":
mod["coefficients"] = [ mod_d["coefficients"] = [
{"ue_reference": str(ue_id), "coef": str(coef)} {"ue_reference": str(ue_id), "coef": str(coef)}
for (ue_id, coef) in module.get_ue_coef_dict().items() for (ue_id, coef) in module.get_ue_coef_dict().items()
] ]
else: else:
mod["coefficients"] = [ mod_d["coefficients"] = [
{"ue_reference": ue_acronyme, "coef": str(coef)} {"ue_reference": ue_acronyme, "coef": str(coef)}
for ( for (
ue_acronyme, ue_acronyme,
@ -185,29 +148,29 @@ def formation_export_dict(
) in module.get_ue_coef_dict_acronyme().items() ) in module.get_ue_coef_dict_acronyme().items()
] ]
# Et les parcours # Et les parcours
mod["parcours"] = [ mod_d["parcours"] = [
p.to_dict(with_annees=False) for p in module.parcours p.to_dict(with_annees=False) for p in module.parcours
] ]
# Et les AC # Et les AC
if ac_as_list: if ac_as_list:
# XML préfère une liste # XML préfère une liste
mod["app_critiques"] = [ mod_d["app_critiques"] = [
x.to_dict(with_code=True) for x in module.app_critiques x.to_dict(with_code=True) for x in module.app_critiques
] ]
else: else:
mod["app_critiques"] = { mod_d["app_critiques"] = {
x.code: x.to_dict() for x in module.app_critiques x.code: x.to_dict() for x in module.app_critiques
} }
if not export_ids: if not export_ids:
del mod["id"] del mod_d["id"]
del mod["ue_id"] del mod_d["ue_id"]
del mod["matiere_id"] del mod_d["matiere_id"]
del mod["module_id"] del mod_d["module_id"]
del mod["formation_id"] del mod_d["formation_id"]
if not export_codes_apo: if not export_codes_apo:
del mod["code_apogee"] del mod_d["code_apogee"]
if mod["ects"] is None: if mod_d["ects"] is None:
del mod["ects"] del mod_d["ects"]
return f_dict return f_dict
@ -236,7 +199,8 @@ def formation_export(
if fmt is None: if fmt is None:
return f_dict return f_dict
filename = f"scodoc_formation_{formation.departement.acronym}_{formation.acronyme or ''}_v{formation.version}" filename = f"""scodoc_formation_{formation.departement.acronym
}_{formation.acronyme or ''}_v{formation.version}"""
return scu.sendResult( return scu.sendResult(
f_dict, f_dict,
name="formation", name="formation",
@ -314,7 +278,7 @@ def formation_import_xml(doc: str | bytes, import_tags=True, use_local_refcomp=F
Returns: Returns:
formation_id, modules_old2new, ues_old2new formation_id, modules_old2new, ues_old2new
""" """
from app.scodoc import sco_edit_formation from app.formations import edit_formation
if isinstance(doc, bytes): if isinstance(doc, bytes):
doc = doc.decode(scu.SCO_ENCODING) doc = doc.decode(scu.SCO_ENCODING)
@ -326,14 +290,14 @@ def formation_import_xml(doc: str | bytes, import_tags=True, use_local_refcomp=F
try: try:
f = dom.getElementsByTagName("formation")[0] # or dom.documentElement f = dom.getElementsByTagName("formation")[0] # or dom.documentElement
D = sco_xml.xml_to_dicts(f) xml_dicts = sco_xml.xml_to_dicts(f)
except Exception as exc: except Exception as exc:
raise ScoFormatError( raise ScoFormatError(
"""Ce document xml ne correspond pas à un programme exporté par ScoDoc. """Ce document xml ne correspond pas à un programme exporté par ScoDoc.
(élément 'formation' inexistant par exemple).""" (élément 'formation' inexistant par exemple)."""
) from exc ) from exc
assert D[0] == "formation" assert xml_dicts[0] == "formation"
f_dict = D[1] f_dict = xml_dicts[1]
f_dict["dept_id"] = g.scodoc_dept_id f_dict["dept_id"] = g.scodoc_dept_id
# Pour les clonages, on prend le refcomp_id donné: # Pour les clonages, on prend le refcomp_id donné:
referentiel_competence_id = ( referentiel_competence_id = (
@ -359,7 +323,7 @@ def formation_import_xml(doc: str | bytes, import_tags=True, use_local_refcomp=F
f_dict["version"] = version + 1 f_dict["version"] = version + 1
# create formation # create formation
formation = sco_edit_formation.do_formation_create(f_dict) formation = edit_formation.do_formation_create(f_dict)
log(f"formation {formation.id} created") log(f"formation {formation.id} created")
ues_old2new = {} # xml ue_id : new ue_id ues_old2new = {} # xml ue_id : new ue_id
@ -370,7 +334,7 @@ def formation_import_xml(doc: str | bytes, import_tags=True, use_local_refcomp=F
modules_a_coefficienter = [] # Liste des modules avec coefs APC modules_a_coefficienter = [] # Liste des modules avec coefs APC
with sco_cache.DeferredSemCacheManager(): with sco_cache.DeferredSemCacheManager():
# -- create UEs # -- create UEs
for ue_info in D[2]: for ue_info in xml_dicts[2]:
assert ue_info[0] == "ue" assert ue_info[0] == "ue"
ue_info[1]["formation_id"] = formation.id ue_info[1]["formation_id"] = formation.id
if "ue_id" in ue_info[1]: if "ue_id" in ue_info[1]:
@ -387,7 +351,7 @@ def formation_import_xml(doc: str | bytes, import_tags=True, use_local_refcomp=F
) )
# Note: si le code est indiqué "" dans le xml, il faut le conserver vide # Note: si le code est indiqué "" dans le xml, il faut le conserver vide
# pour la comparaison ultérieure des formations XXX # pour la comparaison ultérieure des formations XXX
ue_id = sco_edit_ue.do_ue_create(ue_info[1], allow_empty_ue_code=True) ue_id = edit_ue.do_ue_create(ue_info[1], allow_empty_ue_code=True)
ue: UniteEns = db.session.get(UniteEns, ue_id) ue: UniteEns = db.session.get(UniteEns, ue_id)
assert ue assert ue
if xml_ue_id: if xml_ue_id:
@ -435,7 +399,8 @@ def formation_import_xml(doc: str | bytes, import_tags=True, use_local_refcomp=F
assert mat_info[0] == "matiere" assert mat_info[0] == "matiere"
mat_info[1]["ue_id"] = ue_id mat_info[1]["ue_id"] = ue_id
mat_id = sco_edit_matiere.do_matiere_create(mat_info[1]) mat = Matiere.create_from_dict(mat_info[1])
mat_id = mat.id
# -- create modules # -- create modules
for mod_info in mat_info[2]: for mod_info in mat_info[2]:
assert mod_info[0] == "module" assert mod_info[0] == "module"
@ -449,11 +414,12 @@ def formation_import_xml(doc: str | bytes, import_tags=True, use_local_refcomp=F
mod_info[1]["ue_id"] = ue_id mod_info[1]["ue_id"] = ue_id
if not "module_type" in mod_info[1]: if not "module_type" in mod_info[1]:
mod_info[1]["module_type"] = scu.ModuleType.STANDARD mod_info[1]["module_type"] = scu.ModuleType.STANDARD
mod_id = sco_edit_module.do_module_create(mod_info[1]) module = Module.create_from_dict(
mod_info[1], news=True, inval_cache=True
)
if xml_module_id: if xml_module_id:
modules_old2new[int(xml_module_id)] = mod_id modules_old2new[int(xml_module_id)] = module.id
if len(mod_info) > 2: if len(mod_info) > 2:
module: Module = db.session.get(Module, mod_id)
tag_names = [] tag_names = []
ue_coef_dict = {} ue_coef_dict = {}
for child in mod_info[2]: for child in mod_info[2]:
@ -496,7 +462,7 @@ def formation_import_xml(doc: str | bytes, import_tags=True, use_local_refcomp=F
f"Warning: parcours {code_parcours} inexistant !" f"Warning: parcours {code_parcours} inexistant !"
) )
if import_tags and tag_names: if import_tags and tag_names:
sco_tag_module.module_tag_set(mod_id, tag_names) module.set_tags(tag_names)
if module.is_apc() and ue_coef_dict: if module.is_apc() and ue_coef_dict:
modules_a_coefficienter.append((module, ue_coef_dict)) modules_a_coefficienter.append((module, ue_coef_dict))
# Fixe les coefs APC (à la fin pour que les UE soient créées) # Fixe les coefs APC (à la fin pour que les UE soient créées)

View File

@ -180,7 +180,7 @@ def formation_table_recap(formation: Formation, fmt="html") -> Response:
def export_recap_formations_annee_scolaire(annee_scolaire): def export_recap_formations_annee_scolaire(annee_scolaire):
"""Exporte un zip des recap (excel) des formatons de tous les semestres """Exporte un zip des recap (excel) des formations de tous les semestres
de l'année scolaire indiquée. de l'année scolaire indiquée.
""" """
annee_scolaire = int(annee_scolaire) annee_scolaire = int(annee_scolaire)

View File

@ -47,7 +47,7 @@ import app.scodoc.sco_utils as scu
from app import log from app import log
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_formations from app.formations import formation_io
def formsemestre_associate_new_version( def formsemestre_associate_new_version(
@ -202,7 +202,7 @@ def do_formsemestres_associate_new_version(
new_formation_id, new_formation_id,
modules_old2new, modules_old2new,
ues_old2new, ues_old2new,
) = sco_formations.formation_create_new_version(formation_id, redirect=False) ) = formation_io.formation_create_new_version(formation_id, redirect=False)
# Log new ues: # Log new ues:
for ue_id in ues_old2new: for ue_id in ues_old2new:
ue = db.session.get(UniteEns, ue_id) ue = db.session.get(UniteEns, ue_id)
@ -275,13 +275,13 @@ def formations_are_equals(
"""True if the two formations are exactly the same, except for their versions. """True if the two formations are exactly the same, except for their versions.
Can specify either formation2 or its dict repr. Can specify either formation2 or its dict repr.
""" """
fd1 = sco_formations.formation_export_dict( fd1 = formation_io.formation_export_dict(
formation1, export_external_ues=True, ue_reference_style="acronyme" formation1, export_external_ues=True, ue_reference_style="acronyme"
) )
if formation2_dict is None: if formation2_dict is None:
if formation2 is None: if formation2 is None:
raise ValueError("must specify formation2 or formation2_dict") raise ValueError("must specify formation2 or formation2_dict")
formation2_dict = sco_formations.formation_export_dict( formation2_dict = formation_io.formation_export_dict(
formation2, export_external_ues=True, ue_reference_style="acronyme" formation2, export_external_ues=True, ue_reference_style="acronyme"
) )
del fd1["version"] del fd1["version"]

View File

@ -5,7 +5,7 @@ from flask import abort, g
from flask_sqlalchemy.query import Query from flask_sqlalchemy.query import Query
import app import app
from app import db from app import db, log
from app.comp import df_cache from app.comp import df_cache
from app.models import ScoDocModel, SHORT_STR_LEN from app.models import ScoDocModel, SHORT_STR_LEN
from app.models.but_refcomp import ( from app.models.but_refcomp import (
@ -14,6 +14,7 @@ from app.models.but_refcomp import (
ApcParcours, ApcParcours,
ApcParcoursNiveauCompetence, ApcParcoursNiveauCompetence,
) )
from app.models.events import ScolarNews
from app.models.modules import Module from app.models.modules import Module
from app.models.moduleimpls import ModuleImpl from app.models.moduleimpls import ModuleImpl
from app.models.ues import UniteEns, UEParcours from app.models.ues import UniteEns, UEParcours
@ -21,6 +22,7 @@ from app.scodoc import sco_cache
from app.scodoc import codes_cursus from app.scodoc import codes_cursus
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.codes_cursus import UE_STANDARD from app.scodoc.codes_cursus import UE_STANDARD
from app.scodoc.sco_exceptions import ScoNonEmptyFormationObject, ScoValueError
class Formation(ScoDocModel): class Formation(ScoDocModel):
@ -64,7 +66,8 @@ class Formation(ScoDocModel):
def html(self) -> str: def html(self) -> str:
"titre complet pour affichage" "titre complet pour affichage"
return f"""Formation {self.titre} ({self.acronyme}) version {self.version} code <tt>{self.formation_code}</tt>""" return f"""Formation {self.titre} ({self.acronyme}) version {self.version
} code <tt>{self.formation_code}</tt>"""
@classmethod @classmethod
def get_formation(cls, formation_id: int | str, dept_id: int = None) -> "Formation": def get_formation(cls, formation_id: int | str, dept_id: int = None) -> "Formation":
@ -166,6 +169,7 @@ class Formation(ScoDocModel):
sco_cache.invalidate_formsemestre() sco_cache.invalidate_formsemestre()
def invalidate_cached_sems(self): def invalidate_cached_sems(self):
"Invalide caches de tous les formssemestres de la formation"
for sem in self.formsemestres: for sem in self.formsemestres:
sco_cache.invalidate_formsemestre(formsemestre_id=sem.id) sco_cache.invalidate_formsemestre(formsemestre_id=sem.id)
@ -312,7 +316,9 @@ class Matiere(ScoDocModel):
titre = db.Column(db.Text()) titre = db.Column(db.Text())
numero = db.Column(db.Integer, nullable=False, default=0) # ordre de présentation numero = db.Column(db.Integer, nullable=False, default=0) # ordre de présentation
modules = db.relationship("Module", lazy="dynamic", backref="matiere") modules = db.relationship(
"Module", lazy="dynamic", backref="matiere", cascade="all, delete-orphan"
)
_sco_dept_relations = ("UniteEns", "Formation") # accès au dept_id _sco_dept_relations = ("UniteEns", "Formation") # accès au dept_id
def __repr__(self): def __repr__(self):
@ -325,5 +331,73 @@ class Matiere(ScoDocModel):
e.pop("_sa_instance_state", None) e.pop("_sa_instance_state", None)
# ScoDoc7 output_formators # ScoDoc7 output_formators
e["numero"] = e["numero"] if e["numero"] else 0 e["numero"] = e["numero"] if e["numero"] else 0
e["ue_id"] = self.id e["matiere_id"] = self.id
return e return e
def is_locked(self) -> bool:
"""True if matiere can't modified
because it contains modules used in a locked formsemestre.
"""
from app.models.formsemestre import FormSemestre
mat = (
db.session.query(Matiere)
.filter_by(id=self.id)
.join(Module)
.join(ModuleImpl)
.join(FormSemestre)
.filter_by(etat=False)
.all()
)
return bool(mat)
def can_be_deleted(self) -> bool:
"True si la matiere n'est pas utilisée dans des formsemestres"
locked = self.is_locked()
if locked:
return False
if any(m.modimpls.all() for m in self.modules):
return False
return True
def delete(self):
"Delete matière. News, inval cache."
formation = self.ue.formation
log(f"matiere.delete: matiere_id={self.id}")
if not self.can_be_deleted():
# il y a au moins un modimpl dans un module de cette matière
raise ScoNonEmptyFormationObject("Matière", self.titre)
db.session.delete(self)
db.session.commit()
# news
ScolarNews.add(
typ=ScolarNews.NEWS_FORM,
obj=formation.id,
text=f"Modification de la formation {formation.acronyme}",
)
# cache
formation.invalidate_cached_sems()
@classmethod
def create_from_dict(cls, data: dict) -> "Matiere":
"""Create matière from dict. Log, news, cache.
data must include ue_id, a valid UE id.
Commit session.
"""
# check ue
if data.get("ue_id") is None:
raise ScoValueError("UE id missing")
_ = UniteEns.get_ue(data["ue_id"])
mat = super().create_from_dict(data)
db.session.commit()
db.session.refresh(mat)
# news
formation = mat.ue.formation
ScolarNews.add(
typ=ScolarNews.NEWS_FORM,
obj=formation.id,
text=f"Modification de la formation {formation.acronyme}",
)
formation.invalidate_cached_sems()
return mat

View File

@ -7,14 +7,15 @@ from flask_login import current_user
from flask_sqlalchemy.query import Query from flask_sqlalchemy.query import Query
import app import app
from app import db from app import db, log
from app.auth.models import User from app.auth.models import User
from app.comp import df_cache from app.comp import df_cache
from app.models import APO_CODE_STR_LEN, ScoDocModel from app.models import APO_CODE_STR_LEN, ScoDocModel
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.models.evaluations import Evaluation from app.models.evaluations import Evaluation
from app.models.modules import Module from app.models.modules import Module
from app.scodoc.sco_exceptions import AccessDenied, ScoLockedSemError from app.scodoc import sco_cache
from app.scodoc.sco_exceptions import AccessDenied, ScoLockedSemError, ScoValueError
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
@ -65,6 +66,30 @@ class ModuleImpl(ScoDocModel):
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__} {self.id} module={repr(self.module)}>" return f"<{self.__class__.__name__} {self.id} module={repr(self.module)}>"
@classmethod
def create_from_dict(cls, data: dict) -> "ModuleImpl":
"""Create modimpl from dict. Log, inval. cache.
data must include valid formsemestre_id, module_id and responsable_id
Commit session.
"""
from app.models import FormSemestre
# check required args
for required_arg in ("formsemestre_id", "module_id", "responsable_id"):
if required_arg not in data:
raise ScoValueError(f"missing argument: {required_arg}")
_ = FormSemestre.get_formsemestre(data["formsemestre_id"])
_ = Module.get_instance(data["module_id"])
if not db.session.get(User, data["responsable_id"]):
abort(404, "responsable_id invalide")
modimpl = super().create_from_dict(data)
db.session.commit()
db.session.refresh(modimpl)
log(f"ModuleImpl.create: created {modimpl.id} with {data}")
sco_cache.invalidate_formsemestre(formsemestre_id=modimpl.formsemestre_id)
return modimpl
def get_codes_apogee(self) -> set[str]: def get_codes_apogee(self) -> set[str]:
"""Les codes Apogée (codés en base comme "VRT1,VRT2"). """Les codes Apogée (codés en base comme "VRT1,VRT2").
(si non renseigné, ceux du module) (si non renseigné, ceux du module)

View File

@ -1,9 +1,10 @@
"""ScoDoc 9 models : Modules """ScoDoc 9 models : Modules
""" """
from flask import current_app, g import http
from flask import current_app, g, url_for
from app import db from app import db, log
from app import models from app import models
from app.models import APO_CODE_STR_LEN from app.models import APO_CODE_STR_LEN
from app.models.but_refcomp import ( from app.models.but_refcomp import (
@ -12,9 +13,14 @@ from app.models.but_refcomp import (
app_critiques_modules, app_critiques_modules,
parcours_modules, parcours_modules,
) )
from app.models.events import ScolarNews
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.codes_cursus import UE_SPORT from app.scodoc.codes_cursus import UE_SPORT
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import (
ScoValueError,
ScoLockedFormError,
ScoNonEmptyFormationObject,
)
from app.scodoc.sco_utils import ModuleType from app.scodoc.sco_utils import ModuleType
@ -75,11 +81,11 @@ class Module(models.ScoDocModel):
backref=db.backref("modules", lazy=True), backref=db.backref("modules", lazy=True),
) )
_sco_dept_relations = "Formation" # accès au dept_id _sco_dept_relations = ("Formation",) # accès au dept_id
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.ue_coefs = [] self.ue_coefs = []
super(Module, self).__init__(**kwargs) super().__init__(**kwargs)
def __repr__(self): def __repr__(self):
return f"""<Module{ModuleType(self.module_type or ModuleType.STANDARD).name return f"""<Module{ModuleType(self.module_type or ModuleType.STANDARD).name
@ -114,13 +120,39 @@ class Module(models.ScoDocModel):
# on ne peut pas affecter directement parcours # on ne peut pas affecter directement parcours
return super().filter_model_attributes(args, (excluded or set()) | {"parcours"}) return super().filter_model_attributes(args, (excluded or set()) | {"parcours"})
@classmethod
def check_module_code_unicity(cls, code, formation_id, module_id=None) -> bool:
"true si code module unique dans la formation"
from app.models import Formation
formation = Formation.get_formation(formation_id)
query = formation.modules.filter_by(code=code)
if module_id is not None: # edition: supprime le module en cours
query = query.filter(Module.id != module_id)
return query.count() == 0
def from_dict(self, args: dict, excluded: set[str] | None = None) -> bool: def from_dict(self, args: dict, excluded: set[str] | None = None) -> bool:
"""Update object's fields given in dict. Add to session but don't commit. """Update object's fields given in dict. Add to session but don't commit.
True if modification. True if modification.
- can't change ue nor formation - can't change ue nor formation
- can change matiere_id, iff new matiere in same ue - can change matiere_id, iff new matiere in same ue
- can change parcours: parcours list of ApcParcour id or instances. - can change parcours: parcours list of ApcParcour id or instances.
Ne modifie pas les coefficients APC ue_coefs
""" """
args = args.copy()
if "ue_coefs" in args:
del args["ue_coefs"]
if self.is_locked():
# formation verrouillée: empeche de modifier coefficient, matiere, and semestre_id
protected_fields = ("coefficient", "matiere_id", "semestre_id")
for f in protected_fields:
if f in args:
del args[f]
# Unicité du code
if "code" in args and not Module.check_module_code_unicity(
args["code"], self.formation_id, self.id
):
raise ScoValueError("code module déjà utilisé")
# Vérifie les changements de matiere # Vérifie les changements de matiere
new_matiere_id = args.get("matiere_id", self.matiere_id) new_matiere_id = args.get("matiere_id", self.matiere_id)
if new_matiere_id != self.matiere_id: if new_matiere_id != self.matiere_id:
@ -138,20 +170,103 @@ class Module(models.ScoDocModel):
existing_parcours = {p.id for p in self.parcours} existing_parcours = {p.id for p in self.parcours}
new_parcours = args.get("parcours", []) or [] new_parcours = args.get("parcours", []) or []
if existing_parcours != set(new_parcours): if existing_parcours != set(new_parcours):
self._set_parcours_from_list(new_parcours) self.set_parcours_from_list(new_parcours)
return True return True
return modified return modified
@classmethod @classmethod
def create_from_dict(cls, data: dict) -> "Module": def create_from_dict(
cls,
data: dict,
inval_cache=False,
news=False,
) -> "Module":
"""Create from given dict, add parcours. """Create from given dict, add parcours.
Flush session.""" Flush session.
Si news, commit and log news.
"""
from app.models.formations import Formation
# check required arguments
for required_arg in ("code", "formation_id", "ue_id"):
if required_arg not in data:
raise ScoValueError(f"missing argument: {required_arg}")
if not data["code"]:
raise ScoValueError("module code must be non empty")
# Check formation
formation = Formation.get_formation(data["formation_id"])
ue = UniteEns.get_ue(data["ue_id"])
# refuse de créer un module APC avec semestres semestre du module != semestre de l'UE
if formation.is_apc():
if int(data.get("semestre_id", 1)) != ue.semestre_idx:
raise ScoValueError(
"Formation incompatible: indices UE et module différents"
)
module = super().create_from_dict(data) module = super().create_from_dict(data)
db.session.flush() db.session.flush()
module._set_parcours_from_list(data.get("parcours", []) or []) module.set_parcours_from_list(data.get("parcours", []) or [])
log(f"module_create: created {module.id} with {data}")
if news:
db.session.commit()
db.session.refresh(module)
ScolarNews.add(
typ=ScolarNews.NEWS_FORM,
obj=formation.id,
text=f"Modification de la formation {formation.acronyme}",
)
if inval_cache:
formation.invalidate_cached_sems()
return module return module
def _set_parcours_from_list(self, parcours: list[ApcParcours | int]): def is_locked(self) -> bool:
"""True if module cannot be modified
because it is used in a locked formsemestre.
"""
from app.models import FormSemestre, ModuleImpl
mods = (
db.session.query(Module)
.filter_by(id=self.id)
.join(ModuleImpl)
.join(FormSemestre)
.filter_by(etat=False)
.all()
)
return bool(mods)
def can_be_deleted(self) -> bool:
"""True if module can be deleted"""
return self.modimpls.count() == 0
def delete(self):
"Delete module. News, inval cache."
if self.is_locked():
raise ScoLockedFormError()
if not self.can_be_deleted():
raise ScoNonEmptyFormationObject(
"Module",
msg=self.titre or self.code,
dest_url=url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=self.formation_id,
semestre_idx=self.ue.semestre_idx,
),
)
formation = self.formation
db.session.delete(self)
log(f"Module.delete({self.id})")
db.session.commit()
# news
ScolarNews.add(
typ=ScolarNews.NEWS_FORM,
obj=formation.id,
text=f"Modification de la formation {formation.acronyme}",
)
formation.invalidate_cached_sems()
def set_parcours_from_list(self, parcours: list[ApcParcours | int]):
"""Ajoute ces parcours à la liste des parcours du module. """Ajoute ces parcours à la liste des parcours du module.
Chaque élément est soit un objet parcours soit un id. Chaque élément est soit un objet parcours soit un id.
S'assure que chaque parcours est dans le référentiel de compétence S'assure que chaque parcours est dans le référentiel de compétence
@ -449,6 +564,43 @@ class Module(models.ScoDocModel):
db.session.add(self) db.session.add(self)
db.session.flush() db.session.flush()
def set_tags(self, taglist: str | list[str] | None = None):
"""taglist may either be:
a string with tag names separated by commas ("un,deux")
or a list of strings (["un", "deux"])
Remplace les tags existants
"""
# TODO refactoring ScoTag
# TODO code à moderniser (+ revoir classe ScoTag, utiliser modèle)
# TODO Voir ItemSuiviTag et api etud_suivi
from app.scodoc.sco_tag_module import ScoTag, ModuleTag
taglist = taglist or []
if isinstance(taglist, str):
taglist = taglist.split(",")
taglist = [t.strip() for t in taglist]
taglist = [t for t in taglist if t]
log(f"module.set_tags: module_id={self.id} taglist={taglist}")
# Check tags syntax
for tag in taglist:
if not ScoTag.check_tag_title(tag):
log(f"module.set_tags({self.id}): invalid tag title")
return scu.json_error(404, "invalid tag")
newtags = set(taglist)
oldtags = set(t.title for t in self.tags)
to_del = oldtags - newtags
to_add = newtags - oldtags
# should be atomic, but it's not.
for tagname in to_add:
t = ModuleTag(tagname, object_id=self.id)
for tagname in to_del:
t = ModuleTag(tagname)
t.remove_tag_from_object(self.id)
return "", http.HTTPStatus.NO_CONTENT
class ModuleUECoef(db.Model): class ModuleUECoef(db.Model):
"""Coefficients des modules vers les UE (APC, BUT) """Coefficients des modules vers les UE (APC, BUT)
@ -499,7 +651,7 @@ class ModuleUECoef(db.Model):
return d return d
class NotesTag(db.Model): class NotesTag(models.ScoDocModel):
"""Tag sur un module""" """Tag sur un module"""
__tablename__ = "notes_tags" __tablename__ = "notes_tags"
@ -511,6 +663,9 @@ class NotesTag(db.Model):
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True) dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
title = db.Column(db.Text(), nullable=False) title = db.Column(db.Text(), nullable=False)
def __repr__(self):
return f"<Tag {self.id} {self.title!r}>"
@classmethod @classmethod
def get_or_create(cls, title: str, dept_id: int | None = None) -> "NotesTag": def get_or_create(cls, title: str, dept_id: int | None = None) -> "NotesTag":
"""Get tag, or create it if it doesn't yet exists. """Get tag, or create it if it doesn't yet exists.

View File

@ -186,9 +186,9 @@ class UniteEns(models.ScoDocModel):
def is_locked(self) -> tuple[bool, str]: def is_locked(self) -> tuple[bool, str]:
"""True if UE should not be modified""" """True if UE should not be modified"""
from app.scodoc import sco_edit_ue from app.formations import edit_ue
return sco_edit_ue.ue_is_locked(self.id) return edit_ue.ue_is_locked(self.id)
def can_be_deleted(self) -> bool: def can_be_deleted(self) -> bool:
"""True si l'UE n'a pas de moduleimpl rattachés """True si l'UE n'a pas de moduleimpl rattachés

View File

@ -41,8 +41,8 @@ from app import ScoValueError
from app import comp from app import comp
from app.comp.res_but import ResultatsSemestreBUT from app.comp.res_but import ResultatsSemestreBUT
from app.models import FormSemestre, UniteEns from app.models import FormSemestre, UniteEns
import app.pe.pe_affichage as pe_affichage from app.pe import pe_affichage
import app.pe.pe_etudiant as pe_etudiant from app.pe import pe_etudiant
from app.pe.moys import pe_tabletags, pe_moytag from app.pe.moys import pe_tabletags, pe_moytag
from app.scodoc import sco_tag_module from app.scodoc import sco_tag_module
from app.scodoc import codes_cursus as sco_codes from app.scodoc import codes_cursus as sco_codes
@ -59,13 +59,18 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag):
def __init__( def __init__(
self, self,
formsemestre: FormSemestre, formsemestre: FormSemestre,
options={"moyennes_tags": True, "moyennes_ue_res_sae": False}, options: dict | None = None,
): ):
""" """
Args: Args:
formsemestre: le ``FormSemestre`` sur lequel il se base formsemestre: le ``FormSemestre`` sur lequel il se base
options: Un dictionnaire d'options options: Un dictionnaire d'options
""" """
options = (
{"moyennes_tags": True, "moyennes_ue_res_sae": False}
if options is None
else options
)
ResultatsSemestreBUT.__init__(self, formsemestre) ResultatsSemestreBUT.__init__(self, formsemestre)
pe_tabletags.TableTag.__init__(self) pe_tabletags.TableTag.__init__(self)
@ -274,7 +279,8 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag):
pole=None, pole=None,
) -> pd.DataFrame: ) -> pd.DataFrame:
"""Calcule la moyenne par UE des étudiants pour un tag donné, """Calcule la moyenne par UE des étudiants pour un tag donné,
en ayant connaissance des informations sur le tag et des inscriptions des étudiants aux différentes UEs. en ayant connaissance des informations sur le tag et des inscriptions
des étudiants aux différentes UEs.
info_tag détermine les modules pris en compte : info_tag détermine les modules pris en compte :
* si non `None`, seuls les modules rattachés au tag sont pris en compte * si non `None`, seuls les modules rattachés au tag sont pris en compte
@ -342,7 +348,8 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag):
colonnes = [ue.id for ue in self.ues_standards] colonnes = [ue.id for ue in self.ues_standards]
moyennes_ues_tag = moyennes_ues_tag[colonnes] moyennes_ues_tag = moyennes_ues_tag[colonnes]
# Applique le masque d'inscription aux UE pour ne conserver que les UE dans lequel l'étudiant est inscrit # Applique le masque d'inscription aux UE pour ne conserver
# que les UE dans lequel l'étudiant est inscrit
moyennes_ues_tag = moyennes_ues_tag[colonnes] * ues_inscr_parcours_df[colonnes] moyennes_ues_tag = moyennes_ues_tag[colonnes] * ues_inscr_parcours_df[colonnes]
# Transforme les UEs en acronyme # Transforme les UEs en acronyme
@ -405,7 +412,7 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag):
"""Vérifie l'unicité des tags""" """Vérifie l'unicité des tags"""
noms_tags_perso = sorted(list(set(dict_tags["personnalises"].keys()))) noms_tags_perso = sorted(list(set(dict_tags["personnalises"].keys())))
noms_tags_auto = sorted(list(set(dict_tags["auto"].keys()))) # + noms_tags_comp noms_tags_auto = sorted(list(set(dict_tags["auto"].keys()))) # + noms_tags_comp
noms_tags = noms_tags_perso + noms_tags_auto # noms_tags = noms_tags_perso + noms_tags_auto
intersection = list(set(noms_tags_perso) & set(noms_tags_auto)) intersection = list(set(noms_tags_perso) & set(noms_tags_auto))
@ -455,7 +462,7 @@ def get_synthese_tags_personnalises_semestre(formsemestre: FormSemestre):
modimpl_id = modimpl.id modimpl_id = modimpl.id
# Liste des tags pour le module concerné # Liste des tags pour le module concerné
tags = sco_tag_module.module_tag_list(modimpl.module.id) tags = [t.title for t in modimpl.module.tags]
# Traitement des tags recensés, chacun pouvant étant de la forme # Traitement des tags recensés, chacun pouvant étant de la forme
# "mathématiques", "théorie", "pe:0", "maths:2" # "mathématiques", "théorie", "pe:0", "maths:2"

View File

@ -296,13 +296,18 @@ class TF(object):
if not allow_null: if not allow_null:
if val is None or (isinstance(val, str) and not val.strip()): if val is None or (isinstance(val, str) and not val.strip()):
msg.append( msg.append(
"Le champ '%s' doit être renseigné" % descr.get("title", field) f"Le champ '{descr.get('title', field)}' doit être renseigné"
) )
ok = 0 ok = 0
elif val == "" or val == None: elif val in ("", None):
continue # allowed empty field, skip continue # allowed empty field, skip
# type # type
typ = descr.get("type", "string") typ = descr.get("type", "text")
# Option pour striper les chaînes
if isinstance(val, str) and typ == "text" and descr.get("strip", False):
val = val.strip()
self.values[field] = val
if val != "" and val is not None: if val != "" and val is not None:
# check only non-null values # check only non-null values
if typ[:3] == "int": if typ[:3] == "int":
@ -311,24 +316,22 @@ class TF(object):
self.values[field] = val self.values[field] = val
except ValueError: except ValueError:
msg.append( msg.append(
"La valeur du champ '%s' doit être un nombre entier" % field f"La valeur du champ '{field}' doit être un nombre entier"
) )
ok = 0 ok = 0
elif typ == "float" or typ == "real": elif typ in ("float", "real"):
self.values[field] = self.values[field].replace(",", ".") self.values[field] = self.values[field].replace(",", ".")
try: try:
val = float(val.replace(",", ".")) # allow , val = float(val.replace(",", ".")) # allow ,
self.values[field] = val self.values[field] = val
except ValueError: except ValueError:
msg.append( msg.append(f"La valeur du champ {field}' doit être un nombre")
"La valeur du champ '%s' doit être un nombre" % field
)
ok = 0 ok = 0
if ( if (
ok ok
and (typ[:3] == "int" or typ == "float" or typ == "real") and (typ[:3] == "int" or typ == "float" or typ == "real")
and val != "" and val != ""
and val != None and val is not None
): ):
if "min_value" in descr and self.values[field] < descr["min_value"]: if "min_value" in descr and self.values[field] < descr["min_value"]:
msg.append( msg.append(
@ -343,12 +346,12 @@ class TF(object):
) )
ok = 0 ok = 0
if typ[:3] == "int": if typ[:3] == "int":
if not (scu.DB_MIN_INT <= self.values[field] <= scu.DB_MAX_INT): if not scu.DB_MIN_INT <= self.values[field] <= scu.DB_MAX_INT:
msg.append( msg.append(
f"Le champ '{field}' est a une valeur hors limite" f"Le champ '{field}' est a une valeur hors limite"
) )
ok = 0 ok = 0
elif typ == "float" or typ == "real": elif typ in ("float", "real"):
if not ( if not (
scu.DB_MIN_FLOAT <= self.values[field] <= scu.DB_MAX_FLOAT scu.DB_MIN_FLOAT <= self.values[field] <= scu.DB_MAX_FLOAT
): ):
@ -356,7 +359,7 @@ class TF(object):
f"Le champ '{field}' est a une valeur hors limite" f"Le champ '{field}' est a une valeur hors limite"
) )
ok = 0 ok = 0
if ok and (typ[:3] == "str") and "max_length" in descr: if ok and "max_length" in descr and isinstance(self.values[field], str):
if len(self.values[field]) > descr["max_length"]: if len(self.values[field]) > descr["max_length"]:
msg.append( msg.append(
"Le champ '%s' est trop long (max %d caractères)" "Le champ '%s' est trop long (max %d caractères)"

View File

@ -36,6 +36,7 @@ from flask import abort
from app import db, ScoDocJSONEncoder from app import db, ScoDocJSONEncoder
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.formations import edit_ue
from app.models import but_validations from app.models import but_validations
from app.models import BulAppreciations, Evaluation, Matiere, UniteEns from app.models import BulAppreciations, Evaluation, Matiere, UniteEns
from app.models.etudiants import Identite from app.models.etudiants import Identite
@ -44,7 +45,6 @@ from app.models.formsemestre import FormSemestre
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app.scodoc import sco_assiduites from app.scodoc import sco_assiduites
from app.scodoc import sco_edit_ue
from app.scodoc import sco_evaluations from app.scodoc import sco_evaluations
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups from app.scodoc import sco_groups
@ -491,11 +491,11 @@ def dict_decision_jury(
] ]
d["decision_ue"] = [] d["decision_ue"] = []
if decision[ if decision["decisions_ue"]:
"decisions_ue" # and sco_preferences.get_preference( 'bul_show_uevalid', formsemestre_id):
]: # and sco_preferences.get_preference( 'bul_show_uevalid', formsemestre_id): always publish (car utile pour export Apogee) # always publish (car utile pour export Apogee)
for ue_id in decision["decisions_ue"].keys(): for ue_id in decision["decisions_ue"].keys():
ue = sco_edit_ue.ue_list({"ue_id": ue_id})[0] ue = edit_ue.ue_list({"ue_id": ue_id})[0]
d["decision_ue"].append( d["decision_ue"].append(
dict( dict(
ue_id=ue["ue_id"], ue_id=ue["ue_id"],

View File

@ -46,6 +46,7 @@ from xml.etree.ElementTree import Element
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.formations import edit_ue
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app import log from app import log
@ -53,7 +54,6 @@ from app.but.bulletin_but_xml_compat import bulletin_but_xml_compat
from app.models import BulAppreciations, Evaluation, FormSemestre from app.models import BulAppreciations, Evaluation, FormSemestre
from app.scodoc import sco_assiduites from app.scodoc import sco_assiduites
from app.scodoc import codes_cursus from app.scodoc import codes_cursus
from app.scodoc import sco_edit_ue
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_photos from app.scodoc import sco_photos
@ -393,7 +393,7 @@ def make_xml_formsemestre_bulletinetud(
"decisions_ue" "decisions_ue"
]: # and sco_preferences.get_preference( 'bul_show_uevalid', formsemestre_id): always publish (car utile pour export Apogee) ]: # and sco_preferences.get_preference( 'bul_show_uevalid', formsemestre_id): always publish (car utile pour export Apogee)
for ue_id in decision["decisions_ue"].keys(): for ue_id in decision["decisions_ue"].keys():
ue = sco_edit_ue.ue_list({"ue_id": ue_id})[0] ue = edit_ue.ue_list({"ue_id": ue_id})[0]
doc.append( doc.append(
Element( Element(
"decision_ue", "decision_ue",

View File

@ -62,7 +62,6 @@ from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_vdi import ApoEtapeVDI from app.scodoc.sco_vdi import ApoEtapeVDI
from app.scodoc import codes_cursus from app.scodoc import codes_cursus
from app.scodoc import sco_edit_module
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups_copy from app.scodoc import sco_groups_copy
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
@ -939,7 +938,7 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
"formsemestre_id": formsemestre_id, "formsemestre_id": formsemestre_id,
"responsable_id": tf[2][f"MI{module_id}"], "responsable_id": tf[2][f"MI{module_id}"],
} }
_ = sco_moduleimpl.do_moduleimpl_create(modargs) _ = ModuleImpl.create_from_dict(modargs)
else: else:
# Modification du semestre: # Modification du semestre:
# on doit creer les modules nouvellement selectionnés # on doit creer les modules nouvellement selectionnés
@ -971,27 +970,23 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
"formsemestre_id": formsemestre.id, "formsemestre_id": formsemestre.id,
"responsable_id": tf[2]["MI" + str(module_id)], "responsable_id": tf[2]["MI" + str(module_id)],
} }
moduleimpl_id = sco_moduleimpl.do_moduleimpl_create(modargs) modimpl = ModuleImpl.create_from_dict(modargs)
mod = sco_edit_module.module_list({"module_id": module_id})[0] assert modimpl.module_id == module_id
msg += [ mod = modimpl.module
"création de %s (%s)" % (mod["code"] or "?", mod["titre"] or "?") msg += [f"""création de {mod.code or "?"} ({mod.titre or "?"})"""]
]
# INSCRIPTIONS DES ETUDIANTS # INSCRIPTIONS DES ETUDIANTS
log( group_id = tf[2][f"{module_id}!group_id"]
'inscription module: %s = "%s"' log(f"""inscription module: {module_id}!group_id = '{group_id}'""")
% ("%s!group_id" % module_id, tf[2]["%s!group_id" % module_id])
)
group_id = tf[2]["%s!group_id" % module_id]
if group_id: if group_id:
etudids = [ etudids = [
x["etudid"] for x in sco_groups.get_group_members(group_id) x["etudid"] for x in sco_groups.get_group_members(group_id)
] ]
log( log(
"inscription module:module_id=%s,moduleimpl_id=%s: %s" "inscription module:module_id=%s,moduleimpl_id=%s: %s"
% (module_id, moduleimpl_id, etudids) % (module_id, modimpl.id, etudids)
) )
sco_moduleimpl.do_moduleimpl_inscrit_etuds( sco_moduleimpl.do_moduleimpl_inscrit_etuds(
moduleimpl_id, modimpl.id,
formsemestre.id, formsemestre.id,
etudids, etudids,
) )
@ -1002,7 +997,7 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
else: else:
log( log(
"inscription module:module_id=%s,moduleimpl_id=%s: aucun etudiant inscrit" "inscription module:module_id=%s,moduleimpl_id=%s: aucun etudiant inscrit"
% (module_id, moduleimpl_id) % (module_id, modimpl.id)
) )
# #
ok, diag = formsemestre_delete_moduleimpls( ok, diag = formsemestre_delete_moduleimpls(
@ -1022,7 +1017,6 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
sco_moduleimpl.do_moduleimpl_edit( sco_moduleimpl.do_moduleimpl_edit(
modargs, formsemestre_id=formsemestre.id modargs, formsemestre_id=formsemestre.id
) )
mod = sco_edit_module.module_list({"module_id": module_id})[0]
# --- Association des parcours # --- Association des parcours
if formsemestre is None: if formsemestre is None:
formsemestre = FormSemestre.get_formsemestre(formsemestre_id) formsemestre = FormSemestre.get_formsemestre(formsemestre_id)

View File

@ -64,7 +64,6 @@ from app.scodoc import sco_assiduites as scass
from app.scodoc import sco_bulletins from app.scodoc import sco_bulletins
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc import sco_evaluations from app.scodoc import sco_evaluations
from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups from app.scodoc import sco_groups
@ -515,10 +514,9 @@ def formsemestre_page_title(formsemestre_id=None):
def fill_formsemestre(sem: dict): # XXX OBSOLETE def fill_formsemestre(sem: dict): # XXX OBSOLETE
"""Add some fields in formsemestres dicts""" """Add some fields in formsemestres dicts"""
formsemestre_id = sem["formsemestre_id"] formsemestre_id = sem["formsemestre_id"]
formation = Formation.get_formation(sem["formation_id"])
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0] sem["formation"] = formation.to_dict(with_departement=False)
sem["formation"] = F parcours = codes_cursus.get_cursus_from_code(formation.type_parcours)
parcours = codes_cursus.get_cursus_from_code(F["type_parcours"])
if sem["semestre_id"] != -1: if sem["semestre_id"] != -1:
sem["num_sem"] = f""", {parcours.SESSION_NAME} {sem["semestre_id"]}""" sem["num_sem"] = f""", {parcours.SESSION_NAME} {sem["semestre_id"]}"""
else: else:

View File

@ -790,10 +790,10 @@ def groups_table(
Moodle groupe(s) {groups_infos.groups_titles}</a> Moodle groupe(s) {groups_infos.groups_titles}</a>
</li> </li>
<li> <li>
<a class="stdlink" href="{{ <a class="stdlink" href="{
url_for('notes.export_groups_as_moodle_csv', url_for('scolar.export_groups_as_moodle_csv',
scodoc_dept=g.scodoc_dept, formsemestre_id=groups_infos.formsemestre_id) scodoc_dept=g.scodoc_dept, formsemestre_id=groups_infos.formsemestre_id)
}}"> }">
Fichier CSV pour Moodle (tous les groupes)</a> Fichier CSV pour Moodle (tous les groupes)</a>
<em>(voir le paramétrage pour modifier le format des fichiers Moodle exportés)</em> <em>(voir le paramétrage pour modifier le format des fichiers Moodle exportés)</em>
</li>""", </li>""",

View File

@ -51,18 +51,6 @@ _moduleimplEditor = ndb.EditableTable(
) )
def do_moduleimpl_create(args):
"create a moduleimpl"
# TODO remplacer par une methode de ModuleImpl qui appelle
# super().create_from_dict() puis invalide le formsemestre
cnx = ndb.GetDBConnexion()
r = _moduleimplEditor.create(cnx, args)
sco_cache.invalidate_formsemestre(
formsemestre_id=args["formsemestre_id"]
) # > creation moduleimpl
return r
def do_moduleimpl_delete(oid, formsemestre_id=None): def do_moduleimpl_delete(oid, formsemestre_id=None):
"delete moduleimpl (desinscrit tous les etudiants)" "delete moduleimpl (desinscrit tous les etudiants)"
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()

View File

@ -48,11 +48,10 @@ from wtforms import (
HiddenField, HiddenField,
SelectMultipleField, SelectMultipleField,
) )
from app.models import Evaluation, ModuleImpl from app.models import Evaluation, Module, ModuleImpl
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_edit_module
from app.scodoc import sco_evaluations from app.scodoc import sco_evaluations
from app.scodoc import sco_excel from app.scodoc import sco_excel
from app.scodoc.sco_excel import ScoExcelBook, COLORS from app.scodoc.sco_excel import ScoExcelBook, COLORS
@ -243,9 +242,9 @@ class PlacementRunner:
self.moduleimpl_data = sco_moduleimpl.moduleimpl_list( self.moduleimpl_data = sco_moduleimpl.moduleimpl_list(
moduleimpl_id=self.moduleimpl_id moduleimpl_id=self.moduleimpl_id
)[0] )[0]
self.module_data = sco_edit_module.module_list( self.module_data = Module.get_module(
args={"module_id": self.moduleimpl_data["module_id"]} self.moduleimpl_data["module_id"]
)[0] ).to_dict()
self.sem = sco_formsemestre.get_formsemestre( self.sem = sco_formsemestre.get_formsemestre(
self.moduleimpl_data["formsemestre_id"] self.moduleimpl_data["formsemestre_id"]
) )

View File

@ -30,17 +30,16 @@
Implementation expérimentale (Jul. 2016) pour grouper les modules sur Implementation expérimentale (Jul. 2016) pour grouper les modules sur
les avis de poursuites d'études. les avis de poursuites d'études.
TODO: réécrire avec SQLAlchemy.
Pour l'UI, voir https://goodies.pixabay.com/jquery/tag-editor/demo.html Pour l'UI, voir https://goodies.pixabay.com/jquery/tag-editor/demo.html
""" """
import http
import re import re
from flask import g from flask import g
from app import db, log from app import db
from app.models import Formation, NotesTag from app.models import Formation, NotesTag
from app.scodoc import sco_edit_module
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
@ -60,7 +59,7 @@ from app.scodoc.sco_exceptions import ScoValueError
# NOTA: ancien code, n'utile pas de modèles SQLAlchemy # NOTA: ancien code, n'utile pas de modèles SQLAlchemy
class ScoTag(object): class ScoTag:
"""Generic tags for ScoDoc""" """Generic tags for ScoDoc"""
# must be overloaded: # must be overloaded:
@ -208,8 +207,6 @@ class ModuleTag(ScoTag):
# API # API
# TODO placer dans la vraie API et ne plus utiliser sco_publish # TODO placer dans la vraie API et ne plus utiliser sco_publish
def module_tag_search(term: str | int): def module_tag_search(term: str | int):
"""List all used tag names (for auto-completion)""" """List all used tag names (for auto-completion)"""
@ -230,60 +227,6 @@ def module_tag_search(term: str | int):
return scu.sendJSON(data) return scu.sendJSON(data)
def module_tag_list(module_id="") -> list[str]:
"""les noms de tags associés à ce module"""
r = ndb.SimpleDictFetch(
"""SELECT t.title
FROM notes_modules_tags mt, notes_tags t
WHERE mt.tag_id = t.id
AND mt.module_id = %(module_id)s
""",
{"module_id": module_id},
)
return [x["title"] for x in r]
def module_tag_set(module_id="", taglist=None):
"""taglist may either be:
a string with tag names separated by commas ("un,deux")
or a list of strings (["un", "deux"])
Remplace les tags existants
"""
if not taglist:
taglist = []
elif isinstance(taglist, str):
taglist = taglist.split(",")
taglist = [t.strip() for t in taglist]
log(f"module_tag_set: module_id={module_id} taglist={taglist}")
# Check tags syntax
for tag in taglist:
if not ScoTag.check_tag_title(tag):
log(f"module_tag_set({module_id}): invalid tag title")
return scu.json_error(404, "invalid tag")
# TODO code à moderniser (+ revoir classe ScoTag, utiliser modèle)
# TODO Voir ItemSuiviTag et api etud_suivi
# Sanity check:
mod_dict = sco_edit_module.module_list(args={"module_id": module_id})
if not mod_dict:
raise ScoValueError("invalid module !")
newtags = set(taglist)
oldtags = set(module_tag_list(module_id))
to_del = oldtags - newtags
to_add = newtags - oldtags
# should be atomic, but it's not.
for tagname in to_add:
t = ModuleTag(tagname, object_id=module_id)
for tagname in to_del:
t = ModuleTag(tagname)
t.remove_tag_from_object(module_id)
return "", http.HTTPStatus.NO_CONTENT
def split_tagname_coeff(tag: str, separateur=":") -> tuple[str, float]: def split_tagname_coeff(tag: str, separateur=":") -> tuple[str, float]:
"""Découpage d'un tag, tel que saisi par un utilisateur dans le programme, """Découpage d'un tag, tel que saisi par un utilisateur dans le programme,
pour en extraire : pour en extraire :

View File

@ -56,15 +56,13 @@ Solution proposée (nov 2014):
import flask import flask
from flask import flash, g, request, render_template, url_for from flask import flash, g, request, render_template, url_for
from flask_login import current_user from flask_login import current_user
from app.formations import edit_ue
from app.models.formsemestre import FormSemestre from app.models.formsemestre import FormSemestre
from app import db, log from app import db, log
from app.models import Evaluation, Identite, ModuleImpl, UniteEns from app.models import Evaluation, Identite, Matiere, Module, ModuleImpl, UniteEns
from app.scodoc import codes_cursus from app.scodoc import codes_cursus
from app.scodoc import sco_edit_matiere
from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
from app.scodoc import sco_saisie_notes from app.scodoc import sco_saisie_notes
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
@ -97,10 +95,8 @@ def external_ue_create(
# #
formation_id = formsemestre.formation.id formation_id = formsemestre.formation.id
numero = sco_edit_ue.next_ue_numero( numero = edit_ue.next_ue_numero(formation_id, semestre_id=formsemestre.semestre_id)
formation_id, semestre_id=formsemestre.semestre_id ue_id = edit_ue.do_ue_create(
)
ue_id = sco_edit_ue.do_ue_create(
{ {
"formation_id": formation_id, "formation_id": formation_id,
"semestre_idx": formsemestre.semestre_id, "semestre_idx": formsemestre.semestre_id,
@ -114,26 +110,28 @@ def external_ue_create(
) )
ue = db.session.get(UniteEns, ue_id) ue = db.session.get(UniteEns, ue_id)
flash(f"UE créée (code {ue.ue_code})") flash(f"UE créée (code {ue.ue_code})")
matiere_id = sco_edit_matiere.do_matiere_create( matiere = Matiere.create_from_dict(
{"ue_id": ue_id, "titre": titre or acronyme, "numero": 1} {"ue_id": ue_id, "titre": titre or acronyme, "numero": 1}
) )
module_id = sco_edit_module.do_module_create( module = Module.create_from_dict(
{ {
"titre": "UE extérieure", "titre": "UE extérieure",
"code": acronyme, "code": acronyme,
"coefficient": ects, # tous le coef. module est egal à la quantite d'ECTS "coefficient": ects, # tous le coef. module est egal à la quantite d'ECTS
"ue_id": ue_id, "ue_id": ue_id,
"matiere_id": matiere_id, "matiere_id": matiere.id,
"formation_id": formation_id, "formation_id": formation_id,
"semestre_id": formsemestre.semestre_id, "semestre_id": formsemestre.semestre_id,
"module_type": scu.ModuleType.STANDARD, "module_type": scu.ModuleType.STANDARD,
}, },
news=True,
inval_cache=True,
) )
moduleimpl_id = sco_moduleimpl.do_moduleimpl_create( modimpl = ModuleImpl.create_from_dict(
{ {
"module_id": module_id, "module_id": module.id,
"formsemestre_id": formsemestre_id, "formsemestre_id": formsemestre_id,
# affecte le 1er responsable du semestre comme resp. du module # affecte le 1er responsable du semestre comme resp. du module
"responsable_id": ( "responsable_id": (
@ -143,7 +141,6 @@ def external_ue_create(
), ),
}, },
) )
modimpl = db.session.get(ModuleImpl, moduleimpl_id)
assert modimpl assert modimpl
return modimpl return modimpl

View File

@ -1319,19 +1319,18 @@ def format_nomprenom(etud, reverse=False):
return " ".join([x for x in fs if x]) return " ".join([x for x in fs if x])
def format_nom(s, uppercase=True): def format_nom(s: str, uppercase=True) -> str:
"Formatte le nom" "Formatte le nom"
if not s: if not s:
return "" return ""
if uppercase: if uppercase:
return s.upper() return (s.upper()).strip()
else:
return format_prenom(s) return format_prenom(s)
def format_prenom(s): def format_prenom(s: str) -> str:
"""Formatte prenom etudiant pour affichage """Formatte prenom etudiant pour affichage
DEPRECATED: utiliser Identite.prenom_str Pour les étudiants, utiliser Identite.prenom_str
""" """
if not s: if not s:
return "" return ""
@ -1340,7 +1339,7 @@ def format_prenom(s):
for frag in frags: for frag in frags:
fs = frag.split("-") fs = frag.split("-")
r.append("-".join([x.lower().capitalize() for x in fs])) r.append("-".join([x.lower().capitalize() for x in fs]))
return " ".join(r) return (" ".join(r)).strip()
def format_telephone(n: str | None) -> str: def format_telephone(n: str | None) -> str:

View File

@ -78,8 +78,14 @@ from app.decorators import (
permission_required_compat_scodoc7, permission_required_compat_scodoc7,
) )
from app.formations import (
# --------------- edit_formation,
edit_matiere,
edit_module,
edit_ue,
formation_io,
formation_versions,
)
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import ( from app.scodoc.sco_exceptions import (
@ -97,10 +103,6 @@ from app.scodoc import (
sco_cost_formation, sco_cost_formation,
sco_debouche, sco_debouche,
sco_edit_apc, sco_edit_apc,
sco_edit_formation,
sco_edit_matiere,
sco_edit_module,
sco_edit_ue,
sco_etape_apogee_view, sco_etape_apogee_view,
sco_etud, sco_etud,
sco_evaluations, sco_evaluations,
@ -109,9 +111,6 @@ from app.scodoc import (
sco_evaluation_edit, sco_evaluation_edit,
sco_evaluation_recap, sco_evaluation_recap,
sco_export_results, sco_export_results,
sco_formations,
sco_formation_recap,
sco_formation_versions,
sco_formsemestre, sco_formsemestre,
sco_formsemestre_custommenu, sco_formsemestre_custommenu,
sco_formsemestre_edit, sco_formsemestre_edit,
@ -142,6 +141,7 @@ from app.scodoc import (
sco_undo_notes, sco_undo_notes,
sco_users, sco_users,
) )
from app.formations import formation_recap
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.TrivialFormulator import TrivialFormulator
@ -192,7 +192,7 @@ sco_publish(
) )
sco_publish( sco_publish(
"/formsemestre_associate_new_version", "/formsemestre_associate_new_version",
sco_formation_versions.formsemestre_associate_new_version, formation_versions.formsemestre_associate_new_version,
Permission.EditFormation, Permission.EditFormation,
methods=["GET", "POST"], methods=["GET", "POST"],
) )
@ -240,19 +240,19 @@ sco_publish(
sco_publish( sco_publish(
"/formation_create", "/formation_create",
sco_edit_formation.formation_create, edit_formation.formation_create,
Permission.EditFormation, Permission.EditFormation,
methods=["GET", "POST"], methods=["GET", "POST"],
) )
sco_publish( sco_publish(
"/formation_delete", "/formation_delete",
sco_edit_formation.formation_delete, edit_formation.formation_delete,
Permission.EditFormation, Permission.EditFormation,
methods=["GET", "POST"], methods=["GET", "POST"],
) )
sco_publish( sco_publish(
"/formation_edit", "/formation_edit",
sco_edit_formation.formation_edit, edit_formation.formation_edit,
Permission.EditFormation, Permission.EditFormation,
methods=["GET", "POST"], methods=["GET", "POST"],
) )
@ -426,13 +426,13 @@ sco_publish(
) )
sco_publish( sco_publish(
"/ue_create", "/ue_create",
sco_edit_ue.ue_create, edit_ue.ue_create,
Permission.EditFormation, Permission.EditFormation,
methods=["GET", "POST"], methods=["GET", "POST"],
) )
sco_publish( sco_publish(
"/ue_delete", "/ue_delete",
sco_edit_ue.ue_delete, edit_ue.ue_delete,
Permission.EditFormation, Permission.EditFormation,
methods=["GET", "POST"], methods=["GET", "POST"],
) )
@ -443,7 +443,7 @@ sco_publish(
@permission_required(Permission.EditFormation) @permission_required(Permission.EditFormation)
def ue_edit(ue_id: int): def ue_edit(ue_id: int):
"Edition de l'UE" "Edition de l'UE"
return sco_edit_ue.ue_edit(ue_id) return edit_ue.ue_edit(ue_id)
@bp.route("/set_ue_niveau_competence", methods=["POST"]) @bp.route("/set_ue_niveau_competence", methods=["POST"])
@ -485,7 +485,7 @@ def get_ue_niveaux_options_html():
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@scodoc7func @scodoc7func
def ue_table(formation_id=None, semestre_idx=1, msg=""): def ue_table(formation_id=None, semestre_idx=1, msg=""):
return sco_edit_ue.ue_table( return edit_ue.ue_table(
formation_id=formation_id, semestre_idx=semestre_idx, msg=msg formation_id=formation_id, semestre_idx=semestre_idx, msg=msg
) )
@ -528,7 +528,7 @@ def ue_sharing_code():
ue_code = request.args.get("ue_code") ue_code = request.args.get("ue_code")
ue_id = request.args.get("ue_id") ue_id = request.args.get("ue_id")
hide_ue_id = request.args.get("hide_ue_id") hide_ue_id = request.args.get("hide_ue_id")
return sco_edit_ue.ue_sharing_code( return edit_ue.ue_sharing_code(
ue_code=ue_code, ue_code=ue_code,
ue_id=None if ((ue_id is None) or ue_id == "") else int(ue_id), ue_id=None if ((ue_id is None) or ue_id == "") else int(ue_id),
hide_ue_id=( hide_ue_id=(
@ -552,56 +552,56 @@ def formation_table_recap(formation_id: int):
"Tableau récap. de la formation" "Tableau récap. de la formation"
formation = Formation.get_formation(formation_id) formation = Formation.get_formation(formation_id)
fmt = request.args.get("fmt", "html") fmt = request.args.get("fmt", "html")
return sco_formation_recap.formation_table_recap(formation, fmt=fmt) return formation_recap.formation_table_recap(formation, fmt=fmt)
sco_publish( sco_publish(
"/export_recap_formations_annee_scolaire", "/export_recap_formations_annee_scolaire",
sco_formation_recap.export_recap_formations_annee_scolaire, formation_recap.export_recap_formations_annee_scolaire,
Permission.ScoView, Permission.ScoView,
) )
sco_publish( sco_publish(
"/formation_add_malus_modules", "/formation_add_malus_modules",
sco_edit_module.formation_add_malus_modules, edit_module.formation_add_malus_modules,
Permission.EditFormation, Permission.EditFormation,
) )
sco_publish( sco_publish(
"/matiere_create", "/matiere_create",
sco_edit_matiere.matiere_create, edit_matiere.matiere_create,
Permission.EditFormation, Permission.EditFormation,
methods=["GET", "POST"], methods=["GET", "POST"],
) )
sco_publish( sco_publish(
"/matiere_delete", "/matiere_delete",
sco_edit_matiere.matiere_delete, edit_matiere.matiere_delete,
Permission.EditFormation, Permission.EditFormation,
methods=["GET", "POST"], methods=["GET", "POST"],
) )
sco_publish( sco_publish(
"/matiere_edit", "/matiere_edit",
sco_edit_matiere.matiere_edit, edit_matiere.matiere_edit,
Permission.EditFormation, Permission.EditFormation,
methods=["GET", "POST"], methods=["GET", "POST"],
) )
sco_publish( sco_publish(
"/module_create", "/module_create",
sco_edit_module.module_create, edit_module.module_create,
Permission.EditFormation, Permission.EditFormation,
methods=["GET", "POST"], methods=["GET", "POST"],
) )
sco_publish( sco_publish(
"/module_delete", "/module_delete",
sco_edit_module.module_delete, edit_module.module_delete,
Permission.EditFormation, Permission.EditFormation,
methods=["GET", "POST"], methods=["GET", "POST"],
) )
sco_publish( sco_publish(
"/module_edit", "/module_edit",
sco_edit_module.module_edit, edit_module.module_edit,
Permission.EditFormation, Permission.EditFormation,
methods=["GET", "POST"], methods=["GET", "POST"],
) )
sco_publish("/module_list", sco_edit_module.module_table, Permission.ScoView) sco_publish("/module_list", edit_module.module_table, Permission.ScoView)
sco_publish("/module_tag_search", sco_tag_module.module_tag_search, Permission.ScoView) sco_publish("/module_tag_search", sco_tag_module.module_tag_search, Permission.ScoView)
@ -612,9 +612,7 @@ def formation_tag_modules_by_type(formation_id: int, semestre_idx: int):
"""Taggue tous les modules de la formation en fonction de leur type : 'res', 'sae', 'malus' """Taggue tous les modules de la formation en fonction de leur type : 'res', 'sae', 'malus'
Ne taggue pas les modules standards. Ne taggue pas les modules standards.
""" """
formation = Formation.query.filter_by( formation = Formation.get_formation(formation_id)
id=formation_id, dept_id=g.scodoc_dept_id
).first_or_404()
sco_tag_module.formation_tag_modules_by_type(formation) sco_tag_module.formation_tag_modules_by_type(formation)
flash("Formation tagguée") flash("Formation tagguée")
return flask.redirect( return flask.redirect(
@ -631,11 +629,12 @@ def formation_tag_modules_by_type(formation_id: int, semestre_idx: int):
@bp.route("/module_tag_set", methods=["POST"]) @bp.route("/module_tag_set", methods=["POST"])
@scodoc @scodoc
@permission_required(Permission.EditFormationTags) @permission_required(Permission.EditFormationTags)
def module_tag_set(): def module_tag_set(): # TODO passer dans l'API
"""Set tags on module""" """Set tags on module"""
module_id = int(request.form.get("module_id")) module_id = request.form.get("module_id")
module: Module = Module.get_instance(module_id)
taglist = request.form.get("taglist") taglist = request.form.get("taglist")
return sco_tag_module.module_tag_set(module_id, taglist) return module.set_tags(taglist)
@bp.route("/module_clone", methods=["POST"]) @bp.route("/module_clone", methods=["POST"])
@ -643,8 +642,8 @@ def module_tag_set():
@permission_required(Permission.EditFormation) @permission_required(Permission.EditFormation)
def module_clone(): def module_clone():
"""Clone existing module""" """Clone existing module"""
module_id = int(request.form.get("module_id")) module_id = request.form.get("module_id")
module = Module.query.get_or_404(module_id) module: Module = Module.get_instance(module_id)
module2 = module.clone() module2 = module.clone()
db.session.add(module2) db.session.add(module2)
db.session.commit() db.session.commit()
@ -670,7 +669,7 @@ def index_html():
detail = scu.to_bool(request.args.get("detail", False)) detail = scu.to_bool(request.args.get("detail", False))
editable = current_user.has_permission(Permission.EditFormation) editable = current_user.has_permission(Permission.EditFormation)
table = sco_formations.formation_list_table(detail=detail) table = formation_io.formation_list_table(detail=detail)
if fmt != "html": if fmt != "html":
return table.make_page(fmt=fmt, filename=f"Formations-{g.scodoc_dept}") return table.make_page(fmt=fmt, filename=f"Formations-{g.scodoc_dept}")
@ -747,7 +746,7 @@ def index_html():
@scodoc7func @scodoc7func
def formation_export(formation_id, export_ids=False, fmt=None, export_codes_apo=True): def formation_export(formation_id, export_ids=False, fmt=None, export_codes_apo=True):
"Export de la formation au format indiqué (xml ou json)" "Export de la formation au format indiqué (xml ou json)"
return sco_formations.formation_export( return formation_io.formation_export(
formation_id, formation_id,
export_ids=export_ids, export_ids=export_ids,
fmt=fmt, fmt=fmt,
@ -789,9 +788,7 @@ def formation_import_xml_form():
elif tf[0] == -1: elif tf[0] == -1:
return flask.redirect(url_for("notes.index_html", scodoc_dept=g.scodoc_dept)) return flask.redirect(url_for("notes.index_html", scodoc_dept=g.scodoc_dept))
else: else:
formation_id, _, _ = sco_formations.formation_import_xml( formation_id, _, _ = formation_io.formation_import_xml(tf[2]["xmlfile"].read())
tf[2]["xmlfile"].read()
)
return render_template( return render_template(
"sco_page_dept.j2", "sco_page_dept.j2",
@ -813,8 +810,8 @@ def formation_import_xml_form():
) )
sco_publish("/module_move", sco_edit_formation.module_move, Permission.EditFormation) sco_publish("/module_move", edit_formation.module_move, Permission.EditFormation)
sco_publish("/ue_move", sco_edit_formation.ue_move, Permission.EditFormation) sco_publish("/ue_move", edit_formation.ue_move, Permission.EditFormation)
@bp.route("/ue_clone", methods=["POST"]) @bp.route("/ue_clone", methods=["POST"])
@ -2578,21 +2575,21 @@ def check_sem_integrity(formsemestre_id, fix=False):
bad_sem = [] bad_sem = []
formations_set = set() # les formations mentionnées dans les UE et modules formations_set = set() # les formations mentionnées dans les UE et modules
for modimpl in modimpls: for modimpl in modimpls:
mod = sco_edit_module.module_list({"module_id": modimpl["module_id"]})[0] mod = Module.get_instance(modimpl["module_id"])
formations_set.add(mod["formation_id"]) formations_set.add(mod.formation_id)
ue = UniteEns.query.get_or_404(mod["ue_id"]) ue = mod.ue
ue_dict = ue.to_dict() ue_dict = ue.to_dict()
formations_set.add(ue_dict["formation_id"]) formations_set.add(ue_dict["formation_id"])
if ue_dict["formation_id"] != mod["formation_id"]: if ue_dict["formation_id"] != mod.formation_id:
modimpl["mod"] = mod modimpl["mod"] = mod.to_dict()
modimpl["ue"] = ue_dict modimpl["ue"] = ue_dict
bad_ue.append(modimpl) bad_ue.append(modimpl)
if sem["formation_id"] != mod["formation_id"]: if sem["formation_id"] != mod.formation_id:
bad_sem.append(modimpl) bad_sem.append(modimpl)
modimpl["mod"] = mod modimpl["mod"] = mod.to_dict()
H = [ H = [
"<p>formation_id=%s" % sem["formation_id"], f"""<p>formation_id={sem["formation_id"]}""",
] ]
if bad_ue: if bad_ue:
H += [ H += [

View File

@ -41,6 +41,7 @@ from app.decorators import (
scodoc, scodoc,
permission_required, permission_required,
) )
from app.formations import formation_io, formation_versions
from app.forms.formsemestre import ( from app.forms.formsemestre import (
change_formation, change_formation,
edit_modimpls_codes_apo, edit_modimpls_codes_apo,
@ -55,8 +56,6 @@ from app.models import (
) )
from app.scodoc import ( from app.scodoc import (
sco_edt_cal, sco_edt_cal,
sco_formations,
sco_formation_versions,
sco_groups_view, sco_groups_view,
) )
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
@ -78,7 +77,7 @@ def formsemestre_change_formation(formsemestre_id: int):
existant. existant.
""" """
formsemestre = FormSemestre.get_formsemestre(formsemestre_id) formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
formation_dict = sco_formations.formation_export_dict( formation_dict = formation_io.formation_export_dict(
formsemestre.formation, export_external_ues=True, ue_reference_style="acronyme" formsemestre.formation, export_external_ues=True, ue_reference_style="acronyme"
) )
formations = [ formations = [
@ -87,7 +86,7 @@ def formsemestre_change_formation(formsemestre_id: int):
dept_id=formsemestre.dept_id, acronyme=formsemestre.formation.acronyme dept_id=formsemestre.dept_id, acronyme=formsemestre.formation.acronyme
) )
if formation.id != formsemestre.formation.id if formation.id != formsemestre.formation.id
and sco_formation_versions.formations_are_equals( and formation_versions.formations_are_equals(
formation, formation2_dict=formation_dict formation, formation2_dict=formation_dict
) )
] ]
@ -108,7 +107,7 @@ def formsemestre_change_formation(formsemestre_id: int):
new_formation: Formation = Formation.query.filter_by( new_formation: Formation = Formation.query.filter_by(
dept_id=g.scodoc_dept_id, formation_id=new_formation_id dept_id=g.scodoc_dept_id, formation_id=new_formation_id
).first_or_404() ).first_or_404()
sco_formation_versions.formsemestre_change_formation( formation_versions.formsemestre_change_formation(
formsemestre, new_formation formsemestre, new_formation
) )
flash("Formation du semestre modifiée") flash("Formation du semestre modifiée")

View File

@ -86,7 +86,6 @@ from app.scodoc import (
sco_archives_etud, sco_archives_etud,
sco_bug_report, sco_bug_report,
sco_cache, sco_cache,
sco_debouche,
sco_dept, sco_dept,
sco_dump_db, sco_dump_db,
sco_etud, sco_etud,

View File

@ -307,6 +307,7 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
"size": 20, "size": 20,
"allow_null": False, "allow_null": False,
"readonly": edit_only_roles, "readonly": edit_only_roles,
"strip": True,
}, },
), ),
( (
@ -316,6 +317,7 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
"size": 20, "size": 20,
"allow_null": False, "allow_null": False,
"readonly": edit_only_roles, "readonly": edit_only_roles,
"strip": True,
}, },
), ),
] ]
@ -395,6 +397,7 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
"size": 36, "size": 36,
"allow_null": False, "allow_null": False,
"readonly": edit_only_roles, "readonly": edit_only_roles,
"strip": True,
}, },
), ),
( (
@ -455,6 +458,7 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
else "" else ""
), ),
"size": 36, "size": 36,
"strip": True,
"allow_null": not require_email_institutionnel, "allow_null": not require_email_institutionnel,
"readonly": edit_only_roles, "readonly": edit_only_roles,
}, },
@ -809,11 +813,13 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
db.session.add(the_user) db.session.add(the_user)
db.session.commit() db.session.commit()
# envoi éventuel d'un message # envoi éventuel d'un message
if mode == Mode.WELCOME_AND_CHANGE_PASSWORD or mode == Mode.WELCOME_ONLY: if mode in (Mode.WELCOME_AND_CHANGE_PASSWORD, Mode.WELCOME_ONLY):
if mode == Mode.WELCOME_AND_CHANGE_PASSWORD: token = (
token = the_user.get_reset_password_token() the_user.get_reset_password_token()
else: if mode == Mode.WELCOME_AND_CHANGE_PASSWORD
token = None else None
)
cas_force = ScoDocSiteConfig.get("cas_force") cas_force = ScoDocSiteConfig.get("cas_force")
# Le from doit utiliser la préférence du département de l'utilisateur # Le from doit utiliser la préférence du département de l'utilisateur
email.send_email( email.send_email(
@ -1008,7 +1014,7 @@ def get_user_list_xml(dept=None, start="", limit=25):
userlist = [ userlist = [
user user
for user in userlist for user in userlist
if scu.suppress_accents((user.nom or "").lower()).startswith(start) if scu.suppress_accents((user.nom or "").strip().lower()).startswith(start)
] ]
doc = ElementTree.Element("results") doc = ElementTree.Element("results")
for user in userlist[:limit]: for user in userlist[:limit]:

View File

@ -1,7 +1,9 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.7.27" "Infos sur version ScoDoc"
SCOVERSION = "9.7.28"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"

View File

@ -117,7 +117,7 @@ def GET(
print("reply", reply.text) print("reply", reply.text)
raise APIError( raise APIError(
errmsg or f"""erreur get {url} !""", errmsg or f"""erreur get {url} !""",
reply.json(), reply if reply.status_code == 404 else reply.json(),
status_code=reply.status_code, status_code=reply.status_code,
) )
if raw: if raw:
@ -220,6 +220,7 @@ def check_failure_get(path: str, headers: dict, err: str = None):
# ^ Renvoi un 404 # ^ Renvoi un 404
except APIError as api_err: except APIError as api_err:
if err is not None: if err is not None:
if "message" in api_err.payload:
assert api_err.payload["message"] == err assert api_err.payload["message"] == err
else: else:
raise APIError("Le GET n'aurait pas du fonctionner") raise APIError("Le GET n'aurait pas du fonctionner")

View File

@ -177,7 +177,7 @@ def test_formation_export(api_headers):
assert isinstance(module["heures_td"], float) assert isinstance(module["heures_td"], float)
assert isinstance(module["heures_tp"], float) assert isinstance(module["heures_tp"], float)
assert isinstance(module["coefficient"], float) assert isinstance(module["coefficient"], float)
assert isinstance(module["ects"], str) assert isinstance(module["ects"], str) if "ects" in module else True
assert isinstance(module["semestre_id"], int) assert isinstance(module["semestre_id"], int)
assert isinstance(module["numero"], int) assert isinstance(module["numero"], int)
assert isinstance(module["code_apogee"], str) assert isinstance(module["code_apogee"], str)

View File

@ -1,5 +1,6 @@
"""Utilitaires pour les tests de l'API """Utilitaires pour les tests de l'API
""" """
import json import json
@ -37,11 +38,11 @@ def verify_occurences_ids_etuds(json_response) -> bool:
DEPARTEMENT_FIELDS = [ DEPARTEMENT_FIELDS = [
"id",
"acronym", "acronym",
"description",
"visible",
"date_creation", "date_creation",
"description",
"id",
"visible",
] ]
# Champs "données personnelles" # Champs "données personnelles"
@ -67,17 +68,17 @@ ETUD_FIELDS = {
FORMATION_FIELDS = { FORMATION_FIELDS = {
"dept_id",
"acronyme", "acronyme",
"titre_officiel",
"formation_code",
"code_specialite", "code_specialite",
"id", "dept_id",
"titre", "formation_code",
"version",
"type_parcours",
"referentiel_competence_id",
"formation_id", "formation_id",
"id",
"referentiel_competence_id",
"titre_officiel",
"titre",
"type_parcours",
"version",
} }
FORMATION_EXPORT_FIELDS = { FORMATION_EXPORT_FIELDS = {
@ -95,39 +96,42 @@ FORMATION_EXPORT_FIELDS = {
FORMATION_EXPORT_UE_FIELDS = { FORMATION_EXPORT_UE_FIELDS = {
"acronyme", "acronyme",
"code_apogee",
"coefficient",
"color",
"ects",
"is_external",
"matiere",
"numero", "numero",
"reference",
"semestre_idx",
"titre", "titre",
"type", "type",
"ue_code", "ue_code",
"ects",
"is_external",
"code_apogee",
"coefficient",
"semestre_idx",
"color",
"reference",
"matiere",
} }
FORMATION_EXPORT_UE_MATIERE_FIELDS = { FORMATION_EXPORT_UE_MATIERE_FIELDS = {
"titre",
"numero",
"module", "module",
"numero",
"titre",
} }
FORMATION_EXPORT_UE_MATIERE_MODULE_FIELDS = { FORMATION_EXPORT_UE_MATIERE_MODULE_FIELDS = {
"titre",
"abbrev", "abbrev",
"app_critiques",
"code_apogee",
"code", "code",
"coefficient",
"coefficients",
"edt_id",
"heures_cours", "heures_cours",
"heures_td", "heures_td",
"coefficient", "heures_tp",
"ects",
"semestre_id",
"numero",
"code_apogee",
"module_type", "module_type",
"coefficients", "numero",
"parcours",
"semestre_id",
"titre",
} }
FORMATION_EXPORT_UE_MATIERE_MODULE_COEF_FIELDS = { FORMATION_EXPORT_UE_MATIERE_MODULE_COEF_FIELDS = {
@ -136,42 +140,42 @@ FORMATION_EXPORT_UE_MATIERE_MODULE_COEF_FIELDS = {
} }
FORMSEMESTRE_FIELDS = [ FORMSEMESTRE_FIELDS = [
"titre", "block_moyenne_generale",
"gestion_semestrielle", "block_moyennes",
"scodoc7_id",
"date_debut",
"bul_bgcolor", "bul_bgcolor",
"bul_hide_xml",
"date_debut_iso",
"date_debut",
"date_fin_iso",
"date_fin", "date_fin",
"resp_can_edit", "departement",
"dept_id", "dept_id",
"elt_annee_apo",
"elt_sem_apo",
"ens_can_edit_eval",
"etape_apo",
"etat", "etat",
"resp_can_change_ens", "formation_id",
"formation",
"formsemestre_id",
"gestion_compensation",
"gestion_semestrielle",
"id", "id",
"modalite", "modalite",
"ens_can_edit_eval",
"formation_id",
"gestion_compensation",
"elt_sem_apo",
"semestre_id",
"bul_hide_xml",
"elt_annee_apo",
"block_moyenne_generale",
"formsemestre_id",
"titre_num",
"titre_formation",
"date_debut_iso",
"date_fin_iso",
"responsables",
"parcours", "parcours",
"departement", "resp_can_change_ens",
"formation", "resp_can_edit",
"etape_apo", "responsables",
"block_moyennes", "scodoc7_id",
"semestre_id",
"titre_formation",
"titre_num",
"titre",
] ]
FSEM_FIELDS = { FSEM_FIELDS = {
"block_moyennes",
"block_moyenne_generale", "block_moyenne_generale",
"block_moyennes",
"bul_bgcolor", "bul_bgcolor",
"bul_hide_xml", "bul_hide_xml",
"date_debut_iso", "date_debut_iso",
@ -198,123 +202,123 @@ FSEM_FIELDS = {
} }
MODIMPL_FIELDS = { MODIMPL_FIELDS = {
"id",
"formsemestre_id",
"computation_expr", "computation_expr",
"module_id",
"responsable_id",
"moduleimpl_id",
"ens", "ens",
"formsemestre_id",
"id",
"module_id",
"module", "module",
"moduleimpl_id",
"responsable_id",
} }
MODULE_FIELDS = { MODULE_FIELDS = {
"heures_tp",
"code_apogee",
"titre",
"coefficient",
"module_type",
"id",
"ects",
"abbrev", "abbrev",
"ue_id", "code_apogee",
"code", "code",
"coefficient",
"ects",
"formation_id", "formation_id",
"heures_cours", "heures_cours",
"matiere_id",
"heures_td", "heures_td",
"semestre_id", "heures_tp",
"numero", "id",
"matiere_id",
"module_id", "module_id",
"module_type",
"numero",
"semestre_id",
"titre",
"ue_id",
} }
UE_FIELDS = { UE_FIELDS = {
"semestre_idx",
"type",
"formation_id",
"ue_code",
"id",
"ects",
"acronyme", "acronyme",
"is_external",
"numero",
"code_apogee", "code_apogee",
"titre",
"coefficient", "coefficient",
"color", "color",
"ects",
"formation_id",
"id",
"is_external",
"numero",
"semestre_idx",
"titre",
"type",
"ue_code",
"ue_id", "ue_id",
} }
BULLETIN_FIELDS = { BULLETIN_FIELDS = {
"version",
"type",
"date", "date",
"publie", "etat_inscription",
"etudiant", "etudiant",
"formation", "formation",
"formsemestre_id", "formsemestre_id",
"etat_inscription",
"options", "options",
"publie",
"ressources", "ressources",
"saes", "saes",
"ues",
"semestre", "semestre",
"type",
"ues",
"version",
} }
BULLETIN_ETUDIANT_FIELDS = { BULLETIN_ETUDIANT_FIELDS = {
"boursier",
"civilite", "civilite",
"code_ine", "code_ine",
"code_nip", "code_nip",
"codepostaldomicile",
"date_naissance", "date_naissance",
"dept_id",
"dept_acronym", "dept_acronym",
"dept_id",
"dept_naissance",
"description",
"domicile",
"email", "email",
"emailperso", "emailperso",
"etudid", "etudid",
"nom",
"prenom",
"nomprenom",
"lieu_naissance",
"dept_naissance",
"nationalite",
"boursier",
"fiche_url",
"photo_url",
"id",
"domicile",
"villedomicile",
"telephone",
"fax", "fax",
"description", "fiche_url",
"codepostaldomicile", "id",
"lieu_naissance",
"nationalite",
"nom",
"nomprenom",
"paysdomicile", "paysdomicile",
"photo_url",
"prenom",
"telephone",
"telephonemobile", "telephonemobile",
"typeadresse", "typeadresse",
"villedomicile",
} }
BULLETIN_FORMATION_FIELDS = {"id", "acronyme", "titre_officiel", "titre"} BULLETIN_FORMATION_FIELDS = {"id", "acronyme", "titre_officiel", "titre"}
BULLETIN_OPTIONS_FIELDS = { BULLETIN_OPTIONS_FIELDS = {
"show_abs",
"show_abs_modules", "show_abs_modules",
"show_ects", "show_abs",
"show_codemodules", "show_codemodules",
"show_coef",
"show_date_inscr",
"show_ects",
"show_matieres", "show_matieres",
"show_rangs", "show_minmax_eval",
"show_ue_rangs", "show_minmax_mod",
"show_minmax",
"show_mod_rangs", "show_mod_rangs",
"show_moypromo", "show_moypromo",
"show_minmax", "show_rangs",
"show_minmax_mod",
"show_minmax_eval",
"show_coef",
"show_ue_cap_details",
"show_ue_cap_current",
"show_temporary", "show_temporary",
"temporary_txt", "show_ue_cap_current",
"show_ue_cap_details",
"show_ue_rangs",
"show_uevalid", "show_uevalid",
"show_date_inscr", "temporary_txt",
} }
BULLETIN_RESSOURCES_FIELDS = { BULLETIN_RESSOURCES_FIELDS = {
@ -346,23 +350,23 @@ BULLETIN_SAES_FIELDS = {
########### RESSOURCES ET SAES ########### ########### RESSOURCES ET SAES ###########
BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_FIELDS = { BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_FIELDS = {
"id",
"titre",
"code_apogee", "code_apogee",
"url",
"moyenne",
"evaluations", "evaluations",
"id",
"moyenne",
"titre",
"url",
} }
BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_FIELDS = { BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_FIELDS = {
"id", "coef",
"description",
"date", "date",
"description",
"heure_debut", "heure_debut",
"heure_fin", "heure_fin",
"coef", "id",
"poids",
"note", "note",
"poids",
"url", "url",
} }
@ -373,10 +377,10 @@ BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_POIDS_FIELDS = {
} }
BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_NOTE_FIELDS = { BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_NOTE_FIELDS = {
"value",
"min",
"max", "max",
"min",
"moy", "moy",
"value",
} }
@ -384,19 +388,19 @@ BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_NOTE_FIELDS = {
BULLETIN_UES_FIELDS = {"RT1.1", "RT2.1", "RT3.1"} BULLETIN_UES_FIELDS = {"RT1.1", "RT2.1", "RT3.1"}
BULLETIN_UES_UE_FIELDS = { BULLETIN_UES_UE_FIELDS = {
"id", "bonus",
"titre", "capitalise",
"numero",
"type",
"color", "color",
"competence", "competence",
"moyenne", "ECTS",
"bonus", "id",
"malus", "malus",
"capitalise", "moyenne",
"numero",
"ressources", "ressources",
"saes", "saes",
"ECTS", "titre",
"type",
} }
BULLETIN_UES_UE_MOYENNE_FIELDS = {"value", "min", "max", "moy", "rang", "total"} BULLETIN_UES_UE_MOYENNE_FIELDS = {"value", "min", "max", "moy", "rang", "total"}
@ -461,16 +465,16 @@ BULLETIN_UES_UE_ECTS_FIELDS = {"acquis", "total"}
########### SEMESTRE ########### ########### SEMESTRE ###########
BULLETIN_SEMESTRE_FIELDS = { BULLETIN_SEMESTRE_FIELDS = {
"etapes", "absences",
"annee_universitaire",
"date_debut", "date_debut",
"date_fin", "date_fin",
"annee_universitaire",
"numero",
"inscription",
"groupes",
"absences",
"ECTS", "ECTS",
"etapes",
"groupes",
"inscription",
"notes", "notes",
"numero",
"rang", "rang",
} }
@ -484,78 +488,78 @@ BULLETIN_SEMESTRE_RANG_FIELDS = {"value", "total"}
EVAL_FIELDS = { EVAL_FIELDS = {
"id", "coefficient",
"description",
"date_debut", "date_debut",
"date_fin", "date_fin",
"coefficient", "description",
"etat",
"evaluation_type", "evaluation_type",
"id",
"moduleimpl_id", "moduleimpl_id",
"nb_inscrits",
"nb_notes_abs",
"nb_notes_att",
"nb_notes_exc",
"nb_notes_manquantes",
"note_max", "note_max",
"numero", "numero",
"poids", "poids",
"publish_incomplete", "publish_incomplete",
"visibulletin",
"etat",
"nb_inscrits",
"nb_notes_manquantes",
"nb_notes_abs",
"nb_notes_att",
"nb_notes_exc",
"saisie_notes", "saisie_notes",
"visibulletin",
} }
SAISIE_NOTES_FIELDS = {"datetime_debut", "datetime_fin", "datetime_mediane"} SAISIE_NOTES_FIELDS = {"datetime_debut", "datetime_fin", "datetime_mediane"}
REF_COMP_FIELDS = { REF_COMP_FIELDS = {
"dept_id",
"annexe", "annexe",
"specialite", "competences",
"specialite_long", "dept_id",
"type_structure", "parcours",
"type_departement",
"type_titre",
"version_orebut",
"scodoc_date_loaded", "scodoc_date_loaded",
"scodoc_orig_filename", "scodoc_orig_filename",
"competences", "specialite_long",
"parcours", "specialite",
"type_departement",
"type_structure",
"type_titre",
"version_orebut",
} }
ABSENCES_FIELDS = { ABSENCES_FIELDS = {
"jour", "begin",
"matin", "description",
"end",
"estabs", "estabs",
"estjust", "estjust",
"description", "jour",
"begin", "matin",
"end",
} }
ABSENCES_GROUP_ETAT_FIELDS = {"etudid", "list_abs"} ABSENCES_GROUP_ETAT_FIELDS = {"etudid", "list_abs"}
FORMSEMESTRE_ETUD_FIELDS = { FORMSEMESTRE_ETUD_FIELDS = {
"id",
"code_nip",
"code_ine",
"nom",
"nom_usuel",
"prenom",
"civilite", "civilite",
"code_ine",
"code_nip",
"groups", "groups",
"id",
"nom_usuel",
"nom",
"prenom",
} }
FORMSEMESTRE_ETUS_GROUPS_FIELDS = { FORMSEMESTRE_ETUS_GROUPS_FIELDS = {
"partition_id",
"id",
"formsemestre_id",
"partition_name",
"numero",
"bul_show_rank", "bul_show_rank",
"show_in_lists", "formsemestre_id",
"group_id", "group_id",
"group_name", "group_name",
"id",
"numero",
"partition_id",
"partition_name",
"show_in_lists",
} }
EVALUATIONS_FIELDS = { EVALUATIONS_FIELDS = {
@ -573,107 +577,107 @@ EVALUATIONS_FIELDS = {
} }
NOTES_FIELDS = { NOTES_FIELDS = {
"etudid",
"evaluation_id",
"value",
"comment", "comment",
"date", "date",
"etudid",
"evaluation_id",
"uid", "uid",
"value",
} }
PARTITIONS_FIELDS = { PARTITIONS_FIELDS = {
"id",
"formsemestre_id",
"partition_name",
"numero",
"bul_show_rank", "bul_show_rank",
"formsemestre_id",
"id",
"numero",
"partition_name",
"show_in_lists", "show_in_lists",
} }
PARTITION_GROUPS_ETUD_FIELDS = { PARTITION_GROUPS_ETUD_FIELDS = {
"id", "civilite",
"code_ine",
"code_nip",
"dept_id", "dept_id",
"id",
"nom_usuel",
"nom", "nom",
"prenom", "prenom",
"nom_usuel",
"civilite",
"code_nip",
"code_ine",
} }
FORMSEMESTRE_BULLETINS_FIELDS = { FORMSEMESTRE_BULLETINS_FIELDS = {
"version",
"type",
"date", "date",
"publie", "etat_inscription",
"etudiant", "etudiant",
"formation", "formation",
"formsemestre_id", "formsemestre_id",
"etat_inscription",
"options", "options",
"publie",
"ressources", "ressources",
"saes", "saes",
"ues",
"semestre", "semestre",
"type",
"ues",
"version",
} }
FORMSEMESTRE_BULLETINS_ETU_FIELDS = { FORMSEMESTRE_BULLETINS_ETU_FIELDS = {
"boursier",
"civilite", "civilite",
"code_ine", "code_ine",
"code_nip", "code_nip",
"codepostaldomicile",
"date_naissance", "date_naissance",
"dept_id",
"dept_acronym", "dept_acronym",
"dept_id",
"dept_naissance",
"description",
"domicile",
"email", "email",
"emailperso", "emailperso",
"etudid", "etudid",
"nom", "fax",
"prenom",
"nomprenom",
"lieu_naissance",
"dept_naissance",
"nationalite",
"boursier",
"fiche_url", "fiche_url",
"photo_url",
"id", "id",
"codepostaldomicile", "lieu_naissance",
"nationalite",
"nom",
"nomprenom",
"paysdomicile", "paysdomicile",
"photo_url",
"prenom",
"telephone",
"telephonemobile", "telephonemobile",
"typeadresse", "typeadresse",
"domicile",
"villedomicile", "villedomicile",
"telephone",
"fax",
"description",
} }
FORMSEMESTRE_BULLETINS_FORMATION_FIELDS = { FORMSEMESTRE_BULLETINS_FORMATION_FIELDS = {
"id",
"acronyme", "acronyme",
"id",
"titre_officiel", "titre_officiel",
"titre", "titre",
} }
FORMSEMESTRE_BULLETINS_OPT_FIELDS = { FORMSEMESTRE_BULLETINS_OPT_FIELDS = {
"show_abs",
"show_abs_modules", "show_abs_modules",
"show_ects", "show_abs",
"show_codemodules", "show_codemodules",
"show_coef",
"show_date_inscr",
"show_ects",
"show_matieres", "show_matieres",
"show_rangs", "show_minmax_eval",
"show_ue_rangs", "show_minmax_mod",
"show_minmax",
"show_mod_rangs", "show_mod_rangs",
"show_moypromo", "show_moypromo",
"show_minmax", "show_rangs",
"show_minmax_mod",
"show_minmax_eval",
"show_coef",
"show_ue_cap_details",
"show_ue_cap_current",
"show_temporary", "show_temporary",
"temporary_txt", "show_ue_cap_current",
"show_ue_cap_details",
"show_ue_rangs",
"show_uevalid", "show_uevalid",
"show_date_inscr", "temporary_txt",
} }

View File

@ -12,8 +12,8 @@ Usage: pytest tests/scenarios/test_scenario1_formation.py
# code écrit par Fares Amer, mai 2021 et porté sur ScoDoc 8 en août 2021 # code écrit par Fares Amer, mai 2021 et porté sur ScoDoc 8 en août 2021
from tests.unit import sco_fake_gen from tests.unit import sco_fake_gen
from app.scodoc import sco_edit_module from app.formations import formation_io
from app.scodoc import sco_formations from app.models import Formation
@pytest.mark.skip # test obsolete @pytest.mark.skip # test obsolete
@ -32,7 +32,7 @@ def run_scenario1():
doc = f.read() doc = f.read()
# --- Création de la formation # --- Création de la formation
f = sco_formations.formation_import_xml(doc=doc) f = formation_io.formation_import_xml(doc=doc)
# --- Création des semestres # --- Création des semestres
formation_id = f[0] formation_id = f[0]
@ -53,11 +53,11 @@ def run_scenario1():
] ]
# --- Implémentation des modules # --- Implémentation des modules
modules = sco_edit_module.module_list({"formation_id": formation_id}) formation = Formation.get_formation(formation_id)
mods_imp = [] mods_imp = []
for mod in modules: for mod in formation.modules:
mi = G.create_moduleimpl( mi = G.create_moduleimpl(
module_id=mod["module_id"], module_id=mod.id,
formsemestre_id=sems[mod["semestre_id"] - 1]["formsemestre_id"], formsemestre_id=sems[mod.semestre_id - 1]["formsemestre_id"],
) )
mods_imp.append(mi) mods_imp.append(mi)

View File

@ -16,6 +16,7 @@ import typing
from app import db, log from app import db, log
from app.auth.models import User from app.auth.models import User
from app.formations import edit_ue
from app.models import ( from app.models import (
Departement, Departement,
Evaluation, Evaluation,
@ -23,16 +24,13 @@ from app.models import (
FormationModalite, FormationModalite,
Identite, Identite,
Matiere, Matiere,
Module,
ModuleImpl, ModuleImpl,
) )
from app.scodoc import codes_cursus from app.scodoc import codes_cursus
from app.scodoc import sco_edit_matiere
from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_formsemestre_validation from app.scodoc import sco_formsemestre_validation
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_saisie_notes from app.scodoc import sco_saisie_notes
from app.scodoc import sco_synchro_etuds from app.scodoc import sco_synchro_etuds
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
@ -198,20 +196,17 @@ class ScoFake(object):
return: ue_id return: ue_id
""" """
if numero is None: if numero is None:
numero = sco_edit_ue.next_ue_numero(formation_id, 0) numero = edit_ue.next_ue_numero(formation_id, 0)
oid = sco_edit_ue.do_ue_create(locals()) oid = edit_ue.do_ue_create(locals())
oids = sco_edit_ue.ue_list(args={"ue_id": oid}) oids = edit_ue.ue_list(args={"ue_id": oid})
if not oids: if not oids:
raise ScoValueError("ue not created !") raise ScoValueError("ue not created !")
return oid return oid
@logging_meth @logging_meth
def create_matiere(self, ue_id=None, titre=None, numero=0) -> int: def create_matiere(self, ue_id=None, titre=None, numero=0) -> int:
oid = sco_edit_matiere.do_matiere_create(locals()) mat = Matiere.create_from_dict(locals())
oids = sco_edit_matiere.matiere_list(args={"matiere_id": oid}) return mat.id
if not oids:
raise ScoValueError("matiere not created !")
return oid
@logging_meth @logging_meth
def create_module( def create_module(
@ -233,11 +228,8 @@ class ScoFake(object):
matiere = db.session.get(Matiere, matiere_id) matiere = db.session.get(Matiere, matiere_id)
ue_id = matiere.ue.id ue_id = matiere.ue.id
formation_id = matiere.ue.formation.id formation_id = matiere.ue.formation.id
oid = sco_edit_module.do_module_create(locals()) module = Module.create_from_dict(locals(), news=True, inval_cache=True)
oids = sco_edit_module.module_list(args={"module_id": oid}) return module.id
if not oids:
raise ScoValueError(f"module not created ! (oid={oid})")
return oid
@logging_meth @logging_meth
def create_formsemestre( def create_formsemestre(
@ -281,11 +273,8 @@ class ScoFake(object):
) -> int: ) -> int:
if not responsable_id: if not responsable_id:
responsable_id = self.default_user.id responsable_id = self.default_user.id
oid = sco_moduleimpl.do_moduleimpl_create(locals()) modimpl = ModuleImpl.create_from_dict(locals())
oids = sco_moduleimpl.moduleimpl_list(moduleimpl_id=oid) # API inconsistency return modimpl.id
if not oids:
raise ScoValueError("moduleimpl not created !")
return oid
@logging_meth @logging_meth
def inscrit_etudiant(self, formsemestre_id: int, etud: dict): def inscrit_etudiant(self, formsemestre_id: int, etud: dict):

View File

@ -34,10 +34,7 @@
# - moduleimpl_list # - moduleimpl_list
# - do_module_impl_with_module_list # - do_module_impl_with_module_list
# - do_formsemestre_delete # - do_formsemestre_delete
# - module_list # - Module.delete
# - do_module_delete
# - matiere_list
# - do_matiere_delete
# - ue_list # - ue_list
# - do_ue_delete # - do_ue_delete
# - do_formation_delete # - do_formation_delete
@ -48,13 +45,15 @@ import os
import pytest import pytest
from app import db from app import db
from app.models import Formation, ModuleImpl from app.formations import (
from app.scodoc import sco_edit_formation, sco_formsemestre edit_formation,
from app.scodoc import sco_edit_matiere edit_module,
from app.scodoc import sco_edit_module edit_ue,
from app.scodoc import sco_edit_ue formation_io,
)
from app.models import Formation, Matiere, Module, ModuleImpl
from app.scodoc import sco_formsemestre
from app.scodoc import sco_exceptions from app.scodoc import sco_exceptions
from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre_edit from app.scodoc import sco_formsemestre_edit
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
from app.views import notes from app.views import notes
@ -183,7 +182,7 @@ def test_formations(test_client):
) )
# --- Export de formation vers JSON # --- Export de formation vers JSON
exp = sco_formations.formation_export( exp = formation_io.formation_export(
formation_id=formation_id, fmt="json", export_ids=True formation_id=formation_id, fmt="json", export_ids=True
).get_data(as_text=True) ).get_data(as_text=True)
assert isinstance(exp, str) assert isinstance(exp, str)
@ -259,22 +258,23 @@ def test_formations(test_client):
formsemestre_id=sem2["formsemestre_id"] formsemestre_id=sem2["formsemestre_id"]
) )
li_module = sco_edit_module.module_list() li_module = Module.query.all()
assert len(li_module) == 4 assert len(li_module) == 4
# Suppression impossible car utilisé dans le semestre formsemestre_idt: # Suppression impossible car utilisé dans le semestre formsemestre_idt:
module3 = db.session.get(ModuleImpl, mi3).module module3 = db.session.get(ModuleImpl, mi3).module
with pytest.raises(sco_exceptions.ScoNonEmptyFormationObject): with pytest.raises(sco_exceptions.ScoNonEmptyFormationObject):
sco_edit_module.module_delete(module_id=module3.id) edit_module.module_delete(module_id=module3.id)
sco_formsemestre_edit.do_formsemestre_delete(formsemestre_idt) sco_formsemestre_edit.do_formsemestre_delete(formsemestre_idt)
li_module2_before = sco_edit_module.module_list() li_module2_before = Module.query.all()
sco_edit_module.do_module_delete(module3.id) module3.delete()
sco_edit_module.do_module_delete(module_id_t) module_t = db.session.get(Module, module_id_t)
module_t.delete()
# deuxieme methode de supression d'un module # deuxieme methode de supression d'un module
li_module2_after = sco_edit_module.module_list() li_module2_after = Module.query.all()
assert ( assert (
len(li_module2_after) == len(li_module2_before) - 2 len(li_module2_after) == len(li_module2_before) - 2
@ -284,21 +284,23 @@ def test_formations(test_client):
assert len(lim_sem2) == 0 # deuxieme vérification si le module s'est bien sup assert len(lim_sem2) == 0 # deuxieme vérification si le module s'est bien sup
li_mat = sco_edit_matiere.matiere_list() li_mat = Matiere.query.all()
assert len(li_mat) == 4 assert len(li_mat) == 4
sco_edit_matiere.do_matiere_delete(oid=matiere_id3) # on supprime la matiere assert matiere_id3 in [m.matiere_id for m in li_mat]
li_mat2 = sco_edit_matiere.matiere_list() matiere = db.session.get(Matiere, matiere_id3)
matiere.delete() # on supprime la matiere
li_mat2 = Matiere.query.all()
assert len(li_mat2) == 3 # verification de la suppression de la matiere assert len(li_mat2) == 3 # verification de la suppression de la matiere
li_ue = sco_edit_ue.ue_list() li_ue = edit_ue.ue_list()
assert len(li_ue) == 4 assert len(li_ue) == 4
sco_edit_ue.ue_delete(ue_id=uet_id, dialog_confirmed=True) edit_ue.ue_delete(ue_id=uet_id, dialog_confirmed=True)
li_ue2 = sco_edit_ue.ue_list() li_ue2 = edit_ue.ue_list()
assert len(li_ue2) == 3 # verification de la suppression de l'UE assert len(li_ue2) == 3 # verification de la suppression de l'UE
# --- Suppression d'une formation # --- Suppression d'une formation
sco_edit_formation.do_formation_delete(formation_id=formation_id2) edit_formation.do_formation_delete(formation_id=formation_id2)
formation = db.session.get(Formation, formation_id2) formation = db.session.get(Formation, formation_id2)
assert formation is None assert formation is None
@ -315,11 +317,11 @@ def test_import_formation(test_client, filename="formation-exemple-1.xml"):
doc = f.read() doc = f.read()
# --- Création de la formation # --- Création de la formation
f = sco_formations.formation_import_xml(doc) f = formation_io.formation_import_xml(doc)
assert len(f) == 3 # 3-uple assert len(f) == 3 # 3-uple
formation_id = f[0] formation_id = f[0]
# --- Vérification des UE # --- Vérification des UE
ues = sco_edit_ue.ue_list({"formation_id": formation_id}) ues = edit_ue.ue_list({"formation_id": formation_id})
assert len(ues) == 10 assert len(ues) == 10
assert all(not ue["is_external"] for ue in ues) # aucune UE externe dans le XML assert all(not ue["is_external"] for ue in ues) # aucune UE externe dans le XML
# --- Mise en place de 4 semestres # --- Mise en place de 4 semestres
@ -338,17 +340,15 @@ def test_import_formation(test_client, filename="formation-exemple-1.xml"):
) )
] ]
# et les modules # et les modules
modules = sco_edit_module.module_list({"formation_id": formation_id}) formation = Formation.get_formation(formation_id)
for mod in modules: for mod in formation.modules:
moduleimpl_id = G.create_moduleimpl( moduleimpl_id = G.create_moduleimpl(
module_id=mod["module_id"], module_id=mod.id,
formsemestre_id=formsemestre_ids[mod["semestre_id"] - 1], formsemestre_id=formsemestre_ids[mod.semestre_id - 1],
) )
mi = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] mi = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
assert mi["module_id"] == mod["module_id"] assert mi["module_id"] == mod.id
# --- Export formation en XML # --- Export formation en XML
doc1 = sco_formations.formation_export(formation_id, fmt="xml").get_data( doc1 = formation_io.formation_export(formation_id, fmt="xml").get_data(as_text=True)
as_text=True
)
assert isinstance(doc1, str) assert isinstance(doc1, str)

View File

@ -8,16 +8,15 @@ import pytest
import app import app
from app import db from app import db
from app.formations import edit_ue, formation_versions
from app.models import Formation, FormSemestre, FormSemestreDescription from app.models import Formation, FormSemestre, FormSemestreDescription
from app.scodoc import ( from app.scodoc import (
sco_archives_formsemestre, sco_archives_formsemestre,
sco_cost_formation, sco_cost_formation,
sco_debouche, sco_debouche,
sco_edit_ue,
sco_evaluations, sco_evaluations,
sco_evaluation_check_abs, sco_evaluation_check_abs,
sco_evaluation_recap, sco_evaluation_recap,
sco_formation_versions,
sco_formsemestre_edit, sco_formsemestre_edit,
sco_formsemestre_inscriptions, sco_formsemestre_inscriptions,
sco_formsemestre_status, sco_formsemestre_status,
@ -61,7 +60,7 @@ def test_formsemestres_associate_new_version(test_client):
assert {s.semestre_id for s in formsemestres} == {1} assert {s.semestre_id for s in formsemestres} == {1}
# Les rattache à une nouvelle version de la formation: # Les rattache à une nouvelle version de la formation:
formsemestre_ids = [s.id for s in formsemestres] formsemestre_ids = [s.id for s in formsemestres]
sco_formation_versions.do_formsemestres_associate_new_version( formation_versions.do_formsemestres_associate_new_version(
formation.id, formsemestre_ids formation.id, formsemestre_ids
) )
new_formation: Formation = Formation.query.filter_by( new_formation: Formation = Formation.query.filter_by(
@ -91,7 +90,7 @@ def test_formsemestre_misc_views(test_client):
# ----- MENU SEMESTRE # ----- MENU SEMESTRE
_ = sco_formsemestre_status.formsemestre_status(formsemestre_id=formsemestre.id) _ = sco_formsemestre_status.formsemestre_status(formsemestre_id=formsemestre.id)
_ = sco_edit_ue.ue_table(formsemestre.formation_id) _ = edit_ue.ue_table(formsemestre.formation_id)
_ = sco_formsemestre_edit.formsemestre_editwithmodules(formsemestre.id) _ = sco_formsemestre_edit.formsemestre_editwithmodules(formsemestre.id)
_ = sco_preferences.SemPreferences(formsemestre_id=formsemestre.id).edit() _ = sco_preferences.SemPreferences(formsemestre_id=formsemestre.id).edit()
_ = sco_formsemestre_edit.formsemestre_edit_options(formsemestre.id) _ = sco_formsemestre_edit.formsemestre_edit_options(formsemestre.id)
@ -123,7 +122,7 @@ def test_formsemestre_misc_views(test_client):
assert isinstance(ans, (str, Response)) # ici str assert isinstance(ans, (str, Response)) # ici str
# Juste la page dialogue avant opération:: # Juste la page dialogue avant opération::
ans = sco_formsemestre_edit.formsemestre_clone(formsemestre.id) ans = sco_formsemestre_edit.formsemestre_clone(formsemestre.id)
ans = sco_formation_versions.formsemestre_associate_new_version( ans = formation_versions.formsemestre_associate_new_version(
formsemestre.formation_id, formsemestre.id formsemestre.formation_id, formsemestre.id
) )
ans = sco_formsemestre_edit.formsemestre_delete(formsemestre.id) ans = sco_formsemestre_edit.formsemestre_delete(formsemestre.id)

View File

@ -50,7 +50,7 @@ from flask import g
from app import db from app import db
from app.auth.models import User from app.auth.models import User
from app.formations import formation_io
from app.models import ( from app.models import (
ApcParcours, ApcParcours,
DispenseUE, DispenseUE,
@ -63,7 +63,6 @@ from app.models import (
UniteEns, UniteEns,
) )
from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_saisie_notes from app.scodoc import sco_saisie_notes
@ -86,7 +85,7 @@ def setup_formation(formation_infos: dict) -> Formation:
doc = f.read() doc = f.read()
# --- Création de la formation # --- Création de la formation
formation_id, _, _ = sco_formations.formation_import_xml(doc) formation_id, _, _ = formation_io.formation_import_xml(doc)
formation: Formation = db.session.get(Formation, formation_id) formation: Formation = db.session.get(Formation, formation_id)
assert formation assert formation
return formation return formation

View File

@ -17,6 +17,7 @@ from app import db
from app.auth.models import Role, User from app.auth.models import Role, User
from app.but.import_refcomp import orebut_import_refcomp from app.but.import_refcomp import orebut_import_refcomp
from app import models from app import models
from app.formations import formation_io
from app.models import departements from app.models import departements
from app.models import ( from app.models import (
Absence, Absence,
@ -33,8 +34,6 @@ from app.models import (
) )
from app.scodoc import ( from app.scodoc import (
sco_cache, sco_cache,
sco_evaluation_db,
sco_formations,
sco_formsemestre_inscriptions, sco_formsemestre_inscriptions,
sco_groups, sco_groups,
) )
@ -68,7 +67,7 @@ def import_formation(dept_id: int) -> Formation:
with open(FORMATION_XML_FILENAME, encoding="utf-8") as f: with open(FORMATION_XML_FILENAME, encoding="utf-8") as f:
doc = f.read() doc = f.read()
# --- Création de la formation (import programme) # --- Création de la formation (import programme)
f = sco_formations.formation_import_xml(doc) f = formation_io.formation_import_xml(doc)
formation = db.session.get(Formation, f[0]) formation = db.session.get(Formation, f[0])
# --- Association ref. comp. # --- Association ref. comp.
with open(REFCOMP_FILENAME, encoding="utf-8") as f: with open(REFCOMP_FILENAME, encoding="utf-8") as f: