Skip to main content

ModelAdmin

Trait ModelAdmin 

Source
pub trait ModelAdmin: AdminModel {
    // Provided methods
    fn list_display() -> &'static [&'static str] { ... }
    fn list_filter() -> &'static [&'static str] { ... }
    fn search_fields() -> &'static [&'static str] { ... }
    fn search_index_column() -> Option<&'static str> { ... }
    fn ordering() -> &'static [&'static str] { ... }
    fn list_per_page() -> usize { ... }
    fn readonly_fields() -> &'static [&'static str] { ... }
    fn inlines() -> &'static [Inline] { ... }
    fn fieldsets() -> &'static [Fieldset] { ... }
    fn validate(_model: &Self) -> Result<(), Vec<FieldValidationError>> { ... }
    fn bulk_actions() -> &'static [BulkAction] { ... }
    fn execute_bulk_action<'a>(
        action: &'a str,
        _ids: &'a [i64],
        _db: &'a Db,
        _ctx: &'a BulkActionContext<'a>,
    ) -> Pin<Box<dyn Future<Output = Result<BulkActionResult>> + Send + 'a>> { ... }
}
Expand description

Django-style customisation surface for a registered admin model.

Every type that implements AdminModel gets a default impl via the blanket below. Override the methods you care about; everything else inherits sensible defaults.

Provided Methods§

Source

fn list_display() -> &'static [&'static str]

Columns shown on the list page, in order. Default: every field declared on AdminModel::FIELDS.

Returning &[] means “use the model’s full field list” — the list page expands the empty default into M::FIELDS. Any non-empty slice replaces the defaults verbatim.

Source

fn list_filter() -> &'static [&'static str]

Columns offered as filter chips in the sidebar. Default: none.

Source

fn search_fields() -> &'static [&'static str]

Columns searched by the list-page search box (case-insensitive substring match). Default: none.

Source

fn search_index_column() -> Option<&'static str>

Name of a Postgres tsvector column to use for full-text search instead of the framework’s default ILIKE OR-loop across search_fields(). When Some("search_vector"), the list-page WHERE clause switches to <col> @@ websearch_to_tsquery('english', $N) — operators keep typing in the same search box; the index does the work. Maintain the tsvector yourself (a generated column or a trigger; the framework doesn’t write to it). Default: None (the existing ILIKE path).

// 1. Add a generated tsvector column in a migration:
//    ALTER TABLE posts ADD COLUMN search_vector tsvector
//      GENERATED ALWAYS AS (to_tsvector('english',
//        coalesce(title,'') || ' ' || coalesce(body,''))) STORED;
//    CREATE INDEX posts_search_idx ON posts USING gin(search_vector);
//
// 2. Opt in from ModelAdmin:
fn search_index_column() -> Option<&'static str> {
    Some("search_vector")
}
Source

fn ordering() -> &'static [&'static str]

Default ordering. -foo for foo DESC, foo for foo ASC. Multiple entries → multi-column ORDER BY in slice order. Default: ["-id"] (newest first).

Source

fn list_per_page() -> usize

Rows per page on the list view. Default: 50.

Source

fn readonly_fields() -> &'static [&'static str]

Field names rendered as disabled on the change form. The browser does not submit disabled fields, so the framework transparently re-injects the existing row value into the form before calling from_form — readonly columns are persisted unchanged. Applies to edit only; on the add form the listed fields stay editable so the project can supply their initial value. Default: none.

Source

fn inlines() -> &'static [Inline]

Related-children sections rendered below the change form (the parent edit page). Default: empty — no inlines. Each entry references a registered child model by its SINGULAR_NAME and names the FK column on the child that points at the parent. The framework fetches up to max_rows matching rows, renders them as a table of click-through edit links + a per-row Delete link, and appends “Add new {child}” / “View all” affordances.

v1 surface — read-only. Inline rows are display + click-through; in-page editing of inline rows is a future iteration. Adding a child still routes through the child’s normal new-form; the parent FK is filled by the operator. See Inline.

Source

fn fieldsets() -> &'static [Fieldset]

Field grouping on the change form. Default: empty — fall back to the framework’s name heuristic (Default / System / Advanced). A non-empty return replaces the heuristic entirely: each Fieldset renders as one titled section in the order returned, and the fields inside it render in the order listed. Fields that exist on the model but are not referenced by any fieldset get appended to a trailing “Other” section so the form stays complete; misspelt names with no matching field are silently dropped.

Source

fn validate(_model: &Self) -> Result<(), Vec<FieldValidationError>>

Per-row business-rule validation, called by the framework after AdminModel::from_form succeeds but BEFORE the SQL insert / update fires. Default is Ok(()) — projects opt in by overriding. Synchronous: validation can’t query the DB; database-shape errors (UNIQUE violations, FK gone) flow through the existing constraint-translation path automatically and aren’t this hook’s concern.

Returning Err short-circuits both create and update — the row never reaches Postgres. Each FieldValidationError either attaches to a specific field (rendered inline next to that input, with aria-invalid) or surfaces as a global rule violation (rendered in the form’s error banner).

Common shape:

fn validate(model: &Self) -> std::result::Result<(), Vec<FieldValidationError>> {
    let mut errs = Vec::new();
    if model.start_date > model.end_date {
        errs.push(FieldValidationError::field(
            "end_date",
            "End date must not be before the start date.",
        ));
    }
    if errs.is_empty() { Ok(()) } else { Err(errs) }
}
Source

fn bulk_actions() -> &'static [BulkAction]

Custom bulk actions surfaced as extra buttons in the list-view bulk bar (next to the framework’s built-in Delete). Default: none.

BulkAction is metadata only — pair this method with an ModelAdmin::execute_bulk_action override that matches on name and applies the work. The framework’s default dispatcher returns a clear BadRequest for any name it doesn’t recognise, so a forgotten implementation surfaces as an error page rather than a silent no-op.

Source

fn execute_bulk_action<'a>( action: &'a str, _ids: &'a [i64], _db: &'a Db, _ctx: &'a BulkActionContext<'a>, ) -> Pin<Box<dyn Future<Output = Result<BulkActionResult>> + Send + 'a>>

Run a project-defined bulk action against ids. Called once per POST /admin/:model/bulk/:name submission with the full id list — the implementation chooses between a single bulk SQL update and a per-row loop.

The framework wraps this call with one [audit::record] emission per submission (using BulkActionContext.actor, correlation_id, and the BulkActionResult outcome). Projects don’t need to audit the dispatch envelope themselves; any business-level audit emissions inside the action body are still the project’s call.

Two channels for “something went wrong”:

  • Action itself failed — return Err(...). The framework surfaces it as a 4xx/5xx page and still writes an audit row for the attempt.
  • Some rows failed — return Ok(BulkActionResult) with a populated failed list. The framework records a partial-success audit row and renders the per-id failure summary on the next request.

The framework’s built-in delete action does not flow through this method. It runs through the cascade-aware /bulk_delete route. Override delete semantics on the underlying crate::Model / handler layer if you need custom delete behaviour.

The default implementation returns a structured error so a declared-but-unimplemented action surfaces clearly:

use std::future::Future;
use std::pin::Pin;
use rustio_admin::{
    BulkAction, BulkActionContext, BulkActionResult, Db, ModelAdmin, Result,
};

impl ModelAdmin for Loan {
    fn bulk_actions() -> &'static [BulkAction] {
        &[BulkAction {
            name: "mark_overdue",
            label: "Mark overdue",
            destructive: false,
            confirm: true,
            permission: None,
        }]
    }

    fn execute_bulk_action<'a>(
        action: &'a str,
        ids: &'a [i64],
        _db: &'a Db,
        _ctx: &'a BulkActionContext<'a>,
    ) -> Pin<Box<dyn Future<Output = Result<BulkActionResult>> + Send + 'a>> {
        Box::pin(async move {
            match action {
                "mark_overdue" => Ok(BulkActionResult::ok(ids.len())),
                _ => Ok(BulkActionResult::default()),
            }
        })
    }
}

Dyn Compatibility§

This trait is not dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementors§