From af557f9c934aedc005deb2ac22e934d411e636bb Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 18 Jun 2024 20:40:13 +0200 Subject: [PATCH] Import admission: mode avec etudid --- app/scodoc/sco_import_etuds.py | 152 +++++++++++++++++++++------------ app/views/scolar.py | 62 +++++++++----- sco_version.py | 2 +- 3 files changed, 137 insertions(+), 79 deletions(-) diff --git a/app/scodoc/sco_import_etuds.py b/app/scodoc/sco_import_etuds.py index 0a569047b..74362f922 100644 --- a/app/scodoc/sco_import_etuds.py +++ b/app/scodoc/sco_import_etuds.py @@ -28,7 +28,6 @@ """ Importation des étudiants à partir de fichiers CSV """ -import collections import io import os import re @@ -64,6 +63,7 @@ import app.scodoc.sco_utils as scu FORMAT_FILE = "format_import_etudiants.txt" # Champs modifiables via "Import données admission" +# (nom/prénom modifiables en mode "avec etudid") ADMISSION_MODIFIABLE_FIELDS = ( "code_nip", "code_ine", @@ -132,19 +132,27 @@ def sco_import_format(with_codesemestre=True): return r -def sco_import_format_dict(with_codesemestre=True): +def sco_import_format_dict(with_codesemestre=True, use_etudid: bool = False): """Attribut: { 'type': , 'table', 'allow_nulls' , 'description' }""" fmt = sco_import_format(with_codesemestre=with_codesemestre) - R = collections.OrderedDict() + formats = {} for l in fmt: - R[l[0]] = { + formats[l[0]] = { "type": l[1], "table": l[2], "allow_nulls": l[3], "description": l[4], "aliases": l[5], } - return R + if use_etudid: + formats["etudid"] = { + "type": "int", + "table": "identite", + "allow_nulls": False, + "description": "", + "aliases": ["etudid", "id"], + } + return formats def sco_import_generate_excel_sample( @@ -188,8 +196,7 @@ def sco_import_generate_excel_sample( groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids) members = groups_infos.members log( - "sco_import_generate_excel_sample: group_ids=%s %d members" - % (group_ids, len(members)) + f"sco_import_generate_excel_sample: group_ids={group_ids}, {len(members)} members" ) titles = ["etudid"] + titles titles_styles = [style] + titles_styles @@ -234,21 +241,26 @@ def students_import_excel( exclude_cols=["photo_filename"], ) if return_html: - if formsemestre_id: - dest = url_for( + dest_url = ( + url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, ) - else: - dest = url_for("notes.index_html", scodoc_dept=g.scodoc_dept) + if formsemestre_id + else url_for("notes.index_html", scodoc_dept=g.scodoc_dept) + ) H = [html_sco_header.sco_header(page_title="Import etudiants")] H.append("") - H.append("

Import terminé !

") - H.append('

Continuer

' % dest) + H.append(f"
  • {d}
  • ") + H.append( + f""" + ) +

    Import terminé !

    +

    Continuer

    + """ + ) return "\n".join(H) + html_sco_header.sco_footer() @@ -308,13 +320,13 @@ def scolars_import_excel_file( titleslist = [] for t in fs: if t not in titles: - raise ScoValueError('Colonne invalide: "%s"' % t) + raise ScoValueError(f'Colonne invalide: "{t}"') titleslist.append(t) # # ok, same titles # Start inserting data, abort whole transaction in case of error created_etudids = [] np_imported_homonyms = 0 - GroupIdInferers = {} + group_id_inferer = {} try: # --- begin DB transaction linenum = 0 for line in data[1:]: @@ -429,7 +441,7 @@ def scolars_import_excel_file( _import_one_student( formsemestre_id, values, - GroupIdInferers, + group_id_inferer, annee_courante, created_etudids, linenum, @@ -496,13 +508,14 @@ def scolars_import_excel_file( def students_import_admission( - csvfile, type_admission="", formsemestre_id=None, return_html=True -): + csvfile, type_admission="", formsemestre_id=None, return_html=True, use_etudid=False +) -> str: "import donnees admission from Excel file (v2016)" diag = scolars_import_admission( csvfile, formsemestre_id=formsemestre_id, type_admission=type_admission, + use_etudid=use_etudid, ) if return_html: H = [html_sco_header.sco_header(page_title="Import données admissions")] @@ -524,6 +537,7 @@ def students_import_admission( ) return "\n".join(H) + html_sco_header.sco_footer() + return "" def _import_one_student( @@ -599,13 +613,15 @@ def _is_new_ine(cnx, code_ine): # ------ Fonction ré-écrite en nov 2016 pour lire des fichiers sans etudid (fichiers APB) -def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None): +def scolars_import_admission( + datafile, formsemestre_id=None, type_admission=None, use_etudid=False +): """Importe données admission depuis un fichier Excel quelconque - par exemple ceux utilisés avec APB + par exemple ceux utilisés avec APB, avec ou sans etudid Cherche dans ce fichier les étudiants qui correspondent à des inscrits du semestre formsemestre_id. - Le fichier n'a pas l'INE ni le NIP ni l'etudid, la correspondance se fait + Si le fichier n'a pas d'etudid (use_etudid faux), la correspondance se fait via les noms/prénoms qui doivent être égaux (la casse, les accents et caractères spéciaux étant ignorés). @@ -617,23 +633,24 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None dans le fichier importé) du champ type_admission. Si une valeur existe ou est présente dans le fichier importé, ce paramètre est ignoré. - TODO: - - choix onglet du classeur """ - log(f"scolars_import_admission: formsemestre_id={formsemestre_id}") + diag: list[str] = [] members = sco_groups.get_group_members( sco_groups.get_default_group(formsemestre_id) ) etuds_by_nomprenom = {} # { nomprenom : etud } - diag = [] - for m in members: - np = (adm_normalize_string(m["nom"]), adm_normalize_string(m["prenom"])) - if np in etuds_by_nomprenom: - msg = "Attention: hononymie pour %s %s" % (m["nom"], m["prenom"]) - log(msg) - diag.append(msg) - etuds_by_nomprenom[np] = m + etuds_by_etudid = {} # { etudid : etud } + if use_etudid: + etuds_by_etudid = {m["etudid"]: m for m in members} + else: + for m in members: + np = (adm_normalize_string(m["nom"]), adm_normalize_string(m["prenom"])) + if np in etuds_by_nomprenom: + msg = f"""Attention: hononymie pour {m["nom"]} {m["prenom"]}""" + log(msg) + diag.append(msg) + etuds_by_nomprenom[np] = m exceldata = datafile.read() diag2, data = sco_excel.excel_bytes_to_list(exceldata) @@ -644,19 +661,29 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None titles = data[0] # idx -> ('field', convertor) - fields = adm_get_fields(titles, formsemestre_id) - idx_nom = None - idx_prenom = None + fields = adm_get_fields(titles, formsemestre_id, use_etudid=use_etudid) + idx_nom = idx_prenom = idx_etudid = None for idx, field in fields.items(): - if field[0] == "nom": - idx_nom = idx - if field[0] == "prenom": - idx_prenom = idx - if (idx_nom is None) or (idx_prenom is None): + match field[0]: + case "nom": + idx_nom = idx + case "prenom": + idx_prenom = idx + case "etudid": + idx_etudid = idx + + if (not use_etudid and ((idx_nom is None) or (idx_prenom is None))) or ( + use_etudid and idx_etudid is None + ): log("fields indices=" + ", ".join([str(x) for x in fields])) - log("fields titles =" + ", ".join([fields[x][0] for x in fields])) + log("fields titles =" + ", ".join([x[0] for x in fields.values()])) raise ScoFormatError( - "scolars_import_admission: colonnes nom et prenom requises", + ( + """colonne etudid requise + (si l'option "Utiliser l'identifiant d'étudiant ScoDoc" est cochée)""" + if use_etudid + else "colonnes nom et prenom requises" + ), dest_url=url_for( "scolar.form_students_import_infos_admissions", scodoc_dept=g.scodoc_dept, @@ -665,18 +692,31 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None ) modifiable_fields = set(ADMISSION_MODIFIABLE_FIELDS) + if use_etudid: + modifiable_fields |= {"nom", "prenom"} nline = 2 # la premiere ligne de donnees du fichier excel est 2 n_import = 0 for line in data[1:]: - # Retrouve l'étudiant parmi ceux du semestre par (nom, prenom) - nom = adm_normalize_string(line[idx_nom]) - prenom = adm_normalize_string(line[idx_prenom]) - if (nom, prenom) not in etuds_by_nomprenom: - msg = f"""Étudiant {line[idx_nom]} {line[idx_prenom]} inexistant""" - diag.append(msg) + if use_etudid: + try: + etud = etuds_by_etudid.get(int(line[idx_etudid])) + except ValueError: + etud = None + if not etud: + msg = f"""Étudiant avec code etudid={line[idx_etudid]} inexistant""" + diag.append(msg) else: - etud = etuds_by_nomprenom[(nom, prenom)] + # Retrouve l'étudiant parmi ceux du semestre par (nom, prenom) + nom = adm_normalize_string(line[idx_nom]) + prenom = adm_normalize_string(line[idx_prenom]) + etud = etuds_by_nomprenom.get((nom, prenom)) + if not etud: + msg = ( + f"""Étudiant {line[idx_nom]} {line[idx_prenom]} inexistant""" + ) + diag.append(msg) + if etud: cur_adm = sco_etud.admission_list(cnx, args={"id": etud["admission_id"]})[0] # peuple les champs presents dans le tableau args = {} @@ -758,19 +798,19 @@ def adm_normalize_string(s): ) -def adm_get_fields(titles, formsemestre_id): +def adm_get_fields(titles, formsemestre_id: int, use_etudid: bool = False): """Cherche les colonnes importables dans les titres (ligne 1) du fichier excel return: { idx : (field_name, convertor) } """ - format_dict = sco_import_format_dict() + format_dict = sco_import_format_dict(use_etudid=use_etudid) fields = {} idx = 0 for title in titles: title_n = adm_normalize_string(title) - for k in format_dict: - for v in format_dict[k]["aliases"]: + for k, fmt in format_dict.items(): + for v in fmt["aliases"]: if adm_normalize_string(v) == title_n: - typ = format_dict[k]["type"] + typ = fmt["type"] if typ == "real": convertor = adm_convert_real elif typ == "integer" or typ == "int": diff --git a/app/views/scolar.py b/app/views/scolar.py index 0cb128aed..34e7384e1 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -2365,28 +2365,36 @@ def form_students_import_infos_admissions(formsemestre_id=None): Les données sont affichées sur les fiches individuelles des étudiants.

    -

    - Importer ici la feuille excel utilisée pour envoyer le classement Parcoursup. - Seuls les étudiants actuellement inscrits dans ce semestre ScoDoc seront affectés, - les autres lignes de la feuille seront ignorées. - Et seules les colonnes intéressant ScoDoc - seront importées: il est inutile d'éliminer les autres. -
    - Seules les données "admission" seront modifiées - (et pas l'identité de l'étudiant). -
    - Les colonnes "nom" et "prenom" sont requises, ou bien une colonne "etudid". -

    -

    - Avant d'importer vos données, il est recommandé d'enregistrer - les informations actuelles: - exporter les données actuelles de ScoDoc - (ce fichier peut être ré-importé après d'éventuelles modifications) -

    - """, +
    +

    + Vous pouvez importer ici la feuille excel utilisée pour envoyer + le classement Parcoursup. + Seuls les étudiants actuellement inscrits dans ce semestre ScoDoc seront affectés, + les autres lignes de la feuille seront ignorées. + Et seules les colonnes intéressant ScoDoc + seront importées: il est inutile d'éliminer les autres. +

    +

    + Seules les données "admission" seront modifiées + (et pas l'identité de l'étudiant). +

    +

    + Les colonnes "nom" et "prenom" sont requises, + ou bien une colonne "etudid" si la case + "Utiliser l'identifiant d'étudiant ScoDoc" est cochée. + +

    +

    + Avant d'importer vos données, il est recommandé d'enregistrer + les informations actuelles: + exporter les données actuelles de ScoDoc + (ce fichier peut être ré-importé après d'éventuelles modifications) +

    +
    + """, ] tf = TrivialFormulator( @@ -2397,6 +2405,15 @@ def form_students_import_infos_admissions(formsemestre_id=None): "csvfile", {"title": "Fichier Excel:", "input_type": "file", "size": 40}, ), + ( + "use_etudid", + { + "input_type": "boolcheckbox", + "title": "Utiliser l'identifiant d'étudiant ScoDoc (etudid)", + "explanation": """si cochée, utilise le code pour retrouver dans ScoDoc + les étudiants du fichier excel. Sinon, utilise les noms/prénoms.""", + }, + ), ( "type_admission", { @@ -2436,6 +2453,7 @@ def form_students_import_infos_admissions(formsemestre_id=None): tf[2]["csvfile"], type_admission=tf[2]["type_admission"], formsemestre_id=formsemestre_id, + use_etudid=tf[2]["use_etudid"], ) diff --git a/sco_version.py b/sco_version.py index 2d37b22ba..694261910 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.976" +SCOVERSION = "9.6.977" SCONAME = "ScoDoc"