我可以在 Flask-Admin 中对同一列使用 "form_ajax_refs" AND "column_editable_list" 吗?
Can I use "form_ajax_refs" AND "column_editable_list" for the same column in Flask-Admin?
在 Flask-Admin 中,我有一个名为 StructureView
的 Structure
模型视图,其中包含一个 editable 外键字段 称为 power_unit
。 PowerUnit
模型和数据库 table 包含很多很多记录,这些记录显然都是急切加载到 HTML 中,从而减慢了视图的加载时间。
我希望 power_unit
字段的下拉菜单在用户单击该字段以从下拉列表中选择 select 时延迟加载,而不是页面加载。
可以吗?
我在一些地方读到过我应该尝试 form_ajax_refs
制作“可按需搜索”下拉列表,但由于以下原因我无法让它们工作错误,仅在列表视图中字段为“editable”时发生:
Exception: Unsupported field type: <class 'flask_admin.model.fields.AjaxSelectField'>
这是我的模型和我的 Flask-Admin 视图:
class Structure(db.Model):
__tablename__ = 'structures'
__table_args__ = {"schema": "public"}
id = db.Column(INTEGER, primary_key=True)
structure = db.Column(TEXT, nullable=False)
power_unit_id = db.Column(INTEGER, db.ForeignKey('public.power_units.id'))
power_unit = relationship('PowerUnit', back_populates='structures')
class PowerUnit(db.Model):
__tablename__ = 'power_units'
__table_args__ = {"schema": "public"}
id = db.Column(INTEGER, primary_key=True)
power_unit = db.Column(TEXT, nullable=False)
structures = relationship('Structure', back_populates='power_unit')
class StructureView(MyModelView):
"""Flask-Admin view for Structure model (public.structures table)"""
column_list = ('structure', 'power_unit')
form_columns = column_list
column_editable_list = form_columns
# I can't get these "form_ajax_refs" to work due to Exception:
# Unsupported field type: <class 'flask_admin.model.fields.AjaxSelectField'>...
form_ajax_refs = {
'power_unit': {
'fields': [PowerUnit.power_unit], # searchable fields, I think
'minimum_input_length': 0, # show suggestions, even before user input
'placeholder': 'Please select',
'page_size': 10,
},
# The following doesn't work either...
# 'power_unit': QueryAjaxModelLoader(
# 'power_unit', db.session, PowerUnit, fields=['power_unit']
# )
}
这是编辑 power_unit
字段时的长下拉菜单的图片:
当我检查 HTML 时,我看到下拉菜单的一长串名称-值对,并且这个数组对 structures
中的每个 power_unit
单元格重复table 视图,所以要渲染很多 HTML,我认为这会大大减慢页面加载速度。
经过反复试验,我弄明白了。如果 Flask-Admin 本身就支持它,那就太好了,现在它与 Select2 和 x-editable 配合得很好。
首先创建一个自定义小部件,这样我们就不会出现以下错误:
Exception: Unsupported field type: <class 'flask_admin.model.fields.AjaxSelectField'>
这是自定义小部件:
from flask_admin.contrib.sqla.ajax import QueryAjaxModelLoader
from flask_admin.model.widgets import XEditableWidget
from wtforms.widgets import html_params
from flask_admin.helpers import get_url
from flask_admin.babel import gettext
from flask_admin._backwards import Markup
from jinja2 import escape
class CustomWidget(XEditableWidget):
"""WTForms widget that provides in-line editing for the list view.
Determines how to display the x-editable/ajax form based on the
field inside of the FieldList (StringField, IntegerField, etc).
"""
def __init__(self, multiple=False):
self.multiple = multiple
def __call__(self, field, **kwargs):
"""Called when rendering the Jinja2 template.
Previously 'AjaxSelectField' was not supported using form_ajax_refs
for column_editable_list cells"""
# We only need to add the AjaxSelectField and perhaps AjaxSelectMultipleField.
# For all others __call__ stays the same
if field.type not in ('AjaxSelectField', 'AjaxSelectMultipleField'):
return super().__call__(field, **kwargs)
# x-editable-ajax is a custom type I made in flask_admin_form.js for
# lazy-loading the dropdown options by AJAX
kwargs.setdefault('data-role', 'x-editable-ajax')
display_value = kwargs.pop('display_value', '')
kwargs.setdefault('data-value', display_value)
# For the POST request
kwargs.setdefault('data-url', './ajax/update/')
# For the GET request
kwargs.setdefault('data-url-lookup', get_url('.ajax_lookup', name=field.loader.name))
kwargs.setdefault('id', field.id)
kwargs.setdefault('name', field.name)
kwargs.setdefault('href', '#')
kwargs.setdefault('type', 'hidden')
kwargs['data-csrf'] = kwargs.pop("csrf", "")
if self.multiple:
result = []
ids = []
for value in field.data:
data = field.loader.format(value)
result.append(data)
ids.append(as_unicode(data[0]))
separator = getattr(field, 'separator', ',')
kwargs['value'] = separator.join(ids)
kwargs['data-json'] = json.dumps(result)
kwargs['data-multiple'] = u'1'
else:
data = field.loader.format(field.data)
if data:
kwargs['value'] = data[0]
kwargs['data-json'] = json.dumps(data)
placeholder = field.loader.options.get('placeholder', gettext('Search'))
kwargs.setdefault('data-placeholder', placeholder)
minimum_input_length = int(field.loader.options.get('minimum_input_length', 0))
kwargs.setdefault('data-minimum-input-length', minimum_input_length)
if not kwargs.get('pk'):
raise Exception('pk required')
kwargs['data-pk'] = str(kwargs.pop("pk"))
kwargs = self.get_kwargs(field, kwargs)
return Markup(
'<a %s>%s</a>' % (html_params(**kwargs),
escape(display_value))
)
def get_kwargs(self, field, kwargs):
"""Return extra kwargs based on the field type"""
if field.type in ('AjaxSelectField', 'AjaxSelectMultipleField'):
kwargs['data-type'] = 'select2'
else:
super().get_kwargs(field, kwargs)
return kwargs
然后在您的模型视图中覆盖 get_list_form()
方法,以使用您的 CustomWidget。
from flask_admin.contrib.sqla import ModelView
class MyModelView(ModelView):
"""
Customized model view for Flask-Admin page (for database tables)
https://flask-admin.readthedocs.io/en/latest/introduction/#
"""
# Custom templates to include custom JavaScript and override the {% block tail %}
list_template = 'admin/list_custom.html'
can_create = True
can_edit = True
def get_list_form(self):
"""Override this function and supply my own CustomWidget with AJAX
for lazy-loading dropdown options"""
if self.form_args:
# get only validators, other form_args can break FieldList wrapper
validators = dict(
(key, {'validators': value["validators"]})
for key, value in iteritems(self.form_args)
if value.get("validators")
)
else:
validators = None
# Here's where I supply my custom widget!
return self.scaffold_list_form(validators=validators, widget=CustomWidget())
现在是视图,我使用 form_ajax_refs
延迟加载编辑视图中下拉菜单的选项。
class StructureView(MyModelView):
"""Flask-Admin view for Structure model (public.structures table)"""
can_create = True
can_edit = True
column_list = ('structure', 'power_unit')
form_columns = column_list
column_editable_list = column_list
# For lazy-loading the dropdown options in the edit view,
# which really speeds up list view loading time
form_ajax_refs = {
'power_unit': QueryAjaxModelLoader(
'power_unit', db.session, PowerUnit,
fields=['power_unit'], order_by='power_unit'
),
}
这是我的 list_custom.html
模板,用于使用我自己的 flask_admin_form.js
自定义小部件脚本覆盖 {% block tail %}
。
{% extends 'admin/model/list.html' %}
{% block tail %}
{% if filter_groups %}
<div id="filter-groups-data" style="display:none;">{{ filter_groups|tojson|safe }}</div>
<div id="active-filters-data" style="display:none;">{{ active_filters|tojson|safe }}</div>
{% endif %}
<script src="{{ admin_static.url(filename='vendor/bootstrap-daterangepicker/daterangepicker.js', v='1.3.22') }}"></script>
{% if editable_columns %}
<script src="{{ admin_static.url(filename='vendor/x-editable/js/bootstrap3-editable.min.js', v='1.5.1.1') }}"></script>
{% endif %}
<!-- <script src="{ admin_static.url(filename='admin/js/form.js', v='1.0.1') }"></script> -->
<script src="{{ url_for('static', filename='js/flask_admin_form.js') }}"></script>
<script src="{{ admin_static.url(filename='admin/js/filters.js', v='1.0.0') }}"></script>
{{ actionlib.script(_gettext('Please select at least one record.'),
actions,
actions_confirmation) }}
{% endblock %}
最后,在 flask_admin_form.js
(我对默认 filename='admin/js/form.js'
的替换)中,我为 x-editable-ajax
(我的自定义角色)添加了以下情况。为了简洁起见,我没有在此处包含整个 JavaScript 文件。您可以在源代码中找到它here。
注意我添加到 $el.editable(
选项的 select2
:
...
switch (name) {
case "select2-ajax":
processAjaxWidget($el, name);
return true;
case "x-editable":
$el.editable({
params: overrideXeditableParams,
combodate: {
// prevent minutes from showing in 5 minute increments
minuteStep: 1,
maxYear: 2030,
},
});
return true;
case "x-editable-ajax":
var optsSelect2 = {
minimumInputLength: $el.attr("data-minimum-input-length"),
placeholder: "data-placeholder",
allowClear: $el.attr("data-allow-blank") == "1",
multiple: $el.attr("data-multiple") == "1",
ajax: {
// Special data-url just for the GET request
url: $el.attr("data-url-lookup"),
data: function (term, page) {
return {
query: term,
offset: (page - 1) * 10,
limit: 10,
};
},
results: function (data, page) {
var results = [];
for (var k in data) {
var v = data[k];
results.push({ id: v[0], text: v[1] });
}
return {
results: results,
more: results.length == 10,
};
},
},
};
// From x-editable
$el.editable({
params: overrideXeditableParams,
combodate: {
// prevent minutes from showing in 5 minute increments
minuteStep: 1,
maxYear: 2030,
},
// I added the following so the select2 dropdown will lazy-load values from the DB on-demand
select2: optsSelect2,
});
return true;
...
在 Flask-Admin 中,我有一个名为 StructureView
的 Structure
模型视图,其中包含一个 editable 外键字段 称为 power_unit
。 PowerUnit
模型和数据库 table 包含很多很多记录,这些记录显然都是急切加载到 HTML 中,从而减慢了视图的加载时间。
我希望 power_unit
字段的下拉菜单在用户单击该字段以从下拉列表中选择 select 时延迟加载,而不是页面加载。
可以吗?
我在一些地方读到过我应该尝试 form_ajax_refs
制作“可按需搜索”下拉列表,但由于以下原因我无法让它们工作错误,仅在列表视图中字段为“editable”时发生:
Exception: Unsupported field type: <class 'flask_admin.model.fields.AjaxSelectField'>
这是我的模型和我的 Flask-Admin 视图:
class Structure(db.Model):
__tablename__ = 'structures'
__table_args__ = {"schema": "public"}
id = db.Column(INTEGER, primary_key=True)
structure = db.Column(TEXT, nullable=False)
power_unit_id = db.Column(INTEGER, db.ForeignKey('public.power_units.id'))
power_unit = relationship('PowerUnit', back_populates='structures')
class PowerUnit(db.Model):
__tablename__ = 'power_units'
__table_args__ = {"schema": "public"}
id = db.Column(INTEGER, primary_key=True)
power_unit = db.Column(TEXT, nullable=False)
structures = relationship('Structure', back_populates='power_unit')
class StructureView(MyModelView):
"""Flask-Admin view for Structure model (public.structures table)"""
column_list = ('structure', 'power_unit')
form_columns = column_list
column_editable_list = form_columns
# I can't get these "form_ajax_refs" to work due to Exception:
# Unsupported field type: <class 'flask_admin.model.fields.AjaxSelectField'>...
form_ajax_refs = {
'power_unit': {
'fields': [PowerUnit.power_unit], # searchable fields, I think
'minimum_input_length': 0, # show suggestions, even before user input
'placeholder': 'Please select',
'page_size': 10,
},
# The following doesn't work either...
# 'power_unit': QueryAjaxModelLoader(
# 'power_unit', db.session, PowerUnit, fields=['power_unit']
# )
}
这是编辑 power_unit
字段时的长下拉菜单的图片:
当我检查 HTML 时,我看到下拉菜单的一长串名称-值对,并且这个数组对 structures
中的每个 power_unit
单元格重复table 视图,所以要渲染很多 HTML,我认为这会大大减慢页面加载速度。
经过反复试验,我弄明白了。如果 Flask-Admin 本身就支持它,那就太好了,现在它与 Select2 和 x-editable 配合得很好。
首先创建一个自定义小部件,这样我们就不会出现以下错误:
Exception: Unsupported field type: <class 'flask_admin.model.fields.AjaxSelectField'>
这是自定义小部件:
from flask_admin.contrib.sqla.ajax import QueryAjaxModelLoader
from flask_admin.model.widgets import XEditableWidget
from wtforms.widgets import html_params
from flask_admin.helpers import get_url
from flask_admin.babel import gettext
from flask_admin._backwards import Markup
from jinja2 import escape
class CustomWidget(XEditableWidget):
"""WTForms widget that provides in-line editing for the list view.
Determines how to display the x-editable/ajax form based on the
field inside of the FieldList (StringField, IntegerField, etc).
"""
def __init__(self, multiple=False):
self.multiple = multiple
def __call__(self, field, **kwargs):
"""Called when rendering the Jinja2 template.
Previously 'AjaxSelectField' was not supported using form_ajax_refs
for column_editable_list cells"""
# We only need to add the AjaxSelectField and perhaps AjaxSelectMultipleField.
# For all others __call__ stays the same
if field.type not in ('AjaxSelectField', 'AjaxSelectMultipleField'):
return super().__call__(field, **kwargs)
# x-editable-ajax is a custom type I made in flask_admin_form.js for
# lazy-loading the dropdown options by AJAX
kwargs.setdefault('data-role', 'x-editable-ajax')
display_value = kwargs.pop('display_value', '')
kwargs.setdefault('data-value', display_value)
# For the POST request
kwargs.setdefault('data-url', './ajax/update/')
# For the GET request
kwargs.setdefault('data-url-lookup', get_url('.ajax_lookup', name=field.loader.name))
kwargs.setdefault('id', field.id)
kwargs.setdefault('name', field.name)
kwargs.setdefault('href', '#')
kwargs.setdefault('type', 'hidden')
kwargs['data-csrf'] = kwargs.pop("csrf", "")
if self.multiple:
result = []
ids = []
for value in field.data:
data = field.loader.format(value)
result.append(data)
ids.append(as_unicode(data[0]))
separator = getattr(field, 'separator', ',')
kwargs['value'] = separator.join(ids)
kwargs['data-json'] = json.dumps(result)
kwargs['data-multiple'] = u'1'
else:
data = field.loader.format(field.data)
if data:
kwargs['value'] = data[0]
kwargs['data-json'] = json.dumps(data)
placeholder = field.loader.options.get('placeholder', gettext('Search'))
kwargs.setdefault('data-placeholder', placeholder)
minimum_input_length = int(field.loader.options.get('minimum_input_length', 0))
kwargs.setdefault('data-minimum-input-length', minimum_input_length)
if not kwargs.get('pk'):
raise Exception('pk required')
kwargs['data-pk'] = str(kwargs.pop("pk"))
kwargs = self.get_kwargs(field, kwargs)
return Markup(
'<a %s>%s</a>' % (html_params(**kwargs),
escape(display_value))
)
def get_kwargs(self, field, kwargs):
"""Return extra kwargs based on the field type"""
if field.type in ('AjaxSelectField', 'AjaxSelectMultipleField'):
kwargs['data-type'] = 'select2'
else:
super().get_kwargs(field, kwargs)
return kwargs
然后在您的模型视图中覆盖 get_list_form()
方法,以使用您的 CustomWidget。
from flask_admin.contrib.sqla import ModelView
class MyModelView(ModelView):
"""
Customized model view for Flask-Admin page (for database tables)
https://flask-admin.readthedocs.io/en/latest/introduction/#
"""
# Custom templates to include custom JavaScript and override the {% block tail %}
list_template = 'admin/list_custom.html'
can_create = True
can_edit = True
def get_list_form(self):
"""Override this function and supply my own CustomWidget with AJAX
for lazy-loading dropdown options"""
if self.form_args:
# get only validators, other form_args can break FieldList wrapper
validators = dict(
(key, {'validators': value["validators"]})
for key, value in iteritems(self.form_args)
if value.get("validators")
)
else:
validators = None
# Here's where I supply my custom widget!
return self.scaffold_list_form(validators=validators, widget=CustomWidget())
现在是视图,我使用 form_ajax_refs
延迟加载编辑视图中下拉菜单的选项。
class StructureView(MyModelView):
"""Flask-Admin view for Structure model (public.structures table)"""
can_create = True
can_edit = True
column_list = ('structure', 'power_unit')
form_columns = column_list
column_editable_list = column_list
# For lazy-loading the dropdown options in the edit view,
# which really speeds up list view loading time
form_ajax_refs = {
'power_unit': QueryAjaxModelLoader(
'power_unit', db.session, PowerUnit,
fields=['power_unit'], order_by='power_unit'
),
}
这是我的 list_custom.html
模板,用于使用我自己的 flask_admin_form.js
自定义小部件脚本覆盖 {% block tail %}
。
{% extends 'admin/model/list.html' %}
{% block tail %}
{% if filter_groups %}
<div id="filter-groups-data" style="display:none;">{{ filter_groups|tojson|safe }}</div>
<div id="active-filters-data" style="display:none;">{{ active_filters|tojson|safe }}</div>
{% endif %}
<script src="{{ admin_static.url(filename='vendor/bootstrap-daterangepicker/daterangepicker.js', v='1.3.22') }}"></script>
{% if editable_columns %}
<script src="{{ admin_static.url(filename='vendor/x-editable/js/bootstrap3-editable.min.js', v='1.5.1.1') }}"></script>
{% endif %}
<!-- <script src="{ admin_static.url(filename='admin/js/form.js', v='1.0.1') }"></script> -->
<script src="{{ url_for('static', filename='js/flask_admin_form.js') }}"></script>
<script src="{{ admin_static.url(filename='admin/js/filters.js', v='1.0.0') }}"></script>
{{ actionlib.script(_gettext('Please select at least one record.'),
actions,
actions_confirmation) }}
{% endblock %}
最后,在 flask_admin_form.js
(我对默认 filename='admin/js/form.js'
的替换)中,我为 x-editable-ajax
(我的自定义角色)添加了以下情况。为了简洁起见,我没有在此处包含整个 JavaScript 文件。您可以在源代码中找到它here。
注意我添加到 $el.editable(
选项的 select2
:
...
switch (name) {
case "select2-ajax":
processAjaxWidget($el, name);
return true;
case "x-editable":
$el.editable({
params: overrideXeditableParams,
combodate: {
// prevent minutes from showing in 5 minute increments
minuteStep: 1,
maxYear: 2030,
},
});
return true;
case "x-editable-ajax":
var optsSelect2 = {
minimumInputLength: $el.attr("data-minimum-input-length"),
placeholder: "data-placeholder",
allowClear: $el.attr("data-allow-blank") == "1",
multiple: $el.attr("data-multiple") == "1",
ajax: {
// Special data-url just for the GET request
url: $el.attr("data-url-lookup"),
data: function (term, page) {
return {
query: term,
offset: (page - 1) * 10,
limit: 10,
};
},
results: function (data, page) {
var results = [];
for (var k in data) {
var v = data[k];
results.push({ id: v[0], text: v[1] });
}
return {
results: results,
more: results.length == 10,
};
},
},
};
// From x-editable
$el.editable({
params: overrideXeditableParams,
combodate: {
// prevent minutes from showing in 5 minute increments
minuteStep: 1,
maxYear: 2030,
},
// I added the following so the select2 dropdown will lazy-load values from the DB on-demand
select2: optsSelect2,
});
return true;
...