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 committed by iziram
parent 4430eb9a61
commit a4072efe4c
8 changed files with 107 additions and 61 deletions

View File

@ -693,20 +693,20 @@ class DecisionsProposeesAnnee(DecisionsProposees):
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.
Si no_overwrite, ne fait rien si un code est déjà enregistré.
Si l'étudiant est DEM ou DEF, ne fait rien.
"""
if self.inscription_etat != scu.INSCRIT:
return
return False
if code and not code in self.codes:
raise ScoValueError(
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):
self.recorded = True
return # no change
return False # no change
if self.validation:
db.session.delete(self.validation)
db.session.commit()
@ -746,9 +746,10 @@ class DecisionsProposeesAnnee(DecisionsProposees):
next_semestre_id,
)
self.recorded = True
db.session.commit()
self.recorded = True
self.invalidate_formsemestre_cache()
return True
def invalidate_formsemestre_cache(self):
"invalide le résultats des deux formsemestres"
@ -759,13 +760,20 @@ class DecisionsProposeesAnnee(DecisionsProposees):
if self.formsemestre_pair is not None:
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,
et sont donc en mode "automatique".
- 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.
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()
# UEs
for dec_ue in self.decisions_ues.values():
@ -774,25 +782,40 @@ class DecisionsProposeesAnnee(DecisionsProposees):
) and dec_ue.formsemestre.annee_scolaire() == annee_scolaire:
# rappel: le code par défaut est en tête
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
# (no_overwrite=True) sauf en mode test yaml
dec_ue.record(code, no_overwrite=no_overwrite)
# RCUE : enregistre seulement si pas déjà validé "mieux"
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)
# RCUE :
for dec_rcue in self.decisions_rcue_by_niveau.values():
code = dec_rcue.codes[0] if dec_rcue.codes else None
if (not dec_rcue.recorded) and (
(not dec_rcue.validation)
or BUT_CODES_ORDERED.get(dec_rcue.validation.code, 0)
< BUT_CODES_ORDERED.get(code, 0)
if (
(not dec_rcue.recorded)
and ( # enregistre seulement si pas déjà validé "mieux"
(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:
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
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):
"""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
} 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.
Note:
- 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
"""
if self.rcue is None:
return # pas de RCUE a enregistrer
return False # pas de RCUE a enregistrer
if self.inscription_etat != scu.INSCRIT:
return
return False
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):
self.recorded = True
return # no change
return False # no change
parcours_id = self.parcour.id if self.parcour is not None else None
if self.validation:
db.session.delete(self.validation)
@ -1072,6 +1095,7 @@ class DecisionsProposeesRCUE(DecisionsProposees):
)
self.code_valide = code # mise à jour état
self.recorded = True
return True
def erase(self):
"""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.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.
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(
@ -1213,7 +1238,7 @@ class DecisionsProposeesUE(DecisionsProposees):
)
if code == self.code_valide or (self.code_valide is not None and no_overwrite):
self.recorded = True
return # no change
return False # no change
self.erase()
if code is None:
self.validation = None
@ -1244,6 +1269,7 @@ class DecisionsProposeesUE(DecisionsProposees):
sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre.id)
self.code_valide = code # mise à jour
self.recorded = True
return True
def erase(self):
"""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(
formsemestre: FormSemestre, only_adm: bool = True, no_overwrite: bool = True
) -> int:
"""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
"""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".
Normalement, only_adm est True et on n'enregistre que les décisions ADM (de droit).
Si only_adm est faux, on enregistre la première décision proposée par ScoDoc
(mode à n'utiliser que pour les tests)
- 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
(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
(utiliser faux pour certains tests)
Returns: nombre d'étudiants "admis"
Returns: nombre d'étudiants pour lesquels on a enregistré au moins un code.
"""
if not formsemestre.formation.is_apc():
raise ScoValueError("fonction réservée aux formations BUT")
nb_admis = 0
nb_etud_modif = 0
with sco_cache.DeferredSemCacheManager():
for etudid in formsemestre.etuds_inscriptions:
etud: Identite = Identite.query.get(etudid)
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
if deca.admis: # année réussie
nb_admis += 1
if deca.admis or not only_adm:
deca.record_all(no_overwrite=no_overwrite)
nb_etud_modif += deca.record_all(
no_overwrite=no_overwrite, only_validantes=only_adm
)
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(
etudid
) # quelle est la décision du jury ?
if dec and dec["code"] in list(
sco_codes_parcours.CODES_SEM_VALIDES.keys()
): # isinstance( sesMoyennes[i+1], float) and
if dec and (dec["code"] in sco_codes_parcours.CODES_SEM_VALIDES):
# isinstance( sesMoyennes[i+1], float) and
# mT = sesMoyennes[i+1] # substitue la moyenne si le semestre suivant est "valide"
leFid = sem["formsemestre_id"]
else:

View File

@ -187,20 +187,23 @@ CODES_EXPL = {
# Les codes de semestres:
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_ATTENTES = {ATT: True, ATB: True, ATJ: True} # semestre en attente
CODES_SEM_VALIDES_DE_DROIT = {ADM, ADC}
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"
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é"
# Pour le BUT:
CODES_ANNEE_BUT_VALIDES_DE_DROIT = {ADM, PASD}
CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL}
CODES_RCUE = {ADM, AJ, CMP}
BUT_BARRE_UE8 = 8.0 - NOTES_TOLERANCE
BUT_BARRE_UE = BUT_BARRE_RCUE = 10.0 - NOTES_TOLERANCE
BUT_RCUE_SUFFISANT = 8.0 - NOTES_TOLERANCE
@ -230,17 +233,17 @@ BUT_CODES_ORDERED = {
def code_semestre_validant(code: str) -> bool:
"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:
"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:
"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 = {

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.
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()
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
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:
codes = list(sco_codes_parcours.CODES_JURY_SEM)
codes.sort() # fortuitement, cet ordre convient bien !
codes = sorted(sco_codes_parcours.CODES_JURY_SEM)
# fortuitement, cet ordre convient bien !
H.append(
'<tr><td>Code semestre: </td><td><select name="code_etat"><option value="" selected>Choisir...</option>'

View File

@ -8,14 +8,22 @@
{% 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>
<li>Seuls les étudiants qui valident l'année seront affectés:
tous les niveaux de compétences (RCUE) validables
(moyenne annuelle au dessus de 10);
<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>
<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>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>,
notamment sur les UEs en dessous de 10.
</p>
<p class="warning">
Il est nécessaire de relire soigneusement les décisions à l'issue de cette procédure !
</p>

View File

@ -2548,10 +2548,10 @@ def formsemestre_validation_auto_but(formsemestre_id: int = None):
form = jury_but_forms.FormSemestreValidationAutoBUTForm()
if request.method == "POST":
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
)
flash(f"Décisions enregistrées ({nb_admis} admis)")
flash(f"Décisions enregistrées ({nb_etud_modif} étudiants modifiés)")
return redirect(
url_for(
"notes.formsemestre_saisie_jury",
@ -2563,7 +2563,7 @@ def formsemestre_validation_auto_but(formsemestre_id: int = None):
"but/formsemestre_validation_auto_but.html",
form=form,
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,
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)