Two Scoops of Django 3x - Compress 4
Two Scoops of Django 3x - Compress 4
Two Scoops of Django 3x - Compress 4
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.
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
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!
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:
There’s another reason, which we’ll cover shortly. In the meantime, let’s demonstrate this
code in action:
# sprinkles/views.py
from django.shortcuts import get_object_or_404
from django.shortcuts import render
from django.http import HttpRequest, HttpResponse
request = check_sprinkles(request)
return render(request,
"sprinkles/sprinkle_list.html",
{"sprinkles": Sprinkle.objects.all()})
request = check_sprinkles(request)
"sprinkles/sprinkle_preview.html",
{"sprinkle": sprinkle})
Another good feature about this approach is that it’s trivial to integrate into class-based
views:
class SprinkleDetail(DetailView):
"""Standard detail view"""
model = Sprinkle
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.
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.
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:
# sprinkles/decorators.py
import functools
# sprinkles/views.py
from django.shortcuts import get_object_or_404, render
Figure 9.1: If you look at sprinkles closely, you’ll see that they’re Python decorators.
Yes, this technique can be leveraged with decorators. See Example 8.5 which gives a hint as
to how this can be accomplished.
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.
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.
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,
which in the most simplistic definition possible, proceeds from left to right:
class FreshFruitMixin:
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.
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.
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.
docs.djangoproject.com/en/3.2/topics/class-based-views/intro/
#decorating-class-based-views.
# flavors/views.py
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import DetailView
If you forget and switch the order, you will get broken or unpredictable results.
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.
Just as you can add logic to form_valid(), you can also add logic to form_invalid().
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.
class FavoriteMixin:
@cached_property
def likes_and_favorites(self):
"""Returns a dictionary of likes and favorites"""
likes = self.object.likes()
favorites = self.object.favorites()
return {
"likes": likes,
"favorites": favorites,
"favorites_count": favorites.count(),
The nice thing about this is the various flavors/ app templates can now access this property:
{# 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 %}
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:
# 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.
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.
Writing these views is easy, since it’s mostly a matter of using what Django gives us:
# flavors/views.py
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import CreateView, DetailView, UpdateView
class FlavorDetailView(DetailView):
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:
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.
For the confirmation page fix, we change flavors/views.py to contain the following:
# flavors/views.py
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import CreateView, DetailView, UpdateView
class FlavorActionMixin:
@property
def success_msg(self):
return NotImplemented
class FlavorDetailView(DetailView):
model = Flavor
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.
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:
{% 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 %}
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.
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
page.
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:
class FlavorListView(ListView):
model = Flavor
def get_queryset(self):
# Fetch the queryset from the parent get_queryset
queryset = super().get_queryset()
Now, instead of listing all of the flavors, we list only the flavors whose titles contain the
search string.
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:
{# 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>
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.
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:
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.
ä 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
10.8 Summary
This chapter covered:
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.
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)
11.2 Resources
ä docs.djangoproject.com/en/dev/topics/async/
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.
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.
If you recall, using ModelForms with CBVs to implement add/edit forms can be done in
just a few lines of code:
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.
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.
# 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'.
"""
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.
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.
# core/models.py
from django.db import models
class TastyTitleAbstractModel(models.Model):
title = models.CharField(max_length=255,
,→ validators=[validate_tasty])
class Meta:
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.
# flavors/models.py
from django.db import models
from django.urls import reverse
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:
To support these behaviors, we need to create a custom FlavorForm that utilizes our cus-
tom field validator:
# flavors/forms.py
from django import forms
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:
# flavors/views.py
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import CreateView, DetailView, UpdateView
class FlavorActionMixin:
model = Flavor
fields = ['title', 'slug', 'scoops_remaining']
@property
def success_msg(self):
return NotImplemented
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.
ä 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
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:
# flavors/forms.py
from django import forms
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 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.
There we go, an implementation against the impossible condition of too much chocolate!
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
# 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:
# stores/forms.py
from django import forms
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:
model = IceCreamStore
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:
# stores/forms.py
# Call phone and description from the self.fields dict-like object
from django import forms
class IceCreamStoreUpdateForm(forms.ModelForm):
class Meta:
model = IceCreamStore
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:
# stores/forms.py
from django import forms
class IceCreamStoreCreateForm(forms.ModelForm):
class Meta:
model = IceCreamStore
fields = ['title', 'block_address', ]
class IceCreamStoreUpdateForm(IceCreamStoreCreateForm):
class Meta(IceCreamStoreCreateForm.Meta):
# show all the fields!
fields = ['title', 'block_address', 'phone',
'description', ]
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:
# stores/views
from django.views.generic import CreateView, UpdateView
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.
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.
# core/views.py
class TitleSearchMixin:
def get_queryset(self):
# Fetch the queryset from the parent's get_queryset
queryset = super().get_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:
# add to flavors/views.py
from django.views.generic import ListView
# add to stores/views.py
from django.views.generic import ListView
<button type="submit">search</button>
</form>
and
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.
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.
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):
import csv
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:
import csv
class PurchaseForm(forms.ModelForm):
class Meta:
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=','):
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.
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
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.
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/