Django: AJAX ManyToManyField in admin

自作多情 提交于 2019-12-09 06:25:15

问题


I want to display ManyToManyFields in admin just like filter_horizontal does, but populate the options as the user types into the filter field. There are many options and loading them all at once takes a lot of time.

I found django-ajax-filtered-fields but it seems to me an overkill as it requires changes to model classes, when all I want to do is to replace every multiple select field in a form.

Writing a custom widget field that inherits from admin.widgets.FilteredSelectMultiple seems to be the right way. So I am trying to roll my own widget:

class MultiSelectWidget(FilteredSelectMultiple):
    class Media:
        # here should be some js to load options dynamically
        js = (
            "some_js_to_load_ajax_options.js",
        )

    def render_options(self, choices, selected_choices):
        # this initializes the multiple select without any options
        choices = [c for c in self.choices if str(c[0]) in selected_choices]
        self.choices = choices
        return super(MultiSelectWidget, 
                     self).render_options([], selected_choices)

class MyAdminForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super(MyAdminForm, self).__init__(*args, **kwargs)
        self.fields['m2m_field'].widget = MultiSelectWidget('m2m_field', is_stacked=False)
    class Meta:
        model = MyModel

class MyAdmin(admin.ModelAdmin):
    form = MyAdminForm

which renders correctly.

But I am not sure how to implement this some_js_to_load_ajax_options.js ajax part. Should I write my own jQuery snippet or modify SelectFilter2 which comes with admin/media/js? Anybody been there before?

edit: Although not related to the core of the question, as I only want to override the field's widget, the shorter way is to use formfield_overrides:

class MultiSelectWidget(FilteredSelectMultiple):
    # as above

class MyAdmin(admin.ModelAdmin):
    formfield_overrides = {
        models.ManyToManyField: {'widget': MultiSelectWidget},
    }

回答1:


I started from your code and I used a custom javascript to retrieve values from photologue Photo model; please note that I'm using grappelli and the Django url that get the json object is hardcoded; also the field in my model is called "photos":

# urls.py
url(r'^get_json_photos/(?P<query>[\w-]+)/$', 'catalogo.views.get_json_photos', name='get_json_photos'),


# views.py    
from photologue.models import Photo
from django.utils import simplejson as json

def get_json_photos(request, query):
    photos = Photo.objects.filter(title__icontains=query)[:20]
    p = [ {"name":photo.title, "id":photo.id} for photo in photos ]
    response = json.dumps(p)
    return HttpResponse(response, mimetype="application/json")


# admin.py
from django.conf import settings
from django.contrib.admin.widgets import FilteredSelectMultiple

class MyFilteredSelectMultiple(FilteredSelectMultiple):

    class Media:
        js = (settings.ADMIN_MEDIA_PREFIX + "js/core.js",
              settings.ADMIN_MEDIA_PREFIX + "js/SelectBox.js",
              settings.ADMIN_MEDIA_PREFIX + "js/SelectFilter2.js",
              settings.MEDIA_URL + "js/ajax_photo_list.js")


class MyModelMultipleChoiceField(ModelMultipleChoiceField):

    def clean(self, value):
        return [val for val in value]


class GalleryForm(forms.ModelForm):
    photos = MyModelMultipleChoiceField(queryset=Photo.objects.none(), required=False,
        widget=MyFilteredSelectMultiple(verbose_name="photos", is_stacked=False))

    def __init__(self, *args, **kwargs):
        super(GalleryForm, self).__init__(*args, **kwargs)
        try:
            i = kwargs["instance"]
            gallery = Gallery.objects.get(pk=i.pk)
            qs = gallery.photos.all()
        except:
            qs = Photo.objects.none()
        self.fields['photos'].queryset = qs

    class Meta:
        model = Gallery
        widgets = {
            'photos': MyFilteredSelectMultiple(verbose_name="photos", is_stacked=False)
        }


class GalleryAdmin(admin.ModelAdmin):
    list_display = ('title', 'date_added', 'photo_count', 'is_public')
    list_filter = ['date_added', 'is_public']
    date_hierarchy = 'date_added'
    prepopulated_fields = {'title_slug': ('title',)}
    filter_horizontal = ()
    form = GalleryForm


# ajax_photo_list.js 
(function($){
$("#id_photos_input").live("keyup", function(){
    var querystring = $("#id_photos_input").val();
    if (querystring) {
        $.ajax ({
            type: "GET",
            url: "/get_json_photos/"+querystring+"/",
            cache: false,
            success: function(json) {
                if (json) {
                    var list_from = $("#id_photos_from option").map(function() {
                        return parseInt($(this).val());
                    });
                    var list_to = $("#id_photos_to option").map(function() {
                        return parseInt($(this).val());
                    });
                    for (var pid in json) {
                        if ($.inArray(json[pid].id, list_from) == -1 && $.inArray(json[pid].id, list_to) == -1) {
                            $("#id_photos_from").prepend("<option value='"+json[pid].id+"'>"+json[pid].name+"</option>");
                        }
                    }
                    SelectBox.init('id_photos_from');
                    SelectBox.init('id_photos_to');
                }
            }
        });
    }
})
}(django.jQuery));

I'm thinking to make it generic, since is not the first time that I have this problem,




回答2:


I would hack the select filter, it has a nice set of functions that you can use.




回答3:


If the UI of Select2 appeals to you, you could use Django-Select2 in the Admin.

For m2m it might work like you suggested:

class MyAdmin(admin.ModelAdmin):
    formfield_overrides = {
        models.ManyToManyField: {'widget': ModelSelect2MultipleWidget},
    }

    # required to make jquery available to select2
    # has to be loaded via Admin class (and not via widget or form class) for correct order in output
    class Media:
        js = ("ext/js/jquery.min.js",)

Ajax works by adding the following URL pattern to urls.py:

# if using ModelWidget
url(r'^select2/', include('django_select2.urls')),

Of course, you can also provide your own view implementations, see the documentation linked above.

I'm currently not using it for m2m but for reverse foreign key relations, so I'm using it in a custom form in the Django admin, instantiating the widget explicitly. Thus, in case it's not working with formfield_overrides, the long way would be an option.



来源:https://stackoverflow.com/questions/4984110/django-ajax-manytomanyfield-in-admin

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