ScoDoc-Lille/app/models/but_refcomp.py

606 lines
20 KiB
Python

##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""ScoDoc 9 models : Référentiel Compétence BUT 2021
"""
from datetime import datetime
import flask_sqlalchemy
from sqlalchemy.orm import class_mapper
import sqlalchemy
from app import db
from app.scodoc.sco_utils import ModuleType
from app.scodoc.sco_exceptions import ScoValueError
# from https://stackoverflow.com/questions/2537471/method-of-iterating-over-sqlalchemy-models-defined-columns
def attribute_names(cls):
"liste ids (noms de colonnes) d'un modèle"
return [
prop.key
for prop in class_mapper(cls).iterate_properties
if isinstance(prop, sqlalchemy.orm.ColumnProperty)
]
class XMLModel:
"""Mixin class, to ease loading Orebut XMLs"""
_xml_attribs = {} # to be overloaded
id = "_"
@classmethod
def attr_from_xml(cls, args: dict) -> dict:
"""dict with attributes imported from Orébut XML
and renamed for our models.
The mapping is specified by the _xml_attribs
attribute in each model class.
Keep only attributes corresponding to columns in our model:
other XML attributes are simply ignored.
"""
columns = attribute_names(cls)
renamed_attributes = {cls._xml_attribs.get(k, k): v for (k, v) in args.items()}
return {k: renamed_attributes[k] for k in renamed_attributes if k in columns}
def __repr__(self):
return f'<{self.__class__.__name__} {self.id} "{self.titre if hasattr(self, "titre") else ""}">'
class ApcReferentielCompetences(db.Model, XMLModel):
"Référentiel de compétence d'une spécialité"
id = db.Column(db.Integer, primary_key=True)
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
annexe = db.Column(db.Text())
specialite = db.Column(db.Text())
specialite_long = db.Column(db.Text())
type_titre = db.Column(db.Text())
type_structure = db.Column(db.Text())
type_departement = db.Column(db.Text()) # "secondaire", "tertiaire"
version_orebut = db.Column(db.Text())
_xml_attribs = { # Orébut xml attrib : attribute
"type": "type_titre",
"version": "version_orebut",
}
# ScoDoc specific fields:
scodoc_date_loaded = db.Column(db.DateTime, default=datetime.utcnow)
scodoc_orig_filename = db.Column(db.Text())
# Relations:
competences = db.relationship(
"ApcCompetence",
backref="referentiel",
lazy="dynamic",
cascade="all, delete-orphan",
)
parcours = db.relationship(
"ApcParcours",
backref="referentiel",
lazy="dynamic",
cascade="all, delete-orphan",
)
formations = db.relationship("Formation", backref="referentiel_competence")
def __init__(
self,
id,
dept_id,
annexe,
specialite,
specialite_long,
type_titre,
type_structure,
type_departement,
version_orebut,
_xml_attribs,
#scodoc_date_loaded,
scodoc_orig_filename,
# competences,
# parcours,
# formations,
):
self.id = id
self.dept_id = dept_id
self.annexe = annexe
self.specialite = specialite
self.specialite_long = specialite_long
self.type_titre = type_titre
self.type_structure = type_structure
self.type_departement = type_departement
self.version_orebut = version_orebut
self._xml_attribs = _xml_attribs
#self.scodoc_date_loaded = scodoc_date_loaded
self.scodoc_orig_filename = scodoc_orig_filename
# self.competences = competences
# self.parcours = parcours
# self.formations = formations
def __repr__(self):
return f"<ApcReferentielCompetences {self.id} {self.specialite!r}>"
def to_dict(self):
"""Représentation complète du ref. de comp.
comme un dict.
"""
return {
"dept_id": self.dept_id,
"annexe": self.annexe,
"specialite": self.specialite,
"specialite_long": self.specialite_long,
"type_structure": self.type_structure,
"type_departement": self.type_departement,
"type_titre": self.type_titre,
"version_orebut": self.version_orebut,
"scodoc_date_loaded": self.scodoc_date_loaded.isoformat() + "Z"
if self.scodoc_date_loaded
else "",
"scodoc_orig_filename": self.scodoc_orig_filename,
"competences": {x.titre: x.to_dict() for x in self.competences},
"parcours": {x.code: x.to_dict() for x in self.parcours},
}
def get_niveaux_by_parcours(self, annee) -> dict:
"""
Construit la liste des niveaux de compétences pour chaque parcours
de ce référentiel.
Les niveaux sont groupés par parcours, en isolant les niveaux de tronc commun.
Le tronc commun n'est pas identifié comme tel dans les référentiels Orébut:
on cherche les niveaux qui sont présents dans tous les parcours et les range sous
la clé "TC" (toujours présente mais éventuellement liste vide si pas de tronc commun).
résultat:
{
"TC" : [ ApcNiveau ],
parcour.id : [ ApcNiveau ]
}
"""
parcours = self.parcours.order_by(ApcParcours.numero).all()
niveaux_by_parcours = {
parcour.id: ApcNiveau.niveaux_annee_de_parcours(parcour, annee, self)
for parcour in parcours
}
# Cherche tronc commun
niveaux_ids_tc = set.intersection(
*[
{n.id for n in niveaux_by_parcours[parcour_id]}
for parcour_id in niveaux_by_parcours
]
)
# Enleve les niveaux du tronc commun
niveaux_by_parcours_no_tc = {
parcour.id: [
niveau
for niveau in niveaux_by_parcours[parcour.id]
if niveau.id not in niveaux_ids_tc
]
for parcour in parcours
}
# Niveaux du TC
niveaux_tc = []
if len(parcours):
niveaux_parcours_1 = niveaux_by_parcours[parcours[0].id]
niveaux_tc = [
niveau for niveau in niveaux_parcours_1 if niveau.id in niveaux_ids_tc
]
niveaux_by_parcours_no_tc["TC"] = niveaux_tc
return niveaux_by_parcours_no_tc
class ApcCompetence(db.Model, XMLModel):
"Compétence"
id = db.Column(db.Integer, primary_key=True)
referentiel_id = db.Column(
db.Integer, db.ForeignKey("apc_referentiel_competences.id"), nullable=False
)
# les compétences dans Orébut sont identifiées par leur id unique
# (mais id_orebut n'est pas unique car le même ref. pourra être chargé dans plusieurs depts)
id_orebut = db.Column(db.Text(), nullable=True, index=True)
titre = db.Column(db.Text(), nullable=False, index=True)
titre_long = db.Column(db.Text())
couleur = db.Column(db.Text())
numero = db.Column(db.Integer) # ordre de présentation
_xml_attribs = { # xml_attrib : attribute
"id": "id_orebut",
"nom_court": "titre", # was name
"libelle_long": "titre_long",
}
situations = db.relationship(
"ApcSituationPro",
backref="competence",
lazy="dynamic",
cascade="all, delete-orphan",
)
composantes_essentielles = db.relationship(
"ApcComposanteEssentielle",
backref="competence",
lazy="dynamic",
cascade="all, delete-orphan",
)
niveaux = db.relationship(
"ApcNiveau",
backref="competence",
lazy="dynamic",
cascade="all, delete-orphan",
)
def __init__(
self,
id,
referentiel_id,
id_orebut,
titre,
titre_long,
couleur,
numero,
_xml_attribs,
situations,
composantes_essentielles,
niveaux,
):
self.id = id
self.referentiel_id = referentiel_id
self.id_orebut = id_orebut
self.titre = titre
self.titre_long = titre_long
self.couleur = couleur
self.numero = numero
self._xml_attribs = _xml_attribs
self.situations = situations
self.composantes_essentielles = composantes_essentielles
self.niveaux = niveaux
def __repr__(self):
return f"<ApcCompetence {self.id} {self.titre!r}>"
def to_dict(self):
"repr dict recursive sur situations, composantes, niveaux"
return {
"id_orebut": self.id_orebut,
"titre": self.titre,
"titre_long": self.titre_long,
"couleur": self.couleur,
"numero": self.numero,
"situations": [x.to_dict() for x in self.situations],
"composantes_essentielles": [
x.to_dict() for x in self.composantes_essentielles
],
"niveaux": {x.annee: x.to_dict() for x in self.niveaux},
}
def to_dict_bul(self) -> dict:
"dict court pour bulletins"
return {
"id_orebut": self.id_orebut,
"titre": self.titre,
"titre_long": self.titre_long,
"couleur": self.couleur,
"numero": self.numero,
}
class ApcSituationPro(db.Model, XMLModel):
"Situation professionnelle"
id = db.Column(db.Integer, primary_key=True)
competence_id = db.Column(
db.Integer, db.ForeignKey("apc_competence.id"), nullable=False
)
libelle = db.Column(db.Text(), nullable=False)
# aucun attribut (le text devient le libellé)
def __init__(self, id, competence_id, libelle):
self.id = id
self.competence_id = competence_id
self.libelle = libelle
def to_dict(self):
return {"libelle": self.libelle}
class ApcComposanteEssentielle(db.Model, XMLModel):
"Composante essentielle"
id = db.Column(db.Integer, primary_key=True)
competence_id = db.Column(
db.Integer, db.ForeignKey("apc_competence.id"), nullable=False
)
libelle = db.Column(db.Text(), nullable=False)
def __init__(self, id, competence_id, libelle):
self.id = id
self.competence_id = competence_id
self.libelle = libelle
def to_dict(self):
return {"libelle": self.libelle}
class ApcNiveau(db.Model, XMLModel):
"""Niveau de compétence
Chaque niveau peut être associé à deux UE,
des semestres impair et pair de la même année.
"""
__tablename__ = "apc_niveau"
id = db.Column(db.Integer, primary_key=True)
competence_id = db.Column(
db.Integer, db.ForeignKey("apc_competence.id"), nullable=False
)
libelle = db.Column(db.Text(), nullable=False)
annee = db.Column(db.Text(), nullable=False) # "BUT1", "BUT2", "BUT3"
# L'ordre est le niveau (1,2,3) ou (1,2) suivant la competence
ordre = db.Column(db.Integer, nullable=False) # 1, 2, 3
app_critiques = db.relationship(
"ApcAppCritique",
backref="niveau",
lazy="dynamic",
cascade="all, delete-orphan",
)
ues = db.relationship("UniteEns", back_populates="niveau_competence")
def __init__(self, id, competence_id, libelle, annee, ordre, app_critiques):
self.id = id
self.competence_id = competence_id
self.libelle = libelle
self.annee = annee
self.ordre = ordre
self.app_critiques = app_critiques
def __repr__(self):
return f"""<{self.__class__.__name__} ordre={self.ordre!r} annee={
self.annee!r} {self.competence!r}>"""
def to_dict(self):
"as a dict, recursif sur les AC"
return {
"libelle": self.libelle,
"annee": self.annee,
"ordre": self.ordre,
"app_critiques": {x.code: x.to_dict() for x in self.app_critiques},
}
def to_dict_bul(self):
"dict pour bulletins: indique la compétence, pas les ACs (pour l'instant ?)"
return {
"libelle": self.libelle,
"annee": self.annee,
"ordre": self.ordre,
"competence": self.competence.to_dict_bul(),
}
@classmethod
def niveaux_annee_de_parcours(
cls,
parcour: "ApcParcours",
annee: int,
referentiel_competence: ApcReferentielCompetences = None,
) -> flask_sqlalchemy.BaseQuery:
"""Les niveaux de l'année du parcours
Si le parcour est None, tous les niveaux de l'année
"""
if annee not in {1, 2, 3}:
raise ValueError("annee invalide pour un parcours BUT")
if referentiel_competence is None:
raise ScoValueError(
"Pas de référentiel de compétences associé à la formation !"
)
annee_formation = f"BUT{annee}"
if parcour is None:
return ApcNiveau.query.filter(
ApcNiveau.annee == annee_formation,
ApcCompetence.id == ApcNiveau.competence_id,
ApcCompetence.referentiel_id == referentiel_competence.id,
)
else:
return ApcNiveau.query.filter(
ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id,
ApcParcours.id == ApcAnneeParcours.parcours_id,
ApcParcours.referentiel == parcour.referentiel,
ApcParcoursNiveauCompetence.competence_id == ApcCompetence.id,
ApcCompetence.id == ApcNiveau.competence_id,
ApcAnneeParcours.parcours == parcour,
ApcNiveau.annee == annee_formation,
)
app_critiques_modules = db.Table(
"apc_modules_acs",
db.Column(
"module_id",
db.ForeignKey("notes_modules.id", ondelete="CASCADE"),
primary_key=True,
),
db.Column(
"app_crit_id",
db.ForeignKey("apc_app_critique.id"),
primary_key=True,
),
)
class ApcAppCritique(db.Model, XMLModel):
"Apprentissage Critique BUT"
id = db.Column(db.Integer, primary_key=True)
niveau_id = db.Column(db.Integer, db.ForeignKey("apc_niveau.id"), nullable=False)
code = db.Column(db.Text(), nullable=False, index=True)
libelle = db.Column(db.Text())
# modules = db.relationship(
# "Module",
# secondary="apc_modules_acs",
# lazy="dynamic",
# backref=db.backref("app_critiques", lazy="dynamic"),
# )
@classmethod
def app_critiques_ref_comp(
cls,
ref_comp: ApcReferentielCompetences,
annee: str,
competence: ApcCompetence = None,
) -> flask_sqlalchemy.BaseQuery:
"Liste les AC de tous les parcours de ref_comp pour l'année indiquée"
assert annee in {"BUT1", "BUT2", "BUT3"}
query = cls.query.filter(
ApcAppCritique.niveau_id == ApcNiveau.id,
ApcNiveau.competence_id == ApcCompetence.id,
ApcNiveau.annee == annee,
ApcCompetence.referentiel_id == ref_comp.id,
)
if competence is not None:
query = query.filter(ApcNiveau.competence == competence)
return query
def __init__(self, id, niveau_id, code, libelle, modules):
self.id = id
self.niveau_id = niveau_id
self.code = code
self.libelle = libelle
self.modules = modules
def to_dict(self) -> dict:
return {"libelle": self.libelle}
def get_label(self) -> str:
return self.code + " - " + self.titre
def __repr__(self):
return f"<{self.__class__.__name__} {self.code!r}>"
def get_saes(self):
"""Liste des SAE associées"""
return [m for m in self.modules if m.module_type == ModuleType.SAE]
parcours_modules = db.Table(
"parcours_modules",
db.Column(
"parcours_id", db.Integer, db.ForeignKey("apc_parcours.id"), primary_key=True
),
db.Column(
"module_id",
db.Integer,
db.ForeignKey("notes_modules.id", ondelete="CASCADE"),
primary_key=True,
),
)
"""Association parcours <-> modules (many-to-many)"""
parcours_formsemestre = db.Table(
"parcours_formsemestre",
db.Column(
"parcours_id", db.Integer, db.ForeignKey("apc_parcours.id"), primary_key=True
),
db.Column(
"formsemestre_id",
db.Integer,
db.ForeignKey("notes_formsemestre.id", ondelete="CASCADE"),
primary_key=True,
),
)
"""Association parcours <-> formsemestre (many-to-many)"""
class ApcParcours(db.Model, XMLModel):
id = db.Column(db.Integer, primary_key=True)
referentiel_id = db.Column(
db.Integer, db.ForeignKey("apc_referentiel_competences.id"), nullable=False
)
numero = db.Column(db.Integer) # ordre de présentation
code = db.Column(db.Text(), nullable=False)
libelle = db.Column(db.Text(), nullable=False)
annees = db.relationship(
"ApcAnneeParcours",
backref="parcours",
lazy="dynamic",
cascade="all, delete-orphan",
)
def __init__(self, id, referentiel_id, numero, code, libelle, annes):
self.id = id
self.referentiel_id = referentiel_id
self.numero = numero
self.code = code
self.libelle = libelle
self.annes = annes
def __repr__(self):
return f"<{self.__class__.__name__} {self.code!r}>"
def to_dict(self):
return {
"code": self.code,
"numero": self.numero,
"libelle": self.libelle,
"annees": {x.ordre: x.to_dict() for x in self.annees},
}
class ApcAnneeParcours(db.Model, XMLModel):
id = db.Column(db.Integer, primary_key=True)
parcours_id = db.Column(
db.Integer, db.ForeignKey("apc_parcours.id"), nullable=False
)
ordre = db.Column(db.Integer)
"numéro de l'année: 1, 2, 3"
def __init__(self, id, parcours_id, ordre):
self.id = id
self.parcours_id = parcours_id
self.ordre = ordre
def __repr__(self):
return f"<{self.__class__.__name__} ordre={self.ordre!r} parcours={self.parcours.code!r}>"
def to_dict(self):
return {
"ordre": self.ordre,
"competences": {
x.competence.titre: {
"niveau": x.niveau,
"id_orebut": x.competence.id_orebut,
}
for x in self.niveaux_competences
},
}
class ApcParcoursNiveauCompetence(db.Model):
"""Association entre année de parcours et compétence.
Le "niveau" de la compétence est donné ici
(convention Orébut)
"""
competence_id = db.Column(
db.Integer,
db.ForeignKey("apc_competence.id", ondelete="CASCADE"),
primary_key=True,
)
annee_parcours_id = db.Column(
db.Integer,
db.ForeignKey("apc_annee_parcours.id", ondelete="CASCADE"),
primary_key=True,
)
niveau = db.Column(db.Integer, nullable=False) # 1, 2, 3
competence = db.relationship(
ApcCompetence,
backref=db.backref(
"annee_parcours",
passive_deletes=True,
cascade="save-update, merge, delete, delete-orphan",
lazy="dynamic",
),
)
annee_parcours = db.relationship(
ApcAnneeParcours,
backref=db.backref(
"niveaux_competences",
passive_deletes=True,
cascade="save-update, merge, delete, delete-orphan",
),
)
def __repr__(self):
return f"<{self.__class__.__name__} {self.competence!r}<->{self.annee_parcours!r} niveau={self.niveau!r}>"