Elad Silberring
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
There should be one-- and preferably only one --obvious way to do it. Although that way may not be obvious at first unless you're Dutch. If the implementation is easy to explain, it may be a good idea. Namespaces are one honking great idea -- let's do more of those!
>>> import this
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
1. Accepts a HTTP request
2. Does something with data provided by the HTTP request
3. Returns a HTTP response
Nothing else!
FBV:
def my_view(request):
if request.method == 'GET':
# Do logic here
return HttpResponse('FBV')
CBV:
class MyView(View):
def get(self, request):
# Do logic here
return HttpResponse('CBV')
• FBVs are simple, a function that accepts a request and returns a response
• FBVs are extremely explicit
• Do whatever you want (Also a negative)
• Write decorators to reuse functionality (negative: learning curve)
• Writing CBV mixins is trivial, just so long as you aren't touching dispatch.
• Many package authors provide mixins before decorators
• More packages to play with means great power
• Constraints on HTTP types are great for security
• GCBVs make writing CRUD-style apps super fast
• Hence Tom Christie's Django REST Framework
• We prefer CBVs because of of the advantages we list
• FBVs are acceptable, except for DRF
• FBVs are supported in DRF as a second class citizen
from django.http import HttpResponse
from django.views.generic import View
class SimplestView(View):
def get(self, request, *args, **kwargs):
return HttpResponse('I am a GET response')
def post(self, request, *args, **kwargs):
return HttpResponse('I am a POST response')
def put(self, request, *args, **kwargs):
return HttpResponse('I am a PUT response')
def delete(self, request, *args, **kwargs):
return HttpResponse('I am a DELETE response')
from django.http import HttpResponse, HttpResponseNotAllowed
def quote_simplest_unquote_view(request):
if request.method == 'GET':
return HttpResponse('I am a GET response')
elif request.method == 'POST':
return HttpResponse('I am a POST response')
elif request.method == 'PUT':
return HttpResponse('I am a PUT response')
elif request.method == 'DELETE':
return HttpResponse('a DELETE response')
raise HttpResponseNotAllowed()
Text
Text
CBV
FBV
Django provides numerous GCBVs for your subclassing pleasure:
View, RedirectView, TemplateView, ListView,
DetailView, FormView, CreateView, UpdateView,
DeleteView, and the generic date views
They guarantee they'll save you time and code.
Use them and have fun!
Example: Beginning the `sundae` making process
• Think in terms of tracking process through models
• Make a mental list of what objects will be created a:er the form is validated
• `AssembleSundae` model
• Many form pages turn out to be excellent CreateView candidates
• Consider model design changes
If necessary, use a task queue so users can go do something else
• LoginRequiredMixin
• AccessMixin
• PermissionRequiredMixin
• UserPassesTestMixin
class FlavorDetailView(LoginRequiredMixin, DetailView):
model = Flavor
Only authen+cated users can visit the Flavor detail view.
• Django's base view classes always go to the right
• Mixins go to the left of the base view
class FruityFlavorView(FreshFruitMixin, TemplateView):
template_name = "fruity_flavor.html"
from django.contrib import messages
class FlavorActionMixin:
model = Flavor
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)
class FlavorCreateView(LoginRequiredMixin, FlavorActionMixin, CreateView):
success_msg = "Flavor created!"
class FlavorUpdateView(LoginRequiredMixin, FlavorActionMixin, UpdateView):
success_msg = "Flavor updated!"
• Mixins shouldn't inherit from another class
• Otherwise inheritance chain can get messy
• Inheritance chains for some GCBVs are complex
# Don't do this!
from django.urls import path
from django.views.generic import DetailView
from tastings.models import Tasting
urlpatterns = [
path('<int:pk>',
DetailView.as_view(
model=Tasting,
template_name='tastings/detail.html'),
name='detail'),
path('<int:pk>/results/',
DetailView.as_view(
model=Tasting,
template_name='tastings/results.html'),
name='results'),
]
• Loose coupling between views, urls, and models has been replaced with tight coupling, meaning you can never reuse the view definitions.
• Don't Repeat Yourself (DRY) is violated by using the same/similar arguments repeatedly between CBVs.
• Infinite flexibility (for URLs) is destroyed.
• Class inheritance, the primary advantage of Class Based Views, is impossible using this anti-pattern.
• What happens when we need to add in authentication?
** Puting your view code into your URLConfs quickly turns your URLConfs into an unmaintainable mess.
# urls.py
from django.urls import path
from tastings.views import TastingView, TastingResultsView
urlpatterns = [
path('<int:pk>',
TastingView.as_view(),
name='detail'),
path('<int:pk>/results/',
TastingResultsView.as_view(),
name='results'),
]
# views.py
from django.views.generic import DetailView
class TastingView(DetailView):
model = Tasting
template_name = 'tastings/detail.html'
class TastingView(DetailView):
model = Tasting
template_name = 'tastings/results.html'
# views.py
from django.views.generic import DetailView
class TastingMixin:
model = Tasting
class TastingView(TastingMixin, DetailView):
template_name = 'tastings/detail.html'
class TastingView(TastingMixin, DetailView):
template_name = 'tastings/results.html'
• The ability of forms to validate incoming data is important
• Let's explore!
• Always use Django forms to validate incoming data
• Even data coming from sources other than HTTP POST
• Email
• FTP
• REST APIs
• SOAP APIs
• Always.
• Never disable CSRF except for very specific reasons:
• Webhook receivers (that have secret URLs)
• Ancient data feeds that you just have to accept
• If you turn off CSRF, ensure the data is:
• validated
• saved
The Easy Answer: Inside form.is_valid()
The Hard Answer:
1. form.is_valid() calls the form.full_clean() method.
2. form.full_clean() iterates through the form︎ fields and these validate themselves in turn.
1. Incoming data that can't be coerced into a Python object raises a ValidationError.
2. Data is validated against field-specific rules, including custom validators. Errors raise
`ValidationError`.
3. Any clean_<field> methods are called at this Dme.
3. form.full_clean() executes the form.clean() method.
1. Valid form data is saved to the form instance.
2. Form data is saved to the model instance.
• Django encourages using two-2ered approach for incoming data.
• Why?
More Security!
"Explicit is be-er than Implicit" — Zen of Python by Tim Peters
• Implicitly allows all fields except the ones specified
• Dumps all fields to form, we must remember to constrain them
• Forces us to check model changes against 1-* forms
• Better approach is to use fields = ['title', 'description']
• Dangerous shortcut
• Dumps all fields to form, we must remember to constrain them
• Forces us to check model changes against 1-* forms
• Better approach is to use fields = ['title', 'description']
from django import forms
from .models import Store
class StoreForm(forms.ModelForm):
class Meta:
model = Store
# Explicitly specifying the fields we want
fields = (
"title", "address_1", "address_2", "email",
"usstate", "postal_code", "city",
)
Note: Similar behavior exists with Django REST Framework
# users/forms.py
from django import forms
from .models import User
class UserForm(forms.ModelForm):
name = forms.CharField(max_length=100, required=True)
age = forms.IntegerField(required=True)
profession = forms.CharField(required=True)
bio = forms.TextField(required=True)
class Meta:
model = User
Don't redefine the fields in your custom ModelForm class.
• Violates DRY
• Easy to make mistakes (e.g. max_length mismatches)
from django import forms
from .models import User
class UserForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['name'].required = True
self.fields['age'].required = True
self.fields['bio'].required = True
self.fields['profession'].required = True
class Meta:
model = User
• We're not repea,ng code that's in the model
• We're preserving the defini,ons in models.py
• Always use Django forms to validate incoming data
• Even data coming from sources other than HTTP POST
• Never rely on just the ORM to protect your data
• Be wary of the "allproperty"