Most Django tutorials and how-to guides showcase functional views. While knowing how to write functional views is important for understanding how Django works, class-based views are essential to making the most out of the web framework for perfectionists with deadlines.
So, what do class-based views look like?
Take this functional view for example:
def contact(request):
if request.POST:
form = forms.ContactForm(request.POST)
if form.is_valid():
form.submit()
else:
form = forms.ContactForm()
return render(request, 'contact.html', context={'form': form})
Simply, this view will display the contact.html
page, and save the form data upon a POST request.
That is simple enough and does the job, but the team at Django have put in a lot of work creating generic view classes that handle much of the monotonous code. For this example, the generic.CreateView
class can be imported and used like this to accomplish the same task:
from django.views.generic.edit import CreateView
class Contact(CreateView):
template_name = 'contact.html'
form_class = forms.ContactForm
success_url = '/contact'
This view, while only being four lines long and containing no logic on the surface, provides the exact same functionality as the functional view above.
How do class-based views work?
Each of the variables set in Contact
are used in CreateView
's methods.
A note on semantics: A method is simply a function that is called on an object. For more information, consider this article.
It should all seem very self-explanatory, but template_name
defines the template to use, form_class
defines the form, and success_url
is the route to redirect to upon a valid submission.
Here's a look at the method get_form_class()
that is inherited from CreateView
:
def get_form_class(self):
"""Return the form class to use in this view."""
if self.fields is not None and self.form_class:
raise ImproperlyConfigured(
"Specifying both 'fields' and 'form_class' is not permitted."
)
if self.form_class:
return self.form_class
else:
if self.model is not None:
# If a model has been explicitly provided, use it
model = self.model
elif getattr(self, 'object', None) is not None:
# If this view is operating on a single object, use
# the class of that object
model = self.object.__class__
else:
# Try to get a queryset and extract the model class
# from that
model = self.get_queryset().model
if self.fields is None:
raise ImproperlyConfigured(
"Using ModelFormMixin (base class of %s) without "
"the 'fields' attribute is prohibited." % self.__class__.__name__
)
return model_forms.modelform_factory(model, fields=self.fields)
This is code that the Django team has written so that you don't have to, allowing you to keep your code much cleaner. In addition to this, they have tested the class-based view functionality so that it will work when the class variables are set up properly so that, again, you don't have to.
The class-based approach shortens the amount of code in your project, and reduces the amount of time spent debugging.
When I was first getting familiar with Django and writing my own functional views, I often ran into errors. Simple mistakes can lead to hours of time spent debugging code. This problem is mostly resolved by removing the need to write everything by hand.
Looking at the Contact
class-based view, it may seem too simplistic. What if you need to modify a field before saving it to the database? Where would you put that code when there's no visible methods?
It's actually fairly simple, you overwrite it. Take a look:
class Contact(CreateView):
template_name = 'contact.html'
form_class = forms.ContactForm
success_url = '/contact'
def form_valid(self, form):
obj = form.save(commit=False)
obj.timestamp = utils.timezone.now()
self.object = obj.save()
return super().form_valid(form)
Now, this may look complicated, but form_valid
is just a method that was inherited with generic.CreateView
. The inherited method looks like this:
def form_valid(self, form):
"""If the form is valid, save the associated model."""
self.object = form.save()
return super().form_valid(form)
The only thing that is a little more complicated is the return
value. You can read more about Python's super()
method, but this article does a good job of explaining it:
Super can be called upon in a single inheritance, in order to refer to the parent class or multiple classes without explicitly naming them. It’s somewhat of a shortcut, but more importantly, it helps keep your code maintainable for the foreseeable future.
In other words, it basically calls the parent class's version of the method.
Since CreateView
also inherits from FormView
, it too calls super()
. FormView
's version of the form_valid
method looks like this:
def form_valid(self, form):
"""If the form is valid, redirect to the supplied URL."""
return HttpResponseRedirect(self.get_success_url())
Don't let super()
intimidate you
It's one of those things that you'll likely need to work with to understand. The concept is difficult to articulate, even the Python documentation is pretty dense. Just know that when using Django's class-based views, it's generally good form to return the super()
method call when overwriting methods.
As you can see, this method would do nothing but redirect to the success_url
. This is why CreateView
overwrites it, and why we overwrite CreateView
's version. In this way, we're able to add more functionality to existing code without having to manipulate the source.
That's basically it!
I hope that I have sparked some inspiration to at least experiment with Django's Class-Based Views. They will help you write reusable, cleaner code faster and with less bugs. You can further expand on the class-based methodology by writing your own Mixins. They are a great way to add custom, inheritable code.
In addition, a resource that I have found to be endlessly beneficial is Refresh Oxford's Classy Class-Based Views. It is something that I use on a daily basis, and is my go-to resource for figuring out where I'll need to insert my custom functionality within class-based views.