From e46ae763997a7357fdc3615d4f008341030eadd9 Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet
Date: Thu, 15 Jun 2023 08:49:05 +0200
Subject: [PATCH 01/17] =?UTF-8?q?BUT:=20jury:=20validation=20des=20niveaux?=
=?UTF-8?q?=20inf=C3=A9rieurs.=20WIP?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/but/cursus_but.py | 36 +--
app/but/jury_but.py | 223 +++++++++++++++++-
app/comp/res_but.py | 5 +-
app/models/but_validations.py | 12 +-
app/models/formsemestre.py | 2 +-
app/scodoc/codes_cursus.py | 20 +-
app/scodoc/sco_apogee_csv.py | 9 +-
app/scodoc/sco_formsemestre_validation.py | 7 +-
...1224fa255_validation_niveaux_inferieurs.py | 63 +++++
sco_version.py | 2 +-
tests/ressources/yaml/cursus_but_gccd_cy.yaml | 129 ++++++----
11 files changed, 414 insertions(+), 94 deletions(-)
create mode 100644 migrations/versions/c701224fa255_validation_niveaux_inferieurs.py
diff --git a/app/but/cursus_but.py b/app/but/cursus_but.py
index 4654a9cb5..89a7bd669 100644
--- a/app/but/cursus_but.py
+++ b/app/but/cursus_but.py
@@ -104,7 +104,7 @@ class EtudCursusBUT:
self.parcour: ApcParcours = self.inscriptions[-1].parcour
"Le parcours à valider: celui du DERNIER semestre suivi (peut être None)"
self.niveaux_by_annee = {}
- "{ annee : liste des niveaux à valider }"
+ "{ annee:int : liste des niveaux à valider }"
self.niveaux: dict[int, ApcNiveau] = {}
"cache les niveaux"
for annee in (1, 2, 3):
@@ -118,21 +118,6 @@ class EtudCursusBUT:
self.niveaux.update(
{niveau.id: niveau for niveau in self.niveaux_by_annee[annee]}
)
- # Probablement inutile:
- # # Cherche les validations de jury enregistrées pour chaque niveau
- # self.validations_by_niveau = collections.defaultdict(lambda: [])
- # " { niveau_id : [ ApcValidationRCUE ] }"
- # for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
- # self.validations_by_niveau[validation_rcue.niveau().id].append(
- # validation_rcue
- # )
- # self.validation_by_niveau = {
- # niveau_id: sorted(
- # validations, key=lambda v: sco_codes.BUT_CODES_ORDERED[v.code]
- # )[0]
- # for niveau_id, validations in self.validations_by_niveau.items()
- # }
- # "{ niveau_id : meilleure validation pour ce niveau }"
self.validation_par_competence_et_annee = {}
"""{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
@@ -146,7 +131,7 @@ class EtudCursusBUT:
# prend la "meilleure" validation
if (not previous_validation) or (
sco_codes.BUT_CODES_ORDERED[validation_rcue.code]
- > sco_codes.BUT_CODES_ORDERED[previous_validation["code"]]
+ > sco_codes.BUT_CODES_ORDERED[previous_validation.code]
):
self.validation_par_competence_et_annee[niveau.competence.id][
niveau.annee
@@ -206,6 +191,23 @@ class EtudCursusBUT:
)
return d
+ def load_validation_by_niveau(self) -> dict[int, list[ApcValidationRCUE]]:
+ """Cherche les validations de jury enregistrées pour chaque niveau
+ Résultat: { niveau_id : [ ApcValidationRCUE ] }
+ meilleure validation pour ce niveau
+ """
+ validations_by_niveau = collections.defaultdict(lambda: [])
+ for validation_rcue in ApcValidationRCUE.query.filter_by(etud=self.etud):
+ validations_by_niveau[validation_rcue.niveau().id].append(validation_rcue)
+ validation_by_niveau = {
+ niveau_id: sorted(
+ validations, key=lambda v: sco_codes.BUT_CODES_ORDERED[v.code]
+ )[0]
+ for niveau_id, validations in validations_by_niveau.items()
+ if validations
+ }
+ return validation_by_niveau
+
class FormSemestreCursusBUT:
"""L'état des étudiants d'un formsemestre dans leur cursus BUT
diff --git a/app/but/jury_but.py b/app/but/jury_but.py
index 1caa44118..12e14bfb8 100644
--- a/app/but/jury_but.py
+++ b/app/but/jury_but.py
@@ -58,6 +58,7 @@ DecisionsProposeesUE: décisions de jury sur une UE du BUT
DecisionsProposeesRCUE appelera .set_compensable()
si on a la possibilité de la compenser dans le RCUE.
"""
+from datetime import datetime
import html
from operator import attrgetter
import re
@@ -68,6 +69,7 @@ from flask import flash, g, url_for
from app import db
from app import log
+from app.but.cursus_but import EtudCursusBUT
from app.comp.res_but import ResultatsSemestreBUT
from app.comp import res_sem
@@ -92,6 +94,7 @@ from app.models.validations import ScolarFormSemestreValidation
from app.scodoc import sco_cache
from app.scodoc import codes_cursus as sco_codes
from app.scodoc.codes_cursus import (
+ code_rcue_validant,
BUT_CODES_ORDERED,
CODES_RCUE_VALIDES,
CODES_UE_CAPITALISANTS,
@@ -275,6 +278,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
if self.formsemestre_impair is not None:
self.validation = ApcValidationAnnee.query.filter_by(
etudid=self.etud.id,
+ formation_id=self.formsemestre.formation_id,
formsemestre_id=formsemestre_impair.id,
ordre=self.annee_but,
).first()
@@ -755,6 +759,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
self.validation = ApcValidationAnnee(
etudid=self.etud.id,
formsemestre=self.formsemestre_impair,
+ formation_id=self.formsemestre.formation_id,
ordre=self.annee_but,
annee_scolaire=self.annee_scolaire(),
code=code,
@@ -900,6 +905,9 @@ class DecisionsProposeesAnnee(DecisionsProposees):
)
validations = ApcValidationAnnee.query.filter_by(
etudid=self.etud.id,
+ # XXX efface les validations émise depuis ce semestre
+ # et pas toutes celles concernant cette l'année...
+ # (utiliser formation_id pour changer cette politique)
formsemestre_id=self.formsemestre_impair.id,
ordre=self.annee_but,
)
@@ -1035,6 +1043,9 @@ class DecisionsProposeesRCUE(DecisionsProposees):
):
super().__init__(etud=dec_prop_annee.etud)
self.deca = dec_prop_annee
+ self.referentiel_competence_id = (
+ self.deca.formsemestre.formation.referentiel_competence_id
+ )
self.rcue = rcue
if rcue is None: # RCUE non dispo, eg un seul semestre
self.codes = []
@@ -1139,7 +1150,8 @@ class DecisionsProposeesRCUE(DecisionsProposees):
dec_ue.record(sco_codes.ADJR)
# Valide les niveaux inférieurs de la compétence (code ADSUP)
- # TODO
+ if code in CODES_RCUE_VALIDES:
+ self.valide_niveau_inferieur()
if self.rcue.formsemestre_1 is not None:
sco_cache.invalidate_formsemestre(
@@ -1177,6 +1189,189 @@ class DecisionsProposeesRCUE(DecisionsProposees):
return f"{niveau_titre}-{ordre}"
return ""
+ def valide_niveau_inferieur(self) -> None:
+ """Appelé juste après la validation d'un RCUE.
+ *La validation des deux UE du niveau d’une compétence emporte la validation de
+ l’ensemble des UEs du niveau inférieur de cette même compétence.*
+ """
+ if not self.rcue or not self.rcue.ue_1 or not self.rcue.ue_1.niveau_competence:
+ return
+ competence: ApcCompetence = self.rcue.ue_1.niveau_competence.competence
+ ordre_inferieur = self.rcue.ue_1.niveau_competence.ordre - 1
+ if ordre_inferieur < 1:
+ return # pas de niveau inferieur
+
+ # --- Si le RCUE inférieur est déjà validé, ne fait rien
+ validations_rcue = (
+ ApcValidationRCUE.query.filter_by(etudid=self.etud.id)
+ .join(UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id)
+ .join(ApcNiveau)
+ .filter_by(ordre=ordre_inferieur)
+ .join(ApcCompetence)
+ .filter_by(id=competence.id)
+ .all()
+ )
+ if [v for v in validations_rcue if code_rcue_validant(v.code)]:
+ return # déjà validé
+
+ # --- Validations des UEs
+ ues, ue1, ue2 = self._get_ues_inferieures(competence, ordre_inferieur)
+ # Pour chaque UE inférieure non validée, valide:
+ for ue in ues:
+ validations_ue = ScolarFormSemestreValidation.query.filter_by(
+ etudid=self.etud.id, ue_id=ue.id
+ ).all()
+ if [
+ validation
+ for validation in validations_ue
+ if sco_codes.code_ue_validant(validation.code)
+ ]:
+ continue # on a déjà une validation
+ # aucune validation validante
+ validation_ue = validations_ue[0] if validations_ue else None
+ if validation_ue:
+ # Modifie validation existante
+ validation_ue.code = sco_codes.ADSUP
+ validation_ue.event_date = datetime.now()
+ if validation_ue.formsemestre_id is not None:
+ sco_cache.invalidate_formsemestre(
+ formsemestre_id=validation_ue.formsemestre_id
+ )
+ log(f"updating {validation_ue}")
+ else:
+ # Ajoute une validation,
+ # pas de formsemestre ni de note car pas une capitalisation
+ validation_ue = ScolarFormSemestreValidation(
+ etudid=self.etud.id,
+ code=sco_codes.ADSUP,
+ ue_id=ue.id,
+ is_external=True, # pas rattachée à un formsemestre
+ )
+ log(f"recording {validation_ue}")
+ db.session.add(validation_ue)
+
+ # Valide le RCUE inférieur
+ if validations_rcue:
+ # Met à jour validation existante
+ validation_rcue = validations_rcue[0]
+ validation_rcue.code = sco_codes.ADSUP
+ validation_rcue.date = datetime.now()
+ log(f"updating {validation_rcue}")
+ if validation_rcue.formsemestre_id is not None:
+ sco_cache.invalidate_formsemestre(
+ formsemestre_id=validation_rcue.formsemestre_id
+ )
+ else:
+ # Crée nouvelle validation
+ validation_rcue = ApcValidationRCUE(
+ etudid=self.etud.id, ue1_id=ue1.id, ue2_id=ue2.id, code=sco_codes.ADSUP
+ )
+ log(f"recording {validation_rcue}")
+ db.session.add(validation_rcue)
+ db.session.commit()
+ self.valide_annee_inferieure()
+
+ def valide_annee_inferieure(self) -> None:
+ """Si tous les RCUEs de l'année inférieure sont validés, la valide"""
+ # Indice de l'année inférieure:
+ annee_courante = self.rcue.ue_1.niveau_competence.annee # "BUT2"
+ if not re.match(r"^BUT\d$", annee_courante):
+ log("Warning: valide_annee_inferieure invalid annee_courante")
+ return
+ annee_inferieure = int(annee_courante[3]) - 1
+ if annee_inferieure < 1:
+ return
+ # Garde-fou: Année déjà validée ?
+ validations_annee: ApcValidationAnnee = ApcValidationAnnee.query.filter_by(
+ etudid=self.etud.id,
+ ordre=annee_inferieure,
+ formation_id=self.rcue.formsemestre_1.formation_id,
+ ).all()
+ if len(validations_annee) > 1:
+ log(
+ f"warning: {len(validations_annee)} validations d'année\n{validations_annee}"
+ )
+ if [
+ validation_annee
+ for validation_annee in validations_annee
+ if sco_codes.code_annee_validant(validation_annee.code)
+ ]:
+ return # déja valide
+ validation_annee = validations_annee[0] if validations_annee else None
+ # Liste des niveaux à valider:
+ # ici on sort l'artillerie lourde
+ cursus: EtudCursusBUT = EtudCursusBUT(
+ self.etud, self.rcue.formsemestre_1.formation
+ )
+ niveaux_a_valider = cursus.niveaux_by_annee[annee_inferieure]
+ # Pour chaque niveau, cherche validation RCUE
+ validations_by_niveau = cursus.load_validation_by_niveau()
+ ok = True
+ for niveau in niveaux_a_valider:
+ validation_niveau: ApcValidationRCUE = validations_by_niveau.get(niveau.id)
+ if not validation_niveau or not sco_codes.code_rcue_validant(
+ validation_niveau.code
+ ):
+ ok = False
+
+ # Si tous OK, émet validation année
+ if validation_annee: # Modifie la validation antérieure (non validante)
+ validation_annee.code = sco_codes.ADSUP
+ validation_annee.date = datetime.now()
+ log(f"updating {validation_annee}")
+ else:
+ validation_annee = ApcValidationAnnee(
+ etudid=self.etud.id,
+ ordre=annee_inferieure,
+ code=sco_codes.ADSUP,
+ formation_id=self.rcue.formsemestre_1.formation_id,
+ # met cette validation sur l'année scolaire actuelle, pas la précédente (??)
+ annee_scolaire=self.rcue.formsemestre_1.annee_scolaire(),
+ )
+ log(f"recording {validation_annee}")
+ db.session.add(validation_annee)
+ db.session.commit()
+
+ def _get_ues_inferieures(
+ self, competence: ApcCompetence, ordre_inferieur: int
+ ) -> tuple[list[UniteEns], UniteEns, UniteEns]:
+ """Les UEs de cette formation associées au niveau de compétence inférieur ?
+ Note: on ne cherche que dans la formation courante, pas les UEs de
+ même code d'autres formations.
+ """
+ formation: Formation = self.rcue.formsemestre_1.formation
+ ues: list[UniteEns] = (
+ UniteEns.query.filter_by(formation_id=formation.id)
+ .filter(UniteEns.semestre_idx != None)
+ .join(ApcNiveau)
+ .filter_by(ordre=ordre_inferieur)
+ .join(ApcCompetence)
+ .filter_by(id=competence.id)
+ .all()
+ )
+ log(f"valide_niveau_inferieur: {competence} UEs inférieures: {ues}")
+ if len(ues) != 2: # on n'a pas 2 UE associées au niveau inférieur !
+ flash(
+ "Impossible de valider le niveau de compétence inférieur: pas 2 UEs associées'",
+ "warning",
+ )
+ return
+ ues_impaires = [ue for ue in ues if ue.semestre_idx % 2]
+ if len(ues_impaires) != 1:
+ flash(
+ "Impossible de valider le niveau de compétence inférieur: pas d'UE impaire associée"
+ )
+ return
+ ue1 = ues_impaires[0]
+ ues_paires = [ue for ue in ues if not ue.semestre_idx % 2]
+ if len(ues_paires) != 1:
+ flash(
+ "Impossible de valider le niveau de compétence inférieur: pas d'UE paire associée"
+ )
+ return
+ ue2 = ues_paires[0]
+ return ues, ue1, ue2
+
class DecisionsProposeesUE(DecisionsProposees):
"""Décisions de jury sur une UE du BUT
@@ -1383,23 +1578,29 @@ class BUTCursusEtud: # WIP TODO
for competence in self.competences_du_parcours()
)
- def est_diplome(self) -> bool:
- """Vrai si BUT déjà validé"""
- # vrai si la troisième année est validée
- # On cherche les validations de 3ieme annee (ordre=3) avec le même référentiel
- # de formation que nous.
+ def est_annee_validee(self, ordre: int) -> bool:
+ """Vrai si l'année BUT ordre est validée"""
+ # On cherche les validations d'annee avec le même
+ # code formation que nous.
return (
- ApcValidationAnnee.query.filter_by(etudid=self.etud.id, ordre=3)
- .join(FormSemestre, FormSemestre.id == ApcValidationAnnee.formsemestre_id)
- .join(Formation, FormSemestre.formation_id == Formation.id)
+ ApcValidationAnnee.query.filter_by(
+ etudid=self.etud.id,
+ ordre=ordre,
+ formation_id=self.formsemestre.formation_id,
+ )
+ .join(Formation)
.filter(
- Formation.referentiel_competence_id
- == self.formsemestre.formation.referentiel_competence_id
+ Formation.formation_code == self.formsemestre.formation.formation_code
)
.count()
> 0
)
+ def est_diplome(self) -> bool:
+ """Vrai si BUT déjà validé"""
+ # vrai si la troisième année est validée
+ return self.est_annee_validee(3)
+
def competences_du_parcours(self) -> list[ApcCompetence]:
"""Construit liste des compétences du parcours, qui doivent être
validées pour obtenir le diplôme.
diff --git a/app/comp/res_but.py b/app/comp/res_but.py
index e4602327b..a91b1dbbc 100644
--- a/app/comp/res_but.py
+++ b/app/comp/res_but.py
@@ -307,9 +307,10 @@ class ResultatsSemestreBUT(NotesTableCompat):
return ues_ids
def etud_has_decision(self, etudid) -> bool:
- """True s'il y a une décision de jury pour cet étudiant émanant de ce formsemestre.
+ """True s'il y a une décision (quelconque) de jury
+ émanant de ce formsemestre pour cet étudiant.
prend aussi en compte les autorisations de passage.
- Sous-classée en BUT pour les RCUEs et années.
+ Ici sous-classée (BUT) pour les RCUEs et années.
"""
return bool(
super().etud_has_decision(etudid)
diff --git a/app/models/but_validations.py b/app/models/but_validations.py
index a21cd071f..d9b0e7e2d 100644
--- a/app/models/but_validations.py
+++ b/app/models/but_validations.py
@@ -320,7 +320,12 @@ class ApcValidationAnnee(db.Model):
db.Integer, db.ForeignKey("notes_formsemestre.id"), nullable=True
)
"le semestre IMPAIR (le 1er) de l'année"
- annee_scolaire = db.Column(db.Integer, nullable=False) # 2021
+ formation_id = db.Column(
+ db.Integer,
+ db.ForeignKey("notes_formations.id"),
+ nullable=False,
+ )
+ annee_scolaire = db.Column(db.Integer, nullable=False) # eg 2021
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True)
@@ -348,7 +353,7 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
"""
Un dict avec les décisions de jury BUT enregistrées:
- decision_rcue : list[dict]
- - decision_annee : dict
+ - decision_annee : dict (décision issue de ce semestre seulement (à confirmer ?))
Ne reprend pas les décisions d'UE, non spécifiques au BUT.
"""
decisions = {}
@@ -383,8 +388,7 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
etudid=etud.id,
annee_scolaire=formsemestre.annee_scolaire(),
)
- .join(ApcValidationAnnee.formsemestre)
- .join(FormSemestre.formation)
+ .join(Formation)
.filter(Formation.formation_code == formsemestre.formation.formation_code)
.first()
)
diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py
index 680e3ff98..516d8fc51 100644
--- a/app/models/formsemestre.py
+++ b/app/models/formsemestre.py
@@ -859,7 +859,7 @@ class FormSemestre(db.Model):
.order_by(UniteEns.numero)
.all()
)
- vals_annee = (
+ vals_annee = ( # issues de ce formsemestre seulement
ApcValidationAnnee.query.filter_by(
etudid=etudid,
annee_scolaire=self.annee_scolaire(),
diff --git a/app/scodoc/codes_cursus.py b/app/scodoc/codes_cursus.py
index f2285ac12..228fb6d25 100644
--- a/app/scodoc/codes_cursus.py
+++ b/app/scodoc/codes_cursus.py
@@ -122,6 +122,7 @@ ABAN = "ABAN"
ABL = "ABL"
ADM = "ADM" # moyenne gen., barres UE, assiduité: sem. validé
ADC = "ADC" # admis par compensation (eg moy(S1, S2) > 10)
+ADSUP = "ADSUP" # BUT: UE ou RCUE validé par niveau supérieur
ADJ = "ADJ" # admis par le jury
ADJR = "ADJR" # UE admise car son RCUE est ADJ
ATT = "ATT" #
@@ -162,6 +163,7 @@ CODES_EXPL = {
ADJ: "Validé par le Jury",
ADJR: "UE validée car son RCUE est validé ADJ par le jury",
ADM: "Validé",
+ ADSUP: "UE ou RCUE validé car le niveau supérieur est validé",
AJ: "Ajourné (ou UE/BC de BUT en attente pour problème de moyenne)",
ATB: "Décision en attente d'un autre semestre (au moins une UE sous la barre)",
ATJ: "Décision en attente d'un autre semestre (assiduité insuffisante)",
@@ -195,17 +197,18 @@ CODES_SEM_ATTENTES = {ATT, ATB, ATJ} # semestre en attente
CODES_SEM_REO = {NAR} # reorientation
CODES_UE_VALIDES_DE_DROIT = {ADM, CMP} # validation "de droit"
-CODES_UE_VALIDES = CODES_UE_VALIDES_DE_DROIT | {ADJ, ADJR}
+CODES_UE_VALIDES = CODES_UE_VALIDES_DE_DROIT | {ADJ, ADJR, ADSUP}
"UE validée"
CODES_UE_CAPITALISANTS = {ADM}
"UE capitalisée"
CODES_RCUE_VALIDES_DE_DROIT = {ADM, CMP}
-CODES_RCUE_VALIDES = CODES_RCUE_VALIDES_DE_DROIT | {ADJ}
+CODES_RCUE_VALIDES = CODES_RCUE_VALIDES_DE_DROIT | {ADJ, ADSUP}
"Niveau RCUE validé"
# Pour le BUT:
-CODES_ANNEE_BUT_VALIDES_DE_DROIT = {ADM, PASD}
+CODES_ANNEE_BUT_VALIDES_DE_DROIT = {ADM} # PASD était ici mais retiré en juin 23
+CODES_ANNEE_BUT_VALIDES = {ADM, ADSUP}
CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL}
BUT_BARRE_UE8 = 8.0 - NOTES_TOLERANCE
BUT_BARRE_UE = BUT_BARRE_RCUE = 10.0 - NOTES_TOLERANCE
@@ -229,6 +232,7 @@ BUT_CODES_ORDERED = {
PASD: 50,
PAS1NCI: 60,
ADJR: 90,
+ ADSUP: 90,
ADJ: 100,
ADM: 100,
}
@@ -249,6 +253,16 @@ def code_ue_validant(code: str) -> bool:
return code in CODES_UE_VALIDES
+def code_rcue_validant(code: str) -> bool:
+ "Vrai si ce code d'RCUE est validant"
+ return code in CODES_RCUE_VALIDES
+
+
+def code_annee_validant(code: str) -> bool:
+ "Vrai si code d'année BUT validant"
+ return code in CODES_ANNEE_BUT_VALIDES
+
+
DEVENIR_EXPL = {
NEXT: "Passage au semestre suivant",
REDOANNEE: "Redoublement année",
diff --git a/app/scodoc/sco_apogee_csv.py b/app/scodoc/sco_apogee_csv.py
index 898647e5a..1c02c3c16 100644
--- a/app/scodoc/sco_apogee_csv.py
+++ b/app/scodoc/sco_apogee_csv.py
@@ -473,7 +473,10 @@ class ApoEtud(dict):
)
def _but_load_validation_annuelle(self):
- "charge la validation de jury BUT annuelle"
+ """charge la validation de jury BUT annuelle.
+ Ici impose qu'elle soit issue d'un semestre de l'année en cours
+ (pas forcément nécessaire, voir selon les retours des équipes ?)
+ """
# le semestre impair de l'année scolaire
if self.cur_res.formsemestre.semestre_id % 2:
formsemestre = self.cur_res.formsemestre
@@ -490,7 +493,9 @@ class ApoEtud(dict):
return
self.validation_annee_but: ApcValidationAnnee = (
ApcValidationAnnee.query.filter_by(
- formsemestre_id=formsemestre.id, etudid=self.etud["etudid"]
+ formsemestre_id=formsemestre.id,
+ etudid=self.etud["etudid"],
+ formation_id=self.cur_sem["formation_id"],
).first()
)
self.is_nar = (
diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py
index b2686445a..3a173f037 100644
--- a/app/scodoc/sco_formsemestre_validation.py
+++ b/app/scodoc/sco_formsemestre_validation.py
@@ -66,6 +66,7 @@ from app.scodoc import sco_photos
from app.scodoc import sco_preferences
from app.scodoc import sco_pv_dict
+
# ------------------------------------------------------------------------------------
def formsemestre_validation_etud_form(
formsemestre_id=None, # required
@@ -1063,8 +1064,6 @@ def formsemestre_validate_previous_ue(formsemestre_id, etudid):
"""Form. saisie UE validée hors ScoDoc
(pour étudiants arrivant avec un UE antérieurement validée).
"""
- from app.scodoc import sco_formations
-
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
formation: Formation = Formation.query.get_or_404(sem["formation_id"])
@@ -1087,8 +1086,8 @@ def formsemestre_validate_previous_ue(formsemestre_id, etudid):
dans un semestre hors ScoDoc.
Les UE validées dans ScoDoc sont déjà
automatiquement prises en compte. Cette page n'est utile que pour les étudiants ayant
- suivi un début de cursus dans un autre établissement, ou bien dans un semestre géré sans
- ScoDoc et qui redouble ce semestre
+ suivi un début de cursus dans un autre établissement, ou bien dans un semestre géré
+ sans ScoDoc et qui redouble ce semestre
(ne pas utiliser pour les semestres précédents !).
Notez que l'UE est validée, avec enregistrement immédiat de la décision et
diff --git a/migrations/versions/c701224fa255_validation_niveaux_inferieurs.py b/migrations/versions/c701224fa255_validation_niveaux_inferieurs.py
new file mode 100644
index 000000000..08f275091
--- /dev/null
+++ b/migrations/versions/c701224fa255_validation_niveaux_inferieurs.py
@@ -0,0 +1,63 @@
+"""validation niveaux inferieurs
+
+Revision ID: c701224fa255
+Revises: d84bc592584e
+Create Date: 2023-06-11 11:08:05.553898
+
+"""
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.orm import sessionmaker # added by ev
+
+# revision identifiers, used by Alembic.
+revision = "c701224fa255"
+down_revision = "d84bc592584e"
+branch_labels = None
+depends_on = None
+
+Session = sessionmaker()
+
+
+def upgrade():
+ # Ajoute la colonne formation_id, nullable, la peuple puis la rend non nullable
+ op.add_column(
+ "apc_validation_annee", sa.Column("formation_id", sa.Integer(), nullable=True)
+ )
+ op.create_foreign_key(
+ "apc_validation_annee_formation_id_fkey",
+ "apc_validation_annee",
+ "notes_formations",
+ ["formation_id"],
+ ["id"],
+ )
+
+ # Affecte la formation des anciennes validations
+ bind = op.get_bind()
+ session = Session(bind=bind)
+ session.execute(
+ sa.text(
+ """
+ UPDATE apc_validation_annee AS a
+ SET formation_id = (
+ SELECT f.id
+ FROM notes_formations f
+ JOIN notes_formsemestre s ON f.id = s.formation_id
+ WHERE s.id = a.formsemestre_id
+ )
+ WHERE a.formsemestre_id IS NOT NULL;
+ """
+ )
+ )
+ op.alter_column(
+ "apc_validation_annee",
+ "formation_id",
+ nullable=False,
+ )
+
+
+def downgrade():
+ with op.batch_alter_table("apc_validation_annee", schema=None) as batch_op:
+ batch_op.drop_constraint(
+ "apc_validation_annee_formation_id_fkey", type_="foreignkey"
+ )
+ batch_op.drop_column("formation_id")
diff --git a/sco_version.py b/sco_version.py
index 28f9304a4..8d1008831 100644
--- a/sco_version.py
+++ b/sco_version.py
@@ -1,7 +1,7 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
-SCOVERSION = "9.4.83"
+SCOVERSION = "9.4.84"
SCONAME = "ScoDoc"
diff --git a/tests/ressources/yaml/cursus_but_gccd_cy.yaml b/tests/ressources/yaml/cursus_but_gccd_cy.yaml
index 56446387a..668951d2c 100644
--- a/tests/ressources/yaml/cursus_but_gccd_cy.yaml
+++ b/tests/ressources/yaml/cursus_but_gccd_cy.yaml
@@ -1,122 +1,122 @@
# Tests unitaires jury BUT
# Essais avec un BUT GCCD (GC-CD) et un parcours de S1 à S6
-# Le GCCD est un programme à 5 compétences, dont certaines
+# Le GCCD est un programme à 5 compétences, dont certaines
# terminent en S4 ou en S6 selon les parcours.
ReferentielCompetences:
- filename: but-GCCD-05012022-081630.xml
+ filename: but-GCCD-05012022-081630.xml
specialite: GCCD
Formation:
filename: scodoc_formation_BUT_GC-CD_v2.xml
# Association des UEs aux compétences:
ues:
- # S1 tronc commun:
- 'UE1.1':
+ # S1 tronc commun:
+ "UE1.1":
annee: BUT1
competence: "Solutions Bâtiment"
- 'UE1.2':
+ "UE1.2":
annee: BUT1
competence: "Solutions TP"
- 'UE1.3':
+ "UE1.3":
annee: BUT1
competence: "Dimensionner"
- 'UE1.4':
+ "UE1.4":
annee: BUT1
competence: Organiser
- 'UE1.5':
+ "UE1.5":
annee: BUT1
competence: Piloter
- # S2 tronc commun:
- 'UE2.1':
+ # S2 tronc commun:
+ "UE2.1":
annee: BUT1
competence: "Solutions Bâtiment"
- 'UE2.2':
+ "UE2.2":
annee: BUT1
competence: "Solutions TP"
- 'UE2.3':
+ "UE2.3":
annee: BUT1
competence: "Dimensionner"
- 'UE2.4':
+ "UE2.4":
annee: BUT1
competence: Organiser
- 'UE2.5':
+ "UE2.5":
annee: BUT1
competence: Piloter
-
+
# S3 : Tronc commun
- 'UE3.1':
+ "UE3.1":
annee: BUT2
competence: "Solutions Bâtiment"
- 'UE3.2':
+ "UE3.2":
annee: BUT2
competence: "Solutions TP"
- 'UE3.3':
+ "UE3.3":
annee: BUT2
competence: "Dimensionner"
- 'UE3.4':
+ "UE3.4":
annee: BUT2
competence: Organiser
- 'UE3.5':
+ "UE3.5":
annee: BUT2
competence: Piloter
# S4 Tronc commun
- 'UE4.1':
+ "UE4.1":
annee: BUT2
competence: "Solutions Bâtiment"
- 'UE4.2':
+ "UE4.2":
annee: BUT2
competence: "Solutions TP"
- 'UE4.3':
+ "UE4.3":
annee: BUT2
competence: "Dimensionner"
- 'UE4.4':
+ "UE4.4":
annee: BUT2
competence: Organiser
- 'UE4.5':
+ "UE4.5":
annee: BUT2
competence: Piloter
# S5 Parcours BAT + TP
- 'UE5.1': # Parcours BAT seulement
+ "UE5.1": # Parcours BAT seulement
annee: BUT3
parcours: BAT # + RAPEB, BEC
competence: "Solutions Bâtiment"
- 'UE5.2': # Parcours TP seulement
+ "UE5.2": # Parcours TP seulement
annee: BUT3
parcours: TP # + BEC
competence: "Solutions TP"
- 'UE5.3':
+ "UE5.3":
annee: BUT3
parcours: [RAPEB, BEC]
competence: "Dimensionner"
- 'UE5.4':
+ "UE5.4":
annee: BUT3
parcours: [BAT, TP]
competence: Organiser
- 'UE5.5':
+ "UE5.5":
annee: BUT3
parcours: [BAT, TP]
competence: Piloter
# S6 Parcours BAT + TP
- 'UE6.1': # Parcours BAT seulement
+ "UE6.1": # Parcours BAT seulement
annee: BUT3
parcours: BAT # + RAPEB, BEC
competence: "Solutions Bâtiment"
- 'UE6.2': # Parcours TP seulement
+ "UE6.2": # Parcours TP seulement
annee: BUT3
- parcours: [TP,BEC]
+ parcours: [TP, BEC]
competence: "Solutions TP"
- 'UE6.3':
+ "UE6.3":
annee: BUT3
- parcours: [RAPEB,BEC]
+ parcours: [RAPEB, BEC]
competence: "Dimensionner"
- 'UE6.4':
+ "UE6.4":
annee: BUT3
parcours: [BAT, TP]
competence: Organiser
- 'UE6.5':
+ "UE6.5":
annee: BUT3
- parcours: [BAT,TP]
+ parcours: [BAT, TP]
competence: Piloter
modules_parcours:
@@ -126,8 +126,8 @@ Formation:
# - tous les module de S1 à S4 dans tous les parcours
# - SAE communes en S1 et S2 mais différenciées par parcours ensuite
# - en S5, ressources différenciées: on ne les mentionne pas toutes ici
- BAT: [ "R[1-4].*", "SAÉ [1-2]", "SAÉ *.BAT.*", "R5.0[1-7]", "R5.14" ]
- TP: [ "R[1-4].*", "SAÉ [1-2]", "SAÉ *.TP.*", "R5.0[1-4]", "R5.0[89]" ]
+ BAT: ["R[1-4].*", "SAÉ [1-2]", "SAÉ *.BAT.*", "R5.0[1-7]", "R5.14"]
+ TP: ["R[1-4].*", "SAÉ [1-2]", "SAÉ *.TP.*", "R5.0[1-4]", "R5.0[89]"]
FormSemestres:
# S1 et S2 avec les parcours BAT et TP:
@@ -135,32 +135,32 @@ FormSemestres:
idx: 1
date_debut: 2021-09-01
date_fin: 2022-01-15
- codes_parcours: ['BAT', 'TP']
- S2:
+ codes_parcours: ["BAT", "TP"]
+ S2:
idx: 2
date_debut: 2022-01-15
date_fin: 2022-06-30
- codes_parcours: ['BAT', 'TP']
+ codes_parcours: ["BAT", "TP"]
S3:
idx: 3
date_debut: 2022-09-01
date_fin: 2023-01-15
- codes_parcours: ['BAT', 'TP']
+ codes_parcours: ["BAT", "TP"]
S4:
idx: 4
date_debut: 2023-01-16
date_fin: 2023-06-30
- codes_parcours: ['BAT', 'TP']
+ codes_parcours: ["BAT", "TP"]
S5:
idx: 5
date_debut: 2023-09-01
date_fin: 2024-01-15
- codes_parcours: ['BAT', 'TP']
+ codes_parcours: ["BAT", "TP"]
S6:
idx: 6
date_debut: 2024-01-16
date_fin: 2024-06-30
- codes_parcours: ['BAT', 'TP']
+ codes_parcours: ["BAT", "TP"]
Etudiants:
A_ok: # Etudiant parcours BAT qui va tout valider directement
@@ -171,10 +171,18 @@ Etudiants:
parcours: BAT
notes_modules:
"R1.01": 11 # toutes UEs
+ "SAÉ 1-2": EXC
S2:
parcours: BAT
notes_modules:
"R2.01": 12 # toutes UEs
+ attendu: # les codes jury que l'on doit vérifier
+ deca:
+ passage_de_droit: True
+ autorisations_inscription: [3]
+ code_valide:
+ nb_competences: 5
+ nb_rcue_annee: 4
S3:
parcours: BAT
notes_modules:
@@ -186,7 +194,7 @@ Etudiants:
S5:
parcours: BAT
- dispense_ues: ['UE5.2', 'UE5.3']
+ dispense_ues: ["UE5.2", "UE5.3"]
notes_modules:
"R5.01": 15 # toutes UE
"SAÉ 5.BAT.01": 10 # UE5.1
@@ -202,6 +210,7 @@ Etudiants:
parcours: TP
notes_modules:
"R1.01": 11 # toutes UEs
+ "SAÉ 1-2": EXC
S2:
parcours: TP
notes_modules:
@@ -217,10 +226,32 @@ Etudiants:
S5:
parcours: TP
- dispense_ues: ['UE5.1', 'UE5.3']
+ dispense_ues: ["UE5.1", "UE5.3"]
notes_modules:
"R5.01": 15 # toutes UE
"SAÉ 5.BAT.01": 10 # UE5.1
"SAÉ 5.BAT.02": 11 # UE5.4
S6:
parcours: TP
+
+ C: # Etudiant qui passe sans un RCUE et valide en BUT2
+ prenom: Étudiant_TP_but2
+ civilite: M
+ formsemestres:
+ S1:
+ parcours: TP
+ notes_modules:
+ "R1.01": 11 # toutes UEs
+ "SAÉ 1-2": 8 # plombe l'UE 2
+ S2:
+ parcours: TP
+ notes_modules:
+ "R2.01": 11 # toutes UEs
+ S3:
+ parcours: TP
+ notes_modules:
+ "R3.01": 12 # toutes UEs
+ S4:
+ parcours: TP
+ notes_modules:
+ "R4.01": 14 # toutes UE
From 008dd9b50ef1771945ba0cd7568b23dd16f1c63e Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet
Date: Thu, 15 Jun 2023 16:50:22 +0200
Subject: [PATCH 02/17] =?UTF-8?q?Fix=20enregistrement=20jury=20ann=C3=A9e?=
=?UTF-8?q?=20BUT=20et=20passage=20en=20mode=20auto?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/but/jury_but.py | 13 +++++++++----
app/scodoc/codes_cursus.py | 2 +-
2 files changed, 10 insertions(+), 5 deletions(-)
diff --git a/app/but/jury_but.py b/app/but/jury_but.py
index 12e14bfb8..227ead81c 100644
--- a/app/but/jury_but.py
+++ b/app/but/jury_but.py
@@ -279,7 +279,6 @@ class DecisionsProposeesAnnee(DecisionsProposees):
self.validation = ApcValidationAnnee.query.filter_by(
etudid=self.etud.id,
formation_id=self.formsemestre.formation_id,
- formsemestre_id=formsemestre_impair.id,
ordre=self.annee_but,
).first()
else:
@@ -417,6 +416,9 @@ class DecisionsProposeesAnnee(DecisionsProposees):
self.formsemestre_impair and self.formsemestre_impair.modalite == "EXT"
) or (self.formsemestre_pair and self.formsemestre_pair.modalite == "EXT"):
self.codes.insert(0, sco_codes.ADM)
+ # Si validée par niveau supérieur:
+ if self.code_valide == sco_codes.ADSUP:
+ self.codes.insert(0, sco_codes.ADSUP)
self.explanation = f"
{expl_rcues}
"
messages = self.descr_pb_coherence()
if messages:
@@ -730,16 +732,18 @@ class DecisionsProposeesAnnee(DecisionsProposees):
dec_ue.record(code)
for dec_rcue, code in codes_rcues:
dec_rcue.record(code)
- self.record(code_annee)
+ self.record(code_annee, mark_recorded=False)
self.record_autorisation_inscription(code_annee)
self.record_all()
+ self.recorded = True
db.session.commit()
- def record(self, code: str, no_overwrite=False) -> bool:
+ def record(self, code: str, no_overwrite=False, mark_recorded: bool = True) -> 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.
+ Si mark_recorded est vrai, positionne self.recorded
"""
if self.inscription_etat != scu.INSCRIT:
return False
@@ -771,7 +775,8 @@ class DecisionsProposeesAnnee(DecisionsProposees):
etudid=self.etud.id,
msg=f"Validation année BUT{self.annee_but}: {code}",
)
- self.recorded = True
+ if mark_recorded:
+ self.recorded = True
self.invalidate_formsemestre_cache()
return True
diff --git a/app/scodoc/codes_cursus.py b/app/scodoc/codes_cursus.py
index 228fb6d25..6c4336a42 100644
--- a/app/scodoc/codes_cursus.py
+++ b/app/scodoc/codes_cursus.py
@@ -207,7 +207,7 @@ CODES_RCUE_VALIDES = CODES_RCUE_VALIDES_DE_DROIT | {ADJ, ADSUP}
"Niveau RCUE validé"
# Pour le BUT:
-CODES_ANNEE_BUT_VALIDES_DE_DROIT = {ADM} # PASD était ici mais retiré en juin 23
+CODES_ANNEE_BUT_VALIDES_DE_DROIT = {ADM, PASD} # PASD pour enregistrement auto
CODES_ANNEE_BUT_VALIDES = {ADM, ADSUP}
CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL}
BUT_BARRE_UE8 = 8.0 - NOTES_TOLERANCE
From 44dffea8d29843b1c84199cfee8f02fb2edc0d40 Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet
Date: Thu, 15 Jun 2023 17:14:37 +0200
Subject: [PATCH 03/17] Fix: tri des coefs. de modules apc
---
app/models/modules.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/models/modules.py b/app/models/modules.py
index cfc6a994f..85503f278 100644
--- a/app/models/modules.py
+++ b/app/models/modules.py
@@ -242,7 +242,7 @@ class Module(db.Model):
"les coefs d'UE, trié par numéro et acronyme d'UE"
# je n'ai pas su mettre un order_by sur le backref sans avoir
# à redéfinir les relationships...
- return sorted(self.ue_coefs, key=attrgetter("numero", "acronyme"))
+ return sorted(self.ue_coefs, key=lambda uc: (uc.ue.numero, uc.ue.acronyme))
def ue_coefs_list(
self, include_zeros=True, ues: list["UniteEns"] = None
From 021b4ec5f880cf428ed419f6f44dea214167c898 Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet
Date: Thu, 15 Jun 2023 21:53:05 +0200
Subject: [PATCH 04/17] =?UTF-8?q?Jury=20BUT:=20condition=20de=20passage=20?=
=?UTF-8?q?de=20S5:=20toutes=20UEs=20de=20BUT1=20valid=C3=A9es.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/but/cursus_but.py | 39 +++++++++++++++++--
app/but/jury_but.py | 39 ++++++++++++++-----
app/models/validations.py | 2 +-
tests/ressources/yaml/cursus_but_gccd_cy.yaml | 23 +++++++++++
tests/unit/yaml_setup.py | 3 +-
5 files changed, 92 insertions(+), 14 deletions(-)
diff --git a/app/but/cursus_but.py b/app/but/cursus_but.py
index 89a7bd669..d8ca17e15 100644
--- a/app/but/cursus_but.py
+++ b/app/but/cursus_but.py
@@ -14,6 +14,7 @@ Classe raccordant avec ScoDoc 7:
"""
import collections
+from operator import attrgetter
from typing import Union
from flask import g, url_for
@@ -23,8 +24,6 @@ from app import log
from app.comp.res_but import ResultatsSemestreBUT
from app.comp.res_compat import NotesTableCompat
-from app.comp import res_sem
-
from app.models.but_refcomp import (
ApcAnneeParcours,
ApcCompetence,
@@ -45,7 +44,7 @@ from app.models.formsemestre import FormSemestre, FormSemestreInscription
from app.models.ues import UniteEns
from app.models.validations import ScolarFormSemestreValidation
from app.scodoc import codes_cursus as sco_codes
-from app.scodoc.codes_cursus import RED, UE_STANDARD
+from app.scodoc.codes_cursus import code_ue_validant, RED, UE_STANDARD
from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
@@ -360,6 +359,40 @@ class FormSemestreCursusBUT:
"cache { competence_id : competence }"
+def etud_ues_de_but1_non_validees(
+ etud: Identite, formation: Formation, parcour: ApcParcours
+) -> list[UniteEns]:
+ """Vrai si cet étudiant a validé toutes ses UEs de S1 et S2, dans son parcours"""
+ # Les UEs avec décisions, dans les S1 ou S2 d'une formation de même code:
+ validations = (
+ ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
+ .filter(ScolarFormSemestreValidation.ue_id != None)
+ .join(UniteEns)
+ .filter(db.or_(UniteEns.semestre_idx == 1, UniteEns.semestre_idx == 2))
+ .join(Formation)
+ .filter_by(formation_code=formation.formation_code)
+ )
+ codes_validations_by_ue = collections.defaultdict(list)
+ for v in validations:
+ codes_validations_by_ue[v.ue_id].append(v.code)
+
+ # Les UEs du parcours en S1 et S2:
+ ues = formation.query_ues_parcour(parcour).filter(
+ db.or_(UniteEns.semestre_idx == 1, UniteEns.semestre_idx == 2)
+ )
+ # Liste triée des ues non validées
+ return sorted(
+ [
+ ue
+ for ue in ues
+ if any(
+ (not code_ue_validant(code) for code in codes_validations_by_ue[ue.id])
+ )
+ ],
+ key=attrgetter("numero", "acronyme"),
+ )
+
+
def formsemestre_warning_apc_setup(
formsemestre: FormSemestre, res: ResultatsSemestreBUT
) -> str:
diff --git a/app/but/jury_but.py b/app/but/jury_but.py
index 227ead81c..c1ded7b88 100644
--- a/app/but/jury_but.py
+++ b/app/but/jury_but.py
@@ -69,6 +69,7 @@ from flask import flash, g, url_for
from app import db
from app import log
+from app.but import cursus_but
from app.but.cursus_but import EtudCursusBUT
from app.comp.res_but import ResultatsSemestreBUT
from app.comp import res_sem
@@ -363,15 +364,33 @@ class DecisionsProposeesAnnee(DecisionsProposees):
"Vrai si plus de la moitié des RCUE validables"
self.passage_de_droit = self.valide_moitie_rcue and (self.nb_rcues_under_8 == 0)
"Vrai si peut passer dans l'année BUT suivante: plus de la moitié validables et tous > 8"
- # XXX TODO ajouter condition pour passage en S5
+ explanation = ""
+ # Cas particulier du passage en BUT 3: nécessité d’avoir validé toutes les UEs du BUT 1.
+ if self.passage_de_droit and self.annee_but == 2:
+ inscription = formsemestre.etuds_inscriptions.get(etud.id)
+ if inscription:
+ ues_but1_non_validees = cursus_but.etud_ues_de_but1_non_validees(
+ etud, formation, inscription.parcour
+ )
+ self.passage_de_droit = not ues_but1_non_validees
+ explanation += (
+ f"""UEs de BUT1 non validées: {
+ ', '.join(ue.acronyme for ue in ues_but1_non_validees)
+ }. """
+ if ues_but1_non_validees
+ else ""
+ )
+ else:
+ # pas inscrit dans le semestre courant ???
+ self.passage_de_droit = False
- # Enfin calcule les codes des UE:
+ # Enfin calcule les codes des UEs:
for dec_ue in self.decisions_ues.values():
dec_ue.compute_codes()
# Reste à attribuer ADM, ADJ, PASD, PAS1NCI, RED, NAR
plural = self.nb_validables > 1
- expl_rcues = f"""{self.nb_validables} niveau{"x" if plural else ""} validable{
+ explanation += f"""{self.nb_validables} niveau{"x" if plural else ""} validable{
"s" if plural else ""} sur {self.nb_competences}"""
if self.admis:
self.codes = [sco_codes.ADM] + self.codes
@@ -390,7 +409,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
sco_codes.ABL,
sco_codes.EXCLU,
]
- expl_rcues = ""
+ explanation = ""
elif self.passage_de_droit:
self.codes = [sco_codes.PASD, sco_codes.ADJ] + self.codes
elif self.valide_moitie_rcue: # mais au moins 1 rcue insuffisante
@@ -400,7 +419,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
sco_codes.PAS1NCI,
sco_codes.ADJ,
] + self.codes
- expl_rcues += f" et {self.nb_rcues_under_8} < 8"
+ explanation += f" et {self.nb_rcues_under_8} < 8"
else:
self.codes = [
sco_codes.RED,
@@ -409,7 +428,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
sco_codes.ADJ,
sco_codes.PASD, # voir #488 (discutable, conventions locales)
] + self.codes
- expl_rcues += f""" et {self.nb_rcues_under_8} niveau{'x' if self.nb_rcues_under_8 > 1 else ''} < 8"""
+ explanation += f""" et {self.nb_rcues_under_8} niveau{'x' if self.nb_rcues_under_8 > 1 else ''} < 8"""
# Si l'un des semestres est extérieur, propose ADM
if (
@@ -419,7 +438,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
# Si validée par niveau supérieur:
if self.code_valide == sco_codes.ADSUP:
self.codes.insert(0, sco_codes.ADSUP)
- self.explanation = f"
{expl_rcues}
"
+ self.explanation = f"
{explanation}
"
messages = self.descr_pb_coherence()
if messages:
self.explanation += (
@@ -1261,6 +1280,8 @@ class DecisionsProposeesRCUE(DecisionsProposees):
validation_rcue = validations_rcue[0]
validation_rcue.code = sco_codes.ADSUP
validation_rcue.date = datetime.now()
+ db.session.add(validation_rcue)
+ db.session.commit()
log(f"updating {validation_rcue}")
if validation_rcue.formsemestre_id is not None:
sco_cache.invalidate_formsemestre(
@@ -1271,9 +1292,9 @@ class DecisionsProposeesRCUE(DecisionsProposees):
validation_rcue = ApcValidationRCUE(
etudid=self.etud.id, ue1_id=ue1.id, ue2_id=ue2.id, code=sco_codes.ADSUP
)
+ db.session.add(validation_rcue)
+ db.session.commit()
log(f"recording {validation_rcue}")
- db.session.add(validation_rcue)
- db.session.commit()
self.valide_annee_inferieure()
def valide_annee_inferieure(self) -> None:
diff --git a/app/models/validations.py b/app/models/validations.py
index cc5565187..229d15ad5 100644
--- a/app/models/validations.py
+++ b/app/models/validations.py
@@ -11,7 +11,7 @@ from app.models.events import Scolog
class ScolarFormSemestreValidation(db.Model):
- """Décisions de jury"""
+ """Décisions de jury (sur semestre ou UEs)"""
__tablename__ = "scolar_formsemestre_validation"
# Assure unicité de la décision:
diff --git a/tests/ressources/yaml/cursus_but_gccd_cy.yaml b/tests/ressources/yaml/cursus_but_gccd_cy.yaml
index 668951d2c..8852a3643 100644
--- a/tests/ressources/yaml/cursus_but_gccd_cy.yaml
+++ b/tests/ressources/yaml/cursus_but_gccd_cy.yaml
@@ -255,3 +255,26 @@ Etudiants:
parcours: TP
notes_modules:
"R4.01": 14 # toutes UE
+
+ D: # Etudiant arrive en S4 avec une UE manquante en S1
+ prenom: Étudiant_TP_malaise
+ civilite: M
+ formsemestres:
+ S1:
+ parcours: TP
+ notes_modules:
+ "R1.01": 11 # toutes UEs
+ "SAÉ 1-2": 8 # plombe l'UE 2
+ S2:
+ parcours: TP
+ notes_modules:
+ "R2.01": 11 # toutes UEs
+ S3:
+ parcours: TP
+ notes_modules:
+ "R3.01": 12 # toutes UEs
+ S4:
+ parcours: TP
+ notes_modules:
+ "R4.01": 14 # toutes UE
+ "R4.04": 6 # plombe l'UE1
diff --git a/tests/unit/yaml_setup.py b/tests/unit/yaml_setup.py
index f24b92b79..35b016a2f 100644
--- a/tests/unit/yaml_setup.py
+++ b/tests/unit/yaml_setup.py
@@ -146,7 +146,7 @@ def create_formsemestre(
return formsemestre
-def create_evaluations(formsemestre: FormSemestre):
+def create_evaluations(formsemestre: FormSemestre, publish_incomplete=True):
"""Crée une évaluation dans chaque module du semestre"""
for modimpl in formsemestre.modimpls:
evaluation = Evaluation(
@@ -156,6 +156,7 @@ def create_evaluations(formsemestre: FormSemestre):
coefficient=1.0,
note_max=20.0,
numero=1,
+ publish_incomplete=publish_incomplete,
)
db.session.add(evaluation)
From f9b45392316c5fbd6f1a1242c927c6c22fd3df97 Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet
Date: Fri, 16 Jun 2023 07:54:28 +0200
Subject: [PATCH 05/17] =?UTF-8?q?Fix:=20mise=20=C3=A0=20jour=20base=20post?=
=?UTF-8?q?gres?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
migrations/versions/d84bc592584e_extension_unaccent.py | 3 ++-
sco_version.py | 2 +-
tools/debian/postinst | 4 +++-
3 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/migrations/versions/d84bc592584e_extension_unaccent.py b/migrations/versions/d84bc592584e_extension_unaccent.py
index 041da64ce..216ee4eee 100644
--- a/migrations/versions/d84bc592584e_extension_unaccent.py
+++ b/migrations/versions/d84bc592584e_extension_unaccent.py
@@ -25,7 +25,8 @@ def upgrade():
bind = op.get_bind()
session = Session(bind=bind)
# Ajout extension pour recherches sans accents:
- session.execute(sa.text("""CREATE EXTENSION IF NOT EXISTS "unaccent";"""))
+ # erreur: doit s'executer en superuser
+ # session.execute(sa.text("""CREATE EXTENSION IF NOT EXISTS "unaccent";"""))
# Clé étrangère sur identite
session.execute(
diff --git a/sco_version.py b/sco_version.py
index 8d1008831..cb65163a2 100644
--- a/sco_version.py
+++ b/sco_version.py
@@ -1,7 +1,7 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
-SCOVERSION = "9.4.84"
+SCOVERSION = "9.4.85"
SCONAME = "ScoDoc"
diff --git a/tools/debian/postinst b/tools/debian/postinst
index a5fcc1b18..dad8205ee 100755
--- a/tools/debian/postinst
+++ b/tools/debian/postinst
@@ -97,13 +97,15 @@ fi
init_postgres_user
# ------------ BASE DE DONNEES
-# gérées avec Flask-Migrate (Alembic/SQLAlchemy)
# Si la base SCODOC existe, tente de la mettre à jour
# (Ne gère pas les bases DEV et TEST)
n=$(su -c "psql -l | grep -c -E '^[[:blank:]]*SCODOC[[:blank:]]*\|'" "$SCODOC_USER")
if [ "$n" == 1 ]
then
echo "Upgrading existing SCODOC database..."
+ # Ajout extension unaccent (postgres superuser, ajout sur base SCODOC)
+ (cd /tmp; echo 'CREATE EXTENSION IF NOT EXISTS "unaccent";' | su -c psql postgres)
+ # Migrations gérées avec Flask-Migrate (Alembic/SQLAlchemy)
# utilise les scripts dans migrations/version/
# pour mettre à jour notre base (en tant qu'utilisateur scodoc)
export FLASK_ENV="production"
From 84d40091a8bfaa5f226c542c98d1b08b2bef0d8d Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet
Date: Sat, 17 Jun 2023 14:56:04 +0200
Subject: [PATCH 06/17] Fix: ordre des RCUE sur les bulletins
---
app/models/but_validations.py | 8 ++++++--
sco_version.py | 2 +-
2 files changed, 7 insertions(+), 3 deletions(-)
diff --git a/app/models/but_validations.py b/app/models/but_validations.py
index d9b0e7e2d..c2be058b3 100644
--- a/app/models/but_validations.py
+++ b/app/models/but_validations.py
@@ -360,8 +360,12 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
# --- RCUEs: seulement sur semestres pairs XXX à améliorer
if formsemestre.semestre_id % 2 == 0:
# validations émises depuis ce formsemestre:
- validations_rcues = ApcValidationRCUE.query.filter_by(
- etudid=etud.id, formsemestre_id=formsemestre.id
+ validations_rcues = (
+ ApcValidationRCUE.query.filter_by(
+ etudid=etud.id, formsemestre_id=formsemestre.id
+ )
+ .join(UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id)
+ .order_by(UniteEns.numero, UniteEns.acronyme)
)
decisions["decision_rcue"] = [v.to_dict_bul() for v in validations_rcues]
titres_rcues = []
diff --git a/sco_version.py b/sco_version.py
index cb65163a2..02d22d18d 100644
--- a/sco_version.py
+++ b/sco_version.py
@@ -1,7 +1,7 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
-SCOVERSION = "9.4.85"
+SCOVERSION = "9.4.86"
SCONAME = "ScoDoc"
From 756c46df0bcaffa6869cdaaee52319b371ec2cbb Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet
Date: Sun, 18 Jun 2023 09:37:13 +0200
Subject: [PATCH 07/17] =?UTF-8?q?Suppressions=20de=20d=C3=A9cisions=20de?=
=?UTF-8?q?=20jury?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/but/jury_but.py | 9 +-
app/but/jury_but_view.py | 2 +-
app/comp/jury.py | 90 ++++++++++++++++++-
app/models/but_validations.py | 9 +-
app/models/formations.py | 2 +-
app/models/formsemestre.py | 10 ++-
app/models/validations.py | 33 ++++++-
app/scodoc/sco_cache.py | 13 +++
app/scodoc/sco_edit_ue.py | 9 +-
app/scodoc/sco_recapcomplet.py | 4 +-
app/scodoc/sco_semset.py | 7 +-
app/static/css/cursus_but.css | 7 +-
app/templates/bul_head.j2 | 8 +-
app/templates/but/cursus_etud.j2 | 6 +-
app/templates/but/parcour_formation.j2 | 4 +-
.../jury/erase_decisions_annee_formation.j2 | 41 +++++++++
app/views/notes.py | 71 ++++++++++++---
17 files changed, 276 insertions(+), 49 deletions(-)
create mode 100644 app/templates/jury/erase_decisions_annee_formation.j2
diff --git a/app/but/jury_but.py b/app/but/jury_but.py
index c1ded7b88..af9b46731 100644
--- a/app/but/jury_but.py
+++ b/app/but/jury_but.py
@@ -75,11 +75,9 @@ from app.comp.res_but import ResultatsSemestreBUT
from app.comp import res_sem
from app.models.but_refcomp import (
- ApcAnneeParcours,
ApcCompetence,
ApcNiveau,
ApcParcours,
- ApcParcoursNiveauCompetence,
)
from app.models import Scolog, ScolarAutorisationInscription
from app.models.but_validations import (
@@ -89,7 +87,7 @@ from app.models.but_validations import (
)
from app.models.etudiants import Identite
from app.models.formations import Formation
-from app.models.formsemestre import FormSemestre, FormSemestreInscription
+from app.models.formsemestre import FormSemestre
from app.models.ues import UniteEns
from app.models.validations import ScolarFormSemestreValidation
from app.scodoc import sco_cache
@@ -473,7 +471,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
scodoc_dept=g.scodoc_dept,
semestre_idx=formsemestre.semestre_id,
formation_id=formsemestre.formation.id)}">
- {formsemestre.formation.to_html()} ({
+ {formsemestre.formation.html()} ({
formsemestre.formation.id})
@@ -902,7 +900,8 @@ class DecisionsProposeesAnnee(DecisionsProposees):
pour cette année: décisions d'UE, de RCUE, d'année,
et autorisations d'inscription émises.
Efface même si étudiant DEM ou DEF.
- Si à cheval, n'efface que pour le semestre d'origine du deca.
+ Si à cheval ou only_one_sem, n'efface que les décisions UE et les
+ autorisations de passage du semestre d'origine du deca.
(commite la session.)
"""
if only_one_sem or self.a_cheval:
diff --git a/app/but/jury_but_view.py b/app/but/jury_but_view.py
index 0a20e9334..a61f1f14b 100644
--- a/app/but/jury_but_view.py
+++ b/app/but/jury_but_view.py
@@ -246,7 +246,7 @@ def _gen_but_rcue(dec_rcue: DecisionsProposeesRCUE, niveau: ApcNiveau) -> str:
scoplement = (
f"""
Effacer les décisions de jury pour l'année {{annee}} de {{etud.nom_disp()}} ?
+
+
Affectera toutes les décisions concernant l'année {{annee}} de la formation,
+quelle que soit leur origine.
+
+
Les décisions concernées sont:
+
+ {% for validation in validations %}
+
{{ validation.html() | safe}}
+
+ {% endfor %}
+
+
+
+
+{% endif %}
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/app/views/notes.py b/app/views/notes.py
index 70990ac60..e29a93c91 100644
--- a/app/views/notes.py
+++ b/app/views/notes.py
@@ -48,9 +48,9 @@ from app.but.forms import jury_but_forms
from app.but import jury_but_pv
from app.but import jury_but_view
-from app.comp import res_sem
+from app.comp import jury, res_sem
from app.comp.res_compat import NotesTableCompat
-from app.models import ScolarAutorisationInscription, ScolarNews, Scolog
+from app.models import Formation, ScolarAutorisationInscription, ScolarNews, Scolog
from app.models.but_refcomp import ApcNiveau, ApcParcours
from app.models.config import ScoDocSiteConfig
from app.models.etudiants import Identite
@@ -2494,7 +2494,19 @@ def formsemestre_validation_but(
erase_span = f"""effacer décisions"""
+ etudid=deca.etud.id)}" class="stdlink"
+ title="efface décisions issues des jurys de cette année"
+ >effacer décisions
+
+ effacer toutes ses décisions de BUT{deca.annee_but}
+ """
H.append(
f"""
@@ -2815,15 +2827,15 @@ def formsemestre_saisie_jury(formsemestre_id: int, selected_etudid: int = None):
)
@scodoc
@permission_required(Permission.ScoView)
-def formsemestre_jury_but_erase(
- formsemestre_id: int, etudid: int = None, only_one_sem=False
-):
+def formsemestre_jury_but_erase(formsemestre_id: int, etudid: int = None):
"""Supprime la décision de jury BUT pour cette année.
- Si only_one_sem, n'efface que pour le formsemestre indiqué, pas les deux de l'année.
Si l'étudiant n'est pas spécifié, efface les décisions de tous les inscrits.
+ Si only_one_sem, n'efface que pour le formsemestre indiqué, pas les deux de l'année.
"""
only_one_sem = int(request.args.get("only_one_sem") or False)
- formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
+ formsemestre: FormSemestre = FormSemestre.query.filter_by(
+ id=formsemestre_id, dept_id=g.scodoc_dept_id
+ ).first_or_404()
if not formsemestre.can_edit_jury():
raise ScoPermissionDenied(
dest_url=url_for(
@@ -2881,14 +2893,53 @@ def formsemestre_jury_but_erase(
if only_one_sem
else """Les validations de toutes les UE, RCUE (compétences) et année
issues de cette année scolaire seront effacées.
- Les décisions des années scolaires précédentes ne seront pas modifiées.
"""
)
- + """
Cette opération est irréversible !
""",
+ + """
+
Les décisions des années scolaires précédentes ne seront pas modifiées.
+
Cette opération est irréversible !
+ """,
cancel_url=dest_url,
)
+@bp.route(
+ "/erase_decisions_annee_formation///",
+ methods=["GET", "POST"],
+)
+@scodoc
+@permission_required(Permission.ScoEtudInscrit)
+def erase_decisions_annee_formation(etudid: int, formation_id: int, annee: int):
+ """Efface toute les décisions d'une année pour cet étudiant"""
+ etud: Identite = Identite.query.get_or_404(etudid)
+ formation: Formation = Formation.query.filter_by(
+ id=formation_id, dept_id=g.scodoc_dept_id
+ ).first_or_404()
+ if request.method == "POST":
+ jury.erase_decisions_annee_formation(etud, formation, annee, delete=True)
+ flash("Décisions de jury effacées")
+ return redirect(
+ url_for(
+ "scolar.ficheEtud",
+ scodoc_dept=g.scodoc_dept,
+ etudid=etud.id,
+ )
+ )
+ validations = jury.erase_decisions_annee_formation(etud, formation, annee)
+ return render_template(
+ "jury/erase_decisions_annee_formation.j2",
+ annee=annee,
+ cancel_url=url_for(
+ "scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id
+ ),
+ etud=etud,
+ formation=formation,
+ validations=validations,
+ sco=ScoData(),
+ title=f"Effacer décisions de jury {etud.nom} - année {annee}",
+ )
+
+
sco_publish(
"/formsemestre_lettres_individuelles",
sco_pv_forms.formsemestre_lettres_individuelles,
From de23302b3e5f138630736270168bccf8af43b730 Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet
Date: Sun, 18 Jun 2023 21:20:02 +0200
Subject: [PATCH 08/17] =?UTF-8?q?Jury=20BUT:=20ajout=20colonne=20d=C3=A9ci?=
=?UTF-8?q?sion=20ann=C3=A9e=20sur=20table=20r=C3=A9cap.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/comp/res_but.py | 48 +++++++++++++++++++++++++++++++---
app/scodoc/sco_recapcomplet.py | 1 +
app/tables/jury_recap.py | 14 +++++++++-
3 files changed, 58 insertions(+), 5 deletions(-)
diff --git a/app/comp/res_but.py b/app/comp/res_but.py
index a91b1dbbc..2c60f24a0 100644
--- a/app/comp/res_but.py
+++ b/app/comp/res_but.py
@@ -10,17 +10,17 @@ import time
import numpy as np
import pandas as pd
-from app import log
+from app import db, log
from app.comp import moy_ue, moy_sem, inscr_mod
from app.comp.res_compat import NotesTableCompat
from app.comp.bonus_spo import BonusSport
-from app.models import ScoDocSiteConfig
+from app.models import Formation, FormSemestreInscription, ScoDocSiteConfig
from app.models.moduleimpls import ModuleImpl
from app.models.but_refcomp import ApcParcours, ApcNiveau
from app.models.ues import DispenseUE, UniteEns
from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
from app.scodoc import sco_preferences
-from app.scodoc.codes_cursus import UE_SPORT
+from app.scodoc.codes_cursus import BUT_CODES_ORDERED, UE_SPORT
from app.scodoc.sco_utils import ModuleType
@@ -44,7 +44,8 @@ class ResultatsSemestreBUT(NotesTableCompat):
"""Parcours de chaque étudiant { etudid : parcour_id }"""
self.ues_ids_by_parcour: dict[set[int]] = {}
"""{ parcour_id : set }, ue_id de chaque parcours"""
-
+ self.validations_annee: dict[int, ApcValidationAnnee] = {}
+ """chargé par get_validations_annee: jury annuel BUT"""
if not self.load_cached():
t0 = time.time()
self.compute()
@@ -321,3 +322,42 @@ class ResultatsSemestreBUT(NotesTableCompat):
formsemestre_id=self.formsemestre.id, etudid=etudid
).count()
)
+
+ def get_validations_annee(self) -> dict[int, ApcValidationAnnee]:
+ """Les validations des étudiants de ce semestre
+ pour l'année BUT d'une formation compatible avec celle de ce semestre.
+ Attention:
+ 1) la validation ne provient pas nécessairement de ce semestre
+ (redoublants, pair/impair, extérieurs).
+ 2) l'étudiant a pu démissionner ou défaillir.
+ 3) S'il y a plusieurs validations pour le même étudiant, prend la "meilleure".
+
+ Mémorise le résultat (dans l'instance, pas en cache: TODO voir au profiler)
+ """
+ if self.validations_annee:
+ return self.validations_annee
+ annee_but = (self.formsemestre.semestre_id + 1) // 2
+ validations = (
+ ApcValidationAnnee.query.filter_by(ordre=annee_but)
+ .join(Formation)
+ .filter_by(formation_code=self.formsemestre.formation.formation_code)
+ .join(
+ FormSemestreInscription,
+ db.and_(
+ FormSemestreInscription.etudid == ApcValidationAnnee.etudid,
+ FormSemestreInscription.formsemestre_id == self.formsemestre.id,
+ ),
+ )
+ )
+ validation_by_etud = {}
+ for validation in validations:
+ if validation.etudid in validation_by_etud:
+ # keep the "best"
+ if BUT_CODES_ORDERED.get(validation.code, 0) > BUT_CODES_ORDERED.get(
+ validation_by_etud[validation.etudid].code, 0
+ ):
+ validation_by_etud[validation.etudid] = validation
+ else:
+ validation_by_etud[validation.etudid] = validation
+ self.validations_annee = validation_by_etud
+ return self.validations_annee
diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py
index 63a0d4e25..9ebe4e87d 100644
--- a/app/scodoc/sco_recapcomplet.py
+++ b/app/scodoc/sco_recapcomplet.py
@@ -251,6 +251,7 @@ def formsemestre_recapcomplet(
~
valeur manquante
=
UE dispensée
nan
valeur non disponible
+
📍
code jury non enregistré
"""
diff --git a/app/tables/jury_recap.py b/app/tables/jury_recap.py
index 5d9e5b885..8d02065cf 100644
--- a/app/tables/jury_recap.py
+++ b/app/tables/jury_recap.py
@@ -81,11 +81,14 @@ class TableJury(TableRecap):
def add_jury(self):
"""Ajoute la colonne code jury et le lien.
- Le code jury est celui du semestre: cette colonne n'est montrée
+ - Le code jury est celui du semestre: cette colonne n'est montrée
que pour les formations classiques, ce code n'est pas utilisé en BUT.
+ - En BUT, on donne la décision de jury annuelle.
"""
res = self.res
autorisations = res.get_autorisations_inscription()
+ if res.is_apc:
+ validations_annee = res.get_validations_annee()
for row in self.rows:
etud = row.etud
if not res.is_apc:
@@ -115,6 +118,15 @@ class TableJury(TableRecap):
group="jury_code_sem",
classes=["recorded_code"],
)
+ if res.is_apc: # BUT
+ validation_annee = validations_annee.get(etud.id, None)
+ row.add_cell(
+ "decision_annuelle",
+ "Année",
+ validation_annee.code if validation_annee else "",
+ group="jury_code_sem",
+ classes=["recorded_code"],
+ )
# Lien saisie ou visu jury
a_saisir = (not res.validations) or (not res.validations.has_decision(etud))
row.add_cell(
From fdfffb70becf46dbcd214050f2176577196df124 Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet
Date: Sun, 18 Jun 2023 21:42:14 +0200
Subject: [PATCH 09/17] Table jury BUT: ajout explication sur col RCUEs
---
app/static/js/table_recap.js | 578 ++++++++++++++++++-----------------
1 file changed, 302 insertions(+), 276 deletions(-)
diff --git a/app/static/js/table_recap.js b/app/static/js/table_recap.js
index 56d43e3d9..11a5db4d4 100644
--- a/app/static/js/table_recap.js
+++ b/app/static/js/table_recap.js
@@ -1,285 +1,311 @@
// Tableau recap notes
$(function () {
- $(function () {
- if ($('table.table_recap').length == 0) { return; }
+ $(function () {
+ if ($("table.table_recap").length == 0) {
+ return;
+ }
- let hidden_colums = [
- "etud_codes", "identite_detail",
- "partition_aux", "partition_rangs", "admission",
- "col_empty"
- ];
- // Etat (tri des colonnes) de la table:
+ let hidden_colums = [
+ "etud_codes",
+ "identite_detail",
+ "partition_aux",
+ "partition_rangs",
+ "admission",
+ "col_empty",
+ ];
+ // Etat (tri des colonnes) de la table:
- const url = new URL(document.URL);
- const formsemestre_id = url.searchParams.get("formsemestre_id");
- const order_info_key = JSON.stringify([url.pathname, formsemestre_id]);
- const etudids_key = JSON.stringify(["etudids", url.origin, formsemestre_id]);
- const noms_key = JSON.stringify(["noms", url.origin, formsemestre_id]);
- let order_info;
- if (formsemestre_id) {
- const x = localStorage.getItem(order_info_key);
- if (x) {
- try {
- order_info = JSON.parse(x);
- } catch (error) {
- console.error(error);
- }
- }
- }
-
- // Les colonnes visibles sont mémorisées, il faut initialiser l'état des boutons
- function update_buttons_labels(dt) {
- // chaque bouton controle une classe stockée dans le data-group du span
- document.querySelectorAll("button.dt-button").forEach(but => {
- let g_span = but.querySelector("span > span");
- if (g_span) {
- let group = g_span.dataset["group"];
- if (group) {
- // si le group (= la 1ere col.) est visible, but_on
- if (dt.columns("." + group).visible()[0]) {
- but.classList.add("but_on");
- but.classList.remove("but_off");
- } else {
- but.classList.add("but_off");
- but.classList.remove("but_on");
- }
- }
- }
- });
- }
-
- // Changement visibilité groupes colonnes (boutons)
- function toggle_col_but_visibility(e, dt, node, config) {
- let group = node.children()[0].firstChild.dataset.group;
- toggle_col_group_visibility(dt, group, node.hasClass("but_on"));
- }
- function toggle_col_ident_visibility(e, dt, node, config) {
- let onoff = node.hasClass("but_on");
- toggle_col_group_visibility(dt, "identite_detail", onoff);
- toggle_col_group_visibility(dt, "identite_court", !onoff);
- }
- function toggle_col_ressources_visibility(e, dt, node, config) {
- let onoff = node.hasClass("but_on");
- toggle_col_group_visibility(dt, "col_res", onoff);
- toggle_col_group_visibility(dt, "col_ue_bonus", onoff);
- toggle_col_group_visibility(dt, "col_malus", onoff);
- }
- function toggle_col_group_visibility(dt, group, onoff) {
- if (onoff) {
- dt.columns('.' + group).visible(false);
- } else {
- dt.columns('.' + group).visible(true);
- }
- update_buttons_labels(dt);
- }
- // Definition des boutons au dessus de la table:
- let buttons = [
- {
- extend: 'copyHtml5',
- text: 'Copier',
- exportOptions: { orthogonal: 'export' }
- },
- {
- extend: 'excelHtml5',
- // footer: true, // ne fonctionne pas ?
- exportOptions: { orthogonal: 'export' },
- title: document.querySelector('table.table_recap').dataset.filename
- },
- {
- // force affichage de toutes les colonnes
- text: '✴',
- action: function (e, dt, node, config) {
- dt.columns().visible(true);
- update_buttons_labels(dt);
- }
- },
- {
- text: '🔄',
- action: function (e, dt, node, config) {
- localStorage.clear();
- console.log("cleared localStorage");
- location.reload();
- }
- },
- {
- text: 'Civilité',
- action: toggle_col_ident_visibility,
- },
- {
- text: 'Groupes',
- action: toggle_col_but_visibility,
- },
- {
- text: 'Rg',
- action: toggle_col_but_visibility,
- },
- ]; // fin des boutons communs à toutes les tables recap
-
- if ($('table.table_recap').hasClass("jury")) {
- // Table JURY:
- // avec ou sans codes enregistrés
- buttons.push(
- {
- text: 'Codes jury',
- action: toggle_col_but_visibility,
- });
- if ($('table.table_recap').hasClass("apc")) {
- // Boutons spécifiques à la table JURY BUT
- buttons.push(
- {
- text: 'Compétences',
- action: toggle_col_but_visibility,
- });
- buttons.push(
- {
- text: 'RCUEs',
- action: toggle_col_but_visibility,
- });
- }
- } else {
- // BOUTONS SPECIFIQUES A LA TABLE RECAP NON JURY
- buttons.push(
- $('table.table_recap').hasClass("apc") ?
- {
- text: 'Ressources',
- action: toggle_col_ressources_visibility,
- } : {
- name: "toggle_mod",
- text: "Cacher les modules",
- action: function (e, dt, node, config) {
- let onoff = node.hasClass("but_on");
- toggle_col_group_visibility(dt, "col_mod:not(.col_empty)", onoff);
- toggle_col_group_visibility(dt, "col_ue_bonus", onoff);
- toggle_col_group_visibility(dt, "col_malus", onoff);
- }
- }
- );
- if ($('table.table_recap').hasClass("apc")) {
- buttons.push({
- text: 'SAÉs',
- action: toggle_col_but_visibility,
- });
- }
- // S'il y a des colonnes vides:
- if ($('table.table_recap td.col_empty').length > 0) {
- buttons.push({ // modules vides
- text: 'Vides',
- action: toggle_col_but_visibility,
- });
- }
- // Boutons admission (pas en jury)
- if (!$('table.table_recap').hasClass("jury")) {
- buttons.push(
- {
- text: 'Admission',
- action: toggle_col_but_visibility,
- }
- );
- }
- }
- // Boutons évaluations (si présentes)
- if ($('table.table_recap').hasClass("with_evaluations")) {
- buttons.push(
- {
- text: 'Évaluations',
- action: toggle_col_but_visibility,
- }
- );
- }
-
- // ------------- LA TABLE ---------
+ const url = new URL(document.URL);
+ const formsemestre_id = url.searchParams.get("formsemestre_id");
+ const order_info_key = JSON.stringify([url.pathname, formsemestre_id]);
+ const etudids_key = JSON.stringify([
+ "etudids",
+ url.origin,
+ formsemestre_id,
+ ]);
+ const noms_key = JSON.stringify(["noms", url.origin, formsemestre_id]);
+ let order_info;
+ if (formsemestre_id) {
+ const x = localStorage.getItem(order_info_key);
+ if (x) {
try {
- let table = $('table.table_recap').DataTable(
- {
- paging: false,
- searching: true,
- info: false,
- autoWidth: false,
- fixedHeader: {
- header: true,
- footer: false
- },
- orderCellsTop: true, // cellules ligne 1 pour tri
- aaSorting: [], // Prevent initial sorting
- colReorder: true,
- stateSave: true, // enregistre état de la table (tris, ...)
- "columnDefs": [
- {
- // cache les codes, le détail de l'identité, les groupes, les colonnes admission et les vides
- targets: hidden_colums,
- visible: false,
- },
- {
- // Elimine les 0 à gauche pour les exports excel et les "copy"
- targets: ["col_mod", "col_moy_gen", "col_moy_ue", "col_res", "col_sae", "evaluation", "col_rcue"],
- render: function (data, type, row) {
- return type === 'export' ? data.replace(/0(\d\..*)/, '$1') : data;
- }
- },
- {
- // Elimine les "+"" pour les exports
- targets: ["col_ue_bonus", "col_malus"],
- render: function (data, type, row) {
- return type === 'export' ? data.replace(/.*\+(\d?\d?\.\d\d).*/m, '$1').replace(/0(\d\..*)/, '$1') : data;
- }
- },
- {
- // Elimine emoji warning sur UEs
- targets: ["col_ues_validables"],
- render: function (data, type, row) {
- return type === 'export' ? data.replace(/(\d+\/\d+).*/, '$1') : data;
- }
- }
-
- ],
- dom: 'Bfrtip',
- buttons: buttons,
- "drawCallback": function (settings) {
- // permet de conserver l'ordre de tri des colonnes
- let table = $('table.table_recap').DataTable();
- let order_info = JSON.stringify(table.order());
- if (formsemestre_id) {
- localStorage.setItem(order_info_key, order_info);
- }
- let etudids = [];
- document.querySelectorAll("td.identite_court").forEach(e => {
- etudids.push(e.dataset.etudid);
- });
- let noms = [];
- document.querySelectorAll("td.identite_court").forEach(e => {
- noms.push(e.dataset.nomprenom);
- });
- localStorage.setItem(etudids_key, JSON.stringify(etudids));
- localStorage.setItem(noms_key, JSON.stringify(noms));
- },
- "order": order_info,
- }
- );
- update_buttons_labels(table);
+ order_info = JSON.parse(x);
} catch (error) {
- // l'erreur peut etre causee par un ancien storage:
- localStorage.removeItem(etudids_key);
- localStorage.removeItem(noms_key);
- localStorage.removeItem(order_info_key);
- location.reload();
+ console.error(error);
}
- });
- $('table.table_recap tbody').on('click', 'tr', function () {
- if ($(this).hasClass('selected')) {
- $(this).removeClass('selected');
+ }
+ }
+
+ // Les colonnes visibles sont mémorisées, il faut initialiser l'état des boutons
+ function update_buttons_labels(dt) {
+ // chaque bouton controle une classe stockée dans le data-group du span
+ document.querySelectorAll("button.dt-button").forEach((but) => {
+ let g_span = but.querySelector("span > span");
+ if (g_span) {
+ let group = g_span.dataset["group"];
+ if (group) {
+ // si le group (= la 1ere col.) est visible, but_on
+ if (dt.columns("." + group).visible()[0]) {
+ but.classList.add("but_on");
+ but.classList.remove("but_off");
+ } else {
+ but.classList.add("but_off");
+ but.classList.remove("but_on");
+ }
+ }
}
- else {
- $('table.table_recap tr.selected').removeClass('selected');
- $(this).addClass('selected');
- }
- });
- // Pour montrer et surligner l'étudiant sélectionné:
- $(function () {
- let row_selected = document.querySelector(".row_selected");
- if (row_selected) {
- row_selected.scrollIntoView();
- window.scrollBy(0, -125);
- row_selected.classList.add("selected");
- }
- });
+ });
+ }
+
+ // Changement visibilité groupes colonnes (boutons)
+ function toggle_col_but_visibility(e, dt, node, config) {
+ let group = node.children()[0].firstChild.dataset.group;
+ toggle_col_group_visibility(dt, group, node.hasClass("but_on"));
+ }
+ function toggle_col_ident_visibility(e, dt, node, config) {
+ let onoff = node.hasClass("but_on");
+ toggle_col_group_visibility(dt, "identite_detail", onoff);
+ toggle_col_group_visibility(dt, "identite_court", !onoff);
+ }
+ function toggle_col_ressources_visibility(e, dt, node, config) {
+ let onoff = node.hasClass("but_on");
+ toggle_col_group_visibility(dt, "col_res", onoff);
+ toggle_col_group_visibility(dt, "col_ue_bonus", onoff);
+ toggle_col_group_visibility(dt, "col_malus", onoff);
+ }
+ function toggle_col_group_visibility(dt, group, onoff) {
+ if (onoff) {
+ dt.columns("." + group).visible(false);
+ } else {
+ dt.columns("." + group).visible(true);
+ }
+ update_buttons_labels(dt);
+ }
+ // Definition des boutons au dessus de la table:
+ let buttons = [
+ {
+ extend: "copyHtml5",
+ text: "Copier",
+ exportOptions: { orthogonal: "export" },
+ },
+ {
+ extend: "excelHtml5",
+ // footer: true, // ne fonctionne pas ?
+ exportOptions: { orthogonal: "export" },
+ title: document.querySelector("table.table_recap").dataset.filename,
+ },
+ {
+ // force affichage de toutes les colonnes
+ text: '✴',
+ action: function (e, dt, node, config) {
+ dt.columns().visible(true);
+ update_buttons_labels(dt);
+ },
+ },
+ {
+ text: '🔄',
+ action: function (e, dt, node, config) {
+ localStorage.clear();
+ console.log("cleared localStorage");
+ location.reload();
+ },
+ },
+ {
+ text: 'Civilité',
+ action: toggle_col_ident_visibility,
+ },
+ {
+ text: 'Groupes',
+ action: toggle_col_but_visibility,
+ },
+ {
+ text: 'Rg',
+ action: toggle_col_but_visibility,
+ },
+ ]; // fin des boutons communs à toutes les tables recap
+
+ if ($("table.table_recap").hasClass("jury")) {
+ // Table JURY:
+ // avec ou sans codes enregistrés
+ buttons.push({
+ text: 'Codes jury',
+ action: toggle_col_but_visibility,
+ });
+ if ($("table.table_recap").hasClass("apc")) {
+ // Boutons spécifiques à la table JURY BUT
+ buttons.push({
+ text: 'Compétences',
+ action: toggle_col_but_visibility,
+ });
+ buttons.push({
+ text: 'RCUEs',
+ action: toggle_col_but_visibility,
+ });
+ }
+ } else {
+ // BOUTONS SPECIFIQUES A LA TABLE RECAP NON JURY
+ buttons.push(
+ $("table.table_recap").hasClass("apc")
+ ? {
+ text: 'Ressources',
+ action: toggle_col_ressources_visibility,
+ }
+ : {
+ name: "toggle_mod",
+ text: "Cacher les modules",
+ action: function (e, dt, node, config) {
+ let onoff = node.hasClass("but_on");
+ toggle_col_group_visibility(
+ dt,
+ "col_mod:not(.col_empty)",
+ onoff
+ );
+ toggle_col_group_visibility(dt, "col_ue_bonus", onoff);
+ toggle_col_group_visibility(dt, "col_malus", onoff);
+ },
+ }
+ );
+ if ($("table.table_recap").hasClass("apc")) {
+ buttons.push({
+ text: 'SAÉs',
+ action: toggle_col_but_visibility,
+ });
+ }
+ // S'il y a des colonnes vides:
+ if ($("table.table_recap td.col_empty").length > 0) {
+ buttons.push({
+ // modules vides
+ text: 'Vides',
+ action: toggle_col_but_visibility,
+ });
+ }
+ // Boutons admission (pas en jury)
+ if (!$("table.table_recap").hasClass("jury")) {
+ buttons.push({
+ text: 'Admission',
+ action: toggle_col_but_visibility,
+ });
+ }
+ }
+ // Boutons évaluations (si présentes)
+ if ($("table.table_recap").hasClass("with_evaluations")) {
+ buttons.push({
+ text: 'Évaluations',
+ action: toggle_col_but_visibility,
+ });
+ }
+
+ // ------------- LA TABLE ---------
+ try {
+ let table = $("table.table_recap").DataTable({
+ paging: false,
+ searching: true,
+ info: false,
+ autoWidth: false,
+ fixedHeader: {
+ header: true,
+ footer: false,
+ },
+ orderCellsTop: true, // cellules ligne 1 pour tri
+ aaSorting: [], // Prevent initial sorting
+ colReorder: true,
+ stateSave: true, // enregistre état de la table (tris, ...)
+ columnDefs: [
+ {
+ // cache les codes, le détail de l'identité, les groupes, les colonnes admission et les vides
+ targets: hidden_colums,
+ visible: false,
+ },
+ {
+ // Elimine les 0 à gauche pour les exports excel et les "copy"
+ targets: [
+ "col_mod",
+ "col_moy_gen",
+ "col_moy_ue",
+ "col_res",
+ "col_sae",
+ "evaluation",
+ "col_rcue",
+ ],
+ render: function (data, type, row) {
+ return type === "export" ? data.replace(/0(\d\..*)/, "$1") : data;
+ },
+ },
+ {
+ // Elimine les "+"" pour les exports
+ targets: ["col_ue_bonus", "col_malus"],
+ render: function (data, type, row) {
+ return type === "export"
+ ? data
+ .replace(/.*\+(\d?\d?\.\d\d).*/m, "$1")
+ .replace(/0(\d\..*)/, "$1")
+ : data;
+ },
+ },
+ {
+ // Elimine emoji warning sur UEs
+ targets: ["col_ues_validables"],
+ render: function (data, type, row) {
+ return type === "export"
+ ? data.replace(/(\d+\/\d+).*/, "$1")
+ : data;
+ },
+ },
+ ],
+ dom: "Bfrtip",
+ buttons: buttons,
+ drawCallback: function (settings) {
+ // permet de conserver l'ordre de tri des colonnes
+ let table = $("table.table_recap").DataTable();
+ let order_info = JSON.stringify(table.order());
+ if (formsemestre_id) {
+ localStorage.setItem(order_info_key, order_info);
+ }
+ let etudids = [];
+ document.querySelectorAll("td.identite_court").forEach((e) => {
+ etudids.push(e.dataset.etudid);
+ });
+ let noms = [];
+ document.querySelectorAll("td.identite_court").forEach((e) => {
+ noms.push(e.dataset.nomprenom);
+ });
+ localStorage.setItem(etudids_key, JSON.stringify(etudids));
+ localStorage.setItem(noms_key, JSON.stringify(noms));
+ },
+ order: order_info,
+ });
+ update_buttons_labels(table);
+ } catch (error) {
+ // l'erreur peut etre causee par un ancien storage:
+ localStorage.removeItem(etudids_key);
+ localStorage.removeItem(noms_key);
+ localStorage.removeItem(order_info_key);
+ location.reload();
+ }
+ });
+ $("table.table_recap tbody").on("click", "tr", function () {
+ if ($(this).hasClass("selected")) {
+ $(this).removeClass("selected");
+ } else {
+ $("table.table_recap tr.selected").removeClass("selected");
+ $(this).addClass("selected");
+ }
+ });
+ // Pour montrer et surligner l'étudiant sélectionné:
+ $(function () {
+ let row_selected = document.querySelector(".row_selected");
+ if (row_selected) {
+ row_selected.scrollIntoView();
+ window.scrollBy(0, -125);
+ row_selected.classList.add("selected");
+ }
+ });
+ // Ajoute bulle aide sur colonne RCUEs
+ $(function () {
+ // explication colonne RCUEs
+ let th = document.querySelector(
+ "table.table_recap.apc th.col_rcues_validables"
+ );
+ th.title = "RCUEs validables avec ces notes";
+ });
});
From b026349e740d963cd957fe6ea66e1ff2e7cced25 Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet
Date: Mon, 19 Jun 2023 22:07:31 +0200
Subject: [PATCH 10/17] =?UTF-8?q?Affichage=20et=20suppression=20possible?=
=?UTF-8?q?=20de=20toutes=20les=20d=C3=A9cisions=20de=20jury?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/api/jury.py | 144 +++++++++++++++++-
app/but/jury_edit_manual.py | 66 ++++++++
app/models/etudiants.py | 6 +
app/models/validations.py | 10 +-
app/scodoc/sco_page_etud.py | 8 +-
app/static/css/jury_delete_manual.css | 9 ++
.../jury/erase_decisions_annee_formation.j2 | 4 +-
app/templates/jury/jury_delete_manual.j2 | 134 ++++++++++++++++
app/views/notes.py | 15 +-
9 files changed, 386 insertions(+), 10 deletions(-)
create mode 100644 app/but/jury_edit_manual.py
create mode 100644 app/static/css/jury_delete_manual.css
create mode 100644 app/templates/jury/jury_delete_manual.j2
diff --git a/app/api/jury.py b/app/api/jury.py
index 28c0ae8bc..2800c77a9 100644
--- a/app/api/jury.py
+++ b/app/api/jury.py
@@ -12,11 +12,20 @@ from flask_json import as_json
from flask_login import login_required
import app
-from app.api import api_bp as bp, api_web_bp
+from app import db, log
+from app.api import api_bp as bp, api_web_bp, tools
from app.decorators import scodoc, permission_required
from app.scodoc.sco_exceptions import ScoException
from app.but import jury_but_results
-from app.models import FormSemestre
+from app.models import (
+ ApcValidationAnnee,
+ ApcValidationRCUE,
+ FormSemestre,
+ Identite,
+ ScolarAutorisationInscription,
+ ScolarFormSemestreValidation,
+)
+from app.scodoc import sco_cache
from app.scodoc.sco_permissions import Permission
@@ -36,3 +45,134 @@ def decisions_jury(formsemestre_id: int):
return rows
else:
raise ScoException("non implemente")
+
+
+@bp.route(
+ "/etudiant//jury/validation_ue//delete",
+ methods=["POST"],
+)
+@api_web_bp.route(
+ "/etudiant//jury/validation_ue//delete",
+ methods=["POST"],
+)
+@login_required
+@scodoc
+@permission_required(Permission.ScoEtudInscrit)
+@as_json
+def validation_ue_delete(etudid: int, validation_id: int):
+ "Efface cette validation"
+ return _validation_ue_delete(etudid, validation_id)
+
+
+@bp.route(
+ "/etudiant//jury/validation_formsemestre//delete",
+ methods=["POST"],
+)
+@api_web_bp.route(
+ "/etudiant//jury/validation_formsemestre//delete",
+ methods=["POST"],
+)
+@login_required
+@scodoc
+@permission_required(Permission.ScoEtudInscrit)
+@as_json
+def validation_formsemestre_delete(etudid: int, validation_id: int):
+ "Efface cette validation"
+ # c'est la même chose (formations classiques)
+ return _validation_ue_delete(etudid, validation_id)
+
+
+def _validation_ue_delete(etudid: int, validation_id: int):
+ "Efface cette validation (semestres classiques ou UEs)"
+ etud = tools.get_etud(etudid)
+ if etud is None:
+ return "étudiant inconnu", 404
+ validation = ScolarFormSemestreValidation.query.filter_by(
+ id=validation_id, etudid=etudid
+ ).first_or_404()
+ log(f"validation_ue_delete: etuid={etudid} {validation}")
+ db.session.delete(validation)
+ sco_cache.invalidate_formsemestre_etud(etud)
+ db.session.commit()
+ return "ok"
+
+
+@bp.route(
+ "/etudiant//jury/autorisation_inscription//delete",
+ methods=["POST"],
+)
+@api_web_bp.route(
+ "/etudiant//jury/autorisation_inscription//delete",
+ methods=["POST"],
+)
+@login_required
+@scodoc
+@permission_required(Permission.ScoEtudInscrit)
+@as_json
+def autorisation_inscription_delete(etudid: int, validation_id: int):
+ "Efface cette validation"
+ etud = tools.get_etud(etudid)
+ if etud is None:
+ return "étudiant inconnu", 404
+ validation = ScolarAutorisationInscription.query.filter_by(
+ id=validation_id, etudid=etudid
+ ).first_or_404()
+ log(f"autorisation_inscription_delete: etuid={etudid} {validation}")
+ db.session.delete(validation)
+ sco_cache.invalidate_formsemestre_etud(etud)
+ db.session.commit()
+ return "ok"
+
+
+@bp.route(
+ "/etudiant//jury/validation_rcue//delete",
+ methods=["POST"],
+)
+@api_web_bp.route(
+ "/etudiant//jury/validation_rcue//delete",
+ methods=["POST"],
+)
+@login_required
+@scodoc
+@permission_required(Permission.ScoEtudInscrit)
+@as_json
+def validation_rcue_delete(etudid: int, validation_id: int):
+ "Efface cette validation"
+ etud = tools.get_etud(etudid)
+ if etud is None:
+ return "étudiant inconnu", 404
+ validation = ApcValidationRCUE.query.filter_by(
+ id=validation_id, etudid=etudid
+ ).first_or_404()
+ log(f"validation_ue_delete: etuid={etudid} {validation}")
+ db.session.delete(validation)
+ sco_cache.invalidate_formsemestre_etud(etud)
+ db.session.commit()
+ return "ok"
+
+
+@bp.route(
+ "/etudiant//jury/validation_annee_but//delete",
+ methods=["POST"],
+)
+@api_web_bp.route(
+ "/etudiant//jury/validation_annee_but//delete",
+ methods=["POST"],
+)
+@login_required
+@scodoc
+@permission_required(Permission.ScoEtudInscrit)
+@as_json
+def validation_annee_but_delete(etudid: int, validation_id: int):
+ "Efface cette validation"
+ etud = tools.get_etud(etudid)
+ if etud is None:
+ return "étudiant inconnu", 404
+ validation = ApcValidationAnnee.query.filter_by(
+ id=validation_id, etudid=etudid
+ ).first_or_404()
+ log(f"validation_annee_but: etuid={etudid} {validation}")
+ db.session.delete(validation)
+ sco_cache.invalidate_formsemestre_etud(etud)
+ db.session.commit()
+ return "ok"
diff --git a/app/but/jury_edit_manual.py b/app/but/jury_edit_manual.py
new file mode 100644
index 000000000..73e21ecf5
--- /dev/null
+++ b/app/but/jury_edit_manual.py
@@ -0,0 +1,66 @@
+##############################################################################
+# ScoDoc
+# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
+# See LICENSE
+##############################################################################
+
+"""Jury édition manuelle des décisions (correction d'erreurs, parcours hors normes)
+
+Non spécifique au BUT.
+"""
+
+import flask
+from flask import flash, render_template, url_for
+from flask import g, request
+
+from app import db
+
+from app.models import (
+ ApcValidationAnnee,
+ ApcValidationRCUE,
+ FormSemestre,
+ Identite,
+ UniteEns,
+ ScolarAutorisationInscription,
+ ScolarFormSemestreValidation,
+)
+from app.views import ScoData
+
+
+def jury_delete_manual(etud: Identite):
+ """Vue (réservée au chef de dept.)
+ présentant *toutes* les décisions de jury concernant cet étudiant
+ et permettant de les supprimer une à une.
+ """
+ sem_vals = ScolarFormSemestreValidation.query.filter_by(
+ etudid=etud.id, ue_id=None
+ ).order_by(ScolarFormSemestreValidation.event_date)
+ ue_vals = (
+ ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
+ .join(UniteEns)
+ .order_by(ScolarFormSemestreValidation.event_date, UniteEns.numero)
+ )
+ autorisations = ScolarAutorisationInscription.query.filter_by(
+ etudid=etud.id
+ ).order_by(
+ ScolarAutorisationInscription.semestre_id, ScolarAutorisationInscription.date
+ )
+ rcue_vals = (
+ ApcValidationRCUE.query.filter_by(etudid=etud.id)
+ .join(UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id)
+ .order_by(UniteEns.semestre_idx, UniteEns.numero, ApcValidationRCUE.date)
+ )
+ annee_but_vals = ApcValidationAnnee.query.filter_by(etudid=etud.id).order_by(
+ ApcValidationAnnee.ordre, ApcValidationAnnee.date
+ )
+ return render_template(
+ "jury/jury_delete_manual.j2",
+ etud=etud,
+ sem_vals=sem_vals,
+ ue_vals=ue_vals,
+ autorisations=autorisations,
+ rcue_vals=rcue_vals,
+ annee_but_vals=annee_but_vals,
+ sco=ScoData(),
+ title=f"Toutes les décisions de jury enregistrées pour {etud.html_link_fiche()}",
+ )
diff --git a/app/models/etudiants.py b/app/models/etudiants.py
index 8a6047097..d67a84828 100644
--- a/app/models/etudiants.py
+++ b/app/models/etudiants.py
@@ -78,6 +78,12 @@ class Identite(db.Model):
f""
)
+ def html_link_fiche(self) -> str:
+ "lien vers la fiche"
+ return f"""{self.nomprenom}"""
+
@classmethod
def from_request(cls, etudid=None, code_nip=None) -> "Identite":
"""Étudiant à partir de l'etudid ou du code_nip, soit
diff --git a/app/models/validations.py b/app/models/validations.py
index 9a938b6c5..8a1a8dd0d 100644
--- a/app/models/validations.py
+++ b/app/models/validations.py
@@ -76,12 +76,14 @@ class ScolarFormSemestreValidation(db.Model):
d.pop("_sa_instance_state", None)
return d
- def html(self) -> str:
+ def html(self, detail=False) -> str:
"Affichage html"
if self.ue_id is not None:
- return f"""Validation de l'UE {self.ue.acronyme}
- ({self.code}
- le {self.event_date.strftime("%d/%m/%Y")} à {self.event_date.strftime("%Hh%M")})
+ return f"""Validation de l'UE {self.ue.acronyme} de {self.ue.formation.acronyme}
+ {("émise par " + self.formsemestre.html_link_status())
+ if self.formsemestre else ""}
+ :{self.code}
+ le {self.event_date.strftime("%d/%m/%Y")} à {self.event_date.strftime("%Hh%M")}
"""
else:
return f"""Validation du semestre S{
diff --git a/app/scodoc/sco_page_etud.py b/app/scodoc/sco_page_etud.py
index 9901dad36..4b1973768 100644
--- a/app/scodoc/sco_page_etud.py
+++ b/app/scodoc/sco_page_etud.py
@@ -312,7 +312,13 @@ def ficheEtud(etudid=None):
] = f"""inscrire à un autre semestre"""
+ }">inscrire à un autre semestre
+ éditer toutes décisions de jury
+ """
+
else:
info["link_inscrire_ailleurs"] = ""
else:
diff --git a/app/static/css/jury_delete_manual.css b/app/static/css/jury_delete_manual.css
new file mode 100644
index 000000000..6580e089f
--- /dev/null
+++ b/app/static/css/jury_delete_manual.css
@@ -0,0 +1,9 @@
+
+div.jury_decisions_list div {
+ font-size: 120%;
+ font-weight: bold;
+}
+
+div.jury_decisions_list form {
+ display: inline-block;
+}
\ No newline at end of file
diff --git a/app/templates/jury/erase_decisions_annee_formation.j2 b/app/templates/jury/erase_decisions_annee_formation.j2
index 5298437a5..7d0353eb8 100644
--- a/app/templates/jury/erase_decisions_annee_formation.j2
+++ b/app/templates/jury/erase_decisions_annee_formation.j2
@@ -3,7 +3,7 @@
{% block app_content %}
{% if not validations %}
-
Aucune validation de jury enregistrée pour {{etud.nom_disp()}} sur
+
Aucune validation de jury enregistrée pour {{etud.html_link_fiche()}} sur
l'année {{annee}}
de la formation {{ formation.html() }}
@@ -13,7 +13,7 @@ de la formation {{ formation.html() }}
{% else %}
-
Effacer les décisions de jury pour l'année {{annee}} de {{etud.nom_disp()}} ?
+
Effacer les décisions de jury pour l'année {{annee}} de {{etud.html_link_fiche()}} ?
Affectera toutes les décisions concernant l'année {{annee}} de la formation,
quelle que soit leur origine.
Décisions de jury enregistrées pour {{etud.html_link_fiche()|safe}}
+
+
+Cette page liste toutes les décisions de jury connus de ScoDoc concernant cet étudiant
+et permet de les effacer une par une.
+
+
+Attention, il vous appartient de vérifier la cohérence du résultat !
+En principe, l'usage de cette page devrait rester exceptionnel.
+Aucune annulation n'est ici possible (vous devrez re-saisir les décisions via les
+pages de saisie de jury habituelles).
+
+{% if sem_vals.first() %}
+
+
Décisions de semestres
+
+ {% for v in sem_vals %}
+
{{v.html()|safe}}
+
+
+ {% endfor %}
+
+
+{% endif %}
+
+{% if ue_vals.first() %}
+
+
Décisions d'UEs
+
+ {% for v in ue_vals %}
+
{{v.html(detail=True)|safe}}
+
+
+ {% endfor %}
+
+
+{% endif %}
+
+{% if rcue_vals.first() %}
+
+
Décisions de RCUE (niveaux de compétences)
+
+ {% for v in rcue_vals %}
+
{{v.html()|safe}}
+
+
+ {% endfor %}
+
+
+{% endif %}
+
+{% if annee_but_vals.first() %}
+
+
Décisions d'années BUT
+
+ {% for v in annee_but_vals %}
+
{{v.html()|safe}}
+
+
+ {% endfor %}
+
+
+{% endif %}
+
+{% if autorisations.first() %}
+
+
Autorisations d'inscriptions (passages)
+
+ {% for v in autorisations %}
+
{{v.html()|safe}}
+
+
+ {% endfor %}
+
+
+{% endif %}
+
+{% if not(
+ sem_vals.first() or sem_ues.first() or sem_rcues.first()
+ or annee_but_vals.first() or autorisations.first())
+%}
+
""")
# Légende
H.append(
"""
@@ -272,12 +277,12 @@ def _formsemestre_recapcomplet_to_html(
filename: str = "",
mode_jury=False, # saisie décisions jury
selected_etudid=None,
-) -> tuple[str, TableRecap]:
+) -> tuple[str, TableRecap, collections.Counter]:
"""Le tableau recap en html"""
if tabformat not in ("html", "evals"):
raise ScoValueError("invalid table format")
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
- table_html, table = gen_formsemestre_recapcomplet_html_table(
+ table_html, table, freq_codes_annuels = gen_formsemestre_recapcomplet_html_table(
formsemestre,
res,
include_evaluations=(tabformat == "evals"),
@@ -285,7 +290,7 @@ def _formsemestre_recapcomplet_to_html(
filename=filename,
selected_etudid=selected_etudid,
)
- return table_html, table
+ return table_html, table, freq_codes_annuels
def _formsemestre_recapcomplet_to_file(
@@ -447,9 +452,9 @@ def gen_formsemestre_recapcomplet_html_table(
mode_jury=False,
filename="",
selected_etudid=None,
-) -> tuple[str, TableRecap]:
+) -> tuple[str, TableRecap, collections.Counter]:
"""Construit table recap pour le BUT
- Cache le résultat pour le semestre (sauf en mode jury).
+ Cache le résultat pour le semestre.
Note: on cache le HTML et non l'objet Table.
Si mode_jury, occultera colonnes modules (en js)
@@ -461,6 +466,7 @@ def gen_formsemestre_recapcomplet_html_table(
"""
table = None
table_html = None
+ table_html_cached = None
cache_class = {
(True, True): sco_cache.TableJuryWithEvalsCache,
(True, False): sco_cache.TableJuryCache,
@@ -468,8 +474,8 @@ def gen_formsemestre_recapcomplet_html_table(
(False, False): sco_cache.TableRecapCache,
}[(bool(mode_jury), bool(include_evaluations))]
if not selected_etudid:
- table_html = cache_class.get(formsemestre.id)
- if table_html is None:
+ table_html_cached = cache_class.get(formsemestre.id)
+ if table_html_cached is None:
table = _gen_formsemestre_recapcomplet_table(
res,
include_evaluations,
@@ -478,9 +484,14 @@ def gen_formsemestre_recapcomplet_html_table(
selected_etudid=selected_etudid,
)
table_html = table.html()
- cache_class.set(formsemestre.id, table_html)
+ freq_codes_annuels = (
+ table.freq_codes_annuels if hasattr(table, "freq_codes_annuels") else None
+ )
+ cache_class.set(formsemestre.id, (table_html, freq_codes_annuels))
+ else:
+ table_html, freq_codes_annuels = table_html_cached
- return table_html, table
+ return table_html, table, freq_codes_annuels
def _gen_formsemestre_recapcomplet_table(
diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css
index 4f922c9d9..172e90081 100644
--- a/app/static/css/scodoc.css
+++ b/app/static/css/scodoc.css
@@ -2773,6 +2773,8 @@ table.notes_recapcomplet a:hover {
div.table_recap_caption {
width: fit-content;
+ margin-top: 8px;
+ margin-bottom: 8px;
padding: 8px;
border-radius: 8px;
background-color: rgb(202, 255, 180);
diff --git a/app/tables/jury_recap.py b/app/tables/jury_recap.py
index 8d02065cf..3e6f5d24d 100644
--- a/app/tables/jury_recap.py
+++ b/app/tables/jury_recap.py
@@ -78,6 +78,7 @@ class TableJury(TableRecap):
dec_rcue = deca.dec_rcue_by_ue.get(rcue.ue_1.id)
if dec_rcue is not None: # None si l'UE n'est pas associée à un niveau
row.add_rcue_cols(dec_rcue)
+ self.freq_codes_annuels["total"] = len(self.rows)
def add_jury(self):
"""Ajoute la colonne code jury et le lien.
From 8156cce4be039582f7f666b1c46d66440e4f5e44 Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet
Date: Tue, 20 Jun 2023 14:26:01 +0200
Subject: [PATCH 15/17] Fix typo
---
app/templates/jury/jury_delete_manual.j2 | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/templates/jury/jury_delete_manual.j2 b/app/templates/jury/jury_delete_manual.j2
index c7845637b..e423501fb 100644
--- a/app/templates/jury/jury_delete_manual.j2
+++ b/app/templates/jury/jury_delete_manual.j2
@@ -86,7 +86,7 @@ pages de saisie de jury habituelles).
{% endif %}
{% if not(
- sem_vals.first() or sem_ues.first() or sem_rcues.first()
+ sem_vals.first() or ue_vals.first() or rcue_vals.first()
or annee_but_vals.first() or autorisations.first())
%}
From f4277a13366f4eb205dff8230633f77173e8f365 Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet
Date: Tue, 20 Jun 2023 19:56:20 +0200
Subject: [PATCH 16/17] =?UTF-8?q?Jury=20BUT:=20effacement=20d=C3=A9cision?=
=?UTF-8?q?=20ann=C3=A9e=20+=202=20petits=20bugs=20mineurs?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/but/jury_but.py | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/app/but/jury_but.py b/app/but/jury_but.py
index af9b46731..6861c8ecd 100644
--- a/app/but/jury_but.py
+++ b/app/but/jury_but.py
@@ -931,7 +931,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
# XXX efface les validations émise depuis ce semestre
# et pas toutes celles concernant cette l'année...
# (utiliser formation_id pour changer cette politique)
- formsemestre_id=self.formsemestre_impair.id,
+ formsemestre_id=self.formsemestre.id,
ordre=self.annee_but,
)
for validation in validations:
@@ -1286,7 +1286,7 @@ class DecisionsProposeesRCUE(DecisionsProposees):
sco_cache.invalidate_formsemestre(
formsemestre_id=validation_rcue.formsemestre_id
)
- else:
+ elif ue1 and ue2:
# Crée nouvelle validation
validation_rcue = ApcValidationRCUE(
etudid=self.etud.id, ue1_id=ue1.id, ue2_id=ue2.id, code=sco_codes.ADSUP
@@ -1380,20 +1380,20 @@ class DecisionsProposeesRCUE(DecisionsProposees):
"Impossible de valider le niveau de compétence inférieur: pas 2 UEs associées'",
"warning",
)
- return
+ return [], None, None
ues_impaires = [ue for ue in ues if ue.semestre_idx % 2]
if len(ues_impaires) != 1:
flash(
"Impossible de valider le niveau de compétence inférieur: pas d'UE impaire associée"
)
- return
+ return [], None, None
ue1 = ues_impaires[0]
ues_paires = [ue for ue in ues if not ue.semestre_idx % 2]
if len(ues_paires) != 1:
flash(
"Impossible de valider le niveau de compétence inférieur: pas d'UE paire associée"
)
- return
+ return [], None, None
ue2 = ues_paires[0]
return ues, ue1, ue2
From ccc589f4d51f593d5baaa8f4f5b766669c3c878e Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet
Date: Tue, 20 Jun 2023 21:01:40 +0200
Subject: [PATCH 17/17] =?UTF-8?q?Modifie=20effacement=20d=C3=A9cisions=20a?=
=?UTF-8?q?nnuelles=20BUT=20et=20RCUE.=20Am=C3=A9liore=20affichage=20d?=
=?UTF-8?q?=C3=A9cisions?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/but/jury_but.py | 35 +++++++++++--------
app/models/validations.py | 14 +++++---
app/static/css/jury_delete_manual.css | 6 +++-
.../but/formsemestre_validation_auto_but.j2 | 10 ++++--
app/views/notes.py | 7 +++-
5 files changed, 49 insertions(+), 23 deletions(-)
diff --git a/app/but/jury_but.py b/app/but/jury_but.py
index 6861c8ecd..83b1d9ddb 100644
--- a/app/but/jury_but.py
+++ b/app/but/jury_but.py
@@ -902,6 +902,8 @@ class DecisionsProposeesAnnee(DecisionsProposees):
Efface même si étudiant DEM ou DEF.
Si à cheval ou only_one_sem, n'efface que les décisions UE et les
autorisations de passage du semestre d'origine du deca.
+
+ Dans tous les cas, efface les validations de l'année en cours.
(commite la session.)
"""
if only_one_sem or self.a_cheval:
@@ -916,8 +918,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
else:
for dec_ue in self.decisions_ues.values():
dec_ue.erase()
- for dec_rcue in self.decisions_rcue_by_niveau.values():
- dec_rcue.erase()
+
if self.formsemestre_impair:
ScolarAutorisationInscription.delete_autorisation_etud(
self.etud.id, self.formsemestre_impair.id
@@ -926,21 +927,27 @@ class DecisionsProposeesAnnee(DecisionsProposees):
ScolarAutorisationInscription.delete_autorisation_etud(
self.etud.id, self.formsemestre_pair.id
)
- validations = ApcValidationAnnee.query.filter_by(
+ # Efface les RCUEs
+ for dec_rcue in self.decisions_rcue_by_niveau.values():
+ dec_rcue.erase()
+
+ # Efface les validations concernant l'année BUT
+ # de ce semestre
+ validations = (
+ ApcValidationAnnee.query.filter_by(
etudid=self.etud.id,
- # XXX efface les validations émise depuis ce semestre
- # et pas toutes celles concernant cette l'année...
- # (utiliser formation_id pour changer cette politique)
- formsemestre_id=self.formsemestre.id,
ordre=self.annee_but,
)
- for validation in validations:
- db.session.delete(validation)
- Scolog.logdb(
- "jury_but",
- etudid=self.etud.id,
- msg=f"Validation année BUT{self.annee_but}: effacée",
- )
+ .join(Formation)
+ .filter_by(formation_code=self.formsemestre.formation.formation_code)
+ )
+ for validation in validations:
+ db.session.delete(validation)
+ Scolog.logdb(
+ "jury_but",
+ etudid=self.etud.id,
+ msg=f"Validation année BUT{self.annee_but}: effacée",
+ )
# Efface éventuelles validations de semestre
# (en principe inutilisées en BUT)
diff --git a/app/models/validations.py b/app/models/validations.py
index 8a1a8dd0d..7686d7897 100644
--- a/app/models/validations.py
+++ b/app/models/validations.py
@@ -79,17 +79,23 @@ class ScolarFormSemestreValidation(db.Model):
def html(self, detail=False) -> str:
"Affichage html"
if self.ue_id is not None:
- return f"""Validation de l'UE {self.ue.acronyme} de {self.ue.formation.acronyme}
+ return f"""Validation de l'UE {self.ue.acronyme}
+ {('parcours '
+ + ", ".join([p.code for p in self.ue.parcours]))
+ + ""
+ if self.ue.parcours else ""}
+ de {self.ue.formation.acronyme}
{("émise par " + self.formsemestre.html_link_status())
if self.formsemestre else ""}
- :{self.code}
+ : {self.code}
le {self.event_date.strftime("%d/%m/%Y")} à {self.event_date.strftime("%Hh%M")}
"""
else:
return f"""Validation du semestre S{
self.formsemestre.semestre_id if self.formsemestre else "?"}
- ({self.code}
- le {self.event_date.strftime("%d/%m/%Y")} à {self.event_date.strftime("%Hh%M")})
+ {self.formsemestre.html_link_status() if self.formsemestre else ""}
+ : {self.code}
+ le {self.event_date.strftime("%d/%m/%Y")} à {self.event_date.strftime("%Hh%M")}
"""
diff --git a/app/static/css/jury_delete_manual.css b/app/static/css/jury_delete_manual.css
index 6580e089f..6fa14f9ea 100644
--- a/app/static/css/jury_delete_manual.css
+++ b/app/static/css/jury_delete_manual.css
@@ -6,4 +6,8 @@ div.jury_decisions_list div {
div.jury_decisions_list form {
display: inline-block;
-}
\ No newline at end of file
+}
+
+span.parcours {
+ color:blueviolet;
+}
diff --git a/app/templates/but/formsemestre_validation_auto_but.j2 b/app/templates/but/formsemestre_validation_auto_but.j2
index db7de789a..27334aac5 100644
--- a/app/templates/but/formsemestre_validation_auto_but.j2
+++ b/app/templates/but/formsemestre_validation_auto_but.j2
@@ -26,9 +26,13 @@
En conséquence, saisir ensuite manuellement les décisions manquantes,
notamment sur les UEs en dessous de 10.
-
- Il est nécessaire de relire soigneusement les décisions à l'issue de cette procédure !
-
+
+
+
Ne jamais lancer ce calcul avant que toutes les notes ne soient saisies !
+ (verrouiller le semestre ensuite)
+
+
Il est nécessaire de relire soigneusement les décisions à l'issue de cette procédure !