Views and Templates¶
Now we can create blog posts and see them in the admin interface, but no one else can see our blog posts yet.
The homepage test¶
Every site should have a homepage. Let’s write a failing test for that.
We can use the Django test client to create a test to make sure that our homepage returns an HTTP 200 status code (this is the standard response for a successful HTTP request).
Let’s add the following to our blog/tests.py
file:
class ProjectTests(TestCase):
def test_homepage(self):
response = self.client.get('/')
self.assertEqual(response.status_code, 200)
If we run our tests now this test should fail because we haven’t created a homepage yet.
Hint
There’s lots more information on the hypertext transfer protocol (HTTP) and its various status codes on Wikipedia. Quick reference, 200 = OK; 404 = Not Found; 500 = Server Error
Base template and static files¶
Let’s start with base templates based on zurb foundation. First download and extract the Zurb Foundation files (direct link).
Zurb Foundation is a CSS, HTML and JavaScript framework for building the front-end of web sites. Rather than attempt to design a web site entirely from scratch, Foundation gives a good starting place on which to design and build an attractive, standards-compliant web site that works well across devices such as laptops, tablets and phones.
Static files¶
Create a static
directory in our top-level directory (the one with the manage.py
file). Copy the css
directory from the foundation archive to this new static
directory.
Now let’s add this new static
directory to our myblog/settings.py
file:
STATICFILES_DIRS = (
os.path.join(BASE_DIR, 'static'),
)
For more details, see Django’s documentation on static files.
Important
This workshop is focused on Python and Django and so out of necessity we are going to gloss over explaining HTML, CSS and JavaScript a little bit. However, virtually all websites have a front-end built with these fundamental building blocks of the open web.
Template files¶
Templates are a way to dynamically generate a number of documents which are similar but have some data that is slightly different. In the blogging system we are building, we want all of our blog posts to look visually similar but the actual text of a given blog post varies. We will have a single template for what all of our blog posts and the template will contain variables that get replaced when a blog post is rendered. This reuse that Django helps with and the concept of keeping things in a single place is called the DRY principle for Don’t Repeat Yourself.
Create a templates
directory in our top-level directory. Our directory structure should look like
├── blog
│ ├── __init__.py
│ ├── admin.py
│ ├── models.py
│ ├── tests.py
│ └── views.py
├── manage.py
├── myblog
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ ├── views.py
│ └── wsgi.py
├── myblog.sqlite3
├── static
│ └── css
│ ├── foundation.css
│ ├── foundation.min.css
│ └── normalize.css
└── templates
Create a basic HTML file like this and name it templates/index.html
:
{% load staticfiles %}
<!DOCTYPE html>
<html>
<head>
<title>Foundation 4</title>
<link rel="stylesheet" href="{% static "css/foundation.css" %}">
</head>
<body>
<section class="row">
<header class="large-12 columns">
<h1>Welcome to My Blog</h1>
<hr>
</header>
</section>
</body>
</html>
Now let’s add this new templates
directory to our myblog/settings.py
file:
TEMPLATE_DIRS = (
os.path.join(BASE_DIR, 'templates'),
)
For just about everything there is to know about Django templates, read the template documentation.
Tip
In our examples, the templates are going to be used to generate similar HTML pages. However, Django’s template system can be used to generate any type of plain text document such as CSS, JavaScript, CSV or XML.
Views¶
Now let’s create a homepage using the index.html
template we added.
Let’s start by creating a views file: myblog/views.py
referencing the index.html
template:
from django.views.generic.base import TemplateView
class HomeView(TemplateView):
template_name = 'index.html'
home = HomeView.as_view()
Important
We are making this views file in the myblog
project directory (next to the myblog/urls.py
file we are about to change). We are not changing the blog/views.py
file yet. We will use that file later.
Django will be able to find this template in the templates
folder because of our TEMPLATE_DIRS
setting.
Now we need to route the homepage URL to the home view. Our URL file myblog/urls.py
should look something like this:
from django.conf.urls import patterns, include, url
from myblog import views
from django.contrib import admin
admin.autodiscover()
urlpatterns = patterns('',
url(r'^$', views.home),
url(r'^admin/', include(admin.site.urls)),
)
Now let’s visit http://localhost:8000/ in a web browser to check our work. You should see a webpage that looks like this:

Great! Now let’s make sure our new test passes:
$ python manage.py test blog
Creating test database for alias 'default'...
..
----------------------------------------------------------------------
Ran 2 tests in 0.021s
OK
Destroying test database for alias 'default'...
Hint
From a code flow perspective, we now have a working example of how Django
creates dynamic web pages. When an HTTP request to a Django powered web
site is sent, the urls.py
file contains a series of patterns for
matching the URL of that web request. The matching URL delegates the
request to a corresponding view (or to a another set of URLs which map
the request to a view). Finally, the view delegates the request to a
template for rendering the actual HTML.
In web site architecture, this separation of concerns is variously known as a three-tier architecture or a model-view-controller architecture.
Using a base template¶
Templates in Django are generally built up from smaller pieces. This lets you include things like a consistent header and footer on all your pages. Convention is to call one of your templates base.html
and have everything inherit from that.
We’ll start with putting our header and a sidebar in templates/base.html
:
{% load staticfiles %}
<!DOCTYPE html>
<html>
<head>
<title>Foundation 4</title>
<link rel="stylesheet" href="{% static "css/foundation.css" %}">
</head>
<body>
<section class="row">
<header class="large-12 columns">
<h1>Welcome to My Blog</h1>
<hr>
</header>
</section>
<section class="row">
<div class="large-8 columns">
{% block content %}{% endblock %}
</div>
<div class="large-4 columns">
<h3>About Me</h3>
<p>I am a Python developer and I like Django.</p>
</div>
</section>
</body>
</html>
Note
We will not explain the CSS classes we used above (e.g. large-8
, column
, row
). More information on these classes can be found in the Zurb Foundation grid documentation.
There’s a lot of duplicate code between our templates/base.html
and
templates/index.html
. Django’s templates provide a way of having templates
inherit the structure of other templates. This allows a template to define
only a few elements, but retain the overall structure of its parent template.
If we update our index.html
template to extend base.html
we can see
this in action. Delete everything in templates/index.html
and replace it
with the following:
{% extends "base.html" %}
{% block content %}
Page body goes here.
{% endblock content %}
Now our templates/index.html
just overrides the content
block in
templates/base.html
. For more details on this powerful Django feature,
you can read the documentation on template inheritance.
ListViews¶
We put a hard-coded title and article in our filler view. These post details should come from our models and database instead. Let’s write a test for that.
The Django test client
can be used for a simple test of whether text shows up on a page. Let’s add the following to our blog/tests.py
file:
from django.contrib.auth import get_user_model
class ListPostsOnHomePage(TestCase):
"""Test whether our blog posts show up on the homepage"""
def setUp(self):
self.user = get_user_model().objects.create(username='some_user')
def test_one_post(self):
Post.objects.create(title='1-title', body='1-body', author=self.user)
response = self.client.get('/')
self.assertContains(response, '1-title')
self.assertContains(response, '1-body')
def test_two_posts(self):
Post.objects.create(title='1-title', body='1-body', author=self.user)
Post.objects.create(title='2-title', body='2-body', author=self.user)
response = self.client.get('/')
self.assertContains(response, '1-title')
self.assertContains(response, '1-body')
self.assertContains(response, '2-title')
which should fail like this
Creating test database for alias 'default'...
FF..
======================================================================
FAIL: test_one_post (blog.tests.ListPostsOnHomePage)
----------------------------------------------------------------------
Traceback (most recent call last):
...
AssertionError: Couldn't find '1-title' in response
======================================================================
FAIL: test_two_posts (blog.tests.ListPostsOnHomePage)
----------------------------------------------------------------------
Traceback (most recent call last):
...
AssertionError: Couldn't find '1-title' in response
----------------------------------------------------------------------
Ran 4 tests in 0.201s
FAILED (failures=2)
Destroying test database for alias 'default'...
Updating our views¶
One easy way to get all our posts objects to list is to just use a ListView
. That changes our HomeView
only slightly.
from django.views.generic import ListView
from blog.models import Post
class HomeView(ListView):
template_name = 'index.html'
queryset = Post.objects.order_by('-created_at')
home = HomeView.as_view()
Important
Make sure you update your HomeView
to inherit from ListView
. Remember this is still myblog/views.py
.
That small change will provide a post_list
object to our template index.html
which we can then loop over. For some quick documentation on all the Class Based Views in django, take a look at Classy Class Based Views
The last change needed then is just to update our homepage template to add the blog posts. Let’s replace our templates/index.html
file with the following:
{% extends "base.html" %}
{% block content %}
{% for post in post_list %}
<article>
<h2><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h2>
<p class="subheader">
<time>{{ post.modified_at|date }}</time>
</p>
<p></p>
{{ post.body|linebreaks }}
</article>
{% endfor %}
{% endblock content %}
Tip
Notice that we didn’t specify the name post_list
in our code. Django’s class-based generic views often add automatically-named variables to your template context based on your model names. In this particular case the context object name was automatically defined by the get_context_object_name method in the ListView
. Instead of referencing post_list
in our template we could have also referenced the template context variable object_list
instead.
Running the tests here we see that all the tests pass!
Note
Read the Django built-in template tags and filters documentation for more details on the linebreaks and date template filters.
And now, if we add some posts in our admin, they should show up on the homepage. What happens if there are no posts? We should add a test for that
def test_no_posts(self):
response = self.client.get('/')
self.assertContains(response, 'No blog post entries yet.')
And that gives us the expected failure
Creating test database for alias 'default'...
F....
======================================================================
FAIL: test_no_posts (blog.tests.ListPostsOnHomePage)
----------------------------------------------------------------------
Traceback (most recent call last):
...
AssertionError: Couldn't find 'No blog post entries yet' in response
----------------------------------------------------------------------
Ran 5 tests in 0.044s
FAILED (failures=1)
Destroying test database for alias 'default'...
The easiest way to add this is to use the empty clause. See if you can add this in yourself to make the test pass.
What about viewing an individual blog post?
Blog Post Details¶
To save a bit of time let’s make our urls look like http://myblog.com/post/ID/
where ID is the database ID of the blog post we want to see.
Before we create this page, let’s move the template content that displays our blog posts on our homepage into a separate template file so we can reuse it on our blog post details page.
Let’s make a file called templates/_post.html
and put the following in it:
<article>
<h2><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h2>
<p class="subheader">
<time>{{ post.modified_at|date }}</time>
</p>
<p></p>
{{ post.body|linebreaks }}
</article>
Note
The post.get_absolute_url
reference doesn’t do anything yet. Later we will add a get_absolute_url
method to the post model which will make these links work.
Tip
The filename of our includable template starts with _
by convention. This naming convention is recommended by Harris Lapiroff in An Architecture for Django Templates.
Now let’s change our homepage template (templates/index.html
) to include the template file we just made:
{% extends "base.html" %}
{% block content %}
{% for post in post_list %}
{% include "_post.html" with post=post only %}
{% empty %}
<p>No blog post entries yet.</p>
{% endfor %}
{% endblock content %}
Tip
We use the with post=post only
convention in our include
tag for better encapsulation (as mentioned in An Architecture for Django Templates). Check the Django documentation more information on the include tag.
Let’s write a test for that:
from django.contrib.auth import get_user_model
class BlogPostViewTest(TestCase):
def setUp(self):
self.user = get_user_model().objects.create(username='some_user')
self.post = Post.objects.create(title='1-title', body='1-body',
author=self.user)
def test_basic_view(self):
response = self.client.get(self.post.get_absolute_url())
self.assertEqual(response.status_code, 200)
This test fails beacuse we didn’t define get_absolute_url (Django Model Instance Documentation). We need to create a URL and a view for blog post pages now. We’ll need to create a blog/urls.py
file and reference it in the myblog/urls.py
file.
Our blog/urls.py
file is the very short
from django.conf.urls import patterns, url
urlpatterns = patterns('blog.views',
url(r'^post/(?P<pk>\d+)/$', 'post_details'),
)
The urlconf in myblog/urls.py
needs to reference blog.urls
:
url(r'^', include('blog.urls')),
Now we need to define a post_details
view in our blog/views.py
file:
from django.http import HttpResponse
def post_details(request, pk):
return HttpResponse('empty')
We’ll be updating this view later to return something useful.
Finally we need to create the get_absolute_url()
function which should return the post details URL for each posts. We should create a test first. Let’s add the following test to our PostModelTest
class:
def test_get_absolute_url(self):
user = get_user_model().objects.create(username='some_user')
post = Post.objects.create(title="My post title", author=user)
self.assertIsNotNone(post.get_absolute_url())
Now we need to implement get_absolute_url
in our Post
class (found in blog/models.py
):
from django.core.urlresolvers import reverse
# And in our Post model class...
def get_absolute_url(self):
return reverse('blog.views.post_details', kwargs={'pk': self.pk})
We should now have passing tests again.
Let’s make the blog post details page actually display a blog post. First we’ll write some tests in our BlogPostViewTest
class:
def test_blog_title_in_post(self):
response = self.client.get(self.post.get_absolute_url())
self.assertContains(response, self.post.title)
def test_blog_body_in_post(self):
response = self.client.get(self.post.get_absolute_url())
self.assertContains(response, self.post.body)
To implement our blog post page we’ll use another class-based generic view: the DetailView. The DetailView
is a view for displaying the details of an instance of a model and rendering it to a template. Let’s replace our blog/views.py
file with the following:
from django.views.generic import DetailView
from .models import Post
class PostDetails(DetailView):
model = Post
post_details = PostDetails.as_view()
Now we’ll see some TemplateDoesNotExist
errors when running our tests again:
$ python manage.py test blog
Creating test database for alias 'default'...
EEE......
======================================================================
ERROR: test_blog_body_in_post (blog.tests.BlogPostViewTest)
----------------------------------------------------------------------
...
TemplateDoesNotExist: blog/post_detail.html
======================================================================
ERROR: test_blog_title_in_post (blog.tests.BlogPostViewTest)
----------------------------------------------------------------------
...
TemplateDoesNotExist: blog/post_detail.html
----------------------------------------------------------------------
Ran 9 tests in 0.071s
FAILED (errors=3)
Destroying test database for alias 'default'...
These errors are telling us that we’re referencing a blog/post_detail.html
template but we haven’t created that file yet. Let’s create a templates/blog/post_detail.html
. The DetailView
should provide us with a post
context variable that we can use to reference our Post
model instance. Our template should look similar to this:
{% extends "base.html" %}
{% block content %}
{% include "_post.html" with post=post only %}
{% endblock %}
Now our tests should pass again:
$ python manage.py test blog
Creating test database for alias 'default'...
.......
----------------------------------------------------------------------
Ran 8 tests in 0.071s
OK
Destroying test database for alias 'default'...