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
- Template tag - list punctuation for a list of items by shapiromatron 10 months, 1 week ago
- JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 10 months, 2 weeks ago
- Serializer factory with Django Rest Framework by julio 1 year, 5 months ago
- Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
- Help text hyperlinks by sa2812 1 year, 6 months ago
Comments
Problem is that Python slugify is inferior to JavaScript slugify.
#
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.
#
Found two problems with this code in the contribute_to_class method :
#
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.
#
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?
#
@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.
#
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.
#
I can't seem to get this to work on trunk (r9791) with either MySql or Postgresql, can anyone confirm it is working?
Mat
#
MYSQL USERS! -- Mysql reports errors slightly differently, so use code something like this where the IntegrityError gets caught:
#
Please login first before commenting.