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§
Sourcefn list_display() -> &'static [&'static str]
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.
Sourcefn list_filter() -> &'static [&'static str]
fn list_filter() -> &'static [&'static str]
Columns offered as filter chips in the sidebar. Default: none.
Sourcefn search_fields() -> &'static [&'static str]
fn search_fields() -> &'static [&'static str]
Columns searched by the list-page search box (case-insensitive substring match). Default: none.
Sourcefn search_index_column() -> Option<&'static str>
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")
}Sourcefn ordering() -> &'static [&'static str]
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).
Sourcefn list_per_page() -> usize
fn list_per_page() -> usize
Rows per page on the list view. Default: 50.
Sourcefn readonly_fields() -> &'static [&'static str]
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.
Sourcefn inlines() -> &'static [Inline]
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.
Sourcefn fieldsets() -> &'static [Fieldset]
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.
Sourcefn validate(_model: &Self) -> Result<(), Vec<FieldValidationError>>
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) }
}Sourcefn bulk_actions() -> &'static [BulkAction]
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.
Sourcefn 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>>
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 populatedfailedlist. 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".