Creating an Alphabetical Filter in Django’s Admin
by Corey Oordt • Published 15 Jul 2010
Update: See the new blog post about the updated version.
Django’s admin includes a great date hierarchy filter for navigating date data in a model. I’ve come into a few situations where it would be nice to have a similar filter for the alphabet.
If you are managing hundreds or hundreds of thousands of names, tags or other alphabetical data, it would be nice to filter things alphabetically.
Using the date hierarchy as the template, I’ve implemented an alphabetical filter for the admin.
The code and sample project is available on github and will soon be on PyPi.
Implementing the Alphabet Filter
There are three parts:
- The admin
change_list.html
template - The alphabet filter template tag
- The
alphabet.html
template
The change_list.html Template
This template is easy to override on a per-model basis or per-application basis simply by where you save it. The current change_list.html template cannot be installed on a project wide basis as it extends admin/change_list.html
and doing so leads to an infinite loop.
Per Model: <application>/templates/admin/<application>/<model>/change_list.html
or <project>/templates/admin/<application>/<model>/change_list.html
Per Application: <application>/templates/admin/<application>/change_list.html
or <project>/templates/admin/<application>/change_list.html
Project Wide: Would have to copy contrib/admin/templates/admin/change_list.html
to <project>/templates/admin/change_list.html
and apply the appropriate changes.
The template extends admin/change_list.html
adding in some extra styling and overriding the date_hierarchy
block.
{% extends "admin/change_list.html" %} {% load admin_list alphafilter %} {% block extrastyle %} {{block.super}} <style type="text/css" media="screen"> .change-list ul.alphabetfilter li { width: 0.7em; } .inactive { color: #999; } .selected { color: red; } </style> {% endblock %} {% block date_hierarchy %}{% date_hierarchy cl %}{% alphabet cl %}{% endblock %}
The alphafilter
Template Tag
The simplest part of the template tag renders the alphabet.html
template with letters, with each letter linking to the same page and adding a query string in the format <field>__istartswith=<letter>
.
In order to know which field to query, we’ll look for an alphabet_filter
attribute on the model’s admin object, just as you would have a date_hierarchy
attribute.
We’ll get all this information through the ChangeList
object the admin template passes. The primary attributes we’ll be using are:
- model_admin: The
ModelAdmin
object for the model - params: The
GET
parameters - get_query_string: A method to generate a query string based on the current query string.
The first part of our template tag looks like:
def alphabet(cl): if not getattr(cl.model_admin, 'alphabet_filter', False): return field_name = cl.model_admin.alphabet_filter alpha_field = '%s__istartswith' % field_name alpha_lookup = cl.params.get(alpha_field, '') link = lambda d: cl.get_query_string(d)
Django’s date hierarchy only allows the selection of data that is in the database. It doesn’t show every year, it shows the years that have data behind them. While the alphabet is pretty static and we might get away with just listing the alphabet with the query string links, what about numbers, punctuation, foreign languages and the plethora of characters that can be in today’s strings? And wouldn’t it also be nice to know which characters have data behind them and which don’t?
To accomplish this, we’re going to have to look in the database. We’ll create a new function called _get_available_letters
to query the distinct leading characters of the field specified.
def _get_available_letters(field_name, db_table): from django.db import connection, transaction from django.conf import settings qn = connection.ops.quote_name sql = "SELECT DISTINCT UPPER(SUBSTR(%s, 1, 1)) as letter FROM %s" \ % (qn(field_name), qn(db_table)) cursor = connection.cursor() cursor.execute(sql) rows = cursor.fetchall() or () return set([row[0] for row in rows])
The custom query should work for all database engines. The function returns a set
instead of a list to make it easy to keep the list of characters unique.
Going back to the alphabet
function, we’ll deal with the variability in characters and showing which characters are used.
def alphabet(cl): if not getattr(cl.model_admin, 'alphabet_filter', False): return field_name = cl.model_admin.alphabet_filter alpha_field = '%s__istartswith' % field_name alpha_lookup = cl.params.get(alpha_field, '') link = lambda d: cl.get_query_string(d) import string letters_used = _get_available_letters(field_name, cl.model._meta.db_table) all_letters = list(set([x for x in string.uppercase+string.digits]) | letters_used) all_letters.sort()
We end up with two collections: the characters used in the database and all the characters available. The collection of all characters is created by taking a base set of uppercase letters and digits and joining it with the characters used set. The Python string
module has locale-aware constants to handle foreign languages. (Being an Ameri-centric a-hole, I can’t verify this works in other languages. Please let me know!)
The all_letters
variable now is a list that will contain at least digits and letters, but also any other character that happens to be in the database. The letters_used
is a set
of characters found in the data.
The last part of the template tag is setting up the context. We will supply the template with a list of dictionaries, each with link
, title
, active
, and has_entries
. We’ll create the list of choices using a list comprehension and add in a choice of “All” at the beginning.
def alphabet(cl): if not getattr(cl.model_admin, 'alphabet_filter', False): return field_name = cl.model_admin.alphabet_filter alpha_field = '%s__istartswith' % field_name alpha_lookup = cl.params.get(alpha_field, '') link = lambda d: cl.get_query_string(d) import string letters_used = _get_available_letters(field_name, cl.model._meta.db_table) all_letters = list(set([x for x in string.uppercase+string.digits]) | letters_used) all_letters.sort() choices = [{ 'link': link({alpha_field: letter}), 'title': letter, 'active': letter == alpha_lookup, 'has_entries': letter in letters_used,} for letter in all_letters] all_letters = [{ 'link': cl.get_query_string(None,alpha_field), 'title': _('All'), 'active': '' == alpha_lookup, 'has_entries': True },] return {'choices': all_letters + choices} alphabet = register.inclusion_tag('admin/alphabet.html')(alphabet)
The alphabet.html Template
The alphabet.html template is pretty basic. Here it is with some extra formatting for readability:
<div class="xfull"> <ul class="toplinks alphabetfilter"> {% for choice in choices %} <li> {% if choice.has_entries %} <a href="{{ choice.link }}"> {% else %} <span class="inactive"> {% endif %} {% if choice.active %}<span class="selected">{% endif %} {{ choice.title }} {% if choice.active %}</span>{% endif %} {% if choice.link %}</a>{% else %}</span>{% endif %} </li> {% endfor %} </ul><br class="clear" /> </div>
There’s a few conditions to check so we can have proper formatting.
If the choice has entries, render the character within a link, otherwise render the character within a
span
.If the choice is currently selected, render the character within a
span
The Final Product
The example project included with the alphafilter source includes an app called testtags. It has the alphabet filter applied to the “sorted_name” field. In the screen shot you see that ‘All’ is currently selected and all the characters are grey except ‘#’ and ‘O’. The ‘#’ symbol was added to the list only because it exists in the database.
blog comments powered by Disqus