使用 SQLAlchemy 在 flask-admin 中更改顺序或禁用唯一验证器
Change the order or disable the unique validator in flask-admin with SQLAlchemy
我在 Postgres 数据库上使用带有 SQLAlchemy 的 flask-admin。 table 字段之一是 MAC 地址,因此我使用了 postgres macaddr 数据类型。 table模型定义如下:
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 字段是唯一的,这会在提交无效的 MAC 地址时导致 flask-admin 表单出现问题。表格如下所示:
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 中实现。它是自动添加的,并且 运行 在我添加到该字段的任何验证器之前,例如 MAC 地址验证器。这会导致来自 psycopg2 的 DataError,因为唯一验证器似乎只是采用无效的 MAC 值并在数据库的 select 中使用它。
对我来说最好的解决方案似乎是 运行在唯一验证器之前使用 MAC地址验证器。是否有可能更改验证器的顺序?或者 运行 来自 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 -
因为来自 flask-admin 的 Unique 验证器似乎并不总是可取的并且有部分缺陷(它甚至从 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
我也为此苦苦挣扎,我的解决方案涉及编写一个在我的 MethodView 的 on_model_change 中调用的验证函数,引发 wtform ValidationError。
验证函数采用字段输入、数据库 class 和要检查的字段。
# 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
我在 Postgres 数据库上使用带有 SQLAlchemy 的 flask-admin。 table 字段之一是 MAC 地址,因此我使用了 postgres macaddr 数据类型。 table模型定义如下:
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 字段是唯一的,这会在提交无效的 MAC 地址时导致 flask-admin 表单出现问题。表格如下所示:
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 中实现。它是自动添加的,并且 运行 在我添加到该字段的任何验证器之前,例如 MAC 地址验证器。这会导致来自 psycopg2 的 DataError,因为唯一验证器似乎只是采用无效的 MAC 值并在数据库的 select 中使用它。
对我来说最好的解决方案似乎是 运行在唯一验证器之前使用 MAC地址验证器。是否有可能更改验证器的顺序?或者 运行 来自 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 -
因为来自 flask-admin 的 Unique 验证器似乎并不总是可取的并且有部分缺陷(它甚至从 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
我也为此苦苦挣扎,我的解决方案涉及编写一个在我的 MethodView 的 on_model_change 中调用的验证函数,引发 wtform ValidationError。
验证函数采用字段输入、数据库 class 和要检查的字段。
# 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