Django Patterns

Automatically Filling in a User in the Admin

What problem does this pattern solve?

  • Easily keep track of the last user who modified a record
  • Automatically set the “author” of a record

Why should I use it?

It is a safe way to save the user time and still keep track of the information.

Implementation

The important parts of the implementation are:

  1. The ForeignKey to User field needs blank=True
  2. Create a ModelForm that assigns a fake User to the field.
  3. Override the ModelAdmin.save_model() function to assign the request.user to the field.

For discussion we’ll use this model with two relations to the User model:

coolblog/models.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Entry(models.Model):
    title = models.CharField(max_length=250)
    slug = models.SlugField()
    pub_date = models.DateField(default=datetime.datetime.today)
    author = models.ForeignKey(
        User, 
        related_name='entries', 
        blank=True)
    body = models.TextField()
    last_modified = models.DateTimeField(auto_now=True)
    last_modified_by = models.ForeignKey(
        User, 
        related_name='entry_modifiers',
        blank=True)

This Entry model has two fields that we want to fill automatically: author and last_modified_by. Notice both fields have blank=True. This is important so we can get past some initial Django validation.

Faking validation

Whenever you save a model, Django attempts to validate it. Validation will fail without special validation tomfoolery. The first stage of validation fakery was setting blank=True on the fields. The next stage involves setting a temporary value for each field.

coolblog/forms.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from django.contrib.auth.models import User

class EntryForm(forms.ModelForm):
    class Meta:
        model = Entry
    
    def clean_author(self):
        if not self.cleaned_data['author']:
            return User()
        return self.cleaned_data['author']
    
    def clean_last_modified_by(self):
        if not self.cleaned_data['last_modified_by']:
            return User()
        return self.cleaned_data['last_modified_by']

The lower-level Django model validation actually checks if the value of a related field is an instance of the correct class. Since a form’s validation happens before any attempt to save the model, we create a new ModelForm, called EntryForm. The clean_author() and clean_last_modified_by() methods check for an empty value and assigns it an unsaved and empty instance of User, and Django is happy.

Saving the model

In the model’s ModelAdmin, we make a few adjustments.

coolblog/admin.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class EntryAdmin(admin.ModelAdmin):
    form = EntryForm
    
    list_display = ('title', 'pub_date', 'author')
    prepopulated_fields = { 'slug': ['title'] }
    readonly_fields = ('last_modified', 'last_modified_by',)
    fieldsets = ((
        None, {
            'fields': ('title', 'body', 'pub_date')
        }), (
        'Other Information', {
            'fields': ('last_modified', 'last_modified_by', 'slug'),
            'classes': ('collapse',)
        })
    )
    
    def save_model(self, request, obj, form, change):
        if not obj.author.id:
            obj.author = request.user
        obj.last_modified_by = request.user
        obj.save()

First, we set the form attribute to the EntryForm we just created.

Then, since we don’t want the author to worry about selecting themselves in the author field, we left it out of the fieldsets. We left in the last_modified_by field for reference and made it a read-only field.

The final magic comes in the overridden save_model() method on line 17. We check to see if the author attribute actually has an id. If it doesn’t, it must be the empty instance we set in EntryForm, so we assign the author field to the current user. Since we always want to assign or re-assign the user modifying this record, the last_modified_by field is set every time.