Plain Forms¶
A plain Form collects and validates input without a Django model behind it.
It has no Meta.model, no automatic save(), and no instance loading.
The form validates cleaned_data and hands it to on_valid, where the page decides what to do with it.
When To Reach For One¶
Use a plain Form when the submission does not map to a single model write.
Filter and search forms read a value and redirect with it.
Voting and bulk-operation forms run a targeted query or update across many rows.
None of these fit the create-one-row shape that ModelForms covers, so a plain Form is the right base.
Registration¶
Subclassing next.forms.Form registers the class and derives its action name and scope, exactly like a ModelForm.
See Actions for the registration rules.
window_filter_form (shared)¶from django import forms as django_forms
from next.forms import Form
class WindowFilterForm(Form):
window = django_forms.ChoiceField(choices=WINDOW_CHOICES)
A form declared in forms.py takes shared scope and is reachable from any template by its derived name.
See Actions for the full scope rules.
{% form "window_filter_form" %}
{{ form.window }}
<button type="submit">Apply</button>
{% endform %}
Handling Submissions¶
The default on_valid on a plain Form redirects to Meta.success_url when declared, otherwise back to the origin page through redirect_to_origin(request).
See Success Feedback for the redirect contract.
Override on_valid when the submission needs a different redirect or its own logic.
A filter form, for example, redirects with the picked value on the query string.
from django.http import HttpRequest, HttpResponseRedirect
class WindowFilterForm(Form):
window = django_forms.ChoiceField(choices=WINDOW_CHOICES)
def on_valid(self, request: HttpRequest) -> HttpResponseRedirect:
chosen = self.cleaned_data["window"]
return HttpResponseRedirect(f"/stats/?window={chosen}")
The method reads self.cleaned_data directly.
There is no model to save, so the page owns every write.
from django import forms
from django.http import HttpRequest, HttpResponseRedirect
from next.forms import Form
class BulkToggleForm(Form):
enabled_names = forms.MultipleChoiceField(required=False)
def on_valid(self, request: HttpRequest) -> HttpResponseRedirect:
enabled_names = set(self.cleaned_data["enabled_names"])
for flag in Flag.objects.all():
should_be_on = flag.name in enabled_names
if flag.enabled != should_be_on:
flag.enabled = should_be_on
flag.save(update_fields=["enabled", "updated_at"])
return HttpResponseRedirect("/admin/")
Dynamic Choices¶
Populate choices in __init__ when they depend on the database or the request.
Call super().__init__ first, then rewrite the field’s choices or queryset.
from django import forms
from next.forms import Form
class BulkToggleForm(Form):
enabled_names = forms.MultipleChoiceField(required=False)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["enabled_names"].choices = [
(name, name) for name in Flag.objects.values_list("name", flat=True)
]
The same pattern narrows a ModelChoiceField queryset to the submitted parent so Django rejects forged primary keys at field-validation time.
from django import forms as django_forms
from next.forms import Form
class VoteForm(Form):
poll = django_forms.ModelChoiceField(queryset=Poll.objects.all())
choice = django_forms.ModelChoiceField(queryset=Choice.objects.none())
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
poll_pk = self.data.get(self.add_prefix("poll"))
if poll_pk:
self.fields["choice"].queryset = Choice.objects.filter(poll_id=poll_pk)
See Also¶
See also
ModelForms for forms backed by a Django model.
Actions for auto-registration, name derivation, and scope.
Form Templates for the {% form %} tag.
Django Forms for the underlying field and validation API.