2021-08-07 15:20:30 +02:00
|
|
|
# -*- coding: UTF-8 -*
|
|
|
|
|
|
|
|
"""Modèles base de données ScoDoc
|
|
|
|
"""
|
|
|
|
|
2024-07-06 23:28:20 +02:00
|
|
|
from flask import abort, g
|
2022-05-01 23:58:41 +02:00
|
|
|
import sqlalchemy
|
2024-10-29 19:18:36 +01:00
|
|
|
from sqlalchemy import select
|
|
|
|
from sqlalchemy.exc import NoResultFound
|
2024-07-19 09:42:44 +02:00
|
|
|
import app
|
2023-10-08 10:01:23 +02:00
|
|
|
from app import db
|
2022-05-01 23:58:41 +02:00
|
|
|
|
2021-08-07 15:20:30 +02:00
|
|
|
CODE_STR_LEN = 16 # chaine pour les codes
|
|
|
|
SHORT_STR_LEN = 32 # courtes chaine, eg acronymes
|
2022-01-19 23:02:15 +01:00
|
|
|
APO_CODE_STR_LEN = 512 # nb de car max d'un code Apogée (il peut y en avoir plusieurs)
|
2021-08-14 18:54:32 +02:00
|
|
|
GROUPNAME_STR_LEN = 64
|
2023-03-02 22:55:25 +01:00
|
|
|
USERNAME_STR_LEN = 64
|
2021-08-07 15:20:30 +02:00
|
|
|
|
2022-05-01 23:58:41 +02:00
|
|
|
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-10-08 10:01:23 +02:00
|
|
|
|
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
|
2023-10-08 10:01:23 +02:00
|
|
|
|
|
|
|
def clone(self, not_copying=()):
|
2024-08-11 21:39:43 +02:00
|
|
|
"""Clone, not copying the given attrs, and add to session.
|
2023-10-08 10:01:23 +02:00
|
|
|
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":
|
2023-10-08 10:01:23 +02:00
|
|
|
"""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-10-08 10:01:23 +02:00
|
|
|
"""
|
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()
|
2023-10-08 10:01:23 +02:00
|
|
|
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.
|
2023-11-22 17:54:16 +01:00
|
|
|
Add 'id' to excluded."""
|
|
|
|
excluded = excluded or set()
|
|
|
|
excluded.add("id") # always exclude id
|
2023-10-08 10:01:23 +02:00
|
|
|
# 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:
|
2023-11-22 23:31:16 +01:00
|
|
|
"""Convert fields from the given dict to model's attributes values. No side effect.
|
2023-10-16 22:51:31 +02:00
|
|
|
By default, do nothing, but is overloaded by some subclasses.
|
|
|
|
args: dict with args in application.
|
|
|
|
returns: dict to store in model's db.
|
|
|
|
"""
|
2023-10-08 10:01:23 +02:00
|
|
|
# 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
|
2023-10-08 10:01:23 +02:00
|
|
|
for key, value in args_dict.items():
|
2023-12-22 15:21:07 +01:00
|
|
|
if hasattr(self, key) and value != getattr(self, key):
|
2023-10-08 10:01:23 +02:00
|
|
|
setattr(self, key, value)
|
2023-12-22 15:21:07 +01:00
|
|
|
modified = True
|
2023-10-08 10:01:23 +02:00
|
|
|
db.session.add(self)
|
2023-12-22 15:21:07 +01:00
|
|
|
return modified
|
|
|
|
|
2024-08-06 22:30:30 +02:00
|
|
|
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)
|
2023-10-08 10:01:23 +02:00
|
|
|
|
2024-07-06 23:28:20 +02:00
|
|
|
@classmethod
|
|
|
|
def get_instance(cls, oid: int, accept_none=False):
|
2024-10-19 23:52:35 +02:00
|
|
|
"""Instance du modèle ou 404 (ou None si accept_none),
|
2024-07-06 23:28:20 +02:00
|
|
|
cherche uniquement dans le département courant.
|
2024-09-09 16:21:51 +02:00
|
|
|
|
|
|
|
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.
|
|
|
|
|
2024-07-06 23:28:20 +02:00
|
|
|
Si accept_none, return None si l'id est invalide ou ne correspond
|
2024-09-09 16:21:51 +02:00
|
|
|
pas à une instance. Sinon lève 404 en cas d'erreur.
|
2024-07-06 23:28:20 +02:00
|
|
|
"""
|
|
|
|
if not isinstance(oid, int):
|
|
|
|
try:
|
|
|
|
oid = int(oid)
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
if accept_none:
|
|
|
|
return None
|
|
|
|
abort(404, "oid invalide")
|
|
|
|
|
2024-07-19 09:42:44 +02:00
|
|
|
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)
|
|
|
|
|
2024-07-06 23:28:20 +02:00
|
|
|
if accept_none:
|
|
|
|
return query.first()
|
|
|
|
return query.first_or_404()
|
|
|
|
|
2024-10-29 19:18:36 +01:00
|
|
|
# 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)
|
|
|
|
|
2023-10-08 10:01:23 +02:00
|
|
|
|
2021-08-07 15:20:30 +02:00
|
|
|
from app.models.absences import Absence, AbsenceNotification, BilletAbsence
|
2021-08-13 00:34:58 +02:00
|
|
|
from app.models.departements import Departement
|
2021-08-07 15:20:30 +02:00
|
|
|
from app.models.etudiants import (
|
|
|
|
Identite,
|
|
|
|
Adresse,
|
|
|
|
Admission,
|
|
|
|
ItemSuivi,
|
|
|
|
ItemSuiviTag,
|
2021-08-14 18:54:32 +02:00
|
|
|
itemsuivi_tags_assoc,
|
2021-08-07 15:20:30 +02:00
|
|
|
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
|
2022-12-01 13:00:14 +01:00
|
|
|
from app.models.ues import DispenseUE, UniteEns
|
2021-08-07 15:20:30 +02:00
|
|
|
from app.models.formsemestre import (
|
|
|
|
FormSemestre,
|
2021-12-20 20:38:21 +01:00
|
|
|
FormSemestreEtape,
|
2021-11-12 22:17:46 +01:00
|
|
|
FormationModalite,
|
2021-12-20 20:38:21 +01:00
|
|
|
FormSemestreUECoef,
|
|
|
|
FormSemestreUEComputationExpr,
|
|
|
|
FormSemestreCustomMenu,
|
|
|
|
FormSemestreInscription,
|
2021-08-14 18:54:32 +02:00
|
|
|
notes_formsemestre_responsables,
|
2021-11-13 08:25:51 +01:00
|
|
|
NotesSemSet,
|
|
|
|
notes_semset_formsemestre,
|
|
|
|
)
|
2024-08-13 16:47:55 +02:00
|
|
|
from app.models.formsemestre_descr import (
|
|
|
|
FormSemestreDescription,
|
|
|
|
FORMSEMESTRE_DISPOSITIFS,
|
|
|
|
)
|
2021-11-13 08:25:51 +01:00
|
|
|
from app.models.moduleimpls import (
|
2021-11-12 22:17:46 +01:00
|
|
|
ModuleImpl,
|
2021-08-07 15:20:30 +02:00
|
|
|
notes_modules_enseignants,
|
2021-11-12 22:17:46 +01:00
|
|
|
ModuleImplInscription,
|
2021-11-13 08:25:51 +01:00
|
|
|
)
|
|
|
|
from app.models.evaluations import (
|
2021-11-12 22:17:46 +01:00
|
|
|
Evaluation,
|
2021-11-08 19:44:25 +01:00
|
|
|
EvaluationUEPoids,
|
2021-08-07 15:20:30 +02:00
|
|
|
)
|
|
|
|
from app.models.groups import Partition, GroupDescr, group_membership
|
|
|
|
from app.models.notes import (
|
2021-11-12 22:17:46 +01:00
|
|
|
BulAppreciations,
|
2021-08-07 15:20:30 +02:00
|
|
|
NotesNotes,
|
|
|
|
NotesNotesLog,
|
|
|
|
)
|
2022-02-06 16:09:17 +01:00
|
|
|
from app.models.validations import (
|
|
|
|
ScolarFormSemestreValidation,
|
|
|
|
ScolarAutorisationInscription,
|
|
|
|
)
|
2022-01-21 00:46:45 +01:00
|
|
|
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,
|
2022-05-28 11:38:22 +02:00
|
|
|
ApcParcours,
|
2022-10-29 15:42:03 +02:00
|
|
|
ApcReferentielCompetences,
|
|
|
|
ApcSituationPro,
|
2021-12-02 12:08:03 +01:00
|
|
|
)
|
2024-07-06 23:28:20 +02: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
|
2023-11-22 23:31:16 +01:00
|
|
|
from app.models.scolar_event import ScolarEvent
|