diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 62a557034f7e2ebafdc12d7261c9a69b821d55d5..21e719621ddf3e7d30355a49e9407d66c3fc5e5e 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -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 ) diff --git a/app/scodoc/htmlutils.py b/app/scodoc/htmlutils.py index 41e452d4ca34ed76d52e03296b12b3a0b694d1dd..3b49d18c07bbb41d40a47629a2860298af73b265 100644 --- a/app/scodoc/htmlutils.py +++ b/app/scodoc/htmlutils.py @@ -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) diff --git a/app/scodoc/sco_dept.py b/app/scodoc/sco_dept.py index 88f355d25235acda0e4f26c0d79cc7151366ae19..1d5b3f4c59360fd92cd26db332ee36b0675d5e39 100644 --- a/app/scodoc/sco_dept.py +++ b/app/scodoc/sco_dept.py @@ -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}", ) diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index 557765b3a653fd382665317e47e549327031e4e8..c926c4ff8b6cb38eeab9d43ddba0dafc925e2cad 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -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) diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 5184143e8c284cbdd83682ba64696656bd88a459..f634d73baa1d3b8cf1468116d59d449a3b93d8c5 100755 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -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 = [ diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 13afb7ad9d4afdc9cca469931f439c96c1c46664..84a3d77d9405d229a1b16371bdc0c1508c45b6f2 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -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) diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 9da9a87fb0e3c2fd52122aedafa144b42738d5fe..bca1f4e1aec823729d6447929c37d756827c0472 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -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; } diff --git a/app/static/js/scolar_index.js b/app/static/js/scolar_index.js index c2d09670bc8ab08896be020172db233d173e0d34..0a5e2eb16fbab670e685193fadfcd332adebb0d5 100644 --- a/app/static/js/scolar_index.js +++ b/app/static/js/scolar_index.js @@ -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 diff --git a/app/templates/formsemestre_header.j2 b/app/templates/formsemestre_header.j2 index 85d60358747359d84f220dbfa0a9e3b5602bf0aa..10ced6ba448be85f7aaa1234ce02706908f79722 100644 --- a/app/templates/formsemestre_header.j2 +++ b/app/templates/formsemestre_header.j2 @@ -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 %} diff --git a/app/templates/scolar/index.j2 b/app/templates/scolar/index.j2 index e2850bffd123f100ef95e7fef2e13e86011faeb8..4c69d48ec1462a1279c704193e99fb081d411503 100644 --- a/app/templates/scolar/index.j2 +++ b/app/templates/scolar/index.j2 @@ -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 }} diff --git a/app/views/notes.py b/app/views/notes.py index eedb4c67d0e4c478a703f7fbe5ff507b2eaa752c..122d77f137e81088327cb5bf6db2c2925cddc791 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -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) diff --git a/app/views/notes_formsemestre.py b/app/views/notes_formsemestre.py index 54fa6efa9bf241a761caf8a02474e28f5558839a..85e5e9ce95f1c3c7ae69ad6538a00ecae0cae56b 100644 --- a/app/views/notes_formsemestre.py +++ b/app/views/notes_formsemestre.py @@ -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 {'dé' 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)