Django Tips: Variable Choice Lists
Been a while since I added to this series. I've come across a couple of repeated questions lately, so it's time to give back to the knowledge pool again.
This time: using iterators to customise the options presented via the choices
attribute on a model field.
Background
Before launching into the solution, let's consider the problem we are trying to solve. If you have a model field that is intended to hold only one of a number of limited values, Django provides the choice
attribute. You can use it like so:
class Document(models.Model):
CHOICES = [(0, 'private'), (1, 'public')]
...
status = models.IntegerField(choices=CHOICES)
When you use this in a form, only the two choices private and public will be presented and the database will store either 0 or 1, depending on the choice you made.
Aside: People often forget that when you retrieve such a model from the database, although the status
field contains 0 or 1, you can get back the string version of the choice using the get_status_display()
method of the model. Replace status with the name of the field for your own use. This is explained under get_FOO_display() in the Django documentation.
When Isn't This Enough?
There are two cases where the previous example falls a bit short.
The first case is when the list of choices is being updated regularly via changes to the database, or in some other way. In this situation, choices
isn't the right approach to the problem. You are really talking about a dynamic relation to another data set. So model it that way: use a ForeignKey
field to a table containing the list of choices and the values to store.
The second case is more subtle. Suppose you have a document presentation system. Documents on the production site are either public or private (more or less, this is the above example). However, the same code runs on a staging system as well, where documents are initially uploaded, reviewed and edited. On this system, the choices can include something along the lines of "ready for review" and "needs editing". This is a slight variation on similar systems I've implemented for a couple of clients recently, so it's not too unrealistic (although I've simplified a bunch of details).
In the second scenario, above, the list of choices is essentially static. So we are justified in using the choices
attribute. However, the intiial values vary depending upon the system type — which we might reasonably control using a settings variable.
Now, it's generally a good idea to avoid referring to settings.*
in the definition of fields and methods in Django. This way you can safely import the code without needing to have configured the settings module, which usually feels like neater code organisation (import everything, then configure, if you're using manual configuration). To my eye, using settings.FOO
in declarations also looks a litle awkward (intuitively, it feels like a leaky abstraction, since we're delving into the depths of a module at the top-level).
For whatever reasons, whether you agree with me or not, I'm going to avoid using settings in my field declaration. Instead, I'm going to use a little-known (and not usually required) feature of the choices
attribute: you can pass it a Python iterator instead of a sequence. So I can rewrite my example as follows:
from django.db import models
from django.conf import settings
def status_choices():
choice_list = [
('private', 'private'),
('public', 'public')]
if hasattr(settings, 'STAGING') and settings.STAGING:
choice_list.extend([
('review', 'ready for review'),
('edit', 'needs editing')])
for choice in choice_list:
yield choice
class Document(models.Model):
...
status = models.CharField(maxlength=10, choices=status_choices())
You can see here that all the dependency on settings
is inside the iterator function. So it isn't evaluated until Django needs to actually display the choices, which should be long after configuration has taken place. This relies partly on the fact that the Python compiler knows this is a generator function (because of the keyword yield
) and consequently executes none of the code until the first value is retreived from the generator.
I would also draw attention to a couple of other implementation decisions I made in this code:
-
The extra options only appear if the (optional)
settings.STAGING
setting is set toTrue
. Note that this "fails safe", in the sense that if you forget to include theSTAGING
setting, it won't inadvertently expose the extra options and documents to the wider public. I made the setting optional, because I'm just a nice guy, and so had to first check that it existed usinghasattr()
before I tried to access it. You may or may not wish to be that flexible. -
I switched from storing integers, as in my first example, to storing short, readable strings in the database. I prefer this method, because it avoids the problems associated with having magic numbers in the database column. If you see the number '2' in the database, what does it mean? If you see the string 'review', things are a little more mnemonic. I've noticed a tendency for people to use integer values with the
choices
attribute; perhaps they are forgetting it works on pretty much any field andCharField
fields are often a good choice?
Cavaet
If you are very familiar with Django, or tried to experiment a little with this example, you'll realise I have not told the entire truth here. The whole argument about using an iterator to avoid accessing the settings
module too early is pointless. You cannot currently import django.db.models
without configuring the settings
module, so there's a chicken-and-egg problem there. However, I consider that to be a (very small) bug in Django and it's something I want to fix in the near future. You should be able to import modules without having done any configuration.
You probably won't need to use this technique very often at all. Every now and again, though, you will run across a configuration where being able to construct an intelligent choices
list will help the code layout flow more smoothly.