Two Scoops of Django 3x - Compress 4

Download as pdf or txt
Download as pdf or txt
You are on page 1of 50

9 | Best Practices for Function-Based

Views

Since the beginning of the Django project, function-based views have been in frequent use
by developers around the world. While class-based views have risen in usage, the simplicity
of using a function is appealing to both new and experienced developers alike. While the
authors are in the camp of preferring CBVs, we work on projects that use FBVs and here
are some patterns we’ve grown to enjoy.

9.1 Advantages of FBVs


The simplicity of FBVs comes at the expense of code reuse: FBVs don’t have the same ability
to inherit from superclasses the way that CBVs do. They do have the advantage of being
more functional in nature, which lends itself to a number of interesting strategies.

We follow these guidelines when writing FBVs:

ä Less view code is better.


ä Never repeat code in views.
ä Views should handle presentation logic. Try to keep business logic in models when
possible, or in forms if you must.
ä Keep your views simple.
ä Use them to write custom 403, 404, and 500 error handlers.
ä Complex nested-if blocks are to be avoided.

9.2 Passing the HttpRequest Object


There are times where we want to reuse code in views, but not tie it into global actions such
as middleware or context processors. Starting in the introduction of this book, we advised
creating utility functions that can be used across the project.

For many utility functions, we are taking an attribute or attributes from the
django.http.HttpRequest (or HttpRequest for short) object and gathering data or

Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues 113


Chapter 9: Best Practices for Function-Based Views

performing operations. What we’ve found is by having the request object itself as a primary
argument, we have simpler arguments on more methods. This means less cognitive overload
of managing function/method arguments: just pass in the HttpRequest object!

Example 9.1: sprinkles/utils.py

from django.core.exceptions import PermissionDenied


from django.http import HttpRequest

def check_sprinkle_rights(request: HttpRequest) -> HttpRequest:


if request.user.can_sprinkle or request.user.is_staff:
return request

# Return a HTTP 403 back to the user


raise PermissionDenied

The check_sprinkle_rights() function does a quick check against the rights


of the user, raising a django.core.exceptions.PermissionDenied excep-
tion, which triggers a custom HTTP 403 view as we describe in Section 31.4.3:
django.core.exceptions.PermissionDenied.

You’ll note that we return back a HttpRequest object rather than an arbitrary value or even
a None object. We do this because as Python is a dynamically typed language, we can attach
additional attributes to the HttpRequest. For example:

Example 9.2: Enhanced sprinkles/utils.py

from django.core.exceptions import PermissionDenied


from django.http import HttpRequest, HttpResponse

def check_sprinkles(request: HttpRequest) -> HttpRequest:


if request.user.can_sprinkle or request.user.is_staff:
# By adding this value here it means our display templates
# can be more generic. We don't need to have
# {% if request.user.can_sprinkle or
,→ request.user.is_staff %}
# instead just using
# {% if request.can_sprinkle %}
request.can_sprinkle = True
return request

114 Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues


9.2: Passing the HttpRequest Object

# Return a HTTP 403 back to the user


raise PermissionDenied

There’s another reason, which we’ll cover shortly. In the meantime, let’s demonstrate this
code in action:

Example 9.3: Passing the Request Object in FBVs

# sprinkles/views.py
from django.shortcuts import get_object_or_404
from django.shortcuts import render
from django.http import HttpRequest, HttpResponse

from .models import Sprinkle


from .utils import check_sprinkles

def sprinkle_list(request: HttpRequest) -> HttpResponse:


"""Standard list view"""

request = check_sprinkles(request)

return render(request,
"sprinkles/sprinkle_list.html",
{"sprinkles": Sprinkle.objects.all()})

def sprinkle_detail(request: HttpRequest, pk: int) -> HttpResponse:


"""Standard detail view"""

request = check_sprinkles(request)

sprinkle = get_object_or_404(Sprinkle, pk=pk)

return render(request, "sprinkles/sprinkle_detail.html",


{"sprinkle": sprinkle})

def sprinkle_preview(request: HttpRequest) -> HttpResponse:


"""Preview of new sprinkle, but without the
check_sprinkles function being used.
"""
sprinkle = Sprinkle.objects.all()
return render(request,

Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues 115


Chapter 9: Best Practices for Function-Based Views

"sprinkles/sprinkle_preview.html",
{"sprinkle": sprinkle})

Another good feature about this approach is that it’s trivial to integrate into class-based
views:

Example 9.4: Passing the Request Object in a CBV

from django.views.generic import DetailView

from .models import Sprinkle


from .utils import check_sprinkles

class SprinkleDetail(DetailView):
"""Standard detail view"""

model = Sprinkle

def dispatch(self, request, *args, **kwargs):


request = check_sprinkles(request)
return super().dispatch(request, *args, **kwargs)

TIP: Specific Function Arguments Have Their Place


The downside to single argument functions is that specific function arguments like
‘pk’, ‘flavor’ or ‘text’ make it easier to understand the purpose of a function at a glance.
In other words, try to use this technique for actions that are as generic as possible.

Since we’re repeatedly reusing functions inside functions, wouldn’t it be nice to easily rec-
ognize when this is being done? This is when we bring decorators into play.

9.3 Decorators Are Sweet


For once, this isn’t about ice cream, it’s about code! In computer science parlance, syntactic
sugar is a syntax added to a programming language in order to make things easier to read
or to express. In Python, decorators are a feature added not out of necessity, but in order to
make code cleaner and sweeter for humans to read. So yes, Decorators Are Sweet.

When we combine the power of simple functions with the syntactic sugar of decorators, we
get handy, reusable tools like the extremely useful to the point of being ubiquitous
django.contrib.auth.decorators.login_required decorator.

116 Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues


9.3: Decorators Are Sweet

Here’s a sample decorator template for use in function-based views:

Example 9.5: Simple Decorator Template

import functools

def decorator(view_func):
@functools.wraps(view_func)
def new_view_func(request, *args, **kwargs):
# You can modify the request (HttpRequest) object here.
response = view_func(request, *args, **kwargs)
# You can modify the response (HttpResponse) object here.
return response
return new_view_func

That might not make too much sense, so we’ll go through it step-by-step, using in-line code
comments to clarify what we are doing. First, let’s modify the decorator template from the
previous example to match our needs:

Example 9.6: Decorator Example

# sprinkles/decorators.py
import functools

from . import utils

# based off the decorator template from the previous example


def check_sprinkles(view_func):
"""Check if a user can add sprinkles"""
@functools.wraps(view_func)
def new_view_func(request, *args, **kwargs):
# Act on the request object with utils.can_sprinkle()
request = utils.can_sprinkle(request)

# Call the view function


response = view_func(request, *args, **kwargs)

# Return the HttpResponse object


return response
return new_view_func

Then we attach it to the function thus:

Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues 117


Chapter 9: Best Practices for Function-Based Views

Example 9.7: Example of Using a Decorator

# sprinkles/views.py
from django.shortcuts import get_object_or_404, render

from .decorators import check_sprinkles


from .models import Sprinkle

# Attach the decorator to the view


@check_sprinkles
def sprinkle_detail(request: HttpRequest, pk: int) -> HttpResponse:
"""Standard detail view"""

sprinkle = get_object_or_404(Sprinkle, pk=pk)

return render(request, "sprinkles/sprinkle_detail.html",


{"sprinkle": sprinkle})

Figure 9.1: If you look at sprinkles closely, you’ll see that they’re Python decorators.

TIP: What About functools.wraps()?


Astute readers may have noticed that our decorator examples used the
functools.wraps() decorator function from the Python standard library. This is
a convenience tool that copies over metadata including critical info like docstrings
to the newly decorated function. It’s not necessary, but it makes project maintenance
much easier.

118 Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues


9.4: Passing the HttpResponse Object

9.3.1 Be Conservative With Decorators


As with any powerful tool, decorators can be used the wrong way. Too many decorators
can create their own form of obfuscation, making even complex class-based view hierar-
chies seem simple in comparison. When using decorators, establish a limit of how many
decorators can be set on a view and stick with it. Video on the subject: pyvideo.org/
pycon-us-2011/pycon-2011--how-to-write-obfuscated-python.html

9.3.2 Additional Resources on Decorators


ä Decorators Explained:
jeffknupp.com/blog/2013/11/29/improve-your-python-decorators-explained/
ä Decorator Cheat Sheet by author Daniel Feldroy
daniel.feldroy.com/python-decorator-cheatsheet.html

9.4 Passing the HttpResponse Object


Just as with the HttpRequest object, we can also pass around the
HttpResponse object from function to function. Think of this as a selective
Middleware.process_template_response() method. See docs.djangoproject.
com/en/3.2/topics/http/middleware/#process-template-response.

Yes, this technique can be leveraged with decorators. See Example 8.5 which gives a hint as
to how this can be accomplished.

9.5 Additional Resources for Function-Based Views


Luke Plant is a core Django developer with strong opinions in favor of Function-Based
Views. While we don’t agree with most of his anti-CBV arguments he lists in the article
linked below, this nevertheless is of immeasurable value to anyone writing FBVs:

spookylukey.github.io/django-views-the-right-way/

9.6 Summary
Function-based views are still alive and well in the Django world. If we remember that every
function accepts an HttpRequest object and returns an HttpResponse object, we can
use that to our advantage. We can leverage in generic HttpRequest and HttpResponse
altering functions, which can also be used to construct decorator functions.

We’ll close this chapter by acknowledging that every lesson we’ve learned about function-
based views can be applied to what we begin to discuss next chapter, class-based views.

Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues 119


Chapter 9: Best Practices for Function-Based Views

120 Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues


10 | Best Practices for Class-Based
Views

Django provides a standard way to write class-based views (CBVs). In fact, as we mentioned
in previous chapters, a Django view is just a callable that accepts a request object and returns
a response. For function-based views (FBVs), the view function is that callable. For CBVs,
the view class provides an as_view() class method that returns the callable. This mecha-
nism is implemented in django.views.generic.View. All CBVs should inherit from
that class, directly or indirectly.

Django also provides a series of generic class-based views (GCBVs) that implement com-
mon patterns found in most web projects and illustrate the power of CBVs.

PACKAGE TIP: Filling the Missing Parts of Django GCBVs


Out of the box, Django does not provide some very useful mixins for GCBVs. The
django-braces library addresses most of these issues. It provides a set of clearly
coded mixins that make Django GCBVs much easier and faster to implement. The
library is so useful that three of its mixins have been copied into core Django.

10.1 Guidelines When Working With CBVs


ä Less view code is better.
ä Never repeat code in views.
ä Views should handle presentation logic. Try to keep business logic in models when
possible, or in forms if you must.
ä Keep your views simple.
ä Keep your mixins simpler.

Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues 121


Chapter 10: Best Practices for Class-Based Views

TIP: Familarize Yourself With ccbv.co.uk


Arguably this should be placed as the sixth guideline, ccbv.co.uk is so useful that
we felt it deserved its own tipbox. This site takes all the attributes and methods
that every CBV defines or inherits and flattens it into one comprehensive page per
view. Most Django developers, once they get past the tutorials on CBVs, rely on
ccbv.co.uk more than the official documentation.

10.2 Using Mixins With CBVs


Think of mixins in programming along the lines of mixins in ice cream: you can enhance
any ice cream flavor by mixing in crunchy candy bits, sliced fruit, or even bacon.

Figure 10.1: Popular and unpopular mixins used in ice cream.

Soft serve ice cream greatly benefits from mixins: ordinary vanilla soft serve turns into birth-
day cake ice cream when sprinkles, blue buttercream icing, and chunks of yellow cake are
mixed in.

In programming, a mixin is a class that provides functionality to be inherited, but isn’t meant
for instantiation on its own. In programming languages with multiple inheritance, mixins
can be used to add enhanced functionality and behavior to classes.

We can use the power of mixins to compose our own view classes for our Django apps.

When using mixins to compose our own view classes, we recommend these rules of in-
heritance provided by Kenneth Love. The rules follow Python’s method resolution order,

122 Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues


10.3: Which Django GCBV Should Be Used for What Task?

which in the most simplistic definition possible, proceeds from left to right:

1 The base view classes provided by Django always go to the right.


2 Mixins go to the left of the base view.
3 Mixins should not inherit from any other class. Keep your inheritance chain simple!

Example of the rules in action:

Example 10.1: Using Mixins in a View

from django.views.generic import TemplateView

class FreshFruitMixin:

def get_context_data(self, **kwargs):


context = super().get_context_data(**kwargs)
context["has_fresh_fruit"] = True
return context

class FruityFlavorView(FreshFruitMixin, TemplateView):


template_name = "fruity_flavor.html"

In our rather silly example, the FruityFlavorView class inherits from both
FreshFruitMixin and TemplateView.

Since TemplateView is the base view class provided by Django, it goes on the far right
(rule 1), and to its left we place the FreshFruitMixin (rule 2). This way we know that our
methods and properties will execute correctly.

10.3 Which Django GCBV Should Be Used for What


Task?
The power of generic class-based views comes at the expense of simplicity: GCBVs come
with a complex inheritance chain that can have up to eight superclasses on import. Trying to
work out exactly which view to use or which method to customize can be very challenging
at times.

To mitigate this challenge, here’s a handy chart listing the name and purpose of each Django
CBV. All views listed here are assumed to be prefixed with django.views.generic.

Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues 123


Chapter 10: Best Practices for Class-Based Views

Name Purpose Two Scoops Example


View Base view or handy view See Section 10.6:
that can be used for any- Using Just
thing. django.views.generic.View.

RedirectView Redirect user to another Send users who visit ‘/log-


URL in/’ to ‘/login/’.
TemplateView Display a Django HTML The ‘/about/’ page of our
template. site.
ListView List objects List of ice cream flavors.
DetailView Display an object Details on an ice cream fla-
vor.
FormView Submit a form The site’s contact or email
form.
CreateView Create an object Create a new ice cream fla-
vor.
UpdateView Update an object Update an existing ice
cream flavor.
DeleteView Delete an object Delete an unpleasant ice
cream flavor like Vanilla
Steak.
Generic date views For display of objects that Blogs are a common rea-
occur over a range of time. son to use them. For Two
Scoops, we could create a
public history of when fla-
vors have been added to the
database.

Table 10.1: Django CBV Usage Table

124 Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues


10.4: General Tips for Django CBVs

TIP: The Three Schools of Django CBV/GCBV Usage


We’ve found that there are three major schools of thought around CBV and GCBV
usage. They are:

The School of “Use all the generic views!”


This school of thought is based on the idea that since Django provides functionality
to reduce your workload, why not use that functionality? We tend to belong to
this school of thought, and have used it to great success, rapidly building and then
maintaining a number of projects.

The School of “Just use django.views.generic.View”


This school of thought is based on the idea that the base Django CBV does just
enough and is ‘the True CBV, everything else is a Generic CBV’. In the past year,
we’ve found this can be a really useful approach for tricky tasks for which the
resource-based approach of “Use all the views” breaks down. We’ll cover some use
cases for it in this chapter.

The School of “Avoid them unless you’re actually subclassing views”


Jacob Kaplan-Moss says, “My general advice is to start with function views since
they’re easier to read and understand, and only use CBVs where you need them.
Where do you need them? Any place where you need a fair chunk of code to be
reused among multiple views.”

We generally belong to the first school, but it’s good for you to know that there’s no
real consensus on best practices here.

10.4 General Tips for Django CBVs


This section covers useful tips for all or many Django CBV and GCBV implementations.
We’ve found they expedite writing of views, templates, and their tests. These techniques
will work with Class-Based Views or Generic Class-Based Views. As always for CBVs in
Django, they rely on object oriented programming techniques.

10.4.1 Constraining Django CBV/GCBV Access to Authenticated


Users
The Django CBV documentation gives a helpful working example of using
the django.contrib.auth.decorators.login_required decorator with
a CBV, but this example violates the rule of keeping logic out of urls.py:

Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues 125


Chapter 10: Best Practices for Class-Based Views

docs.djangoproject.com/en/3.2/topics/class-based-views/intro/
#decorating-class-based-views.

Fortunately, Django provides a ready implementation of a LoginRequiredMixin object


that you can attach in moments. For example, we could do the following in all of the Django
GCBVs that we’ve written so far:

Example 10.2: Using LoginRequiredMixin

# flavors/views.py
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import DetailView

from .models import Flavor

class FlavorDetailView(LoginRequiredMixin, DetailView):


model = Flavor

TIP: Don’t Forget the GCBV Mixin Order!


Remember that:
ä LoginRequiredMixin must always go on the far left side.
ä The base view class must always go on the far right side.

If you forget and switch the order, you will get broken or unpredictable results.

WARNING: Overriding dispatch() When Using LoginRequired-


Mixin
If you use LoginRequiredMixin and override the dispatch method, make
sure that the first thing you do is call super().dispatch(request, *args,
**kwargs). Any code before the super() call is executed even if the user is not
authenticated.

10.4.2 Performing Custom Actions on Views With Valid Forms


When you need to perform a custom action on a view with a valid form, the form_valid()
method is where the GCBV workflow sends the request.

126 Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues


10.4: General Tips for Django CBVs

Example 10.3: Custom Logic with Valid Forms

from django.contrib.auth.mixins import LoginRequiredMixin


from django.views.generic import CreateView

from .models import Flavor

class FlavorCreateView(LoginRequiredMixin, CreateView):


model = Flavor
fields = ['title', 'slug', 'scoops_remaining']

def form_valid(self, form):


# Do custom logic here
return super().form_valid(form)

To perform custom logic on form data that has already been validated, simply
add the logic to form_valid(). The return value of form_valid() should be a
django.http.HttpResponseRedirect.

10.4.3 Performing Custom Actions on Views With Invalid Forms


When you need to perform a custom action on a view with an invalid form, the
form_invalid() method is where the Django GCBV workflow sends the request. This
method should return a django.http.HttpResponse.

Example 10.4: Overwriting Behavior of form_invalid

from django.contrib.auth.mixins import LoginRequiredMixin


from django.views.generic import CreateView

from .models import Flavor

class FlavorCreateView(LoginRequiredMixin, CreateView):


model = Flavor

def form_invalid(self, form):


# Do custom logic here
return super().form_invalid(form)

Just as you can add logic to form_valid(), you can also add logic to form_invalid().

Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues 127


Chapter 10: Best Practices for Class-Based Views

You’ll see an example of overriding both of these methods in Section 13.5.1: ModelForm
Data Is Saved to the Form, Then the Model Instance.

Figure 10.2: The other CBV: class-based vanilla ice cream.

10.4.4 Using the View Object


If you are using class-based views for rendering content, consider using the view object
itself to provide access to properties and methods that can be called by other methods and
properties. They can also be called from templates. For example:

Example 10.5: Using the View Object

from django.contrib.auth.mixins import LoginRequiredMixin


from django.utils.functional import cached_property
from django.views.generic import UpdateView, TemplateView

from .models import Flavor


from .tasks import update_user_who_favorited

class FavoriteMixin:

@cached_property
def likes_and_favorites(self):
"""Returns a dictionary of likes and favorites"""

128 Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues


10.4: General Tips for Django CBVs

likes = self.object.likes()
favorites = self.object.favorites()
return {
"likes": likes,
"favorites": favorites,
"favorites_count": favorites.count(),

class FlavorUpdateView(LoginRequiredMixin, FavoriteMixin,


,→ UpdateView):
model = Flavor
fields = ['title', 'slug', 'scoops_remaining']

def form_valid(self, form):


update_user_who_favorited(
instance=self.object,
favorites=self.likes_and_favorites['favorites']
)
return super().form_valid(form)

class FlavorDetailView(LoginRequiredMixin, FavoriteMixin,


,→ TemplateView):
model = Flavor

The nice thing about this is the various flavors/ app templates can now access this property:

Example 10.6: Using View Methods in flavors/base.html

{# flavors/base.html #}
{% extends "base.html" %}

{% block likes_and_favorites %}
<ul>
<li>Likes: {{ view.likes_and_favorites.likes }}</li>
<li>Favorites: {{ view.likes_and_favorites.favorites_count
,→ }}</li>
</ul>
{% endblock likes_and_favorites %}

Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues 129


Chapter 10: Best Practices for Class-Based Views

10.5 How GCBVs and Forms Fit Together


A common source of confusion with GCBVs is their usage with Django forms.

Using our favorite example of the ice cream flavor tracking app, let’s chart out a couple of
examples of how form-related views might fit together.

First, let’s define a flavor model to use in this section’s view examples:

Example 10.7: Flavor Model

# flavors/models.py
from django.db import models
from django.urls import reverse

class Flavor(models.Model):
class Scoops(models.IntegerChoices)
SCOOPS_0 = 0
SCOOPS_1 = 1

title = models.CharField(max_length=255)
slug = models.SlugField(unique=True)
scoops_remaining = models.IntegerField(choices=Scoops.choices,
default=Scoops.SCOOPS_0)

def get_absolute_url(self):
return reverse("flavors:detail", kwargs={"slug":
,→ self.slug})

Now, let’s explore some common Django form scenarios that most Django users run into
at one point or another.

10.5.1 Views + ModelForm Example


This is the simplest and most common Django form scenario. Typically when you create a
model, you want to be able to add new records and update existing records that correspond
to the model.

In this example, we’ll show you how to construct a set of views that will create, update and
display Flavor records. We’ll also demonstrate how to provide confirmation of changes.

Here we have the following views:

130 Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues


10.5: How GCBVs and Forms Fit Together

1 FlavorCreateView corresponds to a form for adding new flavors.


2 FlavorUpdateView corresponds to a form for editing existing flavors.
3 FlavorDetailView corresponds to the confirmation page for both flavor creation and
flavor updates.

To visualize our views:

Figure 10.3: Views + ModelForm Flow

Note that we stick as closely as possible to Django naming conventions.


FlavorCreateView subclasses Django’s CreateView, FlavorUpdateView subclasses
Django’s UpdateView, and FlavorDetailView subclasses Django’s DetailView.

Writing these views is easy, since it’s mostly a matter of using what Django gives us:

Example 10.8: Building Views Quickly with CBVs

# flavors/views.py
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import CreateView, DetailView, UpdateView

from .models import Flavor

class FlavorCreateView(LoginRequiredMixin, CreateView):


model = Flavor
fields = ['title', 'slug', 'scoops_remaining']

class FlavorUpdateView(LoginRequiredMixin, UpdateView):


model = Flavor
fields = ['title', 'slug', 'scoops_remaining']

class FlavorDetailView(DetailView):

Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues 131


Chapter 10: Best Practices for Class-Based Views

model = Flavor

Simple at first glance, right? We accomplish so much with just a little bit of code!

But wait, there’s a catch. If we wire these views into a urls.py module and create the necessary
templates, we’ll uncover a problem:

The FlavorDetailView is not a confirmation page.

For now, that statement is correct. Fortunately, we can fix it quickly with a few modifications
to existing views and templates.

The first step in the fix is to use django.contrib.messages to inform the user visiting
the FlavorDetailView that they just added or updated the flavor.

We’ll need to override the FlavorCreateView.form_valid() and


FlavorUpdateView.form_valid() methods. We can do this conveniently for
both views with a FlavorActionMixin.

For the confirmation page fix, we change flavors/views.py to contain the following:

Example 10.9: Success Message Example

# flavors/views.py
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import CreateView, DetailView, UpdateView

from .models import Flavor

class FlavorActionMixin:

fields = ['title', 'slug', 'scoops_remaining']

@property
def success_msg(self):
return NotImplemented

def form_valid(self, form):


messages.info(self.request, self.success_msg)
return super().form_valid(form)

132 Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues


10.5: How GCBVs and Forms Fit Together

class FlavorCreateView(LoginRequiredMixin, FlavorActionMixin,


CreateView):
model = Flavor
success_msg = "Flavor created!"

class FlavorUpdateView(LoginRequiredMixin, FlavorActionMixin,


UpdateView):
model = Flavor
success_msg = "Flavor updated!"

class FlavorDetailView(DetailView):
model = Flavor

Earlier in this chapter, we covered a simpler example of how to override form_valid()


within a GCBV. Here, we reuse a similar form_valid() override method by creating a
mixin to inherit from in multiple views.

Now we’re using Django’s messages framework to display confirmation messages to the
user upon every successful add or edit. We define a FlavorActionMixin whose job is to
queue up a confirmation message corresponding to the action performed in a view.

TIP: This replicates SuccessMessageMixin


Since we wrote this section in 2013, Django has implemented
django.contrib.messages.views.SuccessMessageMixin, which pro-
vides similar functionality.
Reference: https://docs.djangoproject.com/en/3.2/ref/contrib/
messages/#django.contrib.messages.views.SuccessMessageMixin

TIP: Mixins Shouldn’t Inherit From Other Object


Please take notice that the FlavorActionMixin is a base class, not inheriting from
another class in our code rather than a pre-existing mixin or view. It’s important that
mixins have as shallow inheritance chain as possible. Simplicity is a virtue!

After a flavor is created or updated, a list of messages is passed to the context of the
FlavorDetailView. We can see these messages if we add the following code to the views’
template and then create or update a flavor:

Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues 133


Chapter 10: Best Practices for Class-Based Views

Example 10.10: flavor_detail.html

{% if messages %}
<ul class="messages">
{% for message in messages %}
<li id="message_{{ forloop.counter }}"
{% if message.tags %} class="{{ message.tags }}"
{% endif %}>
{{ message }}
</li>
{% endfor %}
</ul>
{% endif %}

TIP: Reuse the Messages Template Code!


It is common practice to put the above code into your project’s base HTML tem-
plate. Doing this allows message support for templates in your project.

To recap, this example demonstrated yet again how to override the form_valid() method,
incorporate this into a mixin, how to incorporate multiple mixins into a view, and gave a
quick introduction to the very useful django.contrib.messages framework.

10.5.2 Views + Form Example


Sometimes you want to use a Django Form rather than a ModelForm. Search forms are a
particularly good use case for this, but you’ll run into other scenarios where this is true as
well.

In this example, we’ll create a simple flavor search form. This involves creating an HTML
form that doesn’t modify any flavor data. The form’s action will query the ORM, and the
records found will be listed on a search results page.

Our intention is that when using our flavor search page, if users do a flavor search for
“Dough”, they should be sent to a page listing ice cream flavors like “Chocolate Chip Cookie
Dough,” “Fudge Brownie Dough,” “Peanut Butter Cookie Dough,” and other flavors con-
taining the string “Dough” in their title. Mmm, we definitely want this feature in our web
application.

There are more complex ways to implement this, but for our simple use case, all we need is
a single view. We’ll use a FlavorListView for both the search page and the search results

134 Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues


10.5: How GCBVs and Forms Fit Together

page.

Here’s an overview of our implementation:

Figure 10.4: Views + Form Flow

In this scenario, we want to follow the standard internet convention for search pages, where
‘q’ is used for the search query parameter. We also want to accept a GET request rather than
a POST request, which is unusual for forms but perfectly fine for this use case. Remember,
this form doesn’t add, edit, or delete objects, so we don’t need a POST request here.

To return matching search results based on the search query, we need to modify the
standard queryset supplied by the ListView. To do this, we override the ListView's
get_queryset() method. We add the following code to flavors/views.py:

Example 10.11: List View Combined with Q Search

from django.views.generic import ListView

from .models import Flavor

class FlavorListView(ListView):
model = Flavor

def get_queryset(self):
# Fetch the queryset from the parent get_queryset
queryset = super().get_queryset()

# Get the q GET parameter


q = self.request.GET.get("q")
if q:
# Return a filtered queryset
return queryset.filter(title__icontains=q)
# Return the base queryset
return queryset

Now, instead of listing all of the flavors, we list only the flavors whose titles contain the
search string.

Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues 135


Chapter 10: Best Practices for Class-Based Views

As we mentioned, search forms are unusual in that unlike nearly every other HTML form
they specify a GET request in the HTML form. This is because search forms are not chang-
ing data, but simply retrieving information from the server. The search form should look
something like this:

Example 10.12: Search Snippet of HTML

{# templates/flavors/_flavor_search.html #}
{% comment %}
Usage: {% include "flavors/_flavor_search.html" %}
{% endcomment %}
<form action="{% url "flavor_list" %}" method="GET">
<input type="text" name="q" />
<button type="submit">search</button>
</form>

TIP: Specify the Form Target in Search Forms


We also take care to specify the URL in the form action, because we’ve found that
search forms are often included in several pages. This is why we prefix them with ‘_’
and create them in such a way as to be included in other templates.

Once we get past overriding the ListView's get_queryset() method, the rest of this
example is just a simple HTML form. We like this kind of simplicity.

10.6 Using Just django.views.generic.View


It’s entirely possible to build a project just using django.views.generic.View for all the
views. It’s not as extreme as one might think. For example, if we look at the official Django
documentation’s introduction to class-based views (docs.djangoproject.com/en/3.
2/topics/class-based-views/intro/#using-class-based-views), we can see
the approach is very close to how function-based views are written. In fact, we highlighted
this two chapters ago in Section 8.6.1: The Simplest Views because it’s important.

Imagine instead of writing function-based views with nested-ifs representing different


HTTP methods or class-based views where the HTTP methods are hidden behind
get_context_data() and form_valid() methods, they are readily accessible to de-
velopers. Imagine something like:

136 Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues


10.6: Using Just django.views.generic.View

Example 10.13: Using the Base View Class

from django.contrib.auth.mixins import LoginRequiredMixin


from django.shortcuts import get_object_or_404
from django.shortcuts import render, redirect
from django.views.generic import View

from .forms import FlavorForm


from .models import Flavor

class FlavorView(LoginRequiredMixin, View):

def get(self, request, *args, **kwargs):


# Handles display of the Flavor object
flavor = get_object_or_404(Flavor, slug=kwargs['slug'])
return render(request,
"flavors/flavor_detail.html",
{"flavor": flavor}
)

def post(self, request, *args, **kwargs):


# Handles updates of the Flavor object
flavor = get_object_or_404(Flavor, slug=kwargs['slug'])
form = FlavorForm(request.POST, instance=flavor)
if form.is_valid():
form.save()
return redirect("flavors:detail", flavor.slug)

While we can do this in a function-based view, it can be argued that the GET/POST
method declarations within the FlavorView are easier to read than the traditional “if
request.method == ...” conditions. In addition, since the inheritance chain is so shal-
low, it means using mixins doesn’t threaten us with cognitive overload.

What we find really useful, even on projects which use a lot of generic class-based views, is
using the django.views.generic.View class with a GET method for displaying JSON,
PDF or other non-HTML content. All the tricks that we’ve used for rendering CSV, Excel,
and PDF files in function-based views apply when using the GET method. For example:

Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues 137


Chapter 10: Best Practices for Class-Based Views

Example 10.14: Using the View Class to Create PDFs

from django.contrib.auth.mixins import LoginRequiredMixin


from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.views.generic import View

from .models import Flavor


from .reports import make_flavor_pdf

class FlavorPDFView(LoginRequiredMixin, View):

def get(self, request, *args, **kwargs):


# Get the flavor
flavor = get_object_or_404(Flavor, slug=kwargs['slug'])

# create the response


response = HttpResponse(content_type='application/pdf')

# generate the PDF stream and attach to the response


response = make_flavor_pdf(response, flavor)

return response

This is a pretty straight-forward example, but if we have to leverage more mixins and deal
with more custom logic, the simplicity of django.views.generic.View makes it much
easier than the more heavyweight views. In essence, we get all the advantages of function-
based views combined with the object-oriented power that CBVs give us.

10.7 Additional Resources


ä docs.djangoproject.com/en/3.2/topics/class-based-views/
ä docs.djangoproject.com/en/3.2/topics/class-based-views/
generic-display/
ä docs.djangoproject.com/en/3.2/topics/class-based-views/
generic-editing/
ä docs.djangoproject.com/en/3.2/topics/class-based-views/mixins/
ä docs.djangoproject.com/en/3.2/ref/class-based-views/
ä The GCBV inspector at ccbv.co.uk
ä python.org/download/releases/2.3/mro/ - For Python 2.3, nevertheless an
excellent guide to how Python handles MRO.
ä daniel.feldroy.com/tag/class-based-views.html

138 Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues


10.8: Summary

ä spapas.github.io/2018/03/19/comprehensive-django-cbv-guide/ -
Serafeim Papastefanos’ lovely deep dive into Django CBVs
ä djangodeconstructed.com/2020/04/27/roll-your-own-class-based-views-in-djang
- Another deep dive into CBVs, this one illustrating how to create a RESTful API
with DRF

PACKAGE TIP: Other Useful CBV Libraries


ä django-extra-views Another great CBV library, django-extra-views covers
the cases that django-braces does not.
ä django-vanilla-views A very interesting library that provides all the power of
classic Django GCBVs in a vastly simplified, easier-to-use package. Works
great in combination with django-braces.

10.8 Summary
This chapter covered:

ä Using mixins with CBVs


ä Which Django CBV should be used for which task
ä General tips for CBV usage
ä Connecting CBVs to forms
ä Using the base django.views.generic.View

The next chapter explores asynchronous views. Chapter 12 explores common CBV/form
patterns. Knowledge of both of these are helpful to have in your developer toolbox.

Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues 139


Chapter 10: Best Practices for Class-Based Views

140 Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues


11 | Asynchronous Views

WARNING: This Chapter is in Progress


We are in the midst of working on this chapter and will expand on it in the days to
come. We are open to suggestions on topics and items to cover, please submit them
to github.com/feldroy/two-scoops-of-django-3.x/issues

11.1 Notes from Analyzing Django 3.1a Pre-Release Async


Views
ä Don’t use Django for simple reads of data to present to the user. The data will typically
be fetched before HTML or JSON rendering can be performed.
ä When using Class-Based Views, keep them as simple as possible. Consider inheriting
from django.views.generic.View. The machinary underneath more complex
views has proven unstable. This recommendation may change as Django’s async story
advances.

Example 11.1: An example of a simple update form that is a working async view

class AsyncViewMixin:
async def __call__(self):
return super().__call__(self)

class SimpleBookUpdateView(LoginRequiredMixin, AsyncViewMixin,


,→ View):
def get(self, request, *args, **kwargs):
flavor = get_object_or_404(Flavor, slug=slug)
return render(request, "flavor_form.html", {"flavor":
,→ Flavor})

Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues 141


Chapter 11: Asynchronous Views

def post(self, request, *args, **kwargs):


form = FlavorForm(request.POST)
if form.is_valid():
sync_to_async(form.save())
else:
return render({'form': form}, "flavor_form.html")
return redirect("flavor:detail")

11.2 Resources
ä docs.djangoproject.com/en/dev/topics/async/

142 Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues


12 | Common Patterns for Forms

Django forms are powerful, flexible, extensible, and robust. For this reason, the Django
admin and CBVs use them extensively. In fact, all the major Django API frameworks use
ModelForms or a similar implementation as part of their validation.

Combining forms, models, and views allows us to get a lot of work done for little effort. The
learning curve is worth it: once you learn to work fluently with these components, you’ll find
that Django provides the ability to create an amazing amount of useful, stable functionality
at an amazing pace.

PACKAGE TIP: Useful Form-Related Packages


ä django-floppyforms for rendering Django inputs in HTML5.
ä django-crispy-forms for advanced form layout controls. By default, forms
are rendered with Bootstrap form elements and styles. This package plays
well with django-floppyforms, so they are often used together.

This chapter goes explicitly into one of the best parts of Django: forms, models, and CBVs
working in concert. This chapter covers five common form patterns that should be in every
Django developer’s toolbox.

12.1 Pattern 1: Simple ModelForm With Default


Validators
The simplest data-changing form that we can make is a ModelForm using several default
validators as-is, without modification. In fact, we already relied on default validators in
Section 10.5.1: Views + ModelForm Example.

If you recall, using ModelForms with CBVs to implement add/edit forms can be done in
just a few lines of code:

Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues 143


Chapter 12: Common Patterns for Forms

Example 12.1: flavors/views.py

from django.contrib.auth.mixins import LoginRequiredMixin


from django.views.generic import CreateView, UpdateView

from .models import Flavor

class FlavorCreateView(LoginRequiredMixin, CreateView):


model = Flavor
fields = ['title', 'slug', 'scoops_remaining']

class FlavorUpdateView(LoginRequiredMixin, UpdateView):


model = Flavor
fields = ['title', 'slug', 'scoops_remaining']

To summarize how we use default validation as-is here:

ä FlavorCreateView and FlavorUpdateView are assigned Flavor as their model.


ä Both views auto-generate a ModelForm based on the Flavor model.
ä Those ModelForms rely on the default field validation rules of the Flavor model.

Yes, Django gives us a lot of great defaults for data validation, but in practice, the defaults
are never enough. We recognize this, so as a first step, the next pattern will demonstrate
how to create a custom field validator.

12.2 Pattern 2: Custom Form Field Validators in


ModelForms
What if we wanted to be certain that every use of the title field across our project’s dessert
apps started with the word ‘Tasty’?

144 Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues


12.2: Pattern 2: Custom Form Field Validators in ModelForms

Figure 12.1: At Tasty Research, every flavor must begin with “Tasty”.

This is a string validation problem that can be solved with a simple custom field validator.

In this pattern, we cover how to create custom single-field validators and demonstrate how
to add them to both abstract models and forms.

Imagine for the purpose of this example that we have a project with two different dessert-
related models: a Flavor model for ice cream flavors, and a Milkshake model for different
types of milkshakes. Assume that both of our example models have title fields.

To validate all editable model titles, we start by creating a validators.py module:

Example 12.2: validators.py

# core/validators.py
from django.core.exceptions import ValidationError

def validate_tasty(value):
"""Raise a ValidationError if the value doesn't start with the
word 'Tasty'.
"""

Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues 145


Chapter 12: Common Patterns for Forms

if not value.startswith('Tasty'):
msg = 'Must start with Tasty'
raise ValidationError(msg)

In Django, a custom field validator is simply a callable (usually a function) that raises an
error if the submitted argument doesn’t pass its test.

While our validate_tasty() validator function just does a simple string check for the
sake of example, it’s good to keep in mind that form field validators can become quite com-
plex in practice.

TIP: Test Your Validators Carefully


Since validators are critical in keeping corruption out of Django project databases,
it’s especially important to write detailed tests for them.

These tests should include thoughtful edge case tests for every condition related to
your validators’ custom logic.

In order to use our validate_tasty() validator function across different dessert models,
we’re going to first add it to an abstract model called TastyTitleAbstractModel, which
we plan to use across our project.

Assuming that our Flavor and Milkshake models are in separate apps, it doesn’t make
sense to put our validator in one app or the other. Instead, we create a core/models.py module
and place the TastyTitleAbstractModel there.

Example 12.3: Adding Custom Validator to a Model

# core/models.py
from django.db import models

from .validators import validate_tasty

class TastyTitleAbstractModel(models.Model):

title = models.CharField(max_length=255,
,→ validators=[validate_tasty])

class Meta:

146 Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues


12.2: Pattern 2: Custom Form Field Validators in ModelForms

abstract = True

The last two lines of the above example code for core/models.py make
TastyTitleAbstractModel an abstract model, which is what we want. See Sec-
tion 6.1.2: Be Careful With Model Inheritance.

Let’s alter the original flavors/models.py Flavor code to use TastyTitleAbstractModel


as the parent class:

Example 12.4: Inheriting Validators

# flavors/models.py
from django.db import models
from django.urls import reverse

from core.models import TastyTitleAbstractModel

class Flavor(TastyTitleAbstractModel):
slug = models.SlugField()
scoops_remaining = models.IntegerField(default=0)

def get_absolute_url(self):
return reverse('flavors:detail', kwargs={'slug':
,→ self.slug})

This works with the Flavor model, and it will work with any other tasty food-based
model such as a WaffleCone or Cake model. Any model that inherits from the
TastyTitleAbstractModel class will throw a validation error if anyone attempts to save
a model with a title that doesn’t start with ‘Tasty’.

Now, let’s explore a couple of questions that might be forming in your head:

ä What if we wanted to use validate_tasty() in just forms?


ä What if we wanted to assign it to other fields besides the title?

To support these behaviors, we need to create a custom FlavorForm that utilizes our cus-
tom field validator:

Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues 147


Chapter 12: Common Patterns for Forms

Example 12.5: Adding Custom Validators to a Model Form

# flavors/forms.py
from django import forms

from .models import Flavor


from core.validators import validate_tasty

class FlavorForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['title'].validators.append(validate_tasty)
self.fields['slug'].validators.append(validate_tasty)

class Meta:
model = Flavor

A nice thing about both examples of validator usage in this pattern is that we haven’t had
to change the validate_tasty() code at all. Instead, we just import and use it in new
places.

Attaching the custom form to the views is our next step. The default behavior of Django
model-based edit views is to auto-generate the ModelForm based on the view’s model at-
tribute. We are going to override that default and pass in our custom FlavorForm. This
occurs in the flavors/views.py module, where we alter the create and update forms as demon-
strated below:

Example 12.6: Overriding the CBV form_class Attribute

# flavors/views.py
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import CreateView, DetailView, UpdateView

from .models import Flavor


from .forms import FlavorForm

class FlavorActionMixin:

model = Flavor
fields = ['title', 'slug', 'scoops_remaining']

148 Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues


12.3: Pattern 3: Overriding the Clean Stage of Validation

@property
def success_msg(self):
return NotImplemented

def form_valid(self, form):


messages.info(self.request, self.success_msg)
return super().form_valid(form)

class FlavorCreateView(LoginRequiredMixin, FlavorActionMixin,


CreateView):
success_msg = 'created'
# Explicitly attach the FlavorForm class
form_class = FlavorForm

class FlavorUpdateView(LoginRequiredMixin, FlavorActionMixin,


UpdateView):
success_msg = 'updated'
# Explicitly attach the FlavorForm class
form_class = FlavorForm

class FlavorDetailView(DetailView):
model = Flavor

The FlavorCreateView and FlavorUpdateView views now use the new FlavorForm
to validate incoming data.

Note that with these modifications, the Flavor model can either be identical to
the one at the start of this chapter, or it can be an altered one that inherits from
TastyTitleAbstractModel.

12.3 Pattern 3: Overriding the Clean Stage of Validation


Let’s discuss some interesting validation use cases:

ä Multi-field validation
ä Validation involving existing data from the database that has already been validated

Both of these are great scenarios for overriding the clean() and clean_<field_name>()
methods with custom validation logic.

After the default and custom field validators are run, Django provides a second stage
and process for validating incoming data, this time via the clean() method and

Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues 149


Chapter 12: Common Patterns for Forms

clean_<field_name>() methods. You might wonder why Django provides more hooks
for validation, so here are our two favorite arguments:

1 The clean() method is the place to validate two or more fields against each other,
since it’s not specific to any one particular field.
2 The clean validation stage is a better place to attach validation against persistent data.
Since the data already has some validation, you won’t waste as many database cycles
on needless queries.

Let’s explore this with another validation example. Perhaps we want to implement an ice
cream ordering form, where users could specify the flavor desired, add toppings, and then
come to our store and pick them up.

Since we want to prevent users from ordering flavors that are out of stock, we’ll put in a
clean_slug() method. With our flavor validation, our form might look like:

Example 12.7: Custom clean_slug() Method

# flavors/forms.py
from django import forms

from flavors.models import Flavor

class IceCreamOrderForm(forms.Form):
"""Normally done with forms.ModelForm. But we use forms.Form
,→ here
to demonstrate that these sorts of techniques work on every
type of form.
"""
slug = forms.ChoiceField(label='Flavor')
toppings = forms.CharField()

def __init__(self, *args, **kwargs):


super().__init__(*args, **kwargs)
# We dynamically set the choices here rather than
# in the flavor field definition. Setting them in
# the field definition means status updates won't
# be reflected in the form without server restarts.
self.fields['slug'].choices = [
(x.slug, x.title) for x in Flavor.objects.all()
]
# NOTE: We could filter by whether or not a flavor

150 Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues


12.3: Pattern 3: Overriding the Clean Stage of Validation

# has any scoops, but this is an example of


# how to use clean_slug, not filter().

def clean_slug(self):
slug = self.cleaned_data['slug']
if Flavor.objects.get(slug=slug).scoops_remaining <= 0:
msg = 'Sorry, we are out of that flavor.'
raise forms.ValidationError(msg)
return slug

For HTML-powered views, the clean_slug() method in our example, upon throwing
an error, will attach a “Sorry, we are out of that flavor” message to the flavor HTML input
field. This is a great shortcut for writing HTML forms!

Now imagine if we get common customer complaints about orders with too much chocolate.
Yes, it’s silly and quite impossible, but we’re just using ‘too much chocolate’ as a completely
mythical example for the sake of making a point.

In any case, let’s use the clean() method to validate the flavor and toppings fields against
each other.

Example 12.8: Custom clean() Form Method

# attach this code to the previous example (12.7)


def clean(self):
cleaned_data = super().clean()
slug = cleaned_data.get('slug', '')
toppings = cleaned_data.get('toppings', '')

# Silly "too much chocolate" validation example


in_slug = 'chocolate' in slug.lower()
in_toppings = 'chocolate' in toppings.lower()
if in_slug and in_toppings:
msg = 'Your order has too much chocolate.'
raise forms.ValidationError(msg)
return cleaned_data

There we go, an implementation against the impossible condition of too much chocolate!

Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues 151


Chapter 12: Common Patterns for Forms

TIP: Common Fields Used in Multi-Field Validation


It is common practice for user account forms involved with email and password entry
to force the user to enter the same data twice. Other things to check for against those
fields include:
ä Strength of the submitted password.
ä If the email model field isn’t set to unique=True, whether or not the email
is unique.

Figure 12.2: Why would they do this to us?

12.4 Pattern 4: Hacking Form Fields


(2 CBVs, 2 Forms, 1 Model)
This is where we start to get fancy. We’re going to cover a situation where two views/forms
correspond to one model. We’ll hack Django forms to produce a form with custom behavior.

It’s not uncommon to have users create a record that contains a few empty fields which need
additional data later. An example might be a list of stores, where we want each store entered
into the system as fast as possible, but want to add more data such as phone number and

152 Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues


12.4: Pattern 4: Hacking Form Fields (2 CBVs, 2 Forms, 1 Model)

description later. Here’s our IceCreamStore model:

Example 12.9: IceCreamStore Model

# stores/models.py
from django.db import models
from django.urls import reverse

class IceCreamStore(models.Model):
title = models.CharField(max_length=100)
block_address = models.TextField()
phone = models.CharField(max_length=20, blank=True)
description = models.TextField(blank=True)

def get_absolute_url(self):
return reverse('stores:store_detail', kwargs={'pk':
,→ self.pk})

The default ModelForm for this model forces the user to enter the title and
block_address field but allows the user to skip the phone and description fields.
That’s great for initial data entry, but as mentioned earlier, we want to have future updates
of the data to require the phone and description fields.

The way we implemented this in the past before we began to delve into their construction
was to override the phone and description fields in the edit form. This resulted in heavily-
duplicated code that looked like this:

Example 12.10: Repeated Heavily Duplicated Code

# stores/forms.py
from django import forms

from .models import IceCreamStore

class IceCreamStoreUpdateForm(forms.ModelForm):
# Don't do this! Duplication of the model field!
phone = forms.CharField(required=True)
# Don't do this! Duplication of the model field!
description = forms.TextField(required=True)

class Meta:

Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues 153


Chapter 12: Common Patterns for Forms

model = IceCreamStore

This form should look very familiar. Why is that?

Well, we’re nearly copying the IceCreamStore model!

This is just a simple example, but when dealing with a lot of fields on a model, the duplication
becomes extremely challenging to manage. In fact, what tends to happen is copy-pasting of
code from models right into forms, which is a gross violation of Don’t Repeat Yourself.

Want to know how gross? Using the above approach, if we add a simple help_text at-
tribute to the description field in the model, it will not show up in the template until we
also modify the description field definition in the form. If that sounds confusing, that’s
because it is.

A better way is to rely on a useful little detail that’s good to remember about Django forms:
instantiated form objects store fields in a dict-like attribute called fields.

Instead of copy-pasting field definitions from models to forms, we can simply modify exist-
ing attributes on specified fields in the __init__() method of the ModelForm:

Example 12.11: Overriding Init to Modify Existing Field Attributes

# stores/forms.py
# Call phone and description from the self.fields dict-like object
from django import forms

from .models import IceCreamStore

class IceCreamStoreUpdateForm(forms.ModelForm):

class Meta:
model = IceCreamStore

def __init__(self, *args, **kwargs):


# Call the original __init__ method before assigning
# field overloads
super().__init__(*args, **kwargs)
self.fields['phone'].required = True
self.fields['description'].required = True

154 Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues


12.4: Pattern 4: Hacking Form Fields (2 CBVs, 2 Forms, 1 Model)

This improved approach allows us to stop copy-pasting code and instead focus on just the
field-specific settings.

An important point to remember is that when it comes down to it, Django forms are just
Python classes. They get instantiated as objects, they can inherit from other classes, and
they can act as superclasses.

Therefore, we can rely on inheritance to trim the line count in our ice cream store forms:

Example 12.12: Using Inheritance to Clean Up Forms

# stores/forms.py
from django import forms

from .models import IceCreamStore

class IceCreamStoreCreateForm(forms.ModelForm):

class Meta:
model = IceCreamStore
fields = ['title', 'block_address', ]

class IceCreamStoreUpdateForm(IceCreamStoreCreateForm):

def __init__(self, *args, **kwargs):


super().__init__(*args, **kwargs)
self.fields['phone'].required = True
self.fields['description'].required = True

class Meta(IceCreamStoreCreateForm.Meta):
# show all the fields!
fields = ['title', 'block_address', 'phone',
'description', ]

WARNING: Use Meta.fields and Never Use Meta.exclude


We use Meta.fields instead of Meta.exclude so that we know exactly what
fields we are exposing. See Section 28.14: Don’t Use ModelForms.Meta.exclude.

Finally, now we have what we need to define the corresponding CBVs. We’ve got our form
classes, so let’s use them in the IceCreamStore create and update views:

Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues 155


Chapter 12: Common Patterns for Forms

Example 12.13: Revised Create and Update Views

# stores/views
from django.views.generic import CreateView, UpdateView

from .forms import IceCreamStoreCreateForm, IceCreamStoreUpdateForm


from .models import IceCreamStore

class IceCreamCreateView(CreateView):
model = IceCreamStore
form_class = IceCreamStoreCreateForm

class IceCreamUpdateView(UpdateView):
model = IceCreamStore
form_class = IceCreamStoreUpdateForm

We now have two views and two forms that work with one model.

12.5 Pattern 5: Reusable Search Mixin View


In this example, we’re going to cover how to reuse a search form in two views that correspond
to two different models.

Assume that both models have a field called title (this pattern also demonstrates why
naming standards in projects is a good thing). This example will demonstrate how a sin-
gle CBV can be used to provide simple search functionality on both the Flavor and
IceCreamStore models.

We’ll start by creating a simple search mixin for our view:

Example 12.14: TitleSearchMixin a simple search class

# core/views.py
class TitleSearchMixin:

def get_queryset(self):
# Fetch the queryset from the parent's get_queryset
queryset = super().get_queryset()

# Get the q GET parameter


q = self.request.GET.get('q')
if q:

156 Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues


12.5: Pattern 5: Reusable Search Mixin View

# return a filtered queryset


return queryset.filter(title__icontains=q)
# No q is specified so we return queryset
return queryset

The above code should look very familiar as we used it almost verbatim in the Forms + View
example. Here’s how you make it work with both the Flavor and IceCreamStore views.
First the flavor view:

Example 12.15: Adding TitleSearchMixin to FlavorListView

# add to flavors/views.py
from django.views.generic import ListView

from .models import Flavor


from core.views import TitleSearchMixin

class FlavorListView(TitleSearchMixin, ListView):


model = Flavor

And we’ll add it to the ice cream store views:

Example 12.16: Adding TitleSearchMixin to IceCreamStoreListView

# add to stores/views.py
from django.views.generic import ListView

from .models import Store


from core.views import TitleSearchMixin

class IceCreamStoreListView(TitleSearchMixin, ListView):


model = Store

As for the form? We just define it in HTML for each ListView:

Example 12.17: Snippet from stores/store_list.html

{# form to go into stores/store_list.html template #}


<form action="" method="GET">
<input type="text" name="q" />

Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues 157


Chapter 12: Common Patterns for Forms

<button type="submit">search</button>
</form>

and

Example 12.18: Snippet from flavors/flavor_list.html

{# form to go into flavors/flavor_list.html template #}


<form action="" method="GET">
<input type="text" name="q" />
<button type="submit">search</button>
</form>

Now we have the same mixin in both views. Mixins are a good way to reuse code, but using
too many mixins in a single class makes for very hard-to-maintain code. As always, try to
keep your code as simple as possible.

12.6 Summary
We began this chapter with the simplest form pattern, using a ModelForm, CBV, and de-
fault validators. We iterated on that with an example of a custom validator.

Next, we explored more complex validation. We covered an example overriding the clean
methods. We also closely examined a scenario involving two views and their corresponding
forms that were tied to a single model.

Finally, we covered an example of creating a reusable search mixin to add the same form to
two different apps.

158 Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues


13 | Form Fundamentals

100% of Django projects should use Forms.


95% of Django projects should use ModelForms.
91% of all Django projects use ModelForms.
80% of ModelForms require trivial logic.
20% of ModelForms require complicated logic.

– Daniel’s made-up statistics™

Django’s forms are really powerful, and knowing how to use them anytime data is coming
from outside your application is part of keeping your data clean.

There are edge cases that can cause a bit of anguish. If you understand the structure of how
forms are composed and how to call them, most edge cases can be readily overcome.

The most important thing to remember about Django forms is they should be used to vali-
date all incoming data.

13.1 Validate All Incoming Data With Django Forms


Django’s forms are a wonderful framework designed to validate Python dictionaries. While
most of the time we use them to validate incoming HTTP requests containing POST, there
is nothing limiting them to be used just in this manner.

For example, let’s say we have a Django app that updates its model via CSV files fetched
from another project. To handle this sort of thing, it’s not uncommon to see code like this
(albeit in not as simplistic an example):

Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues 159


Chapter 13: Form Fundamentals

Example 13.1: How Not to Import CSV

import csv

from django.utils.six import StringIO

from .models import Purchase

def add_csv_purchases(rows):

rows = StringIO.StringIO(rows)
records_added = 0

,→ # Generate a dict per row, with the first CSV row being the keys
for row in csv.DictReader(rows, delimiter=','):
# DON'T DO THIS: Tossing unvalidated data into your model.
Purchase.objects.create(**row)
records_added += 1
return records_added

In fact, what you don’t see is that we’re not checking to see if sellers, stored as a string
in the Purchase model, are actually valid sellers. We could add validation code to our
add_csv_purchases() function, but let’s face it, keeping complex validation code under-
standable as requirements and data changes over time is hard.

A better approach is to validate the incoming data with a Django Form like so:

Example 13.2: How to Safely Import CSV

import csv

from django.utils.six import StringIO

from django import forms

from .models import Purchase, Seller

class PurchaseForm(forms.ModelForm):

class Meta:

160 Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues


13.1: Validate All Incoming Data With Django Forms

model = Purchase

def clean_seller(self):
seller = self.cleaned_data['seller']
try:
Seller.objects.get(name=seller)
except Seller.DoesNotExist:
msg = '{0} does not exist in purchase #{1}.'.format(
seller,
self.cleaned_data['purchase_number']
)
raise forms.ValidationError(msg)
return seller

def add_csv_purchases(rows):

rows = StringIO.StringIO(rows)

records_added = 0
errors = []
# Generate a dict per row, with the first CSV row being the
,→ keys.
for row in csv.DictReader(rows, delimiter=','):

# Bind the row data to the PurchaseForm.


form = PurchaseForm(row)
# Check to see if the row data is valid.
if form.is_valid():
# Row data is valid so save the record.
form.save()
records_added += 1
else:
errors.append(form.errors)

return records_added, errors

What’s really nice about this practice is that rather than cooking up our own validation
system for incoming data, we’re using the well-proven data testing framework built into
Django.

Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues 161


Chapter 13: Form Fundamentals

TIP: What About the code parameter?


Arnaud Limbourg pointed out that the official Django docs recommend passing a
code parameter to ValidationError as follows:
forms.ValidationError(_(’Invalid value’), code=’invalid’)
In our example we don’t include one, but you can use it in your code if you want.

Django core developer Marc Tamlyn says, “On a personal note, I feel that Django’s
docs are maybe a little heavy handed with recommending the use of code as a best
practice everywhere, although it should be encouraged in third party applications. It
is however definitely the best practice for any situation where you wish to check the
nature of the errors - it’s much better than checking the message of the validation
error as this is subject to copy changes.”

Reference:
ä docs.djangoproject.com/en/3.2/ref/forms/validation/
#raising-validationerror

13.2 Use the POST Method in HTML Forms


Every HTML form that alters data must submit its data via the POST method:

Example 13.3: How to Use POST in HTML

<form action="{% url 'flavor_add' %}" method="POST">

The only exception you’ll ever see to using POST in forms is with search forms, which
typically submit queries that don’t result in any alteration of data. Search forms that are
idempotent should use the GET method.

13.3 Always Use CSRF Protection With HTTP Forms


That Modify Data
Django comes with cross-site request forgery protection (CSRF) built in, and usage of it is
introduced in Part 4 of the Django introductory tutorial. It’s easy to use, and Django even
throws a friendly warning during development when you forget to use it. This is a critical
security issue, and here and in our security chapter we recommend always using Django’s
CSRF protection capabilities.

In our experience, the time when CSRF protection isn’t used is when creating machine-
accessible APIs authenticated by proven libraries such as github.com/jazzband/

162 Please submit issues to github.com/feldroy/two- scoops- of- django- 3.x/issues

You might also like