Matthew Daly's Blog

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

5th October 2014 7:56 pm

Introducing Generator-simple-static-blog

I’m a big fan of static site generators. I ditched WordPress for Octopress over two years ago because it was free to host on GitHub Pages and much faster, had much better syntax highlighting, and I liked being able to write posts in Vim, and I’ve never looked back since.

That said, Octopress is written in Ruby, a language I’ve never been that keen on. Ideally I’d prefer to use Python or JavaScript, but none of the solutions I’ve found have been to my liking. Recently I’ve been using Grunt and Yeoman to some extent, and I’ve wondered about the idea of creating a Yeoman generator to build a static blogging engine. After discovering grunt-markdown-blog, I took the plunge and have built a simple blog generator called generator-simple-static-blog.

I’ve published it to NPM, so feel free to check it out. It includes code highlighting with the Zenburn colour scheme by default (although highlight.js includes many other themes, so just switch to another one if you want), and it should be easy to edit the templates. I’ve also included the ability to deploy automatically to GitHub Pages using Grunt.

I don’t anticipate moving over to this from Octopress for the foreseeable future, but it’s been an interesting project for the weekend.

28th September 2014 8:51 pm

Django Blog Tutorial - the Next Generation - Part 9

Yes, I know the eight instalment was meant to be the last one! Within 24 hours of that post going live, Django 1.7 was released, so naturally I’d like to show you how to upgrade to it.

The biggest change is that Django 1.7 introduces its own migration system, which means South is now surplus to requirements. We therefore need to switch from South to Django’s native migrations. Fortunately, this is fairly straightforward.

First of all, activate your virtualenv:

$ virtualenv venv

Then make sure your migrations are up to date:

$ python manage.py syncdb
$ python manage.py migrate

Then, upgrade your Django version and uninstall South:

$ pip install Django --upgrade
$ pip uninstall South
$ pip freeze > requirements.txt

Next, remove South from INSTALLED_APPS in django_tutorial_blog_ng/settings.py.

You now need to delete all of the numbered migration files in blogengine/migrations/, and the relevant .pyc files, but NOT the directory or the __init__.py file. You can do so with this command on Linux or OS X:

$ rm blogengine/migrations/00*

Next, we recreate our migrations with the following command:

$ python manage.py makemigrations
Migrations for 'blogengine':
0001_initial.py:
- Create model Category
- Create model Post
- Create model Tag
- Add field tags to post

Then we run the migrations:

$ python manage.py migrate
Operations to perform:
Synchronize unmigrated apps: sitemaps, django_jenkins, debug_toolbar
Apply all migrations: sessions, admin, sites, flatpages, contenttypes, auth, blogengine
Synchronizing apps without migrations:
Creating tables...
Installing custom SQL...
Installing indexes...
Running migrations:
Applying contenttypes.0001_initial... FAKED
Applying auth.0001_initial... FAKED
Applying admin.0001_initial... FAKED
Applying sites.0001_initial... FAKED
Applying blogengine.0001_initial... FAKED
Applying flatpages.0001_initial... FAKED
Applying sessions.0001_initial... FAKED

Don’t worry too much if the output doesn’t look exactly the same as this - as long as it works, that’s the main thing.

Let’s run our test suite to ensure it works:

$ python manage.py jenkins
Creating test database for alias 'default'...
....FF.F.FFFFFF..............
======================================================================
FAIL: test_create_post (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 385, in test_create_post
self.assertTrue('added successfully' in response.content)
AssertionError: False is not true
======================================================================
FAIL: test_create_post_without_tag (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 417, in test_create_post_without_tag
self.assertTrue('added successfully' in response.content)
AssertionError: False is not true
======================================================================
FAIL: test_delete_category (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 278, in test_delete_category
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 346, in test_delete_tag
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 255, in test_edit_category
self.assertEquals(response.status_code, 200)
AssertionError: 404 != 200
======================================================================
FAIL: test_edit_post (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 447, in test_edit_post
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 323, in test_edit_tag
self.assertEquals(response.status_code, 200)
AssertionError: 404 != 200
======================================================================
FAIL: test_login (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 183, in test_login
self.assertEquals(response.status_code, 200)
AssertionError: 302 != 200
======================================================================
FAIL: test_logout (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 214, in test_logout
self.assertEquals(response.status_code, 200)
AssertionError: 302 != 200
----------------------------------------------------------------------
Ran 29 tests in 7.383s
FAILED (failures=9)
Destroying test database for alias 'default'...

We have an issue here. A load of the tests for the admin interface now fail. If we now try running the dev server, we see this error:

$ python manage.py runserver
Performing system checks...
System check identified no issues (0 silenced).
September 28, 2014 - 20:16:47
Django version 1.7, using settings 'django_tutorial_blog_ng.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
Unhandled exception in thread started by <function wrapper at 0x1024a5ed8>
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/utils/autoreload.py", line 222, in wrapper
fn(*args, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/core/management/commands/runserver.py", line 132, in inner_run
handler = self.get_handler(*args, **options)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/contrib/staticfiles/management/commands/runserver.py", line 25, in get_handler
handler = super(Command, self).get_handler(*args, **options)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/core/management/commands/runserver.py", line 48, in get_handler
return get_internal_wsgi_application()
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/core/servers/basehttp.py", line 66, in get_internal_wsgi_application
sys.exc_info()[2])
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/core/servers/basehttp.py", line 56, in get_internal_wsgi_application
return import_string(app_path)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/utils/module_loading.py", line 26, in import_string
module = import_module(module_path)
File "/usr/local/Cellar/python/2.7.8_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/importlib/__init__.py", line 37, in import_module
__import__(name)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/django_tutorial_blog_ng/wsgi.py", line 14, in <module>
from dj_static import Cling
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/dj_static.py", line 7, in <module>
from django.core.handlers.base import get_path_info
django.core.exceptions.ImproperlyConfigured: WSGI application 'django_tutorial_blog_ng.wsgi.application' could not be loaded; Error importing module: 'cannot import name get_path_info'

Fortunately, the error above is easy to fix by upgrading dj_static:

$ pip install dj_static --upgrade
$ pip freeze > requirements.txt

That resolves the error in serving static files, but not the error with the admin. If you run the dev server, you’ll be able to see that the admin actually works fine. The problem is caused by the test client not following redirects in the admin. We can easily run just the admin tests with the following command:

$ python manage.py test blogengine.tests.AdminTest
Creating test database for alias 'default'...
.FF.F.FFFFFF
======================================================================
FAIL: test_create_post (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 385, in test_create_post
self.assertTrue('added successfully' in response.content)
AssertionError: False is not true
======================================================================
FAIL: test_create_post_without_tag (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 417, in test_create_post_without_tag
self.assertTrue('added successfully' in response.content)
AssertionError: False is not true
======================================================================
FAIL: test_delete_category (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 278, in test_delete_category
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 346, in test_delete_tag
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 255, in test_edit_category
self.assertEquals(response.status_code, 200)
AssertionError: 404 != 200
======================================================================
FAIL: test_edit_post (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 447, in test_edit_post
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 323, in test_edit_tag
self.assertEquals(response.status_code, 200)
AssertionError: 404 != 200
======================================================================
FAIL: test_login (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 183, in test_login
self.assertEquals(response.status_code, 200)
AssertionError: 302 != 200
======================================================================
FAIL: test_logout (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 214, in test_logout
self.assertEquals(response.status_code, 200)
AssertionError: 302 != 200
----------------------------------------------------------------------
Ran 12 tests in 3.283s
FAILED (failures=9)
Destroying test database for alias 'default'...

Let’s commit our changes so far first:

$ git add django_tutorial_blog_ng/ requirements.txt blogengine/
$ git commit -m 'Upgraded to Django 1.7'

Now let’s fix our tests. Here’s the amended version of the AdminTest class:

class AdminTest(BaseAcceptanceTest):
fixtures = ['users.json']
def test_login(self):
# Get login page
response = self.client.get('/admin/', follow=True)
# 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/', follow=True)
self.assertEquals(response.status_code, 200)
# Check 'Log in' in response
self.assertTrue('Log in' in response.content)
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 = CategoryFactory()
# Log in
self.client.login(username='bobsmith', password="password")
# Edit the category
response = self.client.post('/admin/blogengine/category/' + str(category.pk) + '/', {
'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 = CategoryFactory()
# Log in
self.client.login(username='bobsmith', password="password")
# Delete the category
response = self.client.post('/admin/blogengine/category/' + str(category.pk) + '/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)
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 = TagFactory()
# Log in
self.client.login(username='bobsmith', password="password")
# Edit the tag
response = self.client.post('/admin/blogengine/tag/' + str(tag.pk) + '/', {
'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 = TagFactory()
# Log in
self.client.login(username='bobsmith', password="password")
# Delete the tag
response = self.client.post('/admin/blogengine/tag/' + str(tag.pk) + '/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)
def test_create_post(self):
# Create the category
category = CategoryFactory()
# Create the tag
tag = TagFactory()
# 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': str(category.pk),
'tags': str(tag.pk)
},
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_create_post_without_tag(self):
# Create the category
category = CategoryFactory()
# 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': str(category.pk)
},
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 post
post = PostFactory()
# Create the category
category = CategoryFactory()
# Create the tag
tag = TagFactory()
post.tags.add(tag)
# Log in
self.client.login(username='bobsmith', password="password")
# Edit the post
response = self.client.post('/admin/blogengine/post/' + str(post.pk) + '/', {
'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': str(category.pk),
'tags': str(tag.pk)
},
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 post
post = PostFactory()
# Create the tag
tag = TagFactory()
post.tags.add(tag)
# 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/' + str(post.pk) + '/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)

There are two main issues here. The first is that when we try to edit or delete an existing item, or refer to it when creating something else, we can no longer rely on the number representing the primary key being set to 1. So we need to specifically obtain this, rather than hard-coding it to 1. Therefore, whenever we pass through a number to represent an item (with the exception of the site, but including tags, categories and posts), we need to instead fetch its primary key and return it. So, above where we try to delete a post, we replace 1 with str(post.pk). This will solve a lot of the problems. As there’s a lot of them, I won’t go through each one, but you can see the entire class above for reference, and if you’ve followed along so far, you shouldn’t have any problems.

The other issue we need to fix is the login and logout tests. We simply add follow=True to these to ensure that the test client follows the redirects.

Let’s run our tests to make sure they pass:

$ python manage.py jenkins
Creating test database for alias 'default'...
.............................
----------------------------------------------------------------------
Ran 29 tests in 8.210s
OK
Destroying test database for alias 'default'...

With that done, you can commit your changes:

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

Don’t forget to deploy your changes:

$ fab deploy

Our blog has now been happily migrated over to Django 1.7!

28th September 2014 7:53 pm

Changing Date Format from DD/MM/YYYY to YYYY-MM-DD in Vim

Recently I had the occasion to reformat a load of dates in Vim from DD/MM/YYYY to YYYY-MM-DD. In Vim, this is quite simple:

:%s/\(\d\{2}\)\/\(\d\{2}\)\/\(\d\{4}\)/\3-\2-\1/g

This should be easy to adapt to reformatting other date formats.

31st August 2014 10:00 pm

Django Blog Tutorial - the Next Generation - Part 8

Hello again! In our final instalment, we’ll wrap up our blog by:

  • Implementing a sitemap
  • Optimising and tidying up the site
  • Creating a Fabric task for easier deployment

I’ll also cover development tools and practices that can make using Django easier. But first there’s a few housekeeping tasks that need doing…

Don’t forget to activate your virtualenv - you should know how to do this off by heart by now!

Upgrading Django

At the time of writing, Django 1.7 is due any day now, but it’s not out yet so I won’t cover it. The biggest change is the addition of a built-in migration system, but switching from South to this is well-documented. When Django 1.7 comes out, it shouldn’t be difficult to upgrade to it - because we have good test coverage, we shouldn’t have much trouble catching errors.

However, Django 1.6.6 was recently released, and we need to upgrade to it. Just enter the following command to upgrade:

$ pip install Django --upgrade

Then add it to your requirements.txt:

$ pip freeze > requirements.txt

Then commit your changes:

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

Implementing a sitemap

Creating a sitemap for your blog is a good idea - it can be submitted to search engines, so that they can easily find your content. With Django, it’s pretty straightforward too.

First, let’s create a test for our sitemap. Add the following code at the end of tests.py:

class SitemapTest(BaseAcceptanceTest):
def test_sitemap(self):
# Create a post
post = PostFactory()
# Create a flat page
page = FlatPageFactory()
# Get sitemap
response = self.client.get('/sitemap.xml')
self.assertEquals(response.status_code, 200)
# Check post is present in sitemap
self.assertTrue('my-first-post' in response.content)
# Check page is present in sitemap
self.assertTrue('/about/' in response.content)

Run it, and you should see the test fail:

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

Now, let’s implement our sitemap. The sitemap application comes with Django, and needs to be activated in your settings file, under INSTALLED_APPS:

    'django.contrib.sitemaps',

Next, let’s think about what content we want to include in the sitemap. We want to index our flat pages and our blog posts, so our sitemap should reflect that. Create a new file at blogengine/sitemap.py and enter the following text:

from django.contrib.sitemaps import Sitemap
from django.contrib.flatpages.models import FlatPage
from blogengine.models import Post
class PostSitemap(Sitemap):
changefreq = "always"
priority = 0.5
def items(self):
return Post.objects.all()
def lastmod(self, obj):
return obj.pub_date
class FlatpageSitemap(Sitemap):
changefreq = "always"
priority = 0.5
def items(self):
return FlatPage.objects.all()

We define two sitemaps, one for all the posts, and the other for all the flat pages. Note that this works in a very similar way to the syndication framework.

Next, we amend our URLs. Add the following text after the existing imports in your URL file:

from django.contrib.sitemaps.views import sitemap
from blogengine.sitemap import PostSitemap, FlatpageSitemap
# Define sitemaps
sitemaps = {
'posts': PostSitemap,
'pages': FlatpageSitemap
}

Then add the following after the existing routes:

# Sitemap
url(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
name='django.contrib.sitemaps.views.sitemap'),

Here we define what sitemaps we’re going to use, and we define a URL for them. It’s pretty straightforward to use.

Let’s run our tests:

$ python manage.py test blogengine
Creating test database for alias 'default'...
............................
----------------------------------------------------------------------
Ran 28 tests in 6.863s
OK
Destroying test database for alias 'default'...

And done! Let’s commit our changes:

$ git add blogengine/ django_tutorial_blog_ng/settings.py
$ git commit -m 'Implemented a sitemap'

Fixing test coverage

Our blog is now feature-complete, but there are a few gaps in test coverage, so we’ll fix them. If, like me, you’re using Coveralls.io, you can easily see via their web interface where there are gaps in the coverage.

Now, our gaps are all in our view file - if you take a look at my build, you can easily identify the gaps as they’re marked in red.

The first gap is where a tag does not exist. Interestingly, if we look at the code in the view, we can see that some of it is redundant:

class TagPostsFeed(PostsFeed):
def get_object(self, request, slug):
return get_object_or_404(Tag, slug=slug)
def title(self, obj):
return "RSS feed - blog posts tagged %s" % obj.name
def link(self, obj):
return obj.get_absolute_url()
def description(self, obj):
return "RSS feed - blog posts tagged %s" % obj.name
def items(self, obj):
try:
tag = Tag.objects.get(slug=obj.slug)
return tag.post_set.all()
except Tag.DoesNotExist:
return Post.objects.none()

Under the items function, we check to see if the tag exists. However, under get_object we can see that if the object didn’t exist, it would already have returned a 404 error. We can therefore safely amend items to not check, since that try statement will never fail:

class TagPostsFeed(PostsFeed):
def get_object(self, request, slug):
return get_object_or_404(Tag, slug=slug)
def title(self, obj):
return "RSS feed - blog posts tagged %s" % obj.name
def link(self, obj):
return obj.get_absolute_url()
def description(self, obj):
return "RSS feed - blog posts tagged %s" % obj.name
def items(self, obj):
tag = Tag.objects.get(slug=obj.slug)
return tag.post_set.all()

The other two gaps are in our search view - we never get an empty result for the search in the following section:

def getSearchResults(request):
"""
Search for a post by title or text
"""
# Get the query data
query = request.GET.get('q', '')
page = request.GET.get('page', 1)
# Query the database
if query:
results = Post.objects.filter(Q(text__icontains=query) | Q(title__icontains=query))
else:
results = None
# Add pagination
pages = Paginator(results, 5)
# Get specified page
try:
returned_page = pages.page(page)
except EmptyPage:
returned_page = pages.page(pages.num_pages)
# Display the search results
return render_to_response('blogengine/search_post_list.html',
{'page_obj': returned_page,
'object_list': returned_page.object_list,
'search': query})

So replace it with this:

def getSearchResults(request):
"""
Search for a post by title or text
"""
# Get the query data
query = request.GET.get('q', '')
page = request.GET.get('page', 1)
# Query the database
results = Post.objects.filter(Q(text__icontains=query) | Q(title__icontains=query))
# Add pagination
pages = Paginator(results, 5)
# Get specified page
try:
returned_page = pages.page(page)
except EmptyPage:
returned_page = pages.page(pages.num_pages)
# Display the search results
return render_to_response('blogengine/search_post_list.html',
{'page_obj': returned_page,
'object_list': returned_page.object_list,
'search': query})

We don’t need to check whether query is defined because if q is left blank, the value of query will be an empty string, so we may as well pull out the redundant code.

Finally, the other gap is for when a user tries to get an empty search page (eg, page two of something with five or less results). So let’s add another test to our SearchViewTest class:

def test_failing_search(self):
# Search for something that is not present
response = self.client.get('/search?q=wibble')
self.assertEquals(response.status_code, 200)
self.assertTrue('No posts found' in response.content)
# Try to get nonexistent second page
response = self.client.get('/search?q=wibble&page=2')
self.assertEquals(response.status_code, 200)
self.assertTrue('No posts found' in response.content)

Run our tests and check the coverage:

$ coverage run --include="blogengine/*" --omit="blogengine/migrations/*" manage.py test blogengine
$ coverage html

If you open htmlcov/index.html in your browser, you should see that the test coverage is back up to 100%. With that done, it’s time to commit again:

$ git add blogengine/
$ git commit -m 'Fixed gaps in coverage'

Remember, it’s not always possible to achieve 100% test coverage, and you shouldn’t worry too much about it if it’s not possible - it’s possible to ignore code if necessary. However, it’s a good idea to aim for 100%.

Using Fabric for deployment

Next we’ll cover using Fabric, a handy tool for deploying your changes (any pretty much any other task you want to automate). First, you need to install it:

$ pip install Fabric

If you have any problems installing it, you should be able to resolve them via Google - most of them are likely to be absent libraries that Fabric depends upon. Once it’s installed, add it to your requirements.tzt:

$ pip freeze > requirements.txt

Next, create a file called fabfile.py and enter the following text:

#!/usr/bin/env python
from fabric.api import local
def deploy():
"""
Deploy the latest version to Heroku
"""
# Push changes to master
local("git push origin master")
# Push changes to Heroku
local("git push heroku master")
# Run migrations on Heroku
local("heroku run python manage.py migrate")

Now, all this file does is push our changes to Github (or wherever else your repository is hosted) and to Heroku, and runs your migrations. It’s not a terribly big task anyway, but it’s handy to have it in place. Let’s commit our changes:

$ git add fabfile.py requirements.txt
$ git commit -m 'Added Fabric task for deployment'

Then, let’s try it out:

$ fab deploy

There, wasn’t that more convenient? Fabric is much more powerful than this simple demonstration indicates, and can run tasks on remote servers via SSH easily. I recommend you take a look at the documentation to see what else you can do with it. If you’re hosting your site on a VPS, you will probably find Fabric indispensable, as you will need to restart the application every time you push up a new revision.

Tidying up

We want our blog application to play nicely with other Django apps. For instance, say you’re working on a new site that includes a blogging engine. Wouldn’t it make sense to just be able to drop in this blogging engine and have it work immediately? At the moment, some of our URL’s are hard-coded, so we may have problems in doing so. Let’s fix that.

First we’ll amend our tests. Add this at the top of the tests file:

from django.core.urlresolvers import reverse

Next, replace every instance of this:

        response = self.client.get('/')

with this:

response = self.client.get(reverse('blogengine:index'))

Then, rewrite the calls to the search route. For instance, this:

        response = self.client.get('/search?q=first')

should become this:

        response = self.client.get(reverse('blogengine:search') + '?q=first')

I’ll leave changing these as an exercise for the reader, but check the repository if you get stuck.

Next, we need to assign a namespace to our app’s routes:

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', namespace="blogengine")),
# Flat pages
url(r'', include('django.contrib.flatpages.urls')),
)

We then assign names to our routes in the app’s urls.py:

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, PostsFeed, CategoryPostsFeed, TagPostsFeed, getSearchResults
from django.contrib.sitemaps.views import sitemap
from blogengine.sitemap import PostSitemap, FlatpageSitemap
# Define sitemaps
sitemaps = {
'posts': PostSitemap,
'pages': FlatpageSitemap
}
urlpatterns = patterns('',
# Index
url(r'^(?P<page>\d+)?/?$', ListView.as_view(
model=Post,
paginate_by=5,
),
name='index'
),
# 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,
),
name='post'
),
# Categories
url(r'^category/(?P<slug>[a-zA-Z0-9-]+)/?$', CategoryListView.as_view(
paginate_by=5,
model=Category,
),
name='category'
),
# Tags
url(r'^tag/(?P<slug>[a-zA-Z0-9-]+)/?$', TagListView.as_view(
paginate_by=5,
model=Tag,
),
name='tag'
),
# Post RSS feed
url(r'^feeds/posts/$', PostsFeed()),
# Category RSS feed
url(r'^feeds/posts/category/(?P<slug>[a-zA-Z0-9-]+)/?$', CategoryPostsFeed()),
# Tag RSS feed
url(r'^feeds/posts/tag/(?P<slug>[a-zA-Z0-9-]+)/?$', TagPostsFeed()),
# Search posts
url(r'^search', getSearchResults, name='search'),
# Sitemap
url(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
name='django.contrib.sitemaps.views.sitemap'),
)

You also need to amend two of your templates:

<!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="{% url 'blogengine:index' %}">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>
<form action="/search" method="GET" class="navbar-form navbar-left">
<div class="form-group">
<input type="text" name="q" placeholder="Search..." class="form-control"></input>
</div>
<button type="submit" class="btn btn-default">Search</button>
</form>
</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>
{% 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="{% url 'blogengine:search' %}?page={{ page_obj.previous_page_number }}&q={{ search }}">Previous Page</a></li>
{% endif %}
{% if page_obj.has_next %}
<li class="next"><a href="{% url 'blogengine:search' %}?page={{ page_obj.next_page_number }}&q={{ search }}">Next Page</a></li>
{% endif %}
</ul>
{% endblock %}

Let’s run our tests:

$ python manage.py test blogengine/
Creating test database for alias 'default'...
.............................
----------------------------------------------------------------------
Ran 29 tests in 10.456s
OK
Destroying test database for alias 'default'...

And commit our changes:

$ git add .
$ git commit -m 'Now use named routes'

Debugging Django

There are a number of handy ways to debug Django applications. One of the simplest is to use the Python debugger. To use it, just enter the following lines at the point you want to break at:

import pdb
pdb.set_trace()

Now, whenever that line of code is run, you’ll be dropped into an interactive shell that lets you play around to find out what’s going wrong. However, it doesn’t offer autocompletion, so we’ll install ipdb, which is an improved version:

$ pip install ipdb
$ pip freeze > requirements.txt

Now you can use ipdb in much the same way as you would use pdb:

import ipdb
ipdb.set_trace()

Now, ipdb is very useful, but it isn’t much help for profiling your application. For that you need the Django Debug Toolbar. Run the following commands:

$ pip install django-debug-toolbar
$ pip freeze > requirements.txt

Then add the following line to INSTALLED_APPS in your settings file:

    'debug_toolbar',

Then, try running the development server, and you’ll see a toolbar on the right-hand side of the screen that allows you to view some useful data about your page. For instance, you’ll notice a field called SQL - this contains details of the queries carried out when building the page. To actually see the queries carried out, you’ll want to disable caching in your settings file by commenting out all the constants that start with CACHE.

We won’t go into using the toolbar to optimise queries, but using this, you can easily see what queries are being executed on a specific page, how long they take, and the values they return. Sometimes, you may need to optimise a slow query - in this case, Django allows you to drop down to writing raw SQL if necessary.

Note that if you’re running Django in production, you should set DEBUG to False as otherwise it gives rather too much information to potential attackers, and with Django Debug Toolbar installed, that’s even more important.

Please also note that when you disable debug mode, Django no longer handles static files automatically, so you’ll need to run python manage.py collectstatic and commit the staticfiles directory.

Once you’ve disabled debug mode, collected the static files, and re-enables caching, you can commit your changes:

$ git add .
$ git commit -m 'Installed debugging tools'

Optimising static files

We want our blog to get the best SEO results it can, so making it fast is essential. One of the simplest things you can do is to concatenate and minify static assets such as CSS and JavaScript. There are numerous ways to do this, but I generally use Grunt. Let’s set up a Grunt config to concatenate and minify our CSS and JavaScript.

You’ll need to have Node.js installed on your development machine for this. Then, you need to install the Grunt command-line interface:

$ sudo npm install -g grunt-cli

With that done, we need to create a package.json file. You can create one using the command npm init. Here’s mine:

{
"name": "django_tutorial_blog_ng",
"version": "1.0.0",
"description": "Django Tutorial Blog NG =======================",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://github.com/matthewbdaly/django_tutorial_blog_ng.git"
},
"author": "Matthew Daly <matthew@matthewdaly.co.uk> (http://matthewdaly.co.uk/)",
"license": "ISC",
"bugs": {
"url": "https://github.com/matthewbdaly/django_tutorial_blog_ng/issues"
},
"homepage": "https://github.com/matthewbdaly/django_tutorial_blog_ng"
}

Feel free to amend it as you see fit.

Next we install Grunt and the required plugins:

$ npm install grunt grunt-contrib-cssmin grunt-contrib-concat grunt-contrib-uglify --save-dev

We now need to create a Gruntfile for our tasks:

module.exports = function (grunt) {
'use strict';
grunt.initConfig({
concat: {
dist: {
src: [
'blogengine/static/bower_components/bootstrap/dist/css/bootstrap.css',
'blogengine/static/bower_components/bootstrap/dist/css/bootstrap-theme.css',
'blogengine/static/css/code.css',
'blogengine/static/css/main.css',
],
dest: 'blogengine/static/css/style.css'
}
},
uglify: {
dist: {
src: [
'blogengine/static/bower_components/jquery/jquery.js',
'blogengine/static/bower_components/bootstrap/dist/js/bootstrap.js'
],
dest: 'blogengine/static/js/all.min.js'
}
},
cssmin: {
dist: {
src: 'blogengine/static/css/style.css',
dest: 'blogengine/static/css/style.min.css'
}
}
});
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-cssmin');
grunt.registerTask('default', ['concat', 'uglify', 'cssmin']);
};

You’ll also need to change the paths in your base HTML file to point to the minified versions:

<!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 'css/style.min.css' %}">
</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="{% url 'blogengine:index' %}">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>
<form action="/search" method="GET" class="navbar-form navbar-left">
<div class="form-group">
<input type="text" name="q" placeholder="Search..." class="form-control"></input>
</div>
<button type="submit" class="btn btn-default">Search</button>
</form>
</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="{% static 'js/all.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, run the Grunt task:

$ grunt

And collect the static files:

$ python manage.py collectstatic

You’ll also want to add your node_modules folder to your gitignore:

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

Then commit your changes:

$ git add .
$ git commit -m 'Optimised static assets'

Now, our package.json will cause a problem - it will mean that this app is mistakenly identified as a Node.js app. To prevent this, create the .slugignore file:

package.json

Then commit your changes and push them up:

$ git add .slugignore
$ git commit -m 'Added slugignore'
$ fab deploy

If you check, your site should now be loading the minified versions of the static files.

That’s our site done! As usual I’ve tagged the final commit with lesson-8.

Sadly, that’s our final instalment over with! I hope you’ve enjoyed these tutorials, and I look forward to seeing what you create with them.

25th August 2014 5:15 pm

Django Blog Tutorial - the Next Generation - Part 7

Hello once again! In this instalment we’ll cover:

  • Caching your content with Memcached to improve your site’s performance
  • Refactoring and simplifying our tests
  • Implementing additional feeds
  • Creating a simple search engine

Don’t forget to activate your virtualenv:

$ source venv/bin/activate

Now let’s get started!

Memcached

If you frequent (or used to frequent) social media sites like Reddit, Slashdot or Digg, you may be familiar with something called variously the Digg or Slashdot effect, whereby if a page gets submitted to a social media site, and subsequently becomes popular, it can be hit by a huge number of HTTP requests in a very short period of time, slowing it down or even taking the server down completely.

Now, as a general rule of thumb, for most dynamic websites such as blogs, the bottleneck is not the web server or the programming language, but the database. If you have a lot of people hitting the same page over and over again in quick succession, then you’re essentially running the same query over and over again and getting the same result each time, which is expensive in terms of processing power. What you need to be able to do is cache the results of the query in memory for a given period of time so that the number of queries is reduced.

That’s where Memcached comes in. It’s a simple key-value store that allows you to store values in memory for a given period of time so that they can be retrieved without having to query the database. Memcached is a very common choice for caching, and is by far the fastest and most efficient type of cache available for Django. It’s also available on Heroku’s free tier.

Django has a very powerful caching framework that supports numerous types of cache in addition to Memcached, such as:

  • Database caching
  • Filesystem caching
  • Local memory caching

There are also third-party backends for using other caching systems such as Redis.

Now, the cache can be used in a number of different ways. You can cache only certain parts of your site if you wish. However, because our site is heavily content-driven, we should be pretty safe to use the per-site cache, which is the simplest way to set up caching.

In order to set up Memcached, there’s a couple of Python libraries we’ll need. If you want to install them locally, however, you’ll need to install both memcached and libmemcached (on Ubuntu, the packages you need are called memcached and libmemcached-dev) on your development machine. If you don’t want to do this, then just copy and paste these lines into requirements.txt instead:

django-pylibmc-sasl==0.2.4
pylibmc==1.3.0

If you are happy to install these dependencies locally, then run this command once memcached and libmemcached are installed:

$ pip install pylibmc django-pylibmc-sasl

With that done let’s configure Memcached. Open up the settings file and add the following at the bottom:

def get_cache():
import os
try:
os.environ['MEMCACHE_SERVERS'] = os.environ['MEMCACHIER_SERVERS'].replace(',', ';')
os.environ['MEMCACHE_USERNAME'] = os.environ['MEMCACHIER_USERNAME']
os.environ['MEMCACHE_PASSWORD'] = os.environ['MEMCACHIER_PASSWORD']
return {
'default': {
'BACKEND': 'django_pylibmc.memcached.PyLibMCCache',
'TIMEOUT': 300,
'BINARY': True,
'OPTIONS': { 'tcp_nodelay': True }
}
}
except:
return {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'
}
}
CACHES = get_cache()
CACHE_MIDDLEWARE_ALIAS = 'default'
CACHE_MIDDLEWARE_SECONDS = 300
CACHE_MIDDLEWARE_KEY_PREFIX = ''

Then add the following to MIDDLEWARE_CLASSES:

'django.middleware.cache.UpdateCacheMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.cache.FetchFromCacheMiddleware',

That’s it! The first section configures the application to use Memcached to cache the content when running on Heroku, and sets some configuration parameters, while the second section tells Django to use the per-site cache in order to cache all the site content.

Now, Heroku doesn’t include Memcached by default - instead it’s available as an add-on called Memcachier. To use add-ons you need to set up a credit card for billing. We will set it up to use the free developer plan, but if you outgrow this you can easily switch to a paid plan. To add Memcachier, run this command:

$ heroku addons:add memcachier:dev

Please note that Memcachier can take a few minutes to get set up, so you may want to leave it a little while between adding it and pushing up your changes. Now we’ll commit our changes:

$ git add requirements.txt django_tutorial_blog_ng/settings.py
$ git commit -m 'Implemented caching with Memcached'

Then we’ll push them up to our remote repository and to Heroku:

$ git push origin master
$ git push heroku master

And that’s all you need to do to set up Memcached. In addition to storing your query results in Memcached, enabling the caching framework in Django will also set various HTTP headers to enable web proxies and browsers to cache content for an appropriate length of time. If you open up your browser’s developer tools and compare the response headers for your homepage on the latest version of the code with the previous version, you’ll notice that a number of additional headers appear, including Cache-Control, Expires and Last-Modified. These tell web browsers and web proxies how often to request the latest version of the HTML document, in order to help you reduce the bandwidth used.

As you can see, for a site like this where you are the only person adding content, it’s really easy to implement caching with Django, and for a blog there’s very little reason not to do it. If you’re not using Heroku and are instead hosting your site on a VPS, then the configuration will be somewhat different - see here for details. You can also find information on using other cache backends on the same page.

That isn’t all you can do to speed up your site. Heroku doesn’t seem to be very good for serving static files, and if your site is attracting a lot of traffic you might want to host your static files elsewhere, such as on Amazon’s S3 service. Doing so is outside the scope of this tutorial, but for that use case, you should check out django-storages.

Clearing the cache automatically

There is one issue with this implementation. As it is right now, if you view the home page, add a post, then reload the page, you may not see the new post immediately because the cache will continue serving the old version until it has expired. That behaviour is less than ideal - we would like the cache to be cleared automatically when a new post gets added so that users will see the new version immediately. That response will still be cached afterwards, so it only means one extra query.

This is the ideal place to introduce signals. Signals are a way to carry out a given action when an event takes place. In our case, we plan to clear the cache when a post is saved (either created or updated).

Note that as we’ll be testing the behaviour of the cache at this point, you’ll need to install Memcached on your local machine, and we’ll need to change the settings to fall back to our local Memcached instance:

def get_cache():
import os
try:
os.environ['MEMCACHE_SERVERS'] = os.environ['MEMCACHIER_SERVERS'].replace(',', ';')
os.environ['MEMCACHE_USERNAME'] = os.environ['MEMCACHIER_USERNAME']
os.environ['MEMCACHE_PASSWORD'] = os.environ['MEMCACHIER_PASSWORD']
return {
'default': {
'BACKEND': 'django_pylibmc.memcached.PyLibMCCache',
'TIMEOUT': 300,
'BINARY': True,
'OPTIONS': { 'tcp_nodelay': True }
}
}
except:
return {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.PyLibMCCache',
'LOCATION': '127.0.0.1:11211'
}
}
CACHES = get_cache()
CACHE_MIDDLEWARE_ALIAS = 'default'
CACHE_MIDDLEWARE_SECONDS = 300
CACHE_MIDDLEWARE_KEY_PREFIX = ''

If you don’t want to install Memcached locally, you can skip this step, but be aware that the test we write for clearing the cache will always pass if you do skip it.

Then we’ll run our tests to make sure nothing has been broken:

$ python manage.py jenkins blogengine
Creating test database for alias 'default'...
.......................
----------------------------------------------------------------------
Ran 23 tests in 6.164s
OK

Let’s commit:

$ git add django_tutorial_blog_ng/settings.py
$ git commit -m 'Now use Memcached in development'

Now we’ll add a test for clearing the cache to PostViewTest:

def test_clear_cache(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 first 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)
# Create the second post
post = Post()
post.title = 'My second post'
post.text = 'This is [my second blog post](http://127.0.0.1:8000/)'
post.slug = 'my-second-post'
post.pub_date = timezone.now()
post.author = author
post.site = site
post.category = category
post.save()
post.tags.add(tag)
# Fetch the index again
response = self.client.get('/')
# Check second post present
self.assertTrue('my second blog post' in response.content)

This should be fairly self-explanatory. We create one post, and request the index page. We then add a second post, request the index page again, and check for the second post. The test should fail because the cached version is returned, rather than the version in the database.

Now we have a test in place, we can implement a fix. First, add this to the top of your models.py:

from django.db.models.signals import post_save
from django.core.cache import cache

Then add the following at the bottom of the file:

# Define signals
def new_post(sender, instance, created, **kwargs):
cache.clear()
# Set up signals
post_save.connect(new_post, sender=Post)

This is fairly straightforward. What we’re doing is first defining a function called new_post that is called when a new post is created. We then connect it to the post_save signal. When a post is saved, it calls new_post, which clears the cache, making sure users are seeing the latest and greatest version of your site immediately.

Let’s test it:

$ python manage.py jenkins blogengine
Creating test database for alias 'default'...
........................
----------------------------------------------------------------------
Ran 24 tests in 8.473s
OK
Destroying test database for alias 'default'...

There are a number of signals available, and when you create one, you have access to the created object via the instance parameter. Using signals you can implement all kinds of functionality. For instance, you could implement the functionality to send an email when a new post is published.

If you’re using Travis CI, you’ll also need to update the config file:

language: python
python:
- "2.7"
services: memcached
before_install:
- sudo apt-get install -y libmemcached-dev
# 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

Time to commit:

$ git add blogengine/ .travis.yml
$ git commit -m 'Now clear cache when post added'

Formatting for RSS feeds

Now, we want to offer more than one option for RSS feeds. For instance, if your blog is aggregated on a site such as Planet Python, but you also blog about JavaScript, you may want to be able to provide a feed for posts in the python category only.

If you have written any posts that use any of Markdown’s custom formatting, you may notice that if you load your RSS feed in a reader, it isn’t formatted as Markdown. Let’s fix that. First we’ll amend our test:

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.assertTrue('This is my <em>first</em> blog post' in feed_post.description)

Don’t forget to run the tests to make sure they fail. Now, let’s fix it:

from django.shortcuts import render
from django.views.generic import ListView
from blogengine.models import Category, Post, Tag
from django.contrib.syndication.views import Feed
from django.utils.encoding import force_unicode
from django.utils.safestring import mark_safe
import markdown2
# 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()
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):
extras = ["fenced-code-blocks"]
content = mark_safe(markdown2.markdown(force_unicode(item.text),
extras = extras))
return content

All we’re doing here is amending the item_description method of PostsFeed to render it as Markdown. Now let’s run our tests again:

$ python manage.py jenkins
Creating test database for alias 'default'...
........................
----------------------------------------------------------------------
Ran 24 tests in 9.370s
OK
Destroying test database for alias 'default'...

With that done, we’ll commit our changes:

$ git add blogengine/
$ git commit -m 'Fixed rendering for post feed'

Refactoring our tests

Now, before we get into implementing the feed, our tests are a bit verbose. We create a lot of items over and over again - let’s sort that out. Factory Boy is a handy Python module that allows you to create easy-to-use factories for creating objects over and over again in tests. Let’s install it:

$ pip install factory_boy
$ pip freeze > requirements.txt
$ git add requirements.txt
$ git commit -m 'Installed Factory Boy'

Now let’s set up a factory for creating posts. Add this at the top of the test file:

import factory.django

Then, before your actual tests, insert the following:

# Factories
class SiteFactory(factory.django.DjangoModelFactory):
class Meta:
model = Site
django_get_or_create = (
'name',
'domain'
)
name = 'example.com'
domain = 'example.com'

Now, wherever you call Site(), add its attributes, and save it, replace those lines with the following:

        site = SiteFactory()

Much simpler and more concise, I’m sure you’ll agree! Now, let’s run the tests to make sure they aren’t broken:

$ python manage.py test blogengine
Creating test database for alias 'default'...
........................
----------------------------------------------------------------------
Ran 24 tests in 7.482s
OK
Destroying test database for alias 'default'...

Let’s commit again:

$ git add blogengine/tests.py
$ git commit -m 'Now use Factory Boy for site objects'

Let’s do the same thing with Category objects:

class CategoryFactory(factory.django.DjangoModelFactory):
class Meta:
model = Category
django_get_or_create = (
'name',
'description',
'slug'
)
name = 'python'
description = 'The Python programming language'
slug = 'python'

Again, just find every time we call Category() and replace it with the following:

        category = CategoryFactory()

Now if we run our tests, we’ll notice a serious error:

$ python manage.py test blogengine
Creating test database for alias 'default'...
EE..EE.EE.EE...E.EEE..E.
======================================================================
ERROR: test_create_category (blogengine.tests.PostTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 42, in test_create_category
category = CategoryFactory()
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 82, in __call__
return cls.create(**kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 585, in create
return cls._generate(True, attrs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 510, in _generate
obj = cls._prepare(create, **attrs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 485, in _prepare
return cls._create(model_class, *args, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 151, in _create
return cls._get_or_create(model_class, *args, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 142, in _get_or_create
obj, _created = manager.get_or_create(*args, **key_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 154, in get_or_create
return self.get_queryset().get_or_create(**kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 383, in get_or_create
obj.save(force_insert=True, using=self.db)
TypeError: save() got an unexpected keyword argument 'force_insert'
======================================================================
ERROR: test_create_post (blogengine.tests.PostTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 80, in test_create_post
category = CategoryFactory()
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 82, in __call__
return cls.create(**kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 585, in create
return cls._generate(True, attrs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 510, in _generate
obj = cls._prepare(create, **attrs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 485, in _prepare
return cls._create(model_class, *args, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 151, in _create
return cls._get_or_create(model_class, *args, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 142, in _get_or_create
obj, _created = manager.get_or_create(*args, **key_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 154, in get_or_create
return self.get_queryset().get_or_create(**kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 383, in get_or_create
obj.save(force_insert=True, using=self.db)
TypeError: save() got an unexpected keyword argument 'force_insert'
======================================================================
ERROR: test_create_post (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 339, in test_create_post
category = CategoryFactory()
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 82, in __call__
return cls.create(**kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 585, in create
return cls._generate(True, attrs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 510, in _generate
obj = cls._prepare(create, **attrs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 485, in _prepare
return cls._create(model_class, *args, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 151, in _create
return cls._get_or_create(model_class, *args, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 142, in _get_or_create
obj, _created = manager.get_or_create(*args, **key_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 154, in get_or_create
return self.get_queryset().get_or_create(**kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 383, in get_or_create
obj.save(force_insert=True, using=self.db)
TypeError: save() got an unexpected keyword argument 'force_insert'
======================================================================
ERROR: test_create_post_without_tag (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 378, in test_create_post_without_tag
category = CategoryFactory()
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 82, in __call__
return cls.create(**kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 585, in create
return cls._generate(True, attrs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 510, in _generate
obj = cls._prepare(create, **attrs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 485, in _prepare
return cls._create(model_class, *args, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 151, in _create
return cls._get_or_create(model_class, *args, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 142, in _get_or_create
obj, _created = manager.get_or_create(*args, **key_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 154, in get_or_create
return self.get_queryset().get_or_create(**kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 383, in get_or_create
obj.save(force_insert=True, using=self.db)
TypeError: save() got an unexpected keyword argument 'force_insert'
======================================================================
ERROR: test_delete_category (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 245, in test_delete_category
category = CategoryFactory()
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 82, in __call__
return cls.create(**kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 585, in create
return cls._generate(True, attrs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 510, in _generate
obj = cls._prepare(create, **attrs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 485, in _prepare
return cls._create(model_class, *args, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 151, in _create
return cls._get_or_create(model_class, *args, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 142, in _get_or_create
obj, _created = manager.get_or_create(*args, **key_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 154, in get_or_create
return self.get_queryset().get_or_create(**kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 383, in get_or_create
obj.save(force_insert=True, using=self.db)
TypeError: save() got an unexpected keyword argument 'force_insert'
======================================================================
ERROR: test_delete_post (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 467, in test_delete_post
category = CategoryFactory()
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 82, in __call__
return cls.create(**kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 585, in create
return cls._generate(True, attrs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 510, in _generate
obj = cls._prepare(create, **attrs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 485, in _prepare
return cls._create(model_class, *args, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 151, in _create
return cls._get_or_create(model_class, *args, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 142, in _get_or_create
obj, _created = manager.get_or_create(*args, **key_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 154, in get_or_create
return self.get_queryset().get_or_create(**kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 383, in get_or_create
obj.save(force_insert=True, using=self.db)
TypeError: save() got an unexpected keyword argument 'force_insert'
======================================================================
ERROR: test_edit_category (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 221, in test_edit_category
category = CategoryFactory()
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 82, in __call__
return cls.create(**kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 585, in create
return cls._generate(True, attrs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 510, in _generate
obj = cls._prepare(create, **attrs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 485, in _prepare
return cls._create(model_class, *args, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 151, in _create
return cls._get_or_create(model_class, *args, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 142, in _get_or_create
obj, _created = manager.get_or_create(*args, **key_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 154, in get_or_create
return self.get_queryset().get_or_create(**kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 383, in get_or_create
obj.save(force_insert=True, using=self.db)
TypeError: save() got an unexpected keyword argument 'force_insert'
======================================================================
ERROR: test_edit_post (blogengine.tests.AdminTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 410, in test_edit_post
category = CategoryFactory()
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 82, in __call__
return cls.create(**kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 585, in create
return cls._generate(True, attrs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 510, in _generate
obj = cls._prepare(create, **attrs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 485, in _prepare
return cls._create(model_class, *args, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 151, in _create
return cls._get_or_create(model_class, *args, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 142, in _get_or_create
obj, _created = manager.get_or_create(*args, **key_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 154, in get_or_create
return self.get_queryset().get_or_create(**kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 383, in get_or_create
obj.save(force_insert=True, using=self.db)
TypeError: save() got an unexpected keyword argument 'force_insert'
======================================================================
ERROR: test_all_post_feed (blogengine.tests.FeedTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 810, in test_all_post_feed
category = CategoryFactory()
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 82, in __call__
return cls.create(**kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 585, in create
return cls._generate(True, attrs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 510, in _generate
obj = cls._prepare(create, **attrs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 485, in _prepare
return cls._create(model_class, *args, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 151, in _create
return cls._get_or_create(model_class, *args, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 142, in _get_or_create
obj, _created = manager.get_or_create(*args, **key_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 154, in get_or_create
return self.get_queryset().get_or_create(**kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 383, in get_or_create
obj.save(force_insert=True, using=self.db)
TypeError: save() got an unexpected keyword argument 'force_insert'
======================================================================
ERROR: test_category_page (blogengine.tests.PostViewTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 640, in test_category_page
category = CategoryFactory()
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 82, in __call__
return cls.create(**kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 585, in create
return cls._generate(True, attrs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 510, in _generate
obj = cls._prepare(create, **attrs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 485, in _prepare
return cls._create(model_class, *args, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 151, in _create
return cls._get_or_create(model_class, *args, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 142, in _get_or_create
obj, _created = manager.get_or_create(*args, **key_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 154, in get_or_create
return self.get_queryset().get_or_create(**kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 383, in get_or_create
obj.save(force_insert=True, using=self.db)
TypeError: save() got an unexpected keyword argument 'force_insert'
======================================================================
ERROR: test_clear_cache (blogengine.tests.PostViewTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 753, in test_clear_cache
category = CategoryFactory()
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 82, in __call__
return cls.create(**kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 585, in create
return cls._generate(True, attrs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 510, in _generate
obj = cls._prepare(create, **attrs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 485, in _prepare
return cls._create(model_class, *args, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 151, in _create
return cls._get_or_create(model_class, *args, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 142, in _get_or_create
obj, _created = manager.get_or_create(*args, **key_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 154, in get_or_create
return self.get_queryset().get_or_create(**kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 383, in get_or_create
obj.save(force_insert=True, using=self.db)
TypeError: save() got an unexpected keyword argument 'force_insert'
======================================================================
ERROR: test_index (blogengine.tests.PostViewTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 518, in test_index
category = CategoryFactory()
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 82, in __call__
return cls.create(**kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 585, in create
return cls._generate(True, attrs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 510, in _generate
obj = cls._prepare(create, **attrs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 485, in _prepare
return cls._create(model_class, *args, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 151, in _create
return cls._get_or_create(model_class, *args, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 142, in _get_or_create
obj, _created = manager.get_or_create(*args, **key_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 154, in get_or_create
return self.get_queryset().get_or_create(**kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 383, in get_or_create
obj.save(force_insert=True, using=self.db)
TypeError: save() got an unexpected keyword argument 'force_insert'
======================================================================
ERROR: test_post_page (blogengine.tests.PostViewTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 576, in test_post_page
category = CategoryFactory()
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 82, in __call__
return cls.create(**kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 585, in create
return cls._generate(True, attrs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 510, in _generate
obj = cls._prepare(create, **attrs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/base.py", line 485, in _prepare
return cls._create(model_class, *args, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 151, in _create
return cls._get_or_create(model_class, *args, **kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/factory/django.py", line 142, in _get_or_create
obj, _created = manager.get_or_create(*args, **key_fields)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/manager.py", line 154, in get_or_create
return self.get_queryset().get_or_create(**kwargs)
File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/venv/lib/python2.7/site-packages/django/db/models/query.py", line 383, in get_or_create
obj.save(force_insert=True, using=self.db)
TypeError: save() got an unexpected keyword argument 'force_insert'
----------------------------------------------------------------------
Ran 24 tests in 5.162s
FAILED (errors=13)
Destroying test database for alias 'default'...

Thankfully, this is easy to fix. We just need to amend the custom save() method of the Category model:

def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(unicode(self.name))
super(Category, self).save(*args, **kwargs)

That should resolve the issue:

$ python manage.py jenkins
Creating test database for alias 'default'...
........................
----------------------------------------------------------------------
Ran 24 tests in 7.749s
OK
Destroying test database for alias 'default'...

Let’s commit again:

$ git add blogengine/
$ git commit -m 'Category now uses Factory Boy'

Now let’s do the same thing for tags:

class TagFactory(factory.django.DjangoModelFactory):
class Meta:
model = Tag
django_get_or_create = (
'name',
'description',
'slug'
)
name = 'python'
description = 'The Python programming language'
slug = 'python'

And replace the sections where we create new Tag objects:

        tag = TagFactory()

Note that some tags have different values. We can easily pass different values to our TagFactory() to override the default values:

        tag = TagFactory(name='perl', description='The Perl programming language')

The Tag model has the same issue as the Category one did, so let’s fix that:

def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(unicode(self.name))
super(Tag, self).save(*args, **kwargs)

We run our tests again:

$ python manage.py test blogengine/
Creating test database for alias 'default'...
........................
----------------------------------------------------------------------
Ran 24 tests in 7.153s
OK
Destroying test database for alias 'default'...

Time to commit again:

$ git add blogengine/
$ git commit -m 'Now use Factory Boy for tags'

Next we’ll create a factory for adding users. Note that the factory name doesn’t have to match the object name, so you can create factories for different types of users. Here we create a factory for authors - you could, for instance, create a separate factory for subscribers if you wanted:

class AuthorFactory(factory.django.DjangoModelFactory):
class Meta:
model = User
django_get_or_create = ('username','email', 'password',)
username = 'testuser'
email = 'user@example.com'
password = 'password'

And as before, replace those sections where we create users with the following:

        author = AuthorFactory()

Run the tests again:

$ python manage.py test blogengine
Creating test database for alias 'default'...
........................
----------------------------------------------------------------------
Ran 24 tests in 5.808s
OK
Destroying test database for alias 'default'...

We commit our changes:

$ git add blogengine/
$ git commit -m 'Now use Factory Boy for creating authors'

Now we’ll create a flat page factory:

class FlatPageFactory(factory.django.DjangoModelFactory):
class Meta:
model = FlatPage
django_get_or_create = (
'url',
'title',
'content'
)
url = '/about/'
title = 'About me'
content = 'All about me'

And use it for our flat page test:

        page = FlatPageFactory()

Check the tests pass:

$ python manage.py test blogengine
Creating test database for alias 'default'...
........................
----------------------------------------------------------------------
Ran 24 tests in 5.796s
OK
Destroying test database for alias 'default'...

And commit again:

$ git add blogengine/
$ git commit -m 'Now use Factory Boy for flat page test'

Now we’ll create a final factory for posts:

class PostFactory(factory.django.DjangoModelFactory):
class Meta:
model = Post
django_get_or_create = (
'title',
'text',
'slug',
'pub_date'
)
title = 'My first post'
text = 'This is my first blog post'
slug = 'my-first-post'
pub_date = timezone.now()
author = factory.SubFactory(AuthorFactory)
site = factory.SubFactory(SiteFactory)
category = factory.SubFactory(CategoryFactory)

This factory is a little bit different. Because our Post model depends on several others, we need to be able to create those additional objects on demand. By designating them as subfactories, we can easily create the associated objects for our Post object.

That means that not only can we get rid of our Post() calls, but we can also get rid of the factory calls to create the associated objects for Post models. Again, I’ll leave actually doing this as an exercise for the reader, but you can always refer to the GitHub repository if you’re not too sure.

Make sure your tests still pass, then commit the changes:

$ git add blogengine/
$ git commit -m 'Now use Factory Boy for testing posts'

Using Factory Boy made a big difference to the size of the test file - I was able to cut it down by over 200 lines of code. As your application gets bigger, it gets harder to maintain, so do what you can to keep the size down.

Additional RSS feeds

Now, let’s implement our additional RSS feeds. First, we’ll write a test for the category feed. Add this to the FeedTest class:

def test_category_feed(self):
# Create a post
post = PostFactory(text='This is my *first* blog post')
# Create another post in a different category
category = CategoryFactory(name='perl', description='The Perl programming language', slug='perl')
post2 = PostFactory(text='This is my *second* blog post', title='My second post', slug='my-second-post', category=category)
# Fetch the feed
response = self.client.get('/feeds/posts/category/python/')
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.assertTrue('This is my <em>first</em> blog post' in feed_post.description)
# Check other post is not in this feed
self.assertTrue('This is my <em>second</em> blog post' not in response.content)

Here we create two posts in different categories (note that we create a new category and override the post category for it). We then fetch /feeds/posts/category/python/ and assert that it contains only one post, with the content of the first post and not the content of the second.

Run the tests and they should fail:

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

Because we haven’t yet implemented that route, we get a 404 error. So let’s create a route for this:

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, PostsFeed, CategoryPostsFeed
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,
)),
# Post RSS feed
url(r'^feeds/posts/$', PostsFeed()),
# Category RSS feed
url(r'^feeds/posts/category/(?P<slug>[a-zA-Z0-9-]+)/?$', CategoryPostsFeed()),
)

Note that the category RSS feed route is similar to the post RSS feed route, but accepts a slug parameter. We will use this to pass through the slug for the category in question. Also note we import the CategoryPostsFeed view. Now, we need to create that view. Fortunately, because it’s written as a Python class, we can extend the existing PostsFeed class. Open up your views file and amend it to look like this:

from django.shortcuts import get_object_or_404
from django.views.generic import ListView
from blogengine.models import Category, Post, Tag
from django.contrib.syndication.views import Feed
from django.utils.encoding import force_unicode
from django.utils.safestring import mark_safe
import markdown2
# 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()
class PostsFeed(Feed):
title = "RSS feed - posts"
description = "RSS feed - blog posts"
link = '/'
def items(self):
return Post.objects.order_by('-pub_date')
def item_title(self, item):
return item.title
def item_description(self, item):
extras = ["fenced-code-blocks"]
content = mark_safe(markdown2.markdown(force_unicode(item.text),
extras = extras))
return content
class CategoryPostsFeed(PostsFeed):
def get_object(self, request, slug):
return get_object_or_404(Category, slug=slug)
def title(self, obj):
return "RSS feed - blog posts in category %s" % obj.name
def link(self, obj):
return obj.get_absolute_url()
def description(self, obj):
return "RSS feed - blog posts in category %s" % obj.name
def items(self, obj):
return Post.objects.filter(category=obj).order_by('-pub_date')

Note that many of our fields don’t have to be explicitly defined as they are inherited from PostsFeed. We can’t hard-code the title, link or description because they depend on the category, so we instead define methods to return the appropriate text.

Also note get_object() - we define this so that we can ensure the category exists. If it doesn’t exist, then it returns a 404 error rather than showing an empty feed.

We also override items() to filter it to just those posts that are in the given category.

If you run the tests again, they should now pass:

$ python manage.py test blogengine
Creating test database for alias 'default'...
.........................
----------------------------------------------------------------------
Ran 25 tests in 5.867s
OK
Destroying test database for alias 'default'...

Let’s commit our changes:

$ git add blogengine/
$ git commit -m 'Implemented category RSS feed'

Now, we can get our category RSS feed, but how do we navigate to it? Let’s add a link to each category page that directs a user to its RSS feed. To do so, we’ll need to create a new template for category pages. First, let’s add some code to our tests to ensure that the right template is used at all times. Add the following to the end of the test_index method of PostViewTest:

# Check the correct template was used
self.assertTemplateUsed(response, 'blogengine/post_list.html')

Then, add this to the end of test_post_page:

# Check the correct template was used
self.assertTemplateUsed(response, 'blogengine/post_detail.html')

Finally, add this to the end of test_category_page:

# Check the correct template was used
self.assertTemplateUsed(response, 'blogengine/category_post_list.html')

These assertions confirm which template was used to generate which request.

Next, we head into our views file:

class CategoryListView(ListView):
template_name = 'blogengine/category_post_list.html'
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()
def get_context_data(self, **kwargs):
context = super(CategoryListView, self).get_context_data(**kwargs)
slug = self.kwargs['slug']
try:
context['category'] = Category.objects.get(slug=slug)
except Category.DoesNotExist:
context['category'] = None
return context

Note that we first of all change the template used by this view. Then, we override get_context_data to add in additional data. What we’re doing is getting the slug that was passed through, looking up any category for which it is the slug, and returning it as additional context data. Using this method, you can easily add additional data that you may wish to render in your Django templates.

Finally, we create our new template:

{% 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>
<a href="/feeds/posts/category/{{ category.slug }}/">RSS feed for category {{ category.name }}</a>
{% endblock %}

Note that the category has been passed through to the template and is now accessible. If you run the tests, they should now pass:

$ python manage.py jenkins
Creating test database for alias 'default'...
.........................
----------------------------------------------------------------------
Ran 25 tests in 7.232s
OK
Destroying test database for alias 'default'...

With that done. we can commit our changes:

$ git add templates/ blogengine/
$ git commit -m 'Added link to RSS feed from category page'

Next up, let’s implement another RSS feed for tags. First, we’ll implement our test:

def test_tag_feed(self):
# Create a post
post = PostFactory(text='This is my *first* blog post')
tag = TagFactory()
post.tags.add(tag)
post.save()
# Create another post with a different tag
tag2 = TagFactory(name='perl', description='The Perl programming language', slug='perl')
post2 = PostFactory(text='This is my *second* blog post', title='My second post', slug='my-second-post')
post2.tags.add(tag2)
post2.save()
# Fetch the feed
response = self.client.get('/feeds/posts/tag/python/')
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.assertTrue('This is my <em>first</em> blog post' in feed_post.description)
# Check other post is not in this feed
self.assertTrue('This is my <em>second</em> blog post' not in response.content)

This is virtually identical to the test for the categroy feed, but we adjust it to work with the Tag attribute and change the URL. Let’s check that our test fails:

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

As before, we create a route for this:

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, PostsFeed, CategoryPostsFeed, TagPostsFeed
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,
)),
# Post RSS feed
url(r'^feeds/posts/$', PostsFeed()),
# Category RSS feed
url(r'^feeds/posts/category/(?P<slug>[a-zA-Z0-9-]+)/?$', CategoryPostsFeed()),
# Tag RSS feed
url(r'^feeds/posts/tag/(?P<slug>[a-zA-Z0-9-]+)/?$', TagPostsFeed()),
)

Next, we create our view:

class TagPostsFeed(PostsFeed):
def get_object(self, request, slug):
return get_object_or_404(Tag, slug=slug)
def title(self, obj):
return "RSS feed - blog posts tagged %s" % obj.name
def link(self, obj):
return obj.get_absolute_url()
def description(self, obj):
return "RSS feed - blog posts tagged %s" % obj.name
def items(self, obj):
try:
tag = Tag.objects.get(slug=obj.slug)
return tag.post_set.all()
except Tag.DoesNotExist:
return Post.objects.none()

Again, this inherits from PostsFeed, but the syntax for getting posts matching a tag is slightly different because they use a many-to-many relationship.

We also need a template for the tag pages. Add this to the end of the test_tag_page method:

# Check the correct template was used
self.assertTemplateUsed(response, 'blogengine/tag_post_list.html')

Let’s create that template:

{% 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>
<a href="/feeds/posts/tag/{{ tag.slug }}/">RSS feed for tag {{ tag.name }}</a>
{% endblock %}

This is virtually identical to the category template. You’ll also need to apply this template in the view for the tag list, and pass the tag name through as context data:

class TagListView(ListView):
template_name = 'blogengine/tag_post_list.html'
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()
def get_context_data(self, **kwargs):
context = super(TagListView, self).get_context_data(**kwargs)
slug = self.kwargs['slug']
try:
context['tag'] = Tag.objects.get(slug=slug)
except Tag.DoesNotExist:
context['tag'] = None
return context

Let’s run our tests:

$ python manage.py test blogengine
Creating test database for alias 'default'...
..........................
----------------------------------------------------------------------
Ran 26 tests in 5.770s
OK
Destroying test database for alias 'default'...

You may want to do a quick check to ensure your tag feed link works as expected. Time to commit:

$ git add blogengine templates
$ git commit -m 'Implemented tag feeds'

Moving our templates

Before we crack on with implementing search, there’s one more piece of housekeeping. In Django, templates can be applied at project level or at app level. So far, we’ve been storing them in the project, but we would like our app to be as self-contained as possible so it can just be dropped into future projects where we need a blog. That way, it can be easily overridden for specific projects. You can move the folders and update the Git repository at the same time with this command:

$ git mv templates/ blogengine/

We run the tests to make sure nothing untoward has happened:

$ python manage.py test
Creating test database for alias 'default'...
..........................
----------------------------------------------------------------------
Ran 26 tests in 5.847s
OK
Destroying test database for alias 'default'...

And we commit:

$ git commit -m 'Moved templates'

Note that git mv updates Git and moves the files, so you don’t need to call git add.

Implementing search

For our final task today, we will be implementing a very simple search engine. Our requirements are:

  • It should be in the header, to allow for easy access from anywhere in the front end.
  • It should search the title and text of posts.

First, we’ll write our tests:

class SearchViewTest(BaseAcceptanceTest):
def test_search(self):
# Create a post
post = PostFactory()
# Create another post
post2 = PostFactory(text='This is my *second* blog post', title='My second post', slug='my-second-post')
# Search for first post
response = self.client.get('/search?q=first')
self.assertEquals(response.status_code, 200)
# Check the first post is contained in the results
self.assertTrue('My first post' in response.content)
# Check the second post is not contained in the results
self.assertTrue('My second post' not in response.content)
# Search for second post
response = self.client.get('/search?q=second')
self.assertEquals(response.status_code, 200)
# Check the first post is not contained in the results
self.assertTrue('My first post' not in response.content)
# Check the second post is contained in the results
self.assertTrue('My second post' in response.content)

Don’t forget to run the tests to make sure they fail:

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

With that done, we can add the search form to the header:

<!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>
<form action="/search" method="GET" class="navbar-form navbar-left">
<div class="form-group">
<input type="text" name="q" placeholder="Search..." class="form-control"></input>
</div>
<button type="submit" class="btn btn-default">Search</button>
</form>
</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>

Now we’ll actually implement our search. Implementing search using Django’s generic views can be fiddly, so we’ll write our search view as a function instead. First, amend the imports at the top of your view file to look like this:

from django.shortcuts import get_object_or_404, render_to_response
from django.core.paginator import Paginator, EmptyPage
from django.db.models import Q
from django.views.generic import ListView
from blogengine.models import Category, Post, Tag
from django.contrib.syndication.views import Feed
from django.utils.encoding import force_unicode
from django.utils.safestring import mark_safe
import markdown2

Next, add the following code to the end of the file:

def getSearchResults(request):
"""
Search for a post by title or text
"""
# Get the query data
query = request.GET.get('q', '')
page = request.GET.get('page', 1)
# Query the database
if query:
results = Post.objects.filter(Q(text__icontains=query) | Q(title__icontains=query))
else:
results = None
# Add pagination
pages = Paginator(results, 5)
# Get specified page
try:
returned_page = pages.page(page)
except EmptyPage:
returned_page = pages.page(pages.num_pages)
# Display the search results
return render_to_response('blogengine/search_post_list.html',
{'page_obj': returned_page,
'object_list': returned_page.object_list,
'search': query})

As this is the first time we’ve written a view without using generic views, a little explanation is called for. First we get the values of the q and page parameters passed to the view. q contains the query text and page contains the page number. Note also that our page defaults to 1 if not set.

We then use the Q object to perform a query. The Django ORM will AND together keyword argument queries, but that’s not the behaviour we want here. Instead we want to be able to search for content in the title or text, so we need to use a query with an OR statement, which necessitates using the Q object.

Next, we use the Paginator object to manually paginate the results, and if it raises an EmptyPage exception, to just show the last page instead. Finally we render the template blogengine/search_post_list.html, and pass through the parameters page_obj for the returned page, object_list for the objects, and search for the query.

We also need to add a route for our new view:

# Search posts
url(r'^search', 'blogengine.views.getSearchResults'),

Finally, let’s create a new template to show our results:

{% 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="/search?page={{ page_obj.previous_page_number }}&q={{ search }}">Previous Page</a></li>
{% endif %}
{% if page_obj.has_next %}
<li class="next"><a href="/search?page={{ page_obj.next_page_number }}&q={{ search }}">Next Page</a></li>
{% endif %}
</ul>
{% endblock %}

Let’s run our tests:

$ python manage.py test
Creating test database for alias 'default'...
...........................
----------------------------------------------------------------------
Ran 27 tests in 6.421s
OK
Destroying test database for alias 'default'...

Don’t forget to do a quick sense check to make sure it’s all working as expected. Then it’s time to commit:

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

And push up your changes:

$ git push origin master
$ git push heroku master

And that’s the end of this instalment. Please note this particular search solution is quite basic, and if you want something more powerful, you may want to look at Haystack.

As usual, you can get this lesson with git checkout lesson-7 - if you have any problems, the repository should be the first place you look for answers as this is the working code base for the application, and with judicious use of a tool like diff, it’s generally pretty easy to track down most issues.

In our next, and final instalment, we’ll cover:

  • Tidying everything up
  • Implementing an XML sitemap for search engines
  • Optimising our site
  • Using Fabric to make deployment easier

Hope to see you then!

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.