ScoDoc/app/views/jury_validations.py

1064 lines
37 KiB
Python

##############################################################################
#
# 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
#
##############################################################################
"""
Vues sur les jurys et validations
Emmanuel Viennet, 2024
"""
from collections import defaultdict
import datetime
import flask
from flask import flash, g, redirect, render_template, request, url_for
from flask_login import current_user
from app import log
from app.but import (
cursus_but,
jury_edit_manual,
jury_but,
jury_but_validation_auto,
jury_but_view,
)
from app.but.forms import jury_but_forms
from app.comp import jury
from app.decorators import (
scodoc,
scodoc7func,
permission_required,
)
from app.models import (
Evaluation,
Formation,
FormSemestre,
FormSemestreInscription,
Identite,
ScolarAutorisationInscription,
ScolarFormSemestreValidation,
ScolarNews,
ScoDocSiteConfig,
)
from app.scodoc import (
sco_bulletins_json,
sco_cache,
sco_formsemestre_exterieurs,
sco_formsemestre_validation,
sco_preferences,
)
from app.scodoc.codes_cursus import CODES_UE_VALIDES
from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import (
ScoPermissionDenied,
ScoValueError,
)
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_pv_dict import descr_autorisations
from app.tables import bilan_ues
from app.views import notes_bp as bp
from app.views import ScoData
# --- 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.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.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.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()
can_erase = current_user.has_permission(Permission.EtudInscrit)
# --- 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 = ["""<div class="jury_but">"""]
inscription = formsemestre.etuds_inscriptions.get(etudid)
if not inscription:
raise ScoValueError("étudiant non inscrit au semestre")
if inscription.etat != scu.INSCRIT:
return render_template(
"sco_page.j2",
title=f"Validation BUT S{formsemestre.semestre_id}",
sco=ScoData(etud=etud, formsemestre=formsemestre),
cssstyles=[
"css/jury_but.css",
"css/cursus_but.css",
],
javascripts=("js/jury_but.js",),
content=(
"\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>
"""
),
)
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 not read_only and (
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 read_only:
warning += """<div class="warning">Affichage en lecture seule</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 {'' if can_erase else 'link_unauthorized'}"
title="{'' if can_erase else 'réservé au responsable'}"
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)
if can_erase else ''
}"
>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>
<a style="margin-left: 16px;" class="stdlink"
href="{
url_for("notes.validate_dut120_etud",
scodoc_dept=g.scodoc_dept,
etudid=deca.etud.id, formsemestre_id=formsemestre_id)}"
>décerner le DUT "120ECTS"</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 render_template(
"sco_page.j2",
title=f"Validation BUT S{formsemestre.semestre_id}",
sco=ScoData(etud=etud, formsemestre=formsemestre),
cssstyles=[
"css/jury_but.css",
"css/cursus_but.css",
],
javascripts=("js/jury_but.js",),
content="\n".join(H),
)
@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.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.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
)
@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.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.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.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)
@bp.route(
"/formsemestre_jury_erase/<int:formsemestre_id>",
methods=["GET", "POST"],
defaults={"etudid": None},
)
@bp.route(
"/formsemestre_jury_erase/<int:formsemestre_id>/<int:etudid>",
methods=["GET", "POST"],
)
@scodoc
@permission_required(Permission.ScoView)
def formsemestre_jury_erase(formsemestre_id: int, etudid: int = None):
"""Supprime toutes les décisions de jury (classique ou BUT) pour cette année.
Si l'étudiant n'est pas spécifié, efface les décisions de tous les inscrits.
En BUT, si only_one_sem n'efface que pour le formsemestre indiqué, pas les deux de l'année.
En classique, n'affecte que les décisions issues de ce formsemestre.
"""
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,
)
)
is_apc = formsemestre.formation.is_apc()
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]
endpoint = (
"notes.formsemestre_validation_but"
if is_apc
else "notes.formsemestre_validation_etud_form"
)
dest_url = url_for(
endpoint,
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
etudid=etudid,
)
if request.method == "POST":
with sco_cache.DeferredSemCacheManager():
for etud in etuds:
if is_apc:
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
deca.erase(only_one_sem=only_one_sem)
else:
sco_formsemestre_validation.formsemestre_validation_suppress_etud(
formsemestre.id, etud.id
)
log(f"formsemestre_jury_erase({formsemestre_id}, {etud.id})")
flash(
(
"décisions de jury du semestre effacées"
if (only_one_sem or is_apc)
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 or is_apc)
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, ainsi que les validations de DUT en 120 ECTS
obtenues après BUT1/BUT2.
</p>
"""
if is_apc
else ""
+ """
<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.get_etud(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.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(etud=etud),
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.ScoView)
def jury_delete_manual(etudid: int):
"""Efface toute les décisions d'une année pour cet étudiant"""
etud = Identite.get_etud(etudid)
return jury_edit_manual.jury_delete_manual(etud)
@bp.route("/etud_bilan_ects/<int:etudid>")
@scodoc
@permission_required(Permission.ScoView)
def etud_bilan_ects(etudid: int):
"""Page bilan de tous les ECTS acquis par un étudiant.
Plusieurs formations (eg DUT, LP) peuvent être concernées.
"""
etud = Identite.get_etud(etudid)
# Cherche les formations différentes (au sens des ECTS)
# suivies par l'étudiant: regroupe ses formsemestres
# diplome est la clé: en classique le code formation, en BUT le referentiel_competence_id
formsemestre_by_diplome = defaultdict(list)
for formsemestre in etud.get_formsemestres(recent_first=True):
diplome = (
formsemestre.formation.referentiel_competence.id
if (
formsemestre.formation.is_apc()
and formsemestre.formation.referentiel_competence
)
else formsemestre.formation.formation_code
)
formsemestre_by_diplome[diplome].append(formsemestre)
# Pour chaque liste de formsemestres d'un même "diplôme"
# liste les UE validées avec leurs ECTS
ects_by_diplome = {}
titre_by_diplome = {} # { diplome : titre }
validations_by_diplome = {} # { diplome : query validations UEs }
validations_by_ue_code = defaultdict(list) # { ue_code : [validation] }
validations_by_niveau_sem = defaultdict(list) # { niveau_sem : [validation] }
for diplome, formsemestres in formsemestre_by_diplome.items():
formsemestre = formsemestres[0]
titre_by_diplome[diplome] = formsemestre.formation.get_titre_version()
if formsemestre.formation.is_apc():
validations = cursus_but.but_validations_ues(etud, diplome)
else:
validations = ScolarFormSemestreValidation.validations_ues(
etud, formsemestre.formation.formation_code
)
validations_by_diplome[diplome] = [
validation
for validation in validations
if validation.code in CODES_UE_VALIDES
]
ects_by_diplome[diplome] = sum(
(validation.ue.ects or 0.0)
for validation in validations_by_diplome[diplome]
)
for validation in validations:
validations_by_ue_code[validation.ue.ue_code].append(validation)
validations_by_niveau_sem[
(
(
validation.ue.niveau_competence.id
if validation.ue.niveau_competence
else None
),
validation.ue.semestre_idx,
)
].append(validation)
ref_comp_ids = {
v.ue.formation.referentiel_competence_id
for validations in validations_by_ue_code.values()
for v in validations
if v.ue.formation.referentiel_competence_id is not None
}
ue_warnings = []
if len(ref_comp_ids) > 1:
ue_warnings.append(
"""plusieurs référentiels de compétences utilisés&nbsp;!
(ok si plusieurs diplôme différents suivis)"""
)
for ue_code, validations in validations_by_ue_code.items():
ectss = {v.ue.ects for v in validations}
if len(ectss) > 1:
ects_str = ", ".join(
f"{v.ue.acronyme}: {v.ue.ects} ects" for v in validations
)
ue_acros = ", ".join({v.ue.acronyme for v in validations})
ue_warnings.append(
f"""Les UEs {ue_acros} ont le même code ({ue_code
}) mais des ECTS différents: {ects_str}"""
)
for (niveau_id, semestre_idx), validations in validations_by_niveau_sem.items():
if not validations:
continue # safeguard
formation = validations[0].ue.formation
ue_acros = ", ".join({v.ue.acronyme for v in validations})
if niveau_id is None and formation.is_apc():
ue_warnings.append(
f"""Les UEs {ue_acros} du S{semestre_idx
} n'ont pas de niveau de compétence associé !"""
)
ectss = {v.ue.ects for v in validations}
if len(ectss) > 1:
ects_str = ", ".join(
f"{v.ue.acronyme}: {v.ue.ects} ects" for v in validations
)
ue_warnings.append(
f"""Les UEs {ue_acros} du même code niveau de compétence
({validations[0].ue.niveau_competence}) ont des ECTS différents: {ects_str}"""
)
return render_template(
"jury/etud_bilan_ects.j2",
etud=etud,
ects_by_diplome=ects_by_diplome,
formsemestre_by_diplome=formsemestre_by_diplome,
titre_by_diplome=titre_by_diplome,
title=f"Bilan ECTS {etud.nomprenom}",
ue_warnings=ue_warnings,
validations_by_diplome=validations_by_diplome,
sco=ScoData(etud=etud),
)
@bp.route("/bilan_ues/<int:formsemestre_id>")
@scodoc
@permission_required(Permission.ScoView)
def formsemestre_bilan_ues(formsemestre_id: int):
"""Table bilan validations UEs pour les étudiants du semestre"""
fmt = request.args.get("fmt", "html")
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
table = bilan_ues.TableBilanUEs(formsemestre)
if fmt.startswith("xls"):
return scu.send_file(
table.excel(),
scu.make_filename(
f"""{formsemestre.titre_num()}-bilan-ues-{
datetime.datetime.now().strftime("%Y-%m-%dT%Hh%M")}"""
),
scu.XLSX_SUFFIX,
mime=scu.XLSX_MIMETYPE,
)
if fmt == "html":
return render_template(
"jury/formsemestre_bilan_ues.j2",
sco=ScoData(formsemestre=formsemestre),
table=table,
title=f"Bilan UEs {formsemestre.titre_num()}",
)
else:
raise ScoValueError("invalid fmt value")