/accounts/: add an edit page for restricted-valueset attributes (#86937)

This commit is contained in:
Paul Marillonnet 2024-03-06 10:29:49 +01:00
parent 71f15b35c4
commit 9f94256ccf
5 changed files with 246 additions and 2 deletions

View File

@ -151,6 +151,36 @@ class EditProfileForm(NextUrlFormMixin, BaseUserForm):
pass
class EditRestrictedAttributeForm(forms.Form):
def __init__(self, *args, **kwargs):
attribute = kwargs.pop('attribute')
user = kwargs.pop('user')
super().__init__(*args, **kwargs)
if not attribute.multiple:
field = forms.ChoiceField
widget = forms.RadioSelect
else:
field = forms.MultipleChoiceField
widget = forms.CheckboxSelectMultiple
self.fields[attribute.name] = field(
label=attribute.label,
widget=widget(),
choices=((el.name, el.displayed_label) for el in attribute.valueset_elements.all()),
required=attribute.required,
)
# populate initial values
if qs := models.AttributeRestrictedValueSetElement.objects.filter(
attribute=attribute,
serialized_content__in=models.AttributeValue.objects.with_owner(user)
.filter(attribute=attribute)
.values_list('content', flat=True),
):
initial = list(qs.values_list('name', flat=True))
self.fields[attribute.name].initial = initial if attribute.multiple else initial[0]
def modelform_factory(model, **kwargs):
"""Build a modelform for the given model,

View File

@ -0,0 +1,24 @@
{% extends "authentic2/base-page.html" %}
{% load i18n gadjo %}
{% block title %}
{{ view.title }}
{% endblock %}
{% block breadcrumb %}
{{ block.super }}
<a href="..">{% trans "Your account" %}</a>
<a href="">{{ title }}</a>
{% endblock %}
{% block content %}
<form enctype="multipart/form-data" method="post">
{% csrf_token %}
{{ form|with_template }}
<div class="buttons">
<button class="submit-button">{% trans "Submit" %}</button>
<button class="cancel-button" name="cancel" formnovalidate>{% trans "Cancel" %}</button>
</div>
</form>
{% endblock %}

View File

@ -54,6 +54,11 @@ accounts_urlpatterns = [
path('edit/', views.edit_profile, name='profile_edit'),
path('edit/required/', views.edit_required_profile, name='profile_required_edit'),
re_path(r'^edit/(?P<scope>[-\w]+)/$', views.edit_profile, name='profile_edit_with_scope'),
re_path(
r'^edit_restricted_attribute/(?P<attribute>[-\w]+)/$',
views.edit_restricted_attribute,
name='profile_edit_restricted_attribute',
),
path('change-email/', views.email_change, name='email-change'),
path('change-email/verify/', views.email_change_verify, name='email-change-verify'),
path('change-phone/', views.phone_change, name='phone-change'),

View File

@ -179,6 +179,65 @@ edit_profile = decorators.setting_enabled('A2_PROFILE_CAN_EDIT_PROFILE')(
)
class EditRestrictedAttributeView(HomeURLMixin, cbv.HookMixin, cbv.TemplateNamesMixin, FormView):
template_names = ['authentic2/accounts_edit_restricted_attribute.html']
form_class = profile_forms.EditRestrictedAttributeForm
success_url = reverse_lazy('account_management')
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
kwargs['attribute'] = self.attribute
return kwargs
def dispatch(self, request, *args, **kwargs):
self.attribute = get_object_or_404(models.Attribute, name=kwargs['attribute'])
if not self.attribute.valueset_elements.all():
messages.info(
request,
_(f'Attribute “{self.attribute.label}” does not restrict its values.'),
)
return utils_misc.redirect(request, 'account_management')
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['title'] = self.get_title()
return ctx
def get_title(self):
return _(f'Edit your profile attribute “{self.attribute.label}')
def form_valid(self, form):
response = super().form_valid(form)
if self.attribute.name in form.cleaned_data:
if self.attribute.multiple:
atvs = list(
models.AttributeRestrictedValueSetElement.objects.filter(
attribute=self.attribute,
name__in=form.cleaned_data[self.attribute.name],
).values_list('serialized_content', flat=True)
)
setattr(self.request.user.attributes, self.attribute.name, atvs)
else:
atv = models.AttributeRestrictedValueSetElement.objects.get(
attribute=self.attribute,
name=form.cleaned_data[self.attribute.name],
).serialized_content
setattr(self.request.user.attributes, self.attribute.name, atv)
self.request.user.save()
messages.info(
self.request,
_(f'Your profile attribute “{self.attribute.label}” has been updated.'),
)
return response
edit_restricted_attribute = decorators.setting_enabled('A2_PROFILE_CAN_EDIT_PROFILE')(
login_required(EditRestrictedAttributeView.as_view())
)
class EditRequired(EditProfile):
template_names = ['authentic2/accounts_edit_required.html']
@ -766,7 +825,7 @@ class ProfileView(HomeURLMixin, cbv.TemplateNamesMixin, TemplateView):
attribute = None
if attribute:
if not attribute.user_visible:
if not attribute.user_visible or attribute.valueset_elements.all():
continue
html_value = attribute.get_kind().get('html_value', lambda a, b: b)
qs = models.AttributeValue.objects.with_owner(request.user)

View File

@ -20,7 +20,7 @@ from django.urls import reverse
from authentic2.a2_rbac.utils import get_default_ou
from authentic2.apps.authenticators.models import LoginPasswordAuthenticator
from authentic2.models import Attribute
from authentic2.models import Attribute, AttributeRestrictedValueSetElement
from authentic2_idp_oidc.models import OIDCClient
from . import utils
@ -531,3 +531,129 @@ def test_account_view_boolean(app, simple_user, settings):
simple_user.attributes.accept = False
resp = app.get(reverse('account_management'))
assert 'Vrai' not in resp.text
def test_accounts_edit_restricted_attribute_view(app, simple_user, settings):
attribute = Attribute.objects.create(
name='favourite_colors',
label='Favourite colors',
kind='string',
user_visible=True,
user_editable=True,
multiple=True,
)
attribute2 = Attribute.objects.create(
name='favourite_shape',
label='Favourite shape',
kind='string',
user_visible=True,
user_editable=True,
multiple=False,
)
for color, label in (('b', 'Blue'), ('g', 'Green'), ('f', 'Fuschia'), ('c', 'Crimson')):
AttributeRestrictedValueSetElement.objects.create(
attribute=attribute,
serialized_content=color,
displayed_label=label,
name=label.lower(),
)
for shape, label in (('s', 'Square'), ('c', 'Circle'), ('h', 'Hexagon'), ('o', 'Octogon')):
AttributeRestrictedValueSetElement.objects.create(
attribute=attribute2,
serialized_content=shape,
displayed_label=label,
name=label.lower(),
)
assert not simple_user.attributes.favourite_colors
assert attribute.valueset_elements.all()
assert not simple_user.attributes.favourite_shape
assert attribute2.valueset_elements.all()
simple_user.attributes.favourite_colors = ['b', 'f']
simple_user.attributes.favourite_shape = 'o'
simple_user.save()
utils.login(app, simple_user)
resp = app.get(reverse('profile_edit_restricted_attribute', kwargs={'attribute': 'favourite_colors'}))
assert len(resp.pyquery('ul#id_favourite_colors li')) == 4
# test labels
assert {label.text_content().strip() for label in resp.pyquery('ul#id_favourite_colors li label')} == {
'Blue',
'Green',
'Fuschia',
'Crimson',
}
# test input
assert {input.get('value') for input in resp.pyquery('ul#id_favourite_colors li label input')} == {
'blue',
'green',
'fuschia',
'crimson',
}
# test input type
assert all(
input.get('type') == 'checkbox' for input in resp.pyquery('ul#id_favourite_colors li label input')
)
# test initial values
assert {
input.get('value') for input in resp.pyquery('ul#id_favourite_colors li label input[checked]')
} == {
'blue',
'fuschia',
}
resp.form.set('favourite_colors', ['fuschia', 'crimson'])
resp = resp.form.submit()
assert resp.location == '/accounts/'
resp = resp.follow()
assert (
resp.pyquery('ul.messages li.info')[0].text_content()
== 'Your profile attribute “Favourite colors” has been updated.'
)
simple_user.refresh_from_db()
assert set(simple_user.attributes.favourite_colors) == {'f', 'c'} # fuschia, crimson
resp = app.get(reverse('profile_edit_restricted_attribute', kwargs={'attribute': 'favourite_shape'}))
assert len(resp.pyquery('div[role="radiogroup"] label')) == 4
# test labels
assert {label.text_content().strip() for label in resp.pyquery('div[role="radiogroup"] label')} == {
'Square',
'Circle',
'Hexagon',
'Octogon',
}
# test input
assert {input.get('value') for input in resp.pyquery('div[role="radiogroup"] label input')} == {
'square',
'circle',
'hexagon',
'octogon',
}
# test input type
assert all(input.get('type') == 'radio' for input in resp.pyquery('div[role="radiogroup"] label input'))
# test initial value
assert {input.get('value') for input in resp.pyquery('div[role="radiogroup"] label input[checked]')} == {
'octogon',
}
resp.form.set('favourite_shape', 'hexagon')
resp = resp.form.submit()
assert resp.location == '/accounts/'
resp = resp.follow()
assert (
resp.pyquery('ul.messages li.info')[0].text_content()
== 'Your profile attribute “Favourite shape” has been updated.'
)
simple_user.refresh_from_db()
assert simple_user.attributes.favourite_shape == 'h' # hexagon