diff --git a/app/models/__init__.py b/app/models/__init__.py index d29b6bf3..d259966f 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -1,14 +1,25 @@ # -*- coding: UTF-8 -* """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 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) 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.absences import Absence, AbsenceNotification, BilletAbsence diff --git a/app/models/but_refcomp.py b/app/models/but_refcomp.py index 6a9205fb..3b286765 100644 --- a/app/models/but_refcomp.py +++ b/app/models/but_refcomp.py @@ -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): id = db.Column(db.Integer, primary_key=True) referentiel_id = db.Column( @@ -335,6 +350,11 @@ class ApcParcours(db.Model, XMLModel): lazy="dynamic", cascade="all, delete-orphan", ) + # modules = db.relationship( + # "Module", + # secondary=parcours_modules, + # back_populates="parcours", + # ) def __repr__(self): return f"<{self.__class__.__name__} {self.code}>" diff --git a/app/models/modules.py b/app/models/modules.py index 67ff3de0..5ffac904 100644 --- a/app/models/modules.py +++ b/app/models/modules.py @@ -3,6 +3,7 @@ from app import db 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.sco_codes_parcours import UE_SPORT from app.scodoc.sco_utils import ModuleType @@ -44,6 +45,14 @@ class Module(db.Model): 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): self.ue_coefs = [] diff --git a/app/scodoc/sco_edit_apc.py b/app/scodoc/sco_edit_apc.py index c53094ef..11bb53af 100644 --- a/app/scodoc/sco_edit_apc.py +++ b/app/scodoc/sco_edit_apc.py @@ -134,7 +134,10 @@ def html_edit_formation_apc( tag_editable=tag_editable, icons=icons, scu=scu, - ), + semestre_id=semestre_idx, + ) + if ues_by_sem[semestre_idx].count() > 0 + else "", render_template( "pn/form_mods.html", formation=formation, @@ -147,7 +150,10 @@ def html_edit_formation_apc( tag_editable=tag_editable, icons=icons, scu=scu, - ), + semestre_id=semestre_idx, + ) + if ues_by_sem[semestre_idx].count() > 0 + else "", render_template( "pn/form_mods.html", formation=formation, @@ -159,7 +165,10 @@ def html_edit_formation_apc( tag_editable=tag_editable, icons=icons, scu=scu, - ), + semestre_id=semestre_idx, + ) + if ues_by_sem[semestre_idx].count() > 0 + else """créer une UE pour pouvoir ajouter des modules""", ] return "\n".join(H) diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py index ece30a34..3aa00452 100644 --- a/app/scodoc/sco_edit_module.py +++ b/app/scodoc/sco_edit_module.py @@ -33,12 +33,13 @@ from flask import url_for, render_template from flask import g, request from flask_login import current_user -from app import log +from app import db, log from app import models from app.models import APO_CODE_STR_LEN from app.models import Formation, Matiere, Module, UniteEns from app.models import FormSemestre, ModuleImpl from app.models import ScolarNews +from app.models.but_refcomp import ApcParcours import app.scodoc.notesdb as ndb 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 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: matiere = Matiere.query.get_or_404(matiere_id) ue = matiere.ue @@ -472,30 +480,56 @@ def check_module_code_unicity(code, field, formation_id, module_id=None): return len(Mods) == 0 -def module_edit(module_id=None): - """Edit a module""" - from app.scodoc import sco_formations +def module_edit( + module_id=None, + 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 - if not module_id: - raise ScoValueError("invalid module !") - modules = module_list(args={"module_id": module_id}) - if not modules: - raise ScoValueError("invalid module !") - module = modules[0] - a_module = models.Module.query.get(module_id) - unlocked = not module_is_locked(module_id) - formation_id = module["formation_id"] - formation = sco_formations.formation_list(args={"formation_id": formation_id})[0] - parcours = sco_codes_parcours.get_parcours_from_code(formation["type_parcours"]) + # --- Détermination de la formation + orig_semestre_idx = None + if create: + if matiere_id: + matiere = Matiere.query.get_or_404(matiere_id) + ue = matiere.ue + formation = ue.formation + orig_semestre_idx = ue.semestre_idx if semestre_id is None else semestre_id + else: + formation = Formation.query.get_or_404(formation_id) + 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 - 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( - 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) if in_use: # 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: # ne conserve que la 1ere matière de chaque UE, @@ -503,7 +537,8 @@ def module_edit(module_id=None): matieres = [ mat 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 = [ "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: 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] - module["ue_matiere_id"] = "%s!%s" % (module["ue_id"], module["matiere_id"]) + if module: # edition + ue_mat_ids = ["%s!%s" % (mat.ue.id, mat.id) for mat in matieres] + module_dict["ue_matiere_id"] = "%s!%s" % ( + module_dict["ue_id"], + module_dict["matiere_id"], + ) 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 = [ 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"], javascripts=[ "libjs/jQuery-tagEditor/jquery.tag-editor.min.js", @@ -526,17 +590,19 @@ def module_edit(module_id=None): "js/module_tag_editor.js", ], ), - f"""

Modification du module {a_module.code or ''} {a_module.titre or ''}""", - """ (formation %(acronyme)s, version %(version)s)

""" % formation, + f"""

{title}

""", render_template( "scodoc/help/modules.html", is_apc=is_apc, + semestre_id=semestre_id, formsemestres=FormSemestre.query.filter( ModuleImpl.formsemestre_id == FormSemestre.id, ModuleImpl.module_id == module_id, ) .order_by(FormSemestre.date_debut) - .all(), + .all() + if not create + else None, ), ] if not unlocked: @@ -547,28 +613,55 @@ def module_edit(module_id=None): module_types = scu.ModuleType # tous les types else: # ne propose pas SAE et Ressources, sauf si déjà de ce type... - module_types = ( - set(scu.ModuleType) - {scu.ModuleType.RESSOURCE, scu.ModuleType.SAE} - ) | { - scu.ModuleType(a_module.module_type) - if a_module.module_type - else scu.ModuleType.STANDARD + module_types = set(scu.ModuleType) - { + scu.ModuleType.RESSOURCE, + scu.ModuleType.SAE, } + 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 = [ ( "code", { "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, - "validator": lambda val, field, formation_id=formation_id: check_module_code_unicity( - val, field, formation_id, module_id=module_id + "validator": lambda val, field, formation_id=formation.id: check_module_code_unicity( + 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: + Introduction à la démarche ergonomique""", + }, + ), + ( + "abbrev", + { + "size": 20, + "explanation": """nom abrégé (pour bulletins). + Exemple: Intro. à l'ergonomie""", + }, + ), ( "module_type", { @@ -583,50 +676,63 @@ def module_edit(module_id=None): ( "heures_cours", { - "title": "Heures CM :", + "title": "Heures cours :", "size": 4, "type": "float", - "explanation": "nombre d'heures de cours", + "explanation": "nombre d'heures de cours (optionnel)", }, ), ( "heures_td", { - "title": "Heures TD :", + "title": "Heures de TD :", "size": 4, "type": "float", - "explanation": "nombre d'heures de Travaux Dirigés", + "explanation": "nombre d'heures de Travaux Dirigés (optionnel)", }, ), ( "heures_tp", { - "title": "Heures TP :", + "title": "Heures de TP :", "size": 4, "type": "float", - "explanation": "nombre d'heures de Travaux Pratiques", + "explanation": "nombre d'heures de Travaux Pratiques (optionnel)", }, ), ] if is_apc: - coefs_lst = a_module.ue_coefs_list() - if coefs_lst: - coefs_descr_txt = ", ".join( - [f"{ue.acronyme}: {c}" for (ue, c) in coefs_lst] - ) + if module: + coefs_lst = module.ue_coefs_list() + if coefs_lst: + coefs_descr_txt = ", ".join( + [f"{ue.acronyme}: {c}" for (ue, c) in coefs_lst] + ) + else: + coefs_descr_txt = """non définis""" + descr += [ + ( + "ue_coefs", + { + "readonly": True, + "title": "Coefficients vers les UE ", + "default": coefs_descr_txt, + "explanation": "
(passer par la page d'édition de la formation pour modifier les coefficients)", + }, + ) + ] else: - coefs_descr_txt = """non définis""" - descr += [ - ( - "ue_coefs", - { - "readonly": True, - "title": "Coefficients vers les UE ", - "default": coefs_descr_txt, - "explanation": "
(passer par la page d'édition de la formation pour modifier les coefficients)", - }, - ) - ] + descr += [ + ( + "sep_ue_coefs", + { + "input_type": "separator", + "title": """ +
(les coefficients vers les UE se fixent sur la page dédiée) +
""", + }, + ), + ] else: # Module classique avec coef scalaire: descr += [ ( @@ -641,30 +747,64 @@ def module_edit(module_id=None): ), ] descr += [ - ("formation_id", {"input_type": "hidden"}), - ("ue_id", {"input_type": "hidden"}), - ("module_id", {"input_type": "hidden"}), ( - "ue_matiere_id", + "formation_id", { - "input_type": "menu", - "title": "Rattachement :" if is_apc else "Matière :", - "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, + "input_type": "hidden", + "default": formation.id, }, ), ] + 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: # le semestre du module est toujours celui de son UE descr += [ @@ -710,17 +850,56 @@ def module_edit(module_id=None): "numero", { "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", + "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"""Pas de parcours: + associer un référentiel de compétence + """, + }, + ) + ] # force module semestre_idx to its UE - if a_module.ue.semestre_idx: - module["semestre_id"] = a_module.ue.semestre_idx - # Filet de sécurité si jamais l'UE n'a pas non plus de semestre: - if not module["semestre_id"]: - module["semestre_id"] = 1 + if module: + if module.ue.semestre_idx is None: + # Filet de sécurité si jamais l'UE n'a pas non plus de semestre: + module_dict["semestre_id"] = 1 + else: + module_dict["semestre_id"] = module.ue.semestre_idx + tf = TrivialFormulator( request.base_url, scu.get_request_args(), @@ -728,8 +907,9 @@ def module_edit(module_id=None): html_foot_markup="""
""".format( module_id, ",".join(sco_tag_module.module_tag_list(module_id)) ), - initvalues=module, - submitlabel="Modifier ce module", + initvalues=module_dict if module else {}, + submitlabel="Modifier ce module" if module else "Créer ce module", + cancelbutton="Annuler", ) # if tf[0] == 0: @@ -739,38 +919,66 @@ def module_edit(module_id=None): url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, - formation_id=formation_id, - semestre_idx=module["semestre_id"], + formation_id=formation.id, + semestre_idx=orig_semestre_idx, ) ) else: - # l'UE de rattachement peut changer - tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!") - x, y = tf[2]["ue_matiere_id"].split("!") - tf[2]["ue_id"] = int(x) - tf[2]["matiere_id"] = int(y) - old_ue_id = a_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 != a_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 - do_module_edit(tf[2]) + if create: + if not matiere_id: + # formulaire avec choix UE de rattachement + ue = UniteEns.query.get(tf[2]["ue_id"]) + if ue is None: + raise ValueError("UE invalide") + matiere = ue.matieres.first() + if matiere: + tf[2]["matiere_id"] = matiere.id + else: + matiere_id = sco_edit_matiere.do_matiere_create( + {"ue_id": ue.id, "titre": ue.titre, "numero": 1}, + ) + tf[2]["matiere_id"] = matiere_id + + tf[2]["semestre_id"] = ue.semestre_idx + module_id = do_module_create(tf[2]) + module = Module.query.get(module_id) + else: # EDITION MODULE + # l'UE de rattachement peut changer + tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!") + 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( url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, - formation_id=formation_id, + formation_id=formation.id, semestre_idx=tf[2]["semestre_id"], ) ) diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index b400d881..066a49a5 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -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}" initvalues = ue_dict 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: ue = None 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"""

UE du semestre S{ue.semestre_idx}

""" - if is_apc + if is_apc and ue else "", ] @@ -1015,9 +1017,6 @@ def _ue_table_ues( }">transformer en UE ordinaire """ ) H.append("") - 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"]) if ue_editable: H.append( diff --git a/app/templates/pn/form_mods.html b/app/templates/pn/form_mods.html index 10927bc8..4e3031ab 100644 --- a/app/templates/pn/form_mods.html +++ b/app/templates/pn/form_mods.html @@ -84,13 +84,15 @@ url_for("notes.module_create", scodoc_dept=g.scodoc_dept, module_type=module_type|int, - matiere_id=matiere_parent.id + matiere_id=matiere_parent.id, + semestre_id=semestre_id, )}}" {% else %}"{{ url_for("notes.module_create", scodoc_dept=g.scodoc_dept, module_type=module_type|int, - formation_id=formation.id + formation_id=formation.id, + semestre_id=semestre_id, )}}" {% endif %} >{{create_element_msg}} diff --git a/migrations/versions/6002d7d366e5_assoc_ue_niveau.py b/migrations/versions/6002d7d366e5_assoc_ue_niveau.py index 449ff06c..cf6c5932 100644 --- a/migrations/versions/6002d7d366e5_assoc_ue_niveau.py +++ b/migrations/versions/6002d7d366e5_assoc_ue_niveau.py @@ -22,13 +22,39 @@ def upgrade(): "notes_ue", sa.Column("niveau_competence_id", sa.Integer(), nullable=True) ) 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 ### def downgrade(): # ### 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_table("parcours_modules") # ### end Alembic commands ### diff --git a/tests/unit/test_refcomp.py b/tests/unit/test_refcomp.py index f9e92a37..2e56ed3c 100644 --- a/tests/unit/test_refcomp.py +++ b/tests/unit/test_refcomp.py @@ -4,35 +4,57 @@ Utiliser par exemple comme: pytest tests/unit/test_refcomp.py """ -import io + from flask import g -import app + from app import db from app import models from app.but.import_refcomp import orebut_import_refcomp +from app.models import UniteEns from app.models.but_refcomp import ( ApcReferentielCompetences, ApcCompetence, 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): """modèles ref. comp.""" - xml_data = open( - "ressources/referentiels/but2022/competences/but-RT-05012022-081735.xml" - ).read() dept_id = models.Departement.query.first().id - ref = orebut_import_refcomp(xml_data, dept_id) - assert ref.competences.count() == 13 - assert ref.competences[0].situations.count() == 3 - assert ref.competences[0].situations[0].libelle.startswith("Conception ") + ref_comp: ApcReferentielCompetences = orebut_import_refcomp(REF_RT_XML, dept_id) + assert ref_comp.competences.count() == 13 + assert ref_comp.competences[0].situations.count() == 3 + assert ref_comp.competences[0].situations[0].libelle.startswith("Conception ") assert ( - ref.competences[-1].situations[-1].libelle + ref_comp.competences[-1].situations[-1].libelle == "Administration des services multimédia" ) # test cascades on delete - db.session.delete(ref) + db.session.delete(ref_comp) db.session.commit() assert ApcCompetence.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