{% from "admin/components/csrf-field.html" import csrf_field %} {% from "admin/components/ui/field.html" import render_form_input with context %} {% from "admin/components/inline.html" import render_inline_section with context %} {% from "admin/components/icons.html" import icon %} {# HTMX posts to itself and swaps its own outerHTML with the response, so a validation error redisplays the form in place; a plain POST to `action` works identically without JS. The swap targets #resource-form-wrapper -- this whole element -- not the
inside it, because the error response *is* this template. Targeting the nested a fresh wrapper inside the old one on every failed save, duplicating the inline sections and the action bar each time. Layout comes from ui("page"): the record scrolls in a centred column while Save/Cancel stay pinned to the bottom of the viewport, so a long form never buries its own submit button. #}
{{ csrf_field(csrf_token) }} {% if list_token %}{% endif %} {% if non_field_errors %} {# A tinted, outlined panel rather than a solid red block: solid destructive is reserved for buttons, so an error summary does not out-shout Save. #} {% endif %} {# An untitled group renders bare -- no header, no border -- so a form declaring none looks exactly like a flat form. A titled group is always collapsible; `collapsed` only decides whether it starts closed. #} {% for fieldset in fieldsets %} {% if fieldset.title %}
{% if fieldset.description %}

{{ _(fieldset.description) }}

{% endif %}
{% for name in fieldset.fields %} {% set field = model_admin.get_field(name) %} {% set value = data.get(name) if data is not none else (field.get_value(object) if object is not none else none) %} {{ render_form_input(field, value, errors.get(name), relation_options.get(name), model_admin.is_readonly(name, object)) }} {% endfor %}
{% else %} {% for name in fieldset.fields %} {% set field = model_admin.get_field(name) %} {% set value = data.get(name) if data is not none else (field.get_value(object) if object is not none else none) %} {{ render_form_input(field, value, errors.get(name), relation_options.get(name), model_admin.is_readonly(name, object)) }} {% endfor %} {% endif %} {% endfor %} {# Inline sections carry their own
elements, so they have to stay siblings of the record form -- HTML forbids nesting one form inside another. #} {% for inline in inlines %}{{ render_inline_section(inline) }}{% endfor %}
{# The buttons sit outside #resource-form so the bar can span the full content width rather than being trapped in the centred column. `form="resource-form"` re-associates them: the browser submits normally, and htmx resolves the same attribute, so the submitter value still reaches the handler over HTMX. An unclicked submit button's name never makes it into the form data, so `name="_continue"` rides along only when it was the button that submitted -- which is how the handler knows to come back here instead of the detail page. #}
{# Delete lives on the edit form and nowhere else: there is nothing to delete while creating, and the detail page deliberately no longer offers it. Kept alone on the left, so it is never adjacent to the button people click by reflex. #} {% if object is not none and permissions.can_delete %} {{ icon("delete", "w-4 h-4") }} {{ _("Delete") }} {% endif %}
{# Only where adding another makes sense -- offering it on a form the principal may not create from would be a button that 403s. #} {% if permissions.can_create %} {# save_as: only on an existing record, since there is nothing to copy from on a create form. #} {% if allow_save_as %} {% endif %} {% endif %} {{ _("Cancel") }}