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