drf-cbv-image


In this post let’s continue our development of the Django Talk project along with Django Rest Framework as we implement class based views, along with some simple refactoring to keep our code Dry. Essentially, we’re migrating an existing RESTful API based on function based views to class based views.

This is part four of this tutorial series.

Need to catch up? Check out parts one and two where we tackle Django and AJAX as well as part three where we introduce the Django Rest Framework.

Need the code? Download it from the repo.

For a more in-depth tutorial on Django Rest Framework, be sure to check out the third Real Python course.

Refactor

Before jumping into class based views, let’s do a quick refactor of our current code. Within the def post_element() view, make the following updates:

1
2
3
4
5
6
post = get_object_or_404(Post, id=pk)

# try:
#     post = Post.objects.get(pk=pk)
# except Post.DoesNotExist:
#     return HttpResponse(status=404)

So here we are using the get_object_or_404 shortcut to raise a 404 error instead of an exception.

Make sure to also update the imports:

1
from django.shortcuts import render, get_object_or_404

Test it out. Try viewing an element that does not exist – i.e., http://localhost:8000/api/v1/posts/202?format=json. You should see the following response in your browser:

1
2
3
{
    detail: "Not found"
}

If you open the Network tab within Chrome Developer Tools, you should see a 404 status code. Pretty cool, right? Unfortunately, we won’t be using it much longer, as it’s time to say goodbye to our current function based views and add in class based views.

Class Based Views

While functions are easy to work with, it’s often beneficial to use class based views to reuse functionality, especially for large APIs with a number of endpoints.

Comment out the code for the function based views.

Collection

Add in the following code for the class based views for the collection:

1
2
3
4
5
6
7
8
9
10
11
12
class PostCollection(mixins.ListModelMixin,
                     mixins.CreateModelMixin,
                     generics.GenericAPIView):

    queryset = Post.objects.all()
    serializer_class = PostSerializer

    def get(self, request, *args, **kwargs):
        return self.list(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        return self.create(request, *args, **kwargs)

Welcome to the power of mixins!

  1. The ListModelMixin provides the list() function for serializing a collection to JSON and then returning it as a response to a GET request.
  2. The CreateModelMixin meanwhile provides the create() function for creating a new object in response to a POST request.
  3. Finally, the GenericAPIView mixin provides the “core” functionality needed for a RESTful API.

Please consult the official DRF documentation for more info on these mixins.

Member

Now add the code for the member:

1
2
3
4
5
6
7
8
9
10
11
12
class PostMember(mixins.RetrieveModelMixin,
                   mixins.DestroyModelMixin,
                   generics.GenericAPIView):

    queryset = Post.objects.all()
    serializer_class = PostSerializer

    def get(self, request, *args, **kwargs):
        return self.retrieve(request, *args, **kwargs)

    def delete(self, request, *args, **kwargs):
        return self.destroy(request, *args, **kwargs)

Here, we simply use the GenericAPIView for the “core functionality” and the remaining mixins to provide the needed functionality to handle GET and DELETE requests.

URLs

Finally, let’s update urls.py to account for the class based views:

1
2
3
4
5
6
7
8
9
10
11
12
from django.conf.urls import patterns, url
from talk import views


urlpatterns = patterns(
    'talk.views',
    url(r'^$', 'home'),

    # api
    url(r'^api/v1/posts/$', views.PostCollection.as_view()),
    url(r'^api/v1/posts/(?P<pk>[0-9]+)/$', views.PostMember.as_view())
)

Take note of the as_view() method, which provides a bit of magic to treat the class as a view function.

Test this out. Launch the development server, then:

  1. Make sure all posts load
  2. Add a new post
  3. Remove a post

What happened? You should have seen the following error when trying to add a new post:

1
2
3
4
400: {
    "text": ["This field is required."],
    "author": ["This field is required."]
}

Fortunately, this is an easy fix.

Refactor the AJAX

We need to change the way we’re sending the data with the POST request. First, update the_post key to text:

1
data : { text : $('#post-text').val() }, // data sent with the post request

If you test it out now, the error should just indicate that we’re missing the author field. We can grab the logged in user a number of ways, but the easiest is directly from the DOM.

It’s worth noting that we can override the default functionality in the views to grab the username from the request object. However, it’s best to use DRF class based views as intended: Passing all the appropriate parameters – e.g., text and author – in the JSON request and then using the serializer to save them.

Open index.html within the “templates/talk” directory. At the top of the file, you’ll see that we’re accessing the username directly from the request object:

1
<h2>Hi, {{request.user.username}}</h2>

Let’s isolate the actual username to make it easier to grab with jQuery:

1
<h2>Hi, <span id="user">{{request.user.username}}</span></h2>

Now update data again:

1
data : { text : $('#post-text').val(), author: $('#user').text()}

Test it out; all should be well.

Generic Based Views

Want even more with less? Check this out. Comment out the code we just added, and then update the views like so:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from talk.models import Post
from talk.forms import PostForm
from talk.serializers import PostSerializer
from rest_framework import generics
from django.shortcuts import render


def home(request):
    tmpl_vars = {'form': PostForm()}
    return render(request, 'talk/index.html', tmpl_vars)


#########################
### class based views ###
#########################

class PostCollection(generics.ListCreateAPIView):
    queryset = Post.objects.all()
    serializer_class = PostSerializer

class PostMember(generics.RetrieveUpdateDestroyAPIView):
    queryset = Post.objects.all()
    serializer_class = PostSerializer

So, not only can we still handle all the same requests as before, but we can now handle PUT requests to update each member. More. For a lot less.

Before moving on jump back to the function based views and compare the code with the class based views. Which is easier to read? Actually, which is easier to understand? Add comments if necessary to help you better understanding what’s happening under the hood with the class based views. Not only do you sacrifice readability with class based views, but testing is slightly more difficult as well. However, we’re now taking advantage of inheritance, and for larger projects that have a number of similar views, class based views are perfect since you don’t have to write the same code over and over again. Make sure to weigh the pros and cons before jumping to class based views.

Be sure to test the endpoints before moving on.

  1. Make sure all posts load
  2. Add a new post
  3. Remove a post

Curious about the PUT request? Test it out from the Browsable API – i.e., http://localhost:8000/api/v1/posts/1/ – via the HTML form. Want to remove the PUT method handler altogether? Update the code like so:

1
2
3
class PostMember(generics.RetrieveDestroyAPIView):
    queryset = Post.objects.all()
    serializer_class = PostSerializer

Test it out now. Questions? Check out the documentation.

Conclusion

This is all for now. We may jump back to the Django Rest Framework in a future post to look at pagination, permissions, and basic validation, but before that we’ll add on AngularJS to the front-end to consume the data.

Cheers!

Comments