{# One macro per form control, dispatched from `form.html`'s field loop. Split out once that loop's `{% if field.widget == ... %}` chain reached 449 lines and kept growing with every column type Phase 3 added -- a page-layout template and a control library are two different things to review, and mixing them meant every new widget's diff also touched the page around it. `Renderer` runs with `undefined=StrictUndefined` (see `ui/renderer.py`'s docstring: a security control, not a preference), and a macro does not inherit the caller's template context the way an included file would -- so every value a macro needs is a parameter, spelled out at the call site in `form.html`. A macro that quietly relied on some ambient `settings` or `_` would raise the moment it was called from anywhere that variable was not already in scope, which is exactly the failure mode `StrictUndefined` exists to surface early rather than let happen once, in production, on whichever field a project opens first. #} {% macro readonly_control(field) %}
{{ field.value|ff_display }}
{% endmacro %} {% macro textarea_control(field) %} {% endmacro %} {% macro geometry_control(field, map_tile_url, map_center, map_attribution, map_max_zoom) %} {# Every kind gets the map's `data-` hooks -- Phase 4 turns the control next to them into an editor -- but only when a tile host is configured. The admin's CSP names exactly one (`script-src`/`img-src` widen by one origin each, in `admin/security.py`), so without one there is nowhere for a tile to come from and the field is a text box with nothing beside it. A POINT stays the single-line input it has always been: "41.2995, 69.2401" reads and types naturally as one line. Every other kind's WKT does not -- a polygon's rings do not fit a line at all, and making someone scroll a one-line box sideways to check their own coordinates is the kind of thing that sends people around the admin to edit the database directly. A missing `GeometrySpec` defaults to POINT rather than to the untyped "GEOMETRY" kind: it is what a project's own `formfield_overrides = {"col": "point"}` looks like once `canonical_widget` resolves that name to "geometry" -- a plain column standing in for a spatial one has no real kind to read, and the single-line box is what "point" has rendered since before this control had a `kind` to branch on at all. A genuine spatial column always carries a `GeometrySpec` (`_check_spatial` in `orm/sqlalchemy/types.py` sets one on every match), so this default is never read for a real GEOMETRY column. #} {% set kind = field.spec.geometry.kind if field.spec.geometry else "POINT" %} {% if kind == "POINT" %} {% else %} {% endif %} {% endmacro %} {% macro file_control(field, media_url, upload_limit, _) %} {# A native file input cannot be prefilled -- browsers refuse, for good reason -- so a stored value is shown as a link (or a thumbnail, for an image) beside the input rather than inside it. Choosing a new file replaces it on save; the "Clear" checkbox removes it without replacing it, and only appears when there is something to remove. This is also the whole control with script off. With it, the uploader reads this block for the stored value and the clear checkbox, adopts both, and replaces the pair with a drop target that previews what is there and what is about to replace it. #} {% if field.value %}
{% if field.widget == "image" %} {% endif %} {{ field.value.rsplit("/", 1)[-1] }}
{% endif %} {% endmacro %} {% macro color_control(field) %} {# A swatch and the hex beside it, kept in step. The native picker alone cannot be typed into and cannot be read aloud; the text box alone cannot be pointed at. Both, bound together, is the only version that serves everyone. #}
{% endmacro %} {% macro richtext_control(field) %} {# The editor is the project's to choose. FastFort renders the textarea and marks it; `UISettings.richtext_url` names a script that upgrades it -- CKEditor, TinyMCE, whatever the project already licences. Without one this is a textarea, which is a working control rather than a broken editor. #} {% endmacro %} {% macro nullboolean_control(field, _) %} {# Three states, because the column has three. A checkbox would turn "not answered yet" into "answered no" the first time anyone pressed Save. #} {% endmacro %} {% macro checkbox_control(field, _) %} {% endmacro %} {% macro password_control(field, _) %} {# Two boxes, both empty. The stored hash is never sent to the browser, so there is nothing to prefill and nothing to leak. #} {% endmacro %} {% macro _related_actions(field, relation_links, _) %} {% if relation_links.get(field.name) %} {# The buttons Django puts beside a foreign key. `add` opens the target's own form in a popup and drops the row it creates into this control; `change` and `view` open whatever is selected. Script-only: a popup that hands its result back has no meaning without one, and the picker works fine without them. #} {% endif %} {% endmacro %} {% macro select_control(field, autocomplete_url, relation_links, _) %} {% endmacro %} {% macro relations_control(field, autocomplete_url, relation_links, _) %} {{ _("Hold Ctrl (or Cmd) to select more than one.") }} {% endmacro %} {% macro tags_control(field) %} {# Today's behaviour, unchanged: one comma-separated text input. `values.py` still splits on commas and drops blank entries, so a hostile control here would only be a control the parser does not actually reflect. `data-ff-item-type` is what lets Phase 4 validate each entry as the item spec's own type -- an `ARRAY(Integer)` rejecting "banana" client-side, the same way the server already does in `_parse_array`. #} {% endmacro %} {% macro keyvalue_control(field) %} {# One `key: value` pair per line -- what `_parse_hstore` reads and `_hstore_text` writes back, and the shape a plain textarea can hold without any control needing a newline it cannot type. #} {% endmacro %} {% macro range_control(field, input_types, _) %} {% if field.spec.type == "multirange" %} {# Repeatable groups of two boxes and a bounds selector, without script to add and remove them, are not worth the complexity -- one range per line in a textarea is `_parse_multirange`'s own shape, typed directly. #} {% else %} {# Two inputs, not one text box carrying `[1, 10)` notation: a date range typed as brackets and a comma is a hostile control, and the whole point of splitting it is that a date range becomes two date pickers once Phase 4 sees `data-ff-date` on a box whose bound type is DATE -- exactly the marker a plain date column already carries. `Form.bind` reassembles `field__lower`, `field__upper` and `field__bounds` into the one string `values.parse_value` accepts before it ever runs; see `_bind_range`'s docstring in `admin/forms.py`. #} {% set bound_input = input_types.get(field.range_widget, "text") %}
{% endif %} {% endmacro %} {% macro generic_control(field, input_types) %} {# Every widget with no control of its own above -- text, number, decimal, money, date, datetime, time, duration, email, url, uuid, inet, mac, bits, and anything a project registered with `register_widget` but has not yet shipped a template partial for. `input_types.get(field.widget, "text")` is why the last case renders a text box instead of the 500 this used to be: `input_types[field.widget]` raised under `StrictUndefined` for any name `INPUT_TYPES` did not carry, which is exactly the name a brand-new widget always starts out as. #} {% endmacro %}