Search code examples
djangoinline-formset

Django - update inline formset not updating


I'm trying to create an update view which has a few inline formset's in it but for some reason I'm seeing 'id: This field is required.' & none of the inline formset values being set when I click the update button. The fields themselves actually have values in them in the template so I'm not sure why the inline formsets would be empty when trying to save.

Models.py

class ProjectUpdateForm(forms.ModelForm):
class Meta:
    model = Project
    fields = ('name', 'details')

Views.py

class ProjectUpdateView(LoginRequiredMixin, UpdateView):
model = Project
template_name = 'home/project/project_update.html'
context_object_name = "project"
form_class = ProjectUpdateForm

def get_context_data(self, **kwargs):
    ReviewChildFormset = inlineformset_factory(
        Project, AdoptedBudgetReview, fields=('project', 'review'), can_delete=False, extra=0
    )

    itemNames = [{'item': item} for item in ['Concept & Feasibility', 'Planning & Design', 'Procurement', 'Delivery & Construction', 'Finalisation']]
    EstimatedBudgetChildFormset = inlineformset_factory(
        Project, EstimatedBudget, fields=('project', 'item', 'cost', 'time'), can_delete=False, formset=EstimatedInlineFormset, extra=0, widgets={'item': forms.Select(attrs={'disabled': True})},
    )

    FutureExpenditureFormset = inlineformset_factory(
        Project, FutureExpenditure, fields=('project', 'byear', 'internalFunding', 'externalFundingSource', 'externalFunding'), can_delete=False, extra=0,
    )

    PreviousExpenditureFormset = inlineformset_factory(
        Project, PreviousExpenditure, fields=('project', 'byear', 'internalFunding', 'externalFunding'), can_delete=False, extra=0,
    )

    initial = [{'priority': priority} for priority in Priority.objects.all()]
    PriorityChildFormset = inlineformset_factory(
        Project, ProjectPriority, fields=('project', 'priority', 'score'), can_delete=False, extra=0, widgets={'priority': forms.Select(attrs={'disabled': True})},
    )

    context = super().get_context_data(**kwargs)
    if self.request.POST:
        context['adoptedreviews'] = ReviewChildFormset(self.request.POST, instance=self.object)
        context['estimatedbudget'] = EstimatedBudgetChildFormset(self.request.POST, initial=itemNames, instance=self.object)
        context['futureexpenditure'] = FutureExpenditureFormset(self.request.POST, instance=self.object)
        context['previousexpenditure'] = PreviousExpenditureFormset(self.request.POST, instance=self.object)
        context['priorities'] = PriorityChildFormset(self.request.POST, initial=initial, instance=self.object)
    else:
        context['adoptedreviews'] = ReviewChildFormset(instance=self.object)
        context['estimatedbudget'] = EstimatedBudgetChildFormset(initial=itemNames, instance=self.object)
        context['futureexpenditure'] = FutureExpenditureFormset(instance=self.object)
        context['previousexpenditure'] = PreviousExpenditureFormset(instance=self.object)
        context['priorities'] = PriorityChildFormset(initial=initial, instance=self.object)
        
    return context

def form_valid(self, form):
    context = self.get_context_data()
    adoptedreview = context["adoptedreviews"]
    estimatedbudget = context["estimatedbudget"]
    prioritycriteria = context["priorities"]
    futureexpenditure = context["futureexpenditure"]
    previousexpenditure = context["previousexpenditure"]
    form.instance.creator = self.request.user
    if adoptedreview.is_valid() and estimatedbudget.is_valid() and previousexpenditure.is_valid() and futureexpenditure.is_valid and prioritycriteria.is_valid():

        self.object = form.save()
        adoptedreview.instance = self.object
        estimatedbudget.instance = self.object
        futureexpenditure.instance = self.object
        previousexpenditure.instance = self.object
        prioritycriteria.instance = self.object

        adoptedreview.save()
        estimatedbudget.save()
        previousexpenditure.save()
        futureexpenditure.save()
        prioritycriteria.save()
    else:
        return self.form_invalid(form)
    return super(ProjectCreateview, self).form_valid(form)

Template

 <form method="POST">
        {% csrf_token %}
        <fieldset class="form-group">
            <legend class="border-bottom mb-4">Update Project</legend>
            {{ form|crispy }}
        </fieldset>

        <h2>Adopted Budget Review</h2>
        <!-- {{ adoptedreviews|crispy }} -->
        <table class="table table-light">
          <thead>
            <tr>
              <th scope="col"></th>
              <th scope="col">BR1</th>
              <th scope="col">BR2</th>
              <th scope="col">BR3</th>
              <th scope="col">BR4</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <th scope="row">Amount</th>
              {{ adoptedreviews.management_form }}
              {% for adrform in adoptedreviews.forms %}
                 {% for field in adrform.visible_fields %}
                 <td>
                   {{ field.errors.as_ul }}
                   {{ field }}
                 </td>
                 {% endfor %}
              {% endfor %}
            </tr>
          </tbody>
        </table>

        <h2>Estimated Budget - Breakdown Yr1</h2>
        <!-- {{ estimatedbudget|crispy }} -->
        <table class="table table-light">
          {{ estimatedbudget.management_form }}
          <thead>
            <tr>
              <th scope="col">Item - Description</th>
              <th scope="col">Anticipated % cost</th>
              <th scope="col">Anticipated time - weeks</th>
            </tr>
          </thead>
          {% for form in estimatedbudget.forms %}
            <tr>
              <td>
                {{ form.errors }}
                <h4>{{ form.item.value }}</h4>
                {{ form.item.as_hidden }}
              </td>
              <td>
                {{ form.cost }}
              </td>
              <td>
                {{ form.time }}
              </td>
            </tr>
            {% endfor %}
         </table>

        <h2>Future Project Expenditure</h2>
        <!-- {{ futureexpenditure|crispy }} -->
        <table class="table table-light">
          {{ futureexpenditure.management_form }}
          {% for form in futureexpenditure.forms %}
              {% if forloop.first %}
                  <thead>
                  <tr>
                      {% for field in form.visible_fields %}
                          <th scope="col">{{ field.label|capfirst }}</th>
                      {% endfor %}
                  </tr>
                  </thead>

              {% endif %}
                <tbody>
                  <tr class="{% cycle row1 row2 %} formset_row">
                    {% 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 %}
                            {{ field.errors.as_ul }}
                            {{ field }}
                        </td>
                    {% endfor %}
                  </tr>
                </tbody>
            {% endfor %}
         </table>

         <h2>Previous Project Expenditure</h2>
         <table class="table table-light">
           <!-- {{ previousexpenditure|crispy }} -->
          {{ previousexpenditure.management_form }}
          {% for form in previousexpenditure.forms %}
              {% if forloop.first %}
                  <thead>
                  <tr>
                      {% for field in form.visible_fields %}
                          <th scope="col">{{ field.label|capfirst }}</th>
                      {% endfor %}
                  </tr>
                  </thead>

              {% endif %}
                <tbody>
                  <tr class="{% cycle row1 row2 %} previous_formset_row">
                    {% 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 %}
                            {{ field.errors.as_ul }}
                            {{ field }}
                        </td>
                    {% endfor %}
                  </tr>
                </tbody>
            {% endfor %}
         </table>

        <h2>Priority Criteria</h2>
        <!-- {{ priorities|crispy }} -->
        <table class="table table-light">
          <tbody>
            {{ priorities.management_form }}
            {% for priority in priorities.forms %}
            <tr>
              <td>
                {{ priority.priority.errors.as_ul }}
                {% for value, name in priority.priority.field.choices %}
                  {% if value == priority.priority.value %}
                    <h4>{{ name }}</h4>
                  {% endif %}
                {% endfor %}
                {{ priority.priority.as_hidden }}
              </td>
              <td>
                {{ priority.score.errors.as_ul }}
                {{ priority.score }}
              </td>
            </tr>
            {% endfor %}
          </tbody>
        </table>

        <div class="form-group">
            <button class="btn btn-outline-info" type="submit">Update Project</button>
        </div>
    </form>

Solution

  • Whenever one renders a forms manually one should remember to always render it's hidden fields, more so for a formset / inlineformset since it automatically adds some hidden fields to the form. Since the formset is updating multiple instances of course there needs to be something present in the formset to indicate which instance is which, which is done by hidden fields here.

    Hence you need to render the hidden fields of your formsets forms:

    <form method="POST">
            {% csrf_token %}
            <fieldset class="form-group">
                <legend class="border-bottom mb-4">Update Project</legend>
                {{ form|crispy }}
            </fieldset>
    
            <h2>Adopted Budget Review</h2>
            <!-- {{ adoptedreviews|crispy }} -->
            <table class="table table-light">
              <thead>
                <tr>
                  <th scope="col"></th>
                  <th scope="col">BR1</th>
                  <th scope="col">BR2</th>
                  <th scope="col">BR3</th>
                  <th scope="col">BR4</th>
                </tr>
              </thead>
              <tbody>
                <tr>
                  <th scope="row">Amount</th>
                  {{ adoptedreviews.management_form }}
                  {% for adrform in adoptedreviews.forms %}
                     {% for hidden in adrform.hidden_fields %}
                         {{ hidden }}
                     {% endfor %}
                     {% for field in adrform.visible_fields %}
                     <td>
                       {{ field.errors.as_ul }}
                       {{ field }}
                     </td>
                     {% endfor %}
                  {% endfor %}
                </tr>
              </tbody>
            </table>
    
            <h2>Estimated Budget - Breakdown Yr1</h2>
            <!-- {{ estimatedbudget|crispy }} -->
            <table class="table table-light">
              {{ estimatedbudget.management_form }}
              <thead>
                <tr>
                  <th scope="col">Item - Description</th>
                  <th scope="col">Anticipated % cost</th>
                  <th scope="col">Anticipated time - weeks</th>
                </tr>
              </thead>
              {% for form in estimatedbudget.forms %}
                {% for hidden in form.hidden_fields %}
                    {{ hidden }}
                {% endfor %}
                <tr>
                  <td>
                    {{ form.errors }}
                    <h4>{{ form.item.value }}</h4>
                    {{ form.item.as_hidden }}
                  </td>
                  <td>
                    {{ form.cost }}
                  </td>
                  <td>
                    {{ form.time }}
                  </td>
                </tr>
                {% endfor %}
             </table>
    
            <h2>Future Project Expenditure</h2>
            <!-- {{ futureexpenditure|crispy }} -->
            <table class="table table-light">
              {{ futureexpenditure.management_form }}
              {% for form in futureexpenditure.forms %}
                  {% if forloop.first %}
                      <thead>
                      <tr>
                          {% for field in form.visible_fields %}
                              <th scope="col">{{ field.label|capfirst }}</th>
                          {% endfor %}
                      </tr>
                      </thead>
    
                  {% endif %}
                    <tbody>
                      <tr class="{% cycle row1 row2 %} formset_row">
                        {% for hidden in form.hidden_fields %}
                            {{ hidden }}
                        {% endfor %}
                        {% 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 %}
                                {{ field.errors.as_ul }}
                                {{ field }}
                            </td>
                        {% endfor %}
                      </tr>
                    </tbody>
                {% endfor %}
             </table>
    
             <h2>Previous Project Expenditure</h2>
             <table class="table table-light">
               <!-- {{ previousexpenditure|crispy }} -->
              {{ previousexpenditure.management_form }}
              {% for form in previousexpenditure.forms %}
                  {% if forloop.first %}
                      <thead>
                      <tr>
                          {% for field in form.visible_fields %}
                              <th scope="col">{{ field.label|capfirst }}</th>
                          {% endfor %}
                      </tr>
                      </thead>
    
                  {% endif %}
                    <tbody>
                      <tr class="{% cycle row1 row2 %} previous_formset_row">
                        {% for hidden in form.hidden_fields %}
                            {{ hidden }}
                        {% endfor %}
                        {% 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 %}
                                {{ field.errors.as_ul }}
                                {{ field }}
                            </td>
                        {% endfor %}
                      </tr>
                    </tbody>
                {% endfor %}
             </table>
    
            <h2>Priority Criteria</h2>
            <!-- {{ priorities|crispy }} -->
            <table class="table table-light">
              <tbody>
                {{ priorities.management_form }}
                {% for priority in priorities.forms %}
                {% for hidden in priority.hidden_fields %}
                    {{ hidden }}
                {% endfor %}
                <tr>
                  <td>
                    {{ priority.priority.errors.as_ul }}
                    {% for value, name in priority.priority.field.choices %}
                      {% if value == priority.priority.value %}
                        <h4>{{ name }}</h4>
                      {% endif %}
                    {% endfor %}
                    {{ priority.priority.as_hidden }}
                  </td>
                  <td>
                    {{ priority.score.errors.as_ul }}
                    {{ priority.score }}
                  </td>
                </tr>
                {% endfor %}
              </tbody>
            </table>
    
            <div class="form-group">
                <button class="btn btn-outline-info" type="submit">Update Project</button>
            </div>
        </form>
    

    Also since you have multiple formsets you should provide a prefix to them to provide name clashing (See Using more than one formset in a view):

    if self.request.POST:
        context['adoptedreviews'] = ReviewChildFormset(self.request.POST, instance=self.object, prefix='adoptedreviews_form')
        context['estimatedbudget'] = EstimatedBudgetChildFormset(self.request.POST, initial=itemNames, instance=self.object, prefix='estimatedbudget_form')
        context['futureexpenditure'] = FutureExpenditureFormset(self.request.POST, instance=self.object, prefix='futureexpenditure_form')
        context['previousexpenditure'] = PreviousExpenditureFormset(self.request.POST, instance=self.object, prefix='previousexpenditure_form')
        context['priorities'] = PriorityChildFormset(self.request.POST, initial=initial, instance=self.object, prefix='priorities_form')
    else:
        context['adoptedreviews'] = ReviewChildFormset(instance=self.object, prefix='adoptedreviews_form')
        context['estimatedbudget'] = EstimatedBudgetChildFormset(initial=itemNames, instance=self.object, prefix='estimatedbudget_form')
        context['futureexpenditure'] = FutureExpenditureFormset(instance=self.object, prefix='futureexpenditure_form')
        context['previousexpenditure'] = PreviousExpenditureFormset(instance=self.object, prefix='previousexpenditure_form')
        context['priorities'] = PriorityChildFormset(initial=initial, instance=self.object, prefix='priorities_form')
    

    Note: You seem to have weird HTML (multiple tbody elements in a table??), and also you might want to reconsider having so many formsets in a page, this can be considered bad for UX purposes (This can be really confusing for the user, not to mention the developer too). One page should ideally serve one purpose to the user.