unique_slugify

 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
import re

from django.template.defaultfilters import slugify


def unique_slugify(instance, value, slug_field_name='slug', queryset=None,
                   slug_separator='-'):
    """
    Calculates a unique slug of ``value`` for an instance.

    ``slug_field_name`` should be a string matching the name of the field to
    store the slug in (and the field to check against for uniqueness).

    ``queryset`` usually doesn't need to be explicitly provided - it'll default
    to using the ``.all()`` queryset from the model's default manager.
    """
    slug_field = instance._meta.get_field(slug_field_name)
    
    current_slug = getattr(instance, slug_field.attname)
    slug_len = slug_field.max_length

    # Sort out the initial slug. Chop its length down if we need to.
    slug = slugify(value)
    if slug_len:
        slug = slug[:slug_len]
    slug = _slug_strip(slug, slug_separator)
    original_slug = slug

    # Create a queryset, excluding the current instance.
    if not queryset:
        queryset = instance.__class__._default_manager.all()
        if instance.pk:
            queryset = queryset.exclude(pk=instance.pk)

    # Find a unique slug. If one matches, at '-2' to the end and try again
    # (then '-3', etc).
    next = 2
    while not slug or queryset.filter(**{slug_field_name: slug}):
        slug = original_slug
        end = '-%s' % 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

    setattr(instance, slug_field.attname, slug)
    return slug

def _slug_strip(value, separator=None):
    """
    Cleans up a slug by removing slug separator characters that occur at the
    beginning or end of a slug.

    If an alternate separator is used, it will also replace any instances of
    the default '-' separator with the new separator.
    """
    if separator == '-' or not separator:
        re_sep = '-'
    else:
        re_sep = '(?:-|%s)' % re.escape(separator)
        value = re.sub('%s+' % re_sep, separator, value)
    return re.sub(r'^%s+|%s+$' % (re_sep, re_sep), '', value)

More like this

  1. Using the built-in slugify filter outside a template by jcroft 6 years, 2 months ago
  2. AutoSlugField and unique_slugify combined by Ciantic 3 years, 3 months ago
  3. Cache any function (with its arguments) by bkroeze 6 years ago
  4. YAAS (Yet Another Auto Slug) by carljm 4 years, 12 months ago
  5. Validation for 'unique' and 'unique_together' constraints (different version) by miracle2k 5 years, 10 months ago

Comments

seanos (on March 28, 2009):

Thanks for this.

One bug I found:

line 29 should be changed to:

if queryset is None:

otherwise if the argument queryset is empty, it will be re-populated with all rows from the model, leading to slug being modified when it should of been left alone.

#

Ciantic (on January 20, 2010):

I opened ticket #12651 to Django for this, IMO there are way too many auto/unique slug things in djangosnippets for example that one at least should be part of Django.

If you'd like to see unique/auto slug thing for Django, I suggest you drop your opinions there.

This problem has been haunting in Django for ages, people forget it because they come up their own solutions and don't bother to fix Django instead.

#

Ciantic (on January 20, 2010):

Hi!

I decided to combine yours and one other snippet.

#

(Forgotten your password?)