/accounts/: add an edit page for restricted-valueset attributes (#86937)
This commit is contained in:
parent
71f15b35c4
commit
9f94256ccf
|
@ -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,
|
||||
|
||||
|
|
|
@ -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 %}
|
|
@ -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'),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue