Admin Runique

Runique Admin — Variables Tera par vue

Ce document liste toutes les variables disponibles dans le contexte Tera lorsqu'un développeur surcharge un template admin.

Variables globales (toutes les routes)

Ces variables sont injectées sur toutes les routes par l'extracteur Request du framework, avant même que les handlers admin s'exécutent.

VariableType RustDescription
debugboolMode debug activé ou non
csrf_tokenStringToken CSRF masqué — à inclure dans les formulaires POST
csp_nonce&strNonce CSP pour les balises <script> et <style>
static_runiqueStaticConfigConfig des assets statiques Runique (voir ci-dessous)
messagesVec<FlashMessage>Messages flash de la session courante
current_userCurrentUser (optionnel)Données de l'utilisateur connecté, absent si non authentifié
admin_prefixStringPréfixe URL du panel admin — ex : "/admin" ou "/admin-campanile". Toujours utiliser cette variable pour tous les liens admin — ne jamais coder /admin/ en dur.

Champs de static_runique

{{ static_runique.static_url }}   {# URL de base des assets, ex: /static #}
{{ static_runique.static_dir }}   {# Répertoire physique sur disque #}

Variables injectées par les handlers CRUD

Ces variables sont injectées sur toutes les vues CRUD admin via inject_context, après l'extracteur.

VariableTypeDescription
langStringCode de langue courant (ex: "fr")
site_titleStringTitre du site configuré dans AdminConfig
site_urlStringURL de base du site configurée dans AdminConfig
resource_key&strClé de la ressource courante (ex: "users")
current_resource&strIdentique à resource_key
resourceAdminResourceMétadonnées complètes de la ressource courante (voir ci-dessous)
resourcesVec<AdminResource>Toutes les ressources enregistrées dans le registre
registered_rolesVec<String>Tous les rôles enregistrés via register_roles()

Les clés déclarées dans extra: {} du bloc admin!{} sont également injectées en tant que variables Tera de premier niveau. Exemple : extra: { "icon" => "user" }{{ icon }} (accessible directement) ET {{ resource.extra_context.icon }}.

Structure AdminResource

Champ TeraTypeDescription
resource.key&strClé unique de la ressource ("users")
resource.title&strTitre lisible ("Utilisateurs")
resource.model_path&strChemin du modèle SeaORM ("crate::entities::users::Model")
resource.permissions.listVec<String>Rôles autorisés pour la liste
resource.permissions.viewVec<String>Rôles autorisés pour le détail
resource.permissions.createVec<String>Rôles autorisés pour la création
resource.permissions.editVec<String>Rôles autorisés pour l'édition
resource.permissions.deleteVec<String>Rôles autorisés pour la suppression
resource.display.iconString (optionnel)Nom d'icône déclaré
resource.display.paginationusizeEntrées par page (défaut : 25)
resource.extra_contextHashMap<String, String>Clés custom déclarées dans extra: {}
resource.id_typeAdminIdTypeType de la clé primaire (I32, I64, Uuid)

Variables i18n globales (toutes les vues CRUD)

Injectées automatiquement via insert_admin_messages. Le nom de variable Tera est la clé i18n avec les . remplacés par _.

Section base

Variable TeraClé i18n
admin_base_titleadmin.base.title
admin_base_breadcrumbadmin.base.breadcrumb
admin_base_toggleadmin.base.toggle
admin_base_logout_titleadmin.base.logout_title

Section list

Variable TeraClé i18n
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

Section create

Variable TeraClé i18n
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

Section edit

Variable TeraClé i18n
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

Section detail

Variable TeraClé i18n
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

Section delete

Variable TeraClé i18n
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

Vue `login`

Route : GET /admin/login

inject_context n'est pas appelé — les variables resource, resources, resource_key ne sont pas disponibles.

VariableTypeDescription
site_titleStringTitre du site
langStringCode de langue courant
csrf_tokenStringToken CSRF (injecté par l'extracteur de base)
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 (optionnel)StringMessage d'erreur verbatim — injecté uniquement en cas d'échec POST

Clés obligatoires — login

csrf_token, site_title, lang

error est optionnel — présent uniquement après un échec POST. En cas d'échec POST, les messages i18n de la section base sont aussi injectés.

Exemple minimal :

{% 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 %}

Le template login étend admin_template.html (contrat de blocs vides), pas admin_base.html — la sidebar et la topbar ne doivent pas apparaître sur la page de connexion.


Vue `dashboard`

Route : GET /admin/

inject_context n'est pas appelé. La variable current_resource est explicitement None.

VariableTypeDescription
site_titleStringTitre du site
langStringCode de langue courant
resourcesVec<AdminResource>Toutes les ressources enregistrées
resource_countsHashMap<String, u64>Nombre d'entrées par ressource (clé = resource.key)
current_page&strVaut "dashboard"
current_resourceNoneAbsent — aucune ressource sélectionnée
admin_base_*Clés i18n section base (voir ci-dessus)
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

Clés obligatoires — dashboard

resources, resource_counts, current_page

Exemple minimal :

{% 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 %}

Vue `list`

Route : GET /admin/{resource}/list

Pagination

VariableTypeDescription
entriesVec<Value>Enregistrements de la page courante, sérialisés en JSON
totalu64Nombre total d'entrées
pageu64Page courante (commence à 1)
page_countu64Nombre total de pages
has_prevboolIl existe une page précédente
has_nextboolIl existe une page suivante
prev_pageu64Numéro de la page précédente
next_pageu64Numéro de la page suivante
current_page&strVaut "list"

Colonnes

VariableTypeDescription
visible_columnsVec<String>Noms des colonnes à afficher (depuis list_display ou toutes sauf id/password)
column_labelsHashMap<String, String>Label par colonne — vide si list_display non configuré, sinon { "col" => "Label" }

Tri

VariableTypeDescription
sort_byStringColonne de tri active — chaîne vide si aucun tri
sort_dirStringDirection : "asc" ou "desc"
sort_dir_toggleStringDirection opposée à sort_dir — pratique pour les liens d'en-tête

Recherche

VariableTypeDescription
searchStringTerme de recherche courant — chaîne vide si aucune recherche

Filtres sidebar

VariableTypeDescription
filter_valuesHashMap<String, Vec<String>>Valeurs distinctes par colonne de filtre (depuis list_filter)
active_filtersHashMap<String, String>Filtre actif par colonne — "" si aucun filtre actif sur cette colonne
filter_qsStringFragment query string des filtres actifs — à inclure dans les liens de pagination
filter_metaHashMap<String, Object>Pagination sidebar par colonne — voir structure ci-dessous
return_qsStringQuery string complet (tri + recherche + filtres actifs) — à passer aux liens edit/delete pour retrouver l'état de la liste après retour

Note : active_filters est pré-rempli pour toutes les colonnes de list_filter (valeur "" si inactif). Tera lève une erreur si on accède à une clé absente — cette pré-initialisation l'évite. Plusieurs colonnes peuvent avoir une valeur non vide simultanément : les liens de filtre préservent les filtres des autres colonnes.

Structure de filter_meta[colonne]

ChampTypeDescription
current_pageu64Page courante dans la sidebar filtre (commence à 0)
total_pagesu64Nombre total de pages de valeurs distinctes
has_prevboolIl existe une page précédente
has_nextboolIl existe une page suivante
prev_qsStringQuery string complet pour la page précédente — href="?{{ filter_meta[col].prev_qs }}"
next_qsStringQuery string complet pour la page suivante

Clés obligatoires — list

Référencées 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

Les variables i18n de la section list sont listées dans les variables globales ci-dessus.

Exemple minimal :

{% 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>
            {# ajouter les colonnes selon le modèle #}
            <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 %}

Vue `create`

Route : GET /admin/{resource}/create

VariableTypeDescription
form_fieldsFormsFormulaire généré par request.form() — rendu via {% form.field_name %} ou form_fields.html
is_editboolVaut false
m2m_fieldsVec<M2mFieldOptions> (optionnel)Champs many-to-many déclarés via m2m: [...] dans le DSL. Absent si aucun M2M n'est déclaré sur la ressource.

Chaque entrée de m2m_fields expose :

PropriétéTypeDescription
field_nameStringNom du champ — préfixe des inputs (m2m_{field_name}__{id})
labelStringLabel affiché dans le formulaire
choicesVec<(String, String)>Toutes les options disponibles — (id, label)
selectedVec<String>IDs sélectionnés (vide en création)

Exemple Tera :

{% 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 %}

Clés obligatoires — create

form_fields, is_edit

Les variables i18n de la section create sont listées dans les variables globales ci-dessus.

Exemple minimal :

{% 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 gère le token automatiquement pour les formulaires admin #}
    {% 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 %}

Vue `edit`

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

VariableTypeDescription
form_fieldsFormsFormulaire pré-rempli avec les données existantes
is_editboolVaut true
object_idStringID de l'entrée en cours d'édition
m2m_fieldsVec<M2mFieldOptions> (optionnel)Même structure qu'en création — selected contient les IDs déjà associés, les checkboxes correspondantes sont pré-cochées.

Exemple Tera (avec pré-sélection) :

{% 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 %}

Clés obligatoires — edit

form_fields, is_edit, object_id

Les variables i18n de la section edit sont listées dans les variables globales ci-dessus.

Exemple minimal :

{% 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 %}

Vue `detail`

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

VariableTypeDescription
entryValue (optionnel)Enregistrement sérialisé en JSON — absent si get_fn non configurée
object_idStringID de l'entrée

Clés obligatoires — detail

object_id

entry est optionnel — absent si get_fn n'est pas configurée sur la ressource. Les variables i18n de la section detail sont listées dans les variables globales ci-dessus.

Exemple minimal :

{% 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">
    {# itération dynamique sur les champs de l'entrée #}
    {% 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 %}

Vue `delete`

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

VariableTypeDescription
entryValue (optionnel)Enregistrement sérialisé en JSON — absent si get_fn non configurée
object_idStringID de l'entrée à supprimer

Clés obligatoires — delete

object_id

entry est optionnel — absent si get_fn n'est pas configurée sur la ressource. Les variables i18n de la section delete sont listées dans les variables globales ci-dessus.

Exemple minimal :

{% 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 %}

Pattern recommandé pour les variables i18n

{{ admin_create_title }}

Ne pas utiliser | default(value="...") — les variables i18n sont toujours présentes si la langue est configurée.


Surcharger un template

Via .templates() dans le builder, remplace le template pour toutes les ressources :

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

Les templates surchargés ont accès aux mêmes variables que les templates par défaut.

Sous-sections

SectionDescription
Surchargeremplacer le layout ou un composant CRUD
CSRFtoken CSRF, csrf.js, checklist login custom

Revenir au sommaire

SectionDescription
Sommaire templateSommaire templates
SommaireSommaire admin