C H A P T E R   10

image

Finishing the Code-Sharing
Application

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.

Bookmarking Snippets

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:

  • The user the bookmark belongs to
  • The snippet the user bookmarked
  • The date and time when the user bookmarked the snippet

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.

Adding Basic Bookmark Views

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:

  • Instead of querying to see if the user has a bookmark for this snippet and then deleting it manually (which incurs the overhead of two database queries), you simply use 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.
  • You're requiring that bookmark deletion use an HTTP 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'),

Creating a New Template Tag: {% 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:

  • The ability to write a tag that reads ahead a bit in the template to find, for example, the {% else %} clause and the closing tag, and keeps track of what to display
  • The ability to resolve arbitrary variables from the template context, as in the case of a variable such as object

Fortunately, both of these are easy enough to accomplish.

Parsing Ahead in a Django Template

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)

Resolving Variables Inside a Template 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.

Using RequestContext to Automatically Populate Template Variables

But 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:

  • It's tedious and repetitive: Generally, Django encourages you to avoid anything that can be described in that fashion.
  • It doesn't help for views you didn't write yourself: In a lot of cases, you're simply wrapping a generic view, and short of manually passing the 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 Contextdjango.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.

Adding the User Rating System

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:

  • The snippet being rated
  • The user doing the rating
  • The value of the rating—in this case, either a +1 or −1, for a simple “up or down” voting system
  • The date of the rating

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.

Rating Snippets

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:

  • You're going to expect this view to be accessed with a query string like ?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.
  • To prevent ballot stuffing by a user trying to rate the same snippet over and over, you ensure that the view simply changes the value of an existing rating if one is found.

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'),
)

Adding an {% if_rated %} Template Tag

Go 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)

Retrieving a User's Rating

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 toimage
 %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 %}

Looking Ahead

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:

  • Following up on your experiences with the weblog application, you could easily add comments (with moderation) and feeds.
  • You could borrow the content-retrieving template tags you wrote for the weblog and use them to retrieve the latest snippets or adapt them to perform some of the custom queries you've written for this application.
  • You could build out a whole lot of new views and queries; even with the simple set of models you have here, there's a lot of room for interesting ways to explore this application, and what you've set up so far just scratches the surface.
  • You could explore ways of integrating this application with some of the others you've written and used (perhaps a code-sharing site with a weblog that points out the site staff's favorite snippets).

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset