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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,6 +8,8 @@
(transcription paramétrable par votre administrateur ScoDoc). (transcription paramétrable par votre administrateur ScoDoc).
</p> </p>
<div class="but_doc_section">Codes d'année</div> <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"> <div class="but_doc">
<table> <table>
<tr> <tr>
@ -100,7 +102,8 @@
</div> </div>
<div class="but_doc_section">Codes RCUE (niveaux de compétences annuels)</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"> <div class="but_doc">
<table> <table>
<tr> <tr>
@ -161,7 +164,9 @@
</div> </div>
<div class="but_doc_section">Codes des Unités d'Enseignement (UE)</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"> <div class="but_doc">
<table> <table>
<tr> <tr>

View File

@ -8,19 +8,27 @@
{% block app_content %} {% block app_content %}
<div class="sco_help">
<h2>Calcul automatique des décisions de jury du BUT</h2> <h2>Calcul automatique des décisions de jury du BUT</h2>
<ul> <ul>
<li>N'enregistre jamais de décisions de l'année scolaire précédente, même <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. si on a des RCUE "à cheval" sur deux années.
</li> </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>
<li>N'enregistre que les décisions <b>validantes de droit: ADM ou CMP</b>. <li>N'enregistre que les décisions <b>validantes de droit: ADM ou CMP</b>.
</li> </li>
<li>N'enregistre pas de décision si l'étudiant a une ou plusieurs notes en ATTente. <li>N'enregistre pas de décision si l'étudiant a une ou plusieurs notes en ATTente.
</li> </li>
<li>L'assiduité n'est <b>pas</b> prise en compte. <li>L'assiduité n'est <b>pas</b> prise en compte. </li>
</li>
</ul> </ul>
<p> <p>
En conséquence, saisir ensuite <b>manuellement les décisions manquantes</b>, 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> <li>Il est nécessaire de relire soigneusement les décisions à l'issue de cette procédure !</li>
</div> </div>
</div>
<div class="row"> <div class="row">
<div class="col-md-5"> <div class="col-md-10">
{{ wtf.quick_form(form) }} {{ wtf.quick_form(form) }}
</div> </div>
</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" # Saisie de toutes les décisions de jury "automatiques"
# et vérification des résultats attendus: # et vérification des résultats attendus:
for formsemestre in formsemestres: for formsemestre in formsemestres:
formsemestre_validation_auto_but( formsemestre_validation_auto_but(formsemestre, only_adm=False)
formsemestre, only_adm=False, no_overwrite=False
)
yaml_setup_but.but_test_jury(formsemestre, doc) yaml_setup_but.but_test_jury(formsemestre, doc)