From d2b69c2f73eb37429287eaae4a923f32aaabccf4 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 2 Dec 2021 12:08:03 +0100 Subject: [PATCH] =?UTF-8?q?Import=20ref.=20Comp=C3=A9tences=20BUT=20(Or?= =?UTF-8?q?=C3=A9but)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/comp/moy_ue.py | 4 +- app/models/__init__.py | 8 +- app/models/but_pn.py | 36 +-- app/models/but_refcomp.py | 222 ++++++++++++++++++ app/models/formations.py | 5 + app/scodoc/sco_edit_apc.py | 4 +- app/scodoc/sco_moduleimpl_status.py | 2 +- .../versions/00ad500fb118_but_refcomp.py | 207 ++++++++++++++++ tests/unit/test_refcomp.py | 55 +++++ 9 files changed, 510 insertions(+), 33 deletions(-) create mode 100644 app/models/but_refcomp.py create mode 100644 migrations/versions/00ad500fb118_but_refcomp.py create mode 100644 tests/unit/test_refcomp.py diff --git a/app/comp/moy_ue.py b/app/comp/moy_ue.py index 20c85b970..a0bc9461d 100644 --- a/app/comp/moy_ue.py +++ b/app/comp/moy_ue.py @@ -54,7 +54,9 @@ def df_load_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.Data ues = UniteEns.query.filter_by(formation_id=formation_id).filter( UniteEns.type != sco_codes_parcours.UE_SPORT ) - modules = Module.query.filter_by(formation_id=formation_id) + modules = Module.query.filter_by(formation_id=formation_id).order_by( + Module.semestre_id, Module.numero + ) if semestre_idx is not None: ues = ues.filter_by(semestre_idx=semestre_idx) modules = modules.filter_by(semestre_id=semestre_idx) diff --git a/app/models/__init__.py b/app/models/__init__.py index b07f12da6..78af2bb89 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -59,7 +59,6 @@ from app.models.evaluations import ( Evaluation, EvaluationUEPoids, ) -from app.models.but_pn import AppCrit from app.models.groups import Partition, GroupDescr, group_membership from app.models.notes import ( ScolarEvent, @@ -70,3 +69,10 @@ from app.models.notes import ( NotesNotesLog, ) from app.models.preferences import ScoPreference, ScoDocSiteConfig + +from app.models.but_refcomp import ( + ApcReferentielCompetences, + ApcCompetence, + ApcSituationPro, + ApcAppCritique, +) diff --git a/app/models/but_pn.py b/app/models/but_pn.py index 1e142d23d..35afbe16c 100644 --- a/app/models/but_pn.py +++ b/app/models/but_pn.py @@ -7,35 +7,13 @@ from app import db from app.scodoc.sco_utils import ModuleType -Modules_ACs = db.Table( - "modules_acs", - db.Column("module_id", db.ForeignKey("notes_modules.id")), - db.Column("ac_id", db.ForeignKey("app_crit.id")), -) +class APCFormation(db.Model): + """Formation par compétence""" -class AppCrit(db.Model): - "Apprentissage Critique BUT" - __tablename__ = "app_crit" id = db.Column(db.Integer, primary_key=True) - code = db.Column(db.Text(), nullable=False, info={"label": "Code"}) - titre = db.Column(db.Text(), info={"label": "Titre"}) - - modules = db.relationship( - "Module", secondary=Modules_ACs, lazy="dynamic", backref="acs" - ) - - def to_dict(self): - result = dict(self.__dict__) - result.pop("_sa_instance_state", None) - return result - - def get_label(self): - return self.code + " - " + self.titre - - def __repr__(self): - return "".format(self.code) - - def get_saes(self): - """Liste des SAE associées""" - return [m for m in self.modules if m.module_type == ModuleType.SAE] + dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True) + specialite = db.Column(db.Text(), nullable=False) # "RT" + specialite_long = db.Column( + db.Text(), nullable=False + ) # "Réseaux et télécommunications" diff --git a/app/models/but_refcomp.py b/app/models/but_refcomp.py new file mode 100644 index 000000000..dea35173a --- /dev/null +++ b/app/models/but_refcomp.py @@ -0,0 +1,222 @@ +"""ScoDoc 9 models : Référentiel Compétence BUT 2021 +""" +from enum import unique +from typing import Any + +from app import db + +from app.scodoc.sco_utils import ModuleType + + +class XMLModel: + _xml_attribs = {} # to be overloaded + id = "_" + + @classmethod + def attr_from_xml(cls, args: dict) -> dict: + """dict with attributes imported from Orébut XML + and renamed for our models. + The mapping is specified by the _xml_attribs + attribute in each model class. + """ + return {cls._xml_attribs.get(k, k): v for (k, v) in args.items()} + + def __repr__(self): + return f'<{self.__class__.__name__} {self.id} "{self.titre if hasattr(self, "titre") else ""}">' + + +class ApcReferentielCompetences(db.Model, XMLModel): + id = db.Column(db.Integer, primary_key=True) + dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True) + specialite = db.Column(db.Text()) + specialite_long = db.Column(db.Text()) + type_titre = db.Column(db.Text()) + _xml_attribs = { # xml_attrib : attribute + "type": "type_titre", + } + competences = db.relationship( + "ApcCompetence", + backref="referentiel", + lazy="dynamic", + cascade="all, delete-orphan", + ) + parcours = db.relationship( + "ApcParcours", + backref="referentiel", + lazy="dynamic", + cascade="all, delete-orphan", + ) + + def to_dict(self): + """Représentation complète du ref. de comp. + comme un dict. + """ + + +class ApcCompetence(db.Model, XMLModel): + id = db.Column(db.Integer, primary_key=True) + referentiel_id = db.Column( + db.Integer, db.ForeignKey("apc_referentiel_competences.id"), nullable=False + ) + titre = db.Column(db.Text(), nullable=False) + titre_long = db.Column(db.Text()) + couleur = db.Column(db.Text()) + numero = db.Column(db.Integer) # ordre de présentation + couleur = db.Column(db.Text()) + _xml_attribs = { # xml_attrib : attribute + "name": "titre", + "libelle_long": "titre_long", + } + situations = db.relationship( + "ApcSituationPro", + backref="competence", + lazy="dynamic", + cascade="all, delete-orphan", + ) + composantes_essentielles = db.relationship( + "ApcComposanteEssentielle", + backref="competence", + lazy="dynamic", + cascade="all, delete-orphan", + ) + niveaux = db.relationship( + "ApcNiveau", + backref="competence", + lazy="dynamic", + cascade="all, delete-orphan", + ) + + +class ApcSituationPro(db.Model, XMLModel): + id = db.Column(db.Integer, primary_key=True) + competence_id = db.Column( + db.Integer, db.ForeignKey("apc_competence.id"), nullable=False + ) + libelle = db.Column(db.Text(), nullable=False) + # aucun attribut (le text devient le libellé) + + +class ApcComposanteEssentielle(db.Model, XMLModel): + id = db.Column(db.Integer, primary_key=True) + competence_id = db.Column( + db.Integer, db.ForeignKey("apc_competence.id"), nullable=False + ) + libelle = db.Column(db.Text(), nullable=False) + + +class ApcNiveau(db.Model, XMLModel): + id = db.Column(db.Integer, primary_key=True) + competence_id = db.Column( + db.Integer, db.ForeignKey("apc_competence.id"), nullable=False + ) + libelle = db.Column(db.Text(), nullable=False) + annee = db.Column(db.Text(), nullable=False) # "BUT2" + # L'ordre est l'année d'apparition de ce niveau + ordre = db.Column(db.Integer, nullable=False) # 1, 2, 3 + app_critiques = db.relationship( + "ApcAppCritique", + backref="niveau", + lazy="dynamic", + cascade="all, delete-orphan", + ) + + +class ApcAppCritique(db.Model, XMLModel): + "Apprentissage Critique BUT" + id = db.Column(db.Integer, primary_key=True) + niveau_id = db.Column(db.Integer, db.ForeignKey("apc_niveau.id"), nullable=False) + code = db.Column(db.Text(), nullable=False) + libelle = db.Column(db.Text()) + + modules = db.relationship( + "Module", + secondary="apc_modules_acs", + lazy="dynamic", + backref=db.backref("app_critiques", lazy="dynamic"), + ) + + def to_dict(self): + result = dict(self.__dict__) + result.pop("_sa_instance_state", None) + return result + + def get_label(self): + return self.code + " - " + self.titre + + def __repr__(self): + return "".format(self.code) + + def get_saes(self): + """Liste des SAE associées""" + return [m for m in self.modules if m.module_type == ModuleType.SAE] + + +ApcModulesACs = db.Table( + "apc_modules_acs", + db.Column("module_id", db.ForeignKey("notes_modules.id")), + db.Column("app_crit_id", db.ForeignKey("apc_app_critique.id")), +) + + +class ApcParcours(db.Model, XMLModel): + id = db.Column(db.Integer, primary_key=True) + referentiel_id = db.Column( + db.Integer, db.ForeignKey("apc_referentiel_competences.id"), nullable=False + ) + numero = db.Column(db.Integer) # ordre de présentation + code = db.Column(db.Text(), nullable=False) + libelle = db.Column(db.Text(), nullable=False) + annees = db.relationship( + "ApcAnneeParcours", + backref="parcours", + lazy="dynamic", + cascade="all, delete-orphan", + ) + + +class ApcAnneeParcours(db.Model, XMLModel): + id = db.Column(db.Integer, primary_key=True) + parcours_id = db.Column( + db.Integer, db.ForeignKey("apc_parcours.id"), nullable=False + ) + numero = db.Column(db.Integer) # ordre de présentation + + # L'attribut s'appelle "ordre" dans le XML Orébut + _xml_attribs = { # xml_attrib : attribute + "ordre": "numero", + } + + +class ApcParcoursNiveauCompetence(db.Model): + """Association entre année de parcours et compétence. + Le "niveau" de la compétence est donné ici + (convention Orébut) + """ + + competence_id = db.Column( + db.Integer, + db.ForeignKey("apc_competence.id", ondelete="CASCADE"), + primary_key=True, + ) + annee_parcours_id = db.Column( + db.Integer, + db.ForeignKey("apc_annee_parcours.id", ondelete="CASCADE"), + primary_key=True, + ) + niveau = db.Column(db.Integer, nullable=False) # 1, 2, 3 + competence = db.relationship( + ApcCompetence, + backref=db.backref( + "annee_parcours", + passive_deletes=True, + cascade="save-update, merge, delete, delete-orphan", + ), + ) + annee_parcours = db.relationship( + ApcAnneeParcours, + backref=db.backref( + "competences", + passive_deletes=True, + cascade="save-update, merge, delete, delete-orphan", + ), + ) diff --git a/app/models/formations.py b/app/models/formations.py index 45f42d29b..b456eb825 100644 --- a/app/models/formations.py +++ b/app/models/formations.py @@ -35,6 +35,11 @@ class Formation(db.Model): type_parcours = db.Column(db.Integer, default=0, server_default="0") code_specialite = db.Column(db.String(SHORT_STR_LEN)) + # Optionnel, pour les formations type BUT + referentiel_competence_id = db.Column( + db.Integer, db.ForeignKey("apc_referentiel_competences.id") + ) + ues = db.relationship("UniteEns", backref="formation", lazy="dynamic") formsemestres = db.relationship("FormSemestre", lazy="dynamic", backref="formation") ues = db.relationship("UniteEns", lazy="dynamic", backref="formation") diff --git a/app/scodoc/sco_edit_apc.py b/app/scodoc/sco_edit_apc.py index baafdbc2d..6215903b9 100644 --- a/app/scodoc/sco_edit_apc.py +++ b/app/scodoc/sco_edit_apc.py @@ -52,7 +52,9 @@ def html_edit_formation_apc( ressources = formation.modules.filter_by(module_type=ModuleType.RESSOURCE).order_by( Module.semestre_id, Module.numero ) - saes = formation.modules.filter_by(module_type=ModuleType.SAE) + saes = formation.modules.filter_by(module_type=ModuleType.SAE).order_by( + Module.semestre_id, Module.numero + ) if semestre_idx is None: semestre_ids = range(1, parcours.NB_SEM + 1) else: diff --git a/app/scodoc/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py index 28de6d660..5bce46078 100644 --- a/app/scodoc/sco_moduleimpl_status.py +++ b/app/scodoc/sco_moduleimpl_status.py @@ -226,7 +226,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None): H.append("""""") if not sem["etat"]: H.append(scu.icontag("lock32_img", title="verrouillé")) - H.append("""Coef dans le semestre: """) + H.append("""Coef. dans le semestre: """) if modimpl.module.is_apc(): coefs_descr = modimpl.module.ue_coefs_descr() if coefs_descr: diff --git a/migrations/versions/00ad500fb118_but_refcomp.py b/migrations/versions/00ad500fb118_but_refcomp.py new file mode 100644 index 000000000..392c7d8ad --- /dev/null +++ b/migrations/versions/00ad500fb118_but_refcomp.py @@ -0,0 +1,207 @@ +"""but_refcomp + +Revision ID: 00ad500fb118 +Revises: a26b3103697d +Create Date: 2021-12-02 09:01:03.167131 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "00ad500fb118" +down_revision = "a26b3103697d" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "apc_referentiel_competences", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("dept_id", sa.Integer(), nullable=True), + sa.Column("specialite", sa.Text(), nullable=True), + sa.Column("specialite_long", sa.Text(), nullable=True), + sa.Column("type_titre", sa.Text(), nullable=True), + sa.ForeignKeyConstraint( + ["dept_id"], + ["departement.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + op.f("ix_apc_referentiel_competences_dept_id"), + "apc_referentiel_competences", + ["dept_id"], + unique=False, + ) + op.create_table( + "apc_competence", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("referentiel_id", sa.Integer(), nullable=False), + sa.Column("titre", sa.Text(), nullable=False), + sa.Column("titre_long", sa.Text(), nullable=True), + sa.Column("numero", sa.Integer(), nullable=True), + sa.Column("couleur", sa.Text(), nullable=True), + sa.ForeignKeyConstraint( + ["referentiel_id"], + ["apc_referentiel_competences.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "apc_parcours", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("referentiel_id", sa.Integer(), nullable=False), + sa.Column("numero", sa.Integer(), nullable=True), + sa.Column("code", sa.Text(), nullable=False), + sa.Column("libelle", sa.Text(), nullable=False), + sa.ForeignKeyConstraint( + ["referentiel_id"], + ["apc_referentiel_competences.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "apc_annee_parcours", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("parcours_id", sa.Integer(), nullable=False), + sa.Column("numero", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["parcours_id"], + ["apc_parcours.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "apc_composante_essentielle", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("competence_id", sa.Integer(), nullable=False), + sa.Column("libelle", sa.Text(), nullable=False), + sa.ForeignKeyConstraint( + ["competence_id"], + ["apc_competence.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "apc_niveau", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("competence_id", sa.Integer(), nullable=False), + sa.Column("libelle", sa.Text(), nullable=False), + sa.Column("annee", sa.Text(), nullable=False), + sa.Column("ordre", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["competence_id"], + ["apc_competence.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "apc_situation_pro", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("competence_id", sa.Integer(), nullable=False), + sa.Column("libelle", sa.Text(), nullable=False), + sa.ForeignKeyConstraint( + ["competence_id"], + ["apc_competence.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "apc_app_critique", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("niveau_id", sa.Integer(), nullable=False), + sa.Column("code", sa.Text(), nullable=False), + sa.Column("libelle", sa.Text(), nullable=True), + sa.ForeignKeyConstraint( + ["niveau_id"], + ["apc_niveau.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "apc_parcours_niveau_competence", + sa.Column("competence_id", sa.Integer(), nullable=False), + sa.Column("annee_parcours_id", sa.Integer(), nullable=False), + sa.Column("niveau", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["annee_parcours_id"], ["apc_annee_parcours.id"], ondelete="CASCADE" + ), + sa.ForeignKeyConstraint( + ["competence_id"], ["apc_competence.id"], ondelete="CASCADE" + ), + sa.PrimaryKeyConstraint("competence_id", "annee_parcours_id"), + ) + op.create_table( + "apc_modules_acs", + sa.Column("module_id", sa.Integer(), nullable=True), + sa.Column("app_crit_id", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["app_crit_id"], + ["apc_app_critique.id"], + ), + sa.ForeignKeyConstraint( + ["module_id"], + ["notes_modules.id"], + ), + ) + # op.drop_table("app_crit") + # op.drop_table("modules_acs") + op.add_column( + "notes_formations", + sa.Column("referentiel_competence_id", sa.Integer(), nullable=True), + ) + op.create_foreign_key( + "notes_formations_referentiel_competence_id_fkey", + "notes_formations", + "apc_referentiel_competences", + ["referentiel_competence_id"], + ["id"], + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint( + "notes_formations_referentiel_competence_id_fkey", + "notes_formations", + type_="foreignkey", + ) + op.drop_column("notes_formations", "referentiel_competence_id") + # op.create_table( + # "modules_acs", + # sa.Column("module_id", sa.INTEGER(), autoincrement=False, nullable=True), + # sa.Column("ac_id", sa.INTEGER(), autoincrement=False, nullable=True), + # sa.ForeignKeyConstraint( + # ["ac_id"], ["app_crit.id"], name="modules_acs_ac_id_fkey" + # ), + # sa.ForeignKeyConstraint( + # ["module_id"], ["notes_modules.id"], name="modules_acs_module_id_fkey" + # ), + # ) + # op.create_table( + # "app_crit", + # sa.Column("id", sa.INTEGER(), autoincrement=True, nullable=False), + # sa.Column("code", sa.TEXT(), autoincrement=False, nullable=False), + # sa.Column("titre", sa.TEXT(), autoincrement=False, nullable=True), + # sa.PrimaryKeyConstraint("id", name="app_crit_pkey"), + # ) + op.drop_table("apc_modules_acs") + op.drop_table("apc_parcours_niveau_competence") + op.drop_table("apc_app_critique") + op.drop_table("apc_situation_pro") + op.drop_table("apc_niveau") + op.drop_table("apc_composante_essentielle") + op.drop_table("apc_annee_parcours") + op.drop_table("apc_parcours") + op.drop_table("apc_competence") + op.drop_index( + op.f("ix_apc_referentiel_competences_dept_id"), + table_name="apc_referentiel_competences", + ) + op.drop_table("apc_referentiel_competences") + # ### end Alembic commands ### diff --git a/tests/unit/test_refcomp.py b/tests/unit/test_refcomp.py new file mode 100644 index 000000000..5ba5635c2 --- /dev/null +++ b/tests/unit/test_refcomp.py @@ -0,0 +1,55 @@ +"""Test models referentiel compétences + +Utiliser par exemple comme: + pytest tests/unit/test_refcomp.py + +""" +import io +from flask import g +import app +from app import db +from app.but.import_refcomp import orebut_import_refcomp +from app.models.but_refcomp import ( + ApcReferentielCompetences, + ApcCompetence, + ApcSituationPro, +) + +ref_xml = """ + + + + + Conception et administration de l’infrastructure du réseau informatique d’une entreprise + Installation et administration des services réseau informatique d’une entreprise + + + + + + Tests unitaires d'une application. + + + + + + + + + +""" + + +def test_but_refcomp(test_client): + """modèles ref. comp.""" + f = io.StringIO(ref_xml) + ref = orebut_import_refcomp(f) + assert ref.references.count() == 2 + assert ref.competences[0].situations.count() == 2 + assert ref.competences[0].situations[0].libelle.startswith("Conception ") + # test cascades on delete + db.session.delete(ref) + db.session.commit() + assert ApcCompetence.query.count() == 0 + assert ApcSituationPro.query.count() == 0