django admin override filter_horizontal

三世轮回 提交于 2021-02-07 03:52:25

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!