diff --git a/app/forms/main/config_personalized_links.py b/app/forms/main/config_personalized_links.py new file mode 100644 index 000000000..1aed31302 --- /dev/null +++ b/app/forms/main/config_personalized_links.py @@ -0,0 +1,72 @@ +""" +Formulaire configuration liens personalisés (menu "Liens") +""" + +from flask import g, url_for +from flask_wtf import FlaskForm +from wtforms import FieldList, Form, validators +from wtforms.fields.simple import BooleanField, StringField, SubmitField + +from app.models import ScoDocSiteConfig + + +class _PersonalizedLinksForm(FlaskForm): + "form. définition des liens personnalisés" + # construit dynamiquement ci-dessous + + +def PersonalizedLinksForm() -> _PersonalizedLinksForm: + "Création d'un formulaire pour éditer les liens" + + # Formulaire dynamique, on créé une classe ad-hoc + class F(_PersonalizedLinksForm): + pass + + F.links_by_id = dict(enumerate(ScoDocSiteConfig.get_perso_links())) + + def _gen_link_form(idx): + setattr( + F, + f"link_{idx}", + StringField( + f"Titre", + validators=[ + validators.Optional(), + validators.Length(min=1, max=80), + ], + default="", + render_kw={"size": 6}, + ), + ) + setattr( + F, + f"link_url_{idx}", + StringField( + f"URL", + description="adresse, incluant le http.", + validators=[ + validators.Optional(), + validators.URL(), + validators.Length(min=1, max=256), + ], + default="", + ), + ) + setattr( + F, + f"link_with_args_{idx}", + BooleanField( + f"ajouter arguments", + description="query string avec ids", + ), + ) + + # Initialise un champ de saisie par lien + for idx in F.links_by_id: + _gen_link_form(idx) + _gen_link_form("new") + + F.submit = SubmitField("Valider") + F.cancel = SubmitField("Annuler", render_kw={"formnovalidate": True}) + + return F() diff --git a/app/models/config.py b/app/models/config.py index ca1af2881..c436248fc 100644 --- a/app/models/config.py +++ b/app/models/config.py @@ -3,9 +3,13 @@ """Model : site config WORK IN PROGRESS #WIP """ +import json +import urllib.parse + from flask import flash from app import current_app, db, log from app.comp import bonus_spo +from app.scodoc.sco_exceptions import ScoValueError from app.scodoc import sco_utils as scu from datetime import time @@ -342,3 +346,47 @@ class ScoDocSiteConfig(db.Model): log(f"set_month_debut_periode2({month})") return True return False + + @classmethod + def get_perso_links(cls) -> list["PersonalizedLink"]: + "Return links" + data_links = cls.get("personalized_links") + if not data_links: + return [] + try: + links_dict = json.loads(data_links) + except json.decoder.JSONDecodeError as exc: + # Corrupted data ? erase content + cls.set("personalized_links", "") + raise ScoValueError( + "Attention: liens personnalisés erronés: ils ont été effacés." + ) + return [PersonalizedLink(**item) for item in links_dict] + + @classmethod + def set_perso_links(cls, links: list["PersonalizedLink"] = None): + "Store all links" + if not links: + links = [] + links_dict = [link.to_dict() for link in links] + data_links = json.dumps(links_dict) + cls.set("personalized_links", data_links) + + +class PersonalizedLink: + def __init__(self, title: str = "", url: str = "", with_args: bool = False): + self.title = str(title or "") + self.url = str(url or "") + self.with_args = bool(with_args) + + def get_url(self, params: dict = {}) -> str: + if not self.with_args: + return self.url + query_string = urllib.parse.urlencode(params) + if "?" in self.url: + return self.url + "&" + query_string + return self.url + "?" + query_string + + def to_dict(self) -> dict: + "as dict" + return {"title": self.title, "url": self.url, "with_args": self.with_args} diff --git a/app/scodoc/html_sco_header.py b/app/scodoc/html_sco_header.py index 76e8727ab..fd6273709 100644 --- a/app/scodoc/html_sco_header.py +++ b/app/scodoc/html_sco_header.py @@ -30,7 +30,7 @@ import html -from flask import render_template +from flask import g, render_template from flask import request from flask_login import current_user @@ -148,6 +148,8 @@ def sco_header( "Main HTML page header for ScoDoc" from app.scodoc.sco_formsemestre_status import formsemestre_page_title + if etudid is not None: + g.current_etudid = etudid scodoc_flash_status_messages() # Get head message from http request: diff --git a/app/scodoc/sco_formsemestre_custommenu.py b/app/scodoc/sco_formsemestre_custommenu.py index ce9557eb0..3e41fde3e 100644 --- a/app/scodoc/sco_formsemestre_custommenu.py +++ b/app/scodoc/sco_formsemestre_custommenu.py @@ -29,7 +29,10 @@ """ import flask from flask import g, url_for, request +from flask_login import current_user +from app.models.config import ScoDocSiteConfig, PersonalizedLink +from app.models import FormSemestre import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb from app.scodoc.TrivialFormulator import TrivialFormulator @@ -58,6 +61,28 @@ def formsemestre_custommenu_get(formsemestre_id): return vals +def build_context_dict(formsemestre_id: int) -> dict: + """returns a dict with "current" ids, to pass to external links""" + params = { + "dept": g.scodoc_dept, + "formsemestre_id": formsemestre_id, + "user_name": current_user.user_name, + } + cas_id = getattr(current_user, "cas_id", None) + if cas_id: + params["cas_id"] = cas_id + etudid = getattr(g, "current_etudid", None) + if etudid is not None: + params["etudid"] = etudid + evaluation_id = getattr(g, "current_evaluation_id", None) + if evaluation_id is not None: + params["evaluation_id"] = evaluation_id + moduleimpl_id = getattr(g, "current_moduleimpl_id", None) + if moduleimpl_id is not None: + params["moduleimpl_id"] = moduleimpl_id + return params + + def formsemestre_custommenu_html(formsemestre_id): "HTML code for custom menu" menu = [] @@ -66,6 +91,13 @@ def formsemestre_custommenu_html(formsemestre_id): ics_url = sco_edt_cal.formsemestre_get_ics_url(sem) if ics_url: menu.append({"title": "Emploi du temps (ics)", "url": ics_url}) + # Liens globaux (config. générale) + params = build_context_dict(formsemestre_id) + for link in ScoDocSiteConfig.get_perso_links(): + if link.title: + menu.append({"title": link.title, "url": link.get_url(params=params)}) + + # Liens propres à ce semestre menu += formsemestre_custommenu_get(formsemestre_id) menu.append( { @@ -79,9 +111,11 @@ def formsemestre_custommenu_html(formsemestre_id): def formsemestre_custommenu_edit(formsemestre_id): """Dialog to edit the custom menu""" - sem = sco_formsemestre.get_formsemestre(formsemestre_id) - dest_url = ( - scu.NotesURL() + "/formsemestre_status?formsemestre_id=%s" % formsemestre_id + formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) + dest_url = url_for( + "notes.formsemestre_status", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, ) H = [ html_sco_header.html_sem_header("Modification du menu du semestre "), diff --git a/app/scodoc/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py index ac834c4ea..170bfdcae 100644 --- a/app/scodoc/sco_moduleimpl_status.py +++ b/app/scodoc/sco_moduleimpl_status.py @@ -194,6 +194,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None): if not isinstance(moduleimpl_id, int): raise ScoInvalidIdType("moduleimpl_id must be an integer !") modimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id) + g.current_moduleimpl_id = modimpl.id module: Module = modimpl.module formsemestre_id = modimpl.formsemestre_id formsemestre: FormSemestre = modimpl.formsemestre diff --git a/app/templates/config_personalized_links.j2 b/app/templates/config_personalized_links.j2 new file mode 100644 index 000000000..ff3a7f61e --- /dev/null +++ b/app/templates/config_personalized_links.j2 @@ -0,0 +1,84 @@ +{% extends "base.j2" %} +{% import 'bootstrap/wtf.html' as wtf %} + +{% block styles %} +{{super()}} + + +{% endblock %} + + +{% block app_content %} +

{{title}}

+ +
+ +

Les liens définis ici seront affichés dans le menu Liens de tous + les semestres de tous les départements.

+ +

Si on coche "ajouter arguments", une query string est ajoutée par ScoDoc + à la fin du lien, pour passer des informations sur le contexte:

+ +
+ +
+
+ + +
+
+ +{% endblock %} \ No newline at end of file diff --git a/app/templates/configuration.j2 b/app/templates/configuration.j2 index 745cf37e7..43fef06cf 100644 --- a/app/templates/configuration.j2 +++ b/app/templates/configuration.j2 @@ -24,6 +24,20 @@

Configuration générale

Les paramètres donnés ici s'appliquent à tout ScoDoc (tous les départements).
+

ScoDoc

+
+ {{ form_scodoc.hidden_tag() }} +
+
+ {{ wtf.quick_form(form_scodoc) }} +
+
+ +
+ Éditer des liens personnalisés +
+
+

Calcul des "bonus" définis par l'établissement

@@ -74,15 +88,6 @@
-

ScoDoc

- - {{ form_scodoc.hidden_tag() }} -
-
- {{ wtf.quick_form(form_scodoc) }} -
-
- {% endblock %} {% block scripts %} diff --git a/app/views/scodoc.py b/app/views/scodoc.py index 633f04c2f..41af54d6d 100644 --- a/app/views/scodoc.py +++ b/app/views/scodoc.py @@ -61,16 +61,23 @@ from app.decorators import ( scodoc, ) from app.forms.main import config_logos, config_main -from app.forms.main.create_dept import CreateDeptForm +from app.forms.main.config_assiduites import ConfigAssiduitesForm from app.forms.main.config_apo import CodesDecisionsForm from app.forms.main.config_cas import ConfigCASForm -from app.forms.main.config_assiduites import ConfigAssiduitesForm +from app.forms.main.config_personalized_links import PersonalizedLinksForm +from app.forms.main.create_dept import CreateDeptForm from app import models -from app.models import Departement, Identite +from app.models import ( + Departement, + FormSemestre, + FormSemestreInscription, + Identite, + ScoDocSiteConfig, + UniteEns, +) from app.models import departements -from app.models import FormSemestre, FormSemestreInscription -from app.models import ScoDocSiteConfig -from app.models import UniteEns +from app.models.config import PersonalizedLink + from app.scodoc import sco_find_etud from app.scodoc import sco_logos @@ -260,6 +267,38 @@ def config_codes_decisions(): ) +@bp.route("/ScoDoc/config_personalized_links", methods=["GET", "POST"]) +@admin_required +def config_personalized_links(): + """Form config liens perso""" + form = PersonalizedLinksForm() + if request.method == "POST" and form.cancel.data: # cancel button + return redirect(url_for("scodoc.index")) + if form.validate_on_submit(): + links = [] + for idx in list(form.links_by_id) + ["new"]: + title = form.data.get(f"link_{idx}") + url = form.data.get(f"link_url_{idx}") + with_args = form.data.get(f"link_with_args_{idx}") + if title and url: + links.append( + PersonalizedLink(title=title, url=url, with_args=with_args) + ) + ScoDocSiteConfig.set_perso_links(links) + flash("Liens enregistrés") + return redirect(url_for("scodoc.configuration")) + + for idx, link in form.links_by_id.items(): + getattr(form, f"link_{idx}").data = link.title + getattr(form, f"link_url_{idx}").data = link.url + getattr(form, f"link_with_args_{idx}").data = link.with_args + return render_template( + "config_personalized_links.j2", + form=form, + title="Configuration des liens personnalisés", + ) + + @bp.route("/ScoDoc/table_etud_in_accessible_depts", methods=["POST"]) @login_required def table_etud_in_accessible_depts(): diff --git a/tests/unit/test_site_config.py b/tests/unit/test_site_config.py index 92fb5e40d..6dd6d1999 100644 --- a/tests/unit/test_site_config.py +++ b/tests/unit/test_site_config.py @@ -8,7 +8,7 @@ Utiliser comme: """ -from app.models import ScoDocSiteConfig +from app.models.config import ScoDocSiteConfig, PersonalizedLink from app.comp.bonus_spo import BonusIUTRennes1 from app.scodoc import sco_utils as scu @@ -55,3 +55,14 @@ def test_scodoc_site_config(test_client): ScoDocSiteConfig.get_month_debut_annee_scolaire() == scu.MONTH_DEBUT_ANNEE_SCOLAIRE ) + # Links: + assert ScoDocSiteConfig.get_perso_links() == [] + ScoDocSiteConfig.set_perso_links( + [ + PersonalizedLink(title="lien 1", url="http://foo.bar/bar", with_args=True), + PersonalizedLink(title="lien 1", url="http://foo.bar?x=1", with_args=True), + ] + ) + links = ScoDocSiteConfig.get_perso_links() + assert links[0].get_url(params={"y": 2}) == "http://foo.bar/bar?y=2" + assert links[1].get_url(params={"y": 2}) == "http://foo.bar?x=1&y=2"