Django Blog Tutorial - the Next Generation - Part 8
Published by Matthew Daly 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.txt2$ 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 post4 post = PostFactory()56 # Create a flat page7 page = FlatPageFactory()89 # Get sitemap10 response = self.client.get('/sitemap.xml')11 self.assertEquals(response.status_code, 200)1213 # Check post is present in sitemap14 self.assertTrue('my-first-post' in response.content)1516 # Check page is present in sitemap17 self.assertTrue('/about/' in response.content)
Run it, and you should see the test fail:
1$ python manage.py test blogengine2Creating test database for alias 'default'...3...........................F4======================================================================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_sitemap9 self.assertEquals(response.status_code, 200)10AssertionError: 404 != 2001112----------------------------------------------------------------------13Ran 28 tests in 6.873s1415FAILED (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 Sitemap2from django.contrib.flatpages.models import FlatPage3from blogengine.models import Post45class PostSitemap(Sitemap):6 changefreq = "always"7 priority = 0.589 def items(self):10 return Post.objects.all()1112 def lastmod(self, obj):13 return obj.pub_date141516class FlatpageSitemap(Sitemap):17 changefreq = "always"18 priority = 0.51920 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 sitemap2from blogengine.sitemap import PostSitemap, FlatpageSitemap34# Define sitemaps5sitemaps = {6 'posts': PostSitemap,7 'pages': FlatpageSitemap8}
Then add the following after the existing routes:
1 # Sitemap2 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 blogengine2Creating test database for alias 'default'...3............................4----------------------------------------------------------------------5Ran 28 tests in 6.863s67OK8Destroying test database for alias 'default'...
And done! Let's commit our changes:
1$ git add blogengine/ django_tutorial_blog_ng/settings.py2$ 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)45 def title(self, obj):6 return "RSS feed - blog posts tagged %s" % obj.name78 def link(self, obj):9 return obj.get_absolute_url()1011 def description(self, obj):12 return "RSS feed - blog posts tagged %s" % obj.name1314 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)45 def title(self, obj):6 return "RSS feed - blog posts tagged %s" % obj.name78 def link(self, obj):9 return obj.get_absolute_url()1011 def description(self, obj):12 return "RSS feed - blog posts tagged %s" % obj.name1314 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 text4 """5 # Get the query data6 query = request.GET.get('q', '')7 page = request.GET.get('page', 1)89 # Query the database10 if query:11 results = Post.objects.filter(Q(text__icontains=query) | Q(title__icontains=query))12 else:13 results = None1415 # Add pagination16 pages = Paginator(results, 5)1718 # Get specified page19 try:20 returned_page = pages.page(page)21 except EmptyPage:22 returned_page = pages.page(pages.num_pages)2324 # Display the search results25 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 text4 """5 # Get the query data6 query = request.GET.get('q', '')7 page = request.GET.get('page', 1)89 # Query the database10 results = Post.objects.filter(Q(text__icontains=query) | Q(title__icontains=query))1112 # Add pagination13 pages = Paginator(results, 5)1415 # Get specified page16 try:17 returned_page = pages.page(page)18 except EmptyPage:19 returned_page = pages.page(pages.num_pages)2021 # Display the search results22 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 present3 response = self.client.get('/search?q=wibble')4 self.assertEquals(response.status_code, 200)5 self.assertTrue('No posts found' in response.content)67 # Try to get nonexistent second page8 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 blogengine2$ 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 python2from fabric.api import local34def deploy():5 """6 Deploy the latest version to Heroku7 """8 # Push changes to master9 local("git push origin master")1011 # Push changes to Heroku12 local("git push heroku master")1314 # Run migrations on Heroku15 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.txt2$ 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, url23from django.contrib import admin4admin.autodiscover()56urlpatterns = patterns('',7 # Examples:8 # url(r'^$', 'django_tutorial_blog_ng.views.home', name='home'),9 # url(r'^blog/', include('blog.urls')),1011 url(r'^admin/', include(admin.site.urls)),1213 # Blog URLs14 url(r'', include('blogengine.urls', namespace="blogengine")),1516 # Flat pages17 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, url2from django.views.generic import ListView, DetailView3from blogengine.models import Post, Category, Tag4from blogengine.views import CategoryListView, TagListView, PostsFeed, CategoryPostsFeed, TagPostsFeed, getSearchResults5from django.contrib.sitemaps.views import sitemap6from blogengine.sitemap import PostSitemap, FlatpageSitemap78# Define sitemaps9sitemaps = {10 'posts': PostSitemap,11 'pages': FlatpageSitemap12}1314urlpatterns = patterns('',15 # Index16 url(r'^(?P<page>\d+)?/?$', ListView.as_view(17 model=Post,18 paginate_by=5,19 ),20 name='index'21 ),2223 # Individual posts24 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 ),2930 # Categories31 url(r'^category/(?P<slug>[a-zA-Z0-9-]+)/?$', CategoryListView.as_view(32 paginate_by=5,33 model=Category,34 ),35 name='category'36 ),373839 # Tags40 url(r'^tag/(?P<slug>[a-zA-Z0-9-]+)/?$', TagListView.as_view(41 paginate_by=5,42 model=Tag,43 ),44 name='tag'45 ),4647 # Post RSS feed48 url(r'^feeds/posts/$', PostsFeed()),4950 # Category RSS feed51 url(r'^feeds/posts/category/(?P<slug>[a-zA-Z0-9-]+)/?$', CategoryPostsFeed()),5253 # Tag RSS feed54 url(r'^feeds/posts/tag/(?P<slug>[a-zA-Z0-9-]+)/?$', TagPostsFeed()),5556 # Search posts57 url(r'^search', getSearchResults, name='search'),5859 # Sitemap60 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/" >1314 <!-- Place favicon.ico and apple-touch-icon.png in the root directory -->1516 {% 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]-->2930 <!-- Add your site or application content here -->3132 <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>4041 <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>5960 <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>7071 <div class="container">72 {% block header %}73 <div class="page-header">74 <h1>My Django Blog</h1>75 </div>76 {% endblock %}7778 <div class="row">79 {% block content %}{% endblock %}80 </div>81 </div>8283 <div class="container footer">84 <div class="row">85 <div class="span12">86 <p>Copyright © {% now "Y" %}</p>87 </div>88 </div>89 </div>9091 <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>9596 <!-- 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" %}23 {% load custom_markdown %}45 {% 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 %}2930 <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>3839 {% 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.456s67OK8Destroying 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 pdb2pdb.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 ipdb2$ pip freeze > requirements.txt
Now you can use ipdb
in much the same way as you would use pdb
:
1import ipdb2ipdb.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-toolbar2$ 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';34 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 });3233 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/" >1314 <!-- Place favicon.ico and apple-touch-icon.png in the root directory -->1516 {% 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]-->2324 <!-- Add your site or application content here -->2526 <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>3435 <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>5354 <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>6465 <div class="container">66 {% block header %}67 <div class="page-header">68 <h1>My Django Blog</h1>69 </div>70 {% endblock %}7172 <div class="row">73 {% block content %}{% endblock %}74 </div>75 </div>7677 <div class="container footer">78 <div class="row">79 <div class="span12">80 <p>Copyright © {% now "Y" %}</p>81 </div>82 </div>83 </div>8485 <script src="{% static 'js/all.min.js' %}"></script>8687 <!-- 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*.pyc3db.sqlite34reports/5htmlcov/6.coverage7node_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 .slugignore2$ 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.