Runique Admin

Runique Admin — Tera Variables per View

This document lists all variables available in the Tera context when a developer overrides an admin template.

Global Variables (All Routes)

These variables are injected into every route by the framework's Request extractor, before any admin handler runs.

VariableRust TypeDescription
debugboolWhether debug mode is enabled
csrf_tokenStringMasked CSRF token — include in every POST form
csp_nonce&strCSP nonce for <script> and <style> tags
static_runiqueStaticConfigRunique static assets config (see below)
messagesVec<FlashMessage>Flash messages from the current session
current_userCurrentUser (optional)Authenticated user data — absent if not logged in
admin_prefixStringURL prefix of the admin panel — e.g. "/admin" or "/admin-campanile". Always use this variable for all admin links — never hardcode /admin/.

static_runique Fields

{{ static_runique.static_url }}   {# Base URL for assets, e.g. /static #}
{{ static_runique.static_dir }}   {# Physical directory on disk #}

Variables Injected by CRUD Handlers

These variables are injected into all CRUD admin views via inject_context, after the base extractor.

VariableTypeDescription
langStringCurrent language code (e.g. "en")
site_titleStringSite title configured in AdminConfig
site_urlStringBase URL configured in AdminConfig
resource_key&strKey of the current resource (e.g. "users")
current_resource&strSame as resource_key
resourceAdminResourceFull metadata of the current resource (see below)
resourcesVec<AdminResource>All resources registered in the registry
registered_rolesVec<String>All roles registered via register_roles()

Keys declared in extra: {} in admin!{} are also injected as top-level Tera variables. Example: extra: { "icon" => "user" }{{ icon }} (direct access) AND {{ resource.extra_context.icon }}.

AdminResource Structure

Tera FieldTypeDescription
resource.key&strUnique resource key ("users")
resource.title&strHuman-readable title ("Users")
resource.model_path&strSeaORM model path ("crate::entities::users::Model")
resource.permissions.listVec<String>Roles allowed for list
resource.permissions.viewVec<String>Roles allowed for detail
resource.permissions.createVec<String>Roles allowed for create
resource.permissions.editVec<String>Roles allowed for edit
resource.permissions.deleteVec<String>Roles allowed for delete
resource.display.iconString (optional)Icon name declared in config
resource.display.paginationusizeEntries per page (default: 25)
resource.extra_contextHashMap<String, String>Custom keys declared in extra: {}
resource.id_typeAdminIdTypePrimary key type (I32, I64, Uuid)

Global i18n Variables (All CRUD Views)

Automatically injected via insert_admin_messages. The Tera variable name is the i18n key with . replaced by _.

base Section

Tera Variablei18n Key
admin_base_titleadmin.base.title
admin_base_breadcrumbadmin.base.breadcrumb
admin_base_toggleadmin.base.toggle
admin_base_logout_titleadmin.base.logout_title

list Section

Tera Variablei18n Key
admin_list_breadcrumb_adminadmin.list.breadcrumb_admin
admin_list_entries_countadmin.list.entries_count
admin_list_btn_createadmin.list.btn_create
admin_list_th_idadmin.list.th_id
admin_list_th_actionsadmin.list.th_actions
admin_list_bool_trueadmin.list.bool_true
admin_list_bool_falseadmin.list.bool_false
admin_list_btn_detailadmin.list.btn_detail
admin_list_btn_editadmin.list.btn_edit
admin_list_btn_deleteadmin.list.btn_delete
admin_list_confirm_deleteadmin.list.confirm_delete
admin_list_empty_titleadmin.list.empty_title
admin_list_empty_descadmin.list.empty_desc
admin_list_btn_create_firstadmin.list.btn_create_first

create Section

Tera Variablei18n Key
admin_create_titleadmin.create.title
admin_create_breadcrumbadmin.create.breadcrumb
admin_create_card_infoadmin.create.card_info
admin_create_no_fieldsadmin.create.no_fields
admin_create_btn_canceladmin.create.btn_cancel
admin_create_btn_submitadmin.create.btn_submit

edit Section

Tera Variablei18n Key
admin_edit_titleadmin.edit.title
admin_edit_breadcrumbadmin.edit.breadcrumb
admin_edit_card_infoadmin.edit.card_info
admin_edit_no_fieldsadmin.edit.no_fields
admin_edit_btn_canceladmin.edit.btn_cancel
admin_edit_btn_submitadmin.edit.btn_submit

detail Section

Tera Variablei18n Key
admin_detail_titleadmin.detail.title
admin_detail_breadcrumbadmin.detail.breadcrumb
admin_detail_entry_labeladmin.detail.entry_label
admin_detail_btn_listadmin.detail.btn_list
admin_detail_btn_editadmin.detail.btn_edit
admin_detail_btn_deleteadmin.detail.btn_delete
admin_detail_confirm_deleteadmin.detail.confirm_delete

delete Section

Tera Variablei18n Key
admin_delete_titleadmin.delete.title
admin_delete_breadcrumbadmin.delete.breadcrumb
admin_delete_headingadmin.delete.heading
admin_delete_btn_canceladmin.delete.btn_cancel
admin_delete_btn_confirmadmin.delete.btn_confirm
admin_delete_warning_titleadmin.delete.warning.title
admin_delete_warning_descadmin.delete.warning.desc
admin_delete_warning_ofadmin.delete.warning.of
admin_delete_warning_irreversibleadmin.delete.warning.irreversible

`login` View

Route: GET /admin/login

inject_context is not called — variables resource, resources, and resource_key are not available.

VariableTypeDescription
site_titleStringSite title
langStringCurrent language code
csrf_tokenStringCSRF token (injected by the base extractor)
admin_login_titleStringi18n admin.login.title
admin_login_subtitleStringi18n admin.login.subtitle
admin_login_label_usernameStringi18n admin.login.label_username
admin_login_label_passwordStringi18n admin.login.label_password
admin_login_btn_submitStringi18n admin.login.btn_submit
admin_login_error_sessionStringi18n admin.login.error_session
admin_login_error_credentialsStringi18n admin.login.error_credentials
error (optional)StringVerbatim error message — injected only on POST failure

Required keys — login

csrf_token, site_title, lang

error is optional — present only after a POST failure. On POST failure, base section i18n keys are also injected.

Minimal example:

{% extends "admin/admin_template.html" %}

{% block title %}{{ admin_login_title }}{% endblock %}

{% block content %}
<div class="login-container">
    <h1>{{ admin_login_title }}</h1>
    <p>{{ admin_login_subtitle }}</p>

    {% if error %}
    <div class="alert alert-danger">{{ error }}</div>
    {% endif %}

    <form method="POST" action="{{ admin_prefix }}/login">
        <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
        <div>
            <label>{{ admin_login_label_username }}</label>
            <input type="text" name="username" required>
        </div>
        <div>
            <label>{{ admin_login_label_password }}</label>
            <input type="password" name="password" required>
        </div>
        <button type="submit">{{ admin_login_btn_submit }}</button>
    </form>
</div>
{% endblock %}

The login template extends admin_template.html (empty blocks contract), not admin_base.html — sidebar and topbar must not appear on the login page.


`dashboard` View

Route: GET /admin/

inject_context is not called. current_resource is explicitly None.

VariableTypeDescription
site_titleStringSite title
langStringCurrent language code
resourcesVec<AdminResource>All registered resources
resource_countsHashMap<String, u64>Entry count per resource (key = resource.key)
current_page&strValue "dashboard"
current_resourceNoneNot set — no resource selected
admin_base_*i18n base section keys (see above)
admin_dashboard_titleStringi18n admin.dashboard.title
admin_dashboard_subtitleStringi18n admin.dashboard.subtitle
admin_dashboard_card_resourcesStringi18n admin.dashboard.card_resources
admin_dashboard_th_resourceStringi18n admin.dashboard.th_resource
admin_dashboard_th_keyStringi18n admin.dashboard.th_key
admin_dashboard_th_permissionsStringi18n admin.dashboard.th_permissions
admin_dashboard_th_actionsStringi18n admin.dashboard.th_actions
admin_dashboard_btn_listStringi18n admin.dashboard.btn_list
admin_dashboard_btn_createStringi18n admin.dashboard.btn_create
admin_dashboard_see_listStringi18n admin.dashboard.see_list
admin_dashboard_empty_titleStringi18n admin.dashboard.empty_title
admin_dashboard_empty_descStringi18n admin.dashboard.empty_desc

Required keys — dashboard

resources, resource_counts, current_page

Minimal example:

{% extends "admin/admin_base" %}

{% block title %}{{ admin_dashboard_title }}{% endblock %}

{% block content %}
<h1>{{ admin_dashboard_title }}</h1>
<p>{{ admin_dashboard_subtitle }}</p>

{% if resources %}
<h2>{{ admin_dashboard_card_resources }}</h2>
<table class="table">
    <thead>
        <tr>
            <th>{{ admin_dashboard_th_resource }}</th>
            <th>{{ admin_dashboard_th_key }}</th>
            <th>{{ admin_dashboard_th_permissions }}</th>
            <th>{{ admin_dashboard_th_actions }}</th>
        </tr>
    </thead>
    <tbody>
        {% for res in resources %}
        <tr>
            <td>{{ res.title }}</td>
            <td><code>{{ res.key }}</code></td>
            <td>{{ res.permissions.list | join(sep=", ") }}</td>
            <td>
                <a href="{{ admin_prefix }}/{{ res.key }}/list">
                    {{ admin_dashboard_btn_list }}
                    {% if resource_counts[res.key] %}({{ resource_counts[res.key] }}){% endif %}
                </a>
                <a href="{{ admin_prefix }}/{{ res.key }}/create">{{ admin_dashboard_btn_create }}</a>
            </td>
        </tr>
        {% endfor %}
    </tbody>
</table>
{% else %}
<p>{{ admin_dashboard_empty_title }}</p>
<p>{{ admin_dashboard_empty_desc }}</p>
{% endif %}
{% endblock %}

`list` View

Route: GET /admin/{resource}/list

Pagination

VariableTypeDescription
entriesVec<Value>Records for the current page, serialized as JSON
totalu64Total number of entries
pageu64Current page number (starts at 1)
page_countu64Total number of pages
has_prevboolA previous page exists
has_nextboolA next page exists
prev_pageu64Previous page number
next_pageu64Next page number
current_page&strValue "list"

Columns

VariableTypeDescription
visible_columnsVec<String>Column names to display (from list_display, or all except id/password)
column_labelsHashMap<String, String>Label per column — empty if list_display not set, otherwise { "col" => "Label" }

Sorting

VariableTypeDescription
sort_byStringActive sort column — empty string if no sort
sort_dirStringDirection: "asc" or "desc"
sort_dir_toggleStringOpposite of sort_dir — useful for header links

Search

VariableTypeDescription
searchStringCurrent search term — empty string if none

Filter sidebar

VariableTypeDescription
filter_valuesHashMap<String, Vec<String>>Distinct values per filter column (from list_filter)
active_filtersHashMap<String, String>Active filter per column — "" if no active filter on that column
filter_qsStringQuery string fragment for active filters — append to pagination links
filter_metaHashMap<String, Object>Sidebar pagination per column — see structure below
filter_page_sizeu64Number of values shown per page in the filter sidebar (from list_filter_limit)
return_qsStringFull query string (sort + search + active filters) — pass to edit/delete links to restore list state on return

Note: active_filters is pre-populated for all list_filter columns (value "" if inactive). Tera raises an error on missing keys — this pre-init prevents it. Multiple columns can have a non-empty value simultaneously: filter links preserve active filters from other columns.

Required keys for template override

Referenced via runique::utils::constante::admin_ctx::list::REQUIRED:

entries, total, page, page_count, has_prev, has_next,
prev_page, next_page, visible_columns,
sort_by, sort_dir, sort_dir_toggle, search

The i18n variables for the list section are listed in the global variables above.

Minimal example:

{% extends "admin/admin_base" %}

{% block title %}{{ resource.title }}{% endblock %}

{% block content %}
<div class="d-flex justify-content-between align-items-center mb-3">
    <h1>{{ resource.title }}
        <small class="text-muted fs-6">{{ total }} {{ admin_list_entries_count }}</small>
    </h1>
    <a href="{{ admin_prefix }}/{{ resource_key }}/create" class="btn btn-primary">
        {{ admin_list_btn_create }}
    </a>
</div>

{% if entries %}
<table class="table">
    <thead>
        <tr>
            <th>{{ admin_list_th_id }}</th>
            {# add columns based on the model #}
            <th>{{ admin_list_th_actions }}</th>
        </tr>
    </thead>
    <tbody>
        {% for entry in entries %}
        <tr>
            <td>{{ entry.id }}</td>
            <td>
                <a href="{{ admin_prefix }}/{{ resource_key }}/{{ entry.id }}/detail">{{ admin_list_btn_detail }}</a>
                <a href="{{ admin_prefix }}/{{ resource_key }}/{{ entry.id }}/edit">{{ admin_list_btn_edit }}</a>
                <a href="{{ admin_prefix }}/{{ resource_key }}/{{ entry.id }}/delete">{{ admin_list_btn_delete }}</a>
            </td>
        </tr>
        {% endfor %}
    </tbody>
</table>
{% else %}
<p>{{ admin_list_empty_title }}</p>
<p>{{ admin_list_empty_desc }}</p>
<a href="{{ admin_prefix }}/{{ resource_key }}/create">{{ admin_list_btn_create_first }}</a>
{% endif %}
{% endblock %}

`create` View

Route: GET /admin/{resource}/create

VariableTypeDescription
form_fieldsFormsForm generated by request.form() — rendered via {% form.field_name %} or form_fields.html
is_editboolValue false
m2m_fieldsVec<M2mFieldOptions> (optional)Many-to-many fields declared via m2m: [...] in the DSL. Absent if no M2M is declared on the resource.

Each entry in m2m_fields exposes:

PropertyTypeDescription
field_nameStringField name — input name prefix (m2m_{field_name}__{id})
labelStringLabel displayed in the form
choicesVec<(String, String)>All available options — (id, label)
selectedVec<String>Selected IDs (empty on create)

Tera example:

{% if m2m_fields is defined and m2m_fields %}
  {% for field in m2m_fields %}
  <div class="form-group">
    <label>{{ field.label }}</label>
    {% for choice in field.choices %}
    <label>
      <input type="checkbox"
             name="m2m_{{ field.field_name }}__{{ choice[0] }}"
             value="1">
      {{ choice[1] }}
    </label>
    {% endfor %}
  </div>
  {% endfor %}
{% endif %}

Required keys — create

form_fields, is_edit

The i18n variables for the create section are listed in the global variables above.

Minimal example:

{% extends "admin/admin_base" %}

{% block title %}{{ admin_create_title }} — {{ resource.title }}{% endblock %}

{% block content %}
<h1>{{ admin_create_title }} — {{ resource.title }}</h1>
<p class="text-muted">{{ admin_create_card_info }}</p>

<form method="POST" action="{{ admin_prefix }}/{{ resource_key }}/create">
    {# csrf.js handles the token automatically for admin forms #}
    {% if form_fields.html %}
        {{ form_fields.html | safe }}
    {% else %}
        <p>{{ admin_create_no_fields }}</p>
    {% endif %}
    <button type="submit" class="btn btn-primary">{{ admin_create_btn_submit }}</button>
    <a href="{{ admin_prefix }}/{{ resource_key }}/list" class="btn btn-secondary">{{ admin_create_btn_cancel }}</a>
</form>
{% endblock %}

`edit` View

Route: GET /admin/{resource}/{id}/edit

VariableTypeDescription
form_fieldsFormsForm pre-filled with existing data
is_editboolValue true
object_idStringID of the entry being edited
m2m_fieldsVec<M2mFieldOptions> (optional)Same structure as create — selected contains the already-associated IDs, corresponding checkboxes are pre-checked.

Tera example (with pre-selection):

{% if m2m_fields is defined and m2m_fields %}
  {% for field in m2m_fields %}
  <div class="form-group">
    <label>{{ field.label }}</label>
    {% for choice in field.choices %}
    <label>
      <input type="checkbox"
             name="m2m_{{ field.field_name }}__{{ choice[0] }}"
             value="1"
             {% if choice[0] in field.selected %}checked{% endif %}>
      {{ choice[1] }}
    </label>
    {% endfor %}
  </div>
  {% endfor %}
{% endif %}

Required keys — edit

form_fields, is_edit, object_id

The i18n variables for the edit section are listed in the global variables above.

Minimal example:

{% extends "admin/admin_base" %}

{% block title %}{{ admin_edit_title }} — {{ resource.title }} #{{ object_id }}{% endblock %}

{% block content %}
<h1>{{ admin_edit_title }} — {{ resource.title }} <small>#{{ object_id }}</small></h1>
<p class="text-muted">{{ admin_edit_card_info }}</p>

<form method="POST" action="{{ admin_prefix }}/{{ resource_key }}/{{ object_id }}/edit">
    {% if form_fields.html %}
        {{ form_fields.html | safe }}
    {% else %}
        <p>{{ admin_edit_no_fields }}</p>
    {% endif %}
    <button type="submit" class="btn btn-primary">{{ admin_edit_btn_submit }}</button>
    <a href="{{ admin_prefix }}/{{ resource_key }}/list" class="btn btn-secondary">{{ admin_edit_btn_cancel }}</a>
</form>
{% endblock %}

`detail` View

Route: GET /admin/{resource}/{id}/detail

VariableTypeDescription
entryValue (optional)Record serialized as JSON — absent if get_fn is not configured
object_idStringEntry ID

Required keys — detail

object_id

entry is optional — absent if get_fn is not configured on the resource. The i18n variables for the detail section are listed in the global variables above.

Minimal example:

{% extends "admin/admin_base" %}

{% block title %}{{ admin_detail_title }} — {{ resource.title }} #{{ object_id }}{% endblock %}

{% block content %}
<h1>{{ admin_detail_title }} — {{ resource.title }} <small>#{{ object_id }}</small></h1>

{% if entry %}
<dl class="row">
    {% for key, value in entry %}
    <dt class="col-sm-3">{{ key }}</dt>
    <dd class="col-sm-9">{{ value }}</dd>
    {% endfor %}
</dl>
{% endif %}

<a href="{{ admin_prefix }}/{{ resource_key }}/list" class="btn btn-secondary">{{ admin_detail_btn_list }}</a>
<a href="{{ admin_prefix }}/{{ resource_key }}/{{ object_id }}/edit" class="btn btn-primary">{{ admin_detail_btn_edit }}</a>
<a href="{{ admin_prefix }}/{{ resource_key }}/{{ object_id }}/delete" class="btn btn-danger">{{ admin_detail_btn_delete }}</a>
{% endblock %}

`delete` View

Route: GET /admin/{resource}/{id}/delete

VariableTypeDescription
entryValue (optional)Record serialized as JSON — absent if get_fn is not configured
object_idStringID of the entry to delete

Required keys — delete

object_id

entry is optional — absent if get_fn is not configured on the resource. The i18n variables for the delete section are listed in the global variables above.

Minimal example:

{% extends "admin/admin_base" %}

{% block title %}{{ admin_delete_title }} — {{ resource.title }} #{{ object_id }}{% endblock %}

{% block content %}
<h1>{{ admin_delete_heading }}</h1>

<div class="alert alert-danger">
    <strong>{{ admin_delete_warning_title }}</strong>
    <p>{{ admin_delete_warning_desc }}</p>
    {% if entry %}
    <p>{{ admin_delete_warning_of }} <strong>#{{ object_id }}</strong></p>
    {% endif %}
    <p>{{ admin_delete_warning_irreversible }}</p>
</div>

<form method="POST" action="{{ admin_prefix }}/{{ resource_key }}/{{ object_id }}/delete">
    <button type="submit" class="btn btn-danger">{{ admin_delete_btn_confirm }}</button>
    <a href="{{ admin_prefix }}/{{ resource_key }}/list" class="btn btn-secondary">{{ admin_delete_btn_cancel }}</a>
</form>
{% endblock %}

{{ admin_create_title }}

Do not use | default(value="...") — i18n variables are always present when a language is configured.


Overriding a Template

Using .templates() in the builder replaces the template for all resources:

RuniqueApp::builder(config)
    .with_admin(|a| a
        .templates(|t| t
            .with_list("my_theme/list")
            .with_create("my_theme/create")
            .with_edit("my_theme/edit")
            .with_detail("my_theme/detail")
            .with_delete("my_theme/delete")
            .with_dashboard("my_theme/dashboard")
            .with_login("my_theme/login")
            .with_base("my_theme/admin_base")
        )
    )
    .build().await?

Overridden templates have access to the same variables as the default templates.

Sub-sections

SectionDescription
OverrideReplace the layout or a CRUD component
CSRFCSRF token, csrf.js, custom login checklist