WIP: jury BUT: enregistrement des décisions

This commit is contained in:
Emmanuel Viennet 2022-06-22 11:44:03 +02:00
parent d4a8b74c0a
commit c17e2bae47
7 changed files with 278 additions and 42 deletions

View File

@ -58,9 +58,12 @@ DecisionsProposeesUE: décisions de jury sur une UE du BUT
DecisionsProposeesRCUE appelera .set_compensable() DecisionsProposeesRCUE appelera .set_compensable()
si on a la possibilité de la compenser dans le RCUE. si on a la possibilité de la compenser dans le RCUE.
""" """
import html
from operator import attrgetter from operator import attrgetter
import re
from typing import Union from typing import Union
from app import db
from app import log from app import log
from app.comp.res_but import ResultatsSemestreBUT from app.comp.res_but import ResultatsSemestreBUT
from app.comp import res_sem from app.comp import res_sem
@ -72,7 +75,7 @@ from app.models.but_refcomp import (
ApcParcours, ApcParcours,
ApcParcoursNiveauCompetence, ApcParcoursNiveauCompetence,
) )
from app.models import but_validations from app.models import Scolog
from app.models.but_validations import ( from app.models.but_validations import (
ApcValidationAnnee, ApcValidationAnnee,
ApcValidationRCUE, ApcValidationRCUE,
@ -122,10 +125,14 @@ class DecisionsProposees:
self.codes = code + self.codes self.codes = code + self.codes
elif code is not None: elif code is not None:
self.codes = [code] + self.codes self.codes = [code] + self.codes
self.validation = None
"Validation enregistrée"
self.code_valide: str = code_valide self.code_valide: str = code_valide
"La décision actuelle enregistrée" "Code décision actuel enregistré"
self.explanation: str = explanation self.explanation: str = explanation
"Explication à afficher à côté de la décision" "Explication à afficher à côté de la décision"
self.recorded = False
"true si la décision vient d'être enregistrée"
def __repr__(self) -> str: def __repr__(self) -> str:
return f"""<{self.__class__.__name__} valid={self.code_valide return f"""<{self.__class__.__name__} valid={self.code_valide
@ -266,7 +273,15 @@ class DecisionsProposeesAnnee(DecisionsProposees):
explanation: {self.explanation} explanation: {self.explanation}
""" """
def annee_scolaire_sr(self) def annee_scolaire(self) -> int:
"L'année de début de l'année scolaire"
formsemestre = self.formsemestre_impair or self.formsemestre_pair
return formsemestre.annee_scolaire()
def annee_scolaire_str(self) -> str:
"L'année scolaire, eg '2021 - 2022'"
formsemestre = self.formsemestre_impair or self.formsemestre_pair
return formsemestre.annee_scolaire_str().replace(" ", "")
def comp_formsemestres( def comp_formsemestres(
self, formsemestre: FormSemestre self, formsemestre: FormSemestre
@ -397,6 +412,90 @@ class DecisionsProposeesAnnee(DecisionsProposees):
decisions_rcue_by_niveau = {x[1]: x[0] for x in rc_niveaux} decisions_rcue_by_niveau = {x[1]: x[0] for x in rc_niveaux}
return decisions_rcue_by_niveau return decisions_rcue_by_niveau
# def lookup_ue(self, ue_id: int) -> UniteEns:
# "check that ue_id belongs to our UE, if not returns None"
# ues = [ue for ue in self.ues_impair + self.ues_pair if ue.id == ue_id]
# assert len(ues) < 2
# if len(ues):
# return ues[0]
# return None
def record_form(self, form: dict):
"""Enregistre les codes de jury en base
form dict:
- 'code_ue_1896' : 'AJ' code pour l'UE id 1896
- 'code_rcue_6" : 'ADM' code pour le RCUE du niveau 6
- 'code_annee' : 'ADM' code pour l'année
Si les code_rcue et le code_annee ne sont pas fournis,
enregistre ceux par défaut.
"""
for key in form:
code = form[key]
# Codes d'UE
m = re.match(r"^code_ue_(\d+)$", key)
if m:
ue_id = int(m.group(1))
dec_ue = self.decisions_ues.get(ue_id)
if not dec_ue:
raise ScoValueError(f"UE invalide ue_id={ue_id}")
dec_ue.record(code)
else:
# Codes de RCUE
m = re.match(r"^code_rcue_(\d+)$", key)
if m:
niveau_id = int(m.group(1))
dec_rcue = self.decisions_rcue_by_niveau.get(niveau_id)
if not dec_rcue:
raise ScoValueError(f"RCUE invalide niveau_id={niveau_id}")
dec_rcue.record(code)
elif key == "code_annee":
# Code annuel
self.record(code)
self.record_all()
db.session.commit()
def record(self, code: str):
"""Enregistre le code"""
if 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:
return # no change
if self.validation:
db.session.delete(self.validation)
db.session.flush()
self.validation = ApcValidationAnnee(
etudid=self.etud.id,
formsemestre=self.formsemestre_impair,
ordre=self.annee_but,
annee_scolaire=self.annee_scolaire(),
code=code,
)
Scolog.logdb(
method="jury_but",
etudid=self.etud.id,
msg=f"Validation année BUT{self.annee_but}: {code}",
)
db.session.add(self.validation)
self.recorded = True
def record_all(self):
"""Enregistre les codes qui n'ont pas été spécifiés par le formulaire,
et sont donc en mode "automatique"
"""
decisions = (
list(self.decisions_ues.values())
+ list(self.decisions_rcue_by_niveau.values())
+ [self]
)
for dec in decisions:
if not dec.recorded:
dec.record(dec.codes[0]) # rappel: le code par défaut est en tête
class DecisionsProposeesRCUE(DecisionsProposees): class DecisionsProposeesRCUE(DecisionsProposees):
"""Liste des codes de décisions que l'on peut proposer pour """Liste des codes de décisions que l'on peut proposer pour
@ -417,10 +516,10 @@ class DecisionsProposeesRCUE(DecisionsProposees):
): ):
super().__init__(etud=dec_prop_annee.etud) super().__init__(etud=dec_prop_annee.etud)
self.rcue = rcue self.rcue = rcue
self.parcour = dec_prop_annee.parcour
validation = rcue.query_validations().first() self.validation = rcue.query_validations().first()
if validation is not None: if self.validation is not None:
self.code_valide = validation.code self.code_valide = self.validation.code
if rcue.est_compensable(): if rcue.est_compensable():
self.codes.insert(0, sco_codes.CMP) self.codes.insert(0, sco_codes.CMP)
elif rcue.est_validable(): elif rcue.est_validable():
@ -428,6 +527,34 @@ class DecisionsProposeesRCUE(DecisionsProposees):
else: else:
self.codes.insert(0, sco_codes.AJ) self.codes.insert(0, sco_codes.AJ)
def record(self, code: str):
"""Enregistre le code"""
if 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:
return # no change
parcours_id = self.parcour.id if self.parcour is not None else None
if self.validation:
db.session.delete(self.validation)
db.session.flush()
self.validation = ApcValidationRCUE(
etudid=self.etud.id,
formsemestre_id=self.rcue.formsemestre_2.id,
ue1_id=self.rcue.ue_1.id,
ue2_id=self.rcue.ue_2.id,
parcours_id=parcours_id,
code=code,
)
Scolog.logdb(
method="jury_but",
etudid=self.etud.id,
msg=f"Validation RCUE {repr(self.rcue)}",
)
db.session.add(self.validation)
self.recorded = True
class DecisionsProposeesUE(DecisionsProposees): class DecisionsProposeesUE(DecisionsProposees):
"""Décisions de jury sur une UE du BUT """Décisions de jury sur une UE du BUT
@ -460,6 +587,7 @@ class DecisionsProposeesUE(DecisionsProposees):
ue: UniteEns, ue: UniteEns,
): ):
super().__init__(etud=etud) super().__init__(etud=etud)
self.formsemestre = formsemestre
self.ue: UniteEns = ue self.ue: UniteEns = ue
self.rcue: RegroupementCoherentUE = None self.rcue: RegroupementCoherentUE = None
"Le rcu auquel est rattaché cette UE, ou None" "Le rcu auquel est rattaché cette UE, ou None"
@ -503,6 +631,31 @@ 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):
"""Enregistre le code"""
if 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:
return # no change
if self.validation:
db.session.delete(self.validation)
db.session.flush()
self.validation = ScolarFormSemestreValidation(
etudid=self.etud.id,
formsemestre_id=self.formsemestre.id,
ue_id=self.ue.id,
code=code,
)
Scolog.logdb(
method="jury_but",
etudid=self.etud.id,
msg=f"Validation UE {self.ue.id}",
)
db.session.add(self.validation)
self.recorded = True
class BUTCursusEtud: # WIP TODO class BUTCursusEtud: # WIP TODO
"""Validation du cursus d'un étudiant""" """Validation du cursus d'un étudiant"""

View File

@ -277,4 +277,4 @@ class ApcValidationAnnee(db.Model):
formsemestre = db.relationship("FormSemestre", backref="apc_validations_annees") formsemestre = db.relationship("FormSemestre", backref="apc_validations_annees")
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__} {self.id} {self.etud} BUT{self.ordre}:{self.code!r}>" return f"<{self.__class__.__name__} {self.id} {self.etud} BUT{self.ordre}/{self.annee_scolaire}:{self.code!r}>"

View File

@ -32,6 +32,21 @@ class Scolog(db.Model):
authenticated_user = db.Column(db.Text) # login, sans contrainte authenticated_user = db.Column(db.Text) # login, sans contrainte
# zope_remote_addr suppressed # zope_remote_addr suppressed
@classmethod
def logdb(
cls, method: str = None, etudid: int = None, msg: str = None, commit=False
):
"""Add entry in student's log (replacement for old scolog.logdb)"""
entry = Scolog(
method=method,
msg=msg,
etudid=etudid,
authenticated_user=current_user.user_name,
)
db.session.add(entry)
if commit:
db.session.commit()
class ScolarNews(db.Model): class ScolarNews(db.Model):
"""Nouvelles pour page d'accueil""" """Nouvelles pour page d'accueil"""

View File

@ -36,7 +36,7 @@ class ScolarFormSemestreValidation(db.Model):
# NULL pour les UE, True|False pour les semestres: # NULL pour les UE, True|False pour les semestres:
assidu = db.Column(db.Boolean) assidu = db.Column(db.Boolean)
event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
# NULL sauf si compense un semestre: # NULL sauf si compense un semestre: (pas utilisé pour BUT)
compense_formsemestre_id = db.Column( compense_formsemestre_id = db.Column(
db.Integer, db.Integer,
db.ForeignKey("notes_formsemestre.id"), db.ForeignKey("notes_formsemestre.id"),

View File

@ -66,3 +66,27 @@ span.but_explanation {
color: blueviolet; color: blueviolet;
font-style: italic; font-style: italic;
} }
select:disabled {
font-weight: bold;
color: blue;
}
select:invalid {
background: red;
}
select.but_code option.recorded {
color: rgb(3, 157, 3);
font-weight: bold;
}
div.but_niveau_ue.recorded,
div.but_niveau_rcue.recorded {
border-color: rgb(136, 252, 136);
border-width: 2px;
}
div.but_niveau_ue.modified {
background-color: rgb(255, 214, 254);
}

View File

@ -4,3 +4,11 @@
function enable_manual_codes(elt) { function enable_manual_codes(elt) {
$(".jury_but select.manual").prop("disabled", !elt.checked); $(".jury_but select.manual").prop("disabled", !elt.checked);
} }
// changement menu code:
function change_menu_code(elt) {
elt.parentElement.parentElement.classList.remove("recorded");
// TODO: comparer avec valeur enregistrée (à mettre en data-orig ?)
// et colorer en fonction
elt.parentElement.parentElement.classList.add("modified");
}

View File

@ -2231,7 +2231,6 @@ def formsemestre_validation_but(formsemestre_id: int, etudid: int):
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
), ),
) )
# XXX TODO Page expérimentale pour les devs
H = [ H = [
html_sco_header.sco_header( html_sco_header.sco_header(
page_title="Validation BUT", page_title="Validation BUT",
@ -2244,22 +2243,37 @@ def formsemestre_validation_but(formsemestre_id: int, etudid: int):
<div class="jury_but"> <div class="jury_but">
""", """,
] ]
formsemestre = FormSemestre.query.get_or_404(formsemestre_id) formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
etud = Identite.query.get_or_404(etudid) etud = Identite.query.get_or_404(etudid)
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre) res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre) deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
if request.method == "POST":
deca.record_form(request.form)
flash("codes enregistrés")
return flask.redirect(
url_for(
"notes.formsemestre_validation_but",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
etudid=etudid,
)
)
H.append( H.append(
f""" f"""
<form method="POST"> <form method="POST">
<div class="titre_parcours"><h2>Jury BUT{deca.annee_but} - Parcours {deca.parcour.libelle or "non spécifié"} <div class="titre_parcours">
- {deca.formsemestre_impair.annee_scolaire_str()}</h2> <h2>Jury BUT{deca.annee_but}
- Parcours {deca.parcour.libelle or "non spécifié"}
- {deca.annee_scolaire_str()}</h2>
</div> </div>
<div class="but_section_annee"> <div class="but_section_annee">
<div> <div>
<b>Décision de jury pour l'année :</b> { <b>Décision de jury pour l'année :</b> {
_gen_but_select("code_annee", deca.codes, deca.code_valide, disabled=True, klass="manual") _gen_but_select("code_annee", deca.codes, deca.code_valide, disabled=True, klass="manual")
}</div> }
<span>({'non ' if deca.code_valide is None else ''}enregistrée)</span>
</div>
<span class="but_explanation">{deca.explanation}</span> <span class="but_explanation">{deca.explanation}</span>
</div> </div>
<b>Niveaux de compétences et unités d'enseignement :</b> <b>Niveaux de compétences et unités d'enseignement :</b>
@ -2279,36 +2293,26 @@ def formsemestre_validation_but(formsemestre_id: int, etudid: int):
) )
dec_rcue = deca.decisions_rcue_by_niveau[niveau.id] dec_rcue = deca.decisions_rcue_by_niveau[niveau.id]
# Semestre impair # Semestre impair
ue = dec_rcue.rcue.ue_1
H.append( H.append(
f"""<div class="but_niveau_ue"> _gen_but_niveau_ue(
<div title="{ue.titre}">{ue.acronyme}</div> dec_rcue.rcue.ue_1,
<div class="but_note">{scu.fmt_note(dec_rcue.rcue.moy_ue_1)}</div> dec_rcue.rcue.moy_ue_1,
<div class="but_code">{ deca.decisions_ues[dec_rcue.rcue.ue_1.id],
_gen_but_select("code_ue_"+str(ue.id), )
deca.decisions_ues[ue.id].codes,
deca.decisions_ues[ue.id].code_valide
)
}</div>
</div>"""
) )
# Semestre pair # Semestre pair
ue = dec_rcue.rcue.ue_2
H.append( H.append(
f"""<div class="but_niveau_ue"> _gen_but_niveau_ue(
<div title="{ue.titre}">{ue.acronyme}</div> dec_rcue.rcue.ue_2,
<div class="but_note">{scu.fmt_note(dec_rcue.rcue.moy_ue_2)}</div> dec_rcue.rcue.moy_ue_2,
<div class="but_code">{ deca.decisions_ues[dec_rcue.rcue.ue_2.id],
_gen_but_select("code_ue_"+str(ue.id), )
deca.decisions_ues[ue.id].codes,
deca.decisions_ues[ue.id].code_valide
)
}</div>
</div>"""
) )
# RCUE # RCUE
H.append( H.append(
f"""<div class="but_niveau_rcue"> f"""<div class="but_niveau_rcue
{'recorded' if dec_rcue.code_valide is not None else ''}
">
<div class="but_note">{scu.fmt_note(dec_rcue.rcue.moy_rcue)}</div> <div class="but_note">{scu.fmt_note(dec_rcue.rcue.moy_rcue)}</div>
<div class="but_code">{ <div class="but_code">{
_gen_but_select("code_rcue_"+str(niveau.id), _gen_but_select("code_rcue_"+str(niveau.id),
@ -2322,9 +2326,16 @@ def formsemestre_validation_but(formsemestre_id: int, etudid: int):
H.append("</div>") # but_annee H.append("</div>") # but_annee
H.append( H.append(
"""<div class="but_settings"><input type="checkbox" onchange="enable_manual_codes(this)"> """<div class="but_settings">
<em>permettre la saisie manuelles des codes d'année et de niveaux</em> <input type="checkbox" onchange="enable_manual_codes(this)">
</input></div>""" <em>permettre la saisie manuelles des codes d'année et de niveaux.
Dans ce cas, il vous revient de vous assurer de la cohérence entre
vos codes d'UE/RCUE/Année !</em>
</input>
</div>
<input type="submit" value="Enregistrer ces décisions">
"""
) )
H.append("</form>") # but_annee H.append("</form>") # but_annee
@ -2348,11 +2359,36 @@ def _gen_but_select(
"Le menu html select avec les codes" "Le menu html select avec les codes"
h = "\n".join( h = "\n".join(
[ [
f"""<option value="{code}" {'selected' if code == code_valide else ''}>{code}</option>""" f"""<option value="{code}"
{'selected' if code == code_valide else ''}
class="{'recorded' if code == code_valide else ''}"
>{code}</option>"""
for code in codes for code in codes
] ]
) )
return f"""<select name="{name}" class="{klass}" {"disabled" if disabled else ""}>{h}</select>""" return f"""<select required name="{name}"
class="but_code {klass}"
onchange="change_menu_code(this);"
{"disabled" if disabled else ""}
>{h}</select>
"""
def _gen_but_niveau_ue(
ue: UniteEns, moy_ue: float, dec_ue: jury_but.DecisionsProposeesUE
):
return f"""<div class="but_niveau_ue {
'recorded' if dec_ue.code_valide is not None else ''}
">
<div title="{ue.titre}">{ue.acronyme}</div>
<div class="but_note">{scu.fmt_note(moy_ue)}</div>
<div class="but_code">{
_gen_but_select("code_ue_"+str(ue.id),
dec_ue.codes,
dec_ue.code_valide
)
}</div>
</div>"""
@bp.route("/formsemestre_validate_previous_ue", methods=["GET", "POST"]) @bp.route("/formsemestre_validate_previous_ue", methods=["GET", "POST"])