Feature settings
How a feature declares its admin-editable settings, so every feature that injects UI becomes configurable from the admin — without writing a single per-feature settings panel.
Table of contents
- What it is
- SPL rationale: configurability is derived, not hand-built
- The
register_settingsAPI - Reading values:
get_config - Key namespacing
- The generic admin panel
- See also
What it is
Feature settings live in a framework module —
splent_framework.settings.settings_schema — that lets each feature declare a
typed schema of its admin-editable settings from inside its
init_feature(app). The settings feature then renders one generic admin
panel for every declared schema and persists the values through
SettingsService. There is no per-feature panel code and no ad-hoc config
module: declare the schema, get a panel plus typed reads.
This is the SPL way to make any feature that injects UI configurable. The slider
declares scope, autoplay, interval, overlay, and caption_color and
gets a settings panel for free; its layout.hero hook reads get_config("slider")
to decide whether to render (e.g. scope=home → homepage only) and how to
style the carousel.
SPL rationale: configurability is derived, not hand-built
In a Software Product Line the admin’s configuration surface is a point of variability resolved at derivation time from the set of installed features:
- Install a feature that registers a schema → its panel appears under
/admin/settings. - Remove it and re-derive the product → its panel disappears.
Two products built from different feature selections expose different settings panels for free, because the admin surface is exactly the union of the schemas its installed features registered. Nobody writes (or maintains) a settings form per feature, and nobody wires a config table by hand.
The register_settings API
Import it from splent_framework.settings.settings_schema and call it inside
init_feature(app). This is the slider’s full schema:
def init_feature(app):
from splent_framework.settings.settings_schema import register_settings
register_settings(
"slider",
"Slider",
[
{
"key": "scope",
"type": "select",
"default": "home",
"label": "Show on",
"options": [("home", "Home page only"), ("all", "All pages")],
"help": "Where the carousel appears.",
},
{"key": "autoplay", "type": "bool", "default": "1", "label": "Autoplay"},
{"key": "interval", "type": "int", "default": "6", "label": "Interval (seconds)"},
{
"key": "overlay",
"type": "bool",
"default": "1",
"label": "Shade the bottom half",
"help": "Darkens the lower half so the white caption stays legible.",
},
{"key": "caption_color", "type": "color", "default": "#ffffff", "label": "Caption colour"},
],
icon="image",
)
| Parameter | Required | Description |
|---|---|---|
feature |
yes | Stable key, e.g. "slider". Also the panel URL segment and the setting-key namespace. |
title |
yes | Human title shown in the panel and sidebar, e.g. "Slider". |
fields |
yes | List of field dicts (see below). |
icon |
no | Feather icon name for the sidebar entry. Defaults to "sliders". |
Field dicts
Each field is a dict {key, type, default, label, help?, options?}:
| Field key | Required | Description |
|---|---|---|
key |
yes | Short field key, e.g. "autoplay". Namespaced automatically (see below). |
type |
yes | One of bool, int, text, color, select. |
default |
yes | Default value, used until the admin saves one. |
label |
yes | Field label in the form. |
help |
no | Help text shown under the field. |
options |
for select |
List of (value, label) pairs. |
Field types
| Type | Renders as | Stored / cast to |
|---|---|---|
bool |
checkbox | True / False |
int |
number input | int (falls back to the default, else 0) |
text |
text input | string |
color |
colour picker | validated #rrggbb string (else the default) |
select |
dropdown from options |
the chosen value string |
Reading values: get_config
A feature reads its current, typed settings with get_config(feature). It
returns a dict keyed by the short field key (not the namespaced key), with
each value already cast to its type, falling back to the declared default:
from splent_framework.settings.settings_schema import get_config
cfg = get_config("slider")
# {"scope": "home", "autoplay": True, "interval": 6,
# "overlay": True, "caption_color": "#ffffff"}
The slider’s layout.hero hook uses exactly this to stay scope-aware — it bails
out unless cfg["scope"] == "all" or the request is the homepage — and passes
cfg straight into the carousel template for styling. No per-feature config
module is involved.
get_config reads through SettingsService; if the service is somehow
unavailable it still returns the declared defaults, so a feature is never left
without a usable config.
Key namespacing
Setting keys are namespaced as <feature>_<field> automatically, so two
features can both have an interval field without colliding. The slider’s
interval is stored under slider_interval in SettingsService; you only ever
use the short "interval" key in your schema and when reading get_config. The
namespacing is the framework’s concern, not yours.
The generic admin panel
The settings feature renders the whole admin surface from the registered
schemas — there is no per-feature panel code:
/admin/settings— lists every configurable feature (each declared schema), using itstitleandicon./admin/settings/<feature>— an auto-generated form for one feature, built from itsfields. Submitting it persists the namespaced values throughSettingsService.
Companion functions on the schema module:
get_schemas()— all declared schemas, sorted by title (used to render the list page).get_schema(feature)— one schema (used to render its form).clear_settings_schemas()— empties the registry.
Like the nav and asset registries, the schema registry is cleared per app at
feature-load, so /admin/settings only lists the features actually installed in
this product.
See also
- Asset registry — declare CSS/JS files and have the shell compose them
- Navigation registry — the same declare-then-compose pattern for the main menu
hooks.py& template hooks — inject HTML into layout slots (config read withget_configdecides what to render)