Forms¶
iommi forms is an alternative forms system for Django. It is inspired by the standard Django forms, while improving on its weaknesses.
This page explains what iommi forms are and how they differ from Django’s. If you want to do something specific, go to the form cookbook; for the exhaustive list of options see Form and Field.
Where iommi forms differ from Django’s¶
They render to HTML nicely out of the box. The default is bootstrap, several other styles ship with iommi, and you can adapt them to your own design system.
Foreign key relationships get AJAX-backed select widgets, with the endpoint wired up on the same URL as the view. Because it’s the same view, it’s covered by the same permission checks.
__works for going across table/object boundaries, the same way Django does it for QuerySets.Anywhere you can put a value you can put a callable instead, evaluated late.
include=lambda request, **_: request.user.is_staffis how you show a slightly different form to administrators, without a second form class.You can add a CSS class or attribute to exactly one thing without copying a template.
Configuration doesn’t require writing a class that’s only used in one place.
A Form is also a complete view. Form.create, Form.edit and Form.delete
give you the post handler and the redirect too, so there’s no template and no
view function to write. See Views for the CRUD set built on top of these.
Instead of Field subclasses, iommi pre-packages sets of defaults as
shortcuts – Field.boolean, Field.integer, Field.choice and so on. The
difference matters: a shortcut’s config is defaults, not hard coded behavior, so
you can start from one and refine it without subclassing. See
Philosophy for why, and Field for the full list.
Three ways to say the same thing¶
The next three sections build the same form automatically, declaratively and programmatically. They are not three different features to choose between: they are one API seen from three angles, and you can mix them freely. Which one reads best depends on how much you know at import time. Equivalence spells out the mapping between them.
Fully automatic forms¶
Generating forms from Django models automatically is the most powerful and common use for iommi forms:
form = Form.create(auto__model=Album)
Forms in iommi scale with higher complexity:
edit_user_form = Form.edit(
auto__model=User,
instance=lambda user_pk, **_: User.objects.get(pk=user_pk),
fields__username__is_valid=
lambda parsed_data, **_: (
parsed_data.startswith('demo_'),
'needs to start with demo_'
),
fields__is_staff__label__template='tweak_label_tag.html',
# show only for staff
fields__is_staff__include=lambda request, **_: request.user.is_staff,
)
Install like this:
urlpatterns = [
path('users/<user_pk>/edit/', edit_user_form.as_view()),
]
In this case the default behavior for the post handler for Form.edit is a save function like the one we had to define ourselves in the previous example.
Declarative forms¶
You can create forms declaratively, similar to Django forms. There are some important differences between iommi forms and Django forms in this mode, maybe the most important being that in iommi you can pass a callable as a parameter to late evaluate what the value of something is. This is used to restrict a field for staff users in this example:
class UserForm(Form):
first_name = Field.text()
username = Field.text(
is_valid=lambda parsed_data, **_: (
parsed_data.startswith('demo_'),
'needs to start with demo_'
)
)
is_staff = Field.boolean(
# show only for staff
include=lambda request, **_: request.user.is_staff,
label__template='tweak_label_tag.html',
)
class Meta:
instance = lambda params, **_: User.objects.get(pk=params.user_pk)
@staticmethod
def actions__submit__post_handler(user, form, **_):
if not form.is_valid():
return # pragma: no cover
form.apply(user)
user.save()
return HttpResponseRedirect('..')
Install like this:
urlpatterns = [
# Note `UserForm()`, not `UserForm`!
path('users/<user_pk>/edit/', UserForm().as_view()),
]
Note that we don’t need any template here.
Programmatic forms¶
The declarative style is very readable, but sometimes you don’t know until runtime what the form should look like. Creating forms programmatically in iommi is easy (and equivalent to doing it the declarative way):
def edit_user_save_post_handler(form, **_):
if not form.is_valid():
return # pragma: no cover
form.apply(form.instance)
form.instance.save()
return HttpResponseRedirect('..')
def edit_user_view(request, username):
return Form(
instance=User.objects.get(username=username),
fields=dict(
first_name=Field.text(),
username=Field.text(
is_valid=lambda parsed_data, **_: (
parsed_data.startswith('demo_'),
'needs to start with demo_'
),
),
is_staff=Field.boolean(
# show only for staff
include=lambda request, **_: request.user.is_staff,
label__template='tweak_label_tag.html',
),
),
actions__submit__post_handler=edit_user_save_post_handler,
)
Post handlers¶
In the simplest cases, like in a create form, you only have one post handler. You can do this yourself in the classic Django way:
if form.is_valid() and request.method == 'POST':
do_your_thing()
This is fine. But what if you have two buttons? What if you have two forms? What if there are two forms, one with two submit buttons, and a table with a bulk action? Suddenly writing the if statement above becomes very difficult. Post handlers in iommi handle this for you. iommi makes sure that the parts compose cleanly and the right action is called.
By default for create/edit/delete forms you get one post handler by the name
submit. Adding more is easy:
def disable_action(form, **_):
form.instance.disabled = True
form.instance.save()
return HttpResponseRedirect('.')
form = Form.edit(
auto__instance=instance,
actions__disable__post_handler=disable_action,
)
Post handlers can return a few different things:
a
HttpResponseobject which will get returned all the way up the stacka bound
Partof some kind. This could be aTable,Form,Page, etc. This is rendered into aHttpResponseNonewill result in the page being rendered like normaleverything else iommi will attempt to json encode and return as a json response
Saving¶
Form.create and Form.edit come with a post handler that saves for you, so in
the common case there is nothing to write. When you do need to intervene, you
don’t subclass or override a save method: you refine one of the callbacks that
correspond to the steps of a Django multi-step commit.
This mirrors the philosophy elsewhere in iommi – there is a named hook for each step, so you can replace just the step you care about and leave the rest of the default behavior alone.
For the list of callbacks and the order they run in, see
Save callbacks on Form. For worked examples, see
the cookbook.