Commits

jean-philippe serafin committed 270fbd8 Merge
  • Participants
  • Parent commits 27e64cf, 1ae3d52

Comments (0)

Files changed (17)

File .hgignore

File contents unchanged.
   * 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
 
+- 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
 -------------

File docs/ext.rst

 ~~~~~~~~~~~~~~~~~~~~~~~
 .. 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)
 

File docs/i18n.rst

 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.
 
 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
         'wtforms.ext.i18n',
         'wtforms.ext.sqlalchemy',
     ],
+    package_data={
+        'wtforms.ext.i18n': ['messages/wtforms.pot', 'messages/*/*/*'],
+    },
     **extra
 )

File tests/ext_appengine/tests.py

         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

 from wtforms.ext.csrf import SecureForm
 from wtforms.ext.csrf.session import SessionSecureForm
 
+import datetime
 import hashlib
 import hmac
 
     def __init__(self, session):
         self.session = session
 
+class StupidObject(object):
+    a = None
+    csrf_token = None
+
 
 class SecureFormTest(TestCase):
     def test_base_class(self):
         self.assertEqual(form.data, {'a': None})
 
     def test_with_data(self):
-        post_data = DummyPostData(csrf_token=u'test', a='hi')
+        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'})
         # 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')
     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
         assert form.csrf_token._value() != session['csrf']
         assert not form.validate()
         self.assertEqual(form.csrf_token.errors[0], u'CSRF failed')
-        # TODO: More stringent test with timestamps and all that
+        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 = {}

File tests/fields.py

         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):
     class F(Form):
         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)

File tests/runtests.py

 import sys
 from unittest import defaultTestLoader, TextTestRunner, TestSuite
 
-TESTS = ('form', 'fields', 'validators', 'widgets', 'webob_wrapper', 'translations', 'ext_csrf')
+TESTS = ('form', 'fields', 'validators', 'widgets', 'webob_wrapper', 'translations', 'ext_csrf', 'ext_i18n')
 
 def make_suite(prefix='', extra=()):
     tests = TESTS + extra
 
 def main():
     extra_tests = tuple(x for x in sys.argv[1:] if '-' not in x)
-    suite = make_suite('', )
+    suite = make_suite('', extra_tests)
 
     sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
 

File wtforms/ext/appengine/db.py

 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)
 
 

File wtforms/ext/appengine/fields.py

 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/django/fields.py

 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/i18n/form.py

     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:
+    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

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

+=================================
+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/utils.py

     """
     Get a WTForms translation object which wraps the builtin GNUTranslations object.
     """
-    return DefaultTranslations(get_builtin_gnu_translations(languages))
+    translations = get_builtin_gnu_translations(languages)
+
+    if hasattr(translations, 'ugettext'):
+        return DefaultTranslations(translations)
+    else:
+        # Python 3 has no ugettext/ungettext, so just return the translations object.
+        return translations
 
 
 class DefaultTranslations(object):

File wtforms/fields/core.py

             try:
                 self.data = int(valuelist[0])
             except ValueError:
+                self.data = None
                 raise ValueError(self.gettext(u'Not a valid integer value'))
 
 
             try:
                 self.data = decimal.Decimal(valuelist[0])
             except (decimal.InvalidOperation, ValueError):
+                self.data = None
                 raise ValueError(self.gettext(u'Not a valid decimal value'))
 
 
             try:
                 self.data = float(valuelist[0])
             except ValueError:
+                self.data = None
                 raise ValueError(self.gettext(u'Not a valid float value'))
 
 

File wtforms/validators.py

     Validates that the field contains data. This validator will stop the
     validation chain on error.
 
+    If the data is empty, also removes prior errors (such as processing errors)
+    from the field.
+
     :param message:
         Error message to raise in case of a validation error.
     """