{# Field-level error message.
The id is what `aria-describedby` on the control points at, so the message
is read out when focus reaches the field rather than only being visible.
Errors also carry an icon + "Error:" prefix so the failure is not signalled
by the red colour alone (WCAG 1.4.1), and role="alert" so a message that
arrives in an Unpoly fragment swap is announced. #}
{% macro field_error(construct, error_id, error) %}
{% if error %}
{{ icon("cancel", label=construct.t("crud-error-prefix")) }}
{{ construct.t(error.key, error.args) }}
{% endif %}
{% endmacro %}
{# Attributes tying a control to its help text and error message. Emitted on
the control itself so the association survives however the field is laid
out. Help comes first so it is read before the error. #}
{% macro error_attrs(error_id, error, help_id=none) %}
{%- set ids = ([help_id] if help_id else []) + ([error_id] if error else []) -%}
{%- if error %} aria-invalid="true"{% endif -%}
{%- if ids %} aria-describedby="{{ ids | join(' ') }}"{% endif -%}
{% endmacro %}
{# A field's `help=` text: visible, persistent guidance placed between the
label and the control, and referenced by the control's aria-describedby
(pass the same id to `error_attrs`). Unlike a placeholder it stays put
while the user types and is read out by screen readers. Callers derive the
id as `name ~ '-help' if type.help else none`. #}
{% macro field_help(construct, help_id, type) %}
{% if type.help %}{{ construct.t(type.help) }}{% endif %}
{% endmacro %}
{# Placeholder for a text-like control: the field's own `placeholder=` when
set, otherwise the generic "enter ". #}
{% macro placeholder_text(construct, type) -%}
{%- if type.placeholder %}{{ construct.t(type.placeholder) }}{% else %}{{ construct.t("crud-enter-field", {"fieldname": construct.t(type.name) }).lower() }}{% endif -%}
{%- endmacro %}
{# A displayed value, with an empty one left blank rather than printed.
`format_value` hands back the stored attribute itself, which is None for a
null column and for a `Related` pointing at nothing — and Jinja renders that
as the word "None", so an unfilled field reads as though somebody typed it.
The test is against none rather than falsiness, because 0, an empty string
and False are all values somebody meant. #}
{% macro shown(value) %}{%- if value is not none %}{{ value }}{% endif -%}{% endmacro %}
{% macro render_field(construct, field, type, view, data=None, ds=None, errors=None) %}
{% if type.secret and data and type.readonly %}
{# Masked, with no way to change it — and never the stored value, which is
exactly what `secret` exists to keep out of the response. #}
{{ construct.t(type.name).capitalize() }}
{{ icon("lock", size="sm") }}
••••••••
{% elif type.secret and data %}
{# Rendered once out here; the inputs below (skip_label) still point at it. #}
{{ field_help(construct, field ~ '-help', type) }}
{{ icon("lock", size="sm") }}
••••••••
{# A JS state flag, not a control the user operates: the visible
toggle button drives it. Hidden from assistive technology so it
is not announced as a nameless checkbox. #}
{% else %}
{{ _render_field_internal(construct, field, type, view, data, ds, errors=errors) }}
{% endif %}
{% endmacro %}
{% macro _render_field_internal(construct, field, type, view, data=None, ds=None, skip_label=False, disabled=False, errors=None) %}
{# `readonly` is checked before any of the editors below: a readonly field
rendered as a checkbox or a chip picker looks operable, and the read-only
form view is built entirely out of this branch. Lists are excluded — the
list branch has its own readonly handling for rows and the add form. #}
{% set help_id = field ~ '-help' if type.help else none %}
{% if type.readonly == True and type.field_type != 'list' %}
{# Not a
{% elif type.field_type == 'bool' %}
{# After the label here: a checkbox's label sits beside it, not above. #}
{{ field_help(construct, help_id, type) }}
{% if not skip_label %}{% endif %}
{% if not skip_label %}{{ field_help(construct, help_id, type) }}{% endif %}
{% set value = type.form_value(data) if data else None %}
{# A combobox: the roles describe what the keyboard handling in
core.js already does (Arrow/Enter/Escape over the results).
Without them the roving highlight is a CSS class only, so a
screen reader announces nothing as you arrow through and Enter
activates an option the user was never told about. #}
{% elif type.field_type == 'manyrelated' %}
{# Labels the picker group; the search input inside points at it with
aria-labelledby, since a
{% elif type.field_type == 'enum' %}
{% set error = errors.get(field) if errors else None %}
{% set error_id = field ~ '-error' %}
{% if not skip_label %}{% endif %}
{% if not skip_label %}{{ field_help(construct, help_id, type) }}{% endif %}
{{ field_error(construct, error_id, error) }}
{% elif type.field_type == 'list' %}
{% if not skip_label %}{{ construct.t(type.name).capitalize() }}{% endif %}
{% if not skip_label %}{{ field_help(construct, help_id, type) }}{% endif %}
{% if data and ds %}
{% set items = ds.get_list(data, field) %}
{% if items %}
{% if type.layout == 'table' %}
{% if type.columns %}
{% for col in type.columns %}
{% set t = type.list_class()._schema[col] %}
{{ construct.t(t.name).capitalize() }}{% if not (type.readonly or t.readonly) %}{{ field_help(construct, field ~ '-col-' ~ col ~ '-help', t) }}{% endif %}
{% endfor %}
{% else %}
{% for f, t in type.list_class()._schema.items() if t.field_type != 'pk' and t.visible_form %}
{{ construct.t(t.name).capitalize() }}{% if not (type.readonly or t.readonly) %}{{ field_help(construct, field ~ '-col-' ~ f ~ '-help', t) }}{% endif %}
{% endfor %}
{% endif %}
{% if not type.readonly %}
{% if type.can_delete %}{{ construct.t("crud-action-title") }}{% endif %}
{% endif %}
{% for item in items %}
{% set list_ds = type.list_class() %}
{% set list_pk_field = list_ds._primary_key %}
{% set item_id = item[list_pk_field] %}
{% set columns_to_render = type.columns if type.columns else list_ds._schema.keys() %}
{% for f in columns_to_render if list_ds._schema[f].field_type != 'pk' and (type.columns or list_ds._schema[f].visible_form) %}
{% set t = list_ds._schema[f] %}
{% if type.readonly or t.readonly %}
{% if t.field_type == 'bool' %}
{% if item[f] %}
{{ icon("check", style="color: var(--fcu-success-fg); font-variation-settings: 'FILL' 1", label=construct.t("crud-bool-true")) }}
{% else %}
{{ icon("cancel", style="color: var(--fcu-danger-fg); font-variation-settings: 'FILL' 1", label=construct.t("crud-bool-false")) }}
{% endif %}
{% elif t.field_type == 'set' %}
{% if item[f] %}
{% for v in item[f] %}
{% if v in t.sets %}{{ construct.t(t.sets[v]) }}{% endif %}
{% endfor %}
{% endif %}
{% elif t.field_type == 'related' %}
{% set curr = item %}
{% for part in t.related_field.split('.') %}
{% set curr = curr[part] %}
{% endfor %}
{{ shown(curr) }}
{% elif t.field_type == 'enum' %}
{{ shown(t.format_value(f, item)) }}
{% elif t.orgmode %}
{% set v = t.format_value(f, item) %}
{{ (v if v is not none else '') | orgmode }}
{% elif t.form_input_type() == 'richtext' %}
{% set v = t.format_value(f, item) %}
{{ (v if v is not none else '') | markdown }}
{% else %}
{{ shown(t.format_value(f, item)) }}
{% endif %}
{% else %}
{% set input_name = field ~ "-" ~ item_id ~ "-" ~ f %}
{% set sub_help_id = field ~ '-col-' ~ f ~ '-help' if t.help else none %}
{% if t.field_type == 'bool' %}
{% elif t.field_type == 'set' %}
{% set values = item[f] if item and item[f] else [] %}
{% for key, label_key in t.sets.items() %}
{% endfor %}
{% elif t.field_type == 'related' %}
{% elif t.field_type == 'enum' %}
{% elif t.form_input_type() == 'richtext' %}
{% set editor_id = 'editor_' ~ input_name %}
{% set value = item[f] if item[f] is not none else '' %}
{% else %}
{% set list_err = t.validate_value(item[f], item) if item else None %}
{% if list_err %}
{{ construct.t(list_err.key, list_err.args) }}
{% endif %}
{% endif %}
{% endif %}
{% endfor %}
{% if not type.readonly %}
{% if type.can_delete %}
{% endif %}
{% endif %}
{% endfor %}
{% elif type.layout == 'form' %}
{% for item in items %}
{% set list_ds = type.list_class() %}
{% set list_pk_field = list_ds._primary_key %}
{% set item_id = item[list_pk_field] %}
{{ item }}
{% if not type.readonly %}
{% if type.can_delete %}
{% endif %}
{% endif %}
{% if type.template %}
{% include type.template %}
{% else %}
{% set columns_to_render = type.columns if type.columns else list_ds._schema.keys() %}
{% for f in columns_to_render if list_ds._schema[f].field_type != 'pk' and (type.columns or list_ds._schema[f].visible_form) %}
{% set t = list_ds._schema[f] %}
{% set input_name = field ~ "-" ~ item_id ~ "-" ~ f %}
{% set sub_help_id = input_name ~ '-help' if t.help else none %}
{# Readonly rows render text, not a control, so there
is nothing for a
{% endfor %}
{% endif %}
{% endfor %}
{% endif %}
{% else %}
{{ construct.t("crud-no-items-found") }}
{% endif %}
{% endif %}
{% if not type.readonly and type.can_add %}
{% if type.layout == 'form' %}
{{ construct.t("crud-new-item") }}
{% if type.template %}
{% include type.template %}
{% else %}
{% set list_ds = type.list_class() %}
{% for f, t in list_ds._schema.items() if t.field_type != 'pk' and t.visible_form %}
{% set input_name = field ~ "-new-__INDEX__-" ~ f %}
{% set sub_help_id = input_name ~ '-help' if t.help else none %}
{{ construct.t(t.name).capitalize() }}
{{ field_help(construct, sub_help_id, t) }}
{% if t.field_type == 'bool' %}
{% if not skip_label %}{{ construct.t(type.name).capitalize() }}{% endif %}
{# Normalised before it reaches the control: a null rendered as "None"
inside a textarea is a value the next save would store for real. #}
{% set raw = type.format_value(field, data) if data else '' %}
{% set value = raw if raw is not none else '' %}
{% set column = ds.model[field] if ds else None %}
{% set maxlength = column.type.length if column and column.type and column.type.length else '' %}
{% set error = errors.get(field) if errors else (type.validate_value(value) if type.validate_value and data else None) %}
{% set error_id = field ~ '-error' %}
{% if not skip_label %}{{ field_help(construct, help_id, type) }}{% endif %}
{{ field_error(construct, error_id, error) }}
{% elif type.form_input_type() == 'richtext' %}
{% set editor_id = 'editor_' ~ field %}
{% set raw = type.format_value(field, data) if data else '' %}
{% set value = raw if raw is not none else '' %}
{% set column = ds.model[field] if ds else None %}
{% set maxlength = column.type.length if column and column.type and column.type.length else '' %}
{% set error = errors.get(field) if errors else (type.validate_value(value) if type.validate_value and data else None) %}
{% set error_id = field ~ '-error' %}
{# `for` on a
does nothing, so the ProseMirror host is named by
aria-labelledby against a plain instead. The no-JS
{% else %}
{% if not skip_label %}{{ construct.t(type.name).capitalize() }}{% endif %}
{% set value = type.format_value(field,data) if data else None %}
{% set maxlength = type.length if type.length else '' %}
{% set error = errors.get(field) if errors else (type.validate_value(value) if type.validate_value and data else None) %}
{% set error_id = field ~ '-error' %}
{% if not skip_label %}{{ field_help(construct, help_id, type) }}{% endif %}
{{ field_error(construct, error_id, error) }}
{% endif %}
{% endmacro %}
{% macro render_newrel_subfield(construct, input_name, t, value=None, error=None) %}
{% set sub_help_id = input_name ~ '-help' if t.help else none %}
{# One inline-create panel input for a ManyRelated create="form" field.
Supports the types in ManyRelated.INLINE_FORM_TYPES; text renders as a
plain textarea (no ProseMirror inside cloned content). #}
{% if t.field_type == 'bool' %}
{% else %}
{% endif %}
{% if error %}
{{ construct.t(error.key, error.args) }}
{% endif %}
{% endmacro %}
{% macro render_newrel_panel(construct, field, type, index, item=None, errors=None, closable=True) %}
{# Inline create panel for a ManyRelated create="form" field — one staged
related record. Used for the clone (index='__INDEX__'), for
re-rendered staged panels after a validation failure, and (without the
close button) for the noscript fallback. #}
{{ construct.t("crud-new-item") }}
{% if closable %}
{% endif %}
{% for sub_field, t in type.inline_form_fields() %}
{% set input_name = field ~ "-newrel-" ~ index ~ "-" ~ sub_field %}
{% set sub_help_id = input_name ~ '-help' if t.help else none %}
{% endmacro %}
{# Walk a resolved form layout (flask_construct_ui/layout.py) and hand every
field leaf back to the caller, which decides how a single field renders —
`render_field` on the page forms, `render_child_field` in a related tab:
{% call(field, type) render_layout(construct, form_layout, fields, errors) %}
{{ render_field(construct, field, type, view, data, ds, errors=errors) }}
{% endcall %}
`nodes` is none → fall back to the flat `fields` loop. DOM order is layout
order, so reading and tab order match the screen. #}
{% macro render_layout(construct, nodes, fields=none, errors=none) %}
{% set outer = caller %}
{% if nodes is none %}
{% for field, type in fields or [] %}{{ outer(field, type) }}{% endfor %}
{% else %}
{% for node in nodes %}
{% if node.kind == 'field' %}
{{ outer(node.name, node.field) }}
{% elif node.kind == 'row' %}
{% for cell in node.children %}
{{ outer(cell.name, cell.field) }}
{% endfor %}
{% elif node.kind == 'section' %}
{% set title = construct.t(node.title) %}
{% if node.collapsible %}
{# A disclosure works without JS. A section holding an
error is forced open, so a failed submit never hides the field
the error summary links to. The group keeps its name through
aria-label because a