forked from ScoDoc/ScoDoc
- Liste homonymes sur formulaire édition étudiant;
- Ajoute helper class ScoDocModel - Fix clonage étudiant - Tolère données admission manquantes dans recherche get_etud - Fix #771
This commit is contained in:
parent
574fae2170
commit
3fdef1d4a0
@ -44,4 +44,8 @@ def get_etud(etudid=None, nip=None, ine=None) -> models.Identite:
|
||||
query = query.join(Departement).filter(
|
||||
or_(Departement.acronym == acronym for acronym in allowed_depts)
|
||||
)
|
||||
return query.join(Admission).order_by(desc(Admission.annee)).first()
|
||||
etud = query.join(Admission).order_by(desc(Admission.annee)).first()
|
||||
# dans de rares cas (bricolages manuels, bugs), l'étudiant n'a pas de données d'admission
|
||||
if etud is None:
|
||||
etud = query.first()
|
||||
return etud
|
||||
|
@ -4,6 +4,7 @@
|
||||
"""
|
||||
|
||||
import sqlalchemy
|
||||
from app import db
|
||||
|
||||
CODE_STR_LEN = 16 # chaine pour les codes
|
||||
SHORT_STR_LEN = 32 # courtes chaine, eg acronymes
|
||||
@ -21,6 +22,67 @@ convention = {
|
||||
|
||||
metadata_obj = sqlalchemy.MetaData(naming_convention=convention)
|
||||
|
||||
|
||||
class ScoDocModel:
|
||||
"Mixin class for our models. Add somme useful methods for editing, cloning, etc."
|
||||
|
||||
def clone(self, not_copying=()):
|
||||
"""Clone, not copying the given attrs
|
||||
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):
|
||||
"""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 arributes for the given model and ignore others.
|
||||
"""
|
||||
args = cls.filter_model_attributes(data)
|
||||
obj = cls(**args)
|
||||
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.
|
||||
By default, excluded == { 'id' }"""
|
||||
excluded = {"id"} if excluded is None else set()
|
||||
# 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 in the given dict. No side effect."
|
||||
# virtual, by default, do nothing
|
||||
return args
|
||||
|
||||
def from_dict(self, args: dict):
|
||||
"Update object's fields given in dict. Add to session but don't commit."
|
||||
args_dict = self.convert_dict_fields(self.filter_model_attributes(args))
|
||||
for key, value in args_dict.items():
|
||||
if hasattr(self, key):
|
||||
setattr(self, key, value)
|
||||
db.session.add(self)
|
||||
|
||||
|
||||
from app.models.absences import Absence, AbsenceNotification, BilletAbsence
|
||||
from app.models.departements import Departement
|
||||
from app.models.etudiants import (
|
||||
|
@ -88,7 +88,7 @@ class Identite(db.Model):
|
||||
|
||||
def clone(self, not_copying=(), new_dept_id: int = None):
|
||||
"""Clone, not copying the given attrs
|
||||
Clone aussi les adresses.
|
||||
Clone aussi les adresses et infos d'admission.
|
||||
Si new_dept_id est None, le nouvel étudiant n'a pas de département.
|
||||
Attention: la copie n'a pas d'id avant le prochain flush ou commit.
|
||||
"""
|
||||
@ -123,8 +123,10 @@ class Identite(db.Model):
|
||||
for k in not_copying:
|
||||
d.pop(k, None)
|
||||
copy = self.__class__(**d)
|
||||
copy.adresses = [adr.clone() for adr in self.adresses]
|
||||
db.session.add(copy)
|
||||
copy.adresses = [adr.clone() for adr in self.adresses]
|
||||
for admission in self.admission:
|
||||
copy.admission.append(admission.clone())
|
||||
log(
|
||||
f"cloning etud <{self.id} {self.nom!r} {self.prenom!r}> in dept_id={new_dept_id}"
|
||||
)
|
||||
@ -133,7 +135,7 @@ class Identite(db.Model):
|
||||
def html_link_fiche(self) -> str:
|
||||
"lien vers la fiche"
|
||||
return f"""<a class="stdlink" href="{
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=self.id)
|
||||
url_for("scolar.ficheEtud", scodoc_dept=self.departement.acronym, etudid=self.id)
|
||||
}">{self.nomprenom}</a>"""
|
||||
|
||||
@classmethod
|
||||
@ -677,7 +679,7 @@ def pivot_year(y) -> int:
|
||||
return y
|
||||
|
||||
|
||||
class Adresse(db.Model):
|
||||
class Adresse(db.Model, models.ScoDocModel):
|
||||
"""Adresse d'un étudiant
|
||||
(le modèle permet plusieurs adresses, mais l'UI n'en gère qu'une seule)
|
||||
"""
|
||||
@ -704,19 +706,6 @@ class Adresse(db.Model):
|
||||
)
|
||||
description = db.Column(db.Text)
|
||||
|
||||
def clone(self, not_copying=()):
|
||||
"""Clone, not copying the given attrs
|
||||
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
|
||||
|
||||
def to_dict(self, convert_nulls_to_str=False):
|
||||
"""Représentation dictionnaire,"""
|
||||
e = dict(self.__dict__)
|
||||
@ -726,7 +715,7 @@ class Adresse(db.Model):
|
||||
return e
|
||||
|
||||
|
||||
class Admission(db.Model):
|
||||
class Admission(db.Model, models.ScoDocModel):
|
||||
"""Informations liées à l'admission d'un étudiant"""
|
||||
|
||||
__tablename__ = "admissions"
|
||||
@ -818,16 +807,6 @@ class Admission(db.Model):
|
||||
args_dict[key] = value
|
||||
return args_dict
|
||||
|
||||
def from_dict(self, args: dict): # TODO à refactoriser dans une super-classe
|
||||
"update fields given in dict. Add to session but don't commit."
|
||||
args_dict = Admission.convert_dict_fields(args)
|
||||
args_dict.pop("adm_id", None)
|
||||
args_dict.pop("id", None)
|
||||
for key, value in args_dict.items():
|
||||
if hasattr(self, key):
|
||||
setattr(self, key, value)
|
||||
db.session.add(self)
|
||||
|
||||
|
||||
# Suivi scolarité / débouchés
|
||||
class ItemSuivi(db.Model):
|
||||
|
@ -267,33 +267,30 @@ def identite_edit_nocheck(cnx, args):
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def check_nom_prenom(cnx, nom="", prenom="", etudid=None):
|
||||
def check_nom_prenom_homonyms(
|
||||
nom: str = "", prenom: str = "", etudid=None
|
||||
) -> tuple[bool, list[Identite]]:
|
||||
"""Check if nom and prenom are valid.
|
||||
Also check for duplicates (homonyms), excluding etudid :
|
||||
in general, homonyms are allowed, but it may be useful to generate a warning.
|
||||
Returns:
|
||||
True | False, NbHomonyms
|
||||
True | False, homonyms
|
||||
"""
|
||||
if not nom or (not prenom and not scu.CONFIG.ALLOW_NULL_PRENOM):
|
||||
return False, 0
|
||||
return False, []
|
||||
nom = nom.lower().strip()
|
||||
if prenom:
|
||||
prenom = prenom.lower().strip()
|
||||
# Don't allow some special cars (eg used in sql regexps)
|
||||
if scu.FORBIDDEN_CHARS_EXP.search(nom) or scu.FORBIDDEN_CHARS_EXP.search(prenom):
|
||||
return False, 0
|
||||
# Now count homonyms (dans tous les départements):
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
req = """SELECT id
|
||||
FROM identite
|
||||
WHERE lower(nom) ~ %(nom)s
|
||||
and lower(prenom) ~ %(prenom)s
|
||||
"""
|
||||
if etudid:
|
||||
req += " and id <> %(etudid)s"
|
||||
cursor.execute(req, {"nom": nom, "prenom": prenom, "etudid": etudid})
|
||||
res = cursor.dictfetchall()
|
||||
return True, len(res)
|
||||
# Liste homonymes (dans tous les départements):
|
||||
query = Identite.query.filter(
|
||||
Identite.nom.ilike(nom + "%"), Identite.prenom.ilike(prenom + "%")
|
||||
)
|
||||
if etudid is not None:
|
||||
query = query.filter(Identite.id != etudid)
|
||||
return True, query.all()
|
||||
|
||||
|
||||
def _check_duplicate_code(cnx, args, code_name, disable_notify=False, edit=True):
|
||||
|
@ -409,18 +409,19 @@ def scolars_import_excel_file(
|
||||
raise ScoValueError("Code INE dupliqué (%s)" % values["code_ine"])
|
||||
# Check nom/prenom
|
||||
ok = False
|
||||
homonyms = []
|
||||
if "nom" in values and "prenom" in values:
|
||||
ok, nb_homonyms = sco_etud.check_nom_prenom(
|
||||
cnx, nom=values["nom"], prenom=values["prenom"]
|
||||
ok, homonyms = sco_etud.check_nom_prenom_homonyms(
|
||||
nom=values["nom"], prenom=values["prenom"]
|
||||
)
|
||||
if not ok:
|
||||
raise ScoValueError(
|
||||
"nom ou prénom invalide sur la ligne %d" % (linenum)
|
||||
f"nom ou prénom invalide sur la ligne {linenum}"
|
||||
)
|
||||
if nb_homonyms:
|
||||
if homonyms:
|
||||
np_imported_homonyms += 1
|
||||
# Insert in DB tables
|
||||
formsemestre_id_etud = _import_one_student(
|
||||
_import_one_student(
|
||||
cnx,
|
||||
formsemestre_id,
|
||||
values,
|
||||
@ -431,11 +432,11 @@ def scolars_import_excel_file(
|
||||
)
|
||||
|
||||
# Verification proportion d'homonymes: si > 10%, abandonne
|
||||
log("scolars_import_excel_file: detected %d homonyms" % np_imported_homonyms)
|
||||
log(f"scolars_import_excel_file: detected {np_imported_homonyms} homonyms")
|
||||
if check_homonyms and np_imported_homonyms > len(created_etudids) / 10:
|
||||
log("scolars_import_excel_file: too many homonyms")
|
||||
raise ScoValueError(
|
||||
"Il y a trop d'homonymes (%d étudiants)" % np_imported_homonyms
|
||||
f"Il y a trop d'homonymes ({np_imported_homonyms} étudiants)"
|
||||
)
|
||||
except:
|
||||
cnx.rollback()
|
||||
@ -444,7 +445,7 @@ def scolars_import_excel_file(
|
||||
# here we try to remove all created students
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
for etudid in created_etudids:
|
||||
log("scolars_import_excel_file: deleting etudid=%s" % etudid)
|
||||
log(f"scolars_import_excel_file: deleting etudid={etudid}")
|
||||
cursor.execute(
|
||||
"delete from notes_moduleimpl_inscription where etudid=%(etudid)s",
|
||||
{"etudid": etudid},
|
||||
|
@ -1701,10 +1701,8 @@ function deleteJustificatif(justif_id) {
|
||||
|
||||
function errorAlert() {
|
||||
const html = `
|
||||
<h3>Avez vous les droits suffisant pour cette action ?</h3>
|
||||
<p>Si c'est bien le cas : demandez de l'aide sur le canal Assistance de ScoDoc</p>
|
||||
<br>
|
||||
<p><i>pour les développeurs : l'erreur est affichée dans la console JS</i></p>
|
||||
<h4>Il peut s'agir d'un problème de droits, ou d'une modification survenue sur le serveur.</h4>
|
||||
<p>Si le problème persiste, demandez de l'aide sur le Discord d'assistance de ScoDoc</p>
|
||||
|
||||
`;
|
||||
const div = document.createElement("div");
|
||||
|
@ -54,6 +54,7 @@ from app.decorators import (
|
||||
permission_required_compat_scodoc7,
|
||||
)
|
||||
from app.models import (
|
||||
Admission,
|
||||
Departement,
|
||||
FormSemestre,
|
||||
Identite,
|
||||
@ -1374,11 +1375,9 @@ def _etudident_create_or_edit_form(edit):
|
||||
# setup form init values
|
||||
if not etudid:
|
||||
raise ValueError("missing etudid parameter")
|
||||
etud_o: Identite = Identite.get_etud(etudid)
|
||||
descr.append(("etudid", {"default": etudid, "input_type": "hidden"}))
|
||||
H.append(
|
||||
'<h2>Modification d\'un étudiant (<a href="%s">fiche</a>)</h2>'
|
||||
% url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
)
|
||||
H.append(f"""<h2>Modification des données de {etud_o.html_link_fiche()}</h2>""")
|
||||
initvalues = sco_etud.etudident_list(cnx, {"etudid": etudid})
|
||||
assert len(initvalues) == 1
|
||||
initvalues = initvalues[0]
|
||||
@ -1724,9 +1723,10 @@ def _etudident_create_or_edit_form(edit):
|
||||
etudid = tf[2]["etudid"]
|
||||
else:
|
||||
etudid = None
|
||||
ok, NbHomonyms = sco_etud.check_nom_prenom(
|
||||
cnx, nom=tf[2]["nom"], prenom=tf[2]["prenom"], etudid=etudid
|
||||
ok, homonyms = sco_etud.check_nom_prenom_homonyms(
|
||||
nom=tf[2]["nom"], prenom=tf[2]["prenom"], etudid=etudid
|
||||
)
|
||||
nb_homonyms = len(homonyms)
|
||||
if not ok:
|
||||
return (
|
||||
"\n".join(H)
|
||||
@ -1736,16 +1736,29 @@ def _etudident_create_or_edit_form(edit):
|
||||
+ A
|
||||
+ F
|
||||
)
|
||||
# log('NbHomonyms=%s' % NbHomonyms)
|
||||
if not tf[2]["dont_check_homonyms"] and NbHomonyms > 0:
|
||||
if not tf[2]["dont_check_homonyms"] and nb_homonyms > 0:
|
||||
homonyms_html = f"""
|
||||
<div class="homonyms"
|
||||
style="border-radius: 8px; border: 1px solid black; background-color: #fdd6ad; padding: 8px; max-width: 80%;">
|
||||
<div><b>Homonymes</b> (dans tous les départements)</div>
|
||||
<ul>
|
||||
<li>{'</li><li>'.join( [ '<b style="margin-right: 2em;">' + e.departement.acronym + "</b>" + e.html_link_fiche() for e in homonyms ])}
|
||||
</ul>
|
||||
</div>
|
||||
"""
|
||||
return (
|
||||
"\n".join(H)
|
||||
+ tf_error_message(
|
||||
"""Attention: il y a déjà un étudiant portant des noms et prénoms proches. Vous pouvez forcer la présence d'un homonyme en cochant "autoriser les homonymes" en bas du formulaire."""
|
||||
"""Attention: il y a déjà un étudiant portant des noms et prénoms proches
|
||||
(voir liste en bas de page).
|
||||
Vous pouvez forcer la présence d'un homonyme en cochant
|
||||
"autoriser les homonymes" en bas du formulaire.
|
||||
"""
|
||||
)
|
||||
+ tf[1]
|
||||
+ "<p>"
|
||||
+ A
|
||||
+ homonyms_html
|
||||
+ F
|
||||
)
|
||||
|
||||
@ -1754,10 +1767,13 @@ def _etudident_create_or_edit_form(edit):
|
||||
etudid = etud["etudid"]
|
||||
else:
|
||||
# modif d'un etudiant
|
||||
etud_o = db.session.get(Identite, tf[2]["etudid"])
|
||||
etud_o.from_dict(tf[2])
|
||||
db.session.add(etud_o)
|
||||
admission = etud_o.admission.first()
|
||||
if admission is None:
|
||||
# ? ne devrait pas arriver mais...
|
||||
admission = Admission()
|
||||
admission.etudid = etud_o.id
|
||||
admission.from_dict(tf[2])
|
||||
db.session.add(admission)
|
||||
db.session.commit()
|
||||
|
Loading…
Reference in New Issue
Block a user