Set initial selected values for forms.MultipleChoiceField

I want to display initial values as selected on a MultipleChoice form field in Django when the form loads. I populate a formset with different forms. Each form has only one field ‘answer’, which is initialized based on a custom parameter passed to the form’s init() method.

class AnswerForm(forms.Form):
    def __init__(self, *args, **kwargs):
        """
        Initialize label & field 
        :returns None:
        """
        question = kwargs.pop('question')  # A Question object
        super(AnswerForm, self).__init__(*args, **kwargs)
        if question.type == Types.RADIO:
            choices_ = [(op.id, op) for op in question.option_set.all()]
            self.fields['answer'] = forms.ChoiceField(label=question.statement,
                                                      initial=1,
                                                      widget=forms.RadioSelect,
                                                      choices=choices_)
        elif question.type == Types.CHECKBOX:
            choices_ = [(op.id, op) for op in question.option_set.all()]
            self.fields['answer'] = forms.MultipleChoiceField(label=question.statement,
                                                              initial=[1,3],
                                                              widget=forms.CheckboxSelectMultiple,
                                                              choices=choices_)

This renders the following HTML:

image

But it doesn’t get into the form’s cleaned_data. When I submit formset, the request.POST data goes into this view:

    def post(self, request, form_id):
        """
        Process & save the responses obtained from a form into DB
        :param request: An HTTPRequest object
        :param form_id: form id whose responses arrive
        :returns HttpResponse object with a results template
        """
        formset = FormHandler.AnswerFormSet(request.POST, request.FILES,
                                            form_kwargs={'questions': FormHandler.qs})

        if formset.is_valid():
            for form in formset:
                cd = form.cleaned_data
                # Access cd['answer'] here but cd appears to be empty dict {}
                # with no key named 'answer'

The cleaned_data does have the correct ‘answer’ value in the case of Radio, but in this case, it doesn’t contain the list of selected IDs which it should. I’ve checked that request.POST.getlist('form_#_answer') does show the correct list of [‘1’, ‘3’] but it somehow doesn’t get into the formset’s cleaned_data. I’ve spent hours trying to find out why this happens. Can’t find the answer anywhere in the Django docs either. Can anyone explain why this is happening?

Can we see the full definition of AnswerFormSet?

I’m looking at the disparity between the form_kwargs you’re passing here:

(notice the key is ‘questions’)

where your form is looking for a key named ‘question’:

And so I’m wondering about the transition between the two.

Ken

@KenWhitesell
https://forum.djangoproject.com/t/pass-different-parameters-to-each-form-in-formset/4040?u=raphy-n

The code and subsequent posts are nearly a week old. I tend to work under the assumption that you’ve been working on this and trying different things. So rather than trying to piece together bits from different posts across different days, please post the full definition of AnswerFormSet.

Sorry, I didn’t hv access to my code when u asked. I thought my older post wud give u sufficient info. Here’s my forms.py file in which I overrride the get_form_kwargs(index) method of BaseFormSet:

from django import forms
from django.forms import BaseFormSet
from .models import Question, QuestionTypeChoices as Types


class BaseAnswerFormSet(BaseFormSet):

    def get_form_kwargs(self, index):
        """
        Overriding to provide unique Question object to each form in formset
        :param index: form index for list of forms in formset
        :return: Dictionary with only relevant kwargs for a form instance
        """
        kwargs = super().get_form_kwargs(index)
        kwargs['question'] = kwargs['questions'][index]  # Get one Question object from list of Questions provided to formset
        kwargs.pop('questions')  # Remove since not needed by individual form
        return kwargs


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, *args, **kwargs):
        """
        Initialize label & field type/widget
        :returns None:
        """
        self.question = question = kwargs.pop('question')
        super(AnswerForm, self).__init__(*args, **kwargs)
        if question.type == Types.RADIO:
            choices_ = [(op.sort_num, op) for op in question.option_set.all()]
            self.fields['answer'] = forms.ChoiceField(required=True,
                                                      initial=1,
                                                      label=question.statement,
                                                      widget=forms.RadioSelect,
                                                      choices=choices_)
            self.initial['answer'] = 1
        elif question.type == Types.CHECKBOX:
            choices_ = [(op.sort_num, op) for op in question.option_set.all()]
            self.fields['answer'] = forms.MultipleChoiceField(required=True,
                                                              initial=[1, 2],
                                                              label=question.statement,
                                                              widget=forms.CheckboxSelectMultiple,
                                                              choices=choices_)
        elif question.type == Types.TEXT:
            self.fields['answer'] = forms.CharField(required=True,
                                                    initial="Initial text",
                                                    label=question.statement,
                                                    max_length=100)
        elif question.type == Types.NUM:
            min_, max_ = None, None
            if question.option_set.all().exists():
                min_ = int(question.option_set.first().value)
                max_ = int(question.option_set.last().value)
            self.fields['answer'] = forms.IntegerField(required=True,
                                                       initial=min_,
                                                       label=question.statement,
                                                       min_value=min_,
                                                       max_value=max_)
        elif question.type == Types.RANGE:
            min_ = int(question.option_set.first().value)
            max_ = int(question.option_set.last().value)
            self.fields['answer'] = forms.IntegerField(required=True,
                                                       label=question.statement,
                                                       initial=min_,
                                                       min_value=min_,
                                                       max_value=max_,
                                                       widget=forms.NumberInput(
                                                           attrs={
                                                               'type': 'range',
                                                               'min': min_,
                                                               'max': max_,
                                                           }
                                                       ))
        else:
            raise NotImplementedError  # In case of future bugs

Here’s my FormHandler class where I override it’s get & post methods:

class FormHandler(View):
    template_name = 'forms/answer_form.html'
    qs, AnswerFormSet = None, None

    def get(self, request, form_id):
        qs = Question.objects.filter(form=form_id)
        AnswerFormSet = formset_factory(AnswerForm,
                                        formset=BaseAnswerFormSet,
                                        extra=len(qs))
        formset = AnswerFormSet(form_kwargs={'questions': qs})

        FormHandler.qs, FormHandler.AnswerFormSet = qs, AnswerFormSet
        return render(request, FormHandler.template_name, {'formset': formset})

    def post(self, request, form_id):
        """
        Process & save the responses obtained from a form into DB
        :param request: An HTTPRequest object
        :param form_id: form id whose responses arrive
        :returns HttpResponse object with a results template
        """
        formset = FormHandler.AnswerFormSet(request.POST,
                                            form_kwargs={'questions': FormHandler.qs})

        # TODO: Why does empty form return True from is_valid()?
        if formset.is_valid():
            user_resp = UserResponse()  # Every answer is connected to unique UserResponse object in DB
            user_resp.ip = None  # TODO: Get user's real IP addr here

            is_auth = request.user.is_authenticated
            user_resp.user = request.user if is_auth else None

            for form in formset:
                cd = form.cleaned_data
                q = form.question
                if isinstance(cd['answer'], list):
                    for ans in cd['answer']:
                        self.save_response(ans, user_resp, q)
                else:
                    self.save_response(cd['answer'], user_resp, q)
            print("Successful!")
            # TODO: Why does message only show in Admin site?
            messages.success(request, "Your response has been saved successfully!")
            # return redirect('home')
        return render(request, self.template_name, {'formset': formset})

    def save_response(self, value, user_resp, question):
        answer = Answer()
        answer.value = str(value)
        answer.user_resp = user_resp
        answer.question = question
        user_resp.save()
        answer.save()
        return None

It appears to me that you’re trying to reuse the same formset instance between your get and post methods. The problem with doing that is that an instance of a formset only renders the forms once, the first time they’re needed. After that, any further reference to those forms is going to reuse them.

You want to create a fresh formset in both your get and post methods by calling the formset_factory method every time. Do not try to persist the formset at the module layer.

(If you look at the formset examples in the docs, you’ll see that formset_factory is being called regardless of whether that view is being invoked as a get or post)