BUT: associe UE aux parcours. Modification pour #487.

This commit is contained in:
Emmanuel Viennet 2022-10-30 16:07:06 +01:00
parent 59fdc80d60
commit a3593d5a74
13 changed files with 380 additions and 139 deletions

View File

@ -8,14 +8,14 @@
Edition associations UE <-> Ref. Compétence Edition associations UE <-> Ref. Compétence
""" """
from flask import g, url_for from flask import g, url_for
from app import db, log from app.models import ApcReferentielCompetences, Formation, UniteEns
from app.models import Formation, UniteEns
from app.models.but_refcomp import ApcNiveau
from app.scodoc import sco_codes_parcours from app.scodoc import sco_codes_parcours
def form_ue_choix_niveau(formation: Formation, ue: UniteEns) -> str: def form_ue_choix_niveau(ue: UniteEns) -> str:
"""Form. HTML pour associer une UE à un niveau de compétence""" """Form. HTML pour associer une UE à un niveau de compétence.
Le menu select lui meême est vide et rempli en JS par appel à get_ue_niveaux_options_html
"""
if ue.type != sco_codes_parcours.UE_STANDARD: if ue.type != sco_codes_parcours.UE_STANDARD:
return "" return ""
ref_comp = ue.formation.referentiel_competence ref_comp = ue.formation.referentiel_competence
@ -27,11 +27,70 @@ def form_ue_choix_niveau(formation: Formation, ue: UniteEns) -> str:
}">associer un référentiel de compétence</a> }">associer un référentiel de compétence</a>
</div> </div>
</div>""" </div>"""
annee = 1 if ue.semestre_idx is None else (ue.semestre_idx + 1) // 2 # 1, 2, 3 # Les parcours:
niveaux_by_parcours = ref_comp.get_niveaux_by_parcours(annee) parcours_options = []
for parcour in ref_comp.parcours:
parcours_options.append(
f"""<option value="{parcour.id}" {
'selected' if ue.parcour == parcour else ''}
>{parcour.libelle}
</option>"""
)
newline = "\n"
return f"""
<div class="ue_choix_niveau">
<form class="form_ue_choix_niveau">
<div class="cont_ue_choix_niveau">
<div>
<b>Parcours&nbsp;:</b>
<select class="select_parcour"
onchange="set_ue_parcour(this);"
data-ue_id="{ue.id}"
data-setter="{
url_for( "notes.set_ue_parcours", scodoc_dept=g.scodoc_dept)
}">
<option value="" {
'selected' if ue.parcour is None else ''
}>Tous</option>
{newline.join(parcours_options)}
</select>
</div>
<div>
<b>Niveau de compétence&nbsp;:</b>
<select class="select_niveau_ue"
onchange="set_ue_niveau_competence(this);"
data-ue_id="{ue.id}"
data-setter="{
url_for( "notes.set_ue_niveau_competence", scodoc_dept=g.scodoc_dept)
}">
</select>
</div>
</div>
</form>
</div>
"""
def get_ue_niveaux_options_html(ue: UniteEns) -> str:
"""fragment html avec les options du menu de sélection du
niveau de compétences associé à une UE.
Si l'UE n'a pas de parcours associé: présente les niveaux
de tous les parcours.
Si l'UE a un parcours: seulement les niveaux de ce parcours.
"""
ref_comp: ApcReferentielCompetences = ue.formation.referentiel_competence
if ref_comp is None:
return ""
# Les niveaux:
annee = ue.annee() # 1, 2, 3
parcours, niveaux_by_parcours = ref_comp.get_niveaux_by_parcours(
annee, parcour=ue.parcour
)
# Les niveaux déjà associés à d'autres UE du même semestre # Les niveaux déjà associés à d'autres UE du même semestre
autres_ues = formation.ues.filter_by(semestre_idx=ue.semestre_idx) autres_ues = ue.formation.ues.filter_by(semestre_idx=ue.semestre_idx)
niveaux_autres_ues = { niveaux_autres_ues = {
oue.niveau_competence_id for oue in autres_ues if oue.id != ue.id oue.niveau_competence_id for oue in autres_ues if oue.id != ue.id
} }
@ -44,13 +103,13 @@ def form_ue_choix_niveau(formation: Formation, ue: UniteEns) -> str:
else: else:
disabled = "" disabled = ""
options.append( options.append(
f"""<option value="{n.id}" {'selected' f"""<option value="{n.id}" {
if ue.niveau_competence == n else ''} 'selected' if ue.niveau_competence == n else ''}
{disabled}>{n.annee} {n.competence.titre_long} {disabled}>{n.annee} {n.competence.titre_long}
niveau {n.ordre}</option>""" niveau {n.ordre}</option>"""
) )
options.append("""</optgroup>""") options.append("""</optgroup>""")
for parcour in ref_comp.parcours: for parcour in parcours:
if len(niveaux_by_parcours[parcour.id]): if len(niveaux_by_parcours[parcour.id]):
options.append(f"""<optgroup label="Parcours {parcour.libelle}">""") options.append(f"""<optgroup label="Parcours {parcour.libelle}">""")
for n in niveaux_by_parcours[parcour.id]: for n in niveaux_by_parcours[parcour.id]:
@ -65,46 +124,7 @@ def form_ue_choix_niveau(formation: Formation, ue: UniteEns) -> str:
niveau {n.ordre}</option>""" niveau {n.ordre}</option>"""
) )
options.append("""</optgroup>""") options.append("""</optgroup>""")
options_str = "\n".join(options) return (
return f""" f"""<option value="" {'selected' if ue.niveau_competence is None else ''}>aucun</option>"""
<div class="ue_choix_niveau"> + "\n".join(options)
<form class="form_ue_choix_niveau"> )
<b>Niveau de compétence associé:</b>
<select onchange="set_ue_niveau_competence(this);"
data-ue_id="{ue.id}"
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"""
ue = UniteEns.query.get_or_404(ue_id)
autres_ues = ue.formation.ues.filter_by(semestre_idx=ue.semestre_idx)
niveaux_autres_ues = {
oue.niveau_competence_id for oue in autres_ues if oue.id != ue.id
}
if niveau_id in niveaux_autres_ues:
log(
f"set_ue_niveau_competence: denying association of {ue} to already associated {niveau_id}"
)
return "", 409 # conflict
if niveau_id == "":
niveau = ""
# 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()
log(f"set_ue_niveau_competence( {ue}, {niveau} )")
return "", 204

View File

@ -774,7 +774,9 @@ class DecisionsProposeesAnnee(DecisionsProposees):
def list_ue_parcour_etud( def list_ue_parcour_etud(
formsemestre: FormSemestre, etud: Identite, res: ResultatsSemestreBUT formsemestre: FormSemestre, etud: Identite, res: ResultatsSemestreBUT
) -> tuple[ApcParcours, list[UniteEns]]: ) -> tuple[ApcParcours, list[UniteEns]]:
"""Parcour dans lequel l'étudiant est inscrit, et liste des UEs pour ce semestre""" """Parcour dans lequel l'étudiant est inscrit,
et liste des UEs à valider pour ce semestre
"""
if res.etuds_parcour_id[etud.id] is None: if res.etuds_parcour_id[etud.id] is None:
parcour = None parcour = None
# pas de parcour: prend toutes les UEs (non bonus) # pas de parcour: prend toutes les UEs (non bonus)

View File

@ -107,11 +107,15 @@ 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: def get_niveaux_by_parcours(
self, annee, parcour: "ApcParcours" = None
) -> tuple[list["ApcParcours"], dict]:
""" """
Construit la liste des niveaux de compétences pour chaque parcours Construit la liste des niveaux de compétences pour chaque parcours
de ce référentiel. de ce référentiel, ou seulement pour le parcours donné.
Les niveaux sont groupés par parcours, en isolant les niveaux de tronc commun. 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: 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 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). la clé "TC" (toujours présente mais éventuellement liste vide si pas de tronc commun).
@ -122,10 +126,14 @@ class ApcReferentielCompetences(db.Model, XMLModel):
parcour.id : [ ApcNiveau ] parcour.id : [ ApcNiveau ]
} }
""" """
parcours = self.parcours.order_by(ApcParcours.numero).all() parcours_ref = self.parcours.order_by(ApcParcours.numero).all()
if parcour is None:
parcours = parcours_ref
else:
parcours = [parcour]
niveaux_by_parcours = { niveaux_by_parcours = {
parcour.id: ApcNiveau.niveaux_annee_de_parcours(parcour, annee, self) parcour.id: ApcNiveau.niveaux_annee_de_parcours(parcour, annee, self)
for parcour in parcours for parcour in parcours_ref
} }
# Cherche tronc commun # Cherche tronc commun
if niveaux_by_parcours: if niveaux_by_parcours:
@ -154,7 +162,7 @@ class ApcReferentielCompetences(db.Model, XMLModel):
niveau for niveau in niveaux_parcours_1 if niveau.id in niveaux_ids_tc niveau for niveau in niveaux_parcours_1 if niveau.id in niveaux_ids_tc
] ]
niveaux_by_parcours_no_tc["TC"] = niveaux_tc niveaux_by_parcours_no_tc["TC"] = niveaux_tc
return niveaux_by_parcours_no_tc return parcours, niveaux_by_parcours_no_tc
class ApcCompetence(db.Model, XMLModel): class ApcCompetence(db.Model, XMLModel):
@ -436,6 +444,7 @@ class ApcParcours(db.Model, XMLModel):
lazy="dynamic", lazy="dynamic",
cascade="all, delete-orphan", cascade="all, delete-orphan",
) )
ues = db.relationship("UniteEns", back_populates="parcour")
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<{self.__class__.__name__} {self.id} {self.code!r} ref={self.referentiel}>" return f"<{self.__class__.__name__} {self.id} {self.code!r} ref={self.referentiel}>"

View File

@ -205,8 +205,9 @@ class Formation(db.Model):
`formation.query_ues_parcour(parcour).filter_by(semestre_idx=3)` `formation.query_ues_parcour(parcour).filter_by(semestre_idx=3)`
""" """
return UniteEns.query.filter_by(formation=self).filter( return UniteEns.query.filter_by(formation=self).filter(
UniteEns.niveau_competence_id == ApcNiveau.id,
UniteEns.type == UE_STANDARD, UniteEns.type == UE_STANDARD,
UniteEns.niveau_competence_id == ApcNiveau.id,
(UniteEns.parcour_id == parcour.id) | (UniteEns.parcour_id == None),
ApcParcoursNiveauCompetence.competence_id == ApcNiveau.competence_id, ApcParcoursNiveauCompetence.competence_id == ApcNiveau.competence_id,
ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id, ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id,
ApcAnneeParcours.parcours_id == parcour.id, ApcAnneeParcours.parcours_id == parcour.id,

View File

@ -1,9 +1,11 @@
"""ScoDoc 9 models : Unités d'Enseignement (UE) """ScoDoc 9 models : Unités d'Enseignement (UE)
""" """
from app import db from app import db, log
from app.models import APO_CODE_STR_LEN from app.models import APO_CODE_STR_LEN
from app.models import SHORT_STR_LEN from app.models import SHORT_STR_LEN
from app.models.but_refcomp import ApcNiveau, ApcParcours
from app.scodoc.sco_exceptions import ScoFormationConflict
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
@ -49,6 +51,9 @@ class UniteEns(db.Model):
niveau_competence_id = db.Column(db.Integer, db.ForeignKey("apc_niveau.id")) niveau_competence_id = db.Column(db.Integer, db.ForeignKey("apc_niveau.id"))
niveau_competence = db.relationship("ApcNiveau", back_populates="ues") niveau_competence = db.relationship("ApcNiveau", back_populates="ues")
parcour_id = db.Column(db.Integer, db.ForeignKey("apc_parcours.id"), index=True)
parcour = db.relationship("ApcParcours", 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")
@ -83,6 +88,12 @@ class UniteEns(db.Model):
e.pop("module_ue_coefs", None) e.pop("module_ue_coefs", None)
return e return e
def annee(self) -> int:
"""L'année dans la formation (commence à 1).
En APC seulement, en classic renvoie toujours 1.
"""
return 1 if self.semestre_idx is None else (self.semestre_idx - 1) // 2 + 1
def is_locked(self): def is_locked(self):
"""True if UE should not be modified """True if UE should not be modified
(contains modules used in a locked formsemestre) (contains modules used in a locked formsemestre)
@ -135,3 +146,72 @@ class UniteEns(db.Model):
if self.code_apogee: if self.code_apogee:
return {x.strip() for x in self.code_apogee.split(",") if x} return {x.strip() for x in self.code_apogee.split(",") if x}
return set() return set()
def _check_apc_conflict(self, new_niveau_id: int, new_parcour_id: int):
"raises ScoFormationConflict si (niveau, parcours) pas unique dans ce semestre"
# Les UE du même semestre que nous:
ues_sem = self.formation.ues.filter_by(semestre_idx=self.semestre_idx)
if (new_niveau_id, new_parcour_id) in (
(oue.niveau_competence_id, oue.parcour_id)
for oue in ues_sem
if oue.id != self.id
):
log(
f"set_ue_niveau_competence: {self}: ({new_niveau_id}, {new_parcour_id}) déjà associé"
)
raise ScoFormationConflict()
def set_niveau_competence(self, niveau: ApcNiveau):
"""Associe cette UE au niveau de compétence indiqué.
Le niveau doit être dans le parcours de l'UE, s'il y en a un.
Assure que ce soit la seule dans son parcours.
Sinon, raises ScoFormationConflict.
Si niveau est None, désassocie.
"""
if niveau is not None:
self._check_apc_conflict(niveau.id, self.parcour_id)
# Le niveau est-il dans le parcours ? Sinon, erreur
if self.parcour and niveau.id not in (
n.id
for n in niveau.niveaux_annee_de_parcours(
self.parcour, self.annee(), self.formation.referentiel_competence
)
):
log(
f"set_niveau_competence: niveau {niveau} hors parcours {self.parcour}"
)
return
self.niveau_competence = niveau
db.session.add(self)
db.session.commit()
log(f"ue.set_niveau_competence( {self}, {niveau} )")
def set_parcour(self, parcour: ApcParcours):
"""Associe cette UE au parcours indiqué.
Assure que ce soit la seule dans son parcours.
Sinon, raises ScoFormationConflict.
Si niveau est None, désassocie.
"""
if (parcour is not None) and self.niveau_competence is not None:
self._check_apc_conflict(self.niveau_competence.id, parcour.id)
self.parcour = parcour
# Le niveau est-il dans ce parcours ? Sinon, l'enlève
if (
parcour
and self.niveau_competence
and self.niveau_competence.id
not in (
n.id
for n in self.niveau_competence.niveaux_annee_de_parcours(
parcour, self.annee(), self.formation.referentiel_competence
)
)
):
self.niveau_competence = None
db.session.add(self)
db.session.commit()
log(f"ue.set_parcour( {self}, {parcour} )")

View File

@ -458,7 +458,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
if tf[0] == 0: if tf[0] == 0:
niveau_competence_div = "" niveau_competence_div = ""
if ue and is_apc: if ue and is_apc:
niveau_competence_div = apc_edit_ue.form_ue_choix_niveau(formation, ue) 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

View File

@ -172,8 +172,9 @@ class ScoInvalidDateError(ScoValueError):
pass pass
# Pour les API JSON
class APIInvalidParams(Exception): class APIInvalidParams(Exception):
"""Exception pour les API JSON"""
status_code = 400 status_code = 400
def __init__(self, message, status_code=None, payload=None): def __init__(self, message, status_code=None, payload=None):
@ -184,6 +185,11 @@ class APIInvalidParams(Exception):
self.payload = payload self.payload = payload
def to_dict(self): def to_dict(self):
"dict"
rv = dict(self.payload or ()) rv = dict(self.payload or ())
rv["message"] = self.message rv["message"] = self.message
return rv return rv
class ScoFormationConflict(Exception):
"""Conflit cohérence formation (APC)"""

View File

@ -2282,6 +2282,25 @@ div.formation_list_ues div.ue_choix_niveau b {
font-weight: normal; font-weight: normal;
} }
div.cont_ue_choix_niveau {
display: inline-flex;
flex-wrap: wrap;
}
div.cont_ue_choix_niveau>div {
display: inline-flex;
margin-left: 8px;
align-items: center;
}
div.cont_ue_choix_niveau select {
margin-left: 4px;
}
div.cont_ue_choix_niveau select.select_niveau_ue {
width: 490px;
}
div#ue_list_modules { div#ue_list_modules {
background-color: rgb(251, 225, 165); background-color: rgb(251, 225, 165);
border: 1px solid blue; border: 1px solid blue;

View File

@ -11,6 +11,7 @@ $().ready(function () {
}); });
update_bonus_description(); update_bonus_description();
} }
update_menus_niveau_competence();
}); });
function update_bonus_description() { function update_bonus_description() {
@ -36,6 +37,22 @@ function update_ue_list() {
}); });
} }
function set_ue_parcour(elem) {
let ue_id = elem.dataset.ue_id;
let parcour_id = elem.value;
let set_ue_parcour_url = elem.dataset.setter;
$.post(set_ue_parcour_url,
{
ue_id: ue_id,
parcour_id: parcour_id,
},
function (result) {
sco_message("UE associée au parcours");
update_menus_niveau_competence();
}
);
}
function set_ue_niveau_competence(elem) { function set_ue_niveau_competence(elem) {
let ue_id = elem.dataset.ue_id; let ue_id = elem.dataset.ue_id;
let niveau_id = elem.value; let niveau_id = elem.value;
@ -46,7 +63,6 @@ function set_ue_niveau_competence(elem) {
niveau_id: niveau_id, niveau_id: niveau_id,
}, },
function (result) { function (result) {
// alert("niveau de compétence enregistré"); // XXX #frontend à améliorer
sco_message("niveau de compétence enregistré"); sco_message("niveau de compétence enregistré");
update_menus_niveau_competence(); update_menus_niveau_competence();
@ -57,18 +73,33 @@ function set_ue_niveau_competence(elem) {
// Met à jour les niveaux utilisés (disabled) ou non affectés // Met à jour les niveaux utilisés (disabled) ou non affectés
// dans les menus d'association UE <-> niveau // dans les menus d'association UE <-> niveau
function update_menus_niveau_competence() { function update_menus_niveau_competence() {
let selected_niveaux = []; // let selected_niveaux = [];
document.querySelectorAll("form.form_ue_choix_niveau select").forEach( // document.querySelectorAll("form.form_ue_choix_niveau select").forEach(
elem => { selected_niveaux.push(elem.value); } // elem => { selected_niveaux.push(elem.value); }
); // );
document.querySelectorAll("form.form_ue_choix_niveau select").forEach( // document.querySelectorAll("form.form_ue_choix_niveau select").forEach(
// elem => {
// for (let i = 0; i < elem.options.length; i++) {
// elem.options[i].disabled = (i != elem.options.selectedIndex)
// && (selected_niveaux.indexOf(elem.options[i].value) != -1)
// && (elem.options[i].value != "");
// }
// }
// );
// nouveau:
document.querySelectorAll("select.select_niveau_ue").forEach(
elem => { elem => {
for (let i = 0; i < elem.options.length; i++) { let ue_id = elem.dataset.ue_id;
elem.options[i].disabled = (i != elem.options.selectedIndex) $.get("get_ue_niveaux_options_html",
&& (selected_niveaux.indexOf(elem.options[i].value) != -1) {
&& (elem.options[i].value != ""); ue_id: ue_id,
} },
function (result) {
elem.innerHTML = result;
}
);
} }
); );
} }

View File

@ -3,88 +3,86 @@
<div class="formation_list_ues"> <div class="formation_list_ues">
<div class="formation_list_ues_titre">Unités d'Enseignement (UEs)</div> <div class="formation_list_ues_titre">Unités d'Enseignement (UEs)</div>
{% for semestre_idx in semestre_ids %} {% for semestre_idx in semestre_ids %}
<div class="formation_list_ues_sem">Semestre S{{semestre_idx}} (ECTS: {{ects_by_sem[semestre_idx] | safe}})</div> <div class="formation_list_ues_sem">Semestre S{{semestre_idx}} (ECTS: {{ects_by_sem[semestre_idx] | safe}})</div>
<ul class="apc_ue_list"> <ul class="apc_ue_list">
{% for ue in ues_by_sem[semestre_idx] %} {% for ue in ues_by_sem[semestre_idx] %}
<li class="notes_ue_list"> <li class="notes_ue_list">
{% if editable and not loop.first %} {% if editable and not loop.first %}
<a href="{{ url_for('notes.ue_move', <a href="{{ url_for('notes.ue_move',
scodoc_dept=g.scodoc_dept, ue_id=ue.id, after=0 ) scodoc_dept=g.scodoc_dept, ue_id=ue.id, after=0 )
}}" class="aud">{{icons.arrow_up|safe}}</a> }}" class="aud">{{icons.arrow_up|safe}}</a>
{% else %} {% else %}
{{icons.arrow_none|safe}} {{icons.arrow_none|safe}}
{% endif %} {% endif %}
{% if editable and not loop.last %} {% if editable and not loop.last %}
<a href="{{ url_for('notes.ue_move', <a href="{{ url_for('notes.ue_move',
scodoc_dept=g.scodoc_dept, ue_id=ue.id, after=1 ) scodoc_dept=g.scodoc_dept, ue_id=ue.id, after=1 )
}}" class="aud">{{icons.arrow_down|safe}}</a> }}" class="aud">{{icons.arrow_down|safe}}</a>
{% else %} {% else %}
{{icons.arrow_none|safe}} {{icons.arrow_none|safe}}
{% endif %} {% endif %}
</span> </span>
<a class="smallbutton" href="{{ url_for('notes.ue_delete', <a class="smallbutton" href="{{ url_for('notes.ue_delete',
scodoc_dept=g.scodoc_dept, ue_id=ue.id) scodoc_dept=g.scodoc_dept, ue_id=ue.id)
}}">{% if editable and not ue.modules.count() %}{{icons.delete|safe}}{% else %}{{icons.delete_disabled|safe}}{% endif %}</a> }}">{% if editable and not ue.modules.count() %}{{icons.delete|safe}}{% else
%}{{icons.delete_disabled|safe}}{% endif %}</a>
<span class="ue_type_{{ue.type}}">
<span class="ue_type_{{ue.type}}">
<span class="ue_color_indicator" style="background:{{ <span class="ue_color_indicator" style="background:{{
ue.color if ue.color is not none else 'blue'}}"></span> ue.color if ue.color is not none else 'blue'}}"></span>
<b>{{ue.acronyme}} <a class="discretelink" href="{{ <b>{{ue.acronyme}} <a class="discretelink" href="{{
url_for('notes.ue_infos', scodoc_dept=g.scodoc_dept, ue_id=ue.id)}}" url_for('notes.ue_infos', scodoc_dept=g.scodoc_dept, ue_id=ue.id)}}" title="{{ue.acronyme}}: {{
title="{{ue.acronyme}}: {{
('pas de compétence associée' ('pas de compétence associée'
if ue.niveau_competence is none if ue.niveau_competence is none
else 'compétence ' + ue.niveau_competence.annee + ' ' + ue.niveau_competence.competence.titre_long) else 'compétence ' + ue.niveau_competence.annee + ' ' + ue.niveau_competence.competence.titre_long)
if ue.type == 0 if ue.type == 0
else '' else ''
}}" }}">{{ue.titre}}</a>
>{{ue.titre}}</a>
</b> </b>
{% set virg = joiner(", ") %} {% set virg = joiner(", ") %}
<span class="ue_code">( <span class="ue_code">(
{%- if ue.ue_code -%}{{ virg() }}code {{ue.ue_code}} {%- endif -%} {%- if ue.ue_code -%}{{ virg() }}code {{ue.ue_code}} {%- endif -%}
{{ virg() }} {{ virg() }}
{%- if ue.type == 0 -%} {%- if ue.type == 0 -%}
{{ue.ects {{ue.ects
if ue.ects is not none if ue.ects is not none
else '<span class="missing_ue_ects">aucun</span>'|safe else '<span class="missing_ue_ects">aucun</span>'|safe
}} ECTS }} ECTS
{%- endif -%} {%- endif -%}
{%- if ue.code_apogee -%} {%- if ue.code_apogee -%}
{{ virg() }} Apo {{ue.code_apogee}} {{ virg() }} Apo {{ue.code_apogee}}
{%- endif -%} {%- endif -%}
) )
</span>
</span> </span>
{% if editable and not ue.is_locked() %} </span>
<a class="stdlink" href="{{ url_for('notes.ue_edit',
{% if editable and not ue.is_locked() %}
<a class="stdlink" href="{{ url_for('notes.ue_edit',
scodoc_dept=g.scodoc_dept, ue_id=ue.id) scodoc_dept=g.scodoc_dept, ue_id=ue.id)
}}">modifier</a> }}">modifier</a>
{% endif %} {% endif %}
{{ form_ue_choix_niveau(formation, ue)|safe }} {{ form_ue_choix_niveau(ue)|safe }}
{% if ue.type == 1 and ue.modules.count() == 0 %} {% if ue.type == 1 and ue.modules.count() == 0 %}
<span class="warning" title="pas de module, donc pas de bonus calculé">aucun module rattaché !</span> <span class="warning" title="pas de module, donc pas de bonus calculé">aucun module rattaché !</span>
{% endif %} {% endif %}
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
{% if editable %} {% if editable %}
<ul> <ul>
<li class="notes_ue_list notes_ue_list_add"><a class="stdlink" href="{{ <li class="notes_ue_list notes_ue_list_add"><a class="stdlink" href="{{
url_for('notes.ue_create', url_for('notes.ue_create',
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formation_id=formation.id, formation_id=formation.id,
default_semestre_idx=semestre_idx, default_semestre_idx=semestre_idx,
)}}" )}}">ajouter une UE</a>
>ajouter une UE</a> </li>
</li> </ul>
</ul> {% endif %}
{% endif %} {% endfor %}
{% endfor %}
</div> </div>

View File

@ -33,11 +33,10 @@ Emmanuel Viennet, 2021
from operator import itemgetter from operator import itemgetter
import time import time
from xml.etree import ElementTree
import flask import flask
from flask import abort, flash, redirect, render_template, url_for from flask import abort, flash, redirect, render_template, url_for
from flask import current_app, g, request from flask import g, request
from flask_login import current_user from flask_login import current_user
from app import db from app import db
@ -52,6 +51,7 @@ from app.but import jury_but_view
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.models import ScolarNews from app.models import ScolarNews
from app.models.but_refcomp import ApcNiveau, ApcParcours
from app.models.config import ScoDocSiteConfig from app.models.config import ScoDocSiteConfig
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.models.formsemestre import FormSemestre from app.models.formsemestre import FormSemestre
@ -59,6 +59,7 @@ from app.models.formsemestre import FormSemestreUEComputationExpr
from app.models.moduleimpls import ModuleImpl from app.models.moduleimpls import ModuleImpl
from app.models.modules import Module from app.models.modules import Module
from app.models.ues import UniteEns from app.models.ues import UniteEns
from app.scodoc.sco_exceptions import ScoFormationConflict
from app.views import notes_bp as bp from app.views import notes_bp as bp
from app.decorators import ( from app.decorators import (
@ -403,10 +404,50 @@ sco_publish(
@scodoc @scodoc
@permission_required(Permission.ScoChangeFormation) @permission_required(Permission.ScoChangeFormation)
def set_ue_niveau_competence(): def set_ue_niveau_competence():
"associe UE et niveau" """Associe UE et niveau.
Si le niveau_id est "", désassocie."""
ue_id = request.form.get("ue_id") ue_id = request.form.get("ue_id")
niveau_id = request.form.get("niveau_id") niveau_id = request.form.get("niveau_id")
return apc_edit_ue.set_ue_niveau_competence(ue_id, niveau_id) if niveau_id == "":
niveau_id = None
ue: UniteEns = UniteEns.query.get_or_404(ue_id)
niveau = None if niveau_id is None else ApcNiveau.query.get_or_404(niveau_id)
try:
ue.set_niveau_competence(niveau)
except ScoFormationConflict:
return "", 409 # conflict
return "", 204
@bp.route("/set_ue_parcours", methods=["POST"])
@scodoc
@permission_required(Permission.ScoChangeFormation)
def set_ue_parcours():
"""Associe UE et parcours BUT.
Si le parcour_id est "", désassocie."""
ue_id = request.form.get("ue_id")
parcour_id = request.form.get("parcour_id")
if parcour_id == "":
parcour_id = None
ue: UniteEns = UniteEns.query.get_or_404(ue_id)
parcour = None if parcour_id is None else ApcParcours.query.get_or_404(parcour_id)
try:
ue.set_parcour(parcour)
except ScoFormationConflict:
return "", 409 # conflict
return "", 204
@bp.route("/get_ue_niveaux_options_html")
@scodoc
@permission_required(Permission.ScoView)
def get_ue_niveaux_options_html():
"""fragment html avec les options du menu de sélection du
niveau de compétences associé à une UE
"""
ue_id = request.args.get("ue_id")
ue: UniteEns = UniteEns.query.get_or_404(ue_id)
return apc_edit_ue.get_ue_niveaux_options_html(ue)
@bp.route("/ue_list") # backward compat @bp.route("/ue_list") # backward compat

View File

@ -0,0 +1,34 @@
"""Association UE/Parcours
Revision ID: dbb4a0b19dbb
Revises: 6bc3f51154b4
Create Date: 2022-10-29 19:06:12.897905
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "dbb4a0b19dbb"
down_revision = "6bc3f51154b4"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("notes_ue", sa.Column("parcour_id", sa.Integer(), nullable=True))
op.create_index(
op.f("ix_notes_ue_parcour_id"), "notes_ue", ["parcour_id"], unique=False
)
op.create_foreign_key(None, "notes_ue", "apc_parcours", ["parcour_id"], ["id"])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, "notes_ue", type_="foreignkey")
op.drop_index(op.f("ix_notes_ue_parcour_id"), table_name="notes_ue")
op.drop_column("notes_ue", "parcour_id")
# ### end Alembic commands ###

View File

@ -1,7 +1,7 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.3.60" SCOVERSION = "9.3.60-dev"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"