django admin action without selecting objects

Is it possible to create a custom admin action for the django admin that doesn't require selecting some objects to run it on?

If you try to run an action without selecting objects, you get the message:

Items must be selected in order to perform actions on them. No items have been changed.

Is there a way to override this behaviour and let the action run anyway?

Answers


Yuji is on the right track, but I've used a simpler solution that may work for you. If you override response_action as is done below you can replace the empty queryset with a queryset containing all objects before the check happens. This code also checks which action you're running to make sure it's approved to run on all objects before changing the queryset, so you can restrict it to only happen in some cases.

def response_action(self, request, queryset):
    # override to allow for exporting of ALL records to CSV if no chkbox selected
    selected = request.POST.getlist(admin.ACTION_CHECKBOX_NAME)
    if request.META['QUERY_STRING']:
        qd = dictify_querystring(request.META['QUERY_STRING'])
    else:
        qd = None
    data = request.POST.copy()
    if len(selected) == 0 and data['action'] in ('export_to_csv', 'extended_export_to_csv'):
        ct = ContentType.objects.get_for_model(queryset.model)
        klass = ct.model_class()
        if qd:
            queryset = klass.objects.filter(**qd)[:65535] # cap at classic Excel maximum minus 1 row for headers
        else:
            queryset = klass.objects.all()[:65535] # cap at classic Excel maximum minus 1 row for headers
        return getattr(self, data['action'])(request, queryset)
    else:
        return super(ModelAdminCSV, self).response_action(request, queryset)

The accepted answer didn't work for me in django 1.6, so I ended up with this:

from django.contrib import admin

class AdvWebUserAdmin(admin.ModelAdmin):

    ....

    def changelist_view(self, request, extra_context=None):
        if 'action' in request.POST and request.POST['action'] == 'your_action_here':
            if not request.POST.getlist(admin.ACTION_CHECKBOX_NAME):
                post = request.POST.copy()
                for u in MyModel.objects.all():
                    post.update({admin.ACTION_CHECKBOX_NAME: str(u.id)})
                request._set_post(post)
        return super(AdvWebUserAdmin, self).changelist_view(request, extra_context)

When my_action is called and nothing is selected, select all MyModel instances in db.


I wanted this but ultimately decided against using it. Posting here for future reference.


Add an extra property (like acts_on_all) to the action:

def my_action(modeladmin, request, queryset):
    pass
my_action.short_description = "Act on all %(verbose_name_plural)s"
my_action.acts_on_all = True

 

In your ModelAdmin, override changelist_view to check for your property.

If the request method was POST, and there was an action specified, and the action callable has your property set to True, modify the list representing selected objects.

def changelist_view(self, request, extra_context=None):
    try:
        action = self.get_actions(request)[request.POST['action']][0]
        action_acts_on_all = action.acts_on_all
    except (KeyError, AttributeError):
        action_acts_on_all = False

    if action_acts_on_all:
        post = request.POST.copy()
        post.setlist(admin.helpers.ACTION_CHECKBOX_NAME,
                     self.model.objects.values_list('id', flat=True))
        request.POST = post

    return admin.ModelAdmin.changelist_view(self, request, extra_context)

Is there a way to override this behaviour and let the action run anyway?

I'm going to say no there is no easy way.

If you grep your error message, you see that the code is in django.contrib.admin.options.py and the problem code is deep inside the changelist_view.

action_failed = False
selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME)

# Actions with no confirmation
if (actions and request.method == 'POST' and
        'index' in request.POST and '_save' not in request.POST):
    if selected:
        response = self.response_action(request, queryset=cl.get_query_set())
        if response:
            return response
        else:
            action_failed = True
    else:
        msg = _("Items must be selected in order to perform "
                "actions on them. No items have been changed.")
        self.message_user(request, msg)
        action_failed = True

It's also used in the response_action function as well, so you can't just override the changelist_template and use that either -- it's going to be easiest to define your own action-validity checker and runner.


If you really want to use that drop down list, here's an idea with no guarantees.

How about defining a new attribute for your selection-less admin actions: myaction.selectionless = True

Copy the response_action functionality to some extent in your overridden changelist_view that only works on actions with a specific flag specified, then returns the 'real' changelist_view

    # There can be multiple action forms on the page (at the top
    # and bottom of the change list, for example). Get the action
    # whose button was pushed.
    try:
        action_index = int(request.POST.get('index', 0))
    except ValueError:
        action_index = 0

    # Construct the action form.
    data = request.POST.copy()
    data.pop(helpers.ACTION_CHECKBOX_NAME, None)
    data.pop("index", None)

    # Use the action whose button was pushed
    try:
        data.update({'action': data.getlist('action')[action_index]})
    except IndexError:
        # If we didn't get an action from the chosen form that's invalid
        # POST data, so by deleting action it'll fail the validation check
        # below. So no need to do anything here
        pass

    action_form = self.action_form(data, auto_id=None)
    action_form.fields['action'].choices = self.get_action_choices(request)

    # If the form's valid we can handle the action.
    if action_form.is_valid():
        action = action_form.cleaned_data['action']
        select_across = action_form.cleaned_data['select_across']
        func, name, description = self.get_actions(request)[action]

        if func.selectionless:
             func(self, request, {})

You'd still get errors when the 'real' action is called. You could potentially modify the request.POST to remove the action IF the overridden action is called.

Other ways involve hacking way too much stuff. I think at least.


Since object selection isn't part of what you need, it sounds like you might be best served by creating your own admin view.

Making your own admin view is pretty simple:

  1. Write the view function
  2. Put a @staff_member_required decorator on it
  3. Add a pattern to your URLconf that points to that view
  4. Add a link to it by overriding the relevant admin template(s)

You can also use a new 1.1 feature related to this, but you may find it simpler to do as I just described.


Ok, for those of you stubborn enough to want this working, this is an ugly hack(for django 1.3) that will allow ANY action to run even if you didn't select anything.

You have to fool the original changelist_view into thinking that you have something selected.

class UsersAdmin(admin.ModelAdmin):

    def changelist_view(self, request, extra_context=None):
        post = request.POST.copy()
        if helpers.ACTION_CHECKBOX_NAME not in post:
            post.update({helpers.ACTION_CHECKBOX_NAME:None})
            request._set_post(post)
        return super(ContributionAdmin, self).changelist_view(request, extra_context)

So, in your modeladmin you override the changelist_view adding to the request.POST a key that django uses to store the ids of the selected objects.

In your actions you can check if there are no selected items with:

if queryset == None:
    do_your_stuff()

It goes without saying that you are not supposed to do this.


I use the following mixin to create actions that do not require the user to select at least one object. It also allow you to get the queryset that the user just filtered: https://gist.github.com/rafen/eff7adae38903eee76600cff40b8b659

here an example of how to use it (there's more info of how to use it on the link):

@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

I made a change to @AndyTheEntity response, to avoid calling the action once per row.

        def changelist_view(self, request, extra_context=None):
                actions = self.get_actions(request)
                if (actions and request.method == 'POST' and 'index' in request.POST and
                        request.POST['action'].startswith('generate_report')):
                    data = request.POST.copy()
                    data['select_across'] = '1'
                    request.POST = data
                    response = self.response_action(request, queryset=self.get_queryset(request))
                    if response:
                        return response
                return super(BaseReportAdmin, self).changelist_view(request, extra_context)

The simplest solution I found was to create your django admin function as per django docs then in your website admin select any object randomly and run the function. This will pass the item through to your function but you simply don't use it anywhere so it is redundant. Worked for me.


Need Your Help

Node.js module.exports in CoffeeScript

node.js coffeescript

I'm working on a simple example; I can get it to work with Javascript, but there is something wrong with my CoffeeScript version.