1
0
forked from ScoDoc/ScoDoc
ScoDoc/app/views/notes.py

3501 lines
112 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 datetime
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 app import db
from app import models
from app.auth.models import User
from app.but import (
apc_edit_ue,
cursus_but,
jury_edit_manual,
jury_but,
jury_but_pv,
jury_but_validation_auto,
jury_but_view,
)
from app.but import bulletin_but_court # ne pas enlever: ajoute des routes !
from app.but.forms import jury_but_forms
from app.comp import jury, res_sem
from app.comp.res_compat import NotesTableCompat
from app.models import (
ApcNiveau,
Assiduite,
BulAppreciations,
DispenseUE,
Evaluation,
Formation,
FormSemestre,
FormSemestreInscription,
FormSemestreUEComputationExpr,
Identite,
Module,
ModuleImpl,
ScolarAutorisationInscription,
ScolarNews,
Scolog,
ScoDocSiteConfig,
UniteEns,
)
from app.scodoc.sco_exceptions import ScoFormationConflict, ScoPermissionDenied
from app.views import notes_bp as bp
from app.decorators import (
scodoc,
scodoc7func,
permission_required,
permission_required_compat_scodoc7,
)
# ---------------
from app.pe import pe_view # ne pas enlever, ajoute des vues
from app.scodoc import sco_bulletins_json, sco_utils as scu
from app import log, send_scodoc_alarm
from app.scodoc.sco_exceptions import (
AccessDenied,
ScoValueError,
ScoInvalidIdType,
)
from app.scodoc import (
html_sco_header,
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_formsemestre_validation,
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_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_pv_dict import descr_autorisations
from app.scodoc.sco_permissions import Permission
from app.scodoc.TrivialFormulator import TrivialFormulator
from app.views import ScoData
def sco_publish(route, function, permission, methods=("GET",)):
"""Declare a route for a python function,
protected by permission and called following ScoDoc 7 Zope standards.
"""
return bp.route(route, methods=methods)(
scodoc(permission_required(permission)(scodoc7func(function)))
)
# --------------------------------------------------------------------
#
# Notes/ methods
#
# --------------------------------------------------------------------
sco_publish(
"/formsemestre_status",
sco_formsemestre_status.formsemestre_status,
Permission.ScoView,
)
sco_publish(
"/formsemestre_createwithmodules",
sco_formsemestre_edit.formsemestre_createwithmodules,
Permission.EditFormSemestre,
methods=["GET", "POST"],
)
# controle d'acces specifique pour dir. etud:
sco_publish(
"/formsemestre_editwithmodules",
sco_formsemestre_edit.formsemestre_editwithmodules,
Permission.ScoView,
methods=["GET", "POST"],
)
sco_publish(
"/formsemestre_clone",
sco_formsemestre_edit.formsemestre_clone,
Permission.EditFormSemestre,
methods=["GET", "POST"],
)
sco_publish(
"/formsemestre_associate_new_version",
sco_formation_versions.formsemestre_associate_new_version,
Permission.EditFormation,
methods=["GET", "POST"],
)
sco_publish(
"/formsemestre_delete",
sco_formsemestre_edit.formsemestre_delete,
Permission.EditFormSemestre,
methods=["GET", "POST"],
)
sco_publish(
"/formsemestre_delete2",
sco_formsemestre_edit.formsemestre_delete2,
Permission.EditFormSemestre,
methods=["GET", "POST"],
)
sco_publish(
"/formsemestre_note_etuds_sans_note",
sco_formsemestre_status.formsemestre_note_etuds_sans_notes,
Permission.ScoView,
methods=["GET", "POST"],
)
sco_publish(
"/formsemestre_recapcomplet",
sco_recapcomplet.formsemestre_recapcomplet,
Permission.ScoView,
)
sco_publish(
"/evaluations_recap",
sco_evaluation_recap.evaluations_recap,
Permission.ScoView,
)
sco_publish(
"/formsemestres_bulletins",
sco_recapcomplet.formsemestres_bulletins,
Permission.Observateur,
)
sco_publish(
"/moduleimpl_status", sco_moduleimpl_status.moduleimpl_status, Permission.ScoView
)
sco_publish(
"/formsemestre_description",
sco_formsemestre_status.formsemestre_description,
Permission.ScoView,
)
sco_publish(
"/formation_create",
sco_edit_formation.formation_create,
Permission.EditFormation,
methods=["GET", "POST"],
)
sco_publish(
"/formation_delete",
sco_edit_formation.formation_delete,
Permission.EditFormation,
methods=["GET", "POST"],
)
sco_publish(
"/formation_edit",
sco_edit_formation.formation_edit,
Permission.EditFormation,
methods=["GET", "POST"],
)
@bp.route("/formsemestre_bulletinetud")
@scodoc
@permission_required_compat_scodoc7(Permission.ScoView)
@scodoc7func
def formsemestre_bulletinetud(
etudid=None,
formsemestre_id=None,
fmt=None,
version="long",
xml_with_decisions=False,
force_publishing=False,
prefer_mail_perso=False,
code_nip=None,
code_ine=None,
):
fmt = fmt or "html"
if version not in scu.BULLETINS_VERSIONS_BUT:
raise ScoValueError(
"formsemestre_bulletinetud: version de bulletin demandée invalide"
)
if not isinstance(etudid, int):
raise ScoInvalidIdType("formsemestre_bulletinetud: etudid must be an integer !")
if formsemestre_id is not None and not isinstance(formsemestre_id, int):
raise ScoInvalidIdType(
"formsemestre_bulletinetud: formsemestre_id must be an integer !"
)
formsemestre = FormSemestre.query.filter_by(
formsemestre_id=formsemestre_id, dept_id=g.scodoc_dept_id
).first_or_404()
if etudid:
etud = Identite.get_etud(etudid)
elif code_nip:
etud = models.Identite.query.filter_by(
code_nip=str(code_nip), dept_id=formsemestre.dept_id
).first_or_404()
elif code_ine:
etud = models.Identite.query.filter_by(
code_ine=str(code_ine), dept_id=formsemestre.dept_id
).first_or_404()
else:
raise ScoValueError(
"Paramètre manquant: spécifier etudid, code_nip ou code_ine"
)
if version == "butcourt":
return redirect(
url_for(
"notes.bulletin_but_pdf" if fmt == "pdf" else "notes.bulletin_but_html",
scodoc_dept=g.scodoc_dept,
etudid=etud.id,
formsemestre_id=formsemestre_id,
)
)
if fmt == "json":
return sco_bulletins.get_formsemestre_bulletin_etud_json(
formsemestre, etud, version=version, force_publishing=force_publishing
)
if formsemestre.formation.is_apc() and fmt == "html":
return render_template(
"but/bulletin.j2",
appreciations=BulAppreciations.get_appreciations_list(
formsemestre.id, etud.id
),
bul_url=url_for(
"notes.formsemestre_bulletinetud",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
etudid=etud.id,
fmt="json",
force_publishing=1, # pour ScoDoc lui même
version=version,
),
can_edit_appreciations=formsemestre.est_responsable(current_user)
or (current_user.has_permission(Permission.EtudInscrit)),
etud=etud,
formsemestre=formsemestre,
inscription_courante=etud.inscription_courante(),
inscription_str=etud.inscription_descr()["inscription_str"],
is_apc=formsemestre.formation.is_apc(),
menu_autres_operations=sco_bulletins.make_menu_autres_operations(
formsemestre, etud, "notes.formsemestre_bulletinetud", version
),
sco=ScoData(etud=etud),
scu=scu,
time=time,
title=f"Bul. {etud.nom} - BUT",
version=version,
)
if fmt == "oldjson":
fmt = "json"
response = sco_bulletins.formsemestre_bulletinetud(
etud,
formsemestre_id=formsemestre_id,
fmt=fmt,
version=version,
xml_with_decisions=xml_with_decisions,
force_publishing=force_publishing,
prefer_mail_perso=prefer_mail_perso,
)
if fmt == "pdfmail": # ne renvoie rien dans ce cas (mails envoyés)
return redirect(
url_for(
"notes.formsemestre_bulletinetud",
scodoc_dept=g.scodoc_dept,
etudid=etud.id,
formsemestre_id=formsemestre_id,
)
)
return response
sco_publish(
"/formsemestre_evaluations_cal",
sco_evaluations.formsemestre_evaluations_cal,
Permission.ScoView,
)
sco_publish(
"/formsemestre_evaluations_delai_correction",
sco_evaluations.formsemestre_evaluations_delai_correction,
Permission.ScoView,
)
@bp.route("/moduleimpl_evaluation_renumber", methods=["GET", "POST"])
@scodoc
@permission_required_compat_scodoc7(Permission.ScoView)
@scodoc7func
def moduleimpl_evaluation_renumber(moduleimpl_id):
"Renumérote les évaluations, triant par date"
modimpl: ModuleImpl = (
ModuleImpl.query.filter_by(id=moduleimpl_id)
.join(FormSemestre)
.filter_by(dept_id=g.scodoc_dept_id)
.first_or_404()
)
if not modimpl.can_edit_evaluation(current_user):
raise ScoPermissionDenied(
dest_url=url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=modimpl.id,
)
)
Evaluation.moduleimpl_evaluation_renumber(modimpl)
# redirect to moduleimpl page:
return flask.redirect(
url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=moduleimpl_id,
)
)
sco_publish(
"/moduleimpl_evaluation_move",
sco_evaluation_db.moduleimpl_evaluation_move,
Permission.ScoView,
)
sco_publish(
"/formsemestre_list_saisies_notes",
sco_undo_notes.formsemestre_list_saisies_notes,
Permission.ScoView,
)
sco_publish(
"/ue_create",
sco_edit_ue.ue_create,
Permission.EditFormation,
methods=["GET", "POST"],
)
sco_publish(
"/ue_delete",
sco_edit_ue.ue_delete,
Permission.EditFormation,
methods=["GET", "POST"],
)
@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(
"/edit_ue_set_code_apogee",
sco_edit_ue.edit_ue_set_code_apogee,
Permission.EditFormation,
methods=["POST"],
)
sco_publish(
"/formsemestre_edit_uecoefs",
sco_formsemestre_edit.formsemestre_edit_uecoefs,
Permission.ScoView,
methods=["GET", "POST"],
)
@bp.route("/formation_table_recap")
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
def formation_table_recap(formation_id, fmt="html"):
return sco_formation_recap.formation_table_recap(formation_id, fmt=fmt)
sco_publish(
"/export_recap_formations_annee_scolaire",
sco_formation_recap.export_recap_formations_annee_scolaire,
Permission.ScoView,
)
sco_publish(
"/formation_add_malus_modules",
sco_edit_module.formation_add_malus_modules,
Permission.EditFormation,
)
sco_publish(
"/matiere_create",
sco_edit_matiere.matiere_create,
Permission.EditFormation,
methods=["GET", "POST"],
)
sco_publish(
"/matiere_delete",
sco_edit_matiere.matiere_delete,
Permission.EditFormation,
methods=["GET", "POST"],
)
sco_publish(
"/matiere_edit",
sco_edit_matiere.matiere_edit,
Permission.EditFormation,
methods=["GET", "POST"],
)
sco_publish(
"/module_create",
sco_edit_module.module_create,
Permission.EditFormation,
methods=["GET", "POST"],
)
sco_publish(
"/module_delete",
sco_edit_module.module_delete,
Permission.EditFormation,
methods=["GET", "POST"],
)
sco_publish(
"/module_edit",
sco_edit_module.module_edit,
Permission.EditFormation,
methods=["GET", "POST"],
)
sco_publish(
"/edit_module_set_code_apogee",
sco_edit_module.edit_module_set_code_apogee,
Permission.EditFormation,
methods=["GET", "POST"],
)
sco_publish("/module_list", sco_edit_module.module_table, Permission.ScoView)
sco_publish("/module_tag_search", sco_tag_module.module_tag_search, Permission.ScoView)
@bp.route("/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")
@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 = [
html_sco_header.sco_header(page_title="Formations (programmes)"),
f"""<h2>Formations (programmes pédagogiques)</h2>
<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>
"""
)
H.append(html_sco_header.sco_footer())
return "\n".join(H)
# --------------------------------------------------------------------
#
# Notes Methods
#
# --------------------------------------------------------------------
# --- Formations
@bp.route("/formation_export")
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
def formation_export(formation_id, export_ids=False, fmt=None, export_codes_apo=True):
"Export de la formation au format indiqué (xml ou json)"
return sco_formations.formation_export(
formation_id,
export_ids=export_ids,
fmt=fmt,
export_codes_apo=export_codes_apo,
)
@bp.route("/formation_import_xml_form", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.EditFormation)
@scodoc7func
def formation_import_xml_form():
"form import d'une formation en XML"
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
(("xmlfile", {"input_type": "file", "title": "Fichier XML", "size": 30}),),
submitlabel="Importer",
cancelbutton="Annuler",
)
if tf[0] == 0:
return f"""
{ html_sco_header.sco_header(page_title="Import d'une formation") }
<h2>Import d'une formation</h2>
<p>Création d'une formation (avec UE, matières, modules)
à partir un fichier XML (réservé aux utilisateurs avertis).
</p>
<p>S'il s'agit d'une formation par compétence (BUT), assurez-vous d'avoir
chargé le référentiel de compétences AVANT d'importer le fichier formation
(voir <a class="stdlink" href="{
url_for("notes.refcomp_table", scodoc_dept=g.scodoc_dept)
}">page des référentiels</a>).
</p>
{ tf[1] }
{ html_sco_header.sco_footer() }
"""
elif tf[0] == -1:
return flask.redirect(scu.NotesURL())
else:
formation_id, _, _ = sco_formations.formation_import_xml(
tf[2]["xmlfile"].read()
)
return f"""
{ html_sco_header.sco_header(page_title="Import d'une formation") }
<h2>Import effectué !</h2>
<ul>
<li><a class="stdlink" href="{
url_for("notes.ue_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>
{ html_sco_header.sco_footer() }
"""
sco_publish("/module_move", sco_edit_formation.module_move, Permission.EditFormation)
sco_publish("/ue_move", sco_edit_formation.ue_move, Permission.EditFormation)
@bp.route("/ue_clone", methods=["POST"])
@scodoc
@permission_required(Permission.EditFormation)
def ue_clone():
"""Clone existing UE"""
ue_id = int(request.form.get("ue_id"))
ue = UniteEns.query.get_or_404(ue_id)
ue2 = ue.clone()
db.session.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)
# --
header = html_sco_header.html_sem_header(
f"""Enseignants du <a href="{
url_for("notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id)
}">module {modimpl.module.titre or modimpl.module.code}</a>""",
page_title=f"Enseignants du module {modimpl.module.titre or modimpl.module.code}",
javascripts=["libjs/AutoSuggest.js"],
cssstyles=["css/autosuggest_inquisitor.css"],
bodyOnLoad="init_tf_form('')",
)
footer = html_sco_header.sco_footer()
# Liste des enseignants avec forme pour affichage / saisie avec suggestion
userlist = sco_users.get_user_list()
uid2display = {} # uid : forme pour affichage = "NOM Prenom (login)"(login)"
for u in userlist:
uid2display[u.id] = u.get_nomplogin()
allowed_user_names = list(uid2display.values())
H = [
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"""<p class="help">Les enseignants d'un module ont le droit de
saisir et modifier toutes les notes des évaluations de ce module.
</p>
<p class="help">Pour changer le responsable du module, passez par la
page "<a class="stdlink" href="{
url_for("notes.formsemestre_editwithmodules", scodoc_dept=g.scodoc_dept,
formsemestre_id=modimpl.formsemestre_id)
}">Modification du semestre</a>",
accessible uniquement au responsable de la formation (chef de département)
</p>
"""
modform = [
("moduleimpl_id", {"input_type": "hidden"}),
(
"ens_id",
{
"input_type": "text_suggest",
"size": 50,
"title": "Ajouter un enseignant",
"allowed_values": allowed_user_names,
"allow_null": False,
"text_suggest_options": {
"script": url_for(
"users.get_user_list_xml", scodoc_dept=g.scodoc_dept
)
+ "?",
"varname": "start",
"json": False,
"noresults": "Valeur invalide !",
"timeout": 60000,
},
},
),
]
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
modform,
submitlabel="Ajouter enseignant",
cancelbutton="Annuler",
)
if tf[0] == 0:
return header + "\n".join(H) + tf[1] + F + footer
elif tf[0] == -1:
return flask.redirect(
url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=moduleimpl_id,
)
)
else:
ens = 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(
f"""<p class="help">Enseignant {ens.user_name} déjà dans la liste !</p>"""
)
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 header + "\n".join(H) + tf[1] + F + footer
@bp.route("/edit_moduleimpl_resp", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
def edit_moduleimpl_resp(moduleimpl_id: int):
"""Changement d'un enseignant responsable de module
Accessible par Admin et dir des etud si flag resp_can_change_ens
"""
modimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id)
modimpl.can_change_responsable(current_user, raise_exc=True) # access control
H = [
html_sco_header.html_sem_header(
f"""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>""",
javascripts=["libjs/AutoSuggest.js"],
cssstyles=["css/autosuggest_inquisitor.css"],
bodyOnLoad="init_tf_form('')",
)
]
help_str = """<p class="help">Taper le début du nom de l'enseignant.</p>"""
# Liste des enseignants avec forme pour affichage / saisie avec suggestion
userlist = [sco_users.user_info(user=u) for u in sco_users.get_user_list()]
uid2display = {} # uid : forme pour affichage = "NOM Prenom (login)"
for u in userlist:
uid2display[u["id"]] = u["nomplogin"]
allowed_user_names = list(uid2display.values())
initvalues = modimpl.to_dict(with_module=False)
initvalues["responsable_id"] = uid2display.get(
modimpl.responsable_id, modimpl.responsable_id
)
form = [
("moduleimpl_id", {"input_type": "hidden"}),
(
"responsable_id",
{
"input_type": "text_suggest",
"size": 50,
"title": "Responsable du module",
"allowed_values": allowed_user_names,
"allow_null": False,
"text_suggest_options": {
"script": url_for(
"users.get_user_list_xml", scodoc_dept=g.scodoc_dept
)
+ "?",
"varname": "start",
"json": False,
"noresults": "Valeur invalide !",
"timeout": 60000,
},
},
),
]
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
form,
submitlabel="Changer responsable",
cancelbutton="Annuler",
initvalues=initvalues,
)
if tf[0] == 0:
return "\n".join(H) + tf[1] + help_str + html_sco_header.sco_footer()
elif tf[0] == -1:
return flask.redirect(
url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=moduleimpl_id,
)
)
else:
responsable = 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(
{
"nomprenom": etud.nomprenom,
"just": nb_abs_just,
"nojust": nb_abs_nj,
"total": nb_abs,
"_nomprenom_target": url_for(
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id
),
}
)
H = [
html_sco_header.html_sem_header(
f"""Absences du <a href="{
url_for("notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id
)}">module {modimpl.module.titre_str()}</a>""",
page_title=f"Absences du module {modimpl.module.titre_str()}",
)
]
if not rows and fmt == "html":
return (
"\n".join(H)
+ "<p>Aucune absence signalée</p>"
+ html_sco_header.sco_footer()
)
tab = GenTable(
titles={
"nomprenom": "Nom",
"just": "Just.",
"nojust": "Non Just.",
"total": "Total",
},
columns_ids=("nomprenom", "just", "nojust", "total"),
rows=rows,
html_class="table_leftalign",
base_url=f"{request.base_url}?moduleimpl_id={moduleimpl_id}",
filename="absmodule_" + scu.make_filename(modimpl.module.titre_str()),
caption=f"Absences dans le module {modimpl.module.titre_str()}",
preferences=sco_preferences.SemPreferences(),
)
if fmt != "html":
return tab.make_page(fmt=fmt)
return "\n".join(H) + tab.html() + html_sco_header.sco_footer()
@bp.route("/delete_ue_expr/<int:formsemestre_id>/<int:ue_id>", methods=["GET", "POST"])
@scodoc
def delete_ue_expr(formsemestre_id: int, ue_id: int):
"""Efface une expression de calcul d'UE"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
if not formsemestre.can_be_edited_by(current_user):
raise AccessDenied("vous n'avez pas le droit d'effectuer cette opération")
expr = FormSemestreUEComputationExpr.query.filter_by(
formsemestre_id=formsemestre_id, ue_id=ue_id
).first()
if expr is not None:
db.session.delete(expr)
db.session.commit()
flash("formule supprimée")
return flask.redirect(
url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
)
@bp.route("/formsemestre_enseignants_list")
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
def formsemestre_enseignants_list(formsemestre_id, fmt="html"):
"""Liste les enseignants intervenants dans le semestre (resp. modules et chargés de TD)
et indique les absences saisies par chacun.
"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
# resp. de modules et charges de TD
# 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()}"
T = GenTable(
columns_ids=["nom_fmt", "prenom_fmt", "descr_mods", "nbabsadded", "email"],
titles={
"nom_fmt": "Nom",
"prenom_fmt": "Prénom",
"email": "Mail",
"descr_mods": "Modules",
"nbabsadded": "Saisies Abs.",
},
rows=sem_ens_list,
html_sortable=True,
html_class="table_leftalign formsemestre_enseignants_list",
html_with_td_classes=True,
filename=scu.make_filename(f"Enseignants-{formsemestre.titre_annee()}"),
html_title=html_sco_header.html_sem_header(
"Enseignants du semestre", with_page_header=False
),
base_url=f"{request.base_url}?formsemestre_id={formsemestre_id}",
caption="""Tous les enseignants (responsables ou associés aux modules de
ce semestre) apparaissent. Le nombre de saisies d'absences est indicatif.""",
preferences=sco_preferences.SemPreferences(formsemestre_id),
)
return T.make_page(page_title=title, title=title, fmt=fmt)
@bp.route("/edit_enseignants_form_delete", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
def edit_enseignants_form_delete(moduleimpl_id, ens_id: int):
"""remove ens from this modueimpl
ens_id: user.id
"""
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:
<a href="{
url_for("notes.formsemestre_validation_suppress_etud",
scodoc_dept=g.scodoc_dept, etudid=etudid,
formsemestre_id=formsemestre_id)
}">supprimer décision jury</a>
)
"""
)
if not dialog_confirmed:
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
if formsemestre.modalite != "EXT":
msg_ext = """
<p>%s sera désinscrit de tous les modules du semestre %s (%s - %s).</p>
<p>Cette opération ne doit être utilisée que pour corriger une <b>erreur</b> !
Un étudiant réellement inscrit doit le rester, le faire éventuellement <b>démissionner<b>.
</p>
""" % (
etud["nomprenom"],
sem["titre_num"],
sem["date_debut"],
sem["date_fin"],
)
else: # semestre extérieur
msg_ext = """
<p>%s sera désinscrit du semestre extérieur %s (%s - %s).</p>
""" % (
etud["nomprenom"],
sem["titre_num"],
sem["date_debut"],
sem["date_fin"],
)
inscrits = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
args={"formsemestre_id": formsemestre_id}
)
nbinscrits = len(inscrits)
if nbinscrits <= 1:
msg_ext = """<p class="warning">Attention: le semestre extérieur
sera supprimé car il n'a pas d'autre étudiant inscrit.
</p>
"""
return scu.confirm_dialog(
"""<h2>Confirmer la demande de désinscription ?</h2>""" + msg_ext,
dest_url="",
cancel_url=url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
),
parameters={"etudid": etudid, "formsemestre_id": formsemestre_id},
)
sco_formsemestre_inscriptions.do_formsemestre_desinscription(
etudid, formsemestre_id
)
flash("Étudiant désinscrit")
return redirect(
url_for("scolar.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.EtudInscrit,
methods=["GET", "POST"],
)
sco_publish(
"/moduleimpl_inscriptions_stats",
sco_moduleimpl_inscriptions.moduleimpl_inscriptions_stats,
Permission.ScoView,
)
# --- Evaluations
@bp.route("/evaluation_delete", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.EnsView)
@scodoc7func
def evaluation_delete(evaluation_id):
"""Form delete evaluation"""
evaluation: Evaluation = (
Evaluation.query.filter_by(id=evaluation_id)
.join(ModuleImpl)
.join(FormSemestre)
.filter_by(dept_id=g.scodoc_dept_id)
.first_or_404()
)
tit = f"""Suppression de l'évaluation {evaluation.description} ({evaluation.descr_date()})"""
etat = sco_evaluations.do_evaluation_etat(evaluation.id)
H = [
f"""
{html_sco_header.html_sem_header(tit, with_h2=False)}
<h2 class="formsemestre">Module <tt>{evaluation.moduleimpl.module.code}</tt>
{evaluation.moduleimpl.module.titre_str()}</h2>
<h3>{tit}</h3>
<p class="help">Opération <span class="redboldtext">irréversible</span>.
Si vous supprimez l'évaluation, vous ne pourrez pas retrouver les notes associées.
</p>
""",
]
warning = False
if etat["nb_notes_total"]:
warning = True
nb_desinscrits = etat["nb_notes_total"] - etat["nb_notes"]
H.append(
f"""<div class="ue_warning"><span>Il y a {etat["nb_notes_total"]} notes"""
)
if nb_desinscrits:
H.append(
""" (dont {nb_desinscrits} d'étudiants qui ne sont plus inscrits)"""
)
H.append(""" dans l'évaluation</span>""")
if etat["nb_notes"] == 0:
H.append(
"""<p>Vous pouvez quand même supprimer l'évaluation, les notes des étudiants désincrits seront effacées.</p>"""
)
if etat["nb_notes"]:
H.append(
f"""<p>Suppression impossible (effacer les notes d'abord)</p>
<p><a class="stdlink" href="{
url_for("notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept, moduleimpl_id=evaluation.moduleimpl_id)
}">retour au tableau de bord du module</a>
</p>
</div>"""
)
return "\n".join(H) + html_sco_header.sco_footer()
if warning:
H.append("""</div>""")
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
(("evaluation_id", {"input_type": "hidden"}),),
initvalues={"evaluation_id": evaluation.id},
submitlabel="Confirmer la suppression",
cancelbutton="Annuler",
)
if tf[0] == 0:
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
elif tf[0] == -1:
return flask.redirect(
url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=evaluation.moduleimpl_id,
)
)
else:
evaluation.delete()
return (
"\n".join(H)
+ f"""<p>OK, évaluation supprimée.</p>
<p><a class="stdlink" href="{
url_for("notes.moduleimpl_status", scodoc_dept=g.scodoc_dept,
moduleimpl_id=evaluation.moduleimpl_id)
}">Continuer</a></p>"""
+ html_sco_header.sco_footer()
)
@bp.route("/evaluation_edit", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.EnsView)
@scodoc7func
def evaluation_edit(evaluation_id):
"form edit evaluation"
return sco_evaluation_edit.evaluation_create_form(
evaluation_id=evaluation_id, edit=True
)
@bp.route("/evaluation_create", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.EnsView)
@scodoc7func
def evaluation_create(moduleimpl_id):
"form create evaluation"
modimpl = db.session.get(ModuleImpl, moduleimpl_id)
if modimpl is None:
raise ScoValueError("Ce module n'existe pas ou plus !")
return sco_evaluation_edit.evaluation_create_form(
moduleimpl_id=moduleimpl_id, edit=False
)
@bp.route("/evaluation_listenotes", methods=["GET", "POST"]) # API ScoDoc 7 compat
@scodoc
@permission_required_compat_scodoc7(Permission.ScoView)
@scodoc7func
def evaluation_listenotes():
"""Affichage des notes d'une évaluation.
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")
html_content, page_title = sco_liste_notes.do_evaluation_listenotes(
evaluation_id=evaluation_id, moduleimpl_id=moduleimpl_id, fmt=fmt
)
if fmt == "html":
H = html_sco_header.sco_header(
page_title=page_title,
cssstyles=["css/verticalhisto.css"],
javascripts=["js/etud_info.js"],
init_qtip=True,
)
F = html_sco_header.sco_footer()
return H + html_content + F
else:
return html_content
sco_publish(
"/evaluation_list_operations",
sco_undo_notes.evaluation_list_operations,
Permission.ScoView,
)
@bp.route("/evaluation_check_absences_html/<int:evaluation_id>")
@scodoc
@permission_required(Permission.ScoView)
def evaluation_check_absences_html(evaluation_id: int):
"Check absences sur une évaluation"
evaluation: Evaluation = (
Evaluation.query.filter_by(id=evaluation_id)
.join(ModuleImpl)
.join(FormSemestre)
.filter_by(dept_id=g.scodoc_dept_id)
.first_or_404()
)
return sco_evaluation_check_abs.evaluation_check_absences_html(evaluation)
sco_publish(
"/formsemestre_check_absences_html",
sco_evaluation_check_abs.formsemestre_check_absences_html,
Permission.ScoView,
)
# --- Placement des étudiants pour l'évaluation
sco_publish(
"/placement_eval_selectetuds",
sco_placement.placement_eval_selectetuds,
Permission.EnsView,
methods=["GET", "POST"],
)
# --- Saisie des notes
sco_publish(
"/saisie_notes_tableur",
sco_saisie_notes.saisie_notes_tableur,
Permission.EnsView,
methods=["GET", "POST"],
)
sco_publish(
"/feuille_saisie_notes",
sco_saisie_notes.feuille_saisie_notes,
Permission.EnsView,
)
sco_publish("/saisie_notes", sco_saisie_notes.saisie_notes, Permission.EnsView)
sco_publish(
"/do_evaluation_set_missing",
sco_saisie_notes.do_evaluation_set_missing,
Permission.EnsView,
methods=["GET", "POST"],
)
sco_publish(
"/evaluation_suppress_alln",
sco_saisie_notes.evaluation_suppress_alln,
Permission.ScoView,
methods=["GET", "POST"],
)
# --- Bulletins
@bp.route("/formsemestre_bulletins_pdf")
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
def formsemestre_bulletins_pdf(
formsemestre_id,
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,
},
)
# Make each bulletin
nb_sent = 0
for inscription in inscriptions:
sent, _ = sco_bulletins.do_formsemestre_bulletinetud(
formsemestre,
inscription.etud,
version=version,
prefer_mail_perso=prefer_mail_perso,
fmt="pdfmail",
)
if sent:
nb_sent += 1
#
return f"""
{html_sco_header.sco_header()}
<p>{nb_sent} bulletins sur {len(inscriptions)} envoyés par mail !</p>
<p><a class="stdlink" href="{url_for('notes.formsemestre_status',
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id)
}">continuer</a></p>
{html_sco_header.sco_footer()}
"""
sco_publish(
"/external_ue_create_form",
sco_ue_external.external_ue_create_form,
Permission.ScoView,
methods=["GET", "POST"],
)
@bp.route("/appreciation_add_form", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.EnsView)
@scodoc7func
def appreciation_add_form(
etudid=None,
formsemestre_id=None,
appreciation_id=None, # si id, edit
suppress=False, # si true, supress id
):
"form ajout ou edition d'une appreciation"
if appreciation_id: # edit mode
appreciation = db.session.get(BulAppreciations, appreciation_id)
if appreciation is None:
raise ScoValueError("id d'appreciation invalide !")
formsemestre_id = appreciation.formsemestre_id
etudid = appreciation.etudid
etud: Identite = Identite.query.filter_by(
id=etudid, dept_id=g.scodoc_dept_id
).first_or_404()
vals = scu.get_request_args()
if "edit" in vals:
edit = int(vals["edit"])
elif appreciation_id:
edit = 1
else:
edit = 0
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
# check custom access permission
can_edit_app = formsemestre.est_responsable(current_user) or (
current_user.has_permission(Permission.EtudInscrit)
)
if not can_edit_app:
raise AccessDenied("vous n'avez pas le droit d'ajouter une appreciation")
#
bul_url = url_for(
"notes.formsemestre_bulletinetud",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
etudid=etudid,
)
if suppress:
db.session.delete(appreciation)
Scolog.logdb(
method="appreciation_suppress",
etudid=etudid,
)
db.session.commit()
flash("appréciation supprimée")
return flask.redirect(bul_url)
#
if appreciation_id:
action = "Édition"
else:
action = "Ajout"
H = [
html_sco_header.sco_header(),
f"""<h2>{action} d'une appréciation sur {etud.nomprenom}</h2>""",
]
F = html_sco_header.sco_footer()
descr = [
("edit", {"input_type": "hidden", "default": edit}),
("etudid", {"input_type": "hidden"}),
("formsemestre_id", {"input_type": "hidden"}),
("appreciation_id", {"input_type": "hidden"}),
("comment", {"title": "", "input_type": "textarea", "rows": 4, "cols": 60}),
]
if appreciation_id:
initvalues = {
"etudid": etudid,
"formsemestre_id": formsemestre_id,
"comment": appreciation.comment,
}
else:
initvalues = {}
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
descr,
initvalues=initvalues,
cancelbutton="Annuler",
submitlabel="Ajouter appréciation",
)
if tf[0] == 0:
return "\n".join(H) + "\n" + tf[1] + F
elif tf[0] == -1:
return flask.redirect(bul_url)
else:
if edit:
appreciation.author = (current_user.user_name,)
appreciation.comment = tf[2]["comment"].strip()
flash("appréciation modifiée")
else: # nouvelle
appreciation = BulAppreciations(
etudid=etudid,
formsemestre_id=formsemestre_id,
author=current_user.user_name,
comment=tf[2]["comment"].strip(),
)
flash("appréciation ajoutée")
db.session.add(appreciation)
# log
Scolog.logdb(
method="appreciation_add",
etudid=etudid,
msg=appreciation.comment_safe(),
)
db.session.commit()
# ennuyeux mais necessaire (pour le PDF seulement)
sco_cache.invalidate_formsemestre(
pdfonly=True, formsemestre_id=formsemestre_id
) # > appreciation_add
return flask.redirect(bul_url)
# --- FORMULAIRE POUR VALIDATION DES UE ET SEMESTRES
@bp.route("/formsemestre_validation_etud_form")
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
def formsemestre_validation_etud_form(
formsemestre_id,
etudid=None,
etud_index=None,
check=0,
desturl="",
sortcol=None,
):
"Formulaire choix jury pour un étudiant"
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
read_only = not formsemestre.can_edit_jury()
if formsemestre.formation.is_apc():
return redirect(
url_for(
"notes.formsemestre_validation_but",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
etudid=etudid,
)
)
return sco_formsemestre_validation.formsemestre_validation_etud_form(
formsemestre_id,
etudid=etudid,
etud_index=etud_index,
check=check,
read_only=read_only,
dest_url=desturl,
sortcol=sortcol,
)
@bp.route("/formsemestre_validation_etud")
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
def formsemestre_validation_etud(
formsemestre_id,
etudid=None,
codechoice=None,
desturl="",
sortcol=None,
):
"Enregistre choix jury pour un étudiant"
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
if not formsemestre.can_edit_jury():
raise ScoPermissionDenied(
dest_url=url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
)
return sco_formsemestre_validation.formsemestre_validation_etud(
formsemestre_id,
etudid=etudid,
codechoice=codechoice,
desturl=desturl,
sortcol=sortcol,
)
@bp.route("/formsemestre_validation_etud_manu")
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
def formsemestre_validation_etud_manu(
formsemestre_id,
etudid=None,
code_etat="",
new_code_prev="",
devenir="",
assidu=False,
desturl="",
sortcol=None,
):
"Enregistre choix jury pour un étudiant"
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
if not formsemestre.can_edit_jury():
raise ScoPermissionDenied(
dest_url=url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
)
return sco_formsemestre_validation.formsemestre_validation_etud_manu(
formsemestre_id,
etudid=etudid,
code_etat=code_etat,
new_code_prev=new_code_prev,
devenir=devenir,
assidu=assidu,
desturl=desturl,
sortcol=sortcol,
)
# --- Jurys BUT
@bp.route(
"/formsemestre_validation_but/<int:formsemestre_id>/<etudid>",
methods=["GET", "POST"],
)
@scodoc
@permission_required(Permission.ScoView)
def formsemestre_validation_but(
formsemestre_id: int,
etudid: int,
):
"Form. saisie décision jury semestre BUT"
formsemestre: FormSemestre = FormSemestre.query.filter_by(
id=formsemestre_id, dept_id=g.scodoc_dept_id
).first_or_404()
# la route ne donne pas le type d'etudid pour pouvoir construire des URLs
# provisoires avec NEXT et PREV
try:
etudid = int(etudid)
except ValueError as exc:
raise ScoValueError("adresse invalide") from exc
etud = Identite.get_etud(etudid)
nb_etuds = formsemestre.etuds.count()
read_only = not formsemestre.can_edit_jury()
# --- Navigation
prev_lnk = (
f"""{scu.EMO_PREV_ARROW}&nbsp;<a href="{url_for(
"notes.formsemestre_validation_but", scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id, etudid="PREV"
)}" class="stdlink"">précédent</a>
"""
if nb_etuds > 1
else ""
)
next_lnk = (
f"""<a href="{url_for(
"notes.formsemestre_validation_but", scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id, etudid="NEXT"
)}" class="stdlink"">suivant</a>&nbsp;{scu.EMO_NEXT_ARROW}
"""
if nb_etuds > 1
else ""
)
navigation_div = f"""
<div class="but_navigation">
<div class="prev">
{prev_lnk}
</div>
<div class="back_list">
<a href="{
url_for(
"notes.formsemestre_recapcomplet",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
mode_jury=1,
selected_etudid=etud.id
)}" class="stdlink">retour à la liste</a>
</div>
<div class="next">
{next_lnk}
</div>
</div>
"""
H = [
html_sco_header.sco_header(
page_title=f"Validation BUT S{formsemestre.semestre_id}",
formsemestre_id=formsemestre_id,
etudid=etudid,
cssstyles=[
"css/jury_but.css",
"css/cursus_but.css",
],
javascripts=("js/jury_but.js",),
),
"""<div class="jury_but">
""",
]
if formsemestre.etuds_inscriptions[etudid].etat != scu.INSCRIT:
return (
"\n".join(H)
+ f"""
<div>
<div class="bull_head">
<div>
<div class="titre_parcours">Jury BUT</div>
<div class="nom_etud">{etud.html_link_fiche()}</div>
</div>
<div class="bull_photo"><a href="{
etud.url_fiche()
}">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a>
</div>
</div>
<div class="warning">Impossible de statuer sur cet étudiant:
il est démissionnaire ou défaillant (voir <a class="stdlink" href="{
etud.url_fiche()}">sa fiche</a>)
</div>
</div>
{navigation_div}
</div>
"""
+ html_sco_header.sco_footer()
)
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
has_notes_en_attente = deca.has_notes_en_attente()
evaluations_a_debloquer = Evaluation.get_evaluations_blocked_for_etud(
formsemestre, etud
)
if has_notes_en_attente or evaluations_a_debloquer:
read_only = True
if request.method == "POST":
if not read_only:
deca.record_form(request.form)
ScolarNews.add(
typ=ScolarNews.NEWS_JURY,
obj=formsemestre.id,
text=f"""Saisie décision jury dans {formsemestre.html_link_status()}""",
url=url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
),
)
flash("codes enregistrés")
return flask.redirect(
url_for(
"notes.formsemestre_validation_but",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
etudid=etudid,
)
)
warning = ""
if len(deca.niveaux_competences) != len(deca.decisions_rcue_by_niveau):
warning += f"""<div class="warning">Attention: {len(deca.niveaux_competences)}
niveaux mais {len(deca.decisions_rcue_by_niveau)} regroupements RCUE.</div>"""
if (deca.parcour is None) and len(formsemestre.parcours) > 0:
warning += (
"""<div class="warning">L'étudiant n'est pas inscrit à un parcours.</div>"""
)
if formsemestre.date_fin - datetime.date.today() > datetime.timedelta(days=12):
# encore loin de la fin du semestre de départ de ce jury ?
warning += f"""<div class="warning">Le semestre S{formsemestre.semestre_id}
terminera le {formsemestre.date_fin.strftime(scu.DATE_FMT)}&nbsp;:
êtes-vous certain de vouloir enregistrer une décision de jury&nbsp;?
</div>"""
if deca.formsemestre_impair:
inscription = deca.formsemestre_impair.etuds_inscriptions.get(etud.id)
if (not inscription) or inscription.etat != scu.INSCRIT:
etat_ins = scu.ETATS_INSCRIPTION.get(inscription.etat, "inconnu?")
warning += f"""<div class="warning">{etat_ins}
en S{deca.formsemestre_impair.semestre_id}</div>"""
if deca.formsemestre_pair:
inscription = deca.formsemestre_pair.etuds_inscriptions.get(etud.id)
if (not inscription) or inscription.etat != scu.INSCRIT:
etat_ins = scu.ETATS_INSCRIPTION.get(inscription.etat, "inconnu?")
warning += f"""<div class="warning">{etat_ins}
en S{deca.formsemestre_pair.semestre_id}</div>"""
if has_notes_en_attente:
warning += f"""<div class="warning-bloquant">{etud.html_link_fiche()
} a des notes en ATTente dans les modules suivants.
Vous devez régler cela avant de statuer en jury !
<ul class="modimpls_att">
"""
for modimpl in deca.get_modimpls_attente():
warning += f"""<li><a href="{
url_for("notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id)
}" class="stdlink">{modimpl.module.code} {modimpl.module.titre_str()}</a></li>"""
warning += "</ul></div>"
if evaluations_a_debloquer:
links_evals = [
f"""<a class="stdlink" href="{url_for(
'notes.evaluation_listenotes', scodoc_dept=g.scodoc_dept, evaluation_id=e.id
)}">{e.description} en {e.moduleimpl.module.code}</a>"""
for e in evaluations_a_debloquer
]
warning += f"""<div class="warning-bloquant">Impossible de statuer sur cet étudiant:
il a des notes dans des évaluations qui seront débloquées plus tard:
voir {", ".join(links_evals)}
"""
if warning:
warning = f"""<div class="jury_but_warning jury_but_box">{warning}</div>"""
H.append(
f"""
<div>
<div class="bull_head">
<div>
<div class="titre_parcours">Jury BUT{deca.annee_but}
- Parcours {(deca.parcour.libelle if deca.parcour else False) or "non spécifié"}
- {deca.annee_scolaire_str()}</div>
<div class="nom_etud">{etud.html_link_fiche()}</div>
</div>
<div class="bull_photo"><a href="{
etud.url_fiche()}">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a>
</div>
</div>
{warning}
</div>
<form method="post" class="jury_but_box" id="jury_but">
"""
)
H.append(jury_but_view.show_etud(deca, read_only=read_only))
autorisations_idx = deca.get_autorisations_passage()
div_autorisations_passage = (
f"""
<div class="but_autorisations_passage">
<span>Autorisé à passer en&nbsp;:</span>
{ ", ".join( ["S" + str(i) for i in autorisations_idx ] )}
</div>
"""
if autorisations_idx
else """<div class="but_autorisations_passage but_explanation">
pas d'autorisations de passage enregistrées.
</div>
"""
)
H.append(div_autorisations_passage)
if read_only:
H.append(
f"""
<div class="but_explanation">
{"Vous n'avez pas la permission de modifier ces décisions."
if formsemestre.etat
else "Semestre verrouillé."}
Les champs entourés en vert sont enregistrés.
</div>"""
)
else:
erase_span = f"""
<a style="margin-left: 16px;" class="stdlink"
href="{
url_for("notes.erase_decisions_annee_formation",
scodoc_dept=g.scodoc_dept, formation_id=deca.formsemestre.formation.id,
etudid=deca.etud.id, annee=deca.annee_but, formsemestre_id=formsemestre_id)}"
>effacer des décisions de jury</a>
<a style="margin-left: 16px;" class="stdlink"
href="{
url_for("notes.formsemestre_validate_previous_ue",
scodoc_dept=g.scodoc_dept,
etudid=deca.etud.id, formsemestre_id=formsemestre_id)}"
>enregistrer des UEs antérieures</a>
"""
H.append(
f"""<div class="but_settings">
<input type="checkbox" onchange="enable_manual_codes(this)">
<em>permettre la saisie manuelles des codes
{"d'année et " if deca.jury_annuel else ""}
de niveaux.
Dans ce cas, assurez-vous de la cohérence entre les codes d'UE/RCUE/Année !
</em>
</input>
</div>
<div class="but_buttons">
<span><input type="submit" value="Enregistrer ces décisions"></span>
<span>{erase_span}</span>
</div>
"""
)
H.append(navigation_div)
H.append("</form>")
# Affichage cursus BUT
but_cursus = cursus_but.EtudCursusBUT(etud, deca.formsemestre.formation)
H += [
"""<div class="jury_but_box">
<div class="jury_but_box_title"><b>Niveaux de compétences enregistrés :</b></div>
""",
render_template(
"but/cursus_etud.j2",
cursus=but_cursus,
scu=scu,
),
"</div>",
]
H.append(
render_template(
"but/documentation_codes_jury.j2",
nom_univ=f"""Export {sco_preferences.get_preference("InstituteName")
or sco_preferences.get_preference("UnivName")
or "Apogée"}""",
codes=ScoDocSiteConfig.get_codes_apo_dict(),
)
)
H.append(
f"""<div class="but_doc_codes but_warning_rcue_cap">
{scu.EMO_WARNING} Rappel: pour les redoublants, seules les UE <b>capitalisées</b> (note > 10)
lors d'une année précédente peuvent être prise en compte pour former
un RCUE (associé à un niveau de compétence du BUT).
</div>
"""
)
return "\n".join(H) + html_sco_header.sco_footer()
@bp.route(
"/formsemestre_validation_auto_but/<int:formsemestre_id>", methods=["GET", "POST"]
)
@scodoc
@permission_required(Permission.ScoView)
def formsemestre_validation_auto_but(formsemestre_id: int = None):
"Saisie automatique des décisions de jury BUT"
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
if not formsemestre.can_edit_jury():
raise ScoPermissionDenied(
dest_url=url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
)
if not formsemestre.formation.is_apc():
raise ScoValueError(
"formsemestre_validation_auto_but est réservé aux formations APC"
)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
form = jury_but_forms.FormSemestreValidationAutoBUTForm()
if request.method == "POST":
if not form.cancel.data:
nb_etud_modif, _ = (
jury_but_validation_auto.formsemestre_validation_auto_but(formsemestre)
)
flash(f"Décisions enregistrées ({nb_etud_modif} étudiants modifiés)")
return redirect(
url_for(
"notes.formsemestre_recapcomplet",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
mode_jury=1,
)
)
# Avertissement si formsemestre impair
formsemestres_suspects = {}
if formsemestre.semestre_id % 2:
_, decas = jury_but_validation_auto.formsemestre_validation_auto_but(
formsemestre, dry_run=True
)
# regarde si il y a des semestres pairs postérieurs qui ne soient pas bloqués
formsemestres_suspects = {
deca.formsemestre_pair.id: deca.formsemestre_pair
for deca in decas
if deca.formsemestre_pair
and deca.formsemestre_pair.date_debut > formsemestre.date_debut
and not deca.formsemestre_pair.block_moyennes
}
return render_template(
"but/formsemestre_validation_auto_but.j2",
form=form,
formsemestres_suspects=formsemestres_suspects,
sco=ScoData(formsemestre=formsemestre),
title="Calcul automatique jury BUT",
)
@bp.route(
"/formsemestre_validate_previous_ue/<int:formsemestre_id>/<int:etudid>",
methods=["GET", "POST"],
)
@scodoc
@permission_required(Permission.ScoView)
def formsemestre_validate_previous_ue(formsemestre_id, etudid=None):
"Form. saisie UE validée hors ScoDoc"
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
if not formsemestre.can_edit_jury():
raise ScoPermissionDenied(
dest_url=url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
)
etud: Identite = (
Identite.query.filter_by(id=etudid)
.join(FormSemestreInscription)
.filter_by(formsemestre_id=formsemestre_id)
.first_or_404()
)
return sco_formsemestre_validation.formsemestre_validate_previous_ue(
formsemestre, etud
)
sco_publish(
"/formsemestre_ext_create_form",
sco_formsemestre_exterieurs.formsemestre_ext_create_form,
Permission.ScoView,
methods=["GET", "POST"],
)
@bp.route("/formsemestre_ext_edit_ue_validations", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
def formsemestre_ext_edit_ue_validations(formsemestre_id, etudid=None):
"Form. edition UE semestre extérieur"
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
if not formsemestre.can_edit_jury():
raise ScoPermissionDenied(
dest_url=url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
)
return sco_formsemestre_exterieurs.formsemestre_ext_edit_ue_validations(
formsemestre_id, etudid
)
@bp.route("/formsemestre_validation_auto")
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
def formsemestre_validation_auto(formsemestre_id):
"Formulaire saisie automatisee des decisions d'un semestre"
formsemestre: FormSemestre = FormSemestre.query.filter_by(
id=formsemestre_id, dept_id=g.scodoc_dept_id
).first_or_404()
if not formsemestre.can_edit_jury():
raise ScoPermissionDenied(
dest_url=url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
)
if formsemestre.formation.is_apc():
return redirect(
url_for(
"notes.formsemestre_validation_auto_but",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
)
)
return sco_formsemestre_validation.formsemestre_validation_auto(formsemestre_id)
@bp.route("/do_formsemestre_validation_auto")
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
def do_formsemestre_validation_auto(formsemestre_id):
"Formulaire saisie automatisee des decisions d'un semestre"
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
if not formsemestre.can_edit_jury():
raise ScoPermissionDenied(
dest_url=url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
)
return sco_formsemestre_validation.do_formsemestre_validation_auto(formsemestre_id)
@bp.route("/formsemestre_validation_suppress_etud", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
def formsemestre_validation_suppress_etud(
formsemestre_id, etudid, dialog_confirmed=False
):
"""Suppression des décisions de jury pour un étudiant."""
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
if not formsemestre.can_edit_jury():
raise ScoPermissionDenied(
dest_url=url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
)
etud = Identite.get_etud(etudid)
if formsemestre.formation.is_apc():
next_url = url_for(
"scolar.fiche_etud",
scodoc_dept=g.scodoc_dept,
etudid=etudid,
)
else:
next_url = url_for(
"notes.formsemestre_validation_etud_form",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
etudid=etudid,
)
if not dialog_confirmed:
d = sco_bulletins_json.dict_decision_jury(
etud, formsemestre, with_decisions=True
)
descr_ues = [f"{u['acronyme']}: {u['code']}" for u in d.get("decision_ue", [])]
dec_annee = d.get("decision_annee")
if dec_annee:
descr_annee = dec_annee.get("code", "-")
else:
descr_annee = "-"
existing = f"""
<ul>
<li>Semestre : {d.get("decision", {"code":"-"})['code'] or "-"}</li>
<li>Année BUT: {descr_annee}</li>
<li>UEs : {", ".join(descr_ues)}</li>
<li>RCUEs: {len(d.get("decision_rcue", []))} décisions</li>
<li>Autorisations: {descr_autorisations(ScolarAutorisationInscription.query.filter_by(origin_formsemestre_id=formsemestre_id,
etudid=etudid))}
</ul>
"""
return scu.confirm_dialog(
f"""<h2>Confirmer la suppression des décisions du semestre
{formsemestre.titre_mois()} pour {etud.nomprenom}
</h2>
<p>Cette opération est irréversible.</p>
<div>
{existing}
</div>
""",
OK="Supprimer",
dest_url="",
cancel_url=next_url,
parameters={"etudid": etudid, "formsemestre_id": formsemestre_id},
)
sco_formsemestre_validation.formsemestre_validation_suppress_etud(
formsemestre_id, etudid
)
flash("Décisions supprimées")
return flask.redirect(next_url)
# ------------- PV de JURY et archives
sco_publish(
"/formsemestre_pvjury", sco_pv_forms.formsemestre_pvjury, Permission.ScoView
)
sco_publish("/pvjury_page_but", jury_but_pv.pvjury_page_but, Permission.ScoView)
@bp.route("/formsemestre_saisie_jury")
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
def formsemestre_saisie_jury(formsemestre_id: int, selected_etudid: int = None):
"""Page de saisie: liste des étudiants et lien vers page jury
sinon, redirect vers page recap en mode jury
"""
return redirect(
url_for(
"notes.formsemestre_recapcomplet",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
mode_jury=1,
)
)
@bp.route(
"/formsemestre_jury_but_erase/<int:formsemestre_id>",
methods=["GET", "POST"],
defaults={"etudid": None},
)
@bp.route(
"/formsemestre_jury_but_erase/<int:formsemestre_id>/<int:etudid>",
methods=["GET", "POST"],
)
@scodoc
@permission_required(Permission.ScoView)
def formsemestre_jury_but_erase(formsemestre_id: int, etudid: int = None):
"""Supprime la décision de jury BUT pour cette année.
Si l'étudiant n'est pas spécifié, efface les décisions de tous les inscrits.
Si only_one_sem, n'efface que pour le formsemestre indiqué, pas les deux de l'année.
"""
only_one_sem = int(request.args.get("only_one_sem") or False)
formsemestre: FormSemestre = FormSemestre.query.filter_by(
id=formsemestre_id, dept_id=g.scodoc_dept_id
).first_or_404()
if not formsemestre.can_edit_jury():
raise ScoPermissionDenied(
dest_url=url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
)
if not formsemestre.formation.is_apc():
raise ScoValueError("semestre non BUT")
if etudid is None:
etud = None
etuds = formsemestre.get_inscrits(include_demdef=True)
dest_url = url_for(
"notes.formsemestre_recapcomplet",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
mode_jury=1,
)
else:
etud = Identite.get_etud(etudid)
etuds = [etud]
dest_url = url_for(
"notes.formsemestre_validation_but",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
etudid=etudid,
)
if request.method == "POST":
with sco_cache.DeferredSemCacheManager():
for etud in etuds:
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
deca.erase(only_one_sem=only_one_sem)
log(f"formsemestre_jury_but_erase({formsemestre_id}, {etudid})")
flash(
(
"décisions de jury du semestre effacées"
if only_one_sem
else "décisions de jury des semestres de l'année BUT effacées"
)
+ f" pour {len(etuds)} étudiant{'s' if len(etuds) > 1 else ''}"
)
return redirect(dest_url)
return render_template(
"confirm_dialog.j2",
title=f"""Effacer les validations de jury {
("de " + etud.nomprenom)
if etud
else ("des " + str(len(etuds)) + " étudiants inscrits dans ce semestre")
} ?""",
explanation=(
f"""Les validations d'UE et autorisations de passage
du semestre S{formsemestre.semestre_id} seront effacées."""
if only_one_sem
else """Les validations de toutes les UE, RCUE (compétences) et année
issues de cette année scolaire seront effacées.
"""
)
+ """
<p>Les décisions des années scolaires précédentes ne seront pas modifiées.</p>
<p>Efface aussi toutes les validations concernant l'année BUT de ce semestre,
même si elles ont été acquises ailleurs.
</p>
<div class="warning">Cette opération est irréversible !
A n'utiliser que dans des cas exceptionnels, vérifiez bien tous les étudiants ensuite.
</div>
""",
cancel_url=dest_url,
)
@bp.route(
"/erase_decisions_annee_formation/<int:etudid>/<int:formation_id>/<int:annee>",
methods=["GET", "POST"],
)
@scodoc
@permission_required(Permission.EtudInscrit)
def erase_decisions_annee_formation(etudid: int, formation_id: int, annee: int):
"""Efface toute les décisions d'une année pour cet étudiant"""
etud: Identite = Identite.query.get_or_404(etudid)
formation: Formation = Formation.query.filter_by(
id=formation_id, dept_id=g.scodoc_dept_id
).first_or_404()
if request.method == "POST":
jury.erase_decisions_annee_formation(etud, formation, annee, delete=True)
flash("Décisions de jury effacées")
return redirect(
url_for(
"scolar.fiche_etud",
scodoc_dept=g.scodoc_dept,
etudid=etud.id,
)
)
validations = jury.erase_decisions_annee_formation(etud, formation, annee)
formsemestre_origine_id = request.args.get("formsemestre_id")
formsemestre_origine = (
FormSemestre.query.get_or_404(formsemestre_origine_id)
if formsemestre_origine_id
else None
)
return render_template(
"jury/erase_decisions_annee_formation.j2",
annee=annee,
cancel_url=url_for(
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id
),
etud=etud,
formation=formation,
formsemestre_origine=formsemestre_origine,
validations=validations,
sco=ScoData(),
title=f"Effacer décisions de jury {etud.nom} - année {annee}",
)
@bp.route(
"/jury_delete_manual/<int:etudid>",
methods=["GET", "POST"],
)
@scodoc
@permission_required(Permission.EtudInscrit)
def jury_delete_manual(etudid: int):
"""Efface toute les décisions d'une année pour cet étudiant"""
etud: Identite = Identite.query.get_or_404(etudid)
return jury_edit_manual.jury_delete_manual(etud)
sco_publish(
"/formsemestre_lettres_individuelles",
sco_pv_forms.formsemestre_lettres_individuelles,
Permission.ScoView,
methods=["GET", "POST"],
)
sco_publish(
"/formsemestre_pvjury_pdf",
sco_pv_forms.formsemestre_pvjury_pdf,
Permission.ScoView,
methods=["GET", "POST"],
)
sco_publish(
"/feuille_preparation_jury",
sco_prepajury.feuille_preparation_jury,
Permission.ScoView,
)
sco_publish(
"/formsemestre_archive",
sco_archives_formsemestre.formsemestre_archive,
Permission.ScoView,
methods=["GET", "POST"],
)
sco_publish(
"/formsemestre_delete_archive",
sco_archives_formsemestre.formsemestre_delete_archive,
Permission.ScoView,
methods=["GET", "POST"],
)
sco_publish(
"/formsemestre_list_archives",
sco_archives_formsemestre.formsemestre_list_archives,
Permission.ScoView,
)
sco_publish(
"/formsemestre_get_archived_file",
sco_archives_formsemestre.formsemestre_get_archived_file,
Permission.ScoView,
)
sco_publish("/view_apo_csv", sco_etape_apogee_view.view_apo_csv, Permission.EditApogee)
sco_publish(
"/view_apo_csv_store",
sco_etape_apogee_view.view_apo_csv_store,
Permission.EditApogee,
methods=["GET", "POST"],
)
sco_publish(
"/view_apo_csv_download_and_store",
sco_etape_apogee_view.view_apo_csv_download_and_store,
Permission.EditApogee,
methods=["GET", "POST"],
)
sco_publish(
"/view_apo_csv_delete",
sco_etape_apogee_view.view_apo_csv_delete,
Permission.EditApogee,
methods=["GET", "POST"],
)
sco_publish(
"/view_scodoc_etuds", sco_etape_apogee_view.view_scodoc_etuds, Permission.EditApogee
)
sco_publish(
"/view_apo_etuds", sco_etape_apogee_view.view_apo_etuds, Permission.EditApogee
)
sco_publish(
"/apo_semset_maq_status",
sco_etape_apogee_view.apo_semset_maq_status,
Permission.EditApogee,
)
sco_publish(
"/apo_csv_export_results",
sco_etape_apogee_view.apo_csv_export_results,
Permission.EditApogee,
)
@bp.route("/formsemestre_set_apo_etapes", methods=["POST"])
@scodoc
@permission_required(Permission.EditApogee)
def formsemestre_set_apo_etapes():
"""Change les codes étapes du semestre indiqué.
Args: oid=formsemestre_id, value=chaine "V1RT, V1RT2", codes séparés par des virgules
"""
formsemestre_id = int(request.form.get("oid"))
etapes_apo_str = request.form.get("value")
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
current_etapes = {e.etape_apo for e in formsemestre.etapes}
new_etapes = {s.strip() for s in etapes_apo_str.split(",")}
if new_etapes != current_etapes:
formsemestre.etapes = []
for etape_apo in new_etapes:
etape = models.FormSemestreEtape(
formsemestre_id=formsemestre_id, etape_apo=etape_apo
)
formsemestre.etapes.append(etape)
db.session.add(formsemestre)
db.session.commit()
ScolarNews.add(
typ=ScolarNews.NEWS_APO,
text=f"Modification code Apogée du semestre {formsemestre.titre_annee()})",
)
return ("", 204)
@bp.route("/formsemestre_set_elt_annee_apo", methods=["POST"])
@scodoc
@permission_required(Permission.EditApogee)
def formsemestre_set_elt_annee_apo():
"""Change les codes étapes du semestre indiqué.
Args: oid=formsemestre_id, value=chaine "V3ONM, V3ONM1, V3ONM2", codes séparés par des virgules
"""
oid = int(request.form.get("oid"))
value = (request.form.get("value") or "").strip()
formsemestre: FormSemestre = FormSemestre.query.get_or_404(oid)
if value != formsemestre.elt_annee_apo:
formsemestre.elt_annee_apo = value
db.session.add(formsemestre)
db.session.commit()
ScolarNews.add(
typ=ScolarNews.NEWS_APO,
text=f"Modification code Apogée du semestre {formsemestre.titre_annee()})",
)
return ("", 204)
@bp.route("/formsemestre_set_elt_sem_apo", methods=["POST"])
@scodoc
@permission_required(Permission.EditApogee)
def formsemestre_set_elt_sem_apo():
"""Change les codes étapes du semestre indiqué.
Args: oid=formsemestre_id, value=chaine "V3ONM, V3ONM1, V3ONM2", codes séparés par des virgules
"""
try:
oid = int(request.form.get("oid"))
except (TypeError, ValueError) as exc:
raise ScoValueError("paramètre invalide") from exc
value = (request.form.get("value") or "").strip()
formsemestre: FormSemestre = FormSemestre.query.get_or_404(oid)
if value != formsemestre.elt_sem_apo:
formsemestre.elt_sem_apo = value
db.session.add(formsemestre)
db.session.commit()
ScolarNews.add(
typ=ScolarNews.NEWS_APO,
text=f"Modification code Apogée du semestre {formsemestre.titre_annee()})",
)
return ("", 204)
@bp.route("/ue_set_apo", methods=["POST"])
@scodoc
@permission_required(Permission.EditApogee)
def ue_set_apo():
"""Change le code APO de l'UE
Args: oid=ue_id, value=chaine "VRTU12" (1 seul code / UE)
"""
ue_id = int(request.form.get("oid"))
code_apo = (request.form.get("value") or "").strip()
ue = UniteEns.query.get_or_404(ue_id)
if code_apo != ue.code_apogee:
ue.code_apogee = code_apo
db.session.add(ue)
db.session.commit()
ScolarNews.add(
typ=ScolarNews.NEWS_FORM,
text=f"Modification code Apogée d'UE dans la formation {ue.formation.titre} ({ue.formation.acronyme})",
)
return ("", 204)
@bp.route("/module_set_apo", methods=["POST"])
@scodoc
@permission_required(Permission.EditApogee)
def module_set_apo():
"""Change le code APO du module
Args: oid=ue_id, value=chaine "VRTU12" (1 seul code / UE)
"""
oid = int(request.form.get("oid"))
code_apo = (request.form.get("value") or "").strip()
mod = Module.query.get_or_404(oid)
if code_apo != mod.code_apogee:
mod.code_apogee = code_apo
db.session.add(mod)
db.session.commit()
ScolarNews.add(
typ=ScolarNews.NEWS_FORM,
text=f"""Modification code Apogée d'UE dans la formation {
mod.formation.titre} ({mod.formation.acronyme})""",
)
return ("", 204)
# sco_semset
sco_publish("/semset_page", sco_semset.semset_page, Permission.EditApogee)
sco_publish(
"/do_semset_create",
sco_semset.do_semset_create,
Permission.EditApogee,
methods=["GET", "POST"],
)
sco_publish(
"/do_semset_delete",
sco_semset.do_semset_delete,
Permission.EditApogee,
methods=["GET", "POST"],
)
sco_publish(
"/edit_semset_set_title",
sco_semset.edit_semset_set_title,
Permission.EditApogee,
methods=["GET", "POST"],
)
sco_publish(
"/do_semset_add_sem",
sco_semset.do_semset_add_sem,
Permission.EditApogee,
methods=["GET", "POST"],
)
sco_publish(
"/do_semset_remove_sem",
sco_semset.do_semset_remove_sem,
Permission.EditApogee,
methods=["GET", "POST"],
)
# sco_export_result
sco_publish(
"/scodoc_table_results",
sco_export_results.scodoc_table_results,
Permission.EditApogee,
)
sco_publish(
"/apo_compare_csv_form",
sco_apogee_compare.apo_compare_csv_form,
Permission.ScoView,
methods=["GET", "POST"],
)
sco_publish(
"/apo_compare_csv",
sco_apogee_compare.apo_compare_csv,
Permission.ScoView,
methods=["GET", "POST"],
)
# ------------- INSCRIPTIONS: PASSAGE D'UN SEMESTRE A UN AUTRE
sco_publish(
"/formsemestre_inscr_passage",
sco_inscr_passage.formsemestre_inscr_passage,
Permission.EtudInscrit,
methods=["GET", "POST"],
)
sco_publish(
"/formsemestre_synchro_etuds",
sco_synchro_etuds.formsemestre_synchro_etuds,
Permission.ScoView,
methods=["GET", "POST"],
)
# ------------- RAPPORTS STATISTIQUES
sco_publish(
"/formsemestre_report_counts",
sco_report.formsemestre_report_counts,
Permission.ScoView,
)
sco_publish(
"/formsemestre_suivi_cohorte",
sco_report.formsemestre_suivi_cohorte,
Permission.ScoView,
)
sco_publish(
"/formsemestre_suivi_cursus",
sco_report.formsemestre_suivi_cursus,
Permission.ScoView,
)
sco_publish(
"/formsemestre_etuds_lycees",
sco_lycee.formsemestre_etuds_lycees,
Permission.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 = [
html_sco_header.sco_header(),
"<p>formation_id=%s" % sem["formation_id"],
]
if bad_ue:
H += [
"<h2>Modules d'une autre formation que leur UE:</h2>",
"<br>".join([str(x) for x in bad_ue]),
]
if bad_sem:
H += [
"<h2>Module du semestre dans une autre formation:</h2>",
"<br>".join([str(x) for x in bad_sem]),
]
if not bad_ue and not bad_sem:
H.append("<p>Aucun problème à signaler !</p>")
else:
log("check_sem_integrity: problem detected: formations_set=%s" % formations_set)
if sem["formation_id"] in formations_set:
formations_set.remove(sem["formation_id"])
if len(formations_set) == 1:
if fix:
log("check_sem_integrity: trying to fix %s" % formsemestre_id)
formation_id = formations_set.pop()
if sem["formation_id"] != formation_id:
sem["formation_id"] = formation_id
sco_formsemestre.do_formsemestre_edit(sem)
H.append("""<p class="alert">Problème réparé: vérifiez</p>""")
else:
H.append(
"""
<p class="alert">Problème détecté réparable:
<a href="check_sem_integrity?formsemestre_id=%s&fix=1">réparer maintenant</a></p>
"""
% (formsemestre_id,)
)
else:
H.append("""<p class="alert">Problème détecté !</p>""")
return "\n".join(H) + html_sco_header.sco_footer()
@bp.route("/check_form_integrity")
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
def check_form_integrity(formation_id, fix=False):
"debug (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 html_sco_header.sco_header() + txth + html_sco_header.sco_footer()
@bp.route("/check_formsemestre_integrity")
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
def check_formsemestre_integrity(formsemestre_id):
"debug"
log(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 (
html_sco_header.sco_header() + "<br>".join(diag) + html_sco_header.sco_footer()
)