BUT: association UE <-> niveau competence

This commit is contained in:
Emmanuel Viennet 2022-04-29 08:17:04 +02:00
parent 0709b53bbe
commit 1a18fef3e0
10 changed files with 245 additions and 12 deletions

68
app/but/apc_edit_ue.py Normal file
View File

@ -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 """<div class="ue_choix_niveau">pas de référentiel de compétence</div>"""
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("""<optgroup label="Tronc commun">""")
for n in niveaux_by_parcours["TC"]:
options.append(
f"""<option value="{n.id}" {'selected' if ue.niveau_competence == n else ''}>{n.annee} {n.competence.titre_long} niveau {n.ordre}</option>"""
)
options.append("""</optgroup>""")
for parcour in ref_comp.parcours:
if len(niveaux_by_parcours[parcour.id]):
options.append(f"""<optgroup label="Parcours {parcour.libelle}">""")
for n in niveaux_by_parcours[parcour.id]:
options.append(
f"""<option value="{n.id}" {'selected' if ue.niveau_competence == n else ''}>{n.annee} {n.competence.titre_long} niveau {n.ordre}</option>"""
)
options.append("""</optgroup>""")
options_str = "\n".join(options)
return f"""
<div id="ue_choix_niveau">
<form id="form_ue_choix_niveau">
<b>Niveau de compétence associé:</b>
<select onchange="set_ue_niveau_competence();" data-setter="{
url_for( "notes.set_ue_niveau_competence", scodoc_dept=g.scodoc_dept)
}">
<option value="" {'selected' if ue.niveau_competence is None else ''}>aucun</option>
{options_str}
</select>
</form>
</div>
"""
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

View File

@ -7,6 +7,7 @@
""" """
from datetime import datetime from datetime import datetime
import flask_sqlalchemy
from sqlalchemy.orm import class_mapper from sqlalchemy.orm import class_mapper
import sqlalchemy import sqlalchemy
@ -105,6 +106,52 @@ class ApcReferentielCompetences(db.Model, XMLModel):
"parcours": {x.code: x.to_dict() for x in self.parcours}, "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): class ApcCompetence(db.Model, XMLModel):
"Compétence" "Compétence"
@ -186,13 +233,20 @@ class ApcComposanteEssentielle(db.Model, XMLModel):
class ApcNiveau(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) id = db.Column(db.Integer, primary_key=True)
competence_id = db.Column( competence_id = db.Column(
db.Integer, db.ForeignKey("apc_competence.id"), nullable=False db.Integer, db.ForeignKey("apc_competence.id"), nullable=False
) )
libelle = db.Column(db.Text(), nullable=False) libelle = db.Column(db.Text(), nullable=False)
annee = db.Column(db.Text(), nullable=False) # "BUT2" annee = db.Column(db.Text(), nullable=False) # "BUT1", "BUT2", "BUT3"
# L'ordre est l'année d'apparition de ce niveau # L'ordre est le niveau (1,2,3) ou (1,2) suivant la competence
ordre = db.Column(db.Integer, nullable=False) # 1, 2, 3 ordre = db.Column(db.Integer, nullable=False) # 1, 2, 3
app_critiques = db.relationship( app_critiques = db.relationship(
"ApcAppCritique", "ApcAppCritique",
@ -200,9 +254,10 @@ class ApcNiveau(db.Model, XMLModel):
lazy="dynamic", lazy="dynamic",
cascade="all, delete-orphan", cascade="all, delete-orphan",
) )
ues = db.relationship("UniteEns", back_populates="niveau_competence")
def __repr__(self): 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): def to_dict(self):
return { return {
@ -212,6 +267,24 @@ class ApcNiveau(db.Model, XMLModel):
"app_critiques": {x.code: x.to_dict() for x in self.app_critiques}, "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): class ApcAppCritique(db.Model, XMLModel):
"Apprentissage Critique BUT" "Apprentissage Critique BUT"
@ -281,9 +354,10 @@ class ApcAnneeParcours(db.Model, XMLModel):
db.Integer, db.ForeignKey("apc_parcours.id"), nullable=False db.Integer, db.ForeignKey("apc_parcours.id"), nullable=False
) )
ordre = db.Column(db.Integer) ordre = db.Column(db.Integer)
"numéro de l'année: 1, 2, 3"
def __repr__(self): 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): def to_dict(self):
return { return {
@ -321,6 +395,7 @@ class ApcParcoursNiveauCompetence(db.Model):
"annee_parcours", "annee_parcours",
passive_deletes=True, passive_deletes=True,
cascade="save-update, merge, delete, delete-orphan", cascade="save-update, merge, delete, delete-orphan",
lazy="dynamic",
), ),
) )
annee_parcours = db.relationship( annee_parcours = db.relationship(
@ -333,4 +408,4 @@ class ApcParcoursNiveauCompetence(db.Model):
) )
def __repr__(self): 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}>"

View File

@ -42,6 +42,10 @@ class UniteEns(db.Model):
color = db.Column(db.Text()) 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 # relations
matieres = db.relationship("Matiere", lazy="dynamic", backref="ue") matieres = db.relationship("Matiere", lazy="dynamic", backref="ue")
modules = db.relationship("Module", lazy="dynamic", backref="ue") modules = db.relationship("Module", lazy="dynamic", backref="ue")

View File

@ -35,6 +35,7 @@ from flask_login import current_user
from app import db from app import db
from app import log 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 APO_CODE_STR_LEN, SHORT_STR_LEN
from app.models import Formation, UniteEns, ModuleImpl, Module from app.models import Formation, UniteEns, ModuleImpl, Module
from app.models import ScolarNews from app.models import ScolarNews
@ -283,6 +284,11 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
<p class="help">Note: sauf exception, l'UE n'a pas de coefficient associé. <p class="help">Note: sauf exception, l'UE n'a pas de coefficient associé.
Seuls les <em>modules</em> ont des coefficients. Seuls les <em>modules</em> ont des coefficients.
</p>""", </p>""",
f"""
<h4>UE du semestre S{ue.semestre_idx}</h4>
"""
if is_apc
else "",
] ]
ue_types = parcours.ALLOWED_UE_TYPES 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, form_descr,
initvalues=initvalues, initvalues=initvalues,
submitlabel=submitlabel, submitlabel=submitlabel,
cancelbutton="Revenir à la formation",
) )
if tf[0] == 0: 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: if ue and ue.modules.count() and ue.semestre_idx is not None:
modules_div = f"""<div id="ue_list_modules"> modules_div = f"""<div id="ue_list_modules">
<div><b>{ue.modules.count()} modules sont rattachés <div><b>{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 ( return (
"\n".join(H) "\n".join(H)
+ tf[1] + tf[1]
+ niveau_competence_div
+ modules_div + modules_div
+ bonus_div + bonus_div
+ ue_div + ue_div
@ -1004,6 +1015,9 @@ 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

@ -194,8 +194,8 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
raise ScoInvalidIdType("moduleimpl_id must be an integer !") raise ScoInvalidIdType("moduleimpl_id must be an integer !")
modimpl = ModuleImpl.query.get_or_404(moduleimpl_id) modimpl = ModuleImpl.query.get_or_404(moduleimpl_id)
M = modimpl.to_dict() M = modimpl.to_dict()
formsemestre_id = M["formsemestre_id"] formsemestre_id = modimpl.formsemestre_id
Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] Mod = sco_edit_module.module_list(args={"module_id": modimpl.module_id})[0]
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0] F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list( mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list(

View File

@ -2206,7 +2206,7 @@ ul.notes_module_list {
list-style-type: none; list-style-type: none;
} }
div#ue_list_modules { div#ue_choix_niveau {
background-color: rgb(191, 242, 255); background-color: rgb(191, 242, 255);
border: 1px solid blue; border: 1px solid blue;
border-radius: 10px; border-radius: 10px;
@ -2215,6 +2215,15 @@ div#ue_list_modules {
margin-right: 15px; 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 { div#ue_list_etud_validations {
background-color: rgb(220, 250, 220); background-color: rgb(220, 250, 220);
padding-left: 4px; padding-left: 4px;

View File

@ -25,10 +25,27 @@ function update_bonus_description() {
} }
function update_ue_list() { function update_ue_list() {
var ue_id = $("#tf_ue_id")[0].value; let ue_id = $("#tf_ue_id")[0].value;
var ue_code = $("#tf_ue_code")[0].value; let 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 query = SCO_URL + "/Notes/ue_sharing_code?ue_code=" + ue_code + "&hide_ue_id=" + ue_id + "&ue_id=" + ue_id;
$.get(query, '', function (data) { $.get(query, '', function (data) {
$("#ue_list_code").html(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");
}
);
}

View File

@ -40,6 +40,9 @@
else '<span class="missing_ue_ects">aucun</span>'|safe}} ECTS) else '<span class="missing_ue_ects">aucun</span>'|safe}} ECTS)
</span> </span>
</span> </span>
{% if ue.niveau_competence is none %}
<span class="fontred">pas de compétence associée</span>
{% endif %}
{% if editable and not ue.is_locked() %} {% if editable and not ue.is_locked() %}
<a class="stdlink" href="{{ url_for('notes.ue_edit', <a class="stdlink" href="{{ url_for('notes.ue_edit',

View File

@ -53,7 +53,7 @@ from app import db
from app import models from app import models
from app.models import ScolarNews from app.models import ScolarNews
from app.auth.models import User from app.auth.models import User
from app.but import bulletin_but from app.but import apc_edit_ue, bulletin_but
from app.decorators import ( from app.decorators import (
scodoc, scodoc,
scodoc7func, scodoc7func,
@ -416,6 +416,15 @@ sco_publish(
) )
@bp.route("/set_ue_niveau_competence", methods=["POST"])
@permission_required(Permission.ScoChangeFormation)
def set_ue_niveau_competence(scodoc_dept=""):
"associe UE et niveau"
ue_id = request.form.get("ue_id")
niveau_id = request.form.get("niveau_id")
return apc_edit_ue.set_ue_niveau_competence(ue_id, niveau_id)
@bp.route("/ue_list") # backward compat @bp.route("/ue_list") # backward compat
@bp.route("/ue_table") @bp.route("/ue_table")
@scodoc @scodoc

View File

@ -0,0 +1,34 @@
"""assoc UE - Niveau
Revision ID: 6002d7d366e5
Revises: af77ca6a89d0
Create Date: 2022-04-26 12:58:32.929910
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "6002d7d366e5"
down_revision = "af77ca6a89d0"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"notes_ue", sa.Column("niveau_competence_id", sa.Integer(), nullable=True)
)
op.create_foreign_key(
None, "notes_ue", "apc_niveau", ["niveau_competence_id"], ["id"]
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, "notes_ue", type_="foreignkey")
op.drop_column("notes_ue", "niveau_competence_id")
# ### end Alembic commands ###