Django Blog Tutorial - the Next Generation - Part 8

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:

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

classPostSitemap(Sitemap):

changefreq = "always"

priority = 0.5

defitems(self):

return Post.objects.all()

deflastmod(self, obj):

return obj.pub_date

classFlatpageSitemap(Sitemap):

changefreq = "always"

priority = 0.5

defitems(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.

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:

classTagPostsFeed(PostsFeed):

defget_object(self, request, slug):

return get_object_or_404(Tag, slug=slug)

deftitle(self, obj):

return"RSS feed - blog posts tagged %s" % obj.name

deflink(self, obj):

return obj.get_absolute_url()

defdescription(self, obj):

return"RSS feed - blog posts tagged %s" % obj.name

defitems(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:

classTagPostsFeed(PostsFeed):

defget_object(self, request, slug):

return get_object_or_404(Tag, slug=slug)

deftitle(self, obj):

return"RSS feed - blog posts tagged %s" % obj.name

deflink(self, obj):

return obj.get_absolute_url()

defdescription(self, obj):

return"RSS feed - blog posts tagged %s" % obj.name

defitems(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:

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

defdeploy():

"""

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.

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.

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:

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.