# -*- coding: utf-8 -*- ############################################################################## # # ScoDoc # # Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Emmanuel Viennet emmanuel.viennet@viennet.net # ############################################################################## """ Module scolar: vues de .../ScoDoc//Scolarite issu de ScoDoc7 / ZScolar.py Emmanuel Viennet, 2021 """ import datetime import time import requests import flask from flask import abort, flash, make_response, render_template, url_for from flask import g, request from flask_json import as_json from flask_login import current_user from flask_wtf import FlaskForm from flask_wtf.file import FileField, FileAllowed import sqlalchemy as sa from wtforms import SubmitField import app from app import db from app import log from app.decorators import ( scodoc, scodoc7func, permission_required, permission_required_compat_scodoc7, ) from app.models import ( Admission, Departement, FormSemestre, Identite, Partition, ScolarEvent, ScolarNews, Scolog, ) from app.models.etudiants import make_etud_args from app.views import scolar_bp as bp from app.views import ScoData import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb from app.scodoc.sco_permissions import Permission from app.scodoc.sco_exceptions import ( AccessDenied, ScoPermissionDenied, ScoValueError, ) from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message from app.scodoc.gen_tables import GenTable from app.scodoc import ( codes_cursus, html_sco_header, sco_import_etuds, sco_archives_etud, sco_bug_report, sco_cache, sco_debouche, sco_dept, sco_dump_db, sco_etud, sco_edt_cal, sco_find_etud, sco_formsemestre, sco_formsemestre_inscriptions, sco_groups, sco_groups_edit, sco_groups_exports, sco_groups_view, sco_page_etud, sco_permissions_check, sco_photos, sco_portal_apogee, sco_preferences, sco_synchro_etuds, sco_trombino, sco_trombino_tours, sco_up_to_date, ) from app.tables import list_etuds from app.forms.main.create_bug_report import CreateBugReport def sco_publish(route, function, permission, methods=["GET"]): """Declare a route for a python function, protected by permission and called following ScoDoc 7 Zope standards. """ return bp.route(route, methods=methods)( scodoc(permission_required(permission)(scodoc7func(function))) ) # -------------------------------------------------------------------- # # SCOLARITE (/ScoDoc//Scolarite/...) # # -------------------------------------------------------------------- # -------------------------------------------------------------------- # # PREFERENCES # # -------------------------------------------------------------------- @bp.route("/edit_preferences", methods=["GET", "POST"]) @scodoc @permission_required(Permission.EditPreferences) @scodoc7func def edit_preferences(): """Edit global preferences (lien "Paramétrage" département)""" return sco_preferences.get_base_preferences().edit() @bp.route("/formsemestre_edit_preferences", methods=["GET", "POST"]) @scodoc @permission_required(Permission.ScoView) @scodoc7func def formsemestre_edit_preferences(formsemestre_id): """Edit preferences for a semestre""" sem = sco_formsemestre.get_formsemestre(formsemestre_id) ok = ( current_user.has_permission(Permission.EditFormSemestre) or ((current_user.id in sem["responsables"]) and sem["resp_can_edit"]) ) and (sem["etat"]) if ok: return sco_preferences.SemPreferences(formsemestre_id=formsemestre_id).edit() else: raise AccessDenied( "Modification impossible pour %s" % current_user.get_nomplogin() ) @bp.route("/doc_preferences") @scodoc @permission_required(Permission.ScoView) @scodoc7func def doc_preferences(): """List preferences for wiki documentation""" response = make_response(sco_preferences.doc_preferences()) response.headers["Content-Type"] = "text/plain" return response class DeptLogosConfigurationForm(FlaskForm): "Panneau de configuration logos dept" logo_header = FileField( label="Modifier l'image:", description="logo placé en haut des documents PDF", validators=[ FileAllowed( scu.LOGOS_IMAGES_ALLOWED_TYPES, f"n'accepte que les fichiers image {','.join(scu.LOGOS_IMAGES_ALLOWED_TYPES)}", ) ], ) logo_footer = FileField( label="Modifier l'image:", description="logo placé en pied des documents PDF", validators=[ FileAllowed( scu.LOGOS_IMAGES_ALLOWED_TYPES, f"n'accepte que les fichiers image {','.join(scu.LOGOS_IMAGES_ALLOWED_TYPES)}", ) ], ) submit = SubmitField("Enregistrer") # @bp.route("/config_logos", methods=["GET", "POST"]) # @permission_required(Permission.EditPreferences) # def config_logos(scodoc_dept): # "Panneau de configuration général" # form = DeptLogosConfigurationForm() # if form.validate_on_submit(): # if form.logo_header.data: # sco_logos.store_image( # form.logo_header.data, # os.path.join( # scu.SCODOC_LOGOS_DIR, "logos_" + scodoc_dept, "logo_header" # ), # ) # if form.logo_footer.data: # sco_logos.store_image( # form.logo_footer.data, # os.path.join( # scu.SCODOC_LOGOS_DIR, "logos_" + scodoc_dept, "logo_footer" # ), # ) # app.clear_scodoc_cache() # flash(f"Logos enregistrés") # return flask.redirect(url_for("scolar.index_html", scodoc_dept=scodoc_dept)) # # return render_template( # "configuration.j2", # title="Configuration Logos du département", # form=form, # scodoc_dept=scodoc_dept, # ) # # # class DeptLogosConfigurationForm(FlaskForm): # "Panneau de configuration logos dept" # # logo_header = FileField( # label="Modifier l'image:", # description="logo placé en haut des documents PDF", # validators=[ # FileAllowed( # scu.LOGOS_IMAGES_ALLOWED_TYPES, # f"n'accepte que les fichiers image {','.join([e for e in scu.LOGOS_IMAGES_ALLOWED_TYPES])}", # ) # ], # ) # # logo_footer = FileField( # label="Modifier l'image:", # description="logo placé en pied des documents PDF", # validators=[ # FileAllowed( # scu.LOGOS_IMAGES_ALLOWED_TYPES, # f"n'accepte que les fichiers image {','.join([e for e in scu.LOGOS_IMAGES_ALLOWED_TYPES])}", # ) # ], # ) # # submit = SubmitField("Enregistrer") # @bp.route("/config_logos", methods=["GET", "POST"]) # @permission_required(Permission.EditPreferences) # def config_logos(scodoc_dept): # "Panneau de configuration général" # form = DeptLogosConfigurationForm() # if form.validate_on_submit(): # if form.logo_header.data: # sco_logos.store_image( # form.logo_header.data, # os.path.join( # scu.SCODOC_LOGOS_DIR, "logos_" + scodoc_dept, "logo_header" # ), # ) # if form.logo_footer.data: # sco_logos.store_image( # form.logo_footer.data, # os.path.join( # scu.SCODOC_LOGOS_DIR, "logos_" + scodoc_dept, "logo_footer" # ), # ) # app.clear_scodoc_cache() # flash(f"Logos enregistrés") # return flask.redirect(url_for("scolar.index_html", scodoc_dept=scodoc_dept)) # # return render_template( # "configuration.j2", # title="Configuration Logos du département", # form=form, # scodoc_dept=scodoc_dept, # ) # -------------------------------------------------------------------- # # ETUDIANTS # # -------------------------------------------------------------------- @bp.route("/show_etud_log") @scodoc @permission_required(Permission.ScoView) @scodoc7func def show_etud_log(etudid, fmt="html"): """Display log of operations on this student""" etud = Identite.get_etud(etudid) operations = Scolog.query.filter_by(etudid=etud.id).order_by(Scolog.date.desc()) tab = GenTable( titles={ "date": "Date", "authenticated_user": "Utilisateur", "method": "Opération", "msg": "Message", }, columns_ids=("date", "authenticated_user", "method", "msg"), rows=[op.to_dict(convert_date=True) for op in operations], html_sortable=True, html_class="table_leftalign", base_url="%s?etudid=%s" % (request.base_url, etud.id), page_title=f"Opérations sur {etud.nom_prenom()}", html_title=f"""

Opérations effectuées sur l'étudiant{etud.e} { etud.html_link_fiche()}

""", filename="log_" + scu.make_filename(etud.nom_prenom()), html_next_section=f""" """, preferences=sco_preferences.SemPreferences(), table_id="show_etud_log", ) return tab.make_page(fmt=fmt) # ---------- PAGE ACCUEIL (listes) -------------- @bp.route("/") @bp.route("/index_html", alias=True) @scodoc @permission_required(Permission.ScoView) @scodoc7func def index_html(showcodes=0, showsemtable=0): "La page d'accueil département (ScoDoc//Scolarite)" return sco_dept.index_html(showcodes=showcodes, showsemtable=showsemtable) @bp.route("/export_table_dept_formsemestres") @scodoc @permission_required(Permission.ScoView) def export_table_dept_formsemestres(): """La table de tous les semestres non EXt du département, en excel""" table = sco_dept.index_html(showcodes=True, export_table_formsemestres=True) return scu.send_file( table.excel(), f"semestres_{g.scodoc_dept}", suffix=scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE, ) @bp.route("/install_info") @scodoc @permission_required(Permission.ScoView) def install_info(): """Information on install status (html str)""" return sco_up_to_date.is_up_to_date() @bp.route("/dept_news") @scodoc @permission_required(Permission.ScoView) def dept_news(): "Affiche table des dernières opérations" return render_template( "dept_news.j2", title=f"Opérations {g.scodoc_dept}", sco=ScoData() ) @bp.route("/dept_news_json") @scodoc @permission_required(Permission.ScoView) def dept_news_json(): "Table des news du département" start = request.args.get("start", type=int) length = request.args.get("length", type=int) log(f"dept_news_json( start={start}, length={length})") query = ScolarNews.query.filter_by(dept_id=g.scodoc_dept_id) # search search = request.args.get("search[value]") if search: query = query.filter( db.or_( ScolarNews.authenticated_user.like(f"%{search}%"), ScolarNews.text.like(f"%{search}%"), ) ) total_filtered = query.count() # sorting order = [] i = 0 while True: col_index = request.args.get(f"order[{i}][column]") if col_index is None: break col_name = request.args.get(f"columns[{col_index}][data]") if col_name not in ["date", "type", "authenticated_user"]: col_name = "date" descending = request.args.get(f"order[{i}][dir]") == "desc" col = getattr(ScolarNews, col_name) if descending: col = col.desc() order.append(col) i += 1 if order: query = query.order_by(*order) # pagination query = query.offset(start).limit(length) data = [news.to_dict() for news in query] # response return { "data": data, "recordsFiltered": total_filtered, "recordsTotal": ScolarNews.query.count(), "draw": request.args.get("draw", type=int), } sco_publish( "/trombino", sco_trombino.trombino, Permission.ScoView, methods=["GET", "POST"] ) sco_publish( "/pdf_trombino_tours", sco_trombino_tours.pdf_trombino_tours, Permission.ScoView ) sco_publish( "/pdf_feuille_releve_absences", sco_trombino_tours.pdf_feuille_releve_absences, Permission.ScoView, ) sco_publish( "/trombino_copy_photos", sco_trombino.trombino_copy_photos, Permission.ScoView, methods=["GET", "POST"], ) sco_publish( "/groups_export_annotations", sco_groups_exports.groups_export_annotations, Permission.ViewEtudData, ) @bp.route("/groups_lists") @scodoc @permission_required(Permission.ScoView) @scodoc7func def groups_lists( group_ids=(), fmt="html", # Options pour listes: with_codes=0, etat=None, with_paiement=0, # si vrai, ajoute colonnes infos paiement droits et finalisation inscription (lent car interrogation portail) with_archives=0, # ajoute colonne avec noms fichiers archivés with_annotations=0, with_bourse=0, formsemestre_id=None, ): "Listes des étudiants des groupes" return sco_groups_view.groups_lists( group_ids=group_ids, fmt=fmt, with_codes=with_codes, etat=etat, with_paiement=with_paiement, with_archives=with_archives, with_annotations=with_annotations, with_bourse=with_bourse, formsemestre_id=formsemestre_id, ) @bp.route("/groups_photos") @scodoc @permission_required(Permission.ScoView) @scodoc7func def groups_photos( group_ids=(), etat=None, formsemestre_id=None, ): "trombi HTML" return sco_groups_view.groups_photos( group_ids=group_ids, etat=etat, formsemestre_id=formsemestre_id, ) @bp.route("/groups_feuilles") @scodoc @permission_required(Permission.ScoView) @scodoc7func def groups_feuilles( group_ids=(), etat=None, formsemestre_id=None, ): "Feuilles appel, liens assiduité, etc." return sco_groups_view.groups_feuilles( group_ids=group_ids, etat=etat, formsemestre_id=formsemestre_id, ) sco_publish( "/export_groups_as_moodle_csv", sco_groups_view.export_groups_as_moodle_csv, Permission.ScoView, ) # -------------------------- INFOS SUR ETUDIANTS -------------------------- @bp.route("/getEtudInfo") @scodoc @permission_required(Permission.ScoView) @scodoc7func def getEtudInfo(etudid=False, code_nip=False, filled=False, fmt=None): """infos sur un etudiant (API) On peut specifier etudid ou code_nip ou bien cherche dans les arguments de la requête: etudid, code_nip, code_ine (dans cet ordre). """ etud = sco_etud.get_etud_info(etudid=etudid, code_nip=code_nip, filled=filled) if fmt is None: return etud return scu.sendResult(etud, name="etud", fmt=fmt) sco_publish( "/search_etud_in_dept", sco_find_etud.search_etud_in_dept, Permission.ScoView, methods=["GET", "POST"], ) @bp.route("/search_etud_by_name") @bp.route("/Notes/search_etud_by_name") # for JS apis @scodoc @permission_required(Permission.ScoView) @as_json def search_etud_by_name(): """Recherche étudiants par nom ou NIP utilisé par autocomplete formulaire recherche """ term = request.args["term"] data = sco_find_etud.search_etud_by_name(term) return data # XMLgetEtudInfos était le nom dans l'ancienne API ScoDoc 6 @bp.route("/etud_info", methods=["GET", "POST"]) # pour compat anciens clients PHP) @bp.route( "/XMLgetEtudInfos", methods=["GET", "POST"] ) # pour compat anciens clients PHP) @bp.route( "/Absences/XMLgetEtudInfos", methods=["GET", "POST"] ) # pour compat anciens clients PHP @bp.route( "/Notes/XMLgetEtudInfos", methods=["GET", "POST"] ) # pour compat anciens clients PHP @scodoc @permission_required_compat_scodoc7(Permission.ScoView) @scodoc7func def etud_info(etudid=None, fmt="xml"): "Donne les informations sur un etudiant" if not fmt in ("xml", "json"): raise ScoValueError("format demandé non supporté par cette fonction.") t0 = time.time() args = make_etud_args(etudid=etudid) cnx = ndb.GetDBConnexion() etuds = sco_etud.etudident_list(cnx, args) if not etuds: # etudiant non trouvé: message d'erreur d = { "etudid": etudid, "nom": "?", "nom_usuel": "", "prenom": "?", "civilite": "?", "sexe": "?", # for backward compat "email": "?", "emailperso": "", "error": "code etudiant inconnu", } return scu.sendResult(d, name="etudiant", fmt=fmt, force_outer_xml_tag=False) d = {} etud = etuds[0] sco_etud.fill_etuds_info([etud]) etud["date_naissance_iso"] = ndb.DateDMYtoISO(etud["date_naissance"]) for a in ( "etudid", "code_nip", "code_ine", "nom", "nom_usuel", "prenom", "nomprenom", "prenom_etat_civil", "email", "emailperso", "domicile", "codepostaldomicile", "villedomicile", "paysdomicile", "telephone", "telephonemobile", "fax", "bac", "specialite", "annee_bac", "nomlycee", "villelycee", "codepostallycee", "codelycee", "date_naissance_iso", ): d[a] = etud[a] # ne pas quoter car ElementTree.tostring quote déjà d["civilite"] = etud["civilite_str"] # exception: ne sort pas les civilités brutes d["civilite_etat_civil"] = etud["civilite_etat_civil_str"] d["sexe"] = d["civilite"] # backward compat pour anciens clients d["photo_url"] = sco_photos.etud_photo_url(etud) sem = etud["cursem"] if sem: sco_groups.etud_add_group_infos(etud, sem["formsemestre_id"] if sem else None) d["insemestre"] = [ { "current": "1", "formsemestre_id": sem["formsemestre_id"], "date_debut": ndb.DateDMYtoISO(sem["date_debut"]), "date_fin": ndb.DateDMYtoISO(sem["date_fin"]), "etat": sem["ins"]["etat"], "groupes": etud["groupes"], # slt pour semestre courant } ] else: d["insemestre"] = [] for sem in etud["sems"]: if sem != etud["cursem"]: d["insemestre"].append( { "formsemestre_id": sem["formsemestre_id"], "date_debut": ndb.DateDMYtoISO(sem["date_debut"]), "date_fin": ndb.DateDMYtoISO(sem["date_fin"]), "etat": sem["ins"]["etat"], } ) log("etud_info (%gs)" % (time.time() - t0)) return scu.sendResult( d, name="etudiant", fmt=fmt, force_outer_xml_tag=False, quote_xml=False ) # -------------------------- FICHE ETUDIANT -------------------------- sco_publish("/fiche_etud", sco_page_etud.fiche_etud, Permission.ScoView) sco_publish( "/etud_upload_file_form", sco_archives_etud.etud_upload_file_form, Permission.ViewEtudData, methods=["GET", "POST"], ) sco_publish( "/etud_delete_archive", sco_archives_etud.etud_delete_archive, Permission.ViewEtudData, methods=["GET", "POST"], ) sco_publish( "/etud_get_archived_file", sco_archives_etud.etud_get_archived_file, Permission.ViewEtudData, ) sco_publish( "/etudarchive_import_files_form", sco_archives_etud.etudarchive_import_files_form, Permission.ViewEtudData, methods=["GET", "POST"], ) sco_publish( "/etudarchive_generate_excel_sample", sco_archives_etud.etudarchive_generate_excel_sample, Permission.ScoView, ) # Debouche / devenir etudiant sco_publish( "/itemsuivi_suppress", sco_debouche.itemsuivi_suppress, Permission.EtudChangeAdr, methods=["GET", "POST"], ) sco_publish( "/itemsuivi_create", sco_debouche.itemsuivi_create, Permission.EtudChangeAdr, methods=["GET", "POST"], ) sco_publish( "/itemsuivi_set_date", sco_debouche.itemsuivi_set_date, Permission.EtudChangeAdr, methods=["GET", "POST"], ) sco_publish( "/itemsuivi_set_situation", sco_debouche.itemsuivi_set_situation, Permission.EtudChangeAdr, methods=["GET", "POST"], ) sco_publish( "/itemsuivi_list_etud", sco_debouche.itemsuivi_list_etud, Permission.ScoView ) sco_publish("/itemsuivi_tag_list", sco_debouche.itemsuivi_tag_list, Permission.ScoView) sco_publish( "/itemsuivi_tag_search", sco_debouche.itemsuivi_tag_search, Permission.ScoView ) sco_publish( "/itemsuivi_tag_set", sco_debouche.itemsuivi_tag_set, Permission.EtudChangeAdr, methods=["GET", "POST"], ) @bp.route("/doAddAnnotation", methods=["GET", "POST"]) @scodoc @permission_required(Permission.EtudAddAnnotations) @scodoc7func def doAddAnnotation(etudid, comment): "ajoute annotation sur etudiant" _ = Identite.get_etud(etudid) # check existence if comment: cnx = ndb.GetDBConnexion() sco_etud.etud_annotations_create( cnx, args={ "etudid": etudid, "comment": comment, "author": current_user.user_name, }, ) Scolog.logdb(method="addAnnotation", etudid=etudid, commit=True) return flask.redirect( url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid) ) @bp.route("/doSuppressAnnotation", methods=["GET", "POST"]) @scodoc @permission_required(Permission.ScoView) @scodoc7func def doSuppressAnnotation(etudid, annotation_id): """Suppression annotation.""" if not sco_permissions_check.can_suppress_annotation(annotation_id): raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !") cnx = ndb.GetDBConnexion() annos = sco_etud.etud_annotations_list(cnx, args={"id": annotation_id}) if len(annos) != 1: raise ScoValueError("annotation inexistante !") anno = annos[0] log(f"suppress annotation: {anno}") Scolog.logdb(method="SuppressAnnotation", etudid=etudid) sco_etud.etud_annotations_delete(cnx, annotation_id) flash("Annotation supprimée") return flask.redirect( url_for( "scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid, ) ) @bp.route("/form_change_coordonnees", methods=["GET", "POST"]) @scodoc @permission_required(Permission.EtudChangeAdr) @scodoc7func def form_change_coordonnees(etudid): "edit coordonnees etudiant" if not current_user.has_permission(Permission.ViewEtudData): raise ScoPermissionDenied() etud = Identite.get_etud(etudid) cnx = ndb.GetDBConnexion() adrs = sco_etud.adresse_list(cnx, {"etudid": etudid}) if adrs: adr = adrs[0] else: adr = {} # no data for this student H = [ f"""{html_sco_header.sco_header( page_title=f"Changement coordonnées de {etud.nomprenom}" )}

Changement des coordonnées de {etud.nomprenom}

""" ] tf = TrivialFormulator( request.base_url, scu.get_request_args(), ( ("adresse_id", {"input_type": "hidden"}), ("etudid", {"input_type": "hidden"}), ( "email", { "size": 40, "title": "e-mail", "explanation": "adresse institutionnelle", }, ), ( "emailperso", { "size": 40, "title": "e-mail", "explanation": "adresse personnelle", }, ), ( "domicile", {"size": 65, "explanation": "numéro, rue", "title": "Adresse"}, ), ("codepostaldomicile", {"size": 6, "title": "Code postal"}), ("villedomicile", {"size": 20, "title": "Ville"}), ("paysdomicile", {"size": 20, "title": "Pays"}), ("", {"input_type": "separator", "default": " "}), ("telephone", {"size": 13, "title": "Téléphone"}), ("telephonemobile", {"size": 13, "title": "Mobile"}), ), initvalues=adr, submitlabel="Enregistrer", cancelbutton="Annuler", ) dest_url = url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid) if tf[0] == 0: return "\n".join(H) + tf[1] + html_sco_header.sco_footer() elif tf[0] == -1: return flask.redirect(dest_url) else: if adrs: sco_etud.adresse_edit(cnx, args=tf[2]) else: sco_etud.adresse_create(cnx, args=tf[2]) Scolog.logdb(method="changeCoordonnees", etudid=etudid, commit=True) return flask.redirect(dest_url) # --- Gestion des groupes: sco_publish( "/affect_groups", sco_groups_edit.affect_groups, Permission.ScoView, methods=["GET", "POST"], ) sco_publish( "/XMLgetGroupsInPartition", sco_groups.XMLgetGroupsInPartition, Permission.ScoView ) sco_publish( "/formsemestre_partition_list", sco_groups.formsemestre_partition_list, Permission.ScoView, ) sco_publish("/setGroups", sco_groups.setGroups, Permission.ScoView, methods=["POST"]) sco_publish( "/group_rename", sco_groups_edit.group_rename, Permission.ScoView, methods=["GET", "POST"], ) @bp.route("/groups_auto_repartition/", methods=["GET", "POST"]) @scodoc @permission_required(Permission.ScoView) def groups_auto_repartition(partition_id: int): "Réparti les etudiants dans des groupes dans une partition" partition: Partition = Partition.query.get_or_404(partition_id) return sco_groups.groups_auto_repartition(partition) sco_publish( "/edit_partition_form", sco_groups.edit_partition_form, Permission.ScoView, methods=["GET", "POST"], ) sco_publish( "/partition_delete", sco_groups.partition_delete, Permission.ScoView, methods=["GET", "POST"], ) sco_publish( "/partition_set_attr", sco_groups.partition_set_attr, Permission.ScoView, methods=["GET", "POST"], ) sco_publish( "/partition_move", sco_groups.partition_move, Permission.ScoView, methods=["GET", "POST"], ) sco_publish( "/partition_set_name", sco_groups.partition_set_name, Permission.ScoView, methods=["GET", "POST"], ) sco_publish( "/partition_rename", sco_groups.partition_rename, Permission.ScoView, methods=["GET", "POST"], ) sco_publish( "/partition_create", sco_groups.partition_create, Permission.ScoView, # controle d'access ad-hoc methods=["GET", "POST"], ) # Nouvel éditeur de partitions et groupe, @SebL Jul 2022 @bp.route("/partition_editor", methods=["GET", "POST"]) @scodoc @permission_required(Permission.ScoView) @scodoc7func def partition_editor(formsemestre_id: int, edit_partition=False): """Page édition groupes et partitions Si edit_partition, se met en mode édition des partitions. """ formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) edit_partition = bool(int(edit_partition)) if edit_partition else False formsemestre.setup_parcours_groups() return render_template( "scolar/partition_editor.j2", formsemestre=formsemestre, read_only=not formsemestre.can_change_groups(), edit_partition=edit_partition, is_edt_configured=sco_edt_cal.is_edt_configured(), sco=ScoData(formsemestre=formsemestre), scu=scu, ) @bp.route("/students_groups_auto_assignment", methods=["GET", "POST"]) @scodoc @permission_required(Permission.ScoView) @scodoc7func def students_groups_auto_assignment(formsemestre_id: int): """Répartition auto des groupes""" formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) H = [ html_sco_header.sco_header( page_title="Répartition des groupes", ), render_template( "scolar/students_groups_auto_assignment.j2", formsemestre=formsemestre, ), html_sco_header.sco_footer(), ] return "\n".join(H) @bp.route("/create_partition_parcours", methods=["GET", "POST"]) @scodoc @permission_required(Permission.ScoView) @scodoc7func def create_partition_parcours(formsemestre_id): """Création d'une partitions nommée "Parcours" (PARTITION_PARCOURS) avec un groupe par parcours.""" formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) formsemestre.setup_parcours_groups() return flask.redirect( url_for( "scolar.edit_partition_form", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, ) ) sco_publish("/etud_info_html", sco_page_etud.etud_info_html, Permission.ScoView) # --- Gestion des photos: sco_publish("/get_photo_image", sco_photos.get_photo_image, Permission.ScoView) sco_publish("/etud_photo_html", sco_photos.etud_photo_html, Permission.ScoView) @bp.route("/etud_photo_orig_page/") @scodoc @permission_required(Permission.ScoView) def etud_photo_orig_page(etudid): "Page with photo in orig. size" etud = Identite.get_etud(etudid) return f"""{ html_sco_header.sco_header(etudid=etud.id, page_title=etud.nomprenom) }

{etud.nomprenom}

{etud.photo_html(size='orig')}
{html_sco_header.sco_footer()} """ @bp.route("/form_change_photo", methods=["GET", "POST"]) @scodoc @permission_required(Permission.EtudChangeAdr) @scodoc7func def form_change_photo(etudid=None): """Formulaire changement photo étudiant""" etud = Identite.get_etud(etudid) if sco_photos.etud_photo_is_local(etud.photo_filename): photo_loc = "dans ScoDoc" else: photo_loc = "externe" H = [ html_sco_header.sco_header(page_title="Changement de photo"), f"""

Changement de la photo de {etud.nomprenom}

Photo actuelle ({photo_loc}): {sco_photos.etud_photo_html(etudid=etud.id, title="photo actuelle")}

Le fichier ne doit pas dépasser {sco_photos.MAX_FILE_SIZE//1024}Ko (recadrer l'image, format "portrait" de préférence).

L'image sera automagiquement réduite pour obtenir une hauteur de 90 pixels.

""", ] tf = TrivialFormulator( request.base_url, scu.get_request_args(), ( ("etudid", {"default": etud.id, "input_type": "hidden"}), ( "photofile", {"input_type": "file", "title": "Fichier image", "size": 20}, ), ), submitlabel="Valider", cancelbutton="Annuler", ) dest_url = url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id) if tf[0] == 0: return ( "\n".join(H) + f""" {tf[1]}

Supprimer cette photo

{html_sco_header.sco_footer()} """ ) elif tf[0] == -1: return flask.redirect(dest_url) else: data = tf[2]["photofile"].read() status, err_msg = sco_photos.store_photo( etud, data, tf[2]["photofile"].filename ) if status: return flask.redirect(dest_url) else: H.append(f"""

Erreur: {err_msg}

""") return "\n".join(H) + html_sco_header.sco_footer() @bp.route("/form_suppress_photo", methods=["POST", "GET"]) @scodoc @permission_required(Permission.EtudChangeAdr) @scodoc7func def form_suppress_photo(etudid=None, dialog_confirmed=False): """Formulaire suppression photo étudiant""" etud = Identite.get_etud(etudid) if not dialog_confirmed: return scu.confirm_dialog( f"

Confirmer la suppression de la photo de {etud.nom_disp()} ?

", dest_url="", cancel_url=url_for( "scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id ), parameters={"etudid": etud.id}, ) sco_photos.suppress_photo(etud) return flask.redirect( url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id) ) # @bp.route("/form_dem") @scodoc @permission_required(Permission.EtudInscrit) @scodoc7func def form_dem(etudid, formsemestre_id): "Formulaire Démission Etudiant" return _form_dem_of_def( etudid, formsemestre_id, operation_name="Démission", operation_method="do_dem_etudiant", ) @bp.route("/form_def") @scodoc @permission_required(Permission.EtudInscrit) @scodoc7func def form_def(etudid, formsemestre_id): "Formulaire Défaillance Etudiant" return _form_dem_of_def( etudid, formsemestre_id, operation_name="Défaillance", operation_method="do_def_etudiant", ) def _form_dem_of_def( etudid: int, formsemestre_id: int, operation_name: str = "", operation_method: str = "", ): "Formulaire démission ou défaillance Etudiant" etud = Identite.get_etud(etudid) formsemestre: FormSemestre = FormSemestre.query.filter_by( id=formsemestre_id, dept_id=g.scodoc_dept_id ).first_or_404() if not formsemestre.etat: raise ScoValueError("Modification impossible: semestre verrouille") nowdmy = time.strftime(scu.DATE_FMT) # header = html_sco_header.sco_header( page_title=f"""{operation_name} de {etud.nomprenom} (du semestre {formsemestre.titre_mois()})""" ) validations_descr = formsemestre.etud_validations_description_html(etudid) return f""" {header}

{operation_name} de {etud.nomprenom} ({formsemestre.titre_mois()})

Date de la {operation_name.lower()} (J/M/AAAA): 
{'

Attention: il y a des décisions de jury déjà prises !

' if validations_descr else ""} {validations_descr} {('

modifier ces décisions

') if validations_descr else ""}
{html_sco_header.sco_footer()} """ @bp.route("/do_dem_etudiant") @scodoc @permission_required(Permission.EtudInscrit) @scodoc7func def do_dem_etudiant(etudid, formsemestre_id, event_date=None): "Déclare la démission d'un etudiant dans le semestre" return _do_dem_or_def_etud( etudid, formsemestre_id, event_date=event_date, etat_new=scu.DEMISSION, operation_method="dem_etudiant", event_type="DEMISSION", ) @bp.route("/do_def_etudiant") @scodoc @permission_required(Permission.EtudInscrit) @scodoc7func def do_def_etudiant(etudid, formsemestre_id, event_date=None): "Déclare la défaillance d'un etudiant dans le semestre" return _do_dem_or_def_etud( etudid, formsemestre_id, event_date=event_date, etat_new=codes_cursus.DEF, operation_method="defailleEtudiant", event_type="DEFAILLANCE", ) def _do_dem_or_def_etud( etudid, formsemestre_id, event_date=None, etat_new=scu.DEMISSION, # DEMISSION or DEF operation_method="demEtudiant", event_type="DEMISSION", redirect=True, ): "Démission ou défaillance d'un étudiant" sco_formsemestre_inscriptions.do_formsemestre_demission( etudid, formsemestre_id, event_date=event_date, etat_new=etat_new, # DEMISSION or DEF operation_method=operation_method, event_type=event_type, ) if redirect: return flask.redirect( url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid) ) @bp.route("/do_cancel_dem", methods=["GET", "POST"]) @scodoc @permission_required(Permission.EtudInscrit) @scodoc7func def do_cancel_dem(etudid, formsemestre_id, dialog_confirmed=False, args=None): "Annule une démission" return _do_cancel_dem_or_def( etudid, formsemestre_id, dialog_confirmed=dialog_confirmed, args=args, operation_name="démission", etat_current=scu.DEMISSION, etat_new=scu.INSCRIT, operation_method="cancelDem", event_type="DEMISSION", ) @bp.route("/do_cancel_def", methods=["GET", "POST"]) @scodoc @permission_required(Permission.EtudInscrit) @scodoc7func def do_cancel_def(etudid, formsemestre_id, dialog_confirmed=False, args=None): "Annule la défaillance de l'étudiant" return _do_cancel_dem_or_def( etudid, formsemestre_id, dialog_confirmed=dialog_confirmed, args=args, operation_name="défaillance", etat_current=codes_cursus.DEF, etat_new=scu.INSCRIT, operation_method="cancel_def", event_type="DEFAILLANCE", ) def _do_cancel_dem_or_def( etudid, formsemestre_id, dialog_confirmed=False, args=None, operation_name="", # "démission" ou "défaillance" etat_current=scu.DEMISSION, etat_new=scu.INSCRIT, operation_method="cancel_dem", event_type="DEMISSION", ): "Annule une démission ou une défaillance" etud = Identite.get_etud(etudid) formsemestre: FormSemestre = FormSemestre.query.filter_by( id=formsemestre_id, dept_id=g.scodoc_dept_id ).first_or_404() # check lock if not formsemestre.etat: raise ScoValueError("Modification impossible: semestre verrouille") # verif if formsemestre_id not in (inscr.formsemestre_id for inscr in etud.inscriptions()): raise ScoValueError("étudiant non inscrit dans ce semestre !") if etud.inscription_etat(formsemestre_id) != etat_current: raise ScoValueError(f"etudiant non {operation_name} !") if not dialog_confirmed: return scu.confirm_dialog( f"

Confirmer l'annulation de la {operation_name} ?

", dest_url="", cancel_url=url_for( "scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid ), parameters={"etudid": etudid, "formsemestre_id": formsemestre_id}, ) # inscr = next( inscr for inscr in etud.inscriptions() if inscr.formsemestre_id == formsemestre_id ) inscr.etat = etat_new db.session.add(inscr) Scolog.logdb(method=operation_method, etudid=etudid) # Efface les évènements for event in ScolarEvent.query.filter_by( etudid=etudid, formsemestre_id=formsemestre_id, event_type=event_type ): db.session.delete(event) db.session.commit() sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id) flash(f"{operation_name} annulée.") return flask.redirect( url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid) ) @bp.route("/etudident_create_form", methods=["GET", "POST"]) @scodoc @permission_required(Permission.EtudInscrit) @scodoc7func def etudident_create_form(): "formulaire creation individuelle etudiant" return _etudident_create_or_edit_form(edit=False) @bp.route("/etudident_edit_form", methods=["GET", "POST"]) @scodoc @permission_required(Permission.EtudInscrit) @scodoc7func def etudident_edit_form(): "formulaire edition individuelle etudiant" if not current_user.has_permission(Permission.ViewEtudData): raise ScoPermissionDenied() return _etudident_create_or_edit_form(edit=True) def _validate_date_naissance(val: str, field) -> bool: "vrai si date saisie valide (peut être vide)" if not val: return True try: date_naissance = scu.convert_fr_date(val) except ScoValueError: return False return date_naissance < datetime.datetime.now() def _etudident_create_or_edit_form(edit): "Le formulaire HTML" H = [html_sco_header.sco_header()] F = html_sco_header.sco_footer() vals = scu.get_request_args() etudid = vals.get("etudid", None) cnx = ndb.GetDBConnexion() descr = [] if not edit: # creation nouvel etudiant initvalues = {} submitlabel = "Ajouter cet étudiant" H.append( """

Création d'un étudiant

En général, il est recommandé d'importer les étudiants depuis Apogée ou via un fichier Excel (menu Inscriptions dans le semestre).

N'utilisez ce formulaire au cas par cas que pour les cas particuliers ou si votre établissement n'utilise pas d'autre logiciel de gestion des inscriptions.

L'étudiant créé ne sera pas inscrit. Pensez à l'inscrire dans un semestre !

""" ) else: # edition donnees d'un etudiant existant # setup form init values if not etudid: raise ValueError("missing etudid parameter") etud_o: Identite = Identite.get_etud(etudid) descr.append(("etudid", {"default": etudid, "input_type": "hidden"})) H.append(f"""

Modification des données de {etud_o.html_link_fiche()}

""") initvalues = sco_etud.etudident_list(cnx, {"etudid": etudid}) assert len(initvalues) == 1 initvalues = initvalues[0] submitlabel = "Modifier les données" vals = scu.get_request_args() nom = vals.get("nom", None) if nom is None: nom = initvalues.get("nom", None) if nom is None: infos = [] else: prenom = vals.get("prenom", "") if vals.get("tf_submitted", False) and not prenom: prenom = initvalues.get("prenom", "") infos = sco_portal_apogee.get_infos_apogee(nom, prenom) if infos: formatted_infos = [ """
    """ ] nanswers = len(infos) nmax = 10 # nb max de reponse montrées infos = infos[:nmax] for i in infos: formatted_infos.append("
    • ") for k in i.keys(): if k != "nip": item = "
    • %s : %s
    • " % (k, i[k]) else: item = ( '
    • %s : %s
    • ' % (k, i[k], i[k]) ) formatted_infos.append(item) formatted_infos.append("
  1. ") formatted_infos.append("
") m = "%d étudiants trouvés" % nanswers if len(infos) != nanswers: m += " (%d montrés)" % len(infos) A = """
Informations Apogée

%s

%s
""" % ( m, "\n".join(formatted_infos), ) else: A = """

Pas d'informations d'Apogée

""" require_ine = sco_preferences.get_preference("always_require_ine") descr += [ ("adm_id", {"input_type": "hidden"}), ("nom", {"size": 25, "title": "Nom", "allow_null": False}), ("nom_usuel", {"size": 25, "title": "Nom usuel", "allow_null": True}), ( "prenom", { "size": 25, "title": "Prénom", "allow_null": scu.CONFIG.ALLOW_NULL_PRENOM, }, ), ( "civilite", { "input_type": "menu", "labels": ["Homme", "Femme", "Autre/neutre"], "allowed_values": ["M", "F", "X"], "title": "Civilité", }, ), ( "prenom_etat_civil", { "size": 25, "title": "Prénom (état-civil)", "allow_null": True, "explanation": "Si précisé, remplace le prénom d'usage dans les documents officiels", }, ), ( "civilite_etat_civil", { "input_type": "menu", "labels": ["(identique à civilité)", "Homme", "Femme", "Autre/neutre"], "allowed_values": ["", "M", "F", "X"], "title": "Civilité (état-civil)", "explanation": "Si précisé: remplace la civilité d'usage dans les documents officiels", }, ), ( "date_naissance", { "title": "Date de naissance", "input_type": "date", "explanation": "j/m/a", "validator": _validate_date_naissance, }, ), ("lieu_naissance", {"title": "Lieu de naissance", "size": 32}), ("dept_naissance", {"title": "Département de naissance", "size": 5}), ("nationalite", {"size": 25, "title": "Nationalité"}), ( "statut", { "size": 25, "title": "Statut", "explanation": '("salarie", ...) inutilisé par ScoDoc', }, ), ( "boursier", { "input_type": "boolcheckbox", "labels": ["non", "oui"], "title": "Boursier ?", "explanation": "actuellement", }, ), ( "annee", { "size": 5, "title": "Année admission IUT", "type": "int", "allow_null": False, "explanation": "année 1ere inscription (obligatoire)", }, ), # ("sep", {"input_type": "separator", "title": "Scolarité antérieure:"}), ("bac", {"size": 32, "explanation": "série du bac (S, STI, STT, ...)"}), ( "specialite", { "size": 25, "title": "Spécialité", "explanation": "spécialité bac: SVT M, GENIE ELECTRONIQUE, ...", }, ), ( "annee_bac", { "size": 5, "title": "Année bac", "type": "int", "min_value": 1945, "max_value": datetime.date.today().year + 1, "explanation": "année obtention du bac", }, ), ( "math", { "size": 3, "title": "Note de mathématiques", "explanation": "note sur 20 en terminale", }, ), ( "physique", { "size": 3, "title": "Note de physique", "explanation": "note sur 20 en terminale", }, ), ( "anglais", { "size": 3, "title": "Note d'anglais", "explanation": "note sur 20 en terminale", }, ), ( "francais", { "size": 3, "title": "Note de français", "explanation": "note sur 20 obtenue au bac", }, ), ( "type_admission", { "input_type": "menu", "title": "Voie d'admission", "allowed_values": scu.TYPES_ADMISSION, }, ), ( "boursier_prec", { "input_type": "boolcheckbox", "labels": ["non", "oui"], "title": "Boursier ?", "explanation": "dans le cycle précédent (lycée)", }, ), ( "rang", { "size": 1, "type": "int", "title": "Position établissement", "explanation": "rang de notre établissement dans les voeux du candidat (si connu)", }, ), ( "qualite", { "size": 3, "type": "float", "title": "Qualité", "explanation": "Note de qualité attribuée au dossier (par le jury d'adm.)", }, ), ( "decision", { "input_type": "menu", "title": "Décision", "allowed_values": [ "ADMIS", "ATTENTE 1", "ATTENTE 2", "ATTENTE 3", "REFUS", "?", ], }, ), ( "score", { "size": 3, "type": "float", "title": "Score", "explanation": "score calculé lors de l'admission", }, ), ( "classement", { "size": 3, "type": "int", "title": "Classement", "explanation": "Classement par le jury d'admission (de 1 à N)", }, ), ("apb_groupe", {"size": 15, "title": "Groupe APB ou PS"}), ( "apb_classement_gr", { "size": 3, "type": "int", "title": "Classement", "explanation": "Classement par le jury dans le groupe ABP ou PS (de 1 à Ng)", }, ), ("rapporteur", {"size": 50, "title": "Enseignant rapporteur"}), ( "commentaire", { "input_type": "textarea", "rows": 4, "cols": 50, "title": "Note du rapporteur", }, ), ("nomlycee", {"size": 20, "title": "Lycée d'origine"}), ("villelycee", {"size": 15, "title": "Commune du lycée"}), ("codepostallycee", {"size": 15, "title": "Code Postal lycée"}), ( "codelycee", { "size": 15, "title": "Code Lycée", "explanation": "Code national établissement du lycée ou établissement d'origine", }, ), ("sep", {"input_type": "separator", "title": "Codes Apogée: (optionnels)"}), ( "code_nip", { "size": 25, "title": "Numéro NIP", "allow_null": True, "explanation": "numéro identité étudiant (Apogée)", }, ), ( "code_ine", { "size": 25, "title": "Numéro INE", "allow_null": not require_ine, "explanation": "numéro INE", }, ), ( "dont_check_homonyms", { "title": "Autoriser les homonymes", "input_type": "boolcheckbox", "explanation": "ne vérifie pas les noms et prénoms proches", }, ), ] initvalues["dont_check_homonyms"] = False tf = TrivialFormulator( request.base_url, scu.get_request_args(), descr, submitlabel=submitlabel, cancelbutton="Re-interroger Apogee", initvalues=initvalues, ) if tf[0] == 0: return "\n".join(H) + tf[1] + "

" + A + F elif tf[0] == -1: return "\n".join(H) + tf[1] + "

" + A + F # return '\n'.join(H) + '

annulation

' + F else: # form submission if edit: etudid = tf[2]["etudid"] else: etudid = None ok, homonyms = sco_etud.check_nom_prenom_homonyms( nom=tf[2]["nom"], prenom=tf[2]["prenom"], etudid=etudid ) nb_homonyms = len(homonyms) if not ok: return ( "\n".join(H) + tf_error_message("Nom ou prénom invalide") + tf[1] + "

" + A + F ) if not tf[2]["dont_check_homonyms"] and nb_homonyms > 0: homonyms_html = f"""

Homonymes (dans tous les départements)
  • {'
  • '.join( [ '' + e.departement.acronym + "" + e.html_link_fiche() for e in homonyms ])}
""" return ( "\n".join(H) + tf_error_message( """Attention: il y a déjà un étudiant portant des noms et prénoms proches (voir liste en bas de page). Vous pouvez forcer la présence d'un homonyme en cochant "autoriser les homonymes" en bas du formulaire. """ ) + tf[1] + "

" + A + homonyms_html + F ) tf[2]["date_naissance"] = ( scu.convert_fr_date(tf[2]["date_naissance"]) if tf[2]["date_naissance"] else None ) if not edit: etud = sco_etud.create_etud(cnx, args=tf[2]) etudid = etud["etudid"] else: # modif d'un etudiant etud_o.from_dict(tf[2]) admission = etud_o.admission if admission is None: # ? ne devrait pas arriver mais... admission = Admission() etud_o.admission = admission admission.from_dict(tf[2]) db.session.commit() etud = sco_etud.etudident_list(cnx, {"etudid": etud_o.id})[0] sco_etud.fill_etuds_info([etud]) # Inval semesters with this student: to_inval = [s["formsemestre_id"] for s in etud["sems"]] for formsemestre_id in to_inval: sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id) # return flask.redirect( url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid) ) @bp.route("/etud_copy_in_other_dept/", methods=["GET", "POST"]) @scodoc @permission_required( Permission.ScoView ) # il faut aussi EtudInscrit dans le nouveau dept def etud_copy_in_other_dept(etudid: int): """Crée une copie de l'étudiant (avec ses adresses et codes) dans un autre département et l'inscrit à un formsemestre """ etud = Identite.get_etud(etudid) if request.method == "POST": action = request.form.get("action") if action == "cancel": return flask.redirect( url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id) ) try: formsemestre_id = int(request.form.get("formsemestre_id")) except ValueError: log("etud_copy_in_other_dept: invalid formsemestre_id") abort(404, description="formsemestre_id invalide") formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) if not current_user.has_permission( Permission.EtudInscrit, formsemestre.departement.acronym ): raise ScoPermissionDenied("non autorisé") new_etud = etud.clone(new_dept_id=formsemestre.dept_id) db.session.commit() # Attention: change le département pour opérer dans le nouveau # avec les anciennes fonctions ScoDoc7 orig_dept = g.scodoc_dept try: app.set_sco_dept(formsemestre.departement.acronym, open_cnx=False) sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules( formsemestre.id, new_etud.id, method="etud_copy_in_other_dept", dept_id=formsemestre.dept_id, ) finally: app.set_sco_dept(orig_dept, open_cnx=False) flash(f"Etudiant dupliqué et inscrit en {formsemestre.departement.acronym}") # Attention, ce redirect change de département ! return flask.redirect( url_for( "scolar.fiche_etud", scodoc_dept=formsemestre.departement.acronym, etudid=new_etud.id, ) ) departements = { dept.id: dept for dept in Departement.query.order_by(Departement.acronym) if current_user.has_permission(Permission.EtudInscrit, dept.acronym) and dept.id != etud.dept_id } formsemestres_by_dept = { dept.id: dept.formsemestres.filter_by(etat=True) .filter(FormSemestre.modalite != "EXT") .order_by(FormSemestre.date_debut, FormSemestre.semestre_id) .all() for dept in departements.values() } return render_template( "scolar/etud_copy_in_other_dept.j2", departements=departements, etud=etud, formsemestres_by_dept=formsemestres_by_dept, ) @bp.route("/etudident_delete/", methods=["GET", "POST"]) @scodoc @permission_required(Permission.EtudInscrit) @scodoc7func def etudident_delete(etudid: int = -1, dialog_confirmed=False): "Delete a student" etud = Identite.get_etud(etudid) if not dialog_confirmed: return scu.confirm_dialog( f"""

Confirmer la suppression de l'étudiant {etud.nomprenom} ?

Prenez le temps de vérifier que vous devez vraiment supprimer cet étudiant !

Cette opération irréversible efface toute trace de l'étudiant: inscriptions, notes, absences... dans tous les semestres qu'il a fréquenté.

Dans la plupart des cas, vous avez seulement besoin de le désinscrire d'un semestre ! (pour cela, passez par sa fiche, menu associé au semestre)

Vérifier la fiche de {etud.nomprenom}

""", dest_url="", cancel_url=url_for( "scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid ), OK="Supprimer définitivement cet étudiant", parameters={"etudid": etudid}, ) log(f"etudident_delete: {etud}") formsemestre_ids_to_inval = [ ins.formsemestre_id for ins in etud.formsemestre_inscriptions ] # delete in all tables ! # c'est l'ancienne façon de gérer les cascades dans notre pseudo-ORM :) tables = [ "notes_appreciations", "scolar_autorisation_inscription", "scolar_formsemestre_validation", "apc_validation_rcue", "apc_validation_annee", "scolar_events", "notes_notes_log", "notes_notes", "notes_moduleimpl_inscription", "notes_formsemestre_inscription", "group_membership", "etud_annotations", "scolog", "adresse", "absences", "absences_notifications", "billet_absence", ] for table in tables: db.session.execute( sa.text(f"""delete from {table} where etudid=:etudid"""), {"etudid": etudid} ) db.session.delete(etud) db.session.commit() # Inval semestres où il était inscrit: for formsemestre_id in formsemestre_ids_to_inval: sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id) flash("Étudiant supprimé !") return flask.redirect(url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)) @bp.route("/check_group_apogee") @scodoc @permission_required(Permission.EtudInscrit) @scodoc7func def check_group_apogee(group_id, etat=None, fix=False, fixmail=False): """Verification des codes Apogee et mail de tout un groupe. Si fix == True, change les codes avec Apogée. XXX A re-écrire pour API 2: prendre liste dans l'étape et vérifier à partir de cela. """ etat = etat or None members, group, _, sem, _ = sco_groups.get_group_infos(group_id, etat=etat) formsemestre_id = group["formsemestre_id"] title = f"""Étudiants du {group["group_name"] or "semestre"}""" cnx = ndb.GetDBConnexion() H = [ f"""

{title}

""" ] nerrs = 0 # nombre d'anomalies détectées nfix = 0 # nb codes changes nmailmissing = 0 # nb etuds sans mail for t in members: nom, nom_usuel, prenom, etudid, email, code_nip = ( t["nom"], t["nom_usuel"], t["prenom"], t["etudid"], t["email"], t["code_nip"], ) infos = sco_portal_apogee.get_infos_apogee(nom, prenom) if not infos: info_apogee = ( 'Pas d\'information (Modifier identité)' % etudid ) nerrs += 1 else: if len(infos) == 1: nip_apogee = infos[0]["nip"] if code_nip != nip_apogee: if fix: # Update database sco_etud.identite_edit( cnx, args={"etudid": etudid, "code_nip": nip_apogee}, ) info_apogee = ( 'copié %s' % nip_apogee ) nfix += 1 else: info_apogee = '%s' % nip_apogee nerrs += 1 else: info_apogee = "ok" else: info_apogee = ( '%d correspondances (Choisir)' % (len(infos), etudid) ) nerrs += 1 # check mail if email: mailstat = "ok" else: if fixmail and len(infos) == 1 and "mail" in infos[0]: mail_apogee = infos[0]["mail"] adrs = sco_etud.adresse_list(cnx, {"etudid": etudid}) if adrs: adr = adrs[0] # modif adr existante args = {"adresse_id": adr["adresse_id"], "email": mail_apogee} sco_etud.adresse_edit(cnx, args=args, disable_notify=True) else: # creation adresse args = {"etudid": etudid, "email": mail_apogee} sco_etud.adresse_create(cnx, args=args) mailstat = 'copié' else: mailstat = "inconnu" nmailmissing += 1 H.append( '' % ( url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid), nom, nom_usuel, prenom, mailstat, code_nip, info_apogee, ) ) H.append("
NomNom usuelPrénomMail NIP (ScoDoc)Apogée
%s%s%s%s%s%s
") H.append("
    ") if nfix: H.append("
  • %d codes modifiés
  • " % nfix) H.append("
  • Codes NIP: %d anomalies détectées
  • " % nerrs) H.append("
  • Adresse mail: %d étudiants sans adresse
  • " % nmailmissing) H.append("
") H.append( """

Retour au semestre """ % ( request.base_url, formsemestre_id, scu.strnone(group_id), scu.strnone(etat), formsemestre_id, ) ) H.append( """

Retour au semestre """ % ( request.base_url, formsemestre_id, scu.strnone(group_id), scu.strnone(etat), formsemestre_id, ) ) return render_template("sco_page.j2", title=title, content="\n".join(H)) @bp.route("/export_etudiants_courants") @scodoc @permission_required(Permission.ScoView) def export_etudiants_courants(): """Table export de tous les étudiants des formsemestres en cours.""" fmt = request.args.get("fmt", "html") departement = db.session.get(Departement, g.scodoc_dept_id) if not departement: raise ScoValueError("département invalide") formsemestres = FormSemestre.get_dept_formsemestres_courants(departement).all() if not formsemestres: raise ScoValueError("aucun semestre courant !") table = list_etuds.table_etudiants_courants(formsemestres) if fmt.startswith("xls"): return scu.send_file( table.excel(), f"""{formsemestres[0].departement.acronym}-etudiants-{ datetime.datetime.now().strftime("%Y-%m-%dT%Hh%M")}""", scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE, ) elif fmt == "html": return render_template( "scolar/export_etudiants_courants.j2", sco=ScoData(), table=table ) else: raise ScoValueError("invalid fmt value") @bp.route("/form_students_import_excel", methods=["GET", "POST"]) @scodoc @permission_required(Permission.EtudInscrit) @scodoc7func 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( "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 = [ html_sco_header.sco_header(page_title="Import etudiants"), """

Téléchargement d\'une nouvelle liste d\'etudiants

A utiliser pour importer de nouveaux étudiants (typiquement au premier semestre).

Si les étudiants à inscrire sont déjà dans un autre semestre, utiliser le menu "Inscriptions (passage des étudiants) depuis d'autres semestres à partir du semestre destination.

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 "Synchroniser avec étape Apogée".

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.

""", ] # ' if sem: H.append( """

Les étudiants importés seront inscrits dans le semestre %s

""" % sem["titremois"] ) else: H.append( f"""

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). Cliquez ici pour afficher les codes

""" ) H.append("""
  1. """) if formsemestre_id: H.append( """ """ ) else: H.append("""""") H.append( """Obtenir la feuille excel à remplir
  2. """ ) F = html_sco_header.sco_footer() tf = TrivialFormulator( request.base_url, scu.get_request_args(), ( ( "csvfile", {"title": "Fichier Excel:", "input_type": "file", "size": 40}, ), ( "check_homonyms", { "title": "Vérifier les homonymes", "input_type": "boolcheckbox", "explanation": "arrète l'importation si plus de 10% d'homonymes", }, ), ( "require_ine", { "title": "Importer INE", "input_type": "boolcheckbox", "explanation": "n'importe QUE les étudiants avec nouveau code INE", }, ), ("formsemestre_id", {"input_type": "hidden"}), ), initvalues={"check_homonyms": True, "require_ine": False}, submitlabel="Télécharger", ) S = [ """

    Le fichier Excel décrivant les étudiants doit comporter les colonnes suivantes.

    Les colonnes peuvent être placées dans n'importe quel ordre, mais le titre exact (tel que ci-dessous) doit être sur la première ligne.

    Les champs avec un astérisque (*) doivent être présents (nulls non autorisés).

    """ ] for t in sco_import_etuds.sco_import_format( with_codesemestre=(formsemestre_id is None) ): if int(t[3]): ast = "" else: ast = "*" S.append( "" % (t[0], t[1], t[4], ast) ) if tf[0] == 0: return "\n".join(H) + tf[1] + "" + "\n".join(S) + F elif 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"], ) @bp.route("/import_generate_excel_sample") @scodoc @permission_required(Permission.EtudInscrit) @scodoc7func def import_generate_excel_sample(with_codesemestre="1"): "une feuille excel pour importation etudiants" if with_codesemestre: with_codesemestre = int(with_codesemestre) else: with_codesemestre = 0 fmt = sco_import_etuds.sco_import_format() data = sco_import_etuds.sco_import_generate_excel_sample( fmt, with_codesemestre, exclude_cols=["photo_filename"] ) return scu.send_file( data, "ImportEtudiants", scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE ) # --- Données admission @bp.route("/import_generate_admission_sample") @scodoc @permission_required(Permission.ScoView) @scodoc7func def import_generate_admission_sample(formsemestre_id): "une feuille excel pour importation données admissions" group = sco_groups.get_group(sco_groups.get_default_group(formsemestre_id)) fmt = sco_import_etuds.sco_import_format() data = sco_import_etuds.sco_import_generate_excel_sample( fmt, only_tables=["identite", "admissions", "adresse"], exclude_cols=["nationalite", "foto", "photo_filename"], group_ids=[group["group_id"]], ) return scu.send_file( data, "AdmissionEtudiants", scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE ) # return sco_excel.send_excel_file(data, "AdmissionEtudiants" + scu.XLSX_SUFFIX) # --- Données admission depuis fichier excel (version nov 2016) @bp.route("/form_students_import_infos_admissions", methods=["GET", "POST"]) @scodoc @permission_required(Permission.ScoView) @scodoc7func def form_students_import_infos_admissions(formsemestre_id=None): "formulaire import xls" authuser = current_user F = html_sco_header.sco_footer() if not authuser.has_permission(Permission.EtudInscrit): # autorise juste l'export H = [ html_sco_header.sco_header( page_title="Export données admissions (Parcoursup ou autre)", ), f"""

    Téléchargement des informations sur l'admission des étudiants

    Exporter les informations de ScoDoc (classeur Excel) (ce fichier peut être ré-importé après d'éventuelles modifications)

    Vous n'avez pas le droit d'importer les données

    """, ] return "\n".join(H) + F # On a le droit d'importer: H = [ html_sco_header.sco_header(page_title="Import données admissions Parcoursup"), f"""

    Téléchargement des informations sur l'admission des étudiants depuis feuilles import Parcoursup

    A utiliser pour renseigner les informations sur l'origine des étudiants (lycées, bac, etc). Ces informations sont facultatives mais souvent utiles pour mieux connaitre les étudiants et aussi pour effectuer des statistiques (résultats suivant le type de bac...). Les données sont affichées sur les fiches individuelles des étudiants.

    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.

    Seules les données "admission" seront modifiées (et pas l'identité de l'étudiant).

    Les colonnes "nom" et "prenom" sont requises, ou bien une colonne "etudid" si la case "Utiliser l'identifiant d'étudiant ScoDoc" est cochée.

    Avant d'importer vos données, il est recommandé d'enregistrer les informations actuelles: exporter les données actuelles de ScoDoc (ce fichier peut être ré-importé après d'éventuelles modifications)

    """, ] tf = TrivialFormulator( request.base_url, scu.get_request_args(), ( ( "csvfile", {"title": "Fichier Excel:", "input_type": "file", "size": 40}, ), ( "use_etudid", { "input_type": "boolcheckbox", "title": "Utiliser l'identifiant d'étudiant ScoDoc (etudid)", "explanation": """si cochée, utilise le code pour retrouver dans ScoDoc les étudiants du fichier excel. Sinon, utilise les noms/prénoms.""", }, ), ( "type_admission", { "title": "Type d'admission", "explanation": "sera attribué aux étudiants modifiés par cet import n'ayant pas déjà un type", "input_type": "menu", "allowed_values": scu.TYPES_ADMISSION, }, ), ("formsemestre_id", {"input_type": "hidden"}), ), submitlabel="Télécharger", ) help_text = ( """

    Les colonnes importables par cette fonction sont indiquées dans la table ci-dessous. Seule la première feuille du classeur sera utilisée.

    """ + sco_import_etuds.adm_table_description_format().html() + """
    """ ) if tf[0] == 0: return "\n".join(H) + tf[1] + help_text + F elif tf[0] == -1: return flask.redirect( url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, ) ) else: return sco_import_etuds.students_import_admission( tf[2]["csvfile"], type_admission=tf[2]["type_admission"], formsemestre_id=formsemestre_id, use_etudid=tf[2]["use_etudid"], ) @bp.route("/formsemestre_import_etud_admission") @scodoc @permission_required(Permission.EtudChangeAdr) @scodoc7func def formsemestre_import_etud_admission( formsemestre_id=None, import_email=True, tous_courants=False ): """Ré-importe donnees admissions par synchro Portail Apogée. Si tous_courants, le fait pour tous les formsemestres courants du département """ if tous_courants: departement = db.session.get(Departement, g.scodoc_dept_id) formsemestres = FormSemestre.get_dept_formsemestres_courants(departement) else: formsemestres = [FormSemestre.get_formsemestre(formsemestre_id)] diag_by_sem = {} for formsemestre in formsemestres: ( etuds_no_nip, etuds_unknown, changed_mails, ) = sco_synchro_etuds.formsemestre_import_etud_admission( formsemestre.id, import_identite=True, import_email=import_email ) diag = "" if etuds_no_nip: diag += f"""

    Attention: étudiants sans NIP: {', '.join([e.html_link_fiche() for e in etuds_no_nip])}

    """ if etuds_unknown: diag += f"""

    Attention: étudiants inconnus du portail: {', '.join([(e.html_link_fiche() + ' (nip= ' + e.code_nip + ')') for e in etuds_unknown])}

    """ if changed_mails: diag += """

    Adresses mails modifiées:

      """ for etud, old_mail in changed_mails: diag += f"""
    • {etud.nom}: {old_mail} devient {etud.email}
    • """ diag += "
    " diag_by_sem[formsemestre.id] = diag return render_template( "sco_page.j2", title="Ré-import données admission", content=f"""

    Ré-import données admission

    Opération effectuée

    Sur le(s) semestres(s):

    • { '
    • '.join( [(s.html_link_status() + diag_by_sem[s.id]) for s in formsemestres ]) }
    """, ) sco_publish( "/photos_import_files_form", sco_trombino.photos_import_files_form, Permission.EtudChangeAdr, methods=["GET", "POST"], ) sco_publish( "/photos_generate_excel_sample", sco_trombino.photos_generate_excel_sample, Permission.EtudChangeAdr, ) # --- Statistiques @bp.route("/stat_bac") @scodoc @permission_required(Permission.ScoView) @scodoc7func def stat_bac(formsemestre_id): "Renvoie statistisques sur nb d'etudiants par bac" cnx = ndb.GetDBConnexion() ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( args={"formsemestre_id": formsemestre_id} ) Bacs = {} # type bac : nb etud for i in ins: etud = sco_etud.etudident_list(cnx, {"etudid": i["etudid"]})[0] typebac = "%(bac)s %(specialite)s" % etud Bacs[typebac] = Bacs.get(typebac, 0) + 1 return Bacs # --- Dump (assistance) @bp.route("/sco_dump_and_send_db", methods=["GET", "POST"]) @scodoc @permission_required(Permission.ScoView) @scodoc7func def sco_dump_and_send_db(message="", request_url="", traceback_str_base64=""): "Send anonymized data to supervision" r = sco_dump_db.sco_dump_and_send_db( message, request_url, traceback_str_base64=traceback_str_base64 ) status_code = r.status_code try: r_msg = r.json()["message"] except (requests.exceptions.JSONDecodeError, KeyError): r_msg = f"""Erreur: code {status_code} Merci de contacter {scu.SCO_DEV_MAIL} """ H = [html_sco_header.sco_header(page_title="Assistance technique")] if status_code == requests.codes.OK: # pylint: disable=no-member H.append(f"""

    Opération effectuée.

    {r_msg}

    """) else: H.append(f"""

    {r_msg}

    """) flash("Données envoyées au serveur d'assistance") return "\n".join(H) + html_sco_header.sco_footer() # --- Report form (assistance) @bp.route("/sco_bug_report", methods=["GET", "POST"]) @scodoc @permission_required(Permission.ScoView) def sco_bug_report_form(): "Formulaire de création d'un ticket d'assistance" form = CreateBugReport() if request.method == "POST" and form.cancel.data: # cancel button return flask.redirect(url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)) if form.validate_on_submit(): r = sco_bug_report.sco_bug_report( form.title.data, form.message.data, form.etab.data, form.include_dump.data ) status_code = r.status_code try: r_msg = r.json()["message"] except (requests.exceptions.JSONDecodeError, KeyError): log(f"sco_bug_report: error {status_code}") r_msg = f"""Erreur: code {status_code} Merci de contacter {scu.SCO_DEV_MAIL} """ H = [html_sco_header.sco_header(page_title="Assistance technique")] if r.status_code >= 200 and r.status_code < 300: H.append(f"""

    Opération effectuée.

    {r_msg}

    """) else: H.append(f"""

    {r_msg}

    """) return "\n".join(H) + html_sco_header.sco_footer() return render_template( "sco_bug_report.j2", form=form, )
    AttributTypeDescription
    %s%s%s%s