From a5368d0d1e7c75787dc8c87d65c098a7eebdd5d9 Mon Sep 17 00:00:00 2001 From: Kiran Jonnalagadda Date: Fri, 21 Jun 2019 16:44:52 +0530 Subject: [PATCH] Filter and validator for handling lists --- baseframe/forms/filters.py | 16 +++++++++++ baseframe/forms/validators.py | 20 ++++++++++++-- setup.cfg | 1 + tests/test_filters.py | 19 +++++++++++++ tests/test_validators.py | 50 ++++++++++++++++++++++++++++++++++- 5 files changed, 103 insertions(+), 3 deletions(-) diff --git a/baseframe/forms/filters.py b/baseframe/forms/filters.py index fb5b2a11..74e43656 100644 --- a/baseframe/forms/filters.py +++ b/baseframe/forms/filters.py @@ -20,6 +20,8 @@ from coaster.utils import unicode_extended_whitespace +__all__ = ['lower', 'upper', 'strip', 'lstrip', 'rstrip', 'strip_each', 'none_if_empty'] + def lower(): """ @@ -72,6 +74,20 @@ def rstrip_inner(value): return rstrip_inner +def strip_each(chars=unicode_extended_whitespace): + """ + Strip whitespace and remove blank elements from each element in an iterable. + Falsy values are returned unprocessed + + :param chars: If specified, strip these characters instead of whitespace + """ + def strip_each_inner(value): + if value: + return [sline for sline in [line.strip(chars) for line in value] if sline] + return value + return strip_each_inner + + def none_if_empty(): """ If data is empty or evalues to boolean false, replace with None diff --git a/baseframe/forms/validators.py b/baseframe/forms/validators.py index c623274e..44e4bcfb 100644 --- a/baseframe/forms/validators.py +++ b/baseframe/forms/validators.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +from collections import namedtuple from decimal import Decimal from fractions import Fraction import datetime @@ -22,7 +23,7 @@ __local = ['AllUrlsValid', 'IsNotPublicEmailDomain', 'IsPublicEmailDomain', 'NoObfuscatedEmail', 'AllowedIf', 'OptionalIf', 'RequiredIf', 'ValidCoordinates', 'ValidEmail', - 'ValidEmailDomain', 'ValidName', 'ValidUrl'] + 'ValidEmailDomain', 'ValidName', 'ValidUrl', 'ForEach'] __imported = [ # WTForms validators 'DataRequired', 'EqualTo', 'InputRequired', 'Length', 'NumberRange', 'Optional', 'StopValidation', 'URL', 'ValidationError'] @@ -31,7 +32,6 @@ EMAIL_RE = re.compile(r'\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,63}\b', re.I) - _zero_values = (0, 0.0, Decimal('0'), 0j, Fraction(0, 1), datetime.time(0, 0, 0)) @@ -53,6 +53,22 @@ def is_empty(value): return value not in _zero_values and not value +FakeField = namedtuple('FakeField', ['data', 'gettext', 'ngettext']) + + +class ForEach(object): + """ + Runs specified validators on each element of an iterable value + """ + def __init__(self, validators): + self.validators = validators + + def __call__(self, form, field): + for v in self.validators: + for element in field.data: + v(form, FakeField(element, field.gettext, field.ngettext)) + + class AllowedIf(object): """ Validator that allows a value only if another field also has a value. diff --git a/setup.cfg b/setup.cfg index 3bedb566..f3f4a8aa 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,3 @@ [flake8] ignore = I100, I201, E501, E128, E124, E402, W503 +hang-closing = True diff --git a/tests/test_filters.py b/tests/test_filters.py index 4f8432da..96575910 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -192,6 +192,25 @@ def test_rstrip(self): self.assertEqual(rstrip_func('a test '), 'a test') self.assertEqual(rstrip_func(' '), '') + def test_strip_each(self): + strip_each_func = forms.strip_each() + self.assertEqual(strip_each_func(None), None) + self.assertEqual(strip_each_func([]), []) + self.assertEqual(strip_each_func( + [ + ' Left strip', + 'Right strip ', + ' Full strip ', + '', + 'No strip', + '' + ]), [ + 'Left strip', + 'Right strip', + 'Full strip', + 'No strip', + ]) + def test_none_if_empty(self): none_if_empty_func = forms.none_if_empty() self.assertEqual(none_if_empty_func('Test'), 'Test') diff --git a/tests/test_validators.py b/tests/test_validators.py index 9aebc676..36c00997 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -2,9 +2,10 @@ import warnings import urllib3 +from werkzeug.datastructures import MultiDict +from mxsniff import MXLookupException from baseframe.utils import is_public_email_domain from baseframe import forms -from mxsniff import MXLookupException from .fixtures import (TestCaseBaseframe, UrlFormTest, AllUrlsFormTest, PublicEmailDomainFormTest) @@ -142,6 +143,53 @@ def tearDown(self): self.ctx.pop() +class TestForEach(TestFormBase): + class Form(forms.Form): + textlist = forms.TextListField( + validators=[forms.ForEach([forms.URL()])] + ) + + def test_passes_single(self): + self.form.process(formdata=MultiDict({'textlist': "http://www.example.com/"})) + assert self.form.validate() is True + + def test_passes_list(self): + self.form.process(formdata=MultiDict({'textlist': "http://www.example.com\r\nhttp://www.example.org/"})) + assert self.form.validate() is True + + def test_fails_single(self): + self.form.process(formdata=MultiDict({'textlist': "example"})) + assert self.form.validate() is False + + def test_fails_list(self): + self.form.process(formdata=MultiDict({'textlist': "www.example.com\r\nwww.example.org"})) + assert self.form.validate() is False + + def test_fails_mixed1(self): + self.form.process(formdata=MultiDict({'textlist': "http://www.example.com/\r\nwww.example.org"})) + assert self.form.validate() is False + + def test_fails_mixed2(self): + self.form.process(formdata=MultiDict({'textlist': "www.example.com\r\nhttp://www.example.org/"})) + assert self.form.validate() is False + + def test_fails_blanklines(self): + self.form.process(formdata=MultiDict({'textlist': "http://www.example.com\r\n"})) + assert self.form.validate() is False + + +class TestForEachFiltered(TestFormBase): + class Form(forms.Form): + textlist = forms.TextListField( + validators=[forms.ForEach([forms.URL()])], + filters=[forms.strip_each()] + ) + + def test_passes_blanklines(self): + self.form.process(formdata=MultiDict({'textlist': "http://www.example.com\r\n"})) + assert self.form.validate() is True + + class TestAllowedIf(TestFormBase): class Form(forms.Form): other = forms.StringField("Other")