Django's admin actions are a useful way of applying a process to a set of records. Django by default has only “delete selected objects” for most models, but you can add your own.
The UI for choosing an action, however, is just a drop-down box. This means that actions are great for a completely pre-defined action, but if you need other parameters for your action, you normally need an action that redirects to an intermediate page, because there is no UI for collecting additional parameters.
However, there is an in-between method that doesn't require as much work as creating a whole new view. It's useful if there are a small number of possible options for the parameters you want to pass to your action.
Suppose, for example, you have an e-commerce site, and customers can create
orders in your system. You want to use the admin to assign the orders to
different members of staff, and the number of staff is few, but not fixed - your
staff are all in the database as User
objects with is_staff == True
. If
you currently have Andrew, Emily and Joe as staff, it would be nice if you could
have actions like the following appear in the actions drop-down:
Assign order to Andrew
Assign order to Emily
Assign order to Joe
Something like this:
The basic technique to do this is just to dynamically generate the actions. This is possible by implementing ModelAdmin.get_actions. The documentation mentions enabling and disabling actions, but in fact you can use it to create new actions.
The code is only slightly advanced more advanced than the documentation – we create action functions just like in the docs, but we need a function-creating function, which actually returns closures for the parameters. If that sounds tricky for people who aren't yet comfortable with closures etc., the code might actually be clearer. It looks like this:
def make_assign_to_user_action(user): def assign_to_user(modeladmin, request, queryset): for order in queryset: order.assign_to(user) # Method on Order model messages.info(request, "Order {0} assigned to {1}".format(order.id, user.first_name)) assign_to_user.short_description = "Assign to {0}".format(user.first_name) # We need a different '__name__' for each action - Django # uses this as a key in the drop-down box. assign_to_user.__name__ = 'assign_to_user_{0}'.format(user.id) return assign_to_user class OrderAdmin(admin.ModelAdmin): # (some things snipped) def get_actions(self, request): actions = super(OrderAdmin, self).get_actions(request) for user in get_user_model().objects.filter(is_staff=True).order_by('first_name'): action = make_assign_to_user_action(user) actions[action.__name__] = (action, action.__name__, action.short_description) return actions
The actual action is the assign_to_user
function. Actions must take
modeladmin
, request
and queryset
, which leaves no room for
additional parameters, but we get around this by making it a closure which has
access to the user
parameter passed in to make_assign_to_user_action
.
We also need to ensure that each action function gets a unique name. To be safe,
we need to choose a unique distinguishing ID for this name, which won't change
for different requests, and in this case the PK of the user to assign to works
well. This name is used as the value attribute in the drop-down box, just as
short_description
is used for the label.
As per the documentation, the return value from get_actions
needs to be a
dictionary where the keys are action names, and the values are tuples containing
the action callable, the action name, and the action description.
That's it!
Full source code for a working example app, with demo sqlite DB, is available in the djangoadmintips repo. If you liked this, follow my @djangoadmintips account on Twitter.