- Description
- Requirements
- Installation Instructions
- Quick Actions
- Languages Dropdown
- Intermediate Pages and Model Action Tools
- Tools for admin Querysets/Filters
- Tools for admin search/filters
- Widgets
- Settings
This Package is a collection of extensions/tools for the default django administration interface, it includes:
- Quick Actions
- Languages Dropdown Interface
- Intermediate Pages and model actions tools
- Tools for admin
Querysets
/Filters
optemization (helps avoiding N+1 issue) - Tools for admin
search
/filters
- Widgets
Full documentation is avilable on Github Repo
Before you begin, ensure you have met the following requirements:
- Python 3.8+
- Django >= 3.2
You don't need this source code unless you want to modify the package. If you just want to use the package, just run:
pip install django-admin-performance-tools
Add 'django_admin_performance_tools' to your INSTALLED_APPS settings.
INSTALLED_APPS = [
"django_admin_performance_tools",
...
]
Must be added on the top of the list
in settings.py
update TEMPLATES
TEMPLATES = [
{
...
'DIRS': ["templates"],
'OPTIONS': {
'context_processors': [
...
"django_admin_performance_tools.context_processors.settings",
],
},
},
]
Quick Actions is a new feature that allows you to take actions quickly from the admin home page, it is the same as actions in the model admin page, but the main differences are:
- It is not attached to any model
- Actions acts like views but only supports (
POST
,GET
) http methods - Support permissions, so you can write your own logic to control who can see the action
- Form View Action is introduced to enables you to create an action to render any form (It is implemented on top of django FormView and CreateView)
- Wizard Form View Action is introduced to enables you to create multi-step forms (It is implemented on top of django-formtools)
- Template View Action is introduced to enables you to render your own templates
- Base View Action is introduced to enables you to create customized actions as you want
Setup
in settings.py
replace django.contrib.admin
by django_admin_performance_tools.sites.MainAdminConfig
in your INSTALLED_APPS.
INSTALLED_APPS = [
"django_admin_performance_tools",
"django_admin_performance_tools.sites.MainAdminConfig",
...
]
What if you already implemented your own Admin site? all you've to do is to inherit from AbstractAdminSiteMixin
Example:
from django_admin_performance_tools.sites import AbstractAdminSiteMixin
class YourAdmin(AbstractAdminSiteMixin, AdminSite):
"""Your Admin Site"""
by doing that you don't need to replace your admin site in INSTALLED_APPS
Form View Quick Action is used to create an action to render a form (It is implemented on top of django FormView)
Example:
from django_admin_performance_tools.quick_actions import FormViewQuickAction
from django_admin_performance_tools.quick_actions.registry import register_quick_action
@register_quick_action()
class FormAction(FormViewQuickAction):
name = "My Form Action"
form_class = MyForm
def post(self, request, *args, **kwargs):
# Write your logic here
return super().post(request, *args, **kwargs)
-To customize submit button name, you can set submit_button_value
attribute in the FormAction
class
-To customize success redirection of the form you can set success_url
attribute or override get_success_url
function (Default is redirect to the action page).
Create View Quick Action is used to create an action to render a model form (It is implemented on top of django CreateView)
Example:
from django_admin_performance_tools.quick_actions import CreateViewQuickAction
from django_admin_performance_tools.quick_actions.registry import register_quick_action
from .models import MyModel
@register_quick_action()
class CreateFormAction(CreateViewQuickAction):
name = "My Craete Form Action"
model = model
fields = ["name"]
-To customize submit button name, you can set submit_button_value
attribute in the CreateFormAction
class
-To customize success redirection of the form you can set success_url
attribute or override get_success_url
function (Default is redirect to the action page).
Wizard Form View Quick Action is implemented on top of django-formtools library, and it is used to create an action to render a wizard form.
setup
django-formtools
library is required. to install it use the following pip command:
pip install django-formtools
Add 'formtools' to your INSTALLED_APPS settings.
INSTALLED_APPS = [
...
"formtools",
]
Example:
from formtools.wizard.views import SessionWizardView
from django_admin_performance_tools.quick_actions import WizardFormViewQuickAction
from django_admin_performance_tools.quick_actions.registry import register_quick_action
@register_quick_action()
class WizardFormAction(WizardFormViewQuickAction, SessionWizardView):
name = "My Wizard Form Action"
form_list = [Form1, Form2, Form3]
def done(self, form_list, **kwargs):
# form_list[0].cleaned_data
# form_list[1].cleaned_data
# form_list[2].cleaned_data
# Continue writing your logic here
return super().done(form_list, **kwargs)
And that's it 🎉
-To customize last step button name, you can set submit_button_value
attribute in the WizardFormAction
class
-To customize done()
function redirection you can set success_url
attribute or override get_success_url
function (Default is redirect to the action page).
Template View Quick Action is used to create an action to render a template (It is implemented on top of django TemplateView)
Example:
from django_admin_performance_tools.quick_actions import TemplateViewQuickAction
from django_admin_performance_tools.quick_actions.registry import register_quick_action
@register_quick_action()
class TemplateAction(TemplateViewQuickAction):
name = "My Template Action"
template_name = "my_template.html"
Notes
-The template file you created (my_template.html
) must extends from base_quick_action.html
{% extends 'admin/quick_actions/base_quick_action.html' %}
{% block action_body %}
# Write your own HTML
{% endblock %}
QuickAction is used to create a custom action, that means you will've to implement get()
, post()
yourself (It is implemented on top of django View)
Example:
from django_admin_performance_tools.quick_actions import QuickAction
from django_admin_performance_tools.quick_actions.registry import register_quick_action
@register_quick_action()
class CustomAction(QuickAction):
name = "My custom Action"
def get(self, request, *args, **kwargs):
# Write your own logic here
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
# Write your own logic here
return super().post(request, *args, **kwargs)
def get_context_data(self, **kwargs):
super().get_context_data(**kwargs)
return {
# Pass your extra context here
**super().get_context_data(**kwargs),
}
You can override get_context_data()
function to pass extra context values
Quick actions acts like views so you can set permission_required
attribute or override get_permission_required()
, has_permission()
methods to control who can see the actions.
Example:
from django_admin_performance_tools.quick_actions import TemplateViewQuickAction
from django_admin_performance_tools.quick_actions.registry import register_quick_action
@register_quick_action()
class TemplateAction(TemplateViewQuickAction):
name = "My Template Action"
template_name = "my_template.html"
permission_required = ("myapp.add_mymodel")
def has_permission(self):
# Write your own logic here
return super().has_permission()
the previous example will check the following by default:
- The user has add permission on MyModel View
- The user is active and is staff
Notes
- On overriding
has_permission()
you must callsuper()
. Default check the useris_active=True
andis_staff=True
permission_required
attribute default value isNone
@register_quick_action()
decorator register the action to all admin sites, so if you need to register an action to a specific site you can pass sites=[site1, site2, site3]
to the decorator
Example:
from django.contrib import admin
from django_admin_performance_tools.quick_actions import TemplateViewQuickAction
from django_admin_performance_tools.quick_actions.registry import register_quick_action
from .my_sites import site2, site3
@register_quick_action(sites=[admin.site, site2, site3])
class TemplateAction(TemplateViewQuickAction):
name = "My Template Action"
template_name = "my_template.html"
def has_permission(self):
# Write your own logic here
return super().has_permission()
You can define messges to be displayed to the user after form submission
post_success_message
attribute is used forpost
requests
Example:
from django.contrib import admin
from django_admin_performance_tools.quick_actions import TemplateViewQuickAction
from django_admin_performance_tools.quick_actions.registry import register_quick_action
from .my_sites import site2, site3
@register_quick_action()
class TemplateAction(TemplateViewQuickAction):
name = "My Template Action"
template_name = "my_template.html"
post_success_message = "Data Submitted Successfully"
# NOTE: Also you can override the messages using the following method
# You can access the current request by using self.request
def get_post_success_message(self):
# Write your own logic here
Notes
1- All actions must be added/imported in admin.py as to be detected and registered.
url_path
is the URL path of an action, if not set the package will create a url path dynamically from the action class's name
path_name
is the URL path name of an action, if not set the package will create a url path name dynamically from the action class's name
Example 1:
from django.contrib import admin
from django_admin_performance_tools.quick_actions import TemplateViewQuickAction
from django_admin_performance_tools.quick_actions.registry import register_quick_action
from .my_sites import site2, site3
@register_quick_action()
class CreateUserTemplate(TemplateViewQuickAction):
name = "My Template Action"
template_name = "my_template.html"
url_path
value will bewww.mysite.com/admin/quick-actions/create-user-template
path_name
value will bequick-actions-create-user-template
Example 2:
from django.contrib import admin
from django_admin_performance_tools.quick_actions import TemplateViewQuickAction
from django_admin_performance_tools.quick_actions.registry import register_quick_action
from .my_sites import site2, site3
@register_quick_action()
class CreateUserTemplate(TemplateViewQuickAction):
name = "My Template Action"
url_path = "my-template"
path_name = "my-template"
template_name = "my_template.html"
url_path
value will bewww.mysite.com/admin/quick-actions/my-template
path_name
value will bequick-actions-my-template
This will show a dropdown menu in the admin pages that allows you change the site language easily, so all you have to do is to add the following URLs in the main root urls.py
file
Setup
in the root urls.py
add the following:
from django.urls import include, path
urlpatterns = [
path("i18n/", include("django.conf.urls.i18n")),
...
]
Add 'django_admin_performance_tools' to your INSTALLED_APPS setting.
in settings.py
add django.middleware.locale.LocaleMiddleware
to the MIDDLEWARE
MIDDLEWARE = [
...
"django.middleware.locale.LocaleMiddleware",
]
And that is it
We introduce Intermediate Pages
for admin actions to render a form before proceeding to the action logic. This will help if you need to pass extra params to your actions.
Form Example:
from django import forms
from django.contrib import admin
from django.contrin.auth.models import User
from django_admin_performance_tools.intermediate_pages.decorators import intermediate_page
from django_admin_performance_tools.intermediate_pages.forms import IntermediatePageForm
from .models import MyModel
class MyForm(IntermediatePageForm):
user = forms.ModelChoiceField(queryset=User.objects.all())
@admin.register(MyModel)
class MyModelAdmin(admin.ModelAdmin):
actions = ["assign_user"]
@intermediate_page(form=MyForm)
def assign_user(self, request, queryset, submitted_form):
# Write your own Logic
user = submitted_form.cleaned_data.get("user")
queryset.update(user=user)
ModelForm Example:
from django import forms
from django.contrib import admin
from django_admin_performance_tools.intermediate_pages.decorators import intermediate_page
from django_admin_performance_tools.intermediate_pages.forms import IntermediatePageModelForm
from .models import MyModel
class MyForm(IntermediatePageModelForm):
class Meta:
model = MyModel
fields = "__all__"
def clean(self) -> dict[str, Any]:
# Write your own Logic
return super().clean()
def save(self, commit: bool = ...) -> Any:
# Write your own Logic
return super().save(commit)
@admin.register(MyModel)
class MyModelAdmin(admin.ModelAdmin):
actions = ["create_object"]
@intermediate_page(form=MyForm)
def create_object(self, request, queryset, submitted_form):
# Write your own Logic
submitted_form.save()
-IntermediatePageForm is implemented on top of forms.Form
-IntermediatePageModelForm is implemented on top of forms.ModelForm
Note
intermediate_page
decorator checks for form validity, it means that it will not continue to the action logic only if the form is valid.
- form (required): A form inheriting from
IntermediatePageForm
orIntermediatePageModelForm
that is passed to the intermediate page template to be rendered - template: Path of an HTML template to use for rendering the intermediate page. defaults to
admin/intermediate_pages/abstract_form_page.html
that normally renders the form and shows selected objects if any - title: Custom title for the intermediate page, defaults to the action name.
- success_redirect_url: URL to redirect after successful form submission defaults to the model change list page.
The existing Django actions require at least one selected instance to proceed, so
We introduce Non-Selection Actions
for admin actions to proceed to the action without selecting any instances.
also Django admin actions are shown if there is at least one instance, but now all non-selection actions will be shown in actions dropdown even if there is no instances in change list page (and Delete selected models
action will be excluded)
Example:
from django.contrib import admin
from django_admin_performance_tools.mixins import NonSelectionActionsMixin
from .models import MyModel
@admin.register(MyModel)
class MyModelAdmin(NonSelectionActionsMixin, admin.ModelAdmin):
actions = ["assign_user"]
non_selection_actions = ["assign_user"]
def assign_user(self, request, queryset):
# Write your own Logic
No-Selection
actions overrides django ChangeList
class (to show the actions when no instances in the changelist page) so if you've already override it simply you can inherits from NoSelectionActionsChangeListMixin
Example:
from django.contrib import admin
from django_admin_performance_tools.mixins import NonSelectionActionsMixin, NoSelectionActionsChangeListMixin
from .models import MyModel
from django.contrib.admin.views.main import ChangeList
class MyChangeList(NoSelectionActionsChangeListMixin, ChangeList):
# Your own logic
@admin.register(MyModel)
class MyModelAdmin(NonSelectionActionsMixin, admin.ModelAdmin):
actions = ["assign_user"]
non_selection_actions = ["assign_user"]
def assign_user(self, request, queryset):
# Write your own Logic
def get_changelist(self, request, **kwargs):
"""
Return the ChangeList class for use on the changelist page.
"""
if self.non_selection_actions:
return MyChangeList
return ChangeList
The existing Django actions allows all staff users to select all the queryset and apply the action on it, imagine you have a 1 Million records and a staff user selected all the queryset and applied the action on it!
So ,We introduced Max Selection Count
to set max instances to be selected to an action.
Example:
from django.contrib import admin
from django_admin_performance_tools.decorators import check_queryset_max_selection
from .models import MyModel
@admin.register(MyModel)
class MyModelAdmin(admin.ModelAdmin):
actions = ["assign_user"]
@check_queryset_max_selection(max_selection=100)
def assign_user(self, request, queryset):
# Write your own Logic
in case you want to customize max_selection
based on the request you can pass a function
/lambda
that takes request
as an argument instead of an int
number.
if you set max_selection
to -1
means unlimited.
Example:
from django.contrib import admin
from django_admin_performance_tools.decorators import check_queryset_max_selection
from .models import MyModel
def get_max_selection(request):
if request.user.is_superuser:
return 1000
else:
return 10
@admin.register(MyModel)
class MyModelAdmin(admin.ModelAdmin):
actions = ["assign_user"]
@check_queryset_max_selection(max_selection=lambda request: get_max_selection(request))
def assign_user(self, request, queryset):
# Write your own Logic
All ModelAdmin
/StackedInline
/TabularInline
hits the db to fetch all related fields in readonly_fields
list
Example:
from django.contrib import admin
from .models import MyModel, AnotherModel
class MyModel(models.Model):
related_field_1 = models.ForeignKey(...)
related_field_2 = models.ForeignKey(...)
related_field_3 = models.ForeignKey(...)
related_field_4 = models.ForeignKey(...)
class AnotherModel(models.Model):
my_model = models.ForeignKey(MyModel)
related_field_5 = models.ForeignKey(...)
related_field_6 = models.ForeignKey(...)
related_field_7 = models.ForeignKey(...)
class AnotherModelInline(admin.StackedInline):
model = AnotherModel
readonly_fields = [
"related_field_5",
"related_field_6",
"related_field_7"
]
@admin.register(MyModel)
class MyModelAdmin(admin.ModelAdmin):
inlines = [AnotherModelInline]
redaonly_fields = [
"related_field_1",
"related_field_2",
"related_field_3",
"related_field_4"
]
Consider the above example Django will hit the DB 7 times to get the related fields and this number will increase if more inlines are added.
So, We introduce readonly_select_related
to select all read-only fields while rendering the models and its inlines
Example:
from django.contrib import admin
from django_admin_performance_tools.mixins import ReadonlySelectRelatedMixin
from .models import MyModel, AnotherModel
class MyModel(models.Model):
related_field_1 = models.ForeignKey(...)
related_field_2 = models.ForeignKey(...)
related_field_3 = models.ForeignKey(...)
related_field_4 = models.ForeignKey(...)
class AnotherModel(models.Model):
my_model = models.ForeignKey(MyModel)
related_field_5 = models.ForeignKey(...)
related_field_6 = models.ForeignKey(...)
related_field_7 = models.ForeignKey(...)
class AnotherModelInline(ReadonlySelectRelatedMixin, admin.StackedInline):
model = AnotherModel
readonly_fields = [
"related_field_5",
"related_field_6",
"related_field_7"
]
readonly_select_related = [
"related_field_5",
"related_field_6",
"related_field_7"
]
@admin.register(MyModel)
class MyModelAdmin(ReadonlySelectRelatedMixin, admin.ModelAdmin):
inlines = [AnotherModelInline]
redaonly_fields = [
"related_field_1",
"related_field_2",
"related_field_3",
"related_field_4"
]
readonly_select_related = [
"related_field_1",
"related_field_2",
"related_field_3",
"related_field_4"
]
Django list_display
allows functions that are decorated by @admin.display
, in these functions you may access a reverse relation or many-to-many fields of the instance, this means that the admin will hit the DB in every row to get the data and this will lead to a huge number of queries and may cause N+1 issue
Example:
from django.contrib import admin
from django.contrin.auth.models import User
@admin.register(User)
class UserAdmin(admin.ModelAdmin):
list_display = [
"name",
"email,
"groups
]
@admin.display
def groups(self, obj):
groups = []
for group in obj.groups.all():
groups.append(group.name)
return " ".join(groups)
Consider that the page loaded 100 users the above example will hit the DB 100 times to get the related groups of each user.
So, We introduce list_prefetch_related
to prefetch all reverse relations while rendering the changelist page
Example:
from django.contrib import admin
from django.contrin.auth.models import User
from django_admin_performance_tools.mixins import ListPrefetchRelatedMixin
@admin.register(User)
class UserAdmin(ListPrefetchRelatedMixin, admin.ModelAdmin):
list_display = [
"name",
"email,
"groups
]
list_prefetch_related = ["groups"]
@admin.display
def groups(self, obj):
groups = []
for group in obj.groups.all():
groups.append(group.name)
return " ".join(groups)
Default behavior of Django admin is to render all the related fields of an instance in dropdowns and call model's __str__
function for each row in this dropdowns
Example:
from django.contrib import admin
from django.contrin.auth.models import User
from .models import MyModel, AnotherModel
class AnotherModel(models.Model):
name = models.CharField()
user = models.ForeignKey(User)
def __str__(self):
return f"{self.name} User: {self.user.username}"
class MyModel(models.Model):
name = models.CharField()
another_model = models.ForeignKey(AnotherModel)
@admin.register(MyModel)
class MyModelAdmin(admin.ModelAdmin):
fields = [
"name",
"another_model"
]
Consider the above example, a form will be rendered with two fields name
and another_model
imagine that another_model
dropdown has 100 row, and __str__
function of AnotherModel
access the user to get its username, this means that in every row Django will hit the DB to fetch the username of the user.
So, We introduce change_select_related
to select all related fields while rendering the change page
Example:
from django.contrib import admin
from django.contrin.auth.models import User
from django_admin_performance_tools.mixins import AdminChangeSelectRelatedMixin
from .models import MyModel, AnotherModel
class AnotherModel(models.Model):
name = models.CharField()
user = models.ForeignKey(User)
def __str__(self):
return f"{self.name} User: {self.user.username}"
class MyModel(models.Model):
name = models.CharField()
another_model = models.ForeignKey(AnotherModel)
@admin.register(MyModel)
class MyModelAdmin(AdminChangeSelectRelatedMixin, admin.ModelAdmin):
fields = [
"name",
"another_model"
]
change_select_related = ["another_model__user"]
It is supported in inlines too
Example:
from django_admin_performance_tools.mixins import InlineChangeSelectRelatedMixin
from .models import AnotherModel
class AnotherModelInline(InlineChangeSelectRelatedMixin, admin.StackedInline):
model = AnotherModel
change_select_related = [...]
Same as forms dropdowns, related filters get a list of instances and display its __str__
function so if you access a related field in the __str__
it will hit the DB for each row in the related filter choices list.
So, We introduce FilterWithSelectRelated
to select all related fields while rendering the filter choices
Example:
from django.contrib import admin
from django.contrin.auth.models import User
from .models import MyModel, AnotherModel
class AnotherModel(models.Model):
name = models.CharField()
user = models.ForeignKey(User)
def __str__(self):
return f"{self.name} User: {self.user.username}"
class MyModel(models.Model):
name = models.CharField()
another_model = models.ForeignKey(AnotherModel)
@admin.register(MyModel)
class MyModelAdmin(AdminChangeSelectRelatedMixin, admin.ModelAdmin):
list_filter = [
"another_model"
]
in the previous example, if we've 100 choice in the another_model
filter list, it means that we've hit the DB 100 times, but by using FilterWithSelectRelated
we can select user
while rendering the choices.
Example:
from django.contrib import admin
from django.contrin.auth.models import User
from django_admin_performance_tools.filters import FilterWithSelectRelated
from .models import MyModel, AnotherModel
class AnotherModel(models.Model):
name = models.CharField()
user = models.ForeignKey(User)
def __str__(self):
return f"{self.name} User: {self.user.username}"
class MyModel(models.Model):
name = models.CharField()
another_model = models.ForeignKey(AnotherModel)
class AnotherModelFilter(FilterWithSelectRelated):
list_select_related = ["user"]
@admin.register(MyModel)
class MyModelAdmin(AdminChangeSelectRelatedMixin, admin.ModelAdmin):
list_filter = [
AnotherModelFilter
]
Recently Django featured a new admin attribute search_help_text
and it adds a help text below the search bar, So we extended this feature to be more useful and readable by showing all the fields that the admin will search in.
Example:
from django.contrib import admin
from django.contrin.auth.models import User
from django_admin_performance_tools.mixins import SearchHelpTextMixin
@admin.register(User)
class UserAdmin(SearchHelpTextMixin, admin.ModelAdmin):
search_fields = [
"email",
"name",
"username",
"phone_number"
]
Not just field names, you can customize any field to represent another text
Example:
from django.contrib import admin
from django.contrin.auth.models import User
from django_admin_performance_tools.mixins import SearchHelpTextMixin
@admin.register(User)
class UserAdmin(SearchHelpTextMixin, admin.ModelAdmin):
search_fields = [
"email",
"name",
"username",
"phone_number"
]
search_help_text_map = {
"email": "Email Address"
}
There is an abstract class to use if you intend to use all the following features in Model Admin:
- ListPrefetchRelatedMixin
- ReadonlySelectRelatedMixin
- AdminChangeSelectRelatedMixin
- SearchHelpTextMixin
- NonSelectionActionsMixin
can be imported from the following path:
from django_admin_performance_tools.admin import AbstractModelAdmin
There is an abstract classes to use if you intend to use all the following features in Inline Admin:
- ReadonlySelectRelatedMixin
- AdminChangeSelectRelatedMixin
can be imported from the following path:
from django_admin_performance_tools.admin import (
AbstractStackedInline,
AbstractTabularInline
)
Upcomming
- Auto Complete filters with custome title
- Filters with custom title
Sometimes we may need to disable some choices of choice list fields based on who interacting with the field, for example: a super user can change status field to any choice but the staff user may have a limited options to change the status.
Setup
add FORM_RENDERER
in settings.py
.
FORM_RENDERER = "django.forms.renderers.TemplatesSetting"
Add 'django.forms' to your INSTALLED_APPS settings.
INSTALLED_APPS = [
...
"django.forms",
]
Example:
from django import forms
from django_admin_performance_tools.widgets import DisabledSelect
from django.db import models
class StatusEnum(models.IntegerChoices):
INITIAL = 0, "Initial"
DONE = 1, "Done"
CLOSED = 2, "Closed"
class ContactForm1(forms.Form):
status = forms.ChoiceField(
required=False,
choices=StatusEnum.choices,
widget=DisabledSelect(disabled_options=[StatusEnum.DONE, StatusEnum.CLOSED])
)
You can controll some behaves by adding the following in settings.py
- HIDE_QUICK_ACTIONS_DROPDOWN
This will hide the Quick Actions dropdown from admin sites
Default value is False
- QUICK_ACTIONS_URL_PATH_PREFIX
This is the prefix before all Quick-Actions URL paths/names
Default value is quick-actions
- HIDE_LANGUAGE_DROPDOWN
This will hide the Languages dropdown from admin sites
Default value is False
If you are interested to fix an issue or to add new feature, you can just open a pull request.