Django antipatterns 〉pattern 〉Set values to a created/updated object in a class-based view Fork me on GitHub

Often not all fields specified in a model are specified through a form. These for example originate for example through the path, or we make use of the logged in user. Take for example the following model:

from django.conf import settings
from django.db import models

class Comment(models.Model):
comment = models.CharField(max_length=1024)
posted_at = models.DateTimeField(auto_now_add=True)

The form will normally only take the comment as field, not the post and author. We can for example specify the primary key of the post in the path, and the author is normally the logged in user. The form thus looks like:

from django import forms

class CommentForm(forms.ModelForm):
class Meta:
model = Comment
fields = ['comment']

and the path to the view will have a primary key that specifies to what post we comment:

from django.urls import path
from app_name.views import CreateCommentView

urlpatterns = [
path('post/<int:pk>/comment', CreateCommentView.as_view()),
]

# What problems are solved with this?

A class-based view is powerful, but it is often not entirely clear where to add certain logic to alter the code flow slightly. Often people will alter the entire .post(…) method. This destroys most of the boilerplate code in the view. This does not only mean the user needs to define a long view, but it is also likely that the user will not think about everything in advance. For example people often forget to pass request.FILES to the form.

It also makes it easy to wrap the logic to "inject" data into the form instance through a mixin: this makes the logic to inject data more reusable.

# What does this pattern look like?

We override the .form_valid(…) method [Django-doc] of the view with the FormMixin, and there we can alter the .instance wrapped in the form, for example:

from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic.edit import CreateView
from app_name.forms import CommentForm

form_class = CommentForm

def form_valid(self, form):
form.instance.author = self.request.user
form.instance.post_id = self.kwargs['pk']
return super().form_valid(form)

If we need the logic in multiple views, we can easily encapsulate this in a mixin, for example:

from django.contrib.auth.mixins import LoginRequiredMixin

def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)

then the mixin can be used, for example in both views that create a Post and a Comment:

from django.views.generic.edit import CreateView
from app_name.forms import CommentForm, PostForm

class CommentCreateView(SetAuthorMixin, CreateView):
form_class = PostForm

class CommentCreateView(SetAuthorMixin, CreateView):
form_class = CommentForm

def form_valid(self, form):
form.instance.post_id = self.kwargs['pk']
return super().form_valid(form)

This not only makes it easier to reuse logic. It also makes it easier to fix a mistake: if the mistake is only made in one mixin, it is easy to fix the problem for all views with that logic, instead of searching for all views that implemented (variants) of that logic.

For the Django admin, we can override the .save_model method [Django-doc]:

from django.contrib import admin

super().save_model(request, obj, form, change)
For DateTimeFields and DateFields we can make use of the auto_now_add=… [Django-doc] or auto_now=… [Django-doc] can be used to automatically specify the current timestamp to specify when to create or update the object.