# plain.admin **Manage your app with a backend interface.** - [Overview](https://plainframework.com/docs/plain-admin/plain/admin/?llm#overview) - [Admin viewsets](https://plainframework.com/docs/plain-admin/plain/admin/?llm#admin-viewsets) - [Model views](https://plainframework.com/docs/plain-admin/plain/admin/?llm#model-views) - [Object views](https://plainframework.com/docs/plain-admin/plain/admin/?llm#object-views) - [Navigation](https://plainframework.com/docs/plain-admin/plain/admin/?llm#navigation) - [Admin cards](https://plainframework.com/docs/plain-admin/plain/admin/?llm#admin-cards) - [Basic cards](https://plainframework.com/docs/plain-admin/plain/admin/?llm#basic-cards) - [Trend cards](https://plainframework.com/docs/plain-admin/plain/admin/?llm#trend-cards) - [Table cards](https://plainframework.com/docs/plain-admin/plain/admin/?llm#table-cards) - [Admin forms](https://plainframework.com/docs/plain-admin/plain/admin/?llm#admin-forms) - [List filters](https://plainframework.com/docs/plain-admin/plain/admin/?llm#list-filters) - [Actions](https://plainframework.com/docs/plain-admin/plain/admin/?llm#actions) - [Toolbar](https://plainframework.com/docs/plain-admin/plain/admin/?llm#toolbar) - [Impersonate](https://plainframework.com/docs/plain-admin/plain/admin/?llm#impersonate) - [FAQs](https://plainframework.com/docs/plain-admin/plain/admin/?llm#faqs) - [Installation](https://plainframework.com/docs/plain-admin/plain/admin/?llm#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. ![Plain Admin user example](https://assets.plainframework.com/docs/plain-pageviews-user.png) The most common use of the admin is to manage your `plain.models`. To do this, create a [viewset](https://plainframework.com/docs/plain-admin/plain/admin/views/viewsets.py?llm#AdminViewset) with inner/nested views: ```python # 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`](https://plainframework.com/docs/plain-admin/plain/admin/views/viewsets.py?llm#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`](https://plainframework.com/docs/plain-admin/plain/admin/views/models.py?llm#AdminModelListView) - Lists model instances with pagination, search, and sorting - [`AdminModelDetailView`](https://plainframework.com/docs/plain-admin/plain/admin/views/models.py?llm#AdminModelDetailView) - Shows a single model instance with all its fields - [`AdminModelCreateView`](https://plainframework.com/docs/plain-admin/plain/admin/views/models.py?llm#AdminModelCreateView) - Creates new model instances using a form - [`AdminModelUpdateView`](https://plainframework.com/docs/plain-admin/plain/admin/views/models.py?llm#AdminModelUpdateView) - Updates existing model instances - [`AdminModelDeleteView`](https://plainframework.com/docs/plain-admin/plain/admin/views/models.py?llm#AdminModelDeleteView) - Deletes model instances with confirmation ```python @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_initial_objects()` or `get_object()` methods. - [`AdminListView`](https://plainframework.com/docs/plain-admin/plain/admin/views/objects.py?llm#AdminListView) - Base list view for any iterable - [`AdminDetailView`](https://plainframework.com/docs/plain-admin/plain/admin/views/objects.py?llm#AdminDetailView) - Base detail view for any object - [`AdminCreateView`](https://plainframework.com/docs/plain-admin/plain/admin/views/objects.py?llm#AdminCreateView) - Base create view - [`AdminUpdateView`](https://plainframework.com/docs/plain-admin/plain/admin/views/objects.py?llm#AdminUpdateView) - Base update view - [`AdminDeleteView`](https://plainframework.com/docs/plain-admin/plain/admin/views/objects.py?llm#AdminDeleteView) - Base delete view ```python 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_initial_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. ```python 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 ``` Icons come from [Bootstrap Icons](https://icons.getbootstrap.com/). To search available icon names: ```bash uv run plain admin icons # List all ~2,078 icons uv run plain admin icons cart # Search by keyword ``` 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`](https://plainframework.com/docs/plain-admin/plain/admin/cards/base.py?llm#Card) class displays a simple card with a title, optional description, metric, text, and link. ```python 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 columns - `Card.Sizes.LARGE` - 3 columns - `Card.Sizes.FULL` - 4 columns (full width) ### Trend cards The [`TrendCard`](https://plainframework.com/docs/plain-admin/plain/admin/cards/charts.py?llm#TrendCard) displays a bar chart showing data over time. It works with models that have a datetime field. ```python 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`](https://plainframework.com/docs/plain-admin/plain/admin/cards/tables.py?llm#TableCard) displays tabular data with headers, rows, and optional footers. ```python 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](https://plainframework.com/docs/plain/plain/forms/README.md?llm). For model-based forms, use [`ModelForm`](https://plainframework.com/docs/plain-models/plain/models/forms.py?llm#ModelForm). ```python 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. ```html {% extends "admin/base.html" %} {% block content %}
{{ csrf_input }} {{ form.as_p }}
{% endblock %} ``` ## List filters On [`AdminListView`](https://plainframework.com/docs/plain-admin/plain/admin/views/objects.py?llm#AdminListView) and [`AdminModelListView`](https://plainframework.com/docs/plain-admin/plain/admin/views/models.py?llm#AdminModelListView), you can define different `filters` to build predefined views of your data. The filter choices will be shown in the UI, and you can use the current `self.filter` in your view logic. ```python @register_viewset class UserAdmin(AdminViewset): class ListView(AdminModelListView): model = User fields = [ "id", "email", "created_at__date", ] filters = ["Active users", "Inactive users"] def filter_queryset(self, queryset): if self.filter == "Active users": return queryset.filter(is_active=True) elif self.filter == "Inactive users": return queryset.filter(is_active=False) return queryset ``` ## Actions List views support bulk actions on selected items. Define actions as a list of action names, then implement `perform_action()` to handle them. ```python 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`](https://plainframework.com/docs/plain-toolbar/plain/toolbar/README.md?llm) 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: ```python # app/settings.py MIDDLEWARE = [ # ...other middleware "plain.toolbar.ToolbarMiddleware", ] ``` ```html {% 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: ```python # 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`](https://plainframework.com/docs/plain-admin/plain/admin/impersonate/requests.py?llm#get_request_impersonator): ```python 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`: ```python 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: ```python 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](https://pypi.org/project/plain.admin/): ```bash 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: ```python # 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. ```python # 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`](https://plainframework.com/docs/plain-admin/plain/admin/urls.py?llm#AdminRouter) to your root URLs. ```python # 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: ```python # 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.