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.
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:
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.
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.
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.
Language
ModelOpen 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:
pygments
can use to load the appropriate syntax-highlighting moduleBased 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)
Snippet
ModelNow you can write the class that represents a snippet of code: Snippet
. It will need to have several fields:
Entry
model in your weblog.Language
the snippet is written in.User
model to represent the snippet's author.TagField
you saw in the weblog application.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:
description_html
field.highlighted_code
field.pub_date
to the current date and time if this is the first time the snippet is being saved.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.
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:
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.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.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.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 modulesnippets.py
, which will have the URLs for the snippet-oriented viewslanguages.py
, which will have the URLs for the language-oriented viewsAs 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:
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.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 ofdjango.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 ofdjango.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')),
pygments
Syntax HighlightingYou'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.
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')),
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.
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.
top_languages
ViewWhile 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.
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
).