Измените порядок или отключите уникальный валидатор в flask-admin с помощью SQLAlchemy.

Я использую flask-admin с SQLAlchemy в базе данных Postgres. Одним из полей таблицы является MAC-адрес, поэтому я использовал тип данных postgres macaddr. Модель таблицы определяется следующим образом:

class Instances(BaseModel, db.Model):
    __tablename__ = 'instances'

    id = db.Column(db.Integer, primary_key=True)
    mac = db.Column(postgresql.MACADDR, unique=True, nullable=False)
    ipv4 = db.Column(postgresql.INET, default=None)
    dns = db.Column(postgresql.VARCHAR(32))
    state = db.Column(postgresql.ENUM("new", "install", "started", "finished", "ready", name="statetype", create_type=True),
                  default='install', nullable=False)

Поле MAC уникально, и это вызывает проблемы с формами flask-admin при отправке недопустимого MAC-адреса. Форма выглядит следующим образом:

class InstancesView(SecureModelView):
    column_editable_list = ['mac', 'ipv4', 'dns', 'state']  # inline editing
    form_args = {
        'mac': {
            'validators': [validators.required(), validators.MacAddress()]
        },
        'ipv4': {
            'validators': [validators.required(), validators.IPAddress()]
        },
        'dns': {
            'validators': [validators.required()]
        },
        'state': {
            'validators': [validators.required()]
        }
    }

Уникальный валидатор реализован в flask-admin. Он добавляется автоматически и запускается перед любым из валидаторов, которые я добавляю в поле, например валидатор MACAddress. Это приводит к DataError от psycopg2, так как уникальный валидатор, кажется, просто берет недопустимое значение MAC и использует его при выборе в базе данных.

Мне кажется, лучшим решением является запуск валидатора MACAddress перед уникальным валидатором. Есть ли возможность изменить порядок валидаторов? Или, возможно, запустить функцию перед валидаторами из flask-admin?

Ниже вы можете увидеть полную ошибку:

[2017-08-02 10:39:43,649] ERROR in app: Exception on /admin/instances/new/ [POST]
Traceback (most recent call last):
  File "/home/user/.virtualenvs/flask-gateway/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1182, in _execute_context
    context)
  File "/home/user/.virtualenvs/flask-gateway/lib/python3.6/site-packages/sqlalchemy/engine/default.py", line 470, in do_execute
    cursor.execute(statement, parameters)
psycopg2.DataError: invalid input syntax for type macaddr: "00:ad:qw:ew:00:00"
LINE 3: WHERE instances.mac = '00:ad:qw:ew:00:00'
                              ^


The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/user/.virtualenvs/flask-gateway/lib/python3.6/site-packages/flask/app.py", line 1982, in wsgi_app
    response = self.full_dispatch_request()
  File "/home/user/.virtualenvs/flask-gateway/lib/python3.6/site-packages/flask/app.py", line 1614, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/home/user/.virtualenvs/flask-gateway/lib/python3.6/site-packages/flask/app.py", line 1517, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/home/user/.virtualenvs/flask-gateway/lib/python3.6/site-packages/flask/_compat.py", line 33, in reraise
    raise value
  File "/home/user/.virtualenvs/flask-gateway/lib/python3.6/site-packages/flask/app.py", line 1612, in full_dispatch_request
    rv = self.dispatch_request()
  File "/home/user/.virtualenvs/flask-gateway/lib/python3.6/site-packages/flask/app.py", line 1598, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/home/user/.virtualenvs/flask-gateway/lib/python3.6/site-packages/flask_admin/base.py", line 69, in inner
    return self._run_view(f, *args, **kwargs)
  File "/home/user/.virtualenvs/flask-gateway/lib/python3.6/site-packages/flask_admin/base.py", line 368, in _run_view
    return fn(self, *args, **kwargs)
  File "/home/user/.virtualenvs/flask-gateway/lib/python3.6/site-packages/flask_admin/model/base.py", line 1994, in create_view
    if self.validate_form(form):
  File "/home/user/.virtualenvs/flask-gateway/lib/python3.6/site-packages/flask_admin/model/base.py", line 1334, in validate_form
    return validate_form_on_submit(form)
  File "/home/user/.virtualenvs/flask-gateway/lib/python3.6/site-packages/flask_admin/helpers.py", line 65, in validate_form_on_submit
    return is_form_submitted() and form.validate()
  File "/home/user/.virtualenvs/flask-gateway/lib/python3.6/site-packages/wtforms/form.py", line 310, in validate
    return super(Form, self).validate(extra)
  File "/home/user/.virtualenvs/flask-gateway/lib/python3.6/site-packages/wtforms/form.py", line 152, in validate
    if not field.validate(self, extra):
  File "/home/user/.virtualenvs/flask-gateway/lib/python3.6/site-packages/wtforms/fields/core.py", line 204, in validate
    stop_validation = self._run_validation_chain(form, chain)
  File "/home/user/.virtualenvs/flask-gateway/lib/python3.6/site-packages/wtforms/fields/core.py", line 224, in _run_validation_chain
    validator(form, self)
  File "/home/user/.virtualenvs/flask-gateway/lib/python3.6/site-packages/flask_admin/contrib/sqla/validators.py", line 37, in __call__
    .filter(self.column == field.data)
  File "/home/user/.virtualenvs/flask-gateway/lib/python3.6/site-packages/sqlalchemy/orm/query.py", line 2814, in one
    ret = self.one_or_none()
  File "/home/user/.virtualenvs/flask-gateway/lib/python3.6/site-packages/sqlalchemy/orm/query.py", line 2784, in one_or_none
    ret = list(self)
  File "/home/user/.virtualenvs/flask-gateway/lib/python3.6/site-packages/sqlalchemy/orm/query.py", line 2855, in __iter__
    return self._execute_and_instances(context)
  File "/home/user/.virtualenvs/flask-gateway/lib/python3.6/site-packages/sqlalchemy/orm/query.py", line 2878, in _execute_and_instances
    result = conn.execute(querycontext.statement, self._params)
  File "/home/user/.virtualenvs/flask-gateway/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 945, in execute
    return meth(self, multiparams, params)
  File "/home/user/.virtualenvs/flask-gateway/lib/python3.6/site-packages/sqlalchemy/sql/elements.py", line 263, in _execute_on_connection
    return connection._execute_clauseelement(self, multiparams, params)
  File "/home/user/.virtualenvs/flask-gateway/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1053, in _execute_clauseelement
    compiled_sql, distilled_params
  File "/home/user/.virtualenvs/flask-gateway/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1189, in _execute_context
    context)
  File "/home/user/.virtualenvs/flask-gateway/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1402, in _handle_dbapi_exception
    exc_info
  File "/home/user/.virtualenvs/flask-gateway/lib/python3.6/site-packages/sqlalchemy/util/compat.py", line 203, in raise_from_cause
    reraise(type(exception), exception, tb=exc_tb, cause=cause)
  File "/home/user/.virtualenvs/flask-gateway/lib/python3.6/site-packages/sqlalchemy/util/compat.py", line 186, in reraise
    raise value.with_traceback(tb)
  File "/home/user/.virtualenvs/flask-gateway/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1182, in _execute_context
    context)
  File "/home/user/.virtualenvs/flask-gateway/lib/python3.6/site-packages/sqlalchemy/engine/default.py", line 470, in do_execute
    cursor.execute(statement, parameters)
sqlalchemy.exc.DataError: (psycopg2.DataError) invalid input syntax for type macaddr: "00:ad:qw:ew:00:00"
LINE 3: WHERE instances.mac = '00:ad:qw:ew:00:00'
                              ^
 [SQL: 'SELECT instances.id AS instances_id, instances.mac AS instances_mac, instances.ipv4 AS instances_ipv4, instances.dns AS instances_dns, instances.state AS instances_state \nFROM instances \nWHERE instances.mac = %(mac_1)s'] [parameters: {'mac_1': '00:ad:qw:ew:00:00'}]
127.0.0.1 - - [02/Aug/2017 10:39:43] "POST /admin/instances/new/?url=%2Fadmin%2Finstances%2F HTTP/1.1" 500 -

person Julia Niewiejska    schedule 02.08.2017    source источник


Ответы (2)


Я также боролся с этим, мое решение заключалось в написании функции проверки, вызываемой в on_model_change моего MethodView, вызывающей wtform ValidationError.

Функция проверки принимает поле ввода, класс базы данных и поле для проверки.

# Name validator designed for on_model_change
def check_field_double(field_data: str, coll: db.Document, field_name: str):
        names = [ getattr(x, field_name) for x in coll.objects() ]
        if field_data in names:
            print(f"Caught custom validation")
            print(f"field.data: {field_data} in names: {names}")
            raise validators.ValidationError(f'Duplicate {coll._class_name} {field_name}: {field_data}')

Структура метода on_model_change проверяет is_created, чтобы определить, является ли действие редактированием или созданием, и вызывает функцию.

def on_model_change(self, form, model, is_created):
        # Split create & edit on is_created
        if is_created:
            # Validate name - no duplicate
            check_field_double(field_data=form.your_field.data, coll=db_class, field_name='your_field')

        else: # is_created == false this section runs edits
            if form.your_field.data == model.panel_serial:
                # add your own edit logic as required
person Tristan Blandford    schedule 05.03.2021

Так как кажется, что Уникальный валидатор от flask-admin не всегда желателен и отчасти недоработан (его даже убрали из WTForms), а всегда прикрепляется, я разместил тикет в соответствующем трекере. Я также создал обходной путь, используя дополнительное поле, описанное ниже.

Удалите стандартное поле Mac из редактирования и создайте представление, добавьте пользовательское поле и укажите порядок полей в форме:

class InstancesView(SecureModelView):

    # columns excluded from create and edit view
    form_excluded_columns = ['mac', ]
    # extra columns in create and edit view
    form_extra_fields = {
        'mac2': StringField('Mac Address', validators=[validators.required(), validators.mac_address()])
    }
    # column order in create view
    form_create_rules = ('mac2', 'ipv4', 'dns', 'state')
    # column order in edit view
    form_edit_rules = ('mac2', 'ipv4', 'dns', 'state')

Теперь пользовательское поле необходимо предварительно заполнить вручную текущим Mac в режиме редактирования:

def on_form_prefill(self, form, id):
    form.mac2.data = self.session.query(Instances).filter(Instances.id == id).one().mac

При отправке формы данные из нового поля должны быть переданы в исходное поле Mac:

def on_model_change(self, form, model, is_created):

    if len(model.mac2):
        model.mac = model.mac2
person Julia Niewiejska    schedule 03.08.2017