Recreating the "Building a Blog in Django" Screencast

6 min read · Posted on: Jul 6, 2014 · Print this page

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
  1. Add an app called blog
    ./manage startapp blog
  1. 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"]

  1. In qblog/settings.py add "blog" to INSTALLED_APPS and migrate:
    ./manage.py makemigrations blog
    ./manage.py migrate blog
  1. 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
  1. Add "django_markdown" to INSTALLED_APPS.

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

    url(r'^markdown/', include('django_markdown.urls')),
  1. Change blog/admin.py to:
    from django_markdown.admin import MarkdownModelAdmin


    class EntryAdmin(MarkdownModelAdmin):
        ...

Enter test entries now.
Markdown Editor Toolbar Not Showing in Admin?

Despite this being a Python 3.x tutorial, many have tried this in Python 2.x. Which is great, but the snazzy Markdown Editor in admin seems to be missing for them.

When I had checked, I found that the Markdown widget's media i.e. javascript and stylesheets were not getting added in admin. A workaround is to explicitly define form_overrides as follows.

  from django_markdown.widgets import AdminMarkdownWidget
  from django.db.models import TextField

class EntryAdmin(MarkdownModelAdmin): list_display = (“title”, “created”) prepopulated_fields = {“slug”: (“title”,)} # Next line is a workaround for Python 2.x formfield_overrides = {TextField: {‘widget’: AdminMarkdownWidget}}

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
  1. 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')),
    )
  1. 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
  1. 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 %}

  1. 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]
  1. Add to blog/urls.py (last but one line):
    from . import views, feed
    
    url(r'^feed/$', feed.LatestPosts(), name="feed"),

  1. 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"
  1. Add to blog/urls.py:
    url(r'^entry/(?P<slug>\S+)$', views.BlogDetail.as_view(), name="entry_detail"),
  1. 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)
  1. Migrate:
    ./manage.py makemigrations blog
    ./manage.py migrate blog
  1. Add to blog/admin.py:
    admin.site.register(models.Tag)
  1. 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'])
  1. 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.


Arun Ravindran profile pic

Arun Ravindran

Arun is the author of "Django Design Patterns and Best Practices". Works as a Product Manager at Google. Avid open source enthusiast. Keen on Python. Loves to help people learn technology. Find out more about Arun on the about page.

Don't miss any future posts!

Comments →

Next: ▶   Introducing Edge - a Modern Django Project Template

Prev: ◀   Understanding Test Driven Development with Django

Up: ▲   Blog

Featured Posts

Frequent Tags

banner ad for Django book

Comments

powered by Disqus