Add get_instance method to all ScoDoc models

This commit is contained in:
Emmanuel Viennet 2024-07-19 09:42:44 +02:00
parent 98127e7c1d
commit 24ccd8f9f7
16 changed files with 117 additions and 25 deletions

View File

@ -5,6 +5,7 @@
from flask import abort, g from flask import abort, g
import sqlalchemy import sqlalchemy
import app
from app import db from app import db
CODE_STR_LEN = 16 # chaine pour les codes CODE_STR_LEN = 16 # chaine pour les codes
@ -133,11 +134,22 @@ class ScoDocModel(db.Model):
return None return None
abort(404, "oid invalide") abort(404, "oid invalide")
query = ( if g.scodoc_dept:
cls.query.filter_by(id=oid, dept_id=g.scodoc_dept_id) if hasattr(cls, "_sco_dept_relations"):
if g.scodoc_dept # Quand dept_id n'est pas dans le modèle courant,
else cls.query.filter_by(id=oid) # cet attribut indique la liste des tables à joindre pour
) # obtenir le departement.
query = cls.query.filter_by(id=oid)
for relation_name in cls._sco_dept_relations:
query = query.join(getattr(app.models, relation_name))
query = query.filter_by(dept_id=g.scodoc_dept_id)
else:
# département accessible dans le modèle courant
query = cls.query.filter_by(id=oid, dept_id=g.scodoc_dept_id)
else:
# Pas de département courant (API non départementale)
query = cls.query.filter_by(id=oid)
if accept_none: if accept_none:
return query.first() return query.first()
return query.first_or_404() return query.first_or_404()

View File

@ -7,7 +7,10 @@ from app import db
class Absence(db.Model): class Absence(db.Model):
"""une absence (sur une demi-journée)""" """LEGACY
Ce modèle n'est PLUS UTILISE depuis ScoDoc 9.6 et remplacé par assiduité.
une absence (sur une demi-journée)
"""
__tablename__ = "absences" __tablename__ = "absences"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)

View File

@ -61,6 +61,8 @@ class ApcValidationRCUE(ScoDocModel):
ue2 = db.relationship("UniteEns", foreign_keys=ue2_id) ue2 = db.relationship("UniteEns", foreign_keys=ue2_id)
parcour = db.relationship("ApcParcours") parcour = db.relationship("ApcParcours")
_sco_dept_relations = ("Identite",) # pour accéder au département
def __repr__(self): def __repr__(self):
return f"""<{self.__class__.__name__} {self.id} {self.etud} { return f"""<{self.__class__.__name__} {self.id} {self.etud} {
self.ue1}/{self.ue2}:{self.code!r}>""" self.ue1}/{self.ue2}:{self.code!r}>"""
@ -154,6 +156,8 @@ class ApcValidationAnnee(ScoDocModel):
etud = db.relationship("Identite", backref="apc_validations_annees") etud = db.relationship("Identite", backref="apc_validations_annees")
formsemestre = db.relationship("FormSemestre", backref="apc_validations_annees") formsemestre = db.relationship("FormSemestre", backref="apc_validations_annees")
_sco_dept_relations = ("Identite",) # pour accéder au département
def __repr__(self): def __repr__(self):
return f"""<{self.__class__.__name__} {self.id} {self.etud return f"""<{self.__class__.__name__} {self.id} {self.etud
} BUT{self.ordre}/{self.annee_scolaire}:{self.code!r}>""" } BUT{self.ordre}/{self.annee_scolaire}:{self.code!r}>"""
@ -317,6 +321,8 @@ class ValidationDUT120(ScoDocModel):
etud = db.relationship("Identite", backref="validations_dut120") etud = db.relationship("Identite", backref="validations_dut120")
formsemestre = db.relationship("FormSemestre", backref="validations_dut120") formsemestre = db.relationship("FormSemestre", backref="validations_dut120")
_sco_dept_relations = ("Identite",) # pour accéder au département
def __repr__(self): def __repr__(self):
return f"""<ValidationDUT120 {self.etud}>""" return f"""<ValidationDUT120 {self.etud}>"""

View File

@ -1080,8 +1080,9 @@ class Admission(models.ScoDocModel):
return args_dict return args_dict
# Suivi scolarité / débouchés class ItemSuivi(models.ScoDocModel):
class ItemSuivi(db.Model): """Suivi scolarité / débouchés"""
__tablename__ = "itemsuivi" __tablename__ = "itemsuivi"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
@ -1093,6 +1094,8 @@ class ItemSuivi(db.Model):
item_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) item_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
situation = db.Column(db.Text) situation = db.Column(db.Text)
_sco_dept_relations = ("Identite",) # accès au dept_id
class ItemSuiviTag(db.Model): class ItemSuiviTag(db.Model):
__tablename__ = "itemsuivi_tags" __tablename__ = "itemsuivi_tags"
@ -1114,7 +1117,7 @@ itemsuivi_tags_assoc = db.Table(
) )
class EtudAnnotation(db.Model): class EtudAnnotation(models.ScoDocModel):
"""Annotation sur un étudiant""" """Annotation sur un étudiant"""
__tablename__ = "etud_annotations" __tablename__ = "etud_annotations"
@ -1125,6 +1128,8 @@ class EtudAnnotation(db.Model):
author = db.Column(db.Text) # le pseudo (user_name), was zope_authenticated_user author = db.Column(db.Text) # le pseudo (user_name), was zope_authenticated_user
comment = db.Column(db.Text) comment = db.Column(db.Text)
_sco_dept_relations = ("Identite",) # accès au dept_id
def to_dict(self): def to_dict(self):
"""Représentation dictionnaire.""" """Représentation dictionnaire."""
e = dict(self.__dict__) e = dict(self.__dict__)

View File

@ -60,6 +60,8 @@ class Evaluation(models.ScoDocModel):
numero = db.Column(db.Integer, nullable=False, default=0) numero = db.Column(db.Integer, nullable=False, default=0)
ues = db.relationship("UniteEns", secondary="evaluation_ue_poids", viewonly=True) ues = db.relationship("UniteEns", secondary="evaluation_ue_poids", viewonly=True)
_sco_dept_relations = ("ModuleImpl", "FormSemestre") # accès au dept_id
EVALUATION_NORMALE = 0 # valeurs stockées en base, ne pas changer ! EVALUATION_NORMALE = 0 # valeurs stockées en base, ne pas changer !
EVALUATION_RATTRAPAGE = 1 EVALUATION_RATTRAPAGE = 1
EVALUATION_SESSION2 = 2 EVALUATION_SESSION2 = 2

View File

@ -7,7 +7,7 @@ from flask_sqlalchemy.query import Query
import app import app
from app import db from app import db
from app.comp import df_cache from app.comp import df_cache
from app.models import SHORT_STR_LEN from app.models import ScoDocModel, SHORT_STR_LEN
from app.models.but_refcomp import ( from app.models.but_refcomp import (
ApcAnneeParcours, ApcAnneeParcours,
ApcCompetence, ApcCompetence,
@ -23,7 +23,7 @@ from app.scodoc import sco_utils as scu
from app.scodoc.codes_cursus import UE_STANDARD from app.scodoc.codes_cursus import UE_STANDARD
class Formation(db.Model): class Formation(ScoDocModel):
"""Programme pédagogique d'une formation""" """Programme pédagogique d'une formation"""
__tablename__ = "notes_formations" __tablename__ = "notes_formations"
@ -297,7 +297,7 @@ class Formation(db.Model):
db.session.commit() db.session.commit()
class Matiere(db.Model): class Matiere(ScoDocModel):
"""Matières: regroupe les modules d'une UE """Matières: regroupe les modules d'une UE
La matière a peu d'utilité en dehors de la présentation des modules La matière a peu d'utilité en dehors de la présentation des modules
d'une UE. d'une UE.
@ -313,6 +313,7 @@ class Matiere(db.Model):
numero = db.Column(db.Integer, nullable=False, default=0) # ordre de présentation numero = db.Column(db.Integer, nullable=False, default=0) # ordre de présentation
modules = db.relationship("Module", lazy="dynamic", backref="matiere") modules = db.relationship("Module", lazy="dynamic", backref="matiere")
_sco_dept_relations = ("UniteEns", "Formation") # accès au dept_id
def __repr__(self): def __repr__(self):
return f"""<{self.__class__.__name__}(id={self.id}, ue_id={ return f"""<{self.__class__.__name__}(id={self.id}, ue_id={

View File

@ -54,6 +54,7 @@ class Partition(ScoDocModel):
cascade="all, delete-orphan", cascade="all, delete-orphan",
order_by="GroupDescr.numero, GroupDescr.group_name", order_by="GroupDescr.numero, GroupDescr.group_name",
) )
_sco_dept_relations = ("FormSemestre",)
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(Partition, self).__init__(**kwargs) super(Partition, self).__init__(**kwargs)
@ -225,6 +226,11 @@ class GroupDescr(ScoDocModel):
numero = db.Column(db.Integer, nullable=False, default=0) numero = db.Column(db.Integer, nullable=False, default=0)
"Numero = ordre de presentation" "Numero = ordre de presentation"
_sco_dept_relations = (
"Partition",
"FormSemestre",
)
etuds = db.relationship( etuds = db.relationship(
"Identite", "Identite",
secondary="group_membership", secondary="group_membership",

View File

@ -60,6 +60,8 @@ class ModuleImpl(ScoDocModel):
) )
"enseignants du module (sans le responsable)" "enseignants du module (sans le responsable)"
_sco_dept_relations = ("FormSemestre",) # accès au dept_id
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__} {self.id} module={repr(self.module)}>" return f"<{self.__class__.__name__} {self.id} module={repr(self.module)}>"

View File

@ -75,6 +75,8 @@ class Module(models.ScoDocModel):
backref=db.backref("modules", lazy=True), backref=db.backref("modules", lazy=True),
) )
_sco_dept_relations = "Formation" # accès au dept_id
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.ue_coefs = [] self.ue_coefs = []
super(Module, self).__init__(**kwargs) super(Module, self).__init__(**kwargs)

View File

@ -5,11 +5,12 @@
import sqlalchemy as sa import sqlalchemy as sa
from app import db from app import db
from app import models
from app.scodoc import safehtml from app.scodoc import safehtml
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
class BulAppreciations(db.Model): class BulAppreciations(models.ScoDocModel):
"""Appréciations sur bulletins""" """Appréciations sur bulletins"""
__tablename__ = "notes_appreciations" __tablename__ = "notes_appreciations"
@ -27,6 +28,8 @@ class BulAppreciations(db.Model):
author = db.Column(db.Text) # le pseudo (user_name), sans contrainte author = db.Column(db.Text) # le pseudo (user_name), sans contrainte
comment = db.Column(db.Text) # texte libre comment = db.Column(db.Text) # texte libre
_sco_dept_relations = ("Identite",) # accès au dept_id
@classmethod @classmethod
def get_appreciations_list( def get_appreciations_list(
cls, formsemestre_id: int, etudid: int cls, formsemestre_id: int, etudid: int

View File

@ -3,10 +3,10 @@
"""Model : preferences """Model : preferences
""" """
from app import db from app import db, models
class ScoPreference(db.Model): class ScoPreference(models.ScoDocModel):
"""ScoDoc preferences (par département)""" """ScoDoc preferences (par département)"""
__tablename__ = "sco_prefs" __tablename__ = "sco_prefs"
@ -19,5 +19,8 @@ class ScoPreference(db.Model):
value = db.Column(db.Text()) value = db.Column(db.Text())
formsemestre_id = db.Column(db.Integer, db.ForeignKey("notes_formsemestre.id")) formsemestre_id = db.Column(db.Integer, db.ForeignKey("notes_formsemestre.id"))
_sco_dept_relations = ("FormSemestre",) # accès au dept_id
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__} {self.id} {self.departement.acronym} {self.name}={self.value}>" return f"""<{self.__class__.__name__} {self.id} {self.departement.acronym
} {self.name}={self.value}>"""

View File

@ -767,15 +767,16 @@ def scolars_import_admission(
) )
for group_id in group_ids: for group_id in group_ids:
group = db.session.get(GroupDescr, group_id) group: GroupDescr = GroupDescr.get_instance(group_id)
if group.partition.groups_editable: if group.partition.groups_editable:
sco_groups.change_etud_group_in_partition( sco_groups.change_etud_group_in_partition(
args["etudid"], group args["etudid"], group
) )
else: elif not group.partition.is_parcours:
log("scolars_import_admission: partition non editable") log("scolars_import_admission: partition non editable")
diag.append( diag.append(
f"Attention: partition {group.partition} non editable (ignorée)" f"""Attention: partition {
group.partition} (g{group.id}) non editable et ignorée"""
) )
# #

View File

@ -1,7 +1,7 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.7.4" SCOVERSION = "9.7.5"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"

View File

@ -267,9 +267,7 @@ class ScoFake(object):
responsables = (self.default_user.id,) responsables = (self.default_user.id,)
titre = titre or "sans titre" titre = titre or "sans titre"
oid = sco_formsemestre.do_formsemestre_create(locals()) oid = sco_formsemestre.do_formsemestre_create(locals())
oids = sco_formsemestre.do_formsemestre_list( oids = sco_formsemestre.do_formsemestre_list(args={"formsemestre_id": oid})
args={"formsemestre_id": oid}
) # API inconsistency
if not oids: if not oids:
raise ScoValueError("formsemestre not created !") raise ScoValueError("formsemestre not created !")
return oid return oid

View File

@ -42,8 +42,7 @@ DEPT = TestConfig.DEPT_TEST
def test_formsemestres_associate_new_version(test_client): def test_formsemestres_associate_new_version(test_client):
"""Test association à une nouvelle version du programme""" """Test association à une nouvelle version du programme"""
app.set_sco_dept(DEPT) app.set_sco_dept(DEPT)
# Construit la base de test GB une seule fois # Construit la base de test GB
# puis lance les tests de jury
doc, formation, formsemestre_titres = yaml_setup.setup_from_yaml( doc, formation, formsemestre_titres = yaml_setup.setup_from_yaml(
"tests/ressources/yaml/simple_formsemestres.yaml" "tests/ressources/yaml/simple_formsemestres.yaml"
) )

49
tests/unit/test_models.py Normal file
View File

@ -0,0 +1,49 @@
""" Test méthodes génériques sur les modèles ScoDoc
"""
import pytest
from flask import g
import app
from app import db
from app.models import Departement, GroupDescr
from config import TestConfig
from tests.unit import yaml_setup
DEPT = TestConfig.DEPT_TEST
def test_get_instance(test_client):
"""Test accès instance avec ou sans dept"""
assert DEPT
app.set_sco_dept(DEPT)
# Création d'un autre départment
other_dept = Departement(acronym="X666")
db.session.add(other_dept)
db.session.flush()
# Crée qq semestres...
doc, formation, formsemestre_titres = yaml_setup.setup_from_yaml(
"tests/ressources/yaml/simple_formsemestres.yaml"
)
for formsemestre_titre in formsemestre_titres:
formsemestre = yaml_setup.create_formsemestre_with_etuds(
doc, formation, formsemestre_titre
)
assert formsemestre
# prend l'exemple de GroupDescr
# Crée un formsemestre et un groupe
gr = GroupDescr.query.first()
assert gr
oid = gr.id
# Accès sans département (comme le fait l'API non départementale)
g.scodoc_dept = None
g.scodoc_dept_id = -1 # invalide
assert GroupDescr.get_instance(oid, accept_none=True).id == oid
# Accès avec le bon département
app.set_sco_dept(DEPT)
assert GroupDescr.get_instance(oid, accept_none=True).id == oid
# Accès avec un autre département
app.set_sco_dept(other_dept.acronym)
assert GroupDescr.get_instance(oid, accept_none=True) is None