"""This module contains classes to help validate user input.
Example usage, a simple one element form::
from aquarium.util.FormValid import Form, Field
(values, errors) = self.createValidator()(ctx.form)
if errors:
ctx.actionResults("Failed validation.")
return
# Now, use values instead of ctx.form, and continue on your merry way.
def createValidator():
return Form([
Field("source_ip", "IP is invalid",
regex=re.compile(r"\d+\.\d+\.\d+\.\d+"))
])
"""
__docformat__ = "restructuredtext"
# Created: Mon May 3 13:55:15 PDT 2004
# Author: Bob VanZant, Shannon -jj Behrens
# Email: bob@norcalttora.com, jjinux@users.sourceforge.net
#
# Copyright (c) Bob VanZant. All rights reserved.
# TODO:
# - Add support for multiple fields having same name (This is sort of already
# handled. Just never been tested.
# - Add support for per-Form and per-Field filters
#
import re
import types
class FieldInvalid(Exception):
"""Raise this when a field does not meet the proper type."""
def __init__(self, fields, *args):
Exception.__init__(self, *args)
self.fields = fields
######################################
class Form:
"""This class uses the member variable "constraints" as a list to store
elements of type class ``Field``. The form then represents a form of
user input controls, perhaps an HTML form. Add fields to this class,
call the ``validate()`` method and let the magic begin.
It is expected that you will usually just subclass this class and modify
``self.constraints`` at that time. Obviously you don't *need* to do
this.
Setting up rules is done when you add a Field and is documented there
"""
def __init__(self, constraints):
""" A Default constructor that is not often called. """
self.constraints = constraints
def __call__(self, values):
self.validate(values)
return self.flattenResults()
def validate(self, values):
"""The main magic worker. Loops over each of the field elements stored
in ``self.constraints`` and calls its ``validate()`` method. Takes
the results returned by the validation and updates its results dict.
Returns a dictionary containing all of the fields we validated,
indexed by field name.
"""
self.result = {}
for x in self.constraints:
self.result.update( x.validate(values) )
return self.result
def isValid(self):
"""Call this after doing a ``validate()``. Returns true if the form is
100% valid and false otherwise"""
for key in self.result.keys():
if self.result[key].invalid:
return False
# This used to use the stuff below. I don't quite no why
#for x in self.constraints:
# if x.invalid:
# return False
return True
def flattenResults(self):
values = {}
errors = {}
for name in self.result.keys():
values[name] = self.result[name].value
if self.result[name].invalid:
if isinstance(self.result[name].invalid, dict):
for item in self.result[name].invalid.keys():
errors[name + "[%s]" %item] = \
self.result[name].invalid[item]
else:
errors[name] = self.result[name].errorMsg
if errors:
return (None, errors)
else:
return (values, errors)
##############################################
class Field:
"""This class does the majority of the work. A field has many keyword
args that can be set at instantiation that define how the field is
validated. In addition there are two required parameters.
Required parameters:
name
The name of the field. Must be the same as the field name that is
used as the index in the values dict that is passed in to validate
against.
errorMsg
The error message to raise as part of the exception on failure.
Optional parameters:
cast
A function callback that will be used to attempt to cast a value
before attempting to validate it.
default
If given, this is the default value to use for validation when the
user did not specify one. To give the effect of an optional field in
your form, set the default to "".
function
A function callback that will be used to validate this value.
Expected to return true on success and false on failure. Function
should accept one argument, the value.
regex
A compiled regex object (i.e. the result of a ``re.compile()``) that
will be used to validate the value against using ``re.match()``
dependents
A list of tuples of the form ``(regex, Form)``. If the value of this
field matches the given regex we will attempt validation of ``Form``.
Otherwise, ``Form`` is ignored. Since ``Form`` is ignored any data the
user has entered into those fields will not be included in the results
dict and will thus require extra work to send them back to the user.
strip
A boolean that defaults to True stating whether or not to ``.strip()``
a string before attempting validation. Will catch ``AttributeError``
and pass if you tried to ``strip()`` a non-string.
"""
def __init__(self, name, errorMsg, **kargs):
# input params
self.name = name
self.errorMsg = errorMsg
self.options = kargs
# regular old member vars
self.value = ""
self.result = {}
self.invalid = False #innocent until proven guilty
def __repr__(self):
return (
"""<Field name="%s" inv="%s" errorMsg="%s" value="%s">\n""" %(
self.name, self.invalid, self.errorMsg,
self.value) )
def validate(self, values):
"""Attempts to validate this field. Because of dependencies it is
required that "values" be passed in as a dict object containing, at a
minimum, access to values for all Fields in this Field and its
dependencies.
As a postcondition, ``self.result`` is a dictionary containing all
of the ``Fields`` we validated (valid or invalid does not matter).
The dict is indexed by field name and contiains a ``Field`` object.
The value property represents the value after casting (if
applicable). Other properties include the ``errorMsg``, and whether
or not the field is ``invalid``.
Dependencies of a field, if validated, are returned as part of this
dictionary. There is no nesting, so the multi-level paradigm used
to enter a form is lost on return; the results are flattened.
"""
# Try to validate the field, act accordingly on failure/success
try:
if values.get(self.name, None):
self.value = values[self.name]
def valid_helper():
self._strip()
self._cast()
self._validate_function()
self._validate_regex()
if isinstance(self.value, dict):
self.invalid = {}
vals = self.value
finalValues = {}
for item in vals.keys():
self.value = vals[item]
try:
valid_helper()
finalValues[item] = self.value
except FieldInvalid:
self.invalid[item] = self.errorMsg
self.value = finalValues
else:
valid_helper()
elif self.options.has_key("default"):
self.value = self.options["default"]
else:
# Try to cast it anyway, if the cast yields a good error
# message, we'll use it, otherwise its no big deal.
self._cast()
raise FieldInvalid(self)
except FieldInvalid:
self.invalid = True
result = {self.name : self}
# Go validate dependencies
result.update( self._validate_dependencies(values) )
return result
def _strip(self):
"""Strips value, if we're supposed to as defined by the strip karg."""
if self.options.get("strip", True):
try:
self.value = self.value.strip()
except AttributeError:
pass
def _validate_dependencies(self, values):
"""Validates dependencies, if any. Loop over the items in the
dependents list, if the regex matches attempt validation of
the form, bubble up the results. Otherwise, ignore the form.
"""
result = {}
if self.options.has_key("dependents"):
for (condition, form) in self.options["dependents"]:
if condition.match(self.value):
result.update( form.validate(values) )
return result
def _cast(self):
"""Attempt to cast the data if we're supposed to"""
if self.options.has_key("cast"):
try:
self.value = self.options["cast"](self.value)
except:
# Must be a catchall because we don't care what exceptions
# get raised in the exception
raise FieldInvalid( self )
def _validate_function(self):
"""Call the function callback with ``self.value`` if it is defined.
Function may also be a list of functions.
The function should return True on success, False on failure.
Function should not raise any exceptions that it does not
catch itself.
"""
if not self.options.has_key("function"):
return
# Handle a list of functions
if isinstance(self.options["function"], types.ListType):
for func in self.options["function"]:
if not func(self.value):
raise FieldInvalid(self)
# Handle a single function
elif not self.options["function"](self.value):
raise FieldInvalid(self)
return
def _validate_regex(self):
"""Attempt to match against the regex object, if it exists"""
if not self.options.has_key("regex"):
return
if self.options["regex"].match(self.value):
return
else:
raise FieldInvalid(self)
class FieldGroup:
"""This class allows you to group fields together and do validation
based on the results of the validation of those fields
name
A string that represents the name of this group
function
A user-specified function callback that will be called with all of the
values of the group (as a dict) iff each of the values of the group
validate successfully on their own. To ensure that empty fields in a
group validate, use the ``default`` karg when creating each ``Field``.
This function need return a single string (error message) on error or
None if the group is valid.
"""
def __init__(self, name, function, fields, **kargs):
self.name = name
self.function = function
self.fields = fields
self.options = kargs
self.invalid = False
def validate(self, values):
"""Attempts validation of each of the Fields in this group. Returns
results consistent with ``Field.validate`` and ``Form.validate``"""
self.result = {}
for field in self.fields:
self.result.update( field.validate(values) )
values = {}
errors = {}
for name in self.result.keys():
values[name] = self.result[name].value
if self.result[name].invalid:
errors[name] = self.result[name].errorMsg
# Only attempt to validate the group if all fields are valid
if errors:
self.invalid = True
self.errorMsg = ""
else:
errorMsg = self.function(values)
if errorMsg:
self.errorMsg = errorMsg
self.invalid = True
self.result.update({ self.name : self })
return self.result
def __getattr__(self, name):
if name == "value":
return [ field.value
for field in self.fields ]
raise AttributeError("%s not a valid attribute" %name)
def validatePassword(values):
if values["password"] <> values["passwordConfirm"]:
return "Passwords do not match"
elif len(values["password"]) < 6:
return "Password is too short"
else:
return None
def validateMessageBody(values):
entered = values.get("msg_body", None)
upload = values.get("upload", None)
if entered and upload:
return "Please specify only one or the other"
elif (entered and not upload) or (not entered and upload):
return None
else:
return "You must either enter a message or select a file for upload"
reNumbersOnly = re.compile(r"[\d]")
reLettersOnly = re.compile(r"[\w_-]")
reAlphaNum = re.compile(r"[\w\d_-]")
|