Only 1 row for outcomes yet two rows for options

I have a form with three parts; the top part contains information about decisions. This should be a single row of three fields - which it is and this works fine.
I then have two other sections; both having a many-to-1 relationship with the model that the first part is based upon.
The top part displays details on outcomes; 2, maybe three fields.
The lower part displays details on options; again 2, maybe three, fields.

I am creating a blank form that allows me to create a new decision and should begin with 2 blank rows of fields for outcomes and two blank rows for options.

I have almost all of this in place, yet the outcomes will only show a single row of fields.

The template file contains the following…

{% block content %}
<form method="post">
    {% csrf_token %}
    <h1>New Decision</h1>
    {{ form.as_table }} {# Renders fields from DecisionForm #}

    <h4>Objectives</h4>
    {{ outcome_formset.management_form }}
    <div id="outcome-formset-section">
        <table>
            {% for form in outcome_formset %}
                <tr>
                    {% for hidden in form.hidden_fields %}
                        {{ hidden }}
                    {% endfor %}
                    {% for field in form.visible_fields %}
                        <td>
                            {{ field.label_tag }}
                            {{ field }}
                        </td>
                    {% endfor %}
                </tr>
            {% endfor %}
        </table>
    </div>
    <button type="button" id="add-outcome">Add Outcome</button>

    <h4>Options</h4>
    <div id="formset-section">
        <table>
            {% for form in formset %}
                <tr>
                    {% for field in form.visible_fields %}
                        <td>
                            {{ field.label_tag }}
                            {{ field }}
                        </td>
                    {% endfor %}
                </tr>
            {% endfor %}
        </table>
    </div>
    <button type="button" id="add-option">Add Option</button>

    <button type="submit">Save</button>
</form>

The buttons listed work; well, the add buttons do. When I click on these they display a new row of fields for both the options part and for the outcomes part.

In my views.py section I have this:

class DecisionCreateView(CreateView):
    model         = Decision_Model
    form_class    = DecisionForm
    template_name = 'template.html'
    success_url   = reverse_lazy('success_url_name') 

    def get_context_data(self, **kwargs):
        context                      = super().get_context_data(**kwargs)
        organisation_slug            = self.kwargs['organisation_slug']
        organisation                 = Organisation.objects.get(slug=organisation_slug)
        context['organisation_slug'] = organisation_slug
        context['user_org']          = organisation.organisationuser_set.all()

        if self.request.POST:
            context['formset']         = inlineformset_factory(Decision_Model, Option_Model, form = OptionForm, extra = 2)(self.request.POST)
            context['outcome_formset'] = OutcomeFormSet(self.request.POST, prefix='outcome')
        else:
            context['formset']         = inlineformset_factory(Decision_Model, Option_Model, form = OptionForm, extra = 2)()
            context['outcome_formset'] = OutcomeFormSet(prefix='outcome', queryset=Outcome_Model.objects.none())
        return context

    def form_valid(self, form):
        context         = self.get_context_data()
        formset         = context['formset']
        outcome_formset = context['outcome_formset']
        if formset.is_valid() and outcome_formset.is_valid():
            self.object              = form.save()
            formset.instance         = self.object
            formset.save()
            outcome_formset.instance = self.object
            outcome_formset.save()
            return redirect(self.get_success_url())
        else:
            return self.render_to_response(self.get_context_data(form = form))

This view works fine for the options but the outcomes only shows a single row of fields.

Anyone any idea why this is so?

To try and clarify the situation for me.

You’ve got a form with two related formsets, is that correct?

How are you adding the new instances to the form on the page? Are you remembering to update the management form when you add the new instance?

Yes. We have a decision form and two formsets that relate to this first form.

I have altered my code somewhat and now have almost the opposite issue; I have successfully gotten 2 outcome rows but the option rows have completely disappeared! hahahaha!

So, what I did was I realised that both were using the inlineformset_factory but in different ways. The outcome was using a form and a view to create the template data, whilst the option was purely within the view. In doing so, I realised that the OutcomeFormSet still had extra = 1; changing this to 2 resolved the issue.

However, I also noted that the naming convention I had used with the options form was not consistent; it was simply called FormSet and not OptionsFormSet to match the convention agreed (and as used for the outcome formset).

Thus, I created the forms.py section for OptionsFormSet as it appeared for OutcomeFormSet

OutcomeFormSet = forms.inlineformset_factory(
    Decision_Model,
    Outcome_Model,
    form       = OutcomeForm,
    fields     = ('outcome_description','achievement_date'),
    extra      = 2,
    can_delete = False,
)

OptionsFormSet = forms.inlineformset_factory(
    Decision_Model,
    Option_Model,
    form       = OptionForm,
    fields     = ('heading','summary'),
    extra      = 2,
    can_delete = False,
)

I then altered the references within the views.py file…

class DecisionCreateView(LoginRequiredMixin, OrganisationAccessMixin, CreateView):
    model         = Decision_Model
    form_class    = DecisionForm
    template_name = 'credence/decision_create.html'  # Replace with the name of your template file
    success_url   = reverse_lazy('credence/dashboard.html')  # Replace 'success_url_name' with the name of your success URL

    def get_context_data(self, **kwargs):
        context                      = super().get_context_data(**kwargs)
        organisation_slug            = self.kwargs['organisation_slug']
        organisation                 = Organisation.objects.get(slug=organisation_slug)
        context['organisation_slug'] = organisation_slug
        context['user_org']          = organisation.organisationuser_set.all()

        if self.request.POST:
            context['options_formset'] = OptionsFormSet(self.request.POST, prefix = 'options')
            context['outcome_formset'] = OutcomeFormSet(self.request.POST, prefix = 'outcome')
        else:
            context['options_formset'] = OptionsFormSet(prefix = 'options', queryset = Option_Model.objects.none())
            context['outcome_formset'] = OutcomeFormSet(prefix = 'outcome', queryset = Outcome_Model.objects.none())
        return context

    def form_valid(self, form):
        context         = self.get_context_data()
        options_formset = context['options_formset']
        outcome_formset = context['outcome_formset']
        if options_formset.is_valid() and outcome_formset.is_valid():
            self.object              = form.save()
            options_formset.instance = self.object
            options_formset.save()
            outcome_formset.instance = self.object
            outcome_formset.save()
            return redirect(self.get_success_url())
        else:
            return self.render_to_response(self.get_context_data(form = form))

And, finally, used the management_form, etc within the template…

{% extends "core/base.html" %}

{% block title %}
    Decision Create
{% endblock title %}

{% block content %}
<form method="post">
    {% csrf_token %}
    <h1>New Decision</h1>
    {{ form.as_table }} {# Renders fields from DecisionForm #}

    <h4>Objectives</h4>
    {{ outcome_formset.management_form }}
    <div id="outcome-formset-section">
        <table>
            {% for form in outcome_formset %}
                <tr>
                    {% for hidden in form.hidden_fields %}
                        {{ hidden }}
                    {% endfor %}
                    {% for field in form.visible_fields %}
                        <td>
                            {{ field.label_tag }}
                            {{ field }}
                        </td>
                    {% endfor %}
                </tr>
            {% endfor %}
        </table>
    </div>
    <button type="button" id="add-outcome">Add Outcome</button>

    <h4>Options</h4>
    {{ options_formset.management_form }}
    <div id="option-formset-section">
        <table>
            {% for form in option_formset %}
                <tr>
                    {% for field in form.visible_fields %}
                        <td>
                            {{ field.label_tag }}
                            {{ field }}
                        </td>
                    {% endfor %}
                </tr>
            {% endfor %}
        </table>
    </div>
    <button type="button" id="add-option">Add Option</button>

    <button type="submit">Save</button>
</form>

My JavaScript code only deals with the addition of rows via the buttons and both buttons work (clicking adds a new row to each subform); but for clarity I will list the code…

<script>
    $(document).ready(function () {
        const addOptionButton = $("#add-option");
        const formsetSection = $("#option-formset-section");
        let formIdx = 1; // Next form index
    
        addOptionButton.click(function () {
            const optionForm = '<td><label for="id_option_model_set-__prefix__-heading">Heading:</label><input type="text" name="option_model_set-__prefix__-heading" maxlength="50" id="id_option_model_set-__prefix__-heading"></td><td><label for="id_option_model_set-__prefix__-summary">Summary:</label><input type="text" name="option_model_set-__prefix__-summary" maxlength="500" id="id_option_model_set-__prefix__-summary"></td>'.replace(/__prefix__/g, formIdx);
            formsetSection.find('table').append("<tr>" + optionForm + "</tr>");
            formIdx++;
        });
    
        const addOutcomeButton = $("#add-outcome");
        const outcomeFormsetSection = $("#outcome-formset-section");
        let outcomeFormIdx = 1; // Next form index
    
        addOutcomeButton.click(function () {
            const outcomeForm = '<td><label for="id_outcomes-__prefix__-outcome_description">Outcome description:</label><input type="text" name="outcomes-__prefix__-outcome_description" maxlength="100" id="id_outcomes-__prefix__-outcome_description"></td><td><label for="id_outcomes-__prefix__-achievement_date">Achievement date:</label><input type="text" name="outcomes-__prefix__-achievement_date" id="id_outcomes-__prefix__-achievement_date"></td>'.replace(/__prefix__/g, outcomeFormIdx);
            outcomeFormsetSection.find('table').append("<tr>" + outcomeForm + "</tr>");
            outcomeFormIdx++;
        });
    });
</script>

It is very likely that my use of language is incorrect here; I am new to Django.
I reference a subform as a form within a form. I assume a formset is a set of forms; consisting of a form and 1 or more subforms.

Any idea why I am now not seeing any option rows initially?

SOLVED!

I missed off an ‘s’ on option in one row!

Thanks for your help.

While that may be precisely correct from a certain perspective, that’s not how you should be looking at it.

The formset itself is an object containing a list of forms. It’s not the “form and subforms”, it’s only the subforms as you’re describing it.

So, it is simply a set of forms; related or otherwise?

Thank you, by the way. This has been an excellent learning experience. :slight_smile:

It is a list of instances of one form class. All the forms in a formset are of the same type.