Editions formations BUT: meilleurs avertissements et possibilité de changer des associations en super-admin si décision saisies.

This commit is contained in:
Emmanuel Viennet 2025-02-19 16:38:22 +01:00
parent 7015789358
commit 845b0e9363
13 changed files with 125 additions and 48 deletions

View File

@ -209,26 +209,40 @@ def ue_set_parcours(ue_id: int):
return {"status": ok, "message": error_message}
@bp.route(
"/formation/ue/<int:ue_id>/assoc_niveau/<int:niveau_id>/force",
defaults={"force": True},
methods=["POST"],
)
@api_web_bp.route(
"/formation/ue/<int:ue_id>/assoc_niveau/<int:niveau_id>/force",
defaults={"force": True},
methods=["POST"],
)
@bp.route(
"/formation/ue/<int:ue_id>/assoc_niveau/<int:niveau_id>",
defaults={"force": False},
methods=["POST"],
)
@api_web_bp.route(
"/formation/ue/<int:ue_id>/assoc_niveau/<int:niveau_id>",
defaults={"force": False},
methods=["POST"],
)
@login_required
@scodoc
@permission_required(Permission.EditFormation)
@as_json
def ue_assoc_niveau(ue_id: int, niveau_id: int):
"""Associe l'UE au niveau de compétence."""
def ue_assoc_niveau(ue_id: int, niveau_id: int, force=False):
"""Associe l'UE au niveau de compétence.
Si force, modifie l'association même si des décisions de jury sont présentes.
"""
query = UniteEns.query.filter_by(id=ue_id)
if g.scodoc_dept:
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
ue: UniteEns = query.first_or_404()
niveau: ApcNiveau = ApcNiveau.get_or_404(niveau_id)
ok, error_message = ue.set_niveau_competence(niveau)
ok, error_message = ue.set_niveau_competence(niveau, force=force)
if not ok:
if g.scodoc_dept: # "usage web"
flash(error_message, "error")
@ -238,19 +252,31 @@ def ue_assoc_niveau(ue_id: int, niveau_id: int):
return {"status": 0}
@bp.route(
"/formation/ue/<int:ue_id>/desassoc_niveau/force",
defaults={"force": True},
methods=["POST"],
)
@api_web_bp.route(
"/formation/ue/<int:ue_id>/desassoc_niveau/force",
defaults={"force": True},
methods=["POST"],
)
@bp.route(
"/formation/ue/<int:ue_id>/desassoc_niveau",
defaults={"force": False},
methods=["POST"],
)
@api_web_bp.route(
"/formation/ue/<int:ue_id>/desassoc_niveau",
defaults={"force": False},
methods=["POST"],
)
@login_required
@scodoc
@permission_required(Permission.EditFormation)
@as_json
def ue_desassoc_niveau(ue_id: int):
def ue_desassoc_niveau(ue_id: int, force=False):
"""Désassocie cette UE de son niveau de compétence
(si elle n'est pas associée, ne fait rien).
"""
@ -258,7 +284,7 @@ def ue_desassoc_niveau(ue_id: int):
if g.scodoc_dept:
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
ue: UniteEns = query.first_or_404()
ok, error_message = ue.set_niveau_competence(None)
ok, error_message = ue.set_niveau_competence(None, force=force)
if not ok:
if g.scodoc_dept: # "usage web"
flash(error_message, "error")

View File

@ -96,11 +96,13 @@ def but_parcours_validated(etud: Identite, parcour_id: int | None) -> bool:
class EtudCursusBUT:
"""L'état de l'étudiant dans son cursus BUT
Liste des niveaux validés/à valider
(utilisé pour le résumé sur la fiche étudiant)
(utilisé pour le résumé sur la fiche étudiant).
"""
def __init__(self, etud: Identite, formation: Formation):
"""formation indique la spécialité préparée"""
"""formation indique la spécialité préparée.
Peut lever l'exception ScoValueError ou ScoNoReferentielCompetences
"""
# Vérifie que l'étudiant est bien inscrit à un sem. de cette formation
if formation.id not in (
ins.formsemestre.formation.id for ins in etud.formsemestre_inscriptions
@ -142,10 +144,10 @@ class EtudCursusBUT:
if niveau is None:
raise ScoValueError(
f"""UE d'un RCUE ({
validation_rcue.ue1.acronyme}/{validation_rcue.ue1.acronyme
validation_rcue.ue1.acronyme}/{validation_rcue.ue2.acronyme
}) non associée à un niveau de compétence.
Vérifiez la formation et les associations de ses UEs.
Étudiant {etud.nomprenom}.
Étudiant {etud.html_link_fiche()}.
Formations concernées: <a href="{
url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
formation_id=validation_rcue.ue1.formation_id,
@ -156,7 +158,8 @@ class EtudCursusBUT:
formation_id=validation_rcue.ue2.formation_id,
semestre_idx=validation_rcue.ue2.semestre_idx)
}">{validation_rcue.ue2.acronyme}</a>.
"""
""",
safe=True,
)
if not niveau.competence.id in self.validation_par_competence_et_annee:
self.validation_par_competence_et_annee[niveau.competence.id] = {}
@ -625,8 +628,9 @@ def formsemestre_warning_apc_setup(
)
if nb_ues_sans_parcours != nb_ues_tot:
H.append(
"""Le semestre n'est associé à aucun parcours,
f"""Le semestre n'est associé à aucun parcours,
mais les UEs de la formation ont des parcours
({nb_ues_sans_parcours} UEs sans parcours sur {nb_ues_tot} UEs au total).
"""
)
# Vérifie les niveaux de chaque parcours

View File

@ -1473,7 +1473,7 @@ class BonusTarbes(BonusIUTRennes1):
"""Calcul bonus optionnels (sport, culture), règle IUT de Tarbes.
<ul>
<li>Les étudiants opeuvent suivre un ou plusieurs activités optionnelles notées.
<li>Les étudiants peuvent suivre un ou plusieurs activités optionnelles notées.
La meilleure des notes obtenue est prise en compte, si elle est supérieure à 10/20.
</li>
<li>Le trentième des points au dessus de 10 est ajouté à la moyenne des UE en BUT,

View File

@ -464,7 +464,9 @@ class UniteEns(models.ScoDocModel):
> 0
)
def set_niveau_competence(self, niveau: ApcNiveau | None) -> tuple[bool, str]:
def set_niveau_competence(
self, niveau: ApcNiveau | None, force: bool = False
) -> tuple[bool, str]:
"""Associe cette UE au niveau de compétence indiqué.
Le niveau doit être dans l'un des parcours de l'UE (si elle n'est pas
de tronc commun).
@ -473,7 +475,8 @@ class UniteEns(models.ScoDocModel):
Si niveau est None, désassocie.
Si l'UE est utilisée dans un validation de RCUE, on ne peut plus la changer de niveau.
Si l'UE est utilisée dans un validation de RCUE, on ne peut plus la changer
de niveau, sauf si force est vrai.
Returns
- True if (de)association done, False on error.
@ -486,7 +489,7 @@ class UniteEns(models.ScoDocModel):
"La formation n'est pas associée à un référentiel de compétences",
)
# UE utilisée dans des validations RCUE ?
if self.is_used_in_validation_rcue():
if not force and self.is_used_in_validation_rcue():
return (
False,
"UE utilisée dans un RCUE validé: son niveau ne peut plus être modifié",
@ -521,7 +524,7 @@ class UniteEns(models.ScoDocModel):
db.session.commit()
# Invalidation du cache
self.formation.invalidate_cached_sems()
log(f"ue.set_niveau_competence( {self}, {niveau} )")
log(f"ue.set_niveau_competence( {self}, {niveau}, force={force} )")
return True, ""
def set_parcours(self, parcours: list[ApcParcours]) -> tuple[bool, str]:

View File

@ -117,7 +117,7 @@ def do_formsemestre_archive(
dept_id=formsemestre.dept_id,
)
# Tableau recap notes en HTML (pour tous les etudiants, n'utilise pas les groupes)
table_html, _, _ = gen_formsemestre_recapcomplet_html_table(
table_html, _, _, _ = gen_formsemestre_recapcomplet_html_table(
formsemestre, res, include_evaluations=True
)
if table_html:

View File

@ -58,7 +58,7 @@ from app.scodoc import (
)
from app.scodoc.html_sidebar import retreive_formsemestre_from_request
from app.scodoc.sco_bulletins import etud_descr_situation_semestre
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
from app.scodoc.sco_formsemestre_validation import formsemestre_recap_parcours_table
from app.scodoc.sco_permissions import Permission
import app.scodoc.sco_utils as scu
@ -473,7 +473,7 @@ def fiche_etud(etudid=None):
if last_formsemestre and last_formsemestre.formation.is_apc():
try:
but_cursus = cursus_but.EtudCursusBUT(etud, last_formsemestre.formation)
except ScoValueError:
except (ScoValueError, ScoNoReferentielCompetences):
but_cursus = None
refcomp = last_formsemestre.formation.referentiel_competence
if refcomp:

View File

@ -112,7 +112,7 @@ def formsemestre_recapcomplet(
visible_col_ids=visible_col_ids,
)
table_html, _, freq_codes_annuels = _formsemestre_recapcomplet_to_html(
table_html, _, freq_codes_annuels, warnings = _formsemestre_recapcomplet_to_html(
formsemestre,
filename=filename,
mode_jury=mode_jury,
@ -120,11 +120,17 @@ def formsemestre_recapcomplet(
selected_etudid=selected_etudid,
)
H = [
# sco_formsemestre_status.formsemestre_status_head(
# formsemestre_id=formsemestre_id
# ),
]
H = []
if warnings:
H.append(
f"""
<div class="sco_box table-warnings">
<div class="sco_box_title">Avertissements</div>
<ul><li>
{'</li><li>'.join(warnings)}
</li></ul>
</div>"""
)
if len(formsemestre.inscriptions) > 0:
H.append(
f"""<form id="export_menu" name="f" method="get" action="{request.base_url}">
@ -300,15 +306,17 @@ def _formsemestre_recapcomplet_to_html(
if tabformat not in ("html", "evals"):
raise ScoValueError("invalid table format")
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
table_html, table, freq_codes_annuels = gen_formsemestre_recapcomplet_html_table(
formsemestre,
res,
include_evaluations=(tabformat == "evals"),
mode_jury=mode_jury,
filename=filename,
selected_etudid=selected_etudid,
table_html, table, freq_codes_annuels, warnings = (
gen_formsemestre_recapcomplet_html_table(
formsemestre,
res,
include_evaluations=(tabformat == "evals"),
mode_jury=mode_jury,
filename=filename,
selected_etudid=selected_etudid,
)
)
return table_html, table, freq_codes_annuels
return table_html, table, freq_codes_annuels, warnings
def _formsemestre_recapcomplet_to_file(
@ -473,7 +481,7 @@ def gen_formsemestre_recapcomplet_html_table(
mode_jury=False,
filename="",
selected_etudid=None,
) -> tuple[str, TableRecap, collections.Counter]:
) -> tuple[str, TableRecap, collections.Counter, list[str]]:
"""Construit table recap pour le BUT
Cache le résultat pour le semestre.
Note: on cache le HTML et non l'objet Table.
@ -508,11 +516,12 @@ def gen_formsemestre_recapcomplet_html_table(
freq_codes_annuels = (
table.freq_codes_annuels if hasattr(table, "freq_codes_annuels") else None
)
cache_class.set(formsemestre.id, (table_html, freq_codes_annuels))
warnings = table.warnings
cache_class.set(formsemestre.id, (table_html, freq_codes_annuels, warnings))
else:
table_html, freq_codes_annuels = table_html_cached
table_html, freq_codes_annuels, warnings = table_html_cached
return table_html, table, freq_codes_annuels
return table_html, table, freq_codes_annuels, warnings
def _gen_formsemestre_recapcomplet_table(

View File

@ -66,3 +66,9 @@ div.gt_caption {
.dt-scroll-foot {
overflow: visible !important;
}
div.table-warnings {
background-color: yellow;
color: darkred;
max-width: 100%;
}

View File

@ -1284,6 +1284,10 @@ div.sco_box_title {
background-color: rgb(209, 255, 214);
}
div.sco_box.sco_dashed {
border: 1px dashed red;
}
div.vertical_spacing_but {
margin-top: 12px;
}

View File

@ -24,6 +24,7 @@ from app.scodoc.codes_cursus import (
BUT_BARRE_RCUE,
BUT_RCUE_SUFFISANT,
)
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_utils as scu
from app.tables.recap import RowRecap, TableRecap
@ -201,9 +202,13 @@ class TableJury(TableRecap):
self.group_titles[group] = f"Compétences {annee}"
for row in self.rows:
etud = row.etud
cursus_dict = cursus_but.EtudCursusBUT(
etud, self.res.formsemestre.formation
).to_dict()
try:
cursus_dict = cursus_but.EtudCursusBUT(
etud, self.res.formsemestre.formation
).to_dict()
except ScoValueError as exc:
cursus_dict = {}
self.warnings.append(exc.args[0])
first = True
for competence_id in cursus_dict:
for annee in ("BUT1", "BUT2", "BUT3"):

View File

@ -117,6 +117,8 @@ class Table(Element):
#
self.caption = caption
self.origin = origin
self.warnings: list[str] = []
"liste d'avertissements rencontrés en construisant la table"
def _prepare(self):
"""Prepare the table before generation:

View File

@ -124,6 +124,17 @@ Choisissez un parcours...
</div>
{% endif %}
{% if current_user.is_administrator() %}
<div class="sco_box sco_dashed">
<b>Vous êtes super-administrateur.</b>
<div>
<input type="checkbox" id="force_modification" name="force_modification">
<label for="force_modification">forcer modification même si décisions de jury enregistrées (dangereux !)</label>
</div>
</div>
{% endif %}
{% if parcour %}
<div class="help">
@ -150,7 +161,7 @@ Choisissez un parcours...
function ue_assoc_niveau(event, niveau_id) {
let ue_id = event.target.value;
let url = "";
let must_reload = false;
let force = document.getElementById('force_modification').checked;
if (ue_id == "") {
/* Dé-associe */
ue_id = event.target.dataset.ue_id;
@ -162,7 +173,9 @@ function ue_assoc_niveau(event, niveau_id) {
)
}}';
url = desassoc_url.replace('11111', ue_id);
must_reload=true;
if (force) {
url += '/force';
}
} else {
const assoc_url = '{{
url_for(
@ -172,6 +185,9 @@ function ue_assoc_niveau(event, niveau_id) {
)
}}';
url = assoc_url.replace('11111', ue_id).replace('22222', niveau_id);
if (force) {
url += '/force';
}
}
fetch(url, {
method: 'POST',

View File

@ -75,6 +75,8 @@ ETUDID = 1
NIP = "NIP2"
INE = "INE1"
BUL_NB_FIELDS = 15
def test_etudiants_courant(api_headers):
"""
@ -508,7 +510,7 @@ def test_etudiant_bulletin_semestre(api_headers):
)
assert r.status_code == 200
bulletin = r.json()
assert len(bulletin) == 14 # HARDCODED
assert len(bulletin) == BUL_NB_FIELDS
assert verify_fields(bulletin, BULLETIN_FIELDS) is True
assert isinstance(bulletin["version"], str)
@ -845,7 +847,7 @@ def test_etudiant_bulletin_semestre(api_headers):
)
assert r.status_code == 200
bul = r.json()
assert len(bul) == 14 # HARDCODED
assert len(bul) == BUL_NB_FIELDS
######### Test code ine #########
r = requests.get(
@ -856,7 +858,7 @@ def test_etudiant_bulletin_semestre(api_headers):
)
assert r.status_code == 200
bul = r.json()
assert len(bul) == 14 # HARDCODED
assert len(bul) == BUL_NB_FIELDS
######## Bulletin BUT court en pdf #########
r = requests.get(
@ -915,15 +917,15 @@ def test_etudiant_bulletin_semestre(api_headers):
bul = GET(
f"/etudiant/etudid/{ETUDID}/formsemestre/1/bulletin/short", headers=api_headers
)
assert len(bul) == 14 # HARDCODED
assert len(bul) == BUL_NB_FIELDS
######### Test code nip #########
bul = GET(f"/etudiant/nip/{NIP}/formsemestre/1/bulletin/short", headers=api_headers)
assert len(bul) == 14 # HARDCODED
assert len(bul) == BUL_NB_FIELDS
######### Test code ine #########
bul = GET(f"/etudiant/ine/{INE}/formsemestre/1/bulletin/short", headers=api_headers)
assert len(bul) == 14 # HARDCODED
assert len(bul) == BUL_NB_FIELDS
################### SHORT + PDF #####################