Уникальный валидатор в WTForms с использованием моделей SQLAlchemy
Я определил некоторые формы WTForms в приложении, которое использует SQLALchemy для управления операциями с базой данных.
Например, форма управления категориями:
class CategoryForm(Form):
name = TextField(u'name', [validators.Required()])
И вот соответствующая модель SQLAlchemy:
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
Я хотел бы добавить уникальное ограничение для проверки формы (не для самой модели).
Считая документацию WTForms, я нашел способ сделать это с помощью простого класса:
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)
Теперь я могу добавить этот валидатор в CategoryForm следующим образом:
name = TextField(u'name', [validators.Required(), Unique(Category, Category.name)])
Эта проверка отлично работает, когда пользователь пытается добавить категорию, которая уже существует \o/
НО он не будет работать, если пользователь попытается обновить существующую категорию (без изменения атрибута имени).
Если вы хотите обновить существующую категорию: вы создадите форму с атрибутом category для редактирования:
def category_update(category_id):
""" update the given category """
category = Category.query.get(category_id)
form = CategoryForm(request.form, category)
Основная проблема заключается в том, что я не знаю, как получить доступ к существующему объекту категории в валидаторе, который позволил бы мне исключить отредактированный объект из запроса.
Есть ли способ сделать это? Спасибо.
Ответы
Ответ 1
На этапе проверки вы получите доступ ко всем полям. Итак, трюк здесь состоит в том, чтобы передать первичный ключ в вашу форму редактирования, например.
class CategoryEditForm(CategoryForm):
id = IntegerField(widget=HiddenInput())
Затем в уникальном валидаторе измените if-условие на:
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):
Ответ 2
Хотя это не прямой ответ, я добавляю его, потому что этот вопрос флиртует с XY Problem. WTForms основное задание - это проверка содержимого формы. В то время как можно было бы сделать достойное дело, что проверка того, что полевая уникальность может считаться ответственностью валидатора формы, может быть лучше, если за это лежит ответственность механизма хранения.
В тех случаях, когда мне была предоставлена эта проблема, я рассматривал уникальность как оптимистичный случай, разрешал ему передавать форму и отказываться от ограничения базы данных. Затем я поймаю ошибку и добавлю ошибку в форму.
Преимущества несколько. Сначала это значительно упрощает ваш WTForms код, потому что вам не нужно писать сложные схемы проверки. Во-вторых, это может улучшить производительность вашего приложения. Это связано с тем, что вам не нужно отправлять SELECT
, прежде чем пытаться INSERT
эффективно удвоить свой трафик базы данных.
Ответ 3
Уникальный валидатор должен использовать новые и старые данные для сравнения сначала, прежде чем проверять, уникальны ли данные.
class Unique(object):
...
def __call__(self, form, field):
if field.object_data == field.data:
return
check = DBSession.query(model).filter(field == data).first()
if check:
raise ValidationError(self.message)
Кроме того, вы можете также сквозировать нули. В зависимости от того, действительно ли вы уникальны или уникальны, но допускаете null.
Я использую WTForms 1.0.5 и SQLAlchemy 0.9.1.
Ответ 4
Заявление
from wtforms.validators import ValidationError
class Unique(object):
def __init__(self, model=None, pk="id", get_session=None, message=None,ignoreif=None):
self.pk = pk
self.model = model
self.message = message
self.get_session = get_session
self.ignoreif = ignoreif
if not self.ignoreif:
self.ignoreif = lambda field: not field.data
@property
def query(self):
self._check_for_session(self.model)
if self.get_session:
return self.get_session().query(self.model)
elif hasattr(self.model, 'query'):
return getattr(self.model, 'query')
else:
raise Exception(
'Validator requires either get_session or Flask-SQLAlchemy'
' styled query parameter'
)
def _check_for_session(self, model):
if not hasattr(model, 'query') and not self.get_session:
raise Exception('Could not obtain SQLAlchemy session.')
def __call__(self, form, field):
if self.ignoreif(field):
return True
query = self.query
query = query.filter(getattr(self.model,field.id)== form[field.id].data)
if form[self.pk].data:
query = query.filter(getattr(self.model,self.pk)!=form[self.pk].data)
obj = query.first()
if obj:
if self.message is None:
self.message = field.gettext(u'Already exists.')
raise ValidationError(self.message)
Чтобы использовать его
class ProductForm(Form):
id = HiddenField()
code = TextField("Code",validators=[DataRequired()],render_kw={"required": "required"})
name = TextField("Name",validators=[DataRequired()],render_kw={"required": "required"})
barcode = TextField("Barcode",
validators=[Unique(model= Product, get_session=lambda : db)],
render_kw={})