See the live demo here!
user: demo
pass: demo
Django Admin Adapter is a package aiming to convert almost instantly an Admin Site, to a series of battle ready API endpoints, with minimum effort.
There is also a React Front-End to base your entire admin projects on.
- Description
- Installation
- Front end
- Quickstart
- Authentication
- History View
- Extra Views (and overriding existing)
- Sidebar Registry
- Actions
- List Filters
- Throttling
- Customizing Object/List Serializers
- Base Permission Class
- Providing Extra Data
- Development and testing
Django Admin Adapter is a project aiming at converting, with minimum effort, an existing (or new) Django admin project, to a series of battle ready API endpoints.
Imagine having the versatillity and rapid development capabilities of the Django Admin framework, but not the slow template response cycle.
Here is where django admin adapter comes in place.
Create Django Admin projects and expose all the API endpoints of the project instantly,
decoupling the front end, and decreasing drastically the response times.
It needs almost no customization, and works out of the box
with the provided django.contrib.admin.sites.AdminSite class instance
( by default the django.contrib.admin.sites.site ).
Uses Django Rest Framework and Simple JWT, although the authentication system can be overriden and is not a hard dependency.
Important
If the endpoints are consumed by a dettached front-end
e.g. React (see React Django Admin client)
then proper CORS package (e.g. django-cors-headers) must be used,
with proper CORS_ALLOWED_ORIGINS and ALLOWED_HOSTS settings
Important
The object change and add views in order to maintain most of the django admin
functionallity, use multipart data, so make sure the rest_framework.parsers.MultiPartParser is used
in REST_FRAMEWORK settings
Install using pip:
pip install django_admin_adapterAn OpenAPI 3 specification is provided in adapter_openapi3.yaml in the base directory of the repository.
Browse the Swagger API freely.
There is a React project available for Admin API adapter projects. Instructions are provided there for using the app.
Inside your project's base urls.py:
from django_admin_adapter import AdminAPIAdapter
from django.contrib import admin
admin_adapter = AdminAPIAdapter(
admin.site,
sidebar_registry = [
{
"type": "model",
"label": "Users",
"icon": "user-icon",
"app_name": "auth",
"model_name": "user",
},
{
"type": "view",
"label": "Dashboard",
"icon": "dashboard-icon",
"view_name": "custom_dashboard",
"client_view_path": "/dashboard",
},
{
"type": "dropdown",
"label": "Settings",
"icon": "settings-icon",
"dropdown_entries": [
{
"type": "model",
"label": "Groups",
"app_name": "auth",
"model_name": "group",
},
],
},
],
)
urlpatterns = [
path("api/", include(admin_adapter.get_urls())),
]Congrats! Now the endpoints needed to interact with the admin panel are available.
See the OpenAPI specification to learn more about the instantly exposed endpoints.
Also customization and additions as seen later is possible.
Important
The Simple JWT is not a hard dependency, and can be overriden.
If not, the adapter will raise an error at instantiation,
in case the rest_framework_simplejwt is not installed.
Important
To control the throttling to the authentication views
use the authentication_throttle_class argument at initialization.
Note
For best compatibility use a subclass of AbstractBaseUser
for your user model.
The exposed endpoints form the adapter are using the Simple JWT package's token authentication system.
You do not need to add the DEFAULT_AUTHENTICATION_CLASSES to your project's settings.
For each View the authentication_classes are set automatically
when retrieving the adapter's urls.
Any settings provided for the JWT from django.conf.settings is used.
You can customize the tokens' lifetime through the settings of your project ACCESS_TOKEN_LIFETIME , REFRESH_TOKEN_LIFETIME
In case you want to override the TokenObtainPairView and
TokenRefreshView authentication views
you should pass the below arguments when instantiating the adapter:
from django_admin_adapter import AdminAPIAdapter
from django.contrib import admin
admin_adapter = AdminAPIAdapter(
admin.site,
extra_views={
"token_obtain_pair": (
"token/", # or your own path
CustomAdminTokenObtainPairView,
None, # or your throttle classes [AnonRateThrottle, UserRateThrottle]
),
"token_refresh": (
"token/refresh/", # or your own path
CustomAdminTokenRefreshView,
None, # or your throttle classes [AnonRateThrottle, UserRateThrottle]
),
},
)that way the authentication views are changed to your preference.
Also this way the CustomTokenObtainPairSerializer of the Tokens returned can be overriden.
Note
The default token obtain pair serializer has been changed to
provide in the token 2 new fields, the username and the identifier fields.
The first is the username field value (USERNAME_FIELD field)
and the latter is the string representation of the user object.
If you want to change the obtain pair serializer you should pass
the token_obtain_pair_serializer argument to the constructor
as kwarg with the value of your serializer class.
IN CASE YOU NEED TO CHANGE THE TOTAL AUTHENTICATION SYSTEM:
- At Adapter instantiation, pass
authentication_views_namesargument to the constructor as kwarg with the tuple of your auth views names.- At Adapter instantiation, pass the
authentication_classargument to the constructor.- Make sure the two authentication views are overriden or deleted from the VIEWMAP of the adapter.
- If
token_obtain_pair_serializeris used, make sure you provide yours or else an error will be raised at instantiation.
The history view is exposed by default, but an option to restrict the access to the history page per model and object is provided.
To control the access to the history view per model and object,
the has_history_permission method of the ModelAdmin class is used.
Create the following method on your model admin to control behavior:
def has_history_permission(self, request, obj=None):
...
return your_evaluated_booleanIf no method is provided, the history view is accessible to all authenticated and permitted users.
The extra_views parameter allows you to add custom API views to your admin adapter
or override the default built-in views. This provides flexibility to extend the adapter's
functionality with your own endpoints while maintaining consistent authentication,
permissions, and throttling behavior.
Important
If your custom view class does not define permission_classes, but
ingerits from APIView, the permission_classes are set as the settings.DEFAULT_PERMISSION_CLASSES
of the Django Rest Framework, which by default is 'rest_framework.permissions.AllowAny'.
The base permission of the adapter will be appended on those permissions.
Warning
The Views provided from the get_urls of the ModelAdmin are not added to the adapter's urls.
If you want to add them, you should add them manually to the extra_views parameter.
The extra_views parameter accepts a dictionary where:
- Key: A unique view name (string)
- Value: A tuple containing three elements:
- URL path (string)
- View class (subclass of
rest_framework.views.APIView) - Throttle classes list (list/tuple of two throttle classes, or
None)
extra_views = {
"view_name": (
"path/to/view/<str:and_a_parameter>/", # URL path
CustomAPIView, # View class
[AnonThrottle, UserThrottle] # Throttle classes or None
),
}You can add completely new endpoints to your admin API:
from django_admin_adapter import AdminAPIAdapter
from django.contrib import admin
from rest_framework.views import APIView
from rest_framework.response import Response
class DashboardStatsView(APIView):
def get(self, request):
# Your custom logic here
stats = {
"total_users": 100,
"active_sessions": 25,
}
return Response(stats)
admin_adapter = AdminAPIAdapter(
admin.site,
extra_views={
"dashboard_stats": (
"dashboard/stats/",
DashboardStatsView,
None, # Use default throttle classes
),
},
)The new endpoint will be available at: /api/dashboard/stats/
You can override any of the adapter's default views by using the same view name:
from django_admin_adapter import AdminAPIAdapter
from django.contrib import admin
from django_admin_adapter.views import AdminBaseInfoAPIView
class CustomBaseInfoView(AdminBaseInfoAPIView):
def get_extra_data(self, request):
# Add custom data to base_info response
return {
"company_name": "My Company",
"version": "2.0.0",
}
admin_adapter = AdminAPIAdapter(
admin.site,
extra_views={
"base_info": (
"base_info/", # Keep the same path
CustomBaseInfoView,
None,
),
},
)The following view names can be overridden:
Authentication:
password_changetoken_obtain_pairtoken_refresh
Autocomplete:
filter_autocompletefilter_autocomplete_retrieve_labelfield_autocomplete
Base Information:
base_info
List Operations:
admin_list_createadmin_list_infoadmin_list_action_previewadmin_list_action_execute
Object Operations:
admin_object_viewadmin_object_addadmin_object_editadmin_object_delete_confirmadmin_object_historyadmin_object
Each view in extra_views can have custom throttle classes:
Using Default Throttles:
extra_views={
"my_view": (
"my-view/",
MyView,
None, # Uses adapter's default throttle classes
),
}Custom Throttles:
from rest_framework.throttling import AnonRateThrottle, UserRateThrottle
class MyViewAnonThrottle(AnonRateThrottle):
rate = '10/hour'
class MyViewUserThrottle(UserRateThrottle):
rate = '100/hour'
extra_views={
"my_view": (
"my-view/",
MyView,
[MyViewAnonThrottle, MyViewUserThrottle], # Must be exactly 2 classes
),
}The throttle classes list must contain exactly two classes:
- First class: Applied to anonymous (unauthenticated) requests
- Second class: Applied to authenticated user requests
from django_admin_adapter import AdminAPIAdapter
from django.contrib import admin
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.throttling import AnonRateThrottle, UserRateThrottle
from django_admin_adapter.views import AdminTokenObtainPairView
# Custom view with custom logic
class AnalyticsView(APIView):
def get(self, request):
return Response({"analytics": "data"})
# Custom throttles for analytics
class AnalyticsAnonThrottle(AnonRateThrottle):
rate = '5/hour'
class AnalyticsUserThrottle(UserRateThrottle):
rate = '50/hour'
# Override authentication view
class CustomTokenObtainView(AdminTokenObtainPairView):
def post(self, request, *args, **kwargs):
# Add custom logging or validation
response = super().post(request, *args, **kwargs)
# Custom post-processing
return response
admin_adapter = AdminAPIAdapter(
admin.site,
extra_views={
# New custom view
"analytics": (
"analytics/",
AnalyticsView,
[AnalyticsAnonThrottle, AnalyticsUserThrottle],
),
"token_obtain_pair": (
"token/",
CustomTokenObtainView,
None, # Use default throttles
),
},
)Views defined in extra_views automatically receive access to the adapter context
through class attributes:
from rest_framework.views import APIView
from rest_framework.response import Response
class MyCustomView(APIView):
# These attributes are automatically set by the adapter
# _admin_site: The Django admin site instance
# _adapter_instance: The AdminAPIAdapter instance
# _admin_view_name: The view name string
def get(self, request):
# Access the admin site
admin_site = self._admin_site
# Access registered models
registered_models = admin_site._registry.keys()
# Access adapter instance
adapter = self._adapter_instance
return Response({
"view_name": self._admin_view_name,
"models_count": len(registered_models),
})By default, all views except authentication views (token_obtain_pair, token_refresh)
automatically receive:
- Authentication: JWT authentication (or custom if specified)
- Permissions:
AdminSiteBasePermission(result of yourAdminSite'shas_permissionmethod)
Important
If your custom view class already defines permission_classes, the adapter will
append AdminSiteBasePermission to the existing list.
The adapter validates extra_views during initialization and will raise
AdminAPIAdapterError if:
extra_viewsis not a dictionary- View data is not a tuple of exactly 3 elements
- URL path is not a string
- View class is not a subclass of
rest_framework.views.APIView - Throttle classes (if provided) is not a list or tuple
- Throttle classes list does not contain exactly 2 classes
- Any throttle class is not a subclass of
rest_framework.throttling.BaseThrottle
All views defined in extra_views are registered with Django's URL patterns
when you call adapter.get_urls(). The full URL will be:
<your_base_path>/<view_path>
For example:
urlpatterns = [
path("api/", include(admin_adapter.get_urls())),
]A view with path "dashboard/" will be accessible at: /api/dashboard/
The sidebar_registry parameter allows you to customize the navigation sidebar
structure of your admin API interface. This is particularly useful when you want
to organize your admin models and custom views in a specific way, different from
the default Django admin organization.
The sidebar registry is a list of dictionaries, where each dictionary represents a sidebar entry. There are three types of entries:
- model: Links to a Django model admin
- view: Links to a custom API view
- dropdown: A collapsible menu containing nested model or view entries
from django_admin_adapter import AdminAPIAdapter
from django.contrib import admin
sidebar_registry = [
{
"type": "model",
"label": "Users",
"icon": "user-icon",
"app_name": "auth",
"model_name": "user",
},
{
"type": "view",
"label": "Dashboard",
"icon": "dashboard-icon",
"view_name": "custom_dashboard",
"client_view_path": "/dashboard",
},
{
"type": "dropdown",
"label": "Settings",
"icon": "settings-icon",
"dropdown_entries": [
{
"type": "model",
"label": "Groups",
"app_name": "auth",
"model_name": "group",
},
],
},
]
admin_adapter = AdminAPIAdapter(
admin.site,
sidebar_registry=sidebar_registry,
)A model entry links to a registered Django model admin.
Required fields:
type: Must be"model"label: Display name for the sidebar entry (string)app_name: Django app label (e.g.,Model._meta.app_label)model_name: Model name (e.g.,Model._meta.model_name)
Optional fields:
icon: Icon identifier (string)
{
"type": "model",
"label": "Blog Posts",
"icon": "document",
"app_name": "blog",
"model_name": "post",
}Note: The model must be registered with the admin site, otherwise a validation error will be raised.
A view entry links to a custom API view defined in extra_views.
Required fields:
type: Must be"view"label: Display name for the sidebar entry (string)view_name: Name of the view as defined inextra_views(string)client_view_path: Frontend route path for the view (string)
Optional fields:
icon: Icon identifier (string)
{
"type": "view",
"label": "Analytics",
"icon": "chart",
"view_name": "analytics_view",
"client_view_path": "/analytics",
}Note: The view_name must exist in the adapter's extra_views mapping.
A dropdown entry creates a collapsible menu containing nested entries.
Required fields:
type: Must be"dropdown"label: Display name for the dropdown (string)dropdown_entries: List of nested model or view entries
Optional fields:
icon: Icon identifier (string)
{
"type": "dropdown",
"label": "User Management",
"icon": "users",
"dropdown_entries": [
{
"type": "model",
"label": "Users",
"app_name": "auth",
"model_name": "user",
},
{
"type": "model",
"label": "Groups",
"app_name": "auth",
"model_name": "group",
},
],
}Note: Nested dropdowns are not supported. Dropdown entries can only contain model or view types.
from django_admin_adapter import AdminAPIAdapter
from django.contrib import admin
from myapp.views import DashboardView, ReportsView
admin_adapter = AdminAPIAdapter(
admin.site,
sidebar_registry=[
{
"type": "view",
"label": "Dashboard",
"icon": "home",
"view_name": "dashboard",
"client_view_path": "/dashboard",
},
{
"type": "dropdown",
"label": "Content",
"icon": "folder",
"dropdown_entries": [
{
"type": "model",
"label": "Articles",
"icon": "document",
"app_name": "blog",
"model_name": "article",
},
{
"type": "model",
"label": "Categories",
"icon": "tag",
"app_name": "blog",
"model_name": "category",
},
],
},
{
"type": "dropdown",
"label": "User Management",
"icon": "users",
"dropdown_entries": [
{
"type": "model",
"label": "Users",
"app_name": "auth",
"model_name": "user",
},
{
"type": "model",
"label": "Groups",
"app_name": "auth",
"model_name": "group",
},
],
},
{
"type": "view",
"label": "Reports",
"icon": "chart",
"view_name": "reports",
"client_view_path": "/reports",
},
],
extra_views={
"dashboard": (
"dashboard/",
DashboardView,
None,
),
"reports": (
"reports/",
ReportsView,
None,
),
},
)The adapter validates all sidebar entries during initialization and will raise
AdminAPIAdapterError if:
- Entry type is not one of:
"model","view", or"dropdown" - Required fields are missing
- Field types are incorrect (e.g., non-string label)
- Model is not registered with the admin site
- View name is not defined in
extra_views - Nested dropdowns are attempted
- Model does not exist in Django apps
If sidebar_registry is None (default), an empty sidebar will be used.
The Django Admin Adapter provides full support for Django admin actions through two dedicated API endpoints. Actions allow you to perform bulk operations on selected objects in the admin interface.
Important
All actions now must return a Django Rest Framework Response object
and must accept a confirm parameter as the last argument, which
differentiates flow between preview and execution.
Actions in Django Admin are functions that can be performed on multiple selected objects at once. The adapter exposes these actions through a two-step process:
- Preview - Shows what will happen when the action is executed
- Execute - Actually performs the action on the selected objects
This two-step approach ensures users can review the impact of an action before confirming it, preventing accidental bulk operations.
Note
The preview combined with a proper fornt end client design and custom views endpoints, can be utilized for action logical multistep validation.
URL Pattern: /api/<app_name>/<model_name>/action/<action_name>/preview/
Method: POST
View: AdminListActionPreviewAPIView
Purpose: Previews what will happen when an action is executed without actually performing it.
URL Pattern: /api/<app_name>/<model_name>/action/<action_name>/execute/
Method: POST
View: AdminListActionExecuteAPIView
Purpose: Executes the action on the selected objects.
Both preview and execute endpoints expect the same POST data format:
{
"select_across": 0,
"selected_objects": [1, 2, 3, 4, 5]
}Note
Of course any other key, value pair can be provided to enhance the action procedure.
Since the request is passed to the ModelAdmin action, the request.data
can be accessed inside the action preview/execution block.
Parameters:
-
select_across(integer):0- Only selected objects (specified inselected_objects)1- All objects matching the current filter (ignoresselected_objects)
-
selected_objects(array):- List of primary keys of the selected objects
- Required when
select_acrossis0 - Ignored when
select_acrossis1
The preview endpoint returns information about what will happen when the action is executed:
{
"name": "Action Name",
"description": "Description of the action",
// ... additional action-specific data
}For the built-in delete_selected action:
{
"name": "Delete selected objects",
"description": "Delete selected objects",
"deletable_objects": ["Object 1", "Object 2", ["Related Object 1", "Related Object 2"]],
"model_count": {
"articles": "5",
"comments": "12"
},
"perms_needed": [],
"protected": []
}Fields:
name: Display name of the actiondescription: Description of what the action doesdeletable_objects: Nested list of objects that will be deleted (for delete action)model_count: Dictionary mapping model names to count of objects affectedperms_needed: List of required permissions that the user lacksprotected: List of protected objects that cannot be deleted
The execute endpoint returns a success message after performing the action:
{
"messages": ["Successfully deleted 5 Articles."]
}The adapter includes a built-in implementation of Django's delete_selected action.
Preview Example:
POST /api/blog/article/action/delete_selected/preview/
Content-Type: application/json
{
"select_across": 0,
"selected_objects": [1, 2, 3]
}Preview Response:
{
"name": "Delete selected objects",
"description": "Delete selected objects",
"deletable_objects": [
"Article: My First Post",
"Article: Second Post",
"Article: Third Post",
[
"Comment: Great article!",
"Comment: Thanks for sharing"
]
],
"model_count": {
"articles": "3",
"comments": "2"
},
"perms_needed": [],
"protected": []
}Execute Example:
POST /api/blog/article/action/delete_selected/execute/
Content-Type: application/json
{
"select_across": 0,
"selected_objects": [1, 2, 3]
}Execute Response:
{
"messages": ["Successfully deleted 3 Articles."]
}To create custom actions that work with the adapter, you must follow a specific signature:
from django.contrib import admin
from rest_framework.response import Response
from rest_framework import status
from django.utils import timezone
from myapp.models import Article
class ArticleAdmin(admin.ModelAdmin):
actions = ['publish_articles', 'archive_articles']
def publish_articles(self, request, queryset, confirm=False):
"""
Custom action to publish selected articles.
Args:
request: The HTTP request object
queryset: QuerySet of selected objects
confirm: Boolean indicating preview (False) or execute (True)
Returns:
Response object with appropriate data
"""
if not confirm:
# Preview mode - return information about what will happen
count = queryset.count()
unpublished = queryset.filter(status='draft').count()
return Response(
data={
"name": "Publish Articles",
"description": f"Publish {unpublished} draft articles",
"total_selected": count,
"will_be_published": unpublished,
"already_published": count - unpublished,
},
status=status.HTTP_200_OK,
)
# Execute mode - perform the actual action
updated = queryset.filter(status='draft').update(
status='published',
published_at=timezone.now()
)
return Response(
data={
"messages": [f"Successfully published {updated} articles."]
},
status=status.HTTP_200_OK,
)
publish_articles.short_description = "Publish selected articles"
def archive_articles(self, request, queryset, confirm=False):
"""Archive selected articles."""
if not confirm:
return Response(
data={
"name": "Archive Articles",
"description": "Move selected articles to archive",
"count": queryset.count(),
"warning": "Archived articles will not be visible to users",
},
status=status.HTTP_200_OK,
)
queryset.update(status='archived', archived_at=timezone.now())
return Response(
data={
"messages": [f"Successfully archived {queryset.count()} articles."]
},
status=status.HTTP_200_OK,
)
archive_articles.short_description = "Archive selected articles"
admin.site.register(Article, ArticleAdmin)Important
- All actions must accept a
confirmparameter as the last argument - All actions must return a
rest_framework.response.Responseobject - Preview mode (
confirm=False) should return descriptive data without modifying objects - Execute mode (
confirm=True) should perform the actual operation and return success messages
The adapter handles various error conditions:
No objects selected:
{
"messages": ["No objects were selected."]
}Status: 400 Bad Request
Action not found:
Status: 404 Not Found
Permission denied:
Status: 403 Forbidden
Malformed data:
{
"messages": ["Malformed data provided (1)."]
}Status: 400 Bad Request
Select Across Example:
When select_across is set to 1, the action applies to all objects matching the current filter:
POST /api/blog/article/action/publish_articles/preview/?status=draft
Content-Type: application/json
{
"select_across": 1,
"selected_objects": []
}This will preview publishing all draft articles, not just selected ones.
Step 1: Get available actions
GET /api/blog/article/info/Response includes available actions:
{
"actions": [
["", "---------"],
["delete_selected", "Delete selected objects"],
["publish_articles", "Publish selected articles"],
["archive_articles", "Archive selected articles"]
],
// ... other data
}Step 2: Preview the action
POST /api/blog/article/action/publish_articles/preview/
Content-Type: application/json
{
"select_across": 0,
"selected_objects": [1, 2, 3, 4, 5]
}Step 3: Review the preview response
{
"name": "Publish Articles",
"description": "Publish 3 draft articles",
"total_selected": 5,
"will_be_published": 3,
"already_published": 2
}Step 4: Execute the action
POST /api/blog/article/action/publish_articles/execute/
Content-Type: application/json
{
"select_across": 0,
"selected_objects": [1, 2, 3, 4, 5]
}Step 5: Receive confirmation
{
"messages": ["Successfully published 3 articles."]
}The Django Admin Adapter provides custom list filter classes that extend Django's built-in filtering capabilities with API-friendly features. These filters enable advanced filtering options including text input, numeric ranges, date/datetime ranges, and autocomplete-based filtering.
Simple ListFilter classes still are working out of the box,
but Autocomplete and input filters (date, datetime, integer, float, str)
must be implemented from the package's ones.
The adapter includes three main filter types:
- InputFilter - Base class for text, number, date, and datetime input filters (better use helper functions)
- AutocompleteFilter - Autocomplete-based filtering using related models
- Helper Functions -
build_input_filter()andget_range_filters()for easy filter creation
All filters work seamlessly with the Django admin list view and are automatically exposed through the API.
The filter_autocomplete and filter_autocomplete_retrieve_label views are used
for the AutocompleteFilters.
InputFilter is a base class that extends admin.SimpleListFilter to provide input-based filtering instead of predefined choices.
Warning
It is best to create input filters using the
build_input_filter and get_range_filters helper functions.
Key Features:
- Supports multiple field types:
str,int,float,date,datetime - Configurable HTML input attributes (min, max, step, placeholder)
- Automatic type conversion and validation
- Custom lookup operators
Supported Field Types:
| Python Type | HTML Input Type | Example Use Case |
|---|---|---|
str |
text |
Search by name, email, description |
int |
number |
Filter by ID, count, quantity |
float |
number |
Filter by price, rating, percentage |
date |
date |
Filter by birth date, deadline |
datetime |
datetime-local |
Filter by created_at, updated_at |
Date/Datetime Formats:
- Date format:
YYYY-MM-DD(e.g.,2024-12-13) - Datetime format:
YYYY-MM-DDTHH:MM(e.g.,2024-12-13T15:30)
AutocompleteFilter provides autocomplete-based filtering using related models. It leverages the adapter's autocomplete API endpoints to provide a searchable dropdown interface.
Important
- All filter parameter names must be unique within a ModelAdmin!
AutocompleteFilterrequires the related model to havesearch_fieldsdefined!AutocompleteFilterrequires the related model admin's view permission!
Required Attributes:
title: Display title for the filterparameter_name: URL parameter name for the filterrelated_model: The Django model to search againstrelation_query_lookup: The lookup query connecting the related model to the current model
Optional Attributes:
disabled_by_default: Whether the filter is disabled by default (default:False)filter_placeholder: Placeholder text for the autocomplete input
Example:
from django.contrib import admin
from django_admin_adapter.filters import AutocompleteFilter
from myapp.models import Article, Author
class AuthorFilter(AutocompleteFilter):
title = "Author"
parameter_name = "author"
related_model = Author
relation_query_lookup = "author__id"
filter_placeholder = "Search for author..."
class ArticleAdmin(admin.ModelAdmin):
list_filter = [AuthorFilter]
admin.site.register(Article, ArticleAdmin)Requirements:
- The
related_modelmust be registered in the admin site - The
related_model's ModelAdmin must havesearch_fieldsdefined - The user must have
viewpermission on therelated_model's ModelAdmin/Model - The
relation_query_lookupmust be a valid Django ORM lookup
How It Works:
- The filter generates an autocomplete URL:
/api/autocomplete/<model_name>/ - When a user types, the API searches the related model using its
search_fields - Selected values are used to filter the main queryset via the
relation_query_lookup
The build_input_filter() function creates custom InputFilter classes dynamically.
Function Signature:
def build_input_filter(
field_name: str,
title: str,
field_type: str | int | float | date | datetime,
min_value = None,
max_value = None,
lookup_operator: str = "__icontains",
parameter_name: str | None = None,
disabled_by_default: bool = False,
placeholder: str = "",
) -> InputFilter:Parameters:
field_name: Database field name to filter ontitle: Display title for the filterfield_type: Python type (str,int,float,date,datetime)min_value: Minimum value/length for HTML input validationmax_value: Maximum value/length for HTML input validationlookup_operator: Django ORM lookup operator (default:__icontains)parameter_name: Custom URL parameter name (default: same asfield_name)disabled_by_default: Whether filter is disabled by default (relates to visibility in front-end not functionallity)placeholder: Placeholder text for the input field
Examples:
Text Filter:
from django.contrib import admin
from django_admin_adapter.filters import build_input_filter
from myapp.models import Article
class ArticleAdmin(admin.ModelAdmin):
list_filter = [
build_input_filter(
field_name="title",
title="Title Contains",
field_type=str,
lookup_operator="__icontains",
placeholder="Search in title...",
),
]
admin.site.register(Article, ArticleAdmin)Number Filter:
class ProductAdmin(admin.ModelAdmin):
list_filter = [
build_input_filter(
field_name="price",
title="Price Greater Than",
field_type=float,
min_value=0,
max_value=10000,
lookup_operator="__gte",
placeholder="Minimum price",
),
]Date Filter:
class OrderAdmin(admin.ModelAdmin):
list_filter = [
build_input_filter(
field_name="created_at",
title="Created After",
field_type=date,
lookup_operator="__gte",
),
]Datetime Filter:
class LogEntryAdmin(admin.ModelAdmin):
list_filter = [
build_input_filter(
field_name="timestamp",
title="Timestamp After",
field_type=datetime,
lookup_operator="__gte",
),
]The get_range_filters() function creates a pair of filters for minimum and maximum values, perfect for numeric and date/datetime ranges.
Function Signature:
def get_range_filters(
field_name: str,
title: str,
field_type: int | float | date | datetime,
min_value = None,
max_value = None,
parameter_name: str | None = None,
disabled_by_default: bool = False,
placeholder: str = "",
) -> tuple[InputFilter, InputFilter]:Warning
for parameter_name do not use max/min prefix/suffix
It will be appointed automatically.
E.g. instead of price__min or price__max use price
Returns: A tuple of two InputFilter classes:
- Minimum filter (uses
__gtelookup) - Maximum filter (uses
__ltelookup)
Automatic Naming:
- Minimum filter:
<title> Minimumwith parameter<parameter_name>__min - Maximum filter:
<title> Maximumwith parameter<parameter_name>__max
Examples:
Price Range:
from django.contrib import admin
from django_admin_adapter.filters import get_range_filters
from myapp.models import Product
class ProductAdmin(admin.ModelAdmin):
list_filter = [
*get_range_filters(
field_name="price",
title="Price",
field_type=float,
min_value=0,
max_value=10000,
placeholder="Enter price",
),
]
admin.site.register(Product, ProductAdmin)This creates two filters:
- "Price Minimum" - filters
price__gte - "Price Maximum" - filters
price__lte
Date Range:
class OrderAdmin(admin.ModelAdmin):
list_filter = [
*get_range_filters(
field_name="order_date",
title="Order Date",
field_type=date,
),
]This creates:
- "Order Date Minimum" - filters
order_date__gte - "Order Date Maximum" - filters
order_date__lte
Integer Range:
class ArticleAdmin(admin.ModelAdmin):
list_filter = [
*get_range_filters(
field_name="view_count",
title="View Count",
field_type=int,
min_value=0,
),
]E-commerce Product Admin:
from django.contrib import admin
from datetime import date, datetime
from django_admin_adapter.filters import (
build_input_filter,
get_range_filters,
AutocompleteFilter,
)
from myapp.models import Product, Category, Manufacturer
class CategoryFilter(AutocompleteFilter):
title = "Category"
parameter_name = "category"
related_model = Category
relation_query_lookup = "category__id"
filter_placeholder = "Search categories..."
class ManufacturerFilter(AutocompleteFilter):
title = "Manufacturer"
parameter_name = "manufacturer"
related_model = Manufacturer
relation_query_lookup = "manufacturer__id"
class ProductAdmin(admin.ModelAdmin):
list_display = ['name', 'sku', 'price', 'stock', 'category', 'created_at']
search_fields = ['name', 'sku', 'description']
list_filter = [
# Autocomplete filters
CategoryFilter,
ManufacturerFilter,
# Text search
build_input_filter(
field_name="sku",
title="SKU",
field_type=str,
lookup_operator="__icontains",
placeholder="Enter SKU...",
),
# Price range
*get_range_filters(
field_name="price",
title="Price",
field_type=float,
min_value=0,
placeholder="Price",
),
# Stock range
*get_range_filters(
field_name="stock",
title="Stock Quantity",
field_type=int,
min_value=0,
),
# Date range
*get_range_filters(
field_name="created_at",
title="Created Date",
field_type=date,
),
]
admin.site.register(Product, ProductAdmin)Blog Article Admin:
from django.contrib import admin
from datetime import datetime
from django_admin_adapter.filters import (
build_input_filter,
get_range_filters,
AutocompleteFilter,
)
from myapp.models import Article, Author, Tag
class AuthorFilter(AutocompleteFilter):
title = "Author"
parameter_name = "author"
related_model = Author
relation_query_lookup = "author__id"
class ArticleAdmin(admin.ModelAdmin):
list_display = ['title', 'author', 'status', 'published_at', 'view_count']
search_fields = ['title', 'content']
list_filter = [
'status', # Standard Django choice filter
AuthorFilter,
# Title search
build_input_filter(
field_name="title",
title="Title Contains",
field_type=str,
lookup_operator="__icontains",
),
# View count range
*get_range_filters(
field_name="view_count",
title="Views",
field_type=int,
min_value=0,
),
# Published date range
*get_range_filters(
field_name="published_at",
title="Published",
field_type=datetime,
),
# Word count filter
build_input_filter(
field_name="word_count",
title="Minimum Words",
field_type=int,
lookup_operator="__gte",
min_value=0,
),
]
admin.site.register(Article, ArticleAdmin)When you call the list info endpoint, filters are included in the response. See the swagger for the detailed response format.
# Filter by author
GET /api/blog/article/?author=5
# Filter by title
GET /api/blog/article/?title=django
# Filter by view count range
GET /api/blog/article/?view_count__min=100&view_count__max=1000
# Filter by published date range
GET /api/blog/article/?published_at__min=2024-01-01T00:00&published_at__max=2024-12-31T23:59
# Combine multiple filters
GET /api/blog/article/?author=5&status=published&view_count__min=100The adapter provides built-in rate limiting (throttling) capabilities to protect your API from abuse. By default, all endpoints use throttle classes that differentiate between anonymous and authenticated users.
The adapter uses two default throttle classes:
- AdminAnonRateThrottle: Applied to anonymous (unauthenticated) requests
- AdminUserRateThrottle: Applied to authenticated user requests
No throttling is provided for the authentication views.
To configure throttling for the authentication endpoints
provide your throttle class with the authentication_throttle_class argument.
These classes extend Django REST Framework's AnonRateThrottle and UserRateThrottle
respectively, and their rate limits can be configured in your Django settings.
To configure the default throttle rates, add the following to your Django settings:
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_RATES': {
'anon': '100/hour', # Rate for AdminAnonRateThrottle
'user': '1000/hour', # Rate for AdminUserRateThrottle
}
}You can use different time periods: second, minute, hour, or day.
Note
By default Views of the default django_admin_adapter.VIEWMAP have no Throttles set,
and they are set dynamically when building the site's urls. In that case the throttles
are set if the viewmap's throttles classes list is None.
That state is depended from the extra_views argument that overrides
any viewmap with the same name.
You can override the default throttle classes globally for all adapter endpoints and authentication's by passing custom throttle classes during adapter instantiation:
from django_admin_adapter import AdminAPIAdapter
from django.contrib import admin
from rest_framework.throttling import AnonRateThrottle, UserRateThrottle
class CustomAnonThrottle(AnonRateThrottle):
rate = '50/hour'
class CustomUserThrottle(UserRateThrottle):
rate = '500/hour'
class CustomAuthThrottle(UserRateThrottle):
rate = '500/hour'
admin_adapter = AdminAPIAdapter(
admin.site,
default_anon_throttle_class=CustomAnonThrottle,
default_user_throttle_class=CustomUserThrottle,
authentication_throttle_class=CustomAuthThrottle,
)Parameters:
-
default_anon_throttle_class: A subclass ofrest_framework.throttling.BaseThrottlethat will be applied to anonymous requests -
default_user_throttle_class: A subclass ofrest_framework.throttling.BaseThrottlethat will be applied to authenticated user requests
You can also set custom throttle classes for specific views using the extra_views
parameter. This overrides the default throttle classes for that particular view:
from django_admin_adapter import AdminAPIAdapter
from django.contrib import admin
from myapp.views import CustomDashboardView
from myapp.throttling import DashboardAnonThrottle, DashboardUserThrottle
admin_adapter = AdminAPIAdapter(
admin.site,
extra_views={
"custom_dashboard": (
"dashboard/",
CustomDashboardView,
[DashboardAnonThrottle, DashboardUserThrottle], # Custom throttles
),
},
)The throttle classes list must contain exactly two classes:
- First class: Applied to anonymous requests
- Second class: Applied to authenticated user requests
If you pass None instead of a list, the view will use the default throttle classes.
from django_admin_adapter import AdminAPIAdapter
from django.contrib import admin
from rest_framework.throttling import AnonRateThrottle, UserRateThrottle
from myapp.views import ReportsView
from myapp.throttling import ReportsAnonThrottle, ReportsUserThrottle
# Define custom global throttle classes
class StrictAnonThrottle(AnonRateThrottle):
rate = '20/hour'
class StrictUserThrottle(UserRateThrottle):
rate = '200/hour'
admin_adapter = AdminAPIAdapter(
admin.site,
# Set global default throttle classes
default_anon_throttle_class=StrictAnonThrottle,
default_user_throttle_class=StrictUserThrottle,
# Override throttle classes for specific views
extra_views={
"reports": (
"reports/",
ReportsView,
[ReportsAnonThrottle, ReportsUserThrottle], # View-specific throttles
),
# -------------------------------------------
# override an adapter view throttles !!!
"admin_list_create": (
"<str:app_name>/<str:model_name>/",
django_admin_adapter.views.list.AdminListCreateAPIView,
[ReportsAnonThrottle, ReportsUserThrottle], # View-specific throttles
),
},
)The adapter validates throttle classes during initialization and will raise
AdminAPIAdapterError if:
- The provided class is not actually a class
- The class is not a subclass of
rest_framework.throttling.BaseThrottle - Per-view throttle classes are not provided as a list or tuple of exactly 2 classes
For more information on throttling in Django REST Framework, see the DRF Throttling documentation.
The adapter uses three main serializer classes to format data returned by the API endpoints. You can customize these serializers freely.
The adapter provides three default serializer classes:
- object_serializer_class:
AdminObjectViewSerializer- Used for object detail views and readonly field rendering - list_serializer_class:
AdminListSerializer- Used for list views and object listings - history_serializer_class:
AdminHistorySerializer- Used for object history views
These serializers handle the conversion of Django model instances into JSON-serializable data structures that the frontend can consume.
To override the default serializer classes, pass the corresponding serializer to the constructor as kwarg:
from django_admin_adapter import AdminAPIAdapter
from django_admin_adapter.serializers import (
AdminListSerializer,
AdminObjectViewSerializer,
AdminHistorySerializer,
)
from django.contrib import admin
# Create custom serializers
class CustomObjectSerializer(AdminObjectViewSerializer):
def to_representation(self, instance):
data = super().to_representation(instance)
# Add custom field
data['custom_field'] = 'custom_value'
return data
class CustomListSerializer(AdminListSerializer):
def to_representation(self, instance):
data = super().to_representation(instance)
# Modify list representation
data['display_name'] = str(instance).upper()
return data
class CustomHistorySerializer(AdminHistorySerializer):
def to_representation(self, instance):
data = super().to_representation(instance)
# Add custom history formatting
data['formatted_date'] = instance.action_time.strftime('%B %d, %Y')
return data
# Use the custom adapter
admin_adapter = AdminAPIAdapter(
admin.site,
object_serializer_class=CustomObjectSerializer,
list_serializer_class=CustomListSerializer,
history_serializer_class=CustomHistorySerializer,
)
urlpatterns = [
path("api/", include(admin_adapter.get_urls())),
]The base_permission_class class attribute controls the default permission checking mechanism for all API endpoints exposed by the adapter.
This allows you to customize who can access your admin API endpoints beyond the default staff-only requirement.
By default, the adapter uses AdminSiteBasePermission as the base permission class for all non-authentication views. This permission class is automatically appended to any existing permission_classes defined in your custom views, ensuring consistent access control across your admin API.
The permission class is applied during URL pattern generation in the build_view_path method, which means:
- Authentication views (
token_obtain_pair,token_refresh) do NOT receive the base permission class - All other views automatically receive the base permission class in addition to any view-specific permissions
The default base_permission_class is AdminSiteBasePermission, which is defined as:
from rest_framework.permissions import BasePermission
class AdminSiteBasePermission(BasePermission):
"""
Delegate basic permission handling to adapter's
`AdminSite.has_permission`.
"""
def has_permission(self, request, view):
return view._admin_site.has_permission(request)This class delegates permission checking to Django's admin site has_permission method,
which by default requires:
- The user is authenticated
- The user has
is_active=True - The user has
is_staff=True
You can pass the permission class as a keyword argument during instantiation:
from django_admin_adapter import AdminAPIAdapter
from django.contrib import admin
admin_adapter = AdminAPIAdapter(
admin.site,
base_permission_class=SuperuserOnlyPermission,
)from django_admin_adapter import AdminAPIAdapter
from django.contrib import admin
from rest_framework.permissions import BasePermission
class AdminGroupPermission(BasePermission):
"""
Only allow users in the 'Admin' group to access the API.
"""
def has_permission(self, request, view):
return (
request.user.is_authenticated and
request.user.is_active and
request.user.is_staff and
request.user.groups.filter(name='Admin').exists()
)
admin_adapter = AdminAPIAdapter(
admin.site,
base_permission_class=AdminGroupPermission,
)
urlpatterns = [
path("api/", include(admin_adapter.get_urls())),
]from django_admin_adapter import AdminAPIAdapter
from django.contrib import admin
from rest_framework.permissions import BasePermission
class InternalIPPermission(BasePermission):
"""
Only allow requests from internal IP addresses.
"""
ALLOWED_IPS = ['192.168.1.0/24', '10.0.0.0/8']
def has_permission(self, request, view):
if not request.user or not request.user.is_authenticated:
return False
# Check if user is authenticated and staff
if not request.user.is_staff:
return False
# Additional IP check
ip = self.get_client_ip(request)
return self.is_ip_allowed(ip)
def get_client_ip(self, request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
def is_ip_allowed(self, ip):
# Implement IP checking logic
# This is a simplified example
return ip.startswith('192.168.') or ip.startswith('10.')
admin_adapter = AdminAPIAdapter(
admin.site,
base_permission_class=InternalIPPermission,
)Important
- The
base_permission_classis automatically appended to existingpermission_classeson views, not replaced - Authentication views (
token_obtain_pair,token_refresh) do not receive the base permission class - If a custom view in
extra_viewsalready haspermission_classesdefined, the base permission will be added to that list - The permission class must be a subclass of
rest_framework.permissions.BasePermission
Warning
- Changing the
base_permission_classaffects all non-authentication endpoints in your admin API - Make sure your custom permission class properly checks authentication status before performing additional checks
The adapter allows you to provide custom extra data in various API endpoints by implementing specific methods in your ModelAdmin classes or by overriding the adapter's method. This enables you to extend the default API responses with application-specific information.
Extra data methods are called by the adapter's views when building API responses. These methods should return JSON-serializable data (dictionaries, lists, strings, numbers, etc.) that will be included in the response under the extra_data key.
You can add the following methods to your ModelAdmin subclasses to provide extra data for specific views:
Called by AdminListInfoAPIView when retrieving list view information (filters, actions, etc.).
Parameters:
request: The current HTTP request object
Returns: JSON-serializable data or None
Example:
from django.contrib import admin
from myapp.models import Article
class ArticleAdmin(admin.ModelAdmin):
def get_list_extra_data(self, request):
return {
"total_published": Article.objects.filter(status='published').count(),
"total_drafts": Article.objects.filter(status='draft').count(),
"categories": list(Article.objects.values_list('category', flat=True).distinct()),
}
admin.site.register(Article, ArticleAdmin)Response location: /api/<app_name>/<model_name>/info/ → extra_data
Called by AdminObjectViewAPIView when retrieving object view data.
Parameters:
request: The current HTTP request objectobj: The model instance being viewed
Returns: JSON-serializable data or None
Example:
from django.contrib import admin
from myapp.models import Article
class ArticleAdmin(admin.ModelAdmin):
def get_view_extra_data(self, request, obj):
return {
"view_count": obj.views,
"last_modified_by": str(obj.modified_by) if obj.modified_by else None,
"related_articles": [
{"id": a.id, "title": a.title}
for a in Article.objects.filter(category=obj.category).exclude(id=obj.id)[:5]
],
}
admin.site.register(Article, ArticleAdmin)Response location: /api/<app_name>/<model_name>/<pk>/view/ → extra_data
Called by AdminObjectAddAPIView when retrieving the add form data.
Parameters:
request: The current HTTP request object
Returns: JSON-serializable data or None
Example:
from django.contrib import admin
from myapp.models import Article
class ArticleAdmin(admin.ModelAdmin):
def get_add_extra_data(self, request):
return {
"default_author": request.user.id,
"available_templates": ["standard", "featured", "news"],
"suggested_tags": ["technology", "business", "lifestyle"],
}
admin.site.register(Article, ArticleAdmin)Response location: /api/<app_name>/<model_name>/add/ → extra_data
Called by AdminObjectEditAPIView when retrieving the edit form data.
Parameters:
request: The current HTTP request objectobj: The model instance being edited
Returns: JSON-serializable data or None
Example:
from django.contrib import admin
from myapp.models import Article
class ArticleAdmin(admin.ModelAdmin):
def get_edit_extra_data(self, request, obj):
return {
"edit_history_count": obj.revisions.count(),
"last_editor": str(obj.modified_by) if obj.modified_by else None,
"locked_by": str(obj.locked_by) if hasattr(obj, 'locked_by') and obj.locked_by else None,
"word_count": len(obj.content.split()) if obj.content else 0,
}
admin.site.register(Article, ArticleAdmin)Response location: /api/<app_name>/<model_name>/<pk>/edit/ → extra_data
Called by AdminObjectConfirmDeleteAPIView when retrieving delete confirmation data.
Parameters:
request: The current HTTP request objectobj: The model instance being deleted
Returns: JSON-serializable data or None
Example:
from django.contrib import admin
from myapp.models import Article
class ArticleAdmin(admin.ModelAdmin):
def get_delete_extra_data(self, request, obj):
return {
"warning": "This article has been published and has comments.",
"comments_count": obj.comments.count(),
"can_archive_instead": True,
"archive_url": f"/archive/{obj.id}/",
}
admin.site.register(Article, ArticleAdmin)Response location: /api/<app_name>/<model_name>/<pk>/delete/ → extra_data
Called by AdminBaseInfoAPIView when retrieving base information (sidebar, profile, etc.).
Parameters:
request: The current HTTP request object
Returns: JSON-serializable data or None
Example:
from django_admin_adapter import AdminAPIAdapter
from django.contrib import admin
class CustomAdminAPIAdapter(AdminAPIAdapter):
def get_extra_base_info_data(self, request):
return {
"app_version": "2.1.0",
"environment": "production",
"company_name": "My Company",
"support_email": "support@mycompany.com",
"features": {
"dark_mode": True,
"notifications": True,
"analytics": request.user.is_superuser,
},
}
admin_adapter = CustomAdminAPIAdapter(admin.site)Response location: /api/base_info/ → extra
If an extra data method raises an exception, the adapter will not catch it, and the request will fail. Make sure to handle exceptions within your methods:
def get_list_extra_data(self, request):
try:
# Your logic here
return {"data": "value"}
except Exception as e:
# Log the error
logger.error(f"Error in get_list_extra_data: {e}")
# Return None or safe default
return NoneFollow the steps below, to run the project (admin + admin api)
ASSUMPTION: you are using a linux machine, don't know how it'll go on windows.
Before continuing, cd on the /src directory of the cloned repository and run the command below on terminal:
mkdir -p data/admin/media data/admin/static data/admin_api/static data/db data/redis- make sure you have docker properly installed on your machine
- edit the
src/.envfile and changeDOCKER_USERto your username andDOCKER_USER_IDto your user's system id ( runcat /etc/groupin your terminal andwhomai) - run
docker compose build - run
docker compose up - run
docker compose admin python3 manage.py init_dbto initialize the database
If all the steps went ok, then the old-school admin will be available in the localhost:8000
and the api will be available on localhost:8888/api base url.
You can log in the traditional admin with the credentials: username: superuser (or pilot) password: asd!@#123
If you cloned the React project, then running npm start will bring up the React app, and you will be able to log in
the API version of the admin from the localhost:3000
Many ideas and concepts can be implemented in this library. The most propable depending on the community's interest:
- Create mass edit action available from the library.
- Enable prepopulated fields in Add View, depending on GET query parameters.
- Implement more scrolling at autocomplete search.
- Create automatic swagger for all model admins.
- Create automatic swagger for all actions.
If interested with contribution and development with some of these features please contact me freely.
Creating issues wherever bugs are found and giving suggestions for upcoming versions can surely help in maintaining and growing this package.