forked from ScoDoc/ScoDoc
357 lines
13 KiB
Python
357 lines
13 KiB
Python
"""ScoDoc8 models : Formations (hors BUT)
|
|
"""
|
|
from typing import Any
|
|
|
|
from app import db
|
|
from app.models import APO_CODE_STR_LEN
|
|
from app.models import SHORT_STR_LEN
|
|
from app.scodoc import notesdb as ndb
|
|
from app.scodoc import sco_utils as scu
|
|
from app.scodoc.sco_utils import ModuleType
|
|
from app.scodoc import sco_codes_parcours
|
|
from app.comp import df_cache
|
|
|
|
|
|
class Formation(db.Model):
|
|
"""Programme pédagogique d'une formation"""
|
|
|
|
__tablename__ = "notes_formations"
|
|
__table_args__ = (db.UniqueConstraint("dept_id", "acronyme", "titre", "version"),)
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
formation_id = db.synonym("id")
|
|
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
|
|
|
|
acronyme = db.Column(db.Text(), nullable=False)
|
|
titre = db.Column(db.Text(), nullable=False)
|
|
titre_officiel = db.Column(db.Text(), nullable=False)
|
|
version = db.Column(db.Integer, default=1, server_default="1")
|
|
formation_code = db.Column(
|
|
db.String(SHORT_STR_LEN),
|
|
server_default=db.text("notes_newid_fcod()"),
|
|
nullable=False,
|
|
)
|
|
# nb: la fonction SQL notes_newid_fcod doit être créée à part
|
|
type_parcours = db.Column(db.Integer, default=0, server_default="0")
|
|
code_specialite = db.Column(db.String(SHORT_STR_LEN))
|
|
|
|
# Optionnel, pour les formations type BUT
|
|
referentiel_competence_id = db.Column(
|
|
db.Integer, db.ForeignKey("apc_referentiel_competences.id")
|
|
)
|
|
ues = db.relationship("UniteEns", backref="formation", lazy="dynamic")
|
|
formsemestres = db.relationship("FormSemestre", lazy="dynamic", backref="formation")
|
|
ues = db.relationship("UniteEns", lazy="dynamic", backref="formation")
|
|
modules = db.relationship("Module", lazy="dynamic", backref="formation")
|
|
|
|
def __repr__(self):
|
|
return f"<{self.__class__.__name__}(id={self.id}, dept_id={self.dept_id}, acronyme='{self.acronyme}')>"
|
|
|
|
def to_dict(self):
|
|
e = dict(self.__dict__)
|
|
e.pop("_sa_instance_state", None)
|
|
# ScoDoc7 output_formators: (backward compat)
|
|
e["formation_id"] = self.id
|
|
return e
|
|
|
|
def get_parcours(self):
|
|
"""get l'instance de TypeParcours de cette formation"""
|
|
return sco_codes_parcours.get_parcours_from_code(self.type_parcours)
|
|
|
|
def get_module_coefs(self, semestre_idx: int = None):
|
|
"""Les coefs des modules vers les UE (accès via cache)"""
|
|
from app.comp import moy_ue
|
|
|
|
if semestre_idx is None:
|
|
key = f"{self.id}"
|
|
else:
|
|
key = f"{self.id}.{semestre_idx}"
|
|
|
|
modules_coefficients = df_cache.ModuleCoefsCache.get(key)
|
|
if modules_coefficients is None:
|
|
modules_coefficients, _, _ = moy_ue.df_load_module_coefs(
|
|
self.id, semestre_idx
|
|
)
|
|
df_cache.ModuleCoefsCache.set(key, modules_coefficients)
|
|
return modules_coefficients
|
|
|
|
def invalidate_module_coefs(self, semestre_idx: int = None):
|
|
"""Invalide les coefficients de modules cachés.
|
|
Si semestre_idx est None, invalide tous les semestres,
|
|
sinon invalide le semestre indiqué et le cache de la formation.
|
|
"""
|
|
if semestre_idx is None:
|
|
keys = {f"{self.id}.{m.semestre_id}" for m in self.modules}
|
|
else:
|
|
keys = f"{self.id}.{semestre_idx}"
|
|
df_cache.ModuleCoefsCache.delete_many(keys | {f"{self.id}"})
|
|
|
|
|
|
class UniteEns(db.Model):
|
|
"""Unité d'Enseignement (UE)"""
|
|
|
|
__tablename__ = "notes_ue"
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
ue_id = db.synonym("id")
|
|
formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id"))
|
|
acronyme = db.Column(db.Text(), nullable=False)
|
|
numero = db.Column(db.Integer) # ordre de présentation
|
|
titre = db.Column(db.Text())
|
|
# Le semestre_idx n'est pas un id mais le numéro du semestre: 1, 2, ...
|
|
# En ScoDoc7 et pour les formations classiques, il est NULL
|
|
# (le numéro du semestre étant alors déterminé par celui des modules de l'UE)
|
|
# Pour les formations APC, il est obligatoire (de 1 à 6 pour le BUT):
|
|
semestre_idx = db.Column(db.Integer, nullable=True, index=True)
|
|
# Type d'UE: 0 normal ("fondamentale"), 1 "sport", 2 "projet et stage (LP)",
|
|
# 4 "élective"
|
|
type = db.Column(db.Integer, default=0, server_default="0")
|
|
# Les UE sont "compatibles" (pour la capitalisation) ssi elles ont ^m code
|
|
# note: la fonction SQL notes_newid_ucod doit être créée à part
|
|
ue_code = db.Column(
|
|
db.String(SHORT_STR_LEN),
|
|
server_default=db.text("notes_newid_ucod()"),
|
|
nullable=False,
|
|
)
|
|
ects = db.Column(db.Float) # nombre de credits ECTS
|
|
is_external = db.Column(db.Boolean(), default=False, server_default="false")
|
|
# id de l'element pedagogique Apogee correspondant:
|
|
code_apogee = db.Column(db.String(APO_CODE_STR_LEN))
|
|
# coef UE, utilise seulement si l'option use_ue_coefs est activée:
|
|
coefficient = db.Column(db.Float)
|
|
|
|
# relations
|
|
matieres = db.relationship("Matiere", lazy="dynamic", backref="ue")
|
|
modules = db.relationship("Module", lazy="dynamic", backref="ue")
|
|
|
|
def __repr__(self):
|
|
return f"<{self.__class__.__name__}(id={self.id}, formation_id={self.formation_id}, acronyme='{self.acronyme}')>"
|
|
|
|
def to_dict(self):
|
|
"""as a dict, with the same conversions as in ScoDoc7"""
|
|
e = dict(self.__dict__)
|
|
e.pop("_sa_instance_state", None)
|
|
# ScoDoc7 output_formators
|
|
e["ue_id"] = self.id
|
|
e["numero"] = e["numero"] if e["numero"] else 0
|
|
e["ects"] = e["ects"] if e["ects"] else 0.0
|
|
e["coefficient"] = e["coefficient"] if e["coefficient"] else 0.0
|
|
return e
|
|
|
|
def is_locked(self):
|
|
"""True if UE should not be modified
|
|
(contains modules used in a locked formsemestre)
|
|
"""
|
|
# XXX todo : à ré-écrire avec SQLAlchemy
|
|
from app.scodoc import sco_edit_ue
|
|
|
|
return sco_edit_ue.ue_is_locked(self.id)
|
|
|
|
def guess_semestre_idx(self) -> None:
|
|
"""Lorsqu'on prend une ancienne formation non APC,
|
|
les UE n'ont pas d'indication de semestre.
|
|
Cette méthode fixe le semestre en prenant celui du premier module,
|
|
ou à défaut le met à 1.
|
|
"""
|
|
if self.semestre_idx is None:
|
|
module = self.modules.first()
|
|
if module is None:
|
|
self.semestre_idx = 1
|
|
else:
|
|
self.semestre_idx = module.semestre_id
|
|
db.session.add(self)
|
|
db.session.commit()
|
|
|
|
|
|
class Matiere(db.Model):
|
|
"""Matières: regroupe les modules d'une UE
|
|
La matière a peu d'utilité en dehors de la présentation des modules
|
|
d'une UE.
|
|
"""
|
|
|
|
__tablename__ = "notes_matieres"
|
|
__table_args__ = (db.UniqueConstraint("ue_id", "titre"),)
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
matiere_id = db.synonym("id")
|
|
ue_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"))
|
|
titre = db.Column(db.Text())
|
|
numero = db.Column(db.Integer) # ordre de présentation
|
|
|
|
modules = db.relationship("Module", lazy="dynamic", backref="matiere")
|
|
|
|
|
|
class Module(db.Model):
|
|
"""Module"""
|
|
|
|
__tablename__ = "notes_modules"
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
module_id = db.synonym("id")
|
|
titre = db.Column(db.Text())
|
|
abbrev = db.Column(db.Text()) # nom court
|
|
# certains départements ont des codes infiniment longs: donc Text !
|
|
code = db.Column(db.Text(), nullable=False)
|
|
heures_cours = db.Column(db.Float)
|
|
heures_td = db.Column(db.Float)
|
|
heures_tp = db.Column(db.Float)
|
|
coefficient = db.Column(db.Float) # coef PPN (sauf en APC)
|
|
ects = db.Column(db.Float) # Crédits ECTS
|
|
ue_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), index=True)
|
|
formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id"))
|
|
matiere_id = db.Column(db.Integer, db.ForeignKey("notes_matieres.id"))
|
|
# pas un id mais le numéro du semestre: 1, 2, ...
|
|
semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1")
|
|
numero = db.Column(db.Integer) # ordre de présentation
|
|
# id de l'element pedagogique Apogee correspondant:
|
|
code_apogee = db.Column(db.String(APO_CODE_STR_LEN))
|
|
# Type: ModuleType: DEFAULT, MALUS, RESSOURCE, MODULE_SAE (enum)
|
|
module_type = db.Column(db.Integer)
|
|
# Relations:
|
|
modimpls = db.relationship("ModuleImpl", backref="module", lazy="dynamic")
|
|
ues_apc = db.relationship("UniteEns", secondary="module_ue_coef", viewonly=True)
|
|
tags = db.relationship(
|
|
"NotesTag",
|
|
secondary="notes_modules_tags",
|
|
lazy=True,
|
|
backref=db.backref("modules", lazy=True),
|
|
)
|
|
|
|
def __init__(self, **kwargs):
|
|
self.ue_coefs = []
|
|
super(Module, self).__init__(**kwargs)
|
|
|
|
def __repr__(self):
|
|
return (
|
|
f"<Module{ModuleType(self.module_type).name} id={self.id} code={self.code}>"
|
|
)
|
|
|
|
def to_dict(self):
|
|
e = dict(self.__dict__)
|
|
e.pop("_sa_instance_state", None)
|
|
# ScoDoc7 output_formators: (backward compat)
|
|
e["module_id"] = self.id
|
|
e["heures_cours"] = 0.0 if self.heures_cours is None else self.heures_cours
|
|
e["heures_td"] = 0.0 if self.heures_td is None else self.heures_td
|
|
e["heures_tp"] = 0.0 if self.heures_tp is None else self.heures_tp
|
|
e["numero"] = 0 if self.numero is None else self.numero
|
|
e["coefficient"] = 0.0 if self.coefficient is None else self.coefficient
|
|
e["module_type"] = 0 if self.module_type is None else self.module_type
|
|
return e
|
|
|
|
def is_apc(self):
|
|
"True si module SAÉ ou Ressource"
|
|
return self.module_type and scu.ModuleType(self.module_type) in {
|
|
scu.ModuleType.RESSOURCE,
|
|
scu.ModuleType.SAE,
|
|
}
|
|
|
|
def type_name(self):
|
|
return scu.MODULE_TYPE_NAMES[self.module_type]
|
|
|
|
def set_ue_coef(self, ue, coef: float) -> None:
|
|
"""Set coef module vers cette UE"""
|
|
self.update_ue_coef_dict({ue.id: coef})
|
|
|
|
def set_ue_coef_dict(self, ue_coef_dict: dict) -> None:
|
|
"""set coefs vers les UE (remplace existants)
|
|
ue_coef_dict = { ue_id : coef }
|
|
Les coefs nuls (zéro) ne sont pas stockés: la relation est supprimée.
|
|
"""
|
|
ue_coefs = []
|
|
for ue_id, coef in ue_coef_dict.items():
|
|
ue = UniteEns.query.get(ue_id)
|
|
if coef == 0.0:
|
|
self.delete_ue_coef(ue)
|
|
else:
|
|
ue_coefs.append(ModuleUECoef(module=self, ue=ue, coef=coef))
|
|
self.ue_coefs = ue_coefs
|
|
self.formation.invalidate_module_coefs()
|
|
|
|
def update_ue_coef_dict(self, ue_coef_dict: dict):
|
|
"""update coefs vers UE (ajoute aux existants)"""
|
|
current = self.get_ue_coef_dict()
|
|
current.update(ue_coef_dict)
|
|
self.set_ue_coef_dict(current)
|
|
|
|
def get_ue_coef_dict(self):
|
|
"""returns { ue_id : coef }"""
|
|
return {p.ue.id: p.coef for p in self.ue_coefs}
|
|
|
|
def delete_ue_coef(self, ue):
|
|
"""delete coef"""
|
|
ue_coef = ModuleUECoef.query.get((self.id, ue.id))
|
|
if ue_coef:
|
|
db.session.delete(ue_coef)
|
|
self.formation.invalidate_module_coefs()
|
|
|
|
def ue_coefs_descr(self):
|
|
"""List of tuples [ (ue_acronyme, coef) ]"""
|
|
return [(c.ue.acronyme, c.coef) for c in self.ue_coefs]
|
|
|
|
|
|
class ModuleUECoef(db.Model):
|
|
"""Coefficients des modules vers les UE (APC, BUT)
|
|
En mode APC, ces coefs remplacent le coefficient "PPN" du module.
|
|
"""
|
|
|
|
__tablename__ = "module_ue_coef"
|
|
|
|
module_id = db.Column(
|
|
db.Integer,
|
|
db.ForeignKey("notes_modules.id", ondelete="CASCADE"),
|
|
primary_key=True,
|
|
)
|
|
ue_id = db.Column(
|
|
db.Integer,
|
|
db.ForeignKey("notes_ue.id", ondelete="CASCADE"),
|
|
primary_key=True,
|
|
)
|
|
coef = db.Column(
|
|
db.Float,
|
|
nullable=False,
|
|
)
|
|
module = db.relationship(
|
|
Module,
|
|
backref=db.backref(
|
|
"ue_coefs",
|
|
passive_deletes=True,
|
|
cascade="save-update, merge, delete, delete-orphan",
|
|
),
|
|
)
|
|
ue = db.relationship(
|
|
UniteEns,
|
|
backref=db.backref(
|
|
"module_ue_coefs",
|
|
passive_deletes=True,
|
|
cascade="save-update, merge, delete, delete-orphan",
|
|
),
|
|
)
|
|
|
|
|
|
class NotesTag(db.Model):
|
|
"""Tag sur un module"""
|
|
|
|
__tablename__ = "notes_tags"
|
|
__table_args__ = (db.UniqueConstraint("title", "dept_id"),)
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
tag_id = db.synonym("id")
|
|
|
|
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
|
|
title = db.Column(db.Text(), nullable=False)
|
|
|
|
|
|
# Association tag <-> module
|
|
notes_modules_tags = db.Table(
|
|
"notes_modules_tags",
|
|
db.Column(
|
|
"tag_id",
|
|
db.Integer,
|
|
db.ForeignKey("notes_tags.id", ondelete="CASCADE"),
|
|
),
|
|
db.Column(
|
|
"module_id", db.Integer, db.ForeignKey("notes_modules.id", ondelete="CASCADE")
|
|
),
|
|
)
|