Import admission: mode avec etudid

This commit is contained in:
Emmanuel Viennet 2024-06-18 20:40:13 +02:00
parent 8a49d99292
commit af557f9c93
3 changed files with 137 additions and 79 deletions

View File

@ -28,7 +28,6 @@
""" Importation des étudiants à partir de fichiers CSV """ Importation des étudiants à partir de fichiers CSV
""" """
import collections
import io import io
import os import os
import re import re
@ -64,6 +63,7 @@ import app.scodoc.sco_utils as scu
FORMAT_FILE = "format_import_etudiants.txt" FORMAT_FILE = "format_import_etudiants.txt"
# Champs modifiables via "Import données admission" # Champs modifiables via "Import données admission"
# (nom/prénom modifiables en mode "avec etudid")
ADMISSION_MODIFIABLE_FIELDS = ( ADMISSION_MODIFIABLE_FIELDS = (
"code_nip", "code_nip",
"code_ine", "code_ine",
@ -132,19 +132,27 @@ def sco_import_format(with_codesemestre=True):
return r 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' }""" """Attribut: { 'type': , 'table', 'allow_nulls' , 'description' }"""
fmt = sco_import_format(with_codesemestre=with_codesemestre) fmt = sco_import_format(with_codesemestre=with_codesemestre)
R = collections.OrderedDict() formats = {}
for l in fmt: for l in fmt:
R[l[0]] = { formats[l[0]] = {
"type": l[1], "type": l[1],
"table": l[2], "table": l[2],
"allow_nulls": l[3], "allow_nulls": l[3],
"description": l[4], "description": l[4],
"aliases": l[5], "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( def sco_import_generate_excel_sample(
@ -188,8 +196,7 @@ def sco_import_generate_excel_sample(
groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids) groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids)
members = groups_infos.members members = groups_infos.members
log( log(
"sco_import_generate_excel_sample: group_ids=%s %d members" f"sco_import_generate_excel_sample: group_ids={group_ids}, {len(members)} members"
% (group_ids, len(members))
) )
titles = ["etudid"] + titles titles = ["etudid"] + titles
titles_styles = [style] + titles_styles titles_styles = [style] + titles_styles
@ -234,21 +241,26 @@ def students_import_excel(
exclude_cols=["photo_filename"], exclude_cols=["photo_filename"],
) )
if return_html: if return_html:
if formsemestre_id: dest_url = (
dest = url_for( url_for(
"notes.formsemestre_status", "notes.formsemestre_status",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
) )
else: if formsemestre_id
dest = url_for("notes.index_html", scodoc_dept=g.scodoc_dept) else url_for("notes.index_html", scodoc_dept=g.scodoc_dept)
)
H = [html_sco_header.sco_header(page_title="Import etudiants")] H = [html_sco_header.sco_header(page_title="Import etudiants")]
H.append("<ul>") H.append("<ul>")
for d in diag: for d in diag:
H.append("<li>%s</li>" % d) H.append(f"<li>{d}</li>")
H.append("</ul>") H.append(
H.append("<p>Import terminé !</p>") f"""
H.append('<p><a class="stdlink" href="%s">Continuer</a></p>' % dest) </ul>)
<p>Import terminé !</p>
<p><a class="stdlink" href="{dest_url}">Continuer</a></p>
"""
)
return "\n".join(H) + html_sco_header.sco_footer() return "\n".join(H) + html_sco_header.sco_footer()
@ -308,13 +320,13 @@ def scolars_import_excel_file(
titleslist = [] titleslist = []
for t in fs: for t in fs:
if t not in titles: if t not in titles:
raise ScoValueError('Colonne invalide: "%s"' % t) raise ScoValueError(f'Colonne invalide: "{t}"')
titleslist.append(t) # titleslist.append(t) #
# ok, same titles # ok, same titles
# Start inserting data, abort whole transaction in case of error # Start inserting data, abort whole transaction in case of error
created_etudids = [] created_etudids = []
np_imported_homonyms = 0 np_imported_homonyms = 0
GroupIdInferers = {} group_id_inferer = {}
try: # --- begin DB transaction try: # --- begin DB transaction
linenum = 0 linenum = 0
for line in data[1:]: for line in data[1:]:
@ -429,7 +441,7 @@ def scolars_import_excel_file(
_import_one_student( _import_one_student(
formsemestre_id, formsemestre_id,
values, values,
GroupIdInferers, group_id_inferer,
annee_courante, annee_courante,
created_etudids, created_etudids,
linenum, linenum,
@ -496,13 +508,14 @@ def scolars_import_excel_file(
def students_import_admission( 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)" "import donnees admission from Excel file (v2016)"
diag = scolars_import_admission( diag = scolars_import_admission(
csvfile, csvfile,
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
type_admission=type_admission, type_admission=type_admission,
use_etudid=use_etudid,
) )
if return_html: if return_html:
H = [html_sco_header.sco_header(page_title="Import données admissions")] 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 "\n".join(H) + html_sco_header.sco_footer()
return ""
def _import_one_student( 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) # ------ 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 """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 Cherche dans ce fichier les étudiants qui correspondent à des inscrits du
semestre formsemestre_id. 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 via les noms/prénoms qui doivent être égaux (la casse, les accents et caractères spéciaux
étant ignorés). é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. 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é. 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}") log(f"scolars_import_admission: formsemestre_id={formsemestre_id}")
diag: list[str] = []
members = sco_groups.get_group_members( members = sco_groups.get_group_members(
sco_groups.get_default_group(formsemestre_id) sco_groups.get_default_group(formsemestre_id)
) )
etuds_by_nomprenom = {} # { nomprenom : etud } etuds_by_nomprenom = {} # { nomprenom : etud }
diag = [] etuds_by_etudid = {} # { etudid : etud }
for m in members: if use_etudid:
np = (adm_normalize_string(m["nom"]), adm_normalize_string(m["prenom"])) etuds_by_etudid = {m["etudid"]: m for m in members}
if np in etuds_by_nomprenom: else:
msg = "Attention: hononymie pour %s %s" % (m["nom"], m["prenom"]) for m in members:
log(msg) np = (adm_normalize_string(m["nom"]), adm_normalize_string(m["prenom"]))
diag.append(msg) if np in etuds_by_nomprenom:
etuds_by_nomprenom[np] = m msg = f"""Attention: hononymie pour {m["nom"]} {m["prenom"]}"""
log(msg)
diag.append(msg)
etuds_by_nomprenom[np] = m
exceldata = datafile.read() exceldata = datafile.read()
diag2, data = sco_excel.excel_bytes_to_list(exceldata) 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] titles = data[0]
# idx -> ('field', convertor) # idx -> ('field', convertor)
fields = adm_get_fields(titles, formsemestre_id) fields = adm_get_fields(titles, formsemestre_id, use_etudid=use_etudid)
idx_nom = None idx_nom = idx_prenom = idx_etudid = None
idx_prenom = None
for idx, field in fields.items(): for idx, field in fields.items():
if field[0] == "nom": match field[0]:
idx_nom = idx case "nom":
if field[0] == "prenom": idx_nom = idx
idx_prenom = idx case "prenom":
if (idx_nom is None) or (idx_prenom is None): 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 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( 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( dest_url=url_for(
"scolar.form_students_import_infos_admissions", "scolar.form_students_import_infos_admissions",
scodoc_dept=g.scodoc_dept, 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) 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 nline = 2 # la premiere ligne de donnees du fichier excel est 2
n_import = 0 n_import = 0
for line in data[1:]: for line in data[1:]:
# Retrouve l'étudiant parmi ceux du semestre par (nom, prenom) if use_etudid:
nom = adm_normalize_string(line[idx_nom]) try:
prenom = adm_normalize_string(line[idx_prenom]) etud = etuds_by_etudid.get(int(line[idx_etudid]))
if (nom, prenom) not in etuds_by_nomprenom: except ValueError:
msg = f"""Étudiant <b>{line[idx_nom]} {line[idx_prenom]} inexistant</b>""" etud = None
diag.append(msg) if not etud:
msg = f"""Étudiant avec code etudid=<b>{line[idx_etudid]}</b> inexistant"""
diag.append(msg)
else: 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 <b>{line[idx_nom]} {line[idx_prenom]}</b> inexistant"""
)
diag.append(msg)
if etud:
cur_adm = sco_etud.admission_list(cnx, args={"id": etud["admission_id"]})[0] cur_adm = sco_etud.admission_list(cnx, args={"id": etud["admission_id"]})[0]
# peuple les champs presents dans le tableau # peuple les champs presents dans le tableau
args = {} 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 """Cherche les colonnes importables dans les titres (ligne 1) du fichier excel
return: { idx : (field_name, convertor) } return: { idx : (field_name, convertor) }
""" """
format_dict = sco_import_format_dict() format_dict = sco_import_format_dict(use_etudid=use_etudid)
fields = {} fields = {}
idx = 0 idx = 0
for title in titles: for title in titles:
title_n = adm_normalize_string(title) title_n = adm_normalize_string(title)
for k in format_dict: for k, fmt in format_dict.items():
for v in format_dict[k]["aliases"]: for v in fmt["aliases"]:
if adm_normalize_string(v) == title_n: if adm_normalize_string(v) == title_n:
typ = format_dict[k]["type"] typ = fmt["type"]
if typ == "real": if typ == "real":
convertor = adm_convert_real convertor = adm_convert_real
elif typ == "integer" or typ == "int": elif typ == "integer" or typ == "int":

View File

@ -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. Les données sont affichées sur les fiches individuelles des étudiants.
</p> </p>
</div> </div>
<p> <div class="help">
Importer ici la feuille excel utilisée pour envoyer le classement Parcoursup. <p>
Seuls les étudiants actuellement inscrits dans ce semestre ScoDoc seront affectés, Vous pouvez importer ici la feuille excel utilisée pour envoyer
les autres lignes de la feuille seront ignorées. le classement Parcoursup.
Et seules les colonnes intéressant ScoDoc Seuls les étudiants actuellement inscrits dans ce semestre ScoDoc seront affectés,
seront importées: il est inutile d'éliminer les autres. les autres lignes de la feuille seront ignorées.
<br> Et seules les colonnes intéressant ScoDoc
<em>Seules les données "admission" seront modifiées seront importées: il est inutile d'éliminer les autres.
(et pas l'identité de l'étudiant).</em> </p>
<br> <p>
<em>Les colonnes "nom" et "prenom" sont requises, ou bien une colonne "etudid".</em> <em>Seules les données "admission" seront modifiées
</p> (et pas l'identité de l'étudiant).</em>
<p> </p>
Avant d'importer vos données, il est recommandé d'enregistrer <p>
les informations actuelles: <em>Les colonnes "nom" et "prenom" sont requises,
<a class="stdlink" href="{ ou bien une colonne "etudid" si la case
url_for("scolar.import_generate_admission_sample", "Utiliser l'identifiant d'étudiant ScoDoc" est cochée.
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id) </em>
}">exporter les données actuelles de ScoDoc</a> </p>
(ce fichier peut être -importé après d'éventuelles modifications) <p>
</p> Avant d'importer vos données, il est recommandé d'enregistrer
""", les informations actuelles:
<a class="stdlink" href="{
url_for("scolar.import_generate_admission_sample",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
}">exporter les données actuelles de ScoDoc</a>
(ce fichier peut être -importé après d'éventuelles modifications)
</p>
</div>
""",
] ]
tf = TrivialFormulator( tf = TrivialFormulator(
@ -2397,6 +2405,15 @@ def form_students_import_infos_admissions(formsemestre_id=None):
"csvfile", "csvfile",
{"title": "Fichier Excel:", "input_type": "file", "size": 40}, {"title": "Fichier Excel:", "input_type": "file", "size": 40},
), ),
(
"use_etudid",
{
"input_type": "boolcheckbox",
"title": "Utiliser l'identifiant d'étudiant ScoDoc (<tt>etudid</tt>)",
"explanation": """si cochée, utilise le code pour retrouver dans ScoDoc
les étudiants du fichier excel. Sinon, utilise les noms/prénoms.""",
},
),
( (
"type_admission", "type_admission",
{ {
@ -2436,6 +2453,7 @@ def form_students_import_infos_admissions(formsemestre_id=None):
tf[2]["csvfile"], tf[2]["csvfile"],
type_admission=tf[2]["type_admission"], type_admission=tf[2]["type_admission"],
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
use_etudid=tf[2]["use_etudid"],
) )

View File

@ -1,7 +1,7 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.6.976" SCOVERSION = "9.6.977"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"