Django - UpdateView with inline formsets trying to save duplicate records?

Garfonzo picture Garfonzo · Sep 15, 2015 · Viewed 7.1k times · Source

I have an Expense model and an ExpenseLineItem model. Just like a typical expense/invoice, one expense can have several line items to make up the total cost of an invoice. I'm trying to use class based views to create and update expenses. I've successfully coded the CreateView to make a new expense with multiple expense line items.

My problem is when I try and update an existing Expense which already has several expense line items. Here's my code below, and I can't figure out what the issue is. The mixins (TitleMixin, CancelSuccessMixin, SelectedApartment)are mine and work fine.

I'm getting an error that, I believe, means that it's trying to save a new copy of the ExpenseLineItems but fails since those already exist. Almost like I'm not providing an instance argument.

What am I doing wrong?

forms.py

class ExpenseForm(ModelForm):
  class Meta:
    model = Expense
    fields = ['apart', 'inv_num', 'vendor', 'due_date']

ExpenseLineItemFormset = inlineformset_factory(Expense, ExpenseLineItem, fields=('description', 'account', 'amt'), can_delete=False)

Here's my ExpenseUpdate view:

class ExpenseUpdate(TitleMixin, CancelSuccessMixin, SelectedApartment, UpdateView):
  model = Expense
  form_class = ExpenseForm
  template_name = 'accounting/expense.html'

  def get(self, request, *args, **kwargs):
    self.object = self.get_object()
    form_class = self.get_form_class()
    form = self.get_form(form_class)
    expense_line_item_form = ExpenseLineItemFormset(instance = self.object)
    return self.render_to_response(self.get_context_data(form = form, expense_line_item_form = expense_line_item_form))

  def post(self, request, *args, **kwargs):
    self.object = self.get_object()
    form_class = self.get_form_class()
    form = self.get_form(form_class)
    expense_line_item_form = ExpenseLineItemFormset(self.request.POST, instance=self.object)

    if (form.is_valid() and expense_line_item_form.is_valid()):
      return self.form_valid(form, expense_line_item_form)
    return self.form_invalid(form, expense_line_item_form)

  def form_valid(self, form, expense_line_item_form):
    self.object = form.save()
    expense_line_item_form.instance = self.object
    expense_line_item_form.save()
    return HttpResponseRedirect(self.get_success_url())

  def form_invalid(self, form, expense_line_item_form):
    return self.render_to_response(self.get_context_data(form=form, expense_line_item_form=expense_line_item_form))

Error code I get:

MultiValueDictKeyError at /stuff/2/accounting/update-expense/25/
"u'expenselineitem_set-0-id'"
Request Method: POST
Request URL:    http://localhost:8000/stuff/2/accounting/update-expense/25/
Django Version: 1.8.3
Exception Type: MultiValueDictKeyError
Exception Value:    
"u'expenselineitem_set-0-id'"
Exception Location: /usr/local/lib/python2.7/dist-packages/django/utils/datastructures.py in __getitem__, line 322

Edit: Relevant part of my template:

<form class="form-horizontal" action="" method="post">
  {% csrf_token %}
  {% load widget_tweaks %}
 <div class="row">
    <div class="col-md-12">
      <table class="table table-tight">
        <thead>
          <th>Description</th>
          <th class="text-right">Account</th>
          <th class="text-right">Amount</th>
        </thead>
        <tbody>
          {{ expense_line_item_form.management_form }}
          {% for eli in expense_line_item_form %}
          <tr>
            <td>{{ eli.description|attr:'cols:29' }}</td>
            <td class="text-right">{{ eli.account }}</td>
            <td class="text-right">{{ eli.amt }}</td>
          </tr>
          {% endfor %}
        </tbody>
      </table>
    </div>
  <div class="col-md-12 text-right">
    <a href="{{ cancel }}" class="btn btn-default btn-lg">Cancel</a>
    <input class="btn btn-success btn-lg" type="submit" value="Post" />
  </div>
  <br><br>
</form>

EDIT -- Working Form Template I thought I would add the working version of my template, should someone else need it:

    <tbody>
      {{ expense_line_item_form.management_form }}
      {% for eli in expense_line_item_form %}
      <tr>
        <td>{{ eli.id }} {{ eli.description|attr:'cols:29' }}</td> <!-- <<==== Here's where I simply added {{ eli.id }}. That's all I changed :) -->
        <td class="text-right">{{ eli.account }}</td>
        <td class="text-right">{{ eli.amt }}</td>
      </tr>
      {% endfor %}
    </tbody>

Answer

Alasdair picture Alasdair · Sep 15, 2015

You need to include the form id for each form in the formset (it won't be shown to the user, as it is rendered as a hidden input). Without that form, the value is missing from the POST data, and you get a KeyError as you are seeing.

From the formset docs:

Notice how we need to explicitly render {{ form.id }}. This ensures that the model formset, in the POST case, will work correctly. (This example assumes a primary key named id. If you’ve explicitly defined your own primary key that isn’t called id, make sure it gets rendered.)

In your case, you are looping through the formset with {% for eli in expense_line_item_form %}, so you need to include {{ eli.id }}.