Dynamically generated Django admin actions

Posted in:

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:

Screen shot of Django admin page with actions dropdown

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.

Comments §

Comments should load when you scroll to here...