Software | Web | Project Management

Building a simple django blog

I created a simple django blog to use on the Starcross website. Here's a write up about the design decisions I made an some code snippets I used. As with all my posts, I'm interested in your suggestions, comments, and any other contributions.

There are lots of examples of django blogs out there, but they are simple to build so I decided to create one to my exact requirements, and ease any future integration with the rest of the site. Perhaps it will help serve as a introduction if you wish to do the same.

First, I defined two models to store the blog entries and associated comments in the models.py


from django.db import models
from datetime import datetime
from tinymce.models import HTMLField

class BlogEntry(models.Model):
    title = models.CharField(max_length=250)
    entry_text = HTMLField()
    date_published = models.DateTimeField(default=datetime.now())

    def __str__(self):
        return self.title


class Comment(models.Model):
    entry  = models.ForeignKey(BlogEntry)
    name = models.CharField(max_length=250)
    email = models.EmailField(blank=True)
    date_submitted = models.DateTimeField(auto_now_add=True)
    comment_text = HTMLField()

    def __str__(self):
        return self.comment_text

The BlogEntry class just contains a title, content, and date. The Comment has a foreign key for the blog entry, some basic personal details, and the comment text.

I wanted a basic HTML (or 'WYSIWYG') editor, and chose django-tinymce for this purpose. This was mainly because I'm familiar with it from Plone, albeit in a more sophisticated form. Django-packages provides a comparison of editors if you want to check out a sample of what's out there. I was a little surprised one isn't included out-of-the-box at first, as I thought this is a fairly common use case. Perhaps one might become part of django.contrib in future?

django-tinymce provides a HTMLField, which makes it fairly easy to integrate into the views and templates as required. It's configured to appear by default in the django admin site. It's simple to configure, although TinyMCE itself is a bit more complex if you need to fine tune it's appearance

Next, creating views for a list of recent entries, and the entries themselves with a comment form in the views.py


from django.views.generic.list import ListView
from django.views.generic.detail import DetailView
from blog.models import BlogEntry, Comment

class BlogEntryList(ListView):
    model = BlogEntry

    def get_queryset(self):
        return BlogEntry.objects.order_by('date_published').reverse() class BlogEntryView(DetailView): model = BlogEntry def get_context_data(self, **kwargs): context = super(BlogEntryView, self).get_context_data(**kwargs) comment_form = CommentCreateForm() context['comment_form'] = comment_form return context

The class BlogEntryList uses ListView to provide an itemised summary of blog entries. The order of entries is reversed by overriding get_queryset, as we expect to see the newest entries first.

The DetailView provides almost enough as it comes, except I want the comment form to appear at the end of each post. I used Django's ModelForm class to create the form based on the comment model, but it needs to be made available in the template.

This can be done with class based views by overriding get_context_data and adding an instance of the ModelForm to the context. The comment_form object now contains all the fields that can be referenced in the BlogEntryView template

Here's the template for BlogEntryView:


    <h2>{{ blogentry.title }}</h2>

    {% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}

    <div>
        {{ blogentry.entry_text|safe }}
    </div>
    {% for comment in blogentry.comment_set.all %}
        <div class="comment">
            <p><a href="mailto:{{comment.email}}">{{ comment.name }}</a>
                {{ comment.date_submitted|naturaltime }}</p>
            <div>{{ comment.comment_text|safe }}</div>
        </div>
    {% endfor %}
    <form method="post" action="/blog/comment">
        {% csrf_token %}
        <h3>Add a comment</h3>
        {{ comment_form.as_p|safe }}
        <input type="hidden" name="entry" value="{{ blogentry.pk }}">
        <p><input type="submit"></p>
    </form>

We can refer to comment_form alongside the rest of the blog entry data, as it has been added to the context. I then use the as_p function to provide a basic rendering seperated by <p> tags

In order to create comments, I used CreateView. The only thing I needed to add here was to assign the same ModelForm as used above (CommentCreateForm) and redirect submitted comments back to their parent entry page

from django.core.urlresolvers import reverse
class CommentCreate(CreateView): model = Comment form_class = CommentCreateForm def get_success_url(self): return reverse('blog:blogentry', kwargs={'pk': self.object.entry.pk})

The CommentCreateForm also needs little modification, only to hide the entry foreign key field which would otherwise appear as an integer


from django.forms import ModelForm, HiddenInput
from django.views.generic.edit import CreateView

from captcha.fields import CaptchaField
class CommentCreateForm(ModelForm): captcha = CaptchaField() class Meta: model = Comment widgets = {'entry' : HiddenInput()}

I have also added a captcha. Spam is pretty much guaranteed on an open form on the web, so there needs to be some barrier to stop it. Again, there are various packages available, but I used django-simple-captcha. This slots conveniently into the ModelForm and takes care of itself. It uses PIL/Pillow to generate the captcha images, so you may need to install that.

For the purpose of testing, the areas I wanted to cover were the display of entries and comments, as well as comment creation. I created two tests to cover this:


from django.test import TestCase
from django.core.urlresolvers import reverse
from django.utils import timezone
from captcha.conf import settings

from blog.models import BlogEntry


class BlogTests(TestCase):

    test_title = "My new blog entry"
    test_entry = "<p>Welcome to my blog</p>"

    comment_name = "Mr nobody"
    comment_email = "foo@bar.com"
    comment_text = "<p>A great post!</p>"

    def setUp(self):
        self.blogentry = BlogEntry.objects.create(title=self.test_title,
                                           entry_text=self.test_entry,
                                           date_published=timezone.now())

    def test_blogentry_list(self):

        response = self.client.get(reverse('blog:blogentry_list'))
        self.assertEqual(response.status_code,200)
        self.assertContains(response,self.test_title)
        self.assertContains(response,self.test_entry)

    def test_entry_comment_form(self):

        settings.CAPTCHA_TEST_MODE = True

        response = self.client.post(reverse('blog:comment_form'),{'name' : self.comment_name,
                                               'email' : self.comment_email,
                                               'comment_text' : self.comment_text,
                                               'entry' : self.blogentry.pk,
                                               'captcha_0': 'abc',
                                               'captcha_1': 'passed'
                                               },follow=True)

        self.assertEqual(response.status_code,200)
        self.assertContains(response,self.comment_name)
        self.assertContains(response,self.comment_text)

This test suite initally creates a basic blog entry. Then test_blogentry_list checks this entry appears in the preview in the list of all entries. The test_entry_comment_form creates a post and checks this post appears on the following page.

Because I'm using a captcha, this needs to be worked around during the test, and django-simple-captcha enables this with a special setting (CAPTCHA_TEST_MODE), and passing test data in the form (captcha_0 needs to be present and captcha_1 must contain 'passed'). It's possible to override settings in django using a decorator, but this didn't work for me (it had no effect). Instead I imported the captcha settings directly and changed it that way.

Because the comment post results in a redirect back to the entry, it's necessary to tell the test client to follow this by using the follow=True parameter. Otherwise it will return only the initial http response that contains the redirect.

GitHub

You can check out the full code on GitHub if you want to try it yourself. I have simplified slightly in few places for the puposes of this write up, to help emphasise the core elements.

Add a comment

captcha