ScoDoc-PE/app/models/formsemestre.py

466 lines
16 KiB
Python

# -*- coding: UTF-8 -*
"""ScoDoc models: formsemestre
"""
import datetime
from functools import cached_property
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 contient_periode(self, date_debut, date_fin) -> bool:
"""Vrai si l'intervalle [date_debut, date_fin] est
inclus 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[Identite]:
"""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]
@cached_property
def etuds_inscriptions(self) -> dict:
"""Map { etudid : inscription }"""
return {ins.etud.id: ins for ins in self.inscriptions}
# 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"<Etape {self.id} apo={self.etape_apo}>"
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"),
)