diff --git a/app/formations/edit_ue.py b/app/formations/edit_ue.py
index 3712a8543..1b9e3e334 100644
--- a/app/formations/edit_ue.py
+++ b/app/formations/edit_ue.py
@@ -45,7 +45,6 @@ from app.models import (
FormSemestreUECoef,
Matiere,
Module,
- ModuleImpl,
UniteEns,
)
from app.models import ApcValidationRCUE, ScolarFormSemestreValidation, ScolarEvent
@@ -546,6 +545,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
formation_id, int(tf[2]["semestre_idx"])
)
ue_id = do_ue_create(tf[2])
+ matiere_id = None
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)
@@ -597,46 +597,21 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
)
-def _add_ue_semestre_id(ues: list[dict], is_apc):
- """ajoute semestre_id dans les ue, en regardant
- semestre_idx ou à défaut, pour les formations non APC, le premier module
- de chacune.
- Les UE sans modules se voient attribuer le numero UE_SEM_DEFAULT (1000000),
- qui les place à la fin de la liste.
- """
- for ue in ues:
- if ue["semestre_idx"] is not None:
- ue["semestre_id"] = ue["semestre_idx"]
- elif is_apc:
- ue["semestre_id"] = codes_cursus.UE_SEM_DEFAULT
- else:
- # était le comportement ScoDoc7
- 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
-
-
-def next_ue_numero(formation_id, semestre_id=None):
+def next_ue_numero(formation_id, semestre_id=None) -> int:
"""Numero d'une nouvelle UE dans cette formation.
Si le semestre est specifie, cherche les UE ayant des modules de ce semestre
"""
formation = db.session.get(Formation, formation_id)
- ues = ue_list(args={"formation_id": formation_id})
+ ues = formation.ues.all()
if not ues:
return 0
if semestre_id is None:
- return ues[-1]["numero"] + 1000
- else:
- # Avec semestre: (prend le semestre du 1er module de l'UE)
- _add_ue_semestre_id(ues, formation.get_cursus().APC_SAE)
- ue_list_semestre = [ue for ue in ues if ue["semestre_id"] == semestre_id]
- if ue_list_semestre:
- return ue_list_semestre[-1]["numero"] + 10
- else:
- return ues[-1]["numero"] + 1000
+ return ues[-1].numero + 1000
+ # Avec semestre: (prend le semestre du 1er module de l'UE)
+ ue_list_semestre = [ue for ue in ues if ue.get_semestre_id() == semestre_id]
+ if ue_list_semestre:
+ return ue_list_semestre[-1].numero + 10
+ return ues[-1].numero + 1000
def ue_delete(ue_id=None, delete_validations=False, dialog_confirmed=False):
@@ -698,20 +673,22 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""):
show_tags = scu.to_bool(request.args.get("show_tags", 0))
locked = formation.has_locked_sems(semestre_idx)
semestre_ids = range(1, parcours.NB_SEM + 1)
- # transition: on requete ici via l'ORM mais on utilise les fonctions ScoDoc7
- # basées sur des dicts
- ues_obj = UniteEns.query.filter_by(
- formation_id=formation_id, is_external=False
- ).order_by(UniteEns.semestre_idx, UniteEns.numero)
+
+ ues = (
+ formation.ues.filter_by(is_external=False)
+ .order_by(UniteEns.semestre_idx, UniteEns.numero)
+ .all()
+ )
+
# safety check: renumérote les ue s'il en manque ou s'il y a des ex-aequo.
# cela facilite le travail de la passerelle !
- numeros = {ue.numero for ue in ues_obj}
- if (None in numeros) or len(numeros) < ues_obj.count():
- scu.objects_renumber(db, ues_obj)
+ numeros = {ue.numero for ue in ues}
+ if (None in numeros) or len(numeros) < len(ues):
+ scu.objects_renumber(db, ues)
- ues_externes_obj = UniteEns.query.filter_by(
+ ues_externes = UniteEns.query.filter_by(
formation_id=formation_id, is_external=True
- )
+ ).all()
# liste ordonnée des formsemestres de cette formation:
formsemestres = sorted(
FormSemestre.query.filter_by(formation_id=formation_id).all(),
@@ -721,29 +698,25 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""):
if is_apc:
# Pour faciliter la transition des anciens programmes non APC
- for ue in ues_obj:
+ for ue in ues:
ue.guess_semestre_idx()
# vérifie qu'on a bien au moins une matière dans chaque UE
if ue.matieres.count() < 1:
mat = Matiere(ue_id=ue.id)
db.session.add(mat)
# donne des couleurs aux UEs crées avant
- colorie_anciennes_ues(ues_obj)
+ colorie_anciennes_ues(ues)
db.session.commit()
- ues = [ue.to_dict() for ue in ues_obj]
- ues_externes = [ue.to_dict() for ue in ues_externes_obj]
# tri par semestre et numero:
- _add_ue_semestre_id(ues, is_apc)
- _add_ue_semestre_id(ues_externes, is_apc)
- ues.sort(key=lambda u: (u["semestre_id"], u["numero"]))
- ues_externes.sort(key=lambda u: (u["semestre_id"], u["numero"]))
+ ues.sort(key=lambda u: (u.get_semestre_id(), u.numero))
+ ues_externes.sort(key=lambda u: (u.get_semestre_id(), u.numero))
# Codes dupliqués (pour aider l'utilisateur)
seen = set()
duplicated_codes = {
- ue["ue_code"] for ue in ues if ue["ue_code"] in seen or seen.add(ue["ue_code"])
+ ue.ue_code for ue in ues if ue.ue_code in seen or seen.add(ue.ue_code)
}
- ues_with_duplicated_code = [ue for ue in ues if ue["ue_code"] in duplicated_codes]
+ ues_with_duplicated_code = [ue for ue in ues if ue.ue_code in duplicated_codes]
has_perm_change = current_user.has_permission(Permission.EditFormation)
# editable = (not locked) and has_perm_change
@@ -799,8 +772,8 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
formation ont le même code : {
', '.join([
'' + ue["acronyme"] + " (code " + ue["ue_code"] + ")"
+ scodoc_dept=g.scodoc_dept, ue_id=ue.id )
+ + '">' + ue.acronyme + " (code " + ue.ue_code + ")"
for ue in ues_with_duplicated_code ])
}.
Il faut corriger cela, sinon les capitalisations et ECTS seront
@@ -1115,7 +1088,7 @@ def _html_select_semestre_idx(formation_id, semestre_ids, semestre_idx):
def _ue_table_ues(
parcours,
- ues: list[dict],
+ ues: list[UniteEns],
editable,
tag_editable,
has_perm_change,
@@ -1132,33 +1105,25 @@ def _ue_table_ues(
cur_ue_semestre_id = None
iue = 0
for ue in ues:
- if ue["ects"] is None:
- ue["ects_str"] = ""
- else:
- ue["ects_str"] = ", %g ECTS" % ue["ects"]
- if editable:
- klass = "span_apo_edit"
- else:
- klass = ""
+ ects_str = "" if ue.ects is None else f", {ue.ects:g} ECTS"
+ klass = "span_apo_edit" if editable else ""
edit_url = url_for(
"apiweb.ue_set_code_apogee",
scodoc_dept=g.scodoc_dept,
- ue_id=ue["ue_id"],
+ ue_id=ue.id,
)
- ue[
- "code_apogee_str"
- ] = f""", Apo: {
- ue["code_apogee"] or ""
+ ue.code_apogee or ""
}"""
- if cur_ue_semestre_id != ue["semestre_id"]:
- cur_ue_semestre_id = ue["semestre_id"]
- if ue["semestre_id"] == codes_cursus.UE_SEM_DEFAULT:
+ if cur_ue_semestre_id != ue.semestre_id:
+ cur_ue_semestre_id = ue.semestre_id
+ if ue.semestre_id == codes_cursus.UE_SEM_DEFAULT:
lab = "Pas d'indication de semestre:"
else:
- lab = f"""Semestre {ue["semestre_id"]}:"""
+ lab = f"""Semestre {ue.semestre_id}:"""
H.append(
f'
{lab}
'
)
@@ -1166,52 +1131,55 @@ def _ue_table_ues(
H.append('
')
if iue != 0 and editable:
H.append(
- '%s'
- % (ue["ue_id"], arrow_up)
+ f"""{arrow_up}"""
)
else:
H.append(arrow_none)
if iue < len(ues) - 1 and editable:
H.append(
- '%s'
- % (ue["ue_id"], arrow_down)
+ f"""{arrow_down}"""
)
else:
H.append(arrow_none)
- ue["acro_titre"] = str(ue["acronyme"])
- if ue["titre"] != ue["acronyme"]:
- ue["acro_titre"] += " " + str(ue["titre"])
+ acro_titre = ue.acronyme
+ if ue.titre != ue.acronyme:
+ acro_titre += " " + (ue.titre or "")
H.append(
- """%(acro_titre)s (code %(ue_code)s%(ects_str)s, coef. %(coefficient)3.2f%(code_apogee_str)s)
+ f"""acro_titre (code {ue.ue_code}{ects_str}, coef. {
+ (ue.coefficient or 0):3.2f}{code_apogee_str})
"""
- % ue
)
- if ue["type"] != codes_cursus.UE_STANDARD:
+ if ue.type != codes_cursus.UE_STANDARD:
H.append(
- '%s'
- % codes_cursus.UE_TYPE_NAME[ue["type"]]
+ f"""{codes_cursus.UE_TYPE_NAME[ue.type]}"""
)
- if ue["is_external"]:
+ if ue.is_external:
# Cas spécial: si l'UE externe a plus d'un module, c'est peut être une UE
# qui a été déclarée externe par erreur (ou suite à un bug d'import/export xml)
# Dans ce cas, propose de changer le type (même si verrouillée)
- if len(sco_moduleimpl.moduleimpls_in_external_ue(ue["ue_id"])) > 1:
+ if len(sco_moduleimpl.moduleimpls_in_external_ue(ue.id)) > 1:
H.append('')
if has_perm_change:
H.append(
f"""transformer en UE ordinaire """
)
H.append("")
- ue_locked, ue_locked_reason = ue_is_locked(ue["ue_id"])
+ ue_locked, ue_locked_reason = ue.is_locked()
ue_editable = editable and not ue_locked
if ue_editable:
H.append(
f"""modifier"""
)
else:
@@ -1231,11 +1199,14 @@ def _ue_table_ues(
delete_disabled_icon,
)
)
- if (iue >= len(ues) - 1) or ue["semestre_id"] != ues[iue + 1]["semestre_id"]:
+ if (iue >= len(ues) - 1) or (
+ ue.get_semestre_id() != ues[iue + 1].get_semestre_id()
+ ):
H.append(
- f"""
+ f"""
"""
)
@@ -1246,7 +1217,7 @@ def _ue_table_ues(
def _ue_table_matieres(
parcours,
- ue_dict: dict,
+ ue: UniteEns,
editable,
tag_editable,
arrow_up,
@@ -1256,7 +1227,6 @@ 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('')
@@ -1503,16 +1473,18 @@ def do_ue_edit(args, bypass_lock=False, dont_invalidate_cache=False):
"edit an UE"
# check
ue_id = args["ue_id"]
- ue = ue_list({"ue_id": ue_id})[0]
+ ue = UniteEns.get_ue(ue_id)
if not bypass_lock:
- ue_locked, ue_locked_reason = ue_is_locked(ue["ue_id"])
+ ue_locked, ue_locked_reason = ue.is_locked()
if ue_locked:
raise ScoLockedFormError(msg=f"UE verrouillée: {ue_locked_reason}")
# check: acronyme unique dans cette formation
if "acronyme" in args:
new_acro = args["acronyme"]
- ues = ue_list({"formation_id": ue["formation_id"], "acronyme": new_acro})
- if ues and ues[0]["ue_id"] != ue_id:
+ ues = UniteEns.query.filter_by(
+ formation_id=ue.formation_id, acronyme=new_acro
+ ).all()
+ if ues and ues[0].id != ue_id:
raise ScoValueError(
f"""Acronyme d'UE "{args['acronyme']}" déjà utilisé !
(chaque UE doit avoir un acronyme unique dans la formation.)"""
@@ -1521,48 +1493,12 @@ def do_ue_edit(args, bypass_lock=False, dont_invalidate_cache=False):
if "ue_code" in args and not args["ue_code"]:
del args["ue_code"]
- cnx = ndb.GetDBConnexion()
- _ueEditor.edit(cnx, args)
-
- formation = db.session.get(Formation, ue["formation_id"])
+ ue.from_dict(args)
+ db.session.commit()
if not dont_invalidate_cache:
# Invalide les semestres utilisant cette formation
# ainsi que les poids et coefs
- formation.invalidate_module_coefs()
-
-
-def ue_is_locked(ue_id: int) -> tuple[bool, str]:
- """True if UE should not be modified:
- utilisée dans un formsemestre verrouillé ou validations de jury de cette UE.
- Renvoie aussi une explication.
- """
- # before 9.7.23: contains modules used in a locked formsemestre
- # starting from 9.7.23: + existence de validations de jury de cette UE
- ue = UniteEns.query.get(ue_id)
- if not ue:
- return True, "inexistante"
- if ue.formation.is_apc():
- # en APC, interdit toute modification d'UE si utilisée dans un semestre verrouillé
- if False in [formsemestre.etat for formsemestre in ue.formation.formsemestres]:
- return True, "utilisée dans un semestre verrouillé"
- else:
- # en classique: interdit si contient des modules utilisés dans des semestres verrouillés
- # en effet, dans certaines (très anciennes) formations, une UE peut avoir des modules de
- # différents semestre
- if (
- Module.query.filter(Module.ue_id == ue_id)
- .join(Module.modimpls)
- .join(ModuleImpl.formsemestre)
- .filter_by(etat=False)
- .count()
- ):
- return True, "avec modules utilisés dans des semestres verrouillés"
-
- nb_validations = ScolarFormSemestreValidation.query.filter_by(ue_id=ue_id).count()
- if nb_validations > 0:
- return True, f"avec {nb_validations} validations de jury"
-
- return False, ""
+ ue.formation.invalidate_module_coefs()
UE_PALETTE = [
diff --git a/app/models/ues.py b/app/models/ues.py
index 3e74b88b1..f7ad5ea70 100644
--- a/app/models/ues.py
+++ b/app/models/ues.py
@@ -10,6 +10,7 @@ from app.models import APO_CODE_STR_LEN
from app.models import SHORT_STR_LEN
from app.models.but_refcomp import ApcNiveau, ApcParcours
from app.models.modules import Module
+from app.scodoc import codes_cursus
from app.scodoc import sco_utils as scu
@@ -185,10 +186,45 @@ class UniteEns(models.ScoDocModel):
return 1 if self.semestre_idx is None else (self.semestre_idx - 1) // 2 + 1
def is_locked(self) -> tuple[bool, str]:
- """True if UE should not be modified"""
- from app.formations import edit_ue
+ """True if UE should not be modified:
+ utilisée dans un formsemestre verrouillé ou validations de jury de cette UE.
+ Renvoie aussi une explication.
+ """
+ from app.models import FormSemestre, ModuleImpl, ScolarFormSemestreValidation
- return edit_ue.ue_is_locked(self.id)
+ # before 9.7.23: contains modules used in a locked formsemestre
+ # starting from 9.7.23: + existence de validations de jury de cette UE
+ if self.formation.is_apc():
+ # en APC, interdit toute modification d'UE si il y a un formsemestre verrouillé
+ # de cette formation ayant le semestre de cette UE.
+ # (ne détaille pas les parcours, donc si un semestre Sn d'un parcours est verrouillé
+ # cela va verrouiller toutes les UE d'indice Sn, même si pas de ce parcours)
+ # modifié en 9.7.28
+ locked_sems = self.formation.formsemestres.filter_by(
+ etat=False, semestre_id=self.semestre_idx
+ )
+ if locked_sems.count():
+ return True, "utilisée dans un semestre verrouillé"
+ else:
+ # en classique: interdit si contient des modules utilisés dans des semestres verrouillés
+ # en effet, dans certaines (très anciennes) formations, une UE peut avoir des modules de
+ # différents semestre
+ if (
+ Module.query.filter(Module.ue_id == self.id)
+ .join(Module.modimpls)
+ .join(ModuleImpl.formsemestre)
+ .filter_by(etat=False)
+ .count()
+ ):
+ return True, "avec modules utilisés dans des semestres verrouillés"
+
+ nb_validations = ScolarFormSemestreValidation.query.filter_by(
+ ue_id=self.id
+ ).count()
+ if nb_validations > 0:
+ return True, f"avec {nb_validations} validations de jury"
+
+ return False, ""
def can_be_deleted(self) -> bool:
"""True si l'UE n'a pas de moduleimpl rattachés
@@ -214,6 +250,24 @@ class UniteEns(models.ScoDocModel):
db.session.commit()
return self.semestre_idx
+ def get_semestre_id(self) -> int:
+ """L'indice du semestre de l'UE.
+ Regarde semestre_idx ou, pour les formations non APC,
+ le premier module de chacune.
+ Les UE sans modules se voient attribuer le numero UE_SEM_DEFAULT (1000000),
+ qui les place à la fin de la liste.
+ Contrairement à guess_semestre_idx, ne modifie pas l'UE.
+ """
+ if self.semestre_idx is not None:
+ return self.semestre_idx
+ if self.formation.is_apc():
+ return codes_cursus.UE_SEM_DEFAULT
+ # était le comportement ScoDoc7
+ module = self.modules.first()
+ if module:
+ return module.semestre_id
+ return codes_cursus.UE_SEM_DEFAULT
+
def get_ects(self, parcour: ApcParcours = None, only_parcours=False) -> float:
"""Crédits ECTS associés à cette UE.
En BUT, cela peut quelquefois dépendre du parcours.