# -*- coding: UTF-8 -* """Définition d'un étudiant et données rattachées (adresses, annotations, ...) """ from functools import cached_property from flask import abort, url_for from flask import g, request import sqlalchemy from app import db from app import models from app.scodoc import notesdb as ndb from app.scodoc.sco_bac import Baccalaureat import app.scodoc.sco_utils as scu class Identite(db.Model): """étudiant""" __tablename__ = "identite" __table_args__ = ( db.UniqueConstraint("dept_id", "code_nip"), db.UniqueConstraint("dept_id", "code_ine"), ) id = db.Column(db.Integer, primary_key=True) etudid = db.synonym("id") dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True) nom = db.Column(db.Text()) prenom = db.Column(db.Text()) nom_usuel = db.Column(db.Text()) # optionnel (si present, affiché à la place du nom) civilite = db.Column(db.String(1), nullable=False) __table_args__ = (db.CheckConstraint("civilite IN ('M', 'F', 'X')"),) date_naissance = db.Column(db.Date) lieu_naissance = db.Column(db.Text()) dept_naissance = db.Column(db.Text()) nationalite = db.Column(db.Text()) statut = db.Column(db.Text()) boursier = db.Column(db.Boolean()) # True si boursier ('O' en ScoDoc7) photo_filename = db.Column(db.Text()) # Codes INE et NIP pas unique car le meme etud peut etre ds plusieurs dept code_nip = db.Column(db.Text(), index=True) code_ine = db.Column(db.Text(), index=True) # Ancien id ScoDoc7 pour les migrations de bases anciennes # ne pas utiliser après migrate_scodoc7_dept_archives scodoc7_id = db.Column(db.Text(), nullable=True) # adresses = db.relationship("Adresse", lazy="dynamic", backref="etud") billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic") # one-to-one relation: admission = db.relationship("Admission", backref="identite", lazy="dynamic") def __repr__(self): return f"<Etud {self.id} {self.nom} {self.prenom}>" @classmethod def from_request(cls, etudid=None, code_nip=None): """Etudiant à partir de l'etudid ou du code_nip, soit passés en argument soit retrouvés directement dans la requête web. Erreur 404 si inexistant. """ args = make_etud_args(etudid=etudid, code_nip=code_nip) return Identite.query.filter_by(**args).first_or_404() @property def civilite_str(self): """returns 'M.' ou 'Mme' ou '' (pour le genre neutre, personnes ne souhaitant pas d'affichage). """ return {"M": "M.", "F": "Mme", "X": ""}[self.civilite] def sex_nom(self, no_accents=False) -> str: "'M. DUPONTÉ', ou si no_accents, 'M. DUPONTE'" s = f"{self.civilite_str} {(self.nom_usuel or self.nom).upper()}" if no_accents: return scu.suppress_accents(s) return s def nom_disp(self) -> str: "Nom à afficher" if self.nom_usuel: return ( (self.nom_usuel + " (" + self.nom + ")") if self.nom else self.nom_usuel ) else: return self.nom @cached_property def nomprenom(self, reverse=False) -> str: """Civilité/nom/prenom pour affichages: "M. Pierre Dupont" Si reverse, "Dupont Pierre", sans civilité. """ nom = self.nom_usuel or self.nom prenom = self.prenom_str if reverse: fields = (nom, prenom) else: fields = (self.civilite_str, prenom, nom) return " ".join([x for x in fields if x]) @property def prenom_str(self): """Prénom à afficher. Par exemple: "Jean-Christophe" """ if not self.prenom: return "" frags = self.prenom.split() r = [] for frag in frags: fields = frag.split("-") r.append("-".join([x.lower().capitalize() for x in fields])) return " ".join(r) @cached_property def sort_key(self) -> tuple: "clé pour tris par ordre alphabétique" return (self.nom_usuel or self.nom).lower(), self.prenom.lower() def get_first_email(self, field="email") -> str: "Le mail associé à la première adrese de l'étudiant, ou None" return self.adresses[0].email or None if self.adresses.count() > 0 else None def to_dict_scodoc7(self): """Représentation dictionnaire, compatible ScoDoc7 mais sans infos admission """ e = dict(self.__dict__) e.pop("_sa_instance_state", None) # ScoDoc7 output_formators: (backward compat) e["etudid"] = self.id e["date_naissance"] = ndb.DateISOtoDMY(e["date_naissance"]) e["ne"] = {"M": "", "F": "ne"}.get(self.civilite, "(e)") return {k: e[k] or "" for k in e} # convert_null_outputs_to_empty def to_dict_bul(self, include_urls=True): """Infos exportées dans les bulletins""" from app.scodoc import sco_photos d = { "civilite": self.civilite, "code_ine": self.code_ine, "code_nip": self.code_nip, "date_naissance": self.date_naissance.isoformat() if self.date_naissance else None, "email": self.get_first_email(), "emailperso": self.get_first_email("emailperso"), "etudid": self.id, "nom": self.nom_disp(), "prenom": self.prenom, } if include_urls: d["fiche_url"] = url_for( "scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=self.id ) d["photo_url"] = (sco_photos.get_etud_photo_url(self.id),) return d def inscription_courante(self): """La première inscription à un formsemestre _actuellement_ en cours. None s'il n'y en a pas (ou plus, ou pas encore). """ r = [ ins for ins in self.formsemestre_inscriptions if ins.formsemestre.est_courant() ] return r[0] if r else None def inscription_courante_date(self, date_debut, date_fin): """La première inscription à un formsemestre incluant la période [date_debut, date_fin] """ r = [ ins for ins in self.formsemestre_inscriptions if ins.formsemestre.contient_periode(date_debut, date_fin) ] return r[0] if r else None def etat_inscription(self, formsemestre_id): """etat de l'inscription de cet étudiant au semestre: False si pas inscrit, ou scu.INSCRIT, DEMISSION, DEF """ # voir si ce n'est pas trop lent: ins = models.FormSemestreInscription.query.filter_by( etudid=self.id, formsemestre_id=formsemestre_id ).first() if ins: return ins.etat return False def make_etud_args( etudid=None, code_nip=None, use_request=True, raise_exc=False, abort_404=True ) -> dict: """forme args dict pour requete recherche etudiant On peut specifier etudid ou bien (si use_request) cherche dans la requete http: etudid, code_nip, code_ine (dans cet ordre). Résultat: dict avec soit "etudid", soit "code_nip", soit "code_ine" """ args = None if etudid: args = {"etudid": etudid} elif code_nip: args = {"code_nip": code_nip} elif use_request: # use form from current request (Flask global) if request.method == "POST": vals = request.form elif request.method == "GET": vals = request.args else: vals = {} if "etudid" in vals: args = {"etudid": int(vals["etudid"])} elif "code_nip" in vals: args = {"code_nip": str(vals["code_nip"])} elif "code_ine" in vals: args = {"code_ine": str(vals["code_ine"])} if not args: if abort_404: abort(404, "pas d'étudiant sélectionné") elif raise_exc: raise ValueError("make_etud_args: pas d'étudiant sélectionné !") return args class Adresse(db.Model): """Adresse d'un étudiant (le modèle permet plusieurs adresses, mais l'UI n'en gère qu'une seule) """ __tablename__ = "adresse" id = db.Column(db.Integer, primary_key=True) adresse_id = db.synonym("id") etudid = db.Column( db.Integer, db.ForeignKey("identite.id"), ) email = db.Column(db.Text()) # mail institutionnel emailperso = db.Column(db.Text) # email personnel (exterieur) domicile = db.Column(db.Text) codepostaldomicile = db.Column(db.Text) villedomicile = db.Column(db.Text) paysdomicile = db.Column(db.Text) telephone = db.Column(db.Text) telephonemobile = db.Column(db.Text) fax = db.Column(db.Text) typeadresse = db.Column( db.Text, default="domicile", server_default="domicile", nullable=False ) description = db.Column(db.Text) class Admission(db.Model): """Informations liées à l'admission d'un étudiant""" __tablename__ = "admissions" id = db.Column(db.Integer, primary_key=True) adm_id = db.synonym("id") etudid = db.Column( db.Integer, db.ForeignKey("identite.id"), ) # Anciens champs de ScoDoc7, à revoir pour être plus générique et souple # notamment dans le cadre du bac 2021 # de plus, certaines informations liées à APB ne sont plus disponibles # avec Parcoursup annee = db.Column(db.Integer) bac = db.Column(db.Text) specialite = db.Column(db.Text) annee_bac = db.Column(db.Integer) math = db.Column(db.Text) physique = db.Column(db.Float) anglais = db.Column(db.Float) francais = db.Column(db.Float) # Rang dans les voeux du candidat (inconnu avec APB et PS) rang = db.Column(db.Integer) # Qualité et décision du jury d'admission (ou de l'examinateur) qualite = db.Column(db.Float) rapporteur = db.Column(db.Text) decision = db.Column(db.Text) score = db.Column(db.Float) commentaire = db.Column(db.Text) # Lycée d'origine: nomlycee = db.Column(db.Text) villelycee = db.Column(db.Text) codepostallycee = db.Column(db.Text) codelycee = db.Column(db.Text) # 'APB', 'APC-PC', 'CEF', 'Direct', '?' (autre) type_admission = db.Column(db.Text) # était boursier dans le cycle precedent (lycee) ? boursier_prec = db.Column(db.Boolean()) # classement par le jury d'admission (1 à N), # global (pas celui d'APB si il y a des groupes) classement = db.Column(db.Integer) # code du groupe APB apb_groupe = db.Column(db.Text) # classement (1..Ngr) par le jury dans le groupe APB apb_classement_gr = db.Column(db.Integer) def get_bac(self) -> Baccalaureat: "Le bac. utiliser bac.abbrev() pour avoir une chaine de caractères." return Baccalaureat(self.bac, specialite=self.specialite) def to_dict(self, no_nulls=False): """Représentation dictionnaire,""" e = dict(self.__dict__) e.pop("_sa_instance_state", None) if no_nulls: for k in e: if e[k] is None: col_type = getattr( sqlalchemy.inspect(models.Admission).columns, "apb_groupe" ).expression.type if isinstance(col_type, sqlalchemy.Text): e[k] = "" elif isinstance(col_type, sqlalchemy.Integer): e[k] = 0 elif isinstance(col_type, sqlalchemy.Boolean): e[k] = False return e # Suivi scolarité / débouchés class ItemSuivi(db.Model): __tablename__ = "itemsuivi" id = db.Column(db.Integer, primary_key=True) itemsuivi_id = db.synonym("id") etudid = db.Column( db.Integer, db.ForeignKey("identite.id"), ) item_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) situation = db.Column(db.Text) class ItemSuiviTag(db.Model): __tablename__ = "itemsuivi_tags" id = db.Column(db.Integer, primary_key=True) dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True) tag_id = db.synonym("id") title = db.Column(db.Text(), nullable=False, unique=True) # Association tag <-> module itemsuivi_tags_assoc = db.Table( "itemsuivi_tags_assoc", db.Column( "tag_id", db.Integer, db.ForeignKey("itemsuivi_tags.id", ondelete="CASCADE") ), db.Column( "itemsuivi_id", db.Integer, db.ForeignKey("itemsuivi.id", ondelete="CASCADE") ), ) class EtudAnnotation(db.Model): """Annotation sur un étudiant""" __tablename__ = "etud_annotations" id = db.Column(db.Integer, primary_key=True) date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) etudid = db.Column(db.Integer) # sans contrainte (compat ScoDoc 7)) author = db.Column(db.Text) # le pseudo (user_name), was zope_authenticated_user comment = db.Column(db.Text)