how do i show form errors on a template ?

11 views
Skip to first unread message

Jonathan Vanasco

unread,
Mar 10, 2008, 12:56:11 AM3/10/08
to pylons-discuss
I made a FormValidator subclass called DatefieldsValidator (below) ,
which validates a day/month/year field

how should I push errors from there into the template ? can helpers
be used?


---
class DatefieldsValidator(formencode.validators.FormValidator):
"""
Checks if a date is valid

DatefieldsValidator('birth_year','birth_month','birth_day',
require_years_ago=13)
"""
# tells UniqueNameValidator where to unpack the args passed to
__init__
__unpackargs__ = ('*','field_names')
field_names = None

require_years_ago= None

messages = {
'date_invalid':"The date you submitted (%(date_string)s) is
not a real date.",
'date_too_late':"The date you submitted (%(date_string)s) is
not before the cutoff date (%(latest_date_string)s).",
}

def validate_python(self, field_dict, state):
print self.__dict__
date_string= "%04d-%02d-%02d" % (
int(field_dict[self.field_names[0]]),
int(field_dict[self.field_names[1]]),
int(field_dict[self.field_names[2]]),
)
date= None
try:
date= time.strptime(date_string,"%Y-%m-%d")
except:
msg = self.message('date_invalid', state, date_string=
date_string)
raise formencode.api.Invalid(msg, field_dict, state,
error_dict={'form':msg})

if self.require_years_ago:
latest_date_string= datetime.date.today() -
relativedelta(years= self.require_years_ago)
latest_date= time.strptime( "%s"%latest_date_string, "%Y-
%m-%d")
timedelta= time.mktime(latest_date) - time.mktime(date)
if timedelta < 0:
msg = self.message('date_too_late', state,
date_string= date_string, latest_date_string= latest_date_string)
raise formencode.api.Invalid(msg, field_dict, state,
error_dict={'form':msg})




Junya Hayashi

unread,
Mar 10, 2008, 6:11:13 AM3/10/08
to pylons-discuss
Hi Jonathan,

You can get a formencode.api.Invalid object with try-catch directive
in controller.
The object contains error messages, so you can use it in templates.
For example, you can pass "error_dict" to templates like this.

----
from formencode.api import Invalid

class SomeController(BaseControllre):
....
try:
converted = schema.PageSchema().to_python(request.params)
except Invalid, e:
c.error_dict = e.error_dict
render('/edit.mako')


If you want to output error messages automatically,
formencode.htmlfill is useful.

----
from formencode.api import Invalid
from formencode import htmlfill

class SomeController(BaseControllre):
....
try:
converted = schema.PageSchema().to_python(request.params)
except Invalid, e:
htmlfill.render(render('/edit.mako'), defaults=request.params,
errors=c.error_dict)


In default, htmlfill puts error messages just after the input tag.
If you want some other options, please check the source of
formencode.htmlfill.render.

----
Junya Hayashi <ledmo...@gmail.com>

Jonathan Vanasco

unread,
Mar 10, 2008, 11:26:21 AM3/10/08
to pylons-discuss
thanks for the info.

I should have clarified...

I'm not generating the entire form...

i'm using the helpers like h.text_field('email'), and a @validator
decorator

I don't know how to push an error from the schema into the errors dict
its complicated, because the dict has the named files like
'birth_date','birth_month' - which are all field validators
but this compound/chained validator isn't mapped to a name - and I
can't find any docs suggesting how to accomplish this

Mike Orr

unread,
Mar 10, 2008, 4:14:42 PM3/10/08
to pylons-...@googlegroups.com
On Mon, Mar 10, 2008 at 8:26 AM, Jonathan Vanasco <jvan...@gmail.com> wrote:
> I'm not generating the entire form...
>
> i'm using the helpers like h.text_field('email'), and a @validator
> decorator
>
> I don't know how to push an error from the schema into the errors dict
> its complicated, because the dict has the named files like
> 'birth_date','birth_month' - which are all field validators
> but this compound/chained validator isn't mapped to a name - and I
> can't find any docs suggesting how to accomplish this

This is an area that's not documented as well as it should be. It
gets confusing when to raise Invalid and when to return an error
string or error dict, and how you propagate the error message to a
group of HTML controls rather than a single one. I'm collecting
material for a HOWTO, so if anybody has an answer please post it.

With @validate, you put an error placeholder in the template with a
key that can be the field name or any string (for groups of fields).
Then you have to convince FormEncode to set that error key, which is
the harder part. With a normal single-field validator you just raise
Invalid. With multi-field validation you'd put the validator in
.chained_validators, and I guess set an error dict key.

I've done multi-field validation for composite widgets a bit
differently. I use "field.subfield" names to get a dict of subfield
values on the Python side. Then I define certain subfield keys as
"Python values" and others as "HTML values". So ._from_python sets
one of them from the other, and ._to_python does the opposite. But
since I don't call htmlfill to set the initial values (because that
has problems with nonstandard types like dates and booleans), I have
an extra method that returns a compatible dict from discrete database
values (that's a subset of what ._from_python does), and use that to
set my initial values in the template.

--
Mike Orr <slugg...@gmail.com>

Jonathan Vanasco

unread,
Mar 10, 2008, 8:03:17 PM3/10/08
to pylons-discuss
mike-
again, thanks!

could i pester you for an example of what you do?

in the past 24 hours, i've learned that no one seems to be happy with
chained_validators. everything i've read on here, formencode and tg
suggests that everyone finds getting the error message our of it a
PITA

its kind of disappointing.

Jonathan Vanasco

unread,
Mar 10, 2008, 11:30:59 PM3/10/08
to pylons-discuss
> This is an area that's not documented as well as it should be.  It
> gets confusing when to raise Invalid and when to return an error
> string or error dict, and how you propagate the error message to a
> group of HTML controls rather than a single one.  I'm collecting
> material for a HOWTO, so if anybody has an answer please post it.

After combing through the pylons and formencode source, along with
search engine caches... this can help someone take this to the next
level:

I instantiate the chained_validator as such:

chained_validators= [
FormencodeExtensions.DatefieldsValidator('birth_year','birth_month','birth_day',
require_years_ago= 13, error_target= 'birth_date', ),
]

notice the error_target....

in the class, i push the error into the target:

class DatefieldsValidator(formencode.validators.FormValidator):
"""
Checks if a date is valid

DatefieldsValidator('birth_year','birth_month','birth_day',
require_years_ago=13)
"""
# tells UniqueNameValidator where to unpack the args passed to
__init__
__unpackargs__ = ('*','field_names')
field_names = None

require_years_ago= None

messages = {
'date_invalid':"The date you submitted (%(date_string)s) is
not a real date.",
'date_too_late':"The date you submitted (%(date_string)s) is
not before the cutoff date (%(latest_date_string)s).",
}

def validate_python(self, field_dict, state):
date_string= "%04d-%02d-%02d" % (
int(field_dict[self.field_names[0]]),
int(field_dict[self.field_names[1]]),
int(field_dict[self.field_names[2]]),
)
date= None
try:
date= time.strptime(date_string,"%Y-%m-%d")
except:
msg = self.message('date_invalid', state, date_string=
date_string)
raise formencode.api.Invalid(msg, field_dict, state,
error_dict={self.error_target:msg})

if self.require_years_ago:
latest_date_string= datetime.date.today() -
relativedelta(years= self.require_years_ago)
latest_date= time.strptime( "%s"%latest_date_string, "%Y-
%m-%d")
timedelta= time.mktime(latest_date) - time.mktime(date)
if timedelta < 0:
msg = self.message('date_too_late', state,
date_string= date_string, latest_date_string= latest_date_string)
raise formencode.api.Invalid(msg, field_dict, state,
error_dict={self.error_target:msg})

------------

in order to get that to display on the page - and not at the top of
the form - i need to use this in my template:

<form:error name="birth_date">

thats because of the call to formencode.htmfill.render --
If ``auto_insert_errors`` is true (the default) then any errors
for which ``<form:error>`` tags can't be found will be put just
above the associated input field, or at the top of the form if no
field can be found.


the caveat to this, is that this text will appear on templates that
don't have an error:

<form:error name="birth_date">

there's a solution here:
http://wiki.pylonshq.com/display/pylonsdocs/Form+Handling?focusedCommentId=7995399#comment-7995399

but its a bit messy and outdated

Mike Orr

unread,
Mar 11, 2008, 2:01:01 PM3/11/08
to pylons-...@googlegroups.com
On Mon, Mar 10, 2008 at 5:03 PM, Jonathan Vanasco <jvan...@gmail.com> wrote:
> could i pester you for an example of what you do?

This is a quantity range widget. The HTML is:

Min _________ - Max ________ Unit [pulldown]

The HTML fields are "fieldname.entered_min", "fieldname.entered_max",
"fieldname.unit". In the database I have to store all these so I can
redisplay what was entered, and also numeric equivalents for searching
(six values total). To the validator this appears as a sub-dictionary
with six keys. The pulldown has a combination of several mass units
and volume units.

My main validator looks like this:

===
class TheFormValidator(v.Schema):
allow_extra_fields = True
filter_extra_fields = False
fieldname = Range()
fieldname2 = Range()
... other fields ...
pre_validators = [NestedVariables()]
chained_validators = []
===

The Range validator looks like this:

===
import formencode.validators as v

unit_conversion import convert

# Units for input/display. Internal units are gallons and pounds.
VOLUME_UNITS = ['gallons', 'barrels', 'cubic meters']
MASS_UNITS = ['pounds', 'kilograms', 'tons', 'metric tons']

class Range(v.FancyValidator):
"""A composite widget for a quantity range.

The value is a Python dict with the following keys.

KEYS FOR HTML VALUES:
entered_min: low value literal, exactly as entered by user. (string)
entered_max: high value literal, exactly as entered by user. (string)
unit: unit string chosen by user from pulldown. (string)

KEYS FOR PYTHON VALUES:
is_mass: True if the unit is a mass value, False if it's a volume value.
search_min: low value as a float or None. Normalized to gallons for
volume, or pounds for mass.
search_max: high value as a float or None. Normalized to gallons for
volume, or pounds for mass.

.from_python() copies `search_min` and `search_max` into `entered_min`
and `entered_max`, converting the values to `unit`. If any error,
set the HTML keys to the null value. (None, None, "gallons")

.to_python() converts and copies the HTML values to the Python values,
raising Invalid if the input is invalid.

.get_html_values(search_min, search_max, is_mass) builds a compatible dict
from discrete database values.

.get_null_values() builds a compatible dict from the null value.
(min=None, max=None, unit="gallons")

Validation (from HTML):
- `entered_min` and `entered_max` must each be a valid float after
removing commas and whitespace, or blank.
- If both `entered_min` and `entered_max` are blank, change other values
to None, None, False, "gallons".
- If `entered_min` is filled in, `unit` must be also.
- If `entered_max` is filled in, `entered_min` and `unit` must be also,
and `entered_min` must not be numerically greater than `entered_max`.
"""

strip = True

def _from_python(self, field_dict, state):
fd = field_dict
return self.get_html_values(fd["search_min"], fd["search_max"],
fd["is_mass"])

def _to_python(self, field_dict, state):
errors = []
emin = field_dict["entered_min"]
emax = field_dict["entered_max"]
unit = field_dict["unit"]
if not (emin or emax):
return self.get_null_values()
# Parse the individual values and check for errors.
smin = self._parse_float(emin)
smax = self._parse_float(emax)
if smin is False:
errors.append("invalid low value")
if smax is False:
errors.append("invalid high value")
if unit in MASS_UNITS:
is_mass = True
unit_type = "mass"
to_unit = "pounds"
elif unit in VOLUME_UNITS:
is_mass = False
unit_type = "volume"
to_unit = "gallons"
else:
errors.append("invalid unit")
if errors:
message = "; ".join(errors)
raise v.Invalid(message, field_dict, state)
# Check for multi-field errors.
is_smin = smin is not None
is_smax = smax is not None
if is_smin and is_smax:
if smin > smax:
message = "low value can't be greater than high value"
raise v.Invalid(message, field_dict, state)
elif smin:
smax = smin
elif smax:
message = "can't specify high value without low value"
raise v.Invalid(message, field_dict, state)
# Convert values to `to_unit`. Not catching converter exceptions.
if is_smin:
smin = convert(unit_type, unit, to_unit, smin)
if is_smax:
smax = convert(unit_type, unit, to_unit, smax)
# Success.
field_dict["search_min"] = smin
field_dict["search_max"] = smax
field_dict["is_mass"] = is_mass
return field_dict

def get_html_values(self, search_min, search_max, is_mass):
return {
"entered_min": str(search_min) if search_min is not None else "",
"entered_max": str(search_max) if search_max is not None else "",
"unit": "pounds" if is_mass else "gallons",
"search_min": search_min,
"search_max": search_max,
"is_mass": is_mass,
}

def get_null_values(self):
return self.get_html_values(None, None, False)

#### Private helper methods.
def _parse_float(self, s):
"""Parse a numeric string.

Return `s` as a float, None if it's blank, or False if float() raises
an exception.
"""
s = s.replace(",", "").replace(" ", "")
if not s:
return None
try:
value = float(s)
except ValueError:
return False
return value
===


I'm sorry FormEncode is so frustrating for you. I've had my own
similar trouble with it. I avoided it for a long time because I
didn't need complex forms, but now I do. Like it or not, there's no
alternative to FormEncode, at least from the perspective of choosing
Pylons' default form handler. ToscaWidgets does not do validation,
just rendering. It also has a lot of dependencies including C
libraries (which would make Pylons harder to install on
Windows/Macintosh), and the documentation does not go beyond simple
cases. Django Newforms raises as many problems as it solves.
FormAlchemy and dbsprockets are too specialized. Others like
Quixote's form handler are too framework-specific. But any of these
would be fine for certain Pylons applications if you like them. But
only FormEncode/htmlfill follows the Pylons philosophy of "small,
sharp tools" (libraries that do one thing well and don't try to do too
many things). FormEncode may not be as good as we like, but it has
potential for improvement. The documentation hole will be addressed
at the PyCon sprint, and I expect it will be completed in a month or
two.

--
Mike Orr <slugg...@gmail.com>

Jonathan Vanasco

unread,
Mar 11, 2008, 6:07:16 PM3/11/08
to pylons-discuss
thanks mike. i unfortunately live for complex forms. at least i've
never had a 'simple' form use -- almost every form i've ever used has
required some sort of chained dependancy - so i'm used to it.


i wrote a form class in perl that has been wonderful for the past few
years. maybe i should port it. it is kind of like formbuilder (in
perl), but much more lightweight and extensible

basically, it does:
1- define form schema as a class / subclass of other schemas
2- describe fields / elements as dict-like structure. assign
defaults or generators.
3- assign 0 or more validators per field
4- assign 0 or more pre/post validation hooks
5- define validation order, if any

i love it, not because i built it ( and its buggy! ) - but because it
just does all the description / validation / accessing easily.
Reply all
Reply to author
Forward
0 new messages