Plone Archetypes Field Validator
In My First Minimal Plone Content Type we creates a simple content type, let us now see how we can verify that a user inserts something meaningful in the fields. If we for example have a field called email we'd like the system to make sure that we enter something that looks like an email. A nice reference when it comes to Archetypes in Plone is the Archetypes Developer Manual, here is one chapter about fields: [1]
Some old (apparently) documentation can also be found here: [2]
I'd like to demonstrate two things when it comes to validators:
- How to use a standard validator
- How to write your own validator
I will illustrate the first one by having a content type that only contains an email field and a way to validate that a "valid" email is inserted (hopefully this validation also takes care of strange Irish emails like "o'connor@o'connor.com").
The second one we will implement from scratch, starting with a minimal validator that always fails to a simple validator that really does some validation.
How to use a standard validator
It is really easy to use ready made validators (for a comprehensive list see the Archetypes Developer Manual [3] ), you just add a key word argument in the call to the field-constructor, pretty much like this:
schema = BaseSchema.copy() + Schema(( StringField('email', required = 1, validators = ('isEmail'), ), ))
Trying to add a bad email now results in something like this:
And adding a nice regular email results in what you would have expected - a regular page:
This version of MyMessage is available here: [4]
How to write your own validator
Looking in the Archetypes Developer Manual we see the wonderful phrase - so often encountered in the open source community - See source for details, so let us do exactly that (remember: Use the source Luke).
Source Diving
In my Plone installation I find some interesting validators in the folder ~/Plone-3.0/zeocluster/Products/validation/validators and one of them is RangeValidator.py, a condensed version of it follows:
try: from Products.validation.interfaces.IValidator import IValidator except ImportError: import sys, os sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir)) from interfaces.IValidator import IValidator del sys, os class RangeValidator: __implements__ = IValidator def __init__(self, name, ...): self.name = name # ... def __call__(self, value, *args, **kwargs): # ... try: nval = float(value) except ValueError: return ("Validation failed [...]") # [wrong type] if minval <= nval < maxval: return 1 return ("Validation failed [...]") # [not in range]
Even if this does not make much sense in this condensed version you should notice some important parts of this code:
- import IValidator: apparently it isn't trivial to find IValidator so the authors use a dirty trick to find it if something fails.
- the class RangeValidator that "__implements__" IValidator: this it yet another appraoch to object oriented programming and interfaces. Instead of implementing inheritance of an interface (like in C# and I guess also in Java) the authors of the framework sets the member __implements__ to IValidator (meaning what in C# would be an inheritance of an interface). I do not really like this approach but I can live with it. The point is however that we claim to implement an interface called IValidator.
- the method __call__(self, value, *args, **kwargs) seems important.
Given this information we could easily implement our own validator - we will just dig a little more in the source code (Use the source Luke). We really should take a look at the interface we implement - IValidator, it's short and neat:
#~/Plone-3.0/zeocluster/Products/validation/interfaces/IValidator.py from interface import Interface, Attribute class IValidator(Interface): name = Attribute("name of the validator") title = Attribute("title or name of the validator") description = Attribute("description of the validator") def __call__(value, *args, **kwargs): """return True if valid, error string if not""" # ...
As we can see a Validator (something that implements IValidator) has the three members name, title and description. One method (__call__) is implemented that is well documented: return True if valid, error string if not.
Cut to the chase
Just to test the validators we implement a validator that always indicates an error - a very evil validator. I put the following code in a file called validator.py:
try: from Products.validation.interfaces.IValidator import IValidator except ImportError: import sys, os sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir)) from interfaces.IValidator import IValidator del sys, os class EvilValidator: __implements__ = IValidator def __init__(self, name, title='Evil validator', description='You will fail'): self.name = name self.title = title or name self.description = description def __call__(self, value, *args, **kwargs): return('Moahahahaha - you FAIL!')
I am not sure the strange try/catch is really needed, but let's keep just to make sure for now. As you can see the init-method does nothing really important and the call-method always returns an error string.
Of course we need to update our __init__.py-file, I added the following to the top of it:
from Products.validation import validation from validator import EvilValidator validation.register(EvilValidator('evilness'))
Here I give the evil validator the name evilness and register it as a validator.
Our schema needs to be updated in the following way (in message.py):
schema = BaseSchema.copy() + Schema(( StringField('email', required = 1, validators = ('evilness',), ), ))
And that should be it - any item entered in the field should now result in something like this:
You can find this version of my message here: [5]
my Fibonacci validator
I want to make a validator that checks is a value is a Fibonacci number. It is perhaps more complex that it should be but bear with me - it is not that bad really:
from Products.validation.interfaces.IValidator import IValidator class FibonacciValidator: __implements__ = IValidator def __init__(self, name, title=, description=): self.name = name self.title = title or name self.description = description def __call__(self, value, *args, **kwargs): # value must be integer try: value = int(value) except: return("'%s' is not an integer (illegal value)" % str(value)) # deal with negative and trivial cases first if value < 0: return("'%s' is negative (illegal value)" % str(value)) elif value==0 or value==1: return 1 else: low = 0 high = 1 # stop if the largest fibonacci is bigger than value while value > high: # do you remember how fibonacci numbers are defined? # hint: F(n+1) = F(n) + F(n-1) (low, high) = (high, low + high) if value==low or value==high: return 1 # we got this far - return error message return("'%d' is not a fibonacci number - try '%d'") % (value, high) # If we get this far something really bad happened # perhaps integer overflow or something else reallt annoying return "Congratulations, you entered a really bad number [:)]-|--<"
Similar changes as in the example above are needed (see download below if you want the code).
Adding numbers that are not Fibonacci numbers is bad.
But fibonacci numbers work fine:
To my surprise even quite large numbers are detected as bad...
...or good...
Negative values are filtered out:
Non-integers are also filtered out:
Download
The source code of my message including a Fibonacci validator is available right here: [6]
This page belongs in Kategori Programmering.
This page is part of a series of tutorials on Plone Cms.