C H A P T E R   4

image

A Django-Powered Weblog

The simple CMS you built in the last two chapters was a good example of how Django's bundled applications can help you get a project off the ground quickly and without much code. But most of the time, you'll probably be developing things that aren't covered quite so neatly by prebuilt applications included with Django itself. Django still has a lot to offer in these situations, mostly by taking the bulk of repetitive work off your shoulders. Over the rest of this book, you'll be writing applications from scratch and seeing how Django's components can make that a much easier and much less painful process. Let's start with something that's quickly becoming a necessity for any organization that goes online: a weblog.

Compiling a Feature Checklist

Real-world applications usually start with at least a rough specification of what they'll need to do, and I'll follow the same process here. Before you sit down and write the weblog application, you'll need to decide up-front what you want it to do. When I wrote a weblog app for my own personal use, this was the feature list I had in mind:

  • It needs to provide an easy way for you to add and edit entries without writing raw HTML.
  • It should support multiple authors and provide a way to separate entries according to author.
  • Each entry should allow an optional short excerpt to be displayed when a summary is needed.
  • The weblog's authors should be able to create categories and assign entries to them.
  • Authors should be able to decide which entries will be displayed publicly and which will not (in order to, for example, mark an unfinished entry as a draft and come back to it later).
  • Entries should be able to be “featured,” and these entries should be easily retrievable (for display on the weblog's home page, for example).
  • A link log should be provided, as well, to allow posting of interesting or notable links.
  • Both entries and links should support tagging—adding arbitrary descriptive words to provide extra metadata or organization.
  • The link log should integrate with Delicious (a social bookmarking site at http://delicious.com/) or other popular link-sharing services so that links posted to the weblog automatically show up on the service as well.
  • Visitors should be able to browse entries and links by date, by tag, or (in the case of entries) by category.
  • Visitors to the blog should be able to leave comments on entries and links.
  • Comments should be subject to some sort of moderation in order to avoid comment spam.

There are more features you could add here, but this list is enough to keep you busy for a while; it will make use of a broad range of Django's features. So let's get started.

Writing a Django Application

In the last chapter, when you added the search function and SearchKeyword model to the simple CMS, you built a simple Django application—initially created with the manage.py startapp command—to hold them. At the time I didn't spend much time detailing just what goes into a Django application. However, now that you're going to start doing more complex things, it's worth pausing for a moment to go over it, to understand how individual Django applications differ from a Django project.

Projects vs. Applications

As you've seen already, you configure a Django project through its settings module, which—among other things—specifies the database it will connect to and the list of applications it uses. In a way, the defining quality of a project is that it's the “thing” that holds the settings (including both the settings module and the root URLConf module, which specifies the project's base URL configuration).

A project can also contain other code if it makes sense for that code to be part of the project directly, but the necessity for this is fairly rare. Generally, a project exists to provide a “container” for a set of Django applications to work together, and most projects won't ever need anything beyond the initial files created by django-admin.py startproject.

A Django application, on the other hand, is responsible for actually providing some piece of functionality and should try to focus on that functionality as much as possible. An application doesn't have a settings module—that's the job of any projects that use it—but it does provide several other things:

  • An application can (and often does) provide one or more data models.
  • An application usually provides one or more view functions, often related in some way to its data models.
  • An application can provide libraries of custom template tags, which extend Django's template system with extra, application-specific features.
  • An application can (and usually should) provide a URLConf module suitable for being “plugged in” to a project (via the include directive, as you've already seen in the case of the administrative interface and flatpages application bundled with Django).

And, of course, an application should also provide any extra “utility” code needed to support itself, or it should have clear dependencies on other applications or on third-party Python modules that provide that support.

Standalone and Coupled Applications

It is important to be aware of the distinction between two different ways of developing Django applications. One method, which I used in the last chapter, uses the manage.py startapp command to create an application module inside the project's directory. While this is easy and convenient, it does have some drawbacks, most notably in the fact that it “couples” the application to the project. Any other Python code that wants to access that application needs to know that it “lives” inside that particular project. (For example, to import the SearchKeyword model from a separate piece of code, you'd have to import it from cms.search.models instead of just search.models.) Any time you want to reuse the application, you need to either make a copy of the project or create a set of empty directories to emulate the project's directory structure.

The alternative is to develop a standalone application, which acts as an independent, self-contained Python module and doesn't need to be kept inside a project directory in order to work correctly. A standalone application is much easier to reuse and distribute, but setting it up does involve a bit more initial work: the manage.py startapp command can't create things automatically for you unless you're developing an application that's coupled to a particular project.

There are cases where you'll develop one-off applications that don't need to be reusable or distributable. (In those cases, it's perfectly fine to develop them inside of, and coupled to, a particular project; just be wary of the fact that many supposedly “one-off” pieces of code like this do eventually need to be reused elsewhere.) But in general, you'll get more benefit from developing standalone applications that can be reused in many different projects. That's how you'll be working for the rest of this book.

Creating the Weblog Application

Because this is going to be a standalone application, you'll need to create a Python module for it manually instead of relying on manage.py startapp, but that's not too hard. You might remember that all the startapp command really did was create a directory and put three files into it, and that's all you'll need to do to get started.

There are only two things you need to worry about when manually setting up a new application module: what to call it and where to put it. You can call an application by any name that's legal for a Python module: Python allows module names to consist of any combination of letters and numbers and, optionally, underscores to separate words (although the name must start with a letter). Because Django is named after a jazz musician, some developers like to continue the pattern by naming applications after famous jazz figures. (For example, the company I work for sells a CMS called Ellington—named for Duke Ellington—and there's a popular open source e-commerce application named Satchmo in honor of Louis Armstrong.) This isn't required, but it's something I like to do whenever there's not a more obvious name. So when I wrote my own weblog application, I named it Coltrane after John Coltrane. That seemed appropriate, given that Coltrane was known for composition and improvisation, two skills that also make a good blogger.

Where to put the application's code is a slightly trickier question to answer. So far you haven't run into this problem because Django's manage.py script, in order to make initial setup and development easier, somewhat obscures an important requirement for Python code: it has to be placed in a directory that's on the Python path. The Python path is simply a list of directories where Python will search whenever it encounters an import statement. So code that's meant to be imported (as your application will be, in order to be used as part of a Django project) needs to be on the Python path.

When you installed Python, a default Python path was set up for you, and it included a directory called site-packages. When you installed Django, the setup.py installer script placed all of Django's code inside that directory. You can place your own code in site-packages if you'd like, but it's generally not a good idea to do so. The site-packages directory is almost always set up in a part of your computer's file system that requires administrative access to write to, and you won't have much fun constantly jumping through the authentication hoop to place things there. Instead, most Python programmers create a directory where they'll keep their own code and add it to the Python path, so let's do that. Because you've already created a directory to hold your Django projects, go ahead and add it to your Python path and place your standalone applications in it as well. This way, you'll need to add only one directory to the Python path, and you won't be scattering code into multiple locations on your computer.

Now, in the same directory where you created the cms project (in other words, alongside cms, not inside cms), create a new directory named coltrane. Inside that, create four empty files:

  • __init__.py
  • models.py
  • views.py
  • admin.py

This is all you'll need for now: the __init__.py file will tell Python that the coltrane directory is a Python module, and the models.py and views.py files will hold the initial code for the weblog application. Finally, admin.py will let you set up Django's administrative interface for the weblog.

Designing the Models

You're going to need several models to implement all of the features in your list, and a couple of them will be moderately complex. However, you can start with a simple one: the model that will represent categories for entries to be assigned to. Open up the weblog application's models.py file, and add the following:

from django.db import models


class Category(models.Model):
    title = models.CharField(max_length=250)
    slug = models.SlugField(unique=True)
    description = models.TextField()



    def __unicode__(self):
        return self.title

Most of this should be familiar after your first foray into Django models in the last chapter. The import statement pulls in Django's models module, which includes the base Model class and definitions for the different types of fields to represent data. You've already seen the CharField (this one has a longer max_length in order to allow for long category names) and the __unicode__() method (which, for this model, returns the value of the title field). But there are two new field types here: SlugField and TextField.

The meaning of TextField is pretty intuitive. It's meant to store a larger amount of text (in the database, it will become a TEXT column), and it will be used here to provide a useful description of the category.

SlugField is a bit more interesting. It's meant to store a slug: a short, meaningful piece of text, composed entirely of characters that are safe to use in a URL. You use SlugField when you generate the URL for a particular object. This means, for example, that instead of having a URL like /categories?category_id=47, you could have /categories/programming/. This is useful to your site's visitors (because it makes the URL meaningful and easier to remember) and for search-engine indexing. URLs that contain a relevant word often rank higher in Google and other search engines than URLs that don't. The term slug, as befits Django's heritage, comes from the newspaper industry, where it is used in preprint production and sometimes in wire formats as a shorter identifier for a news story.

Note that I've added an extra argument to SlugField: unique=True. Because the slug is going to be used in the URL and the same URL can't refer to two different categories, it needs to be unique. Django's administrative interface will enforce uniqueness for this field, and manage.py syncdb will create the database table with a UNIQUE constraint for that column.

You'll also want to be able to manage categories through Django's administrative interface, so in the admin.py file add the following:

from django.contrib import admin
from coltrane.models import Category

class CategoryAdmin(admin.ModelAdmin):
    pass

admin.site.register(Category, CategoryAdmin)

It's useful when developing an application to stop every once in a while and actually try it out. So go back to the cms project, open its settings file, and add coltrane—the new weblog application—to its INSTALLED_APPS setting:

INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.admin',
    'django.contrib.flatpages',
    'cms.search',
    'coltrane',
)

Because it's directly on the Python path, just adding coltrane will work. Next, run python manage.py syncdb to install the table for the Category model and launch the development server. The admin index page will look like that shown in Figure 4-1.

You can see that the Category model shows up, but it's labeled “Categorys.” That's no good. Django's admin interface generates that label from the name of the model class and tries to pluralize it by adding an “s,” which works most of the time. It doesn't always work, though, and when it doesn't Django lets you specify the correct plural name. Go back to the weblog's models.py file and edit the Category model class to look like the following:

class Category(models.Model):
    title = models.CharField(max_length=250)
    slug = models.SlugField(unique=True)
    description = models.TextField()

    class Meta:
        verbose_name_plural = "Categories"



    def __unicode__(self):
        return self.title

Once you save the file and refresh the admin index page in your browser, you should see something similar to what's shown in Figure 4-2.

image

Figure 4-1. The Django admin interface with the Category model

image

Figure 4-2. The correct pluralization of the Category model

Because you often need to provide extra meta-information about a model, Django lets you add an inner class named Meta, which can specify a large number of common options. In this case, you're using an option called verbose_name_plural, which will return a pluralized name for the model class whenever it's needed. (There's also a verbose_name option, which can specify a singular version if it differs significantly from the class name, but you don't need it here.) You'll see a number of other useful options for the inner Meta class as you flesh out the weblog's models.

If you click in the admin interface to add a category, you'll see the appropriate fields in a nice form: title, slug, and description. But adding a category this way will reveal another shortcoming. Most of the time, the value for the slug field will probably be similar or even identical to the value for the title field (for example, a Programming category should probably have a slug like “programming”). Manually typing the slug every time would be tedious, so why not generate it automatically from the title and let the user manually change it if necessary? This is easy enough to do. In the admin.py file, change the CategoryAdmin class to look like this:

class CategoryAdmin(admin.ModelAdmin):
    prepopulated_fields = { 'slug': ['title'] }

Then save the admin.py file and add a category. The prepopulated_fields argument will turn on a helpful piece of JavaScript in Django's administrative interface, and it will automatically fill in a suggested slug as you type a value into the title field. Note that prepopulated_fields gets a list: this means you could specify multiple fields from which to draw the slug value, which isn't common but is sometimes useful. The JavaScript that generates slugs is also smart enough to recognize, and omit, words like “a,” “an,” “the,” and so on. These are called stop words and generally aren't useful to have in a slug.

Also, note that when Django creates the database table for this model, it will add an index to the slug column. You can manually tell Django to do this with any field (by using the option db_index=True for the field), but SlugField will get the index automatically. This provides a performance boost in the common case of using a slug from a URL to perform database queries.

While you're looking at categories in the admin interface, let's pause and add another useful feature—helpful hints that give the weblog application's users more information as they fill in the data. So edit the definition of the title field in models.py like this:

title = models.CharField(max_length=250, help_text='Maximum 250 characters.')

Next, save the models.py file and look at the admin form again (see Figure 4-3).

image

Figure 4-3. The admin form for adding a category

The string given in the help_text argument shows up underneath the text box for the title field, providing a useful hint about what can be entered there. You can add help_text to any field in your model, and it's generally a good idea to do so whenever there's something users should know while entering data. So let's add it for the slug field as well:

slug = models.SlugField(help_text="image
Suggested value automatically generated from title. Must be unique.")

Next, save the models.py file and refresh the admin form again. You'll see that text show up under the slug field's text box, notifying users that a suggested value will be filled in and reminding them that the slug must be unique.

Before I move on, let's add one more improvement. If you try adding a couple of categories, you might notice that the admin page, which lists all of the categories, doesn't necessarily keep them in any order. It would be nice to have them displayed in an alphabetical list so that a user can scan through them quickly. Again, this is easy enough to do. The inner Meta class accepts an option to specify a default ordering for the model:

class Meta:
    ordering = ['title']
    verbose_name_plural = "Categories"

Save the models.py file after inserting that code. When you refresh the admin page, you'll see that the categories are alphabetized. Unless you specifically override it on a per-query basis, Django will now append the clause ORDER BY title ASC to any database query for the categories table, which will get categories back in the correct alphabetical order. Notice that the value for ordering is a list. You can specify multiple fields here, and they'll be correctly placed into an ORDER BY clause for most queries. (The admin application uses only the first field in the ordering option when retrieving lists of objects.)

One more useful thing you can add is a special method called get_absolute_url(). In Chapter 2, you saw that this is the standard practice for a Django model that wants to specify its own URL, and every model that is intended to be used in a public-facing view should have a get_absolute_url() method. So let's add one:

def get_absolute_url(self):
    return "/categories/%s/" % self.slug

For now, just put this method at the bottom of the Category class; remember that it needs to be indented to be part of the class. You'll see a bit later how to keep all the parts of a Django model class organized.

This method will return a string with the value of the category's slug field interpolated into the correct place. Adding this method will also cause the admin interface to show a View on Site button for each category, though for now it won't be very useful because you haven't yet set up any URLs or views to actually display them.

Building the Entry Model

Now that you have categories to assign entries to, it's time to build the model for the weblog entries. Because it will really be the center of attention for this application, it'll also be the most complex model you'll need to build, so let's take it a bit at a time.

Basic Fields

First off, you need to have a few core fields to hold the title of the entry, the optional excerpt, the text of the entry, and the date the entry was published. So let's start with those. Open up the models.py file and, below the Category model class, start adding the new Entry model. (Don't run manage.py syncdb yet. You'll be adding more fields to this model, and it's best to wait until that's done before having Django create the database tables.) Add these lines first:

class Entry(models.Model):
    title = models.CharField(max_length=250)
    excerpt = models.TextField(blank=True)
    body = models.TextField()
    pub_date = models.DateTimeField()

Also, go ahead and set up a basic admin definition for this model in admin.py. You'll want to change the line that imports the Category model to also import Entry:

from coltrane.models import Category, Entry

And then add the new admin class for the Entry model:

class EntryAdmin(admin.ModelAdmin):
    pass

admin.site.register(Entry, EntryAdmin)

The first three fields in this new model—title, excerpt, and body—are all of types you've seen before. But the pub_date field has a new field type called DateTimeField. It will represent the entry's publication date. Compared to the field types you've seen so far, DateTimeField is unique in several ways:

  • When you store entries into or retrieve them from the database, this field will have as its value a Python datetime object (the datetime class is found in the datetime module, which is a standard part of Python), regardless of how it's actually stored in the database (different databases will, internally, handle it in slightly different ways). Django also provides separate field types, which store only a date or only a time, but DateTimeField handles both. This means you can track not only the date the entry was published, but also the time (so you can eventually display something like “Published on October 7 at 10:00 P.M.”).
  • The exact type of database column created for this field will vary from database to database. Up until now, you've seen fields that consistently become the same type of column (VARCHAR for CharField, for example) no matter what type of database you're using. However, because of variations in column types, Django will use different options as appropriate. For example, DateTimeField will become a DATETIME column in SQLite and a TIMESTAMP column in PostgreSQL.
  • So far, each type of field you've worked with has translated directly into one form input in the administrative interface, usually a text box. A DateTimeField, however, becomes two form inputs: one for the date and one for the time. You'll see this when you start working with entries in the administrative interface.

There's also an option on the excerpt field that you haven't seen before: blank=True. So far, the question of required fields hasn't really come up. You've been working with simple models where there's no need to have some things be optional, so Django's default behavior—to make the field required when entering data through a form in the admin interface and to create a NOT NULL column in the database—has been fine. In this case, though, you need to make the excerpt field optional, and the blank=True option tells Django that it's okay not to enter anything for this field. You can add blank=True to any type of field in a Django model.

Slugs, Useful Defaults, and Uniqueness Constraints

Just as you added a slug for categories, it's a good idea to add one for entries and to set it up to populate a default from the entry's title. So add the following to the Entry model:

slug = models.SlugField()

Then change the EntryAdmin class to automatically populate the slug:

class EntryAdmin(admin.ModelAdmin):
    prepopulated_fields = { 'slug': ['title'] }

With the Category model, you added unique=True to force the slug to be unique, but for entries it would be nice to have something slightly different. Most good weblog software builds URLs that include the publication dates of entries (so that they look like /2007/10/09/entry-title/), which means that all you really need is for the combination of the slug and the publication date to be unique. Django provides an easy way to specify this, through an option called unique_for_date:

slug = models.SlugField(unique_for_date='pub_date')

This will tell Django to allow a particular slug to be used only once on each date. The unique_for_date constraint is one of three date-based constraints supported by Django. The other two are unique_for_month and unique_for_year. Whereas unique_for_date allows a given value to be used only once per day, the other two constrain values to being used once per month and once per year, respectively.

It would also be nice to provide a sensible default value for the pub_date field. Most of the time, entries will be “published” on the same day they're entered, so defaulting to the current date and time would be convenient for the weblog's authors. Django allows you to specify a default value for any type of field by using the default option. The only question is how to specify a default of “right now.”

The answer lies in Python's standard datetime module. This provides a function, datetime. datetime.now(), for obtaining the current date and time and returns the correct type of object (a Python datetime, as previously described) for filling in a DateTimeField. So at the top of the models.py file, add an import statement to make the datetime module available:

import datetime

and then edit the pub_date field to add the default:

pub_date = models.DateTimeField(default=datetime.datetime.now)

Notice that there aren't any parentheses there—it's datetime.datetime.now, not datetime.datetime.now(). When you're specifying a default, Django lets you supply either an appropriate value or a function, which will generate the appropriate value on demand. In this case, you're supplying a function, and Django will call it whenever the default value is needed. This ensures that the correct current datetime is generated each time.

Authors, Comments, and Featured Entries

Because the weblog needs to support multiple authors, you need a way to mark the author of each entry. In the last chapter, when you implemented search keywords, you saw that Django provided the ForeignKey field for relating one model to another (and translates it into a foreign key in the database). The obvious solution is to have a model representing authors and a foreign key on each entry tying it to an author.

This is a case where Django will help you out immensely. The bundled application django.contrib.auth provides a User model. (This is the user account you created when running manage.py syncdb for the first time, which is stored in the database as an instance of the User model.) This model lives in the module django.contrib.auth.models, so you'll need to add an import statement in the weblog's models.py file. From django.contrib.auth.models, import User, and then add the foreign key to the Entry model:

author = models.ForeignKey(User)

Another feature that's easy to add is a per-entry way to allow or disallow comments. You haven't yet seen the code that will actually handle user-submitted comments (that will come a bit later); however, you will need something on the Entry model that allows you to check whether comments should be allowed. So let's add a field for it:

enable_comments = models.BooleanField(default=True)

A BooleanField has only two possible values—True or False—and in web-based forms will be represented by a check box. I give it a default value of True because most people will probably want comments on by default, but an entry's author will be able to uncheck the box in the admin interface to disable comments.

While you're looking at BooleanField, remember that one of the features on your list is the ability to mark entries as “featured” so that they can be singled out for special presentation. That's also easy to do with a BooleanField:

featured = models.BooleanField(default=False)

This time, set the default to False, because only a few specific entries should be featured.

Different Types of Entries

You also need to support entries that are marked as “drafts,” which aren't meant to be shown publicly. This means you'll need some way of recording an entry's status. One way would be to use another BooleanField, with a name like is_draft or, perhaps is_public. Then you could just query for entries with the appropriate value, and authors could check or uncheck the box to control whether an entry shows up publicly.

But it would be better to have something that you can extend later. If there's ever a need for even one more possible value, the BooleanField won't work. The ideal solution would be some way to specify a list of choices and allow the user to select from them; then if you ever need more choices, you can simply add them to the list. Django provides an easy way to do this via an option called choices. Here's how you'll implement it:

STATUS_CHOICES = (
    (1, 'Live'),
    (2, 'Draft'),
)
status = models.IntegerField(choices=STATUS_CHOICES, default=1)

Here you're using IntegerField, which, as its name implies, stores a number—an integer—in the database. But you've used the choices option and defined a set of choices for it. The value passed to the choices option needs to be a list or a tuple, and each item in it also needs to be a list or a tuple with the following two items:

  • The actual value to store in the database
  • A human-readable name to represent the choice

You've also specified a default value: the value associated with the Live status, which will denote weblog entries to be displayed live on the site.

You can use choices with any of Django's model field types, but generally it's most useful with IntegerField (where you can use it to provide meaningful names for a list of numeric choices) and CharField (where, for example, you can use it to store short abbreviations in the database, but still keep track of the full words or phrases they represent).

If you've used other programming languages that support enumerations, this is a similar concept. In fact, you could (and probably should) make it look a little bit more similar. Edit the Entry model so that it begins like this:

class Entry(models.Model):
    LIVE_STATUS = 1
    DRAFT_STATUS = 2
    STATUS_CHOICES = (
        (LIVE_STATUS, 'Live'),
        (DRAFT_STATUS, 'Draft'),
    )

Now instead of hard-coding the integer values anywhere you're doing queries for specific types of entries, you can instead refer to Entry.LIVE_STATUS or Entry.DRAFT_STATUS and know that it'll be the right value. The status field can also be updated:

status = models.IntegerField(choices=STATUS_CHOICES, default=LIVE_STATUS)

And, just to show how easy it is to add new choices, let's throw in a third option: hidden. This common option offered by popular weblogging packages covers situations where an entry isn't really a draft but also shouldn't be shown publicly. Now the relevant part of the Entry model looks like this:

LIVE_STATUS = 1
DRAFT_STATUS = 2
HIDDEN_STATUS = 3
STATUS_CHOICES = (
    (LIVE_STATUS, 'Live'),
    (DRAFT_STATUS, 'Draft'),
    (HIDDEN_STATUS, 'Hidden'),
)

And just as you can refer to Entry.LIVE_STATUS and Entry.DRAFT_STATUS, now you can also refer to Entry.HIDDEN_STATUS.

Categorizing and Tagging Entries

You'll remember that your feature list calls for two types of entry groups: categories (which you've already laid some groundwork for in the form of the Category model) and tags. Setting up the Entry model to use categories is easy:

categories = models.ManyToManyField(Category)

ManyToManyField is another way of relating two models to each other. Whereas a foreign key allows you to relate to only one specific object of the other model class, a ManyToManyField allows you to relate to as many of them as you'd like. In the admin interface, this will be represented as a list of categories presented in an HTML <select multiple> element.

Tagging is a bit trickier because tags ultimately need to be applied to two different models: the Entry model you're writing now and the Link model you'll write (in the next chapter) to represent a link log. You could define two Tag models—one for entries and one for links—or set up multiple many-to-many relationships to allow a single Tag model to suffice for both, but Django provides a simpler solution in the form of a generic relation.

Generic relations actually involve two special field types, GenericForeignKey and GenericRelation, that allow one model to have relationships with any other model installed in your project. Because of the complexity necessary to make this work, they can be a bit tricky to set up and use. You're lucky in this particular case: there's an open source Django application that implements tags via generic relations and that has already done all the hard work.

The application is called django-tagging, and you can download it from http://code.google.com/p/django-tagging/. Grab a copy and unpack it so that the tagging module it provides is on your Python path, then add tagging to your INSTALLED_APPS setting. To add tags to your Entry model, you'll need to import a custom field type defined in django-tagging, so add the following import statement in the weblog's models.py file:

from tagging.fields import TagField

Next, add the following to the Entry model:

tags = TagField()

This may feel a bit strange, but actually it's the right way to handle tagging, for two reasons:

  • Django provides a lot of built-in field types you can add to your models, but there's no way it could cover everything you might need to represent in a model class. So in addition to the built-in fields, Django also provides an API for writing your own custom field types. The TagField provided by django-tagging is simply an example of this.
  • Encapsulating common types of functionality into reusable, “pluggable” applications is precisely what Django tries to encourage. The fact that, in this case, the application was written by someone else and isn't bundled in django.contrib shouldn't be a deterrent. As you work more with Django, you'll likely take advantage of the large ecosystem of third-party applications that save you from having to reinvent the wheel with your own implementations of a lot of common functions.

Writing Entries Without Writing HTML

The last important feature for the Entry model is the ability to write entries without having to compose them in raw HTML. Most popular weblogging applications allow users to write entries using a simpler syntax that will be automatically converted into HTML as needed. There are a number of widely used systems that can take plain text with a little bit of special syntax and perform the conversion. Textile, Markdown, BBCode, and reStructuredText are the most popular.

One way you could handle this is with template filters. As you saw in the last chapter, Django's template system allows you to apply filters to variables in your templates (as you did when you used the escape filter to prevent cross-site scripting attacks). Django includes ready-made template filters for applying Textile, Markdown, and reStructuredText to any piece of text in a template, and that would be an easy solution. Unfortunately, it's also an expensive solution. Running a text-to-HTML converter every time you display an entry will needlessly eat up CPU cycles on your server, especially because the resulting HTML will be the same each time. A better solution would be to generate the HTML once—when the entry is saved to the database—and then retrieve it directly for display.

You could just store the generated HTML in the body and excerpt fields, but that would remove the benefit of using a simpler syntax for writing entries. As soon as you went back to edit an entry, you'd be presented with the HTML instead of the plain text it was generated from. So what you really need is a separate pair of fields that will store the HTML and a bit of code to generate it whenever an entry is saved. If you were worried earlier about database normalization—the principle that information shouldn't be needlessly duplicated—this is a good example of where deliberate denormalization is useful. On most consumer-level web hosting, disk space is far more abundant than processor time, so accepting a bit of redundancy in the database in return for less processing on each page view is a good trade-off to make.

First, let's add the fields:

excerpt_html = models.TextField(editable=False, blank=True)
body_html = models.TextField(editable=False, blank=True)

Like their plain-text counterparts, these both use TextField. Both of them also use the blank option because you don't want users to have to enter anything in these fields. They also add the option editable=False. This tells Django not to bother displaying these fields when it generates forms for the Entry model, because you'll automatically generate the HTML to put into them.

Generating the HTML whenever an entry is saved is actually fairly easy. The base Model class that all Django models inherit from defines a method named save(), and individual models can override that method to provide custom behavior. The only hard part is choosing a text-to-HTML converter to use. I like Markdown, so that's what I'll go with. There's an open source Python Markdown converter available, which you can download at https://sourceforge.net/projects/python-markdown/. It provides a module named markdown, which contains the markdown function for doing text-to-HTML conversion. This means you use one more import statement:

from markdown import markdown

The actual save() method inside the Entry model is fairly short:

def save(self, force_insert=False, force_update=False):
    self.body_html = markdown(self.body)
    if self.excerpt:
        self.excerpt_html = markdown(self.excerpt)
    super(Entry, self).save(force_insert, force_update)

This runs Markdown over the body field and stores the resulting HTML in body_HTML. It also does a similar conversion for the excerpt field (after checking whether an excerpt was entered; remember that it's optional), and then saves the entry. Note that the save() method accepts a couple of extra arguments. Django uses these internally to force certain types of queries when saving to your database. (In some cases, it's necessary to force either an INSERT or an UPDATE query. Normally, Django simply chooses one or the other based on whether it's saving a new object or updating an existing object.) The save() method must accept these arguments and pass them on to the base implementation.

Finishing Touches

Now you have all the fields you'll need to handle your feature list for entries. It's taken a little while to cover the full list, but if you look at the Entry model, you'll notice that it's only around 30 lines of actual code. Django manages to pack a lot of functionality into a very small amount of code. Before moving on, though, let's add a few extra touches to this model to make it a bit easier to work with.

You've already seen with the Category model that Django will try to pluralize the name of the model when displaying it in the admin interface, sometimes with incorrect results. So let's add a plural name for the Entry model as well:

class Meta:
    verbose_name_plural = "Entries"

While you're at it, you can also add default ordering for the model. In this case, you want the entries ordered by date with the newest entries coming first, so you'll add an ordering option inside the inner Meta class:

ordering = ['-pub_date']

Now Django will use ORDER BY pub_date DESC when retrieving lists of entries.

Let's also go ahead and add a __unicode__() method so you can get a simple string representation of an entry:

def __unicode__(self):
    return self.title

It's also a good idea to add help_text to most of the fields. Use your judgment to decide which fields need it, but feel free to compare with and borrow from the full version of the Entry model included in this book.

Finally, let's add one more method: get_absolute_url(). Remember from Chapter 2 that it is standard convention in Django for a model to specify its own URL. In this case, you'll return a URL that includes the entry's publication date and its slug:

def get_absolute_url(self):
    return "/weblog/%s/%s/" % image
               (self.pub_date.strftime("%Y/%b/%d").lower(), self.slug)

Once again, you're using Python's standard string formatting. In this case, you're interpolating two values: the entry's pub_date (with a little extra formatting provided by the strftime() method available on Python datetime objects), and the entry's slug. This particular formatting string will result in a URL like /weblog/2007/oct/09/my-entry/. The %b character in strftime() produces a three-letter abbreviation of the month (which you force into lowercase with the lower() method in order to ensure consistently lowercase URLs). In general, I prefer that abbreviation to a numeric month representation because it's a bit more readable. If you'd prefer the month to be represented numerically, use %m instead of %b.

The Weblog Models So Far

You've now got two of the three models you'll need. Only the Link model still needs to be written, and you'll deal with it in the next chapter. The rest of this chapter will cover the views and URLs for entries in the weblog. But before you move on to that, let's pause to organize the models.py file so it'll be easier to understand and edit later on.

I've mentioned previously that Python has an official style guide. It's a good idea to follow that whenever you're writing Python code because it will make your code clearer and more understandable to anyone who needs to read it (including you). There's also a (much shorter) style guide for Django, which also provides some useful conventions for keeping your code readable. The guideline for model classes is to lay them out in this order:

  1. Any constants and/or lists of choices
  2. The full list of fields
  3. The Meta class, if present
  4. The __unicode__() method
  5. The save() method, if it's being overridden
  6. he get_absolute_url() method, if present
  7. Any additional custom methods

For complex models, I also like to break up the field list into logical groups, with a short comment explaining what each group is. In general, it's easier to find things if you keep field names and options alphabetized whenever possible. So with that in mind, here's the full models.py file so far, organized and formatted so that it's clear and readable:

import datetime

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

from markdown import markdown
from tagging.fields import TagField


class Category(models.Model):
    title = models.CharField(max_length=250,
                             help_text='Maximum 250 characters.')
    slug = models.SlugField(unique=True, help_text="image
Suggested value automatically generated from title. Must be unique")
    description = models.TextField()

    class Meta:
        ordering = ["title"]
        verbose_name_plural = "Categories"

    def __unicode__(self):
        return self.title

    def get_absolute_url(self):
        return "/categories/%s/" % self.slug


class Entry(models.Model):
    LIVE_STATUS = 1
    DRAFT_STATUS = 2
    HIDDEN_STATUS = 3
    STATUS_CHOICES = (
        (LIVE_STATUS, 'Live'),
        (DRAFT_STATUS, 'Draft'),
        (HIDDEN_STATUS, 'Hidden'),
    )
# Core fields.
    title = models.CharField(max_length=250,
                             help_text="Maximum 250 characters.")
    excerpt = models.TextField(blank=True,
                               help_text="A short summary of the entry. Optional.")
    body = models.TextField()
    pub_date = models.DateTimeField(default=datetime.datetime.now)

    # Fields to store generated HTML.
    excerpt_html = models.TextField(editable=False, blank=True)
    body_html = models.TextField(editable=False, blank=True)

    # Metadata.
    author = models.ForeignKey(User)
    enable_comments = models.BooleanField(True)
    featured = models.BooleanField(default=False)
    slug = models.SlugField(unique_for_date='pub_date',
                            help_text="Suggested value automatically generated image
                            from title. Must be unique.")
    status = models.IntegerField(choices=STATUS_CHOICES, default=LIVE_STATUS,
                                 help_text="Only entries with live status image
                                 will be publicly displayed.")

    # Categorization.
    categories = models.ManyToManyField(Category)
    tags = TagField(help_text="Separate tags with spaces.")

    class Meta:
        ordering = ['-pub_date']
        verbose_name_plural = "Entries"

    def __unicode__(self):
        return self.title

    def save(self, force_insert=False, force_update=False):
        self.body_html = markdown(self.body)
        if self.excerpt:
            self.excerpt_html = markdown(self.excerpt)
        super(Entry, self).save(force_insert, force_update)

    def get_absolute_url(self):
        return "/weblog/%s/%s/" % (self.pub_date.strftime("%Y/%b/%d").lower(),
                                   self.slug)

Go ahead and run manage.py syncdb in the project directory. It'll add the new Entry model's table (and the join table for its many-to-many relationship to the Category model), plus a couple of tables for models from the tagging application you're using. Next, use the administrative interface to add a couple of test entries to the weblog; you're about to start writing views for them, so you'll need some entries to work with.

Writing the First Views

Open the views.py file you created inside the coltrane directory and add a couple of import statements at the top to include things that you'll need for these views:

from django.shortcuts import render_to_response
from coltrane.models import Entry

The first line you've seen already: render_to_response() is the shortcuts function that handles loading and rendering a template, as well as returning an HttpResponse. The second line imports the Entry model you just created, so you'll be able to retrieve entries from the database for display.

For your first view, start with a simple index that displays all of the “live” entries. Here's the code:

def entries_index(request):
    return render_to_response('coltrane/entry_index.html',
                              { 'entry_list': Entry.objects.all() })

Next create a coltrane directory in your templates directory (the directory you set up for the cms project's templates), and place an entry_index.html file in it. Add the following HTML to the file:

<html>
    <head>
        <title>Entries index</title>
    </head>
    <body>
        <h1>Entries index</h1>
        {% for entry in entry_list %}
            <h2>{{ entry.title }}</h2>
            <p>Published on {{ entry.pub_date|date:"F j, Y" }}</p>
            {% if entry.excerpt_html %}
              {{ entry.excerpt_html|safe }}
            {% else %}
              {{ entry.body_html|truncatewords_html:"50"|safe }}
            {% endif %}
            <p><a href="{{ entry.get_absolute_url }}">Read full entry</a></p>
        {% endfor %}
    </body>
</html>

Note that you're using a filter to show the excerpt here. You'll remember that Django's template system automatically “escapes” the contents of variables to prevent cross-site scripting attacks. While you want to have that protection most of the time, you know that the contents of these variables are safe because they come from data that was entered into the admin interface by a trusted user. The safe filter lets you tell Django that you trust a particular variable and that it doesn't need any escaping.

Finally, you'll need to set up a URL. Open the urls.py file in the cms directory and, in the list of URL patterns, add the following pattern before the catch-all pattern for the flat pages:

(r'^weblog/$', 'coltrane.views.entries_index'),

At that point, you should be able to visit http://127.0.0.1:8000/weblog/. You'll see all the entries you've created so far, displayed using the template you just created. There are a few things worth noting about the template:

  • You're using a new filter: date. It's the first one you've seen that takes an argument, in this case a formatting string describing how to present a date. The syntax for this is similar to the syntax for the strftime() method, except that it doesn't use percent signs to mark formatting characters. “October 10, 2007” is an example of a result produced by this formatting string.
  • You're using the if tag to test whether there's an excerpt on each entry. If there is, then it's displayed. If there isn't, then the first 50 words of the entry's body will be displayed.
  • When there is no excerpt, the entry's body is cut off via the truncatewords_html filter. This filter's argument tells it how many words to allow. When the limit has been reached, the filter ends the text fragment with ellipses (. . .), indicating to the reader that there's more text in the full entry. As the name implies, the truncatewords_html filter knows how to recognize HTML tags and doesn't count them as words. It also will keep track of open tags and close them if it cuts off the text before a closing tag. (A separate filter, truncatewords, simply cuts off at the specified number of words and pays no attention to HTML.)

Displaying an index of all the entries is a nice first step, but it's only the beginning. You'll also need to be able to display individual entries, and you'll need to query for them based on information you can read from the URL. In this case, the get_absolute_url() method on the Entry model will give a URL that contains the (formatted) pub_date and the slug of the entry. Before you write the view that retrieves the entry, let's take a look at the URL pattern for it. This gives a clue to how you'll get that information out of the URL:

(r'^weblog/(?P<year>d{4})/(?P<month>w{3})/(?P<day>d{2})/(P?<slug>[-w]+)/$',
                      'coltrane.views.entry_detail'),

This is quite a bit more complicated than the URL patterns you've seen so far. The regular expression is looking for several things and includes the strange ?P construct several times. So let's walk through it step by step.

First of all, in Python's regular-expression syntax, a set of parentheses whose contents begin with ?P, followed by a name in brackets and a pattern, matches a “named group.” That is, any text that matches one of these parts of the URL will go into a dictionary, where the keys are the bracketed names and the values are the parts of the text that matched. So this URL is looking for four named groups: year, month, day, and slug.

The actual patterns used in these named groups are fairly simple once that hurdle is cleared:

  • The d{4} for year will match four consecutive digits.
  • The w{3} for month will match three consecutive letters: the %b formatter you used in the get_absolute_url() method will return the month as a three-letter string like “oct” or “jun.”
  • The d{2} for day will match two consecutive digits.
  • The [-w]+ for slug is somewhat tricky. It will match any sequence of consecutive characters where each character is either a letter, a number, or a hyphen. This is precisely the same set of characters Django allows in a SlugField.

When a URL matches this pattern, Django will pass the named groups to the specified view function as keyword arguments. This means the entry_detail view will receive keyword arguments called year, month, day, and slug, which will make the process of looking up the entry much simpler. Let's look at how that works by writing the entry_detail view:

def entry_detail(request, year, month, day, slug):
    import datetime, time
    date_stamp = time.strptime(year+month+day, "%Y%b%d")
    pub_date = datetime.date(*date_stamp[:3])
    return render_to_response('coltrane/entry_detail.html',
                              { 'entry': Entry.objects.get(pub_date__year=image
                                             pub_date.year,
                                             pub_date__month=pub_date.month,
                                             pub_date__day=pub_date.day,
                                                           slug=slug) })

The only complex bit here is parsing the date. First you use the strptime function in Python's standard time module. This function takes a string representing a date or time, as well as a format string like the one passed to strftime(), and parses the result into a time tuple. All you need to do, then, is concatenate the year, month, and day together and supply the same format string used in the get_absolute_url() method. Then you can pass the first three items of that result into datetime.date to get a date object.

Finally, you return a response where the template context will be the entry. The entry is retrieved via the lookup arguments, which look for entries matching the year, month, day, and slug from the URL.

Because you used unique_for_date on the slug field, this combination is enough to uniquely identify any entry in the database. The get method you're using here is also new. filter returns a QuerySet representing the set of all objects that match the query, but get tries to return one, and only one, object. (If no objects match your query, or if more than one object matches, it will raise an exception.)

Go ahead and create the template coltrane/entry_detail.html and fill it in any way you'd like. Then add the new URL pattern to the project's urls.py file if you haven't already, reload the entries index page in your browser, and click the link to one of them to see the new view in action.

The view isn't perfect, though. If you try a properly formatted URL for a nonexistent entry (say, /weblog/1946/sep/12/no-entry-here/), you'll get an error message and a traceback. The exception is Entry.DoesNotExist, which is Django's way of telling you that there wasn't an entry matching your criteria. It would be nice to return an HTTP 404 “Page Not Found” error in this case. You could do that manually by wrapping the query in a try block, catching the DoesNotExist exception, and then returning an appropriate response. But that would be repetitive work. Trying to retrieve something that may or may not exist, and returning a 404 if it doesn't, is something you need to do a lot in web development. So instead of doing it manually, you can use a helper function Django provides for this exact purpose: get_object_or_404(). First, change the import statement at the top of views.py to this:

from django.shortcuts import get_object_or_404, render_to_response

Then you can rewrite the view like this:

def entry_detail(request, year, month, day, slug):
    import datetime, time
    date_stamp = time.strptime(year+month+day, "%Y%b%d")
    pub_date = datetime.date(*date_stamp[:3])
    entry = get_object_or_404(Entry, pub_date__year=pub_date.year,
                                     pub_date__month=pub_date.month,
                                     pub_date__day=pub_date.day,
                                     slug=slug)
    return render_to_response('coltrane/entry_detail.html',
                              { 'entry': entry })

The get_object_or_404() shortcut will use the same get() lookup you just tried, but it will catch the DoesNotExist exception and re-raise the exception django.http.Http404. Django's HTTP-processing code recognizes this exception and will turn it into an HTTP 404 response.

Using Django's Generic Views

So far you've written only two views—an index of entries and a detail view for them—but already it looks like this could get tedious and boring. You're going to need views for the latest entries; for browsing them by day, month, and year; and for browsing them by categories and tags. And what's worse, a lot of it will be awfully repetitive: doing a query based on a date and returning one or more entries as a result. Wouldn't it be nice if you could avoid doing all that work by hand?

As it turns out, you can, by using Django's built-in generic views. There are several extremely common patterns of views that web applications need, regardless of the type of content they're presenting. So Django includes several sets of views, which are designed to work with any model and which take care of these common tasks. Broadly speaking, these tasks break down into four groups:

  • Performing simple redirects and just rendering a template based on a URL
  • Displaying lists of objects and individual objects
  • Creating date-based archives
  • Creating, retrieving, updating, and deleting (sometimes called CRUD) objects

The weblog will rely heavily on date-based archives, so I'll show you how that works. Go into the urls.py file and remove the pattern that routes to your entry_detail view. Replace it with this:

(r'^weblog/(?P<year>d{4})/(?P<month>w{3})/(?P<day>d{2})/(?<slug>[-w]+)/$,
 'django.views.generic.date_based.object_detail', entry_info_dict),

This makes use of a variable named entry_info_dict, which you haven't defined. So above the list of URL patterns (but below the import statements), define it like this:

entry_info_dict = {
    'queryset': Entry.objects.all(),
    'date_field': 'pub_date',
}

Now, make one change to the entry_detail.html template. Anywhere there's a reference to the variable entry (which your view was supplying), change it to object. You can also delete the entry_detail view you previously wrote because it's no longer needed. Next, go back and click through to an entry's URL in your browser. It will be retrieved properly from the database and displayed as specified in your template. URLs for nonexistent entries will return a 404, just as your entry_detail view did once you started using get_object_or_404().

How did Django do that? The answer is actually pretty simple. The generic view wants to receive a couple of arguments that tell it what it needs to do, and from there it can rely on the fact that the Django database API and template system work the same way in all situations.

The queryset argument is the key here because (as you'll remember from Chapter 3) many of Django's database-querying methods actually return a special type of object called a QuerySet, which can be further filtered and modified before it performs its actual query. In this case, you pass the generic view Entry.objects.all(), which is a QuerySet representing all the entries in the database. You also give it the argument date_field, which tells the generic view which field on the model represents the date you want to filter on. The remainder of the required arguments are all in the URL: year, month, day, and slug are received by the generic view the same way they were received by the entry_detail view, and it performs the same database query you were doing.

But because you can reuse the generic view with different sets of arguments, you can use it to create date-based archives for any model, meaning you don't have to write all the repetitive code over and over. (Particularly, you can reuse the generic view with a different value for the queryset argument and possibly date_field and/or slug_field—used if the model's slug field isn't named slug.) All you need to do is set up the right URL pattern and hand it the necessary set of arguments in a dictionary.

The date-based generic views all live in the module django.views.generic.date_based. There are seven of them, but you'll need to use only five for your weblog functionality:

  • object_detail: Provides a view of an individual object (as you've already seen).
  • archive_day: Provides a view of all the objects on a given day.
  • archive_month: Provides a view of all the objects in a given month.
  • archive_year: Provides a list of all the months that have objects in them in a given year, and optionally, a full list of all the objects in that year. (This is optional because it might be an extremely large list.)
  • archive_index: Provides a list of the latest objects.

So let's rewrite the urls.py file to use generic views for entries. It'll end up looking like the following code (but for simplicity's sake, I'm still using the cms project that's already been created):

from django.conf.urls.defaults import *
    from django.contrib import admin
admin.autodiscover()

from coltrane.models import Entry
entry_info_dict = {
    'queryset': Entry.objects.all(),
    'date_field': 'pub_date',
    }

    urlpatterns = patterns('',
         (r'^admin/', include(admin.site_urls)),
         (r'^search/$', 'cms.search.views.search'),
         (r'^weblog/$', 'django.views.generic.date_based.archive_index',
          entry_info_dict),
         (r'^weblog/(?P<year>d{4}/$',
          'django.views.generic.date_based.archive_year',
          entry_info_dict),
         (r'^weblog/(?P<year>d{4}/(?P<month>w{3})/$',
          'django.views.generic.date_based.archive_month',
          entry_info_dict),
         (r'^weblog/(?P<year>d{4}/(?P<month>w{3})/(?P<day>d{2})/$',
          'django.views.generic.date_based.archive_day',
          entry_info_dict),
         (r'^weblog/(?P<year>d{4}/(?P<month>w{3})/(?P<day>d{2})/image
(?P<slug>[-w]+)/$',
          'django.views.generic.date_based.object_detail',
          entry_info_dict),
         (r'', include('django.contrib.flatpages.urls')),
    )

You'll need to create templates for each view. All of the generic views accept an optional argument to specify the name of a custom template to use (the argument, appropriately enough, is called template_name), but by default they'll use the following:

  • archive_index will use coltrane/entry_archive.html.
  • archive_year will use coltrane/entry_archive_year.html.
  • archive_month will use coltrane/entry_archive_month.html.
  • archive_day will use coltrane/entry_archive_day.html.
  • object_detail will use coltrane/entry_detail.html.

The object_detail view, as you've already seen, makes the entry available in a variable named object. In the daily and monthly archive views, you'll get a list of entries as the variable object_list. In both cases, you can customize these views through an optional argument called template_object_name. The yearly archive will—as previously explained—default to simply giving you a list of months in which entries have been published. This will be the variable date_list in the template. The archive_index view will supply its template with a variable called latest, which will contain the latest entries (up to a maximum of 15). You can use the for tag in the appropriate templates (just as you did previously in your hand-rolled entry index) to loop through these lists.

The daily, monthly, and yearly archives also give the template an extra variable representing the date or date range they're working with: day, month, and year, respectively. As you've seen already in the templates for the entry views you wrote by hand, you can use the date template filter to format the dates displayed in your templates however you'd like.

ADMONITION: FILLING OUT THE ENTRY TEMPLATES

If you're interested in seeing a full set of (simple) example templates, check out the sample code for this book (downloadable from the Apress web site). Be aware that they do make use of some features that haven't been introduced yet, but you should be able to understand most of what's going on in them.

Decoupling the URLs

At this point, between the models you've defined, Django's administrative interface, and the date-based generic views, you've got a pretty good weblog application. But already there's a big problem—it's really not reusable because its URLs are “coupled” to the particular setup you've put together:

  • The set of URL patterns for the entries are sitting in the project's urls.py file, which means you would need to copy them into any other project that needs a weblog.
  • The URL patterns and the Entry and Category models' get_absolute_url() methods (though you haven't set up views for categories yet) are all hard-coded and assume a particular URL layout for the site. It's a fairly sensible layout, but some users might want a different setup (for example, /blog/ as the weblog root instead of /weblog/).

Let's fix that. First of all, you've already seen that Django offers the include() function for plugging in a set of URLs at a specific point in a project (as you've done with the administrative application). So let's create a reusable set of URLs that lives inside the weblog application. Go into its directory and create a file named urls.py, then copy the appropriate import statements and URL patterns into it:

from django.conf.urls.defaults import *

from coltrane.models import Entry
   entry_info_dict = {
       'queryset': Entry.objects.all(),
       'date_field': 'pub_date',
       }

   urlpatterns = patterns('',
         (r'^$', 'django.views.generic.date_based.archive_index', entry_info_dict),
         (r'^(?P<year>d{4}/$', 'django.views.generic.date_based.archive_year',
          entry_info_dict),
         (r'^(?P<year>d{4}/(?P<month>w{3})/$',
          'django.views.generic.date_based.archive_month',
          entry_info_dict),
         (r'^(?P<year>d{4}/(?P<month>w{3})/(?P<day>d{2})/$',
          'django.views.generic.date_based.archive_day',
          entry_info_dict),
         (r'^(?P<year>d{4}/(?P<month>w{3})/(?P<day>d{2})/image
(?P<slug>[-w]+)/$',
          'django.views.generic.date_based.object_detail',
          entry_info_dict),
    )

In the project's urls.py file, you can remove the import of the Entry model and the entry_info_dict variable, as well as the URL patterns for the entries (the ones starting with ^weblog/). You can replace them all with one URL pattern:

(r'^weblog/', include('coltrane.urls')),

Notice that the URLConf module inside the weblog application doesn't include the weblog/ prefix on any of its URL patterns. It's relying on the project to decide where to put this set of URLs.

You can also cut down on some repetitive typing here: all the views used in the weblog's URLConf start with django.views.generic.date_based, which isn't fun to type out over and over again. Meanwhile, there's a conspicuous empty string as the first thing in the list. That empty string isn't a URL. It's a special parameter that lets you specify a view prefix, in case all the view functions have identical module paths. Let's take advantage of that:

urlpatterns = patterns('django.views.generic.date_based',
     (r'^$', 'archive_index', entry_info_dict).
     (r'^(?P<year>d{4}/$', 'archive_year', entry_info_dict),
     (r'^(?P<year>d{4}/(?P<month>w{3})/$', 'archive_month', entry_info_dict),
     (r'^(?P<year>d{4}/(?P<month>w{3})/(?P<day>d{2})/$',
      'archive_day',
      entry_info_dict),
     (r'^(?P<year>d{4}/(?P<month>w{3})/(?P<day>d{2})/(?P<slug>[-w]+)/$',
      'object_detail',
      entry_info_dict),
)

Now Django will automatically prepend django.views.generic.date_based to all of these view function names before it tries to load them, which is much nicer.

Now you need to deal with the problem of the get_absolute_url() methods. On the Entry model, get_absolute_url() returns a URL with /weblog/ hard-coded into it, and that's no good. Somebody might plug these URLs into a different part of their site's URL layout. The solution is a pair of features in Django: one lets you give names to your URL patterns, and the other lets you specify that a function like get_absolute_url() should actually return a value by looking for URL patterns with particular names.

First, you need to make one more change to the weblog URLConf:

urlpatterns = patterns('django.views.generic.date_based',
         (r'^$', 'archive_index', entry_info_dict, 'coltrane_entry_archive_index'),
         (r'^(?P<year>d{4}/$', 'archive_year', entry_info_dict, image
'coltrane_entry_archive_year'),
         (r'^(?P<year>d{4}/(?P<month>w{3})/$', 'archive_month', entry_info_dict, image
'coltrane_entry_archive_month'),
         (r'^(?P<year>d{4}/(?P<month>w{3})/(?P<day>d{2}/)$', 'archive_day', image
entry_info_dict, 'coltrane_entry_archive_day'),
         (r'^(?P<year>d{4}/(?P<month>w{3})/(?P<day>d{2})/(?P<slug>[-w]+)/$', image
'object_detail', entry_info_dict, 'coltrane_entry_detail'),
    )

You've added a name to each one of these URL patterns. The names are made up of your application's name (to avoid name collisions with URL patterns in other applications) and a description of what the view is for.

Now you can rewrite the get_absolute_url() method on the Entry model:

def get_absolute_url(self):
    return ('coltrane_entry_detail', (), { 'year': self.pub_date.strftime("%Y"),
                                           'month': self.pub_date. image
                                           strftime("%b").lower(),
                                           'day': self.pub_date.strftime("%d"),
                                           'slug': self.slug })
get_absolute_url = models.permalink(get_absolute_url)

The get_absolute_url() method now returns a tuple, whose elements are as follows:

  • The name of the URL pattern you want to use
  • A tuple of any positional arguments to be included in the URL (in this case, there aren't any)
  • A dictionary of any keyword arguments to be included in the URL

The last line is a new concept: a decorator. Decorators are special functions that do nothing on their own but can be used to change the behavior of other functions. The permalink decorator you're using here (which lives in django.db.models) will actually rewrite the get_absolute_url() function to do a reverse URL lookup. It will scan the project's URLConf to look for the URL pattern with the specified name, then use that pattern's regular expression to create the correct URL string and fill in the proper values for any arguments that need to be embedded in the URL.

Based on the URLConf you've set up for this project, the permalink decorator will find the /weblog/ prefix and follow the include() to coltrane.urls, where it will find the pattern named coltrane_entry_detail and fill in the regular expression with the correct values. For an entry published on October 10, 2007, with the slug test-entry, this process will generate the URL /weblog/2007/oct/10/test-entry/. If you changed the root URLConf to include the weblog URLs under blogs/ instead, you'd generate /blogs/2007/oct/10/test-entry/.

And now you've completely decoupled the entry URLs from the project and from any assumptions about particular site URL layouts. These URLs can be plugged into any project at any point in its URL hierarchy, and between include() and the permalink() decorator, the generated URLs will always be correct.

Looking Ahead

Once again, you've accomplished a lot without writing much actual code. The biggest hurdle in the weblog application so far has simply been getting a handle on the layout of a first “real” Django application and all of the assorted options Django provides to cut down on tedious and repetitive code. And it is flexible enough to be reused in any project where you need a blog.

At this point, you've got a large number of Django's most important concepts under your belt—the basic model/URL/view/template architecture, the syntax of each component, and the general principles of decoupling and code reuse (sometimes called DRY, short for “Don't Repeat Yourself,” a software-development guideline that says whenever possible you should have one, and only one, authoritative version of a piece of data or functionality). You might want to pause here and review what you've written so far because you're going to start picking up the pace and writing code much more quickly. Once you feel comfortable with the concepts and features introduced up to this point, move on to the next chapter. There you'll finish up the weblog models by writing the Link class, and then fill in the rest of the basic views. After that, you'll delve a bit deeper into Django's templating system and some more advanced features.

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

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