Django actions as their own intermediate page
Simple actions in Django’s admin are well documented. For example, I have a PageLink model that contains a ForeignKey to my blog posts (pages) and another ForeignKey to the URLs I’m using on that page. Most URLs, besides getting auto-linked or hard-linked within the post, are also presented as a list at the bottom of the post. Sometimes, though, I don’t want an URL showing up in the list even though I do want some text within the post linking to that URL. So I have a “displayInList” BooleanField.
Making an action to set displayInList to false is easy:
[toggle code]
- from django.template import Template, Context
- hideSuccess = Template('Hid {{ count }} link{{ count|pluralize }}')
-
class PageLinkAdmin(admin.ModelAdmin):
- list_display = ['url', 'page', 'added', 'displayInList', 'category', 'rank']
- list_filter = ['displayInList']
- search_fields = ['page__title', 'url__title']
- ordering = ['-added']
- actions = ['hideFromList']
-
def hideFromList(self, request, queryset):
-
for link in queryset:
- link.displayInList = False
- link.save()
- self.message_user(request, hideSuccess.render(Context({'count':queryset.count()})))
-
for link in queryset:
- hideFromList.short_description = "Do not display in list of links on page"
Which is not very useful as an example, since the same thing can be done with one line in Django 1.1:
- list_editable = ['displayInList']
Actions become more involved when, instead of a simple boolean toggle, we need to be able to make some choices between selecting the items to be affected and applying the change to those items. For example, you can see a “category” in the list_display above. Each PageLink also has a ForeignKey to a KeyWord model. This allows me to divide a longer list of URLs into sections.
Commonly, I don’t even know what the appropriate categories are until I’ve finished writing the post and I’ve already added the URLs. It’d be a lot easier for me to be able to use an action to select all of the PageLinks that I want in a category, rather than have to go to each PageLink one by one. That requires, however, choosing the category.
In cases where a choice must be made (such as choosing from a list of categories), the documentation recommends redirecting to another URL, appending the list of object IDs to the redirect URL as a GET query string and using a view. But besides being a bit wonky, that loses some of the benefits of building the action into the ModelAdmin class.
By not returning anything, the hideFromList action results in Django’s admin just displaying the list of PageLinks again. But we can return a standard Django response object from the action. This is where the wonderful stateless nature of HTTP comes to our rescue. The action menu is just a form. Django doesn’t know anything about the form, it only knows what form data it receives1. As long as the form submission contains a series of “_selected_action” fields containing IDs, and an “action” field containing the action’s name, Django will continue to send those “selected” objects to that action as a query set.
[toggle code]
- from django.shortcuts import render_to_response
- import django.forms as forms
- from django.http import HttpResponseRedirect
- from django.template import Template, Context
-
class PageLinkAdmin(admin.ModelAdmin):
- list_display = ['url', 'page', 'added', 'displayInList', 'category', 'rank']
- list_filter = ['displayInList']
- search_fields = ['page__title', 'url__title']
- ordering = ['-added']
- actions = ['changeCategory']
- categorySuccess = Template('{% load humanize %}Categorized {{ count|apnumber }} link{{ count|pluralize }} as {{ category.key }}')
-
class CategoryForm(forms.Form):
- _selected_action = forms.CharField(widget=forms.MultipleHiddenInput)
- category = forms.ModelChoiceField(KeyWord.objects)
-
def changeCategory(self, request, queryset):
- form = None
-
if 'cancel' in request.POST:
- self.message_user(request, 'Canceled link categorization')
- return
-
elif 'categorize' in request.POST:
- #do the categorization
- form = self.CategoryForm(request.POST)
-
if form.is_valid():
- category = form.cleaned_data['category']
-
for link in queryset:
- link.category = category
- link.save()
- self.message_user(request, self.categorySuccess.render(Context({'count':queryset.count(), 'category':category})))
- return HttpResponseRedirect(request.get_full_path())
-
if not form:
- form = self.CategoryForm(initial={'_selected_action': request.POST.getlist(admin.ACTION_CHECKBOX_NAME)})
- return render_to_response('cms/categorize.html', {'links': queryset, 'form': form, 'path':request.get_full_path()})
- changeCategory.short_description = 'Set category'
If the cancel button is pressed, it returns nothing; if a category has been chosen, it loops through all of the objects in the queryset and performs the action; then, it returns nothing, just as a simple action would have, including sending a user message. I’m using Django’s Template system for string substitution, because it annoys me to see messages such as “changed 1 links”2.
If no category has been chosen yet, it creates a response object from (in this case) “cms/categorize.html” and returns that. Here’s the categorize.html that I use:
[toggle code]
- <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-
<head>
- <title>Categorize Links</title>
- <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
- <meta name="description" content="Link categorization control." />
-
<style type="text/css">
-
h1 {
- text-align: center;
- background-color: green;
- }
-
body {
- margin: 10%;
- border: solid .2em black;
- }
-
p, ul, h2, form {
- margin-left: 10%;
- margin-right: 10%;
- }
-
h1 {
- </style>
- </head>
-
<body>
- <h1>Categorize Links</h1>
- <p>Choose the category for the selected link{{ links|pluralize }}:</p>
-
<form method="post" action="{{ path }}">
-
<table>
- {{ form }}
- </table>
-
<p>
- <input type="hidden" name="action" value="changeCategory" />
- <input type="submit" name="cancel" value="Cancel" />
- <input type="submit" name="categorize" value="Categorize" />
- </p>
-
<table>
- </form>
- <h2>This categorization will affect the following:</h2>
-
<ul>
-
{% for link in links %}
- <li>{{ link.url.linkHTML }} on page {{ link.page.linkHTML }}</li>
- {% endfor %}
-
{% for link in links %}
- </ul>
- </body>
-
<head>
- </html>
The only special thing here is that the action field is hard-coded as a hidden field. Without it, Django won’t recognize that an action’s been submitted on submit.
Note, if you’re attempting to use the IDs on the hidden fields or validate the page, forms.MultipleHiddenInput currently gives each input the same ID. It looks like this will be fixed in the next point revision.
Django doesn’t even know what kind of a form input provided that data. That the action menu is a select means nothing: it’s still just a form field on the submission end, and we can emulate it with a hidden input field (or even a text input field) if we need to.
↑If you aren’t loading django.contrib.humanize in your settings.py file, you don’t have the apnumber filter; remove the “load humanize” tag and the “apnumber” filter from the “categorySuccess” property.
↑
- Django Admin actions at Django
- “The basic workflow of Django’s admin is, in a nutshell, ‘select an object, then change it.’ This works well for a majority of use cases. However, if you need to make the same change to many objects at once, this workflow can be quite tedious. In these cases, Django’s admin lets you write and register ‘actions’—simple functions that get called with a list of objects selected on the change list page.”
- django.contrib.humanize at Django
- “A set of Django template filters useful for adding a ‘human touch’ to data.” It includes adding commas to integers to indicate thousands, a natural day function (yesterday, today, tomorrow), and a few other useful number formatting filters.
- Ticket #11843 MultipleHiddenInput outputs invalid HTML
- “Inputs all get the same ID. CheckboxSelectMultiple already has a fix for this, but not MultipleHiddenInput.”
More Django
- Converting an existing Django model to Django-MPTT
- Using a SQL database to mimic a filesystem will, eventually, create bottlenecks when it comes to traversing the filesystem. One solution is modified preordered tree traversal, which saves the tree structure in an easily-used manner inside the model.
- Two search bookmarklets for Django
- Bookmarklets—JavaScript code in a bookmark—can make working with big Django databases much easier.
- Fixing Django’s feed generator without hacking Django
- It looks like it’s going to be a while before the RSS feed generator in Django is going to get fixed, so I looked into subclassing as a way of getting a working guid in my Django RSS feeds.
- ModelForms and FormViews
- This is just a notice because when I did a search, nothing came up. Don’t use ModelForm with FormView, use UpdateView instead.
- Django: fix_ampersands and abbreviations
- The fix_ampersands filter will miss some cases where ampersands need to be replaced.
- 29 more pages with the topic Django, and other related pages
Very useful post! It works like a charm
Only a couple of questions:
* Can categorize.html page take the admin look and feel? I'll try to {include} admin base templates.
* How to make the select form more scalable? (suppose you have a lot of KeyWords)
Aníbal at 9:48 p.m. November 4th, 2009
hL7mL
It probably can take the admin look and feel; for something that simple, it didn't seem worth it to me to look into it. If you get it working, blog it and post the link!
As far as having lots of keywords, I tend to just make sure they're in alphabetical order and let the browser handle it. But I currently only have a thousand key words. I might change my mind if it were to go up to 10,000.
capvideo at 2:22 a.m. November 5th, 2009
tVAhq
Excellent post!
I was trying to do that without success because I have forgotten:
"The only special thing here is that the action field is hard-coded as a hidden field. Without it, Django won’t recognize that an action’s been submitted on submit."
Maybe you can HIGHLIGHT last paragraph ;)
Pancho Jay in Argentina at 4 p.m. June 28th, 2012
MEe81