From e87bb9a8db02d05e349f5fea563a3c36cd9949ce Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 17 Oct 2023 22:12:28 +0200 Subject: [PATCH] =?UTF-8?q?Am=C3=A9liore=20code=20gestion=20civilit=C3=A9.?= =?UTF-8?q?=20Tests=20unitaires=20=C3=A9tudiants=20et=20import=20excel.=20?= =?UTF-8?q?Diverses=20corrections.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/etudiants.py | 51 +++-- app/scodoc/sco_etud.py | 88 +++++---- app/scodoc/sco_import_etuds.py | 15 +- app/scodoc/sco_utils.py | 6 + app/views/scolar.py | 4 - .../497ba81343f7_identite_admission.py | 36 ++++ tests/ressources/misc/ImportEtudiants.xlsx | Bin 0 -> 9870 bytes tests/unit/setup.py | 27 ++- tests/unit/test_etudiants.py | 187 ++++++++++++++++++ tools/format_import_etudiants.txt | 7 +- 10 files changed, 343 insertions(+), 78 deletions(-) create mode 100644 tests/ressources/misc/ImportEtudiants.xlsx create mode 100644 tests/unit/test_etudiants.py diff --git a/app/models/etudiants.py b/app/models/etudiants.py index 764dbcb9..95f30d18 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -91,8 +91,8 @@ class Identite(db.Model, models.ScoDocModel): unique=True, postgresql_where=(code_ine.isnot(None)), ), - db.CheckConstraint("civilite IN ('M', 'F', 'X')"), - db.CheckConstraint("civilite_etat_civil IN ('M', 'F', 'X')"), + db.CheckConstraint("civilite IN ('M', 'F', 'X')"), # non nullable + db.CheckConstraint("civilite_etat_civil IN ('M', 'F', 'X')"), # nullable ) # ----- Relations adresses = db.relationship( @@ -213,33 +213,40 @@ class Identite(db.Model, models.ScoDocModel): return etud @property - def civilite_str(self): - """returns 'M.' ou 'Mme' ou '' (pour le genre neutre, + def civilite_str(self) -> str: + """returns civilité usuelle: 'M.' ou 'Mme' ou '' (pour le genre neutre, personnes ne souhaitant pas d'affichage). """ return {"M": "M.", "F": "Mme", "X": ""}[self.civilite] @property - def civilite_etat_civil_str(self): - """returns 'M.' ou 'Mme' ou '' (pour le genre neutre, - personnes ne souhaitant pas d'affichage). + def civilite_etat_civil_str(self) -> str: + """returns 'M.' ou 'Mme', selon état civil officiel. + La France ne reconnait pas le genre neutre dans l'état civil: + si cette donnée état civil est précisée, elle est utilisée, + sinon on renvoie la civilité usuelle. """ - return {"M": "M.", "F": "Mme", "X": ""}[self.civilite_etat_civil] + return ( + {"M": "M.", "F": "Mme"}.get(self.civilite_etat_civil, "") + if self.civilite_etat_civil + else self.civilite_str + ) def sex_nom(self, no_accents=False) -> str: - "'M. DUPONTÉ', ou si no_accents, 'M. DUPONTE'" + "'M. DUPONTÉ', ou si no_accents, 'M. DUPONTE'. Civilité usuelle." s = f"{self.civilite_str} {(self.nom_usuel or self.nom).upper()}" if no_accents: return scu.suppress_accents(s) return s @property - def e(self): - "terminaison en français: 'ne', '', 'ou '(e)'" + def e(self) -> str: + "terminaison en français: 'ne', '', 'ou '(e)', selon la civilité usuelle" return {"M": "", "F": "e"}.get(self.civilite, "(e)") def nom_disp(self) -> str: - "Nom à afficher" + """Nom à afficher. + Note: le nom est stocké en base en majuscules.""" if self.nom_usuel: return ( (self.nom_usuel + " (" + self.nom + ")") if self.nom else self.nom_usuel @@ -273,7 +280,8 @@ class Identite(db.Model, models.ScoDocModel): return " ".join(r) @property - def etat_civil(self): + def etat_civil(self) -> str: + "M. Prénom NOM, utilisant les données état civil si présentes, usuelles sinon." if self.prenom_etat_civil: civ = {"M": "M.", "F": "Mme", "X": ""}[self.civilite_etat_civil] return f"{civ} {self.prenom_etat_civil} {self.nom}" @@ -343,7 +351,7 @@ class Identite(db.Model, models.ScoDocModel): if key == "civilite": # requis value = input_civilite(value) elif key == "civilite_etat_civil": - value = input_civilite(value) if value else None + value = input_civilite_etat_civil(value) elif key == "boursier": value = bool(value) elif key == "date_naissance": @@ -375,7 +383,7 @@ class Identite(db.Model, models.ScoDocModel): e_dict.pop("_sa_instance_state", None) # ScoDoc7 output_formators: (backward compat) e_dict["etudid"] = self.id - e_dict["date_naissance"] = ndb.DateISOtoDMY(e_dict["date_naissance"]) + e_dict["date_naissance"] = ndb.DateISOtoDMY(e_dict.get("date_naissance", "")) e_dict["ne"] = self.e e_dict["nomprenom"] = self.nomprenom adresse = self.adresses.first() @@ -711,6 +719,11 @@ def input_civilite(s: str) -> str: raise ScoValueError(f"valeur invalide pour la civilité: {s}") +def input_civilite_etat_civil(s: str) -> str | None: + """Same as input_civilite, but empty gives None (null)""" + return input_civilite(s) if s and s.strip() else None + + PIVOT_YEAR = 70 @@ -786,9 +799,9 @@ class Admission(db.Model, models.ScoDocModel): 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) + physique = db.Column(db.Text) + anglais = db.Column(db.Text) + francais = db.Column(db.Text) # 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) @@ -852,8 +865,6 @@ class Admission(db.Model, models.ScoDocModel): value = None if key in fs_uppercase and value: value = value.upper() - if key == "civilite" or key == "civilite_etat_civil": - value = input_civilite(value) elif key == "annee" or key == "annee_bac": value = pivot_year(value) elif key == "classement" or key == "apb_classement_gr": diff --git a/app/scodoc/sco_etud.py b/app/scodoc/sco_etud.py index efa00eee..a90b54d3 100644 --- a/app/scodoc/sco_etud.py +++ b/app/scodoc/sco_etud.py @@ -38,7 +38,12 @@ from flask import url_for, g from app import db, email from app import log from app.models import Admission, Identite -from app.models.etudiants import input_civilite, make_etud_args, pivot_year +from app.models.etudiants import ( + input_civilite, + input_civilite_etat_civil, + make_etud_args, + pivot_year, +) import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb from app.scodoc.sco_exceptions import ScoGenError, ScoValueError @@ -47,9 +52,13 @@ from app.scodoc import sco_preferences from app.scodoc.scolog import logdb -def format_etud_ident(etud): +def format_etud_ident(etud: dict): """Format identite de l'étudiant (modifié en place) - nom, prénom et formes associees + nom, prénom et formes associees. + + Note: par rapport à Identite.to_dict_bul(), + ajoute les champs: + 'email_default', 'nom_disp', 'nom_usuel', 'civilite_etat_civil_str', 'ne', 'civilite_str' """ etud["nom"] = format_nom(etud["nom"]) if "nom_usuel" in etud: @@ -65,7 +74,7 @@ def format_etud_ident(etud): etud["civilite_etat_civil_str"] = ( format_civilite(etud["civilite_etat_civil"]) if etud["civilite_etat_civil"] - else "" + else etud["civilite_str"] ) # Nom à afficher: if etud["nom_usuel"]: @@ -76,7 +85,7 @@ def format_etud_ident(etud): etud["nom_disp"] = etud["nom"] etud["nomprenom"] = format_nomprenom(etud) # M. Pierre DUPONT - etud["etat_civil"] = format_etat_civil(etud) + etud["etat_civil"] = _format_etat_civil(etud) if etud["civilite"] == "M": etud["ne"] = "" elif etud["civilite"] == "F": @@ -147,12 +156,13 @@ def format_civilite(civilite): raise ScoValueError(f"valeur invalide pour la civilité: {civilite}") from exc -def format_etat_civil(etud: dict): - if etud["prenom_etat_civil"]: - civ = {"M": "M.", "F": "Mme", "X": ""}[etud.get("civilite_etat_civil", "X")] - return f'{civ} {etud["prenom_etat_civil"]} {etud["nom"]}' - else: - return etud["nomprenom"] +def _format_etat_civil(etud: dict) -> str: + "Mme Béatrice DUPONT, en utilisant les données d'état civil si indiquées." + if etud["prenom_etat_civil"] or etud["civilite_etat_civil"]: + return f"""{etud["civilite_etat_civil_str"]} { + etud["prenom_etat_civil"] or etud["prenom"] + } {etud["nom"]}""" + return etud["nomprenom"] def format_lycee(nomlycee): @@ -237,7 +247,7 @@ _identiteEditor = ndb.EditableTable( "prenom": force_uppercase, "prenom_etat_civil": force_uppercase, "civilite": input_civilite, - "civilite_etat_civil": input_civilite, + "civilite_etat_civil": input_civilite_etat_civil, "date_naissance": ndb.DateDMYtoISO, "boursier": bool, }, @@ -300,7 +310,9 @@ def check_nom_prenom_homonyms( def _check_duplicate_code(cnx, args, code_name, disable_notify=False, edit=True): - """Vérifie que le code n'est pas dupliqué""" + """Vérifie que le code n'est pas dupliqué. + Raises ScoGenError si problème. + """ etudid = args.get("etudid", None) if args.get(code_name, None): etuds = identite_list(cnx, {code_name: str(args[code_name])}) @@ -355,11 +367,6 @@ def _check_duplicate_code(cnx, args, code_name, disable_notify=False, edit=True) raise ScoGenError(err_page) -def _check_civilite(args): - civilite = args.get("civilite", "X") or "X" - args["civilite"] = input_civilite(civilite) # TODO: A faire valider - - def identite_edit(cnx, args, disable_notify=False): """Modifie l'identite d'un étudiant. Si pref notification et difference, envoie message notification, sauf si disable_notify @@ -400,7 +407,6 @@ def identite_create(cnx, args): "check unique etudid, then create" _check_duplicate_code(cnx, args, "code_nip", edit=False) _check_duplicate_code(cnx, args, "code_ine", edit=False) - _check_civilite(args) if "etudid" in args: etudid = args["etudid"] @@ -915,12 +921,12 @@ def etud_inscriptions_infos(etudid: int, ne="") -> dict: from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre_inscriptions - etud = {} + infos = {} # Semestres dans lesquel il est inscrit ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( {"etudid": etudid} ) - etud["ins"] = ins + infos["ins"] = ins sems = [] cursem = None # semestre "courant" ou il est inscrit for i in ins: @@ -933,29 +939,31 @@ def etud_inscriptions_infos(etudid: int, ne="") -> dict: # trie les semestres par date de debut, le plus recent d'abord # (important, ne pas changer (suivi cohortes)) sems.sort(key=itemgetter("dateord"), reverse=True) - etud["sems"] = sems - etud["cursem"] = cursem + infos["sems"] = sems + infos["cursem"] = cursem if cursem: - etud["inscription"] = cursem["titremois"] - etud["inscriptionstr"] = "Inscrit en " + cursem["titremois"] - etud["inscription_formsemestre_id"] = cursem["formsemestre_id"] - etud["etatincursem"] = curi["etat"] - etud["situation"] = descr_situation_etud(etudid, ne) + infos["inscription"] = cursem["titremois"] + infos["inscriptionstr"] = "Inscrit en " + cursem["titremois"] + infos["inscription_formsemestre_id"] = cursem["formsemestre_id"] + infos["etatincursem"] = curi["etat"] + infos["situation"] = descr_situation_etud(etudid, ne) else: - if etud["sems"]: - if etud["sems"][0]["dateord"] > time.strftime("%Y-%m-%d", time.localtime()): - etud["inscription"] = "futur" - etud["situation"] = "futur élève" + if infos["sems"]: + if infos["sems"][0]["dateord"] > time.strftime( + "%Y-%m-%d", time.localtime() + ): + infos["inscription"] = "futur" + infos["situation"] = "futur élève" else: - etud["inscription"] = "ancien" - etud["situation"] = "ancien élève" + infos["inscription"] = "ancien" + infos["situation"] = "ancien élève" else: - etud["inscription"] = "non inscrit" - etud["situation"] = etud["inscription"] - etud["inscriptionstr"] = etud["inscription"] - etud["inscription_formsemestre_id"] = None - etud["etatincursem"] = "?" - return etud + infos["inscription"] = "non inscrit" + infos["situation"] = infos["inscription"] + infos["inscriptionstr"] = infos["inscription"] + infos["inscription_formsemestre_id"] = None + infos["etatincursem"] = "?" + return infos def descr_situation_etud(etudid: int, ne="") -> str: diff --git a/app/scodoc/sco_import_etuds.py b/app/scodoc/sco_import_etuds.py index fdd25af7..05e58ecc 100644 --- a/app/scodoc/sco_import_etuds.py +++ b/app/scodoc/sco_import_etuds.py @@ -38,18 +38,14 @@ from flask import g, url_for from app import db, log from app.models import Identite, GroupDescr, ScolarNews -from app.models.etudiants import input_civilite +from app.models.etudiants import input_civilite, input_civilite_etat_civil from app.scodoc.gen_tables import GenTable from app.scodoc.sco_excel import COLORS from app.scodoc.sco_exceptions import ( - AccessDenied, ScoFormatError, ScoException, ScoValueError, - ScoInvalidDateError, - ScoLockedFormError, - ScoGenError, ) from app.scodoc import html_sco_header from app.scodoc import sco_cache @@ -375,6 +371,15 @@ def scolars_import_excel_file( (doit etre 'M', 'F', ou 'MME', 'H', 'X' mais pas '{ val}') ligne {linenum}, colonne {titleslist[i]}""" ) from exc + if titleslist[i].lower() == "civilite_etat_civil": + try: + val = input_civilite_etat_civil(val) + except ScoValueError as exc: + raise ScoValueError( + f"""valeur invalide pour 'civilite' + (doit etre 'M', 'F', vide ou 'MME', 'H', 'X' mais pas '{ + val}') ligne {linenum}, colonne {titleslist[i]}""" + ) from exc # Excel date conversion: if titleslist[i].lower() == "date_naissance": diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index c567b8d3..bbf785a7 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -79,6 +79,12 @@ STATIC_DIR = ( # Attention: suppose que la timezone utilisée par postgresql soit la même ! TIME_ZONE = timezone("/".join(os.path.realpath("/etc/localtime").split("/")[-2:])) +# ----- CIVILITE ETUDIANTS +CIVILITES = {"M": "M.", "F": "Mme", "X": ""} +CIVILITES_ETAT_CIVIL = {"M": "M.", "F": "Mme"} +# Si l'état civil reconnait le genre neutre (X),: +# CIVILITES_ETAT_CIVIL = CIVILITES + # ----- CALCUL ET PRESENTATION DES NOTES NOTES_PRECISION = 1e-4 # evite eventuelles erreurs d'arrondis NOTES_MIN = 0.0 # valeur minimale admise pour une note (sauf malus, dans [-20, 20]) diff --git a/app/views/scolar.py b/app/views/scolar.py index c136252a..7ea49912 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -1542,7 +1542,6 @@ def _etudident_create_or_edit_form(edit): "math", { "size": 3, - "type": "float", "title": "Note de mathématiques", "explanation": "note sur 20 en terminale", }, @@ -1551,7 +1550,6 @@ def _etudident_create_or_edit_form(edit): "physique", { "size": 3, - "type": "float", "title": "Note de physique", "explanation": "note sur 20 en terminale", }, @@ -1560,7 +1558,6 @@ def _etudident_create_or_edit_form(edit): "anglais", { "size": 3, - "type": "float", "title": "Note d'anglais", "explanation": "note sur 20 en terminale", }, @@ -1569,7 +1566,6 @@ def _etudident_create_or_edit_form(edit): "francais", { "size": 3, - "type": "float", "title": "Note de français", "explanation": "note sur 20 obtenue au bac", }, diff --git a/migrations/versions/497ba81343f7_identite_admission.py b/migrations/versions/497ba81343f7_identite_admission.py index aa275ae1..c60ac3de 100644 --- a/migrations/versions/497ba81343f7_identite_admission.py +++ b/migrations/versions/497ba81343f7_identite_admission.py @@ -122,6 +122,24 @@ def upgrade(): with op.batch_alter_table("admissions", schema=None) as batch_op: batch_op.drop_constraint("admissions_etudid_fkey", type_="foreignkey") + batch_op.alter_column( + "physique", + existing_type=sa.DOUBLE_PRECISION(precision=53), + type_=sa.Text(), + existing_nullable=True, + ) + batch_op.alter_column( + "anglais", + existing_type=sa.DOUBLE_PRECISION(precision=53), + type_=sa.Text(), + existing_nullable=True, + ) + batch_op.alter_column( + "francais", + existing_type=sa.DOUBLE_PRECISION(precision=53), + type_=sa.Text(), + existing_nullable=True, + ) # laisse l'ancienne colonne pour downgrade (tests) # batch_op.drop_column('etudid') @@ -160,6 +178,24 @@ def downgrade(): batch_op.create_foreign_key( "admissions_etudid_fkey", "identite", ["etudid"], ["id"], ondelete="CASCADE" ) + batch_op.alter_column( + "francais", + existing_type=sa.Text(), + type_=sa.DOUBLE_PRECISION(precision=53), + existing_nullable=True, + ) + batch_op.alter_column( + "anglais", + existing_type=sa.Text(), + type_=sa.DOUBLE_PRECISION(precision=53), + existing_nullable=True, + ) + batch_op.alter_column( + "physique", + existing_type=sa.Text(), + type_=sa.DOUBLE_PRECISION(precision=53), + existing_nullable=True, + ) with op.batch_alter_table("adresse", schema=None) as batch_op: batch_op.drop_constraint("adresse_etudid_fkey", type_="foreignkey") diff --git a/tests/ressources/misc/ImportEtudiants.xlsx b/tests/ressources/misc/ImportEtudiants.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..4aed711631b82736545e294e253f87c8f2d48f6d GIT binary patch literal 9870 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 tuple[sco_fake_gen.ScoFake, int, list[int], list[int]]: """Crée une formation simple pour les tests. Création à partir de zéro (n'importe pas un fichier xml). Avec 3 UEs en S2 et une UE en S4. """ G = sco_fake_gen.ScoFake(verbose=False) + # If already exists, just use it + formation = Formation.query.filter_by( + dept_id=G.dept.id, + acronyme=acronyme, + titre=titre, + version=1, + ).first() + if formation is not None: + return ( + G, + formation.id, + [ue.id for ue in formation.ues], + [m.id for m in formation.modules], + ) formation_id = G.create_formation( - acronyme="F3", - titre="Formation 2", + acronyme=acronyme, + titre=titre, titre_officiel="Titre officiel 2", type_parcours=parcours.TYPE_CURSUS, ) diff --git a/tests/unit/test_etudiants.py b/tests/unit/test_etudiants.py new file mode 100644 index 00000000..8344c143 --- /dev/null +++ b/tests/unit/test_etudiants.py @@ -0,0 +1,187 @@ +# -*- coding: utf-8 -*- + +"""Test modèle étudiant (identite) + + +Utiliser comme: + pytest tests/unit/test_etudiants.py + +""" +import datetime +from pathlib import Path + +from flask import current_app + +from app import db +from app.models import Admission, Adresse, Departement, FormSemestre, Identite +from app.scodoc import sco_etud +from app.scodoc import sco_import_etuds + +from tests.unit import setup + + +def test_identite(test_client): + "Test de base du modèle identité: création, clonage, ..." + args = { + "civilite": "X", + "code_ine": "code_ine", + "code_nip": "code_nip", + "date_naissance": datetime.date(2000, 1, 2), + "dept_id": 1, + "dept_naissance": "dept_naissance", + "lieu_naissance": "lieu_naissance", + "nationalite": "nationalite", + "nom_usuel": "nom_usuel", + "nom": "nom", + "prenom_etat_civil": "prenom_etat_civil", + "prenom": "prenom", + } + e = Identite.create_etud(**args) + db.session.add(e) + db.session.flush() + admission_id = e.admission_id + admission = db.session.get(Admission, admission_id) + assert admission is not None + assert e.boursier is False + assert e.adresses.count() == 1 + adresses_ids = [a.id for a in e.adresses] + # --- Teste cascade admission: + db.session.delete(e) + db.session.flush() + assert db.session.get(Admission, admission_id) is None + assert db.session.get(Departement, 1) is not None + # --- Teste cascade adresses + for adresse_id in adresses_ids: + assert db.session.get(Adresse, adresse_id) is None + # --- Test cascade département + dept = Departement(acronym="test_identite") + db.session.add(dept) + db.session.flush() + args1 = args | {"dept_id": dept.id} + e = Identite.create_etud(**args1) + db.session.add(e) + db.session.flush() + etudid = e.id + db.session.delete(dept) + db.session.flush() + assert db.session.get(Identite, etudid) is None + + +def test_etat_civil(test_client): + "Test des attributs état civil" + dept = Departement.query.first() + args = {"nom": "nom", "prenom": "prénom", "civilite": "M", "dept_id": dept.id} + # Homme + e = Identite(**args) + db.session.add(e) + db.session.flush() + assert e.civilite_etat_civil_str == "M." + assert e.e == "" + # Femme + e = Identite(**args | {"civilite": "F"}) + db.session.add(e) + db.session.flush() + assert e.civilite_etat_civil_str == "Mme" + assert e.e == "e" + # Homme devenu femme + e = Identite(**(args | {"civilite_etat_civil": "F"})) + db.session.add(e) + db.session.flush() + assert e.civilite_etat_civil_str == "Mme" + assert e.civilite_str == "M." + assert e.e == "" + # Femme devenue neutre + e = Identite(**(args | {"civilite": "X", "civilite_etat_civil": "F"})) + db.session.add(e) + db.session.flush() + assert e.civilite_etat_civil_str == "Mme" + assert e.civilite_str == "" + assert e.e == "(e)" + assert e.prenom_etat_civil is None + # La version dict + e_d = e.to_dict_scodoc7() + assert e_d["civilite"] == "X" + assert e_d["civilite_etat_civil"] == "F" + assert e_d["ne"] == "(e)" + + +def test_etud_legacy(test_client): + "Test certaines fonctions scodoc7 (sco_etud)" + dept = Departement.query.first() + args = {"nom": "nom", "prenom": "prénom", "civilite": "M", "dept_id": dept.id} + # Prénom état civil + e = Identite(**(args)) + db.session.add(e) + db.session.flush() + e_dict = e.to_dict_bul() + sco_etud.format_etud_ident(e_dict) + assert e_dict["nom_disp"] == "NOM" + assert e_dict["prenom_etat_civil"] == "" + + +def test_import_etuds_xlsx(test_client): + "test import étudiant depuis xlsx" + G, formation_id, (ue1_id, ue2_id, ue3_id), module_ids = setup.build_formation_test( + acronyme="IMPXLSX" + ) + formsemestre_id = G.create_formsemestre( + formation_id=formation_id, + semestre_id=1, + date_debut="01/01/2021", + date_fin="30/06/2021", + ) + filename = ( + Path(current_app.config["SCODOC_DIR"]) + / "tests/ressources/misc/ImportEtudiants.xlsx" + ) + with open(filename, mode="rb") as f: + sco_import_etuds.scolars_import_excel_file( + f, formsemestre_id=formsemestre_id, exclude_cols=["photo_filename"] + ) + formsemestre = db.session.get(FormSemestre, formsemestre_id) + # Vérifie tous les champs du premier étudiant + etud = formsemestre.etuds.first() + assert etud.code_nip == "nip1" + assert etud.code_ine == "ine1" + assert etud.nom == "NOM1" + assert etud.nom_usuel == "nom_usuel1" + assert etud.prenom == "PRÉNOM1" + assert etud.civilite == "M" + assert etud.prenom_etat_civil == "PRÉNOM_CIVIL1" + assert etud.civilite_etat_civil == "M" + assert etud.date_naissance == datetime.date(2001, 5, 1) + assert etud.lieu_naissance == "Paris" + assert etud.nationalite == "Belge" + assert etud.boursier is True + # Admission + adm = etud.admission + assert adm.bac == "C" + assert adm.specialite == "SPÉ" + assert adm.annee_bac == 2023 + assert adm.math == "11.0" # deprecated field + assert adm.physique == "12.0" # deprecated field + assert adm.anglais == "13.0" # deprecated field + assert adm.francais == "14.0" # deprecated field + assert adm.boursier_prec is False + assert adm.qualite == 10 + assert adm.rapporteur == "xx" + assert adm.score == 5 + assert adm.classement == 111 + assert adm.nomlycee == "nomlycée" + assert adm.codepostallycee == "75005" + # Adresse + adresse: Adresse = etud.adresses.first() + assert adresse.email == "etud1@etud.no" + assert adresse.emailperso == "etud1@perso.no" + assert adresse.domicile == "1 rue A" + assert adresse.codepostaldomicile == "12345" + assert adresse.villedomicile == "Lima" + assert adresse.paysdomicile == "Pérou" + assert adresse.telephone == "102030405" + assert adresse.telephonemobile == "605040302" + # + + +# mapp.set_sco_dept("TEST_") +# ctx.push() +# login_user(User.query.filter_by(user_name="admin").first()) diff --git a/tools/format_import_etudiants.txt b/tools/format_import_etudiants.txt index 7d4481b8..ae7ebe3b 100644 --- a/tools/format_import_etudiants.txt +++ b/tools/format_import_etudiants.txt @@ -10,7 +10,7 @@ nom_usuel; text; identite; 1; nom usuel (si different); prenom; text; identite; 0; prénom de l'etudiant civilite; text; identite; 0; sexe ('M', 'F', 'X');sexe;genre prenom_etat_civil; text; identite; 1; prénom à l'état-civil (si différent);prenom_etat_civil -civilite_etat_civil; text; identite; 1; sexe ('M', 'F', 'X') à l'état civil;civilite_etat_civil +civilite_etat_civil; text; identite; 1; sexe ('M', 'F', 'X', '') à l'état civil;civilite_etat_civil date_naissance;text;identite; 1; date de naissance (jj/mm/aaaa) lieu_naissance;text;identite; 1; lieu de naissance nationalite; text; identite; 1; nationalite @@ -52,7 +52,4 @@ villedomicile; text; adresse; 1; ville domicile paysdomicile; text; adresse; 1; pays domicile telephone; text; adresse; 1; num. telephone (fixe) telephonemobile; text; adresse; 1; num. telephone (mobile) -# -# Pas tout à fait admission: -debouche;text; admissions;1;(OBSOLETE, ne plus utiliser) situation APRES être passé par chez nous; - +# \ No newline at end of file