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 t开发者_运维知识库o perform actions on them. No items have been changed.
Is there a way to override this behaviour and let the action run anyway?
The accepted answer didn't work for me in django 1.6, so I ended up with this:
from django.contrib import admin
from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
class MyModelAdmin(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(ACTION_CHECKBOX_NAME):
post = request.POST.copy()
for u in MyModel.objects.all():
post.update({ACTION_CHECKBOX_NAME: str(u.id)})
request._set_post(post)
return super(MyModelAdmin, 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)
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)
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.
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)
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
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:
- Write the view function
- Put a
@staff_member_required
decorator on it - Add a pattern to your URLconf that points to that view
- 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.
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.
精彩评论