我可以在 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 中,我有一个名为 StructureViewStructure 模型视图,其中包含一个 editable 外键字段 称为 power_unitPowerUnit 模型和数据库 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;
...