diff --git a/app/models/etudiants.py b/app/models/etudiants.py index 9bb6cc56d22e5e6ed6cd51bc0590b46dc359c5b5..18387a5656c5e523f820f2c1d2b7c1626dfc05d3 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 5e129d69ddf656c9922efa17f07811dff934d204..ceedd9a9c359f6028d9746f34c6f9d7e01468440 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 0000000000000000000000000000000000000000..db5378b5417ccf2790f09fc33eb9f0d2bd1b0cde --- /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 b5c8a126b6ccb7063fdf60acc4206c3c6a082039..688be452419dc0fbf6463eb95bad7d499399430c 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")