From 3054cc605422fcb204a892ffa1598c6d32ef3ef9 Mon Sep 17 00:00:00 2001 From: Guido Longoni Date: Mon, 3 Jul 2023 13:23:36 +0200 Subject: [PATCH] drilldown autocomplete --- django/contatti_app/admin.py | 26 +++- django/contatti_app/drilldown_autocomplete.py | 132 ++++++++++++++++++ .../admin/css/drilldown_autocomplete.css | 14 ++ django/static/admin/js/autocomplete.js | 68 +++++++++ 4 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 django/contatti_app/drilldown_autocomplete.py create mode 100644 django/static/admin/css/drilldown_autocomplete.css create mode 100644 django/static/admin/js/autocomplete.js diff --git a/django/contatti_app/admin.py b/django/contatti_app/admin.py index 32d0d8f..7026d4b 100644 --- a/django/contatti_app/admin.py +++ b/django/contatti_app/admin.py @@ -142,9 +142,33 @@ class EmailAdmin(HiddenModel, PolymorphicParentModelAdmin, PolymorphicChildModel class RecapitoInline(StackedPolymorphicInline): - class IndirizzoInline(StackedPolymorphicInline.Child): + class IndirizzoInline(StackedPolymorphicInline.Child, DrillDownAutocompleteModelAdmin): model = models.Indirizzo autocomplete_fields = ('dug','comune','cap','nazione',) + drilldown_autocomplete_fields = { + 'cap': { + 'linked': { + 'comune': 'comuni', + }, + 'reset_on_included': {}, + 'reset_on_excluded': {}, + 'reset_on_reset': {}, + 'autoupdate_on_reset': False, + 'autoselect_on_singleton': False, + 'included_only': False, + }, + 'comune': { + 'linked': { + 'cap': 'cap', + }, + 'reset_on_included': {}, + 'reset_on_excluded': {}, + 'reset_on_reset': {}, + 'autoupdate_on_reset': False, + 'autoselect_on_singleton': False, + 'included_only': False, + } + } class SedeInline(StackedPolymorphicInline.Child): model = models.Sede diff --git a/django/contatti_app/drilldown_autocomplete.py b/django/contatti_app/drilldown_autocomplete.py new file mode 100644 index 0000000..58d283e --- /dev/null +++ b/django/contatti_app/drilldown_autocomplete.py @@ -0,0 +1,132 @@ +import json +from functools import update_wrapper + +from django.apps import apps +from django.contrib import admin +from django.contrib.admin.views.autocomplete import AutocompleteJsonView +from django.contrib.admin.widgets import AutocompleteMixin, AutocompleteSelect +from django.core.exceptions import FieldDoesNotExist, PermissionDenied +from django.db.models import Case, Q, Value, When +from django.http import Http404, JsonResponse +from django.urls import path + + +class DrillDownAutocompleteJsonView(AutocompleteJsonView): + """Handle AutocompleteWidget's AJAX requests for data.""" + + def process_request(self, request): + """ + Validate request integrity, extract and return request parameters. + + Since the subsequent view permission check requires the target model + admin, which is determined here, raise PermissionDenied if the + requested app, model or field are malformed. + + Raise Http404 if the target model admin is not configured properly with + search_fields. + """ + ( + term, + model_admin, + source_field, + to_field_name + ) = super().process_request(request) + linkedfields = request.GET.get("linkedfields") + if linkedfields: + try: + linkedfields = json.loads(linkedfields) + except json.decoder.JSONDecodeError as e: + raise PermissionDenied from e + app_label = request.GET["app_label"] + model_name = request.GET["model_name"] + source_model = apps.get_model(app_label, model_name) + try: + drilldown_field = self.admin_site._registry[ + source_model].get_drilldown_autocomplete_fields(request)[source_field.name] + except KeyError as e: + raise PermissionDenied from e + remote_model = source_field.remote_field.model + try: + for v in drilldown_field['linked'].values(): + remote_model._meta.get_field(v) + except FieldDoesNotExist as e: + raise PermissionDenied from e + self.remote_model = remote_model + self.linkedfields = linkedfields + self.drilldown_filter_data = { + v: linkedfields[k] for k, v in drilldown_field['linked'].items() if k in linkedfields} + + return term, model_admin, source_field, to_field_name + + def get_queryset(self): + """Return queryset based on ModelAdmin.get_search_results().""" + qs = super().get_queryset().only() + # print('Prima:',qs.query,'\n\n') + if hasattr(self, 'linkedfields'): + drilldown_filter_conditions = Q(**self.drilldown_filter_data) + qs = qs.annotate(ddok=Case(When(drilldown_filter_conditions, then=Value( + 1)), default=Value(0))).order_by('-ddok', *qs.query.order_by) + # print('Dopo:',qs.query,'\n\n') + else: + qs = qs.annotate(ddok=Value(1)) + return qs + + def serialize_result(self, obj, to_field_name): + """ + Convert the provided model object to a dictionary that is added to the + results list. + """ + return {"id": str(getattr(obj, to_field_name)), "text": str(obj), "ddok": obj.ddok} + + +class DrillDownAutocompleteMixin(AutocompleteMixin): + url_name = "%s:drilldown_autocomplete" + + +class DrillDownAutocompleteSelect(AutocompleteSelect, DrillDownAutocompleteMixin): + pass + + +class DrillDownAutocompleteModelAdmin(admin.options.BaseModelAdmin): + drilldown_autocomplete_fields = dict() + class Media: + css = { + 'all': ('admin/css/drilldown_autocomplete.css',) + } + + def get_drilldown_autocomplete_fields(self, request): + return self.drilldown_autocomplete_fields + + def formfield_for_foreignkey(self, db_field, request, **kwargs): + """ + Get a form Field for a ForeignKey. + """ + db = kwargs.get("using") + + if "widget" not in kwargs: + daf = self.get_drilldown_autocomplete_fields(request) + if db_field.name in daf: + kwargs["widget"] = DrillDownAutocompleteSelect( + db_field, self.admin_site, attrs={ + "data-linkedfields": json.dumps(list(daf[db_field.name]['linked'].keys())), + }, using=db + ) + return super().formfield_for_foreignkey(db_field, request, **kwargs) + + def drilldown_autocomplete_view(self, request): + return DrillDownAutocompleteJsonView.as_view(admin_site=self.admin_site)(request) + + def get_urls(self): + def wrap(view, cacheable=False): + def wrapper(*args, **kwargs): + return self.admin_site.admin_view(view, cacheable)(*args, **kwargs) + + wrapper.admin_site = self + return update_wrapper(wrapper, view) + + urls = super().get_urls() + drilldown_urls = [ + path("drilldown/", wrap(self.drilldown_autocomplete_view), + name="drilldown_autocomplete"), + ] + return drilldown_urls + urls diff --git a/django/static/admin/css/drilldown_autocomplete.css b/django/static/admin/css/drilldown_autocomplete.css new file mode 100644 index 0000000..f731f93 --- /dev/null +++ b/django/static/admin/css/drilldown_autocomplete.css @@ -0,0 +1,14 @@ +.select2-container--admin-autocomplete .select2-results__option[aria-selected=true].drilldown_ok:not(:hover) { + background-color: var(--selected-row); +} + +.select2-container--admin-autocomplete .select2-results__option[aria-selected=false].drilldown_ok:not(.select2-results__option--highlighted):not(:hover) { + background-color: var(--body-bg); +} +.select2-container--admin-autocomplete .select2-results__option[aria-selected].drilldown_ko:not(:hover) { + background-color: #888 +} + +span.drilldown_ko { + font-style: italic; +} \ No newline at end of file diff --git a/django/static/admin/js/autocomplete.js b/django/static/admin/js/autocomplete.js new file mode 100644 index 0000000..16a90bd --- /dev/null +++ b/django/static/admin/js/autocomplete.js @@ -0,0 +1,68 @@ +'use strict'; +{ + const $ = django.jQuery; + + $.fn.djangoAdminSelect2 = function() { + $.each(this, function(i, element) { + $(element).select2({ + ajax: { + data: (params)=>{ + var out = { + term: params.term, + page: params.page, + app_label: element.dataset.appLabel, + model_name: element.dataset.modelName, + field_name: element.dataset.fieldName + }; + if (element.dataset.hasOwnProperty('linkedfields')) { + var linkedfields = JSON.parse(element.dataset.linkedfields) + var linkedfields_obj = {}; + var some_obj = false; + for (var i in linkedfields) { + if (linkedfields.hasOwnProperty(i)) { + var field = linkedfields[i]; + var value = document.querySelectorAll('[data-field-name=' + field + '].admin-autocomplete')[0].value; + if (value !== '') { + //console.log(field + '=' + value); + linkedfields_obj[field] = value; + some_obj = true; + } + } + } + if (some_obj) { + out['linkedfields'] = JSON.stringify(linkedfields_obj); + } + } + return out + } + }, + templateResult: (item,container)=>{ + var styleClass = ''; + if (element.dataset.hasOwnProperty('linkedfields')) { + element.classList.add('drilldown'); + if (item.ddok === 1) { + styleClass = 'drilldown_ok'; + } else { + styleClass = 'drilldown_ko'; + } + container.classList.add(styleClass); + } + return $('' + item.text + ''); + } + }); + }); + return this; + } + ; + + $(function() { + // Initialize all autocomplete widgets except the one in the template + // form used when a new formset is added. + $('.admin-autocomplete').not('[name*=__prefix__]').djangoAdminSelect2(); + }); + + document.addEventListener('formset:added', (event)=>{ + $(event.target).find('.admin-autocomplete').djangoAdminSelect2(); + } + ); +}