Associations UE / Parcours: UI

This commit is contained in:
Emmanuel Viennet 2023-04-11 23:56:50 +02:00
parent 5f719442f0
commit e5cdb2ef69
11 changed files with 84 additions and 159 deletions

View File

@ -13,15 +13,15 @@ from app.models import ApcReferentielCompetences, UniteEns
from app.scodoc import codes_cursus from app.scodoc import codes_cursus
def form_ue_choix_niveau(ue: UniteEns) -> str: def form_ue_choix_parcours(ue: UniteEns) -> str:
"""Form. HTML pour associer une UE à un niveau de compétence. """Form. HTML pour associer une UE à ses parcours.
Le menu select lui même est vide et rempli en JS par appel à get_ue_niveaux_options_html Le menu select lui même est vide et rempli en JS par appel à get_ue_niveaux_options_html
""" """
if ue.type != codes_cursus.UE_STANDARD: if ue.type != codes_cursus.UE_STANDARD:
return "" return ""
ref_comp = ue.formation.referentiel_competence ref_comp = ue.formation.referentiel_competence
if ref_comp is None: if ref_comp is None:
return f"""<div class="ue_choix_niveau"> return f"""<div class="ue_advanced">
<div class="warning">Pas de référentiel de compétence associé à cette formation !</div> <div class="warning">Pas de référentiel de compétence associé à cette formation !</div>
<div><a class="stdlink" href="{ url_for('notes.refcomp_assoc_formation', <div><a class="stdlink" href="{ url_for('notes.refcomp_assoc_formation',
scodoc_dept=g.scodoc_dept, formation_id=ue.formation.id) scodoc_dept=g.scodoc_dept, formation_id=ue.formation.id)
@ -29,8 +29,27 @@ def form_ue_choix_niveau(ue: UniteEns) -> str:
</div> </div>
</div>""" </div>"""
return f""" H = [
"""
<div class="ue_advanced"> <div class="ue_advanced">
<h3>Parcours du BUT</h3>
"""
]
# Choix des parcours
ue_pids = [p.id for p in ue.parcours]
H.append("""<form id="choix_parcours">""")
for parcour in ref_comp.parcours:
H.append(
f"""<label><input type="checkbox" name="{parcour.id}" value="{parcour.id}"
{'checked' if parcour.id in ue_pids else ""}
onclick="set_ue_parcour(this);"
data-setter="{url_for("apiweb.set_ue_parcours", scodoc_dept=g.scodoc_dept, ue_id=ue.id)}"
>{parcour.code}</label>"""
)
H.append("""</form>""")
#
H.append(
f"""
<ul> <ul>
<li> <li>
<a class="stdlink" href="{ <a class="stdlink" href="{
@ -41,6 +60,8 @@ def form_ue_choix_niveau(ue: UniteEns) -> str:
</ul> </ul>
</div> </div>
""" """
)
return "\n".join(H)
def get_ue_niveaux_options_html(ue: UniteEns) -> str: def get_ue_niveaux_options_html(ue: UniteEns) -> str:

View File

@ -371,7 +371,7 @@ def formsemestre_warning_apc_setup(
return "" return ""
if formsemestre.formation.referentiel_competence is None: if formsemestre.formation.referentiel_competence is None:
return f"""<div class="formsemestre_status_warning"> return f"""<div class="formsemestre_status_warning">
La <a class=stdlink" href="{ La <a class="stdlink" href="{
url_for("notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formsemestre.formation.id) url_for("notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formsemestre.formation.id)
}">formation n'est pas associée à un référentiel de compétence.</a> }">formation n'est pas associée à un référentiel de compétence.</a>
</div> </div>
@ -404,12 +404,12 @@ def formsemestre_warning_apc_setup(
return f"""<div class="formsemestre_status_warning"> return f"""<div class="formsemestre_status_warning">
Problème dans la configuration de la formation: Problème dans la configuration de la formation:
<ul> <ul>
<li>{ '<li></li>'.join(H) }</li> <li>{ '</li><li>'.join(H) }</li>
</ul> </ul>
<p class="help">Vérifiez les parcours cochés pour ce semestre, <p class="help">Vérifiez les parcours cochés pour ce semestre,
et les associations entre UE et niveaux <a class=stdlink" href="{ et les associations entre UE et niveaux <a class="stdlink" href="{
url_for("notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formsemestre.formation.id) url_for("notes.parcour_formation", scodoc_dept=g.scodoc_dept, formation_id=formsemestre.formation.id)
}">dans la formation. }">dans la formation.</a>
</p> </p>
</div> </div>
""" """

View File

@ -151,6 +151,7 @@ class FormSemestre(db.Model):
secondary=parcours_formsemestre, secondary=parcours_formsemestre,
lazy="subquery", lazy="subquery",
backref=db.backref("formsemestres", lazy=True), backref=db.backref("formsemestres", lazy=True),
order_by=(ApcParcours.numero, ApcParcours.code),
) )
def __init__(self, **kwargs): def __init__(self, **kwargs):

View File

@ -317,7 +317,7 @@ class UniteEns(db.Model):
if niveau.competence.referentiel.id != self.formation.referentiel_competence.id: if niveau.competence.referentiel.id != self.formation.referentiel_competence.id:
return False, "Le niveau n'appartient pas au référentiel de la formation" return False, "Le niveau n'appartient pas au référentiel de la formation"
if niveau.id == self.niveau_competence_id: if niveau.id == self.niveau_competence_id:
return True # nothing to do return True, "" # nothing to do
if (niveau is not None) and (self.niveau_competence_id is not None): if (niveau is not None) and (self.niveau_competence_id is not None):
ok, error_message = self.check_niveau_unique_dans_parcours( ok, error_message = self.check_niveau_unique_dans_parcours(
niveau, self.parcours niveau, self.parcours

View File

@ -476,9 +476,9 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
cancelbutton="Revenir à la formation", cancelbutton="Revenir à la formation",
) )
if tf[0] == 0: if tf[0] == 0:
niveau_competence_div = "" ue_parcours_div = ""
if ue and is_apc: if ue and is_apc:
niveau_competence_div = apc_edit_ue.form_ue_choix_niveau(ue) ue_parcours_div = apc_edit_ue.form_ue_choix_parcours(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
@ -508,7 +508,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
"\n".join(H) "\n".join(H)
+ tf[1] + tf[1]
+ clone_form + clone_form
+ niveau_competence_div + ue_parcours_div
+ modules_div + modules_div
+ bonus_div + bonus_div
+ ue_div + ue_div

View File

@ -19,12 +19,18 @@ div.les_parcours>div.focus {
opacity: 1; opacity: 1;
} }
div.les_parcours>div>a:hover { div.les_parcours>div.link {
background-color: var(--sco-color-background);
color: navy;
}
div.les_parcours>div.parc>a:hover {
color: #ccc; color: #ccc;
} }
div.les_parcours>div>a, div.les_parcours>div.parc>a,
div.les_parcours>div>a:visited { div.les_parcours>div.parc>a:visited {
color: white; color: white;
} }

View File

@ -22,8 +22,8 @@
div.table_niveaux_parcours { div.table_niveaux_parcours {
margin-left: 12px; margin-left: 12px;
margin-top: 12px; margin-top: 12px;
background: rgb(14, 5, 73); background: rgb(210, 210, 210);
color: #eee; color: #111;
border-radius: 8px; border-radius: 8px;
width: fit-content; width: fit-content;
padding: 8px; padding: 8px;
@ -47,7 +47,7 @@ table.table_niveaux_parcours th {
} }
table.table_niveaux_parcours tr.parcours_but { table.table_niveaux_parcours tr.parcours_but {
color: white; color: #111;
} }
table.table_niveaux_parcours tr.parcours_but td { table.table_niveaux_parcours tr.parcours_but td {
@ -70,7 +70,7 @@ table.table_niveaux_parcours tr td:not(:first-child) {
table.table_niveaux_parcours tr.annee_but td:first-child { table.table_niveaux_parcours tr.annee_but td:first-child {
width: 92px; width: 92px;
font-weight: bold; font-weight: bold;
color: #ddd; color: #111;
} }
table.table_niveaux_parcours tr.annee_but td.empty { table.table_niveaux_parcours tr.annee_but td.empty {

View File

@ -5,6 +5,7 @@
--sco-content-min-width: 600px; --sco-content-min-width: 600px;
--sco-content-max-width: 1024px; --sco-content-max-width: 1024px;
--sco-color-explication: rgb(10, 58, 140); --sco-color-explication: rgb(10, 58, 140);
--sco-color-background: rgb(242, 242, 238);
} }
html, html,
@ -12,7 +13,7 @@ body {
margin: 0; margin: 0;
padding: 0; padding: 0;
width: 100%; width: 100%;
background-color: rgb(242, 242, 238); background-color: var(--sco-color-background);
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 12pt; font-size: 12pt;
} }
@ -937,7 +938,7 @@ span.linktitresem a:visited {
a.stdlink, a.stdlink,
a.stdlink:visited { a.stdlink:visited {
color: blue; color: #0e0e9d;
text-decoration: underline; text-decoration: underline;
} }
@ -2539,6 +2540,10 @@ div.ue_advanced {
margin-right: 15px; margin-right: 15px;
} }
div.ue_advanced h3 {
margin-top: 2px;
}
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,7 +11,6 @@ $().ready(function () {
}); });
update_bonus_description(); update_bonus_description();
} }
update_menus_niveau_competence();
}); });
function update_bonus_description() { function update_bonus_description() {
@ -37,131 +36,26 @@ function update_ue_list() {
}); });
} }
function set_ue_parcour(elem) { function set_ue_parcour(checkbox) {
let ue_id = elem.dataset.ue_id; let url = checkbox.dataset.setter;
let parcour_id = elem.value; const checkboxes = document.querySelectorAll('#choix_parcours input[type="checkbox"]:checked');
let set_ue_parcour_url = elem.dataset.setter; const parcours_ids = [];
$.post(set_ue_parcour_url, checkboxes.forEach(function (checkbox) {
{ parcours_ids.push(checkbox.value);
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) { fetch(url, {
let ue_id = elem.dataset.ue_id;
let niveau_id = elem.value;
let set_ue_niveau_competence_url = elem.dataset.setter;
$.post(set_ue_niveau_competence_url,
{
ue_id: ue_id,
niveau_id: niveau_id,
},
function (result) {
sco_message("niveau de compétence enregistré");
update_menus_niveau_competence();
}
);
}
// Met à jour les niveaux utilisés (disabled) ou non affectés
// dans les menus d'association UE <-> niveau
function update_menus_niveau_competence() {
// let selected_niveaux = [];
// document.querySelectorAll("form.form_ue_choix_niveau select").forEach(
// elem => { selected_niveaux.push(elem.value); }
// );
// 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.niveau_select").forEach(
elem => {
let ue_id = elem.dataset.ue_id;
$.get("get_ue_niveaux_options_html",
{
ue_id: ue_id,
},
function (result) {
elem.innerHTML = result;
}
);
}
);
}
// ---- Nouveau formulaire choix parcours et niveau -----
//document.querySelectorAll("select.select_ue_parcours").forEach(
// elem => { elem.addEventListener('change', change_ue_parcours); }
//);
$().ready(function () {
$('select.select_ue_parcours').multiselect(
{
includeSelectAllOption: false,
nonSelectedText: 'choisir...',
// buttonContainer: '<div id="group_ids_sel_container"/>',
onChange: function (element, checked) {
var parent = element.parent();
var selectedOptions = parent.getValue().split(",");
let set_ue_parcours = element.context.dataset.set_ue_parcours;
fetch(set_ue_parcours, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(selectedOptions)
})
.then(response => response.json())
.then(data => {
if (!data.status) {
sco_message(data.message);
// get the option element corresponding to the selected value
var option = parent.find('option[value="' + element.val() + '"]');
// uncheck the option
option.prop('selected', false);
// refresh the multiselect to reflect the change
parent.multiselect('refresh');
}
})
.catch(error => console.error('Error: ' + error));
// // referme le menu apres chaque choix:
// $("#group_selector .btn-group").removeClass('open');
// if ($("#group_ids_sel").hasClass("submit_on_change")) {
// submit_group_selector();
// }
}
}
);
});
function change_ue_parcours(event) {
const multiselect = event.target;
const selectedOptions = Array.from(this.selectedOptions).map(option => option.value);
fetch('/set_option/', { // XXX TODO
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify(selectedOptions) body: JSON.stringify(parcours_ids)
}) })
.then(response => response.json()) .then(response => response.json())
.then(data => console.log('Success!')) .then(data => {
.catch(error => console.error('Error: ' + error)); if (data.status) {
}; sco_message(data.message);
}
});
}

View File

@ -49,13 +49,24 @@
{# Liens vers les différents parcours #} {# Liens vers les différents parcours #}
<div class="les_parcours"> <div class="les_parcours">
{% for parc in formation.referentiel_competence.parcours %} {% for parc in formation.referentiel_competence.parcours %}
<div class="{{'focus' if parcour and parc.id == parcour.id else ''}}"> <div class="parc {{'focus' if parcour and parc.id == parcour.id else ''}}">
<a href="{{ <a href="{{
url_for('notes.parcour_formation', scodoc_dept=g.scodoc_dept, url_for('notes.parcour_formation', scodoc_dept=g.scodoc_dept,
parcour_id=parc.id, formation_id=formation.id ) parcour_id=parc.id, formation_id=formation.id )
}}">{{parc.code}}</a> }}">{{parc.code}}</a>
</div> </div>
{% endfor %} {% endfor %}
<div class="link">
<a class="stdlink" target="_blank" href="{{
url_for('notes.refcomp_show',
scodoc_dept=g.scodoc_dept, refcomp_id=formation.referentiel_competence.id )
}}">référentiel de compétences</a>
</div>
<div class="link"><a class="stdlink" href="{{
url_for('notes.ue_table',
scodoc_dept=g.scodoc_dept, formation_id=formation.id )
}}">formation</a>
</div>
</div> </div>
{# Description d'un parcours #} {# Description d'un parcours #}
@ -107,19 +118,6 @@ Choisissez un parcours...
</div> </div>
{% endif %} {% endif %}
{# Liens bas de page #}
<div class="links">
<div><a class="stdlink" href="{{
url_for('notes.ue_table',
scodoc_dept=g.scodoc_dept, formation_id=formation.id )
}}">Voir la formation</a>
</div>
<div><a class="stdlink" href="{{
url_for('notes.refcomp_show',
scodoc_dept=g.scodoc_dept, refcomp_id=formation.referentiel_competence.id )
}}">Référentiel de compétences</a>
</div>
</div>
{% if parcour %} {% if parcour %}
<div class="help"> <div class="help">

View File

@ -724,4 +724,4 @@ def _compare_formsemestre_resultat(res: list[dict], ref: list[dict]):
for res_d, ref_d in zip(res, ref): for res_d, ref_d in zip(res, ref):
assert sorted(res_d.keys()) == sorted(ref_d.keys()) assert sorted(res_d.keys()) == sorted(ref_d.keys())
for k in res_d: for k in res_d:
assert res_d[k] == ref_d[k] assert res_d[k] == ref_d[k], f"values for key {k} differ."