# -*- 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 """ import html 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 flask_wtf import FlaskForm from flask_wtf.file import FileAllowed from wtforms.validators import DataRequired, Length from wtforms import FileField, StringField, SubmitField from app import db, log, send_scodoc_alarm from app import models from app.auth.models import User from app.but import apc_edit_ue, jury_but_pv from app.comp import 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, Scolog, 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.formations import ( edit_formation, edit_matiere, edit_module, edit_ue, formation_io, formation_versions, ) from app.scodoc import sco_utils as scu from app.scodoc.sco_exceptions import ( AccessDenied, ScoValueError, ScoInvalidIdType, ) from app.scodoc import ( sco_apogee_compare, sco_archives_formsemestre, sco_assiduites, sco_bulletins, sco_bulletins_pdf, sco_cache, sco_cost_formation, sco_debouche, sco_edit_apc, sco_etape_apogee_view, sco_etud, sco_evaluations, sco_evaluation_check_abs, sco_evaluation_db, sco_evaluation_edit, sco_evaluation_recap, sco_export_results, sco_formsemestre, sco_formsemestre_custommenu, sco_formsemestre_edit, sco_formsemestre_exterieurs, sco_formsemestre_inscriptions, sco_formsemestre_status, sco_groups_view, sco_inscr_passage, sco_liste_notes, sco_lycee, sco_moduleimpl, sco_moduleimpl_inscriptions, sco_moduleimpl_status, sco_placement, sco_poursuite_dut, sco_preferences, sco_prepajury, sco_pv_forms, sco_recapcomplet, sco_report, sco_report_but, sco_saisie_excel, sco_saisie_notes, sco_semset, sco_synchro_etuds, sco_tag_module, sco_ue_external, sco_undo_notes, sco_users, ) from app.formations import formation_recap from app.scodoc.gen_tables import GenTable 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", 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", edit_formation.formation_create, Permission.EditFormation, methods=["GET", "POST"], ) sco_publish( "/formation_delete", edit_formation.formation_delete, Permission.EditFormation, methods=["GET", "POST"], ) sco_publish( "/formation_edit", 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, formsemestre=formsemestre), 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", edit_ue.ue_create, Permission.EditFormation, methods=["GET", "POST"], ) sco_publish( "/ue_delete", edit_ue.ue_delete, Permission.EditFormation, methods=["GET", "POST"], ) @bp.route("/ue_edit/<int:ue_id>", methods=["GET", "POST"]) @scodoc @permission_required(Permission.EditFormation) def ue_edit(ue_id: int): "Edition de l'UE" return edit_ue.ue_edit(ue_id) @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_table") @scodoc @permission_required(Permission.ScoView) @scodoc7func def ue_table(formation_id=None, semestre_idx=1, msg=""): return 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: int): 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 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( "/formsemestre_edit_uecoefs", sco_formsemestre_edit.formsemestre_edit_uecoefs, Permission.ScoView, methods=["GET", "POST"], ) @bp.route("/formation_table_recap/<int:formation_id>") @scodoc @permission_required(Permission.ScoView) def formation_table_recap(formation_id: int): "Tableau récap. de la formation" formation = Formation.get_formation(formation_id) fmt = request.args.get("fmt", "html") return formation_recap.formation_table_recap(formation, fmt=fmt) sco_publish( "/export_recap_formations_annee_scolaire", formation_recap.export_recap_formations_annee_scolaire, Permission.ScoView, ) sco_publish( "/formation_add_malus_modules", edit_module.formation_add_malus_modules, Permission.EditFormation, ) sco_publish( "/matiere_create", edit_matiere.matiere_create, Permission.EditFormation, methods=["GET", "POST"], ) sco_publish( "/matiere_delete", edit_matiere.matiere_delete, Permission.EditFormation, methods=["GET", "POST"], ) sco_publish( "/matiere_edit", edit_matiere.matiere_edit, Permission.EditFormation, methods=["GET", "POST"], ) sco_publish( "/module_create", edit_module.module_create, Permission.EditFormation, methods=["GET", "POST"], ) sco_publish( "/module_delete", edit_module.module_delete, Permission.EditFormation, methods=["GET", "POST"], ) sco_publish( "/module_edit", edit_module.module_edit, Permission.EditFormation, methods=["GET", "POST"], ) sco_publish("/module_list", edit_module.module_table, Permission.ScoView) sco_publish("/module_tag_search", sco_tag_module.module_tag_search, Permission.ScoView) @bp.route("/formation_tag_modules_by_type/<int:formation_id>/<int:semestre_idx>") @scodoc @permission_required(Permission.EditFormationTags) def formation_tag_modules_by_type(formation_id: int, semestre_idx: int): """Taggue tous les modules de la formation en fonction de leur type : 'res', 'sae', 'malus' Ne taggue pas les modules standards. """ formation = Formation.get_formation(formation_id) sco_tag_module.formation_tag_modules_by_type(formation) flash("Formation tagguée") return flask.redirect( url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, semestre_idx=semestre_idx, formation_id=formation.id, show_tags=1, ) ) @bp.route("/module_tag_set", methods=["POST"]) @scodoc @permission_required(Permission.EditFormationTags) def module_tag_set(): # TODO passer dans l'API """Set tags on module""" module_id = request.form.get("module_id") module: Module = Module.get_instance(module_id) taglist = request.form.get("taglist") return module.set_tags(taglist) @bp.route("/module_clone", methods=["POST"]) @scodoc @permission_required(Permission.EditFormation) def module_clone(): """Clone existing module""" module_id = request.form.get("module_id") module: Module = Module.get_instance(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", alias=True) @scodoc @permission_required(Permission.ScoView) def index_html(): "Page accueil formations" fmt = request.args.get("fmt", "html") detail = scu.to_bool(request.args.get("detail", False)) editable = current_user.has_permission(Permission.EditFormation) table = formation_io.formation_list_table(detail=detail) if fmt != "html": return table.make_page(fmt=fmt, filename=f"Formations-{g.scodoc_dept}") H = [ f"""<h1>Formations (programmes pédagogiques)</h1> <form> <input type="checkbox" id="detailCheckbox" name="detail" onchange="this.form.submit();" {'checked' if detail else ''}> <label for="detailCheckbox">Informations détaillées</label> </form> """, table.html(), ] if editable: H.append( f""" <div class="scobox"> <div class="help"> <p>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> </div> <ul class="sco-links"> <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> </div> <div class="scobox"> <div class="scobox-title">Référentiels de compétences</div> <div class="help"> Les formations par compétences de type BUT doivent être associées à un référentiel de compétences définissant leur structure en blocs de compétences. Le référentiel doit être chargé avant la définition de la formation s'y référant. </div> <ul class="sco-links"> <li><a class="stdlink" href="{ url_for('notes.refcomp_table', scodoc_dept=g.scodoc_dept) }">Liste des référentiels chargés</a> </li> <li> <a class="stdlink" href="{url_for( 'notes.refcomp_load', scodoc_dept=g.scodoc_dept) }">Charger un nouveau référentiel de compétences Orébut</a> </li> </ul> </div> """ ) return render_template( "sco_page_dept.j2", content="\n".join(H), title="Formations (programmes)", ) # -------------------------------------------------------------------- # # 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 formation_io.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 render_template( "sco_page_dept.j2", title="Import d'une formation", content=f""" <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] } """, ) elif tf[0] == -1: return flask.redirect(url_for("notes.index_html", scodoc_dept=g.scodoc_dept)) else: formation_id, _, _ = formation_io.formation_import_xml(tf[2]["xmlfile"].read()) return render_template( "sco_page_dept.j2", title="Import d'une formation", content=f""" <h2>Import effectué !</h2> <ul> <li><a class="stdlink" href="{ url_for("notes.ue_table", 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> """, ) sco_publish("/module_move", edit_formation.module_move, Permission.EditFormation) sco_publish("/ue_move", 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 = request.form.get("ue_id") ue = UniteEns.get_ue(ue_id) _ = ue.clone() 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" modimpl = ModuleImpl.get_modimpl(moduleimpl_id) modimpl.can_change_ens(raise_exc=True) # page_title = f"Enseignants du module {modimpl.module.titre or modimpl.module.code}" title = f"""<h2>Enseignants du <a class="stdlink" href="{ url_for("notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id) }">module {modimpl.module.titre or modimpl.module.code}</a></h2>""" # 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 = [ f"""<ul><li><b>{ uid2display.get(modimpl.responsable_id, modimpl.responsable_id) }</b> (responsable)</li>""" ] u: User for u in modimpl.enseignants: H.append( f""" <li>{u.get_nomcomplet()} (<a class="stdlink" href="{ url_for('notes.edit_enseignants_form_delete', scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id, ens_id=u.id) }">supprimer</a>) </li>""" ) H.append("</ul>") F = f"""<div class="help space-before-18"> <p>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=modimpl.formsemestre_id) }">Modification du semestre</a>", accessible uniquement au responsable de la formation (chef de département) </p> </div> """ 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 render_template( "sco_page.j2", title=page_title, content=title + "\n".join(H) + tf[1] + F, javascripts=["libjs/AutoSuggest.js"], cssstyles=["css/autosuggest_inquisitor.css"], ) elif tf[0] == -1: return flask.redirect( url_for( "notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id, ) ) else: ens = User.get_user_from_nomplogin(tf[2]["ens_id"]) if ens is None: 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.id for x in modimpl.enseignants) or ens.id == modimpl.responsable_id ): H.append( scu.html_flash_message( f"Enseignant {ens.user_name} déjà dans la liste !" ) ) else: modimpl.enseignants.append(ens) db.session.add(modimpl) db.session.commit() return flask.redirect( url_for( "notes.edit_enseignants_form", scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id, ) ) return render_template( "sco_page.j2", title=page_title, content=title + "\n".join(H) + tf[1] + F, javascripts=["libjs/AutoSuggest.js"], cssstyles=["css/autosuggest_inquisitor.css"], ) @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_responsable(current_user, raise_exc=True) # access control H = [ f"""<h2 class="formsemestre">Modification du responsable du <a class="stdlink" href="{ url_for("notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id) }">module {modimpl.module.titre or ""}</a></h2>""" ] 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 render_template( "sco_page.j2", content="\n".join(H) + tf[1] + help_str, title="Modification responsable module", javascripts=["libjs/AutoSuggest.js"], cssstyles=["css/autosuggest_inquisitor.css"], ) elif tf[0] == -1: return flask.redirect( url_for( "notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id, ) ) else: responsable = User.get_user_from_nomplogin(tf[2]["responsable_id"]) if not responsable: # 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 = responsable 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() inscrits: list[Identite] = sorted( [i.etud for i in modimpl.inscriptions], key=lambda e: e.sort_key ) rows = [] for etud in inscrits: ( nb_abs_nj, nb_abs_just, nb_abs, ) = sco_assiduites.formsemestre_get_assiduites_count( etud.id, modimpl.formsemestre, moduleimpl_id=modimpl.id ) rows.append( { "civilite": etud.civilite_str, "nom": etud.nom_disp(), "prenom": etud.prenom_str, "just": nb_abs_just, "nojust": nb_abs_nj, "total": nb_abs, "_nom_target": url_for( "scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id ), } ) content = f""" <h2>Absences du <a href="{ url_for("notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id )}">module {modimpl.module.titre_str()}</a></h2>""" if not rows and fmt == "html": content += "<p>Aucune absence signalée</p>" tab = GenTable( titles={ "civilite": "Civ.", "nom": "Nom", "prenom": "Prénom", "just": "Just.", "nojust": "Non Just.", "total": "Total", }, columns_ids=("civilite", "nom", "prenom", "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(), table_id="view_module_abs", ) if fmt != "html": return tab.make_page(fmt=fmt) if not tab.is_empty(): content += tab.html() return render_template( "sco_page.j2", content=content, title=f"Absences du module {modimpl.module.titre_str()}", ) @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 # uid : { "mods" : liste des modimpls, ... } sem_ens: dict[int, list[ModuleImpl]] = {} 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()}" table = 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="""<h2 class="formsemestre">Enseignants du semestre</h2>""", 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), table_id="formsemestre_enseignants_list", ) return table.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 """ modimpl = ModuleImpl.get_modimpl(moduleimpl_id) modimpl.can_change_ens(raise_exc=True) # search ens_id ens: User | None = None for ens in modimpl.enseignants: if ens.id == ens_id: break if ens is None: raise ScoValueError(f"invalid ens_id ({ens_id})") modimpl.enseignants.remove(ens) db.session.commit() return flask.redirect( url_for( "notes.edit_enseignants_form", scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id, ) ) # --- Gestion des inscriptions aux semestres 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 avec <a class="stdlink" href="{ url_for("notes.formsemestre_validation_suppress_etud", scodoc_dept=g.scodoc_dept, etudid=etudid, formsemestre_id=formsemestre_id) }">supprimer décision jury</a>) """, safe=True, ) 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.fiche_etud", 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.ScoView, 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} ({evaluation.descr_date()})""" etat = sco_evaluations.do_evaluation_etat(evaluation.id) H = [ f""" <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 render_template("sco_page.j2", title=tit, content="\n".join(H)) 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 render_template("sco_page.j2", title=tit, content="\n".join(H) + tf[1]) 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 render_template( "sco_page.j2", title=tit, content="\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>""", ) @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") @scodoc @permission_required_compat_scodoc7(Permission.ScoView) def evaluation_listenotes(): """Affichage des notes d'une évaluation. Args: - evaluation_id (une seule éval) - ou moduleimpl_id (toutes les évals du module) - group_ids: groupes à lister - fmt : html, xls, pdf, json """ # Arguments evaluation_id = request.args.get("evaluation_id") moduleimpl_id = request.args.get("moduleimpl_id") try: if evaluation_id is not None: evaluation_id = int(evaluation_id) if moduleimpl_id is not None: moduleimpl_id = int(moduleimpl_id) except ValueError as exc: raise ScoValueError("evaluation_listenotes: id invalides !") from exc fmt = request.args.get("fmt", "html") # content, page_title = sco_liste_notes.do_evaluation_listenotes( evaluation_id=evaluation_id, moduleimpl_id=moduleimpl_id, fmt=fmt ) if fmt == "html": return render_template( "sco_page.j2", content=content, title=page_title, cssstyles=["css/verticalhisto.css"], javascripts=["js/groups_view.js"], ) return 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_excel.saisie_notes_tableur, Permission.EnsView, methods=["GET", "POST"], ) sco_publish( "/feuille_saisie_notes", sco_saisie_excel.feuille_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"], ) @bp.route("/form_saisie_notes/<int:evaluation_id>") @scodoc @permission_required(Permission.EnsView) # + controle contextuel def form_saisie_notes(evaluation_id: int): "Formulaire de saisie des notes d'une évaluation" evaluation = Evaluation.get_evaluation(evaluation_id) group_ids = request.args.getlist("group_ids") try: group_ids = [int(gid) for gid in group_ids] except ValueError as exc: raise ScoValueError("group_ids invalide") from exc return sco_saisie_notes.saisie_notes(evaluation, group_ids) @bp.route("/formsemestre_import_notes/<int:formsemestre_id>", methods=["GET", "POST"]) @scodoc @permission_required(Permission.ScoView) # controle contextuel def formsemestre_import_notes(formsemestre_id: int | None = None): "Import via excel des notes de toutes les évals d'un semestre." return _formsemestre_or_modimpl_import_notes(formsemestre_id=formsemestre_id) @bp.route( "/moduleimpl_import_notes/<int:moduleimpl_id>", methods=["GET", "POST"], ) @scodoc @permission_required(Permission.ScoView) # controle contextuel def moduleimpl_import_notes(moduleimpl_id: int | None = None): "Import via excel des notes de toutes les évals d'un module." return _formsemestre_or_modimpl_import_notes(moduleimpl_id=moduleimpl_id) def _formsemestre_or_modimpl_import_notes( formsemestre_id: int | None = None, moduleimpl_id: int | None = None ): """Import via excel des notes de toutes les évals d'un semestre. Ou, si moduleimpl_import_notes, toutes les évals de ce module. """ formsemestre = ( FormSemestre.get_formsemestre(formsemestre_id) if formsemestre_id is not None else None ) modimpl = ( ModuleImpl.get_modimpl(moduleimpl_id) if moduleimpl_id is not None else None ) if not (formsemestre or modimpl): raise ScoValueError("paramètre manquant") dest_url = ( url_for( "notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id, ) if modimpl else url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id, ) ) if formsemestre and not formsemestre.est_chef_or_diretud(): raise ScoPermissionDenied("opération non autorisée", dest_url=dest_url) if modimpl and not modimpl.can_edit_notes(current_user): raise ScoPermissionDenied("opération non autorisée", dest_url=dest_url) class ImportForm(FlaskForm): notefile = FileField( "Fichier d'import", validators=[ DataRequired(), FileAllowed(["xlsx"], "Fichier xlsx seulement !"), ], ) comment = StringField("Commentaire", validators=[Length(max=256)]) submit = SubmitField("Télécharger") form = ImportForm() if form.validate_on_submit(): # Handle file upload and form processing notefile = form.notefile.data comment = form.comment.data # return sco_saisie_excel.formsemestre_import_notes( formsemestre=formsemestre, modimpl=modimpl, notefile=notefile, comment=comment, ) return render_template( "formsemestre/import_notes.j2", evaluations=( formsemestre.get_evaluations() if formsemestre else modimpl.evaluations.all() ), form=form, formsemestre=formsemestre, modimpl=modimpl, title="Importation des notes", sco=ScoData(formsemestre=formsemestre), ) @bp.route("/formsemestre_feuille_import_notes/<int:formsemestre_id>") @scodoc @permission_required(Permission.ScoView) def formsemestre_feuille_import_notes(formsemestre_id: int): """Feuille excel pour importer les notes de toutes les évaluations du semestre""" formsemestre = FormSemestre.get_formsemestre(formsemestre_id) xls = sco_saisie_excel.excel_feuille_import(formsemestre=formsemestre) filename = scu.sanitize_filename(formsemestre.titre_annee()) return scu.send_file(xls, filename, scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE) @bp.route("/moduleimpl_feuille_import_notes/<int:moduleimpl_id>") @scodoc @permission_required(Permission.ScoView) def moduleimpl_feuille_import_notes(moduleimpl_id: int): """Feuille excel pour importer les notes de toutes les évaluations du modimpl""" modimpl = ModuleImpl.get_modimpl(moduleimpl_id) xls = sco_saisie_excel.excel_feuille_import(modimpl=modimpl) filename = scu.sanitize_filename( f"{modimpl.module.code} {modimpl.formsemestre.annee_scolaire_str()}" ) return scu.send_file(xls, filename, scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE) # --- Bulletins @bp.route("/formsemestre_bulletins_pdf") @scodoc @permission_required(Permission.ScoView) @scodoc7func def formsemestre_bulletins_pdf( formsemestre_id, group_ids: list[int] = None, # si indiqué, ne prend que ces groupes version="selectedevals", ): "Publie les bulletins dans un classeur PDF" # Informations sur les groupes à utiliser: groups_infos = sco_groups_view.DisplayedGroupsInfos( group_ids, formsemestre_id=formsemestre_id, select_all_when_unspecified=True, ) pdfdoc, filename = sco_bulletins_pdf.get_formsemestre_bulletins_pdf( formsemestre_id, groups_infos=groups_infos, 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, group_ids: list[int] = None, # si indiqué, ne prend que ces groupes ): """Choix version puis envoi classeur bulletins pdf""" formsemestre = FormSemestre.get_formsemestre(formsemestre_id) # Informations sur les groupes à utiliser: groups_infos = sco_groups_view.DisplayedGroupsInfos( group_ids, formsemestre_id=formsemestre_id, select_all_when_unspecified=True, ) if version: pdfdoc, filename = sco_bulletins_pdf.get_formsemestre_bulletins_pdf( formsemestre_id, groups_infos=groups_infos, version=version ) return scu.sendPDFFile(pdfdoc, filename) return _formsemestre_bulletins_choice( formsemestre, explanation=_EXPL_BULL, groups_infos=groups_infos, title="Choisir la version des bulletins à générer", ) @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, group_ids: list[int] = None, # si indiqué, ne prend que ces groupes ): """Choix version puis envoi classeur bulletins pdf""" # Informations sur les groupes à utiliser: groups_infos = sco_groups_view.DisplayedGroupsInfos( group_ids, formsemestre_id=formsemestre_id, select_all_when_unspecified=True, ) if version: return flask.redirect( url_for( "notes.formsemestre_bulletins_mailetuds", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, version=version, dialog_confirmed=int(dialog_confirmed), prefer_mail_perso=prefer_mail_perso, group_ids=groups_infos.group_ids, ) ) 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, groups_infos=groups_infos, ) # not published def _formsemestre_bulletins_choice( formsemestre: FormSemestre, title="", explanation="", choose_mail=False, groups_infos=None, ): """Choix d'une version de bulletin (pour envois mail ou génération classeur pdf) """ versions_bulletins = ( scu.BULLETINS_VERSIONS_BUT if formsemestre.formation.is_apc() else scu.BULLETINS_VERSIONS ) return render_template( "formsemestre/bulletins_choice.j2", explanation=explanation, choose_mail=choose_mail, formsemestre=formsemestre, menu_groups_choice=sco_groups_view.menu_groups_choice( groups_infos, submit_on_change=True ), sco=ScoData(formsemestre=formsemestre), sco_groups_view=sco_groups_view, title=title, versions_bulletins=versions_bulletins, ) @bp.route("/formsemestre_bulletins_mailetuds", methods=["GET", "POST"]) @scodoc @permission_required(Permission.ScoView) @scodoc7func def formsemestre_bulletins_mailetuds( formsemestre_id, version="long", dialog_confirmed=False, prefer_mail_perso=0, group_ids: list[int] = None, # si indiqué, ne prend que ces groupes ): """Envoie à chaque etudiant son bulletin (inscrit non démissionnaire ni défaillant et ayant un mail renseigné dans ScoDoc) """ groups_infos = sco_groups_view.DisplayedGroupsInfos( group_ids, formsemestre_id=formsemestre_id, select_all_when_unspecified=True, ) etudids = {m["etudid"] for m in groups_infos.members} 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 and inscription.etudid in etudids ] # 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 sélectionnés ?", 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, "group_ids": group_ids, }, ) # 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 render_template( "sco_page.j2", title="Mailing bulletins", content=f""" <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> """, ) 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 = [ f"""<h2>{action} d'une appréciation sur {etud.nomprenom}</h2>""", ] 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 render_template("sco_page.j2", content="\n".join(H) + "\n" + tf[1]) 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) sco_publish( "/formsemestre_ext_create_form", sco_formsemestre_exterieurs.formsemestre_ext_create_form, Permission.ScoView, methods=["GET", "POST"], ) # ------------- 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) 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, ) # 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, ) @bp.route("/apo_compare_csv_form") @scodoc @permission_required(Permission.ScoView) def apo_compare_csv_form(): "Choix de fichiers Apogée à comparer" return render_template( "apogee/apo_compare_form.j2", title="Comparaison de fichiers Apogée" ) @bp.route("/apo_compare_csv", methods=["POST"]) @scodoc @permission_required(Permission.ScoView) def apo_compare_csv(): "Page comparaison 2 fichiers CSV" try: file_a = request.files["file_a"] file_b = request.files["file_b"] autodetect = request.form.get("autodetect", False) except KeyError as exc: raise ScoValueError("invalid arguments") from exc return sco_apogee_compare.apo_compare_csv(file_a, file_b, autodetect=autodetect) # ------------- 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.ViewEtudData, ) 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( "/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 """ formsemestre = 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 = Module.get_instance(modimpl["module_id"]) formations_set.add(mod.formation_id) ue = mod.ue ue_dict = ue.to_dict() formations_set.add(ue_dict["formation_id"]) if ue_dict["formation_id"] != mod.formation_id: modimpl["mod"] = mod.to_dict() modimpl["ue"] = ue_dict bad_ue.append(modimpl) if formsemestre.formation_id != mod.formation_id: bad_sem.append(modimpl) modimpl["mod"] = mod.to_dict() H = [ f"""<p>formation_id={formsemestre.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(f"check_sem_integrity: problem detected: formations_set={formations_set}") if formsemestre.formation_id in formations_set: formations_set.remove(formsemestre.formation_id) if len(formations_set) == 1: if fix: log(f"check_sem_integrity: trying to fix {formsemestre_id}") formation_id = formations_set.pop() if formsemestre.formation_id != formation_id: formsemestre.formation_id = formation_id db.session.add(formsemestre) db.session.commit() sco_cache.invalidate_formsemestre(formsemestre.id) H.append("""<p class="alert">Problème réparé: vérifiez</p>""") else: H.append( f""" <p class="alert">Problème détecté réparable: <a href="{url_for( "notes.check_sem_integrity", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, fix=1)}">réparer maintenant</a></p> """ ) else: H.append("""<p class="alert">Problème détecté !</p>""") return render_template("sco_page.j2", content="\n".join(H)) @bp.route("/check_form_integrity") @scodoc @permission_required(Permission.ScoView) @scodoc7func def check_form_integrity(formation_id, fix=False): "debug (obsolete)" log(f"check_form_integrity: formation_id={formation_id} fix={fix}") formation: Formation = Formation.query.filter_by( dept_id=g.scodoc_dept_id, formation_id=formation_id ).first_or_404() bad = [] for ue in formation.ues: for matiere in ue.matieres: for mod in matiere.modules: if mod.ue_id != ue.id: if fix: # fix mod.ue_id log(f"fix: mod.ue_id = {ue.id} (was {mod.ue_id})") mod.ue_id = ue.id db.session.add(mod) bad.append(mod) if mod.formation_id != formation_id: bad.append(mod) if bad: txth = "<br>".join([html.escape(str(x)) for x in bad]) txt = "\n".join([str(x) for x in bad]) log(f"check_form_integrity: formation_id={formation_id}\ninconsistencies:") log(txt) # Notify by e-mail send_scodoc_alarm("Notes: formation incoherente !", txt) else: txth = "OK" log("ok") return render_template("sco_page.j2", content=txth) @bp.route("/check_formsemestre_integrity") @scodoc @permission_required(Permission.ScoView) @scodoc7func def check_formsemestre_integrity(formsemestre_id): "debug" log(f"check_formsemestre_integrity: formsemestre_id={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 = [] formsemestre = FormSemestre.get_formsemestre(formsemestre_id) for modimpl in formsemestre.modimpls: if modimpl.module.ue_id != modimpl.module.matiere.ue_id: diag.append( f"""moduleimpl {modimpl.id}: module.ue_id={modimpl.module.ue_id } != matiere.ue_id={modimpl.module.matiere.ue_id}""" ) if modimpl.module.ue.formation_id != modimpl.module.formation_id: diag.append( f"""moduleimpl {modimpl.id}: ue.formation_id={ modimpl.module.ue.formation_id} != mod.formation_id={ modimpl.module.formation_id}""" ) if diag: send_scodoc_alarm( f"Notes: formation incoherente dans semestre {formsemestre_id} !", "\n".join(diag), ) log(f"check_formsemestre_integrity: formsemestre_id={formsemestre_id}") log("inconsistencies:\n" + "\n".join(diag)) else: diag = ["OK"] log("ok") return render_template("sco_page.j2", content="<br>".join(diag))