From cb015025cfc2b6a9746353cf2167066d0e0c3bbf Mon Sep 17 00:00:00 2001 From: Johannes Gehrs Date: Sun, 23 Jun 2013 23:52:59 +0200 Subject: [PATCH] initial commit --- .gitignore | 11 ++ .gitmodules | 3 + sample/sample.py | 62 +++++++ sample/static/scripts/parsleyjs | 1 + sample/templates/_formhelpers.html | 30 +++ sample/templates/base.html | 66 +++++++ sample/templates/wtforms_parsley_sample.html | 23 +++ wtforms_parsleyjs.py | 184 +++++++++++++++++++ 8 files changed, 380 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 sample/sample.py create mode 160000 sample/static/scripts/parsleyjs create mode 100644 sample/templates/_formhelpers.html create mode 100644 sample/templates/base.html create mode 100644 sample/templates/wtforms_parsley_sample.html create mode 100644 wtforms_parsleyjs.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..18789ed --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.DS_Store +*.pyc +*.pyo +env +env* +dist +*.egg +*.egg-info +_mailinglist +.tox +.idea \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..0b7ee1f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "sample/static/scripts/parsleyjs"] + path = sample/static/scripts/parsleyjs + url = https://github.com/guillaumepotier/Parsley.js.git diff --git a/sample/sample.py b/sample/sample.py new file mode 100644 index 0000000..71950d5 --- /dev/null +++ b/sample/sample.py @@ -0,0 +1,62 @@ +from flask import Flask, render_template, redirect, url_for, request, flash +from wtforms import Form, validators +from wtforms_parsleyjs import IntegerField, BooleanField, SelectField, TextField + +app = Flask(__name__) + + +@app.route('/parsley_testform', methods=['GET', 'POST']) +def parsley_testform(): + form = ParsleyTestForm(request.form) + if request.method == 'POST': form.validate() + return render_template('wtforms_parsley_sample.html', form=form) + + +class ParsleyTestForm(Form): + email = TextField('E-Mail Address', [ + validators.Email('Sorrry, not a valid email address.') + ], default='test@example.com') + first_value = TextField('Some Value', default='Some value') + second_value = TextField('Should be identical', [ + validators.EqualTo(message='Sorry, values do not match.', + fieldname='first_value') + ], default='Some value') + ip_address = TextField('IP4 Address', [ + validators.IPAddress(message='Sorry, not a valid IP4 Address.') + ], default='127.0.0.1') + string_length = TextField('Length of String (5 to 10)', [ + validators.Length(message='Length should be between 5 and 10 characters.', + min=5, max=10) + ], default='Hello!') + number_range = IntegerField('Number Range (5 to 10)', [ + validators.NumberRange(message='Range should be between 5 and 10.', + min=5, max=10) + ], default=7) + + required_text = TextField('Required Field', [ + validators.Required(message='Sorry, this is a required field.') + ], default='Mandatory text') + required_select = SelectField('Required Select', [ + validators.Required( + message='Sorry, you have to make a choice.') + ], choices=[('', 'Please select an option'), ('cpp', 'C++'), ('py', 'Python'), + ('text', 'Plain Text')]) + required_checkbox = BooleanField('Required Checkbox', [ + validators.Required(message='Sorry, you need to accept this.') + ]) + + regexp = TextField('Regex-Matched Hex Color-Code', [ + validators.Regexp(message='Not a proper color code, sorry.', + regex=r'^#[A-Fa-f0-9]{6}$') + ], default='#7D384F') + url = TextField('URL Field', [ + validators.URL(message='Sorry, this is not a valid URL,') + ], default='http://example.com/parsley') + anyof = TextField('Car, Bike or Plane?', [ + validators.AnyOf(message='Sorry, you can only choose from car, bike and plane', + values=['car', 'bike', 'plane']) + ], default='car') + + +if __name__ == '__main__': + app.run(debug=True) \ No newline at end of file diff --git a/sample/static/scripts/parsleyjs b/sample/static/scripts/parsleyjs new file mode 160000 index 0000000..4dfaaab --- /dev/null +++ b/sample/static/scripts/parsleyjs @@ -0,0 +1 @@ +Subproject commit 4dfaaabfd0cfebabba51b1280457b5ecfe764286 diff --git a/sample/templates/_formhelpers.html b/sample/templates/_formhelpers.html new file mode 100644 index 0000000..3cd92e1 --- /dev/null +++ b/sample/templates/_formhelpers.html @@ -0,0 +1,30 @@ +{% macro render_checkbox(field) %} +
+
+ + {{ render_errors(field) }} +
+
+{% endmacro %} + +{% macro render_field(field) %} + +
+
+ {{ field(**kwargs)|safe }} + {{ render_errors(field) }} +
+
+{% endmacro %} + +{% macro render_errors(field) %} + {% if field.errors %} + + + + {% endif %} +{% endmacro %} diff --git a/sample/templates/base.html b/sample/templates/base.html new file mode 100644 index 0000000..d1f2c87 --- /dev/null +++ b/sample/templates/base.html @@ -0,0 +1,66 @@ + + + + + WTForms-Parsley Sample + + + + + {% block styles %} + + + {% endblock styles %} + + + + + + + +
+ {% block content %} + + {% endblock content %} +
+ + +{% block scripts %} + + + + +{% endblock scripts %} + + diff --git a/sample/templates/wtforms_parsley_sample.html b/sample/templates/wtforms_parsley_sample.html new file mode 100644 index 0000000..760613f --- /dev/null +++ b/sample/templates/wtforms_parsley_sample.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} + +{% block content %} +

Check out the form validation!

+ {% from "_formhelpers.html" import render_field, render_checkbox %} +
+
+ {{ render_field(form.email) }} + {{ render_field(form.first_value) }} + {{ render_field(form.second_value) }} + {{ render_field(form.ip_address) }} + {{ render_field(form.string_length) }} + {{ render_field(form.number_range) }} + {{ render_field(form.required_text) }} + {{ render_field(form.required_select) }} + {{ render_checkbox(form.required_checkbox) }} + {{ render_field(form.regexp) }} + {{ render_field(form.url) }} + {{ render_field(form.anyof) }} +

+

+
+{% endblock content %} \ No newline at end of file diff --git a/wtforms_parsleyjs.py b/wtforms_parsleyjs.py new file mode 100644 index 0000000..8925391 --- /dev/null +++ b/wtforms_parsleyjs.py @@ -0,0 +1,184 @@ +__author__ = 'Johannes Gehrs (jgehrs@gmail.com)' + +import re, copy +from wtforms.validators import Length, NumberRange, Email, EqualTo, IPAddress, \ + Required, Regexp, URL, AnyOf +from wtforms import TextField +from wtforms.widgets import TextInput as _TextInput, PasswordInput as _PasswordInput, \ + CheckboxInput as _CheckboxInput, Select as _Select, Input +from wtforms.fields import TextField as _TextField, BooleanField as _BooleanField, \ + DecimalField as _DecimalField, IntegerField as _IntegerField, \ + FloatField as _FloatField, PasswordField as _PasswordField, \ + SelectField as _SelectField + +def parsley_kwargs(field, kwargs): + """ + Return new *kwargs* for *widget*. + + Generate *kwargs* from the validators present for the field. + + Note that the regex validation relies on the regex pattern being compatible with + both ECMA script and Python. The regex is not converted in any way. + It's possible to simply supply your own "data-regexp" keyword to the field + to explicitly provide the ECMA script regex. + See http://flask.pocoo.org/docs/patterns/wtforms/#forms-in-templates + + Note that the WTForms url vaidator probably is a bit more liberal than the parsley + one. Do check if the behaviour suits your needs. + """ + new_kwargs = copy.deepcopy(kwargs) + for vali in field.validators: + if isinstance(vali, Email): + _email_kwargs(new_kwargs) + if isinstance(vali, EqualTo): + _equal_to_kwargs(new_kwargs, vali) + if isinstance(vali, IPAddress): + _ip_address_kwargs(new_kwargs) + if isinstance(vali, Length): + _length_kwargs(new_kwargs, vali) + if isinstance(vali, NumberRange): + _number_range_kwargs(new_kwargs, vali) + if isinstance(vali, Required): + _required_kwargs(new_kwargs) + _trigger_kwargs(new_kwargs, u'key') + if isinstance(vali, Regexp) and not 'data_regexp' in new_kwargs: + _regexp_kwargs(new_kwargs, vali) + if isinstance(vali, URL): + _url_kwargs(new_kwargs) + if isinstance(vali, AnyOf): + _anyof_kwargs(new_kwargs, vali) + + + if not 'data_trigger' in new_kwargs: + _trigger_kwargs(new_kwargs) + if not 'data-error-message' in new_kwargs and vali.message is not None: + _message_kwargs(new_kwargs, message=vali.message) + + return new_kwargs + + +def _email_kwargs(kwargs): + kwargs[u'data-type']=u'email' + + +def _equal_to_kwargs(kwargs, vali): + kwargs[u'data-equalto']=u'#' + vali.fieldname + + +def _ip_address_kwargs(kwargs): + # Regexp from http://stackoverflow.com/a/4460645 + kwargs[u'data-regexp']=\ + r'^\b(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).' \ + r'(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.' \ + r'(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.' \ + r'(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b$' + + +def _length_kwargs(kwargs, vali): + kwargs[u'data-rangelength'] = u'[' + str(vali.min) + u',' + str(vali.max) + u']' + + +def _number_range_kwargs(kwargs, vali): + kwargs[u'data-range'] = u'[' + str(vali.min) + u',' + str(vali.max) + u']' + + +def _required_kwargs(kwargs): + kwargs[u'data-required'] = u'true' + + +def _regexp_kwargs(kwargs, vali): + # Apparently, this is the best way to check for RegexObject Type + # It's needed because WTForms allows compiled regexps to be passed to the validator + RegexObject = type(re.compile('')) + if isinstance(vali.regex, RegexObject): + regex_string = vali.regex.pattern + else: regex_string = vali.regex + kwargs[u'data-regexp'] = regex_string + + +def _url_kwargs(kwargs): + kwargs[u'data-type'] = u'url' + + +def _string_seq_delimiter(vali, kwargs): + # We normally use a comma as the delimiter - looks clean and it's parsley's default. + # If the strings for which we check contain a comma, we cannot use it as a delimiter. + delimiter = u',' + for value in vali.values: + if value.find(',') != -1: + delimiter = u';;;' + break + if delimiter != u',': kwargs[u'data-inlist-delimiter'] = delimiter + return delimiter + + +def _anyof_kwargs(kwargs, vali): + delimiter = _string_seq_delimiter(vali, kwargs) + kwargs[u'data-inlist'] = delimiter.join(vali.values) + + +def _trigger_kwargs(kwargs, trigger=u'change'): + kwargs[u'data-trigger']=trigger + + +def _message_kwargs(kwargs, message): + kwargs[u'data-error-message'] = message + + +class ParsleyInputMixin(Input): + def __call__(self, field, **kwargs): + kwargs = parsley_kwargs(field, kwargs) + return super(ParsleyInputMixin, self).__call__(field, **kwargs) + + +class TextInput(_TextInput, ParsleyInputMixin): + pass + + +class PasswordInput(_PasswordInput, ParsleyInputMixin): + pass + + +class CheckboxInput(_CheckboxInput, ParsleyInputMixin): + pass + + +class Select(_Select): + def __call__(self, field, **kwargs): + kwargs = parsley_kwargs(field, kwargs) + return super(Select, self).__call__(field, **kwargs) + + +class TextField(_TextField): + def __init__(self, *args, **kwargs): + super(TextField, self).__init__(widget = TextInput(), *args, **kwargs) + + +class IntegerField(_IntegerField): + def __init__(self, *args, **kwargs): + super(IntegerField, self).__init__(widget = TextInput(), *args, **kwargs) + + +class BooleanField(_BooleanField): + def __init__(self, *args, **kwargs): + super(BooleanField, self).__init__(widget = CheckboxInput(), *args, **kwargs) + + +class DecimalField(_DecimalField): + def __init__(self, *args, **kwargs): + super(DecimalField, self).__init__(widget = TextInput(), *args, **kwargs) + + +class FloatField(_FloatField): + def __init__(self, *args, **kwargs): + super(FloatField, self).__init__(widget = TextInput(), *args, **kwargs) + + +class PasswordField(_PasswordField): + def __init__(self, *args, **kwargs): + super(PasswordField, self).__init__(widget = PasswordInput(), *args, **kwargs) + + +class SelectField(_SelectField): + def __init__(self, *args, **kwargs): + super(SelectField, self).__init__(widget = Select(), *args, **kwargs)