forked from ScoDoc/ScoDoc
343 lines
12 KiB
Python
343 lines
12 KiB
Python
# -*- coding: UTF-8 -*
|
|
|
|
"""Décisions de jury (validations) des RCUE et années du BUT
|
|
"""
|
|
from collections import defaultdict
|
|
|
|
import sqlalchemy as sa
|
|
|
|
from app import db
|
|
from app.models import CODE_STR_LEN, ScoDocModel
|
|
from app.models.but_refcomp import ApcNiveau
|
|
from app.models.etudiants import Identite
|
|
from app.models.formsemestre import FormSemestre
|
|
from app.models.ues import UniteEns
|
|
from app.scodoc import sco_preferences
|
|
from app.scodoc import sco_utils as scu
|
|
|
|
|
|
class ApcValidationRCUE(ScoDocModel):
|
|
"""Validation des niveaux de compétences
|
|
|
|
aka "regroupements cohérents d'UE" dans le jargon BUT.
|
|
|
|
Le formsemestre est l'origine, utilisé pour effacer
|
|
"""
|
|
|
|
__tablename__ = "apc_validation_rcue"
|
|
# Assure unicité de la décision:
|
|
__table_args__ = (
|
|
db.UniqueConstraint("etudid", "formsemestre_id", "ue1_id", "ue2_id"),
|
|
)
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
etudid = db.Column(
|
|
db.Integer,
|
|
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
|
index=True,
|
|
nullable=False,
|
|
)
|
|
formsemestre_id = db.Column(
|
|
db.Integer, db.ForeignKey("notes_formsemestre.id"), index=True, nullable=True
|
|
)
|
|
"formsemestre origine du RCUE (celui d'où a été émis la validation)"
|
|
# Les deux UE associées à ce niveau:
|
|
ue1_id = db.Column(
|
|
db.Integer, db.ForeignKey("notes_ue.id", ondelete="CASCADE"), nullable=False
|
|
)
|
|
ue2_id = db.Column(
|
|
db.Integer, db.ForeignKey("notes_ue.id", ondelete="CASCADE"), nullable=False
|
|
)
|
|
# optionnel, le parcours dans lequel se trouve la compétence:
|
|
parcours_id = db.Column(
|
|
db.Integer, db.ForeignKey("apc_parcours.id", ondelete="set null"), nullable=True
|
|
)
|
|
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
|
code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True)
|
|
|
|
etud = db.relationship("Identite", backref="apc_validations_rcues")
|
|
formsemestre = db.relationship("FormSemestre", backref="apc_validations_rcues")
|
|
ue1 = db.relationship("UniteEns", foreign_keys=ue1_id)
|
|
ue2 = db.relationship("UniteEns", foreign_keys=ue2_id)
|
|
parcour = db.relationship("ApcParcours")
|
|
|
|
def __repr__(self):
|
|
return f"""<{self.__class__.__name__} {self.id} {self.etud} {
|
|
self.ue1}/{self.ue2}:{self.code!r}>"""
|
|
|
|
def __str__(self):
|
|
return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}: {
|
|
self.code} enregistrée le {self.date.strftime(scu.DATE_FMT)}"""
|
|
|
|
def html(self) -> str:
|
|
"description en HTML"
|
|
return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}:
|
|
<b>{self.code}</b>
|
|
<em>enregistrée le {self.date.strftime(scu.DATEATIME_FMT)}</em>"""
|
|
|
|
def annee(self) -> str:
|
|
"""l'année BUT concernée: "BUT1", "BUT2" ou "BUT3" """
|
|
niveau = self.niveau()
|
|
return niveau.annee if niveau else None
|
|
|
|
def niveau(self) -> ApcNiveau | None:
|
|
"""Le niveau de compétence associé à cet RCUE."""
|
|
# Par convention, il est donné par la seconde UE
|
|
# à défaut (si l'UE a été désacciée entre temps), la première
|
|
# et à défaut, renvoie None
|
|
return self.ue2.niveau_competence or self.ue1.niveau_competence
|
|
|
|
def to_dict(self):
|
|
"as a dict"
|
|
d = dict(self.__dict__)
|
|
d.pop("_sa_instance_state", None)
|
|
d["etud"] = self.etud.to_dict_short()
|
|
d["ue1"] = self.ue1.to_dict()
|
|
d["ue2"] = self.ue2.to_dict()
|
|
|
|
return d
|
|
|
|
def to_dict_bul(self) -> dict:
|
|
"Export dict pour bulletins: le code et le niveau de compétence"
|
|
niveau = self.niveau()
|
|
return {
|
|
"code": self.code,
|
|
"niveau": None if niveau is None else niveau.to_dict_bul(),
|
|
}
|
|
|
|
def to_dict_codes(self) -> dict:
|
|
"Dict avec seulement les ids et la date - pour cache table jury"
|
|
return {
|
|
"id": self.id,
|
|
"code": self.code,
|
|
"date": self.date,
|
|
"etudid": self.etudid,
|
|
"niveau_id": self.niveau().id,
|
|
"formsemestre_id": self.formsemestre_id,
|
|
}
|
|
|
|
def get_codes_apogee(self) -> set[str]:
|
|
"""Les codes Apogée associés à cette validation RCUE.
|
|
Prend les codes des deux UEs
|
|
"""
|
|
return self.ue1.get_codes_apogee_rcue() | self.ue2.get_codes_apogee_rcue()
|
|
|
|
|
|
class ApcValidationAnnee(ScoDocModel):
|
|
"""Validation des années du BUT"""
|
|
|
|
__tablename__ = "apc_validation_annee"
|
|
# Assure unicité de la décision:
|
|
__table_args__ = (
|
|
db.UniqueConstraint("etudid", "ordre", "referentiel_competence_id"),
|
|
)
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
etudid = db.Column(
|
|
db.Integer,
|
|
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
|
index=True,
|
|
nullable=False,
|
|
)
|
|
ordre = db.Column(db.Integer, nullable=False)
|
|
"numéro de l'année: 1, 2, 3"
|
|
formsemestre_id = db.Column(
|
|
db.Integer, db.ForeignKey("notes_formsemestre.id"), nullable=True
|
|
)
|
|
"le semestre origine, normalement l'IMPAIR (le 1er) de l'année"
|
|
referentiel_competence_id = db.Column(
|
|
db.Integer, db.ForeignKey("apc_referentiel_competences.id"), nullable=False
|
|
)
|
|
annee_scolaire = db.Column(db.Integer, nullable=False) # eg 2021
|
|
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
|
code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True)
|
|
|
|
etud = db.relationship("Identite", backref="apc_validations_annees")
|
|
formsemestre = db.relationship("FormSemestre", backref="apc_validations_annees")
|
|
|
|
def __repr__(self):
|
|
return f"""<{self.__class__.__name__} {self.id} {self.etud
|
|
} BUT{self.ordre}/{self.annee_scolaire}:{self.code!r}>"""
|
|
|
|
def __str__(self):
|
|
return f"""décision sur année BUT{self.ordre} {self.annee_scolaire} : {self.code}"""
|
|
|
|
def to_dict_bul(self) -> dict:
|
|
"dict pour bulletins"
|
|
return {
|
|
"annee_scolaire": self.annee_scolaire,
|
|
"date": self.date.isoformat() if self.date else "",
|
|
"code": self.code,
|
|
"ordre": self.ordre,
|
|
}
|
|
|
|
def html(self) -> str:
|
|
"Affichage html"
|
|
date_str = (
|
|
f"""le {self.date.strftime(scu.DATEATIME_FMT)}"""
|
|
if self.date
|
|
else "(sans date)"
|
|
)
|
|
link = (
|
|
self.formsemestre.html_link_status(
|
|
label=f"{self.formsemestre.titre_formation(with_sem_idx=1)}",
|
|
title=self.formsemestre.titre_annee(),
|
|
)
|
|
if self.formsemestre
|
|
else "externe/antérieure"
|
|
)
|
|
return f"""Validation <b>année BUT{self.ordre}</b> émise par
|
|
{link}
|
|
: <b>{self.code}</b>
|
|
{date_str}
|
|
"""
|
|
|
|
|
|
def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
|
|
"""
|
|
Un dict avec les décisions de jury BUT enregistrées:
|
|
- decision_rcue : list[dict]
|
|
- decision_annee : dict (décision issue de ce semestre seulement (à confirmer ?))
|
|
Ne reprend pas les décisions d'UE, non spécifiques au BUT.
|
|
"""
|
|
decisions = {}
|
|
# --- RCUEs: seulement sur semestres pairs XXX à améliorer
|
|
if formsemestre.semestre_id % 2 == 0:
|
|
# validations émises depuis ce formsemestre:
|
|
validations_rcues = (
|
|
ApcValidationRCUE.query.filter_by(
|
|
etudid=etud.id, formsemestre_id=formsemestre.id
|
|
)
|
|
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id)
|
|
.order_by(UniteEns.numero, UniteEns.acronyme)
|
|
)
|
|
decisions["decision_rcue"] = [v.to_dict_bul() for v in validations_rcues]
|
|
titres_rcues = _build_decisions_rcue_list(decisions["decision_rcue"])
|
|
decisions["descr_decisions_rcue"] = ", ".join(titres_rcues)
|
|
decisions["descr_decisions_rcue_list"] = titres_rcues
|
|
decisions["descr_decisions_niveaux"] = (
|
|
"Niveaux de compétences: " + decisions["descr_decisions_rcue"]
|
|
)
|
|
else:
|
|
decisions["decision_rcue"] = []
|
|
decisions["descr_decisions_rcue"] = ""
|
|
decisions["descr_decisions_niveaux"] = ""
|
|
# --- Année: prend la validation pour l'année scolaire et l'ordre de ce semestre
|
|
if sco_preferences.get_preference("bul_but_code_annuel", formsemestre.id):
|
|
annee_but = (formsemestre.semestre_id + 1) // 2
|
|
validation = ApcValidationAnnee.query.filter_by(
|
|
etudid=etud.id,
|
|
annee_scolaire=formsemestre.annee_scolaire(),
|
|
ordre=annee_but,
|
|
referentiel_competence_id=formsemestre.formation.referentiel_competence_id,
|
|
).first()
|
|
if validation:
|
|
decisions["decision_annee"] = validation.to_dict_bul()
|
|
else:
|
|
decisions["decision_annee"] = None
|
|
else:
|
|
decisions["decision_annee"] = None
|
|
return decisions
|
|
|
|
|
|
def _build_decisions_rcue_list(decisions_rcue: dict) -> list[str]:
|
|
"""Formatte liste des décisions niveaux de compétences / RCUE pour
|
|
lettres individuelles.
|
|
Le résulat est trié par compétence et donne pour chaque niveau avec validation:
|
|
[ 'Administrer: niveau 1 ADM, niveau 2 ADJ', ... ]
|
|
"""
|
|
# Construit { id_competence : validation }
|
|
# où validation est {'code': 'CMP', 'niveau': {'annee': 'BUT3', 'competence': {}, ... }
|
|
validation_by_competence = defaultdict(list)
|
|
for validation in decisions_rcue:
|
|
if validation:
|
|
competence_id = (
|
|
validation.get("niveau", {}).get("competence", {}).get("id_orebut")
|
|
)
|
|
validation_by_competence[competence_id].append(validation)
|
|
# Tri des listes de validation par numéro de compétence
|
|
validations_niveaux = sorted(
|
|
validation_by_competence.values(),
|
|
key=lambda v: (
|
|
v[0].get("niveau", {}).get("competence", {}).get("numero", 0) if v else -1
|
|
),
|
|
)
|
|
titres_rcues = []
|
|
empty = {} # pour syntaxe f-string
|
|
for validations in validations_niveaux:
|
|
if validations:
|
|
v = validations[0]
|
|
titre_competence = (
|
|
v.get("niveau", {}).get("competence", {}).get("titre", "sans titre")
|
|
)
|
|
titres_rcues.append(
|
|
f"""{titre_competence} : """
|
|
+ ", ".join(
|
|
[
|
|
f"niveau {v.get('niveau',empty).get('ordre','?')} {v.get('code', '?')}"
|
|
for v in validations
|
|
]
|
|
)
|
|
)
|
|
return titres_rcues
|
|
|
|
|
|
class ValidationDUT120(ScoDocModel):
|
|
"""Validations du DUT 120
|
|
Ce diplôme est attribué sur demande aux étudiants de BUT ayant acquis les 120 ECTS
|
|
de BUT 1 et BUT 2.
|
|
"""
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
etudid = db.Column(
|
|
db.Integer,
|
|
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
|
index=True,
|
|
nullable=False,
|
|
)
|
|
formsemestre_id = db.Column(
|
|
db.Integer,
|
|
db.ForeignKey("notes_formsemestre.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
)
|
|
"""le semestre origine, dans la plupart des cas le S4 (le diplôme DUT120
|
|
apparaîtra sur les PV de ce formsemestre)"""
|
|
referentiel_competence_id = db.Column(
|
|
db.Integer, db.ForeignKey("apc_referentiel_competences.id"), nullable=False
|
|
) # pas de cascade, on ne doit pas supprimer un référentiel utilisé
|
|
"""Identifie la spécialité de DUT décernée"""
|
|
date = db.Column(
|
|
db.DateTime(timezone=True), server_default=db.func.now(), nullable=False
|
|
)
|
|
"""Date de délivrance"""
|
|
|
|
etud = db.relationship("Identite", backref="validations_dut120")
|
|
formsemestre = db.relationship("FormSemestre", backref="validations_dut120")
|
|
|
|
def __repr__(self):
|
|
return f"""<ValidationDUT120 {self.etud}>"""
|
|
|
|
def html(self) -> str:
|
|
"Affichage html"
|
|
date_str = (
|
|
f"""le {self.date.strftime(scu.DATEATIME_FMT)}"""
|
|
if self.date
|
|
else "(sans date)"
|
|
)
|
|
link = (
|
|
self.formsemestre.html_link_status(
|
|
label=f"{self.formsemestre.titre_formation(with_sem_idx=1)}",
|
|
title=self.formsemestre.titre_annee(),
|
|
)
|
|
if self.formsemestre
|
|
else "externe/antérieure"
|
|
)
|
|
specialite = (
|
|
self.formsemestre.formation.referentiel_competence.get_title()
|
|
if self.formsemestre.formation.referentiel_competence
|
|
else "(désassociée!)"
|
|
)
|
|
return f"""Diplôme de <b>DUT en 120 ECTS du {specialite}</b> émis par
|
|
{link}
|
|
{date_str}
|
|
"""
|