With Django's inlineformset_factory, how do you save an order and its line items from one POST so every line references the order?
answer
- a model formset tied to one parent
- validate both before saving either
- save the parent first
- formset.instance gets the saved parent
- commit=False skips deletions
basics
~20 sBind a ModelForm for the order and an inline formset with instance=order to request.POST, validate both, save the order, set formset.instance to it and call formset.save(), which stamps each new line with the order's key.
solid answer
~40 s`inlineformset_factory(Order, OrderLine, fields=["sku", "quantity"])` builds a model formset limited to one parent's children: with `instance=order` its queryset is the order's lines, and the foreign key is replaced on each form by a hidden `InlineForeignKeyField` that must match the parent. In the view, bind the order's `ModelForm` and the formset to `request.POST`, both with the same `instance`, and validate **both** before saving anything. Then save the order, set `formset.instance = order` and call `formset.save()` — the inline formset assigns `formset.instance` to each new line just before saving it — ideally inside `transaction.atomic()`. `formset.save()` also deletes lines ticked `DELETE` (inline formsets default to `can_delete=True` and `extra=3`), but with `commit=False` nothing is deleted: you loop `formset.deleted_objects` yourself.
code
python · 36 linesfrom django.db import transaction
from django.forms import ModelForm, inlineformset_factory
from django.shortcuts import get_object_or_404, redirect, render
from .models import Order, OrderLine
class OrderForm(ModelForm):
class Meta:
model = Order
fields = ["delivery_note"]
OrderLineFormSet = inlineformset_factory(
Order, OrderLine, fields=["sku", "quantity"], extra=1
)
def edit_order(request, pk=None):
if pk is None:
order = Order(customer=request.user)
else:
order = get_object_or_404(Order, pk=pk, customer=request.user)
if request.method == "POST":
form = OrderForm(request.POST, instance=order)
formset = OrderLineFormSet(request.POST, instance=order)
if form.is_valid() and formset.is_valid():
with transaction.atomic():
order = form.save()
formset.instance = order
formset.save()
return redirect("order-detail", pk=order.pk)
else:
form = OrderForm(instance=order)
formset = OrderLineFormSet(instance=order)
return render(request, "orders/edit.html", {"form": form, "formset": formset})go deeper
Recall that inlineformset_factory takes the parent model, the child model and fields, and that the view passes instance=order on both GET and POST.
Explain the validate-both, save-parent, reassign-instance, save-formset sequence and what formset.save() does with changed, new, untouched and deleted rows.
Catch the commit=False deletion trap, wrap the two saves in one transaction, and scope the parent lookup to the user before binding the formset.
Weigh one inline formset page against separate line endpoints when orders grow large or several people edit them at once, and decide where cross-line rules are enforced.
## What an inline formset is `inlineformset_factory(Order, OrderLine, fields=[...])` builds a **model formset** bound to one parent object: give it `instance=order` and it edits exactly the `OrderLine` rows whose foreign key points at that order. Internally it finds the foreign key from `OrderLine` to `Order`, calls `modelformset_factory`, and filters the formset's queryset by that key. Three things make it different from a plain model formset: - **The foreign key is not a user field.** Django replaces it on every form with an `InlineForeignKeyField` — a hidden field whose posted value must match the parent's key, or the form fails with "The inline value did not match the parent instance." A user cannot move a line to another order through the form. - **Different defaults.** `inlineformset_factory` uses `extra=3` and `can_delete=True`; `formset_factory` and `modelformset_factory` use `extra=1` and `can_delete=False`. - **The prefix** defaults to the reverse accessor name: `lines` when the key declares `related_name="lines"`, otherwise `orderline_set`. If `OrderLine` has two foreign keys to `Order`, the factory raises `ValueError` ("… has more than one ForeignKey to …") until you pass `fk_name=`. If the key is unique, as with a `OneToOneField`, the factory forces `max_num=1`. ## Create and edit in one view The view in the code example serves both pages: with a `pk` it loads the order, without one it starts from an unsaved `Order()`. The order of operations is the whole point: 1. **Bind both** the order's `ModelForm` and the formset to `request.POST`, each with the same `instance`. 2. **Validate both before saving either.** If the lines are invalid, no order may have been written yet. 3. **Save the parent first**, so it has a primary key. 4. **Point the formset at the saved parent** with `formset.instance = order`, then call `formset.save()`. The inline formset's `save_new()` assigns `formset.instance` to each new line just before saving it, so every new line gets the order's key. 5. Wrap steps 3–4 in `transaction.atomic()` so a failure while saving lines does not leave an order with no lines. On a create page the parent is unsaved, so the formset has no existing rows — its queryset is empty — and shows only `extra` blank forms. If you pass no `instance` at all, the formset creates an empty parent object of its own, which is exactly why step 4 reassigns `formset.instance` to the object you saved. ## What formset.save() does `formset.save()` handles existing rows first, then new rows: | row kind | condition | action with `commit=True` | |---|---|---| | existing, ticked `DELETE` | `can_delete=True` | deleted; listed in `formset.deleted_objects` | | existing, changed | `form.has_changed()` | saved; listed in `formset.changed_objects` | | existing, unchanged | — | left alone | | extra, filled in | not ticked for deletion | created; listed in `formset.new_objects` | | extra, untouched | — | ignored | It returns the list of changed and newly created instances. ## The commit=False trap With `formset.save(commit=False)` you get unsaved instances back — useful for setting a computed `unit_price` on each line — but **nothing is deleted**. The lines the user ticked are listed in `formset.deleted_objects`, and deleting them is your job. If the line form has many-to-many fields, call `formset.save_m2m()` after saving the instances. ```python lines = formset.save(commit=False) for line in lines: line.unit_price = current_price(line.sku) # your pricing lookup line.save() for line in formset.deleted_objects: line.delete() ``` ## Letting users reorder lines Pass `can_order=True` and each form gains an `ORDER` integer field, pre-filled with 1, 2, 3 … for existing rows. After `is_valid()`, `formset.ordered_forms` returns the forms sorted by that value, leaving out forms ticked for deletion and untouched spare forms; a blank `ORDER` sorts last. `ORDER` is a form field, not a model field, so persisting the sequence is your job, for example after `formset.save()`: ```python formset.save() for position, form in enumerate(formset.ordered_forms, start=1): form.instance.position = position form.instance.save(update_fields=["position"]) ``` Reading `ordered_forms` on a formset built without `can_order`, or on one that is not valid, raises `AttributeError`. ## Common bugs - **Passing `instance=order` on GET but not on POST.** The POST formset is then bound to a blank parent, its posted lines no longer match that parent, and the formset does not validate. - **Saving the order, then validating the formset.** An invalid formset leaves a saved order behind. - **Omitting `{{ formset.management_form }}`** from a hand-written template, which makes every POST invalid. - **Treating untouched spare rows as errors.** They are ignored on save; only the first `min_num` rows must be filled. - **Calling `save(commit=False)` and forgetting `deleted_objects`**, so ticked lines never disappear.
- What changes if you call formset.save(commit=False)?You get the changed and new line instances back unsaved, so you can set extra attributes before calling `save()` on each. Nothing is deleted: the lines ticked for deletion are only listed in `formset.deleted_objects`, and you must delete them yourself. Call `formset.save_m2m()` afterwards if the line form has many-to-many fields.
- What happens if OrderLine has two foreign keys to Order?`inlineformset_factory` cannot tell which one links the lines to the order and raises `ValueError` saying the model has more than one `ForeignKey` to `Order`. Pass `fk_name="order"` to name the key the formset should follow.
- Can a user move a line to a different order by editing the hidden foreign key?No. The inline formset replaces the key on each form with an `InlineForeignKeyField` whose posted value must equal the parent's key; a different value fails with "The inline value did not match the parent instance." The parent itself must still be loaded with the user's scope in the view.
saying these in an interview costs you the question
- save the order first, then validate the line formset
- formset.save(commit=False) still deletes the lines ticked for deletion
- the instance argument is needed only on the GET branch
- users can pick each line's order from a visible dropdown in an inline formset
- Django defers the order's INSERT until the lines are saved