# -*- coding: UTF-8 -* """ScoDoc models: formsemestre """ import datetime import flask_sqlalchemy from app import db from app.models import APO_CODE_STR_LEN from app.models import SHORT_STR_LEN from app.models import CODE_STR_LEN from app.models import UniteEns import app.scodoc.sco_utils as scu from app.models.ues import UniteEns from app.models.modules import Module from app.models.moduleimpls import ModuleImpl from app.models.etudiants import Identite from app.scodoc import sco_codes_parcours from app.scodoc import sco_preferences from app.scodoc.sco_vdi import ApoEtapeVDI class FormSemestre(db.Model): """Mise en oeuvre d'un semestre de formation""" __tablename__ = "notes_formsemestre" id = db.Column(db.Integer, primary_key=True) formsemestre_id = db.synonym("id") # dept_id est aussi dans la formation, ajouté ici pour # simplifier et accélérer les selects dans notesdb dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True) formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id")) semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1") titre = db.Column(db.Text()) date_debut = db.Column(db.Date()) date_fin = db.Column(db.Date()) etat = db.Column( db.Boolean(), nullable=False, default=True, server_default="true" ) # False si verrouillé modalite = db.Column( db.String(SHORT_STR_LEN), db.ForeignKey("notes_form_modalites.modalite") ) # "FI", "FAP", "FC", ... # gestion compensation sem DUT: gestion_compensation = db.Column( db.Boolean(), nullable=False, default=False, server_default="false" ) # ne publie pas le bulletin XML: bul_hide_xml = db.Column( db.Boolean(), nullable=False, default=False, server_default="false" ) # Bloque le calcul des moyennes (générale et d'UE) block_moyennes = db.Column( db.Boolean(), nullable=False, default=False, server_default="false" ) # semestres decales (pour gestion jurys): gestion_semestrielle = db.Column( db.Boolean(), nullable=False, default=False, server_default="false" ) # couleur fond bulletins HTML: bul_bgcolor = db.Column( db.String(SHORT_STR_LEN), default="white", server_default="white" ) # autorise resp. a modifier semestre: resp_can_edit = db.Column( db.Boolean(), nullable=False, default=False, server_default="false" ) # autorise resp. a modifier slt les enseignants: resp_can_change_ens = db.Column( db.Boolean(), nullable=False, default=True, server_default="true" ) # autorise les ens a creer des evals: ens_can_edit_eval = db.Column( db.Boolean(), nullable=False, default=False, server_default="False" ) # code element semestre Apogee, eg 'VRTW1' ou 'V2INCS4,V2INLS4,...' elt_sem_apo = db.Column(db.Text()) # peut être fort long ! # code element annee Apogee, eg 'VRT1A' ou 'V2INLA,V2INCA,...' elt_annee_apo = db.Column(db.Text()) # Relations: etapes = db.relationship( "FormSemestreEtape", cascade="all,delete", backref="formsemestre" ) modimpls = db.relationship("ModuleImpl", backref="formsemestre", lazy="dynamic") etuds = db.relationship( "Identite", secondary="notes_formsemestre_inscription", viewonly=True, lazy="dynamic", ) responsables = db.relationship( "User", secondary="notes_formsemestre_responsables", lazy=True, backref=db.backref("formsemestres", lazy=True), ) # Ancien id ScoDoc7 pour les migrations de bases anciennes # ne pas utiliser après migrate_scodoc7_dept_archives scodoc7_id = db.Column(db.Text(), nullable=True) def __init__(self, **kwargs): super(FormSemestre, self).__init__(**kwargs) if self.modalite is None: self.modalite = FormationModalite.DEFAULT_MODALITE def to_dict(self): d = dict(self.__dict__) d.pop("_sa_instance_state", None) # ScoDoc7 output_formators: (backward compat) d["formsemestre_id"] = self.id d["date_debut"] = ( self.date_debut.strftime("%d/%m/%Y") if self.date_debut else "" ) d["date_fin"] = self.date_fin.strftime("%d/%m/%Y") if self.date_fin else "" d["responsables"] = [u.id for u in self.responsables] return d def query_ues(self, with_sport=False) -> flask_sqlalchemy.BaseQuery: """UE des modules de ce semestre, triées par numéro. - Formations classiques: les UEs auxquelles appartiennent les modules mis en place dans ce semestre. - Formations APC / BUT: les UEs de la formation qui ont le même numéro de semestre que ce formsemestre. """ if self.formation.get_parcours().APC_SAE: sem_ues = UniteEns.query.filter_by( formation=self.formation, semestre_idx=self.semestre_id ) else: sem_ues = db.session.query(UniteEns).filter( ModuleImpl.formsemestre_id == self.id, Module.id == ModuleImpl.module_id, UniteEns.id == Module.ue_id, ) if not with_sport: sem_ues = sem_ues.filter(UniteEns.type != sco_codes_parcours.UE_SPORT) return sem_ues.order_by(UniteEns.numero) def est_courant(self) -> bool: """Vrai si la date actuelle (now) est dans le semestre (les dates de début et fin sont incluses) """ today = datetime.date.today() return (self.date_debut <= today) and (today <= self.date_fin) def est_courant_date(self, date_debut, date_fin) -> bool: """Vrai si date_debut et date_fin est dans le semestre (les dates de début et fin sont incluses) """ return (self.date_debut <= date_debut) and (date_fin <= self.date_fin) def est_decale(self): """Vrai si semestre "décalé" c'est à dire semestres impairs commençant entre janvier et juin et les pairs entre juillet et decembre """ if self.semestre_id <= 0: return False # formations sans semestres return (self.semestre_id % 2 and self.date_debut.month <= 6) or ( not self.semestre_id % 2 and self.date_debut.month > 6 ) def etapes_apo_str(self) -> str: """Chaine décrivant les étapes de ce semestre ex: "V1RT, V1RT3, V1RT4" """ if not self.etapes: return "" return ", ".join([str(x.etape_apo) for x in self.etapes]) def responsables_str(self, abbrev_prenom=True) -> str: """chaîne "J. Dupond, X. Martin" ou "Jacques Dupond, Xavier Martin" """ if not self.responsables: return "" if abbrev_prenom: return ", ".join([u.get_prenomnom() for u in self.responsables]) else: return ", ".join([u.get_nomcomplet() for u in self.responsables]) def annee_scolaire_str(self): "2021 - 2022" return scu.annee_scolaire_repr(self.date_debut.year, self.date_debut.month) def session_id(self) -> str: """identifiant externe de semestre de formation Exemple: RT-DUT-FI-S1-ANNEE DEPT-TYPE-MODALITE+-S?|SPECIALITE TYPE=DUT|LP*|M* MODALITE=FC|FI|FA (si plusieurs, en inverse alpha) SPECIALITE=[A-Z]+ EON,ASSUR, ... (si pas Sn ou SnD) ANNEE=annee universitaire de debut (exemple: un S2 de 2013-2014 sera S2-2013) """ imputation_dept = sco_preferences.get_preference("ImputationDept", self.id) if not imputation_dept: imputation_dept = sco_preferences.get_preference("DeptName") imputation_dept = imputation_dept.upper() parcours_name = self.formation.get_parcours().NAME modalite = self.modalite # exception pour code Apprentissage: modalite = (modalite or "").replace("FAP", "FA").replace("APP", "FA") if self.semestre_id > 0: decale = "D" if self.est_decale() else "" semestre_id = f"S{self.semestre_id}{decale}" else: semestre_id = self.formation.code_specialite or "" annee_sco = str( scu.annee_scolaire_debut(self.date_debut.year, self.date_debut.month) ) return scu.sanitize_string( "-".join((imputation_dept, parcours_name, modalite, semestre_id, annee_sco)) ) def titre_mois(self) -> str: """Le titre et les dates du semestre, pour affichage dans des listes Ex: "BUT QLIO (PN 2022) semestre 1 FI (Sept 2022 - Jan 2023)" """ return f"""{self.titre_num()} {self.modalite or ''} ({ scu.MONTH_NAMES_ABBREV[self.date_debut.month-1]} { self.date_debut.year} - { scu.MONTH_NAMES_ABBREV[self.date_fin.month -1]} { self.date_fin.year})""" def titre_num(self) -> str: """Le titre est le semestre, ex ""DUT Informatique semestre 2"" """ if self.semestre_id == sco_codes_parcours.NO_SEMESTRE_ID: return self.titre return f"{self.titre} {self.formation.get_parcours().SESSION_NAME} {self.semestre_id}" def get_abs_count(self, etudid): """Les comptes d'absences de cet étudiant dans ce semestre: tuple (nb abs non justifiées, nb abs justifiées) Utilise un cache. """ from app.scodoc import sco_abs return sco_abs.get_abs_count_in_interval( etudid, self.date_debut.isoformat(), self.date_fin.isoformat() ) def get_inscrits(self, include_dem=False) -> list: """Liste des étudiants inscrits à ce semestre Si all, tous les étudiants, avec les démissionnaires. """ if include_dem: return [ins.etud for ins in self.inscriptions] else: return [ins.etud for ins in self.inscriptions if ins.etat == scu.INSCRIT] # Association id des utilisateurs responsables (aka directeurs des etudes) du semestre notes_formsemestre_responsables = db.Table( "notes_formsemestre_responsables", db.Column( "formsemestre_id", db.Integer, db.ForeignKey("notes_formsemestre.id"), ), db.Column("responsable_id", db.Integer, db.ForeignKey("user.id")), ) class FormSemestreEtape(db.Model): """Étape Apogée associées au semestre""" __tablename__ = "notes_formsemestre_etapes" id = db.Column(db.Integer, primary_key=True) formsemestre_id = db.Column( db.Integer, db.ForeignKey("notes_formsemestre.id"), ) etape_apo = db.Column(db.String(APO_CODE_STR_LEN), index=True) def __repr__(self): return f"" def as_apovdi(self): return ApoEtapeVDI(self.etape_apo) class FormationModalite(db.Model): """Modalités de formation, utilisées pour la présentation (grouper les semestres, générer des codes, etc.) """ __tablename__ = "notes_form_modalites" DEFAULT_MODALITE = "FI" id = db.Column(db.Integer, primary_key=True) modalite = db.Column( db.String(SHORT_STR_LEN), unique=True, index=True, default=DEFAULT_MODALITE, server_default=DEFAULT_MODALITE, ) # code titre = db.Column(db.Text()) # texte explicatif # numero = ordre de presentation) numero = db.Column(db.Integer) @staticmethod def insert_modalites(): """Create default modalities""" numero = 0 try: for (code, titre) in ( (FormationModalite.DEFAULT_MODALITE, "Formation Initiale"), ("FAP", "Apprentissage"), ("FC", "Formation Continue"), ("DEC", "Formation Décalées"), ("LIC", "Licence"), ("CPRO", "Contrats de Professionnalisation"), ("DIST", "À distance"), ("ETR", "À l'étranger"), ("EXT", "Extérieur"), ("OTHER", "Autres formations"), ): modalite = FormationModalite.query.filter_by(modalite=code).first() if modalite is None: modalite = FormationModalite( modalite=code, titre=titre, numero=numero ) db.session.add(modalite) numero += 1 db.session.commit() except: db.session.rollback() raise class FormSemestreUECoef(db.Model): """Coef des UE capitalisees arrivant dans ce semestre""" __tablename__ = "notes_formsemestre_uecoef" __table_args__ = (db.UniqueConstraint("formsemestre_id", "ue_id"),) id = db.Column(db.Integer, primary_key=True) formsemestre_uecoef_id = db.synonym("id") formsemestre_id = db.Column( db.Integer, db.ForeignKey("notes_formsemestre.id"), ) ue_id = db.Column( db.Integer, db.ForeignKey("notes_ue.id"), ) coefficient = db.Column(db.Float, nullable=False) class FormSemestreUEComputationExpr(db.Model): """Formules utilisateurs pour calcul moyenne UE""" __tablename__ = "notes_formsemestre_ue_computation_expr" __table_args__ = (db.UniqueConstraint("formsemestre_id", "ue_id"),) id = db.Column(db.Integer, primary_key=True) notes_formsemestre_ue_computation_expr_id = db.synonym("id") formsemestre_id = db.Column( db.Integer, db.ForeignKey("notes_formsemestre.id"), ) ue_id = db.Column( db.Integer, db.ForeignKey("notes_ue.id"), ) # formule de calcul moyenne computation_expr = db.Column(db.Text()) class FormSemestreCustomMenu(db.Model): """Menu custom associe au semestre""" __tablename__ = "notes_formsemestre_custommenu" id = db.Column(db.Integer, primary_key=True) custommenu_id = db.synonym("id") formsemestre_id = db.Column( db.Integer, db.ForeignKey("notes_formsemestre.id"), ) title = db.Column(db.Text()) url = db.Column(db.Text()) idx = db.Column(db.Integer, default=0, server_default="0") # rang dans le menu class FormSemestreInscription(db.Model): """Inscription à un semestre de formation""" __tablename__ = "notes_formsemestre_inscription" __table_args__ = (db.UniqueConstraint("formsemestre_id", "etudid"),) id = db.Column(db.Integer, primary_key=True) formsemestre_inscription_id = db.synonym("id") etudid = db.Column(db.Integer, db.ForeignKey("identite.id"), index=True) formsemestre_id = db.Column( db.Integer, db.ForeignKey("notes_formsemestre.id"), index=True, ) etud = db.relationship( Identite, backref=db.backref("formsemestre_inscriptions", cascade="all, delete-orphan"), ) formsemestre = db.relationship( FormSemestre, backref=db.backref( "inscriptions", cascade="all, delete-orphan", order_by="FormSemestreInscription.etudid", ), ) # I inscrit, D demission en cours de semestre, DEF si "defaillant" etat = db.Column(db.String(CODE_STR_LEN), index=True) # etape apogee d'inscription (experimental 2020) etape = db.Column(db.String(APO_CODE_STR_LEN)) class NotesSemSet(db.Model): """semsets: ensemble de formsemestres pour exports Apogée""" __tablename__ = "notes_semset" id = db.Column(db.Integer, primary_key=True) semset_id = db.synonym("id") dept_id = db.Column(db.Integer, db.ForeignKey("departement.id")) title = db.Column(db.Text) annee_scolaire = db.Column(db.Integer, nullable=True, default=None) # periode: 0 (année), 1 (Simpair), 2 (Spair) sem_id = db.Column(db.Integer, nullable=True, default=None) # Association: many to many notes_semset_formsemestre = db.Table( "notes_semset_formsemestre", db.Column("formsemestre_id", db.Integer, db.ForeignKey("notes_formsemestre.id")), db.Column( "semset_id", db.Integer, db.ForeignKey("notes_semset.id", ondelete="CASCADE"), nullable=False, ), db.UniqueConstraint("formsemestre_id", "semset_id"), )