Admin Panel
The openviper.admin package provides a fully-featured, auto-generated
administration interface for OpenViper models. Mount it at /admin in
routes.py and all registered models are immediately accessible through
a React-based SPA backed by a REST API.
Overview
The admin panel is built around three components:
ModelAdmin — per-model display and behavior configuration.
AdminRegistry — central registry that maps model classes to their
ModelAdmininstances.Admin Site — the router factory (
get_admin_site()) that mounts the API and SPA at a given URL prefix.
Models are registered in admin.py inside each installed app and
auto-discovered when the admin site is first accessed.
Key Classes & Functions
- class openviper.admin.options.ModelAdmin(model_class)
Configuration class for admin model behavior. Subclass it to customize how a model appears in the admin panel.
List view attributes:
- list_display: list[str]
Field names shown as columns in the list view. Defaults to
idplus the first four fields.
- list_display_links: list[str] | None
Fields that are rendered as links to the detail view. Defaults to the first field in
list_display.
- list_editable: list[str]
Fields that can be edited inline in the list view (without opening the detail form).
- ordering: str | list[str] | None
Default ordering for the list view. Prefix with
-for descending. Defaults to["-id"].
FK fields to eager-load in the list view via
select_related. Set toTrueto auto-detect from FK fields inlist_display.
Form view attributes:
- fields: list[str] | None
Explicit list of fields shown in the create/edit form. When
Noneall non-excluded, non-id fields are shown.
- fieldsets: list[tuple[str | None, dict]]
Grouped form layout: list of
(title, {"fields": [...], "classes": [...], "description": "..."})tuples. Settitle=Nonefor an untitled group.
- form_fields: dict[str, dict]
Per-field widget overrides, e.g.
{"body": {"widget": "textarea", "rows": 10}}.
- sensitive_fields: list[str]
Fields never exposed in API responses (default:
["password"]). Extend to include tokens, secrets, API keys, etc.
Actions:
- actions: list[Callable]
List of callables (or method name strings) available as batch actions in the list view. The built-in
delete_selectedaction is always available.
Inlines:
- inlines: list[type[InlineModelAdmin]]
Inline model admin classes for nested editing of related objects.
- child_tables: list[type[ChildTable]]
Alias for
inlinesusing the tabular layout.
UI options:
Permission methods:
- has_delete_permission(request, obj=None) bool
All return
Truefor staff or superuser by default. Override to add object-level permission logic.
CRUD methods:
- class openviper.admin.options.InlineModelAdmin(parent_model)
Configuration for inline (nested) model editing.
- class openviper.admin.options.TabularInline(parent_model)
Subclass of
InlineModelAdminrendered as a horizontal table.
- class openviper.admin.options.StackedInline(parent_model)
Subclass of
InlineModelAdminrendered as vertical cards.
- class openviper.admin.options.ChildTable(parent_model)
Alias for
TabularInline.
- class openviper.admin.registry.AdminRegistry
Central registry for admin-managed models.
- register(model, admin_class=None)
Register model with an optional admin_class. Uses the default
ModelAdminwhen admin_class isNone.
- unregister(model)
Remove a model from the registry.
- get_admin(model) ModelAdmin
Return the
ModelAdmininstance for model.
Example Usage
See also
Working projects that use the admin panel:
examples/todoapp/ — simple admin with
@register,list_display,search_fieldsexamples/ai_moderation_platform/ — custom actions,
ChildTableinlines, multi-app adminexamples/ecommerce_clone/ —
unregister/ re-register pattern,UserRoleInline
Registering a Model
Create myapp/admin.py:
from openviper.admin import register, ModelAdmin
from myapp.models import Post, Comment
@register(Post)
class PostAdmin(ModelAdmin):
list_display = ["title", "author", "is_published", "created_at"]
list_display_links = ["title"]
list_filter = ["is_published", "created_at"]
list_editable = ["is_published"]
search_fields = ["title", "body"]
ordering = "-created_at"
list_per_page = 50
readonly_fields = ["created_at", "updated_at"]
sensitive_fields = ["password", "internal_token"]
fieldsets = [
("Content", {"fields": ["title", "body"]}),
("Publishing", {
"fields": ["author", "is_published"],
"description": "Controls when the post is visible",
}),
("Timestamps", {
"fields": ["created_at", "updated_at"],
"classes": ["collapse"],
}),
]
date_hierarchy = "created_at"
list_select_related = ["author"] # or True for auto-detect
@register(Comment)
class CommentAdmin(ModelAdmin):
pass
Mounting the Admin Site
# routes.py
from openviper.admin.site import get_admin_site
route_paths = [
("/", main_router),
("/admin", get_admin_site()),
]
Now visit http://localhost:8000/admin/ to access the admin panel.
Custom Actions
from openviper.admin import register, ModelAdmin, action, ActionResult
from myapp.models import Post
@register(Post)
class PostAdmin(ModelAdmin):
actions = ["publish_posts", "archive_posts"]
list_display = ["title", "is_published", "is_archived"]
@action(description="Publish selected posts")
async def publish_posts(self, queryset, request):
count = await queryset.update(is_published=True)
return ActionResult(success=True, count=count, message=f"Published {count} posts.")
@action(description="Archive selected posts")
async def archive_posts(self, queryset, request):
count = await queryset.update(is_archived=True, is_published=False)
return ActionResult(success=True, count=count, message=f"Archived {count} posts.")
Inline Editing
from openviper.admin import register, ModelAdmin, ChildTable
from openviper.admin.options import StackedInline
from myapp.models import Post, Comment, Tag
class CommentInline(ChildTable):
model = Comment
fk_name = "post" # FK on Comment pointing to Post
fields = ["author", "body", "created_at"]
readonly_fields = ["created_at"]
extra = 1
max_num = 20
can_delete = True
class TagInline(StackedInline):
model = Tag
fields = ["name", "slug"]
@register(Post)
class PostAdmin(ModelAdmin):
inlines = [CommentInline, TagInline]
Overriding Permissions
@register(Post)
class PostAdmin(ModelAdmin):
def has_delete_permission(self, request=None, obj=None):
# Only superusers can delete posts
if request is None:
return True
user = getattr(request, "user", None)
return getattr(user, "is_superuser", False)
def has_change_permission(self, request=None, obj=None):
# Users can only edit their own posts
if request is None or obj is None:
return True
user = getattr(request, "user", None)
if getattr(user, "is_superuser", False):
return True
return obj.author_id == getattr(user, "pk", None)
Overriding save_model
@register(Post)
class PostAdmin(ModelAdmin):
async def save_model(self, request, obj, form_data, change=False):
if not change:
# Set author automatically on create
obj.author_id = request.user.pk
return await super().save_model(request, obj, form_data, change)
Sensitive Fields
from openviper.admin import register, ModelAdmin
from openviper.auth import get_user_model
User = get_user_model()
@register(User)
class UserAdmin(ModelAdmin):
list_display = ["id", "username", "email", "is_active"]
# 'password' is hidden by default; extend for other secrets:
sensitive_fields = [
"password",
"api_key",
"refresh_token",
]
readonly_fields = ["created_at", "last_login"]