From 35692da42249c64a53057742222b964a9a54bdf2 Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet <emmanuel.viennet@gmail.com>
Date: Wed, 4 Sep 2024 04:09:31 +0200
Subject: [PATCH] =?UTF-8?q?Fix:=20traitement=20erreur=20si=20imports=20?=
=?UTF-8?q?=C3=A9tudiants=20dupliqu=C3=A9s.=20+=20pr=C3=A9sentation=20page?=
=?UTF-8?q?=20d'import?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/models/etudiants.py | 30 ++++-
app/scodoc/sco_import_etuds.py | 40 +++---
app/templates/scolar/students_import_excel.j2 | 103 +++++++++++++++
app/views/scolar.py | 125 ++++--------------
4 files changed, 181 insertions(+), 117 deletions(-)
create mode 100644 app/templates/scolar/students_import_excel.j2
diff --git a/app/models/etudiants.py b/app/models/etudiants.py
index 9bb6cc56..18387a56 100644
--- a/app/models/etudiants.py
+++ b/app/models/etudiants.py
@@ -237,6 +237,8 @@ class Identite(models.ScoDocModel):
Les clés adresses et admission ne SONT PAS utilisées.
(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 "dept" in args:
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:
etud.admission = Admission()
etud.adresses.append(Adresse(typeadresse="domicile"))
- db.session.flush()
-
+ try:
+ 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")
db.session.add(event)
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é.
Raises ScoGenError si problème.
+ Si dest_url === None, pas de lien continuer/annuler.
"""
etudid = etudid or args.get("etudid", 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>
{ '</li><li>'.join(listh) }
</li></ul>
+ """
+ err_page += (
+ f"""
<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>
</p>
"""
+ if dest_url is not None
+ else ""
+ )
log(f"*** error: code {code_name} duplique: {args[code_name]}")
diff --git a/app/scodoc/sco_import_etuds.py b/app/scodoc/sco_import_etuds.py
index 5e129d69..ceedd9a9 100644
--- a/app/scodoc/sco_import_etuds.py
+++ b/app/scodoc/sco_import_etuds.py
@@ -127,7 +127,16 @@ def sco_import_format(with_codesemestre=True):
if fieldname not in aliases:
aliases.insert(0, fieldname) # prepend
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
@@ -249,18 +258,17 @@ def students_import_excel(
if formsemestre_id
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>)
+ return render_template(
+ "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 render_template(
- "sco_page.j2", title="Import etudiants", content="\n".join(H)
+ """,
)
return ""
@@ -488,12 +496,12 @@ def scolars_import_excel_file(
log("scolars_import_excel_file: re-raising exception")
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(
typ=ScolarNews.NEWS_INSCR,
- text="Inscription de %d étudiants" # peuvent avoir ete inscrits a des semestres differents
- % len(created_etudids),
+ text=f"Inscription de {len(created_etudids)} étudiants",
+ # peuvent avoir ete inscrits a des semestres differents
obj=formsemestre_id,
max_frequency=0,
)
@@ -502,8 +510,8 @@ def scolars_import_excel_file(
cnx.commit()
# Invalide les caches des semestres dans lesquels on a inscrit des etudiants:
- for formsemestre_id in formsemestre_to_invalidate:
- sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id)
+ for fid in formsemestre_to_invalidate:
+ sco_cache.invalidate_formsemestre(formsemestre_id=fid)
return diag
diff --git a/app/templates/scolar/students_import_excel.j2 b/app/templates/scolar/students_import_excel.j2
new file mode 100644
index 00000000..db5378b5
--- /dev/null
+++ b/app/templates/scolar/students_import_excel.j2
@@ -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 %}
\ No newline at end of file
diff --git a/app/views/scolar.py b/app/views/scolar.py
index b5c8a126..688be452 100644
--- a/app/views/scolar.py
+++ b/app/views/scolar.py
@@ -2163,73 +2163,23 @@ def export_etudiants_courants():
def form_students_import_excel(formsemestre_id=None):
"formulaire import xls"
formsemestre_id = int(formsemestre_id) if formsemestre_id else None
- if formsemestre_id:
- sem = sco_formsemestre.get_formsemestre(formsemestre_id)
- dest_url = url_for(
+ dest_url = (
+ url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
- else:
- sem = None
- 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:
- H.append(
- f"""
- <p>Pour inscrire directement les étudiants dans un semestre de
- 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_id:
- 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>"""
+ if formsemestre_id is not None
+ else url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)
)
+ formsemestre = (
+ FormSemestre.get_formsemestre(formsemestre_id)
+ if formsemestre_id is not None
+ else None
+ )
+
+ if formsemestre and not formsemestre.etat:
+ raise ScoValueError("Modification impossible: semestre verrouille")
tf = TrivialFormulator(
request.base_url,
@@ -2260,46 +2210,27 @@ def form_students_import_excel(formsemestre_id=None):
initvalues={"check_homonyms": True, "require_ine": False},
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:
return render_template(
- "sco_page.j2",
+ "scolar/students_import_excel.j2",
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)
- else:
- return sco_import_etuds.students_import_excel(
- tf[2]["csvfile"],
- formsemestre_id=int(formsemestre_id) if formsemestre_id else None,
- check_homonyms=tf[2]["check_homonyms"],
- require_ine=tf[2]["require_ine"],
- )
+
+ return sco_import_etuds.students_import_excel(
+ tf[2]["csvfile"],
+ formsemestre_id=int(formsemestre_id) if formsemestre_id else None,
+ check_homonyms=tf[2]["check_homonyms"],
+ require_ine=tf[2]["require_ine"],
+ )
@bp.route("/import_generate_excel_sample")
--
GitLab