#sqlalchemy #unique #validation #wtforms
#sqlalchemy #уникальный #проверка #wtforms
Вопрос:
Я определил некоторые формы 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/
НО он не будет работать, когда пользователь попытается обновить существующую категорию (без изменения атрибута name).
Если вы хотите обновить существующую категорию: вы создадите экземпляр формы с атрибутом category для редактирования:
def category_update(category_id):
""" update the given category """
category = Category.query.get(category_id)
form = CategoryForm(request.form, category)
Основная проблема в том, что я не знаю, как получить доступ к существующему объекту 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):
Комментарии:
1. Похоже, что это рассчитано на то, что клиент (браузер) вернет правильное значение id без изменений. Это верно? Или же серверный код должен сбросить поле id в форме после публикации, но перед проверкой?
2. @FutureNerd это действительно так. В моем случае я использую только
id
включение в область администратора и во внешнем интерфейсе, comparecurrent_user.email != form.email.data
что позволяет мне охватить оба примера. Но 1, поскольку это момент, о котором люди должны определенно знать.
Ответ №2:
Хотя это не прямой ответ, я добавляю его, потому что этот вопрос заигрывает с проблемой XY. WTForms основная задача — проверить, соответствует ли содержимое отправленной формы. Хотя можно было бы привести достойный довод в пользу того, что проверка уникальности поля может считаться обязанностью средства проверки формы, можно было бы привести более веский довод в пользу того, что за это отвечает механизм хранения.
В тех случаях, когда я с этой проблемой обращался уникальность как оптимистичный случае, позволил ей пройти форме представления, а не на базе ограничений. Затем я улавливаю сбой и добавляю ошибку в форму.
Преимуществ несколько. Во-первых, это значительно упрощает ваш код WTForms, потому что вам не нужно писать сложные схемы проверки. Во-вторых, это может повысить производительность вашего приложения. Это потому, что вам не нужно отправлять SELECT
перед попыткой INSERT
эффективного удвоения трафика вашей базы данных.
Комментарии:
1. Извините, не могли бы вы, пожалуйста, привести краткий пример кода, предпочтительно используя Flask? Или, если вы знаете, где я могу посмотреть на один, не могли бы вы, пожалуйста, предоставить ссылку?
Ответ №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)
Кроме того, вы также можете захотеть удалить нули. В зависимости от того, действительно ли вы уникальны или unique, но допускаете нули.
Я использую WTForms 1.0.5 и SQLAlchemy 0.9.1.
Комментарии:
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={})
Ответ №5:
Похоже, то, что вы ищете, может быть легко достигнуто с помощью ModelForm, которая создана для обработки форм, которые тесно связаны с моделями (модель категории в вашем случае).
Чтобы использовать его:
...
from wtforms_components import Unique
from wtforms_alchemy import ModelForm
class CategoryForm(ModelForm):
name = TextField(u'name', [validators.Required(), Unique(Category, Category.name)])
Он будет проверять уникальные значения с учетом текущего значения в модели. Вы можете использовать с ним оригинальный уникальный валидатор.
Ответ №6:
У меня это сработало, просто и непринужденно:
Убедитесь, что каждый раз, когда новая строка создается в DB, у нее должно быть уникальное имя в colomn_name_in_db, иначе это не сработает.
class SomeForm(FlaskForm):
id = IntegerField(widget=HiddenInput())
fieldname = StringField('Field name', validators=[DataRequired()])
...
def validate_fieldname(self, fieldname):
names_in_db = dict(Model.query.with_entities(Model.id,
Model.colomn_name_in_db).filter_by(some_filtes_if_needed).all())
if fieldname.data in names_in_db.values() and names_in_db[int(self.id)] != fieldname.data:
raise ValidationError('Name must be unique')