2022-06-09 07:39:58 +02:00
|
|
|
|
##############################################################################
|
|
|
|
|
# ScoDoc
|
2023-01-02 13:16:27 +01:00
|
|
|
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
2022-06-09 07:39:58 +02:00
|
|
|
|
# See LICENSE
|
|
|
|
|
##############################################################################
|
|
|
|
|
|
|
|
|
|
"""Jury BUT: logique de gestion
|
2022-06-20 17:56:27 +02:00
|
|
|
|
|
|
|
|
|
Utilisation:
|
|
|
|
|
1) chargement page jury, pour un étudiant et un formsemestre BUT quelconque
|
|
|
|
|
- DecisionsProposeesAnnee(formsemestre)
|
|
|
|
|
cherche l'autre formsemestre de la même année scolaire (peut ne pas exister)
|
|
|
|
|
cherche les RCUEs de l'année (BUT1, 2, 3)
|
|
|
|
|
pour un redoublant, le RCUE peut considérer un formsemestre d'une année antérieure.
|
|
|
|
|
|
|
|
|
|
on instancie des DecisionsProposees pour les
|
|
|
|
|
différents éléments (UEs, RCUEs, Année, Diplôme)
|
|
|
|
|
Cela donne
|
|
|
|
|
- les codes possibles (dans .codes)
|
|
|
|
|
- le code actuel si une décision existe déjà (dans code_valide)
|
|
|
|
|
- pour les UEs, le rcue s'il y en a un)
|
|
|
|
|
|
|
|
|
|
2) Validation pour l'utilisateur (form)) => enregistrement code
|
|
|
|
|
- on vérifie que le code soumis est bien dans les codes possibles
|
|
|
|
|
- on enregistre la décision (dans ScolarFormSemestreValidation pour les UE,
|
|
|
|
|
ApcValidationRCUE pour les RCUE, et ApcValidationAnnee pour les années)
|
|
|
|
|
- Si RCUE validé, on déclenche d'éventuelles validations:
|
|
|
|
|
("La validation des deux UE du niveau d’une compétence emporte la validation
|
|
|
|
|
de l’ensemble des UE du niveau inférieur de cette même compétence.")
|
|
|
|
|
|
|
|
|
|
Les jurys de semestre BUT impairs entrainent systématiquement la génération d'une
|
|
|
|
|
autorisation d'inscription dans le semestre pair suivant: `ScolarAutorisationInscription`.
|
|
|
|
|
Les jurys de semestres pairs non (S2, S4, S6): il y a une décision sur l'année (ETP)
|
|
|
|
|
- autorisation en S_2n+1 (S3 ou S5) si: ADM, ADJ, PASD, PAS1CN
|
|
|
|
|
- autorisation en S2n-1 (S1, S3 ou S5) si: RED
|
|
|
|
|
- rien si pour les autres codes d'année.
|
|
|
|
|
|
|
|
|
|
Le formulaire permet de choisir des codes d'UE, RCUE et Année (ETP).
|
|
|
|
|
Mais normalement, les codes d'UE sont à choisir: les RCUE et l'année s'en déduisent.
|
|
|
|
|
Si l'utilisateur coche "décision manuelle", il peut alors choisir les codes RCUE et années.
|
|
|
|
|
|
|
|
|
|
La soumission du formulaire:
|
|
|
|
|
- etud, formation
|
|
|
|
|
- UEs: [(formsemestre, ue, code), ...]
|
|
|
|
|
- RCUE: [(formsemestre, ue, code), ...] le formsemestre est celui d'indice pair du niveau
|
|
|
|
|
(S2, S4 ou S6), il sera regoupé avec celui impair de la même année ou de la suivante.
|
|
|
|
|
- Année: [(formsemestre, code)]
|
|
|
|
|
|
|
|
|
|
DecisionsProposeesAnnee:
|
|
|
|
|
si 1/2 des rcue et aucun < 8 + pour S5 condition sur les UE de BUT1 et BUT2
|
|
|
|
|
=> charger les DecisionsProposeesRCUE
|
|
|
|
|
|
|
|
|
|
DecisionsProposeesRCUE: les RCUEs pour cette année
|
|
|
|
|
validable, compensable, ajourné. Utilise classe RegroupementCoherentUE
|
|
|
|
|
|
|
|
|
|
DecisionsProposeesUE: décisions de jury sur une UE du BUT
|
|
|
|
|
initialisation sans compensation (ue isolée), mais
|
|
|
|
|
DecisionsProposeesRCUE appelera .set_compensable()
|
|
|
|
|
si on a la possibilité de la compenser dans le RCUE.
|
2022-06-09 07:39:58 +02:00
|
|
|
|
"""
|
2023-06-15 08:49:05 +02:00
|
|
|
|
from datetime import datetime
|
2022-06-22 11:44:03 +02:00
|
|
|
|
import html
|
2022-06-09 07:39:58 +02:00
|
|
|
|
from operator import attrgetter
|
2022-06-22 11:44:03 +02:00
|
|
|
|
import re
|
2022-06-20 17:56:27 +02:00
|
|
|
|
from typing import Union
|
2022-06-09 07:39:58 +02:00
|
|
|
|
|
2022-08-17 18:15:48 +02:00
|
|
|
|
import numpy as np
|
2023-01-17 23:14:58 +01:00
|
|
|
|
from flask import flash, g, url_for
|
2022-06-26 09:37:50 +02:00
|
|
|
|
|
2022-06-22 11:44:03 +02:00
|
|
|
|
from app import db
|
2022-06-20 17:56:27 +02:00
|
|
|
|
from app import log
|
2023-06-15 21:53:05 +02:00
|
|
|
|
from app.but import cursus_but
|
2023-06-15 08:49:05 +02:00
|
|
|
|
from app.but.cursus_but import EtudCursusBUT
|
2022-06-09 07:39:58 +02:00
|
|
|
|
from app.comp.res_but import ResultatsSemestreBUT
|
2022-08-17 18:15:48 +02:00
|
|
|
|
from app.comp import res_sem
|
2022-06-20 17:56:27 +02:00
|
|
|
|
|
2022-06-09 07:39:58 +02:00
|
|
|
|
from app.models.but_refcomp import (
|
|
|
|
|
ApcAnneeParcours,
|
|
|
|
|
ApcCompetence,
|
|
|
|
|
ApcNiveau,
|
2022-06-20 17:56:27 +02:00
|
|
|
|
ApcParcours,
|
2022-06-09 07:39:58 +02:00
|
|
|
|
ApcParcoursNiveauCompetence,
|
|
|
|
|
)
|
2022-06-25 03:52:28 +02:00
|
|
|
|
from app.models import Scolog, ScolarAutorisationInscription
|
2022-06-20 17:56:27 +02:00
|
|
|
|
from app.models.but_validations import (
|
|
|
|
|
ApcValidationAnnee,
|
|
|
|
|
ApcValidationRCUE,
|
|
|
|
|
RegroupementCoherentUE,
|
|
|
|
|
)
|
2022-06-09 07:39:58 +02:00
|
|
|
|
from app.models.etudiants import Identite
|
|
|
|
|
from app.models.formations import Formation
|
2022-06-20 17:56:27 +02:00
|
|
|
|
from app.models.formsemestre import FormSemestre, FormSemestreInscription
|
2022-06-09 07:39:58 +02:00
|
|
|
|
from app.models.ues import UniteEns
|
2022-06-20 17:56:27 +02:00
|
|
|
|
from app.models.validations import ScolarFormSemestreValidation
|
2022-07-11 18:18:48 +02:00
|
|
|
|
from app.scodoc import sco_cache
|
2023-02-12 13:36:47 +01:00
|
|
|
|
from app.scodoc import codes_cursus as sco_codes
|
|
|
|
|
from app.scodoc.codes_cursus import (
|
2023-06-15 08:49:05 +02:00
|
|
|
|
code_rcue_validant,
|
2023-01-11 20:42:56 +01:00
|
|
|
|
BUT_CODES_ORDERED,
|
2023-01-11 15:09:12 +01:00
|
|
|
|
CODES_RCUE_VALIDES,
|
2023-02-08 17:56:08 +01:00
|
|
|
|
CODES_UE_CAPITALISANTS,
|
2023-01-11 15:09:12 +01:00
|
|
|
|
CODES_UE_VALIDES,
|
|
|
|
|
RED,
|
|
|
|
|
UE_STANDARD,
|
|
|
|
|
)
|
2022-06-09 07:39:58 +02:00
|
|
|
|
from app.scodoc import sco_utils as scu
|
2023-01-03 13:06:11 +01:00
|
|
|
|
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
|
2022-06-09 07:39:58 +02:00
|
|
|
|
|
|
|
|
|
|
2022-06-26 09:37:50 +02:00
|
|
|
|
class NoRCUEError(ScoValueError):
|
|
|
|
|
"""Erreur en cas de RCUE manquant"""
|
|
|
|
|
|
|
|
|
|
def __init__(self, deca: "DecisionsProposeesAnnee", ue: UniteEns):
|
|
|
|
|
if all(u.niveau_competence for u in deca.ues_pair):
|
|
|
|
|
warning_pair = ""
|
|
|
|
|
else:
|
|
|
|
|
warning_pair = """<div class="warning">certaines UE du semestre pair ne sont pas associées à un niveau de compétence</div>"""
|
|
|
|
|
if all(u.niveau_competence for u in deca.ues_impair):
|
|
|
|
|
warning_impair = ""
|
|
|
|
|
else:
|
|
|
|
|
warning_impair = """<div class="warning">certaines UE du semestre impair ne sont pas associées à un niveau de compétence</div>"""
|
|
|
|
|
msg = (
|
|
|
|
|
f"""<h3>Pas de RCUE pour l'UE {ue.acronyme}</h3>
|
|
|
|
|
{warning_impair}
|
|
|
|
|
{warning_pair}
|
|
|
|
|
<div><b>UE {ue.acronyme}</b>: niveau {html.escape(str(ue.niveau_competence))}</div>
|
|
|
|
|
<div><b>UEs impaires:</b> {html.escape(', '.join(str(u.niveau_competence or "pas de niveau")
|
|
|
|
|
for u in deca.ues_impair))}
|
|
|
|
|
</div>
|
|
|
|
|
"""
|
|
|
|
|
+ deca.infos()
|
|
|
|
|
)
|
|
|
|
|
super().__init__(msg)
|
|
|
|
|
|
|
|
|
|
|
2022-06-20 17:56:27 +02:00
|
|
|
|
class DecisionsProposees:
|
|
|
|
|
"""Une décision de jury proposé, constituée d'une liste de codes et d'une explication.
|
|
|
|
|
Super-classe, spécialisée pour les UE, les RCUE, les années et le diplôme.
|
2022-06-09 07:39:58 +02:00
|
|
|
|
|
2022-12-09 04:24:32 +01:00
|
|
|
|
validation : None ou une instance d'une classe avec un champ code
|
2022-06-20 17:56:27 +02:00
|
|
|
|
ApcValidationRCUE, ApcValidationAnnee ou ScolarFormSemestreValidation
|
|
|
|
|
"""
|
2022-06-09 07:39:58 +02:00
|
|
|
|
|
|
|
|
|
# Codes toujours proposés sauf si include_communs est faux:
|
2022-06-20 17:56:27 +02:00
|
|
|
|
codes_communs = [
|
|
|
|
|
sco_codes.RAT,
|
|
|
|
|
sco_codes.DEF,
|
|
|
|
|
sco_codes.ABAN,
|
|
|
|
|
sco_codes.DEM,
|
|
|
|
|
sco_codes.UEBSL,
|
|
|
|
|
]
|
2022-06-09 07:39:58 +02:00
|
|
|
|
|
2022-06-20 17:56:27 +02:00
|
|
|
|
def __init__(
|
|
|
|
|
self,
|
|
|
|
|
etud: Identite = None,
|
|
|
|
|
code: Union[str, list[str]] = None,
|
|
|
|
|
explanation="",
|
|
|
|
|
code_valide=None,
|
|
|
|
|
include_communs=True,
|
|
|
|
|
):
|
|
|
|
|
self.etud = etud
|
|
|
|
|
self.codes = []
|
|
|
|
|
"Les codes attribuables par ce jury"
|
2022-06-09 07:39:58 +02:00
|
|
|
|
if include_communs:
|
2022-06-21 11:21:17 +02:00
|
|
|
|
self.codes = self.codes_communs.copy()
|
2022-06-09 07:39:58 +02:00
|
|
|
|
if isinstance(code, list):
|
2022-06-21 11:21:17 +02:00
|
|
|
|
self.codes = code + self.codes
|
2022-06-09 07:39:58 +02:00
|
|
|
|
elif code is not None:
|
2022-06-21 11:21:17 +02:00
|
|
|
|
self.codes = [code] + self.codes
|
2022-06-22 11:44:03 +02:00
|
|
|
|
self.validation = None
|
|
|
|
|
"Validation enregistrée"
|
2022-06-20 17:56:27 +02:00
|
|
|
|
self.code_valide: str = code_valide
|
2022-06-22 11:44:03 +02:00
|
|
|
|
"Code décision actuel enregistré"
|
2022-07-18 22:17:27 +02:00
|
|
|
|
# S'assure que le code enregistré est toujours présent dans le menu
|
|
|
|
|
if self.code_valide and self.code_valide not in self.codes:
|
|
|
|
|
self.codes.append(self.code_valide)
|
2022-06-20 17:56:27 +02:00
|
|
|
|
self.explanation: str = explanation
|
2022-06-21 11:21:17 +02:00
|
|
|
|
"Explication à afficher à côté de la décision"
|
2022-06-22 11:44:03 +02:00
|
|
|
|
self.recorded = False
|
|
|
|
|
"true si la décision vient d'être enregistrée"
|
2022-06-09 07:39:58 +02:00
|
|
|
|
|
|
|
|
|
def __repr__(self) -> str:
|
2022-06-20 17:56:27 +02:00
|
|
|
|
return f"""<{self.__class__.__name__} valid={self.code_valide
|
2023-01-09 22:46:27 +01:00
|
|
|
|
} codes={self.codes} explanation={self.explanation}>"""
|
2022-06-09 07:39:58 +02:00
|
|
|
|
|
|
|
|
|
|
2022-06-20 17:56:27 +02:00
|
|
|
|
class DecisionsProposeesAnnee(DecisionsProposees):
|
|
|
|
|
"""Décisions de jury sur une année (ETP) du BUT
|
2022-06-09 07:39:58 +02:00
|
|
|
|
|
2022-06-20 17:56:27 +02:00
|
|
|
|
Le texte:
|
|
|
|
|
La poursuite d'études dans un semestre pair d’une même année est de droit
|
|
|
|
|
pour tout étudiant. La poursuite d’études dans un semestre impair est
|
|
|
|
|
possible si et seulement si l’étudiant a obtenu :
|
|
|
|
|
- la moyenne à plus de la moitié des regroupements cohérents d’UE;
|
|
|
|
|
- et une moyenne égale ou supérieure à 8 sur 20 à chaque RCUE.
|
|
|
|
|
La poursuite d'études dans le semestre 5 nécessite de plus la validation
|
|
|
|
|
de toutes les UE des semestres 1 et 2 dans les conditions de validation
|
|
|
|
|
des points 4.3 (moy_ue >= 10) et 4.4 (compensation rcue), ou par décision
|
|
|
|
|
de jury.
|
2022-06-09 07:39:58 +02:00
|
|
|
|
"""
|
2022-06-20 17:56:27 +02:00
|
|
|
|
|
|
|
|
|
# Codes toujours proposés sauf si include_communs est faux:
|
|
|
|
|
codes_communs = [
|
|
|
|
|
sco_codes.RAT,
|
|
|
|
|
sco_codes.ABAN,
|
|
|
|
|
sco_codes.ABL,
|
2022-06-30 20:13:30 +02:00
|
|
|
|
sco_codes.ATJ,
|
2022-06-20 17:56:27 +02:00
|
|
|
|
sco_codes.DEF,
|
|
|
|
|
sco_codes.DEM,
|
|
|
|
|
sco_codes.EXCLU,
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
|
self,
|
|
|
|
|
etud: Identite,
|
|
|
|
|
formsemestre: FormSemestre,
|
|
|
|
|
):
|
2022-12-25 21:43:22 +01:00
|
|
|
|
assert formsemestre.formation.is_apc()
|
2023-01-03 13:06:11 +01:00
|
|
|
|
if formsemestre.formation.referentiel_competence is None:
|
|
|
|
|
raise ScoNoReferentielCompetences(formation=formsemestre.formation)
|
2022-06-20 17:56:27 +02:00
|
|
|
|
super().__init__(etud=etud)
|
2022-12-20 19:20:00 +01:00
|
|
|
|
self.formsemestre = formsemestre
|
|
|
|
|
"le formsemestre utilisé pour construire ce deca"
|
2022-06-29 18:16:37 +02:00
|
|
|
|
self.formsemestre_id = formsemestre.id
|
2022-09-30 16:20:51 +02:00
|
|
|
|
"l'id du formsemestre utilisé pour construire ce deca"
|
2022-06-20 17:56:27 +02:00
|
|
|
|
formsemestre_impair, formsemestre_pair = self.comp_formsemestres(formsemestre)
|
|
|
|
|
assert (
|
|
|
|
|
(formsemestre_pair is None)
|
|
|
|
|
or (formsemestre_impair is None)
|
|
|
|
|
or (
|
|
|
|
|
((formsemestre_pair.semestre_id - formsemestre_impair.semestre_id) == 1)
|
|
|
|
|
and (
|
|
|
|
|
formsemestre_pair.formation.referentiel_competence_id
|
|
|
|
|
== formsemestre_impair.formation.referentiel_competence_id
|
|
|
|
|
)
|
|
|
|
|
)
|
2022-06-09 07:39:58 +02:00
|
|
|
|
)
|
2022-12-20 19:20:00 +01:00
|
|
|
|
# Si les années scolaires sont distinctes, on est "à cheval"
|
|
|
|
|
self.a_cheval = (
|
|
|
|
|
formsemestre_impair
|
|
|
|
|
and formsemestre_pair
|
|
|
|
|
and formsemestre_impair.annee_scolaire()
|
|
|
|
|
!= formsemestre_pair.annee_scolaire()
|
|
|
|
|
)
|
|
|
|
|
"vrai si on groupe deux semestres d'années scolaires différentes"
|
2022-11-26 12:58:21 +01:00
|
|
|
|
# Si on part d'un semestre IMPAIR, il n'y aura pas de décision année proposée
|
2022-11-27 19:07:42 +01:00
|
|
|
|
# (mais on pourra évidemment valider des UE et même des RCUE)
|
|
|
|
|
self.jury_annuel: bool = formsemestre.semestre_id in (2, 4, 6)
|
2023-02-18 18:49:52 +01:00
|
|
|
|
"vrai si jury de fin d'année scolaire (sem. pair, propose code annuel)"
|
2022-06-20 17:56:27 +02:00
|
|
|
|
|
|
|
|
|
self.formsemestre_impair = formsemestre_impair
|
2022-12-20 19:20:00 +01:00
|
|
|
|
"le 1er semestre du groupement (S1, S3, S5)"
|
2022-06-20 17:56:27 +02:00
|
|
|
|
self.formsemestre_pair = formsemestre_pair
|
2022-12-20 19:20:00 +01:00
|
|
|
|
"le second formsemestre (S2, S4, S6), de la même année scolaire ou d'une précédente"
|
2022-07-04 23:50:55 +02:00
|
|
|
|
formsemestre_last = formsemestre_pair or formsemestre_impair
|
2022-12-20 19:20:00 +01:00
|
|
|
|
"le formsemestre le plus avancé (en indice de semestre) dans le groupement"
|
2022-07-04 23:50:55 +02:00
|
|
|
|
|
|
|
|
|
self.annee_but = (formsemestre_last.semestre_id + 1) // 2
|
2022-06-20 17:56:27 +02:00
|
|
|
|
"le rang de l'année dans le BUT: 1, 2, 3"
|
|
|
|
|
assert self.annee_but in (1, 2, 3)
|
2022-06-26 09:37:50 +02:00
|
|
|
|
self.rcues_annee = []
|
2022-12-20 19:20:00 +01:00
|
|
|
|
"""RCUEs de l'année
|
|
|
|
|
(peuvent concerner l'année scolaire antérieur pour les redoublants
|
|
|
|
|
avec UE capitalisées)
|
|
|
|
|
"""
|
2022-07-04 23:50:55 +02:00
|
|
|
|
self.inscription_etat = etud.inscription_etat(formsemestre_last.id)
|
2022-10-04 21:56:10 +02:00
|
|
|
|
"état de l'inscription dans le semestre le plus avancé (pair si année complète)"
|
|
|
|
|
self.inscription_etat_pair = (
|
|
|
|
|
etud.inscription_etat(formsemestre_pair.id)
|
|
|
|
|
if formsemestre_pair is not None
|
|
|
|
|
else None
|
|
|
|
|
)
|
|
|
|
|
self.inscription_etat_impair = (
|
|
|
|
|
etud.inscription_etat(formsemestre_impair.id)
|
|
|
|
|
if formsemestre_impair is not None
|
|
|
|
|
else None
|
|
|
|
|
)
|
2022-07-04 23:50:55 +02:00
|
|
|
|
|
2022-06-23 06:33:03 +02:00
|
|
|
|
if self.formsemestre_impair is not None:
|
|
|
|
|
self.validation = ApcValidationAnnee.query.filter_by(
|
|
|
|
|
etudid=self.etud.id,
|
2023-06-15 08:49:05 +02:00
|
|
|
|
formation_id=self.formsemestre.formation_id,
|
2022-06-23 06:33:03 +02:00
|
|
|
|
ordre=self.annee_but,
|
|
|
|
|
).first()
|
|
|
|
|
else:
|
|
|
|
|
self.validation = None
|
2022-06-20 17:56:27 +02:00
|
|
|
|
if self.validation is not None:
|
|
|
|
|
self.code_valide = self.validation.code
|
|
|
|
|
self.parcour = None
|
|
|
|
|
"Le parcours considéré (celui du semestre pair, ou à défaut impair)"
|
2022-06-24 08:59:09 +02:00
|
|
|
|
if self.formsemestre_pair is not None:
|
|
|
|
|
self.res_pair: ResultatsSemestreBUT = res_sem.load_formsemestre_results(
|
|
|
|
|
self.formsemestre_pair
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
self.res_pair = None
|
|
|
|
|
if self.formsemestre_impair is not None:
|
|
|
|
|
self.res_impair: ResultatsSemestreBUT = res_sem.load_formsemestre_results(
|
|
|
|
|
self.formsemestre_impair
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
self.res_impair = None
|
|
|
|
|
|
2022-06-20 17:56:27 +02:00
|
|
|
|
self.ues_impair, self.ues_pair = self.compute_ues_annee() # pylint: disable=all
|
2022-06-21 11:21:17 +02:00
|
|
|
|
self.decisions_ues = {
|
2022-07-04 23:50:55 +02:00
|
|
|
|
ue.id: DecisionsProposeesUE(
|
2022-10-04 21:56:10 +02:00
|
|
|
|
etud, formsemestre_impair, ue, self.inscription_etat_impair
|
2022-07-04 23:50:55 +02:00
|
|
|
|
)
|
2022-06-21 11:21:17 +02:00
|
|
|
|
for ue in self.ues_impair
|
|
|
|
|
}
|
|
|
|
|
"{ue_id : DecisionsProposeesUE} pour toutes les UE de l'année"
|
|
|
|
|
self.decisions_ues.update(
|
|
|
|
|
{
|
2022-07-04 23:50:55 +02:00
|
|
|
|
ue.id: DecisionsProposeesUE(
|
2022-10-04 21:56:10 +02:00
|
|
|
|
etud, formsemestre_pair, ue, self.inscription_etat_pair
|
2022-07-04 23:50:55 +02:00
|
|
|
|
)
|
2022-06-21 11:21:17 +02:00
|
|
|
|
for ue in self.ues_pair
|
|
|
|
|
}
|
|
|
|
|
)
|
2022-06-20 17:56:27 +02:00
|
|
|
|
self.rcues_annee = self.compute_rcues_annee()
|
|
|
|
|
|
2022-06-22 14:09:08 +02:00
|
|
|
|
formation = (
|
|
|
|
|
self.formsemestre_impair.formation
|
|
|
|
|
if self.formsemestre_impair
|
|
|
|
|
else self.formsemestre_pair.formation
|
|
|
|
|
)
|
2022-12-20 04:16:38 +01:00
|
|
|
|
(
|
|
|
|
|
parcours,
|
|
|
|
|
niveaux_by_parcours,
|
|
|
|
|
) = formation.referentiel_competence.get_niveaux_by_parcours(
|
2023-04-04 12:02:09 +02:00
|
|
|
|
self.annee_but, [self.parcour] if self.parcour else None
|
2022-12-20 04:16:38 +01:00
|
|
|
|
)
|
|
|
|
|
self.niveaux_competences = niveaux_by_parcours["TC"] + (
|
|
|
|
|
niveaux_by_parcours[self.parcour.id] if self.parcour else []
|
|
|
|
|
)
|
|
|
|
|
"""liste non triée des niveaux de compétences associés à cette année pour cet étudiant.
|
|
|
|
|
= niveaux du tronc commun + niveau du parcours de l'étudiant.
|
|
|
|
|
"""
|
2022-06-21 11:21:17 +02:00
|
|
|
|
self.decisions_rcue_by_niveau = self.compute_decisions_niveaux()
|
|
|
|
|
"les décisions rcue associées aux niveau_id"
|
2022-06-24 16:57:44 +02:00
|
|
|
|
self.dec_rcue_by_ue = self._dec_rcue_by_ue()
|
2022-06-28 11:18:12 +02:00
|
|
|
|
"{ ue_id : DecisionsProposeesRCUE } pour toutes les UE associées à un niveau"
|
2022-06-21 11:21:17 +02:00
|
|
|
|
self.nb_competences = len(self.niveaux_competences)
|
|
|
|
|
"le nombre de niveaux de compétences à valider cette année"
|
2022-06-28 11:18:12 +02:00
|
|
|
|
rcues_avec_niveau = [d.rcue for d in self.decisions_rcue_by_niveau.values()]
|
2022-06-20 17:56:27 +02:00
|
|
|
|
self.nb_validables = len(
|
2022-06-28 11:18:12 +02:00
|
|
|
|
[rcue for rcue in rcues_avec_niveau if rcue.est_validable()]
|
2022-06-09 07:39:58 +02:00
|
|
|
|
)
|
2022-06-21 11:21:17 +02:00
|
|
|
|
"le nombre de comp. validables (éventuellement par compensation)"
|
2022-09-26 20:57:52 +02:00
|
|
|
|
self.nb_rcue_valides = len(
|
|
|
|
|
[rcue for rcue in rcues_avec_niveau if rcue.code_valide()]
|
|
|
|
|
)
|
|
|
|
|
"le nombre de niveaux validés (déc. jury prise)"
|
2022-06-20 17:56:27 +02:00
|
|
|
|
self.nb_rcues_under_8 = len(
|
2022-06-28 11:18:12 +02:00
|
|
|
|
[rcue for rcue in rcues_avec_niveau if not rcue.est_suffisant()]
|
2022-06-09 07:39:58 +02:00
|
|
|
|
)
|
2022-06-21 11:21:17 +02:00
|
|
|
|
"le nb de comp. sous la barre de 8/20"
|
2022-07-04 23:50:55 +02:00
|
|
|
|
# année ADM si toutes RCUE validées (sinon PASD) et non DEM ou DEF
|
|
|
|
|
self.admis = (self.nb_validables == self.nb_competences) and (
|
|
|
|
|
self.inscription_etat == scu.INSCRIT
|
|
|
|
|
)
|
2022-09-25 16:30:02 +02:00
|
|
|
|
"vrai si l'année est réussie, tous niveaux validables ou validés par le jury"
|
2022-06-21 11:21:17 +02:00
|
|
|
|
self.valide_moitie_rcue = self.nb_validables > (self.nb_competences // 2)
|
2023-01-08 19:36:05 +01:00
|
|
|
|
"Vrai si plus de la moitié des RCUE validables"
|
2022-06-21 11:21:17 +02:00
|
|
|
|
self.passage_de_droit = self.valide_moitie_rcue and (self.nb_rcues_under_8 == 0)
|
2023-01-08 19:36:05 +01:00
|
|
|
|
"Vrai si peut passer dans l'année BUT suivante: plus de la moitié validables et tous > 8"
|
2023-06-15 21:53:05 +02:00
|
|
|
|
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: <b>{
|
|
|
|
|
', '.join(ue.acronyme for ue in ues_but1_non_validees)
|
|
|
|
|
}</b>. """
|
|
|
|
|
if ues_but1_non_validees
|
|
|
|
|
else ""
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
# pas inscrit dans le semestre courant ???
|
|
|
|
|
self.passage_de_droit = False
|
2022-06-20 17:56:27 +02:00
|
|
|
|
|
2023-06-15 21:53:05 +02:00
|
|
|
|
# Enfin calcule les codes des UEs:
|
2022-06-21 11:21:17 +02:00
|
|
|
|
for dec_ue in self.decisions_ues.values():
|
|
|
|
|
dec_ue.compute_codes()
|
|
|
|
|
|
2022-06-20 17:56:27 +02:00
|
|
|
|
# Reste à attribuer ADM, ADJ, PASD, PAS1NCI, RED, NAR
|
2022-12-25 01:35:18 +01:00
|
|
|
|
plural = self.nb_validables > 1
|
2023-06-15 21:53:05 +02:00
|
|
|
|
explanation += f"""{self.nb_validables} niveau{"x" if plural else ""} validable{
|
2022-12-25 01:35:18 +01:00
|
|
|
|
"s" if plural else ""} sur {self.nb_competences}"""
|
2022-06-25 02:59:43 +02:00
|
|
|
|
if self.admis:
|
2022-06-20 17:56:27 +02:00
|
|
|
|
self.codes = [sco_codes.ADM] + self.codes
|
2022-12-18 19:42:33 +01:00
|
|
|
|
# elif not self.jury_annuel:
|
|
|
|
|
# self.codes = [] # pas de décision annuelle sur semestres impairs
|
2022-07-04 23:50:55 +02:00
|
|
|
|
elif self.inscription_etat != scu.INSCRIT:
|
|
|
|
|
self.codes = [
|
|
|
|
|
sco_codes.DEM
|
|
|
|
|
if self.inscription_etat == scu.DEMISSION
|
|
|
|
|
else sco_codes.DEF,
|
|
|
|
|
# propose aussi d'autres codes, au cas où...
|
|
|
|
|
sco_codes.DEM
|
|
|
|
|
if self.inscription_etat != scu.DEMISSION
|
|
|
|
|
else sco_codes.DEF,
|
|
|
|
|
sco_codes.ABAN,
|
|
|
|
|
sco_codes.ABL,
|
|
|
|
|
sco_codes.EXCLU,
|
|
|
|
|
]
|
2023-06-15 21:53:05 +02:00
|
|
|
|
explanation = ""
|
2022-06-21 11:21:17 +02:00
|
|
|
|
elif self.passage_de_droit:
|
2022-06-20 17:56:27 +02:00
|
|
|
|
self.codes = [sco_codes.PASD, sco_codes.ADJ] + self.codes
|
2022-06-21 11:21:17 +02:00
|
|
|
|
elif self.valide_moitie_rcue: # mais au moins 1 rcue insuffisante
|
2022-06-26 09:52:50 +02:00
|
|
|
|
self.codes = [
|
|
|
|
|
sco_codes.RED,
|
|
|
|
|
sco_codes.NAR,
|
|
|
|
|
sco_codes.PAS1NCI,
|
|
|
|
|
sco_codes.ADJ,
|
|
|
|
|
] + self.codes
|
2023-06-15 21:53:05 +02:00
|
|
|
|
explanation += f" et {self.nb_rcues_under_8} < 8"
|
2022-06-20 17:56:27 +02:00
|
|
|
|
else:
|
2022-06-26 09:59:43 +02:00
|
|
|
|
self.codes = [
|
|
|
|
|
sco_codes.RED,
|
|
|
|
|
sco_codes.NAR,
|
|
|
|
|
sco_codes.PAS1NCI,
|
|
|
|
|
sco_codes.ADJ,
|
2022-09-25 22:50:56 +02:00
|
|
|
|
sco_codes.PASD, # voir #488 (discutable, conventions locales)
|
2022-06-26 09:59:43 +02:00
|
|
|
|
] + self.codes
|
2023-06-15 21:53:05 +02:00
|
|
|
|
explanation += f""" et {self.nb_rcues_under_8} niveau{'x' if self.nb_rcues_under_8 > 1 else ''} < 8"""
|
2023-01-11 15:09:12 +01:00
|
|
|
|
|
2022-09-29 22:09:19 +02:00
|
|
|
|
# Si l'un des semestres est extérieur, propose ADM
|
|
|
|
|
if (
|
2022-09-29 22:39:26 +02:00
|
|
|
|
self.formsemestre_impair and self.formsemestre_impair.modalite == "EXT"
|
|
|
|
|
) or (self.formsemestre_pair and self.formsemestre_pair.modalite == "EXT"):
|
2022-09-29 22:09:19 +02:00
|
|
|
|
self.codes.insert(0, sco_codes.ADM)
|
2023-06-15 16:50:22 +02:00
|
|
|
|
# Si validée par niveau supérieur:
|
|
|
|
|
if self.code_valide == sco_codes.ADSUP:
|
|
|
|
|
self.codes.insert(0, sco_codes.ADSUP)
|
2023-06-15 21:53:05 +02:00
|
|
|
|
self.explanation = f"<div>{explanation}</div>"
|
2023-01-11 15:09:12 +01:00
|
|
|
|
messages = self.descr_pb_coherence()
|
|
|
|
|
if messages:
|
|
|
|
|
self.explanation += (
|
|
|
|
|
'<div class="warning">'
|
|
|
|
|
+ '</div><div class="warning">'.join(messages)
|
|
|
|
|
+ "</div>"
|
|
|
|
|
)
|
2023-05-15 17:20:38 +02:00
|
|
|
|
|
|
|
|
|
# WIP TODO XXX def get_moyenne_annuelle(self)
|
2022-06-20 17:56:27 +02:00
|
|
|
|
|
|
|
|
|
def infos(self) -> str:
|
2022-11-24 00:11:59 +01:00
|
|
|
|
"""informations, for debugging purpose."""
|
|
|
|
|
text = f"""<b>DecisionsProposeesAnnee</b>
|
2022-06-26 09:37:50 +02:00
|
|
|
|
<ul>
|
|
|
|
|
<li>Etudiant: <a href="{url_for("scolar.ficheEtud",
|
|
|
|
|
scodoc_dept=g.scodoc_dept, etudid=self.etud.id)
|
|
|
|
|
}">{self.etud.nomprenom}</a>
|
|
|
|
|
</li>
|
2022-06-20 17:56:27 +02:00
|
|
|
|
"""
|
2022-11-24 00:11:59 +01:00
|
|
|
|
for formsemestre, title in (
|
|
|
|
|
(self.formsemestre_impair, "formsemestre_impair"),
|
|
|
|
|
(self.formsemestre_pair, "formsemestre_pair"),
|
|
|
|
|
):
|
|
|
|
|
text += f"<li>{title}:"
|
|
|
|
|
if formsemestre is not None:
|
|
|
|
|
text += f"""
|
|
|
|
|
<a href="{url_for("notes.formsemestre_status",
|
|
|
|
|
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
|
|
|
|
|
}">{html.escape(str(formsemestre))}</a>
|
|
|
|
|
<ul>
|
|
|
|
|
<li>Formation: <a href="{url_for('notes.ue_table',
|
|
|
|
|
scodoc_dept=g.scodoc_dept,
|
|
|
|
|
semestre_idx=formsemestre.semestre_id,
|
|
|
|
|
formation_id=formsemestre.formation.id)}">
|
|
|
|
|
{formsemestre.formation.to_html()} ({
|
|
|
|
|
formsemestre.formation.id})</a>
|
|
|
|
|
</li>
|
|
|
|
|
</ul>
|
|
|
|
|
"""
|
|
|
|
|
else:
|
|
|
|
|
text += " aucun."
|
|
|
|
|
text += "</li>"
|
|
|
|
|
|
|
|
|
|
text += f"""
|
|
|
|
|
<li>RCUEs: {html.escape(str(self.rcues_annee))}</li>
|
|
|
|
|
<li>nb_competences: {getattr(self, "nb_competences", "-")}</li>
|
|
|
|
|
<li>nb_validables: {getattr(self, "nb_validables", "-")}</li>
|
|
|
|
|
<li>codes: {self.codes}</li>
|
|
|
|
|
<li>explanation: {self.explanation}</li>
|
|
|
|
|
</ul>
|
|
|
|
|
"""
|
|
|
|
|
return text
|
2022-06-20 17:56:27 +02:00
|
|
|
|
|
2022-06-22 11:44:03 +02:00
|
|
|
|
def annee_scolaire(self) -> int:
|
|
|
|
|
"L'année de début de l'année scolaire"
|
|
|
|
|
formsemestre = self.formsemestre_impair or self.formsemestre_pair
|
|
|
|
|
return formsemestre.annee_scolaire()
|
|
|
|
|
|
|
|
|
|
def annee_scolaire_str(self) -> str:
|
|
|
|
|
"L'année scolaire, eg '2021 - 2022'"
|
|
|
|
|
formsemestre = self.formsemestre_impair or self.formsemestre_pair
|
|
|
|
|
return formsemestre.annee_scolaire_str().replace(" ", "")
|
2022-06-21 18:33:43 +02:00
|
|
|
|
|
2022-06-20 17:56:27 +02:00
|
|
|
|
def comp_formsemestres(
|
|
|
|
|
self, formsemestre: FormSemestre
|
|
|
|
|
) -> tuple[FormSemestre, FormSemestre]:
|
2022-11-24 00:11:59 +01:00
|
|
|
|
"""Les deux formsemestres du niveau auquel appartient formsemestre.
|
|
|
|
|
Complète le niveau avec le formsemestre antérieur le plus récent.
|
|
|
|
|
L'"autre" formsemestre peut ainsi appartenir à l'année scolaire
|
|
|
|
|
antérieure (redoublants).
|
|
|
|
|
-> S_impair, S_pair
|
|
|
|
|
"""
|
2022-06-30 09:37:41 +02:00
|
|
|
|
if not formsemestre.formation.is_apc(): # garde fou
|
|
|
|
|
return None, None
|
2022-06-20 17:56:27 +02:00
|
|
|
|
if formsemestre.semestre_id % 2 == 0:
|
|
|
|
|
other_semestre_id = formsemestre.semestre_id - 1
|
|
|
|
|
else:
|
|
|
|
|
other_semestre_id = formsemestre.semestre_id + 1
|
2022-11-24 00:11:59 +01:00
|
|
|
|
|
2022-06-20 17:56:27 +02:00
|
|
|
|
other_formsemestre = None
|
|
|
|
|
for inscr in self.etud.formsemestre_inscriptions:
|
|
|
|
|
if (
|
|
|
|
|
# Même spécialité BUT (tolère ainsi des variantes de formation)
|
|
|
|
|
(
|
|
|
|
|
inscr.formsemestre.formation.referentiel_competence
|
|
|
|
|
== formsemestre.formation.referentiel_competence
|
|
|
|
|
)
|
|
|
|
|
# L'autre semestre
|
|
|
|
|
and (inscr.formsemestre.semestre_id == other_semestre_id)
|
2022-11-24 00:11:59 +01:00
|
|
|
|
# Antérieur
|
|
|
|
|
and inscr.formsemestre.date_debut < formsemestre.date_debut
|
|
|
|
|
# Et plus le récent possible
|
|
|
|
|
and (
|
|
|
|
|
(other_formsemestre is None)
|
|
|
|
|
or (other_formsemestre.date_debut < inscr.formsemestre.date_debut)
|
|
|
|
|
)
|
2022-06-20 17:56:27 +02:00
|
|
|
|
):
|
|
|
|
|
other_formsemestre = inscr.formsemestre
|
|
|
|
|
if formsemestre.semestre_id % 2 == 0:
|
|
|
|
|
return other_formsemestre, formsemestre
|
|
|
|
|
return formsemestre, other_formsemestre
|
|
|
|
|
|
|
|
|
|
def compute_ues_annee(self) -> list[list[UniteEns], list[UniteEns]]:
|
|
|
|
|
"""UEs à valider cette année pour cet étudiant, selon son parcours.
|
2022-12-20 04:16:38 +01:00
|
|
|
|
Affecte self.parcour suivant l'inscription de l'étudiant et
|
|
|
|
|
ramène [ listes des UE du semestre impair, liste des UE du semestre pair ].
|
2022-06-20 17:56:27 +02:00
|
|
|
|
"""
|
|
|
|
|
ues_sems = []
|
2023-04-04 12:02:09 +02:00
|
|
|
|
for formsemestre, res in (
|
2022-06-24 08:59:09 +02:00
|
|
|
|
(self.formsemestre_impair, self.res_impair),
|
|
|
|
|
(self.formsemestre_pair, self.res_pair),
|
|
|
|
|
):
|
2022-06-30 09:37:41 +02:00
|
|
|
|
if (formsemestre is None) or (not formsemestre.formation.is_apc()):
|
2022-06-20 17:56:27 +02:00
|
|
|
|
ues = []
|
|
|
|
|
else:
|
2022-09-30 16:20:51 +02:00
|
|
|
|
parcour, ues = list_ue_parcour_etud(formsemestre, self.etud, res)
|
|
|
|
|
if parcour is not None:
|
|
|
|
|
self.parcour = parcour
|
2022-06-20 17:56:27 +02:00
|
|
|
|
ues_sems.append(ues)
|
|
|
|
|
return ues_sems
|
|
|
|
|
|
|
|
|
|
def check_ues_ready_jury(self) -> list[str]:
|
|
|
|
|
"""Vérifie que les toutes les UEs (hors bonus) de l'année sont
|
|
|
|
|
bien associées à des niveaux de compétences.
|
|
|
|
|
Renvoie liste vide si ok, sinon liste de message explicatifs
|
|
|
|
|
"""
|
|
|
|
|
messages = []
|
|
|
|
|
for ue in self.ues_impair + self.ues_pair:
|
|
|
|
|
if ue.niveau_competence is None:
|
|
|
|
|
messages.append(
|
|
|
|
|
f"UE {ue.acronyme} non associée à un niveau de compétence"
|
|
|
|
|
)
|
|
|
|
|
if ue.semestre_idx is None:
|
|
|
|
|
messages.append(
|
|
|
|
|
f"UE {ue.acronyme} n'a pas d'indice de semestre dans la formation"
|
2022-06-09 07:39:58 +02:00
|
|
|
|
)
|
2022-06-20 17:56:27 +02:00
|
|
|
|
return messages
|
|
|
|
|
|
|
|
|
|
def compute_rcues_annee(self) -> list[RegroupementCoherentUE]:
|
|
|
|
|
"""Liste des regroupements d'UE à considérer cette année.
|
2022-12-20 19:20:00 +01:00
|
|
|
|
On peut avoir un RCUE à cheval sur plusieurs années (redoublants avec UE capitalisées).
|
2022-06-20 17:56:27 +02:00
|
|
|
|
Si on n'a pas les deux semestres, aucun RCUE.
|
|
|
|
|
"""
|
|
|
|
|
if self.formsemestre_pair is None or self.formsemestre_impair is None:
|
|
|
|
|
return []
|
|
|
|
|
rcues_annee = []
|
|
|
|
|
ues_impair_sans_rcue = {ue.id for ue in self.ues_impair}
|
|
|
|
|
for ue_pair in self.ues_pair:
|
|
|
|
|
rcue = None
|
|
|
|
|
for ue_impair in self.ues_impair:
|
2022-12-20 19:20:00 +01:00
|
|
|
|
if self.a_cheval:
|
|
|
|
|
# l'UE paire DOIT être capitalisée pour être utilisée
|
|
|
|
|
if (
|
|
|
|
|
self.decisions_ues[ue_pair.id].code_valide
|
2023-02-08 17:56:08 +01:00
|
|
|
|
not in CODES_UE_CAPITALISANTS
|
2022-12-20 19:20:00 +01:00
|
|
|
|
):
|
|
|
|
|
continue # ignore cette UE antérieure non capitalisée
|
2023-01-17 16:34:36 +01:00
|
|
|
|
# et l'UE impaire doit être actuellement meilleure que
|
|
|
|
|
# celle éventuellement capitalisée
|
2023-01-27 10:55:05 +01:00
|
|
|
|
if (
|
|
|
|
|
self.decisions_ues[ue_impair.id].ue_status
|
|
|
|
|
and self.decisions_ues[ue_impair.id].ue_status["is_capitalized"]
|
|
|
|
|
):
|
2023-01-17 16:34:36 +01:00
|
|
|
|
continue # ignore cette UE car capitalisée et actuelle moins bonne
|
2022-06-20 17:56:27 +02:00
|
|
|
|
if ue_pair.niveau_competence_id == ue_impair.niveau_competence_id:
|
|
|
|
|
rcue = RegroupementCoherentUE(
|
|
|
|
|
self.etud,
|
|
|
|
|
self.formsemestre_impair,
|
2022-11-24 00:11:59 +01:00
|
|
|
|
self.decisions_ues[ue_impair.id],
|
2022-06-20 17:56:27 +02:00
|
|
|
|
self.formsemestre_pair,
|
2022-11-24 00:11:59 +01:00
|
|
|
|
self.decisions_ues[ue_pair.id],
|
2022-07-04 23:50:55 +02:00
|
|
|
|
self.inscription_etat,
|
2022-06-20 17:56:27 +02:00
|
|
|
|
)
|
2022-06-23 13:51:56 +02:00
|
|
|
|
ues_impair_sans_rcue.discard(ue_impair.id)
|
2022-06-20 17:56:27 +02:00
|
|
|
|
break
|
2023-01-13 12:17:59 +01:00
|
|
|
|
# if rcue is None and not self.a_cheval:
|
|
|
|
|
# raise NoRCUEError(deca=self, ue=ue_pair)
|
2022-12-20 19:20:00 +01:00
|
|
|
|
if rcue is not None:
|
|
|
|
|
rcues_annee.append(rcue)
|
|
|
|
|
# Si jury annuel (pas à cheval), on doit avoir tous les RCUEs:
|
2023-01-13 12:17:59 +01:00
|
|
|
|
# if len(ues_impair_sans_rcue) > 0 and not self.a_cheval:
|
|
|
|
|
# ue = UniteEns.query.get(ues_impair_sans_rcue.pop())
|
|
|
|
|
# raise NoRCUEError(deca=self, ue=ue)
|
2022-06-20 17:56:27 +02:00
|
|
|
|
return rcues_annee
|
|
|
|
|
|
2022-06-21 11:21:17 +02:00
|
|
|
|
def compute_decisions_niveaux(self) -> dict[int, "DecisionsProposeesRCUE"]:
|
2022-06-24 16:57:44 +02:00
|
|
|
|
"""Pour chaque niveau de compétence de cette année, construit
|
2023-01-11 13:37:02 +01:00
|
|
|
|
le DecisionsProposeesRCUE, ou None s'il n'y en a pas
|
2022-06-24 16:57:44 +02:00
|
|
|
|
(ne devrait pas arriver car compute_rcues_annee vérifie déjà cela).
|
2023-01-11 13:37:02 +01:00
|
|
|
|
|
|
|
|
|
Appelé à la construction du deca, donc avant décisions manuelles.
|
2022-06-21 11:21:17 +02:00
|
|
|
|
Return: { niveau_id : DecisionsProposeesRCUE }
|
|
|
|
|
"""
|
|
|
|
|
# Retrouve le RCUE associé à chaque niveau
|
|
|
|
|
rc_niveaux = []
|
|
|
|
|
for niveau in self.niveaux_competences:
|
|
|
|
|
rcue = None
|
|
|
|
|
for rc in self.rcues_annee:
|
|
|
|
|
if rc.ue_1.niveau_competence_id == niveau.id:
|
|
|
|
|
rcue = rc
|
|
|
|
|
break
|
2022-06-22 14:09:08 +02:00
|
|
|
|
if rcue is not None:
|
2022-07-04 23:50:55 +02:00
|
|
|
|
dec_rcue = DecisionsProposeesRCUE(self, rcue, self.inscription_etat)
|
2022-06-22 14:09:08 +02:00
|
|
|
|
rc_niveaux.append((dec_rcue, niveau.id))
|
|
|
|
|
# prévient les UE concernées :-)
|
|
|
|
|
self.decisions_ues[dec_rcue.rcue.ue_1.id].set_rcue(dec_rcue.rcue)
|
|
|
|
|
self.decisions_ues[dec_rcue.rcue.ue_2.id].set_rcue(dec_rcue.rcue)
|
2022-06-21 11:21:17 +02:00
|
|
|
|
# Ordonne par numéro d'UE
|
|
|
|
|
rc_niveaux.sort(key=lambda x: x[0].rcue.ue_1.numero)
|
|
|
|
|
decisions_rcue_by_niveau = {x[1]: x[0] for x in rc_niveaux}
|
|
|
|
|
return decisions_rcue_by_niveau
|
|
|
|
|
|
2022-06-24 16:57:44 +02:00
|
|
|
|
def _dec_rcue_by_ue(self) -> dict[int, "DecisionsProposeesRCUE"]:
|
|
|
|
|
"""construit dict { ue_id : DecisionsProposeesRCUE }
|
|
|
|
|
à partir de self.decisions_rcue_by_niveau"""
|
|
|
|
|
d = {}
|
|
|
|
|
for dec_rcue in self.decisions_rcue_by_niveau.values():
|
|
|
|
|
d[dec_rcue.rcue.ue_1.id] = dec_rcue
|
|
|
|
|
d[dec_rcue.rcue.ue_2.id] = dec_rcue
|
|
|
|
|
return d
|
|
|
|
|
|
2023-02-18 18:49:52 +01:00
|
|
|
|
def formsemestre_ects(self) -> float:
|
|
|
|
|
"ECTS validés dans le formsemestre de départ du deca"
|
|
|
|
|
ues = self.ues_impair if self.formsemestre.semestre_id % 2 else self.ues_pair
|
|
|
|
|
return sum(
|
|
|
|
|
[
|
|
|
|
|
self.decisions_ues[ue.id].ects_acquis()
|
|
|
|
|
for ue in ues
|
|
|
|
|
if ue.id in self.decisions_ues
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
|
2023-01-26 14:49:04 +01:00
|
|
|
|
def next_semestre_ids(self, code: str) -> set[int]:
|
|
|
|
|
"""Les indices des semestres dans lequels l'étudiant est autorisé
|
|
|
|
|
à poursuivre après le semestre courant.
|
|
|
|
|
"""
|
|
|
|
|
# La poursuite d'études dans un semestre pair d’une même année
|
2023-02-19 13:28:59 +01:00
|
|
|
|
# est de droit pour tout étudiant.
|
|
|
|
|
# Pas de redoublements directs de S_impair vers S_impair
|
|
|
|
|
# (pourront être traités manuellement)
|
|
|
|
|
if (
|
|
|
|
|
self.formsemestre.semestre_id % 2
|
|
|
|
|
) and self.formsemestre.semestre_id < sco_codes.CursusBUT.NB_SEM:
|
|
|
|
|
return {self.formsemestre.semestre_id + 1}
|
2023-01-26 14:49:04 +01:00
|
|
|
|
# La poursuite d’études dans un semestre impair est possible si
|
|
|
|
|
# et seulement si l’étudiant a obtenu :
|
|
|
|
|
# - la moyenne à plus de la moitié des regroupements cohérents d’UE ;
|
|
|
|
|
# - et une moyenne égale ou supérieure à 8 sur 20 à chaque RCUE.
|
|
|
|
|
#
|
|
|
|
|
# La condition a paru trop stricte à de nombreux collègues.
|
|
|
|
|
# ScoDoc ne contraint donc pas à la respecter strictement.
|
|
|
|
|
# Si le code est dans BUT_CODES_PASSAGE (ADM, ADJ, PASD, PAS1NCI, ATJ),
|
|
|
|
|
# autorise à passer dans le semestre suivant
|
2023-02-19 13:28:59 +01:00
|
|
|
|
ids = set()
|
2023-01-26 14:49:04 +01:00
|
|
|
|
if (
|
|
|
|
|
self.jury_annuel
|
|
|
|
|
and code in sco_codes.BUT_CODES_PASSAGE
|
2023-02-19 13:28:59 +01:00
|
|
|
|
and self.formsemestre.semestre_id < sco_codes.CursusBUT.NB_SEM
|
2022-06-25 03:52:28 +02:00
|
|
|
|
):
|
2023-01-26 14:49:04 +01:00
|
|
|
|
ids.add(self.formsemestre.semestre_id + 1)
|
|
|
|
|
|
|
|
|
|
if code == RED:
|
|
|
|
|
ids.add(
|
|
|
|
|
self.formsemestre.semestre_id - (self.formsemestre.semestre_id + 1) % 2
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return ids
|
2022-06-22 11:44:03 +02:00
|
|
|
|
|
|
|
|
|
def record_form(self, form: dict):
|
|
|
|
|
"""Enregistre les codes de jury en base
|
2023-01-11 13:37:02 +01:00
|
|
|
|
à partir d'un dict représentant le formulaire jury BUT:
|
2022-06-22 11:44:03 +02:00
|
|
|
|
form dict:
|
|
|
|
|
- 'code_ue_1896' : 'AJ' code pour l'UE id 1896
|
|
|
|
|
- 'code_rcue_6" : 'ADM' code pour le RCUE du niveau 6
|
|
|
|
|
- 'code_annee' : 'ADM' code pour l'année
|
|
|
|
|
|
|
|
|
|
Si les code_rcue et le code_annee ne sont pas fournis,
|
2022-06-25 17:00:00 +02:00
|
|
|
|
et qu'il n'y en a pas déjà, enregistre ceux par défaut.
|
2022-06-22 11:44:03 +02:00
|
|
|
|
"""
|
2022-07-11 18:18:48 +02:00
|
|
|
|
log("jury_but.DecisionsProposeesAnnee.record_form")
|
2023-01-11 13:37:02 +01:00
|
|
|
|
code_annee = None
|
|
|
|
|
codes_rcues = [] # [ (dec_rcue, code), ... ]
|
|
|
|
|
codes_ues = [] # [ (dec_ue, code), ... ]
|
|
|
|
|
for key in form:
|
|
|
|
|
code = form[key]
|
|
|
|
|
# Codes d'UE
|
|
|
|
|
m = re.match(r"^code_ue_(\d+)$", key)
|
|
|
|
|
if m:
|
|
|
|
|
ue_id = int(m.group(1))
|
|
|
|
|
dec_ue = self.decisions_ues.get(ue_id)
|
|
|
|
|
if not dec_ue:
|
|
|
|
|
raise ScoValueError(f"UE invalide ue_id={ue_id}")
|
|
|
|
|
codes_ues.append((dec_ue, code))
|
|
|
|
|
else:
|
|
|
|
|
# Codes de RCUE
|
|
|
|
|
m = re.match(r"^code_rcue_(\d+)$", key)
|
2022-06-22 11:44:03 +02:00
|
|
|
|
if m:
|
2023-01-11 13:37:02 +01:00
|
|
|
|
niveau_id = int(m.group(1))
|
|
|
|
|
dec_rcue = self.decisions_rcue_by_niveau.get(niveau_id)
|
|
|
|
|
if not dec_rcue:
|
|
|
|
|
raise ScoValueError(f"RCUE invalide niveau_id={niveau_id}")
|
|
|
|
|
codes_rcues.append((dec_rcue, code))
|
|
|
|
|
elif key == "code_annee":
|
|
|
|
|
# Code annuel
|
|
|
|
|
code_annee = code
|
|
|
|
|
|
2023-01-11 15:09:12 +01:00
|
|
|
|
with sco_cache.DeferredSemCacheManager():
|
|
|
|
|
# Enregistre les codes, dans l'ordre UE, RCUE, Année
|
|
|
|
|
for dec_ue, code in codes_ues:
|
|
|
|
|
dec_ue.record(code)
|
|
|
|
|
for dec_rcue, code in codes_rcues:
|
|
|
|
|
dec_rcue.record(code)
|
2023-06-15 16:50:22 +02:00
|
|
|
|
self.record(code_annee, mark_recorded=False)
|
2023-02-19 13:28:59 +01:00
|
|
|
|
self.record_autorisation_inscription(code_annee)
|
2023-01-11 15:09:12 +01:00
|
|
|
|
self.record_all()
|
2023-06-15 16:50:22 +02:00
|
|
|
|
self.recorded = True
|
2023-01-11 13:37:02 +01:00
|
|
|
|
|
2023-01-11 15:09:12 +01:00
|
|
|
|
db.session.commit()
|
2022-06-22 11:44:03 +02:00
|
|
|
|
|
2023-06-15 16:50:22 +02:00
|
|
|
|
def record(self, code: str, no_overwrite=False, mark_recorded: bool = True) -> bool:
|
2022-06-25 17:00:00 +02:00
|
|
|
|
"""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é.
|
2022-10-04 21:56:10 +02:00
|
|
|
|
Si l'étudiant est DEM ou DEF, ne fait rien.
|
2023-06-15 16:50:22 +02:00
|
|
|
|
Si mark_recorded est vrai, positionne self.recorded
|
2022-06-25 17:00:00 +02:00
|
|
|
|
"""
|
2022-10-04 21:56:10 +02:00
|
|
|
|
if self.inscription_etat != scu.INSCRIT:
|
2023-01-23 11:38:47 +01:00
|
|
|
|
return False
|
2022-06-22 14:09:08 +02:00
|
|
|
|
if code and not code in self.codes:
|
2022-06-22 11:44:03 +02:00
|
|
|
|
raise ScoValueError(
|
|
|
|
|
f"code annee <tt>{html.escape(code)}</tt> invalide pour formsemestre {html.escape(self.formsemestre)}"
|
|
|
|
|
)
|
2023-01-11 15:09:12 +01:00
|
|
|
|
|
2023-01-26 14:49:04 +01:00
|
|
|
|
if code != self.code_valide and (self.code_valide is None or not no_overwrite):
|
|
|
|
|
# Enregistrement du code annuel BUT
|
|
|
|
|
if self.validation:
|
|
|
|
|
db.session.delete(self.validation)
|
|
|
|
|
db.session.commit()
|
2022-06-25 03:52:28 +02:00
|
|
|
|
if code is None:
|
2023-01-26 14:49:04 +01:00
|
|
|
|
self.validation = None
|
|
|
|
|
else:
|
|
|
|
|
self.validation = ApcValidationAnnee(
|
2022-06-25 03:52:28 +02:00
|
|
|
|
etudid=self.etud.id,
|
2023-01-26 14:49:04 +01:00
|
|
|
|
formsemestre=self.formsemestre_impair,
|
2023-06-15 08:49:05 +02:00
|
|
|
|
formation_id=self.formsemestre.formation_id,
|
2023-01-26 14:49:04 +01:00
|
|
|
|
ordre=self.annee_but,
|
|
|
|
|
annee_scolaire=self.annee_scolaire(),
|
|
|
|
|
code=code,
|
2022-06-25 03:52:28 +02:00
|
|
|
|
)
|
2023-01-26 14:49:04 +01:00
|
|
|
|
db.session.add(self.validation)
|
|
|
|
|
log(f"Recording {self}: {code}")
|
|
|
|
|
Scolog.logdb(
|
|
|
|
|
method="jury_but",
|
|
|
|
|
etudid=self.etud.id,
|
|
|
|
|
msg=f"Validation année BUT{self.annee_but}: {code}",
|
|
|
|
|
)
|
2023-06-15 16:50:22 +02:00
|
|
|
|
if mark_recorded:
|
|
|
|
|
self.recorded = True
|
2023-02-19 13:28:59 +01:00
|
|
|
|
self.invalidate_formsemestre_cache()
|
|
|
|
|
return True
|
2023-01-26 14:49:04 +01:00
|
|
|
|
|
2023-02-19 13:28:59 +01:00
|
|
|
|
def record_autorisation_inscription(self, code: str):
|
|
|
|
|
"""Autorisation d'inscription dans semestre suivant"""
|
|
|
|
|
if self.inscription_etat != scu.INSCRIT:
|
|
|
|
|
# les dem et DEF ne continuent jamais
|
|
|
|
|
return
|
2023-01-26 14:49:04 +01:00
|
|
|
|
ScolarAutorisationInscription.delete_autorisation_etud(
|
|
|
|
|
etudid=self.etud.id,
|
|
|
|
|
origin_formsemestre_id=self.formsemestre.id,
|
|
|
|
|
)
|
|
|
|
|
for next_semestre_id in self.next_semestre_ids(code):
|
|
|
|
|
ScolarAutorisationInscription.autorise_etud(
|
|
|
|
|
self.etud.id,
|
|
|
|
|
self.formsemestre.formation.formation_code,
|
|
|
|
|
self.formsemestre.id,
|
|
|
|
|
next_semestre_id,
|
|
|
|
|
)
|
2022-06-25 03:52:28 +02:00
|
|
|
|
|
2022-07-11 18:18:48 +02:00
|
|
|
|
def invalidate_formsemestre_cache(self):
|
|
|
|
|
"invalide le résultats des deux formsemestres"
|
|
|
|
|
if self.formsemestre_impair is not None:
|
|
|
|
|
sco_cache.invalidate_formsemestre(
|
|
|
|
|
formsemestre_id=self.formsemestre_impair.id
|
|
|
|
|
)
|
|
|
|
|
if self.formsemestre_pair is not None:
|
|
|
|
|
sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre_pair.id)
|
2022-06-22 11:44:03 +02:00
|
|
|
|
|
2023-02-10 23:43:16 +01:00
|
|
|
|
def has_notes_en_attente(self) -> bool:
|
|
|
|
|
"Vrai si l'étudiant a au moins une note en attente dans le semestre origine de ce deca"
|
|
|
|
|
res = (
|
|
|
|
|
self.res_pair
|
|
|
|
|
if self.formsemestre_pair
|
|
|
|
|
and (self.formsemestre.id == self.formsemestre_pair.id)
|
|
|
|
|
else self.res_impair
|
|
|
|
|
)
|
|
|
|
|
return res and self.etud.id in res.get_etudids_attente()
|
|
|
|
|
|
2023-01-23 11:38:47 +01:00
|
|
|
|
def record_all(
|
|
|
|
|
self, no_overwrite: bool = True, only_validantes: bool = False
|
|
|
|
|
) -> bool:
|
2022-10-04 21:56:10 +02:00
|
|
|
|
"""Enregistre les codes qui n'ont pas été spécifiés par le formulaire,
|
2023-01-11 20:42:56 +01:00
|
|
|
|
et sont donc en mode "automatique".
|
|
|
|
|
- Si "à cheval", ne modifie pas les codes UE de l'année scolaire précédente.
|
|
|
|
|
- Pour les RCUE: n'enregistre que si la nouvelle décision est plus favorable que l'ancienne.
|
2023-01-23 11:38:47 +01:00
|
|
|
|
|
2023-02-10 23:43:16 +01:00
|
|
|
|
Si only_validantes, n'enregistre que des décisions "validantes" de droit: ADM ou CMP,
|
|
|
|
|
et seulement si l'étudiant n'a pas de notes en ATTente.
|
2023-01-23 11:38:47 +01:00
|
|
|
|
|
|
|
|
|
Return: True si au moins un code modifié et enregistré.
|
2022-10-04 21:56:10 +02:00
|
|
|
|
"""
|
2023-01-23 11:38:47 +01:00
|
|
|
|
modif = False
|
2023-02-10 23:43:16 +01:00
|
|
|
|
# Vérification notes en attente dans formsemestre origine
|
|
|
|
|
if only_validantes and self.has_notes_en_attente():
|
|
|
|
|
return False
|
|
|
|
|
|
2023-01-23 11:38:47 +01:00
|
|
|
|
# Toujours valider dans l'ordre UE, RCUE, Année
|
2023-01-11 20:42:56 +01:00
|
|
|
|
annee_scolaire = self.formsemestre.annee_scolaire()
|
|
|
|
|
# UEs
|
|
|
|
|
for dec_ue in self.decisions_ues.values():
|
|
|
|
|
if (
|
|
|
|
|
not dec_ue.recorded
|
|
|
|
|
) and dec_ue.formsemestre.annee_scolaire() == annee_scolaire:
|
2022-06-22 14:09:08 +02:00
|
|
|
|
# rappel: le code par défaut est en tête
|
2023-01-11 20:42:56 +01:00
|
|
|
|
code = dec_ue.codes[0] if dec_ue.codes else None
|
2023-01-23 11:38:47 +01:00
|
|
|
|
if (not only_validantes) or code in sco_codes.CODES_UE_VALIDES_DE_DROIT:
|
|
|
|
|
# enregistre le code jury seulement s'il n'y a pas déjà de code
|
|
|
|
|
# (no_overwrite=True) sauf en mode test yaml
|
|
|
|
|
modif |= dec_ue.record(code, no_overwrite=no_overwrite)
|
|
|
|
|
# RCUE :
|
2023-01-11 20:42:56 +01:00
|
|
|
|
for dec_rcue in self.decisions_rcue_by_niveau.values():
|
|
|
|
|
code = dec_rcue.codes[0] if dec_rcue.codes else None
|
2023-01-23 11:38:47 +01:00
|
|
|
|
if (
|
|
|
|
|
(not dec_rcue.recorded)
|
|
|
|
|
and ( # enregistre seulement si pas déjà validé "mieux"
|
|
|
|
|
(not dec_rcue.validation)
|
|
|
|
|
or BUT_CODES_ORDERED.get(dec_rcue.validation.code, 0)
|
|
|
|
|
< BUT_CODES_ORDERED.get(code, 0)
|
|
|
|
|
)
|
|
|
|
|
and ( # décision validante de droit ?
|
|
|
|
|
(
|
|
|
|
|
(not only_validantes)
|
|
|
|
|
or code in sco_codes.CODES_RCUE_VALIDES_DE_DROIT
|
|
|
|
|
)
|
|
|
|
|
)
|
2023-01-11 20:42:56 +01:00
|
|
|
|
):
|
2023-01-23 11:38:47 +01:00
|
|
|
|
modif |= dec_rcue.record(code, no_overwrite=no_overwrite)
|
2023-01-11 20:42:56 +01:00
|
|
|
|
# Année:
|
|
|
|
|
if not self.recorded:
|
|
|
|
|
# rappel: le code par défaut est en tête
|
|
|
|
|
code = self.codes[0] if self.codes else None
|
|
|
|
|
# enregistre le code jury seulement s'il n'y a pas déjà de code
|
|
|
|
|
# (no_overwrite=True) sauf en mode test yaml
|
2023-01-23 11:38:47 +01:00
|
|
|
|
if (
|
|
|
|
|
not only_validantes
|
|
|
|
|
) or code in sco_codes.CODES_ANNEE_BUT_VALIDES_DE_DROIT:
|
|
|
|
|
modif |= self.record(code, no_overwrite=no_overwrite)
|
2023-02-19 13:28:59 +01:00
|
|
|
|
self.record_autorisation_inscription(code)
|
2023-01-23 11:38:47 +01:00
|
|
|
|
return modif
|
2022-06-22 11:44:03 +02:00
|
|
|
|
|
2022-09-30 16:20:51 +02:00
|
|
|
|
def erase(self, only_one_sem=False):
|
2022-06-25 14:18:34 +02:00
|
|
|
|
"""Efface les décisions de jury de cet étudiant
|
|
|
|
|
pour cette année: décisions d'UE, de RCUE, d'année,
|
|
|
|
|
et autorisations d'inscription émises.
|
2022-10-04 21:56:10 +02:00
|
|
|
|
Efface même si étudiant DEM ou DEF.
|
2023-01-08 19:36:05 +01:00
|
|
|
|
Si à cheval, n'efface que pour le semestre d'origine du deca.
|
2023-01-30 19:30:25 +01:00
|
|
|
|
(commite la session.)
|
2022-06-25 14:18:34 +02:00
|
|
|
|
"""
|
2023-01-08 19:36:05 +01:00
|
|
|
|
if only_one_sem or self.a_cheval:
|
2022-09-30 16:20:51 +02:00
|
|
|
|
# N'efface que les autorisations venant de ce semestre,
|
|
|
|
|
# et les validations de ses UEs
|
2022-06-25 14:18:34 +02:00
|
|
|
|
ScolarAutorisationInscription.delete_autorisation_etud(
|
2022-09-30 16:20:51 +02:00
|
|
|
|
self.etud.id, self.formsemestre_id
|
2022-06-25 14:18:34 +02:00
|
|
|
|
)
|
2022-09-30 16:20:51 +02:00
|
|
|
|
for dec_ue in self.decisions_ues.values():
|
|
|
|
|
if dec_ue.formsemestre.id == self.formsemestre_id:
|
|
|
|
|
dec_ue.erase()
|
|
|
|
|
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
|
|
|
|
|
)
|
|
|
|
|
if self.formsemestre_pair:
|
|
|
|
|
ScolarAutorisationInscription.delete_autorisation_etud(
|
|
|
|
|
self.etud.id, self.formsemestre_pair.id
|
|
|
|
|
)
|
|
|
|
|
validations = ApcValidationAnnee.query.filter_by(
|
|
|
|
|
etudid=self.etud.id,
|
2023-06-15 08:49:05 +02:00
|
|
|
|
# XXX efface les validations émise depuis ce semestre
|
|
|
|
|
# et pas toutes celles concernant cette l'année...
|
|
|
|
|
# (utiliser formation_id pour changer cette politique)
|
2022-09-30 16:20:51 +02:00
|
|
|
|
formsemestre_id=self.formsemestre_impair.id,
|
|
|
|
|
ordre=self.annee_but,
|
2022-06-25 14:18:34 +02:00
|
|
|
|
)
|
2022-09-30 16:20:51 +02:00
|
|
|
|
for validation in validations:
|
2023-01-11 15:09:12 +01:00
|
|
|
|
db.session.delete(validation)
|
2022-12-25 01:35:18 +01:00
|
|
|
|
Scolog.logdb(
|
|
|
|
|
"jury_but",
|
|
|
|
|
etudid=self.etud.id,
|
|
|
|
|
msg=f"Validation année BUT{self.annee_but}: effacée",
|
|
|
|
|
)
|
2023-01-11 15:09:12 +01:00
|
|
|
|
|
|
|
|
|
# Efface éventuelles validations de semestre
|
2023-01-11 13:37:02 +01:00
|
|
|
|
# (en principe inutilisées en BUT)
|
|
|
|
|
# et autres UEs (en cas de changement d'architecture de formation depuis le jury ?)
|
|
|
|
|
#
|
|
|
|
|
for validation in ScolarFormSemestreValidation.query.filter_by(
|
|
|
|
|
etudid=self.etud.id, formsemestre_id=self.formsemestre_id
|
|
|
|
|
):
|
|
|
|
|
db.session.delete(validation)
|
|
|
|
|
|
2023-01-11 15:09:12 +01:00
|
|
|
|
db.session.commit()
|
2022-07-11 18:18:48 +02:00
|
|
|
|
self.invalidate_formsemestre_cache()
|
2022-06-25 14:18:34 +02:00
|
|
|
|
|
2022-06-30 23:49:39 +02:00
|
|
|
|
def get_autorisations_passage(self) -> list[int]:
|
2023-01-26 14:49:04 +01:00
|
|
|
|
"""Liste des indices de semestres auxquels on est autorisé à
|
|
|
|
|
s'inscrire depuis le semestre courant.
|
|
|
|
|
"""
|
|
|
|
|
return sorted(
|
|
|
|
|
[
|
|
|
|
|
a.semestre_id
|
|
|
|
|
for a in ScolarAutorisationInscription.query.filter_by(
|
|
|
|
|
etudid=self.etud.id,
|
|
|
|
|
origin_formsemestre_id=self.formsemestre.id,
|
|
|
|
|
)
|
|
|
|
|
]
|
|
|
|
|
)
|
2022-06-30 23:49:39 +02:00
|
|
|
|
|
|
|
|
|
def descr_niveaux_validation(self, line_sep: str = "\n") -> str:
|
|
|
|
|
"""Description textuelle des niveaux validés (enregistrés)
|
|
|
|
|
pour PV jurys
|
|
|
|
|
"""
|
|
|
|
|
validations = [
|
|
|
|
|
dec_rcue.descr_validation()
|
|
|
|
|
for dec_rcue in self.decisions_rcue_by_niveau.values()
|
|
|
|
|
]
|
2022-07-01 22:29:19 +02:00
|
|
|
|
return line_sep.join(v for v in validations if v)
|
|
|
|
|
|
|
|
|
|
def descr_ues_validation(self, line_sep: str = "\n") -> str:
|
|
|
|
|
"""Description textuelle des UE validées (enregistrés)
|
|
|
|
|
pour PV jurys
|
|
|
|
|
"""
|
|
|
|
|
validations = []
|
|
|
|
|
for res in (self.res_impair, self.res_pair):
|
|
|
|
|
if res:
|
|
|
|
|
dec_ues = [
|
|
|
|
|
self.decisions_ues[ue.id]
|
|
|
|
|
for ue in res.ues
|
|
|
|
|
if ue.type == UE_STANDARD and ue.id in self.decisions_ues
|
|
|
|
|
]
|
|
|
|
|
valids = [dec_ue.descr_validation() for dec_ue in dec_ues]
|
|
|
|
|
validations.append(", ".join(v for v in valids if v))
|
|
|
|
|
return line_sep.join(validations)
|
2022-06-30 23:49:39 +02:00
|
|
|
|
|
2023-01-11 15:09:12 +01:00
|
|
|
|
def descr_pb_coherence(self) -> list[str]:
|
|
|
|
|
"""Description d'éventuels problèmes de cohérence entre
|
|
|
|
|
les décisions *enregistrées* d'UE et de RCUE.
|
|
|
|
|
Note: en principe, la cohérence RCUE/UE est assurée au moment de
|
|
|
|
|
l'enregistrement (record).
|
|
|
|
|
Mais la base peut avoir été modifiée par d'autres voies.
|
|
|
|
|
"""
|
|
|
|
|
messages = []
|
|
|
|
|
for dec_rcue in self.decisions_rcue_by_niveau.values():
|
|
|
|
|
if dec_rcue.code_valide in CODES_RCUE_VALIDES:
|
|
|
|
|
for ue in (dec_rcue.rcue.ue_1, dec_rcue.rcue.ue_2):
|
|
|
|
|
dec_ue = self.decisions_ues.get(ue.id)
|
|
|
|
|
if dec_ue:
|
|
|
|
|
if dec_ue.code_valide not in CODES_UE_VALIDES:
|
|
|
|
|
messages.append(
|
|
|
|
|
f"L'UE {ue.acronyme} n'est pas validée mais son RCUE l'est !"
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
messages.append(f"L'UE {ue.acronyme} n'a pas décision (???)")
|
|
|
|
|
return messages
|
|
|
|
|
|
2023-02-21 02:30:48 +01:00
|
|
|
|
def valide_diplome(self) -> bool:
|
|
|
|
|
"Vrai si l'étudiant à validé son diplôme"
|
|
|
|
|
return False # TODO XXX
|
|
|
|
|
|
2022-06-20 17:56:27 +02:00
|
|
|
|
|
2022-09-30 16:20:51 +02:00
|
|
|
|
def list_ue_parcour_etud(
|
|
|
|
|
formsemestre: FormSemestre, etud: Identite, res: ResultatsSemestreBUT
|
|
|
|
|
) -> tuple[ApcParcours, list[UniteEns]]:
|
2022-10-30 16:07:06 +01:00
|
|
|
|
"""Parcour dans lequel l'étudiant est inscrit,
|
2023-01-13 12:17:59 +01:00
|
|
|
|
et liste des UEs à valider pour ce semestre (sans les UE "dispensées")
|
2022-10-30 16:07:06 +01:00
|
|
|
|
"""
|
2022-09-30 16:20:51 +02:00
|
|
|
|
if res.etuds_parcour_id[etud.id] is None:
|
|
|
|
|
parcour = None
|
|
|
|
|
# pas de parcour: prend toutes les UEs (non bonus)
|
|
|
|
|
ues = [ue for ue in res.etud_ues(etud.id) if ue.type == UE_STANDARD]
|
|
|
|
|
ues.sort(key=lambda u: u.numero)
|
|
|
|
|
else:
|
|
|
|
|
parcour = ApcParcours.query.get(res.etuds_parcour_id[etud.id])
|
|
|
|
|
ues = (
|
|
|
|
|
formsemestre.formation.query_ues_parcour(parcour)
|
2023-04-03 17:46:31 +02:00
|
|
|
|
.filter(UniteEns.semestre_idx == formsemestre.semestre_id)
|
2022-09-30 16:20:51 +02:00
|
|
|
|
.order_by(UniteEns.numero)
|
|
|
|
|
.all()
|
|
|
|
|
)
|
2023-01-13 12:17:59 +01:00
|
|
|
|
ues = [ue for ue in ues if (etud.id, ue.id) not in res.dispense_ues]
|
2022-09-30 16:20:51 +02:00
|
|
|
|
return parcour, ues
|
|
|
|
|
|
|
|
|
|
|
2022-06-20 17:56:27 +02:00
|
|
|
|
class DecisionsProposeesRCUE(DecisionsProposees):
|
2022-06-09 07:39:58 +02:00
|
|
|
|
"""Liste des codes de décisions que l'on peut proposer pour
|
2022-06-20 17:56:27 +02:00
|
|
|
|
le RCUE de cet étudiant dans cette année.
|
2022-06-09 07:39:58 +02:00
|
|
|
|
|
|
|
|
|
ADM, CMP, ADJ, AJ, RAT, DEF, ABAN
|
2022-06-20 17:56:27 +02:00
|
|
|
|
"""
|
2022-06-09 07:39:58 +02:00
|
|
|
|
|
2022-06-20 17:56:27 +02:00
|
|
|
|
codes_communs = [
|
|
|
|
|
sco_codes.ADJ,
|
2022-06-30 20:13:30 +02:00
|
|
|
|
sco_codes.ATJ,
|
2022-06-20 17:56:27 +02:00
|
|
|
|
sco_codes.RAT,
|
|
|
|
|
sco_codes.DEF,
|
|
|
|
|
sco_codes.ABAN,
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
def __init__(
|
2022-07-04 23:50:55 +02:00
|
|
|
|
self,
|
|
|
|
|
dec_prop_annee: DecisionsProposeesAnnee,
|
|
|
|
|
rcue: RegroupementCoherentUE,
|
|
|
|
|
inscription_etat: str = scu.INSCRIT,
|
2022-06-20 17:56:27 +02:00
|
|
|
|
):
|
|
|
|
|
super().__init__(etud=dec_prop_annee.etud)
|
2023-01-11 13:37:02 +01:00
|
|
|
|
self.deca = dec_prop_annee
|
2023-06-15 08:49:05 +02:00
|
|
|
|
self.referentiel_competence_id = (
|
|
|
|
|
self.deca.formsemestre.formation.referentiel_competence_id
|
|
|
|
|
)
|
2022-06-20 17:56:27 +02:00
|
|
|
|
self.rcue = rcue
|
2022-06-22 14:09:08 +02:00
|
|
|
|
if rcue is None: # RCUE non dispo, eg un seul semestre
|
|
|
|
|
self.codes = []
|
|
|
|
|
return
|
2022-07-04 23:50:55 +02:00
|
|
|
|
self.inscription_etat = inscription_etat
|
|
|
|
|
"inscription: I, DEM, DEF"
|
2022-06-22 11:44:03 +02:00
|
|
|
|
self.parcour = dec_prop_annee.parcour
|
2022-07-04 23:50:55 +02:00
|
|
|
|
if inscription_etat != scu.INSCRIT:
|
|
|
|
|
self.validation = None # cache toute validation
|
|
|
|
|
self.explanation = "non incrit (dem. ou déf.)"
|
|
|
|
|
self.codes = [
|
|
|
|
|
sco_codes.DEM if inscription_etat == scu.DEMISSION else sco_codes.DEF
|
|
|
|
|
]
|
|
|
|
|
return
|
2022-06-22 11:44:03 +02:00
|
|
|
|
self.validation = rcue.query_validations().first()
|
|
|
|
|
if self.validation is not None:
|
|
|
|
|
self.code_valide = self.validation.code
|
2022-06-21 11:21:17 +02:00
|
|
|
|
if rcue.est_compensable():
|
2022-06-20 17:56:27 +02:00
|
|
|
|
self.codes.insert(0, sco_codes.CMP)
|
2022-07-13 19:34:46 +02:00
|
|
|
|
# les interprétations varient, on autorise aussi ADM:
|
|
|
|
|
self.codes.insert(1, sco_codes.ADM)
|
2022-06-20 17:56:27 +02:00
|
|
|
|
elif rcue.est_validable():
|
|
|
|
|
self.codes.insert(0, sco_codes.ADM)
|
|
|
|
|
else:
|
|
|
|
|
self.codes.insert(0, sco_codes.AJ)
|
2022-09-29 22:09:19 +02:00
|
|
|
|
# Si au moins l'un des semestres est extérieur, propose ADM au cas où
|
|
|
|
|
if (
|
|
|
|
|
dec_prop_annee.formsemestre_impair.modalite == "EXT"
|
|
|
|
|
or dec_prop_annee.formsemestre_pair.modalite == "EXT"
|
|
|
|
|
):
|
|
|
|
|
self.codes.insert(0, sco_codes.ADM)
|
2023-01-08 19:36:05 +01:00
|
|
|
|
# S'il y a une décision enregistrée: si elle est plus favorable que celle que l'on
|
|
|
|
|
# proposerait, la place en tête.
|
|
|
|
|
# Sinon, la place en seconde place
|
|
|
|
|
if self.code_valide and self.code_valide != self.codes[0]:
|
|
|
|
|
code_default = self.codes[0]
|
|
|
|
|
if self.code_valide in self.codes:
|
|
|
|
|
self.codes.remove(self.code_valide)
|
|
|
|
|
if sco_codes.BUT_CODES_ORDERED.get(
|
|
|
|
|
self.code_valide, 0
|
|
|
|
|
) > sco_codes.BUT_CODES_ORDERED.get(code_default, 0):
|
|
|
|
|
self.codes.insert(0, self.code_valide)
|
|
|
|
|
else:
|
|
|
|
|
self.codes.insert(1, self.code_valide)
|
2022-06-20 17:56:27 +02:00
|
|
|
|
|
2023-01-09 22:46:27 +01:00
|
|
|
|
def __repr__(self) -> str:
|
|
|
|
|
return f"""<{self.__class__.__name__} rcue={self.rcue} valid={self.code_valide
|
|
|
|
|
} codes={self.codes} explanation={self.explanation}"""
|
|
|
|
|
|
2023-01-23 11:38:47 +01:00
|
|
|
|
def record(self, code: str, no_overwrite=False) -> bool:
|
2023-01-11 13:37:02 +01:00
|
|
|
|
"""Enregistre le code RCUE.
|
|
|
|
|
Note:
|
|
|
|
|
- si le RCUE est ADJ, les UE non validées sont passées à ADJ
|
|
|
|
|
XXX on pourra imposer ici d'autres règles de cohérence
|
|
|
|
|
"""
|
2022-10-04 21:56:10 +02:00
|
|
|
|
if self.rcue is None:
|
2023-01-23 11:38:47 +01:00
|
|
|
|
return False # pas de RCUE a enregistrer
|
2022-10-04 21:56:10 +02:00
|
|
|
|
if self.inscription_etat != scu.INSCRIT:
|
2023-01-23 11:38:47 +01:00
|
|
|
|
return False
|
2022-06-22 14:09:08 +02:00
|
|
|
|
if code and not code in self.codes:
|
2022-06-22 11:44:03 +02:00
|
|
|
|
raise ScoValueError(
|
|
|
|
|
f"code UE invalide pour ue_id={self.ue.id}: {html.escape(code)}"
|
|
|
|
|
)
|
2022-06-25 17:00:00 +02:00
|
|
|
|
if code == self.code_valide or (self.code_valide is not None and no_overwrite):
|
2022-06-24 15:24:33 +02:00
|
|
|
|
self.recorded = True
|
2023-01-23 11:38:47 +01:00
|
|
|
|
return False # no change
|
2022-06-22 11:44:03 +02:00
|
|
|
|
parcours_id = self.parcour.id if self.parcour is not None else None
|
|
|
|
|
if self.validation:
|
|
|
|
|
db.session.delete(self.validation)
|
2023-01-11 15:09:12 +01:00
|
|
|
|
db.session.commit()
|
2022-06-22 14:09:08 +02:00
|
|
|
|
if code is None:
|
|
|
|
|
self.validation = None
|
|
|
|
|
else:
|
|
|
|
|
self.validation = ApcValidationRCUE(
|
|
|
|
|
etudid=self.etud.id,
|
|
|
|
|
formsemestre_id=self.rcue.formsemestre_2.id,
|
|
|
|
|
ue1_id=self.rcue.ue_1.id,
|
|
|
|
|
ue2_id=self.rcue.ue_2.id,
|
|
|
|
|
parcours_id=parcours_id,
|
|
|
|
|
code=code,
|
|
|
|
|
)
|
2023-01-11 15:09:12 +01:00
|
|
|
|
db.session.add(self.validation)
|
|
|
|
|
db.session.commit()
|
2022-06-22 14:09:08 +02:00
|
|
|
|
Scolog.logdb(
|
|
|
|
|
method="jury_but",
|
|
|
|
|
etudid=self.etud.id,
|
2022-12-25 01:35:18 +01:00
|
|
|
|
msg=f"Validation {self.rcue}: {code}",
|
2023-01-11 15:09:12 +01:00
|
|
|
|
commit=True,
|
2022-06-22 14:09:08 +02:00
|
|
|
|
)
|
2023-01-11 19:09:03 +01:00
|
|
|
|
log(f"rcue.record {self}: {code}")
|
|
|
|
|
|
2023-01-11 13:37:02 +01:00
|
|
|
|
# Modifie au besoin les codes d'UE
|
|
|
|
|
if code == "ADJ":
|
|
|
|
|
deca = self.deca
|
|
|
|
|
for ue_id in (self.rcue.ue_1.id, self.rcue.ue_2.id):
|
|
|
|
|
dec_ue = deca.decisions_ues.get(ue_id)
|
|
|
|
|
if dec_ue and dec_ue.code_valide not in CODES_UE_VALIDES:
|
2023-01-12 14:14:54 +01:00
|
|
|
|
log(f"rcue.record: force ADJR sur {dec_ue}")
|
2023-01-17 23:14:58 +01:00
|
|
|
|
flash(
|
|
|
|
|
f"""UEs du RCUE "{dec_ue.ue.niveau_competence.competence.titre}" passées en ADJR"""
|
|
|
|
|
)
|
2023-01-22 21:44:57 +01:00
|
|
|
|
dec_ue.record(sco_codes.ADJR)
|
2023-01-11 13:37:02 +01:00
|
|
|
|
|
2023-01-17 23:14:58 +01:00
|
|
|
|
# Valide les niveaux inférieurs de la compétence (code ADSUP)
|
2023-06-15 08:49:05 +02:00
|
|
|
|
if code in CODES_RCUE_VALIDES:
|
|
|
|
|
self.valide_niveau_inferieur()
|
2023-01-17 23:14:58 +01:00
|
|
|
|
|
2022-07-11 18:18:48 +02:00
|
|
|
|
if self.rcue.formsemestre_1 is not None:
|
|
|
|
|
sco_cache.invalidate_formsemestre(
|
|
|
|
|
formsemestre_id=self.rcue.formsemestre_1.id
|
|
|
|
|
)
|
|
|
|
|
if self.rcue.formsemestre_2 is not None:
|
|
|
|
|
sco_cache.invalidate_formsemestre(
|
|
|
|
|
formsemestre_id=self.rcue.formsemestre_2.id
|
|
|
|
|
)
|
2023-01-11 13:37:02 +01:00
|
|
|
|
self.code_valide = code # mise à jour état
|
2022-06-22 11:44:03 +02:00
|
|
|
|
self.recorded = True
|
2023-01-23 11:38:47 +01:00
|
|
|
|
return True
|
2022-06-22 11:44:03 +02:00
|
|
|
|
|
2022-06-25 14:18:34 +02:00
|
|
|
|
def erase(self):
|
|
|
|
|
"""Efface la décision de jury de cet étudiant pour cet RCUE"""
|
|
|
|
|
# par prudence, on requete toutes les validations, en cas de doublons
|
|
|
|
|
validations = self.rcue.query_validations()
|
|
|
|
|
for validation in validations:
|
2023-01-11 19:09:03 +01:00
|
|
|
|
log(f"DecisionsProposeesRCUE: deleting {validation}")
|
2022-06-25 14:18:34 +02:00
|
|
|
|
db.session.delete(validation)
|
|
|
|
|
db.session.flush()
|
|
|
|
|
|
2022-06-30 23:49:39 +02:00
|
|
|
|
def descr_validation(self) -> str:
|
|
|
|
|
"""Description validation niveau enregistrée, pour PV jury.
|
2023-02-19 02:54:29 +01:00
|
|
|
|
Si le niveau est validé, donne son acronyme, sinon chaine vide.
|
2022-06-30 23:49:39 +02:00
|
|
|
|
"""
|
|
|
|
|
if self.code_valide in sco_codes.CODES_RCUE_VALIDES:
|
|
|
|
|
if (
|
|
|
|
|
self.rcue and self.rcue.ue_1 and self.rcue.ue_1.niveau_competence
|
|
|
|
|
): # prudence !
|
|
|
|
|
niveau_titre = self.rcue.ue_1.niveau_competence.competence.titre or ""
|
|
|
|
|
ordre = self.rcue.ue_1.niveau_competence.ordre
|
|
|
|
|
else:
|
|
|
|
|
return "?" # oups ?
|
2023-02-19 02:54:29 +01:00
|
|
|
|
return f"{niveau_titre}-{ordre}"
|
2022-06-30 23:49:39 +02:00
|
|
|
|
return ""
|
|
|
|
|
|
2023-06-15 08:49:05 +02:00
|
|
|
|
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()
|
2023-06-15 21:53:05 +02:00
|
|
|
|
db.session.add(validation_rcue)
|
|
|
|
|
db.session.commit()
|
2023-06-15 08:49:05 +02:00
|
|
|
|
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
|
|
|
|
|
)
|
2023-06-15 21:53:05 +02:00
|
|
|
|
db.session.add(validation_rcue)
|
|
|
|
|
db.session.commit()
|
2023-06-15 08:49:05 +02:00
|
|
|
|
log(f"recording {validation_rcue}")
|
|
|
|
|
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
|
|
|
|
|
|
2022-06-20 17:56:27 +02:00
|
|
|
|
|
|
|
|
|
class DecisionsProposeesUE(DecisionsProposees):
|
|
|
|
|
"""Décisions de jury sur une UE du BUT
|
|
|
|
|
|
|
|
|
|
Liste des codes de décisions que l'on peut proposer pour
|
|
|
|
|
cette UE d'un étudiant dans un semestre.
|
|
|
|
|
|
|
|
|
|
Si DEF ou DEM ou ABAN ou ABL sur année BUT: seulement DEF, DEM, ABAN, ABL
|
|
|
|
|
|
|
|
|
|
si moy_ue > 10, ADM
|
|
|
|
|
sinon si compensation dans RCUE: CMP
|
|
|
|
|
sinon: ADJ, AJ
|
|
|
|
|
|
2023-01-12 14:14:54 +01:00
|
|
|
|
et proposer toujours: RAT, DEF, ABAN, ADJR, DEM, UEBSL (codes_communs)
|
2022-06-09 07:39:58 +02:00
|
|
|
|
"""
|
2022-06-20 17:56:27 +02:00
|
|
|
|
|
|
|
|
|
# Codes toujours proposés sauf si include_communs est faux:
|
|
|
|
|
codes_communs = [
|
|
|
|
|
sco_codes.RAT,
|
|
|
|
|
sco_codes.DEF,
|
|
|
|
|
sco_codes.ABAN,
|
2023-01-12 14:14:54 +01:00
|
|
|
|
sco_codes.ADJR,
|
2022-06-30 20:13:30 +02:00
|
|
|
|
sco_codes.ATJ,
|
2022-06-20 17:56:27 +02:00
|
|
|
|
sco_codes.DEM,
|
|
|
|
|
sco_codes.UEBSL,
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
|
self,
|
|
|
|
|
etud: Identite,
|
|
|
|
|
formsemestre: FormSemestre,
|
|
|
|
|
ue: UniteEns,
|
2022-07-04 23:50:55 +02:00
|
|
|
|
inscription_etat: str = scu.INSCRIT,
|
2022-06-20 17:56:27 +02:00
|
|
|
|
):
|
2022-07-18 22:17:27 +02:00
|
|
|
|
# Une UE peut être validée plusieurs fois en cas de redoublement (qu'elle soit capitalisée ou non)
|
|
|
|
|
# mais ici on a restreint au formsemestre donc une seule (prend la première)
|
2023-01-11 13:37:02 +01:00
|
|
|
|
validation = ScolarFormSemestreValidation.query.filter_by(
|
2022-07-18 22:17:27 +02:00
|
|
|
|
etudid=etud.id, formsemestre_id=formsemestre.id, ue_id=ue.id
|
|
|
|
|
).first()
|
|
|
|
|
super().__init__(
|
|
|
|
|
etud=etud,
|
2023-01-11 13:37:02 +01:00
|
|
|
|
code_valide=validation.code if validation is not None else None,
|
2022-07-18 22:17:27 +02:00
|
|
|
|
)
|
2023-01-11 13:37:02 +01:00
|
|
|
|
self.validation = validation
|
2022-06-22 11:44:03 +02:00
|
|
|
|
self.formsemestre = formsemestre
|
2022-06-20 17:56:27 +02:00
|
|
|
|
self.ue: UniteEns = ue
|
|
|
|
|
self.rcue: RegroupementCoherentUE = None
|
2022-07-18 22:17:27 +02:00
|
|
|
|
"Le rcue auquel est rattaché cette UE, ou None"
|
2022-07-04 23:50:55 +02:00
|
|
|
|
self.inscription_etat = inscription_etat
|
2022-10-04 21:56:10 +02:00
|
|
|
|
"inscription: I, DEM, DEF dans le semestre de cette UE"
|
2022-12-17 03:33:04 +01:00
|
|
|
|
self.moy_ue = np.NaN
|
|
|
|
|
self.moy_ue_with_cap = np.NaN
|
|
|
|
|
self.ue_status = {}
|
|
|
|
|
|
2022-07-04 23:50:55 +02:00
|
|
|
|
if ue.type == sco_codes.UE_SPORT:
|
|
|
|
|
self.explanation = "UE bonus, pas de décision de jury"
|
|
|
|
|
self.codes = [] # aucun code proposé
|
|
|
|
|
return
|
|
|
|
|
if inscription_etat != scu.INSCRIT:
|
|
|
|
|
self.validation = None # cache toute validation
|
|
|
|
|
self.explanation = "non incrit (dem. ou déf.)"
|
|
|
|
|
self.codes = [
|
|
|
|
|
sco_codes.DEM if inscription_etat == scu.DEMISSION else sco_codes.DEF
|
|
|
|
|
]
|
|
|
|
|
return
|
2022-06-21 11:21:17 +02:00
|
|
|
|
|
2022-06-20 17:56:27 +02:00
|
|
|
|
# Moyenne de l'UE ?
|
|
|
|
|
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
|
2022-11-24 00:11:59 +01:00
|
|
|
|
# Safety checks:
|
2022-06-20 17:56:27 +02:00
|
|
|
|
if not ue.id in res.etud_moy_ue:
|
|
|
|
|
self.explanation = "UE sans résultat"
|
|
|
|
|
return
|
|
|
|
|
if not etud.id in res.etud_moy_ue[ue.id]:
|
|
|
|
|
self.explanation = "Étudiant sans résultat dans cette UE"
|
|
|
|
|
return
|
2022-11-24 00:11:59 +01:00
|
|
|
|
ue_status = res.get_etud_ue_status(etud.id, ue.id)
|
|
|
|
|
self.moy_ue = ue_status["cur_moy_ue"]
|
|
|
|
|
self.moy_ue_with_cap = ue_status["moy"]
|
|
|
|
|
self.ue_status = ue_status
|
2022-06-21 11:21:17 +02:00
|
|
|
|
|
2022-12-20 19:20:00 +01:00
|
|
|
|
def __repr__(self) -> str:
|
|
|
|
|
return f"""<{self.__class__.__name__} ue={self.ue.acronyme} valid={self.code_valide
|
2023-01-09 22:46:27 +01:00
|
|
|
|
} codes={self.codes} explanation={self.explanation}>"""
|
2022-12-20 19:20:00 +01:00
|
|
|
|
|
2022-06-21 11:21:17 +02:00
|
|
|
|
def set_rcue(self, rcue: RegroupementCoherentUE):
|
|
|
|
|
"""Rattache cette UE à un RCUE. Cela peut modifier les codes
|
2023-01-11 13:37:02 +01:00
|
|
|
|
proposés par compute_codes() (si compensation)"""
|
2022-06-21 11:21:17 +02:00
|
|
|
|
self.rcue = rcue
|
|
|
|
|
|
|
|
|
|
def compute_codes(self):
|
|
|
|
|
"""Calcul des .codes attribuables et de l'explanation associée"""
|
2022-07-04 23:50:55 +02:00
|
|
|
|
if self.inscription_etat != scu.INSCRIT:
|
|
|
|
|
return
|
2022-09-29 22:09:19 +02:00
|
|
|
|
if (
|
2023-02-12 13:36:47 +01:00
|
|
|
|
self.moy_ue > (sco_codes.CursusBUT.BARRE_MOY - sco_codes.NOTES_TOLERANCE)
|
2022-09-29 22:09:19 +02:00
|
|
|
|
) or self.formsemestre.modalite == "EXT":
|
2022-06-20 17:56:27 +02:00
|
|
|
|
self.codes.insert(0, sco_codes.ADM)
|
2023-02-12 13:36:47 +01:00
|
|
|
|
self.explanation = f"Moyenne >= {sco_codes.CursusBUT.BARRE_MOY}/20"
|
2022-06-21 11:21:17 +02:00
|
|
|
|
elif self.rcue and self.rcue.est_compensable():
|
|
|
|
|
self.codes.insert(0, sco_codes.CMP)
|
|
|
|
|
self.explanation = "compensable dans le RCUE"
|
|
|
|
|
else:
|
|
|
|
|
# Échec à valider cette UE
|
|
|
|
|
self.codes = [sco_codes.AJ, sco_codes.ADJ] + self.codes
|
|
|
|
|
self.explanation = "notes insuffisantes"
|
2022-06-09 07:39:58 +02:00
|
|
|
|
|
2023-01-23 11:38:47 +01:00
|
|
|
|
def record(self, code: str, no_overwrite=False) -> bool:
|
2022-09-07 13:07:11 +02:00
|
|
|
|
"""Enregistre le code jury pour cette UE.
|
|
|
|
|
Si no_overwrite, n'enregistre pas s'il y a déjà un code.
|
2023-01-23 11:38:47 +01:00
|
|
|
|
Return: True si code enregistré (modifié)
|
2022-09-07 13:07:11 +02:00
|
|
|
|
"""
|
2022-06-22 14:09:08 +02:00
|
|
|
|
if code and not code in self.codes:
|
2022-06-22 11:44:03 +02:00
|
|
|
|
raise ScoValueError(
|
|
|
|
|
f"code UE invalide pour ue_id={self.ue.id}: {html.escape(code)}"
|
|
|
|
|
)
|
2022-06-25 17:00:00 +02:00
|
|
|
|
if code == self.code_valide or (self.code_valide is not None and no_overwrite):
|
2022-06-24 15:24:33 +02:00
|
|
|
|
self.recorded = True
|
2023-01-23 11:38:47 +01:00
|
|
|
|
return False # no change
|
2022-09-07 13:07:11 +02:00
|
|
|
|
self.erase()
|
2022-06-22 14:09:08 +02:00
|
|
|
|
if code is None:
|
|
|
|
|
self.validation = None
|
2022-12-25 01:35:18 +01:00
|
|
|
|
Scolog.logdb(
|
|
|
|
|
method="jury_but",
|
|
|
|
|
etudid=self.etud.id,
|
|
|
|
|
msg=f"Validation UE {self.ue.id} {self.ue.acronyme}: effacée",
|
2023-01-11 15:09:12 +01:00
|
|
|
|
commit=True,
|
2022-12-25 01:35:18 +01:00
|
|
|
|
)
|
2022-06-22 14:09:08 +02:00
|
|
|
|
else:
|
|
|
|
|
self.validation = ScolarFormSemestreValidation(
|
|
|
|
|
etudid=self.etud.id,
|
|
|
|
|
formsemestre_id=self.formsemestre.id,
|
|
|
|
|
ue_id=self.ue.id,
|
|
|
|
|
code=code,
|
2022-06-24 15:24:33 +02:00
|
|
|
|
moy_ue=self.moy_ue,
|
2022-06-22 14:09:08 +02:00
|
|
|
|
)
|
2023-01-11 15:09:12 +01:00
|
|
|
|
db.session.add(self.validation)
|
|
|
|
|
db.session.commit()
|
2022-06-22 14:09:08 +02:00
|
|
|
|
Scolog.logdb(
|
|
|
|
|
method="jury_but",
|
|
|
|
|
etudid=self.etud.id,
|
2022-12-25 01:35:18 +01:00
|
|
|
|
msg=f"Validation UE {self.ue.id} {self.ue.acronyme}({self.moy_ue}): {code}",
|
2023-01-11 15:09:12 +01:00
|
|
|
|
commit=True,
|
2022-06-22 14:09:08 +02:00
|
|
|
|
)
|
2022-09-30 16:20:51 +02:00
|
|
|
|
log(f"DecisionsProposeesUE: recording {self.validation}")
|
2022-07-11 18:18:48 +02:00
|
|
|
|
|
|
|
|
|
sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre.id)
|
2023-01-11 13:37:02 +01:00
|
|
|
|
self.code_valide = code # mise à jour
|
2022-06-22 11:44:03 +02:00
|
|
|
|
self.recorded = True
|
2023-01-23 11:38:47 +01:00
|
|
|
|
return True
|
2022-06-22 11:44:03 +02:00
|
|
|
|
|
2022-06-25 14:18:34 +02:00
|
|
|
|
def erase(self):
|
|
|
|
|
"""Efface la décision de jury de cet étudiant pour cette UE"""
|
|
|
|
|
# par prudence, on requete toutes les validations, en cas de doublons
|
|
|
|
|
validations = ScolarFormSemestreValidation.query.filter_by(
|
|
|
|
|
etudid=self.etud.id, formsemestre_id=self.formsemestre.id, ue_id=self.ue.id
|
|
|
|
|
)
|
|
|
|
|
for validation in validations:
|
2022-09-07 13:07:11 +02:00
|
|
|
|
log(f"DecisionsProposeesUE: deleting {validation}")
|
2023-01-11 15:09:12 +01:00
|
|
|
|
db.session.delete(validation)
|
2022-12-25 01:35:18 +01:00
|
|
|
|
Scolog.logdb(
|
|
|
|
|
method="jury_but",
|
|
|
|
|
etudid=self.etud.id,
|
|
|
|
|
msg=f"Validation UE {validation.ue.id} {validation.ue.acronyme}: effacée",
|
|
|
|
|
)
|
2023-01-11 15:09:12 +01:00
|
|
|
|
|
|
|
|
|
db.session.commit()
|
2022-06-25 14:18:34 +02:00
|
|
|
|
|
2022-06-30 23:49:39 +02:00
|
|
|
|
def descr_validation(self) -> str:
|
|
|
|
|
"""Description validation niveau enregistrée, pour PV jury.
|
2022-07-01 22:29:19 +02:00
|
|
|
|
Si l'UE est validée, donne son acronyme, sinon chaine vide.
|
2022-06-30 23:49:39 +02:00
|
|
|
|
"""
|
|
|
|
|
if self.code_valide in sco_codes.CODES_UE_VALIDES:
|
|
|
|
|
return f"{self.ue.acronyme}"
|
|
|
|
|
return ""
|
|
|
|
|
|
2023-02-18 18:49:52 +01:00
|
|
|
|
def ects_acquis(self) -> float:
|
|
|
|
|
"""ECTS enregistrés pour cette UE
|
|
|
|
|
(0 si pas de validation enregistrée)
|
|
|
|
|
"""
|
|
|
|
|
if self.validation and self.code_valide in sco_codes.CODES_UE_VALIDES:
|
|
|
|
|
return self.ue.ects
|
|
|
|
|
return 0.0
|
|
|
|
|
|
2022-06-09 07:39:58 +02:00
|
|
|
|
|
2022-06-20 17:56:27 +02:00
|
|
|
|
class BUTCursusEtud: # WIP TODO
|
2022-06-09 07:39:58 +02:00
|
|
|
|
"""Validation du cursus d'un étudiant"""
|
|
|
|
|
|
|
|
|
|
def __init__(self, formsemestre: FormSemestre, etud: Identite):
|
|
|
|
|
if formsemestre.formation.referentiel_competence is None:
|
2023-01-03 13:06:11 +01:00
|
|
|
|
raise ScoNoReferentielCompetences(formation=formsemestre.formation)
|
2022-06-09 07:39:58 +02:00
|
|
|
|
assert len(etud.formsemestre_inscriptions) > 0
|
|
|
|
|
self.formsemestre = formsemestre
|
|
|
|
|
self.etud = etud
|
|
|
|
|
#
|
|
|
|
|
# La dernière inscription en date va donner le parcours (donc les compétences à valider)
|
|
|
|
|
self.last_inscription = sorted(
|
|
|
|
|
etud.formsemestre_inscriptions, key=attrgetter("formsemestre.date_debut")
|
|
|
|
|
)[-1]
|
|
|
|
|
|
|
|
|
|
def est_diplomable(self) -> bool:
|
|
|
|
|
"""Vrai si toutes les compétences sont validables"""
|
|
|
|
|
return all(
|
|
|
|
|
self.competence_validable(competence)
|
|
|
|
|
for competence in self.competences_du_parcours()
|
|
|
|
|
)
|
|
|
|
|
|
2023-06-15 08:49:05 +02:00
|
|
|
|
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.
|
2022-06-09 07:39:58 +02:00
|
|
|
|
return (
|
2023-06-15 08:49:05 +02:00
|
|
|
|
ApcValidationAnnee.query.filter_by(
|
|
|
|
|
etudid=self.etud.id,
|
|
|
|
|
ordre=ordre,
|
|
|
|
|
formation_id=self.formsemestre.formation_id,
|
|
|
|
|
)
|
|
|
|
|
.join(Formation)
|
2022-06-09 07:39:58 +02:00
|
|
|
|
.filter(
|
2023-06-15 08:49:05 +02:00
|
|
|
|
Formation.formation_code == self.formsemestre.formation.formation_code
|
2022-06-09 07:39:58 +02:00
|
|
|
|
)
|
|
|
|
|
.count()
|
|
|
|
|
> 0
|
|
|
|
|
)
|
|
|
|
|
|
2023-06-15 08:49:05 +02:00
|
|
|
|
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)
|
|
|
|
|
|
2022-06-09 07:39:58 +02:00
|
|
|
|
def competences_du_parcours(self) -> list[ApcCompetence]:
|
|
|
|
|
"""Construit liste des compétences du parcours, qui doivent être
|
|
|
|
|
validées pour obtenir le diplôme.
|
|
|
|
|
Le parcours est celui de la dernière inscription.
|
|
|
|
|
"""
|
|
|
|
|
parcour = self.last_inscription.parcour
|
|
|
|
|
query = self.formsemestre.formation.formation.query_competences_parcour(parcour)
|
|
|
|
|
if query is None:
|
|
|
|
|
return []
|
|
|
|
|
return query.all()
|
|
|
|
|
|
|
|
|
|
def competence_validee(self, competence: ApcCompetence) -> bool:
|
|
|
|
|
"""Vrai si la compétence est validée, c'est à dire que tous ses
|
|
|
|
|
niveaux sont validés (ApcValidationRCUE).
|
|
|
|
|
"""
|
2022-06-20 17:56:27 +02:00
|
|
|
|
# XXX A REVOIR
|
2022-06-09 07:39:58 +02:00
|
|
|
|
validations = (
|
|
|
|
|
ApcValidationRCUE.query.filter_by(etudid=self.etud.id)
|
|
|
|
|
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id)
|
|
|
|
|
.join(ApcNiveau, ApcNiveau.id == UniteEns.niveau_competence_id)
|
|
|
|
|
.join(ApcCompetence, ApcCompetence.id == ApcNiveau.competence_id)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def competence_validable(self, competence: ApcCompetence):
|
|
|
|
|
"""Vrai si la compétence est "validable" automatiquement, c'est à dire
|
|
|
|
|
que les conditions de notes sont satisfaites pour l'acquisition de
|
|
|
|
|
son niveau le plus élevé, qu'il ne manque que l'enregistrement de la décision.
|
|
|
|
|
|
|
|
|
|
En vertu de la règle "La validation des deux UE du niveau d’une compétence
|
|
|
|
|
emporte la validation de l'ensemble des UE du niveau inférieur de cette
|
|
|
|
|
même compétence.",
|
|
|
|
|
il suffit de considérer le dernier niveau dans lequel l'étudiant est inscrit.
|
|
|
|
|
"""
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
def ues_emportees(self, niveau: ApcNiveau) -> list[tuple[FormSemestre, UniteEns]]:
|
|
|
|
|
"""La liste des UE à valider si on valide ce niveau.
|
|
|
|
|
Ne liste que les UE qui ne sont pas déjà acquises.
|
|
|
|
|
|
2022-06-29 19:25:08 +02:00
|
|
|
|
Selon la règle donnée par l'arrêté BUT:
|
2022-06-09 07:39:58 +02:00
|
|
|
|
* La validation des deux UE du niveau d’une compétence emporte la validation de
|
|
|
|
|
l'ensemble des UE du niveau inférieur de cette même compétence.
|
|
|
|
|
"""
|
|
|
|
|
pass
|