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,20 +633,21 @@ 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 }
if use_etudid:
etuds_by_etudid = {m["etudid"]: m for m in members}
else:
for m in members: for m in members:
np = (adm_normalize_string(m["nom"]), adm_normalize_string(m["prenom"])) np = (adm_normalize_string(m["nom"]), adm_normalize_string(m["prenom"]))
if np in etuds_by_nomprenom: if np in etuds_by_nomprenom:
msg = "Attention: hononymie pour %s %s" % (m["nom"], m["prenom"]) msg = f"""Attention: hononymie pour {m["nom"]} {m["prenom"]}"""
log(msg) log(msg)
diag.append(msg) diag.append(msg)
etuds_by_nomprenom[np] = m etuds_by_nomprenom[np] = m
@ -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]:
case "nom":
idx_nom = idx idx_nom = idx
if field[0] == "prenom": case "prenom":
idx_prenom = idx idx_prenom = idx
if (idx_nom is None) or (idx_prenom is None): 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:]:
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:
# Retrouve l'étudiant parmi ceux du semestre par (nom, prenom) # Retrouve l'étudiant parmi ceux du semestre par (nom, prenom)
nom = adm_normalize_string(line[idx_nom]) nom = adm_normalize_string(line[idx_nom])
prenom = adm_normalize_string(line[idx_prenom]) prenom = adm_normalize_string(line[idx_prenom])
if (nom, prenom) not in etuds_by_nomprenom: etud = etuds_by_nomprenom.get((nom, prenom))
msg = f"""Étudiant <b>{line[idx_nom]} {line[idx_prenom]} inexistant</b>""" if not etud:
msg = (
f"""Étudiant <b>{line[idx_nom]} {line[idx_prenom]}</b> inexistant"""
)
diag.append(msg) diag.append(msg)
else: if etud:
etud = etuds_by_nomprenom[(nom, prenom)]
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,17 +2365,24 @@ 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>
<div class="help">
<p> <p>
Importer ici la feuille excel utilisée pour envoyer le classement Parcoursup. 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, Seuls les étudiants actuellement inscrits dans ce semestre ScoDoc seront affectés,
les autres lignes de la feuille seront ignorées. les autres lignes de la feuille seront ignorées.
Et seules les colonnes intéressant ScoDoc Et seules les colonnes intéressant ScoDoc
seront importées: il est inutile d'éliminer les autres. seront importées: il est inutile d'éliminer les autres.
<br> </p>
<p>
<em>Seules les données "admission" seront modifiées <em>Seules les données "admission" seront modifiées
(et pas l'identité de l'étudiant).</em> (et pas l'identité de l'étudiant).</em>
<br> </p>
<em>Les colonnes "nom" et "prenom" sont requises, ou bien une colonne "etudid".</em> <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>
<p> <p>
Avant d'importer vos données, il est recommandé d'enregistrer Avant d'importer vos données, il est recommandé d'enregistrer
@ -2386,6 +2393,7 @@ def form_students_import_infos_admissions(formsemestre_id=None):
}">exporter les données actuelles de ScoDoc</a> }">exporter les données actuelles de ScoDoc</a>
(ce fichier peut être -importé après d'éventuelles modifications) (ce fichier peut être -importé après d'éventuelles modifications)
</p> </p>
</div>
""", """,
] ]
@ -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"