Saisie automatique des décisions de jury BUT pour semestres pairs ou impairs.

This commit is contained in:
Emmanuel Viennet 2023-01-23 07:38:47 -03:00
parent 165dac0496
commit d3248a37ad
8 changed files with 107 additions and 61 deletions

View File

@ -693,20 +693,20 @@ class DecisionsProposeesAnnee(DecisionsProposees):
db.session.commit() db.session.commit()
def record(self, code: str, no_overwrite=False): def record(self, code: str, no_overwrite=False) -> 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 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.
""" """
if self.inscription_etat != scu.INSCRIT: if self.inscription_etat != scu.INSCRIT:
return return False
if code and not code in self.codes: if code and not code in self.codes:
raise ScoValueError( raise ScoValueError(
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 or (self.code_valide is not None and no_overwrite): if code == self.code_valide or (self.code_valide is not None and no_overwrite):
self.recorded = True self.recorded = True
return # no change return False # no change
if self.validation: if self.validation:
db.session.delete(self.validation) db.session.delete(self.validation)
db.session.commit() db.session.commit()
@ -746,9 +746,10 @@ class DecisionsProposeesAnnee(DecisionsProposees):
next_semestre_id, next_semestre_id,
) )
self.recorded = True
db.session.commit() db.session.commit()
self.recorded = True
self.invalidate_formsemestre_cache() self.invalidate_formsemestre_cache()
return True
def invalidate_formsemestre_cache(self): def invalidate_formsemestre_cache(self):
"invalide le résultats des deux formsemestres" "invalide le résultats des deux formsemestres"
@ -759,13 +760,20 @@ class DecisionsProposeesAnnee(DecisionsProposees):
if self.formsemestre_pair is not None: if self.formsemestre_pair is not None:
sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre_pair.id) sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre_pair.id)
def record_all(self, no_overwrite: bool = True): def record_all(
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.
- Pour les RCUE: n'enregistre que si la nouvelle décision est plus favorable que l'ancienne. - Pour les RCUE: n'enregistre que si la nouvelle décision est plus favorable que l'ancienne.
Si only_validantes, n'enregistre que des décisions "validantes" de droit: ADM ou CMP.
Return: True si au moins un code modifié et enregistré.
""" """
# Toujours valider dans l'ordre UE, RCUE, Année: modif = False
# Toujours valider dans l'ordre UE, RCUE, Année
annee_scolaire = self.formsemestre.annee_scolaire() annee_scolaire = self.formsemestre.annee_scolaire()
# UEs # UEs
for dec_ue in self.decisions_ues.values(): for dec_ue in self.decisions_ues.values():
@ -774,25 +782,40 @@ class DecisionsProposeesAnnee(DecisionsProposees):
) and dec_ue.formsemestre.annee_scolaire() == annee_scolaire: ) and dec_ue.formsemestre.annee_scolaire() == annee_scolaire:
# 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
# enregistre le code jury seulement s'il n'y a pas déjà de code if (not only_validantes) or code in sco_codes.CODES_UE_VALIDES_DE_DROIT:
# (no_overwrite=True) sauf en mode test yaml # enregistre le code jury seulement s'il n'y a pas déjà de code
dec_ue.record(code, no_overwrite=no_overwrite) # (no_overwrite=True) sauf en mode test yaml
# RCUE : enregistre seulement si pas déjà validé "mieux" modif |= dec_ue.record(code, no_overwrite=no_overwrite)
# 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
if (not dec_rcue.recorded) and ( if (
(not dec_rcue.validation) (not dec_rcue.recorded)
or BUT_CODES_ORDERED.get(dec_rcue.validation.code, 0) and ( # enregistre seulement si pas déjà validé "mieux"
< BUT_CODES_ORDERED.get(code, 0) (not dec_rcue.validation)
or BUT_CODES_ORDERED.get(dec_rcue.validation.code, 0)
< BUT_CODES_ORDERED.get(code, 0)
)
and ( # décision validante de droit ?
(
(not only_validantes)
or code in sco_codes.CODES_RCUE_VALIDES_DE_DROIT
)
)
): ):
dec_rcue.record(code, no_overwrite=no_overwrite) modif |= dec_rcue.record(code, no_overwrite=no_overwrite)
# 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 # enregistre le code jury seulement s'il n'y a pas déjà de code
# (no_overwrite=True) sauf en mode test yaml # (no_overwrite=True) sauf en mode test yaml
self.record(code, no_overwrite=no_overwrite) if (
not only_validantes
) or code in sco_codes.CODES_ANNEE_BUT_VALIDES_DE_DROIT:
modif |= self.record(code, no_overwrite=no_overwrite)
return modif
def erase(self, only_one_sem=False): def erase(self, only_one_sem=False):
"""Efface les décisions de jury de cet étudiant """Efface les décisions de jury de cet étudiant
@ -1005,23 +1028,23 @@ 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): def record(self, code: str, no_overwrite=False) -> 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
XXX on pourra imposer ici d'autres règles de cohérence XXX on pourra imposer ici d'autres règles de cohérence
""" """
if self.rcue is None: if self.rcue is None:
return # pas de RCUE a enregistrer return False # pas de RCUE a enregistrer
if self.inscription_etat != scu.INSCRIT: if self.inscription_etat != scu.INSCRIT:
return return False
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 or (self.code_valide is not None and no_overwrite):
self.recorded = True self.recorded = True
return # 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
if self.validation: if self.validation:
db.session.delete(self.validation) db.session.delete(self.validation)
@ -1072,6 +1095,7 @@ class DecisionsProposeesRCUE(DecisionsProposees):
) )
self.code_valide = code # mise à jour état self.code_valide = code # mise à jour état
self.recorded = True self.recorded = True
return True
def erase(self): def erase(self):
"""Efface la décision de jury de cet étudiant pour cet RCUE""" """Efface la décision de jury de cet étudiant pour cet RCUE"""
@ -1203,9 +1227,10 @@ 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): def record(self, code: str, no_overwrite=False) -> 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. 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: if code and not code in self.codes:
raise ScoValueError( raise ScoValueError(
@ -1213,7 +1238,7 @@ class DecisionsProposeesUE(DecisionsProposees):
) )
if code == self.code_valide or (self.code_valide is not None and no_overwrite): if code == self.code_valide or (self.code_valide is not None and no_overwrite):
self.recorded = True self.recorded = True
return # no change return False # no change
self.erase() self.erase()
if code is None: if code is None:
self.validation = None self.validation = None
@ -1244,6 +1269,7 @@ class DecisionsProposeesUE(DecisionsProposees):
sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre.id) sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre.id)
self.code_valide = code # mise à jour self.code_valide = code # mise à jour
self.recorded = True self.recorded = True
return True
def erase(self): def erase(self):
"""Efface la décision de jury de cet étudiant pour cette UE""" """Efface la décision de jury de cet étudiant pour cette UE"""

View File

@ -18,29 +18,29 @@ 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, no_overwrite: 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.
Ne modifie 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".
Normalement, only_adm est True et on n'enregistre que les décisions ADM (de droit). - Ne modifie jamais de décisions déjà enregistrées (sauf si no_overwrite est faux,
Si only_adm est faux, on enregistre la première décision proposée par ScoDoc ce qui est utilisé pour certains tests unitaires).
(mode à n'utiliser que pour les tests) - 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
(mode à n'utiliser que pour les tests unitaires vérifiant la saisie des jurys)
Si no_overwrite est vrai (défaut), ne -écrit jamais les codes déjà enregistrés Returns: nombre d'étudiants pour lesquels on a enregistré au moins un code.
(utiliser faux pour certains tests)
Returns: nombre d'étudiants "admis"
""" """
if not formsemestre.formation.is_apc(): if not formsemestre.formation.is_apc():
raise ScoValueError("fonction réservée aux formations BUT") raise ScoValueError("fonction réservée aux formations BUT")
nb_admis = 0 nb_etud_modif = 0
with sco_cache.DeferredSemCacheManager(): with sco_cache.DeferredSemCacheManager():
for etudid in formsemestre.etuds_inscriptions: for etudid in formsemestre.etuds_inscriptions:
etud: Identite = Identite.query.get(etudid) etud: Identite = Identite.query.get(etudid)
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre) deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
if deca.admis: # année réussie nb_etud_modif += deca.record_all(
nb_admis += 1 no_overwrite=no_overwrite, only_validantes=only_adm
if deca.admis or not only_adm: )
deca.record_all(no_overwrite=no_overwrite)
db.session.commit() db.session.commit()
return nb_admis return nb_etud_modif

View File

@ -563,9 +563,8 @@ class JuryPE(object):
dec = nt.get_etud_decision_sem( dec = nt.get_etud_decision_sem(
etudid etudid
) # quelle est la décision du jury ? ) # quelle est la décision du jury ?
if dec and dec["code"] in list( if dec and (dec["code"] in sco_codes_parcours.CODES_SEM_VALIDES):
sco_codes_parcours.CODES_SEM_VALIDES.keys() # isinstance( sesMoyennes[i+1], float) and
): # isinstance( sesMoyennes[i+1], float) and
# mT = sesMoyennes[i+1] # substitue la moyenne si le semestre suivant est "valide" # mT = sesMoyennes[i+1] # substitue la moyenne si le semestre suivant est "valide"
leFid = sem["formsemestre_id"] leFid = sem["formsemestre_id"]
else: else:

View File

@ -187,20 +187,23 @@ CODES_EXPL = {
# Les codes de semestres: # Les codes de semestres:
CODES_JURY_SEM = {ADC, ADJ, ADM, AJ, ATB, ATJ, ATT, DEF, NAR, RAT} CODES_JURY_SEM = {ADC, ADJ, ADM, AJ, ATB, ATJ, ATT, DEF, NAR, RAT}
CODES_SEM_VALIDES = {ADM: True, ADC: True, ADJ: True} # semestre validé CODES_SEM_VALIDES_DE_DROIT = {ADM, ADC}
CODES_SEM_ATTENTES = {ATT: True, ATB: True, ATJ: True} # semestre en attente CODES_SEM_VALIDES = CODES_SEM_VALIDES_DE_DROIT | {ADJ} # semestre validé
CODES_SEM_ATTENTES = {ATT, ATB, ATJ} # semestre en attente
CODES_SEM_REO = {NAR: 1} # reorientation CODES_SEM_REO = {NAR} # reorientation
CODES_UE_VALIDES = {ADM: True, CMP: True, ADJ: True, ADJR: True} CODES_UE_VALIDES_DE_DROIT = {ADM, CMP} # validation "de droit"
CODES_UE_VALIDES = CODES_UE_VALIDES_DE_DROIT | {ADJ, ADJR}
"UE validée" "UE validée"
CODES_RCUE_VALIDES = {ADM, CMP, ADJ} CODES_RCUE_VALIDES_DE_DROIT = {ADM, CMP}
CODES_RCUE_VALIDES = CODES_RCUE_VALIDES_DE_DROIT | {ADJ}
"Niveau RCUE validé" "Niveau RCUE validé"
# Pour le BUT: # Pour le BUT:
CODES_ANNEE_BUT_VALIDES_DE_DROIT = {ADM, PASD}
CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL} CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL}
CODES_RCUE = {ADM, AJ, CMP}
BUT_BARRE_UE8 = 8.0 - NOTES_TOLERANCE BUT_BARRE_UE8 = 8.0 - NOTES_TOLERANCE
BUT_BARRE_UE = BUT_BARRE_RCUE = 10.0 - NOTES_TOLERANCE BUT_BARRE_UE = BUT_BARRE_RCUE = 10.0 - NOTES_TOLERANCE
BUT_RCUE_SUFFISANT = 8.0 - NOTES_TOLERANCE BUT_RCUE_SUFFISANT = 8.0 - NOTES_TOLERANCE
@ -230,17 +233,17 @@ BUT_CODES_ORDERED = {
def code_semestre_validant(code: str) -> bool: def code_semestre_validant(code: str) -> bool:
"Vrai si ce CODE entraine la validation du semestre" "Vrai si ce CODE entraine la validation du semestre"
return CODES_SEM_VALIDES.get(code, False) return code in CODES_SEM_VALIDES
def code_semestre_attente(code: str) -> bool: def code_semestre_attente(code: str) -> bool:
"Vrai si ce CODE est un code d'attente (semestre validable plus tard par jury ou compensation)" "Vrai si ce CODE est un code d'attente (semestre validable plus tard par jury ou compensation)"
return CODES_SEM_ATTENTES.get(code, False) return code in CODES_SEM_ATTENTES
def code_ue_validant(code: str) -> bool: def code_ue_validant(code: str) -> bool:
"Vrai si ce code d'UE est validant (ie attribue les ECTS)" "Vrai si ce code d'UE est validant (ie attribue les ECTS)"
return CODES_UE_VALIDES.get(code, False) return code in CODES_UE_VALIDES
DEVENIR_EXPL = { DEVENIR_EXPL = {

View File

@ -890,7 +890,7 @@ def formsemestre_validate_ues(formsemestre_id, etudid, code_etat_sem, assiduite)
car ils ne dépendent que de la note d'UE et de la validation ou non du semestre. car ils ne dépendent que de la note d'UE et de la validation ou non du semestre.
Les UE des semestres NON ASSIDUS ne sont jamais validées (code AJ). Les UE des semestres NON ASSIDUS ne sont jamais validées (code AJ).
""" """
valid_semestre = CODES_SEM_VALIDES.get(code_etat_sem, False) valid_semestre = code_etat_sem in CODES_SEM_VALIDES
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
formsemestre = FormSemestre.query.get_or_404(formsemestre_id) formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)

View File

@ -781,8 +781,8 @@ def form_decision_manuelle(Se, formsemestre_id, etudid, desturl="", sortcol=None
) )
# Choix code semestre: # Choix code semestre:
codes = list(sco_codes_parcours.CODES_JURY_SEM) codes = sorted(sco_codes_parcours.CODES_JURY_SEM)
codes.sort() # fortuitement, cet ordre convient bien ! # fortuitement, cet ordre convient bien !
H.append( H.append(
'<tr><td>Code semestre: </td><td><select name="code_etat"><option value="" selected>Choisir...</option>' '<tr><td>Code semestre: </td><td><select name="code_etat"><option value="" selected>Choisir...</option>'

View File

@ -8,14 +8,22 @@
{% block app_content %} {% block app_content %}
<h2>Calcul automatique des décisions de jury annuelle BUT</h2> <h2>Calcul automatique des décisions de jury du BUT</h2>
<ul> <ul>
<li>Seuls les étudiants qui valident l'année seront affectés: <li>N'enregistre jamais de décisions de l'année scolaire précédente, même
tous les niveaux de compétences (RCUE) validables si on a des RCUE "à cheval" sur deux années.
(moyenne annuelle au dessus de 10); </li>
<li>Ne modifie jamais de décisions déjà enregistrées.
</li>
<li>N'enregistre que les décisions <b>validantes de droit: ADM ou CMP</b>.
</li>
<li>L'assiduité n'est <b>pas</b> prise en compte.
</li> </li>
<li>l'assiduité n'est <b>pas</b> prise en compte;</li>
</ul> </ul>
<p>
En conséquence, saisir ensuite <b>manuellement les décisions manquantes</b>,
notamment sur les UEs en dessous de 10.
</p>
<p class="warning"> <p class="warning">
Il est nécessaire de relire soigneusement les décisions à l'issue de cette procédure ! Il est nécessaire de relire soigneusement les décisions à l'issue de cette procédure !
</p> </p>

View File

@ -2548,10 +2548,10 @@ def formsemestre_validation_auto_but(formsemestre_id: int = None):
form = jury_but_forms.FormSemestreValidationAutoBUTForm() form = jury_but_forms.FormSemestreValidationAutoBUTForm()
if request.method == "POST": if request.method == "POST":
if not form.cancel.data: if not form.cancel.data:
nb_admis = jury_but_validation_auto.formsemestre_validation_auto_but( nb_etud_modif = jury_but_validation_auto.formsemestre_validation_auto_but(
formsemestre formsemestre
) )
flash(f"Décisions enregistrées ({nb_admis} admis)") flash(f"Décisions enregistrées ({nb_etud_modif} étudiants modifiés)")
return redirect( return redirect(
url_for( url_for(
"notes.formsemestre_saisie_jury", "notes.formsemestre_saisie_jury",
@ -2563,7 +2563,7 @@ def formsemestre_validation_auto_but(formsemestre_id: int = None):
"but/formsemestre_validation_auto_but.html", "but/formsemestre_validation_auto_but.html",
form=form, form=form,
sco=ScoData(formsemestre=formsemestre), sco=ScoData(formsemestre=formsemestre),
title=f"Calcul automatique jury BUT", title="Calcul automatique jury BUT",
) )
@ -2641,7 +2641,17 @@ def formsemestre_validation_auto(formsemestre_id):
message="<p>Opération non autorisée pour %s</h2>" % current_user, message="<p>Opération non autorisée pour %s</h2>" % current_user,
dest_url=scu.ScoURL(), dest_url=scu.ScoURL(),
) )
formsemestre: FormSemestre = FormSemestre.query.filter_by(
id=formsemestre_id, dept_id=g.scodoc_dept_id
).first_or_404()
if formsemestre.formation.is_apc():
return redirect(
url_for(
"notes.formsemestre_validation_auto_but",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
)
)
return sco_formsemestre_validation.formsemestre_validation_auto(formsemestre_id) return sco_formsemestre_validation.formsemestre_validation_auto(formsemestre_id)