Session 08

Building a Django Application

Wherein we build a simple blogging app.

A Full Stack Framework

Django comes with:

  • Persistence via the Django ORM
  • CRUD content editing via the automatic Django Admin
  • URL Mapping via urlpatterns
  • Templating via the Django Template Language
  • Caching with levels of configurability
  • Internationalization via i18n hooks
  • Form rendering and handling
  • User authentication and authorization

Pretty much everything you need to make a solid website quickly

Lots of frameworks offer some of these features, if not all.

What is Django’s killer feature

The Django Admin

Works in concert with the Django ORM to provide automatic CRUD functionality

You write the models, it provides the UI

You’ve seen this in action. Pretty neat, eh?

The Django Admin is a great example of the Pareto Priciple, a.k.a. the 80/20 rule:

80% of the problems can be solved by 20% of the effort

The converse also holds true:

Fixing the last 20% of the problems will take the remaining 80% of the effort.

Other Django Advantages

Clearly the most popular full-stack Python web framework at this time

Popularity translates into:

  • Active, present community
  • Plethora of good examples to be found online
  • Rich ecosystem of apps (encapsulated add-on functionality)

Jobs

Django releases in the last 12+ months (a short list):

  • 1.9 (December 2015)
  • 1.8.7 (November 2015)
  • 1.7.11 (November 2015)
  • 1.8.5 (October 2015)
  • 1.7.10 (August 2015)
  • 1.8.3 (July 2015)
  • 1.8 (April 2015)
  • 1.7.7 (March 2015)
  • 1.7.4 (January 2014)

Django 1.8 is the second Long Term Support version, with a guaranteed support period of three years.

Thorough, readable, and discoverable.

Led the way to better documentation for all Python

Read The Docs - built in connection with Django, sponsored by the Django Software Foundation.

Write documentation as part of your python package.

Render new versions of that documentation for every commit.

this is awesome

Where We Stand

For your homework this week, you created a Post model to serve as the heart of our blogging app.

You also took some time to get familiar with the basic workings of the Django ORM.

You made a minor modification to our model class and wrote a test for it.

And you installed the Django Admin site and added your app to it.

Going Further

One of the most common features in a blog is the ability to categorize posts.

Let’s add this feature to our blog!

To do so, we’ll be adding a new model, and making some changes to existing code.

This means that we’ll need to change our database schema.

You’ve seen how to add new tables to a database using the migrate command.

And you’ve created your first migration in setting up the Post model.

This is an example of altering the database schema using Python code.

Starting in Django 1.7, this ability is available built-in to Django.

Before verson 1.7 it was available in an add-on called South.

We want to add a new model to represent the categories our blog posts might fall into.

This model will need to have:

  • a name for the category
  • a longer description
  • a relationship to the Post model
# in models.py
class Category(models.Model):
    name = models.CharField(max_length=128)
    description = models.TextField(blank=True)
    posts = models.ManyToManyField(Post, blank=True,
                                   related_name='categories')

In our Post model, we used a ForeignKeyField field to match an author to her posts.

This models the situation in which a single author can have many posts, while each post has only one author.

We call this a Many to One relationship.

But any given Post might belong in more than one Category.

And it would be a waste to allow only one Post for each Category.

Enter the ManyToManyField

To get these changes set up, we now add a new migration.

We use the makemigrations management command to do so:

(djangoenv)$ ./manage.py makemigrations
Migrations for 'myblog':
  0002_category.py:
    - Create model Category

Once the migration has been created, we can apply it with the migrate management command.

(djangoenv)$ ./manage.py migrate
Operations to perform:
  Apply all migrations: sessions, contenttypes, admin, myblog, auth
Running migrations:
  Rendering model states... DONE
  Applying myblog.0002_category... OK

You can even look at the migration file you just applied, myblog/migrations/0002_category.py to see what happened.

Let’s make Category object look nice the same way we did with Post. Start with a test:

add this to tests.py:

# another import
from myblog.models import Category

# and the test case and test
class CategoryTestCase(TestCase):

    def test_string_representation(self):
        expected = "A Category"
        c1 = Category(name=expected)
        actual = str(c1)
        self.assertEqual(expected, actual)

When you run your tests, you now have two, and one is failing because the Category object doesn’t look right.

(djangoenv)$ ./manage.py test myblog
Creating test database for alias 'default'...
...

Ran 2 tests in 0.011s

FAILED (failures=1)

Do you remember how you made that change for a Post?

class Category(models.Model):
    #...

    def __str__(self):
        return self.name

Adding our new model to the Django admin is equally simple.

Simply add the following line to myblog/admin.py

# a new import
from myblog.models import Category

# and a new admin registration
admin.site.register(Category)

Fire up the Django development server and see what you have in the admin:

(djangoenv)$ ./manage.py runserver
Validating models...
...
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

Point your browser at http://localhost:8000/admin/, log in and play.

Add a few categories, put some posts in them. Visit your posts, add new ones and then categorize them.

BREAK TIME

We’ve completed a data model for our application.

And thanks to Django’s easy-to-use admin, we have a reasonable CRUD application where we can manage blog posts and the categories we put them in.

When we return, we’ll put a public face on our new creation.

If you’ve fallen behind, the app as it stands now is in our class resources as mysite_stage_1

A Public Face

Point your browser at http://localhost:8000/

What do you see?

Why?

We need to add some public pages for our blog.

In Django, the code that builds a page that you can see is called a view.

Django Views

A view can be defined as a callable that takes a request and returns a response.

This should sound pretty familiar to you.

Classically, Django views were functions.

Version 1.3 added support for Class-based Views (a class with a __call__ method is a callable)

Let’s add a really simple view to our app.

It will be a stub for our public UI. Add this to views.py in myblog

from django.http import HttpResponse, HttpResponseRedirect, Http404

def stub_view(request, *args, **kwargs):
    body = "Stub View\n\n"
    if args:
        body += "Args:\n"
        body += "\n".join(["\t%s" % a for a in args])
    if kwargs:
        body += "Kwargs:\n"
        body += "\n".join(["\t%s: %s" % i for i in kwargs.items()])
    return HttpResponse(body, content_type="text/plain")

In your homework tutorial, you learned about Django urlconfs

We used our project urlconf to hook the Django admin into our project.

We want to do the same thing for our new app.

In general, an app that serves any sort of views should contain its own urlconf.

The project urlconf should mainly include these where possible.

Create a new file urls.py inside the myblog app package.

Open it in your editor and add the following code:

from django.conf.urls import url
from myblog.views import stub_view

urlpatterns = [
    url(r'^$',
        stub_view,
        name="blog_index"),
]

In order for our new urls to load, we’ll need to include them in our project urlconf

Open urls.py from the mysite project package and add this:

# add this new import
from django.conf.urls import include

# then modify urlpatterns as follows:
urlpatterns = [
    url(r'^', include('myblog.urls')), #<- add this
    #... other included urls
]

Try reloading http://localhost:8000/

You should see some output now.

Project URL Space

A project is defined by the urls a user can visit.

What should our users be able to see when they visit our blog?

  • A list view that shows blog posts, most recent first.
  • An individual post view, showing a single post (a permalink).

Let’s add urls for each of these.

For now, we’ll use the stub view we’ve created so we can concentrate on the url routing.

We’ve already got a good url for the list page: blog_index at ‘/’

For the view of a single post, we’ll need to capture the id of the post. Add this to urlpatterns in myblog/urls.py:

url(r'^posts/(\d+)/$',
    stub_view,
    name="blog_detail"),

(\d+) captures one or more digits as the post_id.

Load http://localhost:8000/posts/1234/ and see what you get.

When you load the above url, you should see 1234 listed as an arg

Try changing the route like so:

r'^posts/(?P<post_id>\d+)/$'

Reload the same url.

Notice the change.

What’s going on there?

Like Pyramid, Django uses Python regular expressions to build routes.

Unlike Pyramid, Django requires regular expressions to capture segments in a route.

When we built our WSGI book app, we used this same appraoch.

There we learned about regular expression capture groups. We just changed an unnamed capture group to a named one.

How you declare a capture group in your url pattern regexp influences how it will be passed to the view callable.

from django.conf.urls import url
from myblog.views import stub_view

urlpatterns = [
    url(r'^$',
        stub_view,
        name="blog_index"),
    url(r'^posts/(?P<post_id>\d+)/$',
        stub_view,
        name="blog_detail"),
]

Before we begin writing real views, we need to add some tests for the views we are about to create.

We’ll need tests for a list view and a detail view

add the following imports at the top of myblog/tests.py:

import datetime
from django.utils.timezone import utc
class FrontEndTestCase(TestCase):
    """test views provided in the front-end"""
    fixtures = ['myblog_test_fixture.json', ]

    def setUp(self):
        self.now = datetime.datetime.utcnow().replace(tzinfo=utc)
        self.timedelta = datetime.timedelta(15)
        author = User.objects.get(pk=1)
        for count in range(1, 11):
            post = Post(title="Post %d Title" % count,
                        text="foo",
                        author=author)
            if count < 6:
                # publish the first five posts
                pubdate = self.now - self.timedelta * count
                post.published_date = pubdate
            post.save()

Our List View

We’d like our list view to show our posts.

But in this blog, we have the ability to publish posts.

Unpublished posts should not be seen in the front-end views.

We set up our tests to have 5 published, and 5 unpublished posts

Let’s add a test to demonstrate that the right ones show up.

Class FrontEndTestCase(TestCase): # already here
    # ...
    def test_list_only_published(self):
        resp = self.client.get('/')
        # the content of the rendered response is always a bytestring
        resp_text = resp.content.decode(resp.charset)
        self.assertTrue("Recent Posts" in resp_text)
        for count in range(1, 11):
            title = "Post %d Title" % count
            if count < 6:
                self.assertContains(resp, title, count=1)
            else:
                self.assertNotContains(resp, title)

We test first to ensure that each published post is visible in our view.

Note that we also test to ensure that the unpublished posts are not visible.

(djangoenv)$ ./manage.py test myblog
Creating test database for alias 'default'...
.F.
======================================================================
FAIL: test_list_only_published (myblog.tests.FrontEndTestCase)
...
Ran 3 tests in 0.024s

FAILED (failures=1)
Destroying test database for alias 'default'...

Add the view for listing blog posts to views.py.

# add these imports
from django.template import RequestContext, loader
from myblog.models import Post

# and this view
def list_view(request):
    published = Post.objects.exclude(published_date__exact=None)
    posts = published.order_by('-published_date')
    template = loader.get_template('list.html')
    context = RequestContext(request, {
        'posts': posts,
    })
    body = template.render(context)
    return HttpResponse(body, content_type="text/html")
published = Post.objects.exclude(published_date__exact=None)
posts = published.order_by('-published_date')

We begin by using the QuerySet API to fetch all the posts that have published_date set

Using the chaining nature of the API we order these posts by published_date

Remember, at this point, no query has actually been issued to the database.

template = loader.get_template('list.html')

Django uses configuration to determine how to find templates.

By default, Django looks in installed apps for a templates directory

It also provides a place to list specific directories.

Let’s set that up in settings.py

Notice that settings.py already contains a BASE_DIR value which points to the root of our project (where both the project and app packages are located).

In that same file, you’ll find a list bound to the symbol TEMPLATES.

That list contains one dict with an empty list at the key DIRS. Update that empty list as shown here:

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'mysite/templates')],
        ...
    },
]

This will ensure that Django will look in your mysite project folder for a directory containing templates.

The mysite project folder does not contain a templates directory, add one.

Then, in that directory add a new file base.html and add the following:

<!DOCTYPE html>
<html>
  <head>
    <title>My Django Blog</title>
  </head>
  <body>
    <div id="container">
      <div id="content">
      {% block content %}
       [content will go here]
      {% endblock %}
      </div>
    </div>
  </body>
</html>

Templates in Django

Before we move on, a quick word about Django templates.

We’ve seen Jinja2 which was “inspired by Django’s templating system”.

Basically, you already know how to write Django templates.

Django templates do not allow any python expressions.

https://docs.djangoproject.com/en/1.9/ref/templates/builtins/

Our view tries to load list.html.

This template is probably specific to the blog functionality of our site

It is common to keep shared templates in your project directory and specialized ones in app directories.

Add a templates directory to your myblog app, too.

In it, create a new file list.html and add this:

{% extends "base.html" %}{% block content %}
  <h1>Recent Posts</h1>
  {% comment %} here is where the query happens {% endcomment %}
  {% for post in posts %}
  <div class="post">
    <h2>{{ post }}</h2>
    <p class="byline">
      Posted by {{ post.author_name }} &mdash; {{ post.published_date }}
    </p>
    <div class="post-body">
      {{ post.text }}
    </div>
    <ul class="categories">
      {% for category in post.categories.all %}
        <li>{{ category }}</li>
      {% endfor %}
    </ul>
  </div>
  {% endfor %}
{% endblock %}
context = RequestContext(request, {
    'posts': posts,
})
body = template.render(context)

Like Jinja2, django templates are rendered by passing in a context

Django’s RequestContext provides common bits, similar to the context provided automatically by Pyramid

We add our posts to that context so they can be used by the template.

return HttpResponse(body, content_type="text/html")

Finally, we build an HttpResponse and return it.

This is, fundamentally, no different from the stub_view just above.

We need to fix the url for our blog index page

Update urls.py in myblog:

# import the new view
from myblog.views import list_view

# and then update the urlconf
url(r'^$',
    list_view,  #<-- Change this value from stub_view
    name="blog_index"),

Then run your tests again:

(djangoenv)$ ./manage.py test myblog
...
Ran 3 tests in 0.033s

OK

This is a common pattern in Django views:

  • get a template from the loader
  • build a context, usually using a RequestContext
  • render the template
  • return an HttpResponse

So common in fact that Django provides a shortcut for us to use:

render(request, template[, ctx][, ctx_instance])

Let’s replace most of our view with the render shortcut

from django.shortcuts import render # <- already there

# rewrite our view
def list_view(request):
    published = Post.objects.exclude(published_date__exact=None)
    posts = published.order_by('-published_date')
    context = {'posts': posts}
    return render(request, 'list.html', context)

Remember though, all we did manually before is still happening

BREAK TIME

We’ve got the front page for our application working great.

Next, we’ll need to provide a view of a detail page for a single post.

Then we’ll provide a way to log in and to navigate between the public part of our application and the admin behind it.

If you’ve fallen behind, the app as it stands now is in our class resources as mysite_stage_2

Our Detail View

Next, let’s add a view function for the detail view of a post

It will need to get the id of the post to show as an argument

Like the list view, it should only show published posts

But unlike the list view, it will need to return something if an unpublished post is requested.

Let’s start with the tests in views.py

Add the following test to our FrontEndTestCase in myblog/tests.py:

def test_details_only_published(self):
    for count in range(1, 11):
        title = "Post %d Title" % count
        post = Post.objects.get(title=title)
        resp = self.client.get('/posts/%d/' % post.pk)
        if count < 6:
            self.assertEqual(resp.status_code, 200)
            self.assertContains(resp, title)
        else:
            self.assertEqual(resp.status_code, 404)
(djangoenv)$ ./manage.py test myblog
Creating test database for alias 'default'...
.F..
======================================================================
FAIL: test_details_only_published (myblog.tests.FrontEndTestCase)
...
Ran 4 tests in 0.043s

FAILED (failures=1)
Destroying test database for alias 'default'...

Now, add a new view to myblog/views.py:

def detail_view(request, post_id):
    published = Post.objects.exclude(published_date__exact=None)
    try:
        post = published.get(pk=post_id)
    except Post.DoesNotExist:
        raise Http404
    context = {'post': post}
    return render(request, 'detail.html', context)
try:
    post = published.get(pk=post_id)
except Post.DoesNotExist:
    raise Http404

One of the features of the Django ORM is that all models raise a DoesNotExist exception if get returns nothing.

This exception is actually an attribute of the Model you look for.

There’s also an ObjectDoesNotExist for when you don’t know which model you have.

We can use that fact to raise a Not Found exception.

Django will handle the rest for us.

We also need to add detail.html to myblog/templates:

{% extends "base.html" %}

{% block content %}
<a class="backlink" href="/">Home</a>
<h1>{{ post }}</h1>
<p class="byline">
  Posted by {{ post.author_name }} &mdash; {{ post.published_date }}
</p>
<div class="post-body">
  {{ post.text }}
</div>
<ul class="categories">
  {% for category in post.categories.all %}
    <li>{{ category }}</li>
  {% endfor %}
</ul>
{% endblock %}

In order to view a single post, we’ll need a link from the list view

We can use the url template tag (like Pyramid’s request.route_url):

{% url '<view_name>' arg1 arg2 %}

In our list.html template, let’s link the post titles:

{% for post in posts %}
<div class="post">
  <h2>
    <a href="{% url 'blog_detail' post.pk %}">{{ post }}</a>
  </h2>
  ...

Again, we need to insert our new view into the existing myblog/urls.py in myblog:

# import the view
from myblog.views import detail_view

url(r'^posts/(?P<post_id>\d+)/$',
    detail_view, #<-- Change this from stub_view
    name="blog_detail"),
(djangoenv)$ ./manage.py test myblog
...
Ran 4 tests in 0.077s

OK

We’ve got some good stuff to look at now. Fire up the server

Reload your blog index page and click around a bit.

You can now move back and forth between list and detail view.

Try loading the detail view for a post that doesn’t exist

You’ve got a functional Blog

It’s not very pretty, though.

We can fix that by adding some css

This gives us a chance to learn about Django’s handling of static files

Static Files

Like templates, Django expects to find static files in particular locations

It will look for them in a directory named static in any installed apps.

They will be served from the url path in the STATIC_URL setting.

By default, this is /static/

To allow Django to automatically build the correct urls for your static files, you use a special template tag:

{% static <filename> %}

I’ve prepared a css file for us to use. You can find it in the class resources

Create a new directory static in the myblog app.

Copy the django_blog.css file into that new directory.

Next, load the static files template tag into base.html (this must be on the first line of the template):

{% load staticfiles %}

Finally, add a link to the stylesheet using the special template tag:

<title>My Django Blog</title> <!-- This is already present -->
<link type="text/css" rel="stylesheet" href="{% static 'django_blog.css' %}">

Reload http://localhost:8000/ and view the results of your work

We now have a reasonable view of the posts of our blog on the front end

And we have a way to create and categorize posts using the admin

However, we lack a way to move between the two.

Let’s add that ability next.

Ta-Daaaaaa!

So, that’s it. We’ve created a workable, simple blog app in Django.

If you fell behind at some point, the app as it now stands is in our class resources as mysite_stage_3.

There’s much more we could do with this app. And for homework, you’ll do some of it.

Then next session, we’ll work together as pairs to implement a simple feature to extend the blog

Homework

For your homework this week, we’ll fix one glaring problem with our blog admin.

As you created new categories and posts, and related them to each-other, how did you feel about that work?

Although from a data perspective, the category model is the right place for the ManytoMany relationship to posts, this leads to awkward usage in the admin.

It would be much easier if we could designate a category for a post from the Post admin.

Your Assignment

You’ll be reversing that relationship so that you can only add categories to posts

Take the following steps:

  1. Read the documentation about the Django admin.
  2. You’ll need to create a customized ModelAdmin class for the Post and Category models.
  3. And you’ll need to create an InlineModelAdmin to represent Categories on the Post admin view.
  4. Finally, you’ll need to exclude the ‘posts’ field from the form in your Category admin.

All told, those changes should not require more than about 15 total lines of code.

The trick of course is reading and finding out which fifteen lines to write.

If you complete that task in less than 3-4 hours of work, consider looking into other ways of customizing the admin.

  • Change the admin index to say ‘Categories’ instead of ‘Categorys’. (hint, the way to change this has nothing to do with the admin)
  • Add columns for the date fields to the list display of Posts.
  • Display the created and modified dates for your posts when viewing them in the admin.
  • Add a column to the list display of Posts that shows the author. For more fun, make this a link that takes you to the admin page for that user.
  • For the biggest challenge, look into admin actions and add an action to the Post admin that allows you to publish posts in bulk from the Post list display