1
0
forked from ScoDoc/ScoDoc

Séparation vues notes/jurys et validations

This commit is contained in:
Emmanuel Viennet 2024-07-09 13:37:22 +02:00
parent 9deae8cd6a
commit d4fd6527e5
6 changed files with 935 additions and 990 deletions

View File

@ -4,13 +4,10 @@
# See LICENSE
##############################################################################
"""Jury édition manuelle des décisions (correction d'erreurs, parcours hors normes)
Non spécifique au BUT.
"""Jury édition manuelle des décisions RCUE antérieures
"""
from flask import render_template
import sqlalchemy as sa
from app import log
from app.but import cursus_but

View File

@ -45,16 +45,15 @@ import app.scodoc.sco_utils as scu
# ---- Table recap formation
def formation_table_recap(formation_id, fmt="html") -> Response:
def formation_table_recap(formation: Formation, fmt="html") -> Response:
"""Table recapitulant formation."""
T = []
formation = Formation.query.get_or_404(formation_id)
rows = []
ues = formation.ues.order_by(UniteEns.semestre_idx, UniteEns.numero)
can_edit = current_user.has_permission(Permission.EditFormation)
li = 0
for ue in ues:
# L'UE
T.append(
rows.append(
{
"sem": f"S{ue.semestre_idx}" if ue.semestre_idx is not None else "-",
"_sem_order": f"{li:04d}",
@ -83,7 +82,7 @@ def formation_table_recap(formation_id, fmt="html") -> Response:
for mod in modules:
nb_moduleimpls = mod.modimpls.count()
# le module (ou ressource ou sae)
T.append(
rows.append(
{
"sem": (
f"S{mod.semestre_id}"
@ -152,7 +151,7 @@ def formation_table_recap(formation_id, fmt="html") -> Response:
tab = GenTable(
columns_ids=columns_ids,
rows=T,
rows=rows,
titles=titles,
origin=f"Généré par {scu.sco_version.SCONAME} le {scu.timedate_human_repr()}",
caption=title,
@ -168,7 +167,7 @@ def formation_table_recap(formation_id, fmt="html") -> Response:
}"
""",
html_with_td_classes=True,
base_url=f"{request.base_url}?formation_id={formation_id}",
base_url=f"{request.base_url}",
page_title=title,
html_title=f"<h2>{title}</h2>",
pdf_title=title,
@ -192,7 +191,7 @@ def export_recap_formations_annee_scolaire(annee_scolaire):
formation_ids = {formsemestre.formation.id for formsemestre in formsemestres}
for formation_id in formation_ids:
formation = db.session.get(Formation, formation_id)
xls = formation_table_recap(formation_id, fmt="xlsx").data
xls = formation_table_recap(formation, fmt="xlsx").data
filename = (
scu.sanitize_filename(formation.get_titre_version()) + scu.XLSX_SUFFIX
)

View File

@ -140,10 +140,15 @@ class ScoData:
return sco_formsemestre_status.formsemestre_status_menubar(self.formsemestre)
# Ajout des routes
from app.but import bulletin_but_court # ne pas enlever: ajoute des routes !
from app.but import jury_dut120 # ne pas enlever: ajoute des routes !
from app.pe import pe_view # ne pas enlever, ajoute des routes !
from app.views import (
absences,
assiduites,
but_formation,
jury_validations,
notes_formsemestre,
notes,
pn_modules,

View File

@ -0,0 +1,904 @@
##############################################################################
#
# 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
"""
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,
ScolarNews,
ScoDocSiteConfig,
)
from app.scodoc import (
html_sco_header,
sco_bulletins_json,
sco_cache,
sco_formsemestre_exterieurs,
sco_formsemestre_validation,
sco_preferences,
)
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.scodoc.TrivialFormulator import TrivialFormulator
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.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()
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 = [
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 {'' 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 "\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
)
@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)
@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 = 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.get_etud(etudid)
return jury_edit_manual.jury_delete_manual(etud)

View File

@ -30,7 +30,6 @@ Module notes: issu de ScoDoc7 / ZNotes.py
Emmanuel Viennet, 2021
"""
import datetime
import html
from operator import itemgetter
import time
@ -40,24 +39,13 @@ 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 db, log, send_scodoc_alarm
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 import jury_dut120 # ne pas enlever: ajoute des routes !
from app.but.forms import jury_but_forms
from app.but import apc_edit_ue, jury_but_pv
from app.comp import jury, res_sem
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.models import (
ApcNiveau,
@ -72,10 +60,7 @@ from app.models import (
Identite,
Module,
ModuleImpl,
ScolarAutorisationInscription,
ScolarNews,
Scolog,
ScoDocSiteConfig,
UniteEns,
)
from app.scodoc.sco_exceptions import ScoFormationConflict, ScoPermissionDenied
@ -90,9 +75,7 @@ from app.decorators import (
# ---------------
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 import sco_utils as scu
from app.scodoc.sco_exceptions import (
AccessDenied,
@ -131,7 +114,6 @@ from app.scodoc import (
sco_formsemestre_exterieurs,
sco_formsemestre_inscriptions,
sco_formsemestre_status,
sco_formsemestre_validation,
sco_groups_view,
sco_inscr_passage,
sco_liste_notes,
@ -157,7 +139,6 @@ from app.scodoc import (
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
@ -560,12 +541,14 @@ sco_publish(
)
@bp.route("/formation_table_recap")
@bp.route("/formation_table_recap/<int:formation_id>")
@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)
def formation_table_recap(formation_id: int):
"Tableau récap. de la formation"
formation = Formation.get_formation(formation_id)
fmt = request.args.get("fmt", "html")
return sco_formation_recap.formation_table_recap(formation, fmt=fmt)
sco_publish(
@ -829,9 +812,9 @@ sco_publish("/ue_move", sco_edit_formation.ue_move, Permission.EditFormation)
@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()
ue_id = request.form.get("ue_id")
ue = UniteEns.get_ue(ue_id)
_ = ue.clone()
db.session.commit()
flash(f"UE {ue.acronyme} dupliquée")
return flask.redirect(
@ -2205,520 +2188,6 @@ def appreciation_add_form(
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()
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 = [
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 {'' 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 "\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,
@ -2727,150 +2196,6 @@ sco_publish(
)
@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
@ -2878,192 +2203,6 @@ sco_publish(
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_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 = 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,
@ -3306,12 +2445,12 @@ def check_sem_integrity(formsemestre_id, fix=False):
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)
log(f"check_sem_integrity: problem detected: formations_set={formations_set}")
if sem["formation_id"] in formations_set:
formations_set.remove(sem["formation_id"])
if len(formations_set) == 1:
if fix:
log("check_sem_integrity: trying to fix %s" % formsemestre_id)
log(f"check_sem_integrity: trying to fix {formsemestre_id}")
formation_id = formations_set.pop()
if sem["formation_id"] != formation_id:
sem["formation_id"] = formation_id
@ -3319,11 +2458,11 @@ def check_sem_integrity(formsemestre_id, fix=False):
H.append("""<p class="alert">Problème réparé: vérifiez</p>""")
else:
H.append(
"""
f"""
<p class="alert">Problème détecté réparable:
<a href="check_sem_integrity?formsemestre_id=%s&fix=1">réparer maintenant</a></p>
<a href="check_sem_integrity?formsemestre_id={
formsemestre_id}&fix=1">réparer maintenant</a></p>
"""
% (formsemestre_id,)
)
else:
H.append("""<p class="alert">Problème détecté !</p>""")

View File

@ -1,99 +0,0 @@
#!/usr/bin/env python
# -*- mode: python -*-
# -*- coding: utf-8 -*-
"""Exemple connexion sur ScoDoc et utilisation de l'API
Attention: cet exemple est en Python 2.
Voir example-api-1.py pour une version en Python3 plus moderne.
"""
import urllib, urllib2
# A modifier pour votre serveur:
BASEURL = "https://scodoc.xxx.net/ScoDoc/RT/Scolarite"
USER = "XXX"
PASSWORD = "XXX"
values = {
"__ac_name": USER,
"__ac_password": PASSWORD,
}
# Configure memorisation des cookies:
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor())
urllib2.install_opener(opener)
data = urllib.urlencode(values)
req = urllib2.Request(BASEURL, data) # this is a POST http request
response = urllib2.urlopen(req)
# --- Use API
# Affiche la liste des formations en format XML
req = urllib2.Request(BASEURL + "/Notes/formation_list?fmt=xml")
response = urllib2.urlopen(req)
print response.read()[:100] # limite aux 100 premiers caracteres...
# Recupere la liste de tous les semestres:
req = urllib2.Request(BASEURL + "/Notes/formsemestre_list?fmt=json") # format json
response = urllib2.urlopen(req)
js_data = response.read()
# Plus amusant: va retrouver le bulletin de notes du premier etudiant (au hasard donc) du premier semestre (au hasard aussi)
try:
import json # Attention: ceci demande Python >= 2.6
except:
import simplejson as json # python2.4 with simplejson installed
data = json.loads(js_data) # decode la reponse JSON
if not data:
print "Aucun semestre !"
else:
formsemestre_id = str(data[0]["formsemestre_id"])
# Obtient la liste des groupes:
req = urllib2.Request(
BASEURL
+ "/Notes/formsemestre_partition_list?fmt=json&formsemestre_id="
+ str(formsemestre_id)
) # format json
response = urllib2.urlopen(req)
js_data = response.read()
data = json.loads(js_data)
group_id = data[0]["group"][0][
"group_id"
] # premier groupe (normalement existe toujours)
# Liste les étudiants de ce groupe:
req = urllib2.Request(
BASEURL + "/Notes/group_list?fmt=json&with_codes=1&group_id=" + str(group_id)
) # format json
response = urllib2.urlopen(req)
js_data = response.read()
data = json.loads(js_data)
# Le code du premier étudiant:
if not data:
print ("pas d'etudiants dans ce semestre !")
else:
etudid = data[0]["etudid"]
# Récupère bulletin de notes:
req = urllib2.Request(
BASEURL
+ "/Notes/formsemestre_bulletinetud?formsemestre_id="
+ str(formsemestre_id)
+ "&etudid="
+ str(etudid)
+ "&fmt=xml"
) # format XML ici !
response = urllib2.urlopen(req)
xml_bulletin = response.read()
print "----- Bulletin de notes en XML:"
print xml_bulletin
# Récupère la moyenne générale:
import xml.dom.minidom
doc = xml.dom.minidom.parseString(xml_bulletin)
moy = doc.getElementsByTagName("note")[0].getAttribute(
"value"
) # une chaine unicode
print "\nMoyenne generale: ", moy