forked from ScoDoc/ScoDoc
1064 lines
37 KiB
Python
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} <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> {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)} :
|
|
êtes-vous certain de vouloir enregistrer une décision de jury ?
|
|
</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 :</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 !
|
|
(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")
|