belek
belek

Reputation: 957

Get queryset for ModelMultipleChoiceField field based on ModelChoiceField

I need form, where user can select tours based on selected tag. I have implemented it via view, custom html form and some AJAX stuff:

HTML form:

<form id="add-destination" method="post">
    {% csrf_token %}
    <select name="tag_id" id="tag_id">
        <option value=''>---</option>
        {% for tag in tags %}
        <option value={{ tag.id }}>{{ tag.name }}</option>
        {% endfor %}
    </select></br>
    <select multiple="multiple" name="tours" id="tours">
    </select></br>
    <button type="submit">Submit</button>
</form>

View to get tours by tag

def get_tours_by_tag(request):
    tag_id = Tag.objects.get(id=request.GET.get('tag_id', None))
    tours = Inbound.objects.filter(tags=tag_id)
    return JsonResponse({"status": "ok", "tours": serializers.serialize("json", list(tours))})

JS:

$(function(){
    $('#tag_id').change(function(e){
        $('#tours').html('');
        e.preventDefault();
        $.ajax({
            type: 'GET',
            data: $(e.currentTarget).serialize(),
            url: "/ru/tour/get_tours/",
            success: function(data){
               var tour_data = JSON.parse(data.tours);
               if(data.status=='ok'){
                   $.each(tour_data, function(i, item) {
                       $('#tours').append('<option value='+item['pk']+'>'+item['fields']['title']+'</option>');
                   });
               }else{
                 console.log("Not okay!")
               }
            }
        });
    })
});

This way works fine, but I know that it's not right way to solve such problem. And I want to do it by using power of django forms.

So I have model Tours:

class Tour(models.Model):
    title = models.CharField(max_length=255)
    tags = models.ManyToManyField(Tag)

And form:

class FiveDestinationForm(forms.Form):
    tags = forms.ModelChoiceField(queryset=Tag.objects.all())
    tours = forms.ModelMultipleChoiceField(queryset=????)

    fields = ('tag', 'tours')

I found some close enough questions and tried to solve my problem by overriding __init__ method in form, as it was suggested:

class MyForm(forms.Form):
    tags = forms.ModelChoiceField(queryset=Tag.objects.all())
    tours = forms.ModelMultipleChoiceField(queryset=Tour.objects.none())

    def __init__(self, *args, **kwargs):
        qs = kwargs.pop('tours', None)
        super(MyForm, self).__init__(*args, **kwargs)
        self.fields['tours'].queryset = qs

    fields = ('tag', 'tours')

But I get following error:

'NoneType' object has no attribute 'iterator'

So my question is how to properly set queryset for ModelMultipleChoiceField based on what user chosed in ModelChoiceField ?

Upvotes: 1

Views: 1251

Answers (1)

Nuschk
Nuschk

Reputation: 518

The error you're seeing is most probably because the Form does not receive a 'tours' argument (it will receive a dictionary named data instead, containing the fields' data in an unvalidated form) and thus the qs variable will be None.

You should probably do something like this:

class MyForm(forms.Form):
    tag = forms.ModelChoiceField(queryset=Tag.objects.all())
    tours = forms.ModelMultipleChoiceField(queryset=Tour.objects.none())

    def __init__(self, *args, **kwargs):
        super(MyForm, self).__init__(*args, **kwargs)

        if kwargs['data']:
            tag_id = kwargs['data'].get('tag', None)
            if tag_id:
                self.fields['tours'].queryset = Tour.objects.filter(tag_id=tag_id)

Note that we bypass django's form mechanics a bit here and need to parse the tag_id ourselves. It's not a big deal, though.

From a UX vantage point, this is a rather hard problem to get right without the use of ajax. So I wouldn't completely dismiss your solution either.

Upvotes: 2

Related Questions