Form
====
Base class: :doc:`Part`
Describe a Form. Example:
.. code-block:: python
class MyForm(Form):
a = Field()
b = Field.email()
form = MyForm().bind(request=request)
You can also create an instance of a form with this syntax if it's more convenient:
.. code-block:: python
form = Form(
fields=dict(
a=Field(),
b=Field.email(),
),
).bind(request=request)
In the common case the fields namespace will contain only instances of `Field`, but
iommi actually supports arbitrary `Part` objects. For example:
.. code-block:: python
form = Form(
fields=dict(
# Display a and b inside a box
box=html.div(
attrs__class__box=True,
children__a=Field(),
children__b=Field.email(),
),
# And c regularly
c=Field(),
)
)
So that writing the application logic (e.g. validation and post handlers) is independent
of minor changes to the layout, after bind the `fields` namespace of the form will contain
only instances of `Field` keyed by their `_name` independently of how deep they are in the
hierarchy. Given the above, an appropriate post_handler would be:
.. code-block:: python
def post_handler(form, **_):
if not form.is_valid():
return
print(form.fields.a.value, form.fields.b.value, form.fields.c.value)
# And not:
# print(form.fields.box.a.value, form.fields.box.b.value, form.fields.c.value)
Use of Form.layout:
.. code-block:: python
from django.contrib.auth import get_user_model
from django.utils.translation import gettext_lazy
from iommi._web_compat import Template
class UserForm(Form):
class Meta:
auto__model = get_user_model()
layout = Panel(dict(
p_main=Panel.fieldset(dict(
username=Panel.field(),
p_fullname=Panel.row(dict(
first_name=Panel.field(col__attrs__class__firstname=True),
last_name=Panel.field(),
)),
# examples of custom html parts:
p_foo=Panel.row(dict(
p_bar=gettext_lazy('Some plain text'),
p_baz=Template('{% load i18n %}{% translate "Foobar!" %}'),
)),
p_qux=Fragment(template='path/to/template.html'),
p_quux=html.h3('Quux'),
p_corge__template='path/to/another_template.html',
)),
p_permissions=Panel.fieldset(dict(
# ...
)),
p_error=Panel.alert('Error!', level='error'),
))
.. _form-save-callbacks:
Save callbacks
~~~~~~~~~~~~~~
Saving a model in Django mirrors SQL quite closely, and `Form.create`/`Form.edit`
have hooks for all the steps in a multi-step commit. They are called in this
order:
- `extra__new_instance`: called to create a new instance of the model. By default it just calls `form.model()`.
- `extra__save`: called to save each `model_object`. By default it calls `model_object.save()`. Note that the keyword argument is `model_object`, *not* `instance`, and that it is called once per model when the form spans more than one (e.g. `auto__include=['name', 'artist__name']`), so always check `isinstance` before applying model specific behavior.
- `extra__pre_save_all_but_related_fields` (only called for `Form.create`)
- `extra__on_save_all_but_related_fields` (only called for `Form.create`)
- `extra__pre_save` (before `instance.save()`)
- `extra__on_save` (after `instance.save()`)
Note that `Form.create` saves twice, because Django needs a pk before related
fields can be written. The first save happens between
`pre_save_all_but_related_fields` and `on_save_all_but_related_fields`, which
means `extra__pre_save` runs *after* the row already exists and is too late to
fill in a `NOT NULL` column. `Form.edit` saves once, before `extra__on_save`.
After a POST completes, `extra__redirect` is called if present, otherwise
`extra__redirect_to` determines where to redirect to.
See :ref:`the cookbook ` for worked examples.
Refinable members
-----------------
`action_class`
^^^^^^^^^^^^^^
Type: `type[Action]`
Cookbook:
:ref:`custom-member-class`
`actions`
^^^^^^^^^
Type: `Namespace`
Cookbook:
:ref:`form-actions`
`actions_template` (:ref:`evaluated `)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Type: `str | Template`
Default: `iommi/form/actions.html`
Cookbook:
:ref:`form-actions`
`after` (:ref:`evaluated `)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Type: `int | str`
See :ref:`after `
`assets`
^^^^^^^^
Type: `Namespace`
See :ref:`assets `
`attr` (:ref:`evaluated `)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Type: `str`
See :ref:`attr `
`attrs` (:ref:`evaluated `)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Type: :doc:`Attrs`
See :ref:`attributes `
Cookbook:
:ref:`freetext-column`
`auto`
^^^^^^
Type: :doc:`FormAutoConfig`
See :ref:`auto `
Cookbook:
:ref:`include-exclude-fields`
:ref:`reverse-fk-form`
`editable`
^^^^^^^^^^
Type: `bool`
Default: `True`
Cookbook:
:ref:`field-non-editable`
`endpoints`
^^^^^^^^^^^
Type: `Namespace`
See :ref:`endpoints `
`errors`
^^^^^^^^
Type: `Errors`
Cookbook:
:ref:`field-hidden`
`extra`
^^^^^^^
Type: `dict[str, Any]`
See :ref:`extra `
Cookbook:
:ref:`form-save-hooks`
:ref:`form-redirect`
`extra_evaluated`
^^^^^^^^^^^^^^^^^
Type: `dict[str, Any]`
See :ref:`extra `
`extra_params`
^^^^^^^^^^^^^^
See :ref:`extra_params `
`field_group`
^^^^^^^^^^^^^
Type: `Namespace`
Cookbook:
:ref:`group-fields`
`fields`
^^^^^^^^
Type: `Namespace`
Cookbook:
:ref:`dependent-fields`
:ref:`nested-forms`
`fields_template` (:ref:`evaluated `)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Type: `str | Template`
Cookbook:
:ref:`fields-templates`
`h_tag` (:ref:`evaluated `)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Type: `Fragment | str`
See :ref:`title `
`include` (:ref:`evaluated `)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Type: `bool`
See :ref:`include `
`instance`
^^^^^^^^^^
Type: `Any`
Cookbook:
:ref:`form-save-hooks`
:ref:`create-or-edit-forms`
`iommi_style`
^^^^^^^^^^^^^
Type: `str | Style | None`
See :ref:`iommi_style `
`layout` (:ref:`evaluated `)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Type: `Panel | None`
Cookbook:
:ref:`field-write-to-instance`
`layout_render_unused_fields` (:ref:`evaluated `)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Type: `bool`
Default: `False`
Cookbook:
:ref:`field-write-to-instance`
`layout_template` (:ref:`evaluated `)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Type: `str | Template | None`
Default: `lambda form, **_: 'iommi/form/layout.html' if form.layout is not None else None`
Cookbook:
:ref:`field-write-to-instance`
`member_class`
^^^^^^^^^^^^^^
Type: `type[Field]`
Cookbook:
:ref:`custom-member-class`
`model` (:ref:`evaluated `)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Type: `type[Model] | None`
Cookbook:
:ref:`form-model-introspection`
`page_class`
^^^^^^^^^^^^
Type: `type[Page]`
Cookbook:
:ref:`custom-member-class`
`post_validation`
^^^^^^^^^^^^^^^^^
Cookbook:
:ref:`validate-multiple-fields-together`
`read_nested_form_from_instance`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Read the nested forms instance from the parent forms instance.
This is analogous to `Field.read_from_instance` but for nested forms.
Cookbook:
:ref:`nested-forms`
`template` (:ref:`evaluated `)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Type: `str | Template`
Default: `iommi/form/form.html`
See :ref:`template `
`title` (:ref:`evaluated `)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Type: `Fragment | str`
See :ref:`title `
`write_nested_form_to_instance`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Write the nested_form to the instance.
This is analogous to `Field.write_to_instance` but for nested forms.
Cookbook:
:ref:`nested-forms`
Shortcuts
---------
`Form.create`
^^^^^^^^^^^^^
Parent: Form.crud_
Defaults
++++++++
* `actions__submit__post_handler`
* `iommi.form.create_object__post_handler`
* `extra__is_create`
* `True`
`Form.create_or_edit`
^^^^^^^^^^^^^^^^^^^^^
Parent: Form.crud_
Defaults
++++++++
* `actions__submit__post_handler`
* `iommi.form.create_or_edit_object__post_handler`
* `extra__is_create`
* `None`
Cookbook:
:ref:`create-or-edit-forms`
`Form.crud`
^^^^^^^^^^^
Defaults
++++++++
* `extra__pre_save_all_but_related_fields`
* `lambda **kwargs: None, # pragma: no mutate`
* `extra__on_save_all_but_related_fields`
* `lambda **kwargs: None, # pragma: no mutate`
* `extra__pre_save`
* `lambda **kwargs: None, # pragma: no mutate`
* `extra__on_save`
* `lambda **kwargs: None, # pragma: no mutate`
* `extra__on_delete`
* `lambda **kwargs: None, # pragma: no mutate`
* `extra__redirect`
* `lambda redirect_to, **_: HttpResponseRedirect(redirect_to)`
* `extra__redirect_to`
* `None`
* `extra__crud_type`
* `lambda form, **_: 'create' if form.instance is None else 'edit'`
* `extra__new_instance`
* `lambda form, **_: form.model()`
* `auto`
* `Namespace()`
`Form.delete`
^^^^^^^^^^^^^
Parent: Form.crud_
Defaults
++++++++
* `actions__submit__call_target__attribute`
* `delete`
* `actions__submit__post_handler`
* `iommi.form.delete_object__post_handler`
* `extra__crud_type`
* `delete`
* `editable`
* `False`
* `fields__iommi_default_text`
* `{'call_target': , 'include': at 0x7de47bad7f60>, 'after': 0, 'tag': 'p', 'children__text': at 0x7de47badc540>}`
`Form.edit`
^^^^^^^^^^^
Parent: Form.crud_
Defaults
++++++++
* `actions__submit__post_handler`
* `iommi.form.edit_object__post_handler`
* `extra__is_create`
* `False`
Methods
-------
`add_error`
^^^^^^^^^^^
Explicitly add an error message to the form's global error set.
Example:
.. code-block:: python
def post_validation(form, **_):
form.add_error('global error')
form = Form.create(
auto__model=Album,
post_validation=post_validation,
)
.. raw:: html
▼ Hide result
Toggle structure
`apply`
^^^^^^^
Write the new values specified in the form into the instance specified.
`as_view`
^^^^^^^^^
`get_errors`
^^^^^^^^^^^^
Get a dict containing two keys:
- `global` for errors global to the entire form.
- `fields` for errors specific to fields. This is itself a dict with a key for each field.
`get_field`
^^^^^^^^^^^
`get_nested_form`
^^^^^^^^^^^^^^^^^
`is_target`
^^^^^^^^^^^
`is_valid`
^^^^^^^^^^
Is the form valid? Can be called inside forms post_validation hook to determine if the
individual fields were all valid.
`next_value`
^^^^^^^^^^^^
`on_bind`
^^^^^^^^^
`on_refine_done`
^^^^^^^^^^^^^^^^
`own_evaluate_parameters`
^^^^^^^^^^^^^^^^^^^^^^^^^
`validate`
^^^^^^^^^^
Static methods
--------------
`apply_field`
^^^^^^^^^^^^^
Class methods
-------------
`fields_from_model`
^^^^^^^^^^^^^^^^^^^