# -*- mode: python -*- # -*- 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 notes: issu de ScoDoc7 / ZNotes.py Emmanuel Viennet, 2021 """ from operator import itemgetter import time import flask from flask import flash, redirect, render_template, url_for from flask import g, request from flask_login import current_user from app import db from app import models from app.auth.models import User from app.but import ( apc_edit_ue, bulletin_but_court, # ne pas enlever: ajoute des routes ! cursus_but, jury_edit_manual, jury_but, jury_but_pv, jury_but_validation_auto, jury_but_view, ) from app.but.forms import jury_but_forms from app.comp import jury, res_sem from app.comp.res_compat import NotesTableCompat from app.models import ( ApcNiveau, Assiduite, BulAppreciations, DispenseUE, Evaluation, Formation, FormSemestre, FormSemestreInscription, FormSemestreUEComputationExpr, Identite, Module, ModuleImpl, ScolarAutorisationInscription, ScolarNews, Scolog, ScoDocSiteConfig, UniteEns, ) from app.scodoc.sco_exceptions import ScoFormationConflict, ScoPermissionDenied from app.views import notes_bp as bp from app.decorators import ( scodoc, scodoc7func, permission_required, permission_required_compat_scodoc7, ) # --------------- from app.scodoc import sco_bulletins_json, sco_utils as scu from app.scodoc import notesdb as ndb from app import log, send_scodoc_alarm from app.scodoc.scolog import logdb from app.scodoc.sco_exceptions import ( AccessDenied, ScoValueError, ScoInvalidIdType, ) from app.scodoc import html_sco_header from app.pe import pe_view from app.scodoc import sco_apogee_compare from app.scodoc import sco_archives from app.scodoc import sco_archives_formsemestre from app.scodoc import sco_assiduites from app.scodoc import sco_bulletins from app.scodoc import sco_bulletins_pdf from app.scodoc import sco_cache from app.scodoc import sco_cost_formation from app.scodoc import sco_debouche from app.scodoc import sco_edit_apc from app.scodoc import sco_edit_formation from app.scodoc import sco_edit_matiere from app.scodoc import sco_edit_module from app.scodoc import sco_edit_ue from app.scodoc import sco_etape_apogee_view from app.scodoc import sco_etud from app.scodoc import sco_evaluations from app.scodoc import sco_evaluation_check_abs from app.scodoc import sco_evaluation_db from app.scodoc import sco_evaluation_edit from app.scodoc import sco_evaluation_recap from app.scodoc import sco_export_results from app.scodoc import sco_formations from app.scodoc import sco_formation_recap from app.scodoc import sco_formation_versions from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre_custommenu from app.scodoc import sco_formsemestre_edit from app.scodoc import sco_formsemestre_exterieurs from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_status from app.scodoc import sco_formsemestre_validation from app.scodoc import sco_inscr_passage from app.scodoc import sco_liste_notes from app.scodoc import sco_lycee from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl_inscriptions from app.scodoc import sco_moduleimpl_status from app.scodoc import sco_permissions_check from app.scodoc import sco_placement from app.scodoc import sco_poursuite_dut from app.scodoc import sco_preferences from app.scodoc import sco_prepajury from app.scodoc import sco_pv_forms from app.scodoc import sco_recapcomplet from app.scodoc import sco_report from app.scodoc import sco_report_but from app.scodoc import sco_saisie_notes from app.scodoc import sco_semset from app.scodoc import sco_synchro_etuds from app.scodoc import sco_tag_module from app.scodoc import sco_ue_external from app.scodoc import sco_undo_notes from app.scodoc import sco_users from app.scodoc.gen_tables import GenTable from app.scodoc.sco_pv_dict import descr_autorisations from app.scodoc.sco_permissions import Permission from app.scodoc.TrivialFormulator import TrivialFormulator from app.views import ScoData 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))) ) # -------------------------------------------------------------------- # # Notes/ methods # # -------------------------------------------------------------------- sco_publish( "/formsemestre_status", sco_formsemestre_status.formsemestre_status, Permission.ScoView, ) sco_publish( "/formsemestre_createwithmodules", sco_formsemestre_edit.formsemestre_createwithmodules, Permission.EditFormSemestre, methods=["GET", "POST"], ) # controle d'acces specifique pour dir. etud: sco_publish( "/formsemestre_editwithmodules", sco_formsemestre_edit.formsemestre_editwithmodules, Permission.ScoView, methods=["GET", "POST"], ) sco_publish( "/formsemestre_clone", sco_formsemestre_edit.formsemestre_clone, Permission.EditFormSemestre, methods=["GET", "POST"], ) sco_publish( "/formsemestre_associate_new_version", sco_formation_versions.formsemestre_associate_new_version, Permission.EditFormation, methods=["GET", "POST"], ) sco_publish( "/formsemestre_delete", sco_formsemestre_edit.formsemestre_delete, Permission.EditFormSemestre, methods=["GET", "POST"], ) sco_publish( "/formsemestre_delete2", sco_formsemestre_edit.formsemestre_delete2, Permission.EditFormSemestre, methods=["GET", "POST"], ) sco_publish( "/formsemestre_note_etuds_sans_note", sco_formsemestre_status.formsemestre_note_etuds_sans_notes, Permission.ScoView, methods=["GET", "POST"], ) sco_publish( "/formsemestre_recapcomplet", sco_recapcomplet.formsemestre_recapcomplet, Permission.ScoView, ) sco_publish( "/evaluations_recap", sco_evaluation_recap.evaluations_recap, Permission.ScoView, ) sco_publish( "/formsemestres_bulletins", sco_recapcomplet.formsemestres_bulletins, Permission.Observateur, ) sco_publish( "/moduleimpl_status", sco_moduleimpl_status.moduleimpl_status, Permission.ScoView ) sco_publish( "/formsemestre_description", sco_formsemestre_status.formsemestre_description, Permission.ScoView, ) sco_publish( "/formation_create", sco_edit_formation.formation_create, Permission.EditFormation, methods=["GET", "POST"], ) sco_publish( "/formation_delete", sco_edit_formation.formation_delete, Permission.EditFormation, methods=["GET", "POST"], ) sco_publish( "/formation_edit", sco_edit_formation.formation_edit, Permission.EditFormation, methods=["GET", "POST"], ) @bp.route("/formsemestre_bulletinetud") @scodoc @permission_required_compat_scodoc7(Permission.ScoView) @scodoc7func def formsemestre_bulletinetud( etudid=None, formsemestre_id=None, fmt=None, version="long", xml_with_decisions=False, force_publishing=False, prefer_mail_perso=False, code_nip=None, code_ine=None, ): fmt = fmt or "html" if version not in scu.BULLETINS_VERSIONS_BUT: raise ScoValueError( "formsemestre_bulletinetud: version de bulletin demandée invalide" ) if not isinstance(etudid, int): raise ScoInvalidIdType("formsemestre_bulletinetud: etudid must be an integer !") if formsemestre_id is not None and not isinstance(formsemestre_id, int): raise ScoInvalidIdType( "formsemestre_bulletinetud: formsemestre_id must be an integer !" ) formsemestre = FormSemestre.query.filter_by( formsemestre_id=formsemestre_id, dept_id=g.scodoc_dept_id ).first_or_404() if etudid: etud = Identite.get_etud(etudid) elif code_nip: etud = models.Identite.query.filter_by( code_nip=str(code_nip), dept_id=formsemestre.dept_id ).first_or_404() elif code_ine: etud = models.Identite.query.filter_by( code_ine=str(code_ine), dept_id=formsemestre.dept_id ).first_or_404() else: raise ScoValueError( "Paramètre manquant: spécifier etudid, code_nip ou code_ine" ) if version == "butcourt": return redirect( url_for( "notes.bulletin_but_pdf" if fmt == "pdf" else "notes.bulletin_but_html", scodoc_dept=g.scodoc_dept, etudid=etud.id, formsemestre_id=formsemestre_id, ) ) if fmt == "json": return sco_bulletins.get_formsemestre_bulletin_etud_json( formsemestre, etud, version=version, force_publishing=force_publishing ) if formsemestre.formation.is_apc() and fmt == "html": return render_template( "but/bulletin.j2", appreciations=BulAppreciations.get_appreciations_list( formsemestre.id, etud.id ), bul_url=url_for( "notes.formsemestre_bulletinetud", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, etudid=etud.id, fmt="json", force_publishing=1, # pour ScoDoc lui même version=version, ), can_edit_appreciations=formsemestre.est_responsable(current_user) or (current_user.has_permission(Permission.EtudInscrit)), etud=etud, formsemestre=formsemestre, inscription_courante=etud.inscription_courante(), inscription_str=etud.inscription_descr()["inscription_str"], is_apc=formsemestre.formation.is_apc(), menu_autres_operations=sco_bulletins.make_menu_autres_operations( formsemestre, etud, "notes.formsemestre_bulletinetud", version ), sco=ScoData(etud=etud), scu=scu, time=time, title=f"Bul. {etud.nom} - BUT", version=version, ) if fmt == "oldjson": fmt = "json" response = sco_bulletins.formsemestre_bulletinetud( etud, formsemestre_id=formsemestre_id, fmt=fmt, version=version, xml_with_decisions=xml_with_decisions, force_publishing=force_publishing, prefer_mail_perso=prefer_mail_perso, ) if fmt == "pdfmail": # ne renvoie rien dans ce cas (mails envoyés) return redirect( url_for( "notes.formsemestre_bulletinetud", scodoc_dept=g.scodoc_dept, etudid=etud.id, formsemestre_id=formsemestre_id, ) ) return response sco_publish( "/formsemestre_evaluations_cal", sco_evaluations.formsemestre_evaluations_cal, Permission.ScoView, ) sco_publish( "/formsemestre_evaluations_delai_correction", sco_evaluations.formsemestre_evaluations_delai_correction, Permission.ScoView, ) @bp.route("/moduleimpl_evaluation_renumber", methods=["GET", "POST"]) @scodoc @permission_required_compat_scodoc7(Permission.ScoView) @scodoc7func def moduleimpl_evaluation_renumber(moduleimpl_id): "Renumérote les évaluations, triant par date" modimpl: ModuleImpl = ( ModuleImpl.query.filter_by(id=moduleimpl_id) .join(FormSemestre) .filter_by(dept_id=g.scodoc_dept_id) .first_or_404() ) if not modimpl.can_edit_evaluation(current_user): raise ScoPermissionDenied( dest_url=url_for( "notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id, ) ) Evaluation.moduleimpl_evaluation_renumber(modimpl) # redirect to moduleimpl page: return flask.redirect( url_for( "notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id, ) ) sco_publish( "/moduleimpl_evaluation_move", sco_evaluation_db.moduleimpl_evaluation_move, Permission.ScoView, ) sco_publish( "/formsemestre_list_saisies_notes", sco_undo_notes.formsemestre_list_saisies_notes, Permission.ScoView, ) sco_publish( "/ue_create", sco_edit_ue.ue_create, Permission.EditFormation, methods=["GET", "POST"], ) sco_publish( "/ue_delete", sco_edit_ue.ue_delete, Permission.EditFormation, methods=["GET", "POST"], ) sco_publish( "/ue_edit", sco_edit_ue.ue_edit, Permission.EditFormation, methods=["GET", "POST"], ) @bp.route("/set_ue_niveau_competence", methods=["POST"]) @scodoc @permission_required(Permission.EditFormation) def set_ue_niveau_competence(): """Associe UE et niveau. Si le niveau_id est "", désassocie.""" ue_id = request.form.get("ue_id") niveau_id = request.form.get("niveau_id") if niveau_id == "": niveau_id = None ue: UniteEns = UniteEns.query.get_or_404(ue_id) niveau = None if niveau_id is None else ApcNiveau.query.get_or_404(niveau_id) try: ue.set_niveau_competence(niveau) except ScoFormationConflict: return "", 409 # conflict return "", 204 @bp.route("/get_ue_niveaux_options_html") @scodoc @permission_required(Permission.ScoView) def get_ue_niveaux_options_html(): """fragment html avec les options du menu de sélection du niveau de compétences associé à une UE """ ue_id = request.args.get("ue_id") if ue_id is None: log("WARNING: get_ue_niveaux_options_html missing ue_id arg") return "???" ue: UniteEns = UniteEns.query.get_or_404(ue_id) return apc_edit_ue.get_ue_niveaux_options_html(ue) @bp.route("/ue_list") # backward compat @bp.route("/ue_table") @scodoc @permission_required(Permission.ScoView) @scodoc7func def ue_table(formation_id=None, semestre_idx=1, msg=""): return sco_edit_ue.ue_table( formation_id=formation_id, semestre_idx=semestre_idx, msg=msg ) @bp.route("/ue_infos/<int:ue_id>") @scodoc @permission_required(Permission.ScoView) def ue_infos(ue_id): ue = UniteEns.query.get_or_404(ue_id) return sco_edit_apc.html_ue_infos(ue) @bp.route("/ue_set_internal", methods=["GET", "POST"]) @scodoc @permission_required(Permission.EditFormation) @scodoc7func def ue_set_internal(ue_id): """""" ue = db.session.get(UniteEns, ue_id) if not ue: raise ScoValueError("invalid ue_id") ue.is_external = False db.session.add(ue) db.session.commit() # Invalide les semestres de cette formation ue.formation.invalidate_cached_sems() return redirect( url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=ue.formation_id ) ) @bp.route("/ue_sharing_code") @scodoc @permission_required(Permission.ScoView) @scodoc7func def ue_sharing_code(): ue_code = request.args.get("ue_code") ue_id = request.args.get("ue_id") hide_ue_id = request.args.get("hide_ue_id") return sco_edit_ue.ue_sharing_code( ue_code=ue_code, ue_id=None if ((ue_id is None) or ue_id == "") else int(ue_id), hide_ue_id=None if ((hide_ue_id is None) or hide_ue_id == "") else int(hide_ue_id), ) sco_publish( "/edit_ue_set_code_apogee", sco_edit_ue.edit_ue_set_code_apogee, Permission.EditFormation, methods=["POST"], ) sco_publish( "/formsemestre_edit_uecoefs", sco_formsemestre_edit.formsemestre_edit_uecoefs, Permission.ScoView, methods=["GET", "POST"], ) @bp.route("/formation_table_recap") @scodoc @permission_required(Permission.ScoView) @scodoc7func def formation_table_recap(formation_id, fmt="html"): return sco_formation_recap.formation_table_recap(formation_id, fmt=fmt) sco_publish( "/export_recap_formations_annee_scolaire", sco_formation_recap.export_recap_formations_annee_scolaire, Permission.ScoView, ) sco_publish( "/formation_add_malus_modules", sco_edit_module.formation_add_malus_modules, Permission.EditFormation, ) sco_publish( "/matiere_create", sco_edit_matiere.matiere_create, Permission.EditFormation, methods=["GET", "POST"], ) sco_publish( "/matiere_delete", sco_edit_matiere.matiere_delete, Permission.EditFormation, methods=["GET", "POST"], ) sco_publish( "/matiere_edit", sco_edit_matiere.matiere_edit, Permission.EditFormation, methods=["GET", "POST"], ) sco_publish( "/module_create", sco_edit_module.module_create, Permission.EditFormation, methods=["GET", "POST"], ) sco_publish( "/module_delete", sco_edit_module.module_delete, Permission.EditFormation, methods=["GET", "POST"], ) sco_publish( "/module_edit", sco_edit_module.module_edit, Permission.EditFormation, methods=["GET", "POST"], ) sco_publish( "/edit_module_set_code_apogee", sco_edit_module.edit_module_set_code_apogee, Permission.EditFormation, methods=["GET", "POST"], ) sco_publish("/module_list", sco_edit_module.module_table, Permission.ScoView) sco_publish("/module_tag_search", sco_tag_module.module_tag_search, Permission.ScoView) @bp.route("/module_tag_set", methods=["POST"]) @scodoc @permission_required(Permission.EditFormationTags) def module_tag_set(): """Set tags on module""" module_id = int(request.form.get("module_id")) taglist = request.form.get("taglist") return sco_tag_module.module_tag_set(module_id, taglist) @bp.route("/module_clone", methods=["POST"]) @scodoc @permission_required(Permission.EditFormation) def module_clone(): """Clone existing module""" module_id = int(request.form.get("module_id")) module = Module.query.get_or_404(module_id) module2 = module.clone() db.session.add(module2) db.session.commit() flash(f"Module {module.code} dupliqué") return flask.redirect( url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, semestre_idx=module.semestre_id, formation_id=module.formation_id, ) ) # @bp.route("/") @bp.route("/index_html") @scodoc @permission_required(Permission.ScoView) @scodoc7func def index_html(): "Page accueil formations" editable = current_user.has_permission(Permission.EditFormation) H = [ html_sco_header.sco_header(page_title="Programmes formations"), """<h2>Programmes pédagogiques</h2> """, ] T = sco_formations.formation_list_table() H.append(T.html()) if editable: H.append( f""" <p class="help">Une "formation" est un programme pédagogique structuré en UE, matières et modules. Chaque semestre se réfère à une formation. La modification d'une formation affecte tous les semestres qui s'y réfèrent.</p> <ul> <li><a class="stdlink" href="formation_create" id="link-create-formation">Créer une formation</a> </li> <li><a class="stdlink" href="formation_import_xml_form">Importer une formation (xml)</a> </li> <li><a class="stdlink" href="{ url_for("notes.export_recap_formations_annee_scolaire", scodoc_dept=g.scodoc_dept, annee_scolaire=scu.annee_scolaire()-1) }">exporter les formations de l'année scolaire {scu.annee_scolaire()-1} - {scu.annee_scolaire()} </a> </li> <li><a class="stdlink" href="{ url_for("notes.export_recap_formations_annee_scolaire", scodoc_dept=g.scodoc_dept, annee_scolaire=scu.annee_scolaire()) }">exporter les formations de l'année scolaire {scu.annee_scolaire()} - {scu.annee_scolaire()+1} </a> </li> </ul> <h3>Référentiels de compétences</h3> <ul> <li><a class="stdlink" href="{ url_for('notes.refcomp_table', scodoc_dept=g.scodoc_dept) }">Liste des référentiels chargés</a> </li> </ul> """ ) H.append(html_sco_header.sco_footer()) return "\n".join(H) # -------------------------------------------------------------------- # # Notes Methods # # -------------------------------------------------------------------- # --- Formations @bp.route("/formation_export") @scodoc @permission_required(Permission.ScoView) @scodoc7func def formation_export(formation_id, export_ids=False, fmt=None, export_codes_apo=True): "Export de la formation au format indiqué (xml ou json)" return sco_formations.formation_export( formation_id, export_ids=export_ids, fmt=fmt, export_codes_apo=export_codes_apo, ) @bp.route("/formation_import_xml_form", methods=["GET", "POST"]) @scodoc @permission_required(Permission.EditFormation) @scodoc7func def formation_import_xml_form(): "form import d'une formation en XML" tf = TrivialFormulator( request.base_url, scu.get_request_args(), (("xmlfile", {"input_type": "file", "title": "Fichier XML", "size": 30}),), submitlabel="Importer", cancelbutton="Annuler", ) if tf[0] == 0: return f""" { html_sco_header.sco_header(page_title="Import d'une formation") } <h2>Import d'une formation</h2> <p>Création d'une formation (avec UE, matières, modules) à partir un fichier XML (réservé aux utilisateurs avertis). </p> <p>S'il s'agit d'une formation par compétence (BUT), assurez-vous d'avoir chargé le référentiel de compétences AVANT d'importer le fichier formation (voir <a class="stdlink" href="{ url_for("notes.refcomp_table", scodoc_dept=g.scodoc_dept) }">page des référentiels</a>). </p> { tf[1] } { html_sco_header.sco_footer() } """ elif tf[0] == -1: return flask.redirect(scu.NotesURL()) else: formation_id, _, _ = sco_formations.formation_import_xml( tf[2]["xmlfile"].read() ) return f""" { html_sco_header.sco_header(page_title="Import d'une formation") } <h2>Import effectué !</h2> <ul> <li><a class="stdlink" href="{ url_for("notes.ue_list", scodoc_dept=g.scodoc_dept, formation_id=formation_id )}">Voir la formation</a> </li> <li><a class="stdlink" href="{ url_for("notes.formation_delete", scodoc_dept=g.scodoc_dept, formation_id=formation_id )}">Supprimer cette formation</a> (en cas d'erreur, par exemple pour charger auparavant le référentiel de compétences) </li> </ul> { html_sco_header.sco_footer() } """ # sco_publish( # "/formation_create_new_version", # sco_formations.formation_create_new_version, # Permission.EditFormation, # ) # --- UE sco_publish( "/ue_list", sco_edit_ue.ue_list, Permission.ScoView, ) sco_publish("/module_move", sco_edit_formation.module_move, Permission.EditFormation) sco_publish("/ue_move", sco_edit_formation.ue_move, Permission.EditFormation) @bp.route("/ue_clone", methods=["POST"]) @scodoc @permission_required(Permission.EditFormation) def ue_clone(): """Clone existing UE""" ue_id = int(request.form.get("ue_id")) ue = UniteEns.query.get_or_404(ue_id) ue2 = ue.clone() db.session.add(ue2) db.session.commit() flash(f"UE {ue.acronyme} dupliquée") return flask.redirect( url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, semestre_idx=ue.semestre_idx, formation_id=ue.formation_id, ) ) # --- Semestres de formation @bp.route( "/formsemestre_list", methods=["GET", "POST"] ) # pour compat anciens clients PHP @scodoc @permission_required_compat_scodoc7(Permission.ScoView) @scodoc7func def formsemestre_list( fmt="json", formsemestre_id=None, formation_id=None, etape_apo=None, ): """List formsemestres in given format. kw can specify some conditions: examples: formsemestre_list( fmt='json', formation_id='F777') """ log("Warning: calling deprecated view formsemestre_list") try: formsemestre_id = int(formsemestre_id) if formsemestre_id is not None else None formation_id = int(formation_id) if formation_id is not None else None except ValueError: return scu.json_error(404, "invalid id") args = {} L = locals() for argname in ("formsemestre_id", "formation_id", "etape_apo"): if L[argname] is not None: args[argname] = L[argname] sems = sco_formsemestre.do_formsemestre_list(args=args) return scu.sendResult(sems, name="formsemestre", fmt=fmt) sco_publish( "/formsemestre_edit_options", sco_formsemestre_edit.formsemestre_edit_options, Permission.ScoView, methods=["GET", "POST"], ) @bp.route("/formsemestre_flip_lock", methods=["GET", "POST"]) @scodoc @permission_required(Permission.ScoView) # acces vérifié dans la vue @scodoc7func def formsemestre_flip_lock(formsemestre_id, dialog_confirmed=False): "Changement de l'état de verrouillage du semestre" formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) dest_url = url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id, ) if not formsemestre.est_chef_or_diretud(): raise ScoPermissionDenied("opération non autorisée", dest_url=dest_url) if not dialog_confirmed: msg = "verrouillage" if formsemestre.etat else "déverrouillage" return scu.confirm_dialog( f"<h2>Confirmer le {msg} du semestre ?</h2>", help_msg="""Les notes d'un semestre verrouillé ne peuvent plus être modifiées. Un semestre verrouillé peut cependant être déverrouillé facilement à tout moment (par son responsable ou un administrateur). <br> Le programme d'une formation qui a un semestre verrouillé ne peut plus être modifié. """, dest_url="", cancel_url=dest_url, parameters={"formsemestre_id": formsemestre_id}, ) formsemestre.flip_lock() db.session.commit() return flask.redirect(dest_url) sco_publish( "/formsemestre_change_publication_bul", sco_formsemestre_edit.formsemestre_change_publication_bul, Permission.ScoView, methods=["GET", "POST"], ) sco_publish( "/view_formsemestre_by_etape", sco_formsemestre.view_formsemestre_by_etape, Permission.ScoView, ) @bp.route("/formsemestre_custommenu_edit", methods=["GET", "POST"]) @scodoc @permission_required(Permission.ScoView) @scodoc7func def formsemestre_custommenu_edit(formsemestre_id): "Dialogue modif menu" # accessible à tous ! return sco_formsemestre_custommenu.formsemestre_custommenu_edit(formsemestre_id) # --- dialogue modif enseignants/moduleimpl @bp.route("/edit_enseignants_form", methods=["GET", "POST"]) @scodoc @permission_required(Permission.ScoView) @scodoc7func def edit_enseignants_form(moduleimpl_id): "modif liste enseignants/moduleimpl" M, sem = sco_moduleimpl.can_change_ens(moduleimpl_id) # -- header = html_sco_header.html_sem_header( 'Enseignants du <a href="moduleimpl_status?moduleimpl_id=%s">module %s</a>' % (moduleimpl_id, M["module"]["titre"]), page_title="Enseignants du module %s" % M["module"]["titre"], javascripts=["libjs/AutoSuggest.js"], cssstyles=["css/autosuggest_inquisitor.css"], bodyOnLoad="init_tf_form('')", ) footer = html_sco_header.sco_footer() # Liste des enseignants avec forme pour affichage / saisie avec suggestion userlist = sco_users.get_user_list() uid2display = {} # uid : forme pour affichage = "NOM Prenom (login)"(login)" for u in userlist: uid2display[u.id] = u.get_nomplogin() allowed_user_names = list(uid2display.values()) H = [ "<ul><li><b>%s</b> (responsable)</li>" % uid2display.get(M["responsable_id"], M["responsable_id"]) ] for ens in M["ens"]: u = db.session.get(User, ens["ens_id"]) if u: nom = u.get_nomcomplet() else: nom = "? (compte inconnu)" H.append( f""" <li>{nom} (<a class="stdlink" href="{ url_for('notes.edit_enseignants_form_delete', scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id, ens_id=ens["ens_id"]) }">supprimer</a>) </li>""" ) H.append("</ul>") F = f"""<p class="help">Les enseignants d'un module ont le droit de saisir et modifier toutes les notes des évaluations de ce module. </p> <p class="help">Pour changer le responsable du module, passez par la page "<a class="stdlink" href="{ url_for("notes.formsemestre_editwithmodules", scodoc_dept=g.scodoc_dept, formsemestre_id=M["formsemestre_id"]) }">Modification du semestre</a>", accessible uniquement au responsable de la formation (chef de département) </p> """ modform = [ ("moduleimpl_id", {"input_type": "hidden"}), ( "ens_id", { "input_type": "text_suggest", "size": 50, "title": "Ajouter un enseignant", "allowed_values": allowed_user_names, "allow_null": False, "text_suggest_options": { "script": url_for( "users.get_user_list_xml", scodoc_dept=g.scodoc_dept ) + "?", "varname": "start", "json": False, "noresults": "Valeur invalide !", "timeout": 60000, }, }, ), ] tf = TrivialFormulator( request.base_url, scu.get_request_args(), modform, submitlabel="Ajouter enseignant", cancelbutton="Annuler", ) if tf[0] == 0: return header + "\n".join(H) + tf[1] + F + footer elif tf[0] == -1: return flask.redirect( url_for( "notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id, ) ) else: ens_id = User.get_user_id_from_nomplogin(tf[2]["ens_id"]) if not ens_id: H.append( '<p class="help">Pour ajouter un enseignant, choisissez un nom dans le menu</p>' ) else: # et qu'il n'est pas deja: if ( ens_id in [x["ens_id"] for x in M["ens"]] or ens_id == M["responsable_id"] ): H.append( f"""<p class="help">Enseignant {ens_id} déjà dans la liste !</p>""" ) else: sco_moduleimpl.do_ens_create( {"moduleimpl_id": moduleimpl_id, "ens_id": ens_id} ) return flask.redirect( url_for( "notes.edit_enseignants_form", scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id, ) ) return header + "\n".join(H) + tf[1] + F + footer @bp.route("/edit_moduleimpl_resp", methods=["GET", "POST"]) @scodoc @permission_required(Permission.ScoView) @scodoc7func def edit_moduleimpl_resp(moduleimpl_id: int): """Changement d'un enseignant responsable de module Accessible par Admin et dir des etud si flag resp_can_change_ens """ modimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id) modimpl.can_change_ens_by(current_user, raise_exc=True) # access control H = [ html_sco_header.html_sem_header( f"""Modification du responsable du <a href="{ url_for("notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id) }">module {modimpl.module.titre or ""}</a>""", javascripts=["libjs/AutoSuggest.js"], cssstyles=["css/autosuggest_inquisitor.css"], bodyOnLoad="init_tf_form('')", ) ] help_str = """<p class="help">Taper le début du nom de l'enseignant.</p>""" # Liste des enseignants avec forme pour affichage / saisie avec suggestion userlist = [sco_users.user_info(user=u) for u in sco_users.get_user_list()] uid2display = {} # uid : forme pour affichage = "NOM Prenom (login)" for u in userlist: uid2display[u["id"]] = u["nomplogin"] allowed_user_names = list(uid2display.values()) initvalues = modimpl.to_dict(with_module=False) initvalues["responsable_id"] = uid2display.get( modimpl.responsable_id, modimpl.responsable_id ) form = [ ("moduleimpl_id", {"input_type": "hidden"}), ( "responsable_id", { "input_type": "text_suggest", "size": 50, "title": "Responsable du module", "allowed_values": allowed_user_names, "allow_null": False, "text_suggest_options": { "script": url_for( "users.get_user_list_xml", scodoc_dept=g.scodoc_dept ) + "?", "varname": "start", "json": False, "noresults": "Valeur invalide !", "timeout": 60000, }, }, ), ] tf = TrivialFormulator( request.base_url, scu.get_request_args(), form, submitlabel="Changer responsable", cancelbutton="Annuler", initvalues=initvalues, ) if tf[0] == 0: return "\n".join(H) + tf[1] + help_str + html_sco_header.sco_footer() elif tf[0] == -1: return flask.redirect( url_for( "notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id, ) ) else: responsable_id = User.get_user_id_from_nomplogin(tf[2]["responsable_id"]) if not responsable_id: # presque impossible: tf verifie les valeurs (mais qui peuvent changer entre temps) return flask.redirect( url_for( "notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id, ) ) modimpl.responsable_id = responsable_id db.session.add(modimpl) db.session.commit() flash("Responsable modifié") return flask.redirect( url_for( "notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id, ) ) @bp.route("/view_module_abs") @scodoc @permission_required(Permission.ScoView) @scodoc7func def view_module_abs(moduleimpl_id, fmt="html"): """Visualisation des absences a un module""" modimpl: ModuleImpl = ( ModuleImpl.query.filter_by(id=moduleimpl_id) .join(FormSemestre) .filter_by(dept_id=g.scodoc_dept_id) ).first_or_404() debut_sem = modimpl.formsemestre.date_debut fin_sem = modimpl.formsemestre.date_fin inscrits: list[Identite] = sorted( [i.etud for i in modimpl.inscriptions], key=lambda e: e.sort_key ) rows = [] for etud in inscrits: nb_abs, nb_abs_just = sco_assiduites.formsemestre_get_assiduites_count( etud.id, modimpl.formsemestre, moduleimpl_id=modimpl.id ) rows.append( { "nomprenom": etud.nomprenom, "just": nb_abs_just, "nojust": nb_abs - nb_abs_just, "total": nb_abs, "_nomprenom_target": url_for( "scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id ), } ) H = [ html_sco_header.html_sem_header( f"""Absences du <a href="{ url_for("notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id )}">module {modimpl.module.titre_str()}</a>""", page_title=f"Absences du module {modimpl.module.titre_str()}", ) ] if not rows and fmt == "html": return ( "\n".join(H) + "<p>Aucune absence signalée</p>" + html_sco_header.sco_footer() ) tab = GenTable( titles={ "nomprenom": "Nom", "just": "Just.", "nojust": "Non Just.", "total": "Total", }, columns_ids=("nomprenom", "just", "nojust", "total"), rows=rows, html_class="table_leftalign", base_url=f"{request.base_url}?moduleimpl_id={moduleimpl_id}", filename="absmodule_" + scu.make_filename(modimpl.module.titre_str()), caption=f"Absences dans le module {modimpl.module.titre_str()}", preferences=sco_preferences.SemPreferences(), ) if fmt != "html": return tab.make_page(fmt=fmt) return "\n".join(H) + tab.html() + html_sco_header.sco_footer() @bp.route("/delete_ue_expr/<int:formsemestre_id>/<int:ue_id>", methods=["GET", "POST"]) @scodoc def delete_ue_expr(formsemestre_id: int, ue_id: int): """Efface une expression de calcul d'UE""" formsemestre = FormSemestre.get_formsemestre(formsemestre_id) if not formsemestre.can_be_edited_by(current_user): raise AccessDenied("vous n'avez pas le droit d'effectuer cette opération") expr = FormSemestreUEComputationExpr.query.filter_by( formsemestre_id=formsemestre_id, ue_id=ue_id ).first() if expr is not None: db.session.delete(expr) db.session.commit() flash("formule supprimée") return flask.redirect( url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, ) ) @bp.route("/formsemestre_enseignants_list") @scodoc @permission_required(Permission.ScoView) @scodoc7func def formsemestre_enseignants_list(formsemestre_id, fmt="html"): """Liste les enseignants intervenants dans le semestre (resp. modules et chargés de TD) et indique les absences saisies par chacun. """ formsemestre = FormSemestre.get_formsemestre(formsemestre_id) # resp. de modules et charges de TD sem_ens: dict[ int, list[ModuleImpl] ] = {} # uid : { "mods" : liste des modimpls, ... } modimpls = formsemestre.modimpls_sorted for modimpl in modimpls: if not modimpl.responsable_id in sem_ens: sem_ens[modimpl.responsable_id] = {"mods": [modimpl]} else: sem_ens[modimpl.responsable_id]["mods"].append(modimpl) for enseignant in modimpl.enseignants: if not enseignant.id in sem_ens: sem_ens[enseignant.id] = {"mods": [modimpl]} else: sem_ens[enseignant.id]["mods"].append(modimpl) # compte les absences ajoutées par chacun dans tout le semestre for uid, info in sem_ens.items(): # Note : avant 9.6, on utilisait Scolog pour compter les opérations AddAbsence # ici on compte directement les assiduités info["nbabsadded"] = ( Assiduite.query.filter_by(user_id=uid, etat=scu.EtatAssiduite.ABSENT) .filter( Assiduite.date_debut >= formsemestre.date_debut, Assiduite.date_debut <= formsemestre.date_fin, ) .join(Identite) .join(FormSemestreInscription) .filter_by(formsemestre_id=formsemestre.id) .count() ) # description textuelle des modules for uid, info in sem_ens.items(): info["descr_mods"] = ", ".join( [modimpl.module.code for modimpl in sem_ens[uid]["mods"]] ) # ajoute infos sur enseignant: for uid, info in sem_ens.items(): user: User = db.session.get(User, uid) if user: if user.email: info["email"] = user.email info["_email_target"] = f"mailto:{user.email}" info["nom_fmt"] = user.get_nom_fmt() info["prenom_fmt"] = user.get_prenom_fmt() info["sort_key"] = user.sort_key() sem_ens_list = list(sem_ens.values()) sem_ens_list.sort(key=itemgetter("sort_key")) # --- Generate page with table title = f"Enseignants de {formsemestre.titre_mois()}" T = GenTable( columns_ids=["nom_fmt", "prenom_fmt", "descr_mods", "nbabsadded", "email"], titles={ "nom_fmt": "Nom", "prenom_fmt": "Prénom", "email": "Mail", "descr_mods": "Modules", "nbabsadded": "Saisies Abs.", }, rows=sem_ens_list, html_sortable=True, html_class="table_leftalign formsemestre_enseignants_list", html_with_td_classes=True, filename=scu.make_filename(f"Enseignants-{formsemestre.titre_annee()}"), html_title=html_sco_header.html_sem_header( "Enseignants du semestre", with_page_header=False ), base_url=f"{request.base_url}?formsemestre_id={formsemestre_id}", caption="""Tous les enseignants (responsables ou associés aux modules de ce semestre) apparaissent. Le nombre de saisies d'absences est indicatif.""", preferences=sco_preferences.SemPreferences(formsemestre_id), ) return T.make_page(page_title=title, title=title, fmt=fmt) @bp.route("/edit_enseignants_form_delete", methods=["GET", "POST"]) @scodoc @permission_required(Permission.ScoView) @scodoc7func def edit_enseignants_form_delete(moduleimpl_id, ens_id: int): """remove ens from this modueimpl ens_id: user.id """ M, _ = sco_moduleimpl.can_change_ens(moduleimpl_id) # search ens_id ok = False for ens in M["ens"]: if ens["ens_id"] == ens_id: ok = True break if not ok: raise ScoValueError(f"invalid ens_id ({ens_id})") ndb.SimpleQuery( """DELETE FROM notes_modules_enseignants WHERE moduleimpl_id = %(moduleimpl_id)s AND ens_id = %(ens_id)s """, {"moduleimpl_id": moduleimpl_id, "ens_id": ens_id}, ) return flask.redirect( url_for( "notes.edit_enseignants_form", scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id, ) ) # --- Gestion des inscriptions aux semestres # Ancienne API, pas certain de la publier en ScoDoc8 # sco_publish( # "/do_formsemestre_inscription_create", # sco_formsemestre_inscriptions.do_formsemestre_inscription_create, # Permission.EtudInscrit, # ) # sco_publish( # "/do_formsemestre_inscription_edit", # sco_formsemestre_inscriptions.do_formsemestre_inscription_edit, # Permission.EtudInscrit, # ) sco_publish( "/do_formsemestre_inscription_list", sco_formsemestre_inscriptions.do_formsemestre_inscription_list, Permission.ScoView, ) @bp.route("/do_formsemestre_inscription_listinscrits") @scodoc @permission_required(Permission.ScoView) @scodoc7func def do_formsemestre_inscription_listinscrits(formsemestre_id, fmt=None): """Liste les inscrits (état I) à ce semestre et cache le résultat""" r = sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits( formsemestre_id ) return scu.sendResult(r, fmt=fmt, name="inscrits") @bp.route("/formsemestre_desinscription", methods=["GET", "POST"]) @scodoc @permission_required(Permission.EditFormSemestre) @scodoc7func def formsemestre_desinscription(etudid, formsemestre_id, dialog_confirmed=False): """désinscrit l'etudiant de ce semestre (et donc de tous les modules). A n'utiliser qu'en cas d'erreur de saisie. S'il s'agit d'un semestre extérieur et qu'il n'y a plus d'inscrit, le semestre sera supprimé. """ formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) sem = formsemestre.to_dict() # compat # -- check lock if not formsemestre.etat: raise ScoValueError("desinscription impossible: semestre verrouille") # -- Si décisions de jury, désinscription interdite nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) if nt.etud_has_decision(etudid): raise ScoValueError( f"""Désinscription impossible: l'étudiant a une décision de jury (la supprimer avant si nécessaire: <a href="{ url_for("notes.formsemestre_validation_suppress_etud", scodoc_dept=g.scodoc_dept, etudid=etudid, formsemestre_id=formsemestre_id) }">supprimer décision jury</a> ) """ ) if not dialog_confirmed: etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] if formsemestre.modalite != "EXT": msg_ext = """ <p>%s sera désinscrit de tous les modules du semestre %s (%s - %s).</p> <p>Cette opération ne doit être utilisée que pour corriger une <b>erreur</b> ! Un étudiant réellement inscrit doit le rester, le faire éventuellement <b>démissionner<b>. </p> """ % ( etud["nomprenom"], sem["titre_num"], sem["date_debut"], sem["date_fin"], ) else: # semestre extérieur msg_ext = """ <p>%s sera désinscrit du semestre extérieur %s (%s - %s).</p> """ % ( etud["nomprenom"], sem["titre_num"], sem["date_debut"], sem["date_fin"], ) inscrits = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( args={"formsemestre_id": formsemestre_id} ) nbinscrits = len(inscrits) if nbinscrits <= 1: msg_ext = """<p class="warning">Attention: le semestre extérieur sera supprimé car il n'a pas d'autre étudiant inscrit. </p> """ return scu.confirm_dialog( """<h2>Confirmer la demande de désinscription ?</h2>""" + msg_ext, dest_url="", cancel_url=url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, ), parameters={"etudid": etudid, "formsemestre_id": formsemestre_id}, ) sco_formsemestre_inscriptions.do_formsemestre_desinscription( etudid, formsemestre_id ) flash("Étudiant désinscrit") return redirect( url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid) ) sco_publish( "/do_formsemestre_desinscription", sco_formsemestre_inscriptions.do_formsemestre_desinscription, Permission.EtudInscrit, methods=["GET", "POST"], ) @bp.route( "/etud_desinscrit_ue/<int:etudid>/<int:formsemestre_id>/<int:ue_id>", methods=["GET", "POST"], ) @scodoc @permission_required(Permission.EtudInscrit) def etud_desinscrit_ue(etudid, formsemestre_id, ue_id): """ - En classique: désinscrit l'etudiant de tous les modules de cette UE dans ce semestre. - En APC: dispense de l'UE indiquée. """ etud = Identite.get_etud(etudid) ue = UniteEns.query.get_or_404(ue_id) formsemestre = FormSemestre.get_formsemestre(formsemestre_id) if ue.formation.is_apc(): if ( DispenseUE.query.filter_by( formsemestre_id=formsemestre_id, etudid=etudid, ue_id=ue_id ).count() == 0 ): disp = DispenseUE( formsemestre_id=formsemestre_id, ue_id=ue_id, etudid=etudid ) db.session.add(disp) db.session.commit() log(f"etud_desinscrit_ue {etud} {ue}") Scolog.logdb( method="etud_desinscrit_ue", etudid=etud.id, msg=f"Désinscription de l'UE {ue.acronyme} de {formsemestre.titre_annee()}", commit=True, ) sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre.id) else: sco_moduleimpl_inscriptions.do_etud_desinscrit_ue_classic( etudid, formsemestre_id, ue_id ) flash(f"{etud.nomprenom} déinscrit de {ue.acronyme}") return flask.redirect( url_for( "notes.moduleimpl_inscriptions_stats", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, ) ) @bp.route( "/etud_inscrit_ue/<int:etudid>/<int:formsemestre_id>/<int:ue_id>", methods=["GET", "POST"], ) @scodoc @permission_required(Permission.EtudInscrit) def etud_inscrit_ue(etudid, formsemestre_id, ue_id): """ En classic: inscrit l'étudiant à tous les modules de cette UE dans ce semestre. En APC: enlève la dispense de cette UE s'il y en a une. """ formsemestre = FormSemestre.query.filter_by( id=formsemestre_id, dept_id=g.scodoc_dept_id ).first_or_404() etud = Identite.get_etud(etudid) ue = UniteEns.query.get_or_404(ue_id) if ue.formation.is_apc(): for disp in DispenseUE.query.filter_by( formsemestre_id=formsemestre_id, etudid=etud.id, ue_id=ue_id ): db.session.delete(disp) log(f"etud_inscrit_ue {etud} {ue}") Scolog.logdb( method="etud_inscrit_ue", etudid=etud.id, msg=f"Inscription à l'UE {ue.acronyme} de {formsemestre.titre_annee()}", commit=True, ) db.session.commit() sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre.id) else: # Formations classiques: joue sur les inscriptions aux modules sco_moduleimpl_inscriptions.do_etud_inscrit_ue(etud.id, formsemestre_id, ue_id) return flask.redirect( url_for( "notes.moduleimpl_inscriptions_stats", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, ) ) # --- Inscriptions sco_publish( "/formsemestre_inscription_with_modules_form", sco_formsemestre_inscriptions.formsemestre_inscription_with_modules_form, Permission.EtudInscrit, ) sco_publish( "/formsemestre_inscription_with_modules_etud", sco_formsemestre_inscriptions.formsemestre_inscription_with_modules_etud, Permission.EtudInscrit, ) sco_publish( "/formsemestre_inscription_with_modules", sco_formsemestre_inscriptions.formsemestre_inscription_with_modules, Permission.EtudInscrit, ) sco_publish( "/formsemestre_inscription_option", sco_formsemestre_inscriptions.formsemestre_inscription_option, Permission.EtudInscrit, methods=["GET", "POST"], ) sco_publish( "/do_moduleimpl_incription_options", sco_formsemestre_inscriptions.do_moduleimpl_incription_options, Permission.EtudInscrit, ) sco_publish( "/formsemestre_inscrits_ailleurs", sco_formsemestre_inscriptions.formsemestre_inscrits_ailleurs, Permission.ScoView, ) sco_publish( "/moduleimpl_inscriptions_edit", sco_moduleimpl_inscriptions.moduleimpl_inscriptions_edit, Permission.EtudInscrit, methods=["GET", "POST"], ) sco_publish( "/moduleimpl_inscriptions_stats", sco_moduleimpl_inscriptions.moduleimpl_inscriptions_stats, Permission.ScoView, ) # --- Evaluations @bp.route("/evaluation_delete", methods=["GET", "POST"]) @scodoc @permission_required(Permission.EnsView) @scodoc7func def evaluation_delete(evaluation_id): """Form delete evaluation""" evaluation: Evaluation = ( Evaluation.query.filter_by(id=evaluation_id) .join(ModuleImpl) .join(FormSemestre) .filter_by(dept_id=g.scodoc_dept_id) .first_or_404() ) tit = f"""Suppression de l'évaluation {evaluation.description or ""} ({evaluation.descr_date()})""" etat = sco_evaluations.do_evaluation_etat(evaluation.id) H = [ f""" {html_sco_header.html_sem_header(tit, with_h2=False)} <h2 class="formsemestre">Module <tt>{evaluation.moduleimpl.module.code}</tt> {evaluation.moduleimpl.module.titre_str()}</h2> <h3>{tit}</h3> <p class="help">Opération <span class="redboldtext">irréversible</span>. Si vous supprimez l'évaluation, vous ne pourrez pas retrouver les notes associées. </p> """, ] warning = False if etat["nb_notes_total"]: warning = True nb_desinscrits = etat["nb_notes_total"] - etat["nb_notes"] H.append( f"""<div class="ue_warning"><span>Il y a {etat["nb_notes_total"]} notes""" ) if nb_desinscrits: H.append( """ (dont {nb_desinscrits} d'étudiants qui ne sont plus inscrits)""" ) H.append(""" dans l'évaluation</span>""") if etat["nb_notes"] == 0: H.append( """<p>Vous pouvez quand même supprimer l'évaluation, les notes des étudiants désincrits seront effacées.</p>""" ) if etat["nb_notes"]: H.append( f"""<p>Suppression impossible (effacer les notes d'abord)</p> <p><a class="stdlink" href="{ url_for("notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=evaluation.moduleimpl_id) }">retour au tableau de bord du module</a> </p> </div>""" ) return "\n".join(H) + html_sco_header.sco_footer() if warning: H.append("""</div>""") tf = TrivialFormulator( request.base_url, scu.get_request_args(), (("evaluation_id", {"input_type": "hidden"}),), initvalues={"evaluation_id": evaluation.id}, submitlabel="Confirmer la suppression", cancelbutton="Annuler", ) if tf[0] == 0: return "\n".join(H) + tf[1] + html_sco_header.sco_footer() elif tf[0] == -1: return flask.redirect( url_for( "notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=evaluation.moduleimpl_id, ) ) else: evaluation.delete() return ( "\n".join(H) + f"""<p>OK, évaluation supprimée.</p> <p><a class="stdlink" href="{ url_for("notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=evaluation.moduleimpl_id) }">Continuer</a></p>""" + html_sco_header.sco_footer() ) @bp.route("/evaluation_edit", methods=["GET", "POST"]) @scodoc @permission_required(Permission.EnsView) @scodoc7func def evaluation_edit(evaluation_id): "form edit evaluation" return sco_evaluation_edit.evaluation_create_form( evaluation_id=evaluation_id, edit=True ) @bp.route("/evaluation_create", methods=["GET", "POST"]) @scodoc @permission_required(Permission.EnsView) @scodoc7func def evaluation_create(moduleimpl_id): "form create evaluation" modimpl = db.session.get(ModuleImpl, moduleimpl_id) if modimpl is None: raise ScoValueError("Ce module n'existe pas ou plus !") return sco_evaluation_edit.evaluation_create_form( moduleimpl_id=moduleimpl_id, edit=False ) @bp.route("/evaluation_listenotes", methods=["GET", "POST"]) # API ScoDoc 7 compat @scodoc @permission_required_compat_scodoc7(Permission.ScoView) @scodoc7func def evaluation_listenotes(): """Affichage des notes d'une évaluation""" evaluation_id = None moduleimpl_id = None vals = scu.get_request_args() try: if "evaluation_id" in vals: evaluation_id = int(vals["evaluation_id"]) if "moduleimpl_id" in vals and vals["moduleimpl_id"]: moduleimpl_id = int(vals["moduleimpl_id"]) except ValueError as exc: raise ScoValueError("evaluation_listenotes: id invalides !") from exc fmt = vals.get("fmt", "html") html_content, page_title = sco_liste_notes.do_evaluation_listenotes( evaluation_id=evaluation_id, moduleimpl_id=moduleimpl_id, fmt=fmt ) if fmt == "html": H = html_sco_header.sco_header( page_title=page_title, cssstyles=["css/verticalhisto.css"], javascripts=["js/etud_info.js"], init_qtip=True, ) F = html_sco_header.sco_footer() return H + html_content + F else: return html_content sco_publish( "/evaluation_list_operations", sco_undo_notes.evaluation_list_operations, Permission.ScoView, ) @bp.route("/evaluation_check_absences_html/<int:evaluation_id>") @scodoc @permission_required(Permission.ScoView) def evaluation_check_absences_html(evaluation_id: int): "Check absences sur une évaluation" evaluation: Evaluation = ( Evaluation.query.filter_by(id=evaluation_id) .join(ModuleImpl) .join(FormSemestre) .filter_by(dept_id=g.scodoc_dept_id) .first_or_404() ) return sco_evaluation_check_abs.evaluation_check_absences_html(evaluation) sco_publish( "/formsemestre_check_absences_html", sco_evaluation_check_abs.formsemestre_check_absences_html, Permission.ScoView, ) # --- Placement des étudiants pour l'évaluation sco_publish( "/placement_eval_selectetuds", sco_placement.placement_eval_selectetuds, Permission.EnsView, methods=["GET", "POST"], ) # --- Saisie des notes sco_publish( "/saisie_notes_tableur", sco_saisie_notes.saisie_notes_tableur, Permission.EnsView, methods=["GET", "POST"], ) sco_publish( "/feuille_saisie_notes", sco_saisie_notes.feuille_saisie_notes, Permission.EnsView, ) sco_publish("/saisie_notes", sco_saisie_notes.saisie_notes, Permission.EnsView) sco_publish( "/do_evaluation_set_missing", sco_saisie_notes.do_evaluation_set_missing, Permission.EnsView, methods=["GET", "POST"], ) sco_publish( "/evaluation_suppress_alln", sco_saisie_notes.evaluation_suppress_alln, Permission.ScoView, methods=["GET", "POST"], ) # --- Bulletins @bp.route("/formsemestre_bulletins_pdf") @scodoc @permission_required(Permission.ScoView) @scodoc7func def formsemestre_bulletins_pdf(formsemestre_id, version="selectedevals"): "Publie les bulletins dans un classeur PDF" pdfdoc, filename = sco_bulletins_pdf.get_formsemestre_bulletins_pdf( formsemestre_id, version=version ) return scu.sendPDFFile(pdfdoc, filename) _EXPL_BULL = """Versions des bulletins: <ul> <li><bf>courte</bf>: moyennes des modules (en BUT: seulement les moyennes d'UE)</li> <li><bf>intermédiaire</bf>: moyennes des modules et notes des évaluations sélectionnées</li> <li><bf>complète</bf>: toutes les notes</li> </ul>""" @bp.route("/formsemestre_bulletins_pdf_choice") @scodoc @permission_required(Permission.ScoView) @scodoc7func def formsemestre_bulletins_pdf_choice(formsemestre_id, version=None): """Choix version puis envoi classeur bulletins pdf""" formsemestre = FormSemestre.get_formsemestre(formsemestre_id) if version: pdfdoc, filename = sco_bulletins_pdf.get_formsemestre_bulletins_pdf( formsemestre_id, version=version ) return scu.sendPDFFile(pdfdoc, filename) return _formsemestre_bulletins_choice( formsemestre, title="Choisir la version des bulletins à générer", explanation=_EXPL_BULL, ) @bp.route("/etud_bulletins_pdf") @scodoc @permission_required(Permission.ScoView) @scodoc7func def etud_bulletins_pdf(etudid, version="selectedevals"): "Publie tous les bulletins d'un etudiants dans un classeur PDF" if version not in scu.BULLETINS_VERSIONS: raise ScoValueError("etud_bulletins_pdf: version de bulletin demandée invalide") pdfdoc, filename = sco_bulletins_pdf.get_etud_bulletins_pdf(etudid, version=version) return scu.sendPDFFile(pdfdoc, filename) @bp.route("/formsemestre_bulletins_mailetuds_choice") @scodoc @permission_required(Permission.ScoView) @scodoc7func def formsemestre_bulletins_mailetuds_choice( formsemestre_id, version=None, dialog_confirmed=False, prefer_mail_perso=0, ): """Choix version puis envoi classeur bulletins pdf""" if version: return flask.redirect( url_for( "notes.formsemestre_bulletins_mailetuds", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, version=version, dialog_confirmed=dialog_confirmed, prefer_mail_perso=prefer_mail_perso, ) ) formsemestre = FormSemestre.get_formsemestre(formsemestre_id) expl_bull = """Versions des bulletins: <ul> <li><b>courte</b>: moyennes des modules</li> <li><b>intermédiaire</b>: moyennes des modules et notes des évaluations sélectionnées</li> <li><b>complète</b>: toutes les notes</li> """ if formsemestre.formation.is_apc(): expl_bull += """ <li><b>courte spéciale BUT</b>: un résumé en une page pour les BUTs</li> """ expl_bull += "</ul>" return _formsemestre_bulletins_choice( formsemestre, title="Choisir la version des bulletins à envoyer par mail", explanation="""Chaque étudiant (non démissionnaire ni défaillant) ayant une adresse mail connue de ScoDoc recevra une copie PDF de son bulletin de notes, dans la version choisie. </p><p>""" + expl_bull, choose_mail=True, ) # not published def _formsemestre_bulletins_choice( formsemestre: FormSemestre, title="", explanation="", choose_mail=False ): """Choix d'une version de bulletin""" versions = ( scu.BULLETINS_VERSIONS_BUT if formsemestre.formation.is_apc() else scu.BULLETINS_VERSIONS ) H = [ html_sco_header.html_sem_header(title), f""" <form name="f" method="GET" action="{request.base_url}"> <input type="hidden" name="formsemestre_id" value="{formsemestre.id}"></input> """, ] H.append("""<select name="version" class="noprint">""") for version, description in versions.items(): H.append(f"""<option value="{version}">{description}</option>""") H.append("""</select> <input type="submit" value="Générer"/>""") if choose_mail: H.append( """<div> <input type="checkbox" name="prefer_mail_perso" value="1" />Utiliser si possible les adresses personnelles </div>""" ) H.append(f"""<p class="help">{explanation}</p>""") return "\n".join(H) + html_sco_header.sco_footer() @bp.route("/formsemestre_bulletins_mailetuds") @scodoc @permission_required(Permission.ScoView) @scodoc7func def formsemestre_bulletins_mailetuds( formsemestre_id, version="long", dialog_confirmed=False, prefer_mail_perso=0, ): """Envoie à chaque etudiant son bulletin (inscrit non démissionnaire ni défaillant et ayant un mail renseigné dans ScoDoc) """ prefer_mail_perso = int(prefer_mail_perso) formsemestre = FormSemestre.get_formsemestre(formsemestre_id) inscriptions = [ inscription for inscription in formsemestre.inscriptions if inscription.etat == scu.INSCRIT ] # if not sco_bulletins.can_send_bulletin_by_mail(formsemestre_id): raise AccessDenied("vous n'avez pas le droit d'envoyer les bulletins") # Confirmation dialog if not dialog_confirmed: return scu.confirm_dialog( f"<h2>Envoyer les {len(inscriptions)} bulletins par e-mail aux étudiants inscrits ?", dest_url="", cancel_url=url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, ), parameters={ "version": version, "formsemestre_id": formsemestre_id, "prefer_mail_perso": prefer_mail_perso, }, ) # Make each bulletin nb_sent = 0 for inscription in inscriptions: sent, _ = sco_bulletins.do_formsemestre_bulletinetud( formsemestre, inscription.etud, version=version, prefer_mail_perso=prefer_mail_perso, fmt="pdfmail", ) if sent: nb_sent += 1 # return f""" {html_sco_header.sco_header()} <p>{nb_sent} bulletins sur {len(inscriptions)} envoyés par mail !</p> <p><a class="stdlink" href="{url_for('notes.formsemestre_status', scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id) }">continuer</a></p> {html_sco_header.sco_footer()} """ sco_publish( "/external_ue_create_form", sco_ue_external.external_ue_create_form, Permission.ScoView, methods=["GET", "POST"], ) @bp.route("/appreciation_add_form", methods=["GET", "POST"]) @scodoc @permission_required(Permission.EnsView) @scodoc7func def appreciation_add_form( etudid=None, formsemestre_id=None, appreciation_id=None, # si id, edit suppress=False, # si true, supress id ): "form ajout ou edition d'une appreciation" if appreciation_id: # edit mode appreciation = db.session.get(BulAppreciations, appreciation_id) if appreciation is None: raise ScoValueError("id d'appreciation invalide !") formsemestre_id = appreciation.formsemestre_id etudid = appreciation.etudid etud: Identite = Identite.query.filter_by( id=etudid, dept_id=g.scodoc_dept_id ).first_or_404() vals = scu.get_request_args() if "edit" in vals: edit = int(vals["edit"]) elif appreciation_id: edit = 1 else: edit = 0 formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) # check custom access permission can_edit_app = formsemestre.est_responsable(current_user) or ( current_user.has_permission(Permission.EtudInscrit) ) if not can_edit_app: raise AccessDenied("vous n'avez pas le droit d'ajouter une appreciation") # bul_url = url_for( "notes.formsemestre_bulletinetud", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, etudid=etudid, ) if suppress: db.session.delete(appreciation) Scolog.logdb( method="appreciation_suppress", etudid=etudid, ) db.session.commit() flash("appréciation supprimée") return flask.redirect(bul_url) # if appreciation_id: action = "Édition" else: action = "Ajout" H = [ html_sco_header.sco_header(), f"""<h2>{action} d'une appréciation sur {etud.nomprenom}</h2>""", ] F = html_sco_header.sco_footer() descr = [ ("edit", {"input_type": "hidden", "default": edit}), ("etudid", {"input_type": "hidden"}), ("formsemestre_id", {"input_type": "hidden"}), ("appreciation_id", {"input_type": "hidden"}), ("comment", {"title": "", "input_type": "textarea", "rows": 4, "cols": 60}), ] if appreciation_id: initvalues = { "etudid": etudid, "formsemestre_id": formsemestre_id, "comment": appreciation.comment, } else: initvalues = {} tf = TrivialFormulator( request.base_url, scu.get_request_args(), descr, initvalues=initvalues, cancelbutton="Annuler", submitlabel="Ajouter appréciation", ) if tf[0] == 0: return "\n".join(H) + "\n" + tf[1] + F elif tf[0] == -1: return flask.redirect(bul_url) else: if edit: appreciation.author = (current_user.user_name,) appreciation.comment = tf[2]["comment"].strip() flash("appréciation modifiée") else: # nouvelle appreciation = BulAppreciations( etudid=etudid, formsemestre_id=formsemestre_id, author=current_user.user_name, comment=tf[2]["comment"].strip(), ) flash("appréciation ajoutée") db.session.add(appreciation) # log Scolog.logdb( method="appreciation_add", etudid=etudid, msg=appreciation.comment_safe(), ) db.session.commit() # ennuyeux mais necessaire (pour le PDF seulement) sco_cache.invalidate_formsemestre( pdfonly=True, formsemestre_id=formsemestre_id ) # > appreciation_add return flask.redirect(bul_url) # --- FORMULAIRE POUR VALIDATION DES UE ET SEMESTRES @bp.route("/formsemestre_validation_etud_form") @scodoc @permission_required(Permission.ScoView) @scodoc7func def formsemestre_validation_etud_form( formsemestre_id, etudid=None, etud_index=None, check=0, desturl="", sortcol=None, ): "Formulaire choix jury pour un étudiant" formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) read_only = not formsemestre.can_edit_jury() if formsemestre.formation.is_apc(): return redirect( url_for( "notes.formsemestre_validation_but", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, etudid=etudid, ) ) return sco_formsemestre_validation.formsemestre_validation_etud_form( formsemestre_id, etudid=etudid, etud_index=etud_index, check=check, read_only=read_only, dest_url=desturl, sortcol=sortcol, ) @bp.route("/formsemestre_validation_etud") @scodoc @permission_required(Permission.ScoView) @scodoc7func def formsemestre_validation_etud( formsemestre_id, etudid=None, codechoice=None, desturl="", sortcol=None, ): "Enregistre choix jury pour un étudiant" formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) if not formsemestre.can_edit_jury(): raise ScoPermissionDenied( dest_url=url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, ) ) return sco_formsemestre_validation.formsemestre_validation_etud( formsemestre_id, etudid=etudid, codechoice=codechoice, desturl=desturl, sortcol=sortcol, ) @bp.route("/formsemestre_validation_etud_manu") @scodoc @permission_required(Permission.ScoView) @scodoc7func def formsemestre_validation_etud_manu( formsemestre_id, etudid=None, code_etat="", new_code_prev="", devenir="", assidu=False, desturl="", sortcol=None, ): "Enregistre choix jury pour un étudiant" formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) if not formsemestre.can_edit_jury(): raise ScoPermissionDenied( dest_url=url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, ) ) return sco_formsemestre_validation.formsemestre_validation_etud_manu( formsemestre_id, etudid=etudid, code_etat=code_etat, new_code_prev=new_code_prev, devenir=devenir, assidu=assidu, desturl=desturl, sortcol=sortcol, ) # --- Jurys BUT @bp.route( "/formsemestre_validation_but/<int:formsemestre_id>/<etudid>", methods=["GET", "POST"], ) @scodoc @permission_required(Permission.ScoView) def formsemestre_validation_but( formsemestre_id: int, etudid: int, ): "Form. saisie décision jury semestre BUT" formsemestre: FormSemestre = FormSemestre.query.filter_by( id=formsemestre_id, dept_id=g.scodoc_dept_id ).first_or_404() # la route ne donne pas le type d'etudid pour pouvoir construire des URLs # provisoires avec NEXT et PREV try: etudid = int(etudid) except ValueError as exc: raise ScoValueError("adresse invalide") from exc etud = Identite.get_etud(etudid) nb_etuds = formsemestre.etuds.count() read_only = not formsemestre.can_edit_jury() # --- Navigation prev_lnk = ( f"""{scu.EMO_PREV_ARROW} <a href="{url_for( "notes.formsemestre_validation_but", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, etudid="PREV" )}" class="stdlink"">précédent</a> """ if nb_etuds > 1 else "" ) next_lnk = ( f"""<a href="{url_for( "notes.formsemestre_validation_but", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, etudid="NEXT" )}" class="stdlink"">suivant</a> {scu.EMO_NEXT_ARROW} """ if nb_etuds > 1 else "" ) navigation_div = f""" <div class="but_navigation"> <div class="prev"> {prev_lnk} </div> <div class="back_list"> <a href="{ url_for( "notes.formsemestre_recapcomplet", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, mode_jury=1, selected_etudid=etud.id )}" class="stdlink">retour à la liste</a> </div> <div class="next"> {next_lnk} </div> </div> """ H = [ html_sco_header.sco_header( page_title=f"Validation BUT S{formsemestre.semestre_id}", formsemestre_id=formsemestre_id, etudid=etudid, cssstyles=[ "css/jury_but.css", "css/cursus_but.css", ], javascripts=("js/jury_but.js",), ), """<div class="jury_but"> """, ] if formsemestre.etuds_inscriptions[etudid].etat != scu.INSCRIT: return ( "\n".join(H) + f""" <div> <div class="bull_head"> <div> <div class="titre_parcours">Jury BUT</div> <div class="nom_etud">{etud.nomprenom}</div> </div> <div class="bull_photo"><a href="{ url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id) }">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a> </div> </div> <div class="warning">Impossible de statuer sur cet étudiant: il est démissionnaire ou défaillant (voir <a class="stdlink" href="{ url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid) }">sa fiche</a>) </div> </div> {navigation_div} </div> """ + html_sco_header.sco_footer() ) deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre) if len(deca.get_decisions_rcues_annee()) == 0: return jury_but_view.jury_but_semestriel( formsemestre, etud, read_only, navigation_div=navigation_div ) if request.method == "POST": if not read_only: deca.record_form(request.form) ScolarNews.add( typ=ScolarNews.NEWS_JURY, obj=formsemestre.id, text=f"""Saisie décision jury dans {formsemestre.html_link_status()}""", url=url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id, ), ) flash("codes enregistrés") return flask.redirect( url_for( "notes.formsemestre_validation_but", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, etudid=etudid, ) ) warning = "" if len(deca.niveaux_competences) != len(deca.decisions_rcue_by_niveau): warning += f"""<div class="warning">Attention: {len(deca.niveaux_competences)} niveaux mais {len(deca.decisions_rcue_by_niveau)} regroupements RCUE.</div>""" if (deca.parcour is None) and len(formsemestre.parcours) > 0: warning += ( """<div class="warning">L'étudiant n'est pas inscrit à un parcours.</div>""" ) if deca.formsemestre_impair: inscription = deca.formsemestre_impair.etuds_inscriptions.get(etud.id) if (not inscription) or inscription.etat != scu.INSCRIT: etat_ins = scu.ETATS_INSCRIPTION.get(inscription.etat, "inconnu?") warning += f"""<div class="warning">{etat_ins} en S{deca.formsemestre_impair.semestre_id}</div>""" if deca.formsemestre_pair: inscription = deca.formsemestre_pair.etuds_inscriptions.get(etud.id) if (not inscription) or inscription.etat != scu.INSCRIT: etat_ins = scu.ETATS_INSCRIPTION.get(inscription.etat, "inconnu?") warning += f"""<div class="warning">{etat_ins} en S{deca.formsemestre_pair.semestre_id}</div>""" if deca.has_notes_en_attente(): warning += f"""<div class="warning">{etud.nomprenom} a des notes en ATTente. Vous devriez régler cela avant de statuer en jury !</div>""" H.append( f""" <div> <div class="bull_head"> <div> <div class="titre_parcours">Jury BUT{deca.annee_but} - Parcours {(deca.parcour.libelle if deca.parcour else False) or "non spécifié"} - {deca.annee_scolaire_str()}</div> <div class="nom_etud">{etud.nomprenom}</div> </div> <div class="bull_photo"><a href="{ url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id) }">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a> </div> </div> {warning} </div> <form method="post" class="jury_but_box" id="jury_but"> """ ) H.append(jury_but_view.show_etud(deca, read_only=read_only)) autorisations_idx = deca.get_autorisations_passage() div_autorisations_passage = ( f""" <div class="but_autorisations_passage"> <span>Autorisé à passer en :</span> { ", ".join( ["S" + str(i) for i in autorisations_idx ] )} </div> """ if autorisations_idx else """<div class="but_autorisations_passage but_explanation"> pas d'autorisations de passage enregistrées. </div> """ ) H.append(div_autorisations_passage) if read_only: H.append( f""" <div class="but_explanation"> {"Vous n'avez pas la permission de modifier ces décisions." if formsemestre.etat else "Semestre verrouillé."} Les champs entourés en vert sont enregistrés. </div>""" ) else: erase_span = f""" <a style="margin-left: 16px;" class="stdlink" href="{ url_for("notes.erase_decisions_annee_formation", scodoc_dept=g.scodoc_dept, formation_id=deca.formsemestre.formation.id, etudid=deca.etud.id, annee=deca.annee_but, formsemestre_id=formsemestre_id)}" >effacer des décisions de jury</a> <a style="margin-left: 16px;" class="stdlink" href="{ url_for("notes.formsemestre_validate_previous_ue", scodoc_dept=g.scodoc_dept, etudid=deca.etud.id, formsemestre_id=formsemestre_id)}" >enregistrer des UEs antérieures</a> """ H.append( f"""<div class="but_settings"> <input type="checkbox" onchange="enable_manual_codes(this)"> <em>permettre la saisie manuelles des codes {"d'année et " if deca.jury_annuel else ""} de niveaux. Dans ce cas, assurez-vous de la cohérence entre les codes d'UE/RCUE/Année ! </em> </input> </div> <div class="but_buttons"> <span><input type="submit" value="Enregistrer ces décisions"></span> <span>{erase_span}</span> </div> """ ) H.append(navigation_div) H.append("</form>") # Affichage cursus BUT but_cursus = cursus_but.EtudCursusBUT(etud, deca.formsemestre.formation) H += [ """<div class="jury_but_box"> <div class="jury_but_box_title"><b>Niveaux de compétences enregistrés :</b></div> """, render_template( "but/cursus_etud.j2", cursus=but_cursus, scu=scu, ), "</div>", ] H.append( render_template( "but/documentation_codes_jury.j2", nom_univ=f"""Export {sco_preferences.get_preference("InstituteName") or sco_preferences.get_preference("UnivName") or "Apogée"}""", codes=ScoDocSiteConfig.get_codes_apo_dict(), ) ) H.append( f"""<div class="but_doc_codes but_warning_rcue_cap"> {scu.EMO_WARNING} Rappel: pour les redoublants, seules les UE <b>capitalisées</b> (note > 10) lors d'une année précédente peuvent être prise en compte pour former un RCUE (associé à un niveau de compétence du BUT). </div> """ ) return "\n".join(H) + html_sco_header.sco_footer() @bp.route( "/formsemestre_validation_auto_but/<int:formsemestre_id>", methods=["GET", "POST"] ) @scodoc @permission_required(Permission.ScoView) def formsemestre_validation_auto_but(formsemestre_id: int = None): "Saisie automatique des décisions de jury BUT" formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) if not formsemestre.can_edit_jury(): raise ScoPermissionDenied( dest_url=url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, ) ) formsemestre = FormSemestre.get_formsemestre(formsemestre_id) form = jury_but_forms.FormSemestreValidationAutoBUTForm() if request.method == "POST": if not form.cancel.data: nb_etud_modif = jury_but_validation_auto.formsemestre_validation_auto_but( formsemestre ) flash(f"Décisions enregistrées ({nb_etud_modif} étudiants modifiés)") return redirect( url_for( "notes.formsemestre_recapcomplet", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, mode_jury=1, ) ) return render_template( "but/formsemestre_validation_auto_but.j2", form=form, sco=ScoData(formsemestre=formsemestre), title="Calcul automatique jury BUT", ) @bp.route( "/formsemestre_validate_previous_ue/<int:formsemestre_id>/<int:etudid>", methods=["GET", "POST"], ) @scodoc @permission_required(Permission.ScoView) def formsemestre_validate_previous_ue(formsemestre_id, etudid=None): "Form. saisie UE validée hors ScoDoc" formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) if not formsemestre.can_edit_jury(): raise ScoPermissionDenied( dest_url=url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, ) ) etud: Identite = ( Identite.query.filter_by(id=etudid) .join(FormSemestreInscription) .filter_by(formsemestre_id=formsemestre_id) .first_or_404() ) return sco_formsemestre_validation.formsemestre_validate_previous_ue( formsemestre, etud ) sco_publish( "/formsemestre_ext_create_form", sco_formsemestre_exterieurs.formsemestre_ext_create_form, Permission.ScoView, methods=["GET", "POST"], ) @bp.route("/formsemestre_ext_edit_ue_validations", methods=["GET", "POST"]) @scodoc @permission_required(Permission.ScoView) @scodoc7func def formsemestre_ext_edit_ue_validations(formsemestre_id, etudid=None): "Form. edition UE semestre extérieur" formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) if not formsemestre.can_edit_jury(): raise ScoPermissionDenied( dest_url=url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, ) ) return sco_formsemestre_exterieurs.formsemestre_ext_edit_ue_validations( formsemestre_id, etudid ) @bp.route("/formsemestre_validation_auto") @scodoc @permission_required(Permission.ScoView) @scodoc7func def formsemestre_validation_auto(formsemestre_id): "Formulaire saisie automatisee des decisions d'un semestre" formsemestre: FormSemestre = FormSemestre.query.filter_by( id=formsemestre_id, dept_id=g.scodoc_dept_id ).first_or_404() if not formsemestre.can_edit_jury(): raise ScoPermissionDenied( dest_url=url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, ) ) if formsemestre.formation.is_apc(): return redirect( url_for( "notes.formsemestre_validation_auto_but", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id, ) ) return sco_formsemestre_validation.formsemestre_validation_auto(formsemestre_id) @bp.route("/do_formsemestre_validation_auto") @scodoc @permission_required(Permission.ScoView) @scodoc7func def do_formsemestre_validation_auto(formsemestre_id): "Formulaire saisie automatisee des decisions d'un semestre" formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) if not formsemestre.can_edit_jury(): raise ScoPermissionDenied( dest_url=url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, ) ) return sco_formsemestre_validation.do_formsemestre_validation_auto(formsemestre_id) @bp.route("/formsemestre_validation_suppress_etud", methods=["GET", "POST"]) @scodoc @permission_required(Permission.ScoView) @scodoc7func def formsemestre_validation_suppress_etud( formsemestre_id, etudid, dialog_confirmed=False ): """Suppression des décisions de jury pour un étudiant.""" formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) if not formsemestre.can_edit_jury(): raise ScoPermissionDenied( dest_url=url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, ) ) etud = Identite.get_etud(etudid) if formsemestre.formation.is_apc(): next_url = url_for( "scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid, ) else: next_url = url_for( "notes.formsemestre_validation_etud_form", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, etudid=etudid, ) if not dialog_confirmed: d = sco_bulletins_json.dict_decision_jury( etud, formsemestre, with_decisions=True ) descr_ues = [f"{u['acronyme']}: {u['code']}" for u in d.get("decision_ue", [])] dec_annee = d.get("decision_annee") if dec_annee: descr_annee = dec_annee.get("code", "-") else: descr_annee = "-" existing = f""" <ul> <li>Semestre : {d.get("decision", {"code":"-"})['code'] or "-"}</li> <li>Année BUT: {descr_annee}</li> <li>UEs : {", ".join(descr_ues)}</li> <li>RCUEs: {len(d.get("decision_rcue", []))} décisions</li> <li>Autorisations: {descr_autorisations(ScolarAutorisationInscription.query.filter_by(origin_formsemestre_id=formsemestre_id, etudid=etudid))} </ul> """ return scu.confirm_dialog( f"""<h2>Confirmer la suppression des décisions du semestre {formsemestre.titre_mois()} pour {etud.nomprenom} </h2> <p>Cette opération est irréversible.</p> <div> {existing} </div> """, OK="Supprimer", dest_url="", cancel_url=next_url, parameters={"etudid": etudid, "formsemestre_id": formsemestre_id}, ) sco_formsemestre_validation.formsemestre_validation_suppress_etud( formsemestre_id, etudid ) flash("Décisions supprimées") return flask.redirect(next_url) # ------------- PV de JURY et archives sco_publish( "/formsemestre_pvjury", sco_pv_forms.formsemestre_pvjury, Permission.ScoView ) sco_publish("/pvjury_page_but", jury_but_pv.pvjury_page_but, Permission.ScoView) @bp.route("/formsemestre_saisie_jury") @scodoc @permission_required(Permission.ScoView) @scodoc7func def formsemestre_saisie_jury(formsemestre_id: int, selected_etudid: int = None): """Page de saisie: liste des étudiants et lien vers page jury sinon, redirect vers page recap en mode jury """ return redirect( url_for( "notes.formsemestre_recapcomplet", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, mode_jury=1, ) ) @bp.route( "/formsemestre_jury_but_erase/<int:formsemestre_id>", methods=["GET", "POST"], defaults={"etudid": None}, ) @bp.route( "/formsemestre_jury_but_erase/<int:formsemestre_id>/<int:etudid>", methods=["GET", "POST"], ) @scodoc @permission_required(Permission.ScoView) def formsemestre_jury_but_erase(formsemestre_id: int, etudid: int = None): """Supprime la décision de jury BUT pour cette année. Si l'étudiant n'est pas spécifié, efface les décisions de tous les inscrits. Si only_one_sem, n'efface que pour le formsemestre indiqué, pas les deux de l'année. """ only_one_sem = int(request.args.get("only_one_sem") or False) formsemestre: FormSemestre = FormSemestre.query.filter_by( id=formsemestre_id, dept_id=g.scodoc_dept_id ).first_or_404() if not formsemestre.can_edit_jury(): raise ScoPermissionDenied( dest_url=url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, ) ) if not formsemestre.formation.is_apc(): raise ScoValueError("semestre non BUT") if etudid is None: etud = None etuds = formsemestre.get_inscrits(include_demdef=True) dest_url = url_for( "notes.formsemestre_recapcomplet", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, mode_jury=1, ) else: etud = Identite.get_etud(etudid) etuds = [etud] dest_url = url_for( "notes.formsemestre_validation_but", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, etudid=etudid, ) if request.method == "POST": with sco_cache.DeferredSemCacheManager(): for etud in etuds: deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre) deca.erase(only_one_sem=only_one_sem) log(f"formsemestre_jury_but_erase({formsemestre_id}, {etudid})") flash( ( "décisions de jury du semestre effacées" if only_one_sem else "décisions de jury des semestres de l'année BUT effacées" ) + f" pour {len(etuds)} étudiant{'s' if len(etuds) > 1 else ''}" ) return redirect(dest_url) return render_template( "confirm_dialog.j2", title=f"""Effacer les validations de jury { ("de " + etud.nomprenom) if etud else ("des " + str(len(etuds)) + " étudiants inscrits dans ce semestre") } ?""", explanation=( f"""Les validations d'UE et autorisations de passage du semestre S{formsemestre.semestre_id} seront effacées.""" if only_one_sem else """Les validations de toutes les UE, RCUE (compétences) et année issues de cette année scolaire seront effacées. """ ) + """ <p>Les décisions des années scolaires précédentes ne seront pas modifiées.</p> <p>Efface aussi toutes les validations concernant l'année BUT de ce semestre, même si elles ont été acquises ailleurs. </p> <div class="warning">Cette opération est irréversible ! A n'utiliser que dans des cas exceptionnels, vérifiez bien tous les étudiants ensuite. </div> """, cancel_url=dest_url, ) @bp.route( "/erase_decisions_annee_formation/<int:etudid>/<int:formation_id>/<int:annee>", methods=["GET", "POST"], ) @scodoc @permission_required(Permission.EtudInscrit) def erase_decisions_annee_formation(etudid: int, formation_id: int, annee: int): """Efface toute les décisions d'une année pour cet étudiant""" etud: Identite = Identite.query.get_or_404(etudid) formation: Formation = Formation.query.filter_by( id=formation_id, dept_id=g.scodoc_dept_id ).first_or_404() if request.method == "POST": jury.erase_decisions_annee_formation(etud, formation, annee, delete=True) flash("Décisions de jury effacées") return redirect( url_for( "scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id, ) ) validations = jury.erase_decisions_annee_formation(etud, formation, annee) formsemestre_origine_id = request.args.get("formsemestre_id") formsemestre_origine = ( FormSemestre.query.get_or_404(formsemestre_origine_id) if formsemestre_origine_id else None ) return render_template( "jury/erase_decisions_annee_formation.j2", annee=annee, cancel_url=url_for( "scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id ), etud=etud, formation=formation, formsemestre_origine=formsemestre_origine, validations=validations, sco=ScoData(), title=f"Effacer décisions de jury {etud.nom} - année {annee}", ) @bp.route( "/jury_delete_manual/<int:etudid>", methods=["GET", "POST"], ) @scodoc @permission_required(Permission.EtudInscrit) def jury_delete_manual(etudid: int): """Efface toute les décisions d'une année pour cet étudiant""" etud: Identite = Identite.query.get_or_404(etudid) return jury_edit_manual.jury_delete_manual(etud) sco_publish( "/formsemestre_lettres_individuelles", sco_pv_forms.formsemestre_lettres_individuelles, Permission.ScoView, methods=["GET", "POST"], ) sco_publish( "/formsemestre_pvjury_pdf", sco_pv_forms.formsemestre_pvjury_pdf, Permission.ScoView, methods=["GET", "POST"], ) sco_publish( "/feuille_preparation_jury", sco_prepajury.feuille_preparation_jury, Permission.ScoView, ) sco_publish( "/formsemestre_archive", sco_archives_formsemestre.formsemestre_archive, Permission.ScoView, methods=["GET", "POST"], ) sco_publish( "/formsemestre_delete_archive", sco_archives_formsemestre.formsemestre_delete_archive, Permission.ScoView, methods=["GET", "POST"], ) sco_publish( "/formsemestre_list_archives", sco_archives_formsemestre.formsemestre_list_archives, Permission.ScoView, ) sco_publish( "/formsemestre_get_archived_file", sco_archives_formsemestre.formsemestre_get_archived_file, Permission.ScoView, ) sco_publish("/view_apo_csv", sco_etape_apogee_view.view_apo_csv, Permission.EditApogee) sco_publish( "/view_apo_csv_store", sco_etape_apogee_view.view_apo_csv_store, Permission.EditApogee, methods=["GET", "POST"], ) sco_publish( "/view_apo_csv_download_and_store", sco_etape_apogee_view.view_apo_csv_download_and_store, Permission.EditApogee, methods=["GET", "POST"], ) sco_publish( "/view_apo_csv_delete", sco_etape_apogee_view.view_apo_csv_delete, Permission.EditApogee, methods=["GET", "POST"], ) sco_publish( "/view_scodoc_etuds", sco_etape_apogee_view.view_scodoc_etuds, Permission.EditApogee ) sco_publish( "/view_apo_etuds", sco_etape_apogee_view.view_apo_etuds, Permission.EditApogee ) sco_publish( "/apo_semset_maq_status", sco_etape_apogee_view.apo_semset_maq_status, Permission.EditApogee, ) sco_publish( "/apo_csv_export_results", sco_etape_apogee_view.apo_csv_export_results, Permission.EditApogee, ) @bp.route("/formsemestre_set_apo_etapes", methods=["POST"]) @scodoc @permission_required(Permission.EditApogee) def formsemestre_set_apo_etapes(): """Change les codes étapes du semestre indiqué. Args: oid=formsemestre_id, value=chaine "V1RT, V1RT2", codes séparés par des virgules """ formsemestre_id = int(request.form.get("oid")) etapes_apo_str = request.form.get("value") formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) current_etapes = {e.etape_apo for e in formsemestre.etapes} new_etapes = {s.strip() for s in etapes_apo_str.split(",")} if new_etapes != current_etapes: formsemestre.etapes = [] for etape_apo in new_etapes: etape = models.FormSemestreEtape( formsemestre_id=formsemestre_id, etape_apo=etape_apo ) formsemestre.etapes.append(etape) db.session.add(formsemestre) db.session.commit() ScolarNews.add( typ=ScolarNews.NEWS_APO, text=f"Modification code Apogée du semestre {formsemestre.titre_annee()})", ) return ("", 204) @bp.route("/formsemestre_set_elt_annee_apo", methods=["POST"]) @scodoc @permission_required(Permission.EditApogee) def formsemestre_set_elt_annee_apo(): """Change les codes étapes du semestre indiqué. Args: oid=formsemestre_id, value=chaine "V3ONM, V3ONM1, V3ONM2", codes séparés par des virgules """ oid = int(request.form.get("oid")) value = (request.form.get("value") or "").strip() formsemestre: FormSemestre = FormSemestre.query.get_or_404(oid) if value != formsemestre.elt_annee_apo: formsemestre.elt_annee_apo = value db.session.add(formsemestre) db.session.commit() ScolarNews.add( typ=ScolarNews.NEWS_APO, text=f"Modification code Apogée du semestre {formsemestre.titre_annee()})", ) return ("", 204) @bp.route("/formsemestre_set_elt_sem_apo", methods=["POST"]) @scodoc @permission_required(Permission.EditApogee) def formsemestre_set_elt_sem_apo(): """Change les codes étapes du semestre indiqué. Args: oid=formsemestre_id, value=chaine "V3ONM, V3ONM1, V3ONM2", codes séparés par des virgules """ oid = int(request.form.get("oid")) value = (request.form.get("value") or "").strip() formsemestre: FormSemestre = FormSemestre.query.get_or_404(oid) if value != formsemestre.elt_sem_apo: formsemestre.elt_sem_apo = value db.session.add(formsemestre) db.session.commit() ScolarNews.add( typ=ScolarNews.NEWS_APO, text=f"Modification code Apogée du semestre {formsemestre.titre_annee()})", ) return ("", 204) @bp.route("/ue_set_apo", methods=["POST"]) @scodoc @permission_required(Permission.EditApogee) def ue_set_apo(): """Change le code APO de l'UE Args: oid=ue_id, value=chaine "VRTU12" (1 seul code / UE) """ ue_id = int(request.form.get("oid")) code_apo = (request.form.get("value") or "").strip() ue = UniteEns.query.get_or_404(ue_id) if code_apo != ue.code_apogee: ue.code_apogee = code_apo db.session.add(ue) db.session.commit() ScolarNews.add( typ=ScolarNews.NEWS_FORM, text=f"Modification code Apogée d'UE dans la formation {ue.formation.titre} ({ue.formation.acronyme})", ) return ("", 204) @bp.route("/module_set_apo", methods=["POST"]) @scodoc @permission_required(Permission.EditApogee) def module_set_apo(): """Change le code APO du module Args: oid=ue_id, value=chaine "VRTU12" (1 seul code / UE) """ oid = int(request.form.get("oid")) code_apo = (request.form.get("value") or "").strip() mod = Module.query.get_or_404(oid) if code_apo != mod.code_apogee: mod.code_apogee = code_apo db.session.add(mod) db.session.commit() ScolarNews.add( typ=ScolarNews.NEWS_FORM, text=f"""Modification code Apogée d'UE dans la formation { mod.formation.titre} ({mod.formation.acronyme})""", ) return ("", 204) # sco_semset sco_publish("/semset_page", sco_semset.semset_page, Permission.EditApogee) sco_publish( "/do_semset_create", sco_semset.do_semset_create, Permission.EditApogee, methods=["GET", "POST"], ) sco_publish( "/do_semset_delete", sco_semset.do_semset_delete, Permission.EditApogee, methods=["GET", "POST"], ) sco_publish( "/edit_semset_set_title", sco_semset.edit_semset_set_title, Permission.EditApogee, methods=["GET", "POST"], ) sco_publish( "/do_semset_add_sem", sco_semset.do_semset_add_sem, Permission.EditApogee, methods=["GET", "POST"], ) sco_publish( "/do_semset_remove_sem", sco_semset.do_semset_remove_sem, Permission.EditApogee, methods=["GET", "POST"], ) # sco_export_result sco_publish( "/scodoc_table_results", sco_export_results.scodoc_table_results, Permission.EditApogee, ) sco_publish( "/apo_compare_csv_form", sco_apogee_compare.apo_compare_csv_form, Permission.ScoView, methods=["GET", "POST"], ) sco_publish( "/apo_compare_csv", sco_apogee_compare.apo_compare_csv, Permission.ScoView, methods=["GET", "POST"], ) # ------------- INSCRIPTIONS: PASSAGE D'UN SEMESTRE A UN AUTRE sco_publish( "/formsemestre_inscr_passage", sco_inscr_passage.formsemestre_inscr_passage, Permission.EtudInscrit, methods=["GET", "POST"], ) sco_publish( "/formsemestre_synchro_etuds", sco_synchro_etuds.formsemestre_synchro_etuds, Permission.ScoView, methods=["GET", "POST"], ) # ------------- RAPPORTS STATISTIQUES sco_publish( "/formsemestre_report_counts", sco_report.formsemestre_report_counts, Permission.ScoView, ) sco_publish( "/formsemestre_suivi_cohorte", sco_report.formsemestre_suivi_cohorte, Permission.ScoView, ) sco_publish( "/formsemestre_suivi_cursus", sco_report.formsemestre_suivi_cursus, Permission.ScoView, ) sco_publish( "/formsemestre_etuds_lycees", sco_lycee.formsemestre_etuds_lycees, Permission.ScoView, ) sco_publish( "/scodoc_table_etuds_lycees", sco_lycee.scodoc_table_etuds_lycees, Permission.ScoView, ) sco_publish( "/formsemestre_graph_cursus", sco_report.formsemestre_graph_cursus, Permission.ScoView, ) sco_publish( "/formsemestre_but_indicateurs", sco_report_but.formsemestre_but_indicateurs, Permission.ScoView, ) sco_publish( "/formsemestre_poursuite_report", sco_poursuite_dut.formsemestre_poursuite_report, Permission.ScoView, ) sco_publish( "/pe_view_sem_recap", pe_view.pe_view_sem_recap, Permission.ScoView, methods=["GET", "POST"], ) sco_publish( "/report_debouche_date", sco_debouche.report_debouche_date, Permission.ScoView ) sco_publish( "/formsemestre_estim_cost", sco_cost_formation.formsemestre_estim_cost, Permission.ScoView, ) # -------------------------------------------------------------------- # DEBUG @bp.route("/check_sem_integrity") @scodoc @permission_required(Permission.EditFormSemestre) @scodoc7func def check_sem_integrity(formsemestre_id, fix=False): """Debug. Check that ue and module formations are consistents """ sem = sco_formsemestre.get_formsemestre(formsemestre_id) modimpls = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id) bad_ue = [] bad_sem = [] formations_set = set() # les formations mentionnées dans les UE et modules for modimpl in modimpls: mod = sco_edit_module.module_list({"module_id": modimpl["module_id"]})[0] formations_set.add(mod["formation_id"]) ue = sco_edit_ue.ue_list({"ue_id": mod["ue_id"]})[0] formations_set.add(ue["formation_id"]) if ue["formation_id"] != mod["formation_id"]: modimpl["mod"] = mod modimpl["ue"] = ue bad_ue.append(modimpl) if sem["formation_id"] != mod["formation_id"]: bad_sem.append(modimpl) modimpl["mod"] = mod H = [ html_sco_header.sco_header(), "<p>formation_id=%s" % sem["formation_id"], ] if bad_ue: H += [ "<h2>Modules d'une autre formation que leur UE:</h2>", "<br>".join([str(x) for x in bad_ue]), ] if bad_sem: H += [ "<h2>Module du semestre dans une autre formation:</h2>", "<br>".join([str(x) for x in bad_sem]), ] if not bad_ue and not bad_sem: H.append("<p>Aucun problème à signaler !</p>") else: log("check_sem_integrity: problem detected: formations_set=%s" % formations_set) if sem["formation_id"] in formations_set: formations_set.remove(sem["formation_id"]) if len(formations_set) == 1: if fix: log("check_sem_integrity: trying to fix %s" % formsemestre_id) formation_id = formations_set.pop() if sem["formation_id"] != formation_id: sem["formation_id"] = formation_id sco_formsemestre.do_formsemestre_edit(sem) H.append("""<p class="alert">Problème réparé: vérifiez</p>""") else: H.append( """ <p class="alert">Problème détecté réparable: <a href="check_sem_integrity?formsemestre_id=%s&fix=1">réparer maintenant</a></p> """ % (formsemestre_id,) ) else: H.append("""<p class="alert">Problème détecté !</p>""") return "\n".join(H) + html_sco_header.sco_footer() @bp.route("/check_form_integrity") @scodoc @permission_required(Permission.ScoView) @scodoc7func def check_form_integrity(formation_id, fix=False): "debug" log("check_form_integrity: formation_id=%s fix=%s" % (formation_id, fix)) ues = sco_edit_ue.ue_list(args={"formation_id": formation_id}) bad = [] for ue in ues: mats = sco_edit_matiere.matiere_list(args={"ue_id": ue["ue_id"]}) for mat in mats: mods = sco_edit_module.module_list({"matiere_id": mat["matiere_id"]}) for mod in mods: if mod["ue_id"] != ue["ue_id"]: if fix: # fix mod.ue_id log( "fix: mod.ue_id = %s (was %s)" % (ue["ue_id"], mod["ue_id"]) ) mod["ue_id"] = ue["ue_id"] sco_edit_module.do_module_edit(mod) bad.append(mod) if mod["formation_id"] != formation_id: bad.append(mod) if bad: txth = "<br>".join([str(x) for x in bad]) txt = "\n".join([str(x) for x in bad]) log("check_form_integrity: formation_id=%s\ninconsistencies:" % formation_id) log(txt) # Notify by e-mail send_scodoc_alarm("Notes: formation incoherente !", txt) else: txth = "OK" log("ok") return html_sco_header.sco_header() + txth + html_sco_header.sco_footer() @bp.route("/check_formsemestre_integrity") @scodoc @permission_required(Permission.ScoView) @scodoc7func def check_formsemestre_integrity(formsemestre_id): "debug" log("check_formsemestre_integrity: formsemestre_id=%s" % (formsemestre_id)) # verifie que tous les moduleimpl d'un formsemestre # se réfèrent à un module dont l'UE appartient a la même formation # Ancien bug: les ue_id étaient mal copiés lors des création de versions # de formations diag = [] Mlist = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id) for mod in Mlist: if mod["module"]["ue_id"] != mod["matiere"]["ue_id"]: diag.append( "moduleimpl %s: module.ue_id=%s != matiere.ue_id=%s" % ( mod["moduleimpl_id"], mod["module"]["ue_id"], mod["matiere"]["ue_id"], ) ) if mod["ue"]["formation_id"] != mod["module"]["formation_id"]: diag.append( "moduleimpl %s: ue.formation_id=%s != mod.formation_id=%s" % ( mod["moduleimpl_id"], mod["ue"]["formation_id"], mod["module"]["formation_id"], ) ) if diag: send_scodoc_alarm( "Notes: formation incoherente dans semestre %s !" % formsemestre_id, "\n".join(diag), ) log("check_formsemestre_integrity: formsemestre_id=%s" % formsemestre_id) log("inconsistencies:\n" + "\n".join(diag)) else: diag = ["OK"] log("ok") return ( html_sco_header.sco_header() + "<br>".join(diag) + html_sco_header.sco_footer() )