Chapter 4. Building a Blog¶
Topics in this chapter:¶
So far, we have seen how to use django’s admin interface, and generic views. In this chapter we will write custom views and blog entry admin page, store some user details in the session and use date based generic views for our blog archives.
Models with ForeignKeys:¶
To link two Models, we use foreign keys. Since we may have many comments for a blog post, we would like to have a relation from the comment model to the post model. This can be done by using a model field of the type ‘ForeignKey’. It requires a string or class representing the Model to which the ForeignKey is to be created. We would also need ForeignKey to link each blog post to it’s owner, since we want only the admin to be able to create a post.
As we shall see, django simplifies access to foreign keys by automatically creating a related object set on the linked Model.
class Post(models.Model):
text = models.TextField()
...
...
class Comment(models.Model):
text = models.CharField(max_length=100)
post = models.ForeignKey(Post)
...
...
would relate each Post
to multiple Comment
Now, to fetch all comments related to a Post, we can use
post = Post.objects.get(pk=1)
comments = post.comment_set.all()
So, post
gets a <ForeignKeyModel>_set
property which is actually the Manager
for Comments
related to that post
.
ModelForms:¶
We have already seen how the admin
app creates the form for our model by inspecting its fields. If we want to
customize the autogenerated forms, we can use ModelForms. In this chapter we see how to use ModelForms to create
and customize the forms for post and comment models. By convention, all custom forms should be in <app>/forms.py
The simplest way to use ModelForms would be:
class PostForm(ModelForm):
class Meta:
model = Post
The autogenerated fields can be overridden by explicitly specifying the field.
To change the html widget and label used by text
field of Post
model, one would do:
class PostForm(ModelForm):
text = forms.CharField(widget=forms.TextArea, label='Entry')
class Meta:
model = Post
Custom views with forms and decorators:¶
We will write our custom views in this chapter. We have already introduced views in the previous chapter, so we will see how to use forms in our views and how to limit access to certain views using decorators.
form = PostForm(request.POST or None)
Here we are handling GET and POST in the same view. If this is a GET request, the form would be empty else it would be filled with the POST contents.
form.is_valid
validates the form and returns True
or False
We will use these two together to save a valid form or display empty form.
To restrict views to a condition, we can use the user_passes_test
decorator from contrib.auth
.
The decorator takes a callable which should perform the test on the user
argument and return True
or False
.
The view is called only when the user passes the test. If the user is not logged in or does not pass the test,
it redirects to LOGIN_URL
of settings. By default this is /accounts/login
and we will handle this url from urls.py
Some other useful decorators are:
django.contrib.admin.views.decorators import staff_member_required
Restricts view to staff members only.
django.contrib.auth.decorators.login_required
Restricts view to logged in users only
Our blog app:¶
Let’s list out the features we would want to see in our blog:
- Create/Edit blog post (restricted to admin)
- View blog post (public)
- Comment on a blog post (anonymous)
- Store anonymous user details in session
- Show month based blog archives
- Generate RSS feeds
We have two models here: Post
and Comment
. The data we would like store are:
For Post:
- Title
- Text Content
- Slug
- Created Date
- Author
For Comment:
- Name
- Website
- Text Content
- Post related to this comment
- Created Date
Note
Since we want anonymous to be able to comment on a post, we are not relating the comment poster to a registered user.
We want the author
field of the post to be mapped to a registered user and the post
field
to be mapped to a valid Post
. As we shall see, we will ForeignKeys to the appropriate models
to manage these.
Models:¶
We have already seen how to create and integrate an app into our project, so I will start with the models
from django.db import models
from django.template.defaultfilters import slugify
from django.contrib.auth.models import User
# Create your models here.
class Post(models.Model):
title = models.CharField(max_length=100)
slug = models.SlugField(unique=True)
text = models.TextField()
created_on = models.DateTimeField(auto_now_add=True)
author = models.ForeignKey(User)
def __unicode__(self):
return self.title
@models.permalink
def get_absolute_url(self):
return ('blog_post_detail', (),
{
'slug' :self.slug,
})
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
super(Post, self).save(*args, **kwargs)
class Comment(models.Model):
name = models.CharField(max_length=42)
email = models.EmailField(max_length=75)
website = models.URLField(max_length=200, null=True, blank=True)
text = models.TextField()
post = models.ForeignKey(Post)
created_on = models.DateTimeField(auto_now_add=True)
def __unicode__(self):
return self.text
Quite a few new things here, let’s analyze them:
- slug field - it is used for storing slugs (e.g. this-is-a-slug). SEO or something.
- We will be using slugs in the url to fetch a blog post, so this must be unique.
slugify
is a helper function to get slug from a string. We won’t need to get the slug from the form, we will generate it ourself usingslugify
- To autogenerate the slug, we override the
model
save method, whose signature issave(self, *args, **kwargs)
We setself.slug
to the slug generated byslugify
and call the parentsave
method. - This ensures that every time a model is saved, it will have a slug field.
- The
get_absolute_url
of thePost
model points to theblog_post_detail
which takes aslug
parameter. This is thePost
detail view, and it fetches the post based on theslug
. We will soon see how this is implemented. model.ForeignKey
is a ForeignKey field which can be used to link this model to any other model. Here we want to link theauthor
field to aUser
, which is django’s model for a user. It comes fromdjango.contrib.auth
app, which is another useful package shipped with django.- Similarly to link a
Comment
to aPost
we have a ForeignKey from in thepost
field of the comment. - We won’t need the
author
field from thePost
form either, but we will fill it up in the view, where we have access to the logged in user details
Views:¶
The views we would need are:
- Admin should be able to login
- Add/Edit a post - restricted to admin
- View a blog post
- Comment on a blog post
We need to customize our forms to only display fields which need user input, because we will take care of the rest. For example, we have already
seen how to autofill slug field. Next, we would like to autofill post
for Comment
and author
for Post
in the view. Heres our
blog/forms.py
from django import forms
from models import Post, Comment
class PostForm(forms.ModelForm):
class Meta:
model = Post
exclude = ['author', 'slug']
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
exclude = ['post']
For login, we will use django.contrib.auth.views.login
view which is included in the contrib.auth
app. It expects a registration/login.html
which we will steal from django/contrib/admin/templates/admin/login.html
. We will include the login url in the project urls.
from django.conf.urls.defaults import *
# Uncomment the next two lines to enable the admin:
from django.contrib import admin
admin.autodiscover()
urlpatterns = patterns('',
# Example:
# (r'^djen_project/', include('djen_project.foo.urls')),
# Uncomment the admin/doc line below and add 'django.contrib.admindocs'
# to INSTALLED_APPS to enable admin documentation:
# (r'^admin/doc/', include('django.contrib.admindocs.urls')),
# Uncomment the next line to enable the admin:
(r'^accounts/login/$', 'django.contrib.auth.views.login'),
(r'^admin/', include(admin.site.urls)),
(r'^pastebin/', include('pastebin.urls')),
(r'^blog/', include('blog.urls')),
)
In blog/templates/registration/login.html
, copy contents from django/contrib/admin/templates/admin/login.html
{% extends "admin/base_site.html" %}
{% load i18n %}
{% block extrastyle %}{% load adminmedia %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% admin_media_prefix %}css/login.css" />{% endblock %}
{% block bodyclass %}login{% endblock %}
{% block content_title %}{% endblock %}
{% block breadcrumbs %}{% endblock %}
{% block content %}
{% if error_message %}
<p class="errornote">{{ error_message }}</p>
{% endif %}
<div id="content-main">
<form action="{{ app_path }}" method="post" id="login-form">{% csrf_token %}
<div class="form-row">
<label for="id_username">{% trans 'Username:' %}</label> <input type="text" name="username" id="id_username" />
</div>
<div class="form-row">
<label for="id_password">{% trans 'Password:' %}</label> <input type="password" name="password" id="id_password" />
<input type="hidden" name="this_is_the_login_form" value="1" />
</div>
<div class="submit-row">
<label> </label><input type="submit" value="{% trans 'Log in' %}" />
</div>
</form>
<script type="text/javascript">
document.getElementById('id_username').focus()
</script>
</div>
{% endblock %}
For the others, we will write custom views in blog/views.py
.
# Create your views here.
from django.contrib.auth.decorators import user_passes_test
from django.shortcuts import redirect, render_to_response, get_object_or_404
from django.template import RequestContext
from models import Post
from forms import PostForm, CommentForm
@user_passes_test(lambda u: u.is_superuser)
def add_post(request):
form = PostForm(request.POST or None)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.save()
return redirect(post)
return render_to_response('blog/add_post.html',
{ 'form': form },
context_instance=RequestContext(request))
def view_post(request, slug):
post = get_object_or_404(Post, slug=slug)
form = CommentForm(request.POST or None)
if form.is_valid():
comment = form.save(commit=False)
comment.post = post
comment.save()
return redirect(request.path)
return render_to_response('blog/blog_post.html',
{
'post': post,
'form': form,
},
context_instance=RequestContext(request))
Note:
- The
user_passes_test
decorator whether the user is admin or not. If not, it will redirect the user to login page. - We are using the
ModelForms
defined informs.py
to autogenerate forms from our Models. ModelForm
includes asave
method (just like aModels
save method) which saves the model data to the database.commit=False
on a form save gives us the temporaryModel
object so that we can modify it and save permanently. Here, we have used it to autofill theauthor
ofPost
andpost
ofComment
redirect
is a shortcut that redirects usingHttpResponseRedirect
to another url or a model’sget_absolute_url
property.
Templates:¶
The corresponding templates for these views would look like:
blog/templates/blog/add_post.html
:
<h2>Hello {{ user.username }}</h2>
<br />
<h2>Add new post</h2>
<form action="" method="POST">
{% csrf_token %}
<table>
{{ form.as_table }}
</table>
<input type="submit" name="add" value="Add" />
</form>
blog/templates/blog/blog_post.html
:
<h2>{{ post.title }}</h2>
<div class="content">
<p>
{{ post.text }}
</p>
<span>
Written by {{ post.author }} on {{ post.created_on }}
</span>
</div>
{% if post.comment_set.all %}
<h2>Comments</h2>
<div class="comments">
{% for comment in post.comment_set.all %}
<span>
<a href="{{ comment.website }}">{{ comment.name }}</a> said on {{ comment.created_on }}
</span>
<p>
{{ comment.text }}
</p>
{% endfor %}
</div>
{% endif %}
<br />
<h2>Add Comment</h2>
<form action="" method="POST">
{% csrf_token %}
<table>
{{ form.as_table }}
</table>
<input type="submit" name="submit" value="Submit" />
</form>
Note
Since Comment
has a ForeignKey to Post
, each Post
object automatically gets a
comment_set
property which provides an interface to that particular Post
‘s comments.
Sessions:¶
So far we have most of the blog actions covered. Next, let’s look into sessions:
Suppose we want to store the commenter’s details in the session so that he/she does not have to fill them again.
# Create your views here.
from django.contrib.auth.decorators import user_passes_test
from django.shortcuts import redirect, render_to_response, get_object_or_404
from django.template import RequestContext
from models import Post
from forms import PostForm, CommentForm
@user_passes_test(lambda u: u.is_superuser)
def add_post(request):
form = PostForm(request.POST or None)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.save()
return redirect(post)
return render_to_response('blog/add_post.html',
{ 'form': form },
context_instance=RequestContext(request))
def view_post(request, slug):
post = get_object_or_404(Post, slug=slug)
form = CommentForm(request.POST or None)
if form.is_valid():
comment = form.save(commit=False)
comment.post = post
comment.save()
request.session["name"] = comment.name
request.session["email"] = comment.email
request.session["website"] = comment.website
return redirect(request.path)
form.initial['name'] = request.session.get('name')
form.initial['email'] = request.session.get('email')
form.initial['website'] = request.session.get('website')
return render_to_response('blog/blog_post.html',
{
'post': post,
'form': form,
},
context_instance=RequestContext(request))
Note that the form.initial
attribute is a dict
that holds initial data of the form. A session lasts until the user logs out or
clears the cookies (e.g. by closing the browser). django identifies the session using sessionid
cookie.
The default session backend is django.contrib.sessions.backends.db
i.e. database backend, but it can be configured to file
or cache
backend as well.
Date based generic views:¶
We will use date based generic views to get weekly/monthly archives for our blog posts:
from django.conf.urls.defaults import *
from models import Post
urlpatterns = patterns('',
url(r'^post/(?P<slug>[-\w]+)$',
'blog.views.view_post',
name='blog_post_detail'),
url(r'^add/post$',
'blog.views.add_post',
name='blog_add_post'),
url(r'^archive/month/(?P<year>\d+)/(?P<month>\w+)$',
'django.views.generic.date_based.archive_month',
{
'queryset': Post.objects.all(),
'date_field': 'created_on',
},
name='blog_archive_month',
),
url(r'^archive/week/(?P<year>\d+)/(?P<week>\d+)$',
'django.views.generic.date_based.archive_week',
{
'queryset': Post.objects.all(),
'date_field': 'created_on',
},
name='blog_archive_week',
),
)
archive_month
generic views outputs to post_archive_month.html
and archive_week
to post_archive_week.html
<h2>Post archives for {{ month|date:"F" }}, {{ month|date:"Y" }}</h2>
<ul>
{% for post in object_list %}
<li>
<a href="{% url blog_post_detail post.slug %}">{{ post.title }}</a>
</li>
{% endfor %}
</ul>
<h2>Post archives for week {{ week|date:"W" }}, {{ week|date:"Y" }}</h2>
<ul>
{% for post in object_list %}
<li>
<a href="{% url blog_post_detail post.slug %}">{{ post.title }}</a>
</li>
{% endfor %}
</ul>
Now, blog archives should be accessible from /blog/archive/month/2010/oct
or /blog/archive/week/2010/41