Recreating the “Building a Blog in Django” Screencast

Few days back, Django 1.7 release candidate 1 was announced. I have never been so excited about a release in recent memory. It has a new app loading framework and schema migrations baked right in!

It is also the significant release that I had been waiting for to update my original Django screencast. With more than 77 thousand views, it is by far the most popular Django screencast on YouTube.

Back in 2012, when I was working in Django, there were a lot of comparisons between Rails and Django. One of the biggest selling points of Ruby on Rails was their official 15-minute blog screencast. It was fascinating to watch an expert Rails developer build an entire site from scratch especially how deftly they use TextMate, which could nearly read his mind.

Armed with a Linux terminal and Emacs, I set out to create a similar screencast for anyone interested in Django. Live coding was something that I had not tried before. I remember starting at 11 pm one night, hoping to complete my recording by midnight.

But by the time I finished a recording without any major goofups it was 5 am in the morning. So with barely any editing, I directly uploaded the raw footage to YouTube and hit the bed.

Over time, a lot of people have loved the screencast, often asking how they can setup their development environment to resemble mine. Emacs despite not being an IDE is often as productive or more, depending on how well you customise it.

Why the Remake?

I have done several more Django screencasts after that, preferring to demonstrate entire projects in Django rather than focus on a specific feature. But the original still remains a favourite as it is beginner-friendly and short.

Plenty of new cool features like Class-based Views had come to Django since then. I also noted that there were very few tutorials which used Python 3. For starting a new Django project, I would definitely recommend Python 3.

So, I decided to remake it but even shorter. Crazily enough, I wanted to cover a lot more than the first screencast. Here are the topics I wanted to touch upon (ones which were not covered in the first screencast are in bold):

  • New 1.7 defaults
  • Syntax changes in Python 3.4
  • QuerySets and Custom Managers
  • Admin and ModelAdmin
  • Rich-text posts with django-markdown
  • Generic Class-based Views
  • Tags
  • RSS Feeds
  • Migrations
  • Running testcases

Thankfully, Django 1.7 comes with great project defaults like a predefined SQLite 3 database and admin URLs setup, which gives you a running start. Migrations is a real time-saver as you don’t have to keep re-entering your test data.

The entire recording clocked slightly above 15 mins. Here is the final video and the text version below it:

Screencast

People have translated my tutorials into various languages in the past. So I have added subtitles to this screencast. Enjoy the automatic translations in your preferred language!

Text Version

Some prefer to read the transcript of the screencast. Assuming you already have Django 1.7 installed in Python 3.4, you can follow these steps.

  1. Start the project

    django-admin startproject qblog
    cd qblog
    ./manage.py migrate
    ./manage.py createsuperuser
    ./manage.py runserver
    
  2. Add an app called blog

    ./manage startapp blog
    
  3. Open blog/models.py and change its contents to:

    from django.db import models
    from django.core.urlresolvers import reverse
    
    class EntryQuerySet(models.QuerySet):
        def published(self):
            return self.filter(publish=True)
    
    class Entry(models.Model):
        title = models.CharField(max_length=200)
        body = models.TextField()
        slug = models.SlugField(max_length=200, unique=True)
        publish = models.BooleanField(default=False)
        created = models.DateTimeField(auto_now_add=True)
        modified = models.DateTimeField(auto_now=True)
    
        objects = EntryQuerySet.as_manager()
    
        def __str__(self):
            return self.title
    
        class Meta:
            verbose_name = "Blog Entry"
            verbose_name_plural = "Blog Entries"
            ordering = ["-created"]
    
  4. In qblog/settings.py add "blog" to INSTALLED_APPS and migrate:

    ./manage.py makemigrations blog
    ./manage.py migrate blog
    
  5. Change blog/admin.py to:

    from django.contrib import admin
    from . import models
    
    class EntryAdmin(admin.ModelAdmin):
        list_display = ("title", "created")
        prepopulated_fields = {"slug": ("title",)}
    
    admin.site.register(models.Entry, EntryAdmin)
    

    Go to admin. Add an unpublished post and published one. Then open ./manage shell and check the difference between Entry.objects.all() and Entry.objects.published().

Markdown

  1. Install django-markdown

    pip install django-markdown
    
  2. Add "django_markdown" to INSTALLED_APPS.

  3. Add a second line to qblog/urls.py:

    url(r'^markdown/', include('django_markdown.urls')),
    
  4. Change blog/admin.py to:

    from django_markdown.admin import MarkdownModelAdmin
    
    class EntryAdmin(MarkdownModelAdmin):
        ...
    

    Enter test entries now.

Index View

  1. Change blog/views.py to:

    from django.views import generic
    from . import models
    
    class BlogIndex(generic.ListView):
        queryset = models.Entry.objects.published()
        template_name = "home.html"
        paginate_by = 2
    
  2. Create a url mapping in qblog/urls.py:

    urlpatterns = patterns(
        '',
        url(r'^admin/', include(admin.site.urls)),
        url(r'^markdown/', include('django_markdown.urls')),
        url(r'^', include('blog.urls')),
    )
    
  3. Create blog/urls.py:

    from django.conf.urls import patterns, include, url
    from . import views
    
    urlpatterns = patterns(
        '',
        url(r'^$', views.BlogIndex.as_view(), name="index"),
    )
    

Index Template

  1. Make template and static directories. Then, copy the files:

    mkdir templates
    cp -R /somewhere/templates/* templates
    mkdir static
    cp -R /somewhere/static/* static
    
  2. Create templates/home.html with:

    {% extends "base.html" %}
    {% load django_markdown %}
    
    {% block blog_entries %}
    {% for object in object_list %}
      <div class="post">
        <h2>{{ object.title }}</h2>
        <p class="meta">{{ object.created }}</p>
        {{ object.body|markdown }}
      </div>
    {% endfor %}
    {% endblock %}
    
  3. Add to qblog/settings.py:

    TEMPLATE_DIRS = (os.path.join(BASE_DIR, "templates"), )
    STATICFILES_DIRS = (os.path.join(BASE_DIR, "static"), )
    

    Refresh the home page now. Enjoy the first non-admin page.

Feed

  1. Create blog/feed.py with:

    from django.contrib.syndication.views import Feed
    from blog.models import Entry
    
    class LatestPosts(Feed):
        title = "Q Blog"
        link = "/feed/"
        description = "Latest Posts"
    
        def items(self):
            return Entry.objects.published()[:5]
    
  2. Add to blog/urls.py (last but one line):

    url(r'^feed/$', feed.LatestPosts(), name="feed"),
    
  3. Mention in the templates/base.html template’s HEAD:

    <link href="/feed/" rel="alternate" type="application/rss+xml" title="Q Blog Feed" />
    

Entry View

  1. Add to blog/views.py:

    class BlogDetail(generic.DetailView):
        model = models.Entry
        template_name = "post.html"
    
  2. Add to blog/urls.py:

    url(r'^(?P<slug>\S+)$', views.BlogDetail.as_view(), name="entry_detail"),
    
  3. Add to blog/models.py:

    objects = EntryQuerySet.as_manager()
    
    def get_absolute_url(self):
        return reverse("entry_detail", kwargs={"slug": self.slug})
    

Entry Template

  1. Open templates/home.html and save it as post.html while removing the for loop, like this:

    {% extends "base.html" %}
    {% load django_markdown %}
    
    <div class="post">
      <h2><a href="{% url "entry_detail" slug=object.slug %}">{{ object.title }}</a></h2>
      <p class="meta">
        {{ object.created }} |
        Tagged under {{  object.tags.all|join:", " }}
      </p>
      {{ object.body|markdown }}
    </div>
    

    Update templates/home.html with the same <h2> line.

Tags

  1. Add tags to blog/models.py:

    class Tag(models.Model):
        slug = models.SlugField(max_length=200, unique=True)
    
        def __str__(self):
            return self.slug
    
    class Entry(models.Model):
        ....
        tags = models.ManyToManyField(Tag)
    
  2. Migrate:

    ./manage.py makemigrations blog
    ./manage.py migrate blog
    
  3. Add to blog/admin.py:

    admin.site.register(models.Tag)
    
  4. Change template templates/post.html to:

    <p class="meta">{{ object.created }} |
      Tagged under {{ object.tags.all|join:", " }}
    </p>
    

Testing

  1. Add these two test cases in blog/tests.py:

    from django.test import TestCase
    from django.contrib.auth import get_user_model
    from .models import Entry
    
    class BlogPostTest(TestCase):
    
        def test_create_unpublished(self):
            entry = Entry(title="Title Me", body=" ", publish=False)
            entry.save()
            self.assertEqual(Entry.objects.all().count(), 1)
            self.assertEqual(Entry.objects.published().count(), 0)
            entry.publish = True
            entry.save()
            self.assertEqual(Entry.objects.published().count(), 1)
    
    class BlogViewTests(TestCase):
        def test_feed_url(self):
            response = self.client.get('/feed/')
            self.assertIn("xml", response['Content-Type'])
    
  2. Run the tests by ./manage.py test blog and they should both pass.

Final Notes

The entire source is available on Github. If you face any issues, make sure that you are running the same Python/Django versions and compare your code with mine.

As always, I would love to hear your comments and feedback.

Hi! Welcome to ArunRocks, an odd collection of writeups on programming, travel, gadgets and practically anything under the sun. This state of affairs could be blamed on the ecelectic interests of your host, Arun Ravindran. He loves programming in several languages especially Python. In his day job he works as a Solution Manager at Unisys. Read more...

Comments