1
0
forked from ScoDoc/ScoDoc

Liens personnalisables (implements #386): au niveau global, avec paramètres.

This commit is contained in:
Emmanuel Viennet 2023-07-31 16:14:16 +02:00
parent b8767c7536
commit bb1d4f559d
9 changed files with 316 additions and 20 deletions

View File

@ -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()

View File

@ -3,9 +3,13 @@
"""Model : site config WORK IN PROGRESS #WIP """Model : site config WORK IN PROGRESS #WIP
""" """
import json
import urllib.parse
from flask import flash from flask import flash
from app import current_app, db, log from app import current_app, db, log
from app.comp import bonus_spo from app.comp import bonus_spo
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from datetime import time from datetime import time
@ -342,3 +346,47 @@ class ScoDocSiteConfig(db.Model):
log(f"set_month_debut_periode2({month})") log(f"set_month_debut_periode2({month})")
return True return True
return False 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}

View File

@ -30,7 +30,7 @@
import html import html
from flask import render_template from flask import g, render_template
from flask import request from flask import request
from flask_login import current_user from flask_login import current_user
@ -148,6 +148,8 @@ def sco_header(
"Main HTML page header for ScoDoc" "Main HTML page header for ScoDoc"
from app.scodoc.sco_formsemestre_status import formsemestre_page_title from app.scodoc.sco_formsemestre_status import formsemestre_page_title
if etudid is not None:
g.current_etudid = etudid
scodoc_flash_status_messages() scodoc_flash_status_messages()
# Get head message from http request: # Get head message from http request:

View File

@ -29,7 +29,10 @@
""" """
import flask import flask
from flask import g, url_for, request 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.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.TrivialFormulator import TrivialFormulator
@ -58,6 +61,28 @@ def formsemestre_custommenu_get(formsemestre_id):
return vals 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): def formsemestre_custommenu_html(formsemestre_id):
"HTML code for custom menu" "HTML code for custom menu"
menu = [] menu = []
@ -66,6 +91,13 @@ def formsemestre_custommenu_html(formsemestre_id):
ics_url = sco_edt_cal.formsemestre_get_ics_url(sem) ics_url = sco_edt_cal.formsemestre_get_ics_url(sem)
if ics_url: if ics_url:
menu.append({"title": "Emploi du temps (ics)", "url": 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 += formsemestre_custommenu_get(formsemestre_id)
menu.append( menu.append(
{ {
@ -79,9 +111,11 @@ def formsemestre_custommenu_html(formsemestre_id):
def formsemestre_custommenu_edit(formsemestre_id): def formsemestre_custommenu_edit(formsemestre_id):
"""Dialog to edit the custom menu""" """Dialog to edit the custom menu"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id) formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
dest_url = ( dest_url = url_for(
scu.NotesURL() + "/formsemestre_status?formsemestre_id=%s" % formsemestre_id "notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
) )
H = [ H = [
html_sco_header.html_sem_header("Modification du menu du semestre "), html_sco_header.html_sem_header("Modification du menu du semestre "),

View File

@ -194,6 +194,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
if not isinstance(moduleimpl_id, int): if not isinstance(moduleimpl_id, int):
raise ScoInvalidIdType("moduleimpl_id must be an integer !") raise ScoInvalidIdType("moduleimpl_id must be an integer !")
modimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id) modimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id)
g.current_moduleimpl_id = modimpl.id
module: Module = modimpl.module module: Module = modimpl.module
formsemestre_id = modimpl.formsemestre_id formsemestre_id = modimpl.formsemestre_id
formsemestre: FormSemestre = modimpl.formsemestre formsemestre: FormSemestre = modimpl.formsemestre

View File

@ -0,0 +1,84 @@
{% extends "base.j2" %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block styles %}
{{super()}}
<style>
div.lien_perso {
border-left: 3px solid blue;
margin-bottom: 16px;
}
div.lien_perso>div:first-child {
font-weight: bold;
font-size: 120%;
margin-left: 8px;
}
div.lien_perso .form-group, div.lien_perso .checkbox {
margin-left: 32px;
}
div.url > .form-group {
margin-bottom: 0px;
}
div.validation-buttons {
margin-top: 24px;
}
</style>
{% endblock %}
{% block app_content %}
<h1>{{title}}</h1>
<div class="help">
<p>Les liens définis ici seront affichés dans le menu <b>Liens</b> de tous
les semestres de tous les départements.</p>
<p>Si on coche "ajouter arguments", une query string est ajoutée par ScoDoc
à la fin du lien, pour passer des informations sur le contexte:</p>
<ul>
<li><tt>dept</tt> : acronyme du département
<li><tt>formsemestre_id</tt> : id du formsemestre affiché
<li><tt>moduleimpl_id</tt> : id du moduleimpl affiché (si page module)
<li><tt>evaluation_id</tt> : id de l'évaluation affichée (si page d'évaluation)
<li><tt>etudid</tt> : id de l'étudiant (si un étudiant est sélectionné)
<li><tt>user_name</tt> : login scodoc de l'utilisateur
<li><tt>cas_id</tt> : login CAS de l'utilisateur
<ul>
</div>
<div class="row">
<div class="col-md-8">
<form class="form form-horizontal form-personalized-links" method="post" enctype="multipart/form-data" role="form">
{{ form.hidden_tag() }}
{{ wtf.form_errors(form, hiddens="only") }}
{% for idx, link in form.links_by_id.items() %}
<div class="lien_perso">
<div>Lien personnalisé {{idx}}</div>
{{ wtf.form_field( form["link_"+idx|string] ) }}
<div class="url">{{ wtf.form_field( form["link_url_"+idx|string] ) }}</div>
{{ wtf.form_field( form["link_with_args_"+idx|string] ) }}
</div>
{% endfor %}
<div class="lien_perso">
<div>Nouveau lien personnalisé</div>
{{ wtf.form_field( form["link_new"] ) }}
<div class="url">{{ wtf.form_field( form["link_url_new"] ) }}</div>
{{ wtf.form_field( form["link_with_args_new"] ) }}
</div>
<div class="form-group validation-buttons">
{{ wtf.form_field(form.submit) }}
{{ wtf.form_field(form.cancel) }}
</div>
</form>
</div>
</div>
{% endblock %}

View File

@ -24,6 +24,20 @@
<h1>Configuration générale</h1> <h1>Configuration générale</h1>
<div class="sco_help greenboldtext">Les paramètres donnés ici s'appliquent à tout ScoDoc (tous les départements).</div> <div class="sco_help greenboldtext">Les paramètres donnés ici s'appliquent à tout ScoDoc (tous les départements).</div>
<h2>ScoDoc</h2>
<form id="configuration_form_scodoc" class="sco-form" action="" method="post" enctype="multipart/form-data" novalidate>
{{ form_scodoc.hidden_tag() }}
<div class="row">
<div class="col-md-8">
{{ wtf.quick_form(form_scodoc) }}
</div>
</div>
<div style="margin-top: 16px;">
<a class="stdlink" href="{{url_for('scodoc.config_personalized_links')}}">Éditer des liens personnalisés</a>
</div>
</form>
<section> <section>
<h2>Calcul des "bonus" définis par l'établissement</h2> <h2>Calcul des "bonus" définis par l'établissement</h2>
<form id="configuration_form" class="sco-form" action="" method="post" enctype="multipart/form-data" novalidate> <form id="configuration_form" class="sco-form" action="" method="post" enctype="multipart/form-data" novalidate>
@ -74,15 +88,6 @@
</div> </div>
</section> </section>
<h2>ScoDoc</h2>
<form id="configuration_form_scodoc" class="sco-form" action="" method="post" enctype="multipart/form-data" novalidate>
{{ form_scodoc.hidden_tag() }}
<div class="row">
<div class="col-md-8">
{{ wtf.quick_form(form_scodoc) }}
</div>
</div>
</form>
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}

View File

@ -61,16 +61,23 @@ from app.decorators import (
scodoc, scodoc,
) )
from app.forms.main import config_logos, config_main 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_apo import CodesDecisionsForm
from app.forms.main.config_cas import ConfigCASForm 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 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 departements
from app.models import FormSemestre, FormSemestreInscription from app.models.config import PersonalizedLink
from app.models import ScoDocSiteConfig
from app.models import UniteEns
from app.scodoc import sco_find_etud from app.scodoc import sco_find_etud
from app.scodoc import sco_logos 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"]) @bp.route("/ScoDoc/table_etud_in_accessible_depts", methods=["POST"])
@login_required @login_required
def table_etud_in_accessible_depts(): def table_etud_in_accessible_depts():

View File

@ -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.comp.bonus_spo import BonusIUTRennes1
from app.scodoc import sco_utils as scu 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() ScoDocSiteConfig.get_month_debut_annee_scolaire()
== scu.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"