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 admin feature does not read the registry. Its panel lists every db.Model subclass 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.

  • scope filters 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_defaults are 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


Back to top

splent. Distributed by an LGPL license v3. Contact us: drorganvidez@us.es