问题
I'm aiming to make an "advanced" filter_horizontal, one with more filters, but I can't seem to find the widget to override. I know it uses the related_widget_wrapper.html, but if I want to add functionalities to it in a clear way, what is the widget to override.
For now my backup solution is to do a full javascript solution to prepend it with a dropdown on form load (created from javascript) and make ajax calls to modify the filter...but this seems as an overkill.
What i've done so far :
# Override filteredSelectMultiple, add javascript and add attributes on the tag to identify the element, and add parameter url that will contain the ajax call
class AjaxFilterHorizontalWidget(FilteredSelectMultiple):
def __init__(self, url, verbose_name = '', is_stacked=False, attrs=None, choices=()):
self.url = url
super().__init__(verbose_name, is_stacked, attrs, choices)
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
context['widget']['attrs']['data-url'] = self.url
context['widget']['attrs']['data-ajax-select'] = '1'
return context
class Media:
js = ['admin/js/ajax_filter_horizontal.js']
Ajax_filter_horizontal.js
$(document).ready(function () {
$('select[data-ajax-select=1]').each(function (index, item) {
var currentRequest;
var url = $(item).data('url')
// var wrapper = $('#' + $(item).prop('id')).closest('.selector-available')
$(document).on('keyup', $('.selector-filter input'), function () {
if ($('.selector-filter input').val().length < 3) {
$(item).empty()
return
}
currentRequest = $.ajax({
url: url,
data: {q: $('.selector-filter input').val()},
beforeSend : function() {
if(currentRequest != null) {
currentRequest.abort();
}
},
success: function (data) {
$(item).empty()
let item_to = $('#' + $(item).prop('id').replace('_from', '_to'))
if (data.results.length > 500) {
$('#' + $(item).prop('id')).append('<option disabled value="" title="">Too many results, refine your search...</option>')
return
}
for (let instance of data.results) {
if ($('option[value='+instance.id+']', item_to).length == 0) {
$('#' + $(item).prop('id')).append('<option value="'+instance.id+'" title="'+instance.text+'">'+instance.text+'</option>')
}
}
SelectBox.init($(item).prop('id'))
}
})
});
});
});
I had to override the field, just to remove validation(for some reason the validation is also done on the original values, the left side of the filter_horizontal)
class AjaxMultipleChoiceField(MultipleChoiceField):
widget = AjaxFilterHorizontalWidget
def validate(self, value):
pass
"""Validate that the input is a list or tuple."""
# if self.required and not value:
# raise ValidationError(self.error_messages['required'], code='required')
This is how I call it :
self.fields['person'] = `AjaxMultipleChoiceField(widget=AjaxFilterHorizontalWidget(url= '/person-autocomplete-advanced/', verbose_name='People to invite'))`
I can't manage to find where to prefill the values in the "to" section when I'm editing an existing field.
回答1:
Django Model admin overrides BaseModelAdmin
which contains following code.
django.contrib.admin.options.py
class BaseModelAdmin(six.with_metaclass(forms.MediaDefiningClass)):
...
def formfield_for_dbfield(self, db_field, request, **kwargs):
...
if db_field.name in self.raw_id_fields:
kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.remote_field, self.admin_site, using=db)
elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)):
kwargs['widget'] = widgets.FilteredSelectMultiple(
db_field.verbose_name,
db_field.name in self.filter_vertical
)
It can be observed that if there is filter_vertical
or filter_horizontal
argument passed
in ModelAdmin
option than it adds FilteredSelectMultiple
widget.
Below is the source of FilteredSelectMultiple
You may override this if necessary
django.contrib.admin.widgets.py
class FilteredSelectMultiple(forms.SelectMultiple):
"""
A SelectMultiple with a JavaScript filter interface.
Note that the resulting JavaScript assumes that the jsi18n
catalog has been loaded in the page
"""
@property
def media(self): # override this property in your custom class
js = ["core.js", "SelectBox.js", "SelectFilter2.js"]
return forms.Media(js=["admin/js/%s" % path for path in js])
...
def get_context(self, name, value, attrs):
context = super(FilteredSelectMultiple, self).get_context(name, value, attrs)
context['widget']['attrs']['class'] = 'selectfilter'
if self.is_stacked:
context['widget']['attrs']['class'] += 'stacked'
context['widget']['attrs']['data-field-name'] = self.verbose_name
context['widget']['attrs']['data-is-stacked'] = int(self.is_stacked)
return context
For JS or Media overriding
You could observe that media
property on FilteredSelectMultiple
class have several js included you may modify them as per your needs.
For HTML template modifications
FilteredSelectMultiple
overrides django.forms.widgets.SelectMultiple
which ultimately overrides django.forms.widgets.Select
widget.
So it can be said that FilteredSelectMultiple
uses following properties of Select
widget
class Select(ChoiceWidget):
input_type = 'select'
template_name = 'django/forms/widgets/select.html'
option_template_name = 'django/forms/widgets/select_option.html'
add_id_index = False
checked_attribute = {'selected': True}
option_inherits_attrs = False
...
You can override those options inside your FilteredSelectMultiple
class.
I hope the information above is useful for you.
来源:https://stackoverflow.com/questions/55328429/django-admin-override-filter-horizontal