plain.admin
Manage your app with a backend interface.
- Overview
- Admin viewsets
- Admin cards
- Admin forms
- List presets
- Actions
- Toolbar
- Impersonate
- FAQs
- Installation
Overview
The Plain Admin provides a combination of built-in views and the flexibility to create your own. You can use it to quickly get visibility into your app's data and to manage it.

The most common use of the admin is to manage your plain.models. To do this, create a viewset with inner/nested views:
# app/users/admin.py
from plain.admin.views import (
AdminModelDetailView,
AdminModelListView,
AdminModelUpdateView,
AdminViewset,
register_viewset,
)
from plain.models.forms import ModelForm
from .models import User
class UserForm(ModelForm):
class Meta:
model = User
fields = ["email"]
@register_viewset
class UserAdmin(AdminViewset):
class ListView(AdminModelListView):
model = User
fields = [
"id",
"email",
"created_at__date",
]
queryset_order = ["-created_at"]
search_fields = [
"email",
]
class DetailView(AdminModelDetailView):
model = User
class UpdateView(AdminModelUpdateView):
template_name = "admin/users/user_form.html"
model = User
form_class = UserForm
Admin viewsets
The AdminViewset automatically recognizes inner views named ListView, CreateView, DetailView, UpdateView, and DeleteView. It interlinks these views automatically in the UI and sets up form success URLs. You can define additional views too, but you will need to implement a couple methods to hook them up.
Model views
For working with database models, use the model-specific view classes. These handle common patterns like automatic URL paths, queryset ordering, and search.
AdminModelListView- Lists model instances with pagination, search, and sortingAdminModelDetailView- Shows a single model instance with all its fieldsAdminModelCreateView- Creates new model instances using a formAdminModelUpdateView- Updates existing model instancesAdminModelDeleteView- Deletes model instances with confirmation
@register_viewset
class ProductAdmin(AdminViewset):
class ListView(AdminModelListView):
model = Product
fields = ["id", "name", "price", "created_at"]
queryset_order = ["-created_at"]
search_fields = ["name", "description"]
class DetailView(AdminModelDetailView):
model = Product
fields = ["id", "name", "description", "price", "created_at", "updated_at"]
class CreateView(AdminModelCreateView):
model = Product
form_class = ProductForm
class UpdateView(AdminModelUpdateView):
model = Product
form_class = ProductForm
class DeleteView(AdminModelDeleteView):
model = Product
The fields attribute on list and detail views supports the __ syntax for accessing related objects and calling methods. For example, "created_at__date" will call the date() method on the datetime field.
Object views
For working with non-model data (API responses, files, etc.), use the base object views. These require you to implement get_objects() or get_object() methods.
AdminListView- Base list view for any iterableAdminDetailView- Base detail view for any objectAdminCreateView- Base create viewAdminUpdateView- Base update viewAdminDeleteView- Base delete view
from plain.admin.views import AdminListView, AdminViewset, register_viewset
@register_viewset
class ExternalAPIAdmin(AdminViewset):
class ListView(AdminListView):
title = "External Items"
nav_section = "Integrations"
path = "external-items/"
fields = ["id", "name", "status"]
def get_objects(self):
# Fetch from an external API, file, or any data source
return external_api.get_items()
Navigation
Views appear in the admin sidebar based on their nav_section and nav_title attributes. Set nav_section to group related views together.
class ListView(AdminModelListView):
model = Order
nav_section = "Sales" # Groups this view under "Sales" in the sidebar
nav_title = "Orders" # Display name (defaults to model name)
nav_icon = "shopping-cart" # Icon for the section
Setting nav_section = None hides a view from the navigation entirely.
Admin cards
Cards display summary information on admin pages. You can add them to any view by setting the cards attribute.
Basic cards
The base Card class displays a simple card with a title, optional description, metric, text, and link.
from plain.admin.cards import Card
from plain.admin.views import AdminView, register_view
class UsersCard(Card):
title = "Total Users"
size = Card.Sizes.SMALL
def get_metric(self):
from app.users.models import User
return User.query.count()
def get_link(self):
return "/admin/p/user/"
@register_view
class DashboardView(AdminView):
title = "Dashboard"
path = "dashboard/"
nav_section = ""
cards = [UsersCard]
Card sizes control how much horizontal space they occupy in a four-column grid:
Card.Sizes.SMALL- 1 column (default)Card.Sizes.MEDIUM- 2 columnsCard.Sizes.LARGE- 3 columnsCard.Sizes.FULL- 4 columns (full width)
Trend cards
The TrendCard displays a bar chart showing data over time. It works with models that have a datetime field.
from plain.admin.cards import TrendCard
from plain.admin.dates import DatetimeRangeAliases
class SignupsTrendCard(TrendCard):
title = "User Signups"
size = TrendCard.Sizes.MEDIUM
model = User
datetime_field = "created_at"
default_preset = DatetimeRangeAliases.SINCE_30_DAYS_AGO
Trend cards include built-in date range presets like "Today", "This Week", "Last 30 Days", etc. Users can switch between presets in the UI.
For custom chart data, override the get_trend_data() method to return a dict mapping date strings to counts.
Table cards
The TableCard displays tabular data with headers, rows, and optional footers.
from plain.admin.cards import TableCard
class RecentOrdersCard(TableCard):
title = "Recent Orders"
size = TableCard.Sizes.FULL # Tables typically use full width
def get_headers(self):
return ["Order ID", "Customer", "Total", "Status"]
def get_rows(self):
orders = Order.query.order_by("-created_at")[:5]
return [
[order.id, order.customer.name, order.total, order.status]
for order in orders
]
Admin forms
Admin forms work with standard plain.forms. For model-based forms, use ModelForm.
from plain.models.forms import ModelForm
from plain.admin.views import AdminModelUpdateView
class UserForm(ModelForm):
class Meta:
model = User
fields = ["email", "first_name", "last_name", "is_active"]
class UpdateView(AdminModelUpdateView):
model = User
form_class = UserForm
template_name = "admin/users/user_form.html" # Optional custom template
The form template should extend the admin base and use the form rendering helpers.
{% extends "admin/base.html" %}
{% block content %}
<form method="post">
{{ csrf_input }}
{{ form.as_p }}
<button type="submit">Save</button>
</form>
{% endblock %}
List presets
On AdminListView and AdminModelListView, you can define different presets to build predefined views of your data. The preset choices will be shown in the UI, and you can use the current self.preset in your view logic.
@register_viewset
class UserAdmin(AdminViewset):
class ListView(AdminModelListView):
model = User
fields = [
"id",
"email",
"created_at__date",
]
presets = ["Active users", "Inactive users"]
def get_objects(self):
objects = super().get_objects()
if self.preset == "Active users":
objects = objects.filter(is_active=True)
elif self.preset == "Inactive users":
objects = objects.filter(is_active=False)
return objects
Actions
List views support bulk actions on selected items. Define actions as a list of action names, then implement perform_action() to handle them.
class ListView(AdminModelListView):
model = User
fields = ["id", "email", "is_active"]
actions = ["Activate", "Deactivate", "Delete selected"]
def perform_action(self, action, target_ids):
users = User.query.filter(id__in=target_ids)
if action == "Activate":
users.update(is_active=True)
elif action == "Deactivate":
users.update(is_active=False)
elif action == "Delete selected":
users.delete()
# Return None to redirect back to the list, or return a Response
return None
The target_ids parameter contains the IDs of selected items. Users can select individual items or use "Select all" to target the entire filtered queryset.
Toolbar
The admin includes a toolbar component that appears on your frontend when an admin user is logged in. This toolbar provides quick access to the admin and shows a link to edit the current object if one is detected.
The toolbar is registered automatically when you include plain.admin in your installed packages. It uses plain.toolbar to render on your pages.
To enable the toolbar on your frontend, add the toolbar middleware and include the toolbar template tag in your base template:
# app/settings.py
MIDDLEWARE = [
# ...other middleware
"plain.toolbar.ToolbarMiddleware",
]
<!-- In your base template -->
{% load toolbar %}
{% toolbar %}
When viewing a page that has an object in the template context, the toolbar will show a link to that object's admin detail page (if one exists).
Impersonate
The impersonate feature lets admin users log in as another user to debug issues or provide support. This is useful for seeing exactly what a user sees without needing their credentials.
To start impersonating, visit a user's detail page in the admin and click the "Impersonate" link. The admin toolbar will show who you're impersonating and provide a link to stop.
By default, users with is_admin=True can impersonate other users. Admin users cannot be impersonated (for security). You can customize who can impersonate by defining IMPERSONATE_ALLOWED in your settings:
# app/settings.py
def IMPERSONATE_ALLOWED(user):
# Only superusers can impersonate
return user.is_superuser
The impersonate URLs are included automatically with the admin router. You can check if the current request is impersonated using get_request_impersonator:
from plain.admin.impersonate import get_request_impersonator
def my_view(request):
impersonator = get_request_impersonator(request)
if impersonator:
# The request is being impersonated
# `impersonator` is the admin user doing the impersonating
# `request.user` is the user being impersonated
pass
FAQs
How do I customize the admin templates?
Override any admin template by creating a file with the same path in your app's templates directory. For example, to customize the list view, create app/templates/admin/list.html.
How do I add a standalone admin page without a viewset?
Use @register_view instead of @register_viewset:
from plain.admin.views import AdminView, register_view
@register_view
class ReportsView(AdminView):
title = "Reports"
path = "reports/"
nav_section = "Analytics"
template_name = "admin/reports.html"
How do I hide a view from the sidebar?
Set nav_section = None on the view class. The view will still be accessible via its URL.
How do I link to an object's admin page from my templates?
Use the get_model_detail_url function:
from plain.admin.views import get_model_detail_url
url = get_model_detail_url(my_object) # Returns None if no admin view exists
Installation
Install the plain.admin package from PyPI:
uv add plain.admin
The admin uses a combination of other Plain packages, most of which you will already have installed. Ultimately, your settings will look something like this:
# app/settings.py
INSTALLED_PACKAGES = [
"plain.models",
"plain.tailwind",
"plain.auth",
"plain.sessions",
"plain.htmx",
"plain.admin",
"plain.elements",
# other packages...
]
AUTH_USER_MODEL = "users.User"
AUTH_LOGIN_URL = "login"
MIDDLEWARE = [
"plain.sessions.middleware.SessionMiddleware",
"plain.auth.middleware.AuthMiddleware",
"plain.admin.AdminMiddleware",
]
Your User model is expected to have an is_admin field (or attribute) for checking who has permission to access the admin.
# app/users/models.py
from plain import models
@models.register_model
class User(models.Model):
is_admin = models.BooleanField(default=False)
# other fields...
To make the admin accessible, add the AdminRouter to your root URLs.
# app/urls.py
from plain.admin.urls import AdminRouter
from plain.urls import Router, include, path
from . import views
class AppRouter(Router):
namespace = ""
urls = [
include("admin/", AdminRouter),
path("login/", views.LoginView, name="login"),
path("logout/", views.LogoutView, name="logout"),
# other urls...
]
Create your first admin viewset for your User model:
# app/users/admin.py
from plain.admin.views import (
AdminModelDetailView,
AdminModelListView,
AdminViewset,
register_viewset,
)
from .models import User
@register_viewset
class UserAdmin(AdminViewset):
class ListView(AdminModelListView):
model = User
nav_section = "Users"
fields = ["id", "email", "is_admin", "created_at"]
search_fields = ["email"]
class DetailView(AdminModelDetailView):
model = User
Visit /admin/ to see your admin interface.