Runique Admin
Documentation / Runique Admin / Admin template override

Admin template override

The admin's look changes at three levels, lightest first:

How a page is made

admin/admin_template.html   ← contract: <head>, CSRF meta, csrf.js, modal
        ↓ extends
admin_base.html             ← layout: stylesheets, sidebar, top bar
        ↓ extends
admin/list.html, admin/create.html …   ← the pages

Each page is made of blocks. In Runique's templates, a replaceable block is written {% dev_block "name" %}. At boot, Runique turns each {% dev_block %} into an {% include %} of your template when you declared one, of its own otherwise.


Where to declare

For every model: in with_admin, through .templates(|t| …) (type AdminTemplate).

use runique::admin::template::{Block, GlobalBlock, Page};

.with_admin(|a| a
    .templates(|t| t
        .block(Block::ListTable, "my_theme/table.html")
        .global_block(GlobalBlock::Sidebar, "my_theme/sidebar.html")
    )
    .site(site)
)

For one model: on its ModelAdmin, through .templates(|t| …) (type ModelTemplates).

fn article() -> ModelAdmin<article::Entity> {
    ModelAdmin::new()
        .templates(|t| t.block(Block::ListTable, "articles/table.html"))
}

Priority: what the model declares, then what is declared for every model, then Runique. A block's HTML and its stylesheet resolve separately: a model can replace a block's HTML and keep the stylesheet declared for every model.

Paths:

  • a template is relative to your templates folder (TEMPLATES_DIR) and must end in .html or .xml (Tera escapes only those extensions);
  • a stylesheet is relative to your static folder (STATICFILES_DIRS), served the way {% static %} serves it, with its integrity hash.

Override a block

Blocks come in two kinds:

KindTypeReplaceableExamples
Block of a model's pagesBlockfor every model or for oneListTable, FormActions, DeleteWarning
Block of the layout, or of a page about no modelGlobalBlockfor every model onlySidebar, Topbar, DashboardTable

The full list of keys, with the page showing each block and Runique's template to copy as a starting point, is in the block reference.

The template only

The block takes your HTML; Runique's stylesheet stays loaded.

.templates(|t| t.block(Block::DeleteWarning, "my_theme/delete_warning.html"))
{# templates/my_theme/delete_warning.html #}
<div class="admin-warning">
  <p>
    <strong>{{ admin_delete_warning_title }}</strong>
    {{ resource.title }} #{{ object_id }} will be deleted for good.
  </p>
</div>

A block's template gets the context of the page including it (here the delete page's: resource, object_id, the admin_delete_* translations…). See Context keys. The safest start is Runique's template (runique/templates/admin/blocks/…), modified.

The CSS only

The block keeps Runique's HTML and loads your stylesheet.

.templates(|t| t.block_stylesheet(Block::ListTable, "css/admin/table.css"))

Two cases:

  • the block has a stylesheet in Runique (ListTable, DetailFields, DetailGroups, DashboardTable): yours replaces it. It must then define everything for that block, not only what you change;
  • the block has none: yours is added, after Runique's.

Both

The two declarations add up:

.templates(|t| t
    .block(Block::ListTable, "my_theme/table.html")
    .block_stylesheet(Block::ListTable, "css/admin/table.css")
)

For a block of the layout or of a page about no model, the same three cases with global_block and global_block_stylesheet:

.templates(|t| t
    .global_block(GlobalBlock::Sidebar, "my_theme/sidebar.html")
    .global_block_stylesheet(GlobalBlock::Sidebar, "css/admin/sidebar.css")
)

Override a whole page

For every model

MethodPage
.with_dashboard(path)dashboard
.with_list(path)list
.with_htmx(path)list fragment returned to HTMX requests (search, sort, pagination)
.with_create(path)create
.with_edit(path)edit
.with_detail(path)detail
.with_delete(path)delete confirmation
.with_login(path)login
.with_history(path)history
.with_history_diff(path)one history entry

The rights page and the group transfer page can't be replaced.

.with_htmx only touches the HTMX response: the full list page still includes Runique's fragment. Replacing one without the other renders the list differently depending on whether it is loaded or refreshed.

For one model

ModelAdmin::new()
    .templates(|t| t.page(Page::List, "articles/list.html"))

Only Page::List, Page::Create, Page::Edit, Page::Detail and Page::Delete can be replaced per model. Another page refuses the boot. The detail page can also be replaced by a route (declared admin routes); both at once refuse the boot: the route would win and the template never render.

A page's template

Extending admin_base.html keeps Runique's layout (stylesheets, sidebar, top bar, CSRF):

{# templates/articles/list.html #}
{% extends "admin_base.html" %}

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

{% block content %}
<h1>{{ resource.title }} ({{ total }})</h1>
{% for entry in entries %}
  <p><a href="{{ entry.links.detail }}">#{{ entry.id }}</a></p>
{% endfor %}
{% if resource_links.create %}
  <a href="{{ resource_links.create }}" class="btn btn-primary">{{ admin_list_btn_create }}</a>
{% endif %}
{% endblock %}

A replaced page can still include Runique's blocks with {% dev_block "name" %}: they then follow the declared block overrides.

A replaced page's stylesheet

By default your page gets Runique's stylesheets (layout + the page's blocks). With page_stylesheet, it loads only Runique's variables, then yours:

.templates(|t| t
    .with_list("my_theme/list.html")
    .page_stylesheet(Page::List, "css/admin/list.css")
)
// or for one model:
ModelAdmin::new().templates(|t| t
    .page(Page::List, "articles/list.html")
    .page_stylesheet(Page::List, "css/admin/articles_list.css")
)

page_stylesheet is read only when the page is replaced.


Points of attention

  • The stylesheets are written by admin_base.html's {% block extra_css %}. If your page redefines extra_css, call {{ super() }}, otherwise no stylesheet is loaded at all. Same for extra_js (admin.js, the modal, the theme).
  • The CSRF meta and csrf.js come from admin/admin_template.html. A page extending nothing must write them itself (see Examples).
  • Links come from the context (nav.*, resource_links.*, entry.links.*, form_action, cancel_url): don't build URLs by hand, the admin prefix and the rights are already applied. A missing link means the user has no right to it; the gate checks again on the target page anyway.
  • {{ var }} is escaped, {{ var | safe }} is not: use safe only on HTML you produce yourself.
  • A file of your templates with the same name as one of Runique's (for instance admin/blocks/list/table.html) replaces it for everyone, with no declaration and no check. Keep your admin templates under another folder (my_theme/…).

Current limits

  • No global theme stylesheet: to change the CSS variables on every page, go through a block shown everywhere, for instance .global_block_stylesheet(GlobalBlock::Topbar, "css/admin/theme.css") (every page but the login). See CSS theme.
  • .with_htmx is global: no per-model list fragment.
  • A missing page template or stylesheet isn't reported at boot (see above).

Sub-sections

SectionDescription
Block referenceBlock keys, the pages showing them, CSS variables
ExamplesComplete cases: block, CSS, page, standalone page
CSS classesClasses used by Runique's templates
Context keysVariables injected by the backend into each template