1
0
forked from ScoDoc/ScoDoc

BUT: jury: validation des niveaux inférieurs. WIP

This commit is contained in:
Emmanuel Viennet 2023-06-15 08:49:05 +02:00
parent 73023b7806
commit e46ae76399
11 changed files with 414 additions and 94 deletions

View File

@ -104,7 +104,7 @@ class EtudCursusBUT:
self.parcour: ApcParcours = self.inscriptions[-1].parcour self.parcour: ApcParcours = self.inscriptions[-1].parcour
"Le parcours à valider: celui du DERNIER semestre suivi (peut être None)" "Le parcours à valider: celui du DERNIER semestre suivi (peut être None)"
self.niveaux_by_annee = {} self.niveaux_by_annee = {}
"{ annee : liste des niveaux à valider }" "{ annee:int : liste des niveaux à valider }"
self.niveaux: dict[int, ApcNiveau] = {} self.niveaux: dict[int, ApcNiveau] = {}
"cache les niveaux" "cache les niveaux"
for annee in (1, 2, 3): for annee in (1, 2, 3):
@ -118,21 +118,6 @@ class EtudCursusBUT:
self.niveaux.update( self.niveaux.update(
{niveau.id: niveau for niveau in self.niveaux_by_annee[annee]} {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 = {} self.validation_par_competence_et_annee = {}
"""{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }""" """{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
@ -146,7 +131,7 @@ class EtudCursusBUT:
# prend la "meilleure" validation # prend la "meilleure" validation
if (not previous_validation) or ( if (not previous_validation) or (
sco_codes.BUT_CODES_ORDERED[validation_rcue.code] 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][ self.validation_par_competence_et_annee[niveau.competence.id][
niveau.annee niveau.annee
@ -206,6 +191,23 @@ class EtudCursusBUT:
) )
return d 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: class FormSemestreCursusBUT:
"""L'état des étudiants d'un formsemestre dans leur cursus BUT """L'état des étudiants d'un formsemestre dans leur cursus BUT

View File

@ -58,6 +58,7 @@ DecisionsProposeesUE: décisions de jury sur une UE du BUT
DecisionsProposeesRCUE appelera .set_compensable() DecisionsProposeesRCUE appelera .set_compensable()
si on a la possibilité de la compenser dans le RCUE. si on a la possibilité de la compenser dans le RCUE.
""" """
from datetime import datetime
import html import html
from operator import attrgetter from operator import attrgetter
import re import re
@ -68,6 +69,7 @@ from flask import flash, g, url_for
from app import db from app import db
from app import log from app import log
from app.but.cursus_but import EtudCursusBUT
from app.comp.res_but import ResultatsSemestreBUT from app.comp.res_but import ResultatsSemestreBUT
from app.comp import res_sem from app.comp import res_sem
@ -92,6 +94,7 @@ from app.models.validations import ScolarFormSemestreValidation
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc import codes_cursus as sco_codes from app.scodoc import codes_cursus as sco_codes
from app.scodoc.codes_cursus import ( from app.scodoc.codes_cursus import (
code_rcue_validant,
BUT_CODES_ORDERED, BUT_CODES_ORDERED,
CODES_RCUE_VALIDES, CODES_RCUE_VALIDES,
CODES_UE_CAPITALISANTS, CODES_UE_CAPITALISANTS,
@ -275,6 +278,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
if self.formsemestre_impair is not None: if self.formsemestre_impair is not None:
self.validation = ApcValidationAnnee.query.filter_by( self.validation = ApcValidationAnnee.query.filter_by(
etudid=self.etud.id, etudid=self.etud.id,
formation_id=self.formsemestre.formation_id,
formsemestre_id=formsemestre_impair.id, formsemestre_id=formsemestre_impair.id,
ordre=self.annee_but, ordre=self.annee_but,
).first() ).first()
@ -755,6 +759,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
self.validation = ApcValidationAnnee( self.validation = ApcValidationAnnee(
etudid=self.etud.id, etudid=self.etud.id,
formsemestre=self.formsemestre_impair, formsemestre=self.formsemestre_impair,
formation_id=self.formsemestre.formation_id,
ordre=self.annee_but, ordre=self.annee_but,
annee_scolaire=self.annee_scolaire(), annee_scolaire=self.annee_scolaire(),
code=code, code=code,
@ -900,6 +905,9 @@ class DecisionsProposeesAnnee(DecisionsProposees):
) )
validations = ApcValidationAnnee.query.filter_by( validations = ApcValidationAnnee.query.filter_by(
etudid=self.etud.id, 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, formsemestre_id=self.formsemestre_impair.id,
ordre=self.annee_but, ordre=self.annee_but,
) )
@ -1035,6 +1043,9 @@ class DecisionsProposeesRCUE(DecisionsProposees):
): ):
super().__init__(etud=dec_prop_annee.etud) super().__init__(etud=dec_prop_annee.etud)
self.deca = dec_prop_annee self.deca = dec_prop_annee
self.referentiel_competence_id = (
self.deca.formsemestre.formation.referentiel_competence_id
)
self.rcue = rcue self.rcue = rcue
if rcue is None: # RCUE non dispo, eg un seul semestre if rcue is None: # RCUE non dispo, eg un seul semestre
self.codes = [] self.codes = []
@ -1139,7 +1150,8 @@ class DecisionsProposeesRCUE(DecisionsProposees):
dec_ue.record(sco_codes.ADJR) dec_ue.record(sco_codes.ADJR)
# Valide les niveaux inférieurs de la compétence (code ADSUP) # 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: if self.rcue.formsemestre_1 is not None:
sco_cache.invalidate_formsemestre( sco_cache.invalidate_formsemestre(
@ -1177,6 +1189,189 @@ class DecisionsProposeesRCUE(DecisionsProposees):
return f"{niveau_titre}-{ordre}" return f"{niveau_titre}-{ordre}"
return "" return ""
def valide_niveau_inferieur(self) -> None:
"""Appelé juste après la validation d'un RCUE.
*La validation des deux UE du niveau dune compétence emporte la validation de
lensemble 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): class DecisionsProposeesUE(DecisionsProposees):
"""Décisions de jury sur une UE du BUT """Décisions de jury sur une UE du BUT
@ -1383,23 +1578,29 @@ class BUTCursusEtud: # WIP TODO
for competence in self.competences_du_parcours() for competence in self.competences_du_parcours()
) )
def est_diplome(self) -> bool: def est_annee_validee(self, ordre: int) -> bool:
"""Vrai si BUT déjà validé""" """Vrai si l'année BUT ordre est validée"""
# vrai si la troisième année est validée # On cherche les validations d'annee avec le même
# On cherche les validations de 3ieme annee (ordre=3) avec le même référentiel # code formation que nous.
# de formation que nous.
return ( return (
ApcValidationAnnee.query.filter_by(etudid=self.etud.id, ordre=3) ApcValidationAnnee.query.filter_by(
.join(FormSemestre, FormSemestre.id == ApcValidationAnnee.formsemestre_id) etudid=self.etud.id,
.join(Formation, FormSemestre.formation_id == Formation.id) ordre=ordre,
formation_id=self.formsemestre.formation_id,
)
.join(Formation)
.filter( .filter(
Formation.referentiel_competence_id Formation.formation_code == self.formsemestre.formation.formation_code
== self.formsemestre.formation.referentiel_competence_id
) )
.count() .count()
> 0 > 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]: def competences_du_parcours(self) -> list[ApcCompetence]:
"""Construit liste des compétences du parcours, qui doivent être """Construit liste des compétences du parcours, qui doivent être
validées pour obtenir le diplôme. validées pour obtenir le diplôme.

View File

@ -307,9 +307,10 @@ class ResultatsSemestreBUT(NotesTableCompat):
return ues_ids return ues_ids
def etud_has_decision(self, etudid) -> bool: 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. 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( return bool(
super().etud_has_decision(etudid) super().etud_has_decision(etudid)

View File

@ -320,7 +320,12 @@ class ApcValidationAnnee(db.Model):
db.Integer, db.ForeignKey("notes_formsemestre.id"), nullable=True db.Integer, db.ForeignKey("notes_formsemestre.id"), nullable=True
) )
"le semestre IMPAIR (le 1er) de l'année" "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()) date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True) 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: Un dict avec les décisions de jury BUT enregistrées:
- decision_rcue : list[dict] - 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. Ne reprend pas les décisions d'UE, non spécifiques au BUT.
""" """
decisions = {} decisions = {}
@ -383,8 +388,7 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
etudid=etud.id, etudid=etud.id,
annee_scolaire=formsemestre.annee_scolaire(), annee_scolaire=formsemestre.annee_scolaire(),
) )
.join(ApcValidationAnnee.formsemestre) .join(Formation)
.join(FormSemestre.formation)
.filter(Formation.formation_code == formsemestre.formation.formation_code) .filter(Formation.formation_code == formsemestre.formation.formation_code)
.first() .first()
) )

View File

@ -859,7 +859,7 @@ class FormSemestre(db.Model):
.order_by(UniteEns.numero) .order_by(UniteEns.numero)
.all() .all()
) )
vals_annee = ( vals_annee = ( # issues de ce formsemestre seulement
ApcValidationAnnee.query.filter_by( ApcValidationAnnee.query.filter_by(
etudid=etudid, etudid=etudid,
annee_scolaire=self.annee_scolaire(), annee_scolaire=self.annee_scolaire(),

View File

@ -122,6 +122,7 @@ ABAN = "ABAN"
ABL = "ABL" ABL = "ABL"
ADM = "ADM" # moyenne gen., barres UE, assiduité: sem. validé ADM = "ADM" # moyenne gen., barres UE, assiduité: sem. validé
ADC = "ADC" # admis par compensation (eg moy(S1, S2) > 10) 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 ADJ = "ADJ" # admis par le jury
ADJR = "ADJR" # UE admise car son RCUE est ADJ ADJR = "ADJR" # UE admise car son RCUE est ADJ
ATT = "ATT" # ATT = "ATT" #
@ -162,6 +163,7 @@ CODES_EXPL = {
ADJ: "Validé par le Jury", ADJ: "Validé par le Jury",
ADJR: "UE validée car son RCUE est validé ADJ par le jury", ADJR: "UE validée car son RCUE est validé ADJ par le jury",
ADM: "Validé", 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)", 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)", 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)", 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_SEM_REO = {NAR} # reorientation
CODES_UE_VALIDES_DE_DROIT = {ADM, CMP} # validation "de droit" 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" "UE validée"
CODES_UE_CAPITALISANTS = {ADM} CODES_UE_CAPITALISANTS = {ADM}
"UE capitalisée" "UE capitalisée"
CODES_RCUE_VALIDES_DE_DROIT = {ADM, CMP} 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é" "Niveau RCUE validé"
# Pour le BUT: # 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} CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL}
BUT_BARRE_UE8 = 8.0 - NOTES_TOLERANCE BUT_BARRE_UE8 = 8.0 - NOTES_TOLERANCE
BUT_BARRE_UE = BUT_BARRE_RCUE = 10.0 - NOTES_TOLERANCE BUT_BARRE_UE = BUT_BARRE_RCUE = 10.0 - NOTES_TOLERANCE
@ -229,6 +232,7 @@ BUT_CODES_ORDERED = {
PASD: 50, PASD: 50,
PAS1NCI: 60, PAS1NCI: 60,
ADJR: 90, ADJR: 90,
ADSUP: 90,
ADJ: 100, ADJ: 100,
ADM: 100, ADM: 100,
} }
@ -249,6 +253,16 @@ def code_ue_validant(code: str) -> bool:
return code in CODES_UE_VALIDES 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 = { DEVENIR_EXPL = {
NEXT: "Passage au semestre suivant", NEXT: "Passage au semestre suivant",
REDOANNEE: "Redoublement année", REDOANNEE: "Redoublement année",

View File

@ -473,7 +473,10 @@ class ApoEtud(dict):
) )
def _but_load_validation_annuelle(self): 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 # le semestre impair de l'année scolaire
if self.cur_res.formsemestre.semestre_id % 2: if self.cur_res.formsemestre.semestre_id % 2:
formsemestre = self.cur_res.formsemestre formsemestre = self.cur_res.formsemestre
@ -490,7 +493,9 @@ class ApoEtud(dict):
return return
self.validation_annee_but: ApcValidationAnnee = ( self.validation_annee_but: ApcValidationAnnee = (
ApcValidationAnnee.query.filter_by( 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() ).first()
) )
self.is_nar = ( self.is_nar = (

View File

@ -66,6 +66,7 @@ from app.scodoc import sco_photos
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_pv_dict from app.scodoc import sco_pv_dict
# ------------------------------------------------------------------------------------ # ------------------------------------------------------------------------------------
def formsemestre_validation_etud_form( def formsemestre_validation_etud_form(
formsemestre_id=None, # required formsemestre_id=None, # required
@ -1063,8 +1064,6 @@ def formsemestre_validate_previous_ue(formsemestre_id, etudid):
"""Form. saisie UE validée hors ScoDoc """Form. saisie UE validée hors ScoDoc
(pour étudiants arrivant avec un UE antérieurement validée). (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] etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
formation: Formation = Formation.query.get_or_404(sem["formation_id"]) formation: Formation = Formation.query.get_or_404(sem["formation_id"])
@ -1087,8 +1086,8 @@ def formsemestre_validate_previous_ue(formsemestre_id, etudid):
<em>dans un semestre hors ScoDoc</em>.</p> <em>dans un semestre hors ScoDoc</em>.</p>
<p><b>Les UE validées dans ScoDoc sont déjà <p><b>Les UE validées dans ScoDoc sont déjà
automatiquement prises en compte</b>. Cette page n'est utile que pour les étudiants ayant automatiquement prises en compte</b>. Cette page n'est utile que pour les étudiants ayant
suivi un début de cursus dans <b>un autre établissement</b>, ou bien dans un semestre géré <b>sans suivi un début de cursus dans <b>un autre établissement</b>, ou bien dans un semestre géré
ScoDoc</b> et qui <b>redouble</b> ce semestre <b>sans ScoDoc</b> et qui <b>redouble</b> ce semestre
(<em>ne pas utiliser pour les semestres précédents !</em>). (<em>ne pas utiliser pour les semestres précédents !</em>).
</p> </p>
<p>Notez que l'UE est validée, avec enregistrement immédiat de la décision et <p>Notez que l'UE est validée, avec enregistrement immédiat de la décision et

View File

@ -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")

View File

@ -1,7 +1,7 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.4.83" SCOVERSION = "9.4.84"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"

View File

@ -1,122 +1,122 @@
# Tests unitaires jury BUT # Tests unitaires jury BUT
# Essais avec un BUT GCCD (GC-CD) et un parcours de S1 à S6 # 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. # terminent en S4 ou en S6 selon les parcours.
ReferentielCompetences: ReferentielCompetences:
filename: but-GCCD-05012022-081630.xml filename: but-GCCD-05012022-081630.xml
specialite: GCCD specialite: GCCD
Formation: Formation:
filename: scodoc_formation_BUT_GC-CD_v2.xml filename: scodoc_formation_BUT_GC-CD_v2.xml
# Association des UEs aux compétences: # Association des UEs aux compétences:
ues: ues:
# S1 tronc commun: # S1 tronc commun:
'UE1.1': "UE1.1":
annee: BUT1 annee: BUT1
competence: "Solutions Bâtiment" competence: "Solutions Bâtiment"
'UE1.2': "UE1.2":
annee: BUT1 annee: BUT1
competence: "Solutions TP" competence: "Solutions TP"
'UE1.3': "UE1.3":
annee: BUT1 annee: BUT1
competence: "Dimensionner" competence: "Dimensionner"
'UE1.4': "UE1.4":
annee: BUT1 annee: BUT1
competence: Organiser competence: Organiser
'UE1.5': "UE1.5":
annee: BUT1 annee: BUT1
competence: Piloter competence: Piloter
# S2 tronc commun: # S2 tronc commun:
'UE2.1': "UE2.1":
annee: BUT1 annee: BUT1
competence: "Solutions Bâtiment" competence: "Solutions Bâtiment"
'UE2.2': "UE2.2":
annee: BUT1 annee: BUT1
competence: "Solutions TP" competence: "Solutions TP"
'UE2.3': "UE2.3":
annee: BUT1 annee: BUT1
competence: "Dimensionner" competence: "Dimensionner"
'UE2.4': "UE2.4":
annee: BUT1 annee: BUT1
competence: Organiser competence: Organiser
'UE2.5': "UE2.5":
annee: BUT1 annee: BUT1
competence: Piloter competence: Piloter
# S3 : Tronc commun # S3 : Tronc commun
'UE3.1': "UE3.1":
annee: BUT2 annee: BUT2
competence: "Solutions Bâtiment" competence: "Solutions Bâtiment"
'UE3.2': "UE3.2":
annee: BUT2 annee: BUT2
competence: "Solutions TP" competence: "Solutions TP"
'UE3.3': "UE3.3":
annee: BUT2 annee: BUT2
competence: "Dimensionner" competence: "Dimensionner"
'UE3.4': "UE3.4":
annee: BUT2 annee: BUT2
competence: Organiser competence: Organiser
'UE3.5': "UE3.5":
annee: BUT2 annee: BUT2
competence: Piloter competence: Piloter
# S4 Tronc commun # S4 Tronc commun
'UE4.1': "UE4.1":
annee: BUT2 annee: BUT2
competence: "Solutions Bâtiment" competence: "Solutions Bâtiment"
'UE4.2': "UE4.2":
annee: BUT2 annee: BUT2
competence: "Solutions TP" competence: "Solutions TP"
'UE4.3': "UE4.3":
annee: BUT2 annee: BUT2
competence: "Dimensionner" competence: "Dimensionner"
'UE4.4': "UE4.4":
annee: BUT2 annee: BUT2
competence: Organiser competence: Organiser
'UE4.5': "UE4.5":
annee: BUT2 annee: BUT2
competence: Piloter competence: Piloter
# S5 Parcours BAT + TP # S5 Parcours BAT + TP
'UE5.1': # Parcours BAT seulement "UE5.1": # Parcours BAT seulement
annee: BUT3 annee: BUT3
parcours: BAT # + RAPEB, BEC parcours: BAT # + RAPEB, BEC
competence: "Solutions Bâtiment" competence: "Solutions Bâtiment"
'UE5.2': # Parcours TP seulement "UE5.2": # Parcours TP seulement
annee: BUT3 annee: BUT3
parcours: TP # + BEC parcours: TP # + BEC
competence: "Solutions TP" competence: "Solutions TP"
'UE5.3': "UE5.3":
annee: BUT3 annee: BUT3
parcours: [RAPEB, BEC] parcours: [RAPEB, BEC]
competence: "Dimensionner" competence: "Dimensionner"
'UE5.4': "UE5.4":
annee: BUT3 annee: BUT3
parcours: [BAT, TP] parcours: [BAT, TP]
competence: Organiser competence: Organiser
'UE5.5': "UE5.5":
annee: BUT3 annee: BUT3
parcours: [BAT, TP] parcours: [BAT, TP]
competence: Piloter competence: Piloter
# S6 Parcours BAT + TP # S6 Parcours BAT + TP
'UE6.1': # Parcours BAT seulement "UE6.1": # Parcours BAT seulement
annee: BUT3 annee: BUT3
parcours: BAT # + RAPEB, BEC parcours: BAT # + RAPEB, BEC
competence: "Solutions Bâtiment" competence: "Solutions Bâtiment"
'UE6.2': # Parcours TP seulement "UE6.2": # Parcours TP seulement
annee: BUT3 annee: BUT3
parcours: [TP,BEC] parcours: [TP, BEC]
competence: "Solutions TP" competence: "Solutions TP"
'UE6.3': "UE6.3":
annee: BUT3 annee: BUT3
parcours: [RAPEB,BEC] parcours: [RAPEB, BEC]
competence: "Dimensionner" competence: "Dimensionner"
'UE6.4': "UE6.4":
annee: BUT3 annee: BUT3
parcours: [BAT, TP] parcours: [BAT, TP]
competence: Organiser competence: Organiser
'UE6.5': "UE6.5":
annee: BUT3 annee: BUT3
parcours: [BAT,TP] parcours: [BAT, TP]
competence: Piloter competence: Piloter
modules_parcours: modules_parcours:
@ -126,8 +126,8 @@ Formation:
# - tous les module de S1 à S4 dans tous les parcours # - tous les module de S1 à S4 dans tous les parcours
# - SAE communes en S1 et S2 mais différenciées par parcours ensuite # - 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 # - 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" ] 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]" ] TP: ["R[1-4].*", "SAÉ [1-2]", "SAÉ *.TP.*", "R5.0[1-4]", "R5.0[89]"]
FormSemestres: FormSemestres:
# S1 et S2 avec les parcours BAT et TP: # S1 et S2 avec les parcours BAT et TP:
@ -135,32 +135,32 @@ FormSemestres:
idx: 1 idx: 1
date_debut: 2021-09-01 date_debut: 2021-09-01
date_fin: 2022-01-15 date_fin: 2022-01-15
codes_parcours: ['BAT', 'TP'] codes_parcours: ["BAT", "TP"]
S2: S2:
idx: 2 idx: 2
date_debut: 2022-01-15 date_debut: 2022-01-15
date_fin: 2022-06-30 date_fin: 2022-06-30
codes_parcours: ['BAT', 'TP'] codes_parcours: ["BAT", "TP"]
S3: S3:
idx: 3 idx: 3
date_debut: 2022-09-01 date_debut: 2022-09-01
date_fin: 2023-01-15 date_fin: 2023-01-15
codes_parcours: ['BAT', 'TP'] codes_parcours: ["BAT", "TP"]
S4: S4:
idx: 4 idx: 4
date_debut: 2023-01-16 date_debut: 2023-01-16
date_fin: 2023-06-30 date_fin: 2023-06-30
codes_parcours: ['BAT', 'TP'] codes_parcours: ["BAT", "TP"]
S5: S5:
idx: 5 idx: 5
date_debut: 2023-09-01 date_debut: 2023-09-01
date_fin: 2024-01-15 date_fin: 2024-01-15
codes_parcours: ['BAT', 'TP'] codes_parcours: ["BAT", "TP"]
S6: S6:
idx: 6 idx: 6
date_debut: 2024-01-16 date_debut: 2024-01-16
date_fin: 2024-06-30 date_fin: 2024-06-30
codes_parcours: ['BAT', 'TP'] codes_parcours: ["BAT", "TP"]
Etudiants: Etudiants:
A_ok: # Etudiant parcours BAT qui va tout valider directement A_ok: # Etudiant parcours BAT qui va tout valider directement
@ -171,10 +171,18 @@ Etudiants:
parcours: BAT parcours: BAT
notes_modules: notes_modules:
"R1.01": 11 # toutes UEs "R1.01": 11 # toutes UEs
"SAÉ 1-2": EXC
S2: S2:
parcours: BAT parcours: BAT
notes_modules: notes_modules:
"R2.01": 12 # toutes UEs "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: S3:
parcours: BAT parcours: BAT
notes_modules: notes_modules:
@ -186,7 +194,7 @@ Etudiants:
S5: S5:
parcours: BAT parcours: BAT
dispense_ues: ['UE5.2', 'UE5.3'] dispense_ues: ["UE5.2", "UE5.3"]
notes_modules: notes_modules:
"R5.01": 15 # toutes UE "R5.01": 15 # toutes UE
"SAÉ 5.BAT.01": 10 # UE5.1 "SAÉ 5.BAT.01": 10 # UE5.1
@ -202,6 +210,7 @@ Etudiants:
parcours: TP parcours: TP
notes_modules: notes_modules:
"R1.01": 11 # toutes UEs "R1.01": 11 # toutes UEs
"SAÉ 1-2": EXC
S2: S2:
parcours: TP parcours: TP
notes_modules: notes_modules:
@ -217,10 +226,32 @@ Etudiants:
S5: S5:
parcours: TP parcours: TP
dispense_ues: ['UE5.1', 'UE5.3'] dispense_ues: ["UE5.1", "UE5.3"]
notes_modules: notes_modules:
"R5.01": 15 # toutes UE "R5.01": 15 # toutes UE
"SAÉ 5.BAT.01": 10 # UE5.1 "SAÉ 5.BAT.01": 10 # UE5.1
"SAÉ 5.BAT.02": 11 # UE5.4 "SAÉ 5.BAT.02": 11 # UE5.4
S6: S6:
parcours: TP 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