Login

AutoSlugField and unique_slugify combined

Author:
Ciantic
Posted:
January 20, 2010
Language:
Python
Version:
1.1
Score:
1 (after 1 ratings)

Extra field for slugs that does the work.

If the slug value is given, the value is not recreated but correctness is ensured. If value is not given, the field regenerates the slug.

  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
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
import re
from django.db.models import fields
from django.template.defaultfilters import slugify

# -- Authors -----
# Combined from following two snippets:
# crucialfelix (_unique_slugify) http://www.djangosnippets.org/snippets/1321/
# GaretJax (AutoSlugField) http://www.djangosnippets.org/snippets/728/ 
# 
# Improved from those by Ciantic, 2010.

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

    :param 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).

    :param 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)
    
    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 queryset is None:
        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)

class AutoSlugField(fields.SlugField):
    """Auto slug field, creates unique slug for model."""
    
    def __init__(self, prepopulate_from, *args, **kwargs):
        """Create auto slug field.
        
        If field is unique, the uniqueness of the slug is ensured from existing
        slugs by adding extra number at the end of slug.
        
        If field has slug given, it is used instead. If you want to re-generate
        the slug, just set it :const:`None` or :const:`""` so it will be re-
        generated automatically.
        
        :param prepopulate_from: Must be assigned to list of field names which
            are used to prepopulate automatically. 
        
        :type prepopulate_from: sequence
        
        """
        self.prepopulate_separator = kwargs.get("prepopulate_separator", u"-")
        self.prepopulate_from = prepopulate_from
        kwargs["blank"] = True
        super(fields.SlugField, self).__init__(*args, **kwargs)
    
    def pre_save(self, model_instance, add): #@UnusedVariable
        """Pre-save event"""
        current_slug = getattr(model_instance, self.attname)
        
        # Use current slug instead, if it is given.
        # Assumption: There are no empty slugs.
        if not (current_slug is None or current_slug == ""):
            slug = current_slug
        else:
            slug = self.prepopulate_separator.\
                        join(unicode(getattr(model_instance, prepop)) 
                             for prepop in self.prepopulate_from)
        
        if self.unique:
            return _unique_slugify(model_instance, value=slug, 
                                   slug_field_name=self.attname)
        else:
            return slugify(slug)[:self.max_length]

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, 2 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, 6 months ago

Comments

eternicode (on October 13, 2010):

Haven't tested this yet, but one note:

super(fields.SlugField, self).__init__(*args, **kwargs)

Should call super of AutoSlugField:

super(AutoSlugField, self).__init__(*args, **kwargs)

#

ptchaw (on February 7, 2012):

There's another bug here: the attribute for the field isn't set for the last condition:

return slugify(slug)[:self.max_length]

Changing it to:

slug = slugify(slug)[:self.max_length]
setattr(model_instance, self.attname, slug)
return slug

Will ensure it is set

#

Please login first before commenting.