Login

YAAS (Yet Another Auto Slug)

Author:
carljm
Posted:
May 22, 2008
Language:
Python
Version:
.96
Score:
1 (after 1 ratings)

This is the self-populating AutoSlugField I use. It's not the first such snippet, but (IMO) it works a bit more cleanly. It numbers duplicate slugs (to avoid IntegrityErrors on a unique slug field) using an "ask-forgiveness-not-permission" model, which avoids extra queries at each save. And it's simply a custom field, which means adding it to a model is one line.

Usage:

class MyModel(models.Model):
    name = models.CharField(max_length=50)
    slug = AutoSlugField(populate_from='name')
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
from django.db import IntegrityError
from django.template.defaultfilters import slugify

class AutoSlugField (SlugField):
    """
    A SlugField that automatically populates itself at save-time from
    the value of another field.

    Accepts argument populate_from, which should be the name of a single
    field which the AutoSlugField will populate from (default = 'name').

    By default, also sets unique=True, db_index=True, and
    editable=False.

    Accepts additional argument, overwrite_on_save.  If True, will
    re-populate on every save, overwriting any existing value.  If
    False, will not touch existing value and will only populate if
    slug field is empty.  Default is False.

    """
    def __init__ (self, populate_from='name', overwrite_on_save=False,
                  *args, **kwargs):
        kwargs.setdefault('unique', True)
        kwargs.setdefault('db_index', True)
        kwargs.setdefault('editable', False)
        self._save_populate = populate_from
        self._overwrite_on_save = overwrite_on_save
        super(AutoSlugField, self).__init__(*args, **kwargs)

    def _populate_slug(self, model_instance):
        value = getattr(model_instance, self.attname, None)
        prepop = getattr(model_instance, self._save_populate, None)
        if (prepop is not None) and (not value or self._overwrite_on_save):
            value = slugify(prepop)
            setattr(model_instance, self.attname, value)
        return value

    def contribute_to_class (self, cls, name):
        # apparently in inheritance cases, contribute_to_class is called more
        #  than once, so we have to be careful not to overwrite the original
        #  save method.
        if not hasattr(cls, '_orig_save'):
            cls._orig_save = cls.save
            def _new_save (self_, *args, **kwargs):
                counter = 1
                orig_slug = self._populate_slug(self_)
                slug_len = len(orig_slug)
                if slug_len > self.max_length:
                    orig_slug = orig_slug[:self.max_length]
                    slug_len = self.max_length
                setattr(self_, name, orig_slug)
                while True:
                    try:
                        self_._orig_save(*args, **kwargs)
                        break
                    except IntegrityError, e:
                        # check to be sure a slug fight caused the IntegrityError
                        s_e = str(e)
                        if name in s_e and 'unique' in s_e:
                            counter += 1
                            max_len = self.max_length - (len(str(counter)) + 1)
                            if slug_len > max_len:
                                orig_slug = orig_slug[:max_len]
                            setattr(self_, name, "%s-%s" % (orig_slug, counter))
                        else:
                            raise
            cls.save = _new_save
        super(AutoSlugField, self).contribute_to_class(cls, name)

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 10 months, 2 weeks ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 10 months, 3 weeks ago
  3. Serializer factory with Django Rest Framework by julio 1 year, 5 months ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
  5. Help text hyperlinks by sa2812 1 year, 7 months ago

Comments

jokull (on May 27, 2008):

Problem is that Python slugify is inferior to JavaScript slugify.

#

carljm (on May 28, 2008):

Maybe, but I've had problems with the Javascript slugify not being very reliable. For instance, it breaks if the field is completed by the Firefox auto-completer dropdown. In any case, this snippet plays nicely with the Javascript slugify if you like, as it can be set to not overwrite an existing value for the slug field.

#

uandt (on June 23, 2008):

Found two problems with this code in the contribute_to_class method :

  • self_._orig_save needs to accept args and kwargs
  • The IntegrityError check works well using sqlite but raises errors on mySQL

#

carljm (on July 18, 2008):

uandt: Thanks for the feedback. I discovered the IntegrityError issue on MySQL and it's now fixed in the code (by using str(e) instead of e.message).

Good catch on needing to pass args and *kwargs through to _orig_save, that's now fixed as well.

#

dimitri-gnidash (on September 3, 2008):

There is an infrequent problem with this code. When a method that is added through contribute_to_class is also overridden on the model, the method added through contribute_to_class is not called. I am guessing this is because the overridden method is evaluated later?

Does anyone know of a solution?

#

carljm (on September 29, 2008):

@dimitri-gnidash: I can't duplicate the problem you describe. In fact, my test suite covering this code contains a model with an AutoSlugField and an overridden save() method, and the two work together fine. The only potential gotcha is that the AutoSlugField's value won't reflect any change to the populate_from attribute in the overridden save() method; the save() method is called, but the slugification happens first.

Fixing this appears more than a bit difficult, which is why I haven't attempted it yet.

#

carljm (on September 29, 2008):

As of today, I updated the code above to fix another issue. In cases of model inheritance where the superclass has an AutoSlugField, it seems that contribute_to_class is actually called more than once on the subclass, so it needs to be idempotent, otherwise the original save method will be lost and there will be an infinite loop of the new save method calling itself. This is now fixed.

#

msc41 (on January 26, 2009):

I can't seem to get this to work on trunk (r9791) with either MySql or Postgresql, can anyone confirm it is working?

Mat

#

showell (on May 6, 2009):

MYSQL USERS! -- Mysql reports errors slightly differently, so use code something like this where the IntegrityError gets caught:

(orig_slug in s_e and 'Duplicate entry' in s_e)

#

Please login first before commenting.