Chapter 14 — Doing less work
This chapter collects the shortcuts that collapse a multi-step setup or config write into a single call. Each recipe below maps a 4–5 step chain to one line.
14.1 manage inspectdb — adopt rustango against an existing DB
What: Connects to DATABASE_URL, walks information_schema,
emits #[derive(Model)] source for every base table — Django's
inspectdb shape. Pipes to a file the user reviews + edits.
When: You have an existing Postgres schema (legacy app, hand-rolled migrations from another framework, prod DB you want to read into a new admin) and don't want to retype every model by hand.
API: migrate::inspectdb.
# Every public-schema table to stdout
cargo run -- inspectdb
# Single table
cargo run -- inspectdb --table users
# Different schema
cargo run -- inspectdb --schema reporting
# Pipe to a reviewable file
cargo run -- inspectdb > src/legacy/models.rs
Coverage: PRIMARY KEY → primary_key; SERIAL/IDENTITY →
Auto<T>; NOT NULL → required, nullable → Option<T>;
varchar(N) → max_length = N; FK references → fk = "...";
DEFAULT values echoed (typecast suffix stripped).
Verified by: tests/inspectdb_live.rs — full Author/Post
fixture round-trip, FK + uuid + jsonb, unknown-schema friendly
empty comment.
14.2 manage wizard — interactive one-call setup
What: Five opt-in prompts: scaffold app → init tenancy →
migrate registry → create operator → create tenant + first
superuser. Each step is [Y/n]-skippable. Defaults echoed
in the prompt; pressing Enter accepts.
When: First-run setup. Replaces the chain new tenancy users
otherwise have to learn (init-tenancy → migrate-registry →
create-operator → create-tenant → create-superuser).
API: tenancy::manage::wizard.
$ cargo run -- wizard # alias: cargo run -- init
rustango wizard — interactive setup
===================================
Scaffold a new app? [Y/n]
App name (default: blog): blog
Initialize tenancy? [Y/n]
Apply registry migrations now? [Y/n]
Create an operator account? [Y/n]
Operator username (default: admin): admin
Operator password: hunter2
Create a tenant? [Y/n]
Tenant slug (default: acme): acme
...
Verified by: 4 unit tests on the prompt helpers (with
Cursor-injected input) + tests/wizard_live.rs for the
dispatcher wiring.
14.3 HTML CBV: bulk actions + delete-confirmation + FK display
template_views::ListView has three Django-admin-shape flags.
They stack:
use rustango::template_views::{DeleteView, ListView};
ListView::for_model(Item::SCHEMA)
.bulk_actions(true) // built-in delete_selected
.with_delete_confirmation(true) // two-step confirm before bulk DELETE
.with_fk_display(true) // FK columns auto-resolve to display
.tenant_router("/items", tera.clone())
bulk_actions(true) + tenant_action(...)
Mounts POST <prefix> alongside the GET list. Built-in
delete_selected handler always available; user actions stack
via .tenant_action("publish_selected", "Publish", handler).
Form posts action=<name> + repeated _selected_action=<pk>
fields.
Template shape (the form lives inside the list page):
<form method="post" action="/items">
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
<select name="action">
{% for a in bulk_actions %}
<option value="{{ a.name }}">{{ a.label }}</option>
{% endfor %}
</select>
{% for row in object_list %}
<input type="checkbox" name="_selected_action" value="{{ row.id }}">
{% endfor %}
<button>Apply</button>
</form>
Note:
handle_list/handle_list_tenantstamp the CSRF token into the Tera context, so the bulk-action form carries a valid_csrfand POSTs aren't rejected under CSRF-protected setups. Verified bytests/template_views_bulk_actions_live::list_get_stamps_csrf_token_into_context.
with_delete_confirmation(true) — bulk-confirm page
When on, the first POST with action=delete_selected renders
<table>_confirm_bulk_delete.html instead of running the
DELETE. Context: pks (list of strings), objects (full row
data so the template can show what will be deleted),
csrf_token. The confirm form re-submits with confirmed=true
which short-circuits the render and runs the DELETE → 303 to
the list.
with_fk_display(true) — resolve FK ints to display
For every FK column on the schema, runs one batched
SELECT pk, <display_field> FROM <target> WHERE pk = ANY(...)
per page and stamps <column>_display into each row's JSON.
Templates then render:
<td>{{ row.region_id_display | default(value=row.region_id) }}</td>
→ shows "americas" instead of 1.
Verified by: 6 live tests in
tests/template_views_bulk_actions_live.rs (built-in delete +
custom action + 303 redirect + 400 on empty-selection +
confirm-page renders).
14.4 Admin pager SELECT COUNT(*) skip
What: On tables in the millions of rows, the admin's
SELECT COUNT(*) FROM <table> WHERE <filters> runs every page
render and takes seconds even with indexes. Two opt-outs:
admin::Builder::new(pool)
.skip_count_for(["audit_log", "events"]) // per-table opt-in
.build()
Or per-request: ?count=skip (also 0 / false / no) on
any list URL. Pager renders "Page N" + prev/next driven by
has-next-page detection (we fetch page_size + 1 and trim).
API: admin::Builder::skip_count_for.
14.5 Settings-driven logging
What: Cli::with_logging() drives tracing-subscriber
from a [logging] TOML section.
# config/dev_settings.toml
[logging]
level = "info,sqlx=warn"
format = "pretty"
with_line_numbers = true
# config/prod_settings.toml
[logging]
level = "info"
format = "json"
file_dir = "/var/log/myapp"
file_prefix = "app"
file_rotation = "daily"
rustango::manage::Cli::new()
.with_settings_from_env()
.with_logging()
.api(urls::api())
.run().await
access_log middleware emits per-request lines like
method=GET path=/items status=200 duration_ms=43 ip=192.168.65.1.
The client IP is captured via ConnectInfo — both manage.rs and
server/builder.rs serve with
into_make_service_with_connect_info::<SocketAddr>(), so the access
log records the real peer address rather than "-".
For projects behind a reverse proxy:
AccessLogLayer::default().trust_proxy_headers(true)
honors X-Forwarded-For (leftmost = original client) → fall
back to X-Real-IP → fall back to ConnectInfo. Off by default
(both headers are spoofable by direct clients).
API: config::LoggingSettings,
logging::Setup::from_settings,
access_log::AccessLogLayer::trust_proxy_headers.
14.6 make:viewset auto-detects tenancy
What: cargo run -- make:viewset Foo --model Bar reads the
project's Cargo.toml. If the tenancy feature is enabled on
the rustango dep, emits a tenant_router(...) scaffold;
otherwise the static-pool #[derive(ViewSet)] shape. Override
with --no-tenant.
$ cargo run -- make:viewset NewProductViewSet --model Product
make:viewset: auto-detected tenancy mode from Cargo.toml (pass `--no-tenant` to override)
wrote src/new_product_view_set.rs
add `mod new_product_view_set;` to src/main.rs (or `pub mod ...;` to src/lib.rs)
14.7 Other niceties worth knowing
Cli::with_welcome()skips (with atracing::warn!) instead of panicking when yoururls::api()already routesGET /. The welcome page has a cards-grid layout, a version pill, and anicon.pngbrand mark.- Admin
AdminError::Internalredacts DB errors before responding; the raw text goes totracing::error!with acorrelation_idthe user can quote in a bug report. - The admin pager total matches the visible rows when
?q=...is set — the count honors the search filter.