{# 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 error message. Emitted on the control itself so the association survives however the field is laid out. #} {% macro error_attrs(error_id, error) %} {%- if error %} aria-invalid="true" aria-describedby="{{ error_id }}"{% 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", style="font-size: 14px;") }} ••••••••
{% elif type.secret and data %}
{{ icon("lock", style="font-size: 14px;") }} ••••••••
{# 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. #}
{{ icon("lock", style="font-size: 14px;") }} ••••••••
{{ _render_field_internal(construct, field, type, view, data=None, ds=ds, skip_label=True) }}
{% 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. #} {% if type.readonly == True and type.field_type != 'list' %}
{# Not a
{% elif type.field_type == 'bool' %}
{% elif type.field_type == 'set' %}
{% if not skip_label %}{{ construct.t(type.name).capitalize() }}{% endif %} {% set values = data[field] if data and data[field] else [] %}
{% for key, label_key in type.sets.items() %}
{% endfor %}
{% elif type.field_type == 'related' %}
{% if not skip_label %}{% 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 %} {{ field_error(construct, error_id, error) }}
{% elif type.field_type == 'list' %}
{% if not skip_label %}{{ construct.t(type.name).capitalize() }}{% 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] %} {% endfor %} {% else %} {% for f, t in type.list_class()._schema.items() if t.field_type != 'pk' and t.visible_form %} {% endfor %} {% endif %} {% if not type.readonly %} {% 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] %} {% endfor %} {% if not type.readonly %} {% endif %} {% endfor %}
{{ construct.t(t.name).capitalize() }}{{ construct.t(t.name).capitalize() }} {% if type.can_delete %}{{ construct.t("crud-action-title") }}{% endif %}
{% 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 %} {% 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 %}
{% if type.can_delete %} {% endif %}
{% 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 %}
{# 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 %}
{% endif %}
{% elif type.form_input_type() == 'textarea' %}
{% if not skip_label %}{% 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' %} {{ 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
{{ field_error(construct, error_id, error) }}
{% else %}
{% if not skip_label %}{% 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' %} {{ field_error(construct, error_id, error) }}
{% endif %} {% endmacro %} {% macro render_newrel_subfield(construct, input_name, t, value=None, error=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