Django Blog Tutorial - the Next Generation - Part 8

Published by at 31st August 2014 9:00 pm

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:

1$ git add requirements.txt
2$ 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:

1class SitemapTest(BaseAcceptanceTest):
2 def test_sitemap(self):
3 # Create a post
4 post = PostFactory()
5
6 # Create a flat page
7 page = FlatPageFactory()
8
9 # Get sitemap
10 response = self.client.get('/sitemap.xml')
11 self.assertEquals(response.status_code, 200)
12
13 # Check post is present in sitemap
14 self.assertTrue('my-first-post' in response.content)
15
16 # Check page is present in sitemap
17 self.assertTrue('/about/' in response.content)

Run it, and you should see the test fail:

1$ python manage.py test blogengine
2Creating test database for alias 'default'...
3...........................F
4======================================================================
5FAIL: test_sitemap (blogengine.tests.SitemapTest)
6----------------------------------------------------------------------
7Traceback (most recent call last):
8 File "/Users/matthewdaly/Projects/django_tutorial_blog_ng/blogengine/tests.py", line 847, in test_sitemap
9 self.assertEquals(response.status_code, 200)
10AssertionError: 404 != 200
11
12----------------------------------------------------------------------
13Ran 28 tests in 6.873s
14
15FAILED (failures=1)
16Destroying 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:

1from django.contrib.sitemaps import Sitemap
2from django.contrib.flatpages.models import FlatPage
3from blogengine.models import Post
4
5class PostSitemap(Sitemap):
6 changefreq = "always"
7 priority = 0.5
8
9 def items(self):
10 return Post.objects.all()
11
12 def lastmod(self, obj):
13 return obj.pub_date
14
15
16class FlatpageSitemap(Sitemap):
17 changefreq = "always"
18 priority = 0.5
19
20 def items(self):
21 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:

1from django.contrib.sitemaps.views import sitemap
2from blogengine.sitemap import PostSitemap, FlatpageSitemap
3
4# Define sitemaps
5sitemaps = {
6 'posts': PostSitemap,
7 'pages': FlatpageSitemap
8}

Then add the following after the existing routes:

1 # Sitemap
2 url(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
3 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:

1$ python manage.py test blogengine
2Creating test database for alias 'default'...
3............................
4----------------------------------------------------------------------
5Ran 28 tests in 6.863s
6
7OK
8Destroying test database for alias 'default'...

And done! Let's commit our changes:

1$ git add blogengine/ django_tutorial_blog_ng/settings.py
2$ 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:

1class TagPostsFeed(PostsFeed):
2 def get_object(self, request, slug):
3 return get_object_or_404(Tag, slug=slug)
4
5 def title(self, obj):
6 return "RSS feed - blog posts tagged %s" % obj.name
7
8 def link(self, obj):
9 return obj.get_absolute_url()
10
11 def description(self, obj):
12 return "RSS feed - blog posts tagged %s" % obj.name
13
14 def items(self, obj):
15 try:
16 tag = Tag.objects.get(slug=obj.slug)
17 return tag.post_set.all()
18 except Tag.DoesNotExist:
19 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:

1class TagPostsFeed(PostsFeed):
2 def get_object(self, request, slug):
3 return get_object_or_404(Tag, slug=slug)
4
5 def title(self, obj):
6 return "RSS feed - blog posts tagged %s" % obj.name
7
8 def link(self, obj):
9 return obj.get_absolute_url()
10
11 def description(self, obj):
12 return "RSS feed - blog posts tagged %s" % obj.name
13
14 def items(self, obj):
15 tag = Tag.objects.get(slug=obj.slug)
16 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:

1def getSearchResults(request):
2 """
3 Search for a post by title or text
4 """
5 # Get the query data
6 query = request.GET.get('q', '')
7 page = request.GET.get('page', 1)
8
9 # Query the database
10 if query:
11 results = Post.objects.filter(Q(text__icontains=query) | Q(title__icontains=query))
12 else:
13 results = None
14
15 # Add pagination
16 pages = Paginator(results, 5)
17
18 # Get specified page
19 try:
20 returned_page = pages.page(page)
21 except EmptyPage:
22 returned_page = pages.page(pages.num_pages)
23
24 # Display the search results
25 return render_to_response('blogengine/search_post_list.html',
26 {'page_obj': returned_page,
27 'object_list': returned_page.object_list,
28 'search': query})

So replace it with this:

1def getSearchResults(request):
2 """
3 Search for a post by title or text
4 """
5 # Get the query data
6 query = request.GET.get('q', '')
7 page = request.GET.get('page', 1)
8
9 # Query the database
10 results = Post.objects.filter(Q(text__icontains=query) | Q(title__icontains=query))
11
12 # Add pagination
13 pages = Paginator(results, 5)
14
15 # Get specified page
16 try:
17 returned_page = pages.page(page)
18 except EmptyPage:
19 returned_page = pages.page(pages.num_pages)
20
21 # Display the search results
22 return render_to_response('blogengine/search_post_list.html',
23 {'page_obj': returned_page,
24 'object_list': returned_page.object_list,
25 '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:

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

Run our tests and check the coverage:

1$ coverage run --include="blogengine/*" --omit="blogengine/migrations/*" manage.py test blogengine
2$ 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:

1$ git add blogengine/
2$ 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:

1#!/usr/bin/env python
2from fabric.api import local
3
4def deploy():
5 """
6 Deploy the latest version to Heroku
7 """
8 # Push changes to master
9 local("git push origin master")
10
11 # Push changes to Heroku
12 local("git push heroku master")
13
14 # Run migrations on Heroku
15 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:

1$ git add fabfile.py requirements.txt
2$ 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:

1from django.conf.urls import patterns, include, url
2
3from django.contrib import admin
4admin.autodiscover()
5
6urlpatterns = patterns('',
7 # Examples:
8 # url(r'^$', 'django_tutorial_blog_ng.views.home', name='home'),
9 # url(r'^blog/', include('blog.urls')),
10
11 url(r'^admin/', include(admin.site.urls)),
12
13 # Blog URLs
14 url(r'', include('blogengine.urls', namespace="blogengine")),
15
16 # Flat pages
17 url(r'', include('django.contrib.flatpages.urls')),
18)

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

1from django.conf.urls import patterns, url
2from django.views.generic import ListView, DetailView
3from blogengine.models import Post, Category, Tag
4from blogengine.views import CategoryListView, TagListView, PostsFeed, CategoryPostsFeed, TagPostsFeed, getSearchResults
5from django.contrib.sitemaps.views import sitemap
6from blogengine.sitemap import PostSitemap, FlatpageSitemap
7
8# Define sitemaps
9sitemaps = {
10 'posts': PostSitemap,
11 'pages': FlatpageSitemap
12}
13
14urlpatterns = patterns('',
15 # Index
16 url(r'^(?P<page>\d+)?/?$', ListView.as_view(
17 model=Post,
18 paginate_by=5,
19 ),
20 name='index'
21 ),
22
23 # Individual posts
24 url(r'^(?P<pub_date__year>\d{4})/(?P<pub_date__month>\d{1,2})/(?P<slug>[a-zA-Z0-9-]+)/?$', DetailView.as_view(
25 model=Post,
26 ),
27 name='post'
28 ),
29
30 # Categories
31 url(r'^category/(?P<slug>[a-zA-Z0-9-]+)/?$', CategoryListView.as_view(
32 paginate_by=5,
33 model=Category,
34 ),
35 name='category'
36 ),
37
38
39 # Tags
40 url(r'^tag/(?P<slug>[a-zA-Z0-9-]+)/?$', TagListView.as_view(
41 paginate_by=5,
42 model=Tag,
43 ),
44 name='tag'
45 ),
46
47 # Post RSS feed
48 url(r'^feeds/posts/$', PostsFeed()),
49
50 # Category RSS feed
51 url(r'^feeds/posts/category/(?P<slug>[a-zA-Z0-9-]+)/?$', CategoryPostsFeed()),
52
53 # Tag RSS feed
54 url(r'^feeds/posts/tag/(?P<slug>[a-zA-Z0-9-]+)/?$', TagPostsFeed()),
55
56 # Search posts
57 url(r'^search', getSearchResults, name='search'),
58
59 # Sitemap
60 url(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
61 name='django.contrib.sitemaps.views.sitemap'),
62)

You also need to amend two of your templates:

1<!DOCTYPE html>
2<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
3<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
4<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
5<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
6 <head>
7 <meta charset="utf-8">
8 <meta http-equiv="X-UA-Compatible" content="IE=edge">
9 <title>{% block title %}My Django Blog{% endblock %}</title>
10 <meta name="description" content="">
11 <meta name="viewport" content="width=device-width, initial-scale=1">
12 <link rel="alternate" type="application/rss+xml" title="Blog posts" href="/feeds/posts/" >
13
14 <!-- Place favicon.ico and apple-touch-icon.png in the root directory -->
15
16 {% load staticfiles %}
17 <link rel="stylesheet" href="{% static 'bower_components/html5-boilerplate/css/normalize.css' %}">
18 <link rel="stylesheet" href="{% static 'bower_components/html5-boilerplate/css/main.css' %}">
19 <link rel="stylesheet" href="{% static 'bower_components/bootstrap/dist/css/bootstrap.min.css' %}">
20 <link rel="stylesheet" href="{% static 'bower_components/bootstrap/dist/css/bootstrap-theme.min.css' %}">
21 <link rel="stylesheet" href="{% static 'css/main.css' %}">
22 <link rel="stylesheet" href="{% static 'css/code.css' %}">
23 <script src="{% static 'bower_components/html5-boilerplate/js/vendor/modernizr-2.6.2.min.js' %}"></script>
24 </head>
25 <body>
26 <!--[if lt IE 7]>
27 <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>
28 <![endif]-->
29
30 <!-- Add your site or application content here -->
31
32 <div id="fb-root"></div>
33 <script>(function(d, s, id) {
34 var js, fjs = d.getElementsByTagName(s)[0];
35 if (d.getElementById(id)) return;
36 js = d.createElement(s); js.id = id;
37 js.src = "//connect.facebook.net/en_GB/all.js#xfbml=1";
38 fjs.parentNode.insertBefore(js, fjs);
39 }(document, 'script', 'facebook-jssdk'));</script>
40
41 <div class="navbar navbar-static-top navbar-inverse">
42 <div class="container-fluid">
43 <div class="navbar-header">
44 <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#header-nav">
45 <span class="icon-bar"></span>
46 <span class="icon-bar"></span>
47 <span class="icon-bar"></span>
48 </button>
49 <a class="navbar-brand" href="{% url 'blogengine:index' %}">My Django Blog</a>
50 </div>
51 <div class="collapse navbar-collapse" id="header-nav">
52 <ul class="nav navbar-nav">
53 {% load flatpages %}
54 {% get_flatpages as flatpages %}
55 {% for flatpage in flatpages %}
56 <li><a href="{{ flatpage.url }}">{{ flatpage.title }}</a></li>
57 {% endfor %}
58 <li><a href="/feeds/posts/">RSS feed</a></li>
59
60 <form action="/search" method="GET" class="navbar-form navbar-left">
61 <div class="form-group">
62 <input type="text" name="q" placeholder="Search..." class="form-control"></input>
63 </div>
64 <button type="submit" class="btn btn-default">Search</button>
65 </form>
66 </ul>
67 </div>
68 </div>
69 </div>
70
71 <div class="container">
72 {% block header %}
73 <div class="page-header">
74 <h1>My Django Blog</h1>
75 </div>
76 {% endblock %}
77
78 <div class="row">
79 {% block content %}{% endblock %}
80 </div>
81 </div>
82
83 <div class="container footer">
84 <div class="row">
85 <div class="span12">
86 <p>Copyright &copy; {% now "Y" %}</p>
87 </div>
88 </div>
89 </div>
90
91 <script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
92 <script>window.jQuery || document.write('<script src="{% static 'bower_components/html5-boilerplate/js/vendor/jquery-1.10.2.min.js' %}"><\/script>')</script>
93 <script src="{% static 'bower_components/html5-boilerplate/js/plugins.js' %}"></script>
94 <script src="{% static 'bower_components/bootstrap/dist/js/bootstrap.min.js' %}"></script>
95
96 <!-- Google Analytics: change UA-XXXXX-X to be your site's ID. -->
97 <script>
98 (function(b,o,i,l,e,r){b.GoogleAnalyticsObject=l;b[l]||(b[l]=
99 function(){(b[l].q=b[l].q||[]).push(arguments)});b[l].l=+new Date;
100 e=o.createElement(i);r=o.getElementsByTagName(i)[0];
101 e.src='//www.google-analytics.com/analytics.js';
102 r.parentNode.insertBefore(e,r)}(window,document,'script','ga'));
103 ga('create','UA-XXXXX-X');ga('send','pageview');
104 </script>
105 </body>
106</html>
1{% extends "blogengine/includes/base.html" %}
2
3 {% load custom_markdown %}
4
5 {% block content %}
6 {% if object_list %}
7 {% for post in object_list %}
8 <div class="post col-md-12">
9 <h1><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h1>
10 <h3>{{ post.pub_date }}</h3>
11 {{ post.text|custom_markdown }}
12 </div>
13 {% if post.category %}
14 <div class="col-md-12">
15 <a href="{{ post.category.get_absolute_url }}"><span class="label label-primary">{{ post.category.name }}</span></a>
16 </div>
17 {% endif %}
18 {% if post.tags %}
19 <div class="col-md-12">
20 {% for tag in post.tags.all %}
21 <a href="{{ tag.get_absolute_url }}"><span class="label label-success">{{ tag.name }}</span></a>
22 {% endfor %}
23 </div>
24 {% endif %}
25 {% endfor %}
26 {% else %}
27 <p>No posts found</p>
28 {% endif %}
29
30 <ul class="pager">
31 {% if page_obj.has_previous %}
32 <li class="previous"><a href="{% url 'blogengine:search' %}?page={{ page_obj.previous_page_number }}&q={{ search }}">Previous Page</a></li>
33 {% endif %}
34 {% if page_obj.has_next %}
35 <li class="next"><a href="{% url 'blogengine:search' %}?page={{ page_obj.next_page_number }}&q={{ search }}">Next Page</a></li>
36 {% endif %}
37 </ul>
38
39 {% endblock %}

Let's run our tests:

1$ python manage.py test blogengine/
2Creating test database for alias 'default'...
3.............................
4----------------------------------------------------------------------
5Ran 29 tests in 10.456s
6
7OK
8Destroying test database for alias 'default'...

And commit our changes:

1$ git add .
2$ 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:

1import pdb
2pdb.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 auto-completion, so we'll install ipdb, which is an improved version:

1$ pip install ipdb
2$ pip freeze > requirements.txt

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

1import ipdb
2ipdb.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:

1$ pip install django-debug-toolbar
2$ 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:

1$ git add .
2$ 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:

1{
2 "name": "django_tutorial_blog_ng",
3 "version": "1.0.0",
4 "description": "Django Tutorial Blog NG =======================",
5 "main": "index.js",
6 "scripts": {
7 "test": "echo \"Error: no test specified\" && exit 1"
8 },
9 "repository": {
10 "type": "git",
11 "url": "https://github.com/matthewbdaly/django_tutorial_blog_ng.git"
12 },
13 "author": "Matthew Daly <matthew@matthewdaly.co.uk> (http://matthewdaly.co.uk/)",
14 "license": "ISC",
15 "bugs": {
16 "url": "https://github.com/matthewbdaly/django_tutorial_blog_ng/issues"
17 },
18 "homepage": "https://github.com/matthewbdaly/django_tutorial_blog_ng"
19}

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:

1module.exports = function (grunt) {
2 'use strict';
3
4 grunt.initConfig({
5 concat: {
6 dist: {
7 src: [
8 'blogengine/static/bower_components/bootstrap/dist/css/bootstrap.css',
9 'blogengine/static/bower_components/bootstrap/dist/css/bootstrap-theme.css',
10 'blogengine/static/css/code.css',
11 'blogengine/static/css/main.css',
12 ],
13 dest: 'blogengine/static/css/style.css'
14 }
15 },
16 uglify: {
17 dist: {
18 src: [
19 'blogengine/static/bower_components/jquery/jquery.js',
20 'blogengine/static/bower_components/bootstrap/dist/js/bootstrap.js'
21 ],
22 dest: 'blogengine/static/js/all.min.js'
23 }
24 },
25 cssmin: {
26 dist: {
27 src: 'blogengine/static/css/style.css',
28 dest: 'blogengine/static/css/style.min.css'
29 }
30 }
31 });
32
33 grunt.loadNpmTasks('grunt-contrib-concat');
34 grunt.loadNpmTasks('grunt-contrib-uglify');
35 grunt.loadNpmTasks('grunt-contrib-cssmin');
36 grunt.registerTask('default', ['concat', 'uglify', 'cssmin']);
37};

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

1<!DOCTYPE html>
2<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
3<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
4<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
5<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
6 <head>
7 <meta charset="utf-8">
8 <meta http-equiv="X-UA-Compatible" content="IE=edge">
9 <title>{% block title %}My Django Blog{% endblock %}</title>
10 <meta name="description" content="">
11 <meta name="viewport" content="width=device-width, initial-scale=1">
12 <link rel="alternate" type="application/rss+xml" title="Blog posts" href="/feeds/posts/" >
13
14 <!-- Place favicon.ico and apple-touch-icon.png in the root directory -->
15
16 {% load staticfiles %}
17 <link rel="stylesheet" href="{% static 'css/style.min.css' %}">
18 </head>
19 <body>
20 <!--[if lt IE 7]>
21 <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>
22 <![endif]-->
23
24 <!-- Add your site or application content here -->
25
26 <div id="fb-root"></div>
27 <script>(function(d, s, id) {
28 var js, fjs = d.getElementsByTagName(s)[0];
29 if (d.getElementById(id)) return;
30 js = d.createElement(s); js.id = id;
31 js.src = "//connect.facebook.net/en_GB/all.js#xfbml=1";
32 fjs.parentNode.insertBefore(js, fjs);
33 }(document, 'script', 'facebook-jssdk'));</script>
34
35 <div class="navbar navbar-static-top navbar-inverse">
36 <div class="container-fluid">
37 <div class="navbar-header">
38 <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#header-nav">
39 <span class="icon-bar"></span>
40 <span class="icon-bar"></span>
41 <span class="icon-bar"></span>
42 </button>
43 <a class="navbar-brand" href="{% url 'blogengine:index' %}">My Django Blog</a>
44 </div>
45 <div class="collapse navbar-collapse" id="header-nav">
46 <ul class="nav navbar-nav">
47 {% load flatpages %}
48 {% get_flatpages as flatpages %}
49 {% for flatpage in flatpages %}
50 <li><a href="{{ flatpage.url }}">{{ flatpage.title }}</a></li>
51 {% endfor %}
52 <li><a href="/feeds/posts/">RSS feed</a></li>
53
54 <form action="/search" method="GET" class="navbar-form navbar-left">
55 <div class="form-group">
56 <input type="text" name="q" placeholder="Search..." class="form-control"></input>
57 </div>
58 <button type="submit" class="btn btn-default">Search</button>
59 </form>
60 </ul>
61 </div>
62 </div>
63 </div>
64
65 <div class="container">
66 {% block header %}
67 <div class="page-header">
68 <h1>My Django Blog</h1>
69 </div>
70 {% endblock %}
71
72 <div class="row">
73 {% block content %}{% endblock %}
74 </div>
75 </div>
76
77 <div class="container footer">
78 <div class="row">
79 <div class="span12">
80 <p>Copyright &copy; {% now "Y" %}</p>
81 </div>
82 </div>
83 </div>
84
85 <script src="{% static 'js/all.min.js' %}"></script>
86
87 <!-- Google Analytics: change UA-XXXXX-X to be your site's ID. -->
88 <script>
89 (function(b,o,i,l,e,r){b.GoogleAnalyticsObject=l;b[l]||(b[l]=
90 function(){(b[l].q=b[l].q||[]).push(arguments)});b[l].l=+new Date;
91 e=o.createElement(i);r=o.getElementsByTagName(i)[0];
92 e.src='//www.google-analytics.com/analytics.js';
93 r.parentNode.insertBefore(e,r)}(window,document,'script','ga'));
94 ga('create','UA-XXXXX-X');ga('send','pageview');
95 </script>
96 </body>
97</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:

1venv/
2*.pyc
3db.sqlite3
4reports/
5htmlcov/
6.coverage
7node_modules/

Then commit your changes:

1$ git add .
2$ 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:

1$ git add .slugignore
2$ git commit -m 'Added slugignore'
3$ 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.