Уникальный валидатор в WTForms с моделями SQLAlchemy

#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 включение в область администратора и во внешнем интерфейсе, compare current_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')