forked from ScoDoc/ScoDoc
Editions formations BUT: meilleurs avertissements et possibilité de changer des associations en super-admin si décision saisies.
This commit is contained in:
parent
7015789358
commit
845b0e9363
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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]:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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(
|
||||
|
@ -65,4 +65,10 @@ div.gt_caption {
|
||||
|
||||
.dt-scroll-foot {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
div.table-warnings {
|
||||
background-color: yellow;
|
||||
color: darkred;
|
||||
max-width: 100%;
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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"):
|
||||
|
@ -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:
|
||||
|
@ -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',
|
||||
|
@ -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 #####################
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user