# -*- 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}: {self.code} enregistrée le {self.date.strftime(scu.DATEATIME_FMT)}""" 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 année BUT{self.ordre} émise par {link} : {self.code} {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: # Attention, certaines validations de RCUE peuvent ne plus être associées # à un niveau de compétence si l'UE a été déassociée (ce qui ne devrait pas être fait) competence_id = ( (validation.get("niveau") or {}).get("competence") or {} ).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") or {}).get("competence") or {}).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") or {}).get("competence", {})).get( "titre", "sans titre ! A vérifier !" ) titres_rcues.append( f"""{titre_competence} : """ + ", ".join( [ f"niveau {((v.get('niveau') or empty).get('ordre') or '?')} {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"""""" 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 DUT en 120 ECTS du {specialite} émis par {link} {date_str} """