2 Scoops of django

Elad Silberring

The python ZEN

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.

fbv's vs cbv's

Django views do the following:

1. Accepts a HTTP request


2. Does something with data provided by the HTTP request


3. Returns a HTTP response
Nothing else!

The Simplest Views

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')

The advantages of FBVs

• 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)

The advantages of CBVs

• 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

Summary

• 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

Summary

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

The "Use All the Generic CBVs" Approach

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!

Some Form Pages Are CreateViews In Disguise

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

Constrain Views With Access Control Mixins

LoginRequiredMixin

AccessMixin

PermissionRequiredMixin

UserPassesTestMixin

class FlavorDetailView(LoginRequiredMixin, DetailView):

  model = Flavor

Only authen+cated users can visit the Flavor detail view.

Using Mixins

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"

Refactor Shared Behavior Into Mixins

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!"

 

Writing Mixins for Views

• Mixins shouldn't inherit from another class


• Otherwise inheritance chain can get messy


• Inheritance chains for some GCBVs are complex

Warning: Keep View Logic Out of urls.py!

# 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'),

]

 

Problems combining view and routing logic.

• 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.

 

Refactor Shared Behavior Into Mixins

# 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'

 

Seperating Views from URLConf Plus a Mixin

# 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'

 

Form Validation and Security

Incoming Data is Dangerous

• The ability of forms to validate incoming data is important

• Let's explore!

Security Notice #1
 

• Always use Django forms to validate incoming data
• Even data coming from sources other than HTTP POST
• Email
• FTP
• REST APIs
• SOAP APIs

NEVER SAVE UNVALIDATED DATA

Security Notice #2

• 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

Use CSRF Protection

Where Validation Happens

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.

How ModelForm Data Is Saved

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!

Security Notice #3a:

Don't Use `ModelForms.Meta.excludes`

"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']

Security Notice #3b:

ModelForms.Meta.fields = "__all__"

• 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']

Always explicitly specified form fields

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

 

Wrong Way to Customize ModelForm

# 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)

 

Right Way to Customize ModelForms

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

 

Summary

• 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"

2 Django Scoops

By Elad Silberring

2 Django Scoops

A review of Daniel Feldroy webinar

  • 672