Discussion about AutocompleteJsonView (as per #37008)

Hey I recently created a topic on django issue tracker and I was advised to first open a discussion here. Link to the topic:

I feel like this class could be more modular, for example here is my simple idea that would allow writing an autocomplete method for filter on the ModelAdmin class:

from django.apps import apps
from django.contrib.admin.exceptions import NotRegistered
from django.contrib.admin.views.autocomplete import AutocompleteJsonView
from django.core.exceptions import PermissionDenied


class AutoCompleteXJsonView(AutocompleteJsonView):

    def is_autocompletex_request(self, request):
        return any(field.startswith('autocompletex_') for field in request.GET.keys())

    def get_autocompletex_search_results(self, request, queryset, term):
        source_model_admin = self.get_source_model_admin(request)
        field_name = request.GET["field_name"]
        if autocompletex_method := getattr(source_model_admin, f'autocompletex_{field_name}', None):
            return autocompletex_method(request, queryset, term, source_model_admin)
        return self.model_admin.get_search_results(request, queryset, term)

    def get_source_model_admin(self, request):
        app_label = request.GET["app_label"]
        model_name = request.GET["model_name"]

        try:
            source_model = apps.get_model(app_label, model_name)
        except LookupError as e:
            raise PermissionDenied from e

        try:
            source_model_admin = self.admin_site.get_model_admin(source_model)
        except NotRegistered as e:
            raise PermissionDenied from e

        return source_model_admin

    def get_queryset(self):
        qs = self.model_admin.get_queryset(self.request)
        qs = qs.complex_filter(self.source_field.get_limit_choices_to())

        if self.is_autocompletex_request(self.request):
            qs, search_use_distinct = self.get_autocompletex_search_results(
                self.request, qs, self.term,
            )
        else:
            qs, search_use_distinct = self.model_admin.get_search_results(
                self.request, qs, self.term
            )
        if search_use_distinct:
            qs = qs.distinct()
        return qs

With addition of some fields:

from django import forms


class AutocompleteXWidget(forms.HiddenInput):
    class Media:
        js = (
            'admin/js/vendor/jquery/jquery.js',
            'admin/js/jquery.init.js',
            'admin/js/autocompletex.js',
        )


class AutocompleteXField(forms.JSONField):
    def __init__(self, *args, **kwargs):
        defaults = {
            'widget': AutocompleteXWidget(),
            'required': False,
        }
        defaults.update(kwargs)
        super().__init__(*args, **defaults)


class AutoCompleteXForm(forms.ModelForm):
    autocompletex = AutocompleteXField()


class AutoCompleteXMixin:
    autocompletex_initial = {'field_values': []}

    def get_autocompletex_initial(self):
        return getattr(self, 'autocompletex_initial')

    def get_changeform_initial_data(self, request):
        initial = super().get_changeform_initial_data(request)
        initial['autocompletex'] = self.get_autocompletex_initial()
        return initial

And some javascript:

'use strict';
(function ($) {

    let modifyAutocompleteRequest = function (request, $element) {
        const config = JSON.parse($('#id_autocompletex').val() || '{}');
        // noinspection JSUnresolvedReference
        const dependencies = config.field_values || {};
        dependencies.forEach(dep => {
            request['autocompletex_' + dep] = $element.closest('form').find(`[name$="${dep}"]`).val() || '';
        })
        return request;
    }

    const originalDjangoAdminSelect2 = $.fn.djangoAdminSelect2;

    if (typeof originalDjangoAdminSelect2 === 'function') {
        $.fn.djangoAdminSelect2 = function () {
            const originalSelect2 = $.fn.select2;

            $.fn.select2 = function (options) {
                if (options && options.ajax && typeof options.ajax.data === 'function') {
                    const originalData = options.ajax.data;
                    const $select = this;
                    options.ajax.data = (params) => {
                        let req = originalData.call(this, params);
                        return modifyAutocompleteRequest(req, $select);
                    };
                }
                return originalSelect2.apply(this, arguments);
            };

            const result = originalDjangoAdminSelect2.apply(this, arguments);
            $.fn.select2 = originalSelect2;
            return result;
        };
    }
})(django.jQuery);

You could write method as such, on the source model class instead of the model class:

@admin.register(SpeakerIdentificationTask)
class SpeakerIdentificationTaskAdmin(BaseTaskAdmin):
    autocompletex_initial = {'field_values': ['input_meeting']}

    def autocompletex_context_documents(self, request, queryset, search_term, model_admin, **kwargs):
        queryset, use_distinct = model_admin.get_search_results(request, queryset, search_term)
        meeting_id = request.GET.get('input_meeting')
        if meeting_id:
            queryset = queryset.filter(meetings__id=meeting_id).distinct()

        return queryset, use_distinct

It’s just example of how this could work +/-.
Maybe people have better ideas.
I want to start discussion on this topic ^^

Regards
Mateusz

1 Like