Skip to content

Instantly share code, notes, and snippets.

@rafen
Last active January 28, 2024 10:32
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save rafen/eff7adae38903eee76600cff40b8b659 to your computer and use it in GitHub Desktop.
Save rafen/eff7adae38903eee76600cff40b8b659 to your computer and use it in GitHub Desktop.
Django ExtendedActionsMixin allows ModelAdmin classes to execute actions with empty queryset and filtered queryset

ExtendedActionsMixin

This is a Django mixin that allows ModelAdmin classes to execute actions without selecting any objects on the change list of the Django Admin.

To implement it you need to include the Mixin as usual (see https://docs.djangoproject.com/en/1.10/topics/class-based-views/mixins/) and define a class attribute called extended_actions containing a list of string with the name of the actions that you want to be exectued with empty queryset.

If in the action you want to use the same queryset that the user is seeing, you can use the get_filtered_queryset method also provided by the mixin

Example:

@admin.register(Contact)
class ContactAdmin(ExtendedActionsMixin, admin.ModelAdmin):
    list_display = ('name', 'country', 'state')
    actions = ('export',)
    extended_actions = ('export',)
    
    def export(self, request, queryset):
        if not queryset:
            # if not queryset use the queryset filtered by the URL parameters
            queryset = self.get_filtered_queryset(request)

        # As usual do something with the queryset
class ExtendedActionsMixin:
# actions that can be executed with no items selected on the admin change list.
# The filtered queryset displayed to the user will be used instead
extended_actions = []
def changelist_view(self, request, extra_context=None):
# if a extended action is called and there's no checkbox selected, select one with
# invalid id, to get an empty queryset
if "action" in request.POST and request.POST["action"] in self.extended_actions:
if not request.POST.getlist(admin.helpers.ACTION_CHECKBOX_NAME):
post = request.POST.copy()
post.update({admin.helpers.ACTION_CHECKBOX_NAME: 0})
request._set_post(post) # pylint:disable=protected-access
return super().changelist_view(request, extra_context)
def get_changelist_instance(self, request):
"""
Returns a simple ChangeList view instance of the current ModelView.
(It's a simple instance since we don't populate the actions and list filter
as expected since those are not used by this class)
"""
list_display = self.get_list_display(request)
list_display_links = self.get_list_display_links(request, list_display)
list_filter = self.get_list_filter(request)
search_fields = self.get_search_fields(request)
list_select_related = self.get_list_select_related(request)
change_list = self.get_changelist(request)
return change_list(
request,
self.model,
list_display,
list_display_links,
list_filter,
self.date_hierarchy,
search_fields,
list_select_related,
self.list_per_page,
self.list_max_show_all,
self.list_editable,
self,
self.sortable_by,
)
def get_filtered_queryset(self, request):
"""
Returns a queryset filtered by the URLs parameters
"""
change_list = self.get_changelist_instance(request)
return change_list.get_queryset(request)
@thibaut-pro
Copy link

thibaut-pro commented Jun 5, 2020

Thanks for sharing this! It worked great until a recent Django upgrade. admin.ACTION_CHECKBOX_NAME is no longer available.
Replacing it with admin.helpers.ACTION_CHECKBOX_NAME fixed it: https://gist.github.com/thibaut-singlefile/663df42026df103dd20530a338f6c297

@rafen
Copy link
Author

rafen commented Jun 10, 2020

Thanks for sharing this! It worked great until a recent Django upgrade. admin.ACTION_CHECKBOX_NAME is no longer available.
Replacing it with admin.helpers.ACTION_CHECKBOX_NAME fixed it: https://gist.github.com/thibaut-singlefile/663df42026df103dd20530a338f6c297

Thank you @thibaut-singlefile! I do appreciate that you shared your changes. Since someone else finds it useful I will upgrade this Mixin so it works on the latest Django and Python 3.

@gokhanyildiz9535
Copy link

If you are getting this error "__init __ () missing 1 required positional argument: 'sortable_by'". The source of the problem is the return ChangeList {}. Adding self.sortable_by solves the problem. Thanks.

@yoccodog
Copy link

Here is a working version using python 3.8.5 and django 3.1.7. It incorporates the updates from @thibaut-pro and @gokhanyildiz9535 above.

from django.contrib import admin


# https://gist.github.com/rafen/eff7adae38903eee76600cff40b8b659#file-admin-py-L1
class ExtendedActionsMixin:
    # actions that can be executed with no items selected on the admin change list.
    # The filtered queryset displayed to the user will be used instead
    extended_actions = []

    def changelist_view(self, request, extra_context=None):
        # if a extended action is called and there's no checkbox selected, select one with
        # invalid id, to get an empty queryset
        if "action" in request.POST and request.POST["action"] in self.extended_actions:
            if not request.POST.getlist(admin.helpers.ACTION_CHECKBOX_NAME):
                post = request.POST.copy()
                post.update({admin.helpers.ACTION_CHECKBOX_NAME: 0})
                request._set_post(post)  # pylint:disable=protected-access
        return super().changelist_view(request, extra_context)

    def get_changelist_instance(self, request):
        """
        Returns a simple ChangeList view instance of the current ModelView.
        (It's a simple instance since we don't populate the actions and list filter
        as expected since those are not used by this class)
        """
        list_display = self.get_list_display(request)
        list_display_links = self.get_list_display_links(request, list_display)
        list_filter = self.get_list_filter(request)
        search_fields = self.get_search_fields(request)
        list_select_related = self.get_list_select_related(request)

        change_list = self.get_changelist(request)

        return change_list(
            request,
            self.model,
            list_display,
            list_display_links,
            list_filter,
            self.date_hierarchy,
            search_fields,
            list_select_related,
            self.list_per_page,
            self.list_max_show_all,
            self.list_editable,
            self,
            self.sortable_by,
        )

    def get_filtered_queryset(self, request):
        """
        Returns a queryset filtered by the URLs parameters
        """
        change_list = self.get_changelist_instance(request)
        return change_list.get_queryset(request)

@bastiaan85
Copy link

Thank you for supplying this snippet. One question though: why even bother implementing get_changelist_instance here? It leaves out the checkboxes, so regular actions are rendered useless. After removing it, the mixin just works for both classic actions and the 'extended' ones (as expected). Python 3.8.11 Django 3.2.6.

@caramdache
Copy link

One question though: why even bother implementing get_changelist_instance here? It leaves out the checkboxes, so regular actions are rendered useless. After removing it, the mixin just works for both classic actions and the 'extended' ones (as expected). Python 3.8.11 Django 3.2.6.

+1 with Python 3.0 and Django 4.0.3.

@776166
Copy link

776166 commented Dec 2, 2022

Error:
TypeError: ChangeList.__init__() missing 1 required positional argument: 'search_help_text'

Here:
return change_list(

Python3.10, Django==4.1.3

@776166
Copy link

776166 commented Dec 2, 2022

        return change_list(
            request,
            self.model,
            list_display,
            list_display_links,
            list_filter,
            self.date_hierarchy,
            search_fields,
            list_select_related,
            self.list_per_page,
            self.list_max_show_all,
            self.list_editable,
            self,
            self.sortable_by,
            self.search_help_text,  ### <- Here the new line
        )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment