ScoDoc/app/views/notes.py

2688 lines
85 KiB
Python

# -*- 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.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_edit_formation,
sco_edit_matiere,
sco_edit_module,
sco_edit_ue,
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_formations,
sco_formation_recap,
sco_formation_versions,
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.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",
sco_formation_versions.formsemestre_associate_new_version,
Permission.EditFormation,
methods=["GET", "POST"],
)
sco_publish(
"/formsemestre_delete",
sco_formsemestre_edit.formsemestre_delete,
Permission.EditFormSemestre,
methods=["GET", "POST"],
)
sco_publish(
"/formsemestre_delete2",
sco_formsemestre_edit.formsemestre_delete2,
Permission.EditFormSemestre,
methods=["GET", "POST"],
)
sco_publish(
"/formsemestre_note_etuds_sans_note",
sco_formsemestre_status.formsemestre_note_etuds_sans_notes,
Permission.ScoView,
methods=["GET", "POST"],
)
sco_publish(
"/formsemestre_recapcomplet",
sco_recapcomplet.formsemestre_recapcomplet,
Permission.ScoView,
)
sco_publish(
"/evaluations_recap",
sco_evaluation_recap.evaluations_recap,
Permission.ScoView,
)
sco_publish(
"/formsemestres_bulletins",
sco_recapcomplet.formsemestres_bulletins,
Permission.Observateur,
)
sco_publish(
"/moduleimpl_status", sco_moduleimpl_status.moduleimpl_status, Permission.ScoView
)
sco_publish(
"/formsemestre_description",
sco_formsemestre_status.formsemestre_description,
Permission.ScoView,
)
sco_publish(
"/formation_create",
sco_edit_formation.formation_create,
Permission.EditFormation,
methods=["GET", "POST"],
)
sco_publish(
"/formation_delete",
sco_edit_formation.formation_delete,
Permission.EditFormation,
methods=["GET", "POST"],
)
sco_publish(
"/formation_edit",
sco_edit_formation.formation_edit,
Permission.EditFormation,
methods=["GET", "POST"],
)
@bp.route("/formsemestre_bulletinetud")
@scodoc
@permission_required_compat_scodoc7(Permission.ScoView)
@scodoc7func
def formsemestre_bulletinetud(
etudid=None,
formsemestre_id=None,
fmt=None,
version="long",
xml_with_decisions=False,
force_publishing=False,
prefer_mail_perso=False,
code_nip=None,
code_ine=None,
):
fmt = fmt or "html"
if version not in scu.BULLETINS_VERSIONS_BUT:
raise ScoValueError(
"formsemestre_bulletinetud: version de bulletin demandée invalide"
)
if not isinstance(etudid, int):
raise ScoInvalidIdType("formsemestre_bulletinetud: etudid must be an integer !")
if formsemestre_id is not None and not isinstance(formsemestre_id, int):
raise ScoInvalidIdType(
"formsemestre_bulletinetud: formsemestre_id must be an integer !"
)
formsemestre = FormSemestre.query.filter_by(
formsemestre_id=formsemestre_id, dept_id=g.scodoc_dept_id
).first_or_404()
if etudid:
etud = Identite.get_etud(etudid)
elif code_nip:
etud = models.Identite.query.filter_by(
code_nip=str(code_nip), dept_id=formsemestre.dept_id
).first_or_404()
elif code_ine:
etud = models.Identite.query.filter_by(
code_ine=str(code_ine), dept_id=formsemestre.dept_id
).first_or_404()
else:
raise ScoValueError(
"Paramètre manquant: spécifier etudid, code_nip ou code_ine"
)
if version == "butcourt":
return redirect(
url_for(
"notes.bulletin_but_pdf" if fmt == "pdf" else "notes.bulletin_but_html",
scodoc_dept=g.scodoc_dept,
etudid=etud.id,
formsemestre_id=formsemestre_id,
)
)
if fmt == "json":
return sco_bulletins.get_formsemestre_bulletin_etud_json(
formsemestre, etud, version=version, force_publishing=force_publishing
)
if formsemestre.formation.is_apc() and fmt == "html":
return render_template(
"but/bulletin.j2",
appreciations=BulAppreciations.get_appreciations_list(
formsemestre.id, etud.id
),
bul_url=url_for(
"notes.formsemestre_bulletinetud",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
etudid=etud.id,
fmt="json",
force_publishing=1, # pour ScoDoc lui même
version=version,
),
can_edit_appreciations=formsemestre.est_responsable(current_user)
or (current_user.has_permission(Permission.EtudInscrit)),
etud=etud,
formsemestre=formsemestre,
inscription_courante=etud.inscription_courante(),
inscription_str=etud.inscription_descr()["inscription_str"],
is_apc=formsemestre.formation.is_apc(),
menu_autres_operations=sco_bulletins.make_menu_autres_operations(
formsemestre, etud, "notes.formsemestre_bulletinetud", version
),
sco=ScoData(etud=etud),
scu=scu,
time=time,
title=f"Bul. {etud.nom} - BUT",
version=version,
)
if fmt == "oldjson":
fmt = "json"
response = sco_bulletins.formsemestre_bulletinetud(
etud,
formsemestre_id=formsemestre_id,
fmt=fmt,
version=version,
xml_with_decisions=xml_with_decisions,
force_publishing=force_publishing,
prefer_mail_perso=prefer_mail_perso,
)
if fmt == "pdfmail": # ne renvoie rien dans ce cas (mails envoyés)
return redirect(
url_for(
"notes.formsemestre_bulletinetud",
scodoc_dept=g.scodoc_dept,
etudid=etud.id,
formsemestre_id=formsemestre_id,
)
)
return response
sco_publish(
"/formsemestre_evaluations_cal",
sco_evaluations.formsemestre_evaluations_cal,
Permission.ScoView,
)
sco_publish(
"/formsemestre_evaluations_delai_correction",
sco_evaluations.formsemestre_evaluations_delai_correction,
Permission.ScoView,
)
@bp.route("/moduleimpl_evaluation_renumber", methods=["GET", "POST"])
@scodoc
@permission_required_compat_scodoc7(Permission.ScoView)
@scodoc7func
def moduleimpl_evaluation_renumber(moduleimpl_id):
"Renumérote les évaluations, triant par date"
modimpl: ModuleImpl = (
ModuleImpl.query.filter_by(id=moduleimpl_id)
.join(FormSemestre)
.filter_by(dept_id=g.scodoc_dept_id)
.first_or_404()
)
if not modimpl.can_edit_evaluation(current_user):
raise ScoPermissionDenied(
dest_url=url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=modimpl.id,
)
)
Evaluation.moduleimpl_evaluation_renumber(modimpl)
# redirect to moduleimpl page:
return flask.redirect(
url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=moduleimpl_id,
)
)
sco_publish(
"/moduleimpl_evaluation_move",
sco_evaluation_db.moduleimpl_evaluation_move,
Permission.ScoView,
)
sco_publish(
"/formsemestre_list_saisies_notes",
sco_undo_notes.formsemestre_list_saisies_notes,
Permission.ScoView,
)
sco_publish(
"/ue_create",
sco_edit_ue.ue_create,
Permission.EditFormation,
methods=["GET", "POST"],
)
sco_publish(
"/ue_delete",
sco_edit_ue.ue_delete,
Permission.EditFormation,
methods=["GET", "POST"],
)
@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 sco_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 sco_edit_ue.ue_table(
formation_id=formation_id, semestre_idx=semestre_idx, msg=msg
)
@bp.route("/ue_infos/<int:ue_id>")
@scodoc
@permission_required(Permission.ScoView)
def ue_infos(ue_id: 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 sco_edit_ue.ue_sharing_code(
ue_code=ue_code,
ue_id=None if ((ue_id is None) or ue_id == "") else int(ue_id),
hide_ue_id=(
None if ((hide_ue_id is None) or hide_ue_id == "") else int(hide_ue_id)
),
)
sco_publish(
"/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 sco_formation_recap.formation_table_recap(formation, fmt=fmt)
sco_publish(
"/export_recap_formations_annee_scolaire",
sco_formation_recap.export_recap_formations_annee_scolaire,
Permission.ScoView,
)
sco_publish(
"/formation_add_malus_modules",
sco_edit_module.formation_add_malus_modules,
Permission.EditFormation,
)
sco_publish(
"/matiere_create",
sco_edit_matiere.matiere_create,
Permission.EditFormation,
methods=["GET", "POST"],
)
sco_publish(
"/matiere_delete",
sco_edit_matiere.matiere_delete,
Permission.EditFormation,
methods=["GET", "POST"],
)
sco_publish(
"/matiere_edit",
sco_edit_matiere.matiere_edit,
Permission.EditFormation,
methods=["GET", "POST"],
)
sco_publish(
"/module_create",
sco_edit_module.module_create,
Permission.EditFormation,
methods=["GET", "POST"],
)
sco_publish(
"/module_delete",
sco_edit_module.module_delete,
Permission.EditFormation,
methods=["GET", "POST"],
)
sco_publish(
"/module_edit",
sco_edit_module.module_edit,
Permission.EditFormation,
methods=["GET", "POST"],
)
sco_publish("/module_list", sco_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.query.filter_by(
id=formation_id, dept_id=g.scodoc_dept_id
).first_or_404()
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():
"""Set tags on module"""
module_id = int(request.form.get("module_id"))
taglist = request.form.get("taglist")
return sco_tag_module.module_tag_set(module_id, taglist)
@bp.route("/module_clone", methods=["POST"])
@scodoc
@permission_required(Permission.EditFormation)
def module_clone():
"""Clone existing module"""
module_id = int(request.form.get("module_id"))
module = Module.query.get_or_404(module_id)
module2 = module.clone()
db.session.add(module2)
db.session.commit()
flash(f"Module {module.code} dupliqué")
return flask.redirect(
url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
semestre_idx=module.semestre_id,
formation_id=module.formation_id,
)
)
#
@bp.route("/")
@bp.route("/index_html", 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 = sco_formations.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"""
<p class="help">Une "formation" est un programme pédagogique structuré
en UE, matières et modules. Chaque semestre se réfère à une formation.
La modification d'une formation affecte tous les semestres qui s'y
réfèrent.</p>
<ul>
<li><a class="stdlink" href="formation_create" id="link-create-formation">Créer une
formation</a>
</li>
<li><a class="stdlink" href="formation_import_xml_form">Importer une formation (xml)</a>
</li>
<li><a class="stdlink" href="{
url_for("notes.export_recap_formations_annee_scolaire",
scodoc_dept=g.scodoc_dept, annee_scolaire=scu.annee_scolaire()-1)
}">exporter les formations de l'année scolaire
{scu.annee_scolaire()-1} - {scu.annee_scolaire()}
</a>
</li>
<li><a class="stdlink" href="{
url_for("notes.export_recap_formations_annee_scolaire",
scodoc_dept=g.scodoc_dept, annee_scolaire=scu.annee_scolaire())
}">exporter les formations de l'année scolaire
{scu.annee_scolaire()} - {scu.annee_scolaire()+1}
</a>
</li>
</ul>
<h3>Référentiels de compétences</h3>
<ul>
<li><a class="stdlink" href="{
url_for('notes.refcomp_table', scodoc_dept=g.scodoc_dept)
}">Liste des référentiels chargés</a>
</li>
</ul>
"""
)
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 sco_formations.formation_export(
formation_id,
export_ids=export_ids,
fmt=fmt,
export_codes_apo=export_codes_apo,
)
@bp.route("/formation_import_xml_form", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.EditFormation)
@scodoc7func
def formation_import_xml_form():
"form import d'une formation en XML"
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
(("xmlfile", {"input_type": "file", "title": "Fichier XML", "size": 30}),),
submitlabel="Importer",
cancelbutton="Annuler",
)
if tf[0] == 0:
return 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, _, _ = sco_formations.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", sco_edit_formation.module_move, Permission.EditFormation)
sco_publish("/ue_move", sco_edit_formation.ue_move, Permission.EditFormation)
@bp.route("/ue_clone", methods=["POST"])
@scodoc
@permission_required(Permission.EditFormation)
def ue_clone():
"""Clone existing UE"""
ue_id = 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", methods=["GET", "POST"]) # API ScoDoc 7 compat
@scodoc
@permission_required_compat_scodoc7(Permission.ScoView)
@scodoc7func
def evaluation_listenotes():
"""Affichage des notes d'une évaluation.
Si evaluation_id non spécifié, toutes les notes des évaluations de ce modimpl.
"""
evaluation_id = None
moduleimpl_id = None
vals = scu.get_request_args()
try:
if "evaluation_id" in vals:
evaluation_id = int(vals["evaluation_id"])
if "moduleimpl_id" in vals and vals["moduleimpl_id"]:
moduleimpl_id = int(vals["moduleimpl_id"])
except ValueError as exc:
raise ScoValueError("evaluation_listenotes: id invalides !") from exc
fmt = vals.get("fmt", "html")
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"],
)
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("/saisie_notes", sco_saisie_notes.saisie_notes, Permission.EnsView)
sco_publish(
"/do_evaluation_set_missing",
sco_saisie_notes.do_evaluation_set_missing,
Permission.EnsView,
methods=["GET", "POST"],
)
sco_publish(
"/evaluation_suppress_alln",
sco_saisie_notes.evaluation_suppress_alln,
Permission.ScoView,
methods=["GET", "POST"],
)
@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),
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
"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
modimpls = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
bad_ue = []
bad_sem = []
formations_set = set() # les formations mentionnées dans les UE et modules
for modimpl in modimpls:
mod = sco_edit_module.module_list({"module_id": modimpl["module_id"]})[0]
formations_set.add(mod["formation_id"])
ue = UniteEns.query.get_or_404(mod["ue_id"])
ue_dict = ue.to_dict()
formations_set.add(ue_dict["formation_id"])
if ue_dict["formation_id"] != mod["formation_id"]:
modimpl["mod"] = mod
modimpl["ue"] = ue_dict
bad_ue.append(modimpl)
if sem["formation_id"] != mod["formation_id"]:
bad_sem.append(modimpl)
modimpl["mod"] = mod
H = [
"<p>formation_id=%s" % sem["formation_id"],
]
if bad_ue:
H += [
"<h2>Modules d'une autre formation que leur UE:</h2>",
"<br>".join([str(x) for x in bad_ue]),
]
if bad_sem:
H += [
"<h2>Module du semestre dans une autre formation:</h2>",
"<br>".join([str(x) for x in bad_sem]),
]
if not bad_ue and not bad_sem:
H.append("<p>Aucun problème à signaler !</p>")
else:
log(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("""<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="check_sem_integrity?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))