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:
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.
- Start the project
django-admin startproject qblog
cd qblog
./manage.py migrate
./manage.py createsuperuser
./manage.py runserver
- Add an app called
blog
./manage startapp blog
- 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"]
- In
qblog/settings.py
add"blog"
to INSTALLED_APPS and migrate:
./manage.py makemigrations blog
./manage.py migrate blog
- 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
- Install django-markdown
pip install django-markdown
-
Add
"django_markdown"
to INSTALLED_APPS. -
Add a second line to
qblog/urls.py
:
url(r'^markdown/', include('django_markdown.urls')),
- Change
blog/admin.py
to:
from django_markdown.admin import MarkdownModelAdmin
class EntryAdmin(MarkdownModelAdmin):
...
Enter test entries now.
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 TextFieldclass 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
- 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
- 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')),
)
- 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
- Make template and static directories. Then, copy the files:
mkdir templates
cp -R /somewhere/templates/* templates
mkdir static
cp -R /somewhere/static/* static
- 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 %}
- 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
- 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]
- Add to
blog/urls.py
(last but one line):
from . import views, feed
url(r'^feed/$', feed.LatestPosts(), name="feed"),
- Mention in the
templates/base.html
template’sHEAD
:
<link href="/feed/" rel="alternate" type="application/rss+xml" title="Q Blog Feed" />
Entry View
- Add to
blog/views.py
:
class BlogDetail(generic.DetailView):
model = models.Entry
template_name = "post.html"
- Add to
blog/urls.py
:
url(r'^entry/(?P<slug>\S+)$', views.BlogDetail.as_view(), name="entry_detail"),
- Add to
blog/models.py
:
objects = EntryQuerySet.as_manager()
def get_absolute_url(self):
return reverse("entry_detail", kwargs={"slug": self.slug})
Entry Template
- Open
templates/home.html
and save it aspost.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
- 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)
- Migrate:
./manage.py makemigrations blog
./manage.py migrate blog
- Add to
blog/admin.py
:
admin.site.register(models.Tag)
- Change template
templates/post.html
to:
<p class="meta">{{ object.created }} |
Tagged under {{ object.tags.all|join:", " }}
</p>
Testing
- 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'])
- 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.