Admin resources
A registry where a content feature describes how its model appears in the admin panel, with a label, an icon, a menu group, list columns and field widgets. It is the analogue of WordPress register_post_type, except that the data stays in each feature’s own typed table.
Table of contents
What it is
A content feature owns a real SQLAlchemy model in its own table (Event, TeamMember). The registry holds metadata only. register_admin_resource(Model, ...) says how to administer that model, so an admin panel can build a grouped menu and CRUD screens from it instead of listing every db.Model subclass.
| WordPress | SPLENT | |
|---|---|---|
| Declaration | register_post_type() |
register_admin_resource() |
| Storage | Shared wp_posts and post_meta |
The feature’s own table, typed columns |
| Source of truth | WordPress core | The feature’s own model |
| Back office | wp-admin | The admin feature |
The
adminfeature does not read the registry. Its panel lists everydb.Modelsubclass and serves generic CRUD screens under/admin/<model_name>. A registration is stored and the query functions return it, but labels, groups, icons, columns and widgets have no visible effect in/admin.
No shipped feature registers a resource. events, team, projects, tools and about ship their own admin screens under /admin/<type> and link them through layout.authenticated_sidebar (see Content features).
Registering a resource
Call register_admin_resource from the feature’s init_feature(app), next to its service registration. Here is how Event would look through the registry (illustrative).
# A content feature's __init__.py (illustrative)
from splent_framework.admin import register_admin_resource
from splent_io.splent_feature_events.models import Event
from splent_io.splent_feature_events.services import EventsService
def init_feature(app):
register_service(app, "EventsService", EventsService)
register_admin_resource(
Event,
name="event",
label="Event",
label_plural="Events",
icon="calendar",
group="Programme",
order=10,
list_columns=["title", "kind", "room", "starts_at"],
field_widgets={
"summary": "textarea",
"description": "richtext",
"image": "image",
"link": "url",
"kind": "select",
"slug": "slug",
"starts_at": "datetime",
"ends_at": "datetime",
"published": "bool",
},
feature="events",
)
A second feature registers its own model in its own group, TeamMember under "People", say.
Registration is keyed by name. Registering the same name again replaces the entry, so a refinement feature can curate another feature’s resource.
The registry is a module-level singleton in splent_framework.admin.registry, filled while the features load and read-only while requests are served, like template hooks. It is not safe for concurrent writes, so never call register_admin_resource() from a request handler.
AdminResource fields
register_admin_resource builds and stores an AdminResource, with one field per keyword argument.
| Field | Default | Meaning |
|---|---|---|
model |
(required) | The SQLAlchemy model class |
name |
model.__name__.lower() |
URL-safe key, such as "event", and the registry key |
label |
model.__name__ |
Singular label, such as "Event" |
label_plural |
f"{label}s" |
Plural label shown in the menu |
icon |
"file" |
Feather icon name for the menu, such as "calendar" |
group |
"Content" |
Menu section the resource is filed under |
order |
100 |
Sort order within the group |
list_columns |
None |
Columns shown in the list view. None lets the admin choose |
search_fields |
[] |
Columns the admin search box queries |
field_widgets |
{} |
Widget hint per column |
readonly_fields |
[] |
Columns shown but not editable |
scope |
{} |
Query filter, see below |
create_defaults |
{} |
Column values forced on create, see below |
feature |
None |
Short name of the owning feature |
AdminResource.model_name is the model’s class name (model.__name__), the key CRUD routes resolve by.
Widgets
field_widgets maps a column to a widget hint for a form renderer, and a column without a hint is meant to get a default derived from its type. No renderer reads the hints.
| Widget | Use |
|---|---|
text |
Single-line string |
textarea |
Multi-line plain text |
richtext |
WYSIWYG or HTML body |
number |
Numeric input |
bool |
Checkbox |
date |
Date picker |
datetime |
Date and time picker |
select |
Enum or fixed choices |
fk |
Foreign key, a picker for the related record |
image |
Media id or upload |
url |
URL input |
slug |
Slug input |
color |
Colour picker |
scope and create_defaults
These let one table back several menu entries, each with an explicit, typed filter.
-
scopefilters the list view. Registering one model twice with different scopes describes two menu entries over one table.register_admin_resource(Page, name="event_page", label="Event page", scope={"collection": "event"}) register_admin_resource(Page, name="info_page", label="Info page", scope={"collection": "info"}) -
create_defaultsare values forced on create, so a record made through a scoped entry lands inside its scope.register_admin_resource(Page, name="event_page", label="Event page", scope={"collection": "event"}, create_defaults={"collection": "event"})
A resource with empty scope and create_defaults, the common case, describes its whole table. A model registered as several scoped resources is resolved by resource name with get_admin_resource(name), because a lookup by model cannot tell them apart.
Reading the registry
| Function | Returns |
|---|---|
get_admin_resource(name) |
One resource by name, or None |
get_admin_resource_for_model(model_name) |
The resource whose model class name matches, or None. Also None, with a warning in the log, when several resources use that model |
get_admin_resources() |
Every resource keyed by name, in insertion order |
get_admin_groups() |
Resources bucketed by menu group, each sorted by (order, label) |
clear_admin_resources() |
Empties the registry, for test teardown |
get_admin_groups() returns the shape a grouped sidebar needs, one entry per group with its resources in order.
def get_admin_groups() -> dict[str, list[AdminResource]]:
groups: dict[str, list[AdminResource]] = {}
for res in sorted(_resources.values(), key=lambda r: (r.order, r.label)):
groups.setdefault(res.group, []).append(res)
return groups
See also
- Template hooks. The same registry pattern for UI
- Content features. How shipped features build their own admin screens
- Service locator. The other thing features register from
init_feature