Chapter 5. Building a Wiki¶
A wiki application:¶
In this chapter, we will build a wiki from scratch. Basic functionality includes:
- User registration
- Article Management (CRUD) with ReST support
- Audit trail for articles
- Revision history
Reusable Apps®:¶
To manage user registrations, we will use django-registration. You can download it from
django-registration is a great example of a reusable app, which can be customized to fit our requirements while providing the most common pattern by default (sign up, email activation etc)
Some functionality offered by the app:
- User sign-up view
- Activation email view
- Validate activation key and create user account
- Login, logout from
contrib.auth - Management scripts to clear expired registrations
We shall follow the default pattern, i.e. user registration with activation email in the wiki app, although django-registration
allows customization of the process by using backends which should know how to handle the registration. It ships with two such
backends: default and simple
Note
browse through the code of django-registration to see what urls are avaialbe, what context is passed to the templates, which urls are mapped to which views etc.
Looking at named urls from urls.py would be useful for creating links to registration, login etc by using
the url templatetag.
To install, download the app and run:
python setup.py install
This will be installed to the site wide python packages directory but can still be imported from our app since it is a python package.
Now, include registration in your INSTALLED_APPS, do syncdb and include the 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/', include('registration.backends.default.urls')),
(r'^admin/', include(admin.site.urls)),
(r'^pastebin/', include('pastebin.urls')),
(r'^blog/', include('blog.urls')),
)
Note
django-registration provides views for login at accounts/login so we can omit
our previous entry for the same.
The app requires a setting called ACCOUNT_ACTIVATION_DAYS which is the number of days before which the user should complete
registration. If you are not using local_settings.py, create one and add from local_settings import * to settings.py. Now
add this setting to local_settings.py:
# Django registration settings
ACCOUNT_ACTIVATION_DAYS = 7
Now, accounts/register/ provides the user sign-up view and renders to registration/registration_form.html, so lets write the template:
{% extends "registration/base.html" %}
{% comment %}
**registration/registration_form.html**
Used to show the form users will fill out to register. By default, has
the following context:
``form``
The registration form. This will be an instance of some subclass
of ``django.forms.Form``; consult `Django's forms documentation
<http://docs.djangoproject.com/en/dev/topics/forms/>`_ for
information on how to display this in a template.
{% endcomment %}
{% block content %}
<h1>Create new account</h1>
<form action="" method="POST">
{% csrf_token %}
<table>
{{ form.as_table }}
</table>
<input type="submit" name="submit" value="Submit">
</form>
{% endblock %}
Note that form is the user sign-up form passed as context by register of django-registration.
To demostrate template heirarchy, we have used a base template and built all other registration templates on top of it. The base template looks like:
wiki/templates/registration/base.html
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Wiki</title>
{% block extra_head %}
{% endblock %}
</head>
<body>
<div class="content">
<p>
{% block content %}
{% endblock %}
</p>
</div>
</body>
</html>
At the moment, we have extra_head and content blocks. You can place as many blocks as you like with careful planning and hierarchy. For example
extra_head would serve to include child template specific css/scripts. Global css/scripts could be directly included in base.html to make them
available to all child templates. (e.g. something general like jquery.js would go in base while something specific like jquery.form.js would go in
the child template)
Note
Templates outside any subdirectory are considered harmful since they may interfere with templates from other applications.
In general it is better to namespace your templates by putting them inside subdirectories.
E.g.:
wiki/templates/base.html- Wrong!wiki/templates/wiki/base.html- Right.
The reason being that templates from other apps extending base.html would find both wiki/templates/blog/base.html and
wiki/templates/base.html. Then you would be left at the mercy of precedence of TEMPLATE_LOADERS to get the blog base template
and not the wiki base template.
Of course, it can be useful if used correctly, but quite hard to debug if not.
At this point the user can submit a sign-up form. He will be sent an email with subject from wiki/templates/registration/activation_email_subject.text and
content from wiki/templates/registration/activation_email.txt. Let’s write these templates:
A nice base email template would be wiki/templates/registration/email.txt:
Hi!,
{% block body %}
{% endblock %}
Regards,
Admin
Note: This is an autogenerated mail, please don't reply.
In wiki/templates/registration/activation_email_subject.txt
{% comment %}
**registration/activation_email_subject.txt**
Used to generate the subject line of the activation email. Because the
subject line of an email must be a single line of text, any output
from this template will be forcibly condensed to a single line before
being used. This template has the following context:
``activation_key``
The activation key for the new account.
``expiration_days``
The number of days remaining during which the account may be
activated.
``site``
An object representing the site on which the user registered;
depending on whether ``django.contrib.sites`` is installed, this
may be an instance of either ``django.contrib.sites.models.Site``
(if the sites application is installed) or
``django.contrib.sites.models.RequestSite`` (if not). Consult `the
documentation for the Django sites framework
<http://docs.djangoproject.com/en/dev/ref/contrib/sites/>`_ for
details regarding these objects' interfaces.
{% endcomment %}
Your account activation details at {{ site }}
In wiki/templates/registration/activation_email.txt
{% extends "registration/email.txt" %}
{% comment %}
**registration/activation_email.txt**
Used to generate the body of the activation email. Should display a
link the user can click to activate the account. This template has the
following context:
``activation_key``
The activation key for the new account.
``expiration_days``
The number of days remaining during which the account may be
activated.
``site``
An object representing the site on which the user registered;
depending on whether ``django.contrib.sites`` is installed, this
may be an instance of either ``django.contrib.sites.models.Site``
(if the sites application is installed) or
``django.contrib.sites.models.RequestSite`` (if not). Consult `the
documentation for the Django sites framework
<http://docs.djangoproject.com/en/dev/ref/contrib/sites/>`_ for
details regarding these objects' interfaces.
{% endcomment %}
{% block body %}
Please follow the link to activate your account.
http://{{ site }}{% url registration_activate activation_key %}
{% endblock %}
Note the use of url templatetag to get the activation link. Also, the tag returns a relative url, so we use the site context variable
passed by the register view
Note
If you have a mail server configured, well and good. If not, you could use gmail’s smtp server by adding
# Django registration settings
ACCOUNT_ACTIVATION_DAYS = 7
# Email settings for sending accout activation mails
EMAIL_USE_TLS = True
EMAIL_HOST = "smtp.gmail.com"
EMAIL_HOST_USER = "[email protected]"
EMAIL_HOST_PASSWORD = "secret"
EMAIL_PORT = 587
to local_settings.py
Some other templates required by django-registration:
{% extends "registration/base.html" %}
{% comment %}
**registration/activate.html**
Used if account activation fails. With the default setup, has the following context:
``activation_key``
The activation key used during the activation attempt.
{% endcomment %}
{% block content %}
Sorry, your account could not be activated at this time.
{% endblock %}
{% extends "registration/base.html" %}
{% comment %}
**registration/activation_complete.html**
Used after successful account activation. This template has no context
variables of its own, and should simply inform the user that their
account is now active.
{% endcomment %}
{% block content %}
Thanks! Your account has be activated. Please <a href="{% url auth_login %}">login to continue</a>
{% endblock %}
{% extends "registration/base.html" %}
{% comment %}
**registration/registration_complete.html**
Used after successful completion of the registration form. This
template has no context variables of its own, and should simply inform
the user that an email containing account-activation information has
been sent.
{% endcomment %}
{% block content %}
An account activation email has been sent to you. Please check your email and follow the instructions.
{% endblock %}
At this point, a user should be able to sign-up, get the activation email, follow the activation link, complete registration and login.
All this by just writing down the templates. Amazing, isn’t it?
Now you would have noticed that the logged in user is redirected to /accounts/profile. We would next customize the wiki app and redirect
the user to the index page.
Article Management:¶
This is similar to our last app (blog) in many ways. Significant changes would be:
- Allow any registered user to add/edit an article(instead of just the administrator).
- Allow ReST input instead of just plain text.
- Keep track of all edit sessions related to an article.
To demonstrate custom model managers, we would like to show only ‘published’ articles on the index page.
Let’s write down the models:
from django.db import models
from django.contrib.auth.models import User
from django.template.defaultfilters import slugify
# Create your models here.
class PublishedArticlesManager(models.Manager):
def get_query_set(self):
return super(PublishedArticlesManager, self).get_query_set().filter(is_published=True)
class Article(models.Model):
"""Represents a wiki article"""
title = models.CharField(max_length=100)
slug = models.SlugField(max_length=50, unique=True)
text = models.TextField(help_text="Formatted using ReST")
author = models.ForeignKey(User)
is_published = models.BooleanField(default=False, verbose_name="Publish?")
created_on = models.DateTimeField(auto_now_add=True)
objects = models.Manager()
published = PublishedArticlesManager()
def __unicode__(self):
return self.title
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
super(Article, self).save(*args, **kwargs)
@models.permalink
def get_absolute_url(self):
return ('wiki_article_detail', (), { 'slug': self.slug })
class Edit(models.Model):
"""Stores an edit session"""
article = models.ForeignKey(Article)
editor = models.ForeignKey(User)
edited_on = models.DateTimeField(auto_now_add=True)
summary = models.CharField(max_length=100)
class Meta:
ordering = ['-edited_on']
def __unicode__(self):
return "%s - %s - %s" %(self.summary, self.editor, self.edited_on)
@models.permalink
def get_absolute_url(self):
return ('wiki_edit_detail', self.id)
Most of the code should be familiar, some things that are new:
- The Article model will hold all articles, but only those with
is_publishedset toTruewill be displayed on the front page. - We have a defined a custom model manager called
PublishedArticlesManagerwhich is a queryset that only returns the published articles. - Non-published articles would be used only for editing. So, we retain the default model manager by setting
objectstomodels.Manager - Now, to fetch all articles, one would use
Articles.objects.all, whileArtilces.published.allwould return only published articles. - A custom manager should subclass
models.Managerand define the customget_query_setproperty. - The
Editclass would hold an edit session by a registered user on an article. - We see the use of
verbose_nameandhelp_textkeyword arguments. By default, django will replace_with spaces and Capitalize the field name for the label. This can be overridden usingverbose_nameargument.help_textwill be displayed below a field in the renderedModelForm - The
orderingattribute of meta class forEditdefines the default ordering in whicheditswill be returned. This can also be done usingorder_byin the queryset.
Now, we will need urls similar to our previous app, plus we would need a url to see the article history.
from django.conf.urls.defaults import *
from models import Article
urlpatterns = patterns('',
url(r'^$',
'django.views.generic.list_detail.object_list',
{
'queryset': Article.published.all(),
},
name='wiki_article_index'),
url(r'^article/(?P<slug>[-\w]+)$',
'django.views.generic.list_detail.object_detail',
{
'queryset': Article.objects.all(),
},
name='wiki_article_detail'),
url(r'^history/(?P<slug>[-\w]+)$',
'wiki.views.article_history',
name='wiki_article_history'),
url(r'^add/article$',
'wiki.views.add_article',
name='wiki_article_add'),
url(r'^edit/article/(?P<slug>[-\w]+)$',
'wiki.views.edit_article',
name='wiki_article_edit'),
)
Note that:
- We will use the
list_detailgeneric views for the article index page and detail page. - We have to autofill the
authorto the logged-in user, so will write a custom view for that. - Similarly, it would be better to write down custom views for edit article and article history pages.
Here are the forms we will need:
from django import forms
from models import Article, Edit
class ArticleForm(forms.ModelForm):
class Meta:
model = Article
exclude = ['author', 'slug']
class EditForm(forms.ModelForm):
class Meta:
model = Edit
fields = ['summary']
Here:
- We are excluding
authorandslugwhich will be autofilled. - We are inluding the
summaryfield inEditmodel only. The other fields (article,editor,edited_on) will be autofilled.
In our custom views:
# Create your views here.
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.shortcuts import redirect, render_to_response, get_object_or_404
from django.template import RequestContext
from django.views.generic.list_detail import object_list
from models import Article, Edit
from forms import ArticleForm, EditForm
@login_required
def add_article(request):
form = ArticleForm(request.POST or None)
if form.is_valid():
article = form.save(commit=False)
article.author = request.user
article.save()
msg = "Article saved successfully"
messages.success(request, msg, fail_silently=True)
return redirect(article)
return render_to_response('wiki/article_form.html',
{ 'form': form },
context_instance=RequestContext(request))
@login_required
def edit_article(request, slug):
article = get_object_or_404(Article, slug=slug)
form = ArticleForm(request.POST or None, instance=article)
edit_form = EditForm(request.POST or None)
if form.is_valid():
article = form.save()
if edit_form.is_valid():
edit = edit_form.save(commit=False)
edit.article = article
edit.editor = request.user
edit.save()
msg = "Article updated successfully"
messages.success(request, msg, fail_silently=True)
return redirect(article)
return render_to_response('wiki/article_form.html',
{
'form': form,
'edit_form': edit_form,
'article': article,
},
context_instance=RequestContext(request))
def article_history(request, slug):
article = get_object_or_404(Article, slug=slug)
return object_list(request,
queryset=Edit.objects.filter(article__slug=slug),
extra_context={'article': article})
- We are using the
login_requireddecorator to only allow logged-in users to add/edit articles. get_object_or_404is a shortcut method whichgetsan object based on some criteria. While thegetmethod throws anDoesNotExistwhen no match is found, this method automatically issues a404 Not Foundresponse. This is useful when getting an object based on url parameters (slug,idetc.)redirect, as we have seen, would issue aHttpResponseRedirecton thearticle'sget_absolute_urlproperty.edit_articleincludes two forms, one for theArticlemodel and the other for theEditmodel. We save both the forms one by one.- Passing
instanceto the form will populate existing data in the fields. - As planned, the
authorfield ofarticleandeditor,articlefields ofArticleandEditrespectively, are filled up before commitingsave. article_historyview first checks if an article with the givenslugexists. If yes, it forwards the request to theobject_listgeneric view. We also pass thearticlefrom the generic view usingextra_context.- Note the
filteron theEditmodel’s queryset and thelookupon the relatedArticle'sslug.
To display all the articles on the index page:
wiki/templates/wiki/article_list.html:
{% if object_list %}
<h2>Recent Articles</h2>
<ul>
{% for article in object_list %}
<li>
<a href="{% url wiki_article_detail article.slug %}">{{ article.title }}</a>
</li>
{% endfor %}
</ul>
{% else %}
<h2>No articles have been published yet.</h2>
{% endif %}
<a href="{% url wiki_article_add %}">Create new article</a>
We will include links to edit and view history in the article detail page:
wiki/templates/wiki/article_detail.html:
{% load markup %}
{% if messages %}
<div class="messages">
<ul>
{% for message in messages %}
<li class="{{ message.tag }}">
{{ message }}
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if not object.is_published %}
<label>Note: This article has not been published yet</label>
{% endif %}
<h2>{{ object.title }}</h2>
<p>
{{ object.text|restructuredtext }}
</p>
<h3>Actions<h3>
<ul>
<li>
<a href="{% url wiki_article_edit object.slug %}">Edit this article</a>
</li>
<li>
<a href="{% url wiki_article_history object.slug %}">View article history</a>
</li>
</ul>
<a href="{% url wiki_article_index %}">See All</a>
Here we are using the restructuredtext filter provided by django.contrib.markup. To use this, you will need to add
django.contrib.markup to INSTALLED_APPS and use the load templatetag to load markup filters.
Note
You will require docutils for ReST markup to work. Get it from: http://docutils.sourceforge.net/
Here’s the form that would be used to create/edit an article:
wiki/templates/wiki/article_form.html
{% if article %}
<h1>Edit article {{ article }}</h1>
{% else %}
<h1>Create new article</h1>
{% endif %}
<form action="" method="POST">
{% csrf_token %}
<table>
{{ form.as_table }}
{{ edit_form.as_table }}
</table>
<input type="submit" name="submit" value="Submit">
</form>
Note that the same form is used for add article and edit article pages. We pass the article context variable from edit page, so
we can use it to identify if this is an add or edit page. We also render the edit_form passed from edit page. Rendering an undefined
variable does not throw any error in the template, so this works fine in the add page.
The article history template:
wiki/templates/wiki/edit_list.html
<h2>History</h2>
<h3>{{ article }}</h3>
<table border="1" cellspacing="0">
<thead>
<th>Edited</th>
<th>User</th>
<th>Summary</th>
</thead>
<tbody>
{% for edit in object_list %}
<tr>
<td>{{ edit.edited_on }}</td>
<td>{{ edit.editor }}</td>
<td>{{ edit.summary }}</td>
</tr>
{% endfor %}
<tr>
<td>{{ article.created_on }}</td>
<td>{{ article.author }}</td>
<td>New article created</td>
</tr>
</tbody>
</table>
<br />
<a href="{% url wiki_article_detail article.slug %}"><< Back</a>
Displays a table with the history.
Since we are done with our templates, let us redirect our logged in users to the wiki index page:
{% extends "registration/base.html" %}
{% comment %}
**registration/activation_complete.html**
Used after successful account activation. This template has no context
variables of its own, and should simply inform the user that their
account is now active.
{% endcomment %}
{% block content %}
Thanks! Your account has be activated. Please <a href="{% url auth_login %}?next={% url wiki_article_index %}">login to continue</a>
{% endblock %}