Django tips: auto-populated fields
One of these days I’m going to run out of frequently-asked Django questions to answer. But today isn’t that day, so let’s look at one of the most common questions people ask: how do you set up a model with one or more fields that never show up in add/edit forms, and get automatically populated with some specific value?
A simple example
Let’s say you’re writing a to-do list application, and you want to have fields on the list model to store the date it was created and the time it was last updated. The model might look something like this:
class TodoList(models.Model): title = models.CharField(maxlength=100) created = models.DateField() updated = models.DateTimeField()
Now, in an ideal world you’d never publicly display the created
and updated
fields in a form; you’d just have them filled in automatically with the right values. So let’s make that happen by editing the model slightly:
class TodoList(models.Model): title = models.CharField(maxlength=100) created = models.DateField(editable=False) updated = models.DateTimeField(editable=False) def save(self): if not self.id: self.created = datetime.date.today() self.updated = datetime.datetime.today() super(TodoList, self).save()
Of course, you’ll want to add import datetime
up at the top of your models file. Anyway, there are two changes that stand out here:
-
The
created
andupdated
fields now haveeditable=False
. This means that they will never show up in an automatic manipulator or in the admin. They’re still required, they just won’t be displayed anywhere. -
There’s now a custom
save
method on the model which does the actual work. The first time a new to-do list is saved, it won’t have anid
because there’s no row for it in the database yet. So theif not self.id
check tells us that the to-do list is being saved for the first time, and we drop in a single line which fills the current date into thecreated
field. We also want theupdated
field to be updated on every save, so we add a line which sets that to the current date and time. Then thesuper
call saves the list.
It really is that easy, and it works for a lot of field types — just give the field editable=False
and fill it in with a custom save
method. If the field is only supposed to be filled in for the first save, use the if not self.id
trick to accomplish that. The only thing that will vary is how you fill in the fielf; if you wanted to automatically populate a SlugField
, for example, you could import the slugify
function from django.template.defaultfilters
and use that to generate a slug from some other field.
But the thing people really clamor for is a foreign key to the auth User
model which automatically fills in the current user, for doing something like a “created_by” field. This trick won’t work there, because models intentionally have no access to the “current user” — that decoupling makes it possible to use Django for things that aren’t web applications (and thus don’t have HttpRequest
instances which carry user
instances around with them), but it keeps this trick from working out-of-the-box for that particular case.
So how can we auto-populate a user into a model?
The Holy Grail
First, let’s change the model to have a created_by
field:
class TodoList(models.Model): title = models.CharField(maxlength=100) created = models.DateField() updated = models.DateTimeField() created_by = models.ForeignKey(User) def save(self): if not self.id: self.created = datetime.date.today() self.updated = datetime.datetime.today() super(TodoList, self).save()
Of course, you’ll want to add from django.contrib.auth.models import User
up at the top of the file so you can reference User
like that. Now, solving this is going to require two short pieces of code: a custom manager and a custom manipulator. Here’s the manager class:
class TodoListManager(models.Manager): def create_list(self, title, user): new_list = self.model(title=title, created_by=user) new_list.save() return new_list
Then make it the default manager for the TodoList
class by adding objects=TodoListManager()
in the model definition. All of the normal querying methods will work as expected, because we haven’t overridden any of them. But we have added a new method: create_list
, which expects a title and a User
instance, and creates a new to-do list using them (the created
and updated
fields will, as before, be automatically filled in when the new list is saved). To see how that works, let’s look at the manipulator:
from django import forms class TodoListForm(forms.Manipulator): def __init__(self, request): self.fields = ( forms.TextField(field_name='title', length=30, maxlength=100, is_required=True), ) self.request = request def save(self, new_data): return TodoList.objects.create_list(new_data['title'], self.request.user)
Here’s a simple view which puts it all together:
@login_required def create_todo_list(request): manipulator = TodoListForm(request) if request.POST: new_data = request.POST.copy() errors = manipulator.get_validation_errors(new_data) if not errors: manipulator.do_html2python(new_data) new_list = manipulator.save(new_data) return HttpResponseRedirect(new_list.get_absolute_url()) else: errors = new_data = {} form = forms.FormWrapper(manipulator, new_data, errors) return render_to_response('create_todo_list.html', {'form': form})
The trick here is that creating the custom manipulator takes the HttpRequest
as an argument, and later when you use it to save, it pulls the User
out of that request and uses it to create the new to-do list. The view has the login_required
decorator to make sure there will always be a valid user.
And voilà. You have a to-do list which automatically gets the current user assigned to its created_by
field. Not hard at all, right? And as a bonus, the create_list
method on the manager is useful in other circumstances as well — any time you have access to a valid User
object, you can call this method to create a new list with that user filled in.
One caveat
Now, some people are probably going to be unhappy with the fact that the automatic user population doesn’t integrate into Django’s admin application in any way. That’s unavoidable, and my own humble opinion is that this isn’t something that should — the admin is for people who are 100% trusted to do the right thing with your site’s content, so if you’re doing all your model creation through the admin you should just show the created_by
field and trust your site staff to fill it in correctly.
But for the (probably more common) case where users are creating content, it’s great: just point them at a view like the one I’ve listed above, and everything will happen as it should.