January 8, 2011

Provide unique slugs for model class

Sorry my dear readers for this long break between posts, but I have had lots of work lately finishing previous jobs. Recently I've shown you how to synchronize your app with Twitter and also parse list of tweets changing hash tags to proper links. Today I wanted to show you how to synchronize application with youtube but for this purpose I need a way to provide unique slugs for models. So here's the whole code which I'll comment below :

import re

from django.template.defaultfilters import slugify

def try_slug(instance, slug):
    """Check if the slug is free for corresponding model instance
    queryset = instance.__class__._default_manager.all()
    if not queryset.filter(**{"slug": slug}):
        return True
        return False

def unique_slugify(instance, value, slug_field_name='slug', queryset=None, slug_separator='-'):
    """Create unique slug
    Creates unique slug across model class.
    slug_field = instance._meta.get_field(slug_field_name)
    slug_len = slug_field.max_length

    slug = slugify(value)
    slug = slug[:slug_len]
    slug = _slug_strip(slug, slug_separator)
    original_slug = slug
    if queryset is None:
        queryset = instance.__class__._default_manager.all()
    if instance.pk:
        queryset = queryset.exclude(pk=instance.pk)
    next = 2
    while not slug or queryset.filter(**{slug_field_name: slug}):
        slug = original_slug
        end = '%s%s' % (slug_separator, next)
        if slug_len and len(slug) + len(end) > slug_len:
            slug = slug[:slug_len-len(end)]
            slug = _slug_strip(slug, slug_separator)
        slug = '%s%s' % (slug, end)
        next += 1
    return slug

def _slug_strip(value, separator='-'):
    Cleans up a slug by removing slug separator characters that occur at the
    beginning or end of a slug.
    separator = separator or ''
    if separator == '-' or not separator:
        re_sep = '-'
        re_sep = '(?:-|%s)' % re.escape(separator)

    if separator != re_sep:
        value = re.sub('%s+' % re_sep, separator, value)

    if separator:
        if separator != '-':
            re_sep = re.escape(separator)
        value = re.sub(r'^%s+|%s+$' % (re_sep, re_sep), '', value)
    return value

So now some explanations to the code. First function gives us possibility to check if slug is free for specific model class. It takes object instance and slug and returns boolean value. Inside the code we take default 'all' manager of the model and filter queryset with slug.
Second function is the key to solving our problem. The unique_slugify function takes shown parameters. At first we check the max length of the slug field, so that we won't exceed its value. Then we slugify our slug with built in django function, cut it if needed and strip of sepparators different than '-' or 'separator' parameter received by function. Next step is to get the queryset on which we will be working, excluding our model instance. Finally the fun begins. In our while loop condition we check existence of newly created slug or result of filtering our queryset. If both are none we take the separator and add next integer to it.
If the length of concatenated slug and identifier is too long, we cut it off. Finally we're creating slug that will be used in while loop condition filter.
Last function is stripping our slug by separators appearing at the beginning and end of slug. Because it uses many regexes I will leave in your hands work of decyphering it :)

And now for some short sample usage :

free = try_slug(self.instance, self.cleaned_data['slug'])
if not free:
    self.cleaned_data['slug'] = unique_slugify(self.instance, self.cleaned_data['slug'])

No comments:

Post a Comment