Projet

Général

Profil

0001-fields-extend-string-field-validation-support-11455.patch

Frédéric Péters, 10 août 2019 16:53

Télécharger (12,8 ko)

Voir les différences:

Subject: [PATCH] fields: extend string field validation support (#11455)

 tests/test_admin_pages.py |  33 ++++++++++++
 tests/test_form_pages.py  |   4 +-
 tests/test_widgets.py     |  51 +++++++++++++++++-
 wcs/fields.py             |  14 ++++-
 wcs/qommon/form.py        | 107 +++++++++++++++++++++++++++++++++++---
 5 files changed, 197 insertions(+), 12 deletions(-)
tests/test_admin_pages.py
1300 1300
    resp = resp.form.submit('submit')
1301 1301
    assert 'syntax error in Django template: Unexpected end of expression' in resp.body
1302 1302

  
1303
def test_form_edit_string_field_validation(pub):
1304
    create_superuser(pub)
1305
    create_role()
1306

  
1307
    FormDef.wipe()
1308
    formdef = FormDef()
1309
    formdef.name = 'form title'
1310
    formdef.fields = [fields.StringField(id='1', label='1st field', type='string')]
1311
    formdef.store()
1312

  
1313
    app = login(get_app(pub))
1314
    resp = app.get('/backoffice/forms/1/')
1315
    resp = resp.click(href='fields/')
1316
    assert '1st field' in resp.body
1317

  
1318
    resp = resp.click('Edit', href='1/')
1319
    resp.form['validation$type'] = 'Regular Expression'
1320
    resp.form['validation$value_regex'] = r'\d+'
1321
    resp = resp.form.submit('submit').follow()
1322
    assert FormDef.get(formdef.id).fields[0].validation == {'type': 'regex', 'value': r'\d+'}
1323

  
1324
    resp = resp.click('Edit', href='1/')
1325
    resp.form['validation$type'] = 'Django Condition'
1326
    resp.form['validation$value_django'] = 'value|decimal < 20'
1327
    resp = resp.form.submit('submit').follow()
1328
    assert FormDef.get(formdef.id).fields[0].validation == {'type': 'django', 'value': 'value|decimal < 20'}
1329

  
1330
    resp = resp.click('Edit', href='1/')
1331
    resp.form['validation$type'] = 'Django Condition'
1332
    resp.form['validation$value_django'] = '{{ value|decimal < 20 }}'
1333
    resp = resp.form.submit('submit')
1334
    assert 'syntax error' in resp.body
1335

  
1303 1336
def test_form_edit_item_field(pub):
1304 1337
    create_superuser(pub)
1305 1338
    create_role()
tests/test_form_pages.py
4076 4076
def test_form_string_regex_field_submit(pub):
4077 4077
    formdef = create_formdef()
4078 4078
    formdef.fields = [fields.StringField(id='0', label='string', type='string',
4079
        validation=r'\d{5}$', required=False)]
4079
        validation={'type': 'regex', 'value': r'\d{5}$'}, required=False)]
4080 4080
    formdef.store()
4081 4081

  
4082 4082
    formdef.data_class().wipe()
......
4112 4112
    resp = get_app(pub).get('/test/')
4113 4113
    resp.forms[0]['f0'] = 'foobar'
4114 4114
    resp = resp.forms[0].submit('submit')
4115
    assert 'wrong format' in resp.body
4115
    assert 'invalid value' in resp.body
4116 4116

  
4117 4117
def test_form_text_field_submit(pub):
4118 4118
    formdef = create_formdef()
tests/test_widgets.py
567 567
    assert not widget.has_error()
568 568
    assert widget.parse() == 'bar'
569 569

  
570
def test_wcsextrastringwidget_regex_validation():
570 571
    # check regex validation
571 572
    class FakeField: pass
572 573
    fakefield = FakeField()
573
    fakefield.validation = r'\d+'
574
    fakefield.validation = {'type': 'regex', 'value': r'\d+'}
574 575

  
575 576
    widget = WcsExtraStringWidget('test', value='foo', required=False)
576 577
    widget.field = fakefield
......
587 588
    mock_form_submission(req, widget, {'test': 'cdab 12'})
588 589
    assert widget.has_error()
589 590

  
590
    fakefield.validation = r'\d+(\.\d{1,2})?'
591
    fakefield.validation = {'type': 'regex', 'value': r'\d+(\.\d{1,2})?'}
591 592
    widget = WcsExtraStringWidget('test', value='foo', required=False)
592 593
    widget.field = fakefield
593 594
    mock_form_submission(req, widget, {'test': '12'})
......
603 604
    mock_form_submission(req, widget, {'test': '12,34'})
604 605
    assert widget.has_error()
605 606

  
607
def test_wcsextrastringwidget_builtin_validation():
608
    class FakeField: pass
609
    fakefield = FakeField()
610

  
611
    fakefield.validation = {'type': 'digits'}
612
    widget = WcsExtraStringWidget('test', value='foo', required=False)
613
    widget.field = fakefield
614
    mock_form_submission(req, widget, {'test': '12'})
615
    assert not widget.has_error()
616

  
617
    widget = WcsExtraStringWidget('test', value='foo', required=False)
618
    widget.field = fakefield
619
    mock_form_submission(req, widget, {'test': 'az'})
620
    assert widget.has_error()
621

  
622
    fakefield.validation = {'type': 'zipcode-fr'}
623
    widget = WcsExtraStringWidget('test', value='foo', required=False)
624
    widget.field = fakefield
625
    mock_form_submission(req, widget, {'test': '12345'})
626
    assert not widget.has_error()
627

  
628
    widget = WcsExtraStringWidget('test', value='foo', required=False)
629
    widget.field = fakefield
630
    mock_form_submission(req, widget, {'test': '1234'})
631
    assert widget.has_error()
632

  
633
def test_wcsextrastringwidget_django_validation():
634
    class FakeField: pass
635
    fakefield = FakeField()
636

  
637
    fakefield.validation = {'type': 'django', 'value': 'value|decimal and value|decimal < 20'}
638
    widget = WcsExtraStringWidget('test', value='foo', required=False)
639
    widget.field = fakefield
640
    mock_form_submission(req, widget, {'test': '12'})
641
    assert not widget.has_error()
642

  
643
    widget = WcsExtraStringWidget('test', value='foo', required=False)
644
    widget.field = fakefield
645
    mock_form_submission(req, widget, {'test': '35'})
646
    assert widget.has_error()
647

  
648
    widget = WcsExtraStringWidget('test', value='foo', required=False)
649
    widget.field = fakefield
650
    mock_form_submission(req, widget, {'test': 'az'})
651
    assert widget.has_error()
652

  
606 653

  
607 654
def test_widgetdict_widget():
608 655
    widget = WidgetDict('test', value={'a': None, 'b': None, 'c': None})
wcs/fields.py
691 691
                     value=self.size)
692 692
        else:
693 693
            form.add(HiddenWidget, 'size', value=None)
694
        form.add(RegexStringWidget, 'validation', title = _('Validation regex'),
695
                value=self.validation, advanced=(not self.validation))
694
        form.add(ValidationWidget, 'validation', title=_('Validation'),
695
                 value=self.validation, advanced=(not self.validation))
696 696
        form.add(data_sources.DataSourceSelectionWidget, 'data_source',
697 697
                 value=self.data_source,
698 698
                 title=_('Data Source'),
......
720 720
            return None
721 721
        return str(value)
722 722

  
723
    def migrate(self):
724
        if isinstance(self.validation, basestring):
725
            self.validation = {'type': 'regex', 'value': self.validation}
726
            return True
727
        return False
728

  
729
    def init_with_xml(self, element, charset, include_id=False):
730
        super(StringField, self).init_with_xml(element, charset, include_id=include_id)
731
        self.migrate()
732

  
723 733
register_field_class(StringField)
724 734

  
725 735

  
wcs/qommon/form.py
892 892
                    pass
893 893

  
894 894

  
895
class ValidationCondition(Condition):
896
    def __init__(self, django_condition, value):
897
        super(ValidationCondition, self).__init__({'type': 'django', 'value': django_condition})
898
        self.evaluated_value = value
899

  
900
    def get_data(self):
901
        return {'value': self.evaluated_value}
902

  
903

  
904
class ValidationWidget(CompositeWidget):
905
    validation_methods = collections.OrderedDict([
906
        ('digits', {'title': N_('Digits'), 'regex': '\d+'}),
907
        ('phone-fr', {'title': N_('Phone Number (France)'), 'regex': '0[\d\.\s]{9}'}),
908
        ('zipcode-fr', {'title': N_('Zip Code (France)'), 'regex': '\d{5}'}),
909
        ('regex', {'title': N_('Regular Expression')}),
910
        ('django', {'title': N_('Django Condition')}),
911
    ])
912

  
913
    def __init__(self, name, value=None, **kwargs):
914
        super(ValidationWidget, self).__init__(name, value=value, **kwargs)
915
        if not value:
916
            value = {}
917

  
918
        options = [('none', _('None'))] + [(x, _(y['title'])) for x, y in self.validation_methods.items()]
919

  
920
        self.add(SingleSelectWidget, 'type', options=options, value=value.get('type'),
921
                 attrs={'data-dynamic-display-parent': 'true'})
922
        self.parse()
923
        if not self.value:
924
            self.value = {}
925

  
926
        validation_labels = collections.OrderedDict(options)
927
        self.add(RegexStringWidget, 'value_regex', size=80,
928
                 value=value.get('value') if value.get('type') == 'regex' else None,
929
                 attrs={'data-dynamic-display-child-of': 'validation$type',
930
                        'data-dynamic-display-value': validation_labels.get('regex')})
931
        self.add(DjangoConditionWidget, 'value_django', size=80,
932
                 value=value.get('value') if value.get('type') == 'django' else None,
933
                 attrs={'data-dynamic-display-child-of': 'validation$type',
934
                        'data-dynamic-display-value': validation_labels.get('django')})
935
        self._parsed = False
936

  
937
    def _parse(self, request):
938
        values = {}
939
        type_ = self.get('type')
940
        if type_:
941
            values['type'] = type_
942
            value = self.get('value_%s' % type_)
943
            if value:
944
                values['value'] = value
945
        self.value = values or None
946

  
947
    def render_content(self):
948
        r = TemplateIO(html=True)
949
        for widget in self.get_widgets():
950
            r += widget.render_error(widget.get_error())
951
        for widget in self.get_widgets():
952
            r += widget.render_content()
953
        return r.getvalue()
954

  
955
    @classmethod
956
    def get_validation_function(cls, validation):
957
        pattern = cls.get_validation_pattern(validation)
958
        if pattern:
959
            def regex_validation(value):
960
                match = re.match(pattern, value)
961
                return bool(match and match.group() == value)
962
            return regex_validation
963
        if validation['type'] == 'django':
964
            def django_validation(value):
965
                condition = ValidationCondition(validation['value'], value=value)
966
                return condition.evaluate()
967
            return django_validation
968

  
969
    @classmethod
970
    def get_validation_pattern(cls, validation):
971
        validation_method = cls.validation_methods.get(validation['type'])
972
        if validation_method and validation_method.get('regex'):
973
            return validation_method.get('regex')
974
        if validation['type'] == 'regex':
975
            return validation['value']
976
        return None
977

  
978

  
895 979
class WcsExtraStringWidget(StringWidget):
896 980
    field = None
897 981
    prefill = False
......
903 987

  
904 988
    def render_content(self):
905 989
        if self.field and self.field.validation and not 'pattern' in self.attrs:
906
            self.attrs['pattern'] = self.field.validation
907
        s = StringWidget.render_content(self)
908
        return s
990
            pattern = ValidationWidget.get_validation_pattern(self.field.validation)
991
            if pattern:
992
                self.attrs['pattern'] = pattern
993
        return super(WcsExtraStringWidget, self).render_content()
909 994

  
910 995
    def _parse(self, request):
911 996
        StringWidget._parse(self, request)
912 997
        if self.field and self.field.validation and self.value is not None:
913
            match = re.match(self.field.validation, self.value)
914
            if not match or match.group() != self.value:
915
                self.error = _('wrong format')
998
            validation_function = ValidationWidget.get_validation_function(self.field.validation)
999
            if not validation_function(self.value):
1000
                self.error = _('invalid value')
916 1001

  
917 1002

  
918 1003
class DateWidget(StringWidget):
......
2348 2433
                'value_python': self.get_widget('value_python').render_content(),
2349 2434
                'type': self.get_widget('type').render_content()
2350 2435
            }
2436

  
2437

  
2438
class DjangoConditionWidget(StringWidget):
2439
    def _parse(self, request):
2440
        super(DjangoConditionWidget, self)._parse(request)
2441
        if self.value:
2442
            try:
2443
                Condition({'type': 'django', 'value': self.value}).validate()
2444
            except ValidationError as e:
2445
                self.set_error(str(e))
2351
-