Created
November 30, 2012 09:55
-
-
Save wichert/4174867 to your computer and use it in GitHub Desktop.
WTForms and pyramid integration
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<form id="loginForm" method="post" action="${request.route_url('login')}"> | |
<input type="hidden" name="csrf_token" value="${request.session.get_csrf_token()}"/> | |
<fieldset class="concise"> | |
<metal:field tal:define="name 'came_from'" use-macro="snippets['hidden']"/> | |
<metal:field tal:define="name 'login'" use-macro="snippets['text']"/> | |
<metal:field tal:define="name 'password'" use-macro="snippets['password']"/> | |
</fieldset> | |
<div class="buttonBar"> | |
<button type="submit" class="default" i18n:translate="">Login</button> | |
</div> | |
</form> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html xmlns="http://www.w3.org/1999/xhtml" | |
xmlns:i18n="http://xml.zope.org/namespaces/i18n" | |
xmlns:metal="http://xml.zope.org/namespaces/metal" | |
xmlns:tal="http://xml.zope.org/namespaces/tal" | |
tal:condition="False"> | |
<metal:macro define-macro="hidden"> | |
<input tal:define="field form[name]" type="hidden" name="${field.name}" value="${field._value()}"/> | |
</metal:macro> | |
<metal:macro define-macro="text"> | |
<label tal:define="field form[name]">${field.label.text} <sup tal:condition="field.flags.required" class="required">*</sup> | |
<input type="text" required="${'required' if field.flags.required else None}" name="${field.name}" value="${field._value()}"/> | |
<em tal:repeat="msg field.errors" class="message error">${msg}</em> | |
</label> | |
</metal:macro> | |
<metal:macro define-macro="date"> | |
<label tal:define="field form[name]">${field.label.text} <sup tal:condition="field.flags.required" class="required">*</sup> | |
<input type="date" min="2000-01-01" required="${'required' if field.flags.required else None}" name="${field.name}" value="${field._value()}"/> | |
<em tal:repeat="msg field.errors" class="message error">${msg}</em> | |
</label> | |
</metal:macro> | |
<metal:macro define-macro="email"> | |
<label tal:define="field form[name]">${field.label.text} <sup tal:condition="field.flags.required" class="required">*</sup> | |
<input type="email" required="${'required' if field.flags.required else None}" name="${field.name}" value="${field._value()}"/> | |
<em tal:repeat="msg field.errors" class="message error">${msg}</em> | |
</label> | |
</metal:macro> | |
<metal:macro define-macro="url"> | |
<label tal:define="field form[name]">${field.label.text} <sup tal:condition="field.flags.required" class="required">*</sup> | |
<input type="url" required="${'required' if field.flags.required else None}" name="${field.name}" value="${field._value()}" class="span-9"/> | |
<em tal:repeat="msg field.errors" class="message error">${msg}</em> | |
</label> | |
</metal:macro> | |
<metal:macro define-macro="password"> | |
<label tal:define="field form[name]">${field.label.text} <sup tal:condition="field.flags.required" class="required">*</sup> | |
<input type="password" required="${'required' if field.flags.required else None}" name="${field.name}"/> | |
<em tal:repeat="msg field.errors" class="message error">${msg}</em> | |
</label> | |
</metal:macro> | |
<metal:macro define-macro="file"> | |
<label tal:define="field form[name]">${field.label.text} <sup tal:condition="field.flags.required" class="required">*</sup> | |
<input type="file" required="${'required' if field.flags.required else None}" name="${field.name}" /> | |
<em tal:repeat="msg field.errors" class="message error">${msg}</em> | |
</label> | |
</metal:macro> | |
<metal:macro define-macro="number"> | |
<label tal:define="field form[name]">${field.label.text} <sup tal:condition="field.flags.required" class="required">*</sup> | |
<input type="number" required="${'required' if field.flags.required else None}" name="${field.name}" value="${field._value()}" min="0" /> | |
<em tal:repeat="msg field.errors" class="message error">${msg}</em> | |
</label> | |
</metal:macro> | |
<metal:macro define-macro="radioList"> | |
<fieldset class="comprehensive radioList" tal:define="field form[name]"> | |
<legend>${field.label.text} <sup tal:condition="field.flags.required" class="required">*</sup></legend> | |
<label tal:repeat="option field.iter_choices()"> | |
<input type="radio" name="${field.name}" value="${option[0]}" | |
required="${'required' if not field.flags.required else None}" checked="${'checked' if option[2] else None}"/> ${option[1]}</label> | |
<em tal:repeat="msg field.errors" class="message error">${msg}</em> | |
</fieldset> | |
</metal:macro> | |
<metal:macro define-macro="checkList"> | |
<fieldset class="comprehensive checkList" tal:define="field form[name]" > | |
<legend>${field.label.text} <sup tal:condition="field.flags.required" class="required">*</sup></legend> | |
<label tal:repeat="option field.iter_choices()"> | |
<input type="checkbox" name="${field.name}" value="${option[0]}" | |
checked="${'checked' if option[2] else None}"/> ${option[1]}</label> | |
<em tal:repeat="msg field.errors" class="message error">${msg}</em> | |
</fieldset> | |
</metal:macro> | |
<metal:macro define-macro="select"> | |
<label tal:define="field form[name]">${field.label.text} <sup tal:condition="field.flags.required" class="required">*</sup> | |
<select name="${field.name}" required="${'request' if not field.flags.required else None}"> | |
<option tal:repeat="option field.iter_choices()" value="${option[0]}" | |
selected="${'selected' if option[2] else None}">${option[1]}</option> | |
</select> | |
<em tal:repeat="msg field.errors" class="message error">${msg}</em> | |
</label> | |
</metal:macro> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import wtforms | |
import unittest | |
import pyramid.testing | |
from s4u.bfgtools.wtform import BasicFormView | |
class BooleanSelectFieldTests(unittest.TestCase): | |
def BooleanSelectField(self, *a, **kw): | |
from s4u.bfgtools.wtform import BooleanSelectField | |
return BooleanSelectField(*a, **kw) | |
def test_init_default_choices(self): | |
from s4u.bfgtools.wtform import BooleanSelectField | |
field = BooleanSelectField(_form=None, _name='field') | |
self.assertTrue(field.choices is BooleanSelectField.choices) | |
def test_init_custom_choices(self): | |
from s4u.bfgtools.wtform import BooleanSelectField | |
field = BooleanSelectField(_form=None, _name='field', choices='choices') | |
self.assertTrue(field.choices is not BooleanSelectField.choices) | |
self.assertEqual(field.choices, 'choices') | |
def test_iter_choices(self): | |
field = self.BooleanSelectField(_form=None, _name='field') | |
field.data = True | |
self.assertEqual(field.iter_choices(), | |
[('false', 'No', False), ('true', 'Yes', True)]) | |
def test_process_data_None(self): | |
field = self.BooleanSelectField(_form=None, _name='field') | |
field.process_data(None) | |
self.assertEqual(field.data, None) | |
def test_process_data_coerce_to_boolean(self): | |
field = self.BooleanSelectField(_form=None, _name='field') | |
field.process_data([]) | |
self.assertEqual(field.data, False) | |
field.process_data('value') | |
self.assertEqual(field.data, True) | |
def test_to_python(self): | |
field = self.BooleanSelectField(_form=None, _name='field') | |
self.assertEqual(field._to_python('TrUe'), True) | |
self.assertEqual(field._to_python('yes'), True) | |
self.assertEqual(field._to_python('false'), False) | |
self.assertEqual(field._to_python('NO'), False) | |
def test_process_formdata_no_value(self): | |
field = self.BooleanSelectField(_form=None, _name='field') | |
field.data = 'dummy' | |
field.process_formdata([]) | |
self.assertEqual(field.data, None) | |
def test_process_formdata_proper_data(self): | |
field = self.BooleanSelectField(_form=None, _name='field') | |
field.data = 'dummy' | |
field.process_formdata(['false']) | |
self.assertEqual(field.data, False) | |
def test_value_no_value(self): | |
field = self.BooleanSelectField(_form=None, _name='field') | |
field.raw_data = None | |
self.assertEqual(field._value(), None) | |
def test_value_boolean_data(self): | |
field = self.BooleanSelectField(_form=None, _name='field') | |
field.raw_data = ['yes'] | |
self.assertEqual(field._value(), 'true') | |
class DatePartFieldTests(unittest.TestCase): | |
def DatePartField(self, *a, **kw): | |
from s4u.bfgtools.wtform import DatePartField | |
return DatePartField(*a, **kw) | |
def test_process_data_no_date(self): | |
field = self.DatePartField(_form=None, _name='date') | |
field.process_data('dummy') | |
self.assertEqual(field.year, None) | |
self.assertEqual(field.month, None) | |
self.assertEqual(field.day, None) | |
def test_process_data_date_value(self): | |
import datetime | |
field = self.DatePartField(_form=None, _name='date') | |
field.process_data(datetime.date(2012, 2, 3)) | |
self.assertEqual(field.year, 2012) | |
self.assertEqual(field.month, 2) | |
self.assertEqual(field.day, 3) | |
def test_process_default_value(self): | |
import datetime | |
field = self.DatePartField(_form=None, _name='date', | |
default=datetime.date(2012, 2, 3)) | |
field.process(None) | |
self.assertEqual(field.year, 2012) | |
def test_process_default_function(self): | |
import datetime | |
field = self.DatePartField(_form=None, _name='date', | |
default=lambda: datetime.date(2012, 2, 3)) | |
field.process(None) | |
self.assertEqual(field.year, 2012) | |
def test_process_custom_data(self): | |
import datetime | |
field = self.DatePartField(_form=None, _name='date') | |
field.process(None, datetime.date(2012, 2, 3)) | |
self.assertEqual(field.data, datetime.date(2012, 2, 3)) | |
self.assertEqual(field.year, 2012) | |
def test_process_valid_date(self): | |
import datetime | |
import mock | |
formdata = mock.Mock() | |
formdata.getlist.return_value = ['10'] | |
field = self.DatePartField(_form=None, _name='date') | |
field.process(formdata) | |
self.assertEqual(field.raw_data['year'], '10') | |
self.assertEqual(field.raw_data['month'], '10') | |
self.assertEqual(field.raw_data['day'], '10') | |
self.assertEqual(field.data, datetime.date(10, 10, 10)) | |
def test_process_no_form_data(self): | |
field = self.DatePartField(_form=None, _name='date') | |
field.process({}) | |
self.assertEqual(field.raw_data, {}) | |
self.assertEqual(field.data, None) | |
self.assertEqual(field.process_errors, []) | |
def test_process_missing_day(self): | |
import mock | |
field = self.DatePartField(_form=None, _name='date') | |
formdata = mock.Mock() | |
def getlist(key): | |
return ['1975'] if key.endswith('year') else [] | |
formdata.getlist = getlist | |
field.process(formdata) | |
self.assertEqual(field.data, None) | |
self.assertEqual(len(field.process_errors), 1) | |
def test_process_invalid_data(self): | |
import mock | |
formdata = mock.Mock() | |
formdata.getlist.return_value = ['X'] | |
field = self.DatePartField(_form=None, _name='date') | |
field.process(formdata) | |
self.assertTrue(field.process_errors) | |
self.assertEqual(field.data, None) | |
self.assertEqual(field.raw_data['year'], 'X') | |
self.assertEqual(field.raw_data['month'], 'X') | |
self.assertEqual(field.raw_data['day'], 'X') | |
def test_process_bad_date(self): | |
import mock | |
formdata = mock.Mock() | |
formdata.getlist.return_value = ['13'] | |
field = self.DatePartField(_form=None, _name='date') | |
field.process(formdata) | |
self.assertTrue(field.process_errors) | |
self.assertEqual(field.data, None) | |
self.assertEqual(field.raw_data['year'], '13') | |
self.assertEqual(field.raw_data['month'], '13') | |
self.assertEqual(field.raw_data['day'], '13') | |
def test_process_filters(self): | |
import datetime | |
import mock | |
formdata = mock.Mock() | |
flt = mock.Mock(return_value='filtered') | |
formdata.getlist.return_value = ['10'] | |
field = self.DatePartField(_form=None, _name='date', filters=[flt]) | |
field.process(formdata) | |
flt.assert_called_with(datetime.date(10, 10, 10)) | |
self.assertEqual(field.data, 'filtered') | |
def test_process_filter_errors(self): | |
import datetime | |
import mock | |
formdata = mock.Mock() | |
flt1 = mock.Mock(return_value='filtered') | |
flt2 = mock.Mock(side_effect=ValueError('Ooops')) | |
formdata.getlist.return_value = ['10'] | |
field = self.DatePartField(_form=None, _name='date', filters=[flt1, flt2]) | |
field.process(formdata) | |
flt1.assert_called_with(datetime.date(10, 10, 10)) | |
flt2.assert_called_with('filtered') | |
self.assertEqual(field.data, 'filtered') | |
self.assertTrue(field.process_errors) | |
class RequiredTests(unittest.TestCase): | |
def Required(self, *a, **kw): | |
from s4u.bfgtools.wtform import Required | |
return Required(*a, **kw) | |
def test_default_message(self): | |
validator = self.Required() | |
self.assertEqual(validator.message, u'This field is required.') | |
def test_custom_message(self): | |
validator = self.Required(u'Other message') | |
self.assertEqual(validator.message, u'Other message') | |
def test_missing_value(self): | |
import mock | |
from wtforms.validators import StopValidation | |
field = mock.Mock() | |
field.errors = [] | |
field.data = None | |
validator = self.Required() | |
self.assertRaises(StopValidation, | |
validator.__call__, None, field) | |
def test_plain_value(self): | |
import mock | |
field = mock.Mock() | |
field.data = 'value' | |
validator = self.Required() | |
validator(None, field) | |
def test_fieldstorage(self): | |
import mock | |
import cgi | |
field = mock.Mock() | |
field.data = cgi.FieldStorage() | |
validator = self.Required() | |
validator(None, field) | |
class NumberRangeTests(unittest.TestCase): | |
def NumberRange(self, *a, **kw): | |
from s4u.bfgtools.wtform import NumberRange | |
return NumberRange(*a, **kw) | |
def test_custom_lt_message(self): | |
import mock | |
from wtforms import ValidationError | |
validator = self.NumberRange(min=10, msg_less_than='My message') | |
field = mock.Mock() | |
field.data = 5 | |
self.assertRaises(ValidationError, | |
validator.__call__, None, field) | |
field.gettext.assert_called_with('My message') | |
def test_custom_gt_message(self): | |
import mock | |
from wtforms import ValidationError | |
validator = self.NumberRange(max=10, msg_greater_than='My message') | |
field = mock.Mock() | |
field.data = 15 | |
self.assertRaises(ValidationError, | |
validator.__call__, None, field) | |
field.gettext.assert_called_with('My message') | |
def test_custom_between_message(self): | |
import mock | |
from wtforms import ValidationError | |
validator = self.NumberRange(min=5, max=10, | |
msg_between='My message') | |
field = mock.Mock() | |
field.data = 15 | |
self.assertRaises(ValidationError, | |
validator.__call__, None, field) | |
field.gettext.assert_called_with('My message') | |
def test_ok(self): | |
import mock | |
validator = self.NumberRange(min=5, max=10, | |
msg_between='My message') | |
field = mock.Mock() | |
field.data = 8 | |
validator(None, field) | |
class IsImageTests(unittest.TestCase): | |
def IsImage(self, *a, **kw): | |
from s4u.bfgtools.wtform import IsImage | |
return IsImage(*a, **kw) | |
def test_validate_image_bad_image(self): | |
from StringIO import StringIO | |
validator = self.IsImage() | |
self.assertTrue(not validator.validate_image(StringIO('dummy'))) | |
def test_validate_image_png(self): | |
from StringIO import StringIO | |
from s4u.image.testing import PNG | |
validator = self.IsImage() | |
self.assertTrue(validator.validate_image(StringIO(PNG))) | |
def test_invalid_format(self): | |
from StringIO import StringIO | |
import mock | |
validator = self.IsImage() | |
with mock.patch('PIL.Image.open') as mock_open: | |
image = mock.Mock() | |
image.format = 'BMP' | |
mock_open.return_value = image | |
self.assertTrue(not validator.validate_image(StringIO('dummy'))) | |
def test_call_no_image_data(self): | |
import mock | |
field = mock.Mock() | |
field.data = None | |
validator = self.IsImage() | |
validator(None, field) | |
def test_call_bad_image(self): | |
import cgi | |
import mock | |
from wtforms import ValidationError | |
validator = self.IsImage() | |
validator.validate_image = mock.Mock(return_value=False) | |
field = mock.Mock() | |
field.data = cgi.FieldStorage() | |
self.assertRaises(ValidationError, | |
validator.__call__, None, field) | |
def test_call_valid_image(self): | |
import cgi | |
import mock | |
validator = self.IsImage() | |
validator.validate_image = mock.Mock(return_value=True) | |
field = mock.Mock() | |
field.data = cgi.FieldStorage() | |
validator(None, field) | |
class URLTests(unittest.TestCase): | |
def URL(self, *a, **kw): | |
from s4u.bfgtools.wtform import URL | |
return URL(*a, **kw) | |
def test_valid_url(self): | |
import mock | |
field = mock.Mock() | |
field.data = u'http://example.com/' | |
validator = self.URL() | |
validator(None, field) | |
def test_bad_url(self): | |
import mock | |
from wtforms import ValidationError | |
field = mock.Mock() | |
field.data = u'invalid' | |
validator = self.URL() | |
self.assertRaises(ValidationError, validator.__call__, None, field) | |
def test_custom_error_message(self): | |
import mock | |
from wtforms import ValidationError | |
field = mock.Mock() | |
field.data = u'invalid' | |
validator = self.URL(message='other message') | |
try: | |
validator(None, field) | |
except ValidationError as e: | |
self.assertEqual(e.message, 'other message') | |
def test_restrict_schema_invalid_schema(self): | |
import mock | |
from wtforms import ValidationError | |
field = mock.Mock() | |
field.data = u'ftp://example.com' | |
validator = self.URL(['http']) | |
self.assertRaises(ValidationError, validator.__call__, None, field) | |
def test_restrict_schema_allowed_schema(self): | |
import mock | |
field = mock.Mock() | |
field.data = u'http://example.com' | |
validator = self.URL(['http']) | |
validator(None, field) | |
def test_custom_scheme_error_message(self): | |
import mock | |
from wtforms import ValidationError | |
field = mock.Mock() | |
field.data = u'ftp://example.com' | |
validator = self.URL(['http'], scheme_message='other message') | |
try: | |
validator(None, field) | |
except ValidationError as e: | |
self.assertEqual(e.message, 'other message') | |
class EmailTests(unittest.TestCase): | |
def Email(self, *a, **kw): | |
from s4u.bfgtools.wtform import Email | |
return Email(*a, **kw) | |
def test_default_message(self): | |
import mock | |
from wtforms import ValidationError | |
field = mock.Mock() | |
field.data = u'invalid' | |
validator = self.Email() | |
try: | |
validator(None, field) | |
except ValidationError as e: | |
self.assertTrue(e.message is validator._message) | |
def test_custom_message(self): | |
import mock | |
from wtforms import ValidationError | |
field = mock.Mock() | |
field.data = u'invalid' | |
validator = self.Email(message='other message') | |
try: | |
validator(None, field) | |
except ValidationError as e: | |
self.assertEqual(e.message, 'other message') | |
class PyramidTranslationsTests(unittest.TestCase): | |
def PyramidTranslations(self, *a, **kw): | |
from s4u.bfgtools.wtform import PyramidTranslations | |
return PyramidTranslations(*a, **kw) | |
def test_interface(self): | |
translations = self.PyramidTranslations(pyramid.testing.DummyRequest()) | |
self.assertTrue(callable(translations.gettext)) | |
msg = u'foo' # Indirection to fool lingua extraction | |
self.assertEqual(translations.gettext(msg), u'foo') | |
self.assertTrue(callable(translations.ngettext)) | |
class FormTests(unittest.TestCase): | |
def Form(self, *a, **kw): | |
from s4u.bfgtools.wtform import Form | |
return Form(*a, **kw) | |
def test_generate_csrf_token(self): | |
import mock | |
with mock.patch('pyramid.testing.DummySession.get_csrf_token') \ | |
as mock_get: | |
form = self.Form(request=pyramid.testing.DummyRequest()) | |
mock_get.return_value = 'dummy token' | |
self.assertEqual( | |
form.generate_csrf_token('context'), | |
'dummy token') | |
self.assertTrue(mock_get.called) | |
def test_validate_csrf_token_valid_token(self): | |
import mock | |
field = mock.Mock() | |
field.data = 'one' | |
field.current_token = 'one' | |
form = self.Form(request=pyramid.testing.DummyRequest()) | |
form.validate_csrf_token(field) | |
def test_validate_csrf_token_invalid_token(self): | |
import mock | |
field = mock.Mock() | |
field.data = 'one' | |
field.current_token = 'two' | |
form = self.Form(request=pyramid.testing.DummyRequest()) | |
self.assertRaises(ValueError, form.validate_csrf_token, field) | |
class BasicFormViewTests(unittest.TestCase): | |
def test_init_plain_request_uses_default_values(self): | |
class TestView(MockView): | |
def default_data(self): | |
return {'foo': 1} | |
view = TestView(None, pyramid.testing.DummyRequest()) | |
self.assertEqual(view.form['foo'].data, 1) | |
def test_init_post_request_uses_request_data(self): | |
from webob.multidict import MultiDict | |
request = pyramid.testing.DummyRequest(post=MultiDict({'foo': '2'})) | |
view = MockView(None, request) | |
self.assertEqual(view.form['foo'].data, 2) | |
def test_default_data_copy_from_context(self): | |
context = pyramid.testing.DummyModel(foo=3) | |
view = MockView(context, pyramid.testing.DummyRequest()) | |
self.assertEqual(view.form['foo'].data, 3) | |
def test_default_data_not_copied_from_context_if_requested(self): | |
context = pyramid.testing.DummyModel(foo=3) | |
class MyView(MockView): | |
defaults_from_context = False | |
view = MyView(context, pyramid.testing.DummyRequest()) | |
self.assertEqual(view.form['foo'].data, None) | |
def test_do_post_not_implemented(self): | |
self.assertRaises(NotImplementedError, | |
MockView(None, pyramid.testing.DummyRequest()).do_post) | |
def test_call_failed_post(self): | |
import mock | |
from webob.multidict import MultiDict | |
class TestView(MockView): | |
def do_post(self): | |
return None | |
view = TestView(None, pyramid.testing.DummyRequest( | |
post=MultiDict())) | |
view.form = mock.Mock() | |
view.form.validate.return_value = True | |
response = view() | |
self.assertTrue(isinstance(response, dict)) | |
self.assertEqual(response.keys(), ['form']) | |
def test_call_return_do_post_response_if_not_None(self): | |
from webob.multidict import MultiDict | |
marker = [] | |
class TestView(MockView): | |
def do_post(self): | |
return marker | |
request = pyramid.testing.DummyRequest(post=MultiDict({'foo': '2'})) | |
view = TestView(None, request) | |
self.assertTrue(view() is marker) | |
def test_call_get_request(self): | |
view = MockView(None, pyramid.testing.DummyRequest()) | |
response = view() | |
self.assertTrue(isinstance(response, dict)) | |
self.assertEqual(response.keys(), ['form']) | |
def test_call_include_template_data(self): | |
view = MockView(None, pyramid.testing.DummyRequest()) | |
view.template_data = lambda: {'foo': 'bar'} | |
response = view() | |
self.assertTrue(isinstance(response, dict)) | |
self.assertEqual(set(response.keys()), set(['form', 'foo'])) | |
self.assertEqual(response['foo'], 'bar') | |
class TestForm(wtforms.Form): | |
foo = wtforms.IntegerField() | |
class MockView(BasicFormView): | |
form_class = TestForm |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import cgi | |
import datetime | |
import urlparse | |
try: | |
from wtforms.ext.csrf import SecureForm | |
except ImportError: # pragma: no cover | |
# BBB for WTForms <0.6.4 which does not support CSRF | |
from wtforms import Form as SecureForm | |
from wtforms.fields import _unset_value | |
from wtforms.fields import Field | |
import wtforms.validators | |
from wtforms import ValidationError | |
import PIL.Image | |
from pyramid.i18n import get_localizer | |
from s4u.bfgtools import _ | |
class BooleanSelectField(Field): | |
choices = [(False, _(u'No')), (True, _(u'Yes'))] | |
def __init__(self, label=None, validators=None, choices=None, **kwargs): | |
super(BooleanSelectField, self).__init__(label, validators, **kwargs) | |
if choices: | |
self.choices = choices | |
def iter_choices(self): | |
return [('true' if choice[0] else 'false', | |
choice[1], | |
self.data == choice[0]) | |
for choice in self.choices] | |
def process_data(self, value): | |
self.data = bool(value) if value is not None else None | |
def _to_python(self, value): | |
return value.lower() in ['yes', 'true'] | |
def process_formdata(self, valuelist): | |
if not valuelist: | |
self.data = None | |
else: | |
self.data = self._to_python(valuelist[0]) | |
def _value(self): | |
if not self.raw_data: | |
return None | |
else: | |
return 'true' if self._to_python(self.raw_data[0]) else 'false' | |
class DatePartField(wtforms.DateField): | |
"""Custom date field which uses separate fields (with identical name) | |
for day, month and year. | |
""" | |
day = None | |
month = None | |
year = None | |
def process_data(self, value): | |
if not isinstance(value, datetime.date): | |
return | |
self.data = value | |
self.day = value.day | |
self.month = value.month | |
self.year = value.year | |
def process(self, formdata, data=_unset_value): | |
self.data = None | |
self.process_errors = [] | |
if data is _unset_value: | |
try: | |
data = self.default() | |
except TypeError: | |
data = self.default | |
self.process_data(data) | |
self.raw_data = {} | |
if formdata: | |
for part in ['day', 'month', 'year']: | |
name = '%s.%s' % (self.name, part) | |
raw_value = formdata.getlist(name) | |
if not (raw_value and raw_value[0]): | |
continue | |
try: | |
self.raw_data[part] = raw_value[0] | |
setattr(self, part, int(raw_value[0])) | |
except ValueError: | |
self.process_errors.append(_(u'Invalid date.')) | |
if self.raw_data: # Trick the Optional validator | |
self.raw_data[0] = 'dummy' | |
if not self.process_errors: | |
if bool(self.day) ^ bool(self.year): | |
self.process_errors.append(_(u'Please enter both day and year.')) | |
elif self.year and self.month and self.day: | |
try: | |
self.data = datetime.date(self.year, self.month, self.day) | |
except (TypeError, ValueError) as e: | |
self.process_errors.append(_(u'Invalid date.')) | |
return | |
for filter in self.filters: | |
try: | |
self.data = filter(self.data) | |
except ValueError, e: | |
self.process_errors.append(e.args[0]) | |
class Required(wtforms.validators.Required): | |
"""Enhanced version of Required which knows how to deal with file fields. | |
""" | |
message = _(u'This field is required.') | |
def __init__(self, message=None): | |
if message is not None: | |
self.message = message | |
def __call__(self, form, field): | |
if isinstance(field.data, cgi.FieldStorage): | |
return | |
elif isinstance(field.data, bool): | |
return | |
super(Required, self).__call__(form, field) | |
class NumberRange(object): | |
""" | |
Validates that a number is of a minimum and/or maximum value, inclusive. | |
This will work with any comparable number type, such as floats and | |
decimals, not just integers. | |
:param min: | |
The minimum required value of the number. If not provided, minimum | |
value will not be checked. | |
:param max: | |
The maximum value of the number. If not provided, maximum value | |
will not be checked. | |
:param message: | |
Error message to raise in case of a validation error. Can be | |
interpolated using `%(min)s` and `%(max)s` if desired. Useful defaults | |
are provided depending on the existence of min and max. | |
""" | |
msg_between = _(u'Please enter a number between ${min} and ${max}.') | |
msg_less_than = _(u'Please enter a number below or equal to ${min}.') | |
msg_greater_than = _(u'Please enter a number above or equal to ${min}.') | |
def __init__(self, min=None, max=None, | |
msg_between=None, msg_less_than=None, msg_greater_than=None): | |
self.min = min | |
self.max = max | |
if msg_between: | |
self.msg_between = msg_between | |
if msg_less_than: | |
self.msg_less_than = msg_less_than | |
if msg_greater_than: | |
self.msg_greater_than = msg_greater_than | |
def __call__(self, form, field): | |
data = field.data | |
if data is None or (self.min is not None and data < self.min) or \ | |
(self.max is not None and data > self.max): | |
if self.max is None: | |
message = self.msg_less_than % {'min': self.min} | |
elif self.min is None: | |
message = self.msg_greater_than % {'max': self.max} | |
else: | |
message = self.msg_between % \ | |
{'min': self.min, 'max': self.max} | |
message = field.gettext(message) | |
raise ValidationError(message) | |
class BaseValidator(object): | |
"""Base class for custom WTForms validators. | |
""" | |
def __init__(self, message=None): | |
if message is not None: | |
self.message = message | |
class IsImage(BaseValidator): | |
"""Check if uploaded file is valid image. | |
""" | |
message = _(u'Please upload a valid image. Supported image formats ' | |
u'are jpeg, png and gif.') | |
def validate_image(self, data): | |
try: | |
image = PIL.Image.open(data) | |
image.load() # broken JPEG | |
data.seek(0) | |
image = PIL.Image.open(data) # verify needs fresh image | |
image.verify() # broken PNG | |
if image.format not in ['GIF', 'PNG', 'JPEG']: | |
return False | |
except (IOError, OverflowError): | |
return False | |
finally: | |
if hasattr(data, 'seek'): | |
data.seek(0) | |
return True | |
def __call__(self, form, field): | |
if not isinstance(field.data, cgi.FieldStorage): | |
return | |
if not self.validate_image(field.data.file): | |
raise wtforms.ValidationError(self.message) | |
class URL(BaseValidator): | |
message = _(u'Please enter a valid URL.') | |
scheme_message = _(u'Please enter a valid URL.') | |
def __init__(self, allowed_schemes=None, | |
message=None, scheme_message=None): | |
super(URL, self).__init__(message) | |
self.allowed_schemes = allowed_schemes | |
if scheme_message is not None: | |
self.scheme_message = scheme_message | |
def __call__(self, form, field): | |
url = urlparse.urlparse(field.data) | |
if not url.netloc: | |
raise ValidationError(self.message) | |
if self.allowed_schemes and url.scheme not in self.allowed_schemes: | |
raise ValidationError(self.scheme_message) | |
class Email(wtforms.validators.Email): | |
# Customised validator which has a different default message. | |
_message = _(u'Please enter a valid email address.') | |
def __init__(self, *a, **kw): | |
super(Email, self).__init__(*a, **kw) | |
if self.message is None: | |
self.message = self._message | |
class PyramidTranslations(object): | |
"""An WTForms translations handler which uses the Pyramid | |
:py:class:`Localizer <pyramid.i18n.Localizer>`. | |
""" | |
def __init__(self, request): | |
self.localizer = get_localizer(request) | |
self.gettext = self.localizer.translate | |
self.ngettext = self.localizer.pluralize | |
class Form(SecureForm): | |
"""Base form class supporting CSRF and translations. | |
""" | |
def __init__(self, *a, **kw): | |
self.request = kw.pop('request') | |
self._translations = PyramidTranslations(self.request) | |
SecureForm.__init__(self, *a, **kw) | |
def _get_translations(self): | |
return self._translations | |
def generate_csrf_token(self, csrf_context): | |
return self.request.session.get_csrf_token() | |
def validate_csrf_token(self, field): | |
if field.data != field.current_token: | |
raise ValueError('Invalid CSRF') | |
class BasicFormView(object): | |
"""Abstract base class for forms using a `WTForms | |
<http://wtforms.simplecodes.com>`_ form. | |
Derived classes must replace the :py:attr:`form_class` class | |
variable and implement their own :py:meth:`do_post` method. | |
""" | |
#: form class to use. | |
form_class = None | |
#: Load form defaults from the current context | |
defaults_from_context = True | |
def __init__(self, context, request): | |
self.context = context | |
self.request = request | |
if not self.defaults_from_context: | |
context = None | |
if request.method == 'POST': | |
self.form = self.form_class(request.POST, context, | |
request=request, **self.default_data()) | |
else: | |
self.form = self.form_class(None, context, | |
request=request, | |
**self.default_data()) | |
def default_data(self): | |
"""Utility method to return default values for the form. | |
This is only needed if the default values can not be retrieved | |
as attributes on the current context. | |
""" | |
return {} | |
def do_post(self): | |
"""Perform all POST processing. | |
Implementations of this method must return None of processing failed | |
for some reason and the form must be shown again. If processing | |
succeeded a response object must be returned (commonly a | |
:py:class:`HTTPFound <pyramid.httpexceptions.HTTPFound>` instance). | |
""" | |
raise NotImplementedError() | |
def template_data(self): | |
"""Return extra data that should be exposed to templates. | |
""" | |
return {} | |
def __call__(self): | |
if self.request.method == 'POST' and self.form.validate(): | |
response = self.do_post() | |
if response is not None: | |
return response | |
result = {'form': self.form} | |
result.update(self.template_data()) | |
return result |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment