Rustango docs
← Cookbook

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-tenancymigrate-registrycreate-operatorcreate-tenantcreate-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_tenant stamp the CSRF token into the Tera context, so the bulk-action form carries a valid _csrf and POSTs aren't rejected under CSRF-protected setups. Verified by tests/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 a tracing::warn!) instead of panicking when your urls::api() already routes GET /. The welcome page has a cards-grid layout, a version pill, and an icon.png brand mark.
  • Admin AdminError::Internal redacts DB errors before responding; the raw text goes to tracing::error! with a correlation_id the 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.