Unique validator in WTForms with SQLAlchemy models

Jérôme Pigeot picture Jérôme Pigeot · Apr 16, 2011 · Viewed 7.9k times · Source

I defined some WTForms forms in an application that uses SQLALchemy to manage database operations.

For example, a form for managing Categories:

class CategoryForm(Form):
    name = TextField(u'name', [validators.Required()])

And here's the corresponding SQLAlchemy model:

class Category(Base):
    __tablename__= 'category'
    id = Column(Integer, primary_key=True)
    name = Column(Unicode(255))

    def __repr__(self):
        return '<Category %i>'% self.id

    def __unicode__(self):
        return self.name

I would like to add a unique constraint on the form validation (not on the model itself).

Reading the WTForms documentation, I found a way to do it with a simple class:

class Unique(object):
    """ validator that checks field uniqueness """
    def __init__(self, model, field, message=None):
        self.model = model
        self.field = field
        if not message:
            message = u'this element already exists'
        self.message = message

    def __call__(self, form, field):         
        check = self.model.query.filter(self.field == field.data).first()
        if check:
            raise ValidationError(self.message)

Now I can add that validator to the CategoryForm like this:

name = TextField(u'name', [validators.Required(), Unique(Category, Category.name)])

This check works great when the user tries to add a category that already exists \o/ BUT it won't work when the user tries to update an existing category (without changing the name attribute).

When you want to update an existing category : you'll instantiate the form with the category attribute to edit:

def category_update(category_id):
    """ update the given category """
    category = Category.query.get(category_id)
    form = CategoryForm(request.form, category)

The main problem is I don't know how to access the existing category object in the validator which would let me exclude the edited object from the query.

Is there a way to do it? Thanks.

Answer

sayap picture sayap · Nov 2, 2011

In the validation phase, you will have access to all the fields. So the trick here is to pass in the primary key into your edit form, e.g.

class CategoryEditForm(CategoryForm):
    id = IntegerField(widget=HiddenInput())

Then, in the Unique validator, change the if-condition to:

check = self.model.query.filter(self.field == field.data).first()
if 'id' in form:
    id = form.id.data
else:
    id = None
if check and (id is None or id != check.id):