The Slug that flew South

The great thing about Django is that it allows you to write your web apps "code first", i.e. you define your model classes and have your database and all operations on them generated automatically.

class Tag(models.Model):

    name = models.CharField(max_length=30)

The above will result in an entire table (named "blogapp_tag" in my case) with two columns - a primary key column and a `name` column of type varchar(30) NOT NULL. But you don't even need to know this; you can just instantiate objects of that class, fill out their `name` field and call their `save()` method. It will all just work.

That's why I was surprised when I couldn't just add another field and run `manage.py syncdb`. Thankfully, this is taken care of by South.

. venv/bin/activate
pip install South
pip freeze > requirements.txt

You should also add it to your `INSTALLED_APPS` in settings.py. If you're like me and found out too late about it, the documentation says how to convert an existing app.

Now we can go back to implementing slugs. I change my `Post` model to contain the following:

from django.db import models
from django.template.defaultfilters import slugify
from django.core.urlresolvers import reverse

class Post(models.Model):

    title = models.CharField(max_length=100)
    slug = models.SlugField(max_length=100,unique=True,
            blank=True,editable=False)

    def get_absolute_url(self):
        return reverse('blog:detail', kwargs={'slug': self.slug})

    def save(self, *args, **kwargs):
        self.slug = slugify(self.title)
        super(Post, self).save(*args, **kwargs)

`Post.title` already existed, everything else is new. The `get_absolute_url` method is very useful as it allows many other parts of the site to automatically find the URL of your `Post` objects.

We now run:

python manage.py schemamigration blogapp --auto

This creates a file under blogapp/migrations ("blogapp" is the name of my app) named "0002_auto__add_field_post_slug.py". We will need to edit this file - we can't use Python expressions to fill our new column, but we have defined it to be UNIQUE-constrained - the migration will fail with any possible default value. The solution is the following: the migration file contains two methods, `forwards` and `backwards` - I changed the `forwards` method to make the column non-unique initially:

def forwards(self, orm):
    # Adding field 'Post.slug'
    db.add_column('blogapp_post','slug',
        self.gf('django.db.models.fields.SlugField')(default='',
            unique=False,max_length=100),
        keep_default=False)

...and then use the fake ORM to update our objects, save them and set the UNIQUE constraint:

from django.template.defaultfilters import slugify

posts = orm['blogapp.Post'].objects.all()
for post in posts:
    post.slug = slugify(post.title)
    print post.slug, post.title
    post.save()

db.create_unique(u'blogapp_post', ['slug'])

The `orm` gets passed in by South. This is not the Django ORM, but is made to look and feel as much as possible like it. We just `slugify` all titles, print the results to the screen, save the post and finally, when we're done, add the UNIQUE constraint. The fake ORM keeps your models under '<appname>.<modelname>`, where <modelname> is the name of your class in models.py. The `db.create_unique` call, however, requires the table name as it appears in your database.

blog comments powered by Disqus