Welcome to the sixth part of the tutorial series! In this tutorial, we are going to explore in great detail the
Class-Based Views. We are also going to refactor some of the existing views so to take advantage of the built-in
Generic Class-Based Views.
There are many other topics that we are going to touch with this tutorial, such as how to work with pagination,
how to work with Markdown and how to add a simple editor. We are also going to explore a built-in package called
Humanize, which is used to give a “human touch” to the data.
Alright, folks! Let’s implement some code. We have plenty of work to do today!
Views Strategies
At the end of the day, all Django views are functions. Even class-based views (CBV). Behind the scenes, it does all
the magic and ends up returning a view function.
Class-based views were introduced to make it easier for developers to reuse and extend views. There are many benefits
of using them, such as the extendability, the ability to use O.O. techniques such as multiple inheritances, the handling
of HTTP methods are done in separate methods, rather than using conditional branching, and there are also the
Generic Class-Based Views (GCBV).
Before we move forward, let’s clarify what those three terms mean:
Function-Based Views (FBV)
Class-Based Views (CBV)
Generic Class-Based Views (GCBV)
A FBV is the simplest representation of a Django view: it’s just a function that receives an HttpRequest object and
returns an HttpResponse.
A CBV is every Django view defined as a Python class that extends the django.views.generic.View abstract class. A
CBV essentially is a class that wraps a FBV. CBVs are great to extend and reuse code.
GCBVs are built-in CBVs that solve specific problems such as listing views, create, update, and delete views.
Below we are going to explore some examples of the different implementation strategies.
Function-Based View
views.py
urls.py
Class-Based View
A CBV is a view that extends the View class. The main difference here is that the requests are handled inside class
methods named after the HTTP methods, such as get, post, put, head, etc.
So, here we don’t need to do a conditional to check if the request is a POST or if it’s a GET. The code goes
straight to the right method. This logic is handled internally in the View class.
views.py
The way we refer to the CBVs in the urls.py module is a little bit different:
urls.py
Here we need to use the as_view() class method, which returns a view function to the url patterns. In some cases, we
can also feed the as_view() with some keyword arguments, so to customize the behavior of the CBV, just like we did
with some of the authentication views to customize the templates.
Anyway, the good thing about CBV is that we can add more methods, and perhaps do something like this:
It’s also possible to create some generic views that accomplish some tasks so that we can reuse it across the project.
But that’s pretty much all you need to know about CBVs. Simple as that.
Generic Class-Based View
Now about the GCBV. That’s a different story. As I mentioned earlier, those views are built-in CBVs for common use
cases. Their implementation makes heavy usage of multiple inheritances (mixins) and other O.O. strategies.
They are very flexible and can save many hours of work. But in the beginning, it can be difficult to work with them.
When I first started working with Django, I found GCBV hard to work with. At first, it’s hard to tell what is going on,
because the code flow is not obvious, as there is good chunk of code hidden in the parent classes. The documentation
is a little bit challenging to follow too, mostly because the attributes and methods are sometimes spread across eight
parent classes. When working with GCBV, it’s always good to have the ccbv.co.uk
opened for quick reference. No worries, we are going to explore it together.
Now let’s see a GCBV example.
views.py
Here we are using a generic view used to create model objects. It does all the form processing and save the object
if the form is valid.
Since it’s a CBV, we refer to it in the urls.py the same way as any other CBV:
urls.py
Other examples of GCBVs are DetailView, DeleteView, FormView, UpdateView, ListView.
Update View
Let’s get back to the implementation of our project. This time we are going to use a GCBV to implement the
edit post view:
With the UpdateView and the CreateView, we have the option to either define form_class or the fields
attribute. In the example above we are using the fields attribute to create a model form on-the-fly. Internally,
Django will use a model form factory to compose a form of the Post model. Since it’s a very simple form with just
the message field, we can afford to work like this. But for complex form definitions, it’s better to define a model
form externally and refer to it here.
The pk_url_kwarg will be used to identify the name of the keyword argument used to retrieve the Post object.
It’s the same as we define in the urls.py.
If we don’t set the context_object_name attribute, the Post object will be available in the template as
“object.” So, here we are using the context_object_name to rename it to post instead. You will see how we are
using it in the template below.
In this particular example, we had to override the form_valid() method so as to set some extra fields such as the
updated_by and updated_at. You can see what the base form_valid() method looks like here:
UpdateView#form_valid.
Observe now how we are navigating through the post object: post.topic.board.pk. If we didn’t set the
context_object_name to post, it would be available as: object.topic.board.pk. Got it?
Testing The Update View
Create a new test file named test_view_edit_post.py inside the boards/tests folder. Clicking on the link below
you will see many routine tests, just like we have been doing in this tutorial. So I will just highlight the new parts
here:
For now, the important parts are: PostUpdateViewTestCase is a class we defined to be reused across the other
test cases. It’s just the basic setup, creating users, topic, boards, and so on.
The class LoginRequiredPostUpdateViewTests will test if the view is protected with the @login_required
decorator. That is if only authenticated users can access the edit page.
The class UnauthorizedPostUpdateViewTests creates a new user, different from the one who posted and tries to
access the edit page. The application should only authorize the owner of the post to edit it.
Let’s run the tests:
First, let’s fix the problem with the @login_required decorator. The way we use view decorators on class-based views
is a little bit different. We need an extra import:
We can’t decorate the class directly with the @login_required decorator. We have to use the utility
@method_decorator, and pass a decorator (or a list of decorators) and tell which method should be decorated. In
class-based views it’s common to decorate the dispatch method. It’s an internal method Django use (defined inside
the View class). All requests pass through this method, so it’s safe to decorate it.
Run the tests:
Okay! We fixed the @login_required problem. Now we have to deal with the other users editing any posts problem.
The easiest way to solve this problem is by overriding the get_queryset method of the UpdateView. You can see
what the original method looks like here UpdateView#get_queryset.
With the line queryset = super().get_queryset() we are reusing the get_queryset method from the parent class, that
is, the UpateView class. Then, we are adding an extra filter to the queryset, which is filtering the post using
the logged in user, available inside the request object.
Test it again:
All good!
List View
We could refactor some of our existing views to take advantage of the CBV capabilities. Take the home page for example.
We are just grabbing all the boards from the database and listing it in the HTML:
boards/views.py
Here is how we could rewrite it using a GCBV for models listing:
If we check the homepage we will see that nothing really changed, everything is working as expected. But we have to
tweak our tests a little bit because now we are dealing with a class-based view:
We can very easily implement pagination with class-based views. But first I wanted to do a pagination by hand, so that
we can explore better the mechanics behind it, so it doesn’t look like magic.
It wouldn’t really make sense to paginate the boards listing view because we do not expect to have many boards. But
definitely the topics listing and the posts listing need some pagination.
From now on, we will be working on the board_topics view.
First, let’s add some volume of posts. We could just use the application’s user interface and add several posts, or
open the Python shell and write a small script to do it for us:
Good, now we have some data to play with.
Before we jump into the code, let’s experiment a little bit more with the Python shell:
It’s very important always define an ordering to a QuerySet you are going to paginate! Otherwise, it can give you
inconsistent results.
Now let’s import the Paginator utility:
Here we are telling Django to paginate our QuerySet in pages of 20 each. Now let’s explore some of the paginator
properties:
Here we have to pay attention because if we try to get a page that doesn’t exist, the Paginator will throw an
exception:
Or if we try to pass an arbitrary parameter, which is not a page number:
We have to keep those details in mind when designing the user interface.
Now let’s explore the attributes and methods offered by the Page class a little bit:
FBV Pagination
Here is how we do it using regular function-based views:
Now the trick part is to render the pages correctly using the Bootstrap 4 pagination component. But take the time
to read the code and see if it makes sense for you. We are using here all the methods we played with before. And here
in that context, topics is no longer a QuerySet but a paginator.Page instance.
Right after the topics HTML table, we can render the pagination component:
While using pagination with class-based views, the way we interact with the paginator in the template is a little bit
different. It will make available the following variables in the template: paginator, page_obj,
is_paginated, object_list, and also a variable with the name we defined in the context_object_name. In our
case this extra variable will be named topics, and it will be equivalent to object_list.
Now about the whole get_context_data thing, well, that’s how we add stuff to the request context when extending a
GCBV.
But the main point here is the paginate_by attribute. In some cases, just by adding it will be enough.
Now we grab that pagination HTML snippet from the topics.html template, and create a new file named
pagination.html inside the templates/includes folder, alongside with the forms.html file:
Just for testing purpose, you could just add a few posts (or create some using the Python Shell) and change the
paginate_by to a low number, say 2, and see how it’s looking like:
Here we are dealing with user input, so we must take care. When using the markdown function, we are instructing it
to escape the special characters first and then parse the markdown tags. After that, we mark the output string as safe
to be used in the template.
Now in the templates topic_posts.html and reply_topic.html just change from:
To:
From now on the users can already use markdown in the posts:
Markdown Editor
We can also add a very cool Markdown editor called SimpleMD.
Either download the JavaScript library or use their CDN:
Now edit the base.html to make space for extra JavaScripts:
Maybe you have already noticed, but there’s a small issue when someone replies to a post. It’s not updating the
last_update field, so the ordering of the topics is broken right now.
Let’s fix it:
boards/views.py
Next thing we want to do is try to control the view counting system a little bit more. We don’t want to the same user
refreshing the page counting as multiple views. For this we can use sessions:
boards/views.py
Now we could provide a better navigation in the topics listing. Currently the only option is for the user to click
in the topic title and go to the first page. We could workout something like this:
boards/models.py
Then in the topics.html template we could implement something like this:
templates/topics.html
Like a tiny pagination for each topic. Note that I also took the time to add the table-striped class for a better
styling of the table.
In the reply page, we are currently listing all topic replies. We could limit it to just the last ten posts.
boards/models.py
templates/reply_topic.html
Another thing is that when the user replies to a post, we are redirecting the user to the first page again. We could
improve it by sending the user to the last page.
We can add an id to the post card:
templates/topic_posts.html
The important bit here is the <div id="{{ post.pk }}" ...>.
Then we can play with it like this in the views:
boards/views.py
In the topic_post_url we are building a URL with the last page and adding an anchor to the element with id equals
to the post ID.
With this, it will required us to update the following test case:
boards/tests/test_view_reply_topic.py
Next issue, as you can see in the previous screenshot, is to solve the problem with the pagination when the number
of pages is too high.
The easiest way is to tweak the pagination.html template:
templates/includes/pagination.html
Conclusions
With this tutorial, we finalized the implementation of our Django board application. I will probably release a follow-up
implementation tutorial to improve the code. There are many things we can explore together. For example, database
optimizations, improve the user interface, play with file uploads, create a moderation system, and so on.
The next tutorial will be focused on deployment. It’s going to be a complete guide on how to put your code in
production taking care of all the important details.
I hope you enjoyed the sixth part of this tutorial series! The last part is coming out next week, on Oct 16, 2017.
If you would like to get notified when the last part is out, you can subscribe to our mailing list.
The source code of the project is available on GitHub. The current state of the project can be found under the release
tag v0.6-lw. The link below will take you to the right place: