WIP: BUT association modules <-> parcours

This commit is contained in:
Emmanuel Viennet 2022-05-01 23:58:41 +02:00
parent 5d7085b858
commit 72dc72d286
9 changed files with 441 additions and 135 deletions

View File

@ -1,14 +1,25 @@
# -*- coding: UTF-8 -* # -*- coding: UTF-8 -*
"""Modèles base de données ScoDoc """Modèles base de données ScoDoc
XXX version préliminaire ScoDoc8 #sco8 sans département
""" """
import sqlalchemy
CODE_STR_LEN = 16 # chaine pour les codes CODE_STR_LEN = 16 # chaine pour les codes
SHORT_STR_LEN = 32 # courtes chaine, eg acronymes SHORT_STR_LEN = 32 # courtes chaine, eg acronymes
APO_CODE_STR_LEN = 512 # nb de car max d'un code Apogée (il peut y en avoir plusieurs) APO_CODE_STR_LEN = 512 # nb de car max d'un code Apogée (il peut y en avoir plusieurs)
GROUPNAME_STR_LEN = 64 GROUPNAME_STR_LEN = 64
convention = {
"ix": "ix_%(column_0_label)s",
"uq": "uq_%(table_name)s_%(column_0_name)s",
"ck": "ck_%(table_name)s_%(constraint_name)s",
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
"pk": "pk_%(table_name)s",
}
metadata_obj = sqlalchemy.MetaData(naming_convention=convention)
from app.models.raw_sql_init import create_database_functions from app.models.raw_sql_init import create_database_functions
from app.models.absences import Absence, AbsenceNotification, BilletAbsence from app.models.absences import Absence, AbsenceNotification, BilletAbsence

View File

@ -321,6 +321,21 @@ ApcAppCritiqueModules = db.Table(
) )
parcours_modules = db.Table(
"parcours_modules",
db.Column(
"parcours_id", db.Integer, db.ForeignKey("apc_parcours.id"), primary_key=True
),
db.Column(
"module_id",
db.Integer,
db.ForeignKey("notes_modules.id", ondelete="CASCADE"),
primary_key=True,
),
)
"""Association parcours <-> modules (many-to-many)"""
class ApcParcours(db.Model, XMLModel): class ApcParcours(db.Model, XMLModel):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
referentiel_id = db.Column( referentiel_id = db.Column(
@ -335,6 +350,11 @@ class ApcParcours(db.Model, XMLModel):
lazy="dynamic", lazy="dynamic",
cascade="all, delete-orphan", cascade="all, delete-orphan",
) )
# modules = db.relationship(
# "Module",
# secondary=parcours_modules,
# back_populates="parcours",
# )
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__} {self.code}>" return f"<{self.__class__.__name__} {self.code}>"

View File

@ -3,6 +3,7 @@
from app import db from app import db
from app.models import APO_CODE_STR_LEN from app.models import APO_CODE_STR_LEN
from app.models.but_refcomp import parcours_modules
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.sco_codes_parcours import UE_SPORT from app.scodoc.sco_codes_parcours import UE_SPORT
from app.scodoc.sco_utils import ModuleType from app.scodoc.sco_utils import ModuleType
@ -44,6 +45,14 @@ class Module(db.Model):
lazy=True, lazy=True,
backref=db.backref("modules", lazy=True), backref=db.backref("modules", lazy=True),
) )
# BUT
parcours = db.relationship(
"ApcParcours",
secondary=parcours_modules,
lazy="subquery",
# cascade="all, delete",
backref=db.backref("modules", lazy=True),
)
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.ue_coefs = [] self.ue_coefs = []

View File

@ -134,7 +134,10 @@ def html_edit_formation_apc(
tag_editable=tag_editable, tag_editable=tag_editable,
icons=icons, icons=icons,
scu=scu, scu=scu,
), semestre_id=semestre_idx,
)
if ues_by_sem[semestre_idx].count() > 0
else "",
render_template( render_template(
"pn/form_mods.html", "pn/form_mods.html",
formation=formation, formation=formation,
@ -147,7 +150,10 @@ def html_edit_formation_apc(
tag_editable=tag_editable, tag_editable=tag_editable,
icons=icons, icons=icons,
scu=scu, scu=scu,
), semestre_id=semestre_idx,
)
if ues_by_sem[semestre_idx].count() > 0
else "",
render_template( render_template(
"pn/form_mods.html", "pn/form_mods.html",
formation=formation, formation=formation,
@ -159,7 +165,10 @@ def html_edit_formation_apc(
tag_editable=tag_editable, tag_editable=tag_editable,
icons=icons, icons=icons,
scu=scu, scu=scu,
), semestre_id=semestre_idx,
)
if ues_by_sem[semestre_idx].count() > 0
else """<span class="fontred">créer une UE pour pouvoir ajouter des modules</span>""",
] ]
return "\n".join(H) return "\n".join(H)

View File

@ -33,12 +33,13 @@ from flask import url_for, render_template
from flask import g, request from flask import g, request
from flask_login import current_user from flask_login import current_user
from app import log from app import db, log
from app import models from app import models
from app.models import APO_CODE_STR_LEN from app.models import APO_CODE_STR_LEN
from app.models import Formation, Matiere, Module, UniteEns from app.models import Formation, Matiere, Module, UniteEns
from app.models import FormSemestre, ModuleImpl from app.models import FormSemestre, ModuleImpl
from app.models import ScolarNews from app.models import ScolarNews
from app.models.but_refcomp import ApcParcours
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -121,6 +122,13 @@ def module_create(
Sinon, donne le choix de l'UE de rattachement et utilise la première Sinon, donne le choix de l'UE de rattachement et utilise la première
matière de cette UE (si elle n'existe pas, la crée). 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,
)
if matiere_id: if matiere_id:
matiere = Matiere.query.get_or_404(matiere_id) matiere = Matiere.query.get_or_404(matiere_id)
ue = matiere.ue ue = matiere.ue
@ -472,30 +480,56 @@ def check_module_code_unicity(code, field, formation_id, module_id=None):
return len(Mods) == 0 return len(Mods) == 0
def module_edit(module_id=None): def module_edit(
"""Edit a module""" module_id=None,
from app.scodoc import sco_formations create=False,
matiere_id=None,
module_type=None,
semestre_id=None,
formation_id=None,
):
"""Formulaire édition ou création module.
Si create, création nouveau 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).
"""
from app.scodoc import sco_tag_module from app.scodoc import sco_tag_module
if not module_id: # --- Détermination de la formation
raise ScoValueError("invalid module !") orig_semestre_idx = None
modules = module_list(args={"module_id": module_id}) if create:
if not modules: if matiere_id:
raise ScoValueError("invalid module !") matiere = Matiere.query.get_or_404(matiere_id)
module = modules[0] ue = matiere.ue
a_module = models.Module.query.get(module_id) formation = ue.formation
unlocked = not module_is_locked(module_id) orig_semestre_idx = ue.semestre_idx if semestre_id is None else semestre_id
formation_id = module["formation_id"] else:
formation = sco_formations.formation_list(args={"formation_id": formation_id})[0] formation = Formation.query.get_or_404(formation_id)
parcours = sco_codes_parcours.get_parcours_from_code(formation["type_parcours"]) module = None
unlocked = True
else:
if not module_id:
raise ValueError("missing module_id !")
module = models.Module.query.get_or_404(module_id)
module_dict = module.to_dict()
formation = module.formation
unlocked = not module_is_locked(module_id)
parcours = sco_codes_parcours.get_parcours_from_code(formation.type_parcours)
is_apc = parcours.APC_SAE # BUT is_apc = parcours.APC_SAE # BUT
in_use = len(a_module.modimpls.all()) > 0 # il y a des modimpls if not create:
orig_semestre_idx = module.ue.semestre_idx if is_apc else module.semestre_id
if orig_semestre_idx is None:
orig_semestre_idx = 1
# il y a-t-il des modimpls ?
in_use = (module is not None) and (len(module.modimpls.all()) > 0)
matieres = Matiere.query.filter( matieres = Matiere.query.filter(
Matiere.ue_id == UniteEns.id, UniteEns.formation_id == formation_id Matiere.ue_id == UniteEns.id, UniteEns.formation_id == formation.id
).order_by(UniteEns.semestre_idx, UniteEns.numero, Matiere.numero) ).order_by(UniteEns.semestre_idx, UniteEns.numero, Matiere.numero)
if in_use: if in_use:
# restreint aux matières du même semestre # restreint aux matières du même semestre
matieres = matieres.filter(UniteEns.semestre_idx == a_module.ue.semestre_idx) matieres = matieres.filter(UniteEns.semestre_idx == module.ue.semestre_idx)
if is_apc: if is_apc:
# ne conserve que la 1ere matière de chaque UE, # ne conserve que la 1ere matière de chaque UE,
@ -503,7 +537,8 @@ def module_edit(module_id=None):
matieres = [ matieres = [
mat mat
for mat in matieres for mat in matieres
if a_module.matiere.id == mat.id or mat.id == mat.ue.matieres.first().id if ((module is not None) and (module.matiere.id == mat.id))
or (mat.id == mat.ue.matieres.first().id)
] ]
mat_names = [ mat_names = [
"S%s / %s" % (mat.ue.semestre_idx, mat.ue.acronyme) for mat in matieres "S%s / %s" % (mat.ue.semestre_idx, mat.ue.acronyme) for mat in matieres
@ -511,14 +546,43 @@ def module_edit(module_id=None):
else: else:
mat_names = ["%s / %s" % (mat.ue.acronyme, mat.titre or "") for mat in matieres] mat_names = ["%s / %s" % (mat.ue.acronyme, mat.titre or "") for mat in matieres]
ue_mat_ids = ["%s!%s" % (mat.ue.id, mat.id) for mat in matieres] if module: # edition
module["ue_matiere_id"] = "%s!%s" % (module["ue_id"], module["matiere_id"]) 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"],
)
semestres_indices = list(range(1, parcours.NB_SEM + 1)) semestres_indices = list(range(1, parcours.NB_SEM + 1))
# Toutes les UE de la formation (tout parcours):
ues = formation.ues.order_by(
UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme
).all()
# --- Titre de la page
if create:
if is_apc and module_type is not None:
object_name = scu.MODULE_TYPE_NAMES[module_type]
else:
object_name = "Module"
page_title = f"Création {object_name}"
if matiere_id:
title = f"""Création {object_name} dans la matière
{matiere.titre},
(UE {ue.acronyme}), semestre {ue.semestre_idx}
"""
else:
title = f"""Création {object_name} dans la formation
{formation.acronyme}"""
else:
page_title = "Modification du module {module.code or module.titre or ''}"
title = f"""Modification du module {module.code or ''} {module.titre or ''}
(formation {formation.acronyme}, version {formation.version})
"""
H = [ H = [
html_sco_header.sco_header( html_sco_header.sco_header(
page_title=f"Modification du module {a_module.code or a_module.titre or ''}", page_title=page_title,
cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css"], cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css"],
javascripts=[ javascripts=[
"libjs/jQuery-tagEditor/jquery.tag-editor.min.js", "libjs/jQuery-tagEditor/jquery.tag-editor.min.js",
@ -526,17 +590,19 @@ def module_edit(module_id=None):
"js/module_tag_editor.js", "js/module_tag_editor.js",
], ],
), ),
f"""<h2>Modification du module {a_module.code or ''} {a_module.titre or ''}""", f"""<h2>{title}</h2>""",
""" (formation %(acronyme)s, version %(version)s)</h2>""" % formation,
render_template( render_template(
"scodoc/help/modules.html", "scodoc/help/modules.html",
is_apc=is_apc, is_apc=is_apc,
semestre_id=semestre_id,
formsemestres=FormSemestre.query.filter( formsemestres=FormSemestre.query.filter(
ModuleImpl.formsemestre_id == FormSemestre.id, ModuleImpl.formsemestre_id == FormSemestre.id,
ModuleImpl.module_id == module_id, ModuleImpl.module_id == module_id,
) )
.order_by(FormSemestre.date_debut) .order_by(FormSemestre.date_debut)
.all(), .all()
if not create
else None,
), ),
] ]
if not unlocked: if not unlocked:
@ -547,28 +613,55 @@ def module_edit(module_id=None):
module_types = scu.ModuleType # tous les types module_types = scu.ModuleType # tous les types
else: else:
# ne propose pas SAE et Ressources, sauf si déjà de ce type... # ne propose pas SAE et Ressources, sauf si déjà de ce type...
module_types = ( module_types = set(scu.ModuleType) - {
set(scu.ModuleType) - {scu.ModuleType.RESSOURCE, scu.ModuleType.SAE} scu.ModuleType.RESSOURCE,
) | { scu.ModuleType.SAE,
scu.ModuleType(a_module.module_type)
if a_module.module_type
else scu.ModuleType.STANDARD
} }
if module:
module_types |= {
scu.ModuleType(module.module_type)
if module.module_type
else scu.ModuleType.STANDARD
}
# Numéro du module
# cherche le numero adéquat (pour placer le module en fin de liste)
if module:
default_num = module.numero
else:
modules = formation.modules.all()
if modules:
default_num = max([m.numero or 0 for m in modules]) + 10
else:
default_num = 10
descr = [ descr = [
( (
"code", "code",
{ {
"size": 10, "size": 10,
"explanation": "code du module (issu du programme, exemple M1203 ou R2.01. Doit être unique dans la formation)", "explanation": "code du module (issu du programme, exemple M1203, R2.01 , ou SAÉ 3.4. Doit être unique dans la formation)",
"allow_null": False, "allow_null": False,
"validator": lambda val, field, formation_id=formation_id: check_module_code_unicity( "validator": lambda val, field, formation_id=formation.id: check_module_code_unicity(
val, field, formation_id, module_id=module_id val, field, formation_id, module_id=module.id if module else None
), ),
}, },
), ),
("titre", {"size": 30, "explanation": "nom du module"}), (
("abbrev", {"size": 20, "explanation": "nom abrégé (pour bulletins)"}), "titre",
{
"size": 30,
"explanation": """nom du module. Exemple:
<em>Introduction à la démarche ergonomique</em>""",
},
),
(
"abbrev",
{
"size": 20,
"explanation": """nom abrégé (pour bulletins).
Exemple: <em>Intro. à l'ergonomie</em>""",
},
),
( (
"module_type", "module_type",
{ {
@ -583,50 +676,63 @@ def module_edit(module_id=None):
( (
"heures_cours", "heures_cours",
{ {
"title": "Heures CM :", "title": "Heures cours :",
"size": 4, "size": 4,
"type": "float", "type": "float",
"explanation": "nombre d'heures de cours", "explanation": "nombre d'heures de cours (optionnel)",
}, },
), ),
( (
"heures_td", "heures_td",
{ {
"title": "Heures TD :", "title": "Heures de TD :",
"size": 4, "size": 4,
"type": "float", "type": "float",
"explanation": "nombre d'heures de Travaux Dirigés", "explanation": "nombre d'heures de Travaux Dirigés (optionnel)",
}, },
), ),
( (
"heures_tp", "heures_tp",
{ {
"title": "Heures TP :", "title": "Heures de TP :",
"size": 4, "size": 4,
"type": "float", "type": "float",
"explanation": "nombre d'heures de Travaux Pratiques", "explanation": "nombre d'heures de Travaux Pratiques (optionnel)",
}, },
), ),
] ]
if is_apc: if is_apc:
coefs_lst = a_module.ue_coefs_list() if module:
if coefs_lst: coefs_lst = module.ue_coefs_list()
coefs_descr_txt = ", ".join( if coefs_lst:
[f"{ue.acronyme}: {c}" for (ue, c) in coefs_lst] coefs_descr_txt = ", ".join(
) [f"{ue.acronyme}: {c}" for (ue, c) in coefs_lst]
)
else:
coefs_descr_txt = """<span class="missing_value">non définis</span>"""
descr += [
(
"ue_coefs",
{
"readonly": True,
"title": "Coefficients vers les UE ",
"default": coefs_descr_txt,
"explanation": " <br>(passer par la page d'édition de la formation pour modifier les coefficients)",
},
)
]
else: else:
coefs_descr_txt = """<span class="missing_value">non définis</span>""" descr += [
descr += [ (
( "sep_ue_coefs",
"ue_coefs", {
{ "input_type": "separator",
"readonly": True, "title": """
"title": "Coefficients vers les UE ", <div>(<em>les coefficients vers les UE se fixent sur la page dédiée</em>)
"default": coefs_descr_txt, </div>""",
"explanation": " <br>(passer par la page d'édition de la formation pour modifier les coefficients)", },
}, ),
) ]
]
else: # Module classique avec coef scalaire: else: # Module classique avec coef scalaire:
descr += [ descr += [
( (
@ -641,30 +747,64 @@ def module_edit(module_id=None):
), ),
] ]
descr += [ descr += [
("formation_id", {"input_type": "hidden"}),
("ue_id", {"input_type": "hidden"}),
("module_id", {"input_type": "hidden"}),
( (
"ue_matiere_id", "formation_id",
{ {
"input_type": "menu", "input_type": "hidden",
"title": "Rattachement :" if is_apc else "Matière :", "default": formation.id,
"explanation": (
"UE de rattachement"
+ (
" module utilisé, ne peut pas être changé de semestre"
if in_use
else ""
)
)
if is_apc
else "un module appartient à une seule matière.",
"labels": mat_names,
"allowed_values": ue_mat_ids,
"enabled": unlocked,
}, },
), ),
] ]
if module:
descr += [
("ue_id", {"input_type": "hidden"}),
("module_id", {"input_type": "hidden"}),
(
"ue_matiere_id",
{
"input_type": "menu",
"title": "Rattachement :" if is_apc else "Matière :",
"explanation": (
"UE de rattachement, utilisée notamment pour les malus"
+ (
" (module utilisé, ne peut pas être changé de semestre)"
if in_use
else ""
)
)
if is_apc
else "un module appartient à une seule matière.",
"labels": mat_names,
"allowed_values": ue_mat_ids,
"enabled": unlocked,
},
),
]
else: # Création
if matiere_id:
descr += [
("ue_id", {"default": ue.id, "input_type": "hidden"}),
("matiere_id", {"default": matiere_id, "input_type": "hidden"}),
]
else:
# choix de l'UE de rattachement
descr += [
(
"ue_id",
{
"input_type": "menu",
"type": "int",
"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}"
for u in ues
],
"allowed_values": [u.id for u in ues],
},
),
]
if is_apc: if is_apc:
# le semestre du module est toujours celui de son UE # le semestre du module est toujours celui de son UE
descr += [ descr += [
@ -710,17 +850,56 @@ def module_edit(module_id=None):
"numero", "numero",
{ {
"size": 2, "size": 2,
"explanation": "numéro (1,2,3,4...) pour ordre d'affichage", "explanation": "numéro (1, 2, 3, 4, ...) pour ordre d'affichage",
"type": "int", "type": "int",
"default": default_num,
}, },
), ),
] ]
# Choix des parcours
if is_apc:
ref_comp = formation.referentiel_competence
if ref_comp:
descr += [
(
"parcours",
{
"input_type": "checkbox",
"vertical": True,
"labels": [parcour.libelle for parcour in ref_comp.parcours],
"allowed_values": [
str(parcour.id) for parcour in ref_comp.parcours
],
"explanation": "parcours dans lesquels est utilisé ce module.",
},
)
]
if module:
module_dict["parcours"] = [
str(parcour.id) for parcour in module.parcours
]
else:
descr += [
(
"parcours",
{
"input_type": "separator",
"title": f"""<span class="fontred">Pas de parcours:
<a class="stdlink" href="{ url_for('notes.refcomp_assoc_formation',
scodoc_dept=g.scodoc_dept, formation_id=formation.id)
}">associer un référentiel de compétence</a>
</span>""",
},
)
]
# force module semestre_idx to its UE # force module semestre_idx to its UE
if a_module.ue.semestre_idx: if module:
module["semestre_id"] = a_module.ue.semestre_idx if module.ue.semestre_idx is None:
# Filet de sécurité si jamais l'UE n'a pas non plus de semestre: # Filet de sécurité si jamais l'UE n'a pas non plus de semestre:
if not module["semestre_id"]: module_dict["semestre_id"] = 1
module["semestre_id"] = 1 else:
module_dict["semestre_id"] = module.ue.semestre_idx
tf = TrivialFormulator( tf = TrivialFormulator(
request.base_url, request.base_url,
scu.get_request_args(), scu.get_request_args(),
@ -728,8 +907,9 @@ def module_edit(module_id=None):
html_foot_markup="""<div style="width: 90%;"><span class="sco_tag_edit"><textarea data-module_id="{}" class="module_tag_editor">{}</textarea></span></div>""".format( html_foot_markup="""<div style="width: 90%;"><span class="sco_tag_edit"><textarea data-module_id="{}" class="module_tag_editor">{}</textarea></span></div>""".format(
module_id, ",".join(sco_tag_module.module_tag_list(module_id)) module_id, ",".join(sco_tag_module.module_tag_list(module_id))
), ),
initvalues=module, initvalues=module_dict if module else {},
submitlabel="Modifier ce module", submitlabel="Modifier ce module" if module else "Créer ce module",
cancelbutton="Annuler",
) )
# #
if tf[0] == 0: if tf[0] == 0:
@ -739,38 +919,66 @@ def module_edit(module_id=None):
url_for( url_for(
"notes.ue_table", "notes.ue_table",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formation_id=formation_id, formation_id=formation.id,
semestre_idx=module["semestre_id"], semestre_idx=orig_semestre_idx,
) )
) )
else: else:
# l'UE de rattachement peut changer if create:
tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!") if not matiere_id:
x, y = tf[2]["ue_matiere_id"].split("!") # formulaire avec choix UE de rattachement
tf[2]["ue_id"] = int(x) ue = UniteEns.query.get(tf[2]["ue_id"])
tf[2]["matiere_id"] = int(y) if ue is None:
old_ue_id = a_module.ue.id raise ValueError("UE invalide")
new_ue_id = tf[2]["ue_id"] matiere = ue.matieres.first()
if (old_ue_id != new_ue_id) and in_use: if matiere:
new_ue = UniteEns.query.get_or_404(new_ue_id) tf[2]["matiere_id"] = matiere.id
if new_ue.semestre_idx != a_module.ue.semestre_idx: else:
# pas changer de semestre un module utilisé ! matiere_id = sco_edit_matiere.do_matiere_create(
raise ScoValueError( {"ue_id": ue.id, "titre": ue.titre, "numero": 1},
"Module utilisé: il ne peut pas être changé de semestre !" )
) tf[2]["matiere_id"] = matiere_id
# En APC, force le semestre égal à celui de l'UE
if is_apc: tf[2]["semestre_id"] = ue.semestre_idx
selected_ue = UniteEns.query.get(tf[2]["ue_id"]) module_id = do_module_create(tf[2])
if selected_ue is None: module = Module.query.get(module_id)
raise ValueError("UE invalide") else: # EDITION MODULE
tf[2]["semestre_id"] = selected_ue.semestre_idx # l'UE de rattachement peut changer
# Check unicité code module dans la formation tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!")
do_module_edit(tf[2]) x, y = tf[2]["ue_matiere_id"].split("!")
tf[2]["ue_id"] = int(x)
tf[2]["matiere_id"] = int(y)
old_ue_id = module.ue.id
new_ue_id = tf[2]["ue_id"]
if (old_ue_id != new_ue_id) and in_use:
new_ue = UniteEns.query.get_or_404(new_ue_id)
if new_ue.semestre_idx != module.ue.semestre_idx:
# pas changer de semestre un module utilisé !
raise ScoValueError(
"Module utilisé: il ne peut pas être changé de semestre !"
)
# En APC, force le semestre égal à celui de l'UE
if is_apc:
selected_ue = UniteEns.query.get(tf[2]["ue_id"])
if selected_ue is None:
raise ValueError("UE invalide")
tf[2]["semestre_id"] = selected_ue.semestre_idx
# Check unicité code module dans la formation
# ??? TODO
#
do_module_edit(tf[2])
# Modifie les parcours
module.parcours = [
ApcParcours.query.get(int(parcour_id_str))
for parcour_id_str in tf[2]["parcours"]
]
db.session.add(module)
db.session.commit()
return flask.redirect( return flask.redirect(
url_for( url_for(
"notes.ue_table", "notes.ue_table",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formation_id=formation_id, formation_id=formation.id,
semestre_idx=tf[2]["semestre_id"], semestre_idx=tf[2]["semestre_id"],
) )
) )

View File

@ -255,7 +255,9 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
title = f"Modification de l'UE {ue.acronyme} {ue.titre}" title = f"Modification de l'UE {ue.acronyme} {ue.titre}"
initvalues = ue_dict initvalues = ue_dict
submitlabel = "Modifier les valeurs" submitlabel = "Modifier les valeurs"
can_change_semestre_id = (ue.modules.count() == 0) or (ue.semestre_idx is None) can_change_semestre_id = (
(ue.modules.count() == 0) or (ue.semestre_idx is None)
) and ue.niveau_competence is None
else: else:
ue = None ue = None
title = "Création d'une UE" title = "Création d'une UE"
@ -287,7 +289,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
f""" f"""
<h4>UE du semestre S{ue.semestre_idx}</h4> <h4>UE du semestre S{ue.semestre_idx}</h4>
""" """
if is_apc if is_apc and ue
else "", else "",
] ]
@ -1015,9 +1017,6 @@ def _ue_table_ues(
}">transformer en UE ordinaire</a>&nbsp;""" }">transformer en UE ordinaire</a>&nbsp;"""
) )
H.append("</span>") H.append("</span>")
breakpoint()
if ue.niveau_competence is None:
H.append(" pas de compétence associée ")
ue_editable = editable and not ue_is_locked(ue["ue_id"]) ue_editable = editable and not ue_is_locked(ue["ue_id"])
if ue_editable: if ue_editable:
H.append( H.append(

View File

@ -84,13 +84,15 @@
url_for("notes.module_create", url_for("notes.module_create",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
module_type=module_type|int, module_type=module_type|int,
matiere_id=matiere_parent.id matiere_id=matiere_parent.id,
semestre_id=semestre_id,
)}}" )}}"
{% else %}"{{ {% else %}"{{
url_for("notes.module_create", url_for("notes.module_create",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
module_type=module_type|int, module_type=module_type|int,
formation_id=formation.id formation_id=formation.id,
semestre_id=semestre_id,
)}}" )}}"
{% endif %} {% endif %}
>{{create_element_msg}}</a> >{{create_element_msg}}</a>

View File

@ -22,13 +22,39 @@ def upgrade():
"notes_ue", sa.Column("niveau_competence_id", sa.Integer(), nullable=True) "notes_ue", sa.Column("niveau_competence_id", sa.Integer(), nullable=True)
) )
op.create_foreign_key( op.create_foreign_key(
None, "notes_ue", "apc_niveau", ["niveau_competence_id"], ["id"] "notes_ue_niveau_competence_id_fkey",
"notes_ue",
"apc_niveau",
["niveau_competence_id"],
["id"],
)
op.create_table(
"parcours_modules",
sa.Column("parcours_id", sa.Integer(), nullable=False),
sa.Column("module_id", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(
["module_id"],
["notes_modules.id"],
# nom ajouté manuellement:
name="parcours_modules_module_id_fkey",
ondelete="CASCADE",
),
sa.ForeignKeyConstraint(
["parcours_id"],
["apc_parcours.id"],
# nom ajouté manuellement:
name="parcours_modules_parcours_id_fkey",
),
sa.PrimaryKeyConstraint("parcours_id", "module_id"),
) )
# ### end Alembic commands ### # ### end Alembic commands ###
def downgrade(): def downgrade():
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, "notes_ue", type_="foreignkey") op.drop_constraint(
"notes_ue_niveau_competence_id_fkey", "notes_ue", type_="foreignkey"
)
op.drop_column("notes_ue", "niveau_competence_id") op.drop_column("notes_ue", "niveau_competence_id")
op.drop_table("parcours_modules")
# ### end Alembic commands ### # ### end Alembic commands ###

View File

@ -4,35 +4,57 @@ Utiliser par exemple comme:
pytest tests/unit/test_refcomp.py pytest tests/unit/test_refcomp.py
""" """
import io
from flask import g from flask import g
import app
from app import db from app import db
from app import models from app import models
from app.but.import_refcomp import orebut_import_refcomp from app.but.import_refcomp import orebut_import_refcomp
from app.models import UniteEns
from app.models.but_refcomp import ( from app.models.but_refcomp import (
ApcReferentielCompetences, ApcReferentielCompetences,
ApcCompetence, ApcCompetence,
ApcSituationPro, ApcSituationPro,
ApcNiveau,
) )
from tests.unit import setup
REF_RT_XML = open(
"ressources/referentiels/but2022/competences/but-RT-05012022-081735.xml"
).read()
def test_but_refcomp(test_client): def test_but_refcomp(test_client):
"""modèles ref. comp.""" """modèles ref. comp."""
xml_data = open(
"ressources/referentiels/but2022/competences/but-RT-05012022-081735.xml"
).read()
dept_id = models.Departement.query.first().id dept_id = models.Departement.query.first().id
ref = orebut_import_refcomp(xml_data, dept_id) ref_comp: ApcReferentielCompetences = orebut_import_refcomp(REF_RT_XML, dept_id)
assert ref.competences.count() == 13 assert ref_comp.competences.count() == 13
assert ref.competences[0].situations.count() == 3 assert ref_comp.competences[0].situations.count() == 3
assert ref.competences[0].situations[0].libelle.startswith("Conception ") assert ref_comp.competences[0].situations[0].libelle.startswith("Conception ")
assert ( assert (
ref.competences[-1].situations[-1].libelle ref_comp.competences[-1].situations[-1].libelle
== "Administration des services multimédia" == "Administration des services multimédia"
) )
# test cascades on delete # test cascades on delete
db.session.delete(ref) db.session.delete(ref_comp)
db.session.commit() db.session.commit()
assert ApcCompetence.query.count() == 0 assert ApcCompetence.query.count() == 0
assert ApcSituationPro.query.count() == 0 assert ApcSituationPro.query.count() == 0
def test_but_assoc_ue_parcours(test_client):
"""Association UE / Niveau compétence"""
dept_id = models.Departement.query.first().id
G, formation_id, (ue1_id, ue2_id, ue3_id), module_ids = setup.build_formation_test()
ref_comp: ApcReferentielCompetences = orebut_import_refcomp(REF_RT_XML, dept_id)
ue = UniteEns.query.get(ue1_id)
assert ue.niveau_competence is None
niveau = ApcNiveau.query.first()
ue.niveau_competence = niveau
db.session.add(ue)
db.session.commit()
ue = UniteEns.query.get(ue1_id)
assert ue.niveau_competence == niveau
assert len(niveau.ues) == 1
assert niveau.ues[0] == ue