Wednesday, October 31st, 2012

Really starting to love Django's class-based views

If you haven't yet tried or played around with Django's class-based views, I highly recommend them over traditional function-based views. At first, I was terrified of class-based views, not knowing how they worked or how to properly extend them without breaking their functionality. After doing a few projects using class-based views, I can safely say that I am now comfortable with using them in every future Django project as the main view type.

Firstly, don't rely on Django's documentation for class-based views, they are not too helpful when actually modifying their behavior to suit your particular scenario. To enable login_required, do so in your urls.py file, as it is much simpler and easier to locate at a later date which views are actually decorated. Creating a special ProtectedView as exampled in the Django documentation isn't your best route for CRUD views, it may work fine to extend TemplateView, but don't use it for CRUDs, it will only add to your overall development time.

I believe a simple example of how to use these class-based views to your advantage will well suit this article, here is a simple example of a TodoList with user ownership checks:

class TodoList(ListView):
    model = Todo
    all_items = False
    def get_queryset(self):
        queryset = super(TodoList, self).get_queryset().filter(user=self.request.user)
        if not self.all_items:
            return queryset.filter(complete=False)
        return queryset

class TodoDetails(DetailView):
    model = Todo
    def get_queryset(self):
        return super(TodoDetails, self).get_queryset().filter(user=self.request.user)

class TodoCreate(CreateView):
    model = Todo
    form_class = TodoForm
    def form_valid(self, form):
        self.object = form.save(commit=False)
        self.object.user = self.request.user
        self.object.save()
        return super(ModelFormMixin, self).form_valid(form)

This would be placed into your views.py, you may notice something interesting done with the TodoList. There is an extra model-level variable called all_items which is a boolean. Here is how you could use this in your urls.py:

urlpatterns = patterns('',
    url(r'^todo/$', login_required(TodoList.as_view()), name='todo-listl'),
    url(r'^todo/all/$', login_required(TodoList.as_view(all_items=True)), name='todo-list-all'),
    url(r'^todo/add/$', login_required(TodoCreate.as_view()), name='todo-add'),
    url(r'^todo/(?P<pk>\d+)/$', login_required(TodoDetails.as_view()), name='todo-view'),
)

The trick here to extending, is to create a model-level variable which is assigned through your urls.py, these model-level variables cannot be assigned any other way from the request. You can of course create new variables at will, if need them throughout your view. Here are some class variables which are helpful to know about when extending class-view functionality. These variables aren't well documented, but they are there, and the default generic views use them:

kwargs
This is a standard dictionary accessed like self.kwargs['slug']. You can use this to access variables captured in the URL. In function-based views, these are normally based directly to the function as keyword parameters. Class-based views capture them in the same way.
object
If your view access or creates an object, then it will live in self.object, you are free to reference it in most functions within your view class. In the Todo example, it is modified to add the current user to the object before saving.
request
Ah, our good old request variable is of course available to us in every function in a view class, it can be accessed through self.request, it is used in the Todo example to obtain the current user object.

There are other class-specific variables as well, and I do believe that self.args is also available. Hopefully this short article helps developers new and old of Django better understand how class-based views work. I plan on writing additional articles on class-based views in the future.

Comment #1: Posted 2 years, 1 month ago by Anonymous

Might TodoCreate.form_valid() be pared down to

def form_valid(self, form):
self.object.user = self.request.user
return super(TodoCreate, self).form_valid(form)

?

Comment #2: Posted 2 years, 1 month ago by Kevin Veroneau

Sorry that your comment didn't originally post due to the new comments system, but I will answer your good question.

You cannot use the self.object variable in the form_valid() function. If you have previous coded using Django functions, you should know that a model object isn't created until after you validate the form.

Standard function-based view for form processing:
def todo_create(req):
if req.method == 'POST':
form = TodoForm(req.POST)
if form.is_valid():
todo = form.save(commit=False)
todo.user = req.user
todo.save()
redirect(todo)
else:
form = TodoForm()
return render(req, "template.html", {'form':form})

Hopefully this clarifies why self.object won't work in this situation.

Comment #3: Posted 2 years, 1 month ago by Kevin Veroneau

Hrm.. It seems apparent to me that Python course code in comments doesn't render too tell. The indentations are all broken, but you should get the idea.

About Me

My Photo
Names Kevin, hugely into UNIX technologies, not just Linux. I've dabbled with the demons, played with the Sun, and now with the Penguins.




Kevin Veroneau Consulting Services
Do you require the services of a Django contractor? Do you need both a website and hosting services? Perhaps I can help.

If you like what you read, please consider donating to help with hosting costs, and to fund future books to review.

Python Powered | © 2012-2014 Kevin Veroneau