diff --git a/app/formations/edit_matiere.py b/app/formations/edit_matiere.py
index 26add3956..d758ee195 100644
--- a/app/formations/edit_matiere.py
+++ b/app/formations/edit_matiere.py
@@ -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 = [
- "
Suppression de la matière %(titre)s" % mat,
- " dans l'UE (%(acronyme)s))
" % ue_dict,
+ f"""Suppression de la matière {matiere.titre}
+ dans l'UE {matiere.ue.acronyme}
""",
]
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 = [
- """Modification de la matière %(titre)s""" % F,
- f"""(formation ({formation.acronyme}, version {formation.version})
""",
+ f"""Modification de la matière {matiere.titre or 'sans titre'}
+ (formation ({formation.acronyme}, version {formation.version})
""",
]
help_msg = """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
diff --git a/app/formations/edit_module.py b/app/formations/edit_module.py
index 8cb8dccf6..71fd3ade7 100644
--- a/app/formations/edit_module.py
+++ b/app/formations/edit_module.py
@@ -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"""
+ >{
+ ','.join(t.title for t in tags)
+ }
"""
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
):
diff --git a/app/formations/edit_ue.py b/app/formations/edit_ue.py
index 3e4d13641..dbf2a1dbd 100644
--- a/app/formations/edit_ue.py
+++ b/app/formations/edit_ue.py
@@ -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('")
@@ -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 = ['']
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('- ' % klass)
+ H.append(f'
- ')
H.append('')
if im != 0 and editable:
H.append(
- '%s'
- % (mod["module_id"], arrow_up)
+ f"""{arrow_up}"""
)
else:
H.append(arrow_none)
if im < len(modules) - 1 and editable:
H.append(
- '%s'
- % (mod["module_id"], arrow_down)
+ f"""{arrow_down}"""
)
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(
- '%s'
- % (mod["module_id"], icon)
+ f"""{icon}"""
)
-
H.append("")
- 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(
- ''
- % mod
+ f""""""
)
- 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"""{scu.EMO_WARNING} type incompatible """
)
H.append(
- '%s'
- % scu.join_words(mod["code"], mod["titre"])
+ f"""{scu.join_words(mod.code, mod.titre)}"""
)
if mod_editable:
H.append("")
- 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: {
- mod["code_apogee"] or ""
+ mod.code_apogee or ""
}"""
- if tag_editable:
- tag_cls = "module_tag_editor"
- else:
- tag_cls = "module_tag_editor_ro"
- tag_mk = """"""
- 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"""
+
+ """
+ if ue.semestre_idx is not None and mod.semestre_id != ue.semestre_idx:
warning_semestre = ' incohérent ?'
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("
")
if not modules:
@@ -1417,7 +1418,7 @@ def _ue_table_modules(
H.append(
f"""la supprimer
"""
)
@@ -1426,7 +1427,7 @@ def _ue_table_modules(
H.append(
f"""- {create_element_msg}
"""
)
diff --git a/app/formations/formation_io.py b/app/formations/formation_io.py
index bd2135726..bbf265cc5 100644
--- a/app/formations/formation_io.py
+++ b/app/formations/formation_io.py
@@ -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)
diff --git a/app/models/formations.py b/app/models/formations.py
index 1384873f1..5ae12d6f2 100644
--- a/app/models/formations.py
+++ b/app/models/formations.py
@@ -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
diff --git a/app/models/modules.py b/app/models/modules.py
index 8e1d8c5b7..50ff5633e 100644
--- a/app/models/modules.py
+++ b/app/models/modules.py
@@ -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""""
+
@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.
diff --git a/app/pe/moys/pe_ressemtag.py b/app/pe/moys/pe_ressemtag.py
index cff92b94e..e424e8f3e 100644
--- a/app/pe/moys/pe_ressemtag.py
+++ b/app/pe/moys/pe_ressemtag.py
@@ -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"
diff --git a/app/scodoc/sco_tag_module.py b/app/scodoc/sco_tag_module.py
index 5dfc26a55..0d5bdff62 100644
--- a/app/scodoc/sco_tag_module.py
+++ b/app/scodoc/sco_tag_module.py
@@ -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 :
diff --git a/app/scodoc/sco_ue_external.py b/app/scodoc/sco_ue_external.py
index b79500293..97f97259c 100644
--- a/app/scodoc/sco_ue_external.py
+++ b/app/scodoc/sco_ue_external.py
@@ -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,
diff --git a/app/views/notes.py b/app/views/notes.py
index 4aa707309..855eceedd 100644
--- a/app/views/notes.py
+++ b/app/views/notes.py
@@ -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()
diff --git a/tests/unit/sco_fake_gen.py b/tests/unit/sco_fake_gen.py
index 61bbb8f91..ef3976826 100644
--- a/tests/unit/sco_fake_gen.py
+++ b/tests/unit/sco_fake_gen.py
@@ -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(
diff --git a/tests/unit/test_formations.py b/tests/unit/test_formations.py
index 66eea7867..e234e1ff5 100644
--- a/tests/unit/test_formations.py
+++ b/tests/unit/test_formations.py
@@ -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()