Pass different parameters to each form in formset

I need to create multiple forms, with each form rendering different content based on a parameter passed to the form’s init() method.

My Form class is like this:

class AnswerForm(forms.Form):
    """
    Form to display a single question-answer pair. Content dynamically rendered
    depending on the Question object provided from DB
    """

    def __init__(self, q, *args, **kwargs):
        """
        Initialize label & field type/widget
        :param q: Question model object with statement, type, options attributes
        :returns None:
        """
        super(AnswerForm, self).__init__(*args, **kwargs)
        if q.type == Choices.RADIO:
            self.fields['answer'] = forms.ChoiceField(label=q.statement,
                                                      widget=forms.RadioSelect,
                                                      choices=q.option_set.all())
        elif q.type == Choices.CHECKBOX:
            self.fields['answer'] = forms.ChoiceField(label=q.statement,
                                                      widget=forms.CheckboxSelectMultiple,
                                                      choices=q.option_set.all())

I create the formset in a view:

def show_form(request, id=1):
    questions = Question.objects.filter(form=id)
    AnswerFormSet = formset_factory(AnswerForm,
                                    formset=BaseAnswerFormSet)
    formset = AnswerFormSet()
    for i, q in enumerate(questions):
        formset[i]
    return render(request, 'forms/answer_form.html', {'formset': formset})

I tried to customize the BaseFormSet class as follows:

class BaseAnswerFormSet(BaseFormSet):

    def get_form_kwargs(self, index, question):
        kwargs = super().get_form_kwargs(index)
        kwargs['q'] = question
        return kwargs

But I can’t get it to work. The docs for Passing custom parameters to formset forms isn’t clear on how I can achieve this.

I’ve racked my brain on this but can’t figure out a way for it. Help would be much appreciated.

2 Likes

Hi,

I personally found formsets super confusing when I tried to read up on it (so much that to learn it I’ve started on a tutorial series). I agree that the docs on passing custom parameters unfortunately seem extra confusing, like you say.

I found it helpful to go take a look at Django’s source code for formsets to understand what’s going on. I’ll try to explain what I understood so far, and hopefully you can use it to go further.

Let’s start by assuming that we want to understand what’s happening in the docs you linked where they do this:

formset = ArticleFormSet(form_kwargs={'user': request.user})

What happens with the dictionary passed as form_kwargs?

Here’s the code for how a BaseFormSet instance is initialized

class BaseFormSet:
    # ...
    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
                 initial=None, error_class=ErrorList, form_kwargs=None):
        self.is_bound = data is not None or files is not None
        self.prefix = prefix or self.get_default_prefix()
        self.auto_id = auto_id
        self.data = data or {}
        self.files = files or {}
        self.initial = initial
        self.form_kwargs = form_kwargs or {}
        self.error_class = error_class
        self._errors = None
        self._non_form_errors = None

So, whatever was passed as form_kwargs, will be referenced by the formset instance’s form_kwargs attribute. Seems fairly straightforward.

Now let’s look at BaseFormSet's forms method/property:

    @cached_property
    def forms(self):
        """Instantiate forms at first property access."""
        # DoS protection is included in total_form_count()
        return [
            self._construct_form(i, **self.get_form_kwargs(i))
            for i in range(self.total_form_count())
        ]

What’s important is that as the formset instantiates the forms, it does so using a dictionary returned by its get_form_kwargs method, which is passed the index of the form currently being generated.

So if we look at get_form_kwargs

    def get_form_kwargs(self, index):
        """
        Return additional keyword arguments for each individual formset form.

        index will be None if the form being constructed is a new empty
        form.
        """
        return self.form_kwargs.copy()

As you see, the method returns a copy of the full dictionary that the formset instance’s form_kwargs attribute references. This is the default. But what you want is different, if I understand you correctly.

Rather than giving each individual form the same set of arguments, you want to pass each one a Question instance (entry from the database) “of its own”. i. e. here:

class AnswerForm(forms.Form):
    """
    Form to display a single question-answer pair. Content dynamically rendered
    depending on the Question object provided from DB
    """

    def __init__(self, q, *args, **kwargs):

You want the form to be passed just one question, and it doesn’t make sense for all forms to be passed the same one.

So, what’s important is to remember that when doing a call like formset = ArticleFormSet(form_kwargs={'user': request.user}), what’s called form_kwargs is first and foremost really what is passed to the formset.

If we look at a minimal example (since I don’t have your Question model and to make things as simple as possible), assuming we have this Question model in a Django project:

# questions/models.py
from django.db import models


class Question(models.Model):
    text = models.CharField(max_length=100)

We can now jump into the project’s python shell and do this:

# shell
from django.forms import BaseFormSet
from django.forms import formset_factory
from questions.models import Question
from django import forms

Question.objects.create(text='What is your quest?')
Question.objects.create(text='What is your favorite colour?')
Question.objects.create(text='What... is the air-speed velocity of an unladen swallow? ')

class QuestionForm(forms.Form):
    text = forms.CharField(max_length=100)
    def __init__(self, *args, question, **kwargs):
        self.q_text = question.text
        super().__init__(*args, **kwargs)

class BaseQuestionFormSet(BaseFormSet):
    def get_form_kwargs(self, index):
        kwargs = super().get_form_kwargs(index)
        q = kwargs['questions'][index]
        # note that instead of passing a dictionary which includes a copy
        # of the formset's `form_kwargs`, we actually return a dictionary
        # that holds a single key-value pair
        return {'question': q}

# using list to force "eager" evaluation here, since we'll be using all
# questions' data
qs = list(Question.objects.all())
# making sure that when instantiating formsets, they will be set to
# generate as many forms as we have questions (this seems hacky and there must
# be a better way to do this)
QuestionFormSet = formset_factory(QuestionForm, formset=BaseQuestionFormSet,
                                  extra=len(qs))
# ensures that the instantiated formset will have our list of questions
# in its `form_kwargs` dictionary
formset = QuestionFormSet(form_kwargs={'questions': qs})
formset.forms[0].q_text # outputs 'What is your quest?'

So by overriding the BaseFormSet subclass’ get_form_kwargs method, it’s possible to make the QuestionFormSet instances, upon generating their forms, pass different values to each form. And of course, we have to provide the QuerySet/list that the formset should make use of by passing it in with the ‘form_kwargs’ dictionary. A similar solution should be possible in your case. I feel like I’m doing things here that I shouldn’t, but I don’t really know how/where to figure out what is actually the proper way to do it.

I had some help from this SO thread too, if it’s relevant for you.

8 Likes