首页 > 解决方案 > 当用户单击 Django admin 中选定的自动完成字段选项时,如何显示相关对象弹出窗口?

问题描述

我想在用户单击 Django 管理员自动完成多选字段中的选定选项时显示标准相关对象弹出窗口,就像单击ForeignKey字段铅笔图标时一样。

型号如下:

class Author(models.Model):
    name = models.CharField(_('name'), max_length=160)

class Book(models.Model):
    authors = models.ManyToManyField(Author, verbose_name=_('authors'), blank=True)
    ...

是否可以通过扩展 Django admin 来做到这一点?

标签: djangodjango-admindjango-autocomplete-light

解决方案


我发现使用ModelSelect2Multiple来自django-autocomplete-light (DAL) 的小部件更容易添加所需的自定义 HTML。

管理员配置如下:

from dal import autocomplete

@admin.register(Book)
class BookAdmin(admin.ModelAdmin):

    class Media:
        js = [
          '/static/books/js/book-admin.js',
          # other required JS files, see https://github.com/yourlabs/django-autocomplete-light/issues/1143#issuecomment-632755326
        ]

    def formfield_for_manytomany(self, db_field, request, **kwargs):
        if db_field.name == 'authors':
            return forms.ModelMultipleChoiceField(
                required=False,
                label=Author._meta.verbose_name_plural.title(),
                queryset=Author.objects.all(),
                widget=autocomplete.ModelSelect2Multiple(
                    url='author-autocomplete',
                    attrs={'data-html': True}))
        return super().formfield_for_foreignkey(db_field, request, **kwargs)

DAL 视图如下:

from django.utils.safestring import mark_safe

from dal import autocomplete

from .models import Author
from django.urls import reverse


class AuthorAutocomplete(autocomplete.Select2QuerySetView):
    def get_queryset(self):
        if not self.request.user.is_staff:
            return Author.objects.none()

        qs = Author.objects.all()
        if self.q:
            qs = qs.filter(name__icontains=self.q)

        return qs

    def get_selected_result_label(self, item):
        change_url = reverse('admin:books_author_change', kwargs={'object_id': item.id})
        return mark_safe('<span onclick="event.stopPropagation(); showRelatedObjectPopup({'
                         f"href: '{change_url}?_popup=1', id: 'change_id_author'"
                         f'}})">{item.name}</span>')

在 admin 中的Book change 视图的Authors字段中选择新作者时,HTML 元素由 管理,因此存在自定义 HTML,单击新选择的作者会按预期打开弹出窗口。但是自定义 HTML 不会出现在现有选择中,因此必须使用 jQuery 添加点击处理程序:ModelSelect2Multiplebook-admin.js

'use strict';

window.addEventListener("load", function () {
    /**
     * Show related object popup when user clicks on selected author name.
     */
    (function ($) {
        var $authorsSelect2Selections = $('div.form-row.field-authors .select2-selection__choice > span:nth-child(2)');
        var $authorOptions = $('#id_authors > option');
        $authorsSelect2Selections.click(function ($event) {
            $event.stopPropagation();
            var self = this;
            // Find corresponding option by text comparison, assuming that author name is unique
            var $result = $authorOptions.filter(function() {
                return $(this).text() === self.textContent;
            });
            showRelatedObjectPopup({
                href: '/admin/books/author/' + $result.val() + '/change/?_popup=1',
                id: 'change_id_other_authors'
            });
        });
    })(django.jQuery);
});

event.stopPropagation()防止 Select2 下拉菜单打开。

您还需要覆盖dismissChangeRelatedObjectPopup并避免出现问题,这是一个不完整的版本dismissAddRelatedObjectPopupbook-admin.js

  /**
   * Override Django related object popup dismissal functions with DAL amendments.
   * Incomplete.
   */
  (function ($) {
    function dismissChangeRelatedObjectPopupForDAL(win, objId, newRepr, newId) {
      var elem = document.getElementById(win.name);
      if (elem && elem.options && elem.dataset.autocompleteLightUrl) { // this is a DAL element
        $(elem.options).each(function () {
          if (this.value === objId) {
            this.textContent = newRepr;
            // this.value = newId;
          }
        });
        // FIXME: trigger('change') does not update the element as it should and removes popup code
        // $(elem).trigger('change');
        win.close();
      } else {
        dismissChangeRelatedObjectPopupOriginal(win, objId, newRepr, newId);
      }
    }

    window.dismissChangeRelatedObjectPopupOriginal = window.dismissChangeRelatedObjectPopup;
    window.dismissChangeRelatedObjectPopup = dismissChangeRelatedObjectPopupForDAL;

    function dismissAddRelatedObjectPopupForDAL(win, newId, newRepr) {
      var elem = document.getElementById(win.name);
      if (elem && elem.options && elem.dataset.autocompleteLightUrl) { // this is a DAL element
        elem.options[elem.options.length] = new Option(newRepr, newId, true, true);
        // FIXME: trigger('change') adds the new element, but removes popup code
        $(elem).trigger('change');
        win.close();
      } else {
        dismissAddRelatedObjectPopupOriginal(win, newId, newRepr);
      }
    }

    window.dismissAddRelatedObjectPopupOriginal = window.dismissAddRelatedObjectPopup
    window.dismissAddRelatedObjectPopup = dismissAddRelatedObjectPopupForDAL
  })(django.jQuery);

推荐阅读