.. _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