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 }} — {{ 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 }} — {{ 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:
- Read the documentation about the Django admin.
- You’ll need to create a customized ModelAdmin class for the
Post
andCategory
models. - And you’ll need to create an InlineModelAdmin to represent Categories on the Post admin view.
- 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