Building a Blog with Django #2

NOTE: The Python code in this tutorial no longer works with Django! Please read my new article, a Django Blog Redux, for code that works on newer versions of Django. The rest of this article, such as the theory, is still very much applicable and should be read alongside my newer code.


In the previous installment, I covered my database model for this weblog and explained how to get the basic database layout working with the nice Django administration panel.

In this posting I will show you how to actually display your blog posts to your website visitors. I'll assume you already have a webserver working, with Django installed, and I assume you've already added a few blog posts using the admin tool discovered in part 1.

The first step is telling Django what our URL structure will be. Django does this in a very cool way - it lets us use regular expressions to define what URL paths will be used on our website, and what Python code will be called by users requesting those addresses. To keep this clean I will move all of the blog URL configuration into it's own file.

Let's tell Django to use this file. Edit your main urls.py file and add the following inside the urlpatterns section:

(r'^blog/', include('myproject.apps.blog.urls')),

This tells Django to look at the file 'myproject/apps/blog/urls.py' to complete the URL matching. So, open that file up and throw in the following:

from django.conf.urls.defaults import *  
blog_dict = {  
    'app_label': 'blog',  
    'module_name': 'posts',  
    'date_field': 'date',  
}  

urlpatterns = patterns('django.views.generic.date_based',  
    (r'^(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\w{1,2})/(?P<slug>[0-9A-Za-z-]+)/$', 'object_detail', dict(blog_dict, slug_field='slug')),  
    (r'^(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\w{1,2})/$',  'archive_day', blog_dict),  
    (r'^(?P<year>\d{4})/(?P<month>[a-z]{3})/$',  'archive_month', blog_dict),  
    (r'^(?P<year>\d{4})/$',  'archive_year',  blog_dict),  
    (r'^/?$',  'archive_index', blog_dict), )

This urlpatterns setup tells Django to use the built-in date-based generic views. These views let us avoid writing logic in Python to fetch, order, and manipulate data. The above code gives us a number of functions, all accessible from different URL's. There are 5 different requests that can be made, and they all relate to the data in 'blog_dict' - that is, we are looking at the application named 'blog', and within that app the data model called 'posts'. Lastly we tell it to use the field named 'date', inside the 'posts' model, to do our date sorting and breakdowns. In reverse order, the URL's above provide us with the following:

As you can see, those few (relatively) simple lines provide us with quite a bit of very useful functionality. Now all we need to do is display the content when users hit the above web addresses.

First things first, lets create a basic template. All this template will do is hold together the structure of our page.

Edit your settings.py file, and add a variable (near the bottom is OK) called TEMPLATE_DIRS. It should look like this:

TEMPLATE_DIRS = (  
    '/path/to/your/application/templates',  
)

Create a new folder in your application called 'templates', and alter the above path to point to that folder. In that folder, create a file called 'base.html', which will be our plain web template. You can spice it up later. Throw the following into that file:

<html>  
<head>  
<title>My First Django Blog</title>  
</head>  
<body>  
<h1>My First Django Blog</h1>  
{% block content %}Content will go here{% endblock %}  
</body>  
</html>

As you can see this is pretty straightforward HTML, except for one bit: the concept of blocks. This content block will be inserted later by our other templates.

When we use the Django generic views, it expects templates to be in a certain place. It will go through your TEMPLATE_DIRS listed above, looking for a folder named the same as the 'app_label' you defined in urls.py. Hence, create a folder in your template directory called 'blog'. We will put all the blog templates in this folder.

Now we will create the first template: the index. Because we're not yet drilling-down by date, this view is simply called an 'archive' view (reflected in urls.py - called 'archive_index'). Django's generic view templates are named by the model name, followed by the view name. In your 'blog' folder, create 'posts_archive.html' and paste into it the following:

{% extends "base" %}  

{% block content %}  
<h2>Blog: Latest Postings</h2>  
{% for object in latest %}  
        <h2><a id="{{ object.slug }}" xhref="{{ object.get_absolute_url }}">{{ object.title }}</a></h2>  

        {% if object.image %}<img align="right" xsrc="{{ object.get_image_url }}" /> {% endif %}  

        {{ object.body|truncatewords:80 }}  

        <div class="article_menu"><b>Posted on {{ object.date|date:"F j, Y" }}</b> Tags: {% for tag in object.get_tag_list %}{% if not forloop.first %}, {% endif %}{{ tag.title }}{% endfor %}</div>  

{% endfor %}  
{% endblock %}

As you should be able to see, this template will load up your base.html file, and substitute the content block for the one in this file. Django will pass through an array of objects called 'latest'. We run through this group of posts, each time assigning one to the variable 'object'. We then display a snippet of HTML for this post, with the title (linked to the full post), a truncated body (cut off at 80 words - the reader can click the title to read the rest of the post), the posting date, and the tags assigned to this post. Simple!

Now if you visit your /blogs/ URL you should have a list of any postings you added (in part 1, via the Admin screen).

The next step up the list is to list by year. In your blogs template directory create posts_archive_year.html (which is what the generic view will look for):

{% extends "base" %}  
{% block content %}  
<h2>Blog: {{ year }} Archive</h2>  
<ul>  
{% for date in date_list %}  
        <li><a xhref="{{ date|date:"M"|lower }}/">{{ date|date:"F" }}</a></li>  
{% endfor %}  
</ul>  
{% endblock %}

Next, posts_archive_month.html:

{% extends "base" %}  
{% block content %}  
<h2>Blog: {{ month|date:"F" }} {{ year }} Archive</h2>  
{% for object in object_list %}  
        <h2><a id="{{ object.slug }}" xhref="{{ object.get_absolute_url }}">{{ object.title }}</a></h2>  
        {% if object.image %}<img align="right" xsrc="{{ object.get_image_url }}" /> {% endif %}  
        <p>{{ object.body|truncatewords:80 }}</p>  

        <div class="article_menu"><b>Posted on {{ object.date|date:"F j, Y" }}</b></div>  
{% endfor %}  
{% endblock %}

And posts_archive_day.html (pretty much identical to the month view!).

{% extends "base" %}  
{% block content %}  
<h2>Blog: {{ day|date:"F j, Y" }} Archive</h2>  
{% for object in object_list %}  
        <h2><a id="{{ object.slug }}" xhref="{{ object.get_absolute_url }}">{{ object.title }}</a></h2>  
        {% if object.image %}<img align="right" xsrc="{{ object.get_image_url }}" /> {% endif %}  
        <p>{{ object.body|truncatewords:80 }}</p>  

        <div class="article_menu"><b>Posted on {{ object.date|date:"F j, Y" }}</b></div>  
{% endfor %}  
{% endblock %}

Lastly is posts_detail.html - this will display a single post.

{% extends "base" %}  
{% block content %}  
<h2>Blog Entry</h2>  
{% load comments %}  
{% get_free_comment_count for blog.posts object.slug as comment_count %}  
<h3><a id="{{ object.slug }}" xhref="{{ object.get_absolute_url }}">{{ object.title }}</a></h3>  

{% if object.image %}<img align="right" xsrc="{{ object.get_image_url }}" />{% endif %}  

<p>{{ object.body }}</p>  

<div class="article_menu"><b>Posted on {{ object.date|date:"F j, Y" }}</b> Tags: {% for tag in object.get_tag_list %}{% if not forloop.first %}, {% endif %}{{ tag.title }}{% endfor %}  
</div>

You now have a working blog application that allows you to quickly drill-down from year to month to day, showing just a particular subset of results.

In the next article, find out how to see a list of posts for a given tag, and how to let your users leave comments for you (guess what: it's real easy, thanks to Django!)


Thanks for reading.

I'd love to hear your feedback and comments. E-mail me, ross@rossp.org, or get in touch on Twitter where I'm @RossPoulton. Chat soon!