Daehin
Daehin

Reputation: 11

Dynamic FilteredSelectMultiple in django-admin

I don't know if this is even possible, any way, I currently have something as the following:

class Incidence(models.Model):
    ...
    instalation = models.ForeignKey('Instalation')
    machine = models.ManyToManyField('Machine')
    ...

class Machine(models.Model):
    ...
    instalation = models.ForeignKey('Instalation')
    ...

So Machines belongs to instalations and incidences are related to machines and incidences, the idea is to put a dynamic FilteredSelectMultiple widget to select the machines related with the incidence in the admin page. The admin currently is something as:

class IncidenceMachineForm(forms.ModelForm):
    filtered_machine = ModelMultipleChoiceField(
        queryset=Machine.objects.order_by('hostname'),
        required=False, widget=FilteredSelectMultiple("filtered machine name", is_stacked=False)
    )
    class Meta:
        model = Incidence

And then, the modelAdmin uses the form IncidenceMachineForm. The idea is that when you select the instalation of the incidence, only the machines related to that instalation are available for selection. I guess something as this is not possible:

queryset=Machine.objects.filter(instalation=self.instalation).order_by('hostname'),

Any ideas will be highly appreciated. Thanks!

Upvotes: 1

Views: 3165

Answers (3)

mujad
mujad

Reputation: 703

from django.contrib.admin.widgets import FilteredSelectMultiple    

@admin.register(YourModel)
    class YourModelAdmin(admin.ModelAdmin):

        def formfield_for_manytomany(self, db_field, request, **kwargs):
            kwargs['widget'] = FilteredSelectMultiple(
                db_field.verbose_name,
                False,
            )
            return super().formfield_for_manytomany(db_field, request, **kwargs)

fast and don't need to override ModelForm or etc. effect all m2m fields.

Upvotes: 0

sp1111
sp1111

Reputation: 798

I notice that FilteredSelectMultiple widget has already cached, converted and changed the name of original widget after the page is loaded, so changing the "option" list of "select" tag is not enough.

I came up with this solution:

  • wrap "select" list inside another element ("div" for instance)
  • use data received from ajax call to re-create the original list
  • call "SelectFilter.init" to re-construct the FilteredSelectMultiple widget

Here is the code I have tested:

$('#id_instalation').change(function() {
    var selected = $('#id_instalation').val();
    if(selected) {
        $.ajax({
            url: '/url/to/get/machines/' + selected,
            success: function(list) {
                var options = [];
                options.push('<select multiple="multiple" class="selectfilter" name="machine" id="id_machine">');
                for(i in list){
                    options.push('<option value="' + list[i][0] + '">' +
                        list[i][1] + '</option>');
                }
                options.push('</select>');
                $('#machine_wrapper').html(options.join(''));

                // Change title of widget
                var title = $('#id_instalation option:selected"').text().toLowerCase();
                SelectFilter.init("id_machine", title, 0, "/path/to/django/media/");
            },
            error: function() {
                alert('Server error');
            },
        });
    }
}

This is the sample of data returned from ajax call:

[[1, "Machine 1"], [2, "Machine 2"], [3, "Machine 3"]]

For server side implementation, please see Chris Pratt's answer

Note: tested with:

  • jquery-1.7.2
  • django 1.2.5

Upvotes: 2

Chris Pratt
Chris Pratt

Reputation: 239300

You can do that after the model has been saved, and there's an instalation associated with it to use (though the lookup would be instalation=self.instance.instalation).

However, that doesn't do you much good, because if a different instalation is selected the list would still be the one for the old selection, and obviously you get no help when first creating the object.

As a result, the only way to accomplish this is with AJAX. You create a view to receive the selected instalation id, and return a JSON response consisting of machines associated with it. Tie the view into your urlconf, and then hit it with AJAX and update the select box based on the results.

from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404
from django.utils import simplejson

def ajax_admin_get_machines_for_instalation(request):
    instalation_id = request.GET.get('instalation_id')
    if instalation_id is None:
        # instalation_id wasn't provided so return all machines
        machines_qs = Machine.objects.all()
    else:
        instalation = get_object_or_404(Instalation, pk=instalation_id)
        machines_qs = Machine.objects.filter(instalation=instalation)

    # 'name' is the field you want to use for the display value
    machines = machines_qs.values('pk', 'name')

    return HttpResponse(simplejson.dumps(machines), mimetype='application/json')

Then the JS:

(function($){
    $(document).ready(function(){
        function update_machine_options(){
            var selected = $('#id_instalation').val();
            if (selected) {
                $.getJSON('/url/for/ajax/view/', {
                    instalation_id: selected
                }, function(data, jqXHR){
                    var options = [];
                    for (k in data) {
                        options.append('<option value="'+data[k].pk+'">'+data[k].name+'</option>');
                    }
                    $('#id_machine').html(options.join(''));
                });
            }
        }

        update_machine_options();
        $('#id_instalation').change(function(){
            update_machine_options();
        });
    });
})(django.jQuery);

Upvotes: 1

Related Questions