2021-08-07 15:20:30 +02:00
|
|
|
# -*- coding: UTF-8 -*
|
|
|
|
|
|
|
|
"""Définition d'un étudiant
|
|
|
|
et données rattachées (adresses, annotations, ...)
|
|
|
|
"""
|
|
|
|
|
2021-12-26 19:15:47 +01:00
|
|
|
from functools import cached_property
|
|
|
|
from flask import abort, url_for
|
|
|
|
from flask import g, request
|
2021-12-21 15:33:21 +01:00
|
|
|
|
2021-08-07 15:20:30 +02:00
|
|
|
from app import db
|
2021-12-14 23:03:59 +01:00
|
|
|
from app import models
|
2021-08-07 15:20:30 +02:00
|
|
|
from app.models import APO_CODE_STR_LEN
|
|
|
|
from app.models import SHORT_STR_LEN
|
|
|
|
from app.models import CODE_STR_LEN
|
2021-12-30 23:58:38 +01:00
|
|
|
from app.scodoc import notesdb as ndb
|
2021-08-07 15:20:30 +02:00
|
|
|
|
|
|
|
|
|
|
|
class Identite(db.Model):
|
|
|
|
"""étudiant"""
|
|
|
|
|
|
|
|
__tablename__ = "identite"
|
2021-08-15 22:20:07 +02:00
|
|
|
__table_args__ = (
|
|
|
|
db.UniqueConstraint("dept_id", "code_nip"),
|
|
|
|
db.UniqueConstraint("dept_id", "code_ine"),
|
|
|
|
)
|
2021-08-07 15:20:30 +02:00
|
|
|
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
|
|
etudid = db.synonym("id")
|
2021-08-13 00:34:58 +02:00
|
|
|
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
|
2021-08-07 15:20:30 +02:00
|
|
|
|
|
|
|
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())
|
2021-08-15 22:20:07 +02:00
|
|
|
# Codes INE et NIP pas unique car le meme etud peut etre ds plusieurs dept
|
|
|
|
code_nip = db.Column(db.Text())
|
|
|
|
code_ine = db.Column(db.Text())
|
2021-08-27 22:16:10 +02:00
|
|
|
# Ancien id ScoDoc7 pour les migrations de bases anciennes
|
2021-11-01 15:16:51 +01:00
|
|
|
# ne pas utiliser après migrate_scodoc7_dept_archives
|
2021-08-27 22:16:10 +02:00
|
|
|
scodoc7_id = db.Column(db.Text(), nullable=True)
|
2021-09-28 09:14:04 +02:00
|
|
|
#
|
2021-12-05 20:21:51 +01:00
|
|
|
adresses = db.relationship("Adresse", lazy="dynamic", backref="etud")
|
2021-09-28 09:14:04 +02:00
|
|
|
billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic")
|
2021-08-07 15:20:30 +02:00
|
|
|
|
2021-11-20 16:35:09 +01:00
|
|
|
def __repr__(self):
|
|
|
|
return f"<Etud {self.id} {self.nom} {self.prenom}>"
|
|
|
|
|
2021-12-26 19:15:47 +01:00
|
|
|
@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
|
2021-12-04 21:04:09 +01:00
|
|
|
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]
|
|
|
|
|
2021-12-26 19:15:47 +01:00
|
|
|
def nom_disp(self) -> str:
|
|
|
|
"Nom à afficher"
|
2021-12-04 21:04:09 +01:00
|
|
|
if self.nom_usuel:
|
|
|
|
return (
|
|
|
|
(self.nom_usuel + " (" + self.nom + ")") if self.nom else self.nom_usuel
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
return self.nom
|
|
|
|
|
2021-12-26 19:15:47 +01:00
|
|
|
@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)
|
|
|
|
|
2021-12-30 23:58:38 +01:00
|
|
|
@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()
|
|
|
|
|
2021-12-11 16:46:15 +01:00
|
|
|
def get_first_email(self, field="email") -> str:
|
2021-12-26 19:15:47 +01:00
|
|
|
"Le mail associé à la première adrese de l'étudiant, ou None"
|
2021-12-11 16:46:15 +01:00
|
|
|
return self.adresses[0].email or None if self.adresses.count() > 0 else None
|
|
|
|
|
2021-12-30 23:58:38 +01:00
|
|
|
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["date_naissance"] = ndb.DateISOtoDMY(e["date_naissance"])
|
|
|
|
return {k: e[k] or "" for k in e} # convert_null_outputs_to_empty
|
|
|
|
|
2021-12-21 15:33:21 +01:00
|
|
|
def to_dict_bul(self, include_urls=True):
|
2021-12-05 20:21:51 +01:00
|
|
|
"""Infos exportées dans les bulletins"""
|
2021-12-20 22:53:09 +01:00
|
|
|
from app.scodoc import sco_photos
|
|
|
|
|
|
|
|
d = {
|
2021-12-05 20:21:51 +01:00
|
|
|
"civilite": self.civilite,
|
2021-12-21 15:10:20 +01:00
|
|
|
"code_ine": self.code_ine,
|
|
|
|
"code_nip": self.code_nip,
|
2021-12-05 20:21:51 +01:00
|
|
|
"date_naissance": self.date_naissance.isoformat()
|
|
|
|
if self.date_naissance
|
|
|
|
else None,
|
2021-12-11 16:46:15 +01:00
|
|
|
"email": self.get_first_email(),
|
|
|
|
"emailperso": self.get_first_email("emailperso"),
|
2021-12-05 20:21:51 +01:00
|
|
|
"etudid": self.id,
|
|
|
|
"nom": self.nom_disp(),
|
|
|
|
"prenom": self.prenom,
|
|
|
|
}
|
2021-12-21 15:33:21 +01:00
|
|
|
if include_urls:
|
|
|
|
d["fiche_url"] = url_for(
|
|
|
|
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=self.id
|
|
|
|
)
|
2021-12-20 22:53:09 +01:00
|
|
|
d["photo_url"] = (sco_photos.get_etud_photo_url(self.id),)
|
|
|
|
return d
|
2021-12-05 20:21:51 +01:00
|
|
|
|
2021-12-04 21:04:09 +01:00
|
|
|
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
|
|
|
|
|
2021-12-23 19:28:25 +01:00
|
|
|
def inscription_courante_date(self, date_debut, date_fin):
|
2021-12-29 14:41:33 +01:00
|
|
|
"""La première inscription à un formsemestre incluant la
|
|
|
|
période [date_debut, date_fin]
|
|
|
|
"""
|
2021-12-23 19:28:25 +01:00
|
|
|
r = [
|
|
|
|
ins
|
|
|
|
for ins in self.formsemestre_inscriptions
|
2021-12-29 14:41:33 +01:00
|
|
|
if ins.formsemestre.contient_periode(date_debut, date_fin)
|
2021-12-23 19:28:25 +01:00
|
|
|
]
|
|
|
|
return r[0] if r else None
|
|
|
|
|
2021-12-14 23:03:59 +01:00
|
|
|
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:
|
2021-12-20 20:38:21 +01:00
|
|
|
ins = models.FormSemestreInscription.query.filter_by(
|
2021-12-14 23:03:59 +01:00
|
|
|
etudid=self.id, formsemestre_id=formsemestre_id
|
|
|
|
).first()
|
|
|
|
if ins:
|
|
|
|
return ins.etat
|
|
|
|
return False
|
|
|
|
|
2021-08-07 15:20:30 +02:00
|
|
|
|
2021-12-26 19:15:47 +01:00
|
|
|
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
|
|
|
|
|
|
|
|
|
2021-08-07 15:20:30 +02:00
|
|
|
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)
|
2021-08-10 00:23:30 +02:00
|
|
|
typeadresse = db.Column(
|
|
|
|
db.Text, default="domicile", server_default="domicile", nullable=False
|
|
|
|
)
|
2021-08-07 15:20:30 +02:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
# 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)
|
2021-08-29 21:21:41 +02:00
|
|
|
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
|
2021-08-07 15:20:30 +02:00
|
|
|
tag_id = db.synonym("id")
|
2021-08-15 22:13:41 +02:00
|
|
|
title = db.Column(db.Text(), nullable=False, unique=True)
|
2021-08-07 15:20:30 +02:00
|
|
|
|
|
|
|
|
|
|
|
# Association tag <-> module
|
|
|
|
itemsuivi_tags_assoc = db.Table(
|
|
|
|
"itemsuivi_tags_assoc",
|
2021-11-27 19:04:30 +01:00
|
|
|
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")
|
|
|
|
),
|
2021-08-07 15:20:30 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
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())
|
2021-08-14 18:54:32 +02:00
|
|
|
etudid = db.Column(db.Integer) # sans contrainte (compat ScoDoc 7))
|
|
|
|
author = db.Column(db.Text) # le pseudo (user_name), was zope_authenticated_user
|
2021-08-07 15:20:30 +02:00
|
|
|
comment = db.Column(db.Text)
|