Django Model Form Different Choices

No way to tell without the complete context of the problem / errors you’re receiving.

Fixed it :slight_smile:

        new_answers = ProjectQuestionnaireQuestion.objects.filter(
            questionnaire__projectquestionnaireresponse=response
        ).exclude(
            projectquestionnaireanswer__response=response
        )

instead of

        new_answers = ProjectQuestionnaireQuestion.objects.filter(
            questionnaire__response=response
        ).exclude(
            answer__response=response
        )

Now onto the queryset.

Thanks, Ken.

Tom.

Hi Ken,

Sorry to keep asking for help, but i am looking at the documentation for the queryset to render the correct choices for the select field.

# A custom empty label
field1 = forms.ModelChoiceField(queryset=..., empty_label="(Nothing)")

queryset= what is it expecting here? Am i writing the actual query here? Or is it expecting the name of a query created within the view?

I’m probably misunderstanding the documentation, but i can’t see anywhere telling me what it needs to be?

It’s anything that resolves to a Queryset.

What is a queryset?

What creates a queryset?

See Making queries | Django documentation | Django

What i mean is the actual queryset logic goes here, not the results of a query set from the view?

answer = models.ForeignKey(Choice, on_delete=models.CASCADE,null=True, queryset=Choice.objects.filter(something here))

I don’t understand your question.

You can put anything there that returns a queryset.

Ok.

So in a view i have a queryset something = something.object.filter(something)

In my model do i put:

answer = models.ForeignKey(Choice, on_delete=models.CASCADE,null=True, queryset=Choice.objects.filter(something here))```

or

answer = models.ForeignKey(Choice, on_delete=models.CASCADE,null=True, queryset=something

where something is the results of the query set from the view?

Or maybe I’m completely misunderstanding.

I think queryset = is the same as something = so i think i put the actual logic in the model rather than referencing a queryset logic from a view

I wouldn’t say you’re completely misunderstanding. I would expression the observation that it appears you’re not fully comfortable with how data is referenced throughout the system.

You can’t just reference something in the form when that something is defined in the view. You can’t access an object, created in function_x, in function_y without somehow passing that object to function_y. (It can be done, but you need to explicitly do it.)

But what you put there is entirely up to you. You need to decide what that Queryset needs to be.

However, I will point out that this queryset needs to be “adjusted” for each instance of the form being created, because you want different choices to be made available for each question. Each instance of the form being built will need to be “customized” for the question.

Sorry, I’m being an idiot. I read it as being the queryset was applied to the models.py but its actually in the forms.py

And basically, i write my quesyset directly within the field I want the results to be shown.

'answer': forms.Select(choices = Choice.objects.filter(question_id = 1) something along these lines?

And then within my template i need my select options to reference the choices from my quesyset

hi Ken, I still can’t get this to work, i think its because i am using modelformsets.

class AnswerForm(ModelForm):
    class Meta:
        model = ProjectQuestionnaireAnswer
        fields = ('answer','notes')

        widgets = {
            'notes': forms.Textarea(attrs={'class': 'form-control','placeholder': 'Add notes to question here'}),
        }

    def __init__(self, *args, **kwargs):
         question_id = kwargs.pop('question_id')
         super().__init__(*args, **kwargs)
         self.fields['answer'].widget = forms.Select(choices = Choice.objects.filter(question_id = question_id),attrs={'class': 'form-control select2'})

but i am getting the following error when rendering the form within the template

# TypeError at /questions/1/1

cannot unpack non-iterable Choice object

Any clues?

Going to need a lot more detail from the traceback here, along with the view and other directly-related code.

My initial hunch based only on the information provided here is that you’re not passing the question_id into the form, which is causing the Choice query to be null. But there are more questions than answers at this point.

So just to explain what i’m trying to do.

I have a questionnaire which is made up of Questionnaire Model, Answer Model, Question Model, Respose Model and now a Choices Model all of which you basically did for me :slight_smile:

Each question in each questionnaire has different answers (Choices) for which to choose.

When the Questionnaire is rendered all the questions are listed as select boxes.

I’ve updated the form and view to populate the questions related select box with the available choices, but at the moment i am stuck on how to filter each question to its choices as at the moment every select is displaying the choices from question 1 only.

Form.py

    def __init__(self, *args, **kwargs):
         question_id = kwargs.pop('question_id')
         super().__init__(*args, **kwargs)
         self.fields['answer'] = forms.ModelChoiceField(
             queryset=Choice.objects.filter(question_id=question_id),
             widget=forms.Select(attrs={'class': 'form-control select2'})
         )

View.py

def get_questionnaire(request, project_id, questionnaire_id):

    # Get or Create the Response object for the parameters
    next = request.POST.get('next', '/')
    response, created = ProjectQuestionnaireResponse.objects.get_or_create(
        project_name_id=project_id, questionnaire_id=questionnaire_id
    )

    AnswerFormSet = modelformset_factory(ProjectQuestionnaireAnswer, form=AnswerForm, fields=('answer','notes',), extra=0)

    answer_queryset = ProjectQuestionnaireAnswer.objects.filter(response=response
    ).order_by('question__sequence'
    ).select_related('question')

    if request.method == 'POST':
        formset = AnswerFormSet(request.POST, form_kwargs={'question_id': 1})
        instances = formset.save()
        return HttpResponseRedirect(next)
    else:
    # Get the list of questions for which no Answer exists
        new_answers = ProjectQuestionnaireQuestion.objects.filter(
        questionnaire__projectquestionnaireresponse=response
        ).exclude(
        projectquestionnaireanswer__response=response
        )

    # This is safe to execute every time. If all answers exist, nothing happens
    for new_answer in new_answers:
        ProjectQuestionnaireAnswer(question=new_answer, response=response).save()

    answer_formset = AnswerFormSet(queryset=answer_queryset, form_kwargs={'question_id': 1})
                
    return render(request, 'project_evaluation.html', {'formset': answer_formset,'project_name':response.project_name,'title':response.questionnaire.title})

Now i know its this line that is the problem or at least one of possibly many problems.

formset = AnswerFormSet(request.POST, form_kwargs={'question_id': 1})

As this is returning the choices from question 1 only.

Is there a better way/correct way to be doing this?

I’m thinking that i need a for loop in there somewhere so that the choices form part of the for loop for each of the questions.

{% csrf_token %}
{{ form.non_field_errors }}
{{ formset.management_form }}
{% for form in formset %}
<p class="mg-b-10"><span class="tx-warning si si-question mr-1"  data-toggle="tooltip" title="{{form.instance.question.description}}"></span> {{ form.id }} {{ form.instance.question.question }}<span class="tx-danger ml-2">*</span></p>
<div class="parsley-checkbox d-flex mg-b-0 mb-3" id="cbWrapper2">
    {{ form.answer }}
</div><!-- parsley-checkbox -->
<div class="" id="cbErrorContainer2"></div>
{% endfor %}

Thanks again.

Tom.

Ok, so the “twist” here is that you’re using a formset to generated the individual questions / choices.

A formset creates all the individual forms for you, and what you’re needing to do is modify each of those forms as they’re being created.

See get_form_kwargs in Formsets | Django documentation | Django.

You may also find the discussion (and related links) at Pass different parameters to each form in formset to be helpful.

Thanks.

Reading over the links and the docs, i am lost. But will give this my best shot.

Would you mind just confirming that what i have already here is correct and i just need to build this out.

class AnswerForm(ModelForm):
    class Meta:
        model = ProjectQuestionnaireAnswer
        fields = ('answer','notes')

        widgets = {
            'notes': forms.Textarea(attrs={'class': 'form-control','placeholder': 'Add notes to question here'}),
        }

    def __init__(self, *args, **kwargs):
         question_id = kwargs.pop('question_id')
         super().__init__(*args, **kwargs)
         self.fields['answer'] = forms.ModelChoiceField(
             queryset=Choice.objects.filter(question_id=question_id),
             widget=forms.Select(attrs={'class': 'form-control select2'})
         )

Finally got this passing in the choice for each question, but it won’t update the database on POST

    def __init__(self, *args, **kwargs):
        instance = kwargs.get('instance')
        question_id = kwargs.pop('question_id')
        super().__init__(*args, **kwargs)
        self.fields['answer'] = forms.ModelChoiceField(
            queryset=Choice.objects.filter(question=instance),
            widget=forms.Select(attrs={'class': 'form-control select2'})
        )
    all_questions = ProjectQuestionnaireQuestion.objects.filter(questionnaire=questionnaire_id)    
    answer_formset = AnswerFormSet(queryset=all_questions, form_kwargs={'question_id': questionnaire_id})

Which i think is because of this

    if request.method == 'POST':
        formset = AnswerFormSet(request.POST, form_kwargs={'question_id': questionnaire_id})
        instances = formset.save()
        return HttpResponseRedirect(next)
    else:
...

There’s no error just doesn’t update, and i don’t know why :frowning:

You’re not using any “is_valid” checks on your form - if you’ve got any form errors being submitted, they’re not being shown. You’re redirecting to “next” regardless of what’s happening in the submission. This makes it tough to diagnose any problems within the data being submitted or any other validation errors.

Ok. Thanks, Ken.

Im now getting an error
for new_answer in new_answers: UnboundLocalError: local variable 'new_answers' referenced before assignment

I now see the value of if_valid

Hi Ken,

I’ve managed to get the formset working now and onto the next piece which is to record a value for the choice selection.

My thinking was that I have a choice_value within my Choices model. choice_value being Red, Green or Amber So when the answer (Choice) is selected i can also capture the choice_value.

But cause I’m working with formsets this is more complicated.

I could present the choice_value on the form, but hidden? But want the choice_value to be hidden and not viewable in page source so was thinking I could somehow capture this in the function and POST with the form.save()

But i can’t figure out how to identify the query to get the value.

        value = formset.save(commit=False)
        formset.value = choice.choice_value
        instances = formset.save()
class Choice(models.Model):
    question = models.ForeignKey(ProjectQuestionnaireQuestion, on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    choice_value = models.CharField(max_length=20, blank=True)
    choice_weight = models.IntegerField(blank=True, null=True)

    def __str__(self):
        return str(self.choice_text)
class ProjectQuestionnaireAnswer(models.Model):
    YN_Choices = [
        ('Yes', 'Yes'),
        ('No', 'No'),
        ('Unknown', 'Unknown')
    ]
    question = models.ForeignKey(ProjectQuestionnaireQuestion, on_delete=models.CASCADE)
    answer = models.ForeignKey(Choice, on_delete=models.CASCADE,null=True)
    value = models.CharField(max_length=20, blank=True)
    notes = models.TextField(blank=True)
    response = models.ForeignKey(ProjectQuestionnaireResponse, on_delete=models.CASCADE)
    
    class Meta:
        constraints = [
                models.UniqueConstraint(fields=['question','response'], name='project_unique_response2'),
            ]

    def __str__(self):
        return str(self.answer)

Any pointers would be great

Thanks

There’s no need to record the values from the Choice model indepedently. You’ve got the recorded answers as an FK to the Choice model. Whenever you need those values from a supplied answer, you follow the FK reference to the original model.

Of course. :slight_smile:
Thanks Ken.