Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django filter ModelFormSet field choices... different from limiting the Formset's queryset

I understand that it is possible to override the default queryset 'used' by the modelformset. This just limits the objects for which a form is created.

I also found a Stack Overflow question about filtering ForeignKey choices in a Django ModelForm, but not a ModelForm Set and about limiting available choices in a Django formset, but not a Model FormSet. I have included my version of this code below.

What I want to do is render a ModelFormSet, for a school class ('teachinggroup' or 'theclass' to avoid clashing with the 'class' keyword) with one field limited by a queryset. This is for a teacher's class-editing form, to be able to reassign pupils to a different class, but limited to classes within the same cohort.

My models.py

class YearGroup(models.Model):
    intake_year = models.IntegerField(unique=True)
    year_group = models.IntegerField(unique=True, default=7)
    def __unicode__(self):
        return u'%s (%s intake)' % (self.year_group, self.intake_year)

    class Meta:
        ordering = ['year_group']

class TeachingGroup(models.Model):
    year = models.ForeignKey(YearGroup)
    teachers = models.ManyToManyField(Teacher)
    name = models.CharField(max_length=10)
    targetlevel = models.IntegerField()
    def __unicode__(self):
        return u'Y%s %s' % (self.year.year_group, self.name)

    class Meta:
        ordering = ['year', 'name']

My views.py

def edit_pupils(request, teachinggroup):
    theclass = TeachingGroup.objects.get(name__iexact = teachinggroup)
    pupils = theclass.pupil_set.all()

    PupilModelFormSet = modelformset_factory(Pupil)

    classes_by_year = theclass.year.teachinggroup_set.all()
    choices = [t for t in classes_by_year]
#    choices = [t.name for t in classes_by_year] #### I also tried this

    if request.method == 'POST':
        formset = PupilModelFormSet(request.POST,queryset=pupils)
        if formset.is_valid():
            formset.save()
            return redirect(display_class_list, teachinggroup = teachinggroup)
    else:
        formset = PupilModelFormSet(queryset=pupils)
        for form in formset:
            for field in form:
                if 'Teaching group' == field.label:
                    field.choices = choices


    return render_to_response('reassign_pupils.html', locals())

As you can see, I am limiting the choices to the queryset classes_by_year, which is only classes which belong to the same year group. This queryset comes out correctly, as you can see in the rendered page below, but it doesn't affect the form field at all.

My template

{% for form in formset %}
<tr>
    {% for field in form.visible_fields %}
    <td>            {# Include the hidden fields in the form #}
        {% if forloop.first %}
            {% for hidden in form.hidden_fields %}
            {{ hidden }}
            {% endfor %}
        {% endif %}
        <p><span class="bigtable">{{ field }}</span>
        {% if field.errors %}
            <p><div class="alert-message error">
            {{field.errors|striptags}}</p>
            </div>
        {% endif %}
    </td>
    {% endfor %}
</tr>
{% endfor %}
</table>
<input type="submit" value="Submit changes"></p>
</form>
{{ choices }} <!-- included for debugging -->

The page renders with all teaching groups (classes) visible in the select widget, but the tag at the bottom of the page renders as: [<TeachingGroup: Y8 82Ma2>, <TeachingGroup: Y8 82Ma3>], accurately showing only the two classes in Year 8.

Note that I've also read through James Bennett's post So you want a dynamic form as recommended by How can I limit the available choices for a foreign key field in a django modelformset?, but that involves modifying the __init__ method in forms.py, and yet the only way I know how to create a ModelFormSet is with modelformset_factory, which doesn't involve defining any classes in forms.py.

Further to help from Luke Sneeringer, here is my new forms.py entry. After reading Why do I get an object is not iterable error? I realised that some of my problems came from giving a tuple to the field.choices method, when it was expecting a dictionary. I used the .queryset approach instead, and it works fine:

class PupilForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super(PupilForm, self).__init__(*args, **kwargs)
        thepupil = self.instance
        classes_by_year = thepupil.teaching_group.year.teachinggroup_set.all()
        self.fields['teaching_group'].queryset = classes_by_year

class Meta:
    model = Pupil
like image 945
nimasmi Avatar asked Sep 10 '11 11:09

nimasmi


2 Answers

As best as I can tell, you've actually put all the pieces together except one. Here's the final link.

You said you read the dynamic form post, which involves overriding the __init__ method in a forms.Form subclass, which you don't have. But, nothing stops you from having one, and that's where you can override your choices.

Even though modelformset_factory doesn't require an explicit Form class (it constructs one from the model if none is provided), it can take one. Use the form keyword argument:

PupilModelFormset = modelformset_factory(Pupil, form=PupilForm)

Obviously, this requires defining the PupilForm class. I get the impression you already know how to do this, but it should be something like:

from django import forms

class PupilForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super(PupilForm, self).__init__(*args, **kwargs)
        self.fields['teaching_group'].choices = ______ # code to generate choices here

    class Meta:
        model = Pupil

The last problem you might have is that a modelformset_factory just takes the class, which means that the constructor will be called with no arguments. If you need to send an argument dynamically, the way to do it is to make a metaclass that generates the form class itself, and call that metaclass in your modelformset_factory call.

like image 161
Luke Sneeringer Avatar answered Oct 23 '22 10:10

Luke Sneeringer


You can accomplish this by setting field choices of a form in a formset is in the forms init and overwriting the self.fields['field_name'].choices. This worked fine for me but I needed more logic in my view after the formset was initialized. Here is what works for me in Django 1.6.5:

from django.forms.models import modelformset_factory
user_choices = [(1, 'something'), (2, 'something_else')]  # some basic choices
PurchaserChoiceFormSet = modelformset_factory(PurchaserChoice, form=PurchaserChoiceForm, extra=5, max_num=5)
my_formset = PurchaserChoiceFormSet(self.request.POST or None, queryset=worksheet_choices)

# and now for the magical for loop and override each desired fields choices
for choice_form in my_formset:
    choice_form.fields['model'].choices = user_choices

I wasn't able to find the answer for this but tried it out and it works in Django 1.6.5. I figured it out since formsets and for loops seem to go so well together :)

like image 1
radtek Avatar answered Oct 23 '22 10:10

radtek