Подтвердить что ты не робот

Уникальный валидатор в 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)

Основная проблема заключается в том, что я не знаю, как получить доступ к существующему объекту категории в валидаторе, который позволил бы мне исключить отредактированный объект из запроса.

Есть ли способ сделать это? Спасибо.

4b9b3361

Ответ 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={})