Matthew Daly's Blog

I'm a web developer in Norfolk. This is my blog...

25th May 2014 5:23 pm

Django Blog Tutorial - the Next Generation - Part 6

Welcome back! In this tutorial we’ll cover the following:

  • Fixing bugs the TDD way
  • Setting up syntax highlighting for code snippets
  • Tidying up the front end

Apologies, but I’m holding over implementing the search and additional feeds for a future instalment - in the past I’ve tried to cover too much in one post and that has led to me putting them off for much too long. So this instalment and future ones are going to be shorter so I can get them out the door quicker.

Ready? Let’s get started!

Fixing bugs

When someone reports a bug, it’s tempting to just dive straight in and start fixing it. But TDD cautions us against this practice. If we have a bug, then our tests should have caught it. If they don’t, then before we fix the bug, we have to make sure we can catch it if it crops up in future by implementing a test for it, and ensuring that it fails. Once that test is in place, we can then go ahead and fix the bug, safe in the knowledge that if it should reappear in future, we will be warned when our tests run.

As it happens, we have a bug in our web app. If you activate your virtualenv in the usual way and run the development server, and then try to create a new post without adding a tag, you’ll see that it fails as the tag is empty. Now, it’s pretty obvious that this is because the tags attribute of the Post model cannot be blank, so we have a good idea of what we need to do to fix this. But to make sure it never occurs again, we need to implement a test first.

Add the following method to AdminTest:

def test_create_post_without_tag(self):
# Create the category
category = Category()
category.name = 'python'
category.description = 'The Python programming language'
category.save()
# Log in
self.client.login(username='bobsmith', password="password")
# Check response code
response = self.client.get('/admin/blogengine/post/add/')
self.assertEquals(response.status_code, 200)
# Create the new post
response = self.client.post('/admin/blogengine/post/add/', {
'title': 'My first post',
'text': 'This is my first post',
'pub_date_0': '2013-12-28',
'pub_date_1': '22:00:04',
'slug': 'my-first-post',
'site': '1',
'category': '1'
},
follow=True
)
self.assertEquals(response.status_code, 200)
# Check added successfully
self.assertTrue('added successfully' in response.content)
# Check new post now in database
all_posts = Post.objects.all()
self.assertEquals(len(all_posts), 1)

This is virtually identical to our previous method for adding a post, but doesn’t add a tag to the post. If you run python manage.py jenkins, the test should fail. Let’s commit our changes:

$ git add blogengine/tests.py
$ git commit -m 'Added failing test for posts without tags'

Now we’re in a position to fix our bug. Let’s take a look at our models:

class Post(models.Model):
title = models.CharField(max_length=200)
pub_date = models.DateTimeField()
text = models.TextField()
slug = models.SlugField(max_length=40, unique=True)
author = models.ForeignKey(User)
site = models.ForeignKey(Site)
category = models.ForeignKey(Category, blank=True, null=True)
tags = models.ManyToManyField(Tag)

If you compare category and tags, you’ll immediately see that category has the additional parameters blank and null both set to True. So that’s what we need to do for tags as well. Amend the model to look like this:

class Post(models.Model):
title = models.CharField(max_length=200)
pub_date = models.DateTimeField()
text = models.TextField()
slug = models.SlugField(max_length=40, unique=True)
author = models.ForeignKey(User)
site = models.ForeignKey(Site)
category = models.ForeignKey(Category, blank=True, null=True)
tags = models.ManyToManyField(Tag, blank=True, null=True)

You shouldn’t have to create a migration for this. Let’s run our tests:

$ python manage.py jenkins --coverage-html-report=htmlcov
Creating test database for alias 'default'...
.......................
----------------------------------------------------------------------
Ran 23 tests in 7.634s
OK
Destroying test database for alias 'default'...

Our tests pass! So we’ve fixed our bug, and we’ve ensured that if it happens again, we’ll catch it. With that done, it’s time to commit:

$ git add blogengine/models.py
$ git commit -m 'Fixed a bug with the Post model'

Remember: Always make the effort to create a test to reproduce your bug before fixing it. That way, you know you can catch it in future.

Syntax highlighting

This is one feature that not everyone will want to implement. If you want to be able to show code snippets on your blog, then implementing syntax highlighting is well worth your time. However, if that’s not what you want to use your blog for, feel free to skip over this section.

Now, earlier in the series we implemented Markdown support. In Markdown there are two main ways to denote a code block. One is to indent the code by four spaces, while the other is to use fenced code blocks with syntax highlighting. There are many flavours of Markdown available for Python, and unfortunately the one we’ve been using so far doesn’t support fenced code blocks, so we’ll be switching to one that does.

We also need to be able to generate a suitable stylesheet to highlight the code appropriately. For that, we’ll be using Pygments. So let’s first uninstall our existing implementation of Markdown:

$ pip uninstall Markdown

And install the new modules:

$ pip install markdown2 Pygments

Don’t forget to record the changes:

$ pip freeze > requirements.txt

Now, we need to amend our Markdown template tags to use the new version of Markdown:

import markdown2
from django import template
from django.template.defaultfilters import stringfilter
from django.utils.encoding import force_unicode
from django.utils.safestring import mark_safe
register = template.Library()
@register.filter(is_safe=True)
@stringfilter
def custom_markdown(value):
extras = ["fenced-code-blocks"]
return mark_safe(markdown2.markdown(force_unicode(value),
extras = extras))

All we do here is change the Markdown module that gets imported, and amend how it is called. Note that we pass through the parameter fenced-code-blocks to enable this functionality.

If you now run the development server and create a post with some code in it (just copy the Ruby example from here), then view it on the site, you should be able to see that it’s in a <code> block. However, it’s not highlighted yet. Let’s commit our changes:

$ git add requirements/txt blogengine/templatetags/custom_markdown.py
$ git commit -m 'Now use Markdown2 to allow for syntax highlighting'

Now, if you examine the markup for your code blocks using your browser’s developer tools, you’ll notice that the code is wrapped in many different spans with various classes. Pygments can generate a CSS file that uses those classes to highlight your code.

First, let’s create a folder to store our CSS files in:

$ mkdir blogengine/static/css

Next, we’ll add a blank CSS file for any other styling we may want to apply:

$ touch blogengine/static/css/main.css

Then, we generate our CSS file:

$ pygmentize -S default -f html > blogengine/static/css/code.css

We need to include our CSS files in our HTML template:

<!DOCTYPE html>
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>{% block title %}My Django Blog{% endblock %}</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Place favicon.ico and apple-touch-icon.png in the root directory -->
{% load staticfiles %}
<link rel="stylesheet" href="{% static 'bower_components/html5-boilerplate/css/normalize.css' %}">
<link rel="stylesheet" href="{% static 'bower_components/html5-boilerplate/css/main.css' %}">
<link rel="stylesheet" href="{% static 'bower_components/bootstrap/dist/css/bootstrap.min.css' %}">
<link rel="stylesheet" href="{% static 'bower_components/bootstrap/dist/css/bootstrap-theme.min.css' %}">
<link rel="stylesheet" href="{% static 'css/main.css' %}">
<link rel="stylesheet" href="{% static 'css/code.css' %}">
<script src="{% static 'bower_components/html5-boilerplate/js/vendor/modernizr-2.6.2.min.js' %}"></script>
</head>
<body>
<!--[if lt IE 7]>
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
<![endif]-->
<!-- Add your site or application content here -->
<div id="fb-root"></div>
<script>(function(d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/en_GB/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));</script>
<div class="navbar navbar-static-top navbar-inverse">
<div class="navbar-inner">
<div class="container">
<a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</a>
<a class="brand" href="/">My Django Blog</a>
<div class="nav-collapse collapse">
</div>
</div>
</div>
</div>
<div class="container">
{% block header %}
<div class="page-header">
<h1>My Django Blog</h1>
</div>
{% endblock %}
<div class="row">
{% block content %}{% endblock %}
</div>
</div>
<div class="container footer">
<div class="row">
<div class="span12">
<p>Copyright &copy; {% now "Y" %}</p>
</div>
</div>
</div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script>window.jQuery || document.write('<script src="{% static 'bower_components/html5-boilerplate/js/vendor/jquery-1.10.2.min.js' %}"><\/script>')</script>
<script src="{% static 'bower_components/html5-boilerplate/js/plugins.js' %}"></script>
<script src="{% static 'bower_components/bootstrap/dist/js/bootstrap.min.js' %}"></script>
<!-- Google Analytics: change UA-XXXXX-X to be your site's ID. -->
<script>
(function(b,o,i,l,e,r){b.GoogleAnalyticsObject=l;b[l]||(b[l]=
function(){(b[l].q=b[l].q||[]).push(arguments)});b[l].l=+new Date;
e=o.createElement(i);r=o.getElementsByTagName(i)[0];
e.src='//www.google-analytics.com/analytics.js';
r.parentNode.insertBefore(e,r)}(window,document,'script','ga'));
ga('create','UA-XXXXX-X');ga('send','pageview');
</script>
</body>
</html>

Now, if you run the development server and reload the page, your code will be highlighted using the default Pygments style. If you don’t like it, there are plenty to choose from. Run the following command:

$ pygmentize -L styles

That will list the various styles available. For instance, let’s say we want to try the Tango style:

$ pygmentize -S tango -f html > blogengine/static/css/code.css

If you like the Monokai theme in Sublime Text, there’s a Pygments version of that:

$ pygmentize -S monokai -f html > blogengine/static/css/code.css

If you like the Solarized theme, that’s not bundled with Pygments, but can be installed separately:

$ pip install pygments-style-solarized

Then run this for the light version:

$ pygmentize -S solarizedlight -f html > blogengine/static/css/code.css

And this for the dark version:

$ pygmentize -S solarizeddark -f html > blogengine/static/css/code.css

Pick one that you like - I’m going to go for the dark version of Solarized.

Note that this doesn’t actually change the background colour of the code blocks. You will therefore need to set this manually using the CSS file we created earlier. If you’re using Solarized Dark like I am, then this should set it correctly:

div.codehilite pre {
background-color: #002b36;
}

If you’re using Solarized Light, then this should be more appropriate:

div.codehilite pre {
background-color: #fdf6e3;
}

Or, if you’re using Monokai, black will do:

div.codehilite pre {
background-color: #000000;
}

With that done, let’s record the additional Pygments style:

$ pip freeze > requirements.txt

And commit our changes:

$ git add requirements.txt blogengine/static/css/ templates/blogengine/includes/base.html
$ git commit -m 'Styled code with Solarized Dark'

Let’s run our tests:

$ python manage.py jenkins
Creating test database for alias 'default'...
E
======================================================================
ERROR: blogengine.tests (unittest.loader.ModuleImportFailure)
----------------------------------------------------------------------
ImportError: Failed to import test module: blogengine.tests
Traceback (most recent call last):
File "/usr/local/Cellar/python/2.7.6_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/unittest/loader.py", line 254, in _find_tests
module = self._get_module_from_name(name)
File "/usr/local/Cellar/python/2.7.6_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/unittest/loader.py", line 232, in _get_module_from_name
__import__(name)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 7, in <module>
import markdown
ImportError: No module named markdown
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (errors=1)
Destroying test database for alias 'default'...

Whoops! We introduced an error here. If we take a look, we can see that the problem is on line 7, where we import the Markdown module. That makes sense, as we now use a different implementation of Markdown. Fortunately, in Python you can import modules with a different name, which makes this a breeze to fix. Change the line to:

import markdown2 as markdown

Now, if you run your tests, they should pass. It’s important that if a test breaks, you fix it as soon as possible and don’t put it off. Let’s commit these changes:

$ git add blogengine/tests.py
$ git commit -m 'Fixed a broken test'

Our syntax highlighting is now done! If you want to see it in action using the Solarized Dark theme, check out the copy of this blogging engine hosted here.

Tidying up

Now that our blog is up and running on Heroku, it could do with a bit of work on the front end to make it look a bit nicer. If you recall, we’re using Bootstrap for our front end, so you may want to refer to the documentation for that to give you some ideas on how you want to style your blog.

Bootstrap has a nifty pager class for Next and Previous links, so let’s apply that to our post list template:

<ul class="pager">
{% if page_obj.has_previous %}
<li class="previous"><a href="/{{ page_obj.previous_page_number }}/">Previous Page</a></li>
{% endif %}
{% if page_obj.has_next %}
<li class="next"><a href="/{{ page_obj.next_page_number }}/">Next Page</a></li>
{% endif %}
</ul>

Let’s also add labels to our categories and tags. We’ll also place our posts and other content inside proper columns:

{% extends "blogengine/includes/base.html" %}
{% load custom_markdown %}
{% block content %}
{% if object_list %}
{% for post in object_list %}
<div class="post col-md-12">
<h1><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h1>
<h3>{{ post.pub_date }}</h3>
{{ post.text|custom_markdown }}
</div>
{% if post.category %}
<div class="col-md-12">
<a href="{{ post.category.get_absolute_url }}"><span class="label label-primary">{{ post.category.name }}</span></a>
</div>
{% endif %}
{% if post.tags %}
<div class="col-md-12">
{% for tag in post.tags.all %}
<a href="{{ tag.get_absolute_url }}"><span class="label label-success">{{ tag.name }}</span></a>
{% endfor %}
</div>
{% endif %}
{% endfor %}
{% else %}
<p>No posts found</p>
{% endif %}
<ul class="pager">
{% if page_obj.has_previous %}
<li class="previous"><a href="/{{ page_obj.previous_page_number }}/">Previous Page</a></li>
{% endif %}
{% if page_obj.has_next %}
<li class="next"><a href="/{{ page_obj.next_page_number }}/">Next Page</a></li>
{% endif %}
</ul>
{% endblock %}

We’ll also want to tidy up the layout for our individual post pages:

{% extends "blogengine/includes/base.html" %}
{% load custom_markdown %}
{% block content %}
<div class="post col-md-12">
<h1>{{ object.title }}</h1>
<h3>{{ object.pub_date }}</h3>
{{ object.text|custom_markdown }}
</div>
{% if object.category %}
<div class="col-md-12">
<a href="{{ object.category.get_absolute_url }}"><span class="label label-primary">{{ object.category.name }}</span></a>
</div>
{% endif %}
{% if object.tags %}
<div class="col-md-12">
{% for tag in object.tags.all %}
<a href="{{ tag.get_absolute_url }}"><span class="label label-success">{{ tag.name }}</span></a>
{% endfor %}
</div>
{% endif %}
<div class="col-md-12">
<h4>Comments</h4>
<div class="fb-comments" data-href="http://{{ object.site }}{{ object.get_absolute_url }}" data-width="470" data-num-posts="10"></div>
</div>
</div>
{% endblock %}

Time to commit our changes:

$ git add templates/blogengine/
$ git commit -m 'Tidied up post templates'

With that done, let’s turn our attention to our base template. We’ll amend the header to collapse down at smaller screen widths. This is easy to do with Bootstrap:

<!DOCTYPE html>
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>{% block title %}My Django Blog{% endblock %}</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="alternate" type="application/rss+xml" title="Blog posts" href="/feeds/posts/" >
<!-- Place favicon.ico and apple-touch-icon.png in the root directory -->
{% load staticfiles %}
<link rel="stylesheet" href="{% static 'bower_components/html5-boilerplate/css/normalize.css' %}">
<link rel="stylesheet" href="{% static 'bower_components/html5-boilerplate/css/main.css' %}">
<link rel="stylesheet" href="{% static 'bower_components/bootstrap/dist/css/bootstrap.min.css' %}">
<link rel="stylesheet" href="{% static 'bower_components/bootstrap/dist/css/bootstrap-theme.min.css' %}">
<link rel="stylesheet" href="{% static 'css/main.css' %}">
<link rel="stylesheet" href="{% static 'css/code.css' %}">
<script src="{% static 'bower_components/html5-boilerplate/js/vendor/modernizr-2.6.2.min.js' %}"></script>
</head>
<body>
<!--[if lt IE 7]>
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
<![endif]-->
<!-- Add your site or application content here -->
<div id="fb-root"></div>
<script>(function(d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/en_GB/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));</script>
<div class="navbar navbar-static-top navbar-inverse">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#header-nav">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">My Django Blog</a>
</div>
<div class="collapse navbar-collapse" id="header-nav">
<ul class="nav navbar-nav">
{% load flatpages %}
{% get_flatpages as flatpages %}
{% for flatpage in flatpages %}
<li><a href="{{ flatpage.url }}">{{ flatpage.title }}</a></li>
{% endfor %}
<li><a href="/feeds/posts/">RSS feed</a></li>
</ul>
</div>
</div>
</div>
<div class="container">
{% block header %}
<div class="page-header">
<h1>My Django Blog</h1>
</div>
{% endblock %}
<div class="row">
{% block content %}{% endblock %}
</div>
</div>
<div class="container footer">
<div class="row">
<div class="span12">
<p>Copyright &copy; {% now "Y" %}</p>
</div>
</div>
</div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script>window.jQuery || document.write('<script src="{% static 'bower_components/html5-boilerplate/js/vendor/jquery-1.10.2.min.js' %}"><\/script>')</script>
<script src="{% static 'bower_components/html5-boilerplate/js/plugins.js' %}"></script>
<script src="{% static 'bower_components/bootstrap/dist/js/bootstrap.min.js' %}"></script>
<!-- Google Analytics: change UA-XXXXX-X to be your site's ID. -->
<script>
(function(b,o,i,l,e,r){b.GoogleAnalyticsObject=l;b[l]||(b[l]=
function(){(b[l].q=b[l].q||[]).push(arguments)});b[l].l=+new Date;
e=o.createElement(i);r=o.getElementsByTagName(i)[0];
e.src='//www.google-analytics.com/analytics.js';
r.parentNode.insertBefore(e,r)}(window,document,'script','ga'));
ga('create','UA-XXXXX-X');ga('send','pageview');
</script>
</body>
</html>

Here we’ve also added a link to our RSS feed in the header, and another in the page head to facilitate working with browsers that support RSS better.

Our blog’s now looking much more presentable. All that remains is to commit it:

$ git add templates/blogengine/includes/base.html
$ git commit -m 'Amended base template'

Now we can push it to GitHub and deploy it on Heroku:

$ git push origin master
$ git push heroku master

And we’re done! Don’t forget, you can grab this lesson with git checkout lesson-6.

Next time, we’ll cover search (I promise!).

24th May 2014 8:15 pm

Django Blog Tutorial - the Next Generation - Part 5

Hello again! I was originally planning to cover implementing a search system, adding more feeds, and tidying up the front end in this instalment. However, I felt it was time for a change of pace, so instead, we’re going to look at:

  • Checking code coverage and getting it to 100%
  • Using continuous integration
  • Deploying to Heroku

Don’t worry, the original lesson isn’t going anywhere. We’ll still be implementing all of that later on, but today is the day we get your Django blog up and running on the web. That way, you can get a better idea of how Django works in the wild, and you have something concrete to show for your efforts.

Continuous integration

If you’re not familiar with continuous integration, it’s basically a process that carries out some tasks automatically when you push a new commit to the repository. These tasks may include running unit tests, linting your code, and checking it to see what percentage of the code base is covered by the tests (after all, if your tests don’t actually cover every scenario, then there’s more of a chance that something might slip through the net. It’s also possible to implement hooks to automatically deploy your application only if the tests pass.

Typically, you will have a continuous integration server running somewhere that regularly polls your Git repository for changes, and when it finds a new commit, will check it out and run the tests (or whatever other task you configure it to). One of the most popular continuous integration servers around is Jenkins - I use it at work and can highly recommend it. However, we aren’t going to cover using Jenkins here because setting it up is quite a big deal and it really is best kept on a server of its own (although feel free to use it if you prefer). Instead, we’re going to use Travis CI, which integrates nicely with GitHub and is free for open source projects. If you don’t mind your code being publicly available on GitHub, then Travis is a really great way to dip your toe into continuous integration.

NB: You don’t have to use continuous integration at all if you don’t want to - this isn’t a big project so you don’t really need it. If you don’t want to put your code on GitHub, then feel free to just follow along and not bother pushing your code to GitHub and configuring Travis.

Code coverage

As mentioned above, code coverage is a measure of the percentage of the code that is covered by tests. While not infallible, it’s a fairly good guide to how comprehensive your tests are. If you have 100% code coverage, you can be fairly confident that your tests are comprehensive enough to catch most errors, so it’s a good rule of thumb to aim for 100% test coverage on a project.

So how do we check our test coverage? The coverage Python module is the most common tool for this. There’s also a handy Django module called django-jenkins, which is designed to work with Jenkins, but can be used with any continuous integration server, that can not only run your tests, but also check code coverage at the same time. So, make sure your virtualenv is up and running:

$ source venv/bin/activate

Then, run the following command:

$ pip install coverage django-jenkins

Once that’s done, add these to our requirements file:

$ pip freeze > requirements.txt

We now need to configure our Django project to use django-jenkins. Add the following to the bottom of the settings file:

INSTALLED_APPS += ('django_jenkins',)
JENKINS_TASKS = (
'django_jenkins.tasks.run_pylint',
'django_jenkins.tasks.with_coverage',
)
PROJECT_APPS = ['blogengine']

This adds django-jenkins to our installed apps and tells it to include two additional tasks, besides running the tests. The first task runs Pylint to check our code quality (but we aren’t really concerned about that at this point). The second checks the coverage. Finally, we tell django-jenkins that the blogengine app is the only one to be tested.

You’ll also want to add the following lines to your .gitignore:

reports/
htmlcov/

These are the reports generated by django-jenkins, and should not be kept under version control. With that done, it’s time to commit:

$ git add .gitignore django_tutorial_blog_ng/ requirements.txt
$ git commit -m 'Added coverage checking using django-jenkins'

Now, let’s run our tests. From now on, you’ll use the following command to run your tests:

$ python manage.py jenkins

This ensures we check the coverage at the same time. Now, you’ll notice that the reports folder has been created, and it will contain three files, including one called coverage.xml. However, XML isn’t a very friendly format. Happily, we can easily generate reports in HTML instead:

$ python manage.py jenkins --coverage-html-report=htmlcov

Running this command will create another folder called htmlcov/, and in here you will find your report, nicely formatted as HTML. Open up index.html in your web browser and you should see a file-by-file breakdown of your code coverage. Nice, huh?

Now, if your code so far is largely identical to mine, you’ll notice that the model and view files don’t have 100% coverage yet. If you click on each one, you’ll see a handy line-by-line breakdown of the test coverage for each file. You’ll notice that in the views file, the areas of the code for when tags and categories don’t exist are highlighted in pink - this tells you that these lines of code are never executed during the tests. So let’s fix that.

First, our template needs to be able to handle empty lists.

{% extends "blogengine/includes/base.html" %}
{% load custom_markdown %}
{% block content %}
{% if object_list %}
{% for post in object_list %}
<div class="post">
<h1><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h1>
<h3>{{ post.pub_date }}</h3>
{{ post.text|custom_markdown }}
</div>
<a href="{{ post.category.get_absolute_url }}">{{ post.category.name }}</a>
{% for tag in post.tags.all %}
<a href="{{ tag.get_absolute_url }}">{{ tag.name }}</a>
{% endfor %}
{% endfor %}
{% else %}
<p>No posts found</p>
{% endif %}
{% if page_obj.has_previous %}
<a href="/{{ page_obj.previous_page_number }}/">Previous Page</a>
{% endif %}
{% if page_obj.has_next %}
<a href="/{{ page_obj.next_page_number }}/">Next Page</a>
{% endif %}
{% endblock %}

Let’s commit the changes:

$ git add templates/blogengine/post_list.html
$ git commit -m 'Added a "No posts found" message to post list template'

Next, we need to write tests to check that we get a “No posts found” message when we view a tag or category that does not exist. Add the following methods to the class PostViewTest():

def test_nonexistent_category_page(self):
category_url = '/category/blah/'
response = self.client.get(category_url)
self.assertEquals(response.status_code, 200)
self.assertTrue('No posts found' in response.content)
def test_nonexistent_tag_page(self):
tag_url = '/tag/blah/'
response = self.client.get(tag_url)
self.assertEquals(response.status_code, 200)
self.assertTrue('No posts found' in response.content)

Now, let’s run our tests again:

$ python manage.py jenkins --coverage-html-report=htmlcov

Assuming they all pass as expected, then your coverage reports will be regenerated. If you reload the coverage report, you should see that your views now have 100% test coverage. Let’s commit again:

$ git add blogengine/tests.py
$ git commit -m 'Views file now has 100% coverage'

Now, our models still don’t have 100% coverage yet. If you look at the breakdown for models.py, you’ll see that the line if not self.slug: has only partial coverage, because we missed out setting a slug for the categories and tags in our test. So, let’s fix that. In PostTest(), amend the test_create_category() and test_create_tag() methods as follows:

def test_create_category(self):
# Create the category
category = Category()
# Add attributes
category.name = 'python'
category.description = 'The Python programming language'
category.slug = 'python'
# Save it
category.save()
# Check we can find it
all_categories = Category.objects.all()
self.assertEquals(len(all_categories), 1)
only_category = all_categories[0]
self.assertEquals(only_category, category)
# Check attributes
self.assertEquals(only_category.name, 'python')
self.assertEquals(only_category.description, 'The Python programming language')
self.assertEquals(only_category.slug, 'python')
def test_create_tag(self):
# Create the tag
tag = Tag()
# Add attributes
tag.name = 'python'
tag.description = 'The Python programming language'
tag.slug = 'python'
# Save it
tag.save()
# Check we can find it
all_tags = Tag.objects.all()
self.assertEquals(len(all_tags), 1)
only_tag = all_tags[0]
self.assertEquals(only_tag, tag)
# Check attributes
self.assertEquals(only_tag.name, 'python')
self.assertEquals(only_tag.description, 'The Python programming language')
self.assertEquals(only_tag.slug, 'python')

Run the tests again:

$ python manage.py jenkins --coverage-html-report=htmlcov

Then refresh your coverage page, and we should have hit 100% coverage. Excellent news! This means that we can be confident that if any problems get introduced in future, we can pick them up easily. To demonstrate this, let’s upgrade our Django install to the latest version and check everything still works as expected:

$ pip install Django --upgrade

This will upgrade the copy of Django in our virtualenv to the latest version. Then we can run our tests again:

$ python manage.py jenkins --coverage-html-report=htmlcov

Our tests should still pass, indicating that the upgrade to our Django version does not appear to have broken any functionality. Let’s update our requirements.txt:

$ pip freeze > requirements.txt
$ git add requirements.txt
$ git commit -m 'Upgraded Django version'

Preparing our web app for deployment to Heroku

As mentioned previously, I’m going to assume you plan to deploy your site on Heroku. It has good Django support, and you can quite happily host a blog there using their free tariff. If you’d prefer to use another hosting provider, then you should be able to adapt these instructions accordingly.

Now, so far we’ve used SQLite as our database for development purposes. However, SQLite isn’t really suitable for production purposes. Heroku provide a Postgresql database for each web app, so we will use that. To configure it, open up settings.py and amend the database configuration section to look like this:

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.',
'NAME': '',
}
}

Then, add the following at the end of the file:

# Heroku config
# Parse database configuration from $DATABASE_URL
import dj_database_url
DATABASES['default'] = dj_database_url.config(default="sqlite:///db.sqlite3")
# Honor the 'X-Forwarded-Proto' header for request.is_secure()
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# Allow all host headers
ALLOWED_HOSTS = ['*']
# Static asset configuration
STATIC_ROOT = 'staticfiles'
STATICFILES_DIRS = (
os.path.join(BASE_DIR, 'static'),
)

A little explanation is called for. Remember when we first started out, we installed the django-toolbelt package, which included dj-database-url? Well, here we use the dj_database_url module to get the database from an environment variable set on Heroku. We set a default value so as to fall back to SQLite when that variable is not set. The other settings are required by Heroku.

You may want to run your tests again to ensure everything is still working before committing:

$ git add django_tutorial_blog_ng/settings.py
$ git commit -m 'Amended settings to work on desktop and Heroku'

Setting up Continuous Integration and coverage

Now, as mentioned previously, I’ll be demonstrating how to set up Travis CI for our project. Travis CI is only free for open-source projects, so if you don’t want to make your code publicly accessible, you may want to use Jenkins instead. I’ll leave setting that up and running it as an exercise for the reader, but I recommend a plugin called Cobertura which allows you to publish details of your code’s test coverage.

Unfortunately, Travis CI doesn’t have the capability to publish your coverage results. Fortunately, you can use Coveralls.io to pick up the slack. Like Travis, it’s free for open-source projects, and if you host your code on GitHub, it’s pretty easy to use.

You can find instructions on setting up a project on Travis CI here. Once you’ve configured it, you’ll need to set up your .travis.yml file. This is a simple text file that tells Travis CI how to run your tests. Put the following content in the file:

language: python
python:
- "2.7"
# command to install dependencies
install: "pip install -r requirements.txt"
# command to run tests
script: coverage run --include="blogengine/*" --omit="blogengine/migrations/*" manage.py test blogengine
after_success:
coveralls

Now, this tells Travis that this is a Python application, and we should be using Python 2.7. Please note you can test against multiple versions of Python if you wish - just add another line with the Python version you want to test against.

Then, we see that we use Pip to install our requirements. Finally we run our tests with Coverage, in order to generate coverage data, and afterwards call coveralls to pass the coverage data to Coveralls.io.

We also need to install the coveralls module:

$ pip install coveralls
$ pip freeze > requirements.txt

And we need to keep our coverage data out of version control:

env/
*.pyc
db.sqlite3
blogengine/static/bower_components/
reports/
htmlcov/
.coverage

You’ll also want to set up your project on Coveralls, as described here - please note that this requires your project be in a public GitHub repository.

With that done, let’s commit our changes:

$ git add .gitignore .travis.yml requirements.txt
$ git commit -m 'Added Travis config file and Coveralls support'

With that done, assuming you have Travis CI and Coveralls configured, and your code is already hosted on GitHub, then you should be able to just push your code up to trigger the build:

$ git push origin master

If you keep an eye on Travis in your browser, you can watch what happens as your tests are run. If for any reason the build fails, then it shouldn’t be too hard to figure out what has gone wrong using the documentation for Travis and Coveralls - both services are pretty easy to use.

Congratulations - you’re now using Continuous Integration! That wasn’t so hard, was it? Now, every time you push to GitHub, Travis will run your tests, and you’ll get an email if they fail, giving you early warning of any problems, and you can check your coverage at the same time. Both Travis and Coveralls offer badges that you can place in your README file on GitHub to show off your coverage and build status - feel free to add these to your repo.

You may want to try making a change that breaks your tests and committing it, then pushing it up, so that you can see what happens when the build breaks.

Deploying to Heroku

Our final task today is deploying our blog to Heroku so we can see it in action. First of all, if you don’t already have an account with Heroku, you’ll need to sign up here. You should also be prompted to install the Heroku toolbelt. Once that’s done, run the following command:

$ heroku login

You’ll be prompted for your credentials - enter these and you should be able to log in successfully.

Now, in order to run our Django app on Heroku, we’ll need to add a Procfile to tell Heroku what command to run in order to start your app. In this case, we will be using Gunicorn as our web server. Assuming our project is called django_tutorial_blog_ng, this is what you need to put in this file:

web: gunicorn django_tutorial_blog_ng.wsgi

That tells Heroku that the file we need to run for this application is django_tutorial_blog_ng/wsgi.py. To test it, run the following command:

$ foreman start

That will start our web server on port 5000, using Gunicorn rather than the Django development server. You should be able to see it in action here, but you’ll notice a very serious issue - namely that the static files aren’t being served. Now, Django has a command called collectstatic that collects all the static files and drops them into one convenient folder. Heroku will run this command automatically, so we need to ensure our static files are available. Amend your .gitignore file so it no longer excludes our static files:

venv/
*.pyc
db.sqlite3
reports/
htmlcov/
.coverage

We also need to amend our wsgi.py to serve static files:

import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_tutorial_blog_ng.settings")
from django.core.wsgi import get_wsgi_application
from dj_static import Cling
application = Cling(get_wsgi_application())

This should solve our problem. Let’s commit our changes:

$ git add django_tutorial_blog_ng/wsgi.py Procfile blogengine/static/ .gitignore
$ git commit -m 'Configured app for deployment on Heroku'

Now we’re ready to deploy our app!

Deployment

Every Heroku app needs a unique name. If you don’t specify one, then Heroku will generate one for you. Your app will have the domain name appname.herokuapp.com - however, if you already have a domain name lined up for your blog, you can point that domain name at the Heroku app if you wish. I’m going to deploy mine with the name blog-shellshocked-info.

You also need to consider where you want to deploy it. Heroku has two regions - North America and EU. By default it will deploy to North America, but as I’m in the EU, that’s where I want to deploy my app to - obviously if you’re in the Americas, you may be better off sticking with North America.

So, let’s create our app. Here’s the command you need to run:

$ heroku apps:create blog-shellshocked-info --region eu

You will want to change the app name, or remove it entirely if you’re happy for Heroku to generate one for you. If you want to host it in North America, drop the --region eu section.

Once completed, this will have created your new app, and added a Git remote for it, but will not have deployed it. To deploy your app, run this command:

$ git push heroku master

That will push your code up to Heroku. Please note that building the app may take a little while. Once it’s done, you can run heroku open to open it in your web browser. You should see an error message stating that the relation blogengine_post does not exist. That’s because we need to create our database structure. Heroku allows you to easily run commands on your app with heroku run, so let’s create our database and run our migrations:

$ heroku run python manage.py syncdb
$ heroku run python manage.py migrate

These are exactly the same commands you would run locally to create your database, but prefaced with heroku run so that they get run by Heroku. As usual, you will be prompted to create a superuser - you’ll want to do this so you can log into the admin.

That’s all for today! We’ve finally got our site up and running on Heroku, and set up continuous integration so our tests will get run for us. You can see an example of the site working here. As usual, you can check out the latest version of the code with git checkout lesson-5. If you’d like a homework assignment, then take a look at automating deployment to Heroku on successful builds and see if you can get it set up successfully.

Next time around, we’ll get back to implementing our search and tidying up the front end. See you then!

15th February 2014 5:45 pm

Django Blog Tutorial - the Next Generation - Part 4

Hello again! As promised, in this instalment we’ll implement categories and tags, as well as an RSS feed.

As usual, we need to switch into our virtualenv:

$ source venv/bin/activate

Categories

It’s worth taking a little time at this point to set out what we mean by categories and tags in this case, as the two can be very similar. In this case, we’ll use the following criteria:

  • A post can have only one category, or none, but a category can be applied to any number of posts
  • A post can have any number of tags, and a tag can be applied to any number of posts

If you’re not too familiar with relational database theory, the significance of this may not be apparent, so here’s a quick explanation. Because the categories are limited to one per post, the relationship between a post and a category is known as one-to-many. In other words, one post can only have one category, but one category can have many posts. You can therefore define the categories in one table in your database, and refer to them by their ID (the reference to the category in the post table is referred to as a foreign key).

As usual, we will write a test first. Open up blogengine/tests.py and edit the line importing the Post model as follows:

from blogengine.models import Post, Category

Also, add the following method before test_create_post:

def test_create_category(self):
# Create the category
category = Category()
# Add attributes
category.name = 'python'
category.description = 'The Python programming language'
# Save it
category.save()
# Check we can find it
all_categories = Category.objects.all()
self.assertEquals(len(all_categories), 1)
only_category = all_categories[0]
self.assertEquals(only_category, category)
# Check attributes
self.assertEquals(only_category.name, 'python')
self.assertEquals(only_category.description, 'The Python programming language')

This test checks that we can create categories. But categories aren’t much use unless we can actually apply them to our posts. So we need to edit our Post model as well, and to do so we need to have a test in place first. Edit the test_create_post method as follows:

def test_create_post(self):
# Create the category
category = Category()
category.name = 'python'
category.description = 'The Python programming language'
category.save()
# Create the author
author = User.objects.create_user('testuser', 'user@example.com', 'password')
author.save()
# Create the site
site = Site()
site.name = 'example.com'
site.domain = 'example.com'
site.save()
# Create the post
post = Post()
# Set the attributes
post.title = 'My first post'
post.text = 'This is my first blog post'
post.slug = 'my-first-post'
post.pub_date = timezone.now()
post.author = author
post.site = site
post.category = category
# Save it
post.save()
# Check we can find it
all_posts = Post.objects.all()
self.assertEquals(len(all_posts), 1)
only_post = all_posts[0]
self.assertEquals(only_post, post)
# Check attributes
self.assertEquals(only_post.title, 'My first post')
self.assertEquals(only_post.text, 'This is my first blog post')
self.assertEquals(only_post.slug, 'my-first-post')
self.assertEquals(only_post.site.name, 'example.com')
self.assertEquals(only_post.site.domain, 'example.com')
self.assertEquals(only_post.pub_date.day, post.pub_date.day)
self.assertEquals(only_post.pub_date.month, post.pub_date.month)
self.assertEquals(only_post.pub_date.year, post.pub_date.year)
self.assertEquals(only_post.pub_date.hour, post.pub_date.hour)
self.assertEquals(only_post.pub_date.minute, post.pub_date.minute)
self.assertEquals(only_post.pub_date.second, post.pub_date.second)
self.assertEquals(only_post.author.username, 'testuser')
self.assertEquals(only_post.author.email, 'user@example.com')
self.assertEquals(only_post.category.name, 'python')
self.assertEquals(only_post.category.description, 'The Python programming language')

What we’re doing here is adding a category attribute to the posts. This attribute contains a reference to a Category object.

Now, we also want to test adding, editing, and deleting a category from the admin interface. Add this code to the AdminTest class:

def test_create_category(self):
# Log in
self.client.login(username='bobsmith', password="password")
# Check response code
response = self.client.get('/admin/blogengine/category/add/')
self.assertEquals(response.status_code, 200)
# Create the new category
response = self.client.post('/admin/blogengine/category/add/', {
'name': 'python',
'description': 'The Python programming language'
},
follow=True
)
self.assertEquals(response.status_code, 200)
# Check added successfully
self.assertTrue('added successfully' in response.content)
# Check new category now in database
all_categories = Category.objects.all()
self.assertEquals(len(all_categories), 1)
def test_edit_category(self):
# Create the category
category = Category()
category.name = 'python'
category.description = 'The Python programming language'
category.save()
# Log in
self.client.login(username='bobsmith', password="password")
# Edit the category
response = self.client.post('/admin/blogengine/category/1/', {
'name': 'perl',
'description': 'The Perl programming language'
}, follow=True)
self.assertEquals(response.status_code, 200)
# Check changed successfully
self.assertTrue('changed successfully' in response.content)
# Check category amended
all_categories = Category.objects.all()
self.assertEquals(len(all_categories), 1)
only_category = all_categories[0]
self.assertEquals(only_category.name, 'perl')
self.assertEquals(only_category.description, 'The Perl programming language')
def test_delete_category(self):
# Create the category
category = Category()
category.name = 'python'
category.description = 'The Python programming language'
category.save()
# Log in
self.client.login(username='bobsmith', password="password")
# Delete the category
response = self.client.post('/admin/blogengine/category/1/delete/', {
'post': 'yes'
}, follow=True)
self.assertEquals(response.status_code, 200)
# Check deleted successfully
self.assertTrue('deleted successfully' in response.content)
# Check category deleted
all_categories = Category.objects.all()
self.assertEquals(len(all_categories), 0)

This is very similar to the prior code for the posts, and just checks we can create categories via the admin. We also need to check we can apply these categories to posts, and that they don’t break the existing tests:

def test_create_post(self):
# Create the category
category = Category()
category.name = 'python'
category.description = 'The Python programming language'
category.save()
# Log in
self.client.login(username='bobsmith', password="password")
# Check response code
response = self.client.get('/admin/blogengine/post/add/')
self.assertEquals(response.status_code, 200)
# Create the new post
response = self.client.post('/admin/blogengine/post/add/', {
'title': 'My first post',
'text': 'This is my first post',
'pub_date_0': '2013-12-28',
'pub_date_1': '22:00:04',
'slug': 'my-first-post',
'site': '1',
'category': '1'
},
follow=True
)
self.assertEquals(response.status_code, 200)
# Check added successfully
self.assertTrue('added successfully' in response.content)
# Check new post now in database
all_posts = Post.objects.all()
self.assertEquals(len(all_posts), 1)
def test_edit_post(self):
# Create the category
category = Category()
category.name = 'python'
category.description = 'The Python programming language'
category.save()
# Create the author
author = User.objects.create_user('testuser', 'user@example.com', 'password')
author.save()
# Create the site
site = Site()
site.name = 'example.com'
site.domain = 'example.com'
site.save()
# Create the post
post = Post()
post.title = 'My first post'
post.text = 'This is my first blog post'
post.slug = 'my-first-post'
post.pub_date = timezone.now()
post.author = author
post.site = site
post.save()
# Log in
self.client.login(username='bobsmith', password="password")
# Edit the post
response = self.client.post('/admin/blogengine/post/1/', {
'title': 'My second post',
'text': 'This is my second blog post',
'pub_date_0': '2013-12-28',
'pub_date_1': '22:00:04',
'slug': 'my-second-post',
'site': '1',
'category': '1'
},
follow=True
)
self.assertEquals(response.status_code, 200)
# Check changed successfully
self.assertTrue('changed successfully' in response.content)
# Check post amended
all_posts = Post.objects.all()
self.assertEquals(len(all_posts), 1)
only_post = all_posts[0]
self.assertEquals(only_post.title, 'My second post')
self.assertEquals(only_post.text, 'This is my second blog post')
def test_delete_post(self):
# Create the category
category = Category()
category.name = 'python'
category.description = 'The Python programming language'
category.save()
# Create the author
author = User.objects.create_user('testuser', 'user@example.com', 'password')
author.save()
# Create the site
site = Site()
site.name = 'example.com'
site.domain = 'example.com'
site.save()
# Create the post
post = Post()
post.title = 'My first post'
post.text = 'This is my first blog post'
post.slug = 'my-first-post'
post.pub_date = timezone.now()
post.site = site
post.author = author
post.category = category
post.save()
# Check new post saved
all_posts = Post.objects.all()
self.assertEquals(len(all_posts), 1)
# Log in
self.client.login(username='bobsmith', password="password")
# Delete the post
response = self.client.post('/admin/blogengine/post/1/delete/', {
'post': 'yes'
}, follow=True)
self.assertEquals(response.status_code, 200)
# Check deleted successfully
self.assertTrue('deleted successfully' in response.content)
# Check post deleted
all_posts = Post.objects.all()
self.assertEquals(len(all_posts), 0)

Here we basically take our existing post tests in the admin interface and add the category to them. Finally, we edit the PostViewTest class to include categories:

class PostViewTest(BaseAcceptanceTest):
def test_index(self):
# Create the category
category = Category()
category.name = 'python'
category.description = 'The Python programming language'
category.save()
# Create the author
author = User.objects.create_user('testuser', 'user@example.com', 'password')
author.save()
# Create the site
site = Site()
site.name = 'example.com'
site.domain = 'example.com'
site.save()
# Create the post
post = Post()
post.title = 'My first post'
post.text = 'This is [my first blog post](http://127.0.0.1:8000/)'
post.slug = 'my-first-post'
post.pub_date = timezone.now()
post.author = author
post.site = site
post.category = category
post.save()
# Check new post saved
all_posts = Post.objects.all()
self.assertEquals(len(all_posts), 1)
# Fetch the index
response = self.client.get('/')
self.assertEquals(response.status_code, 200)
# Check the post title is in the response
self.assertTrue(post.title in response.content)
# Check the post text is in the response
self.assertTrue(markdown.markdown(post.text) in response.content)
# Check the post date is in the response
self.assertTrue(str(post.pub_date.year) in response.content)
self.assertTrue(post.pub_date.strftime('%b') in response.content)
self.assertTrue(str(post.pub_date.day) in response.content)
# Check the link is marked up properly
self.assertTrue('<a href="http://127.0.0.1:8000/">my first blog post</a>' in response.content)
def test_post_page(self):
# Create the category
category = Category()
category.name = 'python'
category.description = 'The Python programming language'
category.save()
# Create the author
author = User.objects.create_user('testuser', 'user@example.com', 'password')
author.save()
# Create the site
site = Site()
site.name = 'example.com'
site.domain = 'example.com'
site.save()
# Create the post
post = Post()
post.title = 'My first post'
post.text = 'This is [my first blog post](http://127.0.0.1:8000/)'
post.slug = 'my-first-post'
post.pub_date = timezone.now()
post.author = author
post.site = site
post.category = category
post.save()
# Check new post saved
all_posts = Post.objects.all()
self.assertEquals(len(all_posts), 1)
only_post = all_posts[0]
self.assertEquals(only_post, post)
# Get the post URL
post_url = only_post.get_absolute_url()
# Fetch the post
response = self.client.get(post_url)
self.assertEquals(response.status_code, 200)
# Check the post title is in the response
self.assertTrue(post.title in response.content)
# Check the post text is in the response
self.assertTrue(markdown.markdown(post.text) in response.content)
# Check the post date is in the response
self.assertTrue(str(post.pub_date.year) in response.content)
self.assertTrue(post.pub_date.strftime('%b') in response.content)
self.assertTrue(str(post.pub_date.day) in response.content)
# Check the link is marked up properly
self.assertTrue('<a href="http://127.0.0.1:8000/">my first blog post</a>' in response.content)

Now it’s time to run our tests:

(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test blogengine
Creating test database for alias 'default'...
E
======================================================================
ERROR: blogengine.tests (unittest.loader.ModuleImportFailure)
----------------------------------------------------------------------
ImportError: Failed to import test module: blogengine.tests
Traceback (most recent call last):
File "/usr/local/Cellar/python/2.7.6/Frameworks/Python.framework/Versions/2.7/lib/python2.7/unittest/loader.py", line 254, in _find_tests
module = self._get_module_from_name(name)
File "/usr/local/Cellar/python/2.7.6/Frameworks/Python.framework/Versions/2.7/lib/python2.7/unittest/loader.py", line 232, in _get_module_from_name
__import__(name)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 3, in <module>
from blogengine.models import Post, Category
ImportError: cannot import name Category
----------------------------------------------------------------------
Ran 1 test in 0.000s
FAILED (errors=1)
Destroying test database for alias 'default'...

This is the expected result. We need to create our Category model. So let’s do that:

from django.db import models
from django.contrib.auth.models import User
from django.contrib.sites.models import Site
# Create your models here.
class Category(models.Model):
name = models.CharField(max_length=200)
description = models.TextField()
class Post(models.Model):
title = models.CharField(max_length=200)
pub_date = models.DateTimeField()
text = models.TextField()
slug = models.SlugField(max_length=40, unique=True)
author = models.ForeignKey(User)
site = models.ForeignKey(Site)
category = models.ForeignKey(Category, blank=True, null=True)
def get_absolute_url(self):
return "/%s/%s/%s/" % (self.pub_date.year, self.pub_date.month, self.slug)
def __unicode__(self):
return self.title
class Meta:
ordering = ["-pub_date"]

Note that we add Category before Post - this is because Category is a foreign key in Post, and must be defined in order to be used. Also, note that we add the category attribute as a ForeignKey field, like User and Site, indicating that it is an item in another table being references.

We also allow for category to be blank or null, so the user does not have to apply a category if they don’t wish to.

If we run our tests, they should still fail:

(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test blogengine
Creating test database for alias 'default'...
EEFEEEEE...EE
======================================================================
ERROR: test_create_category (blogengine.tests.PostTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 20, in test_create_category
category.save()
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 545, in save
force_update=force_update, update_fields=update_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 573, in save_base
updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 654, in _save_table
result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 687, in _do_insert
using=using, raw=raw)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 232, in _insert
return insert_query(self.model, objs, fields, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 1511, in insert_query
return query.get_compiler(using=using).execute_sql(return_id)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 898, in execute_sql
cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
return self.cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/utils.py", line 99, in __exit__
six.reraise(dj_exc_type, dj_exc_value, traceback)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
return self.cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/sqlite3/base.py", line 450, in execute
return Database.Cursor.execute(self, query, params)
OperationalError: no such table: blogengine_category
======================================================================
ERROR: test_create_post (blogengine.tests.PostTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 37, in test_create_post
category.save()
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 545, in save
force_update=force_update, update_fields=update_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 573, in save_base
updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 654, in _save_table
result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 687, in _do_insert
using=using, raw=raw)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 232, in _insert
return insert_query(self.model, objs, fields, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 1511, in insert_query
return query.get_compiler(using=using).execute_sql(return_id)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 898, in execute_sql
cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
return self.cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/utils.py", line 99, in __exit__
six.reraise(dj_exc_type, dj_exc_value, traceback)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
return self.cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/sqlite3/base.py", line 450, in execute
return Database.Cursor.execute(self, query, params)
OperationalError: no such table: blogengine_category
======================================================================
ERROR: test_create_post (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 215, in test_create_post
category.save()
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 545, in save
force_update=force_update, update_fields=update_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 573, in save_base
updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 654, in _save_table
result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 687, in _do_insert
using=using, raw=raw)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 232, in _insert
return insert_query(self.model, objs, fields, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 1511, in insert_query
return query.get_compiler(using=using).execute_sql(return_id)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 898, in execute_sql
cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
return self.cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/utils.py", line 99, in __exit__
six.reraise(dj_exc_type, dj_exc_value, traceback)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
return self.cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/sqlite3/base.py", line 450, in execute
return Database.Cursor.execute(self, query, params)
OperationalError: no such table: blogengine_category
======================================================================
ERROR: test_delete_category (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 192, in test_delete_category
category.save()
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 545, in save
force_update=force_update, update_fields=update_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 573, in save_base
updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 654, in _save_table
result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 687, in _do_insert
using=using, raw=raw)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 232, in _insert
return insert_query(self.model, objs, fields, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 1511, in insert_query
return query.get_compiler(using=using).execute_sql(return_id)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 898, in execute_sql
cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
return self.cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/utils.py", line 99, in __exit__
six.reraise(dj_exc_type, dj_exc_value, traceback)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
return self.cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/sqlite3/base.py", line 450, in execute
return Database.Cursor.execute(self, query, params)
OperationalError: no such table: blogengine_category
======================================================================
ERROR: test_delete_post (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 304, in test_delete_post
category.save()
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 545, in save
force_update=force_update, update_fields=update_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 573, in save_base
updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 654, in _save_table
result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 687, in _do_insert
using=using, raw=raw)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 232, in _insert
return insert_query(self.model, objs, fields, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 1511, in insert_query
return query.get_compiler(using=using).execute_sql(return_id)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 898, in execute_sql
cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
return self.cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/utils.py", line 99, in __exit__
six.reraise(dj_exc_type, dj_exc_value, traceback)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
return self.cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/sqlite3/base.py", line 450, in execute
return Database.Cursor.execute(self, query, params)
OperationalError: no such table: blogengine_category
======================================================================
ERROR: test_edit_category (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 165, in test_edit_category
category.save()
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 545, in save
force_update=force_update, update_fields=update_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 573, in save_base
updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 654, in _save_table
result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 687, in _do_insert
using=using, raw=raw)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 232, in _insert
return insert_query(self.model, objs, fields, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 1511, in insert_query
return query.get_compiler(using=using).execute_sql(return_id)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 898, in execute_sql
cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
return self.cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/utils.py", line 99, in __exit__
six.reraise(dj_exc_type, dj_exc_value, traceback)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
return self.cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/sqlite3/base.py", line 450, in execute
return Database.Cursor.execute(self, query, params)
OperationalError: no such table: blogengine_category
======================================================================
ERROR: test_edit_post (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 250, in test_edit_post
category.save()
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 545, in save
force_update=force_update, update_fields=update_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 573, in save_base
updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 654, in _save_table
result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 687, in _do_insert
using=using, raw=raw)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 232, in _insert
return insert_query(self.model, objs, fields, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 1511, in insert_query
return query.get_compiler(using=using).execute_sql(return_id)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 898, in execute_sql
cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
return self.cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/utils.py", line 99, in __exit__
six.reraise(dj_exc_type, dj_exc_value, traceback)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
return self.cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/sqlite3/base.py", line 450, in execute
return Database.Cursor.execute(self, query, params)
OperationalError: no such table: blogengine_category
======================================================================
ERROR: test_index (blogengine.tests.PostViewTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 353, in test_index
category.save()
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 545, in save
force_update=force_update, update_fields=update_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 573, in save_base
updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 654, in _save_table
result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 687, in _do_insert
using=using, raw=raw)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 232, in _insert
return insert_query(self.model, objs, fields, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 1511, in insert_query
return query.get_compiler(using=using).execute_sql(return_id)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 898, in execute_sql
cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
return self.cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/utils.py", line 99, in __exit__
six.reraise(dj_exc_type, dj_exc_value, traceback)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
return self.cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/sqlite3/base.py", line 450, in execute
return Database.Cursor.execute(self, query, params)
OperationalError: no such table: blogengine_category
======================================================================
ERROR: test_post_page (blogengine.tests.PostViewTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 403, in test_post_page
category.save()
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 545, in save
force_update=force_update, update_fields=update_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 573, in save_base
updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 654, in _save_table
result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 687, in _do_insert
using=using, raw=raw)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 232, in _insert
return insert_query(self.model, objs, fields, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 1511, in insert_query
return query.get_compiler(using=using).execute_sql(return_id)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 898, in execute_sql
cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
return self.cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/utils.py", line 99, in __exit__
six.reraise(dj_exc_type, dj_exc_value, traceback)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
return self.cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/sqlite3/base.py", line 450, in execute
return Database.Cursor.execute(self, query, params)
OperationalError: no such table: blogengine_category
======================================================================
FAIL: test_create_category (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 142, in test_create_category
self.assertEquals(response.status_code, 200)
AssertionError: 404 != 200
----------------------------------------------------------------------
Ran 13 tests in 3.393s
FAILED (failures=1, errors=9)
Destroying test database for alias 'default'...

The category table hasn’t yet been created, so we need to use South to create and run the migrations:

$ python manage.py schemamigration --auto blogengine
$ python manage.py migrate

If we then run our tests again, some of them will still fail:

Creating test database for alias 'default'...
..F.F.F......
======================================================================
FAIL: test_create_category (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 142, in test_create_category
self.assertEquals(response.status_code, 200)
AssertionError: 404 != 200
======================================================================
FAIL: test_delete_category (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 201, in test_delete_category
self.assertEquals(response.status_code, 200)
AssertionError: 404 != 200
======================================================================
FAIL: test_edit_category (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 175, in test_edit_category
self.assertEquals(response.status_code, 200)
AssertionError: 404 != 200
----------------------------------------------------------------------
Ran 13 tests in 4.047s
FAILED (failures=3)
Destroying test database for alias 'default'...

That’s because we haven’t registered the categories in the admin. So, that’s our next job:

import models
from django.contrib import admin
from django.contrib.auth.models import User
class PostAdmin(admin.ModelAdmin):
prepopulated_fields = {"slug": ("title",)}
exclude = ('author',)
def save_model(self, request, obj, form, change):
obj.author = request.user
obj.save()
admin.site.register(models.Category)
admin.site.register(models.Post, PostAdmin)

Now we try again:

(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test blogengine
Creating test database for alias 'default'...
.............
----------------------------------------------------------------------
Ran 13 tests in 4.092s
OK
Destroying test database for alias 'default'...

It passes! Let’s do a quick sense check before committing. Run the server:

$ python manage.py runserver

If you visit the admin, you’ll see the text for category is Categorys, which is incorrect. We also don’t have a good representation of the category in the admin. Let’s fix that:

from django.db import models
from django.contrib.auth.models import User
from django.contrib.sites.models import Site
# Create your models here.
class Category(models.Model):
name = models.CharField(max_length=200)
description = models.TextField()
def __unicode__(self):
return self.name
class Meta:
verbose_name_plural = 'categories'
class Post(models.Model):
title = models.CharField(max_length=200)
pub_date = models.DateTimeField()
text = models.TextField()
slug = models.SlugField(max_length=40, unique=True)
author = models.ForeignKey(User)
site = models.ForeignKey(Site)
category = models.ForeignKey(Category, blank=True, null=True)
def get_absolute_url(self):
return "/%s/%s/%s/" % (self.pub_date.year, self.pub_date.month, self.slug)
def __unicode__(self):
return self.title
class Meta:
ordering = ["-pub_date"]

Let’s commit our changes:

$ git add blogengine/
$ git commit -m 'Implemented categories'

Now, as yet our categories don’t actually do all that much. We would like to be able to:

  • List all posts under a category
  • Show the post category at the base of the post

So, let’s implement that. First, as usual, we implement tests first:

class PostViewTest(BaseAcceptanceTest):
def test_index(self):
# Create the category
category = Category()
category.name = 'python'
category.description = 'The Python programming language'
category.save()
# Create the author
author = User.objects.create_user('testuser', 'user@example.com', 'password')
author.save()
# Create the site
site = Site()
site.name = 'example.com'
site.domain = 'example.com'
site.save()
# Create the post
post = Post()
post.title = 'My first post'
post.text = 'This is [my first blog post](http://127.0.0.1:8000/)'
post.slug = 'my-first-post'
post.pub_date = timezone.now()
post.author = author
post.site = site
post.category = category
post.save()
# Check new post saved
all_posts = Post.objects.all()
self.assertEquals(len(all_posts), 1)
# Fetch the index
response = self.client.get('/')
self.assertEquals(response.status_code, 200)
# Check the post title is in the response
self.assertTrue(post.title in response.content)
# Check the post text is in the response
self.assertTrue(markdown.markdown(post.text) in response.content)
# Check the post category is in the response
self.assertTrue(post.category.name in response.content)
# Check the post date is in the response
self.assertTrue(str(post.pub_date.year) in response.content)
self.assertTrue(post.pub_date.strftime('%b') in response.content)
self.assertTrue(str(post.pub_date.day) in response.content)
# Check the link is marked up properly
self.assertTrue('<a href="http://127.0.0.1:8000/">my first blog post</a>' in response.content)
def test_post_page(self):
# Create the category
category = Category()
category.name = 'python'
category.description = 'The Python programming language'
category.save()
# Create the author
author = User.objects.create_user('testuser', 'user@example.com', 'password')
author.save()
# Create the site
site = Site()
site.name = 'example.com'
site.domain = 'example.com'
site.save()
# Create the post
post = Post()
post.title = 'My first post'
post.text = 'This is [my first blog post](http://127.0.0.1:8000/)'
post.slug = 'my-first-post'
post.pub_date = timezone.now()
post.author = author
post.site = site
post.category = category
post.save()
# Check new post saved
all_posts = Post.objects.all()
self.assertEquals(len(all_posts), 1)
only_post = all_posts[0]
self.assertEquals(only_post, post)
# Get the post URL
post_url = only_post.get_absolute_url()
# Fetch the post
response = self.client.get(post_url)
self.assertEquals(response.status_code, 200)
# Check the post title is in the response
self.assertTrue(post.title in response.content)
# Check the post category is in the response
self.assertTrue(post.category.name in response.content)
# Check the post text is in the response
self.assertTrue(markdown.markdown(post.text) in response.content)
# Check the post date is in the response
self.assertTrue(str(post.pub_date.year) in response.content)
self.assertTrue(post.pub_date.strftime('%b') in response.content)
self.assertTrue(str(post.pub_date.day) in response.content)
# Check the link is marked up properly
self.assertTrue('<a href="http://127.0.0.1:8000/">my first blog post</a>' in response.content)

All we do here is assert that for both the post pages and the index, the text from the category name is shown in the response. We also need to check the category-specific route works. Add this method to PostViewTest:

def test_category_page(self):
# Create the category
category = Category()
category.name = 'python'
category.description = 'The Python programming language'
category.save()
# Create the author
author = User.objects.create_user('testuser', 'user@example.com', 'password')
author.save()
# Create the site
site = Site()
site.name = 'example.com'
site.domain = 'example.com'
site.save()
# Create the post
post = Post()
post.title = 'My first post'
post.text = 'This is [my first blog post](http://127.0.0.1:8000/)'
post.slug = 'my-first-post'
post.pub_date = timezone.now()
post.author = author
post.site = site
post.category = category
post.save()
# Check new post saved
all_posts = Post.objects.all()
self.assertEquals(len(all_posts), 1)
only_post = all_posts[0]
self.assertEquals(only_post, post)
# Get the category URL
category_url = post.category.get_absolute_url()
# Fetch the category
response = self.client.get(category_url)
self.assertEquals(response.status_code, 200)
# Check the category name is in the response
self.assertTrue(post.category.name in response.content)
# Check the post text is in the response
self.assertTrue(markdown.markdown(post.text) in response.content)
# Check the post date is in the response
self.assertTrue(str(post.pub_date.year) in response.content)
self.assertTrue(post.pub_date.strftime('%b') in response.content)
self.assertTrue(str(post.pub_date.day) in response.content)
# Check the link is marked up properly
self.assertTrue('<a href="http://127.0.0.1:8000/">my first blog post</a>' in response.content)

This is very similar to the previous tests, but fetches the absolute URL for the category, and ensures the category name and post content are shown. Now, let’s run our new tests:

(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test blogengine
Creating test database for alias 'default'...
...........EFF
======================================================================
ERROR: test_category_page (blogengine.tests.PostViewTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 494, in test_category_page
category_url = post.category.get_absolute_url()
AttributeError: 'Category' object has no attribute 'get_absolute_url'
======================================================================
FAIL: test_index (blogengine.tests.PostViewTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 391, in test_index
self.assertTrue(post.category.name in response.content)
AssertionError: False is not true
======================================================================
FAIL: test_post_page (blogengine.tests.PostViewTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 446, in test_post_page
self.assertTrue(post.category.name in response.content)
AssertionError: False is not true
----------------------------------------------------------------------
Ran 14 tests in 5.017s
FAILED (failures=2, errors=1)
Destroying test database for alias 'default'...

Let’s take a look at why they failed. test_category_page failed because the Category object had no method get_absolute_url. So we need to implement one. To do so, we really need to add a slug field, like the posts already have. Ideally, we want this to be populated automatically, but with the option to create one manually. So, edit the models as follows:

from django.db import models
from django.contrib.auth.models import User
from django.contrib.sites.models import Site
from django.utils.text import slugify
# Create your models here.
class Category(models.Model):
name = models.CharField(max_length=200)
description = models.TextField()
slug = models.SlugField(max_length=40, unique=True, blank=True, null=True)
def save(self):
if not self.slug:
self.slug = slugify(unicode(self.name))
super(Category, self).save()
def get_absolute_url(self):
return "/category/%s/" % (self.slug)
def __unicode__(self):
return self.name
class Meta:
verbose_name_plural = 'categories'
class Post(models.Model):
title = models.CharField(max_length=200)
pub_date = models.DateTimeField()
text = models.TextField()
slug = models.SlugField(max_length=40, unique=True)
author = models.ForeignKey(User)
site = models.ForeignKey(Site)
category = models.ForeignKey(Category, blank=True, null=True)
def get_absolute_url(self):
return "/%s/%s/%s/" % (self.pub_date.year, self.pub_date.month, self.slug)
def __unicode__(self):
return self.title
class Meta:
ordering = ["-pub_date"]

We’re adding the slug attribute to the Category model here. However, we’re also overriding the save method to detect if the slug is set, and if not, to create a slug using the slugify function, and set it as the category’s slug. We also define an absolute URL for the category.

Now, if you run the tests, they will fail because we haven’t made the changes to the database. So, we use South again:

$ python manage.py schemamigration --auto blogengine

Then run the migration:

$ python manage.py migrate

Now, running our tests will show that the tables are in place, but we still have some work to do. The index and post pages don’t show our categories, so we’ll fix that. First, we’ll fix our post list:

{% extends "blogengine/includes/base.html" %}
{% load custom_markdown %}
{% block content %}
{% for post in object_list %}
<div class="post">
<h1><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h1>
<h3>{{ post.pub_date }}</h3>
{{ post.text|custom_markdown }}
</div>
<a href="{{ post.category.get_absolute_url }}">{{ post.category.name }}</a>
{% endfor %}
{% if page_obj.has_previous %}
<a href="/{{ page_obj.previous_page_number }}/">Previous Page</a>
{% endif %}
{% if page_obj.has_next %}
<a href="/{{ page_obj.next_page_number }}/">Next Page</a>
{% endif %}
{% endblock %}

Next, we’ll take care of our post detail page:

{% extends "blogengine/includes/base.html" %}
{% load custom_markdown %}
{% block content %}
<div class="post">
<h1>{{ object.title }}</h1>
<h3>{{ object.pub_date }}</h3>
{{ object.text|custom_markdown }}
<a href="{{ object.category.get_absolute_url }}">{{ object.category.name }}</a>
<h4>Comments</h4>
<div class="fb-comments" data-href="http://{{ post.site }}{{ post.get_absolute_url }}" data-width="470" data-num-posts="10"></div>
</div>
{% endblock %}

Note that in both cases we include a link to the category URL.

Now, we should only have one failing test outstanding - the category page. For this, generic views aren’t sufficient as we need to limit the queryset to only show those posts with a specific category. Fortunately, we can extend Django’s generic views to add this functionality. First, we edit our URLconfs:

from django.conf.urls import patterns, url
from django.views.generic import ListView, DetailView
from blogengine.models import Post, Category
from blogengine.views import CategoryListView
urlpatterns = patterns('',
# Index
url(r'^(?P<page>\d+)?/?$', ListView.as_view(
model=Post,
paginate_by=5,
)),
# Individual posts
url(r'^(?P<pub_date__year>\d{4})/(?P<pub_date__month>\d{1,2})/(?P<slug>[a-zA-Z0-9-]+)/?$', DetailView.as_view(
model=Post,
)),
# Categories
url(r'^category/(?P<slug>[a-zA-Z0-9-]+)/?$', CategoryListView.as_view(
paginate_by=5,
model=Category,
)),
)

Note we import a new view from blogengine.views called CategoryListView. Next, we create that listview:

from django.shortcuts import render
from django.views.generic import ListView
from blogengine.models import Category, Post
# Create your views here.
class CategoryListView(ListView):
def get_queryset(self):
slug = self.kwargs['slug']
try:
category = Category.objects.get(slug=slug)
return Post.objects.filter(category=category)
except Category.DoesNotExist:
return Post.objects.none()

This is quite simple. We import the ListView, as well as our models. Then we extend ListView by getting the slug from the request, fetching the appropriate category, and returning only those posts that have that category. If the category does not exist, we return the empty Post object list. We haven’t had to set the template manually as it is inherited from ListView.

If you run the tests, they should now pass:

Creating test database for alias 'default'...
..............
----------------------------------------------------------------------
Ran 14 tests in 5.083s
OK
Destroying test database for alias 'default'...

So let’s commit our changes:

$ git add blogengine/ templates/
$ git commit -m 'Categories are now shown'

Tags

Tags are fairly similar to categories, but more complex. The relationship they have is called many-to-many - in other words, a tag can be applied to many posts, and one post can have many tags. This is more difficult to model with a relational database. The usual way to do so is to create an intermediate table between the posts and tags, to identify mappings between the two. Fortunately, Django makes this quite easy.

Let’s write the tests for our tagging system. As with the categories, we’ll write the tests for creating and editing them first, and add in tests for them being visible later. First we’ll create a test for creating a new tag object:

def test_create_tag(self):
# Create the tag
tag = Tag()
# Add attributes
tag.name = 'python'
tag.description = 'The Python programming language'
# Save it
tag.save()
# Check we can find it
all_tags = Tag.objects.all()
self.assertEquals(len(all_tags), 1)
only_tag = all_tags[0]
self.assertEquals(only_tag, tag)
# Check attributes
self.assertEquals(only_tag.name, 'python')
self.assertEquals(only_tag.description, 'The Python programming language')

Next, we’ll amend the test for creating a post to include tags:

def test_create_post(self):
# Create the category
category = Category()
category.name = 'python'
category.description = 'The Python programming language'
category.save()
# Create the tag
tag = Tag()
tag.name = 'python'
tag.description = 'The Python programming language'
tag.save()
# Create the author
author = User.objects.create_user('testuser', 'user@example.com', 'password')
author.save()
# Create the site
site = Site()
site.name = 'example.com'
site.domain = 'example.com'
site.save()
# Create the post
post = Post()
# Set the attributes
post.title = 'My first post'
post.text = 'This is my first blog post'
post.slug = 'my-first-post'
post.pub_date = timezone.now()
post.author = author
post.site = site
post.category = category
# Save it
post.save()
# Add the tag
post.tags.add(tag)
post.save()
# Check we can find it
all_posts = Post.objects.all()
self.assertEquals(len(all_posts), 1)
only_post = all_posts[0]
self.assertEquals(only_post, post)
# Check attributes
self.assertEquals(only_post.title, 'My first post')
self.assertEquals(only_post.text, 'This is my first blog post')
self.assertEquals(only_post.slug, 'my-first-post')
self.assertEquals(only_post.site.name, 'example.com')
self.assertEquals(only_post.site.domain, 'example.com')
self.assertEquals(only_post.pub_date.day, post.pub_date.day)
self.assertEquals(only_post.pub_date.month, post.pub_date.month)
self.assertEquals(only_post.pub_date.year, post.pub_date.year)
self.assertEquals(only_post.pub_date.hour, post.pub_date.hour)
self.assertEquals(only_post.pub_date.minute, post.pub_date.minute)
self.assertEquals(only_post.pub_date.second, post.pub_date.second)
self.assertEquals(only_post.author.username, 'testuser')
self.assertEquals(only_post.author.email, 'user@example.com')
self.assertEquals(only_post.category.name, 'python')
self.assertEquals(only_post.category.description, 'The Python programming language')
# Check tags
post_tags = only_post.tags.all()
self.assertEquals(len(post_tags), 1)
only_post_tag = post_tags[0]
self.assertEquals(only_post_tag, tag)
self.assertEquals(only_post_tag.name, 'python')
self.assertEquals(only_post_tag.description, 'The Python programming language')

Note the difference in how we apply the tags. Because a post can have more than one tag, we can’t just define post.tag in the same way. Instead, we have post.tags, which you can think of as a list, and we use the add method to add a new tag. Note also that the post must already exist before we can add a tag.

We also need to create acceptance tests for creating, editing and deleting tags:

def test_create_tag(self):
# Log in
self.client.login(username='bobsmith', password="password")
# Check response code
response = self.client.get('/admin/blogengine/tag/add/')
self.assertEquals(response.status_code, 200)
# Create the new tag
response = self.client.post('/admin/blogengine/tag/add/', {
'name': 'python',
'description': 'The Python programming language'
},
follow=True
)
self.assertEquals(response.status_code, 200)
# Check added successfully
self.assertTrue('added successfully' in response.content)
# Check new tag now in database
all_tags = Tag.objects.all()
self.assertEquals(len(all_tags), 1)
def test_edit_tag(self):
# Create the tag
tag = Tag()
tag.name = 'python'
tag.description = 'The Python programming language'
tag.save()
# Log in
self.client.login(username='bobsmith', password="password")
# Edit the tag
response = self.client.post('/admin/blogengine/tag/1/', {
'name': 'perl',
'description': 'The Perl programming language'
}, follow=True)
self.assertEquals(response.status_code, 200)
# Check changed successfully
self.assertTrue('changed successfully' in response.content)
# Check tag amended
all_tags = Tag.objects.all()
self.assertEquals(len(all_tags), 1)
only_tag = all_tags[0]
self.assertEquals(only_tag.name, 'perl')
self.assertEquals(only_tag.description, 'The Perl programming language')
def test_delete_tag(self):
# Create the tag
tag = Tag()
tag.name = 'python'
tag.description = 'The Python programming language'
tag.save()
# Log in
self.client.login(username='bobsmith', password="password")
# Delete the tag
response = self.client.post('/admin/blogengine/tag/1/delete/', {
'post': 'yes'
}, follow=True)
self.assertEquals(response.status_code, 200)
# Check deleted successfully
self.assertTrue('deleted successfully' in response.content)
# Check tag deleted
all_tags = Tag.objects.all()
self.assertEquals(len(all_tags), 0)

These tests are virtually identical to those for the Category objects, as we plan for our Tag objects to be very similar. Finally, we need to amend the acceptance tests for Post objects to include a tag:

def test_create_post(self):
# Create the category
category = Category()
category.name = 'python'
category.description = 'The Python programming language'
category.save()
# Create the tag
tag = Tag()
tag.name = 'python'
tag.description = 'The Python programming language'
tag.save()
# Log in
self.client.login(username='bobsmith', password="password")
# Check response code
response = self.client.get('/admin/blogengine/post/add/')
self.assertEquals(response.status_code, 200)
# Create the new post
response = self.client.post('/admin/blogengine/post/add/', {
'title': 'My first post',
'text': 'This is my first post',
'pub_date_0': '2013-12-28',
'pub_date_1': '22:00:04',
'slug': 'my-first-post',
'site': '1',
'category': '1',
'tags': '1'
},
follow=True
)
self.assertEquals(response.status_code, 200)
# Check added successfully
self.assertTrue('added successfully' in response.content)
# Check new post now in database
all_posts = Post.objects.all()
self.assertEquals(len(all_posts), 1)
def test_edit_post(self):
# Create the category
category = Category()
category.name = 'python'
category.description = 'The Python programming language'
category.save()
# Create the tag
tag = Tag()
tag.name = 'python'
tag.description = 'The Python programming language'
tag.save()
# Create the author
author = User.objects.create_user('testuser', 'user@example.com', 'password')
author.save()
# Create the site
site = Site()
site.name = 'example.com'
site.domain = 'example.com'
site.save()
# Create the post
post = Post()
post.title = 'My first post'
post.text = 'This is my first blog post'
post.slug = 'my-first-post'
post.pub_date = timezone.now()
post.author = author
post.site = site
post.save()
post.tags.add(tag)
post.save()
# Log in
self.client.login(username='bobsmith', password="password")
# Edit the post
response = self.client.post('/admin/blogengine/post/1/', {
'title': 'My second post',
'text': 'This is my second blog post',
'pub_date_0': '2013-12-28',
'pub_date_1': '22:00:04',
'slug': 'my-second-post',
'site': '1',
'category': '1',
'tags': '1'
},
follow=True
)
self.assertEquals(response.status_code, 200)
# Check changed successfully
self.assertTrue('changed successfully' in response.content)
# Check post amended
all_posts = Post.objects.all()
self.assertEquals(len(all_posts), 1)
only_post = all_posts[0]
self.assertEquals(only_post.title, 'My second post')
self.assertEquals(only_post.text, 'This is my second blog post')
def test_delete_post(self):
# Create the category
category = Category()
category.name = 'python'
category.description = 'The Python programming language'
category.save()
# Create the tag
tag = Tag()
tag.name = 'python'
tag.description = 'The Python programming language'
tag.save()
# Create the author
author = User.objects.create_user('testuser', 'user@example.com', 'password')
author.save()
# Create the site
site = Site()
site.name = 'example.com'
site.domain = 'example.com'
site.save()
# Create the post
post = Post()
post.title = 'My first post'
post.text = 'This is my first blog post'
post.slug = 'my-first-post'
post.pub_date = timezone.now()
post.site = site
post.author = author
post.category = category
post.save()
post.tags.add(tag)
post.save()
# Check new post saved
all_posts = Post.objects.all()
self.assertEquals(len(all_posts), 1)
# Log in
self.client.login(username='bobsmith', password="password")
# Delete the post
response = self.client.post('/admin/blogengine/post/1/delete/', {
'post': 'yes'
}, follow=True)
self.assertEquals(response.status_code, 200)
# Check deleted successfully
self.assertTrue('deleted successfully' in response.content)
# Check post deleted
all_posts = Post.objects.all()
self.assertEquals(len(all_posts), 0)

Here we’re just adding tags to our Post objects.

Now it’s time to run our tests to make sure they fail as expected:

(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test blogengine
Creating test database for alias 'default'...
E
======================================================================
ERROR: blogengine.tests (unittest.loader.ModuleImportFailure)
----------------------------------------------------------------------
ImportError: Failed to import test module: blogengine.tests
Traceback (most recent call last):
File "/usr/local/Cellar/python/2.7.6/Frameworks/Python.framework/Versions/2.7/lib/python2.7/unittest/loader.py", line 254, in _find_tests
module = self._get_module_from_name(name)
File "/usr/local/Cellar/python/2.7.6/Frameworks/Python.framework/Versions/2.7/lib/python2.7/unittest/loader.py", line 232, in _get_module_from_name
__import__(name)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 3, in <module>
from blogengine.models import Post, Category, Tag
ImportError: cannot import name Tag
----------------------------------------------------------------------
Ran 1 test in 0.000s
FAILED (errors=1)
Destroying test database for alias 'default'...

So here we can’t import our Tag model, because we haven’t created it. So, we’ll do that:

class Tag(models.Model):
name = models.CharField(max_length=200)
description = models.TextField()
slug = models.SlugField(max_length=40, unique=True, blank=True, null=True)
def save(self):
if not self.slug:
self.slug = slugify(unicode(self.name))
super(Tag, self).save()
def get_absolute_url(self):
return "/tag/%s/" % (self.slug)
def __unicode__(self):
return self.name

Our Tag model is very much like our Category model, but we don’t need to change the verbose_name_plural value, and we amend the absolute URL to show it as a tag rather than a category.

We also need to amend our Post model to include a tags field:

class Post(models.Model):
title = models.CharField(max_length=200)
pub_date = models.DateTimeField()
text = models.TextField()
slug = models.SlugField(max_length=40, unique=True)
author = models.ForeignKey(User)
site = models.ForeignKey(Site)
category = models.ForeignKey(Category, blank=True, null=True)
tags = models.ManyToManyField(Tag)
def get_absolute_url(self):
return "/%s/%s/%s/" % (self.pub_date.year, self.pub_date.month, self.slug)
def __unicode__(self):
return self.title
class Meta:
ordering = ["-pub_date"]

Note that tags is a ManyToManyField, and we pass through the model we wish to use, much like we did with the categories. The difference is that one tag can be applied to many posts and a post can have many tags, so we need an intermediate database table to handle the relationship between the two. With Django’s ORM we can handle this quickly and easily.

Run our tests and they should still fail:

(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test blogengine
Creating test database for alias 'default'...
.EE.EF.EE.EE......
======================================================================
ERROR: test_create_post (blogengine.tests.PostTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 64, in test_create_post
tag.save()
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/models.py", line 34, in save
super(Tag, self).save()
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 545, in save
force_update=force_update, update_fields=update_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 573, in save_base
updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 654, in _save_table
result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 687, in _do_insert
using=using, raw=raw)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 232, in _insert
return insert_query(self.model, objs, fields, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 1511, in insert_query
return query.get_compiler(using=using).execute_sql(return_id)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 898, in execute_sql
cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
return self.cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/utils.py", line 99, in __exit__
six.reraise(dj_exc_type, dj_exc_value, traceback)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
return self.cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/sqlite3/base.py", line 450, in execute
return Database.Cursor.execute(self, query, params)
OperationalError: no such table: blogengine_tag
======================================================================
ERROR: test_create_tag (blogengine.tests.PostTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 41, in test_create_tag
tag.save()
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/models.py", line 34, in save
super(Tag, self).save()
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 545, in save
force_update=force_update, update_fields=update_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 573, in save_base
updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 654, in _save_table
result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 687, in _do_insert
using=using, raw=raw)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 232, in _insert
return insert_query(self.model, objs, fields, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 1511, in insert_query
return query.get_compiler(using=using).execute_sql(return_id)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 898, in execute_sql
cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
return self.cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/utils.py", line 99, in __exit__
six.reraise(dj_exc_type, dj_exc_value, traceback)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
return self.cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/sqlite3/base.py", line 450, in execute
return Database.Cursor.execute(self, query, params)
OperationalError: no such table: blogengine_tag
======================================================================
ERROR: test_create_post (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 335, in test_create_post
tag.save()
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/models.py", line 34, in save
super(Tag, self).save()
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 545, in save
force_update=force_update, update_fields=update_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 573, in save_base
updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 654, in _save_table
result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 687, in _do_insert
using=using, raw=raw)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 232, in _insert
return insert_query(self.model, objs, fields, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 1511, in insert_query
return query.get_compiler(using=using).execute_sql(return_id)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 898, in execute_sql
cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
return self.cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/utils.py", line 99, in __exit__
six.reraise(dj_exc_type, dj_exc_value, traceback)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
return self.cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/sqlite3/base.py", line 450, in execute
return Database.Cursor.execute(self, query, params)
OperationalError: no such table: blogengine_tag
======================================================================
ERROR: test_delete_post (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 440, in test_delete_post
tag.save()
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/models.py", line 34, in save
super(Tag, self).save()
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 545, in save
force_update=force_update, update_fields=update_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 573, in save_base
updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 654, in _save_table
result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 687, in _do_insert
using=using, raw=raw)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 232, in _insert
return insert_query(self.model, objs, fields, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 1511, in insert_query
return query.get_compiler(using=using).execute_sql(return_id)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 898, in execute_sql
cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
return self.cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/utils.py", line 99, in __exit__
six.reraise(dj_exc_type, dj_exc_value, traceback)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
return self.cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/sqlite3/base.py", line 450, in execute
return Database.Cursor.execute(self, query, params)
OperationalError: no such table: blogengine_tag
======================================================================
ERROR: test_delete_tag (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 306, in test_delete_tag
tag.save()
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/models.py", line 34, in save
super(Tag, self).save()
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 545, in save
force_update=force_update, update_fields=update_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 573, in save_base
updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 654, in _save_table
result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 687, in _do_insert
using=using, raw=raw)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 232, in _insert
return insert_query(self.model, objs, fields, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 1511, in insert_query
return query.get_compiler(using=using).execute_sql(return_id)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 898, in execute_sql
cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
return self.cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/utils.py", line 99, in __exit__
six.reraise(dj_exc_type, dj_exc_value, traceback)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
return self.cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/sqlite3/base.py", line 450, in execute
return Database.Cursor.execute(self, query, params)
OperationalError: no such table: blogengine_tag
======================================================================
ERROR: test_edit_post (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 377, in test_edit_post
tag.save()
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/models.py", line 34, in save
super(Tag, self).save()
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 545, in save
force_update=force_update, update_fields=update_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 573, in save_base
updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 654, in _save_table
result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 687, in _do_insert
using=using, raw=raw)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 232, in _insert
return insert_query(self.model, objs, fields, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 1511, in insert_query
return query.get_compiler(using=using).execute_sql(return_id)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 898, in execute_sql
cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
return self.cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/utils.py", line 99, in __exit__
six.reraise(dj_exc_type, dj_exc_value, traceback)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
return self.cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/sqlite3/base.py", line 450, in execute
return Database.Cursor.execute(self, query, params)
OperationalError: no such table: blogengine_tag
======================================================================
ERROR: test_edit_tag (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 279, in test_edit_tag
tag.save()
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/models.py", line 34, in save
super(Tag, self).save()
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 545, in save
force_update=force_update, update_fields=update_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 573, in save_base
updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 654, in _save_table
result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/base.py", line 687, in _do_insert
using=using, raw=raw)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 232, in _insert
return insert_query(self.model, objs, fields, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 1511, in insert_query
return query.get_compiler(using=using).execute_sql(return_id)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 898, in execute_sql
cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
return self.cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/utils.py", line 99, in __exit__
six.reraise(dj_exc_type, dj_exc_value, traceback)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/util.py", line 53, in execute
return self.cursor.execute(sql, params)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/backends/sqlite3/base.py", line 450, in execute
return Database.Cursor.execute(self, query, params)
OperationalError: no such table: blogengine_tag
======================================================================
FAIL: test_create_tag (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 256, in test_create_tag
self.assertEquals(response.status_code, 200)
AssertionError: 404 != 200
----------------------------------------------------------------------
Ran 18 tests in 3.981s
FAILED (failures=1, errors=7)
Destroying test database for alias 'default'...

Again, we can easily see why they failed - the blogengine_tag table is not in place. So let’s create and run our migrations to fix that:

$ python manage.py schemamigration --auto blogengine
$ python manage.py migrate

Now, we run our tests again:

(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test blogengine
Creating test database for alias 'default'...
.....F..F..F......
======================================================================
FAIL: test_create_tag (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 256, in test_create_tag
self.assertEquals(response.status_code, 200)
AssertionError: 404 != 200
======================================================================
FAIL: test_delete_tag (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 315, in test_delete_tag
self.assertEquals(response.status_code, 200)
AssertionError: 404 != 200
======================================================================
FAIL: test_edit_tag (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 289, in test_edit_tag
self.assertEquals(response.status_code, 200)
AssertionError: 404 != 200
----------------------------------------------------------------------
Ran 18 tests in 5.124s
FAILED (failures=3)
Destroying test database for alias 'default'...

We can’t yet amend our tags in the admin, because we haven’t registered them. So we do that next:

import models
from django.contrib import admin
from django.contrib.auth.models import User
class PostAdmin(admin.ModelAdmin):
prepopulated_fields = {"slug": ("title",)}
exclude = ('author',)
def save_model(self, request, obj, form, change):
obj.author = request.user
obj.save()
admin.site.register(models.Category)
admin.site.register(models.Tag)
admin.site.register(models.Post, PostAdmin)

Now, if we run our tests, they should pass:

(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test blogengine
Creating test database for alias 'default'...
..................
----------------------------------------------------------------------
Ran 18 tests in 5.444s
OK
Destroying test database for alias 'default'...

Time to commit our changes:

$ git add blogengine/
$ git commit -m 'Implemented tags'

Now, like with the categories beforehand, we want to be able to show the tags applied to a post at the base of it, and list all posts for a specific tag. So, first of all, we’ll amend our PostViewTest class to check for the tags:

class PostViewTest(BaseAcceptanceTest):
def test_index(self):
# Create the category
category = Category()
category.name = 'python'
category.description = 'The Python programming language'
category.save()
# Create the tag
tag = Tag()
tag.name = 'perl'
tag.description = 'The Perl programming language'
tag.save()
# Create the author
author = User.objects.create_user('testuser', 'user@example.com', 'password')
author.save()
# Create the site
site = Site()
site.name = 'example.com'
site.domain = 'example.com'
site.save()
# Create the post
post = Post()
post.title = 'My first post'
post.text = 'This is [my first blog post](http://127.0.0.1:8000/)'
post.slug = 'my-first-post'
post.pub_date = timezone.now()
post.author = author
post.site = site
post.category = category
post.save()
post.tags.add(tag)
# Check new post saved
all_posts = Post.objects.all()
self.assertEquals(len(all_posts), 1)
# Fetch the index
response = self.client.get('/')
self.assertEquals(response.status_code, 200)
# Check the post title is in the response
self.assertTrue(post.title in response.content)
# Check the post text is in the response
self.assertTrue(markdown.markdown(post.text) in response.content)
# Check the post category is in the response
self.assertTrue(post.category.name in response.content)
# Check the post tag is in the response
post_tag = all_posts[0].tags.all()[0]
self.assertTrue(post_tag.name in response.content)
# Check the post date is in the response
self.assertTrue(str(post.pub_date.year) in response.content)
self.assertTrue(post.pub_date.strftime('%b') in response.content)
self.assertTrue(str(post.pub_date.day) in response.content)
# Check the link is marked up properly
self.assertTrue('<a href="http://127.0.0.1:8000/">my first blog post</a>' in response.content)
def test_post_page(self):
# Create the category
category = Category()
category.name = 'python'
category.description = 'The Python programming language'
category.save()
# Create the tag
tag = Tag()
tag.name = 'perl'
tag.description = 'The Perl programming language'
tag.save()
# Create the author
author = User.objects.create_user('testuser', 'user@example.com', 'password')
author.save()
# Create the site
site = Site()
site.name = 'example.com'
site.domain = 'example.com'
site.save()
# Create the post
post = Post()
post.title = 'My first post'
post.text = 'This is [my first blog post](http://127.0.0.1:8000/)'
post.slug = 'my-first-post'
post.pub_date = timezone.now()
post.author = author
post.site = site
post.category = category
post.save()
post.tags.add(tag)
post.save()
# Check new post saved
all_posts = Post.objects.all()
self.assertEquals(len(all_posts), 1)
only_post = all_posts[0]
self.assertEquals(only_post, post)
# Get the post URL
post_url = only_post.get_absolute_url()
# Fetch the post
response = self.client.get(post_url)
self.assertEquals(response.status_code, 200)
# Check the post title is in the response
self.assertTrue(post.title in response.content)
# Check the post category is in the response
self.assertTrue(post.category.name in response.content)
# Check the post tag is in the response
post_tag = all_posts[0].tags.all()[0]
self.assertTrue(post_tag.name in response.content)
# Check the post text is in the response
self.assertTrue(markdown.markdown(post.text) in response.content)
# Check the post date is in the response
self.assertTrue(str(post.pub_date.year) in response.content)
self.assertTrue(post.pub_date.strftime('%b') in response.content)
self.assertTrue(str(post.pub_date.day) in response.content)
# Check the link is marked up properly
self.assertTrue('<a href="http://127.0.0.1:8000/">my first blog post</a>' in response.content)

We create a tag near the top, and check for the text in the page (note that to avoid false positives from the categories, we set the name of the tags to something different). We do this on both the index and post pages.

We also need to put a test in place for the tag-specific page:

def test_tag_page(self):
# Create the tag
tag = Tag()
tag.name = 'python'
tag.description = 'The Python programming language'
tag.save()
# Create the author
author = User.objects.create_user('testuser', 'user@example.com', 'password')
author.save()
# Create the site
site = Site()
site.name = 'example.com'
site.domain = 'example.com'
site.save()
# Create the post
post = Post()
post.title = 'My first post'
post.text = 'This is [my first blog post](http://127.0.0.1:8000/)'
post.slug = 'my-first-post'
post.pub_date = timezone.now()
post.author = author
post.site = site
post.save()
post.tags.add(tag)
# Check new post saved
all_posts = Post.objects.all()
self.assertEquals(len(all_posts), 1)
only_post = all_posts[0]
self.assertEquals(only_post, post)
# Get the tag URL
tag_url = post.tags.all()[0].get_absolute_url()
# Fetch the tag
response = self.client.get(tag_url)
self.assertEquals(response.status_code, 200)
# Check the tag name is in the response
self.assertTrue(post.tags.all()[0].name in response.content)
# Check the post text is in the response
self.assertTrue(markdown.markdown(post.text) in response.content)
# Check the post date is in the response
self.assertTrue(str(post.pub_date.year) in response.content)
self.assertTrue(post.pub_date.strftime('%b') in response.content)
self.assertTrue(str(post.pub_date.day) in response.content)
# Check the link is marked up properly
self.assertTrue('<a href="http://127.0.0.1:8000/">my first blog post</a>' in response.content)

Again, this is virtually identical to the category page, adjusted to allow for the fact that we need to get a specific tag. If we now run our tests, they should fail as expected:

(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test blogengine
Creating test database for alias 'default'...
................FFF
======================================================================
FAIL: test_index (blogengine.tests.PostViewTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 540, in test_index
self.assertTrue(post_tag.name in response.content)
AssertionError: False is not true
======================================================================
FAIL: test_post_page (blogengine.tests.PostViewTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 607, in test_post_page
self.assertTrue(post_tag.name in response.content)
AssertionError: False is not true
======================================================================
FAIL: test_tag_page (blogengine.tests.PostViewTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 714, in test_tag_page
self.assertEquals(response.status_code, 200)
AssertionError: 404 != 200
----------------------------------------------------------------------
Ran 19 tests in 5.375s
FAILED (failures=3)
Destroying test database for alias 'default'...

So, we need to implement the following things:

  • Show tags on the index page
  • Show tags on the post pages
  • Create a page listing the posts with a specific tag

As we have seen already with the categories, this is actually quite simple. First, we’ll sort out the tags on the index page:

{% extends "blogengine/includes/base.html" %}
{% load custom_markdown %}
{% block content %}
{% for post in object_list %}
<div class="post">
<h1><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h1>
<h3>{{ post.pub_date }}</h3>
{{ post.text|custom_markdown }}
</div>
<a href="{{ post.category.get_absolute_url }}">{{ post.category.name }}</a>
{% for tag in post.tags.all %}
<a href="{{ tag.get_absolute_url }}">{{ tag.name }}</a>
{% endfor %}
{% endfor %}
{% if page_obj.has_previous %}
<a href="/{{ page_obj.previous_page_number }}/">Previous Page</a>
{% endif %}
{% if page_obj.has_next %}
<a href="/{{ page_obj.next_page_number }}/">Next Page</a>
{% endif %}
{% endblock %}

This is quite simple. We retrieve all the tags with post.tags.all and loop through them. We then do basically the same for the individual post pages:

{% extends "blogengine/includes/base.html" %}
{% load custom_markdown %}
{% block content %}
<div class="post">
<h1>{{ object.title }}</h1>
<h3>{{ object.pub_date }}</h3>
{{ object.text|custom_markdown }}
<a href="{{ object.category.get_absolute_url }}">{{ object.category.name }}</a>
{% for tag in post.tags.all %}
<a href="{{ tag.get_absolute_url }}">{{ tag.name }}</a>
{% endfor %}
<h4>Comments</h4>
<div class="fb-comments" data-href="http://{{ post.site }}{{ post.get_absolute_url }}" data-width="470" data-num-posts="10"></div>
</div>
{% endblock %}

This should resolve two of our outstanding tests:

(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test blogengine
Creating test database for alias 'default'...
..................F
======================================================================
FAIL: test_tag_page (blogengine.tests.PostViewTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 714, in test_tag_page
self.assertEquals(response.status_code, 200)
AssertionError: 404 != 200
----------------------------------------------------------------------
Ran 19 tests in 5.440s
FAILED (failures=1)
Destroying test database for alias 'default'...

The final test is for the tag pages. As we saw with the categories, we can limit our querysets on specific pages. So we’ll extend the ListView generic view again to handle tags:

from django.shortcuts import render
from django.views.generic import ListView
from blogengine.models import Category, Post, Tag
# Create your views here.
class CategoryListView(ListView):
def get_queryset(self):
slug = self.kwargs['slug']
try:
category = Category.objects.get(slug=slug)
return Post.objects.filter(category=category)
except Category.DoesNotExist:
return Post.objects.none()
class TagListView(ListView):
def get_queryset(self):
slug = self.kwargs['slug']
try:
tag = Tag.objects.get(slug=slug)
return tag.post_set.all()
except Tag.DoesNotExist:
return Post.objects.none()

Note that here, Tag objects have access to their assigned Post objects - we just use post_set to refer to them and get all of the posts associated with that tag. Next we’ll add the URLconfs:

from django.conf.urls import patterns, url
from django.views.generic import ListView, DetailView
from blogengine.models import Post, Category, Tag
from blogengine.views import CategoryListView, TagListView
urlpatterns = patterns('',
# Index
url(r'^(?P<page>\d+)?/?$', ListView.as_view(
model=Post,
paginate_by=5,
)),
# Individual posts
url(r'^(?P<pub_date__year>\d{4})/(?P<pub_date__month>\d{1,2})/(?P<slug>[a-zA-Z0-9-]+)/?$', DetailView.as_view(
model=Post,
)),
# Categories
url(r'^category/(?P<slug>[a-zA-Z0-9-]+)/?$', CategoryListView.as_view(
paginate_by=5,
model=Category,
)),
# Tags
url(r'^tag/(?P<slug>[a-zA-Z0-9-]+)/?$', TagListView.as_view(
paginate_by=5,
model=Tag,
)),
)

We import the Tag model and the TagListView view, and use them to set up the tag page.

If we now run our tests again, they should pass:

(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test blogengine
Creating test database for alias 'default'...
...................
----------------------------------------------------------------------
Ran 19 tests in 5.473s
OK
Destroying test database for alias 'default'...

Well done! Time to commit:

$ git add templates/ blogengine/
$ git commit -m 'Tags are now shown'

RSS Feed

For the final task today, we’ll implement an RSS feed for our posts. Django ships with a handy syndication framework that makes it easy to implement this kind of functionality.

As usual, we’ll create some tests first. In this case, we won’t be adding any new models, so we don’t need to test them. Instead we can jump straight into creating acceptance tests for our feed. For now we’ll just create one type of feed: a feed of all the blog posts. In a later instalment we’ll add feeds for categories and tags.

First of all, we’ll implement our test. Now, in order to test our feed, we need to have a solution in place for parsing an RSS feed. Django won’t do this natively, so we’ll install the feedparser Python module. Run the following commands:

$ pip install feedparser
$ pip freeze > requirements.txt

Once that’s done, feedparser should be available. You may wish to refer to the documentation as we go to help.

Let’s write our test for the RSS feed. First, we import feedparser near the top of the file:

import feedparser

Then we define a new class for our feed tests. Put this towards the end of the file - I put it just before the flat page tests:

class FeedTest(BaseAcceptanceTest):
def test_all_post_feed(self):
# Create the category
category = Category()
category.name = 'python'
category.description = 'The Python programming language'
category.save()
# Create the tag
tag = Tag()
tag.name = 'python'
tag.description = 'The Python programming language'
tag.save()
# Create the author
author = User.objects.create_user('testuser', 'user@example.com', 'password')
author.save()
# Create the site
site = Site()
site.name = 'example.com'
site.domain = 'example.com'
site.save()
# Create a post
post = Post()
post.title = 'My first post'
post.text = 'This is my first blog post'
post.slug = 'my-first-post'
post.pub_date = timezone.now()
post.author = author
post.site = site
post.category = category
# Save it
post.save()
# Add the tag
post.tags.add(tag)
post.save()
# Check we can find it
all_posts = Post.objects.all()
self.assertEquals(len(all_posts), 1)
only_post = all_posts[0]
self.assertEquals(only_post, post)
# Fetch the feed
response = self.client.get('/feeds/posts/')
self.assertEquals(response.status_code, 200)
# Parse the feed
feed = feedparser.parse(response.content)
# Check length
self.assertEquals(len(feed.entries), 1)
# Check post retrieved is the correct one
feed_post = feed.entries[0]
self.assertEquals(feed_post.title, post.title)
self.assertEquals(feed_post.description, post.text)

Run the tests and you’ll see something like this:

(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test blogengine
Creating test database for alias 'default'...
..............F.....
======================================================================
FAIL: test_all_post_feed (blogengine.tests.FeedTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 781, in test_all_post_feed
self.assertEquals(response.status_code, 200)
AssertionError: 404 != 200
----------------------------------------------------------------------
Ran 20 tests in 6.743s
FAILED (failures=1)
Destroying test database for alias 'default'...

We’re getting a 404 error because the post feed isn’t implemented. So let’s implement it. We’re going to use Django’s syndication framework, which will make it easy, but we need to enable it. Open up django_tutorial_blog_ng/settings/py and add the following under INSTALLED_APPS:

    'django.contrib.syndication',

Next, we need to enable the URLconf for this RSS feed. Open up blogengine/urls.py and amend the import fromblogengine.views` near the top:

from blogengine.views import CategoryListView, TagListView, PostsFeed

Further down, add in the following code to define the URL for the feed:

# Post RSS feed
url(r'^feeds/posts/$', PostsFeed()),

Note that we imported the PostsFeed class, but that hasn’t yet been defined. So we need to do that. First of all, add this line near the top:

from django.contrib.syndication.views import Feed

This imports the syndication views - yes, they’re another generic view! Our PostsFeed class is going to inherit from Feed. Next, we define the class:

class PostsFeed(Feed):
title = "RSS feed - posts"
link = "feeds/posts/"
description = "RSS feed - blog posts"
def items(self):
return Post.objects.order_by('-pub_date')
def item_title(self, item):
return item.title
def item_description(self, item):
return item.text

This is fairly straightforward. We define our title, link, and description for the feed inside the class definition. We define the items method which sets what items are returned by the RSS feed. We also define the item_title and item_description methods.

Now, if we run our tests, they should pass:

(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test blogengine
Creating test database for alias 'default'...
....................
----------------------------------------------------------------------
Ran 20 tests in 5.933s
OK
Destroying test database for alias 'default'...

Let’s commit our changes:

$ git add blogengine/ django_tutorial_blog_ng/ requirements.txt
$ git commit -m 'RSS feed implemented'

And that’s enough for now. Don’t forget, you can get the code for this lesson by cloning the repository from Github and running git checkout lesson-4 to switch to this lesson.

Next time we’ll:

  • Implement a search system
  • Add feeds for categories and posts
  • Tidy up the user interface

Hope to see you then!

25th January 2014 11:38 am

My First Yeoman Generator

At work I use the Skeleton boilerplate a lot - my boss, who handles most of the design work, likes it and generally uses it for his designs. I’ve also been using Grunt a lot lately, so it was inevitable that I’d probably start to look for a Yeoman generator for working with it.

There was an existing Yeoman generator for Skeleton, but it didn’t really do what I wanted. I wanted something that:

  • Included jQuery and Modernizr
  • Automatically concatenates and minifies all the JavaScript and CSS
  • Will automatically rebuild on changes
  • Includes LiveReload and a development server
  • Includes automatic deployment via FTP

After looking through the documentation for Yeoman, it was actually quick and easy to throw together my own generator and put it up. It’s available here, and the GitHub repository is here.

Future plans for it include:

  • Adding auto-prefixing for CSS
  • Removing redundant CSS rules automatically
  • Possibly, alternate deployment methods

Frustratingly, NPM seems to be playing up at present - it’s not picking up the README file, and the Yeoman site isn’t pulling it in. Any idea why, anyone?

3rd January 2014 12:57 pm

Django Blog Tutorial - the Next Generation - Part 3

Hello again! In this instalment, we’re going to do the following:

  • Add support for flat pages
  • Add support for multiple authors
  • Add a third-party comment system

Flat pages

Django ships with a number of useful apps - we’ve already used the admin interface. The flat pages app is another very handy app that comes with Django, and we’ll use it to allow the blog author to create a handful of flat pages.

First of all, you’ll need to install the flatpages app. Edit the INSTALLED_APPS setting as follows:

INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'south',
'blogengine',
'django.contrib.sites',
'django.contrib.flatpages',
)

Note that we needed to enable the sites framework as well. You’ll also need to set the SITE_ID setting:

SITE_ID = 1

With that done, run python manage.py syncdb to create the required database tables. Now, let’s use the sqlall command to take a look at the database structure generated for the flat pages:

BEGIN;
CREATE TABLE "django_flatpage_sites" (
"id" integer NOT NULL PRIMARY KEY,
"flatpage_id" integer NOT NULL,
"site_id" integer NOT NULL REFERENCES "django_site" ("id"),
UNIQUE ("flatpage_id", "site_id")
)
;
CREATE TABLE "django_flatpage" (
"id" integer NOT NULL PRIMARY KEY,
"url" varchar(100) NOT NULL,
"title" varchar(200) NOT NULL,
"content" text NOT NULL,
"enable_comments" bool NOT NULL,
"template_name" varchar(70) NOT NULL,
"registration_required" bool NOT NULL
)
;
CREATE INDEX "django_flatpage_sites_872c4601" ON "django_flatpage_sites" ("flatpage_id");
CREATE INDEX "django_flatpage_sites_99732b5c" ON "django_flatpage_sites" ("site_id");
CREATE INDEX "django_flatpage_c379dc61" ON "django_flatpage" ("url");
COMMIT;

As mentioned previously, all models in Django have an id attribute by default. Each flat page also has a URL, title, and content.

Also note the separate django_flatpage_sites table, which maps sites to flat pages. Django can run multiple sites from the same web app, and so flat pages must be allocated to a specific site. This relationship is a many-to-many relationship, so one flat page can appear on more than one site.

The other fields are hidden by default in the admin and can be ignored. Let’s have a go with Django’s handy shell to explore the flatpage. Run python manage.py shell and you’ll be able to interact with your Django application interactively:

(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py shell
Python 2.7.6 (default, Nov 23 2013, 13:53:45)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from django.contrib.flatpages.models import *
>>> FlatPage
<class 'django.contrib.flatpages.models.FlatPage'>
>>> from django.contrib.sites.models import Site
>>> Site.objects.all()
[<Site: example.com>]

As you can see, flatpages is a Django app similar to the blogengine one, with its own models, as is sites. You can see that the FlatPage class is a model. We can create an instance of it and save it interactively:

>>> f = FlatPage()
>>> f.url = '/about/'
>>> f.title = 'About me'
>>> f.content = 'All about me'
>>> f.save()
>>> f.sites.add(Site.objects.all()[0])
>>> f.save()

Note that because the relationship between the site and the flat page is a many-to-many relationship, we need to save it first, then use the add method to add the site to the list of sites.

We can retrieve it:

>>> FlatPage.objects.all()
[<FlatPage: /about/ -- About me>]
>>> FlatPage.objects.all()[0]
<FlatPage: /about/ -- About me>
>>> FlatPage.objects.all()[0].title
u'About me'

This command is often handy for debugging problems with your models interactively. If you now run the server and visit the admin, you should notice that the Flatpages app is now visible there, and the ‘About me’ flat page is now shown in there.

Let’s also take a look at the SQL required for the Site model. Run python manage.py sqlall sites:

BEGIN;
CREATE TABLE "django_site" (
"id" integer NOT NULL PRIMARY KEY,
"domain" varchar(100) NOT NULL,
"name" varchar(50) NOT NULL
)
;
COMMIT;

Again, very simple - just a domain and a name.

So, now that we have a good idea of how the flat page system works, we can write a test for it. We don’t need to write unit tests for the model because Django already does that, but we do need to write an acceptance test to ensure we can create flat pages and they will be where we expect them to be. Add the following to the top of the test file:

from django.contrib.flatpages.models import FlatPage
from django.contrib.sites.models import Site

Now, before we write this test, there’s some duplication to resolve. We have two tests that subclass LiveServerTestCase, and both have the same method, setUp. We can save ourselves some hassle by creating a new class containing this method and having both these tests inherit from it. We’ll do that now because the flat page test can also be based on it. Create the following class just after PostTest:

class BaseAcceptanceTest(LiveServerTestCase):
def setUp(self):
self.client = Client()

Then remove the setUp method from each of the two tests based on LiveServerTestCase, and change their parent class to BaseAcceptanceTest:

class AdminTest(BaseAcceptanceTest):
class PostViewTest(BaseAcceptanceTest):

With that done, run the tests and they should pass. Commit your changes:

$ git add blogengine/tests.py django_tutorial_blog_ng/settings.py
$ git commit -m 'Added flatpages to installed apps'

Now we can get started in earnest on our test for the flat pages:

class FlatPageViewTest(BaseAcceptanceTest):
def test_create_flat_page(self):
# Create flat page
page = FlatPage()
page.url = '/about/'
page.title = 'About me'
page.content = 'All about me'
page.save()
# Add the site
page.sites.add(Site.objects.all()[0])
page.save()
# Check new page saved
all_pages = FlatPage.objects.all()
self.assertEquals(len(all_pages), 1)
only_page = all_pages[0]
self.assertEquals(only_page, page)
# Check data correct
self.assertEquals(only_page.url, '/about/')
self.assertEquals(only_page.title, 'About me')
self.assertEquals(only_page.content, 'All about me')
# Get URL
page_url = only_page.get_absolute_url()
# Get the page
response = self.client.get(page_url)
self.assertEquals(response.status_code, 200)
# Check title and content in response
self.assertTrue('About me' in response.content)
self.assertTrue('All about me' in response.content)

Let’s run our tests:

(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test
Creating test database for alias 'default'...
......F..
======================================================================
FAIL: test_create_flat_page (blogengine.tests.FlatPageViewTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 272, in test_create_flat_page
self.assertEquals(response.status_code, 200)
AssertionError: 404 != 200
----------------------------------------------------------------------
Ran 9 tests in 2.760s
FAILED (failures=1)

We can see why it’s failed - in our flat page test, the status code is 404, indicating the page was not found. This just means we haven’t put flat page support into our URLconf. So let’s fix that:

from django.conf.urls import patterns, include, url
from django.contrib import admin
admin.autodiscover()
urlpatterns = patterns('',
# Examples:
# url(r'^$', 'django_tutorial_blog_ng.views.home', name='home'),
# url(r'^blog/', include('blog.urls')),
url(r'^admin/', include(admin.site.urls)),
# Blog URLs
url(r'', include('blogengine.urls')),
# Flat pages
url(r'', include('django.contrib.flatpages.urls')),
)

Let’s run our tests again:

(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test
Creating test database for alias 'default'...
......E..
======================================================================
ERROR: test_create_flat_page (blogengine.tests.FlatPageViewTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 276, in test_create_flat_page
response = self.client.get(page_url)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/test/client.py", line 473, in get
response = super(Client, self).get(path, data=data, **extra)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/test/client.py", line 280, in get
return self.request(**r)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/test/client.py", line 444, in request
six.reraise(*exc_info)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/core/handlers/base.py", line 114, in get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/contrib/flatpages/views.py", line 45, in flatpage
return render_flatpage(request, f)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/utils/decorators.py", line 99, in _wrapped_view
response = view_func(request, *args, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/contrib/flatpages/views.py", line 60, in render_flatpage
t = loader.get_template(DEFAULT_TEMPLATE)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/template/loader.py", line 138, in get_template
template, origin = find_template(template_name)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/template/loader.py", line 131, in find_template
raise TemplateDoesNotExist(name)
TemplateDoesNotExist: flatpages/default.html
----------------------------------------------------------------------
Ran 9 tests in 3.557s
FAILED (errors=1)
Destroying test database for alias 'default'...

Our test still fails, but we can easily see why - the template flatpages/default.html doesn’t exist. So we create it:

{% extends "blogengine/includes/base.html" %}
{% load custom_markdown %}
{% block content %}
<div class="post">
<h1>{{ flatpage.title }}</h1>
{{ flatpage.content|custom_markdown }}
</div>
{% endblock %}

This template is based on the blog post one, and just changes a handful of variable names. Note that it can still inherit from the blogengine base template, and in this case we’re using that for the sake of consistency.

If you run your tests, you should now see that they pass, so we’ll commit our changes:

$ git add templates/ django_tutorial_blog_ng/ blogengine/
$ git commit -m 'Implemented flat page support'

Multiple authors

Next we’ll add support for multiple authors. Now, Django already has a User model, and we’ll leverage that to represent the authors. But first we’ll write our test:

from django.test import TestCase, LiveServerTestCase, Client
from django.utils import timezone
from blogengine.models import Post
from django.contrib.flatpages.models import FlatPage
from django.contrib.sites.models import Site
from django.contrib.auth.models import User
import markdown
# Create your tests here.
class PostTest(TestCase):
def test_create_post(self):
# Create the author
author = User.objects.create_user('testuser', 'user@example.com', 'password')
author.save()
# Create the post
post = Post()
# Set the attributes
post.title = 'My first post'
post.text = 'This is my first blog post'
post.slug = 'my-first-post'
post.pub_date = timezone.now()
post.author = author
# Save it
post.save()
# Check we can find it
all_posts = Post.objects.all()
self.assertEquals(len(all_posts), 1)
only_post = all_posts[0]
self.assertEquals(only_post, post)
# Check attributes
self.assertEquals(only_post.title, 'My first post')
self.assertEquals(only_post.text, 'This is my first blog post')
self.assertEquals(only_post.slug, 'my-first-post')
self.assertEquals(only_post.pub_date.day, post.pub_date.day)
self.assertEquals(only_post.pub_date.month, post.pub_date.month)
self.assertEquals(only_post.pub_date.year, post.pub_date.year)
self.assertEquals(only_post.pub_date.hour, post.pub_date.hour)
self.assertEquals(only_post.pub_date.minute, post.pub_date.minute)
self.assertEquals(only_post.pub_date.second, post.pub_date.second)
self.assertEquals(only_post.author.username, 'testuser')
self.assertEquals(only_post.author.email, 'user@example.com')
class BaseAcceptanceTest(LiveServerTestCase):
def setUp(self):
self.client = Client()
class AdminTest(BaseAcceptanceTest):
fixtures = ['users.json']
def test_login(self):
# Get login page
response = self.client.get('/admin/')
# Check response code
self.assertEquals(response.status_code, 200)
# Check 'Log in' in response
self.assertTrue('Log in' in response.content)
# Log the user in
self.client.login(username='bobsmith', password="password")
# Check response code
response = self.client.get('/admin/')
self.assertEquals(response.status_code, 200)
# Check 'Log out' in response
self.assertTrue('Log out' in response.content)
def test_logout(self):
# Log in
self.client.login(username='bobsmith', password="password")
# Check response code
response = self.client.get('/admin/')
self.assertEquals(response.status_code, 200)
# Check 'Log out' in response
self.assertTrue('Log out' in response.content)
# Log out
self.client.logout()
# Check response code
response = self.client.get('/admin/')
self.assertEquals(response.status_code, 200)
# Check 'Log in' in response
self.assertTrue('Log in' in response.content)
def test_create_post(self):
# Log in
self.client.login(username='bobsmith', password="password")
# Check response code
response = self.client.get('/admin/blogengine/post/add/')
self.assertEquals(response.status_code, 200)
# Create the new post
response = self.client.post('/admin/blogengine/post/add/', {
'title': 'My first post',
'text': 'This is my first post',
'pub_date_0': '2013-12-28',
'pub_date_1': '22:00:04',
'slug': 'my-first-post'
},
follow=True
)
self.assertEquals(response.status_code, 200)
# Check added successfully
self.assertTrue('added successfully' in response.content)
# Check new post now in database
all_posts = Post.objects.all()
self.assertEquals(len(all_posts), 1)
def test_edit_post(self):
# Create the author
author = User.objects.create_user('testuser', 'user@example.com', 'password')
author.save()
# Create the post
post = Post()
post.title = 'My first post'
post.text = 'This is my first blog post'
post.slug = 'my-first-post'
post.pub_date = timezone.now()
post.author = author
post.save()
# Log in
self.client.login(username='bobsmith', password="password")
# Edit the post
response = self.client.post('/admin/blogengine/post/1/', {
'title': 'My second post',
'text': 'This is my second blog post',
'pub_date_0': '2013-12-28',
'pub_date_1': '22:00:04',
'slug': 'my-second-post'
},
follow=True
)
self.assertEquals(response.status_code, 200)
# Check changed successfully
self.assertTrue('changed successfully' in response.content)
# Check post amended
all_posts = Post.objects.all()
self.assertEquals(len(all_posts), 1)
only_post = all_posts[0]
self.assertEquals(only_post.title, 'My second post')
self.assertEquals(only_post.text, 'This is my second blog post')
def test_delete_post(self):
# Create the author
author = User.objects.create_user('testuser', 'user@example.com', 'password')
author.save()
# Create the post
post = Post()
post.title = 'My first post'
post.text = 'This is my first blog post'
post.slug = 'my-first-post'
post.pub_date = timezone.now()
post.author = author
post.save()
# Check new post saved
all_posts = Post.objects.all()
self.assertEquals(len(all_posts), 1)
# Log in
self.client.login(username='bobsmith', password="password")
# Delete the post
response = self.client.post('/admin/blogengine/post/1/delete/', {
'post': 'yes'
}, follow=True)
self.assertEquals(response.status_code, 200)
# Check deleted successfully
self.assertTrue('deleted successfully' in response.content)
# Check post amended
all_posts = Post.objects.all()
self.assertEquals(len(all_posts), 0)
class PostViewTest(BaseAcceptanceTest):
def test_index(self):
# Create the author
author = User.objects.create_user('testuser', 'user@example.com', 'password')
author.save()
# Create the post
post = Post()
post.title = 'My first post'
post.text = 'This is [my first blog post](http://127.0.0.1:8000/)'
post.slug = 'my-first-post'
post.pub_date = timezone.now()
post.author = author
post.save()
# Check new post saved
all_posts = Post.objects.all()
self.assertEquals(len(all_posts), 1)
# Fetch the index
response = self.client.get('/')
self.assertEquals(response.status_code, 200)
# Check the post title is in the response
self.assertTrue(post.title in response.content)
# Check the post text is in the response
self.assertTrue(markdown.markdown(post.text) in response.content)
# Check the post date is in the response
self.assertTrue(str(post.pub_date.year) in response.content)
self.assertTrue(post.pub_date.strftime('%b') in response.content)
self.assertTrue(str(post.pub_date.day) in response.content)
# Check the link is marked up properly
self.assertTrue('<a href="http://127.0.0.1:8000/">my first blog post</a>' in response.content)
def test_post_page(self):
# Create the author
author = User.objects.create_user('testuser', 'user@example.com', 'password')
author.save()
# Create the post
post = Post()
post.title = 'My first post'
post.text = 'This is [my first blog post](http://127.0.0.1:8000/)'
post.slug = 'my-first-post'
post.pub_date = timezone.now()
post.author = author
post.save()
# Check new post saved
all_posts = Post.objects.all()
self.assertEquals(len(all_posts), 1)
only_post = all_posts[0]
self.assertEquals(only_post, post)
# Get the post URL
post_url = only_post.get_absolute_url()
# Fetch the post
response = self.client.get(post_url)
self.assertEquals(response.status_code, 200)
# Check the post title is in the response
self.assertTrue(post.title in response.content)
# Check the post text is in the response
self.assertTrue(markdown.markdown(post.text) in response.content)
# Check the post date is in the response
self.assertTrue(str(post.pub_date.year) in response.content)
self.assertTrue(post.pub_date.strftime('%b') in response.content)
self.assertTrue(str(post.pub_date.day) in response.content)
# Check the link is marked up properly
self.assertTrue('<a href="http://127.0.0.1:8000/">my first blog post</a>' in response.content)
class FlatPageViewTest(BaseAcceptanceTest):
def test_create_flat_page(self):
# Create flat page
page = FlatPage()
page.url = '/about/'
page.title = 'About me'
page.content = 'All about me'
page.save()
# Add the site
page.sites.add(Site.objects.all()[0])
page.save()
# Check new page saved
all_pages = FlatPage.objects.all()
self.assertEquals(len(all_pages), 1)
only_page = all_pages[0]
self.assertEquals(only_page, page)
# Check data correct
self.assertEquals(only_page.url, '/about/')
self.assertEquals(only_page.title, 'About me')
self.assertEquals(only_page.content, 'All about me')
# Get URL
page_url = str(only_page.get_absolute_url())
# Get the page
response = self.client.get(page_url)
self.assertEquals(response.status_code, 200)
# Check title and content in response
self.assertTrue('About me' in response.content)
self.assertTrue('All about me' in response.content)

Here we create a User object to represent the author. Note the create_user convenience method for creating new users quickly and easily.

We’re going to exclude the author field from the admin - instead it’s going to be automatically populated based on the session data, so that when a user creates a post they are automatically set as the author. We therefore don’t need to make any changes for the acceptance tests for posts - our changes to the unit tests for the Post model are sufficient.

Run the tests, and they should fail:

(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test
Creating test database for alias 'default'...
E........
======================================================================
ERROR: test_create_post (blogengine.tests.PostTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 45, in test_create_post
self.assertEquals(only_post.author.username, 'testuser')
AttributeError: 'Post' object has no attribute 'author'
----------------------------------------------------------------------
Ran 9 tests in 3.620s
FAILED (errors=1)
Destroying test database for alias 'default'...

Let’s add the missing author attribute:

from django.db import models
from django.contrib.auth.models import User
# Create your models here.
class Post(models.Model):
title = models.CharField(max_length=200)
pub_date = models.DateTimeField()
text = models.TextField()
slug = models.SlugField(max_length=40, unique=True)
author = models.ForeignKey(User)
def get_absolute_url(self):
return "/%s/%s/%s/" % (self.pub_date.year, self.pub_date.month, self.slug)
def __unicode__(self):
return self.title
class Meta:
ordering = ["-pub_date"]

Next, create the migrations:

$ python manage.py schemamigration --auto blogengine

You’ll be prompted to either quit or provide a default author ID - select option 2 to provide the ID, then enter 1, which should be your own user account ID. Then run the migrations:

$ python manage.py migrate

Let’s run our tests again:

(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test
Creating test database for alias 'default'...
.F.F.....
======================================================================
FAIL: test_create_post (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 118, in test_create_post
self.assertTrue('added successfully' in response.content)
AssertionError: False is not true
======================================================================
FAIL: test_edit_post (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 154, in test_edit_post
self.assertTrue('changed successfully' in response.content)
AssertionError: False is not true
----------------------------------------------------------------------
Ran 9 tests in 3.390s
FAILED (failures=2)
Destroying test database for alias 'default'...

Our test still fails because the author field isn’t set automatically. So we’ll amend the admin to automatically set the author when the Post object is saved:

import models
from django.contrib import admin
from django.contrib.auth.models import User
class PostAdmin(admin.ModelAdmin):
prepopulated_fields = {"slug": ("title",)}
exclude = ('author',)
def save_model(self, request, obj, form, change):
obj.author = request.user
obj.save()
admin.site.register(models.Post, PostAdmin)

This tells the admin to exclude the author field from any form for a post, and when the model is saved, to set the author to the user making the HTTP request. Now run the tests, and they should pass:

(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test
Creating test database for alias 'default'...
.........
----------------------------------------------------------------------
Ran 9 tests in 4.086s
OK
Destroying test database for alias 'default'...

Time to commit again:

$ git add blogengine/
$ git commit -m 'Added author field'

Comments

The previous version of this tutorial implemented comments using Django’s own comment system. However, this has since been deprecated from Django and turned into a separate project. So we have two options for how to implement comments:

Now, if you want to use the Django comment system, you can do so, and it shouldn’t be too hard to puzzle out how to implement it using the documentation and my prior post. However, in my humble opinion, using a third-party comment system is the way to go for blog comments - they make it extremely easy for people to log in with multiple services without you having to write lots of additional code. They also make it significantly easier to moderate comments, and they’re generally pretty good at handling comment spam.

Some of the available providers include:

For demonstration purposes, we’ll use Facebook comments, but this shouldn’t require much work to adapt it to the other providers.

First of all, we need to include the Facebook JavaScript SDK:

<!-- Add your site or application content here -->
<div id="fb-root"></div>
<script>(function(d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/en_GB/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));</script>
<div class="navbar navbar-static-top navbar-inverse">
<div class="navbar-inner">
<div class="container">
<a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</a>
<a class="brand" href="/">My Django Blog</a>
<div class="nav-collapse collapse">
</div>
</div>
</div>
</div>

Now, the Facebook comment system requires that you pass through the absolute page URL when initialising the comments. At present we can’t do that without hard-coding the domain name in our template, which we want to avoid. So, we need to add a site field to each post to identify the site it’s associated with.

As usual, we update our tests first:

from django.test import TestCase, LiveServerTestCase, Client
from django.utils import timezone
from blogengine.models import Post
from django.contrib.flatpages.models import FlatPage
from django.contrib.sites.models import Site
from django.contrib.auth.models import User
import markdown
# Create your tests here.
class PostTest(TestCase):
def test_create_post(self):
# Create the author
author = User.objects.create_user('testuser', 'user@example.com', 'password')
author.save()
# Create the site
site = Site()
site.name = 'example.com'
site.domain = 'example.com'
site.save()
# Create the post
post = Post()
# Set the attributes
post.title = 'My first post'
post.text = 'This is my first blog post'
post.slug = 'my-first-post'
post.pub_date = timezone.now()
post.author = author
post.site = site
# Save it
post.save()
# Check we can find it
all_posts = Post.objects.all()
self.assertEquals(len(all_posts), 1)
only_post = all_posts[0]
self.assertEquals(only_post, post)
# Check attributes
self.assertEquals(only_post.title, 'My first post')
self.assertEquals(only_post.text, 'This is my first blog post')
self.assertEquals(only_post.slug, 'my-first-post')
self.assertEquals(only_post.site.name, 'example.com')
self.assertEquals(only_post.site.domain, 'example.com')
self.assertEquals(only_post.pub_date.day, post.pub_date.day)
self.assertEquals(only_post.pub_date.month, post.pub_date.month)
self.assertEquals(only_post.pub_date.year, post.pub_date.year)
self.assertEquals(only_post.pub_date.hour, post.pub_date.hour)
self.assertEquals(only_post.pub_date.minute, post.pub_date.minute)
self.assertEquals(only_post.pub_date.second, post.pub_date.second)
self.assertEquals(only_post.author.username, 'testuser')
self.assertEquals(only_post.author.email, 'user@example.com')
class BaseAcceptanceTest(LiveServerTestCase):
def setUp(self):
self.client = Client()
class AdminTest(BaseAcceptanceTest):
fixtures = ['users.json']
def test_login(self):
# Get login page
response = self.client.get('/admin/')
# Check response code
self.assertEquals(response.status_code, 200)
# Check 'Log in' in response
self.assertTrue('Log in' in response.content)
# Log the user in
self.client.login(username='bobsmith', password="password")
# Check response code
response = self.client.get('/admin/')
self.assertEquals(response.status_code, 200)
# Check 'Log out' in response
self.assertTrue('Log out' in response.content)
def test_logout(self):
# Log in
self.client.login(username='bobsmith', password="password")
# Check response code
response = self.client.get('/admin/')
self.assertEquals(response.status_code, 200)
# Check 'Log out' in response
self.assertTrue('Log out' in response.content)
# Log out
self.client.logout()
# Check response code
response = self.client.get('/admin/')
self.assertEquals(response.status_code, 200)
# Check 'Log in' in response
self.assertTrue('Log in' in response.content)
def test_create_post(self):
# Log in
self.client.login(username='bobsmith', password="password")
# Check response code
response = self.client.get('/admin/blogengine/post/add/')
self.assertEquals(response.status_code, 200)
# Create the new post
response = self.client.post('/admin/blogengine/post/add/', {
'title': 'My first post',
'text': 'This is my first post',
'pub_date_0': '2013-12-28',
'pub_date_1': '22:00:04',
'slug': 'my-first-post',
'site': '1'
},
follow=True
)
self.assertEquals(response.status_code, 200)
# Check added successfully
self.assertTrue('added successfully' in response.content)
# Check new post now in database
all_posts = Post.objects.all()
self.assertEquals(len(all_posts), 1)
def test_edit_post(self):
# Create the author
author = User.objects.create_user('testuser', 'user@example.com', 'password')
author.save()
# Create the site
site = Site()
site.name = 'example.com'
site.domain = 'example.com'
site.save()
# Create the post
post = Post()
post.title = 'My first post'
post.text = 'This is my first blog post'
post.slug = 'my-first-post'
post.pub_date = timezone.now()
post.author = author
post.site = site
post.save()
# Log in
self.client.login(username='bobsmith', password="password")
# Edit the post
response = self.client.post('/admin/blogengine/post/1/', {
'title': 'My second post',
'text': 'This is my second blog post',
'pub_date_0': '2013-12-28',
'pub_date_1': '22:00:04',
'slug': 'my-second-post',
'site': '1'
},
follow=True
)
self.assertEquals(response.status_code, 200)
# Check changed successfully
self.assertTrue('changed successfully' in response.content)
# Check post amended
all_posts = Post.objects.all()
self.assertEquals(len(all_posts), 1)
only_post = all_posts[0]
self.assertEquals(only_post.title, 'My second post')
self.assertEquals(only_post.text, 'This is my second blog post')
def test_delete_post(self):
# Create the author
author = User.objects.create_user('testuser', 'user@example.com', 'password')
author.save()
# Create the site
site = Site()
site.name = 'example.com'
site.domain = 'example.com'
site.save()
# Create the post
post = Post()
post.title = 'My first post'
post.text = 'This is my first blog post'
post.slug = 'my-first-post'
post.pub_date = timezone.now()
post.site = site
post.author = author
post.save()
# Check new post saved
all_posts = Post.objects.all()
self.assertEquals(len(all_posts), 1)
# Log in
self.client.login(username='bobsmith', password="password")
# Delete the post
response = self.client.post('/admin/blogengine/post/1/delete/', {
'post': 'yes'
}, follow=True)
self.assertEquals(response.status_code, 200)
# Check deleted successfully
self.assertTrue('deleted successfully' in response.content)
# Check post amended
all_posts = Post.objects.all()
self.assertEquals(len(all_posts), 0)
class PostViewTest(BaseAcceptanceTest):
def test_index(self):
# Create the author
author = User.objects.create_user('testuser', 'user@example.com', 'password')
author.save()
# Create the site
site = Site()
site.name = 'example.com'
site.domain = 'example.com'
site.save()
# Create the post
post = Post()
post.title = 'My first post'
post.text = 'This is [my first blog post](http://127.0.0.1:8000/)'
post.slug = 'my-first-post'
post.pub_date = timezone.now()
post.author = author
post.site = site
post.save()
# Check new post saved
all_posts = Post.objects.all()
self.assertEquals(len(all_posts), 1)
# Fetch the index
response = self.client.get('/')
self.assertEquals(response.status_code, 200)
# Check the post title is in the response
self.assertTrue(post.title in response.content)
# Check the post text is in the response
self.assertTrue(markdown.markdown(post.text) in response.content)
# Check the post date is in the response
self.assertTrue(str(post.pub_date.year) in response.content)
self.assertTrue(post.pub_date.strftime('%b') in response.content)
self.assertTrue(str(post.pub_date.day) in response.content)
# Check the link is marked up properly
self.assertTrue('<a href="http://127.0.0.1:8000/">my first blog post</a>' in response.content)
def test_post_page(self):
# Create the author
author = User.objects.create_user('testuser', 'user@example.com', 'password')
author.save()
# Create the site
site = Site()
site.name = 'example.com'
site.domain = 'example.com'
site.save()
# Create the post
post = Post()
post.title = 'My first post'
post.text = 'This is [my first blog post](http://127.0.0.1:8000/)'
post.slug = 'my-first-post'
post.pub_date = timezone.now()
post.author = author
post.site = site
post.save()
# Check new post saved
all_posts = Post.objects.all()
self.assertEquals(len(all_posts), 1)
only_post = all_posts[0]
self.assertEquals(only_post, post)
# Get the post URL
post_url = only_post.get_absolute_url()
# Fetch the post
response = self.client.get(post_url)
self.assertEquals(response.status_code, 200)
# Check the post title is in the response
self.assertTrue(post.title in response.content)
# Check the post text is in the response
self.assertTrue(markdown.markdown(post.text) in response.content)
# Check the post date is in the response
self.assertTrue(str(post.pub_date.year) in response.content)
self.assertTrue(post.pub_date.strftime('%b') in response.content)
self.assertTrue(str(post.pub_date.day) in response.content)
# Check the link is marked up properly
self.assertTrue('<a href="http://127.0.0.1:8000/">my first blog post</a>' in response.content)
class FlatPageViewTest(BaseAcceptanceTest):
def test_create_flat_page(self):
# Create flat page
page = FlatPage()
page.url = '/about/'
page.title = 'About me'
page.content = 'All about me'
page.save()
# Add the site
page.sites.add(Site.objects.all()[0])
page.save()
# Check new page saved
all_pages = FlatPage.objects.all()
self.assertEquals(len(all_pages), 1)
only_page = all_pages[0]
self.assertEquals(only_page, page)
# Check data correct
self.assertEquals(only_page.url, '/about/')
self.assertEquals(only_page.title, 'About me')
self.assertEquals(only_page.content, 'All about me')
# Get URL
page_url = str(only_page.get_absolute_url())
# Get the page
response = self.client.get(page_url)
self.assertEquals(response.status_code, 200)
# Check title and content in response
self.assertTrue('About me' in response.content)
self.assertTrue('All about me' in response.content)

All we’ve done here is to add the site attribute when creating a new post using the Django database API, and when we create one via the admin, we add an additional site aparameter to the HTTP POST request with a value of 1. Run the tests and they should fail:

Creating test database for alias 'default'...
E........
======================================================================
ERROR: test_create_post (blogengine.tests.PostTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 46, in test_create_post
self.assertEquals(only_post.site.name, 'example.com')
AttributeError: 'Post' object has no attribute 'site'
----------------------------------------------------------------------
Ran 9 tests in 4.313s
FAILED (errors=1)
Destroying test database for alias 'default'...

So we need to add the site attribute to the Post model. Let’s do that:

from django.db import models
from django.contrib.auth.models import User
from django.contrib.sites.models import Site
# Create your models here.
class Post(models.Model):
title = models.CharField(max_length=200)
pub_date = models.DateTimeField()
text = models.TextField()
slug = models.SlugField(max_length=40, unique=True)
author = models.ForeignKey(User)
site = models.ForeignKey(Site)
def get_absolute_url(self):
return "/%s/%s/%s/" % (self.pub_date.year, self.pub_date.month, self.slug)
def __unicode__(self):
return self.title
class Meta:
ordering = ["-pub_date"]

Now create and run the migrations - you’ll be prompted to create a default value for the site attribute as well:

(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py schemamigration --auto blogengine
? The field 'Post.site' does not have a default specified, yet is NOT NULL.
? Since you are adding this field, you MUST specify a default
? value to use for existing rows. Would you like to:
? 1. Quit now, and add a default to the field in models.py
? 2. Specify a one-off value to use for existing columns now
? Please select a choice: 2
? Please enter Python code for your one-off default value.
? The datetime module is available, so you can do e.g. datetime.date.today()
>>> 1
+ Added field site on blogengine.Post
Created 0005_auto__add_field_post_site.py. You can now apply this migration with: ./manage.py migrate blogengine
(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py migrate
Running migrations for blogengine:
- Migrating forwards to 0005_auto__add_field_post_site.
> blogengine:0005_auto__add_field_post_site
- Loading initial data for blogengine.
Installed 0 object(s) from 0 fixture(s)

Our tests should then pass:

(venv)Smith:django_tutorial_blog_ng matthewdaly$ python manage.py test
Creating test database for alias 'default'...
.........
----------------------------------------------------------------------
Ran 9 tests in 4.261s
OK
Destroying test database for alias 'default'...

Now we can include our full page URL on the post detail page:

{% extends "blogengine/includes/base.html" %}
{% load custom_markdown %}
{% block content %}
<div class="post">
<h1>{{ object.title }}</h1>
<h3>{{ object.pub_date }}</h3>
{{ object.text|custom_markdown }}
<h4>Comments</h4>
<div class="fb-comments" data-href="http://{{ post.site }}{{ post.get_absolute_url }}" data-width="470" data-num-posts="10"></div>
</div>
{% endblock %}

If you want to customise the comments, take a look at the documentation for Facebook Comments.

With that done, we can commit our changes:

$ git add blogengine/ templates/
$ git commit -m 'Implemented Facebook comments'

And that wraps up this lesson. As usual, you can easily switch to today’s lesson with git checkout lesson-3. Next time we’ll implement categories and tags, and create an RSS feed for our blog posts.

Recent Posts

Better Strings in PHP

Forcing SSL in Codeigniter

Logging to the ELK Stack With Laravel

Full-text Search With Mariadb

Building a Letter Classifier in PHP With Tesseract OCR and PHP ML

About me

I'm a web and mobile app developer based in Norfolk. My skillset includes Python, PHP and Javascript, and I have extensive experience working with CodeIgniter, Laravel, Django, Phonegap and Angular.js.