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
"""
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("<ul>")
for d in diag:
H.append("<li>%s</li>" % d)
H.append("</ul>")
H.append("<p>Import terminé !</p>")
H.append('<p><a class="stdlink" href="%s">Continuer</a></p>' % dest)
H.append(f"<li>{d}</li>")
H.append(
f"""
</ul>)
<p>Import terminé !</p>
<p><a class="stdlink" href="{dest_url}">Continuer</a></p>
"""
)
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 <b>{line[idx_nom]} {line[idx_prenom]} inexistant</b>"""
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=<b>{line[idx_etudid]}</b> 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 <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]
# 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":

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.
</p>
</div>
<p>
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.
<br>
<em>Seules les données "admission" seront modifiées
(et pas l'identité de l'étudiant).</em>
<br>
<em>Les colonnes "nom" et "prenom" sont requises, ou bien une colonne "etudid".</em>
</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 class="help">
<p>
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.
</p>
<p>
<em>Seules les données "admission" seront modifiées
(et pas l'identité de l'étudiant).</em>
</p>
<p>
<em>Les colonnes "nom" et "prenom" sont requises,
ou bien une colonne "etudid" si la case
"Utiliser l'identifiant d'étudiant ScoDoc" est cochée.
</em>
</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(
@ -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 (<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",
{
@ -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"],
)

View File

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