Reputation: 4218
Have a look at this example
class MyForm(ForM):
...snip...
quantity = DecimalField(u'Quantity', [NumberRange(1, 8)])
...snip...
This works nicely if the user is co-operative and enters something that can be coerced to a numeric type. However, if the user enters, say: "asdf" into this field in the browser then DecimalField
throws a Type Error
when I try to render it in the browser.
Here's the relevant parts of the traceback:
Traceback (most recent call last):
File "/path/to/app/venv/local/lib/python2.7/site-packages/jinja2/environment.py", line 894, in render
return self.environment.handle_exception(exc_info, True)
File "/path/to/app/www/templates/submodule/user/submission.html", line 26, in block "submodule_content"
{{form.quantity}}
File "/path/to/app/venv/local/lib/python2.7/site-packages/wtforms/fields/core.py", line 139, in __call__
return self.widget(self, **kwargs)
File "/path/to/app/venv/local/lib/python2.7/site-packages/wtforms/widgets/core.py", line 123, in __call__
kwargs['value'] = field._value()
File "/path/to/app/venv/local/lib/python2.7/site-packages/wtforms/fields/core.py", line 542, in _value
return format % self.data
TypeError: a float is required
Instead of this behaviour, I'd like the field to have an error added to itself, so that I can still render t.
My current solution involves using a TextField
instead of a DecimalField
and providing an IsNumeric
validator with a call method like:
def __call__(self, form, field):
if field.data:
try:
field.data = float(field.data)
except ValueError as e:
raise ValidationError(self.message)
This works nearly perfectly, and is my current solution, but surely there must be a wtforms-idiomatic way to do this.
Upvotes: 1
Views: 5294
Reputation: 11543
I had to deal with similar problem. SelectMultipleField
with coerce=text_type
(which is a default kwarg) converts input data to str
/unicode
. That means it 'coerced' None
(a value fed into my field when user didn't select anything) into "None"
- which was huge pain to debug. your question clearly indicates that it was not only me who dove into wtforms internal validation process.
The correct solution I found is, you write a custom subclass of StringField
(textfield) and override __call__
, process_data
, and process_formdata
methods to suit your needs. __call__
is for rendering them into html(which should be done by another widget
callable), process_data
is usually for getting data from python objects(which means server-side), and process_formdata
is like process_data
except it deals with user-submitted forms. or, if you want DRY(don't repeat yourself), you can override __init__
and make your custom validator default.
edit:
The DecimalField
's idiotic code is what causes TypeError
. Take a look at its _value()
method, which is used by DecimalField
's rendering widget to get X for <input type="text" ... value=X>
def _value(self):
if self.raw_data:
return self.raw_data[0]
elif self.data is not None:
if self.places is not None: # <-- this line brings you the pain
if hasattr(self.data, 'quantize'):
exp = decimal.Decimal('.1') ** self.places
if self.rounding is None:
quantized = self.data.quantize(exp)
else:
quantized = self.data.quantize(exp, rounding=self.rounding)
return text_type(quantized)
else:
# If for some reason, data is a float or int, then format
# as we would for floats using string formatting.
format = '%%0.%df' % self.places
return format % self.data
else:
return text_type(self.data)
else:
return ''
it checks field.places
before the type of field.data
- field.places
is declared upon field creation, and is merely saying that you want N digits of precision; it does not indicate that you have N digit of Decimal
as your fields.data
, but wtforms devs somehow decided that checking field.places
is enough to assume the field contains corresponding floating point number. (the codes checks whether there is quantize
attribute on field.data
but that only works for decimals)
I think you have to write your own subclass of DecimalField
. here's my attempt at writing a non-idiot version of DecimalField
:
from wtforms.fields.core import DecimalField as _DecimalField
class DecimalField(_DecimalField):
def _value(self):
try:
float(self.data) #check whether you have a 'number'
return super(DeciamlField, self)._value()
except (TypeError, ValueError): #self.data is 'None', 'asdf' ...
return text_type(self.data) if self.data else ''
Upvotes: 3