Refactoring et uniformisation tables jury/recap.

This commit is contained in:
Emmanuel Viennet 2023-02-12 01:13:43 +01:00
parent fa911907ad
commit 4db6ee368a
18 changed files with 380 additions and 413 deletions

View File

@ -13,6 +13,7 @@
import datetime import datetime
from functools import cached_property from functools import cached_property
from flask_login import current_user
import flask_sqlalchemy import flask_sqlalchemy
from flask import flash, g from flask import flash, g
from sqlalchemy import and_, or_ from sqlalchemy import and_, or_
@ -20,6 +21,7 @@ from sqlalchemy.sql import text
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app import db, log from app import db, log
from app.auth.models import User
from app.models import APO_CODE_STR_LEN, CODE_STR_LEN, SHORT_STR_LEN from app.models import APO_CODE_STR_LEN, CODE_STR_LEN, SHORT_STR_LEN
from app.models.but_refcomp import ( from app.models.but_refcomp import (
ApcAnneeParcours, ApcAnneeParcours,
@ -535,10 +537,32 @@ class FormSemestre(db.Model):
else: else:
return ", ".join([u.get_nomcomplet() for u in self.responsables]) return ", ".join([u.get_nomcomplet() for u in self.responsables])
def est_responsable(self, user): def est_responsable(self, user: User):
"True si l'user est l'un des responsables du semestre" "True si l'user est l'un des responsables du semestre"
return user.id in [u.id for u in self.responsables] return user.id in [u.id for u in self.responsables]
def est_chef_or_diretud(self, user: User = None):
"Vrai si utilisateur (par def. current) est admin, chef dept ou responsable du semestre"
user = user or current_user
return user.has_permission(Permission.ScoImplement) or self.est_responsable(
user
)
def can_edit_jury(self, user: User = None):
"""Vrai si utilisateur (par def. current) peut saisir decision de jury
dans ce semestre: vérifie permission et verrouillage.
"""
user = user or current_user
return self.etat and self.est_chef_or_diretud(user)
def can_edit_pv(self, user: User = None):
"Vrai si utilisateur (par def. current) peut editer un PV de jury de ce semestre"
user = user or current_user
# Autorise les secrétariats, repérés via la permission ScoEtudChangeAdr
return self.est_chef_or_diretud(user) or user.has_permission(
Permission.ScoEtudChangeAdr
)
def annee_scolaire(self) -> int: def annee_scolaire(self) -> int:
"""L'année de début de l'année scolaire. """L'année de début de l'année scolaire.
Par exemple, 2022 si le semestre va de septembre 2022 à février 2023.""" Par exemple, 2022 si le semestre va de septembre 2022 à février 2023."""

View File

@ -251,7 +251,7 @@ def sco_header(
#gtrcontent {{ #gtrcontent {{
margin-left: {params["margin_left"]}; margin-left: {params["margin_left"]};
height: 100%%; height: 100%%;
margin-bottom: 10px; margin-bottom: 16px;
}} }}
</style> </style>
""" """

View File

@ -47,7 +47,7 @@
qui est une description (humaine, format libre) de l'archive. qui est une description (humaine, format libre) de l'archive.
""" """
import chardet from typing import Union
import datetime import datetime
import glob import glob
import json import json
@ -56,10 +56,11 @@ import os
import re import re
import shutil import shutil
import time import time
from typing import Union
import chardet
import flask import flask
from flask import g, request from flask import flash, g, request, url_for
from flask_login import current_user from flask_login import current_user
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -70,9 +71,7 @@ from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.models import Departement, FormSemestre from app.models import Departement, FormSemestre
from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc.sco_exceptions import ( from app.scodoc.sco_exceptions import AccessDenied, ScoPermissionDenied
AccessDenied,
)
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_bulletins_pdf from app.scodoc import sco_bulletins_pdf
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
@ -314,7 +313,7 @@ def do_formsemestre_archive(
""" """
from app.scodoc.sco_recapcomplet import ( from app.scodoc.sco_recapcomplet import (
gen_formsemestre_recapcomplet_excel, gen_formsemestre_recapcomplet_excel,
gen_formsemestre_recapcomplet_html, gen_formsemestre_recapcomplet_html_table,
gen_formsemestre_recapcomplet_json, gen_formsemestre_recapcomplet_json,
) )
@ -338,7 +337,7 @@ def do_formsemestre_archive(
if data: if data:
PVArchive.store(archive_id, "Tableau_moyennes" + scu.XLSX_SUFFIX, data) PVArchive.store(archive_id, "Tableau_moyennes" + scu.XLSX_SUFFIX, data)
# Tableau recap notes en HTML (pour tous les etudiants, n'utilise pas les groupes) # Tableau recap notes en HTML (pour tous les etudiants, n'utilise pas les groupes)
table_html = gen_formsemestre_recapcomplet_html( table_html, _ = gen_formsemestre_recapcomplet_html_table(
formsemestre, res, include_evaluations=True formsemestre, res, include_evaluations=True
) )
if table_html: if table_html:
@ -416,8 +415,15 @@ def formsemestre_archive(formsemestre_id, group_ids=[]):
"""Make and store new archive for this formsemestre. """Make and store new archive for this formsemestre.
(all students or only selected groups) (all students or only selected groups)
""" """
if not sco_permissions_check.can_edit_pv(formsemestre_id): formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
raise AccessDenied("opération non autorisée pour %s" % str(current_user)) if not formsemestre.can_edit_pv():
raise ScoPermissionDenied(
dest_url=url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
)
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
if not group_ids: if not group_ids:
@ -579,26 +585,38 @@ def formsemestre_list_archives(formsemestre_id):
def formsemestre_get_archived_file(formsemestre_id, archive_name, filename): def formsemestre_get_archived_file(formsemestre_id, archive_name, filename):
"""Send file to client.""" """Send file to client."""
sem = sco_formsemestre.get_formsemestre(formsemestre_id) formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
sem_archive_id = formsemestre_id sem_archive_id = formsemestre.id
return PVArchive.get_archived_file(sem_archive_id, archive_name, filename) return PVArchive.get_archived_file(sem_archive_id, archive_name, filename)
def formsemestre_delete_archive(formsemestre_id, archive_name, dialog_confirmed=False): def formsemestre_delete_archive(formsemestre_id, archive_name, dialog_confirmed=False):
"""Delete an archive""" """Delete an archive"""
if not sco_permissions_check.can_edit_pv(formsemestre_id): formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
raise AccessDenied("opération non autorisée pour %s" % str(current_user)) if not formsemestre.can_edit_pv():
sem = sco_formsemestre.get_formsemestre(formsemestre_id) raise ScoPermissionDenied(
dest_url=url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
)
sem_archive_id = formsemestre_id sem_archive_id = formsemestre_id
archive_id = PVArchive.get_id_from_name(sem_archive_id, archive_name) archive_id = PVArchive.get_id_from_name(sem_archive_id, archive_name)
dest_url = "formsemestre_list_archives?formsemestre_id=%s" % (formsemestre_id) dest_url = url_for(
"notes.formsemestre_list_archives",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
if not dialog_confirmed: if not dialog_confirmed:
return scu.confirm_dialog( return scu.confirm_dialog(
"""<h2>Confirmer la suppression de l'archive du %s ?</h2> f"""<h2>Confirmer la suppression de l'archive du {
<p>La suppression sera définitive.</p>""" PVArchive.get_archive_date(archive_id).strftime("%d/%m/%Y %H:%M")
% PVArchive.get_archive_date(archive_id).strftime("%d/%m/%Y %H:%M"), } ?</h2>
<p>La suppression sera définitive.</p>
""",
dest_url="", dest_url="",
cancel_url=dest_url, cancel_url=dest_url,
parameters={ parameters={
@ -608,4 +626,5 @@ def formsemestre_delete_archive(formsemestre_id, archive_name, dialog_confirmed=
) )
PVArchive.delete_archive(archive_id) PVArchive.delete_archive(archive_id)
return flask.redirect(dest_url + "&head_message=Archive%20supprimée") flash("Archive supprimée")
return flask.redirect(dest_url)

View File

@ -1208,7 +1208,7 @@ def make_menu_autres_operations(
"formsemestre_id": formsemestre.id, "formsemestre_id": formsemestre.id,
"etudid": etud.id, "etudid": etud.id,
}, },
"enabled": sco_permissions_check.can_validate_sem(formsemestre.id), "enabled": formsemestre.can_edit_jury(),
}, },
{ {
"title": "Enregistrer note d'une UE externe", "title": "Enregistrer note d'une UE externe",
@ -1217,7 +1217,7 @@ def make_menu_autres_operations(
"formsemestre_id": formsemestre.id, "formsemestre_id": formsemestre.id,
"etudid": etud.id, "etudid": etud.id,
}, },
"enabled": sco_permissions_check.can_validate_sem(formsemestre.id) "enabled": formsemestre.can_edit_jury()
and not formsemestre.formation.is_apc(), and not formsemestre.formation.is_apc(),
}, },
{ {
@ -1227,7 +1227,7 @@ def make_menu_autres_operations(
"formsemestre_id": formsemestre.id, "formsemestre_id": formsemestre.id,
"etudid": etud.id, "etudid": etud.id,
}, },
"enabled": sco_permissions_check.can_validate_sem(formsemestre.id), "enabled": formsemestre.can_edit_jury(),
}, },
{ {
"title": "Éditer PV jury", "title": "Éditer PV jury",

View File

@ -27,22 +27,21 @@
"""Exception handling """Exception handling
""" """
from flask_login import current_user
# --- Exceptions # --- Exceptions
MSGPERMDENIED = "l'utilisateur %s n'a pas le droit d'effectuer cette operation"
class ScoException(Exception): class ScoException(Exception):
pass "super classe de toutes les exceptions ScoDoc."
class InvalidNoteValue(ScoException): class InvalidNoteValue(ScoException):
pass "Valeur note invalide. Usage interne saisie note."
class ScoValueError(ScoException): class ScoValueError(ScoException):
"Exception avec page d'erreur utilisateur, et qui stoque dest_url" "Exception avec page d'erreur utilisateur, et qui stoque dest_url"
# mal nommée: super classe de toutes les exceptions avec page
# d'erreur gentille.
def __init__(self, msg, dest_url=None): def __init__(self, msg, dest_url=None):
super().__init__(msg) super().__init__(msg)
self.dest_url = dest_url self.dest_url = dest_url
@ -53,7 +52,9 @@ class ScoPermissionDenied(ScoValueError):
def __init__(self, msg=None, dest_url=None): def __init__(self, msg=None, dest_url=None):
if msg is None: if msg is None:
msg = "Opération non autorisée !" msg = f"""Opération non autorisée pour {
current_user.get_nomcomplet() if current_user else "?"
}. Pas la permission, ou objet verrouillé."""
super().__init__(msg, dest_url=dest_url) super().__init__(msg, dest_url=dest_url)

View File

@ -431,17 +431,18 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
}, },
{ {
"title": "Saisie des décisions du jury", "title": "Saisie des décisions du jury",
"endpoint": "notes.formsemestre_saisie_jury", "endpoint": "notes.formsemestre_recapcomplet",
"args": { "args": {
"formsemestre_id": formsemestre_id, "formsemestre_id": formsemestre_id,
"mode_jury": 1,
}, },
"enabled": sco_permissions_check.can_validate_sem(formsemestre_id), "enabled": formsemestre.can_edit_jury(),
}, },
{ {
"title": "Éditer les PV et archiver les résultats", "title": "Éditer les PV et archiver les résultats",
"endpoint": "notes.formsemestre_archive", "endpoint": "notes.formsemestre_archive",
"args": {"formsemestre_id": formsemestre_id}, "args": {"formsemestre_id": formsemestre_id},
"enabled": sco_permissions_check.can_edit_pv(formsemestre_id), "enabled": formsemestre.can_edit_pv(),
}, },
{ {
"title": "Documents archivés", "title": "Documents archivés",

View File

@ -72,9 +72,9 @@ def formsemestre_validation_etud_form(
etudid=None, # one of etudid or etud_index is required etudid=None, # one of etudid or etud_index is required
etud_index=None, etud_index=None,
check=0, # opt: si true, propose juste une relecture du parcours check=0, # opt: si true, propose juste une relecture du parcours
desturl=None, dest_url=None,
sortcol=None, sortcol=None,
readonly=True, read_only=True,
): ):
"""Formulaire de validation des décisions de jury""" """Formulaire de validation des décisions de jury"""
formsemestre: FormSemestre = FormSemestre.query.filter_by( formsemestre: FormSemestre = FormSemestre.query.filter_by(
@ -111,7 +111,7 @@ def formsemestre_validation_etud_form(
etud_index_prev = etud_index - 1 etud_index_prev = etud_index - 1
if etud_index_prev < 0: if etud_index_prev < 0:
etud_index_prev = None etud_index_prev = None
if readonly: if read_only:
check = True check = True
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
@ -216,13 +216,13 @@ def formsemestre_validation_etud_form(
H.append( H.append(
formsemestre_recap_parcours_table( formsemestre_recap_parcours_table(
Se, etudid, with_links=(check and not readonly) Se, etudid, with_links=(check and not read_only)
) )
) )
if check: if check:
if not desturl: if not dest_url:
desturl = url_tableau dest_url = url_tableau
H.append(f'<ul><li><a href="{desturl}">Continuer</a></li></ul>') H.append(f'<ul><li><a href="{dest_url}">Continuer</a></li></ul>')
return "\n".join(H + footer) return "\n".join(H + footer)
@ -342,8 +342,8 @@ def formsemestre_validation_etud_form(
<input type="hidden" name="formsemestre_id" value="%s"/>""" <input type="hidden" name="formsemestre_id" value="%s"/>"""
% (etudid, formsemestre_id) % (etudid, formsemestre_id)
) )
if desturl: if dest_url:
H.append('<input type="hidden" name="desturl" value="%s"/>' % desturl) H.append('<input type="hidden" name="desturl" value="%s"/>' % dest_url)
if sortcol: if sortcol:
H.append('<input type="hidden" name="sortcol" value="%s"/>' % sortcol) H.append('<input type="hidden" name="sortcol" value="%s"/>' % sortcol)

View File

@ -55,10 +55,6 @@ _SCO_PERMISSIONS = (
), ),
# 27 à 39 ... réservé pour "entreprises" # 27 à 39 ... réservé pour "entreprises"
(1 << 40, "ScoEtudChangePhoto", "Modifier la photo d'un étudiant"), (1 << 40, "ScoEtudChangePhoto", "Modifier la photo d'un étudiant"),
# Api scodoc9
# XXX à revoir
# (1 << 42, "APIEditAllNotes", "API: Modifier toutes les notes"),
# (1 << 43, "APIAbsChange", "API: Saisir des absences"),
) )

View File

@ -101,30 +101,7 @@ def can_edit_suivi():
return current_user.has_permission(Permission.ScoEtudChangeAdr) return current_user.has_permission(Permission.ScoEtudChangeAdr)
def can_validate_sem(formsemestre_id): def is_chef_or_diretud(sem): # remplacé par formsemestre.est_chef_or_diretud
"Vrai si utilisateur peut saisir decision de jury dans ce semestre"
from app.scodoc import sco_formsemestre
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
if not sem["etat"]:
return False # semestre verrouillé
return is_chef_or_diretud(sem)
def can_edit_pv(formsemestre_id):
"Vrai si utilisateur peut editer un PV de jury de ce semestre"
from app.scodoc import sco_formsemestre
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
if is_chef_or_diretud(sem):
return True
# Autorise les secrétariats, repérés via la permission ScoEtudChangeAdr
# (ceci nous évite d'ajouter une permission Zope aux installations existantes)
return current_user.has_permission(Permission.ScoEtudChangeAdr)
def is_chef_or_diretud(sem):
"Vrai si utilisateur est admin, chef dept ou responsable du semestre" "Vrai si utilisateur est admin, chef dept ou responsable du semestre"
if ( if (
current_user.has_permission(Permission.ScoImplement) current_user.has_permission(Permission.ScoImplement)

View File

@ -1243,7 +1243,7 @@ class BasePreferences(object):
{ {
"initvalue": 0, "initvalue": 0,
"title": "Afficher toutes les évaluations sur les bulletins", "title": "Afficher toutes les évaluations sur les bulletins",
"explanation": "y compris incomplètes ou futures (déconseillé, risque de publier des notes non définitives)", "explanation": "y compris incomplètes ou futures (déconseillé, risque de publier des notes non définitives; n'affecte pas le calcul des moyennes)",
"input_type": "boolcheckbox", "input_type": "boolcheckbox",
"category": "bul", "category": "bul",
"labels": ["non", "oui"], "labels": ["non", "oui"],

View File

@ -516,18 +516,18 @@ def pvjury_table(
def formsemestre_pvjury(formsemestre_id, format="html", publish=True): def formsemestre_pvjury(formsemestre_id, format="html", publish=True):
"""Page récapitulant les décisions de jury""" """Page récapitulant les décisions de jury"""
# Bretelle provisoire pour BUT 9.3.0
# XXX TODO
formsemestre = FormSemestre.query.get_or_404(formsemestre_id) formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
is_apc = formsemestre.formation.is_apc() is_apc = formsemestre.formation.is_apc()
if format == "html" and is_apc and formsemestre.semestre_id % 2 == 0: if format == "html" and is_apc:
from app.tables import jury_recap return redirect(
url_for(
return jury_recap.formsemestre_saisie_jury_but( "notes.formsemestre_recapcomplet",
formsemestre, read_only=True, mode="recap" scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
mode_jury=1,
)
) )
# /XXX
footer = html_sco_header.sco_footer() footer = html_sco_header.sco_footer()
dpv = dict_pvjury(formsemestre_id, with_prev=True) dpv = dict_pvjury(formsemestre_id, with_prev=True)

View File

@ -51,7 +51,6 @@ from app.scodoc import sco_evaluations
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_status from app.scodoc import sco_formsemestre_status
from app.scodoc import sco_permissions_check
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.tables.recap import TableRecap from app.tables.recap import TableRecap
from app.tables.jury_recap import TableJury from app.tables.jury_recap import TableJury
@ -95,17 +94,26 @@ def formsemestre_recapcomplet(
mode_jury = int(mode_jury) mode_jury = int(mode_jury)
xml_with_decisions = int(xml_with_decisions) xml_with_decisions = int(xml_with_decisions)
force_publishing = int(force_publishing) force_publishing = int(force_publishing)
filename = scu.sanitize_filename(
data = _do_formsemestre_recapcomplet( f"""recap-{formsemestre.titre_num()}-{time.strftime("%Y-%m-%d")}"""
formsemestre_id,
format=tabformat,
mode_jury=mode_jury,
xml_with_decisions=xml_with_decisions,
force_publishing=force_publishing,
selected_etudid=selected_etudid,
) )
if is_file: if is_file:
return data return _formsemestre_recapcomplet_to_file(
formsemestre,
tabformat=tabformat,
filename=filename,
xml_with_decisions=xml_with_decisions,
force_publishing=force_publishing,
)
table_html, table = _formsemestre_recapcomplet_to_html(
formsemestre,
filename=filename,
mode_jury=mode_jury,
tabformat=tabformat,
selected_etudid=selected_etudid,
)
H = [ H = [
html_sco_header.sco_header( html_sco_header.sco_header(
page_title=f"{formsemestre.sem_modalite()}: " page_title=f"{formsemestre.sem_modalite()}: "
@ -131,64 +139,90 @@ def formsemestre_recapcomplet(
H.append( H.append(
'<select name="tabformat" onchange="document.f.submit()" class="noprint">' '<select name="tabformat" onchange="document.f.submit()" class="noprint">'
) )
for (format, label) in ( for (fmt, label) in (
("html", "Tableau"), ("html", "Tableau"),
("evals", "Avec toutes les évaluations"), ("evals", "Avec toutes les évaluations"),
("xlsx", "Excel (non formaté)"), ("xlsx", "Excel (non formaté)"),
("xlsall", "Excel avec évaluations"), ("xlsall", "Excel avec évaluations"),
("xml", "Bulletins XML (obsolète)"),
("json", "Bulletins JSON"), ("json", "Bulletins JSON"),
): ):
if format == tabformat: if fmt == tabformat:
selected = " selected" selected = " selected"
else: else:
selected = "" selected = ""
H.append(f'<option value="{format}"{selected}>{label}</option>') H.append(f'<option value="{fmt}"{selected}>{label}</option>')
H.append("</select>")
H.append( H.append(
f"""&nbsp;(cliquer sur un nom pour afficher son bulletin ou <a class="stdlink" f"""
</select>&nbsp;(cliquer sur un nom pour afficher son bulletin ou
<a class="stdlink"
href="{url_for('notes.formsemestre_bulletins_pdf', href="{url_for('notes.formsemestre_bulletins_pdf',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id) scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
}">ici avoir le classeur papier</a>) }">ici avoir le classeur papier</a>)
</form>
""" """
) )
H.append(data)
H.append(table_html) # La table
if len(formsemestre.inscriptions) > 0: if len(formsemestre.inscriptions) > 0:
H.append("</form>") H.append("""<div class="links_under_recap"><ul>""")
H.append( H.append(
f"""<p><a class="stdlink" href="{url_for('notes.formsemestre_pvjury', f"""<li><a class="stdlink" href="{url_for('notes.formsemestre_recapcomplet',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id) scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, mode_jury=1)
}">Voir les décisions du jury</a></p>""" }">Décisions du jury</a>
</li>
"""
) )
if sco_permissions_check.can_validate_sem(formsemestre_id): if formsemestre.can_edit_jury():
H.append("<p>")
if mode_jury: if mode_jury:
H.append( H.append(
f"""<p><a class="stdlink" href="{url_for('notes.formsemestre_validation_auto', f"""<li><a class="stdlink" href="{url_for('notes.formsemestre_validation_auto',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id) scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
}">Calcul automatique des décisions du jury</a> }">Calcul automatique des décisions du jury</a>
</p><a class="stdlink" href="{url_for('notes.formsemestre_jury_but_erase', </li>
<li><a class="stdlink" href="{url_for('notes.formsemestre_jury_but_erase',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, only_one_sem=1) scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, only_one_sem=1)
}">Effacer <em>toutes</em> les décisions de jury du semestre</a> }">Effacer <em>toutes</em> les décisions de jury (BUT) du semestre</a>
<p> </li>
</p>
""" """
) )
else: H.append("</ul></div>")
H.append(
f"""<a class="stdlink" href="{url_for('notes.formsemestre_recapcomplet',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, mode_jury=1)
}">Saisie des décisions du jury</a>"""
)
H.append("</p>")
if sco_preferences.get_preference("use_ue_coefs", formsemestre_id): if sco_preferences.get_preference("use_ue_coefs", formsemestre_id):
H.append( H.append(
""" """
<p class="infop">utilise les coefficients d'UE pour calculer la moyenne générale.</p> <p class="infop">utilise les coefficients d'UE pour calculer la moyenne générale.</p>
""" """
) )
if mode_jury and table and sum(table.freq_codes_annuels.values()) > 0:
H.append(
f"""
<div class="jury_stats">
<div>Nb d'étudiants avec décision annuelle:
{sum(table.freq_codes_annuels.values())} / {len(table)}
</div>
<div><b>Codes annuels octroyés:</b></div>
<table class="jury_stats_codes">
"""
)
for code in sorted(table.freq_codes_annuels.keys()):
H.append(
f"""<tr>
<td>{code}</td>
<td style="text-align:right">{table.freq_codes_annuels[code]}</td>
<td style="text-align:right">{
(100*table.freq_codes_annuels[code] / len(table)):2.1f}%
</td>
</tr>"""
)
H.append(
"""
</table>
</div>
"""
)
H.append(html_sco_header.sco_footer()) H.append(html_sco_header.sco_footer())
# HTML or binary data ? # HTML or binary data ?
if len(H) > 1: if len(H) > 1:
@ -199,62 +233,69 @@ def formsemestre_recapcomplet(
return H return H
def _do_formsemestre_recapcomplet( def _formsemestre_recapcomplet_to_html(
formsemestre_id=None, formsemestre: FormSemestre,
format="html", # html, xml, xls, xlsall, json tabformat="html", # "html" or "evals"
xml_nodate=False, # format XML sans dates (sert pour debug cache: comparaison de XML) filename: str = "",
mode_jury=False, # saisie décisions jury mode_jury=False, # saisie décisions jury
selected_etudid=None,
) -> tuple[str, TableRecap]:
"""Le tableau recap en html"""
if tabformat not in ("html", "evals"):
raise ScoValueError("invalid table format")
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
table_html, table = gen_formsemestre_recapcomplet_html_table(
formsemestre,
res,
include_evaluations=(tabformat == "evals"),
mode_jury=mode_jury,
filename=filename,
selected_etudid=selected_etudid,
)
return table_html, table
def _formsemestre_recapcomplet_to_file(
formsemestre: FormSemestre,
tabformat: str = "json", # xml, xls, xlsall, json
filename: str = "",
xml_nodate=False, # format XML sans dates (sert pour debug cache: comparaison de XML)
xml_with_decisions=False, xml_with_decisions=False,
force_publishing=True, force_publishing=True,
selected_etudid=None,
): ):
"""Calcule et renvoie le tableau récapitulatif.""" """Calcule et renvoie le tableau récapitulatif."""
formsemestre = FormSemestre.query.get_or_404(formsemestre_id) if tabformat.startswith("xls"):
filename = scu.sanitize_filename(
f"""recap-{formsemestre.titre_num()}-{time.strftime("%Y-%m-%d")}"""
)
if format == "html" or format == "evals":
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
data = gen_formsemestre_recapcomplet_html( include_evaluations = tabformat in {
formsemestre, "xlsall",
res, "csv ",
include_evaluations=(format == "evals"), } # csv not supported anymore
mode_jury=mode_jury, if tabformat != "csv":
filename=filename, tabformat = "xlsx"
selected_etudid=selected_etudid,
)
return data
elif format.startswith("xls"):
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
include_evaluations = format in {"xlsall", "csv "} # csv not supported anymore
if format != "csv":
format = "xlsx"
data, filename = gen_formsemestre_recapcomplet_excel( data, filename = gen_formsemestre_recapcomplet_excel(
res, res,
include_evaluations=include_evaluations, include_evaluations=include_evaluations,
filename=filename, filename=filename,
) )
return scu.send_file(data, filename=filename, mime=scu.get_mime_suffix(format)) return scu.send_file(data, filename=filename, mime=scu.get_mime_suffix(format))
elif format == "xml": elif tabformat == "xml":
data = gen_formsemestre_recapcomplet_xml( data = gen_formsemestre_recapcomplet_xml(
formsemestre_id, formsemestre.id,
xml_nodate, xml_nodate,
xml_with_decisions=xml_with_decisions, xml_with_decisions=xml_with_decisions,
force_publishing=force_publishing, force_publishing=force_publishing,
) )
return scu.send_file(data, filename=filename, suffix=scu.XML_SUFFIX) return scu.send_file(data, filename=filename, suffix=scu.XML_SUFFIX)
elif format == "json": elif tabformat == "json":
data = gen_formsemestre_recapcomplet_json( data = gen_formsemestre_recapcomplet_json(
formsemestre_id, formsemestre.id,
xml_nodate=xml_nodate, xml_nodate=xml_nodate,
xml_with_decisions=xml_with_decisions, xml_with_decisions=xml_with_decisions,
force_publishing=force_publishing, force_publishing=force_publishing,
) )
return scu.sendJSON(data, filename=filename) return scu.sendJSON(data, filename=filename)
raise ScoValueError(f"Format demandé invalide: {format}") raise ScoValueError(f"Format demandé invalide: {tabformat}")
def gen_formsemestre_recapcomplet_xml( def gen_formsemestre_recapcomplet_xml(
@ -368,22 +409,26 @@ def formsemestres_bulletins(annee_scolaire):
return scu.sendJSON(js_list) return scu.sendJSON(js_list)
def gen_formsemestre_recapcomplet_html( def gen_formsemestre_recapcomplet_html_table(
formsemestre: FormSemestre, formsemestre: FormSemestre,
res: NotesTableCompat, res: NotesTableCompat,
include_evaluations=False, include_evaluations=False,
mode_jury=False, mode_jury=False,
filename="", filename="",
selected_etudid=None, selected_etudid=None,
): ) -> tuple[str, TableRecap]:
"""Construit table recap pour le BUT """Construit table recap pour le BUT
Cache le résultat pour le semestre (sauf en mode jury). Cache le résultat pour le semestre (sauf en mode jury).
Note: on cache le HTML et non l'objet Table.
Si mode_jury, cache colonnes modules et affiche un lien vers la saisie de la décision de jury Si mode_jury, occultera colonnes modules (en js)
et affiche un lien vers la saisie de la décision de jury
Return: data, filename Return: html (str), table (None sauf en mode jury ou si pas cachée)
data est une chaine, le <div>...</div> incluant le tableau.
html est une chaine, le <div>...</div> incluant le tableau.
""" """
table = None
table_html = None table_html = None
if not (mode_jury or selected_etudid): if not (mode_jury or selected_etudid):
if include_evaluations: if include_evaluations:
@ -392,7 +437,7 @@ def gen_formsemestre_recapcomplet_html(
table_html = sco_cache.TableRecapCache.get(formsemestre.id) table_html = sco_cache.TableRecapCache.get(formsemestre.id)
# en mode jury ne cache pas la table html # en mode jury ne cache pas la table html
if mode_jury or (table_html is None): if mode_jury or (table_html is None):
table_html = _gen_formsemestre_recapcomplet_html( table = _gen_formsemestre_recapcomplet_table(
formsemestre, formsemestre,
res, res,
include_evaluations, include_evaluations,
@ -400,48 +445,37 @@ def gen_formsemestre_recapcomplet_html(
filename, filename,
selected_etudid=selected_etudid, selected_etudid=selected_etudid,
) )
table_html = table.html()
if not mode_jury: if not mode_jury:
if include_evaluations: if include_evaluations:
sco_cache.TableRecapWithEvalsCache.set(formsemestre.id, table_html) sco_cache.TableRecapWithEvalsCache.set(formsemestre.id, table_html)
else: else:
sco_cache.TableRecapCache.set(formsemestre.id, table_html) sco_cache.TableRecapCache.set(formsemestre.id, table_html)
return table_html return table_html, table
def _gen_formsemestre_recapcomplet_html( def _gen_formsemestre_recapcomplet_table(
formsemestre: FormSemestre, formsemestre: FormSemestre,
res: ResultatsSemestre, res: ResultatsSemestre,
include_evaluations=False, include_evaluations=False,
mode_jury=False, mode_jury=False,
filename: str = "", filename: str = "",
selected_etudid=None, selected_etudid=None,
) -> str: ) -> TableRecap:
"""Génère le html""" """Construit la table récap."""
table_class = TableJury if mode_jury else TableRecap table_class = TableJury if mode_jury else TableRecap
table = table_class( table = table_class(
res, res,
convert_values=True, convert_values=True,
include_evaluations=include_evaluations, include_evaluations=include_evaluations,
mode_jury=mode_jury, mode_jury=mode_jury,
read_only=not formsemestre.can_edit_jury(),
) )
table.data["filename"] = filename table.data["filename"] = filename
table.select_row(selected_etudid) table.select_row(selected_etudid)
return f""" return table
<div class="table_recap">
{
'<div class="message">aucun étudiant !</div>'
if table.is_empty()
else table.html(
extra_classes=[
'table_recap',
'apc' if formsemestre.formation.is_apc() else 'classic',
'jury' if mode_jury else ''
])
}
</div>
"""
def gen_formsemestre_recapcomplet_excel( def gen_formsemestre_recapcomplet_excel(

View File

@ -1,6 +1,7 @@
/* /*
* DataTables style for ScoDoc gen_tables * DataTables style for ScoDoc gen_tables
* generated using https://datatables.net/manual/styling/theme-creator * generated using https://datatables.net/manual/styling/theme-creator
* and customized by hand
*/ */
/* /*
@ -374,9 +375,11 @@ table.dataTable td {
float: left; float: left;
} }
.dataTables_wrapper .dataTables_filter { .dataTables_wrapper div.dataTables_filter {
float: right; float: left;
text-align: right; text-align: left;
margin-left: 64px;
margin-top: 4px;
} }
.dataTables_wrapper .dataTables_filter input { .dataTables_wrapper .dataTables_filter input {

View File

@ -35,7 +35,7 @@ h3 {
} }
div#gtrcontent { div#gtrcontent {
margin-bottom: 4ex; margin-bottom: 16px;
} }
.gtrcontent { .gtrcontent {
@ -4015,8 +4015,14 @@ div.table_recap {
background: linear-gradient(to bottom, rgb(51, 255, 0) 0%, lightgray 100%); background: linear-gradient(to bottom, rgb(51, 255, 0) 0%, lightgray 100%);
} }
/* Non supproté par les navigateurs (en Fev. 2023)
.table_recap button:has(span a.clearreaload) {
}
*/
div.table_recap table.table_recap { div.table_recap table.table_recap {
width: auto; width: auto;
margin-left: 0px;
/* font-family: Consolas, monaco, monospace; */ /* font-family: Consolas, monaco, monospace; */
} }
@ -4344,6 +4350,9 @@ div.table_jury_but_links {
margin-bottom: 16px; margin-bottom: 16px;
} }
div.links_under_recap ul li {
padding-bottom: 8px;
}
/* ------------- Tableau stats jury BUT -------- */ /* ------------- Tableau stats jury BUT -------- */
table.jury_stats_codes { table.jury_stats_codes {

View File

@ -8,11 +8,6 @@ $(function () {
"partition_aux", "partition_rangs", "admission", "partition_aux", "partition_rangs", "admission",
"col_empty" "col_empty"
]; ];
let mode_jury_but_bilan = $('table.table_recap').hasClass("table_jury_but_bilan");
if (mode_jury_but_bilan) {
// table bilan décisions: cache les notes
hidden_colums = hidden_colums.concat(["col_lien_saisie_but"]);
}
// Etat (tri des colonnes) de la table: // Etat (tri des colonnes) de la table:
const url = new URL(document.URL); const url = new URL(document.URL);
@ -99,7 +94,7 @@ $(function () {
} }
}, },
{ {
text: '<a title="Rétablir l\'affichage par défaut">&#10135;</a>', text: '<a title="Rétablir l\'affichage par défaut" class="clearreload">&#128260;</a>',
action: function (e, dt, node, config) { action: function (e, dt, node, config) {
localStorage.clear(); localStorage.clear();
console.log("cleared localStorage"); console.log("cleared localStorage");
@ -124,7 +119,7 @@ $(function () {
// table jury: avec ou sans codes enregistrés // table jury: avec ou sans codes enregistrés
buttons.push( buttons.push(
{ {
text: '<span data-group="recorded_code">Code jurys</span>', text: '<span data-group="recorded_code">Codes jury</span>',
action: toggle_col_but_visibility, action: toggle_col_but_visibility,
}); });
} else { } else {
@ -165,6 +160,15 @@ $(function () {
); );
} }
} }
// Boutons évaluations (si présentes)
if ($('table.table_recap').hasClass("with_evaluations")) {
buttons.push(
{
text: '<span data-group="eval">Évaluations</span>',
action: toggle_col_but_visibility,
}
);
}
// ------------- LA TABLE --------- // ------------- LA TABLE ---------
try { try {

View File

@ -114,7 +114,7 @@ class TableJury(TableRecap):
"jury_link", "jury_link",
"", "",
f"""{("&#10152; saisir" if a_saisir else "modifier") f"""{("&#10152; saisir" if a_saisir else "modifier")
if res.formsemestre.etat else "voir"} décisions""", if not self.read_only else "voir"} décisions""",
group="col_jury_link", group="col_jury_link",
classes=["fontred"] if a_saisir else [], classes=["fontred"] if a_saisir else [],
target=url_for( target=url_for(
@ -250,149 +250,3 @@ class RowJury(RowRecap):
# f"""{int(ects_valides)}""", # f"""{int(ects_valides)}""",
# "col_code_annee", # "col_code_annee",
# ) # )
def formsemestre_saisie_jury_but(
formsemestre: FormSemestre,
read_only: bool = False,
selected_etudid: int = None,
mode="jury",
) -> str:
"""formsemestre est un semestre PAIR
Si readonly, ne montre pas le lien "saisir la décision"
=> page html complète
Si mode == "recap", table recap des codes, sans liens de saisie.
"""
# pour chaque etud de res2 trié
# S1: UE1, ..., UEn
# S2: UE1, ..., UEn
#
# UE1_s1, UE1_s2, moy_rcue, UE2... , Nbrcue_validables, Nbrcue<8, passage_de_droit, valide_moitie_rcue
#
# Pour chaque etud de res2 trié
# DecisionsProposeesAnnee(etud, formsemestre2)
# Pour le 1er etud, faire un check_ues_ready_jury(self) -> page d'erreur
# -> rcue .ue_1, .ue_2 -> stroe moy ues, rcue.moy_rcue, etc
if formsemestre.formation.referentiel_competence is None:
raise ScoNoReferentielCompetences(formation=formsemestre.formation)
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
table = TableJury(
res,
convert_values=True,
mode_jury=True,
read_only=read_only,
classes=[
"table_jury_but_bilan" if mode == "recap" else "",
"table_recap",
"apc",
"jury table_jury_but",
],
selected_row_id=selected_etudid,
)
if table.is_empty():
return (
'<div class="table_recap"><div class="message">aucun étudiant !</div></div>'
)
table.data["filename"] = scu.sanitize_filename(
f"""jury-but-{formsemestre.titre_num()}-{time.strftime("%Y-%m-%d")}"""
)
table_html = table.html()
H = [
html_sco_header.sco_header(
page_title=f"{formsemestre.sem_modalite()}: jury BUT",
no_side_bar=True,
init_qtip=True,
javascripts=["js/etud_info.js", "js/table_recap.js"],
),
sco_formsemestre_status.formsemestre_status_head(
formsemestre_id=formsemestre.id
),
]
if mode == "recap":
H.append(
f"""<h3>Décisions de jury enregistrées pour les étudiants de ce semestre</h3>
<div class="table_jury_but_links">
<div>
<ul>
<li><a href="{url_for(
"notes.pvjury_table_but",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
}" class="stdlink">Tableau PV de jury</a>
</li>
<li><a href="{url_for(
"notes.formsemestre_lettres_individuelles",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
}" class="stdlink">Courriers individuels (classeur pdf)</a>
</li>
</div>
</div>
"""
)
H.append(
f"""
<div class="table_recap">
{table_html}
</div>
<div class="table_jury_but_links">
"""
)
if (mode == "recap") and not read_only:
H.append(
f"""
<p><a class="stdlink" href="{url_for(
"notes.formsemestre_saisie_jury",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
}">Saisie des décisions du jury</a>
</p>"""
)
else:
H.append(
f"""
<p><a class="stdlink" href="{url_for(
"notes.formsemestre_validation_auto_but",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
}">Calcul automatique des décisions du jury</a>
</p>
<p><a class="stdlink" href="{url_for(
"notes.formsemestre_jury_but_recap",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
}">Tableau récapitulatif des décisions du jury</a>
</p>
"""
)
H.append(
f"""
</div>
<div class="jury_stats">
<div>Nb d'étudiants avec décision annuelle:
{sum(table.freq_codes_annuels.values())} / {len(table)}
</div>
<div><b>Codes annuels octroyés:</b></div>
<table class="jury_stats_codes">
"""
)
for code in sorted(table.freq_codes_annuels.keys()):
H.append(
f"""<tr>
<td>{code}</td>
<td style="text-align:right">{table.freq_codes_annuels[code]}</td>
<td style="text-align:right">{
(100*table.freq_codes_annuels[code] / len(table)):2.1f}%
</td>
</tr>"""
)
H.append(
f"""
</table>
</div>
{html_sco_header.sco_footer()}
"""
)
return "\n".join(H)

View File

@ -54,6 +54,7 @@ class TableRecap(tb.Table):
mode_jury=False, mode_jury=False,
row_class=None, row_class=None,
finalize=True, finalize=True,
read_only: bool = True,
**kwargs, **kwargs,
): ):
self.rows: list["RowRecap"] = [] # juste pour que VSCode nous aide sur .rows self.rows: list["RowRecap"] = [] # juste pour que VSCode nous aide sur .rows
@ -61,7 +62,7 @@ class TableRecap(tb.Table):
self.res = res self.res = res
self.include_evaluations = include_evaluations self.include_evaluations = include_evaluations
self.mode_jury = mode_jury self.mode_jury = mode_jury
self.read_only = read_only # utilisé seulement dans sous-classes
parcours = res.formsemestre.formation.get_parcours() parcours = res.formsemestre.formation.get_parcours()
self.barre_moy = parcours.BARRE_MOY - scu.NOTES_TOLERANCE self.barre_moy = parcours.BARRE_MOY - scu.NOTES_TOLERANCE
self.barre_valid_ue = parcours.NOTES_BARRE_VALID_UE self.barre_valid_ue = parcours.NOTES_BARRE_VALID_UE
@ -103,7 +104,7 @@ class TableRecap(tb.Table):
self.add_cursus() self.add_cursus()
self.add_admissions() self.add_admissions()
# tri par rang croissant # Tri par rang croissant
if not res.formsemestre.block_moyenne_generale: if not res.formsemestre.block_moyenne_generale:
self.sort_rows(key=lambda row: row.rang_order) self.sort_rows(key=lambda row: row.rang_order)
else: else:
@ -361,6 +362,7 @@ class TableRecap(tb.Table):
pour tous les étudiants de la table. pour tous les étudiants de la table.
Les colonnes ont la classe css "evaluation" Les colonnes ont la classe css "evaluation"
""" """
self.group_titles["eval"] = "Évaluations"
# nouvelle ligne pour description évaluations: # nouvelle ligne pour description évaluations:
row_descr_eval = tb.BottomRow( row_descr_eval = tb.BottomRow(
self, self,
@ -382,7 +384,7 @@ class TableRecap(tb.Table):
for e in evals: for e in evals:
col_id = f"eval_{e.id}" col_id = f"eval_{e.id}"
title = f'{modimpl.module.code} {eval_index} {e.jour.isoformat() if e.jour else ""}' title = f'{modimpl.module.code} {eval_index} {e.jour.isoformat() if e.jour else ""}'
col_classes = ["evaluation"] col_classes = []
if first_eval: if first_eval:
col_classes.append("first") col_classes.append("first")
elif first_eval_of_mod: elif first_eval_of_mod:
@ -408,13 +410,15 @@ class TableRecap(tb.Table):
"EXC": "exc", "EXC": "exc",
}.get(content, "") }.get(content, "")
] ]
row.add_cell(col_id, title, content, "", classes=classes) row.add_cell(
col_id, title, content, group="eval", classes=classes
)
else: else:
row.add_cell( row.add_cell(
col_id, col_id,
title, title,
"ni", "ni",
"", group="eval",
classes=col_classes + ["non_inscrit"], classes=col_classes + ["non_inscrit"],
) )
@ -505,6 +509,24 @@ class TableRecap(tb.Table):
group="cursus", group="cursus",
) )
def html(self, extra_classes: list[str] = None) -> str:
"""HTML: pour les tables recap, un div au contenu variable"""
return f"""
<div class="table_recap">
{
'<div class="message">aucun étudiant !</div>'
if self.is_empty()
else super().html(
extra_classes=[
"table_recap",
"apc" if self.res.formsemestre.formation.is_apc() else "classic",
"jury" if self.mode_jury else "",
"with_evaluations" if self.include_evaluations else "",
])
}
</div>
"""
class RowRecap(tb.Row): class RowRecap(tb.Row):
"Ligne de la table recap, pour un étudiant" "Ligne de la table recap, pour un étudiant"

View File

@ -59,7 +59,7 @@ from app.models.formsemestre import FormSemestreUEComputationExpr
from app.models.moduleimpls import ModuleImpl from app.models.moduleimpls import ModuleImpl
from app.models.modules import Module from app.models.modules import Module
from app.models.ues import DispenseUE, UniteEns from app.models.ues import DispenseUE, UniteEns
from app.scodoc.sco_exceptions import ScoFormationConflict from app.scodoc.sco_exceptions import ScoFormationConflict, ScoPermissionDenied
from app.tables import jury_recap from app.tables import jury_recap
from app.views import notes_bp as bp from app.views import notes_bp as bp
@ -2257,8 +2257,8 @@ def formsemestre_validation_etud_form(
sortcol=None, sortcol=None,
): ):
"Formulaire choix jury pour un étudiant" "Formulaire choix jury pour un étudiant"
readonly = not sco_permissions_check.can_validate_sem(formsemestre_id) formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
formsemestre = FormSemestre.query.get_or_404(formsemestre_id) read_only = not formsemestre.can_edit_jury()
if formsemestre.formation.is_apc(): if formsemestre.formation.is_apc():
return redirect( return redirect(
url_for( url_for(
@ -2273,8 +2273,8 @@ def formsemestre_validation_etud_form(
etudid=etudid, etudid=etudid,
etud_index=etud_index, etud_index=etud_index,
check=check, check=check,
readonly=readonly, read_only=read_only,
desturl=desturl, dest_url=desturl,
sortcol=sortcol, sortcol=sortcol,
) )
@ -2291,10 +2291,14 @@ def formsemestre_validation_etud(
sortcol=None, sortcol=None,
): ):
"Enregistre choix jury pour un étudiant" "Enregistre choix jury pour un étudiant"
if not sco_permissions_check.can_validate_sem(formsemestre_id): formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
return scu.confirm_dialog( if not formsemestre.can_edit_jury():
message="<p>Opération non autorisée pour %s</h2>" % current_user, raise ScoPermissionDenied(
dest_url=scu.ScoURL(), dest_url=url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
) )
return sco_formsemestre_validation.formsemestre_validation_etud( return sco_formsemestre_validation.formsemestre_validation_etud(
@ -2321,10 +2325,14 @@ def formsemestre_validation_etud_manu(
sortcol=None, sortcol=None,
): ):
"Enregistre choix jury pour un étudiant" "Enregistre choix jury pour un étudiant"
if not sco_permissions_check.can_validate_sem(formsemestre_id): formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
return scu.confirm_dialog( if not formsemestre.can_edit_jury():
message="<p>Opération non autorisée pour %s</h2>" % current_user, raise ScoPermissionDenied(
dest_url=scu.ScoURL(), dest_url=url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
) )
return sco_formsemestre_validation.formsemestre_validation_etud_manu( return sco_formsemestre_validation.formsemestre_validation_etud_manu(
@ -2364,7 +2372,7 @@ def formsemestre_validation_but(
etudid = int(etudid) etudid = int(etudid)
except ValueError: except ValueError:
abort(404, "invalid etudid") abort(404, "invalid etudid")
read_only = not sco_permissions_check.can_validate_sem(formsemestre_id) read_only = not formsemestre.can_edit_jury()
# --- Navigation # --- Navigation
prev_lnk = ( prev_lnk = (
@ -2391,9 +2399,13 @@ def formsemestre_validation_but(
{prev_lnk} {prev_lnk}
</div> </div>
<div class="back_list"> <div class="back_list">
<a href="{url_for( <a href="{
"notes.formsemestre_saisie_jury", scodoc_dept=g.scodoc_dept, url_for(
formsemestre_id=formsemestre_id, selected_etudid=etud.id "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> )}" class="stdlink">retour à la liste</a>
</div> </div>
<div class="next"> <div class="next">
@ -2583,15 +2595,16 @@ def formsemestre_validation_but(
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def formsemestre_validation_auto_but(formsemestre_id: int = None): def formsemestre_validation_auto_but(formsemestre_id: int = None):
"Saisie automatique des décisions de jury BUT" "Saisie automatique des décisions de jury BUT"
if not sco_permissions_check.can_validate_sem(formsemestre_id): formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
return scu.confirm_dialog( if not formsemestre.can_edit_jury():
message=f"<p>Opération non autorisée pour {current_user}</h2>", raise ScoPermissionDenied(
dest_url=url_for( dest_url=url_for(
"notes.formsemestre_status", "notes.formsemestre_status",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
), )
) )
formsemestre = FormSemestre.query.get_or_404(formsemestre_id) formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
form = jury_but_forms.FormSemestreValidationAutoBUTForm() form = jury_but_forms.FormSemestreValidationAutoBUTForm()
if request.method == "POST": if request.method == "POST":
@ -2602,9 +2615,10 @@ def formsemestre_validation_auto_but(formsemestre_id: int = None):
flash(f"Décisions enregistrées ({nb_etud_modif} étudiants modifiés)") flash(f"Décisions enregistrées ({nb_etud_modif} étudiants modifiés)")
return redirect( return redirect(
url_for( url_for(
"notes.formsemestre_saisie_jury", "notes.formsemestre_recapcomplet",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
mode_jury=1,
) )
) )
return render_template( return render_template(
@ -2621,11 +2635,16 @@ def formsemestre_validation_auto_but(formsemestre_id: int = None):
@scodoc7func @scodoc7func
def formsemestre_validate_previous_ue(formsemestre_id, etudid=None): def formsemestre_validate_previous_ue(formsemestre_id, etudid=None):
"Form. saisie UE validée hors ScoDoc" "Form. saisie UE validée hors ScoDoc"
if not sco_permissions_check.can_validate_sem(formsemestre_id): formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
return scu.confirm_dialog( if not formsemestre.can_edit_jury():
message="<p>Opération non autorisée pour %s</h2>" % current_user, raise ScoPermissionDenied(
dest_url=scu.ScoURL(), dest_url=url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
) )
return sco_formsemestre_validation.formsemestre_validate_previous_ue( return sco_formsemestre_validation.formsemestre_validate_previous_ue(
formsemestre_id, etudid formsemestre_id, etudid
) )
@ -2645,11 +2664,16 @@ sco_publish(
@scodoc7func @scodoc7func
def formsemestre_ext_edit_ue_validations(formsemestre_id, etudid=None): def formsemestre_ext_edit_ue_validations(formsemestre_id, etudid=None):
"Form. edition UE semestre extérieur" "Form. edition UE semestre extérieur"
if not sco_permissions_check.can_validate_sem(formsemestre_id): formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
return scu.confirm_dialog( if not formsemestre.can_edit_jury():
message="<p>Opération non autorisée pour %s</h2>" % current_user, raise ScoPermissionDenied(
dest_url=scu.ScoURL(), 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( return sco_formsemestre_exterieurs.formsemestre_ext_edit_ue_validations(
formsemestre_id, etudid formsemestre_id, etudid
) )
@ -2668,11 +2692,16 @@ sco_publish(
@scodoc7func @scodoc7func
def etud_ue_suppress_validation(etudid, formsemestre_id, ue_id): def etud_ue_suppress_validation(etudid, formsemestre_id, ue_id):
"""Suppress a validation (ue_id, etudid) and redirect to formsemestre""" """Suppress a validation (ue_id, etudid) and redirect to formsemestre"""
if not sco_permissions_check.can_validate_sem(formsemestre_id): formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
return scu.confirm_dialog( if not formsemestre.can_edit_jury():
message="<p>Opération non autorisée pour %s</h2>" % current_user, raise ScoPermissionDenied(
dest_url=scu.ScoURL(), dest_url=url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
) )
return sco_formsemestre_validation.etud_ue_suppress_validation( return sco_formsemestre_validation.etud_ue_suppress_validation(
etudid, formsemestre_id, ue_id etudid, formsemestre_id, ue_id
) )
@ -2684,14 +2713,18 @@ def etud_ue_suppress_validation(etudid, formsemestre_id, ue_id):
@scodoc7func @scodoc7func
def formsemestre_validation_auto(formsemestre_id): def formsemestre_validation_auto(formsemestre_id):
"Formulaire saisie automatisee des decisions d'un semestre" "Formulaire saisie automatisee des decisions d'un semestre"
if not sco_permissions_check.can_validate_sem(formsemestre_id):
return scu.confirm_dialog(
message="<p>Opération non autorisée pour %s</h2>" % current_user,
dest_url=scu.ScoURL(),
)
formsemestre: FormSemestre = FormSemestre.query.filter_by( formsemestre: FormSemestre = FormSemestre.query.filter_by(
id=formsemestre_id, dept_id=g.scodoc_dept_id id=formsemestre_id, dept_id=g.scodoc_dept_id
).first_or_404() ).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(): if formsemestre.formation.is_apc():
return redirect( return redirect(
url_for( url_for(
@ -2709,10 +2742,14 @@ def formsemestre_validation_auto(formsemestre_id):
@scodoc7func @scodoc7func
def do_formsemestre_validation_auto(formsemestre_id): def do_formsemestre_validation_auto(formsemestre_id):
"Formulaire saisie automatisee des decisions d'un semestre" "Formulaire saisie automatisee des decisions d'un semestre"
if not sco_permissions_check.can_validate_sem(formsemestre_id): formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
return scu.confirm_dialog( if not formsemestre.can_edit_jury():
message="<p>Opération non autorisée pour %s</h2>" % current_user, raise ScoPermissionDenied(
dest_url=scu.ScoURL(), 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) return sco_formsemestre_validation.do_formsemestre_validation_auto(formsemestre_id)
@ -2726,13 +2763,16 @@ def formsemestre_validation_suppress_etud(
formsemestre_id, etudid, dialog_confirmed=False formsemestre_id, etudid, dialog_confirmed=False
): ):
"""Suppression des décisions de jury pour un étudiant.""" """Suppression des décisions de jury pour un étudiant."""
if not sco_permissions_check.can_validate_sem(formsemestre_id): formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
return scu.confirm_dialog( if not formsemestre.can_edit_jury():
message="<p>Opération non autorisée pour %s</h2>" % current_user, raise ScoPermissionDenied(
dest_url=scu.ScoURL(), dest_url=url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
) )
etud = Identite.query.get_or_404(etudid) etud = Identite.query.get_or_404(etudid)
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
if formsemestre.formation.is_apc(): if formsemestre.formation.is_apc():
next_url = url_for( next_url = url_for(
"scolar.ficheEtud", "scolar.ficheEtud",
@ -2800,15 +2840,8 @@ sco_publish("/pvjury_table_but", jury_but_pv.pvjury_table_but, Permission.ScoVie
@scodoc7func @scodoc7func
def formsemestre_saisie_jury(formsemestre_id: int, selected_etudid: int = None): def formsemestre_saisie_jury(formsemestre_id: int, selected_etudid: int = None):
"""Page de saisie: liste des étudiants et lien vers page jury """Page de saisie: liste des étudiants et lien vers page jury
en semestres pairs de BUT, table spécifique avec l'année
sinon, redirect vers page recap en mode jury sinon, redirect vers page recap en mode jury
""" """
read_only = not sco_permissions_check.can_validate_sem(formsemestre_id)
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
if formsemestre.formation.is_apc() and formsemestre.semestre_id % 2 == 0:
return jury_recap.formsemestre_saisie_jury_but(
formsemestre, read_only, selected_etudid=selected_etudid
)
return redirect( return redirect(
url_for( url_for(
"notes.formsemestre_recapcomplet", "notes.formsemestre_recapcomplet",
@ -2819,23 +2852,6 @@ def formsemestre_saisie_jury(formsemestre_id: int, selected_etudid: int = None):
) )
@bp.route("/formsemestre_jury_but_recap")
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
def formsemestre_jury_but_recap(formsemestre_id: int, selected_etudid: int = None):
"""Tableau affichage des codes"""
read_only = not sco_permissions_check.can_validate_sem(formsemestre_id)
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
if not (formsemestre.formation.is_apc() and formsemestre.semestre_id % 2 == 0):
raise ScoValueError(
"formsemestre_jury_but_recap: réservé aux semestres pairs de BUT"
)
return jury_recap.formsemestre_saisie_jury_but(
formsemestre, read_only=read_only, selected_etudid=selected_etudid, mode="recap"
)
@bp.route( @bp.route(
"/formsemestre_jury_but_erase/<int:formsemestre_id>", "/formsemestre_jury_but_erase/<int:formsemestre_id>",
methods=["GET", "POST"], methods=["GET", "POST"],
@ -2855,18 +2871,25 @@ def formsemestre_jury_but_erase(
Si l'étudiant n'est pas spécifié, efface les décisions de tous les inscrits. Si l'étudiant n'est pas spécifié, efface les décisions de tous les inscrits.
""" """
only_one_sem = int(request.args.get("only_one_sem") or False) only_one_sem = int(request.args.get("only_one_sem") or False)
if not sco_permissions_check.can_validate_sem(formsemestre_id): formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
raise ScoValueError("opération non autorisée") if not formsemestre.can_edit_jury():
formsemestre = FormSemestre.query.get_or_404(formsemestre_id) raise ScoPermissionDenied(
dest_url=url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
)
if not formsemestre.formation.is_apc(): if not formsemestre.formation.is_apc():
raise ScoValueError("semestre non BUT") raise ScoValueError("semestre non BUT")
if etudid is None: if etudid is None:
etud = None etud = None
etuds = formsemestre.get_inscrits(include_demdef=True) etuds = formsemestre.get_inscrits(include_demdef=True)
dest_url = url_for( dest_url = url_for(
"notes.formsemestre_saisie_jury", "notes.formsemestre_recapcomplet",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
mode_jury=1,
) )
else: else:
etud: Identite = Identite.query.get_or_404(etudid) etud: Identite = Identite.query.get_or_404(etudid)