Projet

Général

Profil

0001-forms-implement-locked-fields-by-renaming-and-widget.patch

Benjamin Dauvergne, 19 juin 2019 23:20

Télécharger (12,3 ko)

Voir les différences:

Subject: [PATCH] forms: implement locked fields by renaming and widget change
 (#32954)

It simplifies the code (no need to implement a special clean() method)
and it covers the case of field with widget not supporting the readonly
HTML attribute like those based on <select> or <input type="radio">
tags.
 src/authentic2/forms/mixins.py  | 77 ++++++++++++++++++++++++++++
 src/authentic2/forms/profile.py | 36 +++++++------
 tests/test_profile.py           | 90 ++++++++++++++++++++++++---------
 3 files changed, 162 insertions(+), 41 deletions(-)
 create mode 100644 src/authentic2/forms/mixins.py
src/authentic2/forms/mixins.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2019 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from collections import OrderedDict
18

  
19
from django import forms
20
from django.utils.translation import ugettext as _
21

  
22

  
23
class LockedFieldFormMixin(object):
24
    def __init__(self, *args, **kwargs):
25
        super(LockedFieldFormMixin, self).__init__(*args, **kwargs)
26
        self.__lock_fields()
27

  
28
    def __lock_fields(self):
29
        # Locked fields are modified to use a read-only TextInput
30
        # widget remapped to a name which will be ignored by Form
31
        # implementation
32
        locked_fields = {}
33
        for name in self.fields:
34
            if not self.is_field_locked(name):
35
                continue
36
            field = self.fields[name]
37
            initial = self.initial[name]
38
            try:
39
                choices = field.choices
40
            except AttributeError:
41
                # BooleanField case
42
                if isinstance(initial, bool):
43
                    initial = _('Yes') if initial else _('No')
44
                else:
45
                    # Most other fields case
46
                    try:
47
                        initial = field.widget.format_value(initial)
48
                    except AttributeError:
49
                        # Django 1.8
50
                        try:
51
                            initial = field.widget._format_value(initial)
52
                        except AttributeError:
53
                            pass
54
            else:
55
                for key, label in choices:
56
                    if initial == key:
57
                        initial = label
58
                        break
59
            locked_fields[name] = forms.CharField(
60
                label=field.label,
61
                help_text=field.help_text,
62
                initial=initial,
63
                required=False,
64
                widget=forms.TextInput(attrs={'readonly': ''}))
65
        if not locked_fields:
66
            return
67

  
68
        new_fields = OrderedDict()
69
        for name in self.fields:
70
            if name in locked_fields:
71
                new_fields[name + '@disabled'] = locked_fields[name]
72
            else:
73
                new_fields[name] = self.fields[name]
74
        self.fields = new_fields
75

  
76
    def is_field_locked(self, name):
77
        raise NotImplementedError
src/authentic2/forms/profile.py
14 14
# You should have received a copy of the GNU Affero General Public License
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16

  
17
from collections import OrderedDict
17 18

  
18 19
from django.forms.models import modelform_factory as dj_modelform_factory
19 20
from django import forms
......
22 23
from ..custom_user.models import User
23 24
from .. import app_settings, models
24 25
from .utils import NextUrlFormMixin
26
from .mixins import LockedFieldFormMixin
25 27

  
26 28

  
27 29
class DeleteAccountForm(forms.Form):
......
66 68
        return password
67 69

  
68 70

  
69
class BaseUserForm(forms.ModelForm):
71
class BaseUserForm(LockedFieldFormMixin, forms.ModelForm):
70 72
    error_messages = {
71 73
        'duplicate_username': _("A user with that username already exists."),
72 74
    }
......
76 78

  
77 79
        self.attributes = models.Attribute.objects.all()
78 80
        initial = kwargs.setdefault('initial', {})
79
        if kwargs.get('instance'):
80
            instance = kwargs['instance']
81
            for av in models.AttributeValue.objects.with_owner(instance):
82
                if av.attribute.name in self.declared_fields:
83
                    if av.verified:
84
                        self.declared_fields[av.attribute.name].widget.attrs['readonly'] = 'readonly'
85
                    initial[av.attribute.name] = av.to_python()
81
        instance = kwargs.get('instance')
82
        # extended attributes are not model fields, their initial value must be
83
        # explicitely defined
84
        self.atvs = []
85
        self.locked_fields = set()
86
        if instance:
87
            self.atvs = models.AttributeValue.objects.select_related('attribute').with_owner(instance)
88
            for atv in self.atvs:
89
                name = atv.attribute.name
90
                if name in self.declared_fields:
91
                    initial[name] = atv.to_python()
92
                # helper data for LockedFieldFormMixin
93
                if atv.verified:
94
                    self.locked_fields.add(name)
86 95
        super(BaseUserForm, self).__init__(*args, **kwargs)
87 96

  
88
    def clean(self):
89
        from authentic2 import models
90

  
91
        # make sure verified fields are not modified
92
        for av in models.AttributeValue.objects.with_owner(
93
                self.instance).filter(verified=True):
94
            self.cleaned_data[av.attribute.name] = av.to_python()
95
        super(BaseUserForm, self).clean()
97
    def is_field_locked(self, name):
98
        # helper method for LockedFieldFormMixin
99
        return name in self.locked_fields
96 100

  
97 101
    def save_attributes(self):
98 102
        # only save non verified attributes here
tests/test_profile.py
29 29
    url = reverse('profile_edit')
30 30
    resp = app.get(url, status=200)
31 31

  
32
    attribute = Attribute.objects.create(
32
    phone = Attribute.objects.create(
33 33
        name='phone', label='phone',
34
        kind='string', user_visible=True, user_editable=True)
35

  
36
    resp = app.get(url, status=200)
37
    resp = app.post(url, params={
38
                        'csrfmiddlewaretoken': resp.form['csrfmiddlewaretoken'].value,
39
                        'edit-profile-first_name': resp.form['edit-profile-first_name'].value,
40
                        'edit-profile-last_name': resp.form['edit-profile-last_name'].value,
41
                        'edit-profile-phone': '1234'
42
                    },
43
                    status=302)
34
        kind='phone_number', user_visible=True, user_editable=True)
35
    title = Attribute.objects.create(
36
        name='title', label='title',
37
        kind='title', user_visible=True, user_editable=True)
38
    agreement = Attribute.objects.create(
39
        name='agreement', label='agreement',
40
        kind='boolean', user_visible=True, user_editable=True)
41

  
42
    resp = old_resp = app.get(url, status=200)
43
    resp.form['edit-profile-phone'] = '1234'
44
    resp.form['edit-profile-title'] = 'Mrs'
45
    resp.form['edit-profile-agreement'] = False
46
    resp = resp.form.submit()
44 47
    # verify that missing next_url in POST is ok
45 48
    assert resp['Location'].endswith(reverse('account_management'))
46
    assert attribute.get_value(simple_user) == '1234'
49
    assert phone.get_value(simple_user) == '1234'
50
    assert title.get_value(simple_user) == 'Mrs'
51
    assert agreement.get_value(simple_user) is False
47 52

  
48 53
    resp = app.get(url, status=200)
49 54
    resp.form.set('edit-profile-phone', '0123456789')
50 55
    resp = resp.form.submit().follow()
51
    assert attribute.get_value(simple_user) == '0123456789'
56
    assert phone.get_value(simple_user) == '0123456789'
52 57

  
53 58
    resp = app.get(url, status=200)
54 59
    resp.form.set('edit-profile-phone', '9876543210')
55 60
    resp = resp.form.submit('cancel').follow()
56
    assert attribute.get_value(simple_user) == '0123456789'
61
    assert phone.get_value(simple_user) == '0123456789'
57 62

  
58
    attribute.set_value(simple_user, '0123456789', verified=True)
63
    phone.set_value(simple_user, '0123456789', verified=True)
64
    title.set_value(simple_user, 'Mr', verified=True)
65
    agreement.set_value(simple_user, True, verified=True)
59 66
    resp = app.get(url, status=200)
60
    resp.form.set('edit-profile-phone', '1234567890')
61
    assert 'readonly' in resp.form['edit-profile-phone'].attrs
67
    assert 'edit-profile-phone' not in resp.form.fields
68
    assert 'edit-profile-title' not in resp.form.fields
69
    assert 'edit-profile-agreement' not in resp.form.fields
70
    assert 'readonly' in resp.form['edit-profile-phone@disabled'].attrs
71
    assert resp.form['edit-profile-phone@disabled'].value == '0123456789'
72
    assert resp.form['edit-profile-title@disabled'].value == 'Mr'
73
    assert resp.form['edit-profile-agreement@disabled'].value == 'Yes'
74
    resp.form.set('edit-profile-phone@disabled', '1234')
75
    resp.form.set('edit-profile-title@disabled', 'Mrs')
76
    resp.form.set('edit-profile-agreement@disabled', 'False')
62 77
    resp = resp.form.submit().follow()
63
    assert attribute.get_value(simple_user) == '0123456789'
78
    assert phone.get_value(simple_user) == '0123456789'
79
    assert title.get_value(simple_user) == 'Mr'
80
    assert agreement.get_value(simple_user) is True
64 81

  
65
    resp = app.get(url, status=200)
66
    assert 'phone' in resp
67
    assert 'readonly' in resp.form['edit-profile-phone'].attrs
82
    resp = old_resp.form.submit()
83
    assert phone.get_value(simple_user) == '0123456789'
84
    assert title.get_value(simple_user) == 'Mr'
85
    assert agreement.get_value(simple_user) is True
68 86

  
69
    attribute.disabled = True
70
    attribute.save()
87
    phone.disabled = True
88
    phone.save()
71 89
    resp = app.get(url, status=200)
72
    assert 'phone' not in resp
73
    assert attribute.get_value(simple_user) == '0123456789'
90
    assert 'edit-profile-phone@disabled' not in resp
91
    assert 'edit-profile-title@disabled' in resp
92
    assert 'edit-profile-agreement@disabled' in resp
93
    assert phone.get_value(simple_user) == '0123456789'
74 94

  
75 95

  
76 96
def test_account_edit_next_url(app, simple_user, external_redirect_next_url, assert_external_redirect):
......
135 155
    resp = app.get(reverse('profile_edit_with_scope', kwargs={'scope': 'address'}),
136 156
                   status=200)
137 157
    assert get_fields(resp) == set(['city', 'zipcode', 'next_url'])
158

  
159

  
160
def test_account_edit_locked_title(app, simple_user):
161
    Attribute.objects.create(
162
        name='title', label='title',
163
        kind='title', user_visible=True, user_editable=True)
164
    simple_user.attributes.title = 'Monsieur'
165

  
166
    utils.login(app, simple_user)
167
    url = reverse('profile_edit')
168
    response = app.get(url, status=200)
169
    assert len(response.pyquery('input[type="radio"][name="edit-profile-title"]')) == 2
170
    assert len(response.pyquery('input[type="radio"][name="edit-profile-title"][readonly="true"]')) == 0
171
    assert len(response.pyquery('select[name="edit-profile-title"]')) == 0
172

  
173
    simple_user.verified_attributes.title = 'Monsieur'
174

  
175
    response = app.get(url, status=200)
176
    assert len(response.pyquery('input[type="radio"][name="edit-profile-title"]')) == 0
177
    assert len(response.pyquery('input[type="text"][name="edit-profile-title@disabled"][readonly]')) == 1
138
-