Page accueil dept: sélection de plusieurs formsemestres et menu associé.

This commit is contained in:
Emmanuel Viennet 2025-01-14 20:10:51 +01:00
parent aae5068b7e
commit 867575ac78
12 changed files with 312 additions and 139 deletions

View File

@ -646,17 +646,18 @@ class FormSemestre(models.ScoDocModel):
)
return [db.session.get(ModuleImpl, modimpl_id) for modimpl_id in cursor]
def can_be_edited_by(self, user: User):
"""Vrai si user peut modifier ce semestre (est chef ou l'un des responsables)"""
def can_be_edited_by(self, user: User | None = None, allow_locked=False) -> bool:
"""Vrai si user (par def. current) peut modifier ce semestre
(est chef ou l'un des responsables).
Si le semestre est verrouillé, faux sauf si allow_locked.
"""
user = user or current_user
if user.passwd_must_be_changed or not user.has_permission(
Permission.EditFormSemestre
): # pas chef
if not self.resp_can_edit or user.id not in [
resp.id for resp in self.responsables
]:
): # pas chef de dept.
if not self.resp_can_edit or not self.est_responsable(user):
return False
return True
return allow_locked or not self.etat
def est_courant(self) -> bool:
"""Vrai si la date actuelle (now) est dans le semestre
@ -902,13 +903,6 @@ class FormSemestre(models.ScoDocModel):
"True si l'user est l'un des responsables du semestre"
return user.id in [u.id for u in self.responsables]
def est_chef_or_diretud(self, user: User | None = None) -> bool:
"Vrai si utilisateur (par def. current) est admin, chef dept ou responsable du semestre"
user = user or current_user
return user.has_permission(Permission.EditFormSemestre) or self.est_responsable(
user
)
def can_change_groups(self, user: User = None) -> bool:
"""Vrai si l'utilisateur (par def. current) peut changer les groupes dans
ce semestre: vérifie permission et verrouillage (mais pas si la partition est éditable).
@ -926,10 +920,7 @@ class FormSemestre(models.ScoDocModel):
"""Vrai si utilisateur (par def. current) peut saisir decision de jury
dans ce semestre: vérifie permission et verrouillage.
"""
user = user or current_user
if user.passwd_must_be_changed:
return False
return self.etat and self.est_chef_or_diretud(user)
return self.can_be_edited_by(user)
def can_edit_pv(self, user: User = None):
"Vrai si utilisateur (par def. current) peut editer un PV de jury de ce semestre"
@ -937,7 +928,7 @@ class FormSemestre(models.ScoDocModel):
if user.passwd_must_be_changed:
return False
# Autorise les secrétariats, repérés via la permission EtudChangeAdr
return self.est_chef_or_diretud(user) or user.has_permission(
return self.can_be_edited_by(user, allow_locked=True) or user.has_permission(
Permission.EtudChangeAdr
)

View File

@ -100,15 +100,11 @@ def make_menu(title, items, css_class="", alone=False) -> str:
def gen_menu_items(items):
H.append("<ul>")
for item in items:
if not item.get("enabled", True):
cls = ' class="ui-state-disabled"'
else:
cls = ""
cls = f''' class="sco_menu_item {
'' if item.get("enabled", True) else 'ui-state-disabled'
}"'''
the_id = item.get("id", "")
if the_id:
li_id = 'id="%s" ' % the_id
else:
li_id = ""
li_id = f'id="{the_id}" ' if the_id else ""
if "endpoint" in item:
args = item.get("args", {})
item["urlq"] = url_for(
@ -134,7 +130,7 @@ def make_menu(title, items, css_class="", alone=False) -> str:
H = []
if alone:
H.append('<ul class="sco_dropdown_menu %s">' % css_class)
H.append("""<li><a href="#">%s</a>""" % title)
H.append("""<li class="sco_menu_title"><a href="#">%s</a>""" % title)
gen_menu_items(items)
H.append("</li>")
if alone:
@ -142,12 +138,8 @@ def make_menu(title, items, css_class="", alone=False) -> str:
return "".join(H)
"""
HTML <-> text conversions.
http://stackoverflow.com/questions/328356/extracting-text-from-html-file-using-python
"""
# HTML <-> text conversions.
# http://stackoverflow.com/questions/328356/extracting-text-from-html-file-using-python
class _HTMLToText(HTMLParser):
def __init__(self):
HTMLParser.__init__(self)

View File

@ -36,6 +36,7 @@ from flask_sqlalchemy.query import Query
import app
from app import log
from app.models import FormSemestre, ScolarNews, ScoDocSiteConfig
from app.scodoc import htmlutils
import app.scodoc.sco_utils as scu
from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_permissions import Permission
@ -83,6 +84,42 @@ def index_html(showcodes=0, showsemtable=0, export_table_formsemestres=False):
sco_modalites.group_formsemestres_by_modalite(current_formsemestres)
)
passerelle_disabled = ScoDocSiteConfig.is_passerelle_disabled()
menu_items = [
{
"title": "Verrouiller les semestres sélectionnés",
"endpoint": "notes.formsemestres_lock",
"args": {
"unlock": False,
},
},
{
"title": "Déverrouiller les semestres sélectionnés",
"endpoint": "notes.formsemestres_lock",
"args": {
"unlock": True,
},
},
{
"title": "Publier les semestres sélectionnés sur la passerelle",
"endpoint": "notes.formsemestres_enable_publish",
"args": {
"enable": True,
},
},
{
"title": "Ne pas publier les semestres sélectionnés sur la passerell ",
"endpoint": "notes.formsemestres_enable_publish",
"args": {
"enable": False,
},
},
]
menu_formsemestres_action = (
htmlutils.make_menu("_", menu_items, alone=True)
if current_user.has_permission(Permission.EditFormSemestre)
else ""
)
return render_template(
"scolar/index.j2",
current_user=current_user,
@ -98,6 +135,7 @@ def index_html(showcodes=0, showsemtable=0, export_table_formsemestres=False):
icon_hidden="" if passerelle_disabled else scu.ICON_HIDDEN,
icon_published="" if passerelle_disabled else scu.ICON_PUBLISHED,
locked_formsemestres=locked_formsemestres,
menu_formsemestres_action=menu_formsemestres_action,
modalites=modalites,
nb_locked=locked_formsemestres.count(),
nb_user_accounts=sco_users.get_users_count(dept=g.scodoc_dept),
@ -107,6 +145,7 @@ def index_html(showcodes=0, showsemtable=0, export_table_formsemestres=False):
showcodes=showcodes,
showsemtable=showsemtable,
sco=ScoData(),
title=f"ScoDoc {g.scodoc_dept}",
)

View File

@ -1706,42 +1706,6 @@ def formsemestre_edit_options(formsemestre_id):
)
def formsemestre_change_publication_bul(formsemestre_id, dialog_confirmed=False):
"""Change état publication bulletins sur portail"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
ok, err = sco_permissions_check.check_access_diretud(formsemestre)
if not ok:
return err
etat = not formsemestre.bul_hide_xml
status_url = url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
)
if not dialog_confirmed:
msg = "non" if etat else ""
return scu.confirm_dialog(
f"<h2>Confirmer la {msg} publication des bulletins ?</h2>",
help_msg="""Il est parfois utile de désactiver la diffusion des bulletins,
par exemple pendant la tenue d'un jury ou avant harmonisation des notes.
<br>
Ce réglage n'a d'effet que si votre établissement a interfacé ScoDoc avec
une passerelle étudiant.
""",
dest_url="",
cancel_url=status_url,
parameters={"bul_hide_xml": etat, "formsemestre_id": formsemestre_id},
)
formsemestre.bul_hide_xml = etat
db.session.add(formsemestre)
db.session.commit()
log(f"formsemestre_change_publication_bul: {formsemestre} -> {etat}")
return flask.redirect(status_url)
def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
"""Changement manuel des coefficients des UE capitalisées."""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)

View File

@ -417,7 +417,7 @@ def formsemestre_status_menubar(formsemestre: FormSemestre | None) -> str:
"title": "Importer les notes",
"endpoint": "notes.formsemestre_import_notes",
"args": {"formsemestre_id": formsemestre_id},
"enabled": formsemestre.est_chef_or_diretud(),
"enabled": formsemestre.can_be_edited_by(),
},
]
menu_jury = [

View File

@ -1648,6 +1648,9 @@ def confirm_dialog(
template="sco_page.j2",
):
"""HTML confirmation dialog: submit (POST) to same page or dest_url if given."""
from app.models import FormSemestre, Identite
from app.views import ScoData
parameters = parameters or {}
# dialog de confirmation simple
parameters[target_variable] = 1
@ -1687,9 +1690,21 @@ def confirm_dialog(
)
H.append("</form>")
if help_msg:
H.append('<p class="help">' + help_msg + "</p>")
H.append('<div class="scobox help explanation">' + help_msg + "</div>")
if add_headers:
return render_template(template, content="\n".join(H))
formsemestre = (
FormSemestre.get_formsemestre(parameters["formsemestre_id"])
if "formsemestre_id" in parameters
else None
)
etud = (
Identite.get_etud(parameters["etudid"]) if "etudid" in parameters else None
)
return render_template(
template,
content="\n".join(H),
sco=ScoData(formsemestre=formsemestre, etud=etud),
)
return "\n".join(H)

View File

@ -1732,18 +1732,16 @@ h2.formsemestre,
color: black;
}
.formsemestre_page_title .eye,
formsemestre_page_title .eye img {
display: inline-block;
margin-left: 12px;
.formsemestre_page_title .infos span {
margin-right: 16px;
}
.formsemestre_page_title .infos span.lock,
formsemestre_page_title .lock img {
div.formsemestre_page_title .infos span.lock,
div.formsemestre_page_title .infos span.eye,
span.lock img,
span.eye img {
display: inline-block;
vertical-align: middle;
margin-left: 8px;
margin-right: 8px;
}
#formnotes .tf-explanation {
@ -1854,10 +1852,6 @@ div.inscr_addremove_menu {
width: 150px;
}
.formsemestre_page_title .infos span {
padding-right: 25px;
}
.formsemestre_page_title span.semtitle {
font-size: 12pt;
}

View File

@ -16,7 +16,55 @@ $(document).ready(function () {
orderCellsTop: true, // cellules ligne 1 pour tri
aaSorting: [], // Prevent initial sorting
};
$("table.semlist").DataTable(table_options);
const table = new DataTable("table.semlist", table_options);
// Sélection de semestres et mise à jour du menu associé
table.on('click', 'tbody tr', function (e) {
e.currentTarget.classList.toggle('selected');
var nbSelectedRows = table.rows('.selected').count();
if (nbSelectedRows == 0) {
document.getElementById("formsemestres-select-infos").style.display = 'none';
}
else {
document.getElementById("formsemestres-select-infos").style.display = 'inline';
if (nbSelectedRows > 1) {
document.querySelector("#formsemestres-select-menu li.sco_menu_title a").childNodes[1].nodeValue = nbSelectedRows + " semestres sélectionnés";
} else {
document.querySelector("#formsemestres-select-menu li.sco_menu_title a").childNodes[1].nodeValue = nbSelectedRows + " semestre sélectionné";
}
}
});
// Lien déselectionner
document.getElementById("formsemestres-deselect").addEventListener('click', function (e) {
e.preventDefault();
table.rows('.selected').nodes().to$().removeClass('selected');
document.getElementById("formsemestres-select-infos").style.display = 'none';
});
// Modification des liens de la section formsemestres-actions: ajout des formsemestres selectionnés:
const links = document.querySelectorAll('#formsemestres-select-menu li.sco_menu_item a');
links.forEach(link => {
link.addEventListener('click', function(event) {
// Prevent the default action (navigation)
event.preventDefault();
// Build the query string with formsemestre_id parameters
const selectedRows = document.querySelectorAll('tr.selected');
const selectedFormsemestreIds = Array.from(selectedRows).map(row => row.dataset.formsemestre_id);
const queryString = selectedFormsemestreIds
.map(id => `formsemestre_ids=${encodeURIComponent(id)}`)
.join('&');
// Construct the new URL
const originalHref = link.getAttribute('href');
const newHref = originalHref.includes('?')
? `${originalHref}&${queryString}` // If there's already a query string
: `${originalHref}?${queryString}`; // If no query string exists
// Navigate to the new URL
window.location.href = newHref;
});
});
// Edition des codes Apo
let table_editable = document.querySelector("table#semlist.apo_editable");
if (table_editable) {
let save_url = document.querySelector("table#semlist.apo_editable").dataset

View File

@ -8,7 +8,7 @@
scodoc_dept=g.scodoc_dept, formsemestre_id=sco.formsemestre.id)
}}">{{sco.formsemestre.titre}}</a>
<a title="{{sco.formsemestre.etapes_apo_str()}}">
{% if sco.formsemestre.semestre_id != -1 %}, {{sco.formsemestre.formation.get_cursus().SESSION_NAME}}
{% if sco.formsemestre.semestre_id != -1 %} {{sco.formsemestre.formation.get_cursus().SESSION_NAME}}
{{sco.formsemestre.semestre_id}}
{% endif %}</a>
{% if sco.formsemestre.modalite %} en {{sco.formsemestre.modalite}}{% endif %}
@ -35,12 +35,16 @@
</span>
<span class="eye">
{% if not scu.is_passerelle_disabled() %}
<a href="{{url_for('notes.formsemestre_change_publication_bul', scodoc_dept=g.scodoc_dept,
formsemestre_id=sco.formsemestre.id)}}">
{% if sco.formsemestre.bul_hide_xml %}
<a href="{{url_for('notes.formsemestres_enable_publish', scodoc_dept=g.scodoc_dept,
formsemestre_id=sco.formsemestre.id, enable=True)}}">
{{ scu.ICON_HIDDEN|safe}}
</a>
{% else %}
<a href="{{url_for('notes.formsemestres_enable_publish', scodoc_dept=g.scodoc_dept,
formsemestre_id=sco.formsemestre.id, enable=False)}}">
{{ scu.ICON_PUBLISHED|safe }}
</a>
{% endif %}
</a>
{% endif %}

View File

@ -30,11 +30,15 @@ a.disabled-link {
margin-left: 16px;
color: purple;
font-weight: normal;
display: none;
}
#formsemestres-select-infos a {
a#formsemestres-deselect {
margin-left: 8px;
text-decoration: underline;
}
#formsemestres-select-menu ul {
display: inline-block;
}
div.semlist {
padding-right: 8px;
}
@ -253,6 +257,12 @@ div.effectif {
<a href="{{
url_for('scolar.export_table_dept_formsemestres', scodoc_dept=g.scodoc_dept)
}}">{{scu.ICON_XLS|safe}}</a>
{% if menu_formsemestres_action %}
<span id="formsemestres-select-infos">
<span id="formsemestres-select-menu">{{menu_formsemestres_action|safe}}</span>
<a id="formsemestres-deselect" href="#">tout désélectionner</a>
</span>
{% endif %}
</summary>
<div class="semlist">
{{ html_table_formsemestres|safe }}

View File

@ -904,48 +904,6 @@ sco_publish(
methods=["GET", "POST"],
)
@bp.route("/formsemestre_flip_lock", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.ScoView) # acces vérifié dans la vue
@scodoc7func
def formsemestre_flip_lock(formsemestre_id, dialog_confirmed=False):
"Changement de l'état de verrouillage du semestre"
formsemestre: FormSemestre = FormSemestre.get_or_404(formsemestre_id)
dest_url = url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
)
if not formsemestre.est_chef_or_diretud():
raise ScoPermissionDenied("opération non autorisée", dest_url=dest_url)
if not dialog_confirmed:
msg = "verrouillage" if formsemestre.etat else "déverrouillage"
return scu.confirm_dialog(
f"<h2>Confirmer le {msg} du semestre ?</h2>",
help_msg="""Les notes d'un semestre verrouillé ne peuvent plus être modifiées.
Un semestre verrouillé peut cependant être déverrouillé facilement à tout moment
(par son responsable ou un administrateur).
<br>
Le programme d'une formation qui a un semestre verrouillé ne peut plus être modifié.
""",
dest_url="",
cancel_url=dest_url,
parameters={"formsemestre_id": formsemestre_id},
)
formsemestre.flip_lock()
db.session.commit()
return flask.redirect(dest_url)
sco_publish(
"/formsemestre_change_publication_bul",
sco_formsemestre_edit.formsemestre_change_publication_bul,
Permission.ScoView,
methods=["GET", "POST"],
)
sco_publish(
"/view_formsemestre_by_etape",
sco_formsemestre.view_formsemestre_by_etape,
@ -1942,7 +1900,7 @@ def _formsemestre_or_modimpl_import_notes(
formsemestre_id=formsemestre.id,
)
)
if formsemestre and not formsemestre.est_chef_or_diretud():
if formsemestre and not formsemestre.can_be_edited_by():
raise ScoPermissionDenied("opération non autorisée", dest_url=dest_url)
if modimpl and not modimpl.can_edit_notes(current_user):
raise ScoPermissionDenied("opération non autorisée", dest_url=dest_url)

View File

@ -33,6 +33,7 @@ import datetime
import io
import zipfile
import flask
from flask import flash, redirect, render_template, url_for
from flask import current_app, g, request
import PIL
@ -56,11 +57,8 @@ from app.models import (
FORMSEMESTRE_DISPOSITIFS,
ScoDocSiteConfig,
)
from app.scodoc import (
sco_edt_cal,
sco_groups_view,
)
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_edt_cal, sco_groups_view
from app.scodoc.sco_exceptions import ScoPermissionDenied, ScoValueError
from app.scodoc.sco_permissions import Permission
from app.scodoc import sco_utils as scu
from app.views import notes_bp as bp
@ -475,3 +473,163 @@ def formsemestres_import_from_description_sample():
return scu.send_file(
xls, "ImportSemestres", scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE
)
@bp.route("/formsemestre_flip_lock/<int:formsemestre_id>", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.ScoView) # acces vérifié dans la vue
def formsemestre_flip_lock(formsemestre_id: int):
"Changement de l'état de verrouillage du semestre. Si GET, dialogue de confirmation."
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
dest_url = url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
)
if not formsemestre.can_be_edited_by(allow_locked=True):
raise ScoPermissionDenied("opération non autorisée", dest_url=dest_url)
if request.method == "GET":
msg = "verrouillage" if formsemestre.etat else "déverrouillage"
return scu.confirm_dialog(
f"<h2>Confirmer le {msg} du semestre ?</h2>",
help_msg="""Les notes d'un semestre verrouillé ne peuvent plus être modifiées.
Un semestre verrouillé peut cependant être déverrouillé facilement à tout moment
(par son responsable ou un administrateur).
<br>
Le programme d'une formation qui a un semestre verrouillé ne peut plus être modifié.
""",
dest_url="",
cancel_url=dest_url,
parameters={"formsemestre_id": formsemestre_id},
)
formsemestre.flip_lock()
db.session.commit()
return flask.redirect(dest_url)
@bp.route("/formsemestres_unlock", defaults={"unlock": True}, methods=["GET", "POST"])
@bp.route("/formsemestres_lock", defaults={"unlock": False}, methods=["GET", "POST"])
@scodoc
@permission_required(Permission.ScoView) # acces vérifié dans la vue
def formsemestres_lock(unlock: bool = False):
"Lock formsemestres (or unlock if unlock is True). If GET, asks for confirmation."
dest_url = url_for("scolar.index_html", scodoc_dept=g.scodoc_dept, showsemtable=1)
try:
formsemestre_ids = request.args.getlist("formsemestre_ids", type=int)
except ValueError as exc:
raise ScoValueError("argument formsemestre_ids invalide") from exc
if not formsemestre_ids:
raise ScoValueError("aucun semestre sélectionné")
formsemestres = [
FormSemestre.get_formsemestre(formsemestre_id)
for formsemestre_id in formsemestre_ids
]
for formsemestre in formsemestres:
if not formsemestre.can_be_edited_by(allow_locked=True):
raise ScoPermissionDenied("opération non autorisée", dest_url=dest_url)
if request.method == "GET":
return scu.confirm_dialog(
f"<h2>Confirmer le {'' if unlock else ''}verrouillage des semestres ?</h2>",
help_msg=f"""
<div>
Les semestres suivants seront modifiés:
<ul>
<li>{'</li><li>'.join([ s.html_link_status() for s in formsemestres ])}</li>
</ul>
</div>
<div>
Les notes d'un semestre verrouillé ne peuvent plus être modifiées.
Un semestre verrouillé peut cependant être déverrouillé facilement à tout moment
(par son responsable ou un administrateur).
<br>
Le programme d'une formation qui a un semestre verrouillé ne peut plus être modifié.
</div>
""",
dest_url="",
cancel_url=dest_url,
parameters={"formsemestre_ids": formsemestre_ids},
)
for formsemestre in formsemestres:
formsemestre.etat = unlock
db.session.add(formsemestre)
db.session.commit()
if unlock:
flash(f"{len(formsemestres)} semestres déverrouillés")
else:
flash(f"{len(formsemestres)} semestres verrouillés")
return redirect(dest_url)
@bp.route(
"/formsemestres_enable_publish", defaults={"enable": True}, methods=["GET", "POST"]
)
@bp.route(
"/formsemestres_disable_publish",
defaults={"enable": False},
methods=["GET", "POST"],
)
@scodoc
@permission_required(Permission.ScoView) # acces vérifié dans la vue
def formsemestres_enable_publish(enable: bool = False):
"""Change état publication bulletins sur portail.
Peut affecter un (formsemestre_id) ou plusieurs (formsemestre_ids) semestres.
"""
arg_name = (
"formsemestre_ids" if "formsemestre_ids" in request.args else "formsemestre_id"
)
try:
formsemestre_ids = request.args.getlist(arg_name, type=int)
except ValueError as exc:
raise ScoValueError("argument formsemestre_ids invalide") from exc
if not formsemestre_ids:
raise ScoValueError("aucun semestre sélectionné")
formsemestres = [
FormSemestre.get_formsemestre(formsemestre_id)
for formsemestre_id in formsemestre_ids
]
dest_url = (
url_for("scolar.index_html", scodoc_dept=g.scodoc_dept, showsemtable=1)
if "formsemestre_ids" in request.args
else url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestres[0].id,
)
)
for formsemestre in formsemestres:
if not formsemestre.can_be_edited_by(allow_locked=True):
raise ScoPermissionDenied("opération non autorisée", dest_url=dest_url)
if request.method == "GET":
return scu.confirm_dialog(
f"<h2>Confirmer la {'' if enable else 'non'} publication des bulletins ?</h2>",
help_msg="""Il est parfois utile de désactiver la diffusion des bulletins sur
la passerelle, par exemple pendant la tenue d'un jury ou avant harmonisation
des notes.
<br>
Ce réglage n'a d'effet que si votre établissement a interfacé ScoDoc avec
une passerelle étudiant.
""",
dest_url="",
cancel_url=dest_url,
parameters={"formsemestre_ids": formsemestre_ids},
)
for formsemestre in formsemestres:
formsemestre.bul_hide_xml = not enable
db.session.add(formsemestre)
log(
f"formsemestres_enable_publish: {formsemestre} -> {formsemestre.bul_hide_xml}"
)
db.session.commit()
s = "s" if len(formsemestres) > 1 else ""
if enable:
flash(f"{len(formsemestres)} semestre{s} publié{s}")
else:
flash(f"{len(formsemestres)} semestre{s} non publié{s}")
return flask.redirect(dest_url)