diff --git a/app/but/jury_but.py b/app/but/jury_but.py index 3f8fa9a7a..fe670d70e 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -77,7 +77,7 @@ from app.models.but_refcomp import ( ApcNiveau, ApcParcours, ) -from app.models import Scolog, ScolarAutorisationInscription +from app.models import Evaluation, Scolog, ScolarAutorisationInscription from app.models.but_validations import ( ApcValidationAnnee, ApcValidationRCUE, @@ -260,11 +260,11 @@ class DecisionsProposeesAnnee(DecisionsProposees): else [] ) # ---- Niveaux et RCUEs - niveaux_by_parcours = ( - formsemestre.formation.referentiel_competence.get_niveaux_by_parcours( - self.annee_but, [self.parcour] if self.parcour else None - )[1] - ) + niveaux_by_parcours = formsemestre.formation.referentiel_competence.get_niveaux_by_parcours( + self.annee_but, [self.parcour] if self.parcour else None + )[ + 1 + ] self.niveaux_competences = niveaux_by_parcours["TC"] + ( niveaux_by_parcours[self.parcour.id] if self.parcour else [] ) @@ -358,13 +358,17 @@ class DecisionsProposeesAnnee(DecisionsProposees): # self.codes = [] # pas de décision annuelle sur semestres impairs elif self.inscription_etat != scu.INSCRIT: self.codes = [ - sco_codes.DEM - if self.inscription_etat == scu.DEMISSION - else sco_codes.DEF, + ( + sco_codes.DEM + if self.inscription_etat == scu.DEMISSION + else sco_codes.DEF + ), # propose aussi d'autres codes, au cas où... - sco_codes.DEM - if self.inscription_etat != scu.DEMISSION - else sco_codes.DEF, + ( + sco_codes.DEM + if self.inscription_etat != scu.DEMISSION + else sco_codes.DEF + ), sco_codes.ABAN, sco_codes.ABL, sco_codes.EXCLU, @@ -595,11 +599,9 @@ class DecisionsProposeesAnnee(DecisionsProposees): # Ordonne par numéro d'UE niv_rcue = sorted( self.rcue_by_niveau.items(), - key=lambda x: x[1].ue_1.numero - if x[1].ue_1 - else x[1].ue_2.numero - if x[1].ue_2 - else 0, + key=lambda x: ( + x[1].ue_1.numero if x[1].ue_1 else x[1].ue_2.numero if x[1].ue_2 else 0 + ), ) return { niveau_id: DecisionsProposeesRCUE(self, rcue, self.inscription_etat) @@ -816,9 +818,15 @@ class DecisionsProposeesAnnee(DecisionsProposees): Return: True si au moins un code modifié et enregistré. """ modif = False - # Vérification notes en attente dans formsemestre origine - if only_validantes and self.has_notes_en_attente(): - return False + if only_validantes: + if self.has_notes_en_attente(): + # notes en attente dans formsemestre origine + return False + if Evaluation.get_evaluations_blocked_for_etud( + self.formsemestre, self.etud + ): + # évaluation(s) qui seront débloquées dans le futur + return False # Toujours valider dans l'ordre UE, RCUE, Année annee_scolaire = self.formsemestre.annee_scolaire() @@ -1488,9 +1496,11 @@ class DecisionsProposeesUE(DecisionsProposees): self.validation = None # cache toute validation self.explanation = "non inscrit (dem. ou déf.)" self.codes = [ - sco_codes.DEM - if res.get_etud_etat(etud.id) == scu.DEMISSION - else sco_codes.DEF + ( + sco_codes.DEM + if res.get_etud_etat(etud.id) == scu.DEMISSION + else sco_codes.DEF + ) ] return diff --git a/app/models/evaluations.py b/app/models/evaluations.py index cea6c62e0..f58560ff9 100644 --- a/app/models/evaluations.py +++ b/app/models/evaluations.py @@ -488,6 +488,29 @@ class Evaluation(models.ScoDocModel): """ return NotesNotes.query.filter_by(etudid=etud.id, evaluation_id=self.id).first() + @classmethod + def get_evaluations_blocked_for_etud( + cls, formsemestre, etud: Identite + ) -> list["Evaluation"]: + """Liste des évaluations de ce semestre avec note pour cet étudiant et date blocage + et date blocage < FOREVER. + Si non vide, une note apparaitra dans le futur pour cet étudiant: il faut + donc interdire la saisie du jury. + """ + now = datetime.datetime.now(scu.TIME_ZONE) + return ( + Evaluation.query.filter( + Evaluation.blocked_until != None, # pylint: disable=C0121 + Evaluation.blocked_until >= now, + ) + .join(ModuleImpl) + .filter_by(formsemestre_id=formsemestre.id) + .join(ModuleImplInscription) + .filter_by(etudid=etud.id) + .join(NotesNotes) + .all() + ) + class EvaluationUEPoids(db.Model): """Poids des évaluations (BUT) @@ -657,3 +680,6 @@ def _moduleimpl_evaluation_insert_before( db.session.add(e) db.session.commit() return n + + +from app.models.moduleimpls import ModuleImpl, ModuleImplInscription diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py index 7c68ddf9f..df4770fa3 100644 --- a/app/scodoc/sco_formsemestre_validation.py +++ b/app/scodoc/sco_formsemestre_validation.py @@ -34,7 +34,7 @@ from flask import url_for, flash, g, request from flask_login import current_user import sqlalchemy as sa -from app.models.etudiants import Identite +from app.models import Identite, Evaluation import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu from app import db, log @@ -232,7 +232,9 @@ def formsemestre_validation_etud_form( H.append( tf_error_message( f"""Impossible de statuer sur cet étudiant: il a des notes en - attente dans des évaluations de ce semestre (voir tableau de bord) @@ -241,6 +243,26 @@ def formsemestre_validation_etud_form( ) return "\n".join(H + footer) + evaluations_a_debloquer = Evaluation.get_evaluations_blocked_for_etud( + formsemestre, etud + ) + if evaluations_a_debloquer: + links_evals = [ + f"""{e.description} en {e.moduleimpl.module.code}""" + for e in evaluations_a_debloquer + ] + H.append( + tf_error_message( + f"""Impossible de statuer sur cet étudiant: + il a des notes dans des évaluations qui seront débloquées plus tard: + voir {", ".join(links_evals)} + """ + ) + ) + return "\n".join(H + footer) + # Infos si pas de semestre précédent if not Se.prev: if Se.sem["semestre_id"] == 1: diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 23e0b42e7..4dbebb4fe 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -3391,14 +3391,24 @@ li.tf-msg { padding-bottom: 5px; } -.warning { - font-weight: bold; +.warning, .warning-bloquant { color: red; + margin-left: 16px; + margin-bottom: 8px; + min-width: var(--sco-content-min-width); + max-width: var(--sco-content-max-width); } .warning::before { - content: url(/ScoDoc/static/icons/warning_img.png); - vertical-align: -80%; + content:""; + margin-right: 8px; + height:32px; + width: 32px; + background-size: 32px 32px; + background-image: url(/ScoDoc/static/icons/warning_std.svg); + background-repeat: no-repeat; + display: inline-block; + vertical-align: -40%; } .warning-light { @@ -3411,6 +3421,19 @@ li.tf-msg { /* EMO_WARNING, "⚠️" */ } +.warning-bloquant::before { + content:""; + margin-right: 8px; + height:32px; + width: 32px; + background-size: 32px 32px; + background-image: url(/ScoDoc/static/icons/warning_bloquant.svg); + background-repeat: no-repeat; + display: inline-block; + vertical-align: -40%; +} + + p.error { font-weight: bold; color: red; diff --git a/app/static/icons/warning_bloquant.svg b/app/static/icons/warning_bloquant.svg new file mode 100644 index 000000000..81b4416e3 --- /dev/null +++ b/app/static/icons/warning_bloquant.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/static/icons/warning_std.svg b/app/static/icons/warning_std.svg new file mode 100644 index 000000000..ae68b668e --- /dev/null +++ b/app/static/icons/warning_std.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/views/notes.py b/app/views/notes.py index 1dfec6745..96ae65556 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -2408,6 +2408,12 @@ def formsemestre_validation_but( ) deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre) + has_notes_en_attente = deca.has_notes_en_attente() + evaluations_a_debloquer = Evaluation.get_evaluations_blocked_for_etud( + formsemestre, etud + ) + if has_notes_en_attente or evaluations_a_debloquer: + read_only = True if request.method == "POST": if not read_only: deca.record_form(request.form) @@ -2452,9 +2458,21 @@ def formsemestre_validation_but( etat_ins = scu.ETATS_INSCRIPTION.get(inscription.etat, "inconnu?") warning += f"""