ScoDoc/app/models/__init__.py

250 lines
8.2 KiB
Python
Raw Permalink Normal View History

# -*- coding: UTF-8 -*
"""Modèles base de données ScoDoc
"""
from flask import abort, g
import sqlalchemy
from sqlalchemy import select
from sqlalchemy.exc import NoResultFound
import app
from app import db
CODE_STR_LEN = 16 # chaine pour les codes
SHORT_STR_LEN = 32 # courtes chaine, eg acronymes
APO_CODE_STR_LEN = 512 # nb de car max d'un code Apogée (il peut y en avoir plusieurs)
GROUPNAME_STR_LEN = 64
USERNAME_STR_LEN = 64
convention = {
"ix": "ix_%(column_0_label)s",
"uq": "uq_%(table_name)s_%(column_0_name)s",
"ck": "ck_%(table_name)s_%(constraint_name)s",
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
"pk": "pk_%(table_name)s",
}
metadata_obj = sqlalchemy.MetaData(naming_convention=convention)
2023-12-22 15:21:07 +01:00
class ScoDocModel(db.Model):
"""Superclass for our models. Add some useful methods for editing, cloning, etc.
- clone() : clone object and add copy to session, do not commit.
- create_from_dict() : create instance from given dict, applying conversions.
- convert_dict_fields() : convert dict values, called before instance creation.
By default, do nothing.
- from_dict() : update object using data from dict. data is first converted.
- edit() : update from wtf form.
"""
__abstract__ = True # declare an abstract class for SQLAlchemy
def clone(self, not_copying=()):
"""Clone, not copying the given attrs, and add to session.
Attention: la copie n'a pas d'id avant le prochain flush ou commit.
"""
d = dict(self.__dict__)
d.pop("id", None) # get rid of id
d.pop("_sa_instance_state", None) # get rid of SQLAlchemy special attr
for k in not_copying:
d.pop(k, None)
copy = self.__class__(**d)
db.session.add(copy)
return copy
@classmethod
2023-12-22 15:21:07 +01:00
def create_from_dict(cls, data: dict) -> "ScoDocModel":
"""Create a new instance of the model with attributes given in dict.
The instance is added to the session (but not flushed nor committed).
2024-02-03 23:25:05 +01:00
Use only relevant attributes for the given model and ignore others.
"""
2023-12-22 15:21:07 +01:00
if data:
args = cls.convert_dict_fields(cls.filter_model_attributes(data))
if args:
obj = cls(**args)
else:
obj = cls()
else:
obj = cls()
db.session.add(obj)
return obj
@classmethod
def filter_model_attributes(cls, data: dict, excluded: set[str] = None) -> dict:
"""Returns a copy of dict with only the keys belonging to the Model and not in excluded.
Add 'id' to excluded."""
excluded = excluded or set()
excluded.add("id") # always exclude id
# Les attributs du modèle qui sont des variables: (élimine les __ et les alias comme adm_id)
my_attributes = [
a
for a in dir(cls)
if isinstance(
getattr(cls, a), sqlalchemy.orm.attributes.InstrumentedAttribute
)
]
# Filtre les arguments utiles
return {
k: v for k, v in data.items() if k in my_attributes and k not in excluded
}
@classmethod
def convert_dict_fields(cls, args: dict) -> dict:
"""Convert fields from the given dict to model's attributes values. No side effect.
By default, do nothing, but is overloaded by some subclasses.
args: dict with args in application.
returns: dict to store in model's db.
"""
# virtual, by default, do nothing
return args
2023-12-22 15:21:07 +01:00
def from_dict(self, args: dict, excluded: set[str] | None = None) -> bool:
"""Update object's fields given in dict. Add to session but don't commit.
True if modification.
"""
2023-11-21 22:28:50 +01:00
args_dict = self.convert_dict_fields(
self.filter_model_attributes(args, excluded=excluded)
)
2023-12-22 15:21:07 +01:00
modified = False
for key, value in args_dict.items():
2023-12-22 15:21:07 +01:00
if hasattr(self, key) and value != getattr(self, key):
setattr(self, key, value)
2023-12-22 15:21:07 +01:00
modified = True
db.session.add(self)
2023-12-22 15:21:07 +01:00
return modified
def to_dict(self) -> dict:
"dict"
d = dict(self.__dict__)
d.pop("_sa_instance_state", None)
return d
2023-12-22 15:21:07 +01:00
def edit_from_form(self, form) -> bool:
"""Generic edit method for updating model instance.
True if modification.
"""
args = {field.name: field.data for field in form}
return self.from_dict(args)
@classmethod
def get_instance(cls, oid: int, accept_none=False):
"""Instance du modèle ou 404 (ou None si accept_none),
cherche uniquement dans le département courant.
Ne fonctionne que si le modèle a un attribut dept_id
ou que l'attribut de classe _sco_dept_relations indique les jointures
à effectuer pour trouver le département.
Si accept_none, return None si l'id est invalide ou ne correspond
pas à une instance. Sinon lève 404 en cas d'erreur.
"""
if not isinstance(oid, int):
try:
oid = int(oid)
except (TypeError, ValueError):
if accept_none:
return None
abort(404, "oid invalide")
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()
# Compatibilité avec SQLAlchemy 2.0
@classmethod
def get_or_404(cls, oid: int | str):
"""Get instance or abort 404"""
stmt = select(cls).where(cls.id == oid)
try:
return db.session.execute(stmt).scalar_one()
except NoResultFound:
abort(404)
from app.models.absences import Absence, AbsenceNotification, BilletAbsence
2021-08-13 00:34:58 +02:00
from app.models.departements import Departement
from app.models.etudiants import (
Identite,
Adresse,
Admission,
ItemSuivi,
ItemSuiviTag,
itemsuivi_tags_assoc,
EtudAnnotation,
)
from app.models.events import Scolog, ScolarNews
2021-12-08 22:33:32 +01:00
from app.models.formations import Formation, Matiere
2021-12-14 10:31:33 +01:00
from app.models.modules import Module, ModuleUECoef, NotesTag, notes_modules_tags
from app.models.ues import DispenseUE, UniteEns
from app.models.formsemestre import (
FormSemestre,
FormSemestreEtape,
FormationModalite,
FormSemestreUECoef,
FormSemestreUEComputationExpr,
FormSemestreCustomMenu,
FormSemestreInscription,
notes_formsemestre_responsables,
2021-11-13 08:25:51 +01:00
NotesSemSet,
notes_semset_formsemestre,
)
from app.models.formsemestre_descr import (
FormSemestreDescription,
FORMSEMESTRE_DISPOSITIFS,
)
2021-11-13 08:25:51 +01:00
from app.models.moduleimpls import (
ModuleImpl,
notes_modules_enseignants,
ModuleImplInscription,
2021-11-13 08:25:51 +01:00
)
from app.models.evaluations import (
Evaluation,
EvaluationUEPoids,
)
from app.models.groups import Partition, GroupDescr, group_membership
from app.models.notes import (
BulAppreciations,
NotesNotes,
NotesNotesLog,
)
2022-02-06 16:09:17 +01:00
from app.models.validations import (
ScolarFormSemestreValidation,
ScolarAutorisationInscription,
)
from app.models.preferences import ScoPreference
2021-12-02 12:08:03 +01:00
from app.models.but_refcomp import (
ApcAppCritique,
2022-10-29 15:42:03 +02:00
ApcCompetence,
ApcNiveau,
ApcParcours,
2022-10-29 15:42:03 +02:00
ApcReferentielCompetences,
ApcSituationPro,
2021-12-02 12:08:03 +01:00
)
from app.models.but_validations import (
ApcValidationAnnee,
ApcValidationRCUE,
ValidationDUT120,
)
2022-05-29 17:34:03 +02:00
2022-01-25 22:18:49 +01:00
from app.models.config import ScoDocSiteConfig
2023-04-17 15:34:00 +02:00
from app.models.assiduites import Assiduite, Justificatif
from app.models.scolar_event import ScolarEvent