Django admin actions and intermediate pages | Tagging multiple articles at once

The other day I created an "Ireland" tag on this blog and thought, hey, wouldn't it be cool to go back and tag all the posts I wrote about events in Ireland, so I have an easy way to see what's going on locally?

Of course retagging each post manually would suck so I decided to create a new Django admin action, with an intermediate page that would present me with my current list of tags so I could select one, then update.

I also wanted:

  1. To keep the Django admin look and feel
  2. To have the nice admin message "X posts were successfully updated"

Now the Django documentation is very clear at explaining and showing how to do each of those, but not together.

Here's how I ended up doing it, see links at the end for other helpful resources. I'm quite pleased with this solution because it's done the Django way (through the use of forms, etc) rather than by working around Django.

Directly in the ModelAdmin section for blog posts, create an inner class for the new form (tag picker in my case), and a function to process the form like you would in a normal view. Don't forget to add the new method to the actions list.

actions = ['add_tag']

class AddTagForm(forms.Form):
    _selected_action = forms.CharField(widget=forms.MultipleHiddenInput)
    tag = forms.ModelChoiceField(Tag.objects)

def add_tag(self, request, queryset):
    form = None

    if 'apply' in request.POST:
        form = self.AddTagForm(request.POST)

        if form.is_valid():
            tag = form.cleaned_data['tag']

            count = 0
            for article in queryset:
                count += 1

            plural = ''
            if count != 1:
                plural = 's'

            self.message_user(request, "Successfully added tag %s to %d article%s." % (tag, count, plural))
            return HttpResponseRedirect(request.get_full_path())

    if not form:
        form = self.AddTagForm(initial={'_selected_action': request.POST.getlist(admin.ACTION_CHECKBOX_NAME)})

    return render_to_response('admin/add_tag.html', {'articles': queryset,
                                                     'tag_form': form,
add_tag.short_description = "Add tag to articles"

And here's my intermediate page (quite simple!), which uses the admin look & feel and is located in templates/admin/add_tag.html.

{% extends "admin/base_site.html" %}

{% block content %}

<p>Select tag to apply:</p>

<form action="" method="post">

    {{ tag_form }}

    <p>The tag will be applied to:</p>

    <ul>{{ articles|unordered_list }}</ul>

    <input type="hidden" name="action" value="add_tag" />
    <input type="submit" name="apply" value="Apply tag" />

{% endblock %}

The hidden field is essential for Django to recognise the form submission as an admin action.

Update: Follow this ticket to learn how to apply an admin action to selected items on multiple pages -- or look at the comments below for tips!

Example of the code in action:

Selecting articles and the new admin action --> Select the tag to apply --> The message is displayed