From 24ccd8f9f7dd899fe30adeb08a528fdc142838c3 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 19 Jul 2024 09:42:44 +0200 Subject: [PATCH] Add get_instance method to all ScoDoc models --- app/models/__init__.py | 22 +++++++++++---- app/models/absences.py | 5 +++- app/models/but_validations.py | 6 ++++ app/models/etudiants.py | 11 ++++++-- app/models/evaluations.py | 2 ++ app/models/formations.py | 7 +++-- app/models/groups.py | 6 ++++ app/models/moduleimpls.py | 2 ++ app/models/modules.py | 2 ++ app/models/notes.py | 5 +++- app/models/preferences.py | 9 ++++-- app/scodoc/sco_import_etuds.py | 7 +++-- sco_version.py | 2 +- tests/unit/sco_fake_gen.py | 4 +-- tests/unit/test_formsemestre.py | 3 +- tests/unit/test_models.py | 49 +++++++++++++++++++++++++++++++++ 16 files changed, 117 insertions(+), 25 deletions(-) create mode 100644 tests/unit/test_models.py diff --git a/app/models/__init__.py b/app/models/__init__.py index c7216e4b1..433635708 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -5,6 +5,7 @@ from flask import abort, g import sqlalchemy +import app from app import db CODE_STR_LEN = 16 # chaine pour les codes @@ -133,11 +134,22 @@ class ScoDocModel(db.Model): return None abort(404, "oid invalide") - query = ( - cls.query.filter_by(id=oid, dept_id=g.scodoc_dept_id) - if g.scodoc_dept - else cls.query.filter_by(id=oid) - ) + if g.scodoc_dept: + if hasattr(cls, "_sco_dept_relations"): + # Quand dept_id n'est pas dans le modèle courant, + # 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: return query.first() return query.first_or_404() diff --git a/app/models/absences.py b/app/models/absences.py index d31d9e270..27aab114b 100644 --- a/app/models/absences.py +++ b/app/models/absences.py @@ -7,7 +7,10 @@ from app import db 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" id = db.Column(db.Integer, primary_key=True) diff --git a/app/models/but_validations.py b/app/models/but_validations.py index bd1aea001..b5d962192 100644 --- a/app/models/but_validations.py +++ b/app/models/but_validations.py @@ -61,6 +61,8 @@ class ApcValidationRCUE(ScoDocModel): ue2 = db.relationship("UniteEns", foreign_keys=ue2_id) parcour = db.relationship("ApcParcours") + _sco_dept_relations = ("Identite",) # pour accéder au département + def __repr__(self): return f"""<{self.__class__.__name__} {self.id} {self.etud} { self.ue1}/{self.ue2}:{self.code!r}>""" @@ -154,6 +156,8 @@ class ApcValidationAnnee(ScoDocModel): etud = db.relationship("Identite", 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): return f"""<{self.__class__.__name__} {self.id} {self.etud } BUT{self.ordre}/{self.annee_scolaire}:{self.code!r}>""" @@ -317,6 +321,8 @@ class ValidationDUT120(ScoDocModel): etud = db.relationship("Identite", backref="validations_dut120") formsemestre = db.relationship("FormSemestre", backref="validations_dut120") + _sco_dept_relations = ("Identite",) # pour accéder au département + def __repr__(self): return f"""""" diff --git a/app/models/etudiants.py b/app/models/etudiants.py index 55a11dccf..61ca8e5b8 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -1080,8 +1080,9 @@ class Admission(models.ScoDocModel): return args_dict -# Suivi scolarité / débouchés -class ItemSuivi(db.Model): +class ItemSuivi(models.ScoDocModel): + """Suivi scolarité / débouchés""" + __tablename__ = "itemsuivi" 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()) situation = db.Column(db.Text) + _sco_dept_relations = ("Identite",) # accès au dept_id + class ItemSuiviTag(db.Model): __tablename__ = "itemsuivi_tags" @@ -1114,7 +1117,7 @@ itemsuivi_tags_assoc = db.Table( ) -class EtudAnnotation(db.Model): +class EtudAnnotation(models.ScoDocModel): """Annotation sur un étudiant""" __tablename__ = "etud_annotations" @@ -1125,6 +1128,8 @@ class EtudAnnotation(db.Model): author = db.Column(db.Text) # le pseudo (user_name), was zope_authenticated_user comment = db.Column(db.Text) + _sco_dept_relations = ("Identite",) # accès au dept_id + def to_dict(self): """Représentation dictionnaire.""" e = dict(self.__dict__) diff --git a/app/models/evaluations.py b/app/models/evaluations.py index d2607f0da..6ec196697 100644 --- a/app/models/evaluations.py +++ b/app/models/evaluations.py @@ -60,6 +60,8 @@ class Evaluation(models.ScoDocModel): numero = db.Column(db.Integer, nullable=False, default=0) 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_RATTRAPAGE = 1 EVALUATION_SESSION2 = 2 diff --git a/app/models/formations.py b/app/models/formations.py index 15e8fa15e..65538fda4 100644 --- a/app/models/formations.py +++ b/app/models/formations.py @@ -7,7 +7,7 @@ from flask_sqlalchemy.query import Query import app from app import db 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 ( ApcAnneeParcours, ApcCompetence, @@ -23,7 +23,7 @@ from app.scodoc import sco_utils as scu from app.scodoc.codes_cursus import UE_STANDARD -class Formation(db.Model): +class Formation(ScoDocModel): """Programme pédagogique d'une formation""" __tablename__ = "notes_formations" @@ -297,7 +297,7 @@ class Formation(db.Model): db.session.commit() -class Matiere(db.Model): +class Matiere(ScoDocModel): """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. @@ -313,6 +313,7 @@ class Matiere(db.Model): numero = db.Column(db.Integer, nullable=False, default=0) # ordre de présentation modules = db.relationship("Module", lazy="dynamic", backref="matiere") + _sco_dept_relations = ("UniteEns", "Formation") # accès au dept_id def __repr__(self): return f"""<{self.__class__.__name__}(id={self.id}, ue_id={ diff --git a/app/models/groups.py b/app/models/groups.py index c654efa70..429989f4b 100644 --- a/app/models/groups.py +++ b/app/models/groups.py @@ -54,6 +54,7 @@ class Partition(ScoDocModel): cascade="all, delete-orphan", order_by="GroupDescr.numero, GroupDescr.group_name", ) + _sco_dept_relations = ("FormSemestre",) def __init__(self, **kwargs): super(Partition, self).__init__(**kwargs) @@ -225,6 +226,11 @@ class GroupDescr(ScoDocModel): numero = db.Column(db.Integer, nullable=False, default=0) "Numero = ordre de presentation" + _sco_dept_relations = ( + "Partition", + "FormSemestre", + ) + etuds = db.relationship( "Identite", secondary="group_membership", diff --git a/app/models/moduleimpls.py b/app/models/moduleimpls.py index f9c92b1bf..cb4c8a3b6 100644 --- a/app/models/moduleimpls.py +++ b/app/models/moduleimpls.py @@ -60,6 +60,8 @@ class ModuleImpl(ScoDocModel): ) "enseignants du module (sans le responsable)" + _sco_dept_relations = ("FormSemestre",) # accès au dept_id + def __repr__(self): return f"<{self.__class__.__name__} {self.id} module={repr(self.module)}>" diff --git a/app/models/modules.py b/app/models/modules.py index 0870a3921..ea15181c8 100644 --- a/app/models/modules.py +++ b/app/models/modules.py @@ -75,6 +75,8 @@ class Module(models.ScoDocModel): backref=db.backref("modules", lazy=True), ) + _sco_dept_relations = "Formation" # accès au dept_id + def __init__(self, **kwargs): self.ue_coefs = [] super(Module, self).__init__(**kwargs) diff --git a/app/models/notes.py b/app/models/notes.py index 61eb17733..91401e8fa 100644 --- a/app/models/notes.py +++ b/app/models/notes.py @@ -5,11 +5,12 @@ import sqlalchemy as sa from app import db +from app import models from app.scodoc import safehtml import app.scodoc.sco_utils as scu -class BulAppreciations(db.Model): +class BulAppreciations(models.ScoDocModel): """Appréciations sur bulletins""" __tablename__ = "notes_appreciations" @@ -27,6 +28,8 @@ class BulAppreciations(db.Model): author = db.Column(db.Text) # le pseudo (user_name), sans contrainte comment = db.Column(db.Text) # texte libre + _sco_dept_relations = ("Identite",) # accès au dept_id + @classmethod def get_appreciations_list( cls, formsemestre_id: int, etudid: int diff --git a/app/models/preferences.py b/app/models/preferences.py index c6b81edf1..2e21d1f33 100644 --- a/app/models/preferences.py +++ b/app/models/preferences.py @@ -3,10 +3,10 @@ """Model : preferences """ -from app import db +from app import db, models -class ScoPreference(db.Model): +class ScoPreference(models.ScoDocModel): """ScoDoc preferences (par département)""" __tablename__ = "sco_prefs" @@ -19,5 +19,8 @@ class ScoPreference(db.Model): value = db.Column(db.Text()) formsemestre_id = db.Column(db.Integer, db.ForeignKey("notes_formsemestre.id")) + _sco_dept_relations = ("FormSemestre",) # accès au dept_id + 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}>""" diff --git a/app/scodoc/sco_import_etuds.py b/app/scodoc/sco_import_etuds.py index 74362f922..48244246c 100644 --- a/app/scodoc/sco_import_etuds.py +++ b/app/scodoc/sco_import_etuds.py @@ -767,15 +767,16 @@ def scolars_import_admission( ) 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: sco_groups.change_etud_group_in_partition( args["etudid"], group ) - else: + elif not group.partition.is_parcours: log("scolars_import_admission: partition non editable") diag.append( - f"Attention: partition {group.partition} non editable (ignorée)" + f"""Attention: partition { + group.partition} (g{group.id}) non editable et ignorée""" ) # diff --git a/sco_version.py b/sco_version.py index 53404442e..5fb0673aa 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.7.4" +SCOVERSION = "9.7.5" SCONAME = "ScoDoc" diff --git a/tests/unit/sco_fake_gen.py b/tests/unit/sco_fake_gen.py index 0090d53a1..607b96e48 100644 --- a/tests/unit/sco_fake_gen.py +++ b/tests/unit/sco_fake_gen.py @@ -267,9 +267,7 @@ class ScoFake(object): responsables = (self.default_user.id,) titre = titre or "sans titre" oid = sco_formsemestre.do_formsemestre_create(locals()) - oids = sco_formsemestre.do_formsemestre_list( - args={"formsemestre_id": oid} - ) # API inconsistency + oids = sco_formsemestre.do_formsemestre_list(args={"formsemestre_id": oid}) if not oids: raise ScoValueError("formsemestre not created !") return oid diff --git a/tests/unit/test_formsemestre.py b/tests/unit/test_formsemestre.py index afc1277ef..e9677c4f8 100644 --- a/tests/unit/test_formsemestre.py +++ b/tests/unit/test_formsemestre.py @@ -42,8 +42,7 @@ DEPT = TestConfig.DEPT_TEST def test_formsemestres_associate_new_version(test_client): """Test association à une nouvelle version du programme""" app.set_sco_dept(DEPT) - # Construit la base de test GB une seule fois - # puis lance les tests de jury + # Construit la base de test GB doc, formation, formsemestre_titres = yaml_setup.setup_from_yaml( "tests/ressources/yaml/simple_formsemestres.yaml" ) diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py new file mode 100644 index 000000000..bab144c8b --- /dev/null +++ b/tests/unit/test_models.py @@ -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