# -*- coding: UTF-8 -* """Décisions de jury (validations) des RCUE et années du BUT """ from typing import Union import flask_sqlalchemy from app import db from app.models import CODE_STR_LEN from app.models.but_refcomp import ApcNiveau from app.models.etudiants import Identite from app.models.formations import Formation from app.models.formsemestre import FormSemestre from app.models.ues import UniteEns from app.scodoc import codes_cursus as sco_codes from app.scodoc import sco_utils as scu class ApcValidationRCUE(db.Model): """Validation des niveaux de compétences aka "regroupements cohérents d'UE" dans le jargon BUT. Le formsemestre est celui du semestre PAIR du niveau de compétence """ __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 pair du RCUE" # Les deux UE associées à ce niveau: ue1_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False) ue2_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), 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("%d/%m/%Y")}""" def to_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("%d/%m/%Y")} à {self.date.strftime("%Hh%M")}""" 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: """Le niveau de compétence associé à cet RCUE.""" # Par convention, il est donné par la seconde UE return self.ue2.niveau_competence def to_dict(self): "as a dict" d = dict(self.__dict__) d.pop("_sa_instance_state", None) 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, } # Attention: ce n'est pas un modèle mais une classe ordinaire: class RegroupementCoherentUE: """Le regroupement cohérent d'UE, dans la terminologie du BUT, est le couple d'UEs de la même année (BUT1,2,3) liées au *même niveau de compétence*. La moyenne (10/20) au RCUE déclenche la compensation des UE. """ def __init__( self, etud: Identite, formsemestre_1: FormSemestre, dec_ue_1: "DecisionsProposeesUE", formsemestre_2: FormSemestre, dec_ue_2: "DecisionsProposeesUE", inscription_etat: str, ): ue_1 = dec_ue_1.ue ue_2 = dec_ue_2.ue # Ordonne les UE dans le sens croissant (S1,S2) ou (S3,S4)... if formsemestre_1.semestre_id > formsemestre_2.semestre_id: (ue_1, formsemestre_1), (ue_2, formsemestre_2) = ( (ue_2, formsemestre_2), (ue_1, formsemestre_1), ) assert formsemestre_1.semestre_id % 2 == 1 assert formsemestre_2.semestre_id % 2 == 0 assert abs(formsemestre_1.semestre_id - formsemestre_2.semestre_id) == 1 assert ue_1.niveau_competence_id == ue_2.niveau_competence_id self.etud = etud self.formsemestre_1 = formsemestre_1 "semestre impair" self.ue_1 = ue_1 self.formsemestre_2 = formsemestre_2 "semestre pair" self.ue_2 = ue_2 # Stocke les moyennes d'UE if inscription_etat != scu.INSCRIT: self.moy_rcue = None self.moy_ue_1 = self.moy_ue_2 = "-" self.moy_ue_1_val = self.moy_ue_2_val = 0.0 return self.moy_ue_1 = dec_ue_1.moy_ue_with_cap self.moy_ue_1_val = self.moy_ue_1 if self.moy_ue_1 is not None else 0.0 self.moy_ue_2 = dec_ue_2.moy_ue_with_cap self.moy_ue_2_val = self.moy_ue_2 if self.moy_ue_2 is not None else 0.0 # Calcul de la moyenne au RCUE (utilise les moy d'UE capitalisées) if (self.moy_ue_1 is not None) and (self.moy_ue_2 is not None): # Moyenne RCUE (les pondérations par défaut sont 1.) self.moy_rcue = ( self.moy_ue_1 * ue_1.coef_rcue + self.moy_ue_2 * ue_2.coef_rcue ) / (ue_1.coef_rcue + ue_2.coef_rcue) else: self.moy_rcue = None def __repr__(self) -> str: return f"""<{self.__class__.__name__} { self.ue_1.acronyme}({self.moy_ue_1}) { self.ue_2.acronyme}({self.moy_ue_2})>""" def __str__(self) -> str: return f"""RCUE { self.ue_1.acronyme}({self.moy_ue_1}) + { self.ue_2.acronyme}({self.moy_ue_2})""" def query_validations( self, ) -> flask_sqlalchemy.BaseQuery: # list[ApcValidationRCUE] """Les validations de jury enregistrées pour ce RCUE""" niveau = self.ue_2.niveau_competence return ( ApcValidationRCUE.query.filter_by( etudid=self.etud.id, ) .join(UniteEns, UniteEns.id == ApcValidationRCUE.ue2_id) .join(ApcNiveau, UniteEns.niveau_competence_id == ApcNiveau.id) .filter(ApcNiveau.id == niveau.id) ) def other_ue(self, ue: UniteEns) -> UniteEns: """L'autre UE du regroupement. Si ue ne fait pas partie du regroupement, ValueError""" if ue.id == self.ue_1.id: return self.ue_2 elif ue.id == self.ue_2.id: return self.ue_1 raise ValueError(f"ue {ue} hors RCUE {self}") def est_enregistre(self) -> bool: """Vrai si ce RCUE, donc le niveau de compétences correspondant a une décision jury enregistrée """ return self.query_validations().count() > 0 def est_compensable(self): """Vrai si ce RCUE est validable (uniquement) par compensation c'est à dire que sa moyenne est > 10 avec une UE < 10. Note: si ADM, est_compensable est faux. """ return ( (self.moy_rcue is not None) and (self.moy_rcue > sco_codes.BUT_BARRE_RCUE) and ( (self.moy_ue_1_val < sco_codes.NOTES_BARRE_GEN) or (self.moy_ue_2_val < sco_codes.NOTES_BARRE_GEN) ) ) def est_suffisant(self) -> bool: """Vrai si ce RCUE est > 8""" return (self.moy_rcue is not None) and ( self.moy_rcue > sco_codes.BUT_RCUE_SUFFISANT ) def est_validable(self) -> bool: """Vrai si ce RCUE satisfait les conditions pour être validé, c'est à dire que la moyenne des UE qui le constituent soit > 10 """ return (self.moy_rcue is not None) and ( self.moy_rcue > sco_codes.BUT_BARRE_RCUE ) def code_valide(self) -> Union[ApcValidationRCUE, None]: "Si ce RCUE est ADM, CMP ou ADJ, la validation. Sinon, None" validation = self.query_validations().first() if (validation is not None) and ( validation.code in sco_codes.CODES_RCUE_VALIDES ): return validation return None # unused # def find_rcues( # formsemestre: FormSemestre, ue: UniteEns, etud: Identite, inscription_etat: str # ) -> list[RegroupementCoherentUE]: # """Les RCUE (niveau de compétence) à considérer pour cet étudiant dans # ce semestre pour cette UE. # Cherche les UEs du même niveau de compétence auxquelles l'étudiant est inscrit. # En cas de redoublement, il peut y en avoir plusieurs, donc plusieurs RCUEs. # Résultat: la liste peut être vide. # """ # if (ue.niveau_competence is None) or (ue.semestre_idx is None): # return [] # if ue.semestre_idx % 2: # S1, S3, S5 # other_semestre_idx = ue.semestre_idx + 1 # else: # other_semestre_idx = ue.semestre_idx - 1 # cursor = db.session.execute( # text( # """SELECT # ue.id, formsemestre.id # FROM # notes_ue ue, # notes_formsemestre_inscription inscr, # notes_formsemestre formsemestre # WHERE # inscr.etudid = :etudid # AND inscr.formsemestre_id = formsemestre.id # AND formsemestre.semestre_id = :other_semestre_idx # AND ue.formation_id = formsemestre.formation_id # AND ue.niveau_competence_id = :ue_niveau_competence_id # AND ue.semestre_idx = :other_semestre_idx # """ # ), # { # "etudid": etud.id, # "other_semestre_idx": other_semestre_idx, # "ue_niveau_competence_id": ue.niveau_competence_id, # }, # ) # rcues = [] # for ue_id, formsemestre_id in cursor: # other_ue = UniteEns.query.get(ue_id) # other_formsemestre = FormSemestre.get_formsemestre(formsemestre_id) # rcues.append( # RegroupementCoherentUE( # etud, formsemestre, ue, other_formsemestre, other_ue, inscription_etat # ) # ) # # safety check: 1 seul niveau de comp. concerné: # assert len({rcue.ue_1.niveau_competence_id for rcue in rcues}) == 1 # return rcues class ApcValidationAnnee(db.Model): """Validation des années du BUT""" __tablename__ = "apc_validation_annee" # Assure unicité de la décision: __table_args__ = (db.UniqueConstraint("etudid", "annee_scolaire", "ordre"),) 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 IMPAIR (le 1er) de l'année" annee_scolaire = db.Column(db.Integer, nullable=False) # 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(), "code": self.code, "ordre": self.ordre, } 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 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 ) decisions["decision_rcue"] = [v.to_dict_bul() for v in validations_rcues] titres_rcues = [] for dec_rcue in decisions["decision_rcue"]: niveau = dec_rcue["niveau"] if niveau is None: titres_rcues.append(f"""pas de compétence: code {dec_rcue["code"]}""") else: titres_rcues.append( f"""{niveau["competence"]["titre"]} {niveau["ordre"]}: { dec_rcue["code"]}""" ) decisions["descr_decisions_rcue"] = ", ".join(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 de ce semestre validation = ( ApcValidationAnnee.query.filter_by( etudid=etud.id, annee_scolaire=formsemestre.annee_scolaire(), ) .join(ApcValidationAnnee.formsemestre) .join(FormSemestre.formation) .filter(Formation.formation_code == formsemestre.formation.formation_code) .first() ) if validation: decisions["decision_annee"] = validation.to_dict_bul() else: decisions["decision_annee"] = None return decisions