Backend 'Matieres': utilise uniquement modèle.

This commit is contained in:
ilona 2024-10-11 14:06:02 +02:00
parent 056be2e152
commit 1dfba157c2
12 changed files with 300 additions and 357 deletions

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.formations import 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 = 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.formations import edit_ue
cnx = ndb.GetDBConnexion()
# check
ue = 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,56 +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.formations import edit_module, edit_ue
cnx = ndb.GetDBConnexion()
# check
matiere = Matiere.query.get_or_404(oid)
mat = matiere_list({"matiere_id": oid})[0] # compat sco7
ue = 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(f"do_matiere_delete: matiere_id={matiere.id}")
# delete all modules in this matiere
mods = edit_module.module_list({"matiere_id": matiere.id})
for mod in mods:
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.formations import 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",
@ -226,22 +132,20 @@ def matiere_delete(matiere_id=None):
),
)
mat = matiere_list(args={"matiere_id": matiere_id})[0]
ue_dict = 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_dict,
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_dict["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",
)
@ -254,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.formations import edit_ue
F = matiere_list(args={"matiere_id": matiere_id})
if not F:
raise ScoValueError("Matière inexistante !")
F = F[0]
ues = 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 = 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
@ -316,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(
@ -335,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",
@ -347,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

@ -767,6 +767,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 +775,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,10 +836,14 @@ def module_edit(
if matiere:
tf[2]["matiere_id"] = matiere.id
else:
matiere_id = 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])
@ -946,12 +953,6 @@ def module_is_locked(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
):

View File

@ -547,9 +547,10 @@ 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 = 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:
_ = edit_module.do_module_create(
@ -676,9 +677,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
@ -1240,7 +1242,7 @@ def _ue_table_ues(
def _ue_table_matieres(
parcours,
ue,
ue_dict: dict,
editable,
tag_editable,
arrow_up,
@ -1250,26 +1252,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 = 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 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 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 = edit_module.module_list(args={"matiere_id": mat["matiere_id"]})
modules = mat.modules.all()
H.append(
_ue_table_modules(
parcours,
@ -1291,14 +1294,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>")
@ -1307,9 +1313,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,
@ -1326,89 +1332,84 @@ def _ue_table_modules(
H = ['<ul class="notes_module_list">']
im = 0
for mod in modules:
mod["nb_moduleimpls"] = 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 edit_module.module_is_locked( Mod['module_id'])
)
mod_editable = editable
# and not edit_module.module_is_locked(Mod['module_id'])
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:
@ -1417,7 +1418,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>
"""
)
@ -1426,7 +1427,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

@ -36,8 +36,8 @@ from flask_login import current_user
import app.scodoc.sco_utils as scu
from app import db
from app import log
from app.formations import edit_matiere, edit_module, edit_ue
from app.models import Formation, FormSemestre, Module, UniteEns
from app.formations import edit_module, edit_ue
from app.models import Formation, FormSemestre, Matiere, Module, UniteEns
from app.models import ScolarNews
from app.models.but_refcomp import (
ApcAppCritique,
@ -113,35 +113,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 = 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"]
del mat_d["id"]
del mat_d["matiere_id"]
del mat_d["ue_id"]
mods = 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"]
mat_d["module"] = mods
for mod_d in mods:
module: Module = db.session.get(Module, mod_d["module_id"])
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,
@ -149,29 +149,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
@ -399,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 = 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"
@ -460,7 +461,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

@ -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):
@ -166,6 +168,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 +315,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 +330,75 @@ 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 cannot be be 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."
from app.models import ScolarNews
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

@ -1,9 +1,10 @@
"""ScoDoc 9 models : Modules
"""
import http
from flask import current_app, g
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 (
@ -75,11 +76,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
@ -449,6 +450,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 +537,7 @@ class ModuleUECoef(db.Model):
return d
class NotesTag(db.Model):
class NotesTag(models.ScoDocModel):
"""Tag sur un module"""
__tablename__ = "notes_tags"
@ -511,6 +549,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

@ -274,7 +274,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 +343,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 +407,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 +457,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

@ -30,16 +30,15 @@
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.formations import edit_module
from app import db
from app.models import Formation, NotesTag
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
@ -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 = 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,12 +56,12 @@ 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_matiere, edit_module, edit_ue
from app.formations import edit_module, 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, ModuleImpl, UniteEns
from app.scodoc import codes_cursus
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_saisie_notes
@ -110,7 +110,7 @@ def external_ue_create(
)
ue = db.session.get(UniteEns, ue_id)
flash(f"UE créée (code {ue.ue_code})")
matiere_id = edit_matiere.do_matiere_create(
matiere = Matiere.create_from_dict(
{"ue_id": ue_id, "titre": titre or acronyme, "numero": 1}
)
@ -120,7 +120,7 @@ def external_ue_create(
"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,

View File

@ -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()

View File

@ -205,11 +205,8 @@ class ScoFake(object):
@logging_meth
def create_matiere(self, ue_id=None, titre=None, numero=0) -> int:
oid = edit_matiere.do_matiere_create(locals())
oids = 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(

View File

@ -36,8 +36,6 @@
# - do_formsemestre_delete
# - module_list
# - do_module_delete
# - matiere_list
# - do_matiere_delete
# - ue_list
# - do_ue_delete
# - do_formation_delete
@ -50,12 +48,11 @@ import pytest
from app import db
from app.formations import (
edit_formation,
edit_matiere,
edit_module,
edit_ue,
formation_io,
)
from app.models import Formation, ModuleImpl
from app.models import Formation, Matiere, ModuleImpl
from app.scodoc import sco_formsemestre
from app.scodoc import sco_exceptions
from app.scodoc import sco_formsemestre_edit
@ -287,10 +284,12 @@ def test_formations(test_client):
assert len(lim_sem2) == 0 # deuxieme vérification si le module s'est bien sup
li_mat = edit_matiere.matiere_list()
li_mat = Matiere.query.all()
assert len(li_mat) == 4
edit_matiere.do_matiere_delete(oid=matiere_id3) # on supprime la matiere
li_mat2 = 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 = edit_ue.ue_list()