Part 6: The Admin and Static Files

The application now has user-owned polls. In this part you will add the Reinhardt admin route at /admin/ and wire the static-file pipeline that serves the WASM bundle, CSS, images, and admin assets.

The admin registration is useful for exercising model metadata, route wiring, and the admin API surface. It is not a replacement for the owner-checked server functions from Part 5.

Register Poll Models with the Admin

Create src/apps/polls/server/admin.rs. The example registers Question first:

use crate::apps::polls::models::{Choice, Question};
use reinhardt::admin;

#[admin(model,
    for = Question,
    name = "Question",
    list_display = [id, question_text, pub_date, author_id],
    fields = [question_text, author_id],
    list_filter = [pub_date],
    search_fields = [question_text],
    ordering = [(pub_date, desc)],
    readonly_fields = [id, pub_date],
    list_per_page = 25,
    permissions = allow_all,
)]
pub struct QuestionAdmin;

Then register choices:

#[admin(model,
    for = Choice,
    name = "Choice",
    list_display = [id, question_id, choice_text, votes],
    fields = [question_id, choice_text, votes],
    list_filter = [question_id],
    search_fields = [choice_text],
    ordering = [(id, asc)],
    readonly_fields = [id],
    list_per_page = 50,
    permissions = allow_all,
)]
pub struct ChoiceAdmin;

QuestionAdmin includes author_id because Part 5 made question ownership part of the schema.

Part 2 already exposed the polls app from src/apps.rs:

pub mod polls;

Now expose the server module from the polls app parent, and expose admin from src/apps/polls/server.rs so the site configuration can import it:

#[cfg(server)]
pub mod server;
pub mod admin;
pub mod serializers;

Configure the Admin Site

Create src/config/admin.rs and register the app admins:

use crate::apps::polls::server::admin::{ChoiceAdmin, QuestionAdmin};
use crate::config::settings::get_settings;
use reinhardt::HasCoreSettings;
use reinhardt::admin::AdminSite;

pub fn configure_admin() -> AdminSite {
    let mut site = AdminSite::new("Polls Tutorial Admin");
    let settings = get_settings();

    site.configure(|config| {
        config.site_title = "Polls Tutorial - Admin".into();
        config.site_header = "Polls Administration".into();
        config.list_per_page = 25;
    });
    site.set_jwt_secret(settings.core().secret_key.as_bytes());

    site.register("Question", QuestionAdmin)
        .expect("Failed to register QuestionAdmin");
    site.register("Choice", ChoiceAdmin)
        .expect("Failed to register ChoiceAdmin");

    site
}

The site configuration is metadata. Database access is supplied later through DI when routes are mounted.

Expose the configuration module from src/config.rs:

#[cfg(server)]
pub mod admin;

Mount Admin Routes

In src/config/urls.rs, import the admin helpers and your site configuration:

#[cfg(server)]
use reinhardt::admin::{admin_routes_with_di, admin_static_routes};
#[cfg(server)]
use std::sync::Arc;

#[cfg(server)]
use crate::config::admin::configure_admin;

Mount /admin/ and /static/admin/:

#[cfg(server)]
let router = {
    let admin_site = Arc::new(configure_admin());
    let (admin_router, admin_di) = admin_routes_with_di(admin_site);
    router
        .mount("/admin/", admin_router)
        .mount("/static/admin/", admin_static_routes())
        .with_di_registrations(admin_di)
};

admin_routes_with_di returns both a router and DI registrations. Keep the with_di_registrations(admin_di) call; admin handlers need the project's database connection.

Register WASM Build Artifacts

The pages runtime needs to find the WASM bundle produced by wasm-pack. Register dist-wasm/ in src/config/wasm.rs:

use reinhardt::reinhardt_apps::AppStaticFilesConfig;

inventory::submit! {
    AppStaticFilesConfig {
        app_label: "tutorial-wasm",
        static_dir: "dist-wasm",
        url_prefix: "",
    }
}

The generated Pages template uses <crate-name>-wasm here, so a project named tutorial registers tutorial-wasm. This tells collectstatic that the generated WASM artifacts are application static files.

Understand the Static Directories

The example uses several directories with different responsibilities:

static/
+-- css/style.css          # hand-written project CSS
+-- images/poll-icon.svg   # project static asset
dist-wasm/                 # wasm-pack output
dist/                      # runserver static directory for dev
staticfiles/               # collectstatic output
static/admin/              # generated admin assets

dist/, dist-wasm/, staticfiles/, and static/admin/ are generated and gitignored.

The core cargo-make tasks are:

[tasks.collectstatic]
description = "Collect static files into STATIC_ROOT"
command = "cargo"
args = ["run", "--bin", "manage", "collectstatic"]
[tasks.wasm-bindgen-dev]
description = "Generate WASM bindings using wasm-pack (debug mode)"
dependencies = ["wasm-compile-dev"]
command = "wasm-pack"
args = [
    "build",
    "--target", "web",
    "--out-dir", "dist-wasm",
    "--dev",
    "--no-typescript"
]
[tasks.dev]
description = "Start development environment (checks, builds WASM, runs server with auto-reload)"
dependencies = ["clean-cache", "quality", "wasm-build-dev", "runserver"]

Follow the Dev Build Path

cargo make wasm-build-dev runs wasm-pack, then calls scripts/wasm-build-dev.sh:

echo "Running collectstatic..."
cargo run --bin manage collectstatic --no-input
mkdir -p dist
find dist-wasm -maxdepth 1 -type f \( -name '*.js' -o -name '*.wasm' -o -name '*.d.ts' \) -exec cp -f {} dist/ \;
echo "WASM build and collectstatic completed"

The final cargo make dev step starts the server with pages enabled and avoids rebuilding over the just-created bundle:

cargo run --bin manage -- runserver --with-pages --no-override-wasm

Release-mode local testing uses:

cargo make dev-release

That path runs wasm-build-release, collectstatic, migrate, and the release-mode dev server script.

Checkpoint

Run the admin-enabled app:

cargo make dev

Open http://127.0.0.1:8000/admin/. The route should load the embedded admin shell and /static/admin/ assets. If the admin WASM SPA has not been built, the shell intentionally shows a placeholder message; the tutorial checkpoint is route/static wiring plus admin registration, not browser-based model editing.

Before continuing:

  • QuestionAdmin and ChoiceAdmin are registered in configure_admin().
  • /admin/ is mounted with admin_routes_with_di.
  • /static/admin/ is mounted with admin_static_routes().
  • dist-wasm/ is registered through AppStaticFilesConfig.
  • cargo make dev builds WASM before starting the pages server.