With the addition of the forms for user submissions, your code-sharing application is nearly complete. Only three features are left to implement from the original list. Then you can wrap up the application with a few final views. Let's get started.
Currently, your application's users can keep track of their favorite snippets by bookmarking them in a web browser or posting bookmarks to a service like Delicious. However, it would be nice to give each user the ability to track a personalized list of snippets directly on the site. This will cut down on the amount of clutter in each user's general-purpose bookmarks, and it will provide a useful social metric—most-bookmarked snippets—that you can track and display publicly.
To support this feature, you first need a model representing a user's bookmark. This is a pretty simple model, because all it needs to do is track a few pieces of information:
You can manage this by opening up cab/models.py
and adding a new Bookmark
model with three fields for this information:
class Bookmark(models.Model):
snippet = models.ForeignKey(Snippet)
user = models.ForeignKey(User, related_name='cab_bookmarks')
date = models.DateTimeField(editable=False)
class Meta:
ordering = ['-date']
def __unicode__(self):
return "%s bookmarked by %s" % (self.snippet, self.user)
def save(self):
if not self.id:
self.date = datetime.datetime.now()
super(Bookmark, self).save()
There's only one new feature in use here, and that's the related_name
argument to the foreign key pointing at the User
model. The fact that you've created a foreign key to User
means that Django will add a new attribute to every User
object, which you'll be able to use to access each user's bookmarks. By default, this attribute would be named bookmark_set
based on the name of your Bookmark
model. For example, you could query for a user's bookmarks like this:
from django.contrib.auth.models import User
u = User.objects.get(pk=1)
bookmarks = u.bookmark_set.all()
However, this can create a problem: If you ever use any other application with a bookmarking system, and if that application names its model Bookmark
, you'll get a naming conflict because the bookmark_set
attribute of a User
can't simultaneously refer to two different models.
The solution to this is the related_name
argument to ForeignKey
, which lets you manually specify the name of the new attribute on User
, which you'll use to access bookmarks. In this case, you'll use the name cab_bookmarks
. So once this model is installed and you have some bookmarks in your database, you'll be able to run queries like this:
from django.contrib.auth.models import User
u = User.objects.get(pk=1)
bookmarks = u.cab_bookmarks.all()
Generally, it's a good idea to use related_name
any time you're creating a relationship from a model with a common name.
Also, note that because users will manage their bookmarks entirely through public-facing views, you don't need to activate the admin interface for the Bookmark
model.
Go ahead and run manage.py syncdb
to install the Bookmark
model into your database. Again, syncdb
is smart enough to realize that it needs to create only one new table.
Now you can add a couple of views to let users bookmark snippets and remove their bookmarks later if they wish. Create a file in cab/views
called bookmarks.py
, and start with the add_bookmark
view:
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render_to_response
from django.contrib.auth.decorators import login_required
from cab.models import Bookmark, Snippet
def add_bookmark(request, snippet_id):
snippet = get_object_or_404(Snippet, pk=snippet_id)
try:
Bookmark.objects.get(user__pk=request.user.id,
snippet__pk=snippet.id)
except Bookmark.DoesNotExist:
bookmark = Bookmark.objects.create(user=request.user,
snippet=snippet)
return HttpResponseRedirect(snippet.get_absolute_url())
add_bookmark = login_required(add_bookmark)
The logic here is pretty simple. You check whether the user already has a bookmark for this snippet, and if not—in which case the Bookmark.DoesNotExist
exception will be raised—you create one. Either way, you return a redirect back to the snippet, and, of course, you ensure that the user must be logged in to do this.
Deleting a bookmark is similarly easy:
def delete_bookmark(request, snippet_id):
if request.method == 'POST':
snippet = get_object_or_404(Snippet, pk=snippet_id)
Bookmark.objects.filter(user__pk=request.user.id,
snippet__pk=snippet.id).delete()
return HttpResponseRedirect(snippet.get_absolute_url())
else:
return render_to_response('cab/confirm_bookmark_delete.html',
{ 'snippet': snippet })
delete_bookmark = login_required(delete_bookmark)
With the delete_bookmark
view, you're using two important techniques:
filter()
to create a QuerySet
of any bookmarks that match this user and this snippet. You then call the delete()
method of that QuerySet
. This issues only one query—a DELETE
query, whose FROM
clause limits it to the correct rows, if any exist.POST
. If the request method isn't POST
, you display a confirmation page instead.This last point bears emphasizing, because requiring HTTP POST
and a confirmation screen for anything that deletes content—even trivial-seeming content like a bookmark—is an extremely important habit to get into. Not only does it prevent accidental deletion by a user who clicks the wrong link on a page, but it also adds a small measure of security against a common type of web-based attack: cross-site request forgery (CSRF). In a CSRF attack, a hacker lures a user of your site to a page that contains a hidden link or form pointing back to your application. The hacker exploits the fact that because the HTTP requests are coming from the user, many applications allow modification or deletion of content.
Additionally, it's generally good practice to require POST
for any operation that alters or deletes data on the server. The HTTP specification states that certain methods, including GET
, should be considered safe and generally should not have side effects.
Templating the confirmation page is easy enough. You can display some information about the snippet the user is about to “unbookmark,” and then you can include a simple form that submits the confirmation via POST
:
<form method="post" action="">
<p><input type="submit" value="Delete bookmark"></p>
</form>
It's easy enough to set up URLs for adding and deleting bookmarks. You can create cab/urls/bookmarks.py
and start filling it in:
from django.conf.urls.defaults import *
from cab.views import bookmarks
urlpatterns = patterns('',
url(r'^add/(?P<snippet_id>d+)/$',
bookmarks.add_bookmark,
name='cab_bookmark_add'),
url(r'^delete/(?P<snippet_id>d+)/$',
bookmarks.delete_bookmark,
name='cab_bookmark_delete'),
)
Now that you've got views in place for managing bookmarks, go ahead and write one to show a list of the current user's bookmarks. This is just a wrapper around the object_list
generic view:
from django.views.generic.list_detail import object_list
def user_bookmarks(request):
return object_list(queryset=Bookmark.objects.filter(user__pk=request.user.id),
template_name='cab/user_bookmarks.html',
paginate_by=20)
You can set up a URL for the view so that the root of the bookmark URLs simply shows the user's bookmarks:
url(r'^$', bookmarks.user_bookmarks, name='cab_user_bookmarks'),
Finally, to round out the bookmark-oriented views, add one that queries for the most-bookmarked snippets. Because this query returns Snippet
objects, place it on the SnippetManager
in cab/managers.py
:
def most_bookmarked(self):
return self.annotate(score=Count('bookmark')).order_by('score')
Now write the most_bookmarked
view in cab/views/popular.py
:
def most_bookmarked(request):
return object_list(queryset=Snippet.objects.most_bookmarked(),
template_name='cab/most_bookmarked.html',
paginate_by=20)
Then add the URL pattern in cab/urls/popular.py
:
url(r'^bookmarks/$', popular.most_bookmarked, name='cab_most_bookmarked'),
{% if_bookmarked %}
To go with the add_bookmark
and delete_bookmark
views, you might want to indicate when displaying a snippet whether a user has already bookmarked it. That way, you could either hide any links to bookmarking views you might otherwise show or switch to showing a link or button to delete the bookmark.
You could set this up to be part of the snippet's detail view, but that's not necessarily the only place you might want this functionality. If you're showing a list of snippets, for example, you might want a quick and easy way to determine where to show a link for bookmarking and where not to. The ideal solution would be a template tag, which can tell whether a user has already bookmarked a specific snippet. Something that works like this would be ideal:
{% if_bookmarked user object %}
<form method="post" action="{% url cab_bookmark_delete object.id %}">
<p><input type="submit" value="Delete bookmark"></p>
</form>
{% else %}
<p><a href="{% url cab_bookmark_add object.id %}">Add bookmark</a></p>
{% endif_bookmarked %}
But how can you write this? So far, all of your custom template tags have been pretty simple. They typically just read their arguments and spit something back out into the context. Writing this tag requires two new techniques:
{% else %}
clause and the closing tag, and keeps track of what to displayobject
Fortunately, both of these are easy enough to accomplish.
You'll recall from Chapter 6 when you wrote your first custom template tags that the compilation function for a tag receives two arguments, conventionally called parser
and token
. At the time, you were concerned only with the token
portion because it contained the arguments you were interested in. However, now you're in a situation where parser
—which is the actual object that's parsing the template—is going to come in handy.
Before diving in too deeply, let's go ahead and lay out the infrastructure for the custom tag. In the cab
directory, create a new directory called templatetags
, and in that directory, create two new files: __init__.py
and snippets.py
. Then, open up cab/templatetags/snippets.py
and fill in a couple of necessary imports:
from django import template
from cab.models import Bookmark
Now, you can start writing the compilation function for the {% if_bookmarked %}
tag:
def do_if_bookmarked(parser, token):
bits = token.contents.split()
if len(bits) != 3:
raise template.TemplateSyntaxError("%s tag takes two arguments" % bits[0])
This compilation function looks at the syntax used to call the tag—which is of the form {% if_bookmarked user snippet %}
—and verifies that it has the right number of arguments, bailing out immediately with a TemplateSyntaxError
if it doesn't.
Now you can turn your attention to the parser
argument and see how it can help you out. You want to read ahead in the template until you find either an {% else %}
or an {% endif_bookmarked %}
tag. You can do just that by calling the parse()
method of the parser
object and passing a list of things you'd like it to look for. The result of this parsing will be an instance of the class django.template.NodeList
, which is—as the name implies—a list of template nodes:
nodelist_true = parser.parse(('else', 'endif_bookmarked'))
You're storing this result in a variable called nodelist_true
because—in terms of this tag's if
/else
-style behavior—it corresponds to the output you want to display if the condition is true (if the user has bookmarked the snippet).
The call to parser.parse()
moves ahead in the template to just before the first item in the list you told it to look for. This means you now want to look at the next token and find out if it's an {% else %}
. If it is, you'll need to do a bit more parsing:
token = parser.next_token()
if token.contents == 'else':
nodelist_false = parser.parse(('endif_bookmarked',))
parser.delete_first_token()
else:
nodelist_false = template.NodeList()
If the first thing the parser finds from your list is indeed an {% else %}
, then you want to read ahead again to {% endif_bookmarked %}
to get the output to display when the user hasn't bookmarked the snippet. This is another NodeList
, which you store in the variable nodelist_false
.
If, on the other hand, the parser finds an {% endif_bookmarked %}
with no {% else %}
, then you simply create an empty NodeList
. If the user hasn't bookmarked the snippet, then you shouldn't display anything when there's no {% else %}
clause.
Finally, you return a Node
class, passing the two arguments gathered from the tag and the two NodeList
instances. Although you haven't defined it yet, the Node
class you're going to use will be called IfBookmarkedNode
:
return IfBookmarkedNode(bits[1], bits[2], nodelist_true, nodelist_false)
Node
Now you can begin writing the IfBookmarkedNode
. Obviously, it needs to subclass template.Node
, and it needs to accept four arguments in its __init__()
method. You'll simply store the two NodeList
instances for later use when you render the template:
class IfBookmarkedNode(template.Node):
def __init__(self, user, snippet, nodelist_true, nodelist_false):
self.nodelist_true = nodelist_true
self.nodelist_false = nodelist_false
But what about the user
and snippet
variables? Right now, they're the raw strings from the template, and you don't yet know what values they'll actually resolve to when you look at the context. You need some way of saying that these are actually template variables that you need to resolve later. Fortunately, that's easy enough to do:
self.user = template.Variable(user)
self.snippet = template.Variable(snippet)
The Variable
class in django.template
handles the hard work for you. When given the template context to work with, it knows how to resolve the variable and gives you back the actual value it corresponds to.
Now you can start to write the render()
method:
def render(self, context):
user = self.user.resolve(context)
snippet = self.snippet.resolve(context)
Each Variable
instance has a method called resolve()
, which handles the actual business of resolving the variable. If the variable turns out not to correspond to anything, it'll even handle raising an exception—django.template.VariableDoesNotExist
—automatically for you. Of course, you've seen that it's usually a good idea for custom template tags to fail silently when possible, so catch that exception and just have the tag return nothing when one of the variables is invalid:
def render(self, context):
try:
user = self.user.resolve(context)
snippet = self.snippet.resolve(context)
except template.VariableDoesNotExist:
return ''
If you get past this point, then you know that these variables resolved successfully, and you can use them to query for an existing Bookmark
. The only tricky thing now is figuring out what to return in each case. You have two NodeList
instances, and you want to render one or the other according to whether the user has bookmarked the snippet. Fortunately, that's easy. Just as a Node
must have a render()
method that accepts the context and returns a string, so too must NodeList
:
if Bookmark.objects.filter(user__pk=user.id,
snippet__pk=snippet.id):
return self.nodelist_true.render(context)
else:
return self.nodelist_false.render(context)
Now you have a finished tag. After you register it, cab/templatetags/snippets.py
looks like this:
from django import template
from cab.models import Bookmark
def do_if_bookmarked(parser, token):
bits = token.contents.split()
if len(bits) != 3:
raise template.TemplateSyntaxError("%s tag takes two arguments" % bits[0])
nodelist_true = parser.parse(('else', 'endif_bookmarked'))
token = parser.next_token()
if token.contents == 'else':
nodelist_false = parser.parse(('endif_bookmarked',))
parser.delete_first_token()
else:
nodelist_false = template.NodeList()
return IfBookmarkedNode(bits[1], bits[2], nodelist_true, nodelist_false)
class IfBookmarkedNode(template.Node):
def __init__(self, user, snippet, nodelist_true, nodelist_false):
self.nodelist_true = nodelist_true
self.nodelist_false = nodelist_false
self.user = template.Variable(user)
self.snippet = template.Variable(snippet)
def render(self, context):
try:
user = self.user.resolve(context)
snippet = self.snippet.resolve(context)
except template.VariableDoesNotExist:
return ''
if Bookmark.objects.filter(user__pk=user.id,
snippet__pk=snippet.id):
return self.nodelist_true.render(context)
else:
return self.nodelist_false.render(context)
register = template.Library()
register.tag('if_bookmarked', do_if_bookmarked)
Now you can simply do {% load snippets %}
in a template and use the {% if_bookmarked %}
tag.
RequestContext
to Automatically Populate Template VariablesBut you can only use the {% if_bookmarked %}
tag if the template where you're using the tag has an available variable that represents the currently logged-in user. This is a slightly trickier proposition because so far, you haven't been writing your views to pass the current user as a variable to the templates they use. Mostly that's because you haven't had much need to do so. You've been doing everything with the logged-in user at the view level by accessing request.user
, so you haven't really run into a case—until now—where you genuinely needed to have a variable for the user available in templates.
You could simply go back at this point and make the necessary change in all your hand-written views, but that immediately brings up two disadvantages:
extra_context
argument every time you use a generic view, there doesn't seem to be any way to solve this. Plus, this approach might not help if you need to use views from someone else's application. If that person hasn't written views to accept an argument similar to extra_context
, you won't be able to do anything.Fortunately, there's an easier solution. As you'll recall from the first hand-written views back in Chapter 3, the dictionary of variables and values passed to a template is an instance of django.template.Context
. Because this is an ordinary Python class, you can subclass it to add customizable behavior. Django includes one very useful subclass of Context
—django.template.RequestContext
—that can automatically populate some extra variables each time it's used without needing those variables explicitly declared and defined in each view.
RequestContext
gets its name from the fact that it makes use of functions called context processors (which I mentioned briefly in Chapter 6). Each context processor is a function that receives a Django HttpRequest
object as an argument and returns a dictionary of variables based on that HttpRequest
. RequestContext
then automatically adds those variables to the context, in addition to any variables explicitly passed to the context during the process of executing a view function.
In normal use, RequestContext
reads its list of context-processor functions from the setting TEMPLATE_CONTEXT_PROCESSORS
. The default set happens to include a context processor that reads request.user
to get the current user and adds it to the context as the variable {{ user }}
. This just happens to be exactly what you want here. As long as a view uses RequestContext
, its template can rely on the fact that the variable {{ user }}
will be available and will correspond to the currently active user.
Using RequestContext
is trivially easy; you simply import it:
from django.template import RequestContext
You can use it anywhere you need a context for a template. The only difference between a normal Context
and RequestContext
is that the latter must receive the HttpRequest
object as an argument. For example, in a view, you might write this:
context = RequestContext(request, { 'foo': 'bar' })
It works with the render_to_response()
shortcut as well, although the usage is slightly different. For example, where you'd normally write this:
return render_to_response('example.html',
{ 'foo': 'bar' })
you'd instead write this:
return render_to_response('example.html',
{ 'foo': 'bar' },
context_instance=RequestContext(request))
And for cases where you're wrapping a generic view, you don't even have to do anything—Django's generic views default to using RequestContext
. So far, you've written only three views in this application that don't use generic views—the delete_bookmark
, add_snippet
, and edit_snippet
views, to be precise—so it's not too hard to go back and add the use of RequestContext
to them. Because the rest are generic views or wrap generic views, they're already using RequestContext
.
The only thing left to implement from the feature list is a rating system that lets users mark particular snippets they found useful (or not useful, as the case may be). Once again, start with a data model. As with the bookmarking system, it's fairly simple. You need to collect four pieces of information:
You can easily build out this Rating
model in cab/models.py
:
class Rating(models.Model):
RATING_UP = 1
RATING_DOWN = −1
RATING_CHOICES = ((RATING_UP, 'useful'),
(RATING_DOWN, 'not useful'))
snippet = models.ForeignKey(Snippet)
user = models.ForeignKey(User, related_name='cab_rating')
rating = models.IntegerField(choices=RATING_CHOICES)
date = models.DateTimeField()
def __unicode__(self):
return "%s rating %s (%s)" % (self.user, self.snippet,
self.get_rating_display())
def save(self):
if not self.id:
self.date = datetime.datetime.now()
super(Rating, self).save()
As with the Bookmark
model, you're setting related_name
explicitly on the relationship to the User
model in order to avoid any potential name clashes with other applications that might define rating systems. Meanwhile, the rating value uses an integer field, with appropriately named constants, to handle the actual “up” and “down” rating values, in much the same fashion as the status
field on the weblog's Entry
model. There is one new item, though: in the __unicode__()
method, you're calling a method named get_rating_display()
. Any time a model has a field with choices like this, Django automatically adds a method—whose name is derived from the name of the field—that will return the human-readable value for the currently selected value.
While you're in the cab/models.py
file, you can also add a method to the Snippet
model that calculates a snippet's total score by summing all of the ratings attached to it. This method will use Django's aggregate support again, but with a different type of aggregate filter: django.db.models.Sum
. This filter, as its name implies, adds up a set of values in the database and returns the sum.
You'll also use a different method to apply the aggregate. Previously, you used the annotate
method, because you needed to add an extra piece of information to the results returned by the query. But now you just want to directly return the aggregated value and nothing else, so you'll use a different method called aggregate
. If you have a Snippet
object in a variable named snippet
, and you want the sum of all the ratings attached to it, you can write the query like this:
from django.db.models import Sum
total_rating = snippet.rating_set.aggregate(Sum('rating'))
You can then add this functionality as a get_score
method on the Snippet
model (remember to place the import
statement for the Sum
aggregate at the top of the models.py
file):
def get_score(self):
return self.rating_set.aggregate(Sum('rating'))
Finally, in cab/managers.py
, you can add one more method on the SnippetManager
for calculating the top-rated snippets (again, remember to add the import
statement for the Sum
aggregate):
def top_rated(self):
return self.annotate(score=Sum('rating')).order_by('score')
This takes care of all the custom queries you'll need, so go ahead and run manage.py syncdb
to install the Rating
model.
Letting users rate snippets is pretty easy. All you need is a view that gets a snippet ID and an “up” or “down” rating, then adds a new Rating
object. The view logic is simple. Create one more view file—cab/views/ratings.py
—and place this code in it:
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.contrib.auth.decorators import login_required
from cab.models import Rating, Snippet
def rate(request, snippet_id):
snippet = get_object_or_404(Snippet, pk=snippet_id)
if 'rating' not in request.GET or request.GET['rating'] not in ('1', '-1'):
return HttpResponseRedirect(snippet.get_absolute_url())
try:
rating = Rating.objects.get(user__pk=request.user.id,
snippet__pk=snippet.id)
except Rating.DoesNotExist:
rating = Rating(user=request.user,
snippet=snippet)
rating.rating = int(request.GET['rating'])
rating.save()
return HttpResponseRedirect(snippet.get_absolute_url())
rate = login_required(rate)
Only two moderately tricky things are going on here:
?rating=1
or ?rating=−1
, so you verify that this string is present and that it has an acceptable value. If not, you simply redirect back to the snippet.Setting up the URL for this view should be fairly easy. You can simply add a cab/urls/ratings.py
file and set up the necessary URL pattern:
from django.conf.urls.defaults import *
from cab.views.ratings import rate
urlpatterns = patterns('',
url(r'^(?P<snippet_id>d+)$', rate, name='cab_snippet_rate'),
)
{% if_rated %}
Template TagGo ahead and add an {% if_rated %}
template tag that resembles the {% if_bookmarked %}
tag you developed earlier in this chapter. The compilation function for it should look familiar (once again, this goes into cab/templatetags/snippets.py
):
def do_if_rated(parser, token):
bits = token.contents.split()
if len(bits) != 3:
raise template.TemplateSyntaxError("%s tag takes two arguments" % bits[0])
nodelist_true = parser.parse(('else', 'endif_rated'))
token = parser.next_token()
if token.contents == 'else':
nodelist_false = parser.parse(('endif_rated',))
parser.delete_first_token()
else:
nodelist_false = template.NodeList()
return IfRatedNode(bits[1], bits[2], nodelist_true, nodelist_false)
Once again, you use the ability to parse ahead in the template to work out the structure of the if
/else
possibilities for the tag and store a pair of NodeList
instances to pass as arguments to the Node
class, which you can call IfRatedNode
. First, you need to change the import
statement at the top of the file from
from cab.models import Bookmark
to
from cab.models import Bookmark, Rating
Then you can write the IfRatedNode
class:
class IfRatedNode(template.Node):
def __init__(self, user, snippet, nodelist_true, nodelist_false):
self.nodelist_true = nodelist_true
self.nodelist_false = nodelist_false
self.user = template.Variable(user)
self.snippet = template.Variable(snippet)
def render(self, context):
try:
user = self.user.resolve(context)
snippet = self.snippet.resolve(context)
except template.VariableDoesNotExist:
return ''
if Rating.objects.filter(user__pk=user.id,
snippet__pk=snippet.id):
return self.nodelist_true.render(context)
else:
return self.nodelist_false.render(context)
At the bottom of the file, you can register the tag:
register.tag('if_rated', do_if_rated)
Now that you have the {% if_rated %}
tag, you can add a second, complementary tag to retrieve the user's rating for a particular snippet. This new {% get_rating %}
tag lets you set up a template like this:
{% load snippets %}
{% if_rated user snippet %}
{% get_rating user snippet as rating %}
<p>You rated this snippet <strong>{{ rating.get_rating_display }}</strong>.</p>
{% endif_rated %}
When a user has rated a snippet, this code should end up displaying something like, “You rated this snippet useful.”
This new tag's compilation function, do_get_rating
, is straightforward:
def do_get_rating(parser, token):
bits = token.contents.split()
if len(bits) != 5:
raise template.TemplateSyntaxError("%s tag takes four arguments" % bits[0])
if bits[3] != 'as':
raise template.TemplateSyntaxError("Third argument to
%s must be 'as'" % bits[0])
return GetRatingNode(bits[1], bits[2], bits[4])
The Node
class, which you'll call GetRatingNode
, is also easy to write. You just need to resolve the user
and snippet
variables, retrieve the Rating
, and put it into the context:
class GetRatingNode(template.Node):
def __init__(self, user, snippet, varname):
self.user = template.Variable(user)
self.snippet = template.Variable(snippet)
self.varname = varname
def render(self, context):
try:
user = self.user.resolve(context)
snippet = self.snippet.resolve(context)
except template.VariableDoesNotExist:
return ''
rating = Rating.objects.get(user__pk=user.id,
snippet__pk=snippet.id)
context[self.varname] = rating
return ''
Next, you register the tag:
register.tag('get_rating', do_get_rating)
Then you can use the tag like this (in the detail view of a snippet, for example):
{% load snippets %}
{% if_rated user object %}
{% get_rating user snippet as rating %}
<p>You rated this snippet {{ rating.get_rating_display }}.</p>
{% else %}
<p>Rate this snippet:
<a href="{% url cab_snippet_rate object.id %}?rating=1">useful</a> or
<a href="{% url cab_snippet_rate object.id %}?rating=-1">not useful</a>.</p>
{% endif_rated %}
At this point, you've implemented everything on your original feature list for the code-sharing application. Users can submit and edit snippets, tag them, and sort them by language. You also have bookmarking and rating features as well as some aggregate views to display things like the top-rated and most-bookmarked snippets and the most-used languages. Along the way, you've learned how to work with Django's form system, and you've picked up some advanced tricks for working with the object-relational mapper and the template engine.
Of course, you could still add a lot more features at this point:
By now, you've reached a point where you can start building out these features on your own and tailor this application to work precisely the way you want it to. Consider some of these ideas and think about how you'd implement them, then sit down and write the code. Then start brainstorming some things you'd like that aren't on the preceding list, and try your hand at them too. Because if you've made it this far, you're ready to make use of your knowledge and put Django to work for you.
In recognition of that, I'm not going to dictate any more feature lists or implementations to you. Instead, in the next two chapters, I'll change gears a bit and talk about some general best practices for developing your Django applications and getting the most out of them.