Administration Sections¶
MedUX provides a dedicated administration area at /administration/
for plugin-contributed admin UIs — user/group/role management, system
tools, tenant-level configuration screens, and anything else that is
too rich for the generic /settings/ page.
This page explains how the area is structured, how a plugin contributes a new section to it, how access is gated in a multi-tenant setup, and how the user-visible left rail is built.
When to Use Which Extension Point¶
MedUX has two overlapping extension points for "configuration"-shaped plugin surfaces. Pick the right one up front:
| You want to add... | Use |
|---|---|
| A single form with a handful of fields that persist via scoped settings | conjunto.settings.interfaces.ISettingsForm (rendered on the /settings/ page) |
| A full admin screen: list tables, modal CRUD, wizards, multi-step flows | medux.common.api.interfaces.IAdministrationSection (this page) |
Just raw URL patterns under /adm/<namespace>/ without menu/permission metadata |
medux.common.api.interfaces.IAdministrationURL (the older, thinner interface) |
IAdministrationURL is the legacy minimal hook and still works —
IAdministrationSection is a superset that additionally carries menu
metadata and a uniform permission-gating helper, so new plugins should
prefer it.
The Moving Parts¶
Four things cooperate to make an administration screen appear:
IAdministrationSectionplugin — declares URL patterns, a namespace, display metadata, and the required permission. This is the plugin developer's entry point.- URL collection loop in
src/medux/common/urls/__init__.pymerges everyIAdministrationURLand everyIAdministrationSectionplugin into onepath("administration/", include((adm_urlpatterns, "adm"), namespace="adm"))tree. Multiple plugins may contribute to the same namespace (e.g.tenant/); their patterns are concatenated. - Left-rail sidebar, rendered by
medux/common/templates/common/administration.html. It overrides conjunto'ssidebarblock and loops over the"administration"menu registry, so everyIMenuItem(menu="administration")entry — directly or as a nested child — appears in the rail on every administration page. TenantAdminRequiredMixinenforces the tenant-admin contract on views that need it, usinguser_is_tenant_admin()as the authoritative check.
The first two are the data layer, the last two are the UI layer. A plugin opts into each independently — you can ship URLs without a menu entry, or a menu entry without being tenant-gated, or both.
Contributing a Section — Minimum Recipe¶
Place plugin code in your plugin app. The example below is the real
"User roles" section that ships with medux.common and lives in
src/medux/common/views/roles.py.
1. Declare the Section¶
# myplugin/views/admin.py
from django.urls import path
from django.utils.translation import gettext_lazy as _
from medux.common.api.interfaces import (
IAdministrationSection,
TenantAdminRequiredMixin,
)
class MyAdminSection(IAdministrationSection):
"""Example administration section."""
namespace = "myplugin" # mounted at /administration/myplugin/
name = "widgets" # unique slug
title = _("Widgets")
icon = "puzzle" # Tabler icon name
weight = 50
parent_slug = "tenant" # nest under the "Tenant" rail group
url_name = "widget-list" # first URL (the entry point)
tenant_admin_required = True # only tenant admins may enter
urlpatterns = [
path("widgets/", WidgetListView.as_view(), name="widget-list"),
path("widgets/<int:pk>/edit/", WidgetEditView.as_view(), name="widget-edit"),
]
Important: the plugin class must be imported at startup so that
GDAPS's __init_subclass__ hook can register it. The common MedUX
convention is to import it from your app's urls.py:
# myplugin/urls.py
from . import views # noqa: F401 — registers IAdministrationSection subclasses
2. Guard the Views¶
Views that should only be reachable by tenant administrators inherit
from TenantAdminRequiredMixin:
from django.views.generic import ListView
from conjunto.tenants.mixins import TenantRequiredMixin
from medux.common.api.interfaces import TenantAdminRequiredMixin
class WidgetListView(TenantAdminRequiredMixin, TenantRequiredMixin, ListView):
model = Widget
template_name = "myplugin/admin/widget_list.html"
Order matters: TenantRequiredMixin (from conjunto) redirects to the
tenant picker if no tenant is bound on the request, and the admin
check happens only once a tenant is available. Always put
TenantRequiredMixin after the admin check in the MRO.
3. Wire the Rail Entry¶
A plugin may want the section to show up on the administration rail
on the left. Add an IMenuItem:
# myplugin/menus.py
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from conjunto.menu import IMenuItem
class WidgetsAdmin(IMenuItem):
title = _("Widgets")
slug = "myplugin__widgets"
parent = "tenant" # nest under the TenantAdmin rail group
url = reverse_lazy("adm:myplugin:widget-list")
icon = "puzzle"
weight = 50
permission_required = "common.change_tenant"
menu vs. parent are mutually exclusive. Conjunto's menu
registry raises ValueError when both are set, because a child's
menu is determined by walking up the parent chain. If you want a
top-level entry, set menu = "administration" and omit parent; if
you want a nested entry, set parent and omit menu.
The Permission Model¶
user_is_tenant_admin(request)¶
Defined in medux/common/api/interfaces.py. It OR-combines three
independent signals:
- Superuser —
request.user.is_superusershort-circuits toTrue. - Legacy permission —
request.user.has_perm("common.change_tenant"). - Conjunto role membership — a
TenantMembershiprow withrole="admin"forrequest.tenant.
The permission string is exported as TENANT_ADMIN_PERMISSION.
TenantAdminRequiredMixin¶
A view mixin whose dispatch calls user_is_tenant_admin(request)
and raises PermissionDenied if it returns False.
IAdministrationSection.has_permission(request)¶
A classmethod that AND-combines permission_required (an optional
Django permission string) with tenant_admin_required (a bool that
gates on user_is_tenant_admin). Superusers always pass.
Multi-Tenant Considerations¶
Use request.tenant (bound by ConjuntoMiddleware) as the single
source of truth for "which tenant am I in right now". When querying
TenantMembership rows, wrap writes in
conjunto.tenants.context.override_tenant(request.tenant).
Reference: The User Roles Section¶
src/medux/common/views/roles.py ships a complete working example:
UserRolesAdministrationSection— theIAdministrationSectionplugin with two URLs under/administration/tenant/UserRolesListView— lists users, renders roles as Tabler badges, exposes a "Manage roles" modal button per rowManageUserRolesView— modalFormViewwith dynamic role choices- HTMX success flow — modal returns 204 with
HX-Trigger: user_roles:changed, list wrapper refetches itself
Read it end-to-end when designing your own section — it exercises every building block on this page.