écran de configuration de l’apparence par défaut des pages de login/authz (#75139) #35
|
@ -44,9 +44,14 @@ from authentic2.forms.fields import (
|
|||
)
|
||||
from authentic2.forms.mixins import SlugMixin
|
||||
from authentic2.forms.profile import BaseUserForm
|
||||
from authentic2.models import APIClient, PasswordReset, Service
|
||||
from authentic2.models import APIClient, PasswordReset, Service, Setting
|
||||
from authentic2.passwords import generate_password, get_min_password_strength
|
||||
from authentic2.utils.misc import send_email_change_email, send_password_reset_mail, send_templated_mail
|
||||
from authentic2.utils.misc import (
|
||||
RUNTIME_SETTINGS,
|
||||
send_email_change_email,
|
||||
send_password_reset_mail,
|
||||
send_templated_mail,
|
||||
)
|
||||
from authentic2.validators import EmailValidator
|
||||
|
||||
from . import app_settings, fields, utils
|
||||
|
@ -985,3 +990,22 @@ class ServiceForm(forms.ModelForm):
|
|||
class Meta:
|
||||
model = Service
|
||||
fields = ['name', 'slug', 'ou', 'unauthorized_url']
|
||||
|
||||
|
||||
class ServicesSettingsForm(forms.Form):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
for setting in Setting.objects.filter_namespace('sso'):
|
||||
if RUNTIME_SETTINGS[setting.key]['type'] == 'url':
|
||||
field = forms.URLField
|
||||
else:
|
||||
field = forms.CharField
|
||||
|
||||
self.fields[setting.key] = field(
|
||||
initial=setting.value,
|
||||
label=RUNTIME_SETTINGS[setting.key]['name'],
|
||||
required=False,
|
||||
)
|
||||
if RUNTIME_SETTINGS[setting.key]['type'] == 'colour':
|
||||
self.fields[setting.key].widget = forms.TextInput(attrs={'type': 'color'})
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
from django.contrib import messages
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from authentic2.models import Service
|
||||
from authentic2.models import Service, Setting
|
||||
|
||||
from . import forms, role_views, tables, views
|
||||
|
||||
|
@ -150,3 +150,24 @@ class ServiceDeleteView(views.BaseDeleteView):
|
|||
|
||||
|
||||
delete_service = ServiceDeleteView.as_view()
|
||||
|
||||
|
||||
class ServicesSettingsView(views.FormView):
|
||||
template_name = 'authentic2/manager/services_settings.html'
|
||||
form_class = forms.ServicesSettingsForm
|
||||
title = _('Edit services-related settings')
|
||||
success_url = '..'
|
||||
permissions = ['authentic2.change_service']
|
||||
|
||||
def form_valid(self, form):
|
||||
for key, value in form.cleaned_data.items():
|
||||
try:
|
||||
setting = Setting.objects.get(key=key)
|
||||
except Setting.DoesNotExist:
|
||||
continue
|
||||
setting.value = value
|
||||
setting.save()
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
services_settings = ServicesSettingsView.as_view()
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
{% if view.can_add %}
|
||||
<a href="{% url "a2-manager-add-oidc-service" %}">{% trans "Add OIDC service" %}</a>
|
||||
{% endif %}
|
||||
<a rel="popup" href="{% url "a2-manager-services-settings" %}">{% trans "Settings" %}</a>
|
||||
</span>
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
{% extends "authentic2/manager/form.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="{% url 'a2-manager-services' %}">{% trans 'Services' %}</a>
|
||||
<a href="#">{% trans "Edit services configuration" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block page_title %}
|
||||
{% trans "Edit services configuration" %}
|
||||
{% endblock %}
|
|
@ -173,6 +173,7 @@ urlpatterns = required(
|
|||
path('organizational-units/import/', ou_views.ous_import, name='a2-manager-ous-import'),
|
||||
# Services
|
||||
path('services/', service_views.listing, name='a2-manager-services'),
|
||||
path('services/settings/', service_views.services_settings, name='a2-manager-services-settings'),
|
||||
path('services/<int:service_pk>/', service_views.service_detail, name='a2-manager-service'),
|
||||
path(
|
||||
'services/<int:service_pk>/settings/',
|
||||
|
|
|
@ -112,5 +112,10 @@ class AttributeManager(managers.QueryManager.from_queryset(GetByNameQuerySet)):
|
|||
pass
|
||||
|
||||
|
||||
class SettingManager(models.Manager):
|
||||
def filter_namespace(self, ns):
|
||||
return self.filter(key__startswith=f'{ns}:')
|
||||
|
||||
|
||||
ServiceManager = BaseServiceManager.from_queryset(ServiceQuerySet)
|
||||
AttributeValueManager = managers.QueryManager.from_queryset(AttributeValueQuerySet)
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 3.2.18 on 2023-04-11 08:16
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('authentic2', '0045_auto_20221222_1013'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Setting',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
('key', models.CharField(max_length=128, unique=True, verbose_name='key')),
|
||||
('value', models.JSONField(blank=True, verbose_name='value')),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -0,0 +1,39 @@
|
|||
from django.db import migrations
|
||||
|
||||
|
||||
def initialize_services_runtime_settings(apps, schema_editor):
|
||||
from authentic2.utils.misc import RUNTIME_SETTINGS
|
||||
|
||||
Setting = apps.get_model('authentic2', 'Setting')
|
||||
|
||||
if Setting.objects.filter(key__startswith='sso:').count() == 4:
|
||||
return
|
||||
for key, data in RUNTIME_SETTINGS.items():
|
||||
|
||||
Setting.objects.get_or_create(
|
||||
key=key,
|
||||
defaults={
|
||||
'value': data['value'],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def clear_services_runtime_settings(apps, schema_editor):
|
||||
Setting = apps.get_model('authentic2', 'Setting')
|
||||
|
||||
# default config has been extended, do not try to revert it
|
||||
if Setting.objects.filter(key__startswith='sso:').count() != 4:
|
||||
return
|
||||
|
||||
Setting.objects.filter(key__startswith='sso:').delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('authentic2', '0046_runtimesetting'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
initialize_services_runtime_settings, reverse_code=clear_services_runtime_settings
|
||||
),
|
||||
]
|
|
@ -843,3 +843,10 @@ class SMSCode(models.Model):
|
|||
duration = cls.CODE_DURATION
|
||||
expires = expires or (timezone.now() + datetime.timedelta(seconds=duration))
|
||||
return cls.objects.create(kind=kind, phone=phone, expires=expires)
|
||||
|
||||
|
||||
class Setting(models.Model):
|
||||
bdauvergne
commented
Appeler ça setting ou config j'ai déjà mal aux doigts de devoir écrire Runtime :) Appeler ça setting ou config j'ai déjà mal aux doigts de devoir écrire Runtime :)
|
||||
key = models.CharField(verbose_name=_('key'), max_length=128, unique=True)
|
||||
value = JSONField(verbose_name=_('value'), blank=True)
|
||||
|
||||
objects = managers.SettingManager()
|
||||
|
|
|
@ -45,6 +45,7 @@ from django.urls import reverse
|
|||
from django.utils import html, timezone
|
||||
from django.utils.encoding import iri_to_uri, uri_to_iri
|
||||
from django.utils.formats import localize
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import ngettext
|
||||
|
||||
from authentic2.saml.saml2utils import filter_attribute_private_key, filter_element_private_key
|
||||
|
@ -1352,3 +1353,27 @@ def parse_phone_number(phonenumber):
|
|||
except phonenumbers.NumberParseException:
|
||||
pass
|
||||
return parsed_pn
|
||||
|
||||
|
||||
RUNTIME_SETTINGS = {
|
||||
'sso:default_service_colour': {
|
||||
'name': _('Default service colour'),
|
||||
'value': '',
|
||||
'type': 'colour',
|
||||
},
|
||||
'sso:default_service_logo_url': {
|
||||
'name': _('Default service logo URL'),
|
||||
'value': '',
|
||||
'type': 'url',
|
||||
},
|
||||
'sso:default_service_name': {
|
||||
'name': _('Default service name'),
|
||||
'value': '',
|
||||
'type': None,
|
||||
},
|
||||
'sso:default_service_home_url': {
|
||||
bdauvergne
commented
La différence setting_type/widget ne me paraît pas utile à ce stade, on peu se contenter d'un champ type avec trois valeurs url, colour et text, text étant la valeur par défaut si c'est None / absent. La différence setting_type/widget ne me paraît pas utile à ce stade, on peu se contenter d'un champ type avec trois valeurs url, colour et text, text étant la valeur par défaut si c'est None / absent.
pmarillonnet
commented
Ok, j’ai fait la modif. Ok, j’ai fait la modif.
|
||||
'name': _('Default service home URL'),
|
||||
'value': '',
|
||||
'type': 'url',
|
||||
},
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ from authentic2.a2_rbac.models import OrganizationalUnit as OU
|
|||
from authentic2.a2_rbac.models import Permission, Role
|
||||
from authentic2.a2_rbac.utils import get_default_ou, get_operation
|
||||
from authentic2.apps.journal.models import Event
|
||||
from authentic2.models import Service
|
||||
from authentic2.models import Service, Setting
|
||||
from authentic2.validators import EmailValidator
|
||||
|
||||
from .utils import assert_event, get_link_from_mail, login, request_select2, text_content
|
||||
|
@ -1358,6 +1358,40 @@ def test_manager_service_search(app, admin, ou1):
|
|||
assert 'Example Service' not in resp.text
|
||||
|
||||
|
||||
def test_manager_services_settings(app, admin):
|
||||
for setting in Setting.objects.filter_namespace('sso'):
|
||||
assert setting.value == ''
|
||||
|
||||
resp = login(app, admin, 'a2-manager-services-settings')
|
||||
resp.form.submit()
|
||||
|
||||
for setting in Setting.objects.filter_namespace('sso'):
|
||||
assert setting.value == ''
|
||||
|
||||
resp = app.get(reverse('a2-manager-services-settings'))
|
||||
resp.form.set('sso:default_service_home_url', 'https://www.example.com/')
|
||||
resp.form.set('sso:default_service_logo_url', 'https://www.example.com/logo.png')
|
||||
resp.form.set('sso:default_service_colour', '#dedede')
|
||||
resp.form.set('sso:default_service_name', 'Some default name')
|
||||
resp.form.submit()
|
||||
|
||||
assert Setting.objects.get(key='sso:default_service_home_url').value == 'https://www.example.com/'
|
||||
assert Setting.objects.get(key='sso:default_service_logo_url').value == 'https://www.example.com/logo.png'
|
||||
assert Setting.objects.get(key='sso:default_service_colour').value == '#dedede'
|
||||
assert Setting.objects.get(key='sso:default_service_name').value == 'Some default name'
|
||||
|
||||
resp = app.get(reverse('a2-manager-services-settings'))
|
||||
resp.form.set('sso:default_service_home_url', 'https://www2.example.com/')
|
||||
resp.form.set('sso:default_service_logo_url', '')
|
||||
resp.form.set('sso:default_service_name', 'Some other name')
|
||||
resp.form.submit()
|
||||
|
||||
assert Setting.objects.get(key='sso:default_service_home_url').value == 'https://www2.example.com/'
|
||||
assert Setting.objects.get(key='sso:default_service_logo_url').value == ''
|
||||
assert Setting.objects.get(key='sso:default_service_colour').value == '#dedede'
|
||||
assert Setting.objects.get(key='sso:default_service_name').value == 'Some other name'
|
||||
|
||||
|
||||
def test_manager_menu_json(app, admin):
|
||||
expected = [
|
||||
{
|
||||
|
|
|
@ -58,3 +58,17 @@ def test_migration_custom_user_0028_user_email_verified_date(transactional_db, m
|
|||
User = new_apps.get_model('custom_user', 'User')
|
||||
user = User.objects.get()
|
||||
assert user.email_verified_date == user.date_joined
|
||||
|
||||
|
||||
def test_migration_custom_user_0047_initialize_services_runtime_settings(transactional_db, migration):
|
||||
old_apps = migration.before([('authentic2', '0046_runtimesetting')])
|
||||
|
||||
Setting = old_apps.get_model('authentic2', 'Setting')
|
||||
assert Setting.objects.count() == 0
|
||||
|
||||
new_apps = migration.apply([('authentic2', '0047_initialize_services_runtime_settings')])
|
||||
Setting = new_apps.get_model('authentic2', 'Setting')
|
||||
assert Setting.objects.count() == 4
|
||||
assert Setting.objects.filter(key__startswith='sso:').count() == 4
|
||||
for setting in Setting.objects.filter(key__startswith='sso:'):
|
||||
assert setting.value == ''
|
||||
|
|
Loading…
Reference in New Issue
Je ne mettrai pas ça en base, je ferai plutôt un dictionnaire global avec la déclaration des settings disponibles.
Un peu comme les types d'attributs, on devrait d'ailleurs essayer de partager quelque chose ici à terme, mais là pour 4 attributs de type url, string et couleur on s'en passera.
Plutôt une liste quand même si on veut être certain que l'ordre soit conservé (les dicos sont ordonnées depuis python 3.x mais bon je ne mettrai pas ma main à couper que ça ne changera pas, je ne sais pas si on peut vraiment en dépendre), une liste de :
par exemple.
On peut vraiment en dépendre.
Changed in version 3.7: Dictionary order is guaranteed to be insertion order. This behavior was an implementation detail of CPython from 3.6. -- https://docs.python.org/3/library/stdtypes.html#dict
Va pour le dico alors.