django-admin-performance-tools


Namedjango-admin-performance-tools JSON
Version 1.0.1 PyPI version JSON
download
home_pagehttps://github.com/muhammedattif
SummaryDjango Admin Performance Tools
upload_time2024-01-13 15:59:37
maintainerMuhammed Atif
docs_urlNone
author
requires_python>=3.8
licenseMIT
keywords django admin
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # Django Admin Performance Tools

[![python](https://img.shields.io/badge/Python-v3.8-3776AB.svg?style=flat&logo=python&logoColor=yellow)](https://www.python.org)  [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit)  [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)


## Table of Contents

- [Description](#1--description)
- [Requirements](#2--requirements)
- [Installation Instructions](#3--installation-instructions)
- [Quick Actions](#4--quick-actions)
- [Languages Dropdown](#5--languages-dropdown)
- [Intermediate Pages and Model Action Tools](#6--intermediate-pages-and-model-action-tools)
- [Tools for admin Querysets/Filters](#7--tools-for-admin-querysets-and-filters-optemization)
- [Tools for admin search/filters](#8--tools-for-admin-search-and-filters)
- [Widgets](#9--widgets)
- [Settings](#10--Settings)

# 1- Description

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][github-repo]**

# 2- Requirements
Before you begin, ensure you have met the following requirements:
* Python 3.8+
* Django >= 3.2

---

# 3- Installation Instructions

You don't need this source code unless you want to modify the package. If you just
want to use the package, just run:

```bash
pip install django-admin-performance-tools
```

Add 'django_admin_performance_tools' to your INSTALLED_APPS settings.
```python
INSTALLED_APPS = [
    "django_admin_performance_tools",
    ...
]
```
**Must be added on the top of the list**


in `settings.py` update TEMPLATES
```python

TEMPLATES = [
    {
        ...
        'DIRS': ["templates"],
        'OPTIONS': {
            'context_processors': [
                ...
                "django_admin_performance_tools.context_processors.settings",
            ],
        },
    },
]
```

---

# 4- Quick Actions

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.
```python
INSTALLED_APPS = [
    "django_admin_performance_tools",
    "django_admin_performance_tools.sites.MainAdminConfig",
    ...
]
```


![Alt text](/docs/images/quick_actions_dropdown.gif?raw=true "Quick Actions Dropdown")
🚀🚀

What if you already implemented your own Admin site? all you've to do is to inherit from `AbstractAdminSiteMixin`

**Example:**

```python
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`

## 4.1- FormViewQuickAction

Form View Quick Action is used to create an action to render a form (It is implemented on top of django FormView)

**Example:**

```python
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).


## 4.2- CreateViewQuickAction

Create View Quick Action is used to create an action to render a model form (It is implemented on top of django CreateView)

**Example:**

```python
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).


## 4.3- WizardFormViewQuickAction

Wizard Form View Quick Action is implemented on top of [django-formtools][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:

```bash
pip install django-formtools
```

Add 'formtools' to your INSTALLED_APPS settings.
```python
INSTALLED_APPS = [
    ...
    "formtools",
]
```

**Example:**

```python
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 🎉

![Alt text](/docs/images/wizard_actions.gif?raw=true "Wizard Actions")

-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).

## 4.4- TemplateViewQuickAction

Template View Quick Action is used to create an action to render a template (It is implemented on top of django TemplateView)

**Example:**

```python
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`

```bash
{% extends 'admin/quick_actions/base_quick_action.html' %}

{% block action_body %}
    # Write your own HTML
{% endblock %}
```

## 4.5- Abstract QuickAction

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:**

```python
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


## 4.6- Control who can see the actions

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:**

```python
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 call `super()`. Default check the user `is_active=True` and `is_staff=True`
- `permission_required` attribute default value is `None`

## 4.7- Register Quick Actions to Multiple Admin Sites

`@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:**

```python
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()
```

## 4.8- Redirect Success Messages

You can define messges to be displayed to the user after form submission

- `post_success_message` attribute is used for `post` requests


**Example:**

```python
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.**

## 4.9- URLs and Path Name

`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:**

```python
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 be `www.mysite.com/admin/quick-actions/create-user-template`
- `path_name` value will be `quick-actions-create-user-template`


**Example 2:**

```python
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 be `www.mysite.com/admin/quick-actions/my-template`
- `path_name` value will be `quick-actions-my-template`


---

## 5- Languages Dropdown

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:

```python
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

```python
MIDDLEWARE = [
    ...
    "django.middleware.locale.LocaleMiddleware",
]
```

And that is it

![Alt text](/docs/images/languages_dropdown.png?raw=true "Languages Dropdown")

---

# 6- Intermediate Pages and Model Action Tools

## 6.1- Intermediate Pages

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:**

```python
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:**

```python
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.

### 6.1.1 intermediate_page decorator params

- **form** (required): A form inheriting from `IntermediatePageForm` or `IntermediatePageModelForm` 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.


## 6.2- Non-Selection Actions

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:**

```python
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:**

```python
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
```

![Alt text](/docs/images/non_selection_actions.gif?raw=true "Disabled Select")

## 6.3- Max Selection Count

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:**

```python
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:**

```python
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
```

----

# 7- Tools for admin Querysets and Filters optemization

## 7.1- Readonly Select Related

All `ModelAdmin`/`StackedInline`/`TabularInline` hits the db to fetch all related fields in `readonly_fields` list


**Example:**

```python
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:**

```python
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"
    ]
```


## 7.2- List Prefetch Related

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:**

```python
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:**

```python
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)
```


## 7.3- Change Prefetch Related

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:**

```python
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:**

```python
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:**

```python
from django_admin_performance_tools.mixins import InlineChangeSelectRelatedMixin
from .models import AnotherModel

class AnotherModelInline(InlineChangeSelectRelatedMixin, admin.StackedInline):
    model = AnotherModel
    change_select_related = [...]
```

# 8- Tools for admin search and filters

## 8.1 Select Related with Filter Related Fields

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:**

```python
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:**

```python
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
    ]
```


## 8.2 Search Help Text

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.

![Alt text](/docs/images/search_help_text.png?raw=true "Search Help Text")

**Example:**

```python

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:**

```python

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:

```python
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:

```python
from django_admin_performance_tools.admin import (
    AbstractStackedInline,
    AbstractTabularInline
)
```

**Upcomming**
- Auto Complete filters with custome title
- Filters with custom title

---

# 9- Widgets

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.


![Alt text](/docs/images/disabled_select.gif?raw=true "Disabled Select")


**Setup**

add `FORM_RENDERER` in `settings.py`.
```python
FORM_RENDERER = "django.forms.renderers.TemplatesSetting"
```

Add 'django.forms' to your INSTALLED_APPS settings.
```python
INSTALLED_APPS = [
    ...
    "django.forms",
]
```

**Example:**

```python

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])
    )
```
---

# 10- Settings

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`


[github-repo]: https://github.com/muhammedattif/Django-Admin-Performance-Tools
[django-formtools]: https://pypi.org/project/django-formtools/


## Contributing

If you are interested to fix an issue or to add new feature, you can just open a pull request.

### Contributors
<a href = "https://github.com/muhammedattif/Django-Admin-Performance-Tools/graphs/contributors">
  <img src = "https://contrib.rocks/image?repo=muhammedattif/Django-Admin-Performance-Tools"/>
</a>

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/muhammedattif",
    "name": "django-admin-performance-tools",
    "maintainer": "Muhammed Atif",
    "docs_url": null,
    "requires_python": ">=3.8",
    "maintainer_email": "muhamedattif@gmail.com",
    "keywords": "Django Admin",
    "author": "",
    "author_email": "",
    "download_url": "https://files.pythonhosted.org/packages/ea/a9/d519dd22d33faca3d77371bd389f60dfe9d73feea9f4b3eba577088afc59/django-admin-performance-tools-1.0.1.tar.gz",
    "platform": null,
    "description": "# Django Admin Performance Tools\n\n[![python](https://img.shields.io/badge/Python-v3.8-3776AB.svg?style=flat&logo=python&logoColor=yellow)](https://www.python.org)  [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit)  [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)\n\n\n## Table of Contents\n\n- [Description](#1--description)\n- [Requirements](#2--requirements)\n- [Installation Instructions](#3--installation-instructions)\n- [Quick Actions](#4--quick-actions)\n- [Languages Dropdown](#5--languages-dropdown)\n- [Intermediate Pages and Model Action Tools](#6--intermediate-pages-and-model-action-tools)\n- [Tools for admin Querysets/Filters](#7--tools-for-admin-querysets-and-filters-optemization)\n- [Tools for admin search/filters](#8--tools-for-admin-search-and-filters)\n- [Widgets](#9--widgets)\n- [Settings](#10--Settings)\n\n# 1- Description\n\nThis Package is a collection of extensions/tools for the default django administration interface, it includes:\n-   Quick Actions\n-   Languages Dropdown Interface\n-   Intermediate Pages and model actions tools\n-   Tools for admin `Querysets`/`Filters` optemization (helps avoiding N+1 issue)\n-   Tools for admin `search`/`filters`\n-   Widgets\n\n---\n\n**Full documentation is avilable on [Github Repo][github-repo]**\n\n# 2- Requirements\nBefore you begin, ensure you have met the following requirements:\n* Python 3.8+\n* Django >= 3.2\n\n---\n\n# 3- Installation Instructions\n\nYou don't need this source code unless you want to modify the package. If you just\nwant to use the package, just run:\n\n```bash\npip install django-admin-performance-tools\n```\n\nAdd 'django_admin_performance_tools' to your INSTALLED_APPS settings.\n```python\nINSTALLED_APPS = [\n    \"django_admin_performance_tools\",\n    ...\n]\n```\n**Must be added on the top of the list**\n\n\nin `settings.py` update TEMPLATES\n```python\n\nTEMPLATES = [\n    {\n        ...\n        'DIRS': [\"templates\"],\n        'OPTIONS': {\n            'context_processors': [\n                ...\n                \"django_admin_performance_tools.context_processors.settings\",\n            ],\n        },\n    },\n]\n```\n\n---\n\n# 4- Quick Actions\n\nQuick 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:\n-   It is not attached to any model\n-   Actions acts like views but only supports (`POST`, `GET`) http methods\n-   Support permissions, so you can write your own logic to control who can see the action\n-   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)\n-   Wizard Form View Action is introduced to enables you to create multi-step forms (It is implemented on top of django-formtools)\n-   Template View Action is introduced to enables you to render your own templates\n-   Base View Action is introduced to enables you to create customized actions as you want\n\n**Setup**\n\nin `settings.py` replace `django.contrib.admin` by `django_admin_performance_tools.sites.MainAdminConfig` in your INSTALLED_APPS.\n```python\nINSTALLED_APPS = [\n    \"django_admin_performance_tools\",\n    \"django_admin_performance_tools.sites.MainAdminConfig\",\n    ...\n]\n```\n\n\n![Alt text](/docs/images/quick_actions_dropdown.gif?raw=true \"Quick Actions Dropdown\")\n\ud83d\ude80\ud83d\ude80\n\nWhat if you already implemented your own Admin site? all you've to do is to inherit from `AbstractAdminSiteMixin`\n\n**Example:**\n\n```python\nfrom django_admin_performance_tools.sites import AbstractAdminSiteMixin\n\n\nclass YourAdmin(AbstractAdminSiteMixin, AdminSite):\n    \"\"\"Your Admin Site\"\"\"\n```\nby doing that you don't need to replace your admin site in `INSTALLED_APPS`\n\n## 4.1- FormViewQuickAction\n\nForm View Quick Action is used to create an action to render a form (It is implemented on top of django FormView)\n\n**Example:**\n\n```python\nfrom django_admin_performance_tools.quick_actions import FormViewQuickAction\nfrom django_admin_performance_tools.quick_actions.registry import register_quick_action\n\n@register_quick_action()\nclass FormAction(FormViewQuickAction):\n    name = \"My Form Action\"\n    form_class = MyForm\n\n    def post(self, request, *args, **kwargs):\n        # Write your logic here\n        return super().post(request, *args, **kwargs)\n\n```\n\n-To customize submit button name, you can set `submit_button_value` attribute in the `FormAction` class\n\n-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).\n\n\n## 4.2- CreateViewQuickAction\n\nCreate View Quick Action is used to create an action to render a model form (It is implemented on top of django CreateView)\n\n**Example:**\n\n```python\nfrom django_admin_performance_tools.quick_actions import CreateViewQuickAction\nfrom django_admin_performance_tools.quick_actions.registry import register_quick_action\n\nfrom .models import MyModel\n\n@register_quick_action()\nclass CreateFormAction(CreateViewQuickAction):\n    name = \"My Craete Form Action\"\n    model = model\n    fields = [\"name\"]\n```\n\n-To customize submit button name, you can set `submit_button_value` attribute in the `CreateFormAction` class\n\n-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).\n\n\n## 4.3- WizardFormViewQuickAction\n\nWizard Form View Quick Action is implemented on top of [django-formtools][django-formtools] library, and it is used to create an action to render a wizard form.\n\n**setup**\n\n`django-formtools` library is required. to install it use the following pip command:\n\n```bash\npip install django-formtools\n```\n\nAdd 'formtools' to your INSTALLED_APPS settings.\n```python\nINSTALLED_APPS = [\n    ...\n    \"formtools\",\n]\n```\n\n**Example:**\n\n```python\nfrom formtools.wizard.views import SessionWizardView\n\nfrom django_admin_performance_tools.quick_actions import WizardFormViewQuickAction\nfrom django_admin_performance_tools.quick_actions.registry import register_quick_action\n\n@register_quick_action()\nclass WizardFormAction(WizardFormViewQuickAction, SessionWizardView):\n    name = \"My Wizard Form Action\"\n    form_list = [Form1, Form2, Form3]\n\n    def done(self, form_list, **kwargs):\n        # form_list[0].cleaned_data\n        # form_list[1].cleaned_data\n        # form_list[2].cleaned_data\n        # Continue writing your logic here\n        return super().done(form_list, **kwargs)\n```\n\nAnd that's it \ud83c\udf89\n\n![Alt text](/docs/images/wizard_actions.gif?raw=true \"Wizard Actions\")\n\n-To customize last step button name, you can set `submit_button_value` attribute in the `WizardFormAction` class\n\n-To customize `done()` function redirection you can set `success_url` attribute or override `get_success_url` function (Default is redirect to the action page).\n\n## 4.4- TemplateViewQuickAction\n\nTemplate View Quick Action is used to create an action to render a template (It is implemented on top of django TemplateView)\n\n**Example:**\n\n```python\nfrom django_admin_performance_tools.quick_actions import TemplateViewQuickAction\nfrom django_admin_performance_tools.quick_actions.registry import register_quick_action\n\n@register_quick_action()\nclass TemplateAction(TemplateViewQuickAction):\n    name = \"My Template Action\"\n    template_name = \"my_template.html\"\n```\n\n**Notes**\n\n-The template file you created (`my_template.html`) must extends from `base_quick_action.html`\n\n```bash\n{% extends 'admin/quick_actions/base_quick_action.html' %}\n\n{% block action_body %}\n    # Write your own HTML\n{% endblock %}\n```\n\n## 4.5- Abstract QuickAction\n\nQuickAction 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)\n\n**Example:**\n\n```python\nfrom django_admin_performance_tools.quick_actions import QuickAction\nfrom django_admin_performance_tools.quick_actions.registry import register_quick_action\n\n@register_quick_action()\nclass CustomAction(QuickAction):\n    name = \"My custom Action\"\n\n    def get(self, request, *args, **kwargs):\n        # Write your own logic here\n        return super().get(request, *args, **kwargs)\n\n    def post(self, request, *args, **kwargs):\n        # Write your own logic here\n        return super().post(request, *args, **kwargs)\n\n    def get_context_data(self, **kwargs):\n        super().get_context_data(**kwargs)\n        return {\n            # Pass your extra context here\n            **super().get_context_data(**kwargs),\n        }\n```\n\nYou can override `get_context_data()` function to pass extra context values\n\n\n## 4.6- Control who can see the actions\n\nQuick 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.\n\n**Example:**\n\n```python\nfrom django_admin_performance_tools.quick_actions import TemplateViewQuickAction\nfrom django_admin_performance_tools.quick_actions.registry import register_quick_action\n\n@register_quick_action()\nclass TemplateAction(TemplateViewQuickAction):\n    name = \"My Template Action\"\n    template_name = \"my_template.html\"\n    permission_required = (\"myapp.add_mymodel\")\n\n    def has_permission(self):\n        # Write your own logic here\n        return super().has_permission()\n```\nthe previous example will check the following by default:\n- The user has add permission on MyModel View\n- The user is active and is staff\n\n\n**Notes**\n\n- On overriding `has_permission()` you must call `super()`. Default check the user `is_active=True` and `is_staff=True`\n- `permission_required` attribute default value is `None`\n\n## 4.7- Register Quick Actions to Multiple Admin Sites\n\n`@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\n\n**Example:**\n\n```python\nfrom django.contrib import admin\n\nfrom django_admin_performance_tools.quick_actions import TemplateViewQuickAction\nfrom django_admin_performance_tools.quick_actions.registry import register_quick_action\nfrom .my_sites import site2, site3\n\n@register_quick_action(sites=[admin.site, site2, site3])\nclass TemplateAction(TemplateViewQuickAction):\n    name = \"My Template Action\"\n    template_name = \"my_template.html\"\n\n    def has_permission(self):\n        # Write your own logic here\n        return super().has_permission()\n```\n\n## 4.8- Redirect Success Messages\n\nYou can define messges to be displayed to the user after form submission\n\n- `post_success_message` attribute is used for `post` requests\n\n\n**Example:**\n\n```python\nfrom django.contrib import admin\n\nfrom django_admin_performance_tools.quick_actions import TemplateViewQuickAction\nfrom django_admin_performance_tools.quick_actions.registry import register_quick_action\nfrom .my_sites import site2, site3\n\n@register_quick_action()\nclass TemplateAction(TemplateViewQuickAction):\n    name = \"My Template Action\"\n    template_name = \"my_template.html\"\n    post_success_message = \"Data Submitted Successfully\"\n\n    # NOTE: Also you can override the messages using the following method\n    # You can access the current request by using self.request\n    def get_post_success_message(self):\n        # Write your own logic here\n```\n\n---\n**Notes**\n\n1- **All actions must be added/imported in admin.py as to be detected and registered.**\n\n## 4.9- URLs and Path Name\n\n`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\n\n`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\n\n\n**Example 1:**\n\n```python\nfrom django.contrib import admin\n\nfrom django_admin_performance_tools.quick_actions import TemplateViewQuickAction\nfrom django_admin_performance_tools.quick_actions.registry import register_quick_action\nfrom .my_sites import site2, site3\n\n@register_quick_action()\nclass CreateUserTemplate(TemplateViewQuickAction):\n    name = \"My Template Action\"\n    template_name = \"my_template.html\"\n```\n\n- `url_path` value will be `www.mysite.com/admin/quick-actions/create-user-template`\n- `path_name` value will be `quick-actions-create-user-template`\n\n\n**Example 2:**\n\n```python\nfrom django.contrib import admin\n\nfrom django_admin_performance_tools.quick_actions import TemplateViewQuickAction\nfrom django_admin_performance_tools.quick_actions.registry import register_quick_action\nfrom .my_sites import site2, site3\n\n@register_quick_action()\nclass CreateUserTemplate(TemplateViewQuickAction):\n    name = \"My Template Action\"\n    url_path = \"my-template\"\n    path_name = \"my-template\"\n    template_name = \"my_template.html\"\n```\n\n- `url_path` value will be `www.mysite.com/admin/quick-actions/my-template`\n- `path_name` value will be `quick-actions-my-template`\n\n\n---\n\n## 5- Languages Dropdown\n\nThis 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\n\n**Setup**\n\nin the root `urls.py` add the following:\n\n```python\nfrom django.urls import include, path\n\nurlpatterns = [\n    path(\"i18n/\", include(\"django.conf.urls.i18n\")),\n    ...\n]\n```\nAdd 'django_admin_performance_tools' to your INSTALLED_APPS setting.\nin `settings.py` add `django.middleware.locale.LocaleMiddleware` to the MIDDLEWARE\n\n```python\nMIDDLEWARE = [\n    ...\n    \"django.middleware.locale.LocaleMiddleware\",\n]\n```\n\nAnd that is it\n\n![Alt text](/docs/images/languages_dropdown.png?raw=true \"Languages Dropdown\")\n\n---\n\n# 6- Intermediate Pages and Model Action Tools\n\n## 6.1- Intermediate Pages\n\nWe 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.\n\n**Form Example:**\n\n```python\nfrom django import forms\nfrom django.contrib import admin\nfrom django.contrin.auth.models import User\n\nfrom django_admin_performance_tools.intermediate_pages.decorators import intermediate_page\nfrom django_admin_performance_tools.intermediate_pages.forms import IntermediatePageForm\n\nfrom .models import MyModel\n\nclass MyForm(IntermediatePageForm):\n    user = forms.ModelChoiceField(queryset=User.objects.all())\n\n@admin.register(MyModel)\nclass MyModelAdmin(admin.ModelAdmin):\n\n    actions = [\"assign_user\"]\n\n    @intermediate_page(form=MyForm)\n    def assign_user(self, request, queryset, submitted_form):\n        # Write your own Logic\n        user = submitted_form.cleaned_data.get(\"user\")\n        queryset.update(user=user)\n```\n\n\n**ModelForm Example:**\n\n```python\nfrom django import forms\nfrom django.contrib import admin\n\nfrom django_admin_performance_tools.intermediate_pages.decorators import intermediate_page\nfrom django_admin_performance_tools.intermediate_pages.forms import IntermediatePageModelForm\n\nfrom .models import MyModel\n\nclass MyForm(IntermediatePageModelForm):\n\n    class Meta:\n        model = MyModel\n        fields = \"__all__\"\n\n    def clean(self) -> dict[str, Any]:\n        # Write your own Logic\n        return super().clean()\n\n    def save(self, commit: bool = ...) -> Any:\n        # Write your own Logic\n        return super().save(commit)\n\n@admin.register(MyModel)\nclass MyModelAdmin(admin.ModelAdmin):\n\n    actions = [\"create_object\"]\n\n    @intermediate_page(form=MyForm)\n    def create_object(self, request, queryset, submitted_form):\n        # Write your own Logic\n        submitted_form.save()\n```\n\n-**IntermediatePageForm** is implemented on top of `forms.Form`\n\n-**IntermediatePageModelForm** is implemented on top of `forms.ModelForm`\n\n**Note**\n\n`intermediate_page` decorator checks for form validity, it means that it will not continue to the action logic only if the form is valid.\n\n### 6.1.1 intermediate_page decorator params\n\n- **form** (required): A form inheriting from `IntermediatePageForm` or `IntermediatePageModelForm` that is passed to the intermediate page template to be rendered\n- **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\n- **title**: Custom title for the intermediate page, defaults to the action name.\n- **success_redirect_url**: URL to redirect after successful form submission defaults to the model change list page.\n\n\n## 6.2- Non-Selection Actions\n\nThe existing Django actions require at least one selected instance to proceed, so\nWe introduce `Non-Selection Actions` for admin actions to proceed to the action without selecting any instances.\n\nalso 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)\n\n**Example:**\n\n```python\nfrom django.contrib import admin\n\nfrom django_admin_performance_tools.mixins import NonSelectionActionsMixin\n\nfrom .models import MyModel\n\n@admin.register(MyModel)\nclass MyModelAdmin(NonSelectionActionsMixin, admin.ModelAdmin):\n\n    actions = [\"assign_user\"]\n    non_selection_actions = [\"assign_user\"]\n\n    def assign_user(self, request, queryset):\n        # Write your own Logic\n```\n\n`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`\n\n\n**Example:**\n\n```python\nfrom django.contrib import admin\n\nfrom django_admin_performance_tools.mixins import NonSelectionActionsMixin, NoSelectionActionsChangeListMixin\n\nfrom .models import MyModel\n\nfrom django.contrib.admin.views.main import ChangeList\n\nclass MyChangeList(NoSelectionActionsChangeListMixin, ChangeList):\n    # Your own logic\n\n@admin.register(MyModel)\nclass MyModelAdmin(NonSelectionActionsMixin, admin.ModelAdmin):\n\n    actions = [\"assign_user\"]\n    non_selection_actions = [\"assign_user\"]\n\n    def assign_user(self, request, queryset):\n        # Write your own Logic\n\n    def get_changelist(self, request, **kwargs):\n        \"\"\"\n        Return the ChangeList class for use on the changelist page.\n        \"\"\"\n        if self.non_selection_actions:\n            return MyChangeList\n        return ChangeList\n```\n\n![Alt text](/docs/images/non_selection_actions.gif?raw=true \"Disabled Select\")\n\n## 6.3- Max Selection Count\n\nThe 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!\n\nSo ,We introduced `Max Selection Count` to set max instances to be selected to an action.\n\n**Example:**\n\n```python\nfrom django.contrib import admin\n\nfrom django_admin_performance_tools.decorators import check_queryset_max_selection\n\nfrom .models import MyModel\n\n@admin.register(MyModel)\nclass MyModelAdmin(admin.ModelAdmin):\n\n    actions = [\"assign_user\"]\n\n    @check_queryset_max_selection(max_selection=100)\n    def assign_user(self, request, queryset):\n        # Write your own Logic\n```\n\nin 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.\n\nif you set `max_selection` to `-1` means unlimited.\n\n**Example:**\n\n```python\nfrom django.contrib import admin\n\nfrom django_admin_performance_tools.decorators import check_queryset_max_selection\n\nfrom .models import MyModel\n\ndef get_max_selection(request):\n    if request.user.is_superuser:\n        return 1000\n    else:\n        return 10\n\n@admin.register(MyModel)\nclass MyModelAdmin(admin.ModelAdmin):\n\n    actions = [\"assign_user\"]\n\n    @check_queryset_max_selection(max_selection=lambda request: get_max_selection(request))\n    def assign_user(self, request, queryset):\n        # Write your own Logic\n```\n\n----\n\n# 7- Tools for admin Querysets and Filters optemization\n\n## 7.1- Readonly Select Related\n\nAll `ModelAdmin`/`StackedInline`/`TabularInline` hits the db to fetch all related fields in `readonly_fields` list\n\n\n**Example:**\n\n```python\nfrom django.contrib import admin\nfrom .models import MyModel, AnotherModel\n\nclass MyModel(models.Model):\n\n    related_field_1 = models.ForeignKey(...)\n    related_field_2 = models.ForeignKey(...)\n    related_field_3 = models.ForeignKey(...)\n    related_field_4 = models.ForeignKey(...)\n\n\nclass AnotherModel(models.Model):\n\n    my_model = models.ForeignKey(MyModel)\n    related_field_5 = models.ForeignKey(...)\n    related_field_6 = models.ForeignKey(...)\n    related_field_7 = models.ForeignKey(...)\n\nclass AnotherModelInline(admin.StackedInline):\n    model = AnotherModel\n    readonly_fields = [\n        \"related_field_5\",\n        \"related_field_6\",\n        \"related_field_7\"\n    ]\n\n\n@admin.register(MyModel)\nclass MyModelAdmin(admin.ModelAdmin):\n    inlines = [AnotherModelInline]\n    redaonly_fields = [\n        \"related_field_1\",\n        \"related_field_2\",\n        \"related_field_3\",\n        \"related_field_4\"\n    ]\n\n```\n\nConsider 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.\n\nSo, We introduce `readonly_select_related` to select all read-only fields while rendering the models and its inlines\n\n**Example:**\n\n```python\nfrom django.contrib import admin\nfrom django_admin_performance_tools.mixins import ReadonlySelectRelatedMixin\nfrom .models import MyModel, AnotherModel\n\nclass MyModel(models.Model):\n\n    related_field_1 = models.ForeignKey(...)\n    related_field_2 = models.ForeignKey(...)\n    related_field_3 = models.ForeignKey(...)\n    related_field_4 = models.ForeignKey(...)\n\n\nclass AnotherModel(models.Model):\n\n    my_model = models.ForeignKey(MyModel)\n    related_field_5 = models.ForeignKey(...)\n    related_field_6 = models.ForeignKey(...)\n    related_field_7 = models.ForeignKey(...)\n\nclass AnotherModelInline(ReadonlySelectRelatedMixin, admin.StackedInline):\n    model = AnotherModel\n    readonly_fields = [\n        \"related_field_5\",\n        \"related_field_6\",\n        \"related_field_7\"\n    ]\n    readonly_select_related = [\n        \"related_field_5\",\n        \"related_field_6\",\n        \"related_field_7\"\n    ]\n\n\n@admin.register(MyModel)\nclass MyModelAdmin(ReadonlySelectRelatedMixin, admin.ModelAdmin):\n\n    inlines = [AnotherModelInline]\n    redaonly_fields = [\n        \"related_field_1\",\n        \"related_field_2\",\n        \"related_field_3\",\n        \"related_field_4\"\n    ]\n    readonly_select_related = [\n        \"related_field_1\",\n        \"related_field_2\",\n        \"related_field_3\",\n        \"related_field_4\"\n    ]\n```\n\n\n## 7.2- List Prefetch Related\n\nDjango `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\n\n**Example:**\n\n```python\nfrom django.contrib import admin\nfrom django.contrin.auth.models import User\n\n@admin.register(User)\nclass UserAdmin(admin.ModelAdmin):\n\n    list_display = [\n        \"name\",\n        \"email,\n        \"groups\n    ]\n\n    @admin.display\n    def groups(self, obj):\n        groups = []\n        for group in obj.groups.all():\n            groups.append(group.name)\n        return \" \".join(groups)\n\n```\n\nConsider that the page loaded 100 users the above example will hit the DB 100 times to get the related groups of each user.\n\nSo, We introduce `list_prefetch_related` to prefetch all reverse relations while rendering the changelist page\n**Example:**\n\n```python\nfrom django.contrib import admin\nfrom django.contrin.auth.models import User\nfrom django_admin_performance_tools.mixins import ListPrefetchRelatedMixin\n\n@admin.register(User)\nclass UserAdmin(ListPrefetchRelatedMixin, admin.ModelAdmin):\n\n    list_display = [\n        \"name\",\n        \"email,\n        \"groups\n    ]\n    list_prefetch_related = [\"groups\"]\n\n    @admin.display\n    def groups(self, obj):\n        groups = []\n        for group in obj.groups.all():\n            groups.append(group.name)\n        return \" \".join(groups)\n```\n\n\n## 7.3- Change Prefetch Related\n\nDefault 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\n\n\n**Example:**\n\n```python\nfrom django.contrib import admin\nfrom django.contrin.auth.models import User\nfrom .models import MyModel, AnotherModel\n\n\nclass AnotherModel(models.Model):\n\n    name = models.CharField()\n    user = models.ForeignKey(User)\n\n    def __str__(self):\n        return f\"{self.name} User: {self.user.username}\"\n\n\nclass MyModel(models.Model):\n\n    name = models.CharField()\n    another_model = models.ForeignKey(AnotherModel)\n\n@admin.register(MyModel)\nclass MyModelAdmin(admin.ModelAdmin):\n\n    fields = [\n        \"name\",\n        \"another_model\"\n    ]\n```\n\nConsider 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.\n\nSo, We introduce `change_select_related` to select all related fields while rendering the change page\n**Example:**\n\n```python\nfrom django.contrib import admin\nfrom django.contrin.auth.models import User\nfrom django_admin_performance_tools.mixins import AdminChangeSelectRelatedMixin\nfrom .models import MyModel, AnotherModel\n\n\nclass AnotherModel(models.Model):\n\n    name = models.CharField()\n    user = models.ForeignKey(User)\n\n    def __str__(self):\n        return f\"{self.name} User: {self.user.username}\"\n\n\nclass MyModel(models.Model):\n\n    name = models.CharField()\n    another_model = models.ForeignKey(AnotherModel)\n\n@admin.register(MyModel)\nclass MyModelAdmin(AdminChangeSelectRelatedMixin, admin.ModelAdmin):\n\n    fields = [\n        \"name\",\n        \"another_model\"\n    ]\n    change_select_related = [\"another_model__user\"]\n```\n\nIt is supported in inlines too\n\n**Example:**\n\n```python\nfrom django_admin_performance_tools.mixins import InlineChangeSelectRelatedMixin\nfrom .models import AnotherModel\n\nclass AnotherModelInline(InlineChangeSelectRelatedMixin, admin.StackedInline):\n    model = AnotherModel\n    change_select_related = [...]\n```\n\n# 8- Tools for admin search and filters\n\n## 8.1 Select Related with Filter Related Fields\n\nSame 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.\n\n\nSo, We introduce `FilterWithSelectRelated` to select all related fields while rendering the filter choices\n\n**Example:**\n\n```python\nfrom django.contrib import admin\nfrom django.contrin.auth.models import User\nfrom .models import MyModel, AnotherModel\n\n\nclass AnotherModel(models.Model):\n\n    name = models.CharField()\n    user = models.ForeignKey(User)\n\n    def __str__(self):\n        return f\"{self.name} User: {self.user.username}\"\n\n\nclass MyModel(models.Model):\n\n    name = models.CharField()\n    another_model = models.ForeignKey(AnotherModel)\n\n@admin.register(MyModel)\nclass MyModelAdmin(AdminChangeSelectRelatedMixin, admin.ModelAdmin):\n\n    list_filter = [\n        \"another_model\"\n    ]\n```\n\nin 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.\n\n\n**Example:**\n\n```python\nfrom django.contrib import admin\nfrom django.contrin.auth.models import User\nfrom django_admin_performance_tools.filters import FilterWithSelectRelated\nfrom .models import MyModel, AnotherModel\n\n\nclass AnotherModel(models.Model):\n\n    name = models.CharField()\n    user = models.ForeignKey(User)\n\n    def __str__(self):\n        return f\"{self.name} User: {self.user.username}\"\n\n\nclass MyModel(models.Model):\n\n    name = models.CharField()\n    another_model = models.ForeignKey(AnotherModel)\n\n\nclass AnotherModelFilter(FilterWithSelectRelated):\n    list_select_related = [\"user\"]\n\n\n@admin.register(MyModel)\nclass MyModelAdmin(AdminChangeSelectRelatedMixin, admin.ModelAdmin):\n\n    list_filter = [\n        AnotherModelFilter\n    ]\n```\n\n\n## 8.2 Search Help Text\n\nRecently 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.\n\n![Alt text](/docs/images/search_help_text.png?raw=true \"Search Help Text\")\n\n**Example:**\n\n```python\n\nfrom django.contrib import admin\nfrom django.contrin.auth.models import User\nfrom django_admin_performance_tools.mixins import SearchHelpTextMixin\n\n@admin.register(User)\nclass UserAdmin(SearchHelpTextMixin, admin.ModelAdmin):\n\n    search_fields = [\n        \"email\",\n        \"name\",\n        \"username\",\n        \"phone_number\"\n    ]\n\n```\n\nNot just field names, you can customize any field to represent another text\n\n**Example:**\n\n```python\n\nfrom django.contrib import admin\nfrom django.contrin.auth.models import User\nfrom django_admin_performance_tools.mixins import SearchHelpTextMixin\n\n@admin.register(User)\nclass UserAdmin(SearchHelpTextMixin, admin.ModelAdmin):\n\n    search_fields = [\n        \"email\",\n        \"name\",\n        \"username\",\n        \"phone_number\"\n    ]\n    search_help_text_map = {\n        \"email\": \"Email Address\"\n    }\n```\n\nThere is an abstract class to use if you intend to use all the following features in Model Admin:\n\n- ListPrefetchRelatedMixin\n- ReadonlySelectRelatedMixin\n- AdminChangeSelectRelatedMixin\n- SearchHelpTextMixin\n- NonSelectionActionsMixin\n\ncan be imported from the following path:\n\n```python\nfrom django_admin_performance_tools.admin import AbstractModelAdmin\n```\n\nThere is an abstract classes to use if you intend to use all the following features in Inline Admin:\n\n- ReadonlySelectRelatedMixin\n- AdminChangeSelectRelatedMixin\n\ncan be imported from the following path:\n\n```python\nfrom django_admin_performance_tools.admin import (\n    AbstractStackedInline,\n    AbstractTabularInline\n)\n```\n\n**Upcomming**\n- Auto Complete filters with custome title\n- Filters with custom title\n\n---\n\n# 9- Widgets\n\nSometimes 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.\n\n\n![Alt text](/docs/images/disabled_select.gif?raw=true \"Disabled Select\")\n\n\n**Setup**\n\nadd `FORM_RENDERER` in `settings.py`.\n```python\nFORM_RENDERER = \"django.forms.renderers.TemplatesSetting\"\n```\n\nAdd 'django.forms' to your INSTALLED_APPS settings.\n```python\nINSTALLED_APPS = [\n    ...\n    \"django.forms\",\n]\n```\n\n**Example:**\n\n```python\n\nfrom django import forms\nfrom django_admin_performance_tools.widgets import DisabledSelect\nfrom django.db import models\n\nclass StatusEnum(models.IntegerChoices):\n    INITIAL = 0, \"Initial\"\n    DONE = 1, \"Done\"\n    CLOSED = 2, \"Closed\"\n\nclass ContactForm1(forms.Form):\n    status = forms.ChoiceField(\n        required=False,\n        choices=StatusEnum.choices,\n        widget=DisabledSelect(disabled_options=[StatusEnum.DONE, StatusEnum.CLOSED])\n    )\n```\n---\n\n# 10- Settings\n\nYou can controll some behaves by adding the following in `settings.py`\n\n**- HIDE_QUICK_ACTIONS_DROPDOWN**\n\nThis will hide the Quick Actions dropdown from admin sites\n\nDefault value is `False`\n\n\n**- QUICK_ACTIONS_URL_PATH_PREFIX**\n\nThis is the prefix before all Quick-Actions URL paths/names\n\nDefault value is `quick-actions`\n\n\n**- HIDE_LANGUAGE_DROPDOWN**\n\nThis will hide the Languages dropdown from admin sites\n\nDefault value is `False`\n\n\n[github-repo]: https://github.com/muhammedattif/Django-Admin-Performance-Tools\n[django-formtools]: https://pypi.org/project/django-formtools/\n\n\n## Contributing\n\nIf you are interested to fix an issue or to add new feature, you can just open a pull request.\n\n### Contributors\n<a href = \"https://github.com/muhammedattif/Django-Admin-Performance-Tools/graphs/contributors\">\n  <img src = \"https://contrib.rocks/image?repo=muhammedattif/Django-Admin-Performance-Tools\"/>\n</a>\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "Django Admin Performance Tools",
    "version": "1.0.1",
    "project_urls": {
        "Homepage": "https://github.com/muhammedattif"
    },
    "split_keywords": [
        "django",
        "admin"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "a819276aedb70885014d231de8cb225b4b7ba40def3cfbafea74b71a689e6a91",
                "md5": "b5ce256b8c72b52bfbcfb47fe5c38db5",
                "sha256": "60565c20b4a2a9c364875ade73f6caf7e92b2313b4491dc2816181d98d430f32"
            },
            "downloads": -1,
            "filename": "django_admin_performance_tools-1.0.1-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "b5ce256b8c72b52bfbcfb47fe5c38db5",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.8",
            "size": 33053,
            "upload_time": "2024-01-13T15:59:23",
            "upload_time_iso_8601": "2024-01-13T15:59:23.036772Z",
            "url": "https://files.pythonhosted.org/packages/a8/19/276aedb70885014d231de8cb225b4b7ba40def3cfbafea74b71a689e6a91/django_admin_performance_tools-1.0.1-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "eaa9d519dd22d33faca3d77371bd389f60dfe9d73feea9f4b3eba577088afc59",
                "md5": "e38978b4cabf5a99057e2b2ccb32da67",
                "sha256": "ce8398f54b36738a9f93ba2391c4c268f44b9ab7d52ae81e05df17c68dbf4b8b"
            },
            "downloads": -1,
            "filename": "django-admin-performance-tools-1.0.1.tar.gz",
            "has_sig": false,
            "md5_digest": "e38978b4cabf5a99057e2b2ccb32da67",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8",
            "size": 2019181,
            "upload_time": "2024-01-13T15:59:37",
            "upload_time_iso_8601": "2024-01-13T15:59:37.129667Z",
            "url": "https://files.pythonhosted.org/packages/ea/a9/d519dd22d33faca3d77371bd389f60dfe9d73feea9f4b3eba577088afc59/django-admin-performance-tools-1.0.1.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-01-13 15:59:37",
    "github": false,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "lcname": "django-admin-performance-tools"
}
        
Elapsed time: 0.15848s