Projet

Général

Profil

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

Benjamin Dauvergne, 19 juin 2019 00:00

Télécharger (9,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  | 63 +++++++++++++++++++++++++++++++++
 src/authentic2/forms/profile.py | 36 ++++++++++---------
 tests/test_profile.py           | 41 ++++++++++++++-------
 3 files changed, 111 insertions(+), 29 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

  
21

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

  
27
    def __lock_fields(self):
28
        # Locked fields are modified to use a read-only TextInput
29
        # widget remapped to a name which will be ignored by Form
30
        # implementation
31
        locked_fields = {}
32
        for name in self.fields:
33
            if not self.is_field_locked(name):
34
                continue
35
            field = self.fields[name]
36
            initial = self.initial[name]
37
            try:
38
                choices = field.choices
39
            except AttributeError:
40
                initial = field.widget.format_value(initial)
41
            else:
42
                for key, label in choices:
43
                    if initial == key:
44
                        initial = label
45
                        break
46
            locked_fields[name] = forms.CharField(
47
                label=field.label,
48
                help_text=field.help_text,
49
                initial=initial,
50
                widget=forms.TextInput(attrs={'readonly': ''}))
51
        if not locked_fields:
52
            return
53

  
54
        new_fields = OrderedDict()
55
        for name in self.fields:
56
            if name in locked_fields:
57
                new_fields[name + '@disabled'] = locked_fields[name]
58
            else:
59
                new_fields[name] = self.fields[name]
60
        self.fields = new_fields
61

  
62
    def is_field_locked(self, name):
63
        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
35 35

  
36 36
    resp = app.get(url, status=200)
37 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)
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
    }, status=302)
44 43
    # verify that missing next_url in POST is ok
45 44
    assert resp['Location'].endswith(reverse('account_management'))
46 45
    assert attribute.get_value(simple_user) == '1234'
......
57 56

  
58 57
    attribute.set_value(simple_user, '0123456789', verified=True)
59 58
    resp = app.get(url, status=200)
60
    resp.form.set('edit-profile-phone', '1234567890')
61
    assert 'readonly' in resp.form['edit-profile-phone'].attrs
59
    assert 'edit-profile-phone' not in resp.form.fields
60
    assert 'readonly' in resp.form['edit-profile-phone@disabled'].attrs
62 61
    resp = resp.form.submit().follow()
63 62
    assert attribute.get_value(simple_user) == '0123456789'
64 63

  
65
    resp = app.get(url, status=200)
66
    assert 'phone' in resp
67
    assert 'readonly' in resp.form['edit-profile-phone'].attrs
68

  
69 64
    attribute.disabled = True
70 65
    attribute.save()
71 66
    resp = app.get(url, status=200)
72
    assert 'phone' not in resp
67
    assert 'edit-profile-phone@disabled' not in resp
73 68
    assert attribute.get_value(simple_user) == '0123456789'
74 69

  
75 70

  
......
135 130
    resp = app.get(reverse('profile_edit_with_scope', kwargs={'scope': 'address'}),
136 131
                   status=200)
137 132
    assert get_fields(resp) == set(['city', 'zipcode', 'next_url'])
133

  
134

  
135
def test_account_edit_locked_title(app, simple_user):
136
    Attribute.objects.create(
137
        name='title', label='title',
138
        kind='title', user_visible=True, user_editable=True)
139
    simple_user.attributes.title = 'Monsieur'
140

  
141
    utils.login(app, simple_user)
142
    url = reverse('profile_edit')
143
    response = app.get(url, status=200)
144
    assert len(response.pyquery('input[type="radio"][name="edit-profile-title"]')) == 2
145
    assert len(response.pyquery('input[type="radio"][name="edit-profile-title"][readonly="true"]')) == 0
146
    assert len(response.pyquery('select[name="edit-profile-title"]')) == 0
147

  
148
    simple_user.verified_attributes.title = 'Monsieur'
149

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