# -*- 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) 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 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). Use only relevant attributes for the given model and ignore others. """ 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 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. """ args_dict = self.convert_dict_fields( self.filter_model_attributes(args, excluded=excluded) ) modified = False for key, value in args_dict.items(): if hasattr(self, key) and value != getattr(self, key): setattr(self, key, value) modified = True db.session.add(self) return modified def to_dict(self) -> dict: "dict" d = dict(self.__dict__) d.pop("_sa_instance_state", None) return d 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 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 from app.models.formations import Formation, Matiere 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, NotesSemSet, notes_semset_formsemestre, ) from app.models.formsemestre_descr import ( FormSemestreDescription, FORMSEMESTRE_DISPOSITIFS, ) from app.models.moduleimpls import ( ModuleImpl, notes_modules_enseignants, ModuleImplInscription, ) from app.models.evaluations import ( Evaluation, EvaluationUEPoids, ) from app.models.groups import Partition, GroupDescr, group_membership from app.models.notes import ( BulAppreciations, NotesNotes, NotesNotesLog, ) from app.models.validations import ( ScolarFormSemestreValidation, ScolarAutorisationInscription, ) from app.models.preferences import ScoPreference from app.models.but_refcomp import ( ApcAppCritique, ApcCompetence, ApcNiveau, ApcParcours, ApcReferentielCompetences, ApcSituationPro, ) from app.models.but_validations import ( ApcValidationAnnee, ApcValidationRCUE, ValidationDUT120, ) from app.models.config import ScoDocSiteConfig from app.models.assiduites import Assiduite, Justificatif from app.models.scolar_event import ScolarEvent