Open mccarthysean opened 3 years ago
After much trial-and-error, I've found a solution. It would be great if we could support this natively, now that it works so well with Select2 and x-editable.
First create a custom widget so we don't get the following error:
Exception: Unsupported field type: <class 'flask_admin.model.fields.AjaxSelectField'>
Here's the custom widget:
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"""
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", "")
minimum_input_length = int(field.loader.options.get('minimum_input_length', 0))
kwargs.setdefault('data-minimum-input-length', minimum_input_length)
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)
allow_blank = getattr(field, 'allow_blank', False)
if allow_blank and not self.multiple:
kwargs['data-allow-blank'] = u'1'
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'
# kwargs['data-source'] = []
if field.type == 'QuerySelectMultipleField':
# kwargs['data-role'] = 'x-editable-ajax'
kwargs['data-role'] = 'x-editable-select2-multiple'
else:
super().get_kwargs(field, kwargs)
return kwargs
Then override the get_list_form()
method in your model view, to use your 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())
Now for the view, where I use form_ajax_refs
to lazy-load the options for the dropdown menus in the edit view.
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'
),
}
Here's my list_custom.html
template, for overriding the {% block tail %}
with my own flask_admin_form.js
script for my custom widget.
{% 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 %}
Finally, in the flask_admin_form.js
(my replacement for the default filename='admin/js/form.js'
), I add the following case for x-editable-ajax
(my custom role). I didn't include the whole JavaScript file here for brevity. You can find it here in the source code.
Notice the select2
I added to the $el.editable(
options:
...
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;
...
is there anything i can do to help getting this into main?
I was kind of hoping someone would offer to help, if I left the solution here. I'm not a git expert so I don't know the sequence of events to get this into the main flask admin package.
If you want to help you can either copy and paste the solution I've put here, or explain to me the exact sequence of events so I can put it in myself. Thanks
Here's my StackOverflow question that made me realize this was an issue.
Apparently
form_ajax_refs
does not work for editable fields in the list view. I was having performance issues eager-loading relationship values for editable fields in the list view, so I triedform_ajax_refs
to lazy-load it on-demand, but I kept getting the following error:It happens when the
form_ajax_refs
field is also in thecolumn_editable_list
.Here's my setup:
In Flask-Admin, I have a view of my
Structure
model calledStructureView
which contains an editable foreign key field calledpower_unit
. ThePowerUnit
model and database table contains many, many records, which are all apparently eager-loaded into the HTML, slowing down the loading time for the view.I'd like the dropdown menu for the
power_unit
field to lazy-load when the user clicks on the field to select something from the dropdown list, and not on page-load.Here are my models and my Flask-Admin view:
Here's a picture of the long dropdown menu when editing the
power_unit
field. This is my motivation for usingform_ajax_refs
in the first place:When I inspect the HTML, I see a long array of name-value pairs for the dropdown menu, and this array is repeated for every
power_unit
cell in thestructures
table view, so it's a lot of HTML to render, which I think slows down the page loading considerably.