diff --git a/app/but/apc_edit_ue.py b/app/but/apc_edit_ue.py new file mode 100644 index 000000000..1e439c71c --- /dev/null +++ b/app/but/apc_edit_ue.py @@ -0,0 +1,68 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +""" +Edition associations UE <-> Ref. Compétence +""" +from flask import g, url_for +from app import db, log +from app.models import UniteEns +from app.models.but_refcomp import ApcNiveau + + +def form_ue_choix_niveau(ue: UniteEns) -> str: + """Form. HTML pour associer une UE à un niveau de compétence""" + ref_comp = ue.formation.referentiel_competence + if ref_comp is None: + return """
pas de référentiel de compétence
""" + annee = (ue.semestre_idx + 1) // 2 # 1, 2, 3 + niveaux_by_parcours = ref_comp.get_niveaux_by_parcours(annee) + + options = [] + if niveaux_by_parcours["TC"]: + options.append("""""") + for n in niveaux_by_parcours["TC"]: + options.append( + f"""""" + ) + options.append("""""") + for parcour in ref_comp.parcours: + if len(niveaux_by_parcours[parcour.id]): + options.append(f"""""") + for n in niveaux_by_parcours[parcour.id]: + options.append( + f"""""" + ) + options.append("""""") + options_str = "\n".join(options) + return f""" +
+
+ Niveau de compétence associé: + +
+
+ """ + + +def set_ue_niveau_competence(ue_id: int, niveau_id: int): + """Associe le niveau et l'UE""" + log(f"set_ue_niveau_competence( {ue_id}, {niveau_id} )") + ue = UniteEns.query.get_or_404(ue_id) + if niveau_id == "": + # suppression de l'association + ue.niveau_competence = None + else: + niveau = ApcNiveau.query.get_or_404(niveau_id) + ue.niveau_competence = niveau + db.session.add(ue) + db.session.commit() + return "", 204 diff --git a/app/models/but_refcomp.py b/app/models/but_refcomp.py index 0750f1a9f..6a9205fb3 100644 --- a/app/models/but_refcomp.py +++ b/app/models/but_refcomp.py @@ -7,6 +7,7 @@ """ from datetime import datetime +import flask_sqlalchemy from sqlalchemy.orm import class_mapper import sqlalchemy @@ -105,6 +106,52 @@ class ApcReferentielCompetences(db.Model, XMLModel): "parcours": {x.code: x.to_dict() for x in self.parcours}, } + def get_niveaux_by_parcours(self, annee) -> dict: + """ + Construit la liste des niveaux de compétences pour chaque parcours + de ce référentiel. + Les niveaux sont groupés par parcours, en isolant les niveaux de tronc commun. + Le tronc commun n'est pas identifié comme tel dans les référentiels Orébut: + on cherche les niveaux qui sont présents dans tous les parcours et les range sous + la clé "TC" (toujours présente mais éventuellement liste vide si pas de tronc commun). + + résultat: + { + "TC" : [ ApcNiveau ], + parcour.id : [ ApcNiveau ] + } + """ + parcours = self.parcours.order_by(ApcParcours.numero).all() + niveaux_by_parcours = { + parcour.id: ApcNiveau.niveaux_annee_de_parcours(parcour, annee) + for parcour in parcours + } + # Cherche tronc commun + niveaux_ids_tc = set.intersection( + *[ + {n.id for n in niveaux_by_parcours[parcour_id]} + for parcour_id in niveaux_by_parcours + ] + ) + # Enleve les niveaux du tronc commun + niveaux_by_parcours_no_tc = { + parcour.id: [ + niveau + for niveau in niveaux_by_parcours[parcour.id] + if niveau.id not in niveaux_ids_tc + ] + for parcour in parcours + } + # Niveaux du TC + niveaux_tc = [] + if len(parcours): + niveaux_parcours_1 = niveaux_by_parcours[parcours[0].id] + niveaux_tc = [ + niveau for niveau in niveaux_parcours_1 if niveau.id in niveaux_ids_tc + ] + niveaux_by_parcours_no_tc["TC"] = niveaux_tc + return niveaux_by_parcours_no_tc + class ApcCompetence(db.Model, XMLModel): "Compétence" @@ -186,13 +233,20 @@ class ApcComposanteEssentielle(db.Model, XMLModel): class ApcNiveau(db.Model, XMLModel): + """Niveau de compétence + Chaque niveau peut être associé à deux UE, + des semestres impair et pair de la même année. + """ + + __tablename__ = "apc_niveau" + 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 + annee = db.Column(db.Text(), nullable=False) # "BUT1", "BUT2", "BUT3" + # L'ordre est le niveau (1,2,3) ou (1,2) suivant la competence ordre = db.Column(db.Integer, nullable=False) # 1, 2, 3 app_critiques = db.relationship( "ApcAppCritique", @@ -200,9 +254,10 @@ class ApcNiveau(db.Model, XMLModel): lazy="dynamic", cascade="all, delete-orphan", ) + ues = db.relationship("UniteEns", back_populates="niveau_competence") def __repr__(self): - return f"<{self.__class__.__name__} ordre={self.ordre}>" + return f"<{self.__class__.__name__} ordre={self.ordre} annee={self.annee} {self.competence}>" def to_dict(self): return { @@ -212,6 +267,24 @@ class ApcNiveau(db.Model, XMLModel): "app_critiques": {x.code: x.to_dict() for x in self.app_critiques}, } + @classmethod + def niveaux_annee_de_parcours( + cls, parcour: "ApcParcours", annee: int + ) -> flask_sqlalchemy.BaseQuery: + """Les niveaux de l'année du parcours""" + if annee not in {1, 2, 3}: + raise ValueError("annee invalide pour un parcours BUT") + annee_formation = f"BUT{annee}" + return ApcNiveau.query.filter( + ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id, + ApcParcours.id == ApcAnneeParcours.parcours_id, + ApcParcours.referentiel == parcour.referentiel, + ApcParcoursNiveauCompetence.competence_id == ApcCompetence.id, + ApcCompetence.id == ApcNiveau.competence_id, + ApcAnneeParcours.parcours == parcour, + ApcNiveau.annee == annee_formation, + ) + class ApcAppCritique(db.Model, XMLModel): "Apprentissage Critique BUT" @@ -281,9 +354,10 @@ class ApcAnneeParcours(db.Model, XMLModel): db.Integer, db.ForeignKey("apc_parcours.id"), nullable=False ) ordre = db.Column(db.Integer) + "numéro de l'année: 1, 2, 3" def __repr__(self): - return f"<{self.__class__.__name__} ordre={self.ordre}>" + return f"<{self.__class__.__name__} ordre={self.ordre} parcours={self.parcours.code}>" def to_dict(self): return { @@ -321,6 +395,7 @@ class ApcParcoursNiveauCompetence(db.Model): "annee_parcours", passive_deletes=True, cascade="save-update, merge, delete, delete-orphan", + lazy="dynamic", ), ) annee_parcours = db.relationship( @@ -333,4 +408,4 @@ class ApcParcoursNiveauCompetence(db.Model): ) def __repr__(self): - return f"<{self.__class__.__name__} {self.competence} {self.annee_parcours}>" + return f"<{self.__class__.__name__} {self.competence}<->{self.annee_parcours} niveau={self.niveau}>" diff --git a/app/models/ues.py b/app/models/ues.py index 518bd7219..7d5e0cb9d 100644 --- a/app/models/ues.py +++ b/app/models/ues.py @@ -42,6 +42,10 @@ class UniteEns(db.Model): color = db.Column(db.Text()) + # BUT + niveau_competence_id = db.Column(db.Integer, db.ForeignKey("apc_niveau.id")) + niveau_competence = db.relationship("ApcNiveau", back_populates="ues") + # relations matieres = db.relationship("Matiere", lazy="dynamic", backref="ue") modules = db.relationship("Module", lazy="dynamic", backref="ue") diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index fd25e5da3..b400d881d 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -35,6 +35,7 @@ from flask_login import current_user from app import db from app import log +from app.but import apc_edit_ue from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN from app.models import Formation, UniteEns, ModuleImpl, Module from app.models import ScolarNews @@ -283,6 +284,11 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No

Note: sauf exception, l'UE n'a pas de coefficient associé. Seuls les modules ont des coefficients.

""", + f""" +

UE du semestre S{ue.semestre_idx}

+ """ + if is_apc + else "", ] ue_types = parcours.ALLOWED_UE_TYPES @@ -416,8 +422,12 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No form_descr, initvalues=initvalues, submitlabel=submitlabel, + cancelbutton="Revenir à la formation", ) if tf[0] == 0: + niveau_competence_div = "" + if ue and is_apc: + niveau_competence_div = apc_edit_ue.form_ue_choix_niveau(ue) if ue and ue.modules.count() and ue.semestre_idx is not None: modules_div = f"""
{ue.modules.count()} modules sont rattachés @@ -435,6 +445,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No return ( "\n".join(H) + tf[1] + + niveau_competence_div + modules_div + bonus_div + ue_div @@ -1004,6 +1015,9 @@ 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/scodoc/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py index d6def3697..c568a7f62 100644 --- a/app/scodoc/sco_moduleimpl_status.py +++ b/app/scodoc/sco_moduleimpl_status.py @@ -194,8 +194,8 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None): raise ScoInvalidIdType("moduleimpl_id must be an integer !") modimpl = ModuleImpl.query.get_or_404(moduleimpl_id) M = modimpl.to_dict() - formsemestre_id = M["formsemestre_id"] - Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] + formsemestre_id = modimpl.formsemestre_id + Mod = sco_edit_module.module_list(args={"module_id": modimpl.module_id})[0] sem = sco_formsemestre.get_formsemestre(formsemestre_id) F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0] mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list( diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 6a0d9e73d..2c7d4652f 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -2206,7 +2206,7 @@ ul.notes_module_list { list-style-type: none; } -div#ue_list_modules { +div#ue_choix_niveau { background-color: rgb(191, 242, 255); border: 1px solid blue; border-radius: 10px; @@ -2215,6 +2215,15 @@ div#ue_list_modules { margin-right: 15px; } +div#ue_list_modules { + background-color: rgb(251, 225, 165); + border: 1px solid blue; + border-radius: 10px; + padding: 10px; + margin-top: 10px; + margin-right: 15px; +} + div#ue_list_etud_validations { background-color: rgb(220, 250, 220); padding-left: 4px; diff --git a/app/static/js/edit_ue.js b/app/static/js/edit_ue.js index 8424496ca..0293a82ab 100644 --- a/app/static/js/edit_ue.js +++ b/app/static/js/edit_ue.js @@ -25,10 +25,27 @@ function update_bonus_description() { } function update_ue_list() { - var ue_id = $("#tf_ue_id")[0].value; - var ue_code = $("#tf_ue_code")[0].value; - var query = SCO_URL + "/Notes/ue_sharing_code?ue_code=" + ue_code + "&hide_ue_id=" + ue_id + "&ue_id=" + ue_id; + let ue_id = $("#tf_ue_id")[0].value; + let ue_code = $("#tf_ue_code")[0].value; + let query = SCO_URL + "/Notes/ue_sharing_code?ue_code=" + ue_code + "&hide_ue_id=" + ue_id + "&ue_id=" + ue_id; $.get(query, '', function (data) { $("#ue_list_code").html(data); }); } + +function set_ue_niveau_competence() { + let ue_id = document.querySelector("#tf_ue_id").value; + let select = document.querySelector("#form_ue_choix_niveau select"); + let niveau_id = select.value; + let set_ue_niveau_competence_url = select.dataset.setter; + $.post(set_ue_niveau_competence_url, + { + ue_id: ue_id, + niveau_id: niveau_id, + }, + function (result) { + // obj.classList.remove("sco_wait"); + // obj.classList.add("sco_modified"); + } + ); +} \ No newline at end of file diff --git a/app/templates/pn/form_ues.html b/app/templates/pn/form_ues.html index a91f29425..c603c6c52 100644 --- a/app/templates/pn/form_ues.html +++ b/app/templates/pn/form_ues.html @@ -40,6 +40,9 @@ else 'aucun'|safe}} ECTS) + {% if ue.niveau_competence is none %} + pas de compétence associée + {% endif %} {% if editable and not ue.is_locked() %}