Jury BUT:

- Modification gestion de l'enregistrement des codes.
- Signale quand un RCUE change de code.
- Calcul auto du jury: peut modifier les décisions RCUE.
This commit is contained in:
Emmanuel Viennet 2023-06-22 19:00:56 +02:00 committed by iziram
parent b696f772bf
commit 449c1f0cb0
10 changed files with 103 additions and 64 deletions

View File

@ -278,11 +278,15 @@ class DecisionsProposeesAnnee(DecisionsProposees):
)
if self.formsemestre_impair is not None:
self.validation = ApcValidationAnnee.query.filter_by(
etudid=self.etud.id,
formation_id=self.formsemestre.formation_id,
ordre=self.annee_but,
).first()
self.validation = (
ApcValidationAnnee.query.filter_by(
etudid=self.etud.id,
ordre=self.annee_but,
)
.join(Formation)
.filter_by(formation_code=self.formsemestre.formation.formation_code)
.first()
)
else:
self.validation = None
if self.validation is not None:
@ -721,7 +725,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
et qu'il n'y en a pas déjà, enregistre ceux par défaut.
"""
log("jury_but.DecisionsProposeesAnnee.record_form")
code_annee = None
code_annee = self.codes[0] # si pas dans le form, valeur par defaut
codes_rcues = [] # [ (dec_rcue, code), ... ]
codes_ues = [] # [ (dec_ue, code), ... ]
for key in form:
@ -753,16 +757,15 @@ class DecisionsProposeesAnnee(DecisionsProposees):
dec_ue.record(code)
for dec_rcue, code in codes_rcues:
dec_rcue.record(code)
self.record(code_annee) # XXX , mark_recorded=False)
self.record(code_annee)
self.record_autorisation_inscription(code_annee)
self.record_all()
self.recorded = True
db.session.commit()
def record(self, code: str, no_overwrite=False, mark_recorded: bool = True) -> bool:
def record(self, code: str, mark_recorded: bool = True) -> bool:
"""Enregistre le code de l'année, et au besoin l'autorisation d'inscription.
Si no_overwrite, ne fait rien si un code est déjà enregistré.
Si l'étudiant est DEM ou DEF, ne fait rien.
Si mark_recorded est vrai, positionne self.recorded
"""
@ -773,23 +776,34 @@ class DecisionsProposeesAnnee(DecisionsProposees):
f"code annee <tt>{html.escape(code)}</tt> invalide pour formsemestre {html.escape(self.formsemestre)}"
)
if code != self.code_valide and (self.code_valide is None or not no_overwrite):
if code != self.code_valide:
# Enregistrement du code annuel BUT
if self.validation:
db.session.delete(self.validation)
db.session.commit()
if code is None:
self.validation = None
if self.validation:
db.session.delete(self.validation)
self.validation = None
db.session.commit()
else:
self.validation = ApcValidationAnnee(
etudid=self.etud.id,
formsemestre=self.formsemestre_impair,
formation_id=self.formsemestre.formation_id,
ordre=self.annee_but,
annee_scolaire=self.annee_scolaire(),
code=code,
)
if self.validation is None:
self.validation = ApcValidationAnnee(
etudid=self.etud.id,
formsemestre=self.formsemestre_impair,
formation_id=self.formsemestre.formation_id,
ordre=self.annee_but,
annee_scolaire=self.annee_scolaire(),
code=code,
)
else: # Update validation année BUT
self.validation.etud = self.etud
self.validation.formsemestre = self.formsemestre_impair
self.validation.formation_id = self.formsemestre.formation_id
self.validation.ordre = self.annee_but
self.validation.annee_scolaire = self.annee_scolaire()
self.validation.code = code
self.validation.date = datetime.now()
db.session.add(self.validation)
db.session.commit()
log(f"Recording {self}: {code}")
Scolog.logdb(
method="jury_but",
@ -840,9 +854,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
)
return res and self.etud.id in res.get_etudids_attente()
def record_all(
self, no_overwrite: bool = True, only_validantes: bool = False
) -> bool:
def record_all(self, only_validantes: bool = False) -> bool:
"""Enregistre les codes qui n'ont pas été spécifiés par le formulaire,
et sont donc en mode "automatique".
- Si "à cheval", ne modifie pas les codes UE de l'année scolaire précédente.
@ -868,9 +880,8 @@ class DecisionsProposeesAnnee(DecisionsProposees):
# rappel: le code par défaut est en tête
code = dec_ue.codes[0] if dec_ue.codes else None
if (not only_validantes) or code in sco_codes.CODES_UE_VALIDES_DE_DROIT:
# enregistre le code jury seulement s'il n'y a pas déjà de code
# (no_overwrite=True) sauf en mode test yaml
modif |= dec_ue.record(code, no_overwrite=no_overwrite)
# enregistre le code jury
modif |= dec_ue.record(code)
# RCUE :
for dec_rcue in self.decisions_rcue_by_niveau.values():
code = dec_rcue.codes[0] if dec_rcue.codes else None
@ -888,17 +899,15 @@ class DecisionsProposeesAnnee(DecisionsProposees):
)
)
):
modif |= dec_rcue.record(code, no_overwrite=no_overwrite)
modif |= dec_rcue.record(code)
# Année:
if not self.recorded:
# rappel: le code par défaut est en tête
code = self.codes[0] if self.codes else None
# enregistre le code jury seulement s'il n'y a pas déjà de code
# (no_overwrite=True) sauf en mode test yaml
if (
not only_validantes
) or code in sco_codes.CODES_ANNEE_BUT_VALIDES_DE_DROIT:
modif |= self.record(code, no_overwrite=no_overwrite)
modif |= self.record(code)
self.record_autorisation_inscription(code)
return modif
@ -1133,7 +1142,7 @@ class DecisionsProposeesRCUE(DecisionsProposees):
return f"""<{self.__class__.__name__} rcue={self.rcue} valid={self.code_valide
} codes={self.codes} explanation={self.explanation}"""
def record(self, code: str, no_overwrite=False) -> bool:
def record(self, code: str) -> bool:
"""Enregistre le code RCUE.
Note:
- si le RCUE est ADJ, les UE non validées sont passées à ADJ
@ -1147,7 +1156,7 @@ class DecisionsProposeesRCUE(DecisionsProposees):
raise ScoValueError(
f"code UE invalide pour ue_id={self.ue.id}: {html.escape(code)}"
)
if code == self.code_valide or (self.code_valide is not None and no_overwrite):
if code == self.code_valide:
self.recorded = True
return False # no change
parcours_id = self.parcour.id if self.parcour is not None else None
@ -1322,11 +1331,15 @@ class DecisionsProposeesRCUE(DecisionsProposees):
if annee_inferieure < 1:
return
# Garde-fou: Année déjà validée ?
validations_annee: ApcValidationAnnee = ApcValidationAnnee.query.filter_by(
etudid=self.etud.id,
ordre=annee_inferieure,
formation_id=self.rcue.formsemestre_1.formation_id,
).all()
validations_annee: ApcValidationAnnee = (
ApcValidationAnnee.query.filter_by(
etudid=self.etud.id,
ordre=annee_inferieure,
)
.join(Formation)
.filter_by(formation_code=self.rcue.formsemestre_1.formation.code)
.all()
)
if len(validations_annee) > 1:
log(
f"warning: {len(validations_annee)} validations d'année\n{validations_annee}"
@ -1519,16 +1532,15 @@ class DecisionsProposeesUE(DecisionsProposees):
self.codes = [sco_codes.AJ, sco_codes.ADJ] + self.codes
self.explanation = "notes insuffisantes"
def record(self, code: str, no_overwrite=False) -> bool:
def record(self, code: str) -> bool:
"""Enregistre le code jury pour cette UE.
Si no_overwrite, n'enregistre pas s'il y a déjà un code.
Return: True si code enregistré (modifié)
"""
if code and not code in self.codes:
raise ScoValueError(
f"code UE invalide pour ue_id={self.ue.id}: {html.escape(code)}"
)
if code == self.code_valide or (self.code_valide is not None and no_overwrite):
if code == self.code_valide:
self.recorded = True
return False # no change
self.erase()
@ -1627,7 +1639,6 @@ class BUTCursusEtud: # WIP TODO
ApcValidationAnnee.query.filter_by(
etudid=self.etud.id,
ordre=ordre,
formation_id=self.formsemestre.formation_id,
)
.join(Formation)
.filter(

View File

@ -16,14 +16,12 @@ from app.scodoc.sco_exceptions import ScoValueError
def formsemestre_validation_auto_but(
formsemestre: FormSemestre, only_adm: bool = True, no_overwrite: bool = True
formsemestre: FormSemestre, only_adm: bool = True
) -> int:
"""Calcul automatique des décisions de jury sur une "année" BUT.
- N'enregistre jamais de décisions de l'année scolaire précédente, même
si on a des RCUE "à cheval".
- Ne modifie jamais de décisions déjà enregistrées (sauf si no_overwrite est faux,
ce qui est utilisé pour certains tests unitaires).
- Normalement, only_adm est True et on n'enregistre que les décisions validantes
de droit: ADM ou CMP.
En revanche, si only_adm est faux, on enregistre la première décision proposée par ScoDoc
@ -38,9 +36,7 @@ def formsemestre_validation_auto_but(
for etudid in formsemestre.etuds_inscriptions:
etud = Identite.get_etud(etudid)
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
nb_etud_modif += deca.record_all(
no_overwrite=no_overwrite, only_validantes=only_adm
)
nb_etud_modif += deca.record_all(only_validantes=only_adm)
db.session.commit()
return nb_etud_modif

View File

@ -155,6 +155,7 @@ def _gen_but_select(
disabled: bool = False,
klass: str = "",
data: dict = {},
code_valide_label: str = "",
) -> str:
"Le menu html select avec les codes"
# if disabled: # mauvaise idée car le disabled est traité en JS
@ -164,7 +165,10 @@ def _gen_but_select(
f"""<option value="{code}"
{'selected' if code == code_valide else ''}
class="{'recorded' if code == code_valide else ''}"
>{code}</option>"""
>{code
if ((code != code_valide) or not code_valide_label)
else code_valide_label
}</option>"""
for code in codes
]
)
@ -246,6 +250,7 @@ def _gen_but_rcue(dec_rcue: DecisionsProposeesRCUE, niveau: ApcNiveau) -> str:
"""
code_propose_menu = dec_rcue.code_valide # le code enregistré
code_valide_label = code_propose_menu
if dec_rcue.validation:
if dec_rcue.code_valide == dec_rcue.codes[0]:
descr_validation = dec_rcue.validation.html()
@ -257,6 +262,9 @@ def _gen_but_rcue(dec_rcue: DecisionsProposeesRCUE, niveau: ApcNiveau) -> str:
> sco_codes.BUT_CODES_ORDER[dec_rcue.code_valide]
):
code_propose_menu = dec_rcue.codes[0]
code_valide_label = (
f"{dec_rcue.codes[0]} (actuel {dec_rcue.code_valide})"
)
scoplement = f"""<div class="scoplement">{descr_validation}</div>"""
else:
scoplement = "" # "pas de validation"
@ -282,7 +290,8 @@ def _gen_but_rcue(dec_rcue: DecisionsProposeesRCUE, niveau: ApcNiveau) -> str:
code_propose_menu,
disabled=True,
klass="manual code_rcue",
data = { "niveau_id" : str(niveau.id)}
data = { "niveau_id" : str(niveau.id)},
code_valide_label = code_valide_label,
)}
</div>
</div>

View File

@ -223,8 +223,15 @@ BUT_CODES_PASSAGE = {
# les codes, du plus "défavorable" à l'étudiant au plus favorable:
# (valeur par défaut 0)
BUT_CODES_ORDER = {
NAR: 0,
ABAN: 0,
ABL: 0,
DEM: 0,
DEF: 0,
EXCLU: 0,
NAR: 0,
UEBSL: 0,
RAT: 5,
RED: 6,
AJ: 10,
ATJ: 20,
CMP: 50,
@ -233,7 +240,7 @@ BUT_CODES_ORDER = {
PASD: 60,
ADJR: 90,
ADSUP: 90,
ADJ: 100,
ADJ: 90,
ADM: 100,
}

View File

@ -495,7 +495,9 @@ class ApoEtud(dict):
ApcValidationAnnee.query.filter_by(
formsemestre_id=formsemestre.id,
etudid=self.etud["etudid"],
formation_id=self.cur_sem["formation_id"],
formation_id=self.cur_sem[
"formation_id"
], # XXX utiliser formation_code
).first()
)
self.is_nar = (

View File

@ -168,6 +168,7 @@ div.but_niveau_ue.recorded_different,
div.but_niveau_rcue.recorded_different {
box-shadow: 0 0 0 3px red;
outline: dashed 3px var(--color-recorded);
background-color: yellow;
}
div.but_niveau_ue.annee_prec {

View File

@ -1128,7 +1128,8 @@ div.sco_help {
padding: 8px;
border-radius: 4px;
font-style: italic;
background-color: rgb(200, 200, 220);
max-width: 800px;
background-color: rgb(209, 255, 214);
}
div.vertical_spacing_but {

View File

@ -8,6 +8,8 @@
(transcription paramétrable par votre administrateur ScoDoc).
</p>
<div class="but_doc_section">Codes d'année</div>
<em>Les codes d'année BUT sont associés à la formation et non au semestre: on ne valide
qu'une seule fois BUT1, BUT2 puis BUT3.</em>
<div class="but_doc">
<table>
<tr>
@ -100,7 +102,8 @@
</div>
<div class="but_doc_section">Codes RCUE (niveaux de compétences annuels)</div>
<em>Les codes de RCUE sont associés à la formation: chaque niveau de compétence
est validé une fois au maximum. En cas de redoublement, le code RCUE peut changer.</em>
<div class="but_doc">
<table>
<tr>
@ -161,7 +164,9 @@
</div>
<div class="but_doc_section">Codes des Unités d'Enseignement (UE)</div>
<em>Les codes d'UE sont associés aux UE d'un semestre. En cas de redoublement,
l'UE antérieure garde son code, non écrasé par le redoublement. Chaque UE suivie a son code.
</em>
<div class="but_doc">
<table>
<tr>

View File

@ -8,19 +8,27 @@
{% block app_content %}
<div class="sco_help">
<h2>Calcul automatique des décisions de jury du BUT</h2>
<ul>
<li>N'enregistre jamais de décisions de l'année scolaire précédente, même
si on a des RCUE "à cheval" sur deux années.
</li>
<li>Ne modifie jamais de décisions déjà enregistrées.
<li><b>Attention: peut modifier des décisions déjà enregistrées</b>, si la
validation de droit est calculée. Par exemple, vous aviez saisi <b>RAT</b>
pour un étudiant dont les moyennes d'UE dépassent 10 mais qui pour une
raison particulière ne valide pas son année. Le calcul automatique peut
remplacer ce <b>RAT</b> par un <b>ADM</b>, ScoDoc considérant que les
conditions sont satisfaites. On peut éviter cela en laissant une note de
l'étudiant en ATTente.
</li>
<li>N'enregistre que les décisions <b>validantes de droit: ADM ou CMP</b>.
</li>
<li>N'enregistre pas de décision si l'étudiant a une ou plusieurs notes en ATTente.
</li>
<li>L'assiduité n'est <b>pas</b> prise en compte.
</li>
<li>L'assiduité n'est <b>pas</b> prise en compte. </li>
</ul>
<p>
En conséquence, saisir ensuite <b>manuellement les décisions manquantes</b>,
@ -34,9 +42,10 @@
<li>Il est nécessaire de relire soigneusement les décisions à l'issue de cette procédure !</li>
</div>
</div>
<div class="row">
<div class="col-md-5">
<div class="col-md-10">
{{ wtf.quick_form(form) }}
</div>
</div>

View File

@ -108,9 +108,7 @@ def test_but_jury_GEII_lyon(test_client):
# Saisie de toutes les décisions de jury "automatiques"
# et vérification des résultats attendus:
for formsemestre in formsemestres:
formsemestre_validation_auto_but(
formsemestre, only_adm=False, no_overwrite=False
)
formsemestre_validation_auto_but(formsemestre, only_adm=False)
yaml_setup_but.but_test_jury(formsemestre, doc)