Use ModelForm for CRUD¶
Problem¶
You want create, update, and delete pages for a model.
Solution¶
Use one ModelForm for create and update, plus a tiny confirmation form for delete.
Register one action per operation and place each action next to the page that triggers it.
Walkthrough¶
Define the form.
from django import forms
from next.forms import Form, ModelForm
from notes.models import Note
class NoteForm(ModelForm):
class Meta:
model = Note
fields = ("title", "body")
class DeleteNoteForm(Form):
confirm = forms.BooleanField()
Create Page¶
from django.http import HttpResponseRedirect
from django.urls import reverse
from next.forms import action
from notes.forms import NoteForm
@action("create_note", form_class=NoteForm)
def create_note(form: NoteForm) -> HttpResponseRedirect:
form.save()
return HttpResponseRedirect(reverse("next:page_"))
URL names follow the page_{path} convention where path segments are joined with underscores and captured-parameter brackets are dropped.
See File Router for the full naming rules.
Update Page¶
from types import SimpleNamespace
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import reverse
from next.forms import action
from next.pages import context
from next.urls import DUrl
from notes.forms import NoteForm
from notes.models import Note
@context("update_note")
def edit_form(note_id: DUrl["id", int]) -> SimpleNamespace:
note = get_object_or_404(Note, pk=note_id)
return SimpleNamespace(form=NoteForm(instance=note))
@action("update_note", form_class=NoteForm)
def update_note(form: NoteForm, note_id: DUrl["id", int]) -> HttpResponseRedirect:
form.instance = get_object_or_404(Note, pk=note_id)
form.save()
return HttpResponseRedirect(reverse("next:page_notes_id", kwargs={"id": note_id}))
Delete Action¶
from django.http import HttpResponseRedirect
from django.urls import reverse
from next.forms import action
from next.urls import DUrl
from notes.forms import DeleteNoteForm
from notes.models import Note
@action("delete_note", form_class=DeleteNoteForm)
def delete_note(form: DeleteNoteForm, note_id: DUrl["id", int]) -> HttpResponseRedirect:
Note.objects.filter(pk=note_id).delete()
return HttpResponseRedirect(reverse("next:page_"))
Templates¶
{% form @action="create_note" %}
{{ form.title }}
{{ form.body }}
<button type="submit">Create</button>
{% endform %}
{% form @action="update_note" %}
{{ form.title }}
{{ form.body }}
<button type="submit">Save</button>
{% endform %}
{% form @action="delete_note" %}
<input type="hidden" name="confirm" value="on">
<button type="submit" class="danger">Delete</button>
{% endform %}
Verification¶
Walk through the flow once. Create a note, edit it, delete it, and confirm the index reflects each step.
Tests assert the same flow.
from next.testing.client import NextClient
from notes.models import Note
def test_crud_flow(db) -> None:
client = NextClient()
client.post_action("create_note", {"title": "T", "body": ""})
note = Note.objects.get(title="T")
client.post_action("update_note", {"title": "T2", "body": "", "_url_param_id": note.id})
assert Note.objects.get(pk=note.id).title == "T2"
client.post_action("delete_note", {"confirm": "on", "_url_param_id": note.id})
assert not Note.objects.filter(pk=note.id).exists()
The _url_param_id key in the POST data mirrors the hidden field that the {% form %} tag emits for a captured URL parameter.
The dispatcher casts each recovered value to int when the cast succeeds and falls back to the original string when it does not, so a handler can declare DUrl["id", int] and receive the integer on both the initial render and the re-render.
Annotate the parameter as DUrl["id", str] to opt out of the int-first coercion.
See Also¶
See also
ModelForms for the ModelForm topic guide. Validation and Re-render for the re-render flow.