.. _cookbook-forms:
Forms
-----
.. _show-fields:
How do I specify which fields to show?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. uses Field.include
.. uses FormAutoConfig.model
.. uses FormAutoConfig.include
.. uses FormAutoConfig.exclude
.. uses FormAutoConfig.default_included
Pass `include=False` to hide the field or `include=True` to show it. By default fields are shown, except the primary key field that is by default hidden. You can also pass a callable here like so:
.. code-block:: python
Form.create(
auto__model=Album,
fields__name__include=
lambda request, **_: request.GET.get('some_parameter') == 'hello!',
)
This will show the field `name` only if the GET parameter `some_parameter` is set to `hello!`.
To be more precise, `include` turns off the entire field. See :ref:`field-non-editable` and :ref:`field-hidden`
Use `auto__include` to specify the complete list of fields you want:
.. code-block:: python
form = Form.create(
auto__model=Album,
auto__include=['name', 'artist'],
)
.. raw:: html
▼ Hide result
Toggle structure
Instead of using `auto__include`, you can also use `auto__exclude` to just exclude the fields you don't want:
.. code-block:: python
form = Form.create(
auto__model=Album,
auto__exclude=['year'],
)
.. raw:: html
▼ Hide result
Toggle structure
There is also a config option `default_included` which is by default `True`, which is where iommi's default behavior of showing all fields comes from. If you set it to `False` fields are now opt-in:
.. code-block:: python
form = Form.create(
auto__model=Album,
auto__default_included=False,
# Turn on only the name field
fields__name__include=True,
)
.. raw:: html
▼ Hide result
Toggle structure
.. _supply-custom-parser-field:
How do I supply a custom parser for a field?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. uses Field.parse
Pass a callable to the `parse` member of the field:
.. code-block:: python
form = Form(
auto__model=Track,
fields__index__parse=lambda field, string_value, **_: int(string_value[:-3]),
)
.. _field-non-editable:
How do I make a field non-editable?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. uses Field.editable
.. uses Field.parsed_data
There are two cases: A) non-editable and you want to show the value to the
user, or B) non-editable but do not show it ("hardcoded").
A) Show the value
^^^^^^^^^^^^^^^^^
Pass a callable or `bool` to the `editable` member of the field:
.. code-block:: python
form = Form(
auto__model=Album,
fields__name__editable=lambda request, **_: request.user.is_staff,
fields__artist__editable=False,
)
For a normal user:
.. raw:: html
▼ Hide result
Toggle structure
For a staff user:
.. raw:: html
▼ Hide result
Toggle structure
.. _field-hardcoded:
B) Hardcode the value
^^^^^^^^^^^^^^^^^^^^^
A common use case is to navigate to some object, then create a sub-object.
In this example we have a url like `/artists/Black Sabbath/`, where the
artist name is parsed into an `Artist` instance by an iommi path decoder.
Then under that we have `/artists/Black Sabbath/create_album/`, and in this
form, we don't want to make the user choose Black Sabbath again. We
accomplish this with the `hardcoded` shortcut:
.. code-block:: python
form = Form.create(
auto__model=Album,
fields__artist=Field.hardcoded(
parsed_data=lambda params, **_: params.artist,
),
)
.. raw:: html
▼ Hide result
Toggle structure
.. _form-non-editable:
How do I make an entire form non-editable?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. uses Form.editable
.. uses FormAutoConfig.instance
This is a very common case so there's a special syntax for this: pass a `bool` to the form:
.. code-block:: python
form = Form.edit(
auto__instance=album,
editable=False,
)
.. raw:: html
▼ Hide result
Toggle structure
.. _custom-validator:
How do I supply a custom validator?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. uses Field.is_valid
Pass a callable that has the arguments `form`, `field`, and `parsed_data`. Return a tuple `(is_valid, 'error message if not valid')`.
.. code-block:: python
form = Form.create(
auto__model=Album,
auto__include=['name'],
fields__name__is_valid=lambda form, field, parsed_data, **_: (
parsed_data == 'only this value is valid',
'invalid!',
),
)
.. raw:: html
▼ Hide result
Toggle structure
You can also raise `ValidationError`:
.. code-block:: python
def name_is_valid(parsed_data, **_):
if parsed_data != 'only this value is valid':
raise ValidationError('invalid!')
return True, ''
form = Form.create(
auto__model=Album,
auto__include=['name'],
fields__name__is_valid=name_is_valid,
)
.. raw:: html
▼ Hide result
Toggle structure
.. _validate-multiple-fields-together:
How do I validate multiple fields together?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. uses Field.is_valid
.. uses Form.post_validation
.. uses Field.post_validation
Refine the `post_validation` hook on the `form`. It is run after all the individual fields validation
has run, so you can read the parsed `value` of each field and call `form.add_error()` to report a
problem that spans more than one field. Note that it is run even if the individual fields validation
was not successful.
.. code-block:: python
def post_validation(form, **_):
if form.fields.password.value != form.fields.password_confirm.value:
form.add_error('The passwords do not match')
form = Form(
fields__password=Field.password(),
fields__password_confirm=Field.password(display_name='Confirm password'),
post_validation=post_validation,
actions__submit__post_handler=lambda form, **_: None,
)
.. raw:: html
▼ Hide result
Toggle structure
There is a matching `post_validation` hook on each `Field` if you only need to look at a single field
after it has been parsed and validated.
How do I exclude a field?
~~~~~~~~~~~~~~~~~~~~~~~~~
See `How do I say which fields to include when creating a form from a model?`_
.. _include-exclude-fields:
How do I say which fields to include when creating a form from a model?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. uses Form.auto
.. uses Field.include
`Form()` has four methods to select which fields are included in the final form:
1. the `auto__include` parameter: this is a list of strings for members of the model to use to generate the form.
2. the `auto__exclude` parameter: the inverse of `include`. If you use this the form gets all the fields from the model excluding the ones with names you supply in `exclude`.
3. for more advanced usages you can also pass the `include` parameter to a specific field like `fields__my_field__include=True`. Here you can supply either a `bool` or a callable like `fields__my_field__include=lambda request, **_: request.user.is_staff`.
4. you can also add fields that are not present in the model by passing configuration like `fields__foo__attr='bar__baz'` (this means create a `Field` called `foo` that reads its data from `bar.baz`). You can either pass configuration data like that, or pass an entire `Field` instance.
.. _field-initial-value:
How do I supply a custom initial value?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. uses Field.initial
Pass a value or callable to the `initial` member:
.. code-block:: python
form = Form(
auto__model=Album,
fields__name__initial='Paranoid',
fields__year__initial=lambda field, form, **_: 1970,
)
.. raw:: html
▼ Hide result
Toggle structure
If there are `GET` parameters in the request, iommi will use them to fill in the appropriate fields. This is very handy for supplying links with partially filled in forms from just a link on another part of the site.
.. _field-required:
How do I set if a field is required?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. uses Field.required
Normally this will be handled automatically by looking at the model definition, but sometimes you want a form to be more strict than the model. Pass a `bool` or a callable to the `required` member:
.. code-block:: python
form = Form.create(
auto__model=Album,
fields__name__required=True,
fields__year__required=lambda field, form, **_: True,
)
.. raw:: html
▼ Hide result
Toggle structure
To show the field as required before posting, you can add a CSS class rendering to your style definition:
.. code-block:: python
IOMMI_DEFAULT_STYLE = Style(
bootstrap,
Field__attrs__class__required=lambda field, **_: field.required,
)
...and this CSS added to your site's custom style sheet:
.. code-block:: css
.required label:after {
content: " *";
color: red;
}
For the following result:
.. raw:: html
▼ Hide result
Toggle structure
See the style docs for more information on defining a custom style for your project.
.. _field-order:
How do I change the order of the fields?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. uses Field.after
You can change the order in your model definitions as this is what iommi uses. If that's not practical you can use the `after` member. It's either the name of a field or an index. There is a special value `LAST` to put a field last.
.. code-block:: python
from iommi import LAST
form = Form(
auto__model=Album,
fields__name__after=LAST,
fields__year__after='artist',
fields__artist__after=0,
)
.. raw:: html
▼ Hide result
Toggle structure
This will make the field order `artist`, `year`, `name`.
If there are multiple fields with the same index or name the order of the fields will be used to disambiguate.
.. _field-search-fields:
How do I specify which model fields the search of a choice_queryset uses?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. uses Field.search_fields
`Form.choice_queryset` uses the registered search fields for filtering and ordering.
See `Registrations `_ for how to register one. If present it will default
to a model field `name`.
In special cases you can override which attributes it uses for
searching by specifying `search_fields`:
.. code-block:: python
form = Form(
auto__model=Album,
fields__name__search_fields=('name', 'year'),
)
This last method is discouraged though, because it will mean searching behaves
differently in different parts of your application for the same data.
.. _field-related-multi-select:
How do I make a foreign key field a multi-select?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. uses Field.related
.. uses Field.multi_choice_queryset
A foreign key uses the `related` shortcut, which renders as a single-value
select. Pass `multi_select=True` to render it as a multi-select
(`multi_choice_queryset`) instead:
.. code-block:: python
form = Form(
auto__model=Album,
fields__artist__multi_select=True,
)
.. raw:: html
▼ Hide result
Toggle structure
How do I insert a CSS class or HTML attribute?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
See :doc:`Attrs`.
.. _field-template:
How do I override rendering of an entire field?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. uses Field.template
Pass a template name:
.. code-block:: python
form = Form(
auto__model=Album,
fields__year__template='my_template.html',
)
.. raw:: html
▼ Hide result
Toggle structure
or a `Template` object:
.. code-block:: python
form = Form(
auto__model=Album,
fields__year__template=Template('This is from the inline template'),
)
.. raw:: html
▼ Hide result
Toggle structure
.. _field-input-template:
How do I override rendering of the input field?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. uses Field.input
.. uses Fragment.template
Pass a template name or a `Template` object to the `input` namespace:
.. code-block:: python
form = Form(
auto__model=Album,
fields__year__input__template='my_template.html',
)
.. raw:: html
▼ Hide result
Toggle structure
.. code-block:: python
form = Form(
auto__model=Album,
fields__year__input__template=Template('This is from the inline template'),
)
.. raw:: html
▼ Hide result
Toggle structure
.. _project-wide-field-rendering:
How do I change how fields are rendered everywhere in my project?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. uses Style
Define a custom style and override the appropriate fields. For
example here is how you could change `Field.date` to use a text
based input control (as opposed to the date picker that `input type='date'`
uses).
.. code-block:: python
my_style = Style(bootstrap, Field__shortcuts__date__input__attrs__type='text')
When you do that you will get English language relative date parsing
(e.g. "yesterday", "3 days ago") for free, because iommi used to use a
text based input control and the parser is applied no matter what
(it's just that when using the default date picker control it will
always only see ISO-8601 dates).
.. raw:: html
▼ Hide result
Toggle structure
.. _form-redirect:
How do I change where the form redirects to after completion?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. uses FormAutoConfig.instance
.. uses Form.extra
iommi by default redirects to `..` after edit/create/delete. You can
override this via two methods:
- `extra__redirect_to`: a string with the url to redirect to. Relative URLs also work.
- `extra__redirect`: a callable that gets at least the keyword arguments `request`, `redirect_to`, `form`.
Form that after create redirects to the edit page of the object:
.. code-block:: python
form = Form.create(
auto__model=Album,
extra__redirect=lambda form, **_: HttpResponseRedirect(form.instance.get_absolute_url() + 'edit/'),
)
Form that after edit stays on the edit page:
.. code-block:: python
form = Form.edit(
auto__instance=album,
extra__redirect_to='.',
)
.. _dependent-fields:
How do I make a fields choices depend on another field?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. uses Field.choices
.. uses Form.fields
The contents of the form is sent with any AJAX requests, so we can
access the value of the other fields to do the filtering:
.. code-block:: python
def album_choices(form, **_):
if form.fields.artist.value:
return Album.objects.filter(artist=form.fields.artist.value)
else:
return Album.objects.all()
.. code-block:: python
Form(
auto__model=Track,
fields__artist=Field.choice_queryset(
attr=None,
choices=Artist.objects.all(),
after=0,
),
fields__album__choices=album_choices,
)
.. raw:: html
▼ Hide result
Toggle structure
.. _reverse-fk-form:
How do I enable a reverse foreign key relationship?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. uses Form.auto
.. uses FormAutoConfig.instance
By default reverse foreign key relationships are hidden. To turn it on, pass `include=True` to the field. Note that these are read only, because the semantics of hijacking another models foreign keys would be quite weird.
.. code-block:: python
f = Form(
auto__instance=black_sabbath,
fields__albums__include=True,
)
.. raw:: html
▼ Hide result
Toggle structure
.. _non-rendered-field:
How do I set an initial value on a field that is not in the form?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. uses Field.non_rendered
.. uses Field.editable
.. uses Field.initial
You do have to include the field, but you can make it not rendered by using
the `non_rendered` shortcut and setting `initial`.
.. code-block:: python
f = Form.create(
auto__model=Album,
fields__artist=Field.non_rendered(initial=black_sabbath),
fields__year=Field.non_rendered(initial='1980'),
)
.. raw:: html
▼ Hide result
Toggle structure
If you post this form you will get this object:
.. raw:: html
▼ Hide result
Toggle structure
By default this will be non-editable, but you can allow editing (via the
URL `GET` parameters) by setting `editable=True`.
.. code-block:: python
f = Form.create(
auto__model=Album,
fields__artist=Field.non_rendered(initial=black_sabbath),
fields__year=Field.non_rendered(
initial='1980',
editable=True,
),
)
.. raw:: html
► Show result
Toggle structure
Accessing this create form with `?year=1999` in the URL will create this object on submit:
.. raw:: html
▼ Hide result
Toggle structure
.. _group-fields:
How do I group fields?
~~~~~~~~~~~~~~~~~~~~~~
.. uses Field.group
.. uses Form.field_group
Use the `group` parameter. Adjacent fields with the same `group` are wrapped
together; customize how that wrapper renders via the form's `field_group`
namespace:
.. code-block:: python
form = Form(
auto__model=Album,
fields__year__group='metadata',
fields__artist__group='metadata',
)
.. raw:: html
▼ Hide result
Toggle structure
The wrapper around each group is a `field_group` fragment, so you can style it via
its `attrs`. For example, to put a red border around the group:
.. code-block:: python
form = Form(
auto__model=Album,
fields__year__group='metadata',
fields__artist__group='metadata',
field_group__attrs__style={
'border': '1px solid red',
'padding': '0.5rem',
},
)
.. raw:: html
▼ Hide result
Toggle structure
.. _field-reverse-m2m:
How do I show a reverse many-to-many relationship?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. uses Field.include
By default reverse many-to-many relationships are hidden. To turn it on, pass `include=True` to the field:
.. code-block:: python
form = Form(
auto__model=Genre,
instance=heavy_metal,
fields__albums__include=True,
)
.. raw:: html
▼ Hide result
Toggle structure
.. _nested-forms:
How do I nest multiple forms?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. uses Form.fields
.. uses save_nested_forms
.. uses Action.post_handler
.. uses Form.read_nested_form_from_instance
.. uses Form.write_nested_form_to_instance
You need to use the ``save_nested_forms`` post handler to have a single save button for all the nested forms and edit tables.
When a nested form is bound to an instance, iommi reads the nested instance via
`read_nested_form_from_instance` and writes it back via
`write_nested_form_to_instance`. Override these if a nested form's instance isn't
simply an attribute of the parent's instance:
.. code-block:: python
from iommi.form import save_nested_forms
class MyNestedForm(Form):
edit_ozzy = Form.edit(
auto__model=Artist,
instance=lambda **_: Artist.objects.get(name='Ozzy Osbourne'),
)
create_artist = Form.create(auto__model=Artist)
edit_albums = EditTable(
auto__model=Album,
auto__include=['name', 'year'],
columns__name__field__include=True,
)
class Meta:
actions__submit__post_handler = save_nested_forms
.. raw:: html
▼ Hide result
Toggle structure
.. _fields-templates:
How do I use templates for fields?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. uses Form.fields_template
Sometimes field groups just aren't enough and you may want to use a template to make your forms pretty:
.. code-block:: python
class CommentForm(Form):
class Meta:
# language=html
fields_template = Template(
{{ fields.album.input }}
{{ fields.name }}
{{ fields.email }}
{{ fields.comment }}
)
name = Field()
email = Field()
comment = Field.textarea()
album = Field.hardcoded(parsed_data=lambda **_: Album.objects.get(name='Heaven & Hell'))
.. raw:: html
▼ Hide result
Toggle structure
.. _dependent-fields2:
How do I make a field that depends on the choice in another field?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. uses Field.choices
This only works for cases when the choices are fetched via ajax, but this is also the common case:
.. code-block:: python
def album_choices(form, **_):
if form.fields.artist.value:
return Album.objects.filter(artist=form.fields.artist.value)
else:
return Album.objects.all()
form = Form(
auto__model=Track,
# First choose an artist
fields__artist=Field.choice_queryset(
attr=None,
choices=Artist.objects.all(),
after=0,
),
# Then choose an album
fields__album__choices=album_choices,
)
.. raw:: html
▼ Hide result
Toggle structure
.. _create-or-edit-forms:
How do I make a create or edit form?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. uses Field.editable
.. uses Form.create_or_edit
.. uses Form.instance
.. uses FormAutoConfig.type
If you don't know until runtime if you want `Form.create` or `Form.edit`,
you can use the `Form.create_or_edit` shortcut. If the `instance` is `None`
it will become a create form, otherwise an edit form:
.. code-block:: python
form = Form.create_or_edit(
auto__model=Album,
)
.. raw:: html
▼ Hide result
Toggle structure
Using a lambda for a create form:
.. code-block:: python
form = Form.create_or_edit(
auto__model=Album,
instance=lambda **_: None,
)
.. raw:: html
▼ Hide result
Toggle structure
Now an edit form:
.. code-block:: python
form = Form.create_or_edit(
auto__model=Album,
instance=lambda **_: Album.objects.first(),
)
.. raw:: html
▼ Hide result
Toggle structure
.. _field-hidden:
How do I create a hidden field?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. uses Field.hidden
Use the `Field.hidden` shortcut:
.. code-block:: python
form = Form.create(
auto__model=Album,
fields__artist=Field.hidden(),
)
.. raw:: html
▼ Hide result
Toggle structure
How do I access errors?
~~~~~~~~~~~~~~~~~~~~~~~
.. uses Form.errors
.. uses Field.errors
Forms render errors automatically, but sometimes it's useful to programmatically access the errors. There is `Form.get_errors` for all errors, and `Field.get_errors` for individual fields.
.. code-block:: python
def post_validation(**_):
raise ValidationError('Hardcoded error on the form')
form = Form.create(
auto__model=Album,
fields__name__is_valid=lambda **_: (False, 'Hardcoded error on the field'),
post_validation=post_validation,
)
.. raw:: html
► Show result
Toggle structure
`Form.get_errors` returns this dictionary:
.. code-block:: python
{
'fields': {
'artist': {
'This field is required',
},
'name': {
'Hardcoded error on the field',
},
'year': {
'This field is required',
},
},
'global': {
'Hardcoded error on the form',
},
}
You can get the errors on a given `Field` like this (note that you get a `set`!):
.. code-block:: python
form.fields.name.get_errors()
.. code-block:: python
{"Hardcoded error on the field"}
.. _field-write-to-instance:
How do I customize how a field value is written to the instance?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. uses Field.write_to_instance
Sometimes you need to customize how a field value is written to a model instance. The `write_to_instance` callback gives you full control over this process. It receives the field, instance, and value as parameters.
Here are some common use cases:
Password fields
^^^^^^^^^^^^^^^
Django's password fields require special handling with `set_password()`:
.. code-block:: python
def write_password(form, instance, value, **_):
if form.is_valid():
instance.set_password(value)
instance.save()
form = Form.edit(
auto__model=User,
auto__include=['password'],
fields__password__write_to_instance=write_password,
)
Data transformation
^^^^^^^^^^^^^^^^^^^
Transform the data before saving (e.g., uppercase, calculations):
.. code-block:: python
form = Form.create(
auto__model=Album,
fields__name__write_to_instance=lambda field, instance, value, **_: setattr(instance, field.attr, value.upper()),
)
Many-to-many relationships
^^^^^^^^^^^^^^^^^^^^^^^^^^
Many-to-many fields use `.set()` instead of direct assignment:
.. code-block:: python
def genre_write_to_instance(instance, value, **_):
instance.genres.set(value or [])
form = Form.edit(
auto__model=Album,
auto__include=['genres'],
fields__genres__write_to_instance=genre_write_to_instance,
)
The default implementation
^^^^^^^^^^^^^^^^^^^^^^^^^^
If you want to extend the default behavior rather than replace it:
.. code-block:: python
def log_and_write(field, instance, value, **kwargs):
# Custom logic before writing
print(f"Writing {value} to {field.attr}")
# Call the default implementation
Field.write_to_instance(field=field, instance=instance, value=value, **kwargs)
# Custom logic after writing
print(f"Successfully wrote to {instance}")
form = Form.edit(
auto__model=Album,
auto__include=['name'],
fields__name__write_to_instance=log_and_write,
)
.. _complex-layouts-forms:
How do I make complex layouts for forms?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. uses Form.layout
.. uses Form.layout_template
.. uses Form.layout_render_unused_fields
.. uses Panel.children
.. uses Panel.col
.. uses Panel.col_class
.. uses Panel.fieldset_legend
.. uses Panel.nested_path
.. uses Panel._parent_form
.. uses PanelCol.children
You can have more complex layout using the `panel` system. Set `layout` to a
`Panel` describing how the fields are arranged. By default any field you didn't
place in the layout raises an error; set `layout_render_unused_fields=True` to
render them after the layout instead, and point `layout_template` at your own
template to control the surrounding markup.
The pieces of a layout are `Panel` shortcuts: `Panel.row` lays out its `children`
side by side (each wrapped in a `col`, whose class is `col_class`), `Panel.field`
places a single field, and `Panel.fieldset` groups children under a
`fieldset_legend`. iommi links each `Panel` field to the matching `Form` field for
you - it tracks the owning form in `_parent_form` and uses `nested_path` to reach
fields of nested forms - and checks that every field is placed exactly once:
.. code-block:: python
class UserForm(Form):
class Meta:
auto__model = User
auto__exclude = ["password", "user_permissions"]
layout = Panel(dict(
p_main=Panel.card(
dict(
p_access=Panel.row(dict(
username=Panel.field(col__attrs__class__foo=True), # test also custom col attrs
change_password_btn=Panel.part(
Fragment(
text="Change password form is somewhere else",
tag="div",
),
include=lambda form, **_: form.editable and form.instance,
),
)),
p_fullname=Panel.row(dict(
first_name=Panel.field(),
last_name=Panel.field(),
)),
email=Panel.field(),
is_active=Panel.field(),
p_dates=Panel.row(dict(
last_login=Panel.field(),
date_joined=Panel.field(),
)),
p_permissions=Panel.fieldset(
dict(
p_roles=Panel.row(dict(
is_superuser=Panel.field(),
is_staff=Panel.field(),
)),
groups=Panel.field(),
),
legend=gettext_lazy("Permissions")
),
p_error=Panel.alert("Error!", level="error"),
),
header=lambda form, **_: form.instance.username if form.instance is not None else "New user",
footer=Template("Let's put something in the footer"),
),
))
.. raw:: html
▼ Hide result
Toggle structure
`Panel` fields are mapped to their corresponding `Form` fields automatically, and checked. That means that
if you create a complex layout and forget a field you will get an error, and vice versa.
The same way you can also use layouts for filter forms via `Table.query__form__layout`
.. _dropfile-dropimage:
How to make cool drag&drop file or image fields?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you don't want a regular file-inputs, you can use `Field.dropfile` and `Field.dropimage`:
.. code-block:: python
form = Form.create(
fields__my_document=Field.dropfile(display_name="My document"),
fields__my_image=Field.dropimage(display_name="My image"),
fields__existing_files=Field.dropfile(
display_name="Existing multiple files",
is_list=True,
initial=existing_files,
),
)
.. raw:: html
▼ Hide result
Toggle structure
And of course you can also use `registrations `__ to enable drag&drop for all file/image fields:
.. code-block:: python
register_field_factory(FileField, shortcut_name='dropfile')
register_field_factory(ImageField, shortcut_name='dropimage')
.. _form-actions:
How do I customize a form's buttons?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. uses Form.actions
.. uses Form.actions_template
A form's buttons live in `actions`. Add, rename or restyle them there, and point
`actions_template` at your own template to control how the row of buttons is laid
out:
.. code-block:: python
form = Form.create(
auto__model=Album,
actions__submit__display_name='Save album',
actions__back=Action(display_name='Back', attrs__href='/'),
)
.. raw:: html
▼ Hide result
Toggle structure
.. _form-model-introspection:
How do I read the model information iommi found for a form?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. uses Form.model
.. uses Field.model
.. uses Field.model_field
.. uses Field.model_field_name
When you build a form with `auto__model`, iommi sets `model` on the form and
`model_field`/`model_field_name` on each field for you. You rarely set these
yourself, but reading them lets you write field logic that adapts to the model.
Here `is_valid` reads the Django field's `verbose_name` off `model_field` to build
an error message that stays in sync with the model:
.. code-block:: python
def year_is_valid(field, parsed_data, **_):
if parsed_data is not None and parsed_data < 1900:
return False, f'The {field.model_field.verbose_name} must be 1900 or later'
return True, ''
form = Form.create(
auto__model=Album,
fields__year__is_valid=year_is_valid,
)
.. raw:: html
▼ Hide result
Toggle structure
.. _customize-choices-display:
How do I customize how choices are displayed?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. uses Field.choice_display_name_formatter
.. uses Field.choice_id_formatter
.. uses Field.choice_to_optgroup
.. uses Field.empty_label
.. uses Field.is_list
.. uses Field.is_boolean
For a `Field.choice` (and the multi-select `Field.multi_choice`, which sets
`is_list`), control how each choice is shown with `choice_display_name_formatter`,
which value identifies it with `choice_id_formatter`, and how the options are
grouped with `choice_to_optgroup`. `empty_label` sets the text of the blank option
of a non-required choice. Boolean fields set `is_boolean`:
.. code-block:: python
form = Form(
fields__color=Field.choice(
choices=['red', 'green', 'blue'],
required=False,
choice_display_name_formatter=lambda choice, **_: choice.upper(),
empty_label='(pick a color)',
),
)
.. raw:: html
▼ Hide result
Toggle structure
.. _field-label-help:
How do I customize a field's label and help text?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. uses Field.label
.. uses Field.help
.. uses Field.help_text
Set `display_name` for the label text (or configure the `label` fragment for its
markup). `help_text` sets the help string - it defaults to the Django model
field's help text - and `help` is the fragment that renders it:
.. code-block:: python
form = Form(
auto__model=Album,
fields__name__display_name='Album title',
fields__name__help_text='The name as printed on the cover',
)
.. raw:: html
▼ Hide result
Toggle structure
.. _field-raw-data:
How do I control how a field's input is read and rendered?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. uses Field.raw_data
.. uses Field.strip_input
.. uses Field.parse_empty_string_as_none
.. uses Field.render_value
.. uses Field.render_value_on_error
.. uses Field.non_editable_input
Several hooks control the round trip between the raw request string and the value
shown to the user:
- `strip_input` (default `True`) runs `.strip()` on the incoming string.
- `parse_empty_string_as_none` (default `True`) turns an empty string into `None` before parsing.
- `render_value` turns the parsed value back into a string for display.
- `render_value_on_error` decides what to show when parsing failed (defaults to the raw input).
- `raw_data` is the unparsed string from the request.
- `non_editable_input` is the fragment used to render the value when the field is not editable.
For example, to always display the value upper-cased:
.. code-block:: python
form = Form(
fields__foo=Field(
initial='hello',
render_value=lambda form, field, value, **_: value.upper(),
),
)
.. raw:: html
▼ Hide result
Toggle structure
.. _field-read-from-instance:
How do I change how a field reads its value from the instance?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. uses Field.read_from_instance
By default a field reads its value as `getattr_path(instance, field.attr)`.
Override `read_from_instance` to compute the value some other way (the matching
write hook is `write_to_instance`):
.. code-block:: python
form = Form.edit(
auto__model=Album,
instance=album,
fields__name__read_from_instance=lambda field, instance, **_: instance.name.upper(),
)
.. raw:: html
▼ Hide result
Toggle structure
.. _form-save-hooks:
How do I hook into saving a create/edit form?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. uses Form.extra
.. uses Form.instance
.. uses FormAutoConfig.model
If all you want is to set a value that isn't in the form, use
:ref:`Field.hardcoded ` instead: it's simpler and it keeps the
value visible in the form definition.
For anything else, `Form.create` and `Form.edit` expose a callback per step of
the save. To supply a value the form doesn't collect, build the object yourself
in `extra__new_instance`:
.. code-block:: python
form = Form.create(
auto__model=Album,
auto__exclude=['artist'],
extra__new_instance=lambda form, **_: Album(artist=black_sabbath),
)
.. note::
On `Form.create` the instance is saved *twice*, because Django has to have a
pk before related fields can be written. That means `extra__pre_save` runs
after the first insert, so it is too late to fill in a `NOT NULL` column. Use
`extra__new_instance` or `extra__pre_save_all_but_related_fields` for that.
On `Form.edit` there is only one save and `extra__pre_save` runs before it.
The full list of callbacks, and the order they run in, is documented under
:ref:`Save callbacks ` on `Form`.
The one to be careful with is `extra__save`. It receives the object to save as
`model_object` (**not** `instance`), and it is called once per model when the
form spans more than one, so always check which model you got before doing
anything model specific:
.. code-block:: python
class AlbumForm(Form):
class Meta:
auto__model = Album
auto__include = ['name', 'artist__name']
@staticmethod
def extra__save(model_object, **_):
if isinstance(model_object, Album):
model_object.save()
else:
# this also gets called for saving artist.name
model_object.save()
.. raw:: html
▼ Hide result
Toggle structure