C H A P T E R   8

image

A Social Code-Sharing Site

So far you've been using Django to build content management applications. In these types of applications, an administrator logs in to a special interface and posts some content, after which the system displays that content publicly with little or no interaction from general site visitors. While this sort of application covers a huge amount of common web-development tasks, it doesn't cover everything, and it's not the limit of what Django can do.

So for your third Django application, I'll show you how to build a user-driven application with much more interactivity and some social-style features—specifically, a community-based repository of useful, reusable code.

You can find a live example of this type of code-sharing site at www.djangosnippets.org/, which is geared toward Django users. In the next few chapters, you'll see how to build a similar application that you can deploy any time you need a place for multiple users to share bits of code with one another.

Compiling a Feature Checklist

As with the weblog application, the first thing you should do is get a rough idea of the features you'd like to include. Use this feature list as a starting point:

  • Snippets of code with full descriptions of what they do
  • Categorization by programming language, and full language-aware syntax highlighting of the rendered code
  • A bookmark feature so that users can easily come back and find their favorite snippets
  • A rating feature that lets users indicate whether a particular piece of code was useful to them
  • Tagging for organizing snippets and finding related pieces of code
  • Lists of the most popular snippets by overall rating and by the number of times they've been bookmarked
  • A list of the most active authors (users who've submitted the most snippets)

In keeping with the tradition of naming applications after notable jazz musicians, I'm going to call this application cab, in honor of the singer/bandleader Cab Calloway. Cab was known for his skill at scat singing—singing with short syllables of sometimes nonsensical words—which seems appropriate for an application focused on lots of short bits of code.

Setting Up the Application

Once again, you'll need to create a new Python module to hold the application code. It should live directly on the Python import path, in the same directory as the coltrane application you built for the weblog. Now that you know how to do this manually, let's take a shortcut. Go into the directory where you want to create the application and type the following:

django-admin.py startapp cab

Remember that on some systems, you'll need to type out the full path to the django-admin.py command.

Previously, you've encountered startapp only in the context of a specific project, where it created a new application directory inside the project's directory. However, it works just fine for creating standalone application modules, and it takes some of the tedium out of starting with a new application. Using the django-admin.py startapp command creates a new directory called cab and populates it with an empty __init__.py file and the basic models.py and views.py files for a new Django application.

In time, you'll end up replacing the views.py file with a views module containing several files, but for simpler applications, this setup will be all you need.

Before you go any further, you need to set up one other thing. For syntax highlighting of the code snippets, you'll be using a Python library called pygments. Its official site is at http://pygments.org/, which has documentation and interactive examples, but to download it, visit http://pypi.python.org/pypi/Pygments, which is the page for the pygments project in the Python Package Index (formerly known, and sometimes still referred to, as the Python Cheese Shop, in honor of a famous Monty Python comedy sketch).

The Python Package Index is an incredibly useful resource for Python programmers. Right now it's tracking more than 6,000 third-party libraries and applications written in Python, all categorized and all with a full history of releases. Any time you find yourself wondering if Python has a library for something you need to do, you should try a search there—the odds are good that someone's already written at least some of the code you'll need and listed it in the index.

As I'm writing this, the current version of pygments is 1.0, so you should be able to download a package named Pygments-1.0.tar.gz. Once you've downloaded the package, open it up; on most operating systems, you can just double-click the file. This creates a directory called Pygments-1.0. On a command line, go into that directory and type:

python setup.py install

This installs the pygments library on your computer. Once that's done, you should be able to launch a Python interpreter and type import pygments without seeing any errors.

Building the Initial Models

Now that you've got your application module set up and the pygments library installed, you can start building your models. Logically, you're going to want a model to represent the snippets of code; let's call this model Snippet. You'll also want a model to represent the language in which a particular code snippet is written. We'll call that model Language. This will make it much easier to store some extra metadata, handle the syntax highlighting, and sort snippets by language. I'll cover the Language model first.

The Language Model

Open up the models.py file in the cab directory. The django-admin.py script has already filled in an import statement that pulls in Django's model classes, so you can start working immediately. Start with the Language model that represents the different programming languages. It'll need five fields:

  • The name of the language
  • A unique slug to identify it in URLs
  • A language code that pygments can use to load the appropriate syntax-highlighting module
  • A file extension to use when offering a snippet in this language for download
  • A MIME type to use when sending a snippet file in this language

Based on what you already know about Django's model system, this is easy to set up:

class Language(models.Model):
    name = models.CharField(max_length=100)
    slug = models.SlugField(unique=True)
    language_code = models.CharField(max_length=50)
    mime_type = models.CharField(max_length=100)

Because the values (all strings) that go into these fields won't be very long, I've kept the field lengths fairly short.

Now, the most logical ordering for languages is alphabetical by name, so you can add that and set up the string representation of a Language to be its name:

class Meta:
    ordering = ['name']

def __unicode__(self):
    return self.name

You can also define a get_absolute_url() method. Even though you haven't yet set up any views or URLs, go ahead and write it using the permalink decorator, so it'll do a reverse URL lookup when the time comes. When you do write the URLs, the name for the URL pattern that corresponds to a specific Language is going to be cab_language_detail, and it's going to take the Language's slug as an argument:

def get_absolute_url(self):
    return ('cab_language_detail', (), { 'slug': self.slug })
get_absolute_url = models.permalink(get_absolute_url)

You'll want one more method on the Language model to help pygments with the syntax highlighting. pygments works by reading through a piece of text while using a specialized piece of code called a lexer, which knows the rules of the particular programming language the text is written in. The pygments download includes lexers for a large set of languages, each one identified by a code name, and pygments includes a function that, given the code name of a language, returns the lexer for that language.

Let's add a method to the Language model that uses that function to return the appropriate lexer for a given language. The function you want is pygments.lexers.get_lexer_by_name(), which means you'll need to add a new import statement at the top of your models.py file:

from pygments import lexers

Then you can write the method:

def get_lexer(self):
    return lexers.get_lexer_by_name(self.language_code)

Now the Language model is done, and your models.py file looks like this:

from django.db import models
from pygments import lexers

class Language(models.Model):
    name = models.CharField(max_length=100)
    slug = models.SlugField(unique=True)
    language_code = models.CharField(max_length=50)
    mime_type = models.CharField(max_length=100)

    class Meta
        ordering = ['name']

    def __unicode__(self):
        return self.name

    def get_absolute_url(self):
        return ('cab_language_detail', (), { 'slug': self.slug })
    get_absolute_url = models.permalink(get_absolute_url)

    def get_lexer(self):
        return lexers.get_lexer_by_name(self.language_code)

The Snippet Model

Now you can write the class that represents a snippet of code: Snippet. It will need to have several fields:

  • A title and description. You'll set up the description so that there are two fields: one to store the raw input, and one to store an HTML version. This is similar to the way you set up the excerpt and body fields for the Entry model in your weblog.
  • A foreign key pointing at the Language the snippet is written in.
  • A foreign key to Django's User model to represent the snippet's author.
  • A list of tags, for which you'll use the TagField you saw in the weblog application.
  • The actual code, which, again, you'll store as two fields so that you can keep a rendered, syntax-highlighted HTML version separate from the original input.
  • A bit of metadata that includes the date and time when the snippet was first posted, and the date and time when it was last updated.

To start, you'll need to import the TagField you've used previously:

from tagging.fields import TagField

You'll also need Django's User model:

from django.contrib.auth.models import User

Then you can build out the basic fields:

class Snippet(models.Model):
    title = models.CharField(max_length=255)
    language = models.ForeignKey(Language)
    author = models.ForeignKey(User)
    description = models.TextField()
    description_html = models.TextField(editable=False)
    code = models.TextField()
    highlighted_code = models.TextField(editable=False)
    tags = TagField()
    pub_date = models.DateTimeField(editable=False)
    updated_date = models.DateTimeField(editable=False)

Note that you've marked several of these fields as noneditable. They'll be filled in automatically by the custom save() method that you'll write in a moment.

The logical ordering for snippets is by the descending order of the pub_date field. You'll also want to give the Snippet model a string representation (which will use the title of the snippet):

class Meta:
    ordering = ['-pub_date']

def __unicode__(self):
    return self.title

Before you write the save() method, go ahead and add a method that knows how to apply the syntax highlighting. For this, you'll need two more items from pygments: the formatters module, which knows how to output highlighted code in various formats; and the highlight() function, which puts everything together to produce highlighted output. So change the import line from this:

from pygments import lexers

to this:

from pygments import formatters, highlight, lexers

The highlight() function from pygments takes three arguments: the code to highlight, the lexer to use, and the formatter to generate the output. The code comes from the code field on the Snippet model, and the lexer comes from the get_lexer() method you defined on the Language model. Then just use the HTML formatter built into pygments as the output formatter:

def highlight(self):
    return highlight(self.code,
                     self.language.get_lexer(),
                     formatters.HtmlFormatter(linenos=True))

The linenos=True argument to the formatter tells pygments to generate the output with line numbers so that it's easier to read the code and identify specific lines.

Before you write the save() method, go ahead and import the Python markdown module, and use that for generating the HTML version of the description:

from markdown import markdown

You're also going to need Python's datetime module:

import datetime

Now you can write the save() method, which needs to perform the following actions:

  • Convert the plain-text description to HTML, and store that in the description_html field.
  • Do the syntax highlighting, and store the resulting HTML in the highlighted_code field.
  • Set the pub_date to the current date and time if this is the first time the snippet is being saved.
  • Set the updated_date to the current date and time whenever the snippet is saved.

Here's the code:

def save(self, force_insert=False, force_update=False):
    if not self.id:
        self.pub_date = datetime.datetime.now()
    self.updated_date = datetime.datetime.now()
    self.description_html = markdown(self.description)
    self.highlighted_code = self.highlight()
    super(Snippet, self).save(force_insert, force_update)

Finally, add a get_absolute_url() method. The view that shows a particular Snippet is called cab_snippet_detail, and it takes the id of the Snippet as an argument:

def get_absolute_url(self):
    return ('cab_snippet_detail', (), { 'object_id': self.id })
get_absolute_url = models.permalink(get_absolute_url)

The finished model looks like this:

class Snippet(models.Model):
    title = models.CharField(max_length=255)
    language = models.ForeignKey(Language)
    author = models.ForeignKey(User)
    description = models.TextField()
    description_html = models.TextField(editable=False)
    code = models.TextField()
    highlighted_code = models.TextField(editable=False)
    tags = TagField()
    pub_date = models.DateTimeField(editable=False)
    updated_date = models.DateTimeField(editable=False)

    class Meta:
        ordering = ['-pub_date']

    def __unicode__(self):
        return self.title

    def save(self, force_insert=False, force_update=False):
        if not self.id:
            self.pub_date = datetime.datetime.now()
        self.updated_date = datetime.datetime.now()
        self.description_html = markdown(self.description)
        self.highlighted_code = self.highlight()
        super(Snippet, self).save(force_insert, force_update)

def get_absolute_url(self):
        return ('cab_snippet_detail', (), { 'object_id': self.id })
    get_absolute_url = models.permalink(get_absolute_url)

    def highlight(self):
        return highlight(self.code,
                         self.language.get_lexer(),
                         formatters.HtmlFormatter(linenos=True))

This handles the core of the application—code snippets organized by language—so now you can pause and start working on some initial views to get a feel for how things will look.

Go ahead and create an admin.py file as well, and set up a basic administrative interface for these models so you can use it to start interacting with the application.

Testing the Application

As you build out these views and the rest of the cab code-sharing application, I'm going to assume you've already got a Django project set up with a database and a template directory. If you'd like, you can keep using the existing project you've worked with for the two previous applications. However, this application isn't really related to either the simple CMS or the weblog, so if you'd like to start a new project now to work with this application, feel free to do so. In either case, you'll need to do three things:

  1. Add cab to the INSTALLED_APPS list of the project that you'll use to test and work with this application: If you're starting a new project, you'll also want to add django.contrib.admin and tagging to the list.
  2. Run manage.py syncdb to install the models you've written so far: Later, when you write the rest of the models, you can run it again to install them. The syncdb command knows how to figure out which models are already installed and sets up only the new ones.
  3. Use the admin interface to create some Language objects and fill in some Snippets: For a list of the languages pygments supports, and the language codes for the lexers, read pygments' lexer documentation online at http://pygments.org/docs/lexers/. In the next chapter, you'll see how to set up public-facing views that let ordinary users submit snippets without having to use the admin interface.

Building Initial Views for Snippets and Languages

As you wrote the weblog application, you relied heavily on Django's generic views to provide the date-based archives and detail views of the entries and links. Using date-based browsing doesn't make as much sense for this application, but you can certainly benefit from using the non-date-based generic views.

In the cab directory, create a new directory called urls, and in it create three files:

  • __init__.py, to mark this directory as a Python module
  • snippets.py, which will have the URLs for the snippet-oriented views
  • languages.py, which will have the URLs for the language-oriented views

As you did with the weblog's URLs, you'll keep each group of URLs for this application in its own file. This means you'll have several files in cab/urls, but the benefit in flexibility and reusability is worth it.

In urls/snippets.py, fill in the following code:

from django.conf.urls.defaults import *
from django.views.generic.list_detail import object_list, object_detail
from cab.models import Snippet

snippet_info = { 'queryset': Snippet.objects.all() }

urlpatterns = patterns('',
                       url(r'^$',
                           object_list,
                           dict(snippet_info, paginate_by=20),
                           name='cab_snippet_list'),
                       url(r'^(?P<object_id>d+)/$',
                           object_detail,
                           snippet_info,
                           name='cab_snippet_detail'),
)

This sets up two things:

  • A list of snippets, in the order in which they were posted: Note the extra argument you've passed here—paginate_by. This tells the generic view that you'd like it to show only 20 snippets a t a time. You'll see in a moment how to work with this pagination in the templates.
  • A detail view for individual Snippet objects: This is simply the object_detail generic view.

You should be able to set up the templates for this pretty easily. The list template gets a variable called {{ object_list }}, which is a list of Snippet instances, and the detail template gets a variable called {{ object }}, which is a specific Snippet. The generic views look for the cab/snippet_list.html and cab/snippet_detail.html templates.

The only tricky thing is handling the pagination of snippets in the list view. The template gets only 20 snippets at a time, so you need to display Next and Previous links to let the user navigate through them.

To handle this, the generic view provides two extra variables:

paginator: This is an instance of django.core.paginator.Paginator. It knows how many total pages of snippets there are and how many total snippets are involved.

page_obj: This is an instance of django.core.paginator.Page. It knows its own page number and whether there's a next or previous page.

In the snippet_list.html template, you could use something like this:

<p>{{ page }};
{% if page.has_previous %}
<a href="?page={{ page.previous_page_number }}">Previous page</a>
{% endif %}
{% if page.has_next_page %}
<a href="?page={{ page.next_page_number }}">Next page</a>
{% endif %}</p>

You can find a full example in the source code available for this book (downloadable from the Apress web site).

The object_list generic view knows to look for the page variable in the URL's query string, and it adjusts the snippets it displays accordingly. Meanwhile, the Page object knows how to print itself smartly; in the template, {{ page }} displays something like “Page 2 of 6."

To set up these views, add a pattern like this to your project's root urls.py file:

(r'^snippets/', include('cab.urls.snippets')),

CSS for pygments Syntax Highlighting

You'll have noticed in the Snippet detail view that the code sample doesn't actually appear to be highlighted in any way. This is because pygments, by default, simply generates HTML with some class names filled in to mark things like language keywords. It expects that you'll use a style sheet to change the presentation appropriately.

To get a head start on styling the highlighted code, look through some of the samples in the online demo of pygments at http://pygments.org/demo/. pygments comes with several styles built in, and once you've found one you like, you can have it output the appropriate CSS. You can then save that to a file and use it as your style sheet.

Here's a simple example of how to get the appropriate CSS information from a pygments style. This assumes that you've created a pygments.css file that you'll write the styles into, and that you've decided you like the “murphy” style. Open a Python interpreter and type the following:

>>> from pygments import formatters, styles
>>> style = styles.get_style_by_name('murphy')
>>> formatter = formatters.HtmlFormatter(style=style)
>>> outfile = open('pygments.css', 'w')
>>> outfile.write(formatter.get_style_defs())
>>> outfile.close()

The pygments.css file now contains a list of CSS style rules for the “murphy” style. You can tweak them a bit if you'd like. You can also have pygments automatically add more specific information to the CSS selector it uses, if you know that the highlighted blocks will appear only inside certain page elements. Consult the documentation for the pygments HtmlFormatter class for full details on how the get_style_defs() method works.

Views for Languages

To show a list of the languages that snippets have been submitted in, you can use the object_list generic view again. However, displaying a list of snippets for a particular Language is going to require a little bit of code. You'll need to write a wrapper around a generic view, as you did in Chapter 5, to show the list of entries in a particular category.

Go ahead and delete the views.py file in the cab application's directory and create a views directory. In it, put these two files:

  • __init__.py
  • languages.py

languages.py is where you'll put your first hand-written view for this application.

In views/languages.py, add the following code to set up the wrapper around the generic view:

from django.shortcuts import get_object_or_404
from django.views.generic.list_detail import object_list
from cab.models import Language

def language_detail(request, slug):
    language = get_object_or_404(Language, slug=slug)
    return object_list(request,
                       queryset=language.snippet_set.all(),
                       paginate_by=20,
                       template_name='cab/language_detail.html',
                       extra_context={ 'language': language })

This returns a paginated list of snippets for a particular language. Now you can go to urls/languages.py and fill in a couple of URL patterns:

from django.conf.urls.defaults import *
from django.views.generic.list_detail import object_list
from cab.models import Language
from cab.views.languages import language_detail

language_info = { 'queryset': Language.objects.all(),
                  'paginate_by': 20 }

urlpatterns = patterns('',
                       url(r'^$',
                           object_list,
                           language_info,
                           name='cab_language_list'),
                       url(r'^(?P<slug>[-w]+)/$',
                           language_detail,
                           name='cab_language_detail'),
)

Again, you should have no trouble setting up some basic templates to handle these views. The template names are cab/language_list.html and cab/language_detail.html.

To see these views in action, add a line like the following to your project's root urls.py file:

(r'^languages/', include('cab.urls.languages')),

An Advanced View: Top Authors

Because any user of the application will be allowed to submit a snippet of code, you'll want to have a way to show the names of users who've submitted the most snippets. Let's write a view called top_authors to handle that.

Inside the cab/views directory, create a new file called popular.py. You'll use this file for this top_authors view, as well as for some other views you'll write later to list snippets that are rated most highly and bookmarked most often.

Start the popular.py file with a couple of imports:

from django.contrib.auth.models import User
from django.views.generic.list_detail import object_list

It might seem a bit strange to import a generic view here, because it's hard to see any way you can use one for a query like this. In fact, even if you've been reading through the Django database API documentation, it might not be obvious how to do this query. So first, let's consider how the query will work.

Django's database API allows you to specify more than just queries that return instances of your models; you can also write queries that make use of your database's underlying support for more advanced features. In this case, you want the ability to use what are called “aggregate” queries, which calculate things like the number of database rows that fulfill some condition, the average of a collection of rows, and so on.

Django provides a number of built-in aggregate filters, but the one you want here is django.db.models.Count, which allowsyou to write a query that takes into account the number of snippets a particular author has posted. First, you'll need to import it:

from django.db.models import Count

Then you can write a query like this:

User.objects.annotate(score=Count('snippet')).order_by('score')

The annotate method tells Django to add an extra attribute to every User returned by this query: the attribute will be named score, and it will contain the number of snippets posted by the user. The order_by method tells Django how to order the results of the query, and it is passed score as an argument. The result, then, will be a list of users arranged in order from most snippets posted to fewest.

And because this is a Django QuerySet, you can pass it to the object_list view:

def top_authors(request):
    top_authors_qs = User.objects.annotate(score=Count('snippet')).order_by('score')
    return object_list(request, queryset=top_authors_qs,
                       template_name='cab/top_authors.html',
                       paginate_by=20)

You'll end up with a paginated list of users ordered by their snippet counts. Then you can wire up a URL for it. Let's add a new file in the urls directory, popular.py, and use it for all of these top views. In it, you place the following:

from django.conf.urls.defaults import *
from cab.views import popular

urlpatterns = patterns('',
                       url(r'^authors/$',
                           popular.top_authors,
                           name='cab_top_authors'),
)

Once again, you can wire this up in your project's root urls.py file:

(r'^popular/', include('cab.urls.popular')),

After you've created the cab/top_authors.html template, you'll see some results. Of course, the results won't be that impressive right now, because the application has only one user—you. However, when deployed live on a site with multiple users, the top_authors view will be a nice feature.

Improving the View of Top Authors

You can make this feature even better by encapsulating the top-authors query in a reusable way. Right now, it's a bit of a mouthful, and you wouldn't want to type it out over and over if you ever needed to reuse it.

Let's write a custom manager for the Snippet model and make the top-authors query a method on the manager. Because you're going to end up writing several custom managers for this application, let's go ahead and create a managers.py file in the cab directory. Then, inside it, put the following code:

from django.db import models
from django.contrib.auth.models import User
from django.db.models import Count

class SnippetManager(models.Manager):
    def top_authors(self):
        return User.objects.annotate(score=Count('snippet')).order_by('score')

In cab/models.py, add a new import statement at the top:

from cab import managers

In the definition of the Snippet model, add the custom manager:

objects = managers.SnippetManager()

Now you can rewrite the top_authors view like this:

from django.views.generic.list_detail import object_list
from cab.models import Snippet

def top_authors(request):
    return object_list(request, queryset=Snippet.objects.top_authors(),
                       template_name='cab/top_authors.html',
                       paginate_by=20)

That's much nicer.

Adding a top_languages View

While you're adding these features, go ahead and add the ability to show the most popular languages through a view called top_languages. This will involve a query similar to the top_authors view, so it'll be easy to write now.

One important design decision, though, is where to put the method to do this query. You could put it on the SnippetManager and probably even rework the top_authors() method into a top_objects() method. This new method could return the top authors, the top languages, or—later, when you've built out the models for them—the most-bookmarked or highest-rated snippets according to what argument it received. That would cut down on the number of times you'd have to write methods to do this sort of query. However, a disadvantage to this approach is that, logically, the list of top languages doesn't “belong” with the Snippet model; it belongs with the Language model. Because it's better to present a logical API for your application's users than to be lazy about writing code, go ahead and give Language a custom manager and put this query there.

In cab/managers.py, add the following:

class LanguageManager(models.Manager):
    def top_languages(self):
        return self.annotate(score=Count('snippet')).order_by('score')

In cab/models.py, you can add the manager in the definition of the Language model:

objects = managers.LanguageManager()

In cab/views/popular.py, you can change the import statement from

from cab.models import Snippet

to

from cab.models import Language, Snippet

Write this view:

def top_languages(request):
    return object_list(request,
                       queryset=Language.objects.top_languages(),
                       template_name='cab/top_languages.html',
                       paginate_by=20)

and change cab/urls/popular.py to the following:

from django.conf.urls.defaults import *
from cab.views import popular

urlpatterns = patterns('',
                       url(r'^authors/$',
                           popular.top_authors,
                           name='cab_top_authors'),
                       url(r'^languages/$',
                           popular.top_languages,
                           name='cab_top_languages'),
)

Now you can create the cab/top_languages.html template and add some snippets in various languages to see the results change.

Looking Ahead

Now that you've got the core of this code-sharing application in place, you'll learn to implement some of the user interactions in the next chapter. For one thing, you'll get an introduction to Django's form-processing system, so you can see how to let users submit snippets without going through the admin interface.

If you'd like a little challenge before moving on to form handling, try writing a view that lists tags ordered by the number of snippets that use them. Take a look in the tagging application to see how the tags work, and check out the Django contenttypes framework documentation (www.djangoproject.com/documentation/contenttypes/) to get a feel for the generic relations that the tags use. If you get stumped, you can find a working example in the source code associated with this book (download it from the Source Code/Download area of the Apress web site at www.apress.com).

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

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