# -*- 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/", 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/")
@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/")
@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//")
@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"""Formations (programmes pédagogiques)
""",
table.html(),
]
if editable:
H.append(
f"""
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.
Référentiels de compétences
"""
)
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"""
Import d'une formation
Création d'une formation (avec UE, matières, modules)
à partir un fichier XML (réservé aux utilisateurs avertis).
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 page des référentiels).
{ 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"""
Import effectué !
""",
)
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"Confirmer le {msg} du semestre ?
",
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).
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""""""
# 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"""- {
uid2display.get(modimpl.responsable_id, modimpl.responsable_id)
} (responsable)
"""
]
u: User
for u in modimpl.enseignants:
H.append(
f"""
- {u.get_nomcomplet()} (supprimer)
"""
)
H.append("
")
F = f"""
Les enseignants d'un module ont le droit de
saisir et modifier toutes les notes des évaluations de ce module.
Pour changer le responsable du module, passez par la
page "Modification du semestre",
accessible uniquement au responsable de la formation (chef de département)
"""
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(
'Pour ajouter un enseignant, choisissez un nom dans le menu
'
)
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""""""
]
help_str = """Taper le début du nom de l'enseignant.
"""
# 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"""
"""
if not rows and fmt == "html":
content += "Aucune absence signalée
"
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//", 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="""""",
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
supprimer décision jury)
""",
safe=True,
)
if not dialog_confirmed:
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
if formsemestre.modalite != "EXT":
msg_ext = """
%s sera désinscrit de tous les modules du semestre %s (%s - %s).
Cette opération ne doit être utilisée que pour corriger une erreur !
Un étudiant réellement inscrit doit le rester, le faire éventuellement démissionner.
""" % (
etud["nomprenom"],
sem["titre_num"],
sem["date_debut"],
sem["date_fin"],
)
else: # semestre extérieur
msg_ext = """
%s sera désinscrit du semestre extérieur %s (%s - %s).
""" % (
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 = """Attention: le semestre extérieur
sera supprimé car il n'a pas d'autre étudiant inscrit.
"""
return scu.confirm_dialog(
"""Confirmer la demande de désinscription ?
""" + 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///",
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///",
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"""
{tit}
Opération irréversible.
Si vous supprimez l'évaluation, vous ne pourrez pas retrouver les notes associées.
""",
]
warning = False
if etat["nb_notes_total"]:
warning = True
nb_desinscrits = etat["nb_notes_total"] - etat["nb_notes"]
H.append(
f"""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""")
if etat["nb_notes"] == 0:
H.append(
"""
Vous pouvez quand même supprimer l'évaluation, les notes des étudiants désincrits seront effacées.
"""
)
if etat["nb_notes"]:
H.append(
f"""
Suppression impossible (effacer les notes d'abord)
retour au tableau de bord du module
"""
)
return render_template("sco_page.j2", title=tit, content="\n".join(H))
if warning:
H.append("""""")
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"""OK, évaluation supprimée.
Continuer
""",
)
@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/")
@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/")
@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/", 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/",
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/")
@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/")
@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:
- courte: moyennes des modules (en BUT: seulement les moyennes d'UE)
- intermédiaire: moyennes des modules et notes des évaluations sélectionnées
- complète: toutes les notes
"""
@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:
- courte: moyennes des modules
- intermédiaire: moyennes des modules et notes des évaluations sélectionnées
- complète: toutes les notes
"""
if formsemestre.formation.is_apc():
expl_bull += """
- courte spéciale BUT: un résumé en une page pour les BUTs
"""
expl_bull += "
"
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.
"""
+ 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"
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"""
{nb_sent} bulletins sur {len(inscriptions)} envoyés par mail !
continuer
""",
)
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"""{action} d'une appréciation sur {etud.nomprenom}
""",
]
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
"""
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 = 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 sem["formation_id"] != mod.formation_id:
bad_sem.append(modimpl)
modimpl["mod"] = mod.to_dict()
H = [
f"""formation_id={sem["formation_id"]}""",
]
if bad_ue:
H += [
"
Modules d'une autre formation que leur UE:
",
"
".join([str(x) for x in bad_ue]),
]
if bad_sem:
H += [
"Module du semestre dans une autre formation:
",
"
".join([str(x) for x in bad_sem]),
]
if not bad_ue and not bad_sem:
H.append("Aucun problème à signaler !
")
else:
log(f"check_sem_integrity: problem detected: formations_set={formations_set}")
if sem["formation_id"] in formations_set:
formations_set.remove(sem["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 sem["formation_id"] != formation_id:
sem["formation_id"] = formation_id
sco_formsemestre.do_formsemestre_edit(sem)
H.append("""Problème réparé: vérifiez
""")
else:
H.append(
f"""
Problème détecté réparable:
réparer maintenant
"""
)
else:
H.append("""Problème détecté !
""")
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 = "
".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="
".join(diag))