Django editable table: keep row identities and validation when saving a formset

For a table that edits existing Django model rows, render a model formset inside an HTML table. Include its management form, each row's hidden primary key, validation errors, and the ordinary CSRF token. Bind the submitted data to the formset and save only after the whole formset is valid. contenteditable cells alone do not provide that server-side path. Django model formsets

Suppose a desk coordinator needs to change available quantities for several equipment records. “Recorder” and “Headphones” may appear next to each other, but their screen position is not their database identity. The server must update the correct records even when the page displays a different order.

Start with a deliberately small editable field set

The following integration fragments target Django 5.2 documentation. They assume an existing Equipment model with owner, name, and quantity fields, authenticated users, an existing template route, and normal CSRF middleware. They do not create a complete application.

from django import forms
from django.forms import modelformset_factory
from .models import Equipment


class EquipmentForm(forms.ModelForm):
    quantity = forms.IntegerField(min_value=0, max_value=500)

    class Meta:
        model = Equipment
        fields = ["quantity"]


EquipmentFormSet = modelformset_factory(
    Equipment,
    form=EquipmentForm,
    extra=0,
    edit_only=True,
)

The upper limit of 500 is an illustrative desk rule. Replace it with the actual requirement. extra=0 removes blank creation rows; edit_only=True states that this screen updates existing objects. Restricting the editable fields also prevents an innocent layout change from exposing unrelated model fields.

Bind the same authorized rows on GET and POST

Scope the queryset to rows the current user may edit. Keep that restriction when processing the submission. This example also restricts the hidden primary-key field's choices to the same queryset:

from django.contrib.auth.decorators import login_required
from django.db import transaction
from django.shortcuts import redirect, render


@login_required
def equipment_table(request):
    rows = Equipment.objects.filter(owner=request.user).order_by("pk")
    formset = EquipmentFormSet(
        request.POST if request.method == "POST" else None,
        queryset=rows,
    )
    for form in formset:
        form.fields[Equipment._meta.pk.name].queryset = rows

    if request.method == "POST" and formset.is_valid():
        with transaction.atomic():
            formset.save()
        return redirect(request.path)

    return render(request, "equipment/table.html", {"formset": formset})

Use your actual authorization rules; ownership is only this example's rule. A hidden input is submitted by the browser and is not an authorization boundary. This view does not implement concurrent-edit conflict detection. If several coordinators edit the same record, decide how the application will detect an outdated submission.

The transaction groups the save operations. It does not make invalid data valid, and it does not provide a user-facing conflict policy. Django describes those boundaries in its transaction documentation.

Render the fields that connect the rows

In equipment/table.html, keep the management form outside the table and hidden row fields inside a cell:

<form method="post">
  {% csrf_token %}
  {{ formset.management_form }}
  {{ formset.non_form_errors }}
  <table>
    <thead>
      <tr><th scope="col">Equipment</th><th scope="col">Available</th></tr>
    </thead>
    <tbody>
      {% for form in formset %}
        <tr>
          <th scope="row">
            {{ form.instance.name }}
            {% for hidden in form.hidden_fields %}
              {{ hidden }}{{ hidden.errors }}
            {% endfor %}
            {{ form.non_field_errors }}
          </th>
          <td>
            {{ form.quantity.label_tag }}
            {{ form.quantity }}{{ form.quantity.errors }}
          </td>
        </tr>
      {% endfor %}
    </tbody>
  </table>
  <button type="submit">Save quantities</button>
</form>

The management form supplies bookkeeping such as the submitted form count. The hidden primary key associates each form with its existing record. A manually rendered table needs both; the names printed in the first column cannot replace them. Django formset management data

Check an invalid row before trusting a successful save

Use two fictional records, Recorder with quantity 6 and Headphones with quantity 12. Change Recorder to 7 and submit Headphones as -1. If browser validation prevents that submission, use your development test client to exercise server validation. The server should return a bound formset with the invalid field's error and retain the attempted values; it should not save Recorder's 7 while rejecting Headphones.

Then correct Headphones to 10 and submit again. Reopen the page and inspect the actual records: Recorder should be 7 and Headphones 10. Also test an identifier outside the user's authorized rows. The submission must not change another user's equipment.

Run those development checks before relying on the table for actual stock changes. The standalone editable HTML table solves a different problem with browser editing and local persistence. For Django model rows, finish by checking the saved record identities, visible errors, and the table at the width coordinators use.

Keep reading