1. David Baumgold
  2. wtforms

Commits

David Baumgold  committed 8030464 Merge

Merged from canonical version

  • Participants
  • Parent commits 022b863, 39fdd9b
  • Branches default

Comments (0)

Files changed (52)

File .hgignore

View file
 ^docs/html
 ^dist/
 ^MANIFEST$
+^WTForms\.egg-info
+^env/
+wtforms/ext/i18n/.*\.mo$

File .hgtags

View file
 eab645ef8ca1dee6fd39ea28f93a1a37a4cb2347 0.6.1
 8e5a93665d108cc8977936e6ab54706ebc05587a 0.6.2
 6a6954927a13911fae6941fd0aac13b8d6372a42 0.6.3
+643ce2169fb4359f8b541102e7db22fed4871215 1.0
+b83e12f59ac1be690533141025d17f7cd0bc1467 1.0.1

File AUTHORS.txt

View file
 
 Contributors:
 
+- Adam Lowry
 - Ali Aafshar
-- Adam Lowry
+- Andreas Madsack
 - Christopher Grebs
 - Eduardo Schettino
 - Emil Vladev
+- Jean-Philippe Serafin
 - Rodrigo Moraes
 - Sebastian Wiesner
 - Vinay Sajip

File CHANGES.txt

View file
 WTForms Changelog
 =================
 
+Version 1.0.2
+-------------
+Not yet released
+
+- Add in ability to convert relationships to ext.sqlalchemy model_form
+
+Version 1.0.1
+-------------
+Released February 29, 2012
+
+- Fixed issues related to building for python 3 and python pre-releases.
+
+- Add object_data to fields to get at the originally passed data.
+
+Version 1.0
+-----------
+Released February 28, 2012
+
+- Output HTML5 compact syntax by default.
+
+- Substantial code reorg, cleanup, and test improvements
+
+- Added ext.csrf for a way to implement CSRF protection
+
+- ext.sqlalchemy:
+  * Support PGInet, MACADDR, and UUID field conversion
+  * Support callable defaults
+
+- ext.appengine:
+  * model_form now supports generating forms with the same ordering as model.
+  * ReferencePropertyField now gets get_label like the other ORM fields
+
+- Add localization support for WTForms built-in messages
+
+- Python 3 support (via 2to3)
+
+- Minor changes/fixes:
+  * An empty label string can be specified on fields if desired
+  * Option widget can now take kwargs customization
+  * Field subclasses can provide default validators as a class property
+  * DateTimeField can take time in microseconds
+  * Numeric fields all set .data to None on coercion error for consistency.
+
+
 Version 0.6.3
 -------------
 Released April 24, 2011

File MANIFEST.in

View file
 recursive-include tests *
 recursive-exclude docs/_build *
 recursive-exclude tests *.pyc
+recursive-include wtforms/ext/i18n/messages *

File docs/conf.py

View file
 # All configuration values have a default value; values that are commented out
 # serve to show the default value.
 
-import sys, os
+def _fix_import_path():
+    """
+    Don't want to pollute the config globals, so do path munging 
+    here in this function
+    """
+    import sys, os
 
-sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
+    try:
+        import wtforms
+    except ImportError:
+        parent_dir = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
+        build_lib = os.path.join(parent_dir, 'build', 'lib')
+        if os.path.isdir(build_lib):
+            sys.path.insert(0, build_lib)
+        else:
+            sys.path.insert(0, parent_dir)
 
-# If your extensions are in another directory, add it here. If the directory
-# is relative to the documentation root, use os.path.abspath to make it
-# absolute, like shown here.
-#sys.path.append(os.path.abspath('some/directory'))
+_fix_import_path()
 
 # General configuration
 # ---------------------
 # other places throughout the built documents.
 #
 # The short X.Y version.
-version = '0.6.4'
+version = '1.0.2'
 # The full version, including alpha/beta/rc tags.
-release = '0.6.4dev'
+release = '1.0.2dev'
 
 
 # There are two options for replacing |today|: either, you set today to some

File docs/crash_course.rst

View file
+.. _crash-course:
+
 Crash Course
 ============
 
 While you almost never use all three methods together in practice, it
 illustrates how WTForms looks up the `username` field:
 
-1. Check if `request.POST` has a `username` key.
-2. Check if `user` has an attribute named `username`.
-3. Check if a keyword argument named `username` was provided.
-4. Finally, if everything else fails, use the default value provided by the
-   field, if any.
+1. If a form was submitted (request.POST is not empty), process the form
+   input. Even if there was no form input for this field in particular, if
+   there exists form input of any sort, then we will process the form input.
+
+2. If there was no form input, then try the following in order:
+
+   1. Check if `user` has an attribute named `username`.
+   2. Check if a keyword argument named `username` was provided.
+   3. Finally, if everything else fails, use the default value provided by the
+      field, if any.
 
 
 Validators
 
     {% if form.errors %}
         <ul class="errors">
-            {% for field_name, field_errors in form.errors if field_errors %}
+            {% for field_name, field_errors in form.errors|dictsort if field_errors %}
                 {% for error in field_errors %}
                     <li>{{ form[field_name].label }}: {{ error }}</li>
                 {% endfor %}
 
 For more complex validators that take parameters, check the :ref:`custom-validators` section. 
 
+Next Steps
+----------
+
+The crash course has just skimmed the surface on how you can begin using
+WTForms to handle form input and validation in your application. For more
+information, you'll want to check the following:
+
+ - The :ref:`WTForms documentation <doc-index>` has API documentation for the entire library.
+ - :ref:`specific_problems` can help you tackle specific
+   integration issues with WTForms and other frameworks.
+ - The `mailing list`_ is where you can get help, discuss bugs in WTForms, and
+   propose new features.
+
+.. _mailing list: http://groups.google.com/group/wtforms/
+

File docs/ext.rst

View file
 ~~~~~~~~~~~~~~~~~~~~~~~
 .. module:: wtforms.ext.appengine.fields
 
-.. autoclass:: ReferencePropertyField(default field arguments, reference_class=None, label_attr=None, allow_blank=False, blank_text=u'')
+.. autoclass:: ReferencePropertyField(default field arguments, reference_class=None, get_label=None, allow_blank=False, blank_text=u'')
 
 .. autoclass:: StringListPropertyField(default field arguments)
 
 .. autoclass:: QuerySelectField(default field args, query_factory=None, get_pk=None, get_label=None, allow_blank=False, blank_text=u'')
 
 .. autoclass:: QuerySelectMultipleField(default field args, query_factory=None, get_pk=None, get_label=None, allow_blank=False, blank_text=u'')
+
+
+CSRF
+----
+.. module:: wtforms.ext.csrf
+
+The CSRF package includes tools that help you implement checking against
+cross-site request forgery ("csrf"). Due to the large number of variations on
+approaches people take to CSRF (and the fact that many make compromises) the
+base implementation allows you to plug in a number of CSRF validation
+approaches.
+
+CSRF implementations are made by subclassing
+:class:`~wtforms.ext.csrf.form.SecureForm`. For utility, we have provided one
+possible CSRF implementation in the package that can be used with many
+frameworks for session-based hash secure keying,
+:class:`~wtforms.ext.csrf.session.SessionSecureForm`.
+
+All CSRF implementations hinge around creating a special token, which is put in
+a hidden field on the form named 'csrf_token', which must be rendered in your
+template to be passed from the browser back to your view. There are many
+different methods of generating this token, but they are usually the result of
+a cryptographic hash function against some data which would be hard to forge.
+
+.. module:: wtforms.ext.csrf.form
+
+.. autoclass:: SecureForm
+
+    .. automethod:: generate_csrf_token
+
+    .. automethod:: validate_csrf_token
+
+Creating your own CSRF implementation
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Here we will sketch out a simple theoretical CSRF implementation which
+generates a hash token based on the user's IP.
+
+**Note** This is a simplistic example meant to illustrate creating a CSRF
+implementation. This isn't recommended to be used in production because the
+token is deterministic and non-changing per-IP, which means this isn't the
+most secure implementation of CSRF.
+
+First, let's create our SecureForm base class::
+
+    from wtforms.ext.csrf import SecureForm
+    from hashlib import md5
+
+    SECRET_KEY = '1234567890'
+
+    class IPSecureForm(SecureForm):
+        """
+        Generate a CSRF token based on the user's IP. I am probably not very
+        secure, so don't use me.
+        """
+
+        def generate_csrf_token(self, csrf_context):
+            # csrf_context is passed transparently from the form constructor,
+            # in this case it's the IP address of the user
+            token = md5(SECRET_KEY + csrf_context).hexdigest()
+            return token
+
+        def validate_csrf_token(self, field):
+            if field.data != field.current_token:
+                raise ValueError('Invalid CSRF')
+
+
+Now that we have this taken care of, let's write a simple form and view which would implement this::
+
+    class RegistrationForm(IPSecureForm):
+        name = TextField('Your Name')
+        email = TextField('Email', [validators.email()])
+
+    def register(request):
+        form = RegistrationForm(request.POST, csrf_context=request.ip)
+
+        if request.method == 'POST' and form.validate():
+            pass # We're all good, create a user or whatever it is you do
+        elif form.csrf_token.errors:
+            pass # If we're here we suspect the user of cross-site request forgery
+        else:
+            pass # Any other errors
+
+        return render('register.html', form=form)
+
+And finally, a simple template:
+
+.. code-block:: html+jinja
+
+    <form action="register" method="POST">
+        {{ form.csrf_token }}
+        <p>{{ form.name.label }}: {{ form.name }}</p>
+        <p>{{ form.email.label }}: {{ form.email }}</p>
+        <input type="submit" value="Register">
+    </form>
+
+
+Please note that implementing CSRF detection is not fool-proof, and even with
+the best CSRF protection implementation, it's possible for requests to be
+forged by expert attackers. However, a good CSRF protection would make it
+infeasible for someone from an external site to hijack a form submission from
+another user and perform actions as them without additional a priori knowledge.
+
+In addition, it's important to understand that very often, the more strict the
+CSRF protection, the higher the chance of false positives occurring (ie,
+legitimate users getting blocked by your CSRF protection) and choosing a CSRF
+implementation is actually a matter of compromise. We will attempt to provide a
+handful of usable reference algorithms built in to this library in the future, to
+allow that choice to be easy.
+
+Some tips on criteria people often examine when evaluating CSRF implementations:
+
+ * **Reproducability** If a token is based on attributes about the user, it
+   gains the advantage that one does not need secondary storage in which to
+   store the value between requests. However, if the same attributes can be
+   reproduced by an attacker, then the attacker can potentially forge this
+   information.
+
+ * **Reusability**. It might be desired to make a completely different token
+   every use, and disallow users from re-using past tokens. This is an
+   extremely powerful protection, but can have consequences on if the user uses
+   the back button (or in some cases runs forms simultaneously in multiple
+   browser tabs) and submits an old token, or otherwise. A possible compromise
+   is to allow reusability in a time window (more on that later).
+
+ * **Time Ranges** Many CSRF approaches use time-based expiry to make sure that
+   a token cannot be (re)used beyond a certain point. Care must be taken in
+   choosing the time criteria for this to not lock out legitimate users. For
+   example, if a user might walk away while filling out a long-ish form, or to
+   go look for their credit card, the time for expiry should take that into
+   consideration to provide a balance between security and limiting user
+   inconvenience.
+
+ * **Requirements** Some CSRF-prevention methods require the use of browser
+   cookies, and some even require client-side scripting support. The webmaster
+   implementing the CSRF needs to consider that such requirements (though
+   effective) may lock certain legitimate users out, and make this
+   determination whether it is a good idea to use. For example, for a site
+   already using cookies for login, adding another for CSRF isn't as big of a
+   deal, but for other sites it may not be feasible.
+
+
+Session-based CSRF implementation
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. automodule:: wtforms.ext.csrf.session
+
+**Usage**
+
+First, create a SessionSecureForm subclass that you can use as your base class
+for any forms you want CSRF support for::
+
+    from wtforms.ext.csrf.session import SessionSecureForm
+
+    class MyBaseForm(SessionSecureForm):
+        SECRET_KEY = 'EPj00jpfj8Gx1SjnyLxwBBSQfnQ9DJYe0Ym'
+        TIME_LIMIT = timedelta(minutes=20)
+
+Now incorporate it into any form/view by further subclassing::
+
+    class Registration(MyBaseForm):
+        name = TextField()
+
+    def view(request):
+        form = Registration(request.POST, csrf_context=request.session)
+        # rest of view here
+
+Note that request.session is passed as the ``csrf_context=`` parameter, this is
+so that the CSRF token can be stored in your session for comparison on a later
+request.
+
+.. autoclass:: SessionSecureForm
+
+    A provided CSRF implementation which puts CSRF data in a session. Must be
+    subclassed to be used.
+    
+    **Class Attributes**
+    .. attribute:: SECRET_KEY
+        
+        Must be set by subclasses to a random byte string that will be used to generate HMAC digests. 
+
+    .. attribute:: TIME_LIMIT
+
+        If None, CSRF tokens never expire. If set to a ``datetime.timedelta``,
+        this is how long til a generated token expires. Defaults to
+        ``timedelta(minutes=30)``
+

File docs/faq.rst

View file
 What versions of Python are supported?
 --------------------------------------
 
-WTForms supports Python versions 2.4 and up. Presently, Python 3.x is not
-supported, as we are waiting for library support by the major frameworks. As
-soon as we have something we can test/deploy on we will consider making the
-provisions to support WTForms for Python 3.x.
+WTForms supports Python versions 2.5 and up. Presently (as of December 2011),
+Python 3.x is not officially supported, but the development version has made
+headway on this. We expect the upcoming 1.0 release to fully support Python 3
+using the '2to3' tool.
 
 
 How can I contribute to WTForms?
 for your file input, even if the form is prefixed.
 
 
+Why does blank input not go back to the default value?
+------------------------------------------------------
+
+A key design decision of WTForms was that form data -always- takes precedence
+when there's a form submission. That is, if a field exists on a form, and a
+form was posted, but that field's value was missing, it will not revert to a
+default, but instead store an empty value (and in some cases cause a validation
+error.)
+
+This is for a number of reasons:
+
+1. Security. If a form reverted to defaults on missing data, then an evil user
+   could potentially cause problems by submitting a hand-coded form with key
+   missing fields.
+
+2. Bug-finding. If you omitted a field in your template, it might fall through
+   to the default and you'd possibly miss it.
+
+3. Consistency.
+
+See the following mailing list posts for more discussion on the topic:
+ - http://groups.google.com/group/wtforms/browse_frm/thread/6755a45a13878e9 
+ - http://groups.google.com/group/wtforms/msg/fa409c8c89b6f62d 
+
+
 How do I... [convoluted combination of libraries]
 -------------------------------------------------
 

File docs/fields.rst

View file
         If form data is processed, is the valuelist given from the formdata
         wrapper. Otherwise, `raw_data` will be `None`.
 
+    .. attribute:: object_data
+
+        This is the data passed from an object or from kwargs to the field,
+        stored unmodified. This can be used by templates, widgets, validators
+        as needed (for comparison, for example)
+
     **Rendering**
 
     To render a field, simply call it, providing any values the widget expects

File docs/forms.rst

View file
 
     .. automethod:: __contains__
 
+    .. automethod:: _get_translations
+
 Defining Forms
 --------------
 

File docs/home_page.rst

-
-WTForms is a forms validation and rendering library for python development. It is available under the BSD license.
-
-The latest version is |version|, released April 24, 2011 (:ref:`download <download-installation>`)
-
-.. include:: crash_course.rst
-
-Next Steps
-----------
-
-The crash course has just skimmed the surface on how you can begin using
-WTForms to handle form input and validation in your application. For more
-information, you'll want to check the following:
-
- - The :ref:`WTForms documentation <doc-index>` has API documentation for the entire library.
- - :ref:`specific_problems` can help you tackle specific
-   integration issues with WTForms and other frameworks.
- - The `mailing list`_ is where you can get help, discuss bugs in WTForms, and
-   propose new features.
-
-.. _mailing list: http://groups.google.com/group/wtforms/

File docs/i18n.rst

View file
+Internationalization (i18n)
+===========================
+
+Localizing strings in WTForms is a topic that frequently comes up in the
+mailing list. While WTForms does not provide its own localization library, you
+can integrate WTForms with almost any gettext-like framework easily.
+
+In WTForms, the majority of messages that are transmitted are provided by you,
+the user. However, there is support for translating some of the *built-in*
+messages in WTForms (such as errors which occur during data coercion) so that
+the user can make sure the user experience is consistent.
+
+Translating user-provided messages
+----------------------------------
+
+This is not actually any specific feature in WTForms, but because the question
+is asked so frequently, we need to address it here: **WTForms does -not-
+translate any user-provided strings.**
+
+This is not to say they can't be translated, but that it's up to you to deal
+with providing a translation for any passed-in messages. WTForms waits until
+the last moment (usually validation time) before doing anything with the passed
+in message (such as interpolating strings) thus giving you the opportunity to
+e.g. change your locale before validation occurs, if you are using a suitable
+"lazy proxy".
+
+Here's a simple example of how one would provide translated strings to WTForms::
+
+    from somelibrary import ugettext_lazy as _
+    from wtforms import Form, TextField, IntegerField, validators as v
+
+    class RegistrationForm(Form):
+        name = TextField(_(u'Name'), [v.Required(_(u'Please provide your name'))])
+        age = IntegerField(
+            _(u'Age'),
+            [v.NumberRange(min=12, message=_(u'Must be at least %(min)d years old.'))]
+        )
+
+The field label is left un-perturbed until rendering time in a template, so you
+can easily provide translations for field labels if so desired. In addition,
+validator messages with format strings are not interpolated until the
+validation is run, so you can provide localization there as well.
+
+
+Translating built-in messages
+-----------------------------
+
+There are some messages in WTForms which are provided by the framework, namely
+default validator messages and errors occuring during the processing (data
+coercion) stage. For example, in the case of the IntegerField above, if someone
+entered a value which was not valid as an integer, then a message like "Not a
+valid integer value" would be displayed.
+
+
+Writing your own translations provider
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+For this case, we provide the ability to give a translations object on a
+subclass of Form, which will then be called to translate built-in strings.
+
+An example of writing a simple translations object::
+
+    from mylibrary import ugettext, ungettext
+    from wtforms import Form
+
+    class MyTranslations(object):
+        def gettext(self, string):
+            return ugettext(string)
+
+        def ngettext(self, singular, plural, n):
+            return ungettext(singular, plural, n)
+
+    class MyBaseForm(Form):
+        def _get_translations(self):
+            return MyTranslations()
+
+You would then use this new base Form class as the base class for any forms you
+create, and any built-in messages from WTForms will be passed to your
+gettext/ngettext implementations.
+
+You control the object's constructor, its lifecycle, and everything else about
+it, so you could, for example, pass the locale per-form instantiation to the
+translation object's constructor, and anything else you need to do for
+translations to work for you.
+
+
+Using the built-in translations provider
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. module:: wtforms.ext.i18n.form
+
+WTForms now includes a basic translations provider which uses the stdlib
+`gettext` module to localize strings based on locale information distributed
+with the package. As of this writing, we are waiting for more translations to
+be submitted, but we hope that soon we will provide given localizations
+
+To use the builtin translations provider, simply import
+:class:`wtforms.ext.i18n.form.Form` and use that as your base Form class.
+
+.. autoclass:: wtforms.ext.i18n.form.Form

File docs/index.rst

View file
 WTForms Documentation
 =====================
 
-This is the documentation for WTForms |version|, generated |today|.
+This is the documentation for WTForms |release|, generated |today|.
 
-For a quick introduction, check the `WTForms website`_.
-
-.. _WTForms website: http://wtforms.simplecodes.com/
+For a quick introduction, as well as download/installation instructions, check out the :doc:`crash_course`.
 
 **API**
 
    faq
    specific_problems
    crash_course
+   i18n
 
 **Indices and tables:**
 

File docs/specific_problems.rst

View file
         {% if field.errors %}
             {% set css_class = 'has_error ' + kwargs.pop('class', '') %}
             {{ field(class=css_class, **kwargs) }}
-            <ul class="errors">{% for error in errors %}<li>{{ error|e }}</li>{% endfor %}</ul>
+            <ul class="errors">{% for error in field.errors %}<li>{{ error|e }}</li>{% endfor %}</ul>
         {% else %}
             {{ field(**kwargs) }}
         {% endif %}

File docs/validators.rst

View file
 Built-in validators
 -------------------
 
+.. autoclass:: wtforms.validators.DataRequired
+
+   This also sets the ``required`` :attr:`flag <wtforms.fields.Field.flags>` on
+   fields it is used on.
+
 .. autoclass:: wtforms.validators.Email
 
 .. autoclass:: wtforms.validators.EqualTo
     passwords specified at all. Because Required stops the validation chain,
     EqualTo is not run in the case the password field is left empty.
 
+.. autoclass:: wtforms.validators.InputRequired
+
+   This also sets the ``required`` :attr:`flag <wtforms.fields.Field.flags>` on
+   fields it is used on.
+
 .. autoclass:: wtforms.validators.IPAddress
 
 .. autoclass:: wtforms.validators.Length
 
+.. autoclass:: wtforms.validators.MacAddress
+
 .. autoclass:: wtforms.validators.NumberRange
 
 .. autoclass:: wtforms.validators.Optional
    This also sets the ``optional`` :attr:`flag <wtforms.fields.Field.flags>` on
    fields it is used on.
 
-.. autoclass:: wtforms.validators.Required
-
-   This also sets the ``required`` :attr:`flag <wtforms.fields.Field.flags>` on
-   fields it is used on.
 
 .. autoclass:: wtforms.validators.Regexp
 
 .. autoclass:: wtforms.validators.URL
 
+.. autoclass:: wtforms.validators.UUID
+
 .. autoclass:: wtforms.validators.AnyOf
 
 .. autoclass:: wtforms.validators.NoneOf

File setup.cfg

View file
+[extract_messages]
+copyright_holder=WTForms Team
+output_file=wtforms/ext/i18n/messages/wtforms.pot
+
+[init_catalog]
+input_file=wtforms/ext/i18n/messages/wtforms.pot
+output_dir=wtforms/ext/i18n/messages/
+domain=wtforms
+
+[compile_catalog]
+directory=wtforms/ext/i18n/messages/
+domain=wtforms

File setup.py

View file
 import os, sys
 sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
 
-from distutils.core import setup
-import wtforms
+extra = {}
+
+try:
+    from setuptools import setup
+    has_setuptools = True
+    extra['test_suite'] = 'tests.runtests'
+except ImportError:
+    from distutils.core import setup
+    has_setuptools = False
+
+if sys.version_info >= (3, ):
+    if not has_setuptools:
+        raise Exception('Python3 support in WTForms requires distribute.')
+    extra['use_2to3'] = True
+    extra['use_2to3_exclude_fixers'] = ['lib2to3.fixes.fix_filter', 'lib2to3.fixes.filter']
 
 setup(
     name='WTForms',
-    version=wtforms.__version__,
+    version='1.0.2',
     url='http://wtforms.simplecodes.com/',
     license='BSD',
     author='Thomas Johansson, James Crasta',
         'wtforms.widgets',
         'wtforms.ext',
         'wtforms.ext.appengine',
+        'wtforms.ext.csrf',
         'wtforms.ext.dateutil',
         'wtforms.ext.django',
         'wtforms.ext.django.templatetags',
+        'wtforms.ext.i18n',
         'wtforms.ext.sqlalchemy',
-    ]
+    ],
+    package_data={
+        'wtforms.ext.i18n': ['messages/wtforms.pot', 'messages/*/*/*'],
+    },
+    **extra
 )

File tests/__init__.py

Empty file added.

File tests/ext_appengine/tests.py

View file
         self.assertEqual(form.is_admin.label.text, 'Administrative rights')
 
     def test_reference_property(self):
-        keys = []
+        keys = ['__None']
         for name in ['foo', 'bar', 'baz']:
             author = Author(name=name, age=26)
             author.put()

File tests/ext_csrf.py

View file
+from unittest import TestCase
+
+from wtforms.fields import TextField
+from wtforms.ext.csrf import SecureForm
+from wtforms.ext.csrf.session import SessionSecureForm
+
+import datetime
+import hashlib
+import hmac
+
+class DummyPostData(dict):
+    def getlist(self, key):
+        v = self[key]
+        if not isinstance(v, (list, tuple)):
+            v = [v]
+        return v
+
+class InsecureForm(SecureForm):
+    def generate_csrf_token(self, csrf_context):
+        return csrf_context
+
+    a = TextField()
+
+class FakeSessionRequest(object):
+    def __init__(self, session):
+        self.session = session
+
+class StupidObject(object):
+    a = None
+    csrf_token = None
+
+
+class SecureFormTest(TestCase):
+    def test_base_class(self):
+        self.assertRaises(NotImplementedError, SecureForm)
+
+    def test_basic_impl(self):
+        form = InsecureForm(csrf_context=42)
+        self.assertEqual(form.csrf_token.current_token, 42)
+        self.assert_(not form.validate())
+        self.assertEqual(len(form.csrf_token.errors), 1)
+        self.assertEqual(form.csrf_token._value(), 42)
+        # Make sure csrf_token is taken out from .data
+        self.assertEqual(form.data, {'a': None})
+
+    def test_with_data(self):
+        post_data = DummyPostData(csrf_token=u'test', a=u'hi')
+        form = InsecureForm(post_data, csrf_context=u'test')
+        self.assert_(form.validate())
+        self.assertEqual(form.data, {'a': u'hi'})
+
+        form = InsecureForm(post_data, csrf_context=u'something')
+        self.assert_(not form.validate())
+
+        # Make sure that value is still the current token despite
+        # the posting of a different value
+        self.assertEqual(form.csrf_token._value(), u'something')
+
+        # Make sure populate_obj doesn't overwrite the token
+        obj = StupidObject()
+        form.populate_obj(obj)
+        self.assertEqual(obj.a, u'hi')
+        self.assertEqual(obj.csrf_token, None)
+
+    def test_with_missing_token(self):
+        post_data = DummyPostData(a='hi')
+        form = InsecureForm(post_data, csrf_context=u'test')
+        self.assert_(not form.validate())
+
+        self.assertEqual(form.csrf_token.data, u'')
+        self.assertEqual(form.csrf_token._value(), u'test')
+
+
+
+class SessionSecureFormTest(TestCase):
+    class SSF(SessionSecureForm):
+        SECRET_KEY = 'abcdefghijklmnop'.encode('ascii')
+
+    class BadTimeSSF(SessionSecureForm):
+        SECRET_KEY = 'abcdefghijklmnop'.encode('ascii')
+        TIME_LIMIT = datetime.timedelta(-1, 86300)
+
+    class NoTimeSSF(SessionSecureForm):
+        SECRET_KEY = 'abcdefghijklmnop'.encode('ascii')
+        TIME_LIMIT = None
+
+    def test_basic(self):
+        self.assertRaises(Exception, SessionSecureForm)
+        self.assertRaises(TypeError, self.SSF)
+        session = {}
+        form = self.SSF(csrf_context=FakeSessionRequest(session))
+        assert 'csrf' in session
+
+    def test_timestamped(self):
+        session = {}
+        postdata = DummyPostData(csrf_token=u'fake##fake')
+        form = self.SSF(postdata, csrf_context=session)
+        assert 'csrf' in session
+        assert form.csrf_token._value()
+        assert form.csrf_token._value() != session['csrf']
+        assert not form.validate()
+        self.assertEqual(form.csrf_token.errors[0], u'CSRF failed')
+        good_token = form.csrf_token._value()
+
+        # Now test a valid CSRF with invalid timestamp
+        evil_form = self.BadTimeSSF(csrf_context=session)
+        bad_token = evil_form.csrf_token._value()
+        
+        postdata = DummyPostData(csrf_token=bad_token)
+        form = self.SSF(postdata, csrf_context=session)
+        assert not form.validate()
+        self.assertEqual(form.csrf_token.errors[0], u'CSRF token expired')
+
+
+    def test_notime(self):
+        session = {}
+        form = self.NoTimeSSF(csrf_context=session)
+        hmacced = hmac.new(form.SECRET_KEY, session['csrf'].encode('utf8'), digestmod=hashlib.sha1)
+        self.assertEqual(form.csrf_token._value(), '##%s' % hmacced.hexdigest())
+        assert not form.validate()
+        self.assertEqual(form.csrf_token.errors[0], u'CSRF token missing') 
+
+        # Test with pre-made values
+        session = {'csrf': u'00e9fa5fe507251ac5f32b1608e9282f75156a05'}
+        postdata = DummyPostData(csrf_token=u'##d21f54b7dd2041fab5f8d644d4d3690c77beeb14')
+
+        form = self.NoTimeSSF(postdata, csrf_context=session)
+        assert form.validate()

File tests/ext_i18n.py

View file
+from unittest import TestCase
+from wtforms.ext.i18n.utils import get_translations
+
+class I18NTest(TestCase):
+    def test_failure(self):
+        self.assertRaises(IOError, get_translations, [])
+
+    def test_us_translation(self):
+        translations = get_translations(['en_US'])
+        self.assertEqual(translations.gettext(u'Invalid Mac address.'), u'Invalid MAC address.')
+
+
+if __name__ == '__main__':
+    from unittest import main
+    main()
+

File tests/ext_sqlalchemy.py

View file
 #!/usr/bin/env python
 
-from sqlalchemy import create_engine
+from sqlalchemy import create_engine, ForeignKey
 from sqlalchemy.schema import MetaData, Table, Column
-from sqlalchemy.types import String, Integer
-from sqlalchemy.orm import scoped_session, sessionmaker
+from sqlalchemy.types import String, Integer, Date
+from sqlalchemy.orm import sessionmaker, relationship, backref
+from sqlalchemy.ext.declarative import declarative_base
 
 from unittest import TestCase
 
 from wtforms.ext.sqlalchemy.fields import QuerySelectField, QuerySelectMultipleField
 from wtforms.form import Form
+from wtforms.fields import TextField
+from wtforms.ext.sqlalchemy.orm import model_form
+from wtforms.validators import Optional, Required, Length
+from wtforms.ext.sqlalchemy.validators import Unique
 
 
 class LazySelect(object):
     def _do_tables(self, mapper, engine):
         metadata = MetaData()
 
-        test_table = Table('test', metadata, 
+        test_table = Table('test', metadata,
             Column('id', Integer, primary_key=True, nullable=False),
             Column('name', String, nullable=False),
         )
 
-        pk_test_table = Table('pk_test', metadata, 
+        pk_test_table = Table('pk_test', metadata,
             Column('foobar', String, primary_key=True, nullable=False),
             Column('baz', String, nullable=False),
         )
 
         Test = type('Test', (Base, ), {})
-        PKTest = type('PKTest', (Base, ), {'__unicode__': lambda x: x.baz })
+        PKTest = type('PKTest', (Base, ), {
+            '__unicode__': lambda x: x.baz,
+            '__str__': lambda x: x.baz,
+        })
 
         mapper(Test, test_table, order_by=[test_table.c.name])
         mapper(PKTest, pk_test_table, order_by=[pk_test_table.c.baz])
         self.assert_(form.validate())
 
 
+class ModelFormTest(TestCase):
+    def setUp(self):
+        Model = declarative_base()
+
+        student_course = Table(
+            'student_course', Model.metadata,
+            Column('student_id', Integer, ForeignKey('student.id')),
+            Column('course_id', Integer, ForeignKey('course.id'))
+        )
+
+        class Course(Model):
+            __tablename__ = "course"
+            id = Column(Integer, primary_key=True)
+            name = Column(String(255), nullable=False)
+
+        class School(Model):
+            __tablename__ = "school"
+            id = Column(Integer, primary_key=True)
+            name = Column(String(255), nullable=False)
+
+        class Student(Model):
+            __tablename__ = "student"
+            id = Column(Integer, primary_key=True)
+            full_name = Column(String(255), nullable=False, unique=True)
+            dob = Column(Date(), nullable=True)
+            current_school_id = Column(Integer, ForeignKey(School.id),
+                nullable=False)
+
+            current_school = relationship(School, backref=backref('students'))
+            courses = relationship("Course", secondary=student_course,
+                backref=backref("students", lazy='dynamic'))
+
+        self.School = School
+        self.Student = Student
+
+        engine = create_engine('sqlite:///:memory:', echo=False)
+        Session = sessionmaker(bind=engine)
+        self.metadata = Model.metadata
+        self.metadata.create_all(bind=engine)
+        self.sess = Session()
+
+    def test_nullable_field(self):
+        student_form = model_form(self.Student, self.sess)()
+        self.assertTrue(issubclass(Optional,
+            student_form._fields['dob'].validators[0].__class__))
+
+    def test_required_field(self):
+        student_form = model_form(self.Student, self.sess)()
+        self.assertTrue(issubclass(Required,
+            student_form._fields['full_name'].validators[0].__class__))
+
+    def test_unique_field(self):
+        student_form = model_form(self.Student, self.sess)()
+        self.assertTrue(issubclass(Unique,
+            student_form._fields['full_name'].validators[1].__class__))
+
+    def test_include_pk(self):
+        form_class = model_form(self.Student, self.sess, exclude_pk=False)
+        student_form = form_class()
+        self.assertIn('id', student_form._fields)
+
+    def test_exclude_pk(self):
+        form_class = model_form(self.Student, self.sess, exclude_pk=True)
+        student_form = form_class()
+        self.assertNotIn('id', student_form._fields)
+
+    def test_exclude_fk(self):
+        student_form = model_form(self.Student, self.sess)()
+        self.assertNotIn('current_school_id', student_form._fields)
+
+    def test_include_fk(self):
+        student_form = model_form(self.Student, self.sess, exclude_fk=False)()
+        self.assertIn('current_school_id', student_form._fields)
+
+    def test_convert_many_to_one(self):
+        student_form = model_form(self.Student, self.sess)()
+        self.assertTrue(issubclass(QuerySelectField,
+            student_form._fields['current_school'].__class__))
+
+    def test_convert_one_to_many(self):
+        school_form = model_form(self.School, self.sess)()
+        self.assertTrue(issubclass(QuerySelectMultipleField,
+            school_form._fields['students'].__class__))
+
+    def test_convert_many_to_many(self):
+        student_form = model_form(self.Student, self.sess)()
+        self.assertTrue(issubclass(QuerySelectMultipleField,
+            student_form._fields['courses'].__class__))
+
+
+class UniqueValidatorTest(TestCase):
+    def setUp(self):
+        Model = declarative_base()
+
+        class User(Model):
+            __tablename__ = "user"
+            id = Column(Integer, primary_key=True)
+            username = Column(String(255), nullable=False, unique=True)
+
+        engine = create_engine('sqlite:///:memory:', echo=False)
+        Session = sessionmaker(bind=engine)
+        self.metadata = Model.metadata
+        self.metadata.create_all(bind=engine)
+        self.sess = Session()
+
+        self.sess.add(User(username='batman'))
+        self.sess.commit()
+
+        class UserForm(Form):
+            username = TextField('Username', [
+                Length(min=4, max=25),
+                Unique(lambda: self.sess, User, User.username)
+            ])
+
+        self.UserForm = UserForm
+
+    def test_validate(self):
+        user_form = self.UserForm(DummyPostData(username=[u'spiderman']))
+        self.assertTrue(user_form.validate())
+
+    def test_wrong(self):
+        user_form = self.UserForm(DummyPostData(username=[u'batman']))
+        self.assertFalse(user_form.validate())
+
+
 if __name__ == '__main__':
     from unittest import main
     main()

File tests/fields.py

View file
 #!/usr/bin/env python
+import sys
+
 from datetime import date, datetime
 from decimal import Decimal, ROUND_UP, ROUND_DOWN
 from unittest import TestCase
 from wtforms.form import Form
 
 
+PYTHON_VERSION = sys.version_info 
+
 class DummyPostData(dict):
     def getlist(self, key):
         v = self[key]
         self.assertEqual(unicode(label), expected)
         self.assertEqual(label.__html__(), expected)
         self.assertEqual(label().__html__(), expected)
-        self.assertEqual(label('hello'), u"""<label for="test">hello</label>""")
+        self.assertEqual(label(u'hello'), u"""<label for="test">hello</label>""")
         self.assertEqual(TextField(u'hi').bind(Form(), 'a').label.text, u'hi')
-        self.assertEqual(repr(label), "Label('test', u'Caption')") 
+        if PYTHON_VERSION < (3, ):
+            self.assertEqual(repr(label), "Label('test', u'Caption')") 
+        else:
+            self.assertEqual(repr(label), "Label('test', 'Caption')") 
 
     def test_auto_label(self):
-        t1 = TextField().bind(Form(), 'foo_bar')
-        self.assertEqual(t1.label.text, 'Foo Bar')
+        t1 = TextField().bind(Form(), u'foo_bar')
+        self.assertEqual(t1.label.text, u'Foo Bar')
 
-        t2 = TextField('').bind(Form(), 'foo_bar')
-        self.assertEqual(t2.label.text, '')
+        t2 = TextField(u'').bind(Form(), u'foo_bar')
+        self.assertEqual(t2.label.text, u'')
 
 
 class FlagsTest(TestCase):
 
 class FiltersTest(TestCase):
     class F(Form):
-        a = TextField(default=' hello', filters=[lambda x: x.strip()])
+        a = TextField(default=u' hello', filters=[lambda x: x.strip()])
+        b = TextField(default=u'42', filters=[lambda x: int(x)])
 
-    def test(self):
-        self.assertEqual(self.F().a.data, 'hello')
-        self.assertEqual(self.F(DummyPostData(a=['  foo bar  '])).a.data, 'foo bar')
+    def test_working(self):
+        form = self.F()
+        self.assertEqual(form.a.data, u'hello')
+        self.assertEqual(form.b.data, 42)
+        assert form.validate()
+
+    def test_failure(self):
+        form = self.F(DummyPostData(a=[u'  foo bar  '], b=[u'hi']))
+        self.assertEqual(form.a.data, u'foo bar')
+        self.assertEqual(form.b.data, u'hi')
+        self.assertEqual(len(form.b.process_errors), 1)
+        assert not form.validate()
 
 
 class FieldTest(TestCase):
     class F(Form):
-        a = TextField(default='hello')
+        a = TextField(default=u'hello')
 
     def setUp(self):
         self.field = self.F().a 
 
+    def test_unbound_field(self):
+        unbound = self.F.a
+        assert unbound.creation_counter != 0
+        assert unbound.field_class is TextField
+        self.assertEqual(unbound.args, ())
+        self.assertEqual(unbound.kwargs, {'default': u'hello'})
+        assert repr(unbound).startswith(u'<UnboundField(TextField')
+
     def test_htmlstring(self):
         self.assert_(isinstance(self.field.__html__(), widgets.HTMLString))
 
 
     def test_iterable_options(self):
         form = self.F()
-        self.assert_(isinstance(list(form.a)[0], form.a._Option))
-        self.assertEqual(list(unicode(x) for x in form.a), [u'<option selected value="a">hello</option>', '<option value="btest">bye</option>'])
-        self.assert_(isinstance(list(form.a)[0].widget, widgets.Option))
+        first_option = list(form.a)[0]
+        self.assert_(isinstance(first_option, form.a._Option))
+        self.assertEqual(list(unicode(x) for x in form.a), [u'<option selected value="a">hello</option>', u'<option value="btest">bye</option>'])
+        self.assert_(isinstance(first_option.widget, widgets.Option))
         self.assert_(isinstance(list(form.b)[0].widget, widgets.TextInput))
+        self.assertEqual(first_option(disabled=True), u'<option disabled selected value="a">hello</option>')
+
+    def test_default_coerce(self):
+        F = make_form(a=SelectField(choices=[('a', 'Foo')]))
+        form = F(DummyPostData(a=[]))
+        assert not form.validate()
+        self.assertEqual(form.a.data, u'None')
+        self.assertEqual(len(form.a.errors), 1)
+        self.assertEqual(form.a.errors[0], 'Not a valid choice')
 
 
 class SelectMultipleFieldTest(TestCase):
         form = self.F(DummyPostData(a=[], b=['']))
         self.assertEqual(form.a.data, None)
         self.assertEqual(form.a.raw_data, [])
-        self.assertEqual(form.b.data, 48)
+        self.assertEqual(form.b.data, None)
         self.assertEqual(form.b.raw_data, [''])
         self.assert_(not form.validate())
         self.assertEqual(len(form.b.process_errors), 1)
         self.assertEqual(form.a._value(), u'2.10')
         self.assert_(form.validate())
         form = F(DummyPostData(a='2,1'), a=Decimal(5))
-        self.assertEqual(form.a.data, Decimal(5))
+        self.assertEqual(form.a.data, None)
         self.assertEqual(form.a.raw_data, ['2,1'])
         self.assert_(not form.validate())
 
         self.assert_(form.b.validate(form))
         form = self.F(DummyPostData(a=[], b=['']))
         self.assertEqual(form.a.data, None)
-        self.assertEqual(form.b.data, 48.0)
+        self.assertEqual(form.b.data, None)
         self.assertEqual(form.b.raw_data, [u''])
         self.assert_(not form.validate())
         self.assertEqual(len(form.b.process_errors), 1)
         a = DateField()
         b = DateField(format='%m/%d %Y')
 
-    def test(self):
+    def test_basic(self):
         d = date(2008, 5, 7)
         form = self.F(DummyPostData(a=['2008-05-07'], b=['05/07', '2008']))
         self.assertEqual(form.a.data, d)
         self.assertEqual(form.b.data, d)
         self.assertEqual(form.b._value(), '05/07 2008')
 
+    def test_failure(self):
+        form = self.F(DummyPostData(a=['2008-bb-cc'], b=['hi']))
+        assert not form.validate()
+        self.assertEqual(len(form.a.process_errors), 1)
+        self.assertEqual(len(form.a.errors), 1)
+        self.assertEqual(len(form.b.errors), 1)
+        assert u'not match format' in form.a.process_errors[0]
+
 
 class DateTimeFieldTest(TestCase):
     class F(Form):
         a = DateTimeField()
         b = DateTimeField(format='%Y-%m-%d %H:%M')
 
-    def test(self):
+    def test_basic(self):
         d = datetime(2008, 5, 5, 4, 30, 0, 0)
         form = self.F(DummyPostData(a=['2008-05-05', '04:30:00'], b=['2008-05-05 04:30']))
         self.assertEqual(form.a.data, d)
         self.assert_(form.validate())
         form = self.F(DummyPostData(a=['2008-05-05']))
         self.assert_(not form.validate())
-        self.assert_("not match format" in form.a.errors[0])
+        self.assert_(u'not match format' in form.a.errors[0])
+
+    def test_microseconds(self):
+        if PYTHON_VERSION < (2, 6):
+            return # Microsecond formatting support was only added in 2.6
+
+        d = datetime(2011, 5, 7, 3, 23, 14, 424200)
+        F = make_form(a=DateTimeField(format='%Y-%m-%d %H:%M:%S.%f'))
+        form = F(DummyPostData(a=['2011-05-07 03:23:14.4242']))
+        self.assertEqual(d, form.a.data)
 
 
 class SubmitFieldTest(TestCase):

File tests/runtests.py

View file
 import sys
 from unittest import defaultTestLoader, TextTestRunner, TestSuite
 
-TESTS = ['form', 'fields', 'validators', 'widgets', 'webob_wrapper', 'translations']
-TESTS.extend([x for x in sys.argv[1:] if '-' not in x])
+TESTS = ('form', 'fields', 'validators', 'widgets', 'webob_wrapper', 'translations', 'ext_csrf', 'ext_i18n')
 
-sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
+def make_suite(prefix='', extra=()):
+    tests = TESTS + extra
+    test_names = list(prefix + x for x in tests)
+    suite = TestSuite()
+    suite.addTest(defaultTestLoader.loadTestsFromNames(test_names))
+    return suite
 
-suite = TestSuite()
-suite.addTest(defaultTestLoader.loadTestsFromNames(TESTS))
+def additional_tests():
+    """
+    This is called automatically by setup.py test
+    """
+    return make_suite('tests.')
 
-runner = TextTestRunner(verbosity=(sys.argv.count('-v') - sys.argv.count('-q') + 1))
-result = runner.run(suite)
-sys.exit(not result.wasSuccessful())
+def main():
+    extra_tests = tuple(x for x in sys.argv[1:] if '-' not in x)
+    suite = make_suite('', extra_tests)
+
+    sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
+
+    runner = TextTestRunner(verbosity=(sys.argv.count('-v') - sys.argv.count('-q') + 1))
+    result = runner.run(suite)
+    sys.exit(not result.wasSuccessful())
+
+if __name__ == '__main__':
+    main()

File tests/validators.py

View file
 #!/usr/bin/env python
 from unittest import TestCase
-from wtforms.validators import StopValidation, ValidationError, email, equal_to, ip_address, length, required, optional, regexp, url, NumberRange, AnyOf, NoneOf
+from wtforms.validators import (
+    StopValidation, ValidationError, email, equal_to,
+    ip_address, length, required, optional, regexp,
+    url, NumberRange, AnyOf, NoneOf, mac_address, UUID
+)
+from functools import partial
 
 class DummyTranslations(object):
     def gettext(self, string):
         self.assertRaises(ValidationError, ip_address(), self.form, DummyField('1278.0.0.1'))
         self.assertRaises(ValidationError, ip_address(), self.form, DummyField('127.0.0.abc'))
 
+    def test_mac_address(self):
+        self.assertEqual(mac_address()(self.form, 
+                                       DummyField('01:23:45:67:ab:CD')), None)
+
+        check_fail = partial(
+            self.assertRaises, ValidationError, 
+            mac_address(), self.form
+        )
+
+        check_fail(DummyField('00:00:00:00:00'))
+        check_fail(DummyField('01:23:45:67:89:'))
+        check_fail(DummyField('01:23:45:67:89:gh'))
+        check_fail(DummyField('123:23:45:67:89:00'))
+
+
+    def test_uuid(self):
+        self.assertEqual(UUID()(self.form, DummyField(
+                    '2bc1c94f-0deb-43e9-92a1-4775189ec9f8')), None)
+        self.assertRaises(ValidationError, UUID(), self.form, 
+                          DummyField('2bc1c94f-deb-43e9-92a1-4775189ec9f8'))
+        self.assertRaises(ValidationError, UUID(), self.form, 
+                          DummyField('2bc1c94f-0deb-43e9-92a1-4775189ec9f'))
+        self.assertRaises(ValidationError, UUID(), self.form, 
+                          DummyField('gbc1c94f-0deb-43e9-92a1-4775189ec9f8'))
+        self.assertRaises(ValidationError, UUID(), self.form, 
+                          DummyField('2bc1c94f 0deb-43e9-92a1-4775189ec9f8'))
+
     def test_length(self):
         field = DummyField('foobar')
         self.assertEqual(length(min=2, max=6)(self.form, field), None)
         self.assertRaises(ValidationError, v, self.form, DummyField(None))
         self.assertRaises(ValidationError, v, self.form, DummyField(0))
         self.assertRaises(ValidationError, v, self.form, DummyField(12))
+        self.assertRaises(ValidationError, v, self.form, DummyField(-5))
 
         onlymin = NumberRange(min=5)
         self.assertEqual(onlymin(self.form, DummyField(500)), None)

File tests/widgets.py

View file
 
     _value       = lambda x: x.data
     __unicode__  = lambda x: x.data
+    __str__      = lambda x: x.data
     __call__     = lambda x, **k: x.data
     __iter__     = lambda x: iter(x.data)
     iter_choices = lambda x: iter(x.data)

File tests/wtforms

View file
+../wtforms/

File wtforms/__init__.py

View file
 from wtforms.widgets import __all__ as widgets_all
 __all__ = fields_all + widgets_all
 
-__version__ = '0.6.4dev'
+__version__ = '1.0.2dev'

File wtforms/ext/appengine/db.py

View file
 def convert_ReferenceProperty(model, prop, kwargs):
     """Returns a form field for a ``db.ReferenceProperty``."""
     kwargs['reference_class'] = prop.reference_class
+    kwargs.setdefault('allow_blank', not prop.required)
     return ReferencePropertyField(**kwargs)
 
 
         'RatingProperty':        convert_RatingProperty,
     }
 
+    # Don't automatically add a required validator for these properties
+    NO_AUTO_REQUIRED = frozenset(['ListProperty', 'StringListProperty', 'BooleanProperty'])
+
     def __init__(self, converters=None):
         """
         Constructs the converter, setting the converter callables.
         :param field_args:
             Optional keyword arguments to construct the field.
         """
+        prop_type_name = type(prop).__name__
         kwargs = {
             'label': prop.name.replace('_', ' ').title(),
             'default': prop.default_value(),
         if field_args:
             kwargs.update(field_args)
 
-        if prop.required:
+        if prop.required and prop_type_name not in self.NO_AUTO_REQUIRED:
             kwargs['validators'].append(validators.required())
 
         if prop.choices:
             kwargs['choices'] = [(v, v) for v in prop.choices]
             return f.SelectField(**kwargs)
         else:
-            converter = self.converters.get(type(prop).__name__, None)
+            converter = self.converters.get(prop_type_name, None)
             if converter is not None:
                 return converter(model, prop, kwargs)
 
     # Get the field names we want to include or exclude, starting with the
     # full list of model properties.
     props = model.properties()
-    field_names = props.keys()
+    sorted_props = sorted(props.iteritems(), key=lambda prop: prop[1].creation_counter)
+    field_names = list(x[0] for x in sorted_props)
+
     if only:
         field_names = list(f for f in only if f in field_names)
     elif exclude:

File wtforms/ext/appengine/fields.py

View file
 import decimal
+import operator
+import warnings
 
 from wtforms import fields, widgets
 
         A db.Model class which will be used to generate the default query
         to make the list of items. If this is not specified, The `query`
         property must be overridden before validation.
-    :param label_attr:
-        If specified, use this attribute on the model class as the label
-        associated with each option. Otherwise, the model object's
-        `__str__` or `__unicode__` will be used.
+    :param get_label:
+        If a string, use this attribute on the model class as the label
+        associated with each option. If a one-argument callable, this callable
+        will be passed model instance and expected to return the label text.
+        Otherwise, the model object's `__str__` or `__unicode__` will be used.
     :param allow_blank:
         If set to true, a blank choice will be added to the top of the list
         to allow `None` to be chosen.
     widget = widgets.Select()
 
     def __init__(self, label=None, validators=None, reference_class=None,
-                 label_attr=None, allow_blank=False, blank_text=u'', **kwargs):
+                 label_attr=None, get_label=None, allow_blank=False,
+                 blank_text=u'', **kwargs):
         super(ReferencePropertyField, self).__init__(label, validators,
                                                      **kwargs)
-        self.label_attr = label_attr
+        if label_attr is not None:
+            warnings.warn('label_attr= will be removed in WTForms 1.1, use get_label= instead.', DeprecationWarning)
+            self.get_label = operator.attrgetter(label_attr)
+        elif get_label is None:
+            self.get_label = lambda x: x
+        elif isinstance(get_label, basestring):
+            self.get_label = operator.attrgetter(get_label)
+        else:
+            self.get_label = get_label
+
         self.allow_blank = allow_blank
         self.blank_text = blank_text
         self._set_data(None)
 
         for obj in self.query:
             key = str(obj.key())
-            label = self.label_attr and getattr(obj, self.label_attr) or obj
+            label = self.get_label(obj)
             yield (key, label, self.data and ( self.data.key( ) == obj.key() ) )
 
     def process_formdata(self, valuelist):

File wtforms/ext/csrf/__init__.py

View file
+from wtforms.ext.csrf.form import SecureForm

File wtforms/ext/csrf/fields.py

View file
+from wtforms.fields import HiddenField
+
+
+class CSRFTokenField(HiddenField):
+    current_token = None
+
+    def _value(self):
+        """
+        We want to always return the current token on render, regardless of
+        whether a good or bad token was passed.
+        """
+        return self.current_token
+
+    def populate_obj(self, *args):
+        """
+        Don't populate objects with the CSRF token
+        """
+        pass

File wtforms/ext/csrf/form.py

View file
+from wtforms.form import Form
+from wtforms.validators import ValidationError
+
+from .fields import CSRFTokenField
+
+
+class SecureForm(Form):
+    """
+    Form that enables CSRF processing via subclassing hooks.
+    """
+    csrf_token = CSRFTokenField()
+
+    def __init__(self, formdata=None, obj=None, prefix='', csrf_context=None, **kwargs):
+        """
+        :param csrf_context: 
+            Optional extra data which is passed transparently to your 
+            CSRF implementation.
+        """
+        super(SecureForm, self).__init__(formdata, obj, prefix, **kwargs)
+        self.csrf_token.current_token = self.generate_csrf_token(csrf_context)
+
+    def generate_csrf_token(self, csrf_context):
+        """
+        Implementations must override this to provide a method with which one
+        can get a CSRF token for this form.
+
+        A CSRF token should be a string which can be generated
+        deterministically so that on the form POST, the generated string is
+        (usually) the same assuming the user is using the site normally.
+
+        :param csrf_context: 
+            A transparent object which can be used as contextual info for
+            generating the token.
+        """
+        raise NotImplementedError()
+
+    def validate_csrf_token(self, field):
+        """
+        Override this method to provide custom CSRF validation logic.
+
+        The default CSRF validation logic simply checks if the recently
+        generated token equals the one we received as formdata.
+        """
+        if field.current_token != field.data:
+            raise ValidationError(field.gettext(u'Invalid CSRF Token'))
+
+    @property
+    def data(self):
+        d = super(SecureForm, self).data
+        d.pop('csrf_token')
+        return d

File wtforms/ext/csrf/session.py

View file
+"""
+A provided CSRF implementation which puts CSRF data in a session.
+
+This can be used fairly comfortably with many `request.session` type
+objects, including the Werkzeug/Flask session store, Django sessions, and
+potentially other similar objects which use a dict-like API for storing
+session keys.
+
+The basic concept is a randomly generated value is stored in the user's
+session, and an hmac-sha1 of it (along with an optional expiration time,
+for extra security) is used as the value of the csrf_token. If this token
+validates with the hmac of the random value + expiration time, and the
+expiration time is not passed, the CSRF validation will pass.
+"""
+
+import hmac
+import os
+
+from hashlib import sha1
+from datetime import datetime, timedelta
+
+from ...validators import ValidationError
+from .form import SecureForm
+
+__all__ = ('SessionSecureForm', )
+
+class SessionSecureForm(SecureForm):
+    TIME_FORMAT = '%Y%m%d%H%M%S'
+    TIME_LIMIT = timedelta(minutes=30)
+    SECRET_KEY = None
+
+    def generate_csrf_token(self, csrf_context):
+        if self.SECRET_KEY is None:
+            raise Exception('must set SECRET_KEY in a subclass of this form for it to work')
+        if csrf_context is None:
+            raise TypeError('Must provide a session-like object as csrf context')
+
+        session = getattr(csrf_context, 'session', csrf_context)
+
+        if 'csrf' not in session:
+            session['csrf'] = sha1(os.urandom(64)).hexdigest()
+
+        self.csrf_token.csrf_key = session['csrf']
+        if self.TIME_LIMIT:
+            expires = (datetime.now() + self.TIME_LIMIT).strftime(self.TIME_FORMAT)
+            csrf_build = '%s%s' % (session['csrf'], expires)
+        else:
+            expires = ''
+            csrf_build = session['csrf']
+
+        hmac_csrf = hmac.new(self.SECRET_KEY, csrf_build.encode('utf8'), digestmod=sha1) 
+        return '%s##%s' % (expires, hmac_csrf.hexdigest())
+
+    def validate_csrf_token(self, field):
+        if not field.data or '##' not in field.data:
+            raise ValidationError(field.gettext(u'CSRF token missing'))
+
+        expires, hmac_csrf = field.data.split('##')
+
+        check_val = (field.csrf_key + expires).encode('utf8')
+
+        hmac_compare = hmac.new(self.SECRET_KEY, check_val, digestmod=sha1)
+        if hmac_compare.hexdigest() != hmac_csrf:
+            raise ValidationError(field.gettext(u'CSRF failed'))
+
+        if self.TIME_LIMIT:
+            now_formatted = datetime.now().strftime(self.TIME_FORMAT)
+            if now_formatted > expires:
+                raise ValidationError(field.gettext(u'CSRF token expired'))

File wtforms/ext/django/fields.py

View file
 Useful form fields for use with the Django ORM.
 """
 import operator
-import warnings
 
 from wtforms import widgets
 from wtforms.fields import SelectFieldBase
     """
     widget = widgets.Select()
 
-    def __init__(self, label=None, validators=None, queryset=None, get_label=None, label_attr=None, allow_blank=False, blank_text=u'', **kwargs):
+    def __init__(self, label=None, validators=None, queryset=None, get_label=None, allow_blank=False, blank_text=u'', **kwargs):
         super(QuerySetSelectField, self).__init__(label, validators, **kwargs)
         self.allow_blank = allow_blank
         self.blank_text = blank_text
         if queryset is not None:
             self.queryset = queryset.all() # Make sure the queryset is fresh
 
-        if label_attr is not None:
-            warnings.warn('label_attr= will be removed in WTForms 0.7, use get_label= instead.', DeprecationWarning)
-            self.get_label = operator.attrgetter(label_attr)
-        elif get_label is None:
+        if get_label is None:
             self.get_label = lambda x: x
         elif isinstance(get_label, basestring):
             self.get_label = operator.attrgetter(get_label)

File wtforms/ext/django/i18n.py

View file
+from django.utils.translation import ugettext, ungettext
+from wtforms import form
+
+class DjangoTranslations(object):
+    """
+    A translations object for WTForms that gets its messages from django's
+    translations providers.
+    """
+    def gettext(self, string):
+        return ugettext(string)
+
+    def ngettext(self, singular, plural, n):
+        return ungettext(singular, plural, n)
+
+
+class Form(form.Form):
+    """
+    A Form derivative which uses the translations engine from django.
+    """
+    _django_translations = DjangoTranslations()
+
+    def _get_translations(self):
+        return self._django_translations

File wtforms/ext/i18n/__init__.py

View file
+

File wtforms/ext/i18n/form.py

View file
+from wtforms import form
+from wtforms.ext.i18n.utils import get_translations
+
+translations_cache = {}
+
+class Form(form.Form):
+    """
+    Base form for a simple localized WTForms form.
+
+    This will use the stdlib gettext library to retrieve an appropriate
+    translations object for the language, by default using the locale
+    information from the environment.
+
+    If the LANGUAGES class variable is overridden and set to a sequence of
+    strings, this will be a list of languages by priority to use instead, e.g::
+
+        LANGUAGES = ['en_GB', 'en']
+
+    Translations objects are cached to prevent having to get a new one for the
+    same languages every instantiation. 
+    """
+    LANGUAGES = None
+
+    def _get_translations(self):
+        languages = tuple(self.LANGUAGES) if self.LANGUAGES else None
+        if languages not in translations_cache:
+            translations_cache[languages] = get_translations(languages)
+        return translations_cache[languages]

File wtforms/ext/i18n/messages/README.txt

View file
+=================================
+Translation Submission Guidelines
+=================================
+
+To create a translation, the easiest way to start is to run:
+
+ $ python setup.py init_catalog --locale <your locale>
+
+Which will copy the template to the right location. To run that setup.py
+sub-command, you need Babel and setuptools/distribute installed.
+
+.po files:
+ - must be a valid utf-8 text file
+ - should have the header filled out appropriately
+ - should translate all messages
+
+You probably want to try setup.py compile_catalog and try loading your
+translations up to verify you did it all right.
+
+Submitting
+----------
+
+The best ways to submit your translation are as a pull request on bitbucket, or
+an email to james+i18n@simplecodes.com, with the file included as an attachment.
+
+utf-8 text may not format nicely in an email body, so please refrain from
+pasting the translations into an email body, and include them as an attachment
+instead. Also do not post translation files in the issue tracker text box, or
+onto the mailing list either, because again formatting may be broken.

File wtforms/ext/i18n/messages/en/LC_MESSAGES/wtforms.po

View file
+# English (United States) translations for WTForms.
+# Copyright (C) 2012 WTForms Team
+# This file is distributed under the same license as the WTForms project.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: WTForms 0.6.4dev\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2012-01-31 12:58-0700\n"
+"PO-Revision-Date: 2012-01-31 13:03-0700\n"
+"Last-Translator: James Crasta <james@simplecodes.com>\n"
+"Language-Team: en_US <james+i18n@simplecodes.com>\n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 0.9.6\n"
+
+#: wtforms/validators.py:51
+#, python-format
+msgid "Invalid field name '%s'."
+msgstr "Invalid field name '%s'."
+
+#: wtforms/validators.py:58
+#, python-format
+msgid "Field must be equal to %(other_name)s."
+msgstr "Field must be equal to %(other_name)s."
+
+#: wtforms/validators.py:90
+#, python-format
+msgid "Field must be at least %(min)d character long."
+msgid_plural "Field must be at least %(min)d characters long."
+msgstr[0] "Field must be at least %(min)d character long."
+msgstr[1] "Field must be at least %(min)d characters long."
+
+#: wtforms/validators.py:93
+#, python-format
+msgid "Field cannot be longer than %(max)d character."
+msgid_plural "Field cannot be longer than %(max)d characters."
+msgstr[0] "Field cannot be longer than %(max)d character."
+msgstr[1] "Field cannot be longer than %(max)d characters."
+
+#: wtforms/validators.py:96
+#, python-format
+msgid "Field must be between %(min)d and %(max)d characters long."
+msgstr "Field must be between %(min)d and %(max)d characters long."
+
+#: wtforms/validators.py:131
+#, python-format
+msgid "Number must be greater than %(min)s."
+msgstr "Number must be greater than %(min)s."
+
+#: wtforms/validators.py:133
+#, python-format
+msgid "Number must be less than %(max)s."
+msgstr "Number must be less than %(max)s."
+
+#: wtforms/validators.py:135
+#, python-format
+msgid "Number must be between %(min)s and %(max)s."
+msgstr "Number must be between %(min)s and %(max)s."
+
+#: wtforms/validators.py:171
+msgid "This field is required."
+msgstr "This field is required." 
+
+#: wtforms/validators.py:199
+msgid "Invalid input."
+msgstr "Invalid input."
+
+#: wtforms/validators.py:218
+msgid "Invalid email address."
+msgstr "Invalid email address."
+
+#: wtforms/validators.py:235
+msgid "Invalid IP address."
+msgstr "Invalid IP address."
+
+#: wtforms/validators.py:253
+msgid "Invalid Mac address."
+msgstr "Invalid MAC address."
+
+#: wtforms/validators.py:278
+msgid "Invalid URL."
+msgstr "Invalid URL."
+
+#: wtforms/validators.py:296
+msgid "Invalid UUID."
+msgstr "Invalid UUID."
+
+#: wtforms/validators.py:323
+#, python-format
+msgid "Invalid value, must be one of: %(values)s."
+msgstr "Invalid value, must be one of: %(values)s."
+
+#: wtforms/validators.py:350
+#, python-format
+msgid "Invalid value, can't be any of: %(values)s."
+msgstr "Invalid value, can't be any of: %(values)s."
+
+#: wtforms/ext/appengine/fields.py:74 wtforms/ext/django/fields.py:90
+#: wtforms/ext/sqlalchemy/fields.py:122 wtforms/ext/sqlalchemy/fields.py:172
+#: wtforms/ext/sqlalchemy/fields.py:177 wtforms/fields/core.py:413
+msgid "Not a valid choice"
+msgstr "Not a valid choice"
+
+#: wtforms/ext/appengine/fields.py:93
+msgid "Not a valid list"
+msgstr "Not a valid list"
+
+#: wtforms/ext/csrf/form.py:45
+msgid "Invalid CSRF Token"
+msgstr "Invalid CSRF Token"
+
+#: wtforms/ext/csrf/session.py:56
+msgid "CSRF token missing"
+msgstr "CSRF token missing"
+
+#: wtforms/ext/csrf/session.py:64
+msgid "CSRF failed"
+msgstr "CSRF failed"
+
+#: wtforms/ext/csrf/session.py:69
+msgid "CSRF token expired"
+msgstr "CSRF token expired"
+
+#: wtforms/ext/dateutil/fields.py:48
+msgid "Please input a date/time value"
+msgstr "Please input a date/time value"
+
+#: wtforms/ext/dateutil/fields.py:60
+msgid "Invalid date/time input"
+msgstr "Invalid date/time input"
+
+#: wtforms/fields/core.py:406
+msgid "Invalid Choice: could not coerce"
+msgstr "Invalid Choice: could not coerce"
+
+#: wtforms/fields/core.py:439
+msgid "Invalid choice(s): one or more data inputs could not be coerced"
+msgstr "Invalid choice(s): one or more data inputs could not be coerced"
+
+#: wtforms/fields/core.py:446
+#, python-format
+msgid "'%(value)s' is not a valid choice for this field"
+msgstr "'%(value)s' is not a valid choice for this field"
+
+#: wtforms/fields/core.py:500
+msgid "Not a valid integer value"
+msgstr "Not a valid integer value"
+
+#: wtforms/fields/core.py:546
+msgid "Not a valid decimal value"
+msgstr "Not a valid decimal value"
+
+#: wtforms/fields/core.py:572
+msgid "Not a valid float value"
+msgstr "Not a valid float value"
+

File wtforms/ext/i18n/messages/fa/LC_MESSAGES/wtforms.po

View file
+# Farsi translations for WTForms.
+# Copyright (C) 2012 WTForms Team
+# This file is distributed under the same license as the WTForms project.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: WTForms 0.6.4dev\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2012-01-31 12:58-0700\n"
+"PO-Revision-Date: 2012-02-01 09:01+0330\n"
+"Last-Translator: Efazati <mohammad@efazati.org>\n"
+"Language-Team: fa <mohammad@efazati.org>\n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 0.9.6\n"
+
+#: wtforms/validators.py:51
+#, python-format
+msgid "Invalid field name '%s'."
+msgstr "فیلد '%s' اشتباه است."
+
+#: wtforms/validators.py:58
+#, python-format
+msgid "Field must be equal to %(other_name)s."
+msgstr "مقدار فیلد باید برابر %(other_name)s باشد."
+
+#: wtforms/validators.py:90
+#, python-format
+msgid "Field must be at least %(min)d character long."
+msgid_plural "Field must be at least %(min)d characters long."
+msgstr[0] "طول فیلد حداقل باید %(min)d حرف باشد."
+msgstr[1] "طول فیلد حداقل باید %(min)d حرف باشد."
+
+#: wtforms/validators.py:93
+#, python-format
+msgid "Field cannot be longer than %(max)d character."
+msgid_plural "Field cannot be longer than %(max)d characters."
+msgstr[0] "Field cannot be longer than %(max)d character."
+msgstr[1] "طول فیلد حداکثر باید %(max)d حرف باشد."
+
+#: wtforms/validators.py:96
+#, python-format
+msgid "Field must be between %(min)d and %(max)d characters long."
+msgstr "طول فیلد باید بین %(min)d تا (max)d حرف باشد."
+
+#: wtforms/validators.py:131
+#, python-format
+msgid "Number must be greater than %(min)s."
+msgstr "عدد باید از %(min)s بزرگتر باشد."
+
+#: wtforms/validators.py:133
+#, python-format
+msgid "Number must be less than %(max)s."
+msgstr "عدد باید از %(max)s. کوچکتر باشد."
+
+#: wtforms/validators.py:135
+#, python-format
+msgid "Number must be between %(min)s and %(max)s."
+msgstr "عدد باید بین %(min)s  تا  %(max)s باشد."
+
+#: wtforms/validators.py:171
+msgid "This field is required."
+msgstr "این فیلد اجباریست."
+
+#: wtforms/validators.py:199