Compare commits

...

7 Commits

39 changed files with 953 additions and 1078 deletions

View File

@ -30,7 +30,7 @@ from app.models import (
Module,
UniteEns,
)
from app.scodoc import sco_formations
from app.formations import formation_io
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)
app.set_sco_dept(formation.departement.acronym)
try:
data = sco_formations.formation_export(formation_id, export_ids)
data = formation_io.formation_export(formation_id, export_ids)
except ValueError:
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
from app import db
from app.formations import edit_ue
from app.models import SHORT_STR_LEN
from app.models.formations import Formation
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 codes_cursus
from app.scodoc import sco_edit_ue
def formation_delete(formation_id=None, dialog_confirmed=False):
@ -123,7 +123,7 @@ def do_formation_delete(formation_id):
db.session.flush()
# Suppression des 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)

View File

@ -25,16 +25,14 @@
#
##############################################################################
"""Ajout/Modification/Supression matieres
(portage from DTML)
"""Ajout/Modification/Suppression matieres
"""
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.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
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
@ -44,59 +42,9 @@ from app.scodoc.sco_exceptions import (
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):
"""Creation d'une matiere"""
"""Formulaire création d'une matiere"""
ue: UniteEns = UniteEns.query.get_or_404(ue_id)
default_numero = max([mat.numero for mat in ue.matieres] or [9]) + 1
H = [
@ -153,8 +101,8 @@ associé.
if tf[0] == -1:
return flask.redirect(dest_url)
# check unicity
mats = matiere_list(args={"ue_id": ue_id, "titre": tf[2]["titre"]})
if mats:
nb_mats = Matiere.query.filter_by(ue_id=ue_id, titre=tf[2]["titre"]).count()
if nb_mats:
return render_template(
"sco_page.j2",
title="Création d'une matière",
@ -164,57 +112,14 @@ associé.
+ tf[1]
),
)
_ = do_matiere_create(tf[2])
Matiere.create_from_dict(tf[2])
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):
"""Delete matière"""
from app.scodoc import sco_edit_ue
matiere = Matiere.query.get_or_404(matiere_id)
if not can_delete_matiere(matiere):
"""Form delete matière"""
matiere = Matiere.get_instance(matiere_id)
if not matiere.can_be_deleted():
# il y a au moins un modimpl dans un module de cette matière
raise ScoNonEmptyFormationObject(
"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 = [
"<h2>Suppression de la matière %(titre)s" % mat,
" dans l'UE (%(acronyme)s))</h2>" % UE,
f"""<h2>Suppression de la matière {matiere.titre}
dans l'UE {matiere.ue.acronyme}</h2>""",
]
dest_url = url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=str(UE["formation_id"]),
formation_id=matiere.ue.formation_id,
)
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
(("matiere_id", {"input_type": "hidden"}),),
initvalues=mat,
initvalues=matiere.to_dict(),
submitlabel="Confirmer la suppression",
cancelbutton="Annuler",
)
@ -255,29 +158,23 @@ def matiere_delete(matiere_id=None):
if tf[0] == -1:
return flask.redirect(dest_url)
do_matiere_delete(matiere_id)
matiere.delete()
return flask.redirect(dest_url)
def matiere_edit(matiere_id=None):
"""Edit matiere"""
from app.scodoc import sco_edit_ue
F = matiere_list(args={"matiere_id": matiere_id})
if not F:
raise ScoValueError("Matière inexistante !")
F = F[0]
ues = sco_edit_ue.ue_list(args={"ue_id": F["ue_id"]})
if not 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]
"""Form edit matiere"""
matiere: Matiere = Matiere.get_instance(matiere_id)
if matiere.is_locked():
raise ScoLockedFormError()
ue = matiere.ue
formation = ue.formation
ues = matiere.ue.formation.ues
ue_names = [f"{u.acronyme} ({u.titre or ''})" for u in ues]
ue_ids = [u.id for u in ues]
H = [
"""<h2>Modification de la matière %(titre)s""" % F,
f"""(formation ({formation.acronyme}, version {formation.version})</h2>""",
f"""<h2>Modification de la matière {matiere.titre or 'sans titre'}
(formation ({formation.acronyme}, version {formation.version})</h2>""",
]
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
@ -317,14 +214,14 @@ associé.
},
),
),
initvalues=F,
initvalues=matiere.to_dict(),
submitlabel="Modifier les valeurs",
)
dest_url = url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=str(ue["formation_id"]),
formation_id=formation.id,
)
if tf[0] == 0:
return render_template(
@ -336,8 +233,8 @@ associé.
return flask.redirect(dest_url)
else:
# check unicity
mats = matiere_list(args={"ue_id": tf[2]["ue_id"], "titre": tf[2]["titre"]})
if len(mats) > 1 or (len(mats) == 1 and mats[0]["matiere_id"] != matiere_id):
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].id != matiere_id):
return render_template(
"sco_page.j2",
title="Modification d'une matière",
@ -348,32 +245,18 @@ associé.
),
)
modif = False
# changement d'UE ?
if tf[2]["ue_id"] != F["ue_id"]:
log("attaching mat %s to new UE %s" % (matiere_id, tf[2]["ue_id"]))
ndb.SimpleQuery(
"UPDATE notes_modules SET ue_id = %(ue_id)s WHERE matiere_id=%(matiere_id)s",
{"ue_id": tf[2]["ue_id"], "matiere_id": matiere_id},
)
do_matiere_edit(tf[2])
if tf[2]["ue_id"] != ue.id:
log(f"attaching mat {matiere_id} to new UE id={tf[2]['ue_id']}")
new_ue = UniteEns.get_ue(tf[2]["ue_id"])
if new_ue.formation_id != formation.id:
raise ScoValueError("UE does not belong to the same formation")
matiere.ue = new_ue
modif = True
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)
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_login import current_user
from app import db, log
from app import db
from app import models
from app.models import APO_CODE_STR_LEN
from app.models import Formation, Matiere, Module, UniteEns
from app.models import FormSemestre, ModuleImpl
from app.models import ScolarNews
from app.models.but_refcomp import ApcAppCritique, ApcParcours
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_exceptions import (
ScoValueError,
ScoLockedFormError,
ScoGenError,
ScoNonEmptyFormationObject,
)
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):
"""Delete a module"""
"""Formulaire suppression d'un module"""
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(
"Module",
msg=module.titre,
@ -221,7 +81,7 @@ def module_delete(module_id=None):
request.base_url,
scu.get_request_args(),
(("module_id", {"input_type": "hidden"}),),
initvalues=mod,
initvalues=module.to_dict(),
submitlabel="Confirmer la suppression",
cancelbutton="Annuler",
)
@ -231,37 +91,38 @@ def module_delete(module_id=None):
title="Suppression d'un module",
content="\n".join(H) + tf[1],
)
elif tf[0] == -1:
return flask.redirect(dest_url)
else:
do_module_delete(module_id)
if tf[0] == -1: # cancel
return flask.redirect(dest_url)
module.delete()
return flask.redirect(dest_url)
def do_module_edit(vals: dict) -> None:
"edit a module"
# check
mod = module_list({"module_id": vals["module_id"]})[0]
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]
module = Module.get_instance(vals["module_id"])
# edit
cnx = ndb.GetDBConnexion()
_moduleEditor.edit(cnx, vals)
db.session.get(Formation, mod["formation_id"]).invalidate_cached_sems()
modif = module.from_dict(vals)
if modif:
module.formation.invalidate_cached_sems()
def check_module_code_unicity(code, field, formation_id, module_id=None):
"true si code module unique dans la formation"
modules = module_list(args={"code": code, "formation_id": formation_id})
if module_id: # edition: supprime le module en cours
modules = [m for m in modules if m["module_id"] != module_id]
return len(modules) == 0
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 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
de cette UE (si elle n'existe pas, la crée).
"""
from app.scodoc import sco_tag_module
# --- Détermination de la formation
orig_semestre_idx = semestre_id
ue = None
if create:
if matiere_id:
matiere = Matiere.query.get_or_404(matiere_id)
matiere = Matiere.get_instance(matiere_id)
ue = matiere.ue
formation = ue.formation
orig_semestre_idx = ue.semestre_idx if semestre_id is None else semestre_id
@ -300,7 +159,7 @@ def module_edit(
ue = module.ue
module_dict = module.to_dict()
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)
is_apc = parcours.APC_SAE # BUT
@ -326,17 +185,14 @@ def module_edit(
if (module and module.matiere and (module.matiere.id == mat.id))
or (mat.id == mat.ue.matieres.first().id)
]
mat_names = [
"S%s / %s" % (mat.ue.semestre_idx, mat.ue.acronyme) for mat in matieres
]
mat_names = [f"S{mat.ue.semestre_idx} / {mat.ue.acronyme}" for mat in matieres]
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
ue_mat_ids = ["%s!%s" % (mat.ue.id, mat.id) for mat in matieres]
module_dict["ue_matiere_id"] = "%s!%s" % (
module_dict["ue_id"],
module_dict["matiere_id"],
ue_mat_ids = [f"{mat.ue.id}!{mat.id}" for mat in matieres]
module_dict["ue_matiere_id"] = (
f"{module_dict['ue_id']}!{module_dict['matiere_id']}"
)
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,
R2.01, ou SAÉ 3.4. Doit être unique dans la formation)""",
"allow_null": False,
"validator": lambda val, field, formation_id=formation.id: check_module_code_unicity(
val, field, formation_id, module_id=module.id if module else None
"validator": lambda val, _, formation_id=formation.id: Module.check_module_code_unicity(
val, formation_id, module_id=module.id if module else None
),
},
),
@ -602,7 +458,8 @@ def module_edit(
"title": "UE de rattachement",
"explanation": "utilisée notamment pour les malus",
"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
],
"allowed_values": [u.id for u in ues],
@ -631,7 +488,8 @@ def module_edit(
"input_type": "menu",
"type": "int",
"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],
"allowed_values": semestres_indices,
"enabled": unlocked,
@ -767,6 +625,7 @@ def module_edit(
module_dict["semestre_id"] = 1
else:
module_dict["semestre_id"] = module.ue.semestre_idx
tags = module.tags if module else []
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
@ -774,7 +633,9 @@ def module_edit(
html_foot_markup=(
f"""<div class="scobox sco_tag_module_edit"><span
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
else ""
@ -833,14 +694,17 @@ def module_edit(
if matiere:
tf[2]["matiere_id"] = matiere.id
else:
matiere_id = sco_edit_matiere.do_matiere_create(
{"ue_id": ue.id, "titre": ue.titre or "", "numero": 1},
matiere = Matiere.create_from_dict(
{
"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
module_id = do_module_create(tf[2])
module = db.session.get(Module, module_id)
module = Module.create_from_dict(tf[2], news=True)
else: # EDITION MODULE
# l'UE de rattachement peut changer
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.commit()
module.formation.invalidate_cached_sems()
return flask.redirect(
url_for(
"notes.ue_table",
@ -897,30 +762,36 @@ def module_edit(
def module_table(formation_id):
"""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 = [
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">
""",
]
editable = current_user.has_permission(Permission.EditFormation)
for module_dict in module_list(args={"formation_id": formation_id}):
H.append('<li class="notes_module_list">%s' % module_dict)
for module in formation.modules:
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:
H.append(
'<a href="module_edit?module_id=%(module_id)s">modifier</a>'
% module_dict
)
H.append(
'<a href="module_delete?module_id=%(module_id)s">supprimer</a>'
% module_dict
f"""
<a class="stdlink" href="{
url_for('notes.module_edit', scodoc_dept=g.scodoc_dept, module_id=module.id)
}">modifier</a>
<a class="stdlink" href="{
url_for('notes.module_delete', scodoc_dept=g.scodoc_dept, module_id=module.id)
}">supprimer</a>
"""
)
H.append("</li>")
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(
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)
for ue in ues:
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
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
# le semestre ? ou affecter le malus au semestre 1 ???
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:
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 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_moduleimpl
from app.scodoc import sco_tag_module
_ueEditor = ndb.EditableTable(
"notes_ue",
@ -376,7 +373,8 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
"type": "float",
"min_value": 0,
"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,
"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",
"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,
},
),
@ -446,7 +445,8 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
"input_type": "boolcheckbox",
"title": "UE 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",
"default": True,
"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"]:
# rappel: en APC, toutes les UE ont une matière, créée ici
# (inutilisée mais à laquelle les modules sont rattachés)
matiere_id = sco_edit_matiere.do_matiere_create(
{"ue_id": ue_id, "titre": tf[2]["titre"], "numero": 1},
matiere = Matiere.create_from_dict(
{"ue_id": ue_id, "titre": tf[2]["titre"], "numero": 1}
)
matiere_id = matiere.id
if cursus.UE_IS_MODULE:
# 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"],
"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"],
},
)
db.session.commit()
ue = db.session.get(UniteEns, ue_id)
flash(f"UE créée (code {ue.ue_code})")
else:
@ -608,9 +611,10 @@ def _add_ue_semestre_id(ues: list[dict], is_apc):
ue["semestre_id"] = codes_cursus.UE_SEM_DEFAULT
else:
# était le comportement ScoDoc7
modules = sco_edit_module.module_list(args={"ue_id": ue["ue_id"]})
if modules:
ue["semestre_id"] = modules[0]["semestre_id"]
ue = UniteEns.get_ue(ue["ue_id"])
module = ue.modules.first()
if module:
ue["semestre_id"] = module.semestre_id
else:
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)
def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
"""Liste des matières et modules d'une formation, avec liens pour
éditer (si non verrouillée).
def ue_table(formation_id=None, semestre_idx=1, msg=""):
"""Page affiochage ou édition d'une formation
avec UEs, matières et module,
et liens pour éditer si non verrouillée et permission.
"""
from app.scodoc import sco_formsemestre_validation
@ -1241,7 +1246,7 @@ def _ue_table_ues(
def _ue_table_matieres(
parcours,
ue,
ue_dict: dict,
editable,
tag_editable,
arrow_up,
@ -1251,26 +1256,27 @@ def _ue_table_matieres(
delete_disabled_icon,
):
"""Édition de programme: liste des matières (et leurs modules) d'une UE."""
ue = UniteEns.get_ue(ue_dict["ue_id"])
H = []
if not parcours.UE_IS_MODULE:
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:
if not parcours.UE_IS_MODULE:
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(
f"""<a class="stdlink" href="{
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)
if editable and not sco_edit_matiere.matiere_is_locked(mat["matiere_id"]):
H.append(f"{mat.titre or 'sans titre'}")
if editable and not mat.is_locked():
H.append("</a>")
modules = sco_edit_module.module_list(args={"matiere_id": mat["matiere_id"]})
modules = mat.modules.all()
H.append(
_ue_table_modules(
parcours,
@ -1292,14 +1298,17 @@ def _ue_table_matieres(
H.append("<li>Aucune matière dans cette UE ! ")
if editable:
H.append(
"""<a class="stdlink" href="ue_delete?ue_id=%(ue_id)s">supprimer l'UE</a>"""
% ue
f"""<a class="stdlink" href="{
url_for('notes.ue_delete', scodoc_dept=g.scodoc_dept, ue_id=ue.id)
}">supprimer l'UE</a>"""
)
H.append("</li>")
if editable and not parcours.UE_IS_MODULE:
H.append(
'<li><a class="stdlink" href="matiere_create?ue_id=%(ue_id)s">créer une matière</a> </li>'
% ue
f"""<li><a class="stdlink" href="{
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:
H.append("</ul>")
@ -1308,9 +1317,9 @@ def _ue_table_matieres(
def _ue_table_modules(
parcours,
ue,
mat,
modules,
ue: UniteEns,
mat: Matiere,
modules: list[Module],
editable,
tag_editable,
arrow_up,
@ -1318,7 +1327,6 @@ def _ue_table_modules(
arrow_none,
delete_icon,
delete_disabled_icon,
unit_name="matière",
add_suppress_link=True, # lien "supprimer cette matière"
empty_list_msg="Aucun élément dans cette matière",
create_element_msg="créer un module",
@ -1327,91 +1335,83 @@ def _ue_table_modules(
H = ['<ul class="notes_module_list">']
im = 0
for mod in modules:
mod["nb_moduleimpls"] = sco_edit_module.module_count_moduleimpls(
mod["module_id"]
)
nb_moduleimpls = mod.modimpls.count()
klass = "notes_module_list"
if mod["module_type"] == ModuleType.MALUS:
if mod.module_type == ModuleType.MALUS:
klass += " module_malus"
H.append('<li class="%s">' % klass)
H.append(f'<li class="{klass}">')
H.append('<span class="notes_module_list_buts">')
if im != 0 and editable:
H.append(
'<a href="module_move?module_id=%s&after=0" class="aud">%s</a>'
% (mod["module_id"], arrow_up)
f"""<a href="module_move?module_id={mod.id}&after=0" class="aud">{arrow_up}</a>"""
)
else:
H.append(arrow_none)
if im < len(modules) - 1 and editable:
H.append(
'<a href="module_move?module_id=%s&after=1" class="aud">%s</a>'
% (mod["module_id"], arrow_down)
f"""<a href="module_move?module_id={mod.id}&after=1" class="aud">{arrow_down}</a>"""
)
else:
H.append(arrow_none)
im += 1
if mod["nb_moduleimpls"] == 0 and editable:
icon = delete_icon
else:
icon = delete_disabled_icon
icon = delete_icon if nb_moduleimpls == 0 and editable else delete_disabled_icon
H.append(
'<a class="smallbutton" href="module_delete?module_id=%s">%s</a>'
% (mod["module_id"], icon)
f"""<a class="smallbutton" href="{
url_for("notes.module_delete", scodoc_dept=g.scodoc_dept, module_id=mod.id)
}">{icon}</a>"""
)
H.append("</span>")
mod_editable = (
editable # and not sco_edit_module.module_is_locked( Mod['module_id'])
)
mod_editable = editable
if mod_editable:
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">'
% mod
f"""<a class="discretelink" title="Modifier le module numéro %(numero)s, utilisé par
{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(
f"""<span class="invalid-module-type">{scu.EMO_WARNING} type incompatible </span>"""
)
H.append(
'<span class="formation_module_tit">%s</span>'
% scu.join_words(mod["code"], mod["titre"])
f"""<span class="formation_module_tit">{scu.join_words(mod.code, mod.titre)}</span>"""
)
if mod_editable:
H.append("</a>")
heurescoef = (
"%(heures_cours)s/%(heures_td)s/%(heures_tp)s, coef. %(coefficient)s" % mod
heures = (
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(
"apiweb.formation_module_set_code_apogee",
scodoc_dept=g.scodoc_dept,
module_id=mod["module_id"],
module_id=mod.id,
)
heurescoef += f""", Apo: <span
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}">{
mod["code_apogee"] or ""
mod.code_apogee or ""
}</span>"""
if tag_editable:
tag_cls = "module_tag_editor"
else:
tag_cls = "module_tag_editor_ro"
tag_mk = """<span class="sco_tag_edit"><form><textarea data-module_id="{}" class="{}">{}</textarea></form></span>"""
tag_edit = tag_mk.format(
mod["module_id"],
tag_cls,
",".join(sco_tag_module.module_tag_list(mod["module_id"])),
)
if ue["semestre_idx"] is not None and mod["semestre_id"] != ue["semestre_idx"]:
tag_cls = "module_tag_editor" if tag_editable else "module_tag_editor_ro"
tag_edit = f"""<span class="sco_tag_edit">
<form>
<textarea data-module_id="{mod.id}" class="{tag_cls}">{
",".join([ tag.title for tag in mod.tags ])
}</textarea>
</form>
</span>"""
if ue.semestre_idx is not None and mod.semestre_id != ue.semestre_idx:
warning_semestre = ' <span class="red">incohérent ?</span>'
else:
warning_semestre = ""
H.append(
" %s %s%s" % (parcours.SESSION_NAME, mod["semestre_id"], warning_semestre)
+ " (%s)" % heurescoef
+ tag_edit
f""" {parcours.SESSION_NAME} {mod.semestre_id}{warning_semestre}
{heurescoef}{tag_edit}"""
)
H.append("</li>")
if not modules:
@ -1420,7 +1420,7 @@ def _ue_table_modules(
H.append(
f"""<a class="stdlink" href="{
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>
"""
)
@ -1429,7 +1429,7 @@ def _ue_table_modules(
H.append(
f"""<li> <a class="stdlink" href="{
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>
"""
)

View File

@ -34,10 +34,10 @@ from flask import flash, g, request, url_for
from flask_login import current_user
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app import db
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.but_refcomp import (
ApcAppCritique,
@ -48,49 +48,12 @@ from app.models.but_refcomp import (
)
from app.scodoc import sco_cache
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_tag_module
from app.scodoc import sco_xml
import sco_version
from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_exceptions import ScoValueError, ScoFormatError
from app.scodoc.sco_permissions import Permission
_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
import sco_version
def formation_export_dict(
@ -149,35 +112,35 @@ def formation_export_dict(
ue_dict.pop("code_apogee_rcue", None)
if ue_dict.get("ects") is None:
ue_dict.pop("ects", None)
mats = sco_edit_matiere.matiere_list({"ue_id": ue.id})
mats.sort(key=lambda m: m["numero"] or 0)
ue_dict["matiere"] = mats
for mat in mats:
matiere_id = mat["matiere_id"]
mats = ue.matieres.all()
mats.sort(key=lambda m: m.numero)
mats_dict = [mat.to_dict() for mat in mats]
ue_dict["matiere"] = mats_dict
for mat_d in mats_dict:
matiere_id = mat_d["matiere_id"]
if not export_ids:
del mat["id"]
del mat["matiere_id"]
del mat["ue_id"]
mods = sco_edit_module.module_list({"matiere_id": matiere_id})
mods.sort(key=lambda m: (m["numero"] or 0, m["code"]))
mat["module"] = mods
for mod in mods:
module_id = mod["module_id"]
del mat_d["id"]
del mat_d["matiere_id"]
del mat_d["ue_id"]
mat = db.session.get(Matiere, matiere_id)
mods = mat.modules.all()
mods.sort(key=lambda m: (m.numero, m.code))
mat_d["module"] = [mod.to_dict() for mod in mods]
for module, mod_d in zip(mods, mat_d["module"]):
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:
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():
# Exporte les coefficients
if ue_reference_style == "id":
mod["coefficients"] = [
mod_d["coefficients"] = [
{"ue_reference": str(ue_id), "coef": str(coef)}
for (ue_id, coef) in module.get_ue_coef_dict().items()
]
else:
mod["coefficients"] = [
mod_d["coefficients"] = [
{"ue_reference": ue_acronyme, "coef": str(coef)}
for (
ue_acronyme,
@ -185,29 +148,29 @@ def formation_export_dict(
) in module.get_ue_coef_dict_acronyme().items()
]
# Et les parcours
mod["parcours"] = [
mod_d["parcours"] = [
p.to_dict(with_annees=False) for p in module.parcours
]
# Et les AC
if ac_as_list:
# 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
]
else:
mod["app_critiques"] = {
mod_d["app_critiques"] = {
x.code: x.to_dict() for x in module.app_critiques
}
if not export_ids:
del mod["id"]
del mod["ue_id"]
del mod["matiere_id"]
del mod["module_id"]
del mod["formation_id"]
del mod_d["id"]
del mod_d["ue_id"]
del mod_d["matiere_id"]
del mod_d["module_id"]
del mod_d["formation_id"]
if not export_codes_apo:
del mod["code_apogee"]
if mod["ects"] is None:
del mod["ects"]
del mod_d["code_apogee"]
if mod_d["ects"] is None:
del mod_d["ects"]
return f_dict
@ -236,7 +199,8 @@ def formation_export(
if fmt is None:
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(
f_dict,
name="formation",
@ -314,7 +278,7 @@ def formation_import_xml(doc: str | bytes, import_tags=True, use_local_refcomp=F
Returns:
formation_id, modules_old2new, ues_old2new
"""
from app.scodoc import sco_edit_formation
from app.formations import edit_formation
if isinstance(doc, bytes):
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:
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:
raise ScoFormatError(
"""Ce document xml ne correspond pas à un programme exporté par ScoDoc.
(élément 'formation' inexistant par exemple)."""
) from exc
assert D[0] == "formation"
f_dict = D[1]
assert xml_dicts[0] == "formation"
f_dict = xml_dicts[1]
f_dict["dept_id"] = g.scodoc_dept_id
# Pour les clonages, on prend le refcomp_id donné:
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
# 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")
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
with sco_cache.DeferredSemCacheManager():
# -- create UEs
for ue_info in D[2]:
for ue_info in xml_dicts[2]:
assert ue_info[0] == "ue"
ue_info[1]["formation_id"] = formation.id
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
# 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)
assert ue
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"
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
for mod_info in mat_info[2]:
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
if not "module_type" in mod_info[1]:
mod_info[1]["module_type"] = scu.ModuleType.STANDARD
mod_id = sco_edit_module.do_module_create(mod_info[1])
module = Module.create_from_dict(
mod_info[1], news=True, inval_cache=True
)
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:
module: Module = db.session.get(Module, mod_id)
tag_names = []
ue_coef_dict = {}
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 !"
)
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:
modules_a_coefficienter.append((module, ue_coef_dict))
# 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):
"""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.
"""
annee_scolaire = int(annee_scolaire)

View File

@ -47,7 +47,7 @@ import app.scodoc.sco_utils as scu
from app import log
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_formations
from app.formations import formation_io
def formsemestre_associate_new_version(
@ -202,7 +202,7 @@ def do_formsemestres_associate_new_version(
new_formation_id,
modules_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:
for ue_id in ues_old2new:
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.
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"
)
if formation2_dict is None:
if formation2 is None:
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"
)
del fd1["version"]

View File

@ -5,7 +5,7 @@ from flask import abort, g
from flask_sqlalchemy.query import Query
import app
from app import db
from app import db, log
from app.comp import df_cache
from app.models import ScoDocModel, SHORT_STR_LEN
from app.models.but_refcomp import (
@ -14,6 +14,7 @@ from app.models.but_refcomp import (
ApcParcours,
ApcParcoursNiveauCompetence,
)
from app.models.events import ScolarNews
from app.models.modules import Module
from app.models.moduleimpls import ModuleImpl
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 sco_utils as scu
from app.scodoc.codes_cursus import UE_STANDARD
from app.scodoc.sco_exceptions import ScoNonEmptyFormationObject, ScoValueError
class Formation(ScoDocModel):
@ -64,7 +66,8 @@ class Formation(ScoDocModel):
def html(self) -> str:
"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
def get_formation(cls, formation_id: int | str, dept_id: int = None) -> "Formation":
@ -166,6 +169,7 @@ class Formation(ScoDocModel):
sco_cache.invalidate_formsemestre()
def invalidate_cached_sems(self):
"Invalide caches de tous les formssemestres de la formation"
for sem in self.formsemestres:
sco_cache.invalidate_formsemestre(formsemestre_id=sem.id)
@ -312,7 +316,9 @@ class Matiere(ScoDocModel):
titre = db.Column(db.Text())
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
def __repr__(self):
@ -325,5 +331,73 @@ class Matiere(ScoDocModel):
e.pop("_sa_instance_state", None)
# ScoDoc7 output_formators
e["numero"] = e["numero"] if e["numero"] else 0
e["ue_id"] = self.id
e["matiere_id"] = self.id
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
import app
from app import db
from app import db, log
from app.auth.models import User
from app.comp import df_cache
from app.models import APO_CODE_STR_LEN, ScoDocModel
from app.models.etudiants import Identite
from app.models.evaluations import Evaluation
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 import sco_utils as scu
@ -65,6 +66,30 @@ class ModuleImpl(ScoDocModel):
def __repr__(self):
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]:
"""Les codes Apogée (codés en base comme "VRT1,VRT2").
(si non renseigné, ceux du module)

View File

@ -1,9 +1,10 @@
"""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.models import APO_CODE_STR_LEN
from app.models.but_refcomp import (
@ -12,9 +13,14 @@ from app.models.but_refcomp import (
app_critiques_modules,
parcours_modules,
)
from app.models.events import ScolarNews
from app.scodoc import sco_utils as scu
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
@ -75,11 +81,11 @@ class Module(models.ScoDocModel):
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):
self.ue_coefs = []
super(Module, self).__init__(**kwargs)
super().__init__(**kwargs)
def __repr__(self):
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
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:
"""Update object's fields given in dict. Add to session but don't commit.
True if modification.
- can't change ue nor formation
- can change matiere_id, iff new matiere in same ue
- 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
new_matiere_id = args.get("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}
new_parcours = args.get("parcours", []) or []
if existing_parcours != set(new_parcours):
self._set_parcours_from_list(new_parcours)
self.set_parcours_from_list(new_parcours)
return True
return modified
@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.
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)
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
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.
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
@ -449,6 +564,43 @@ class Module(models.ScoDocModel):
db.session.add(self)
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):
"""Coefficients des modules vers les UE (APC, BUT)
@ -499,7 +651,7 @@ class ModuleUECoef(db.Model):
return d
class NotesTag(db.Model):
class NotesTag(models.ScoDocModel):
"""Tag sur un module"""
__tablename__ = "notes_tags"
@ -511,6 +663,9 @@ class NotesTag(db.Model):
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
title = db.Column(db.Text(), nullable=False)
def __repr__(self):
return f"<Tag {self.id} {self.title!r}>"
@classmethod
def get_or_create(cls, title: str, dept_id: int | None = None) -> "NotesTag":
"""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]:
"""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:
"""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.comp.res_but import ResultatsSemestreBUT
from app.models import FormSemestre, UniteEns
import app.pe.pe_affichage as pe_affichage
import app.pe.pe_etudiant as pe_etudiant
from app.pe import pe_affichage
from app.pe import pe_etudiant
from app.pe.moys import pe_tabletags, pe_moytag
from app.scodoc import sco_tag_module
from app.scodoc import codes_cursus as sco_codes
@ -59,13 +59,18 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag):
def __init__(
self,
formsemestre: FormSemestre,
options={"moyennes_tags": True, "moyennes_ue_res_sae": False},
options: dict | None = None,
):
"""
Args:
formsemestre: le ``FormSemestre`` sur lequel il se base
options: Un dictionnaire d'options
"""
options = (
{"moyennes_tags": True, "moyennes_ue_res_sae": False}
if options is None
else options
)
ResultatsSemestreBUT.__init__(self, formsemestre)
pe_tabletags.TableTag.__init__(self)
@ -274,7 +279,8 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag):
pole=None,
) -> pd.DataFrame:
"""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 :
* 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]
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]
# Transforme les UEs en acronyme
@ -405,7 +412,7 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag):
"""Vérifie l'unicité des tags"""
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 = noms_tags_perso + noms_tags_auto
# noms_tags = noms_tags_perso + 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
# 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
# "mathématiques", "théorie", "pe:0", "maths:2"

View File

@ -296,13 +296,18 @@ class TF(object):
if not allow_null:
if val is None or (isinstance(val, str) and not val.strip()):
msg.append(
"Le champ '%s' doit être renseigné" % descr.get("title", field)
f"Le champ '{descr.get('title', field)}' doit être renseigné"
)
ok = 0
elif val == "" or val == None:
elif val in ("", None):
continue # allowed empty field, skip
# 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:
# check only non-null values
if typ[:3] == "int":
@ -311,24 +316,22 @@ class TF(object):
self.values[field] = val
except ValueError:
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
elif typ == "float" or typ == "real":
elif typ in ("float", "real"):
self.values[field] = self.values[field].replace(",", ".")
try:
val = float(val.replace(",", ".")) # allow ,
self.values[field] = val
except ValueError:
msg.append(
"La valeur du champ '%s' doit être un nombre" % field
)
msg.append(f"La valeur du champ {field}' doit être un nombre")
ok = 0
if (
ok
and (typ[:3] == "int" or typ == "float" or typ == "real")
and val != ""
and val != None
and val is not None
):
if "min_value" in descr and self.values[field] < descr["min_value"]:
msg.append(
@ -343,12 +346,12 @@ class TF(object):
)
ok = 0
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(
f"Le champ '{field}' est a une valeur hors limite"
)
ok = 0
elif typ == "float" or typ == "real":
elif typ in ("float", "real"):
if not (
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"
)
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"]:
msg.append(
"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.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.formations import edit_ue
from app.models import but_validations
from app.models import BulAppreciations, Evaluation, Matiere, UniteEns
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.notesdb as ndb
from app.scodoc import sco_assiduites
from app.scodoc import sco_edit_ue
from app.scodoc import sco_evaluations
from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups
@ -491,11 +491,11 @@ def dict_decision_jury(
]
d["decision_ue"] = []
if decision[
"decisions_ue"
]: # and sco_preferences.get_preference( 'bul_show_uevalid', formsemestre_id): always publish (car utile pour export Apogee)
if decision["decisions_ue"]:
# 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():
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(
dict(
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.res_compat import NotesTableCompat
from app.formations import edit_ue
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
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.scodoc import sco_assiduites
from app.scodoc import codes_cursus
from app.scodoc import sco_edit_ue
from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups
from app.scodoc import sco_photos
@ -393,7 +393,7 @@ def make_xml_formsemestre_bulletinetud(
"decisions_ue"
]: # 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():
ue = sco_edit_ue.ue_list({"ue_id": ue_id})[0]
ue = edit_ue.ue_list({"ue_id": ue_id})[0]
doc.append(
Element(
"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_vdi import ApoEtapeVDI
from app.scodoc import codes_cursus
from app.scodoc import sco_edit_module
from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups_copy
from app.scodoc import sco_moduleimpl
@ -939,7 +938,7 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
"formsemestre_id": formsemestre_id,
"responsable_id": tf[2][f"MI{module_id}"],
}
_ = sco_moduleimpl.do_moduleimpl_create(modargs)
_ = ModuleImpl.create_from_dict(modargs)
else:
# Modification du semestre:
# 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,
"responsable_id": tf[2]["MI" + str(module_id)],
}
moduleimpl_id = sco_moduleimpl.do_moduleimpl_create(modargs)
mod = sco_edit_module.module_list({"module_id": module_id})[0]
msg += [
"création de %s (%s)" % (mod["code"] or "?", mod["titre"] or "?")
]
modimpl = ModuleImpl.create_from_dict(modargs)
assert modimpl.module_id == module_id
mod = modimpl.module
msg += [f"""création de {mod.code or "?"} ({mod.titre or "?"})"""]
# INSCRIPTIONS DES ETUDIANTS
log(
'inscription module: %s = "%s"'
% ("%s!group_id" % module_id, tf[2]["%s!group_id" % module_id])
)
group_id = tf[2]["%s!group_id" % module_id]
group_id = tf[2][f"{module_id}!group_id"]
log(f"""inscription module: {module_id}!group_id = '{group_id}'""")
if group_id:
etudids = [
x["etudid"] for x in sco_groups.get_group_members(group_id)
]
log(
"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(
moduleimpl_id,
modimpl.id,
formsemestre.id,
etudids,
)
@ -1002,7 +997,7 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
else:
log(
"inscription module:module_id=%s,moduleimpl_id=%s: aucun etudiant inscrit"
% (module_id, moduleimpl_id)
% (module_id, modimpl.id)
)
#
ok, diag = formsemestre_delete_moduleimpls(
@ -1022,7 +1017,6 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
sco_moduleimpl.do_moduleimpl_edit(
modargs, formsemestre_id=formsemestre.id
)
mod = sco_edit_module.module_list({"module_id": module_id})[0]
# --- Association des parcours
if formsemestre is None:
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_cache
from app.scodoc import sco_evaluations
from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups
@ -515,10 +514,9 @@ def formsemestre_page_title(formsemestre_id=None):
def fill_formsemestre(sem: dict): # XXX OBSOLETE
"""Add some fields in formsemestres dicts"""
formsemestre_id = sem["formsemestre_id"]
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
sem["formation"] = F
parcours = codes_cursus.get_cursus_from_code(F["type_parcours"])
formation = Formation.get_formation(sem["formation_id"])
sem["formation"] = formation.to_dict(with_departement=False)
parcours = codes_cursus.get_cursus_from_code(formation.type_parcours)
if sem["semestre_id"] != -1:
sem["num_sem"] = f""", {parcours.SESSION_NAME} {sem["semestre_id"]}"""
else:

View File

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

View File

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

View File

@ -30,17 +30,16 @@
Implementation expérimentale (Jul. 2016) pour grouper les modules sur
les avis de poursuites d'études.
TODO: réécrire avec SQLAlchemy.
Pour l'UI, voir https://goodies.pixabay.com/jquery/tag-editor/demo.html
"""
import http
import re
from flask import g
from app import db, log
from app import db
from app.models import Formation, NotesTag
from app.scodoc import sco_edit_module
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
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
class ScoTag(object):
class ScoTag:
"""Generic tags for ScoDoc"""
# must be overloaded:
@ -208,8 +207,6 @@ class ModuleTag(ScoTag):
# API
# TODO placer dans la vraie API et ne plus utiliser sco_publish
def module_tag_search(term: str | int):
"""List all used tag names (for auto-completion)"""
@ -230,60 +227,6 @@ def module_tag_search(term: str | int):
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]:
"""Découpage d'un tag, tel que saisi par un utilisateur dans le programme,
pour en extraire :

View File

@ -56,15 +56,13 @@ Solution proposée (nov 2014):
import flask
from flask import flash, g, request, render_template, url_for
from flask_login import current_user
from app.formations import edit_ue
from app.models.formsemestre import FormSemestre
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 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_saisie_notes
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
@ -97,10 +95,8 @@ def external_ue_create(
#
formation_id = formsemestre.formation.id
numero = sco_edit_ue.next_ue_numero(
formation_id, semestre_id=formsemestre.semestre_id
)
ue_id = sco_edit_ue.do_ue_create(
numero = edit_ue.next_ue_numero(formation_id, semestre_id=formsemestre.semestre_id)
ue_id = edit_ue.do_ue_create(
{
"formation_id": formation_id,
"semestre_idx": formsemestre.semestre_id,
@ -114,26 +110,28 @@ def external_ue_create(
)
ue = db.session.get(UniteEns, ue_id)
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}
)
module_id = sco_edit_module.do_module_create(
module = Module.create_from_dict(
{
"titre": "UE extérieure",
"code": acronyme,
"coefficient": ects, # tous le coef. module est egal à la quantite d'ECTS
"ue_id": ue_id,
"matiere_id": matiere_id,
"matiere_id": matiere.id,
"formation_id": formation_id,
"semestre_id": formsemestre.semestre_id,
"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,
# affecte le 1er responsable du semestre comme resp. du module
"responsable_id": (
@ -143,7 +141,6 @@ def external_ue_create(
),
},
)
modimpl = db.session.get(ModuleImpl, moduleimpl_id)
assert modimpl
return modimpl

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -117,7 +117,7 @@ def GET(
print("reply", reply.text)
raise APIError(
errmsg or f"""erreur get {url} !""",
reply.json(),
reply if reply.status_code == 404 else reply.json(),
status_code=reply.status_code,
)
if raw:
@ -220,7 +220,8 @@ def check_failure_get(path: str, headers: dict, err: str = None):
# ^ Renvoi un 404
except APIError as api_err:
if err is not None:
assert api_err.payload["message"] == err
if "message" in api_err.payload:
assert api_err.payload["message"] == err
else:
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_tp"], 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["numero"], int)
assert isinstance(module["code_apogee"], str)

View File

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

View File

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

View File

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

View File

@ -8,16 +8,15 @@ import pytest
import app
from app import db
from app.formations import edit_ue, formation_versions
from app.models import Formation, FormSemestre, FormSemestreDescription
from app.scodoc import (
sco_archives_formsemestre,
sco_cost_formation,
sco_debouche,
sco_edit_ue,
sco_evaluations,
sco_evaluation_check_abs,
sco_evaluation_recap,
sco_formation_versions,
sco_formsemestre_edit,
sco_formsemestre_inscriptions,
sco_formsemestre_status,
@ -61,7 +60,7 @@ def test_formsemestres_associate_new_version(test_client):
assert {s.semestre_id for s in formsemestres} == {1}
# Les rattache à une nouvelle version de la formation:
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
)
new_formation: Formation = Formation.query.filter_by(
@ -91,7 +90,7 @@ def test_formsemestre_misc_views(test_client):
# ----- MENU SEMESTRE
_ = 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_preferences.SemPreferences(formsemestre_id=formsemestre.id).edit()
_ = 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
# Juste la page dialogue avant opération::
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
)
ans = sco_formsemestre_edit.formsemestre_delete(formsemestre.id)

View File

@ -50,7 +50,7 @@ from flask import g
from app import db
from app.auth.models import User
from app.formations import formation_io
from app.models import (
ApcParcours,
DispenseUE,
@ -63,7 +63,6 @@ from app.models import (
UniteEns,
)
from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups
from app.scodoc import sco_saisie_notes
@ -86,7 +85,7 @@ def setup_formation(formation_infos: dict) -> Formation:
doc = f.read()
# --- 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)
assert formation
return formation

View File

@ -17,6 +17,7 @@ from app import db
from app.auth.models import Role, User
from app.but.import_refcomp import orebut_import_refcomp
from app import models
from app.formations import formation_io
from app.models import departements
from app.models import (
Absence,
@ -33,8 +34,6 @@ from app.models import (
)
from app.scodoc import (
sco_cache,
sco_evaluation_db,
sco_formations,
sco_formsemestre_inscriptions,
sco_groups,
)
@ -68,7 +67,7 @@ def import_formation(dept_id: int) -> Formation:
with open(FORMATION_XML_FILENAME, encoding="utf-8") as f:
doc = f.read()
# --- 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])
# --- Association ref. comp.
with open(REFCOMP_FILENAME, encoding="utf-8") as f: