diff --git a/app/but/apc_edit_ue.py b/app/but/apc_edit_ue.py
new file mode 100644
index 00000000..1e439c71
--- /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 parcour in ref_comp.parcours:
+ if len(niveaux_by_parcours[parcour.id]):
+ options.append(f"""""")
+ options_str = "\n".join(options)
+ return f"""
+
+
+
+ """
+
+
+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 0750f1a9..6a9205fb 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 518bd721..7d5e0cb9 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 fd25e5da..b400d881 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 d6def369..c568a7f6 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 6a0d9e73..2c7d4652 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 8424496c..0293a82a 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 a91f2942..c603c6c5 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() %}