Fix: traitement erreur si imports étudiants dupliqués. + présentation page d'import

This commit is contained in:
Emmanuel Viennet 2024-09-04 04:09:31 +02:00
parent ad754ebd24
commit 35692da422
4 changed files with 181 additions and 117 deletions

View File

@ -237,6 +237,8 @@ class Identite(models.ScoDocModel):
Les clés adresses et admission ne SONT PAS utilisées. Les clés adresses et admission ne SONT PAS utilisées.
(added to session but not flushed nor commited) (added to session but not flushed nor commited)
""" """
check_etud_duplicate_code(args, "code_nip", dest_url=None)
check_etud_duplicate_code(args, "code_ine", dest_url=None)
if not "dept_id" in args: if not "dept_id" in args:
if "dept" in args: if "dept" in args:
departement = Departement.query.filter_by(acronym=args["dept"]).first() departement = Departement.query.filter_by(acronym=args["dept"]).first()
@ -248,8 +250,19 @@ class Identite(models.ScoDocModel):
if args.get("admission_id", None) is None: if args.get("admission_id", None) is None:
etud.admission = Admission() etud.admission = Admission()
etud.adresses.append(Adresse(typeadresse="domicile")) etud.adresses.append(Adresse(typeadresse="domicile"))
try:
db.session.flush() db.session.flush()
except sqlalchemy.exc.IntegrityError as e:
db.session.rollback()
if "unique_dept_nip_except_null" in str(e):
raise ScoValueError(
"Code NIP déjà utilisé pour un autre étudiant"
) from e
if "unique_dept_ine_except_null" in str(e):
raise ScoValueError(
"Code INE déjà utilisé pour un autre étudiant"
) from e
raise
event = ScolarEvent(etud=etud, event_type="CREATION") event = ScolarEvent(etud=etud, event_type="CREATION")
db.session.add(event) db.session.add(event)
log(f"Identite.create {etud}") log(f"Identite.create {etud}")
@ -796,9 +809,12 @@ class Identite(models.ScoDocModel):
) )
def check_etud_duplicate_code(args, code_name, edit=True, etudid: int | None = None): def check_etud_duplicate_code(
args, code_name, edit=True, etudid: int | None = None, dest_url: str | None = ""
):
"""Vérifie que le code n'est pas dupliqué. """Vérifie que le code n'est pas dupliqué.
Raises ScoGenError si problème. Raises ScoGenError si problème.
Si dest_url === None, pas de lien continuer/annuler.
""" """
etudid = etudid or args.get("etudid", None) etudid = etudid or args.get("etudid", None)
if not args.get(code_name, None): if not args.get(code_name, None):
@ -837,11 +853,17 @@ def check_etud_duplicate_code(args, code_name, edit=True, etudid: int | None = N
<ul><li> <ul><li>
{ '</li><li>'.join(listh) } { '</li><li>'.join(listh) }
</li></ul> </li></ul>
"""
err_page += (
f"""
<p> <p>
<a href="{ url_for(dest_endpoint, scodoc_dept=g.scodoc_dept, **parameters) } <a href="{ dest_url or url_for(dest_endpoint, scodoc_dept=g.scodoc_dept, **parameters) }
">{submit_label}</a> ">{submit_label}</a>
</p> </p>
""" """
if dest_url is not None
else ""
)
log(f"*** error: code {code_name} duplique: {args[code_name]}") log(f"*** error: code {code_name} duplique: {args[code_name]}")

View File

@ -127,7 +127,16 @@ def sco_import_format(with_codesemestre=True):
if fieldname not in aliases: if fieldname not in aliases:
aliases.insert(0, fieldname) # prepend aliases.insert(0, fieldname) # prepend
if with_codesemestre or fs[0] != "codesemestre": if with_codesemestre or fs[0] != "codesemestre":
r.append((fieldname, typ, table, allow_nulls, description, aliases)) r.append(
(
fieldname,
typ,
table,
scu.to_bool(allow_nulls),
description,
aliases,
)
)
return r return r
@ -249,18 +258,17 @@ def students_import_excel(
if formsemestre_id if formsemestre_id
else url_for("notes.index_html", scodoc_dept=g.scodoc_dept) else url_for("notes.index_html", scodoc_dept=g.scodoc_dept)
) )
H = ["<ul>"]
for d in diag:
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 render_template( return render_template(
"sco_page.j2", title="Import etudiants", content="\n".join(H) "sco_page.j2",
title="Import etudiants",
content=f"""
<h2>Import etudiants</h2>
<p>Import terminé !</p>
<ul>
{''.join('<li>' + d + '</li>' for d in diag)}
</ul>
<p><a class="stdlink" href="{dest_url}">Continuer</a></p>
""",
) )
return "" return ""
@ -488,12 +496,12 @@ def scolars_import_excel_file(
log("scolars_import_excel_file: re-raising exception") log("scolars_import_excel_file: re-raising exception")
raise raise
diag.append("Import et inscription de %s étudiants" % len(created_etudids)) diag.append(f"Import et inscription de {len(created_etudids)} étudiants")
ScolarNews.add( ScolarNews.add(
typ=ScolarNews.NEWS_INSCR, typ=ScolarNews.NEWS_INSCR,
text="Inscription de %d étudiants" # peuvent avoir ete inscrits a des semestres differents text=f"Inscription de {len(created_etudids)} étudiants",
% len(created_etudids), # peuvent avoir ete inscrits a des semestres differents
obj=formsemestre_id, obj=formsemestre_id,
max_frequency=0, max_frequency=0,
) )
@ -502,8 +510,8 @@ def scolars_import_excel_file(
cnx.commit() cnx.commit()
# Invalide les caches des semestres dans lesquels on a inscrit des etudiants: # Invalide les caches des semestres dans lesquels on a inscrit des etudiants:
for formsemestre_id in formsemestre_to_invalidate: for fid in formsemestre_to_invalidate:
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id) sco_cache.invalidate_formsemestre(formsemestre_id=fid)
return diag return diag

View File

@ -0,0 +1,103 @@
{% extends "sco_page.j2" %}
{% block styles %}
{{super()}}
<style>
table.import_format {
border-collapse: collapse;
width: 100%;
}
table.import_format td {
border: 1px solid #ddd;
padding: 8px;
}
</style>
{% endblock %}
{% block app_content %}
<h2 class="formsemestre">Téléchargement d'une nouvelle liste d'etudiants</h2>
<div class="scobox help explanation">
<p>A utiliser pour importer de <b>nouveaux</b> étudiants (typiquement au
<b>premier semestre</b>).
</p>
<p class="fontred">Si les étudiants à inscrire sont déjà dans un autre
semestre, utiliser le menu "<em>Inscriptions (passage des étudiants)
depuis d'autres semestres</em> à partir du semestre destination.
</p>
<p class="fontred">Si vous avez un portail Apogée, il est en général préférable d'importer les
étudiants depuis Apogée, via le menu "<em>Synchroniser avec étape Apogée</em>".
</p>
<p class="space-before-18">
L'opération se déroule en deux étapes. Dans un premier temps,
vous téléchargez une feuille Excel type. Vous devez remplir
cette feuille, une ligne décrivant chaque étudiant. Ensuite,
vous indiquez le nom de votre fichier dans la case "Fichier Excel"
ci-dessous, et cliquez sur "Télécharger" pour envoyer au serveur
votre liste.
</p>
{% if formsemestre %}
<p style="color: red">Les étudiants importés seront inscrits dans
le semestre <b>{{formsemestre.html_link_status()|safe}}</b>
</p>
{% else %}
<div class="warning">Cette fonction est réservé à certains cas particuliers.
Pour importer et inscrire de nouveaux étudiants dans un semestre de
formation, passer par le menu "<em>Inscriptions / Importer des étudiants</em>"
du semestre visé.
</div>
{% endif %}
</div>
<div class="scobox">
<div class="scobox-title">Feuille excel à remplir</div>
<div class="vspaced">
{% if formsemestre %}
<a class="stdlink" href="{{
url_for('scolar.import_generate_excel_sample', scodoc_dept=g.scodoc_dept, with_codesemestre=0)
}}">
{% else %}
<a class="stdlink" href="{{
url_for('scolar.import_generate_excel_sample', scodoc_dept=g.scodoc_dept)
}}">
{% endif -%}
Obtenir la feuille excel vierge</a> (que vous importerez ci-dessous après l'avoir remplie)
</div>
</div>
<div class="scobox">
<div class="scobox-title">Importation des données</div>
{{ tf_form | safe }}
</div>
<div class="scobox explanation">
<p>Le fichier Excel décrivant les étudiants doit comporter les colonnes suivantes.</p>
<p>Les colonnes peuvent être placées dans n'importe quel ordre, mais
le <b>titre</b> exact (tel que ci-dessous) doit être sur la première ligne.
</p>
<p>
Les champs avec un astérisque (*) doivent être présents (vides non autorisés).
</p>
<table class="import_format">
<tr>
<td><b>Attribut</b></td>
<td><b>Type</b></td>
<td><b>Description</b></td>
<td><b>Requis</b></td>
</tr>
{% for t in import_format %}
<tr>
<td>{{t[0]}}</td>
<td>{{t[1]}}</td>
<td>{{t[4]}}</td>
<td>{{'*' if t[3] else ''}}</td>
</tr>
{% endfor %}
</div>
{% endblock %}

View File

@ -2163,73 +2163,23 @@ def export_etudiants_courants():
def form_students_import_excel(formsemestre_id=None): def form_students_import_excel(formsemestre_id=None):
"formulaire import xls" "formulaire import xls"
formsemestre_id = int(formsemestre_id) if formsemestre_id else None formsemestre_id = int(formsemestre_id) if formsemestre_id else None
if formsemestre_id: dest_url = (
sem = sco_formsemestre.get_formsemestre(formsemestre_id) url_for(
dest_url = 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 is not None
sem = None else url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)
dest_url = url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)
if sem and not sem["etat"]:
raise ScoValueError("Modification impossible: semestre verrouille")
H = [
"""<h2 class="formsemestre">Téléchargement d\'une nouvelle liste d\'etudiants</h2>
<div style="color: red">
<p>A utiliser pour importer de <b>nouveaux</b> étudiants (typiquement au
<b>premier semestre</b>).</p>
<p>Si les étudiants à inscrire sont déjà dans un autre
semestre, utiliser le menu "<em>Inscriptions (passage des étudiants)
depuis d'autres semestres</em> à partir du semestre destination.
</p>
<p>Si vous avez un portail Apogée, il est en général préférable d'importer les
étudiants depuis Apogée, via le menu "<em>Synchroniser avec étape Apogée</em>".
</p>
</div>
<p>
L'opération se déroule en deux étapes. Dans un premier temps,
vous téléchargez une feuille Excel type. Vous devez remplir
cette feuille, une ligne décrivant chaque étudiant. Ensuite,
vous indiquez le nom de votre fichier dans la case "Fichier Excel"
ci-dessous, et cliquez sur "Télécharger" pour envoyer au serveur
votre liste.
</p>
""",
] # '
if sem:
H.append(
"""<p style="color: red">Les étudiants importés seront inscrits dans
le semestre <b>%s</b></p>"""
% sem["titremois"]
) )
else: formsemestre = (
H.append( FormSemestre.get_formsemestre(formsemestre_id)
f""" if formsemestre_id is not None
<p>Pour inscrire directement les étudiants dans un semestre de else None
formation, il suffit d'indiquer le code de ce semestre
(qui doit avoir été créé au préalable).
<a class="stdlink" href="{
url_for("scolar.index_html", showcodes=1, scodoc_dept=g.scodoc_dept)
}">Cliquez ici pour afficher les codes</a>
</p>
"""
) )
H.append("""<ol><li>""") if formsemestre and not formsemestre.etat:
if formsemestre_id: raise ScoValueError("Modification impossible: semestre verrouille")
H.append(
"""
<a class="stdlink" href="import_generate_excel_sample?with_codesemestre=0">
"""
)
else:
H.append("""<a class="stdlink" href="import_generate_excel_sample">""")
H.append(
"""Obtenir la feuille excel à remplir</a></li>
<li>"""
)
tf = TrivialFormulator( tf = TrivialFormulator(
request.base_url, request.base_url,
@ -2260,40 +2210,21 @@ def form_students_import_excel(formsemestre_id=None):
initvalues={"check_homonyms": True, "require_ine": False}, initvalues={"check_homonyms": True, "require_ine": False},
submitlabel="Télécharger", submitlabel="Télécharger",
) )
S = [
"""<hr/><p>Le fichier Excel décrivant les étudiants doit comporter les colonnes suivantes.
<p>Les colonnes peuvent être placées dans n'importe quel ordre, mais
le <b>titre</b> exact (tel que ci-dessous) doit être sur la première ligne.
</p>
<p>
Les champs avec un astérisque (*) doivent être présents (nulls non autorisés).
</p>
<p>
<table>
<tr><td><b>Attribut</b></td><td><b>Type</b></td><td><b>Description</b></td></tr>"""
]
for t in sco_import_etuds.sco_import_format(
with_codesemestre=(formsemestre_id is None)
):
if int(t[3]):
ast = ""
else:
ast = "*"
S.append(
"<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>"
% (t[0], t[1], t[4], ast)
)
if tf[0] == 0: if tf[0] == 0:
return render_template( return render_template(
"sco_page.j2", "scolar/students_import_excel.j2",
title="Import etudiants", title="Import etudiants",
content="\n".join(H) + tf[1] + "</li></ol>" + "\n".join(S), import_format=sco_import_etuds.sco_import_format(
with_codesemestre=(formsemestre_id is None)
),
tf_form=tf[1],
formsemestre=formsemestre,
sco=ScoData(formsemestre=formsemestre),
) )
elif tf[0] == -1: if tf[0] == -1:
return flask.redirect(dest_url) return flask.redirect(dest_url)
else:
return sco_import_etuds.students_import_excel( return sco_import_etuds.students_import_excel(
tf[2]["csvfile"], tf[2]["csvfile"],
formsemestre_id=int(formsemestre_id) if formsemestre_id else None, formsemestre_id=int(formsemestre_id) if formsemestre_id else None,