[snowy] Import autoslug, start using it



commit 1b9c82648d61e17bd5e4d08bd2b17931e3ab4cfe
Author: Brad Taylor <brad getcoded net>
Date:   Mon May 18 16:34:48 2009 -0400

    Import autoslug, start using it
---
 lib/autoslug/__init__.py |   15 ++++
 lib/autoslug/fields.py   |  163 ++++++++++++++++++++++++++++++++++++++++++++++
 lib/autoslug/settings.py |   33 +++++++++
 notes/models.py          |    5 +-
 settings.py              |    1 +
 5 files changed, 216 insertions(+), 1 deletions(-)

diff --git a/lib/autoslug/__init__.py b/lib/autoslug/__init__.py
new file mode 100644
index 0000000..e10fbaf
--- /dev/null
+++ b/lib/autoslug/__init__.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+#
+#  Copyright (c) 2008â??2009 Andy Mikhailenko
+#
+#  This file is part of django-autoslug.
+#
+#  django-autoslug is free software under terms of the GNU Lesser
+#  General Public License version 3 (LGPLv3) as published by the Free
+#  Software Foundation. See the file README for copying conditions.
+#
+
+__author__  = 'Andy Mikhailenko'
+__license__ = 'GNU Lesser General Public License (GPL), Version 3'
+__url__     = 'http://bitbucket.org/neithere/django-autoslug/'
+__version__ = '1.0.1'
diff --git a/lib/autoslug/fields.py b/lib/autoslug/fields.py
new file mode 100644
index 0000000..8771431
--- /dev/null
+++ b/lib/autoslug/fields.py
@@ -0,0 +1,163 @@
+# -*- coding: utf-8 -*-
+#
+#  Copyright (c) 2008â??2009 Andy Mikhailenko
+#
+#  This file is part of django-autoslug.
+#
+#  django-autoslug is free software under terms of the GNU Lesser
+#  General Public License version 3 (LGPLv3) as published by the Free
+#  Software Foundation. See the file README for copying conditions.
+#
+
+# django
+from django.db.models.fields import DateField, SlugField
+
+# app
+from autoslug.settings import slugify
+
+class AutoSlugField(SlugField):
+    """
+    AutoSlugField is a slug field that can automatically do the following on save:
+    - populate itself from another field (using 'populate_from'),
+    - use custom slugify() function (can be defined in settings), and
+    - preserve uniqueness of the value (using 'unique' or 'unique_with').
+    
+    None of the tasks is mandatory, i.e. you can have auto-populated non-unique fields,
+    manually entered unique ones (absolutely unique or within a given date) or both.
+
+    Uniqueness is preserved by checking if the slug is unique with given constraints
+    (unique_with) or globally (unique) and adding a number to the slug to make
+    it unique. See examples below.
+    
+    IMPORTANT: always declare attributes with AutoSlugField *after* attributes
+    from which you wish to 'populate_from' or check 'unique_with' because autosaved
+    dates and other such fields must be already processed before checking occurs.
+    
+    Usage examples:
+    
+    # slugify but allow non-unique slugs
+    slug = AutoSlugField()
+
+    # globally unique, silently fix on conflict ("foo" --> "foo-1".."foo-n")
+    slug = AutoSlugField(unique=True)
+    
+    # autoslugify value from title attr; default editable to False
+    slug = AutoSlugField(populate_from='title')
+    
+    # same as above but force editable=True
+    slug = AutoSlugField(populate_from='title', editable=True)
+
+    # ensure that slug is unique with given date (not globally)
+    slug = AutoSlugField(unique_with='pub_date')
+    
+    # ensure that slug is unique with given date AND category
+    slug = AutoSlugField(unique_with=('pub_date','category'))
+    
+    # mix above-mentioned behaviour bits
+    slug = AutoSlugField(populate_from='title', unique_with='pub_date')
+
+    Please don't forget to declare your slug attribute after the fields
+    referenced in `populate_from` and `unique_with`.
+    """
+    def __init__(self, *args, **kwargs):
+        kwargs['max_length'] = kwargs.get('max_length', 50)
+
+        # autopopulated slug is not editable unless told so
+        self.populate_from = kwargs.pop('populate_from', None)
+        if self.populate_from:
+            kwargs.setdefault('editable', False)
+
+        # unique_with value can be string or tuple
+        self.unique_with = kwargs.pop('unique_with', ())
+        if isinstance(self.unique_with, basestring):
+            self.unique_with = (self.unique_with,)
+        
+        # backward compatibility
+        if kwargs.get('unique_with_date'):
+            from warnings import warn
+            warn('Using unique_with_date="foo" in AutoSlugField is deprecated, '\
+                 'use unique_with=("foo",) instead.', DeprecationWarning)
+            self.unique_with += (kwargs['unique_with_date'],)
+
+        # force full uniqueness unless more granular is specified
+        if not self.unique_with:
+            kwargs.setdefault('unique', True)
+
+        # Set db_index=True unless it's been set manually.
+        if 'db_index' not in kwargs:
+            kwargs['db_index'] = True
+        super(SlugField, self).__init__(*args, **kwargs)
+
+    def pre_save(self, instance, add):
+        # get currently entered slug
+        value = self.value_from_object(instance)
+        
+        # autopopulate (unless the field is editable and has some value)
+        # SNOWY: put self.populate_from before value so that slug is always updated
+        if self.populate_from: # and not self.editable:
+            slug = slugify(getattr(instance, self.populate_from))
+        elif value:
+            slug = slugify(value)
+
+        if not slug:
+            # no incoming value,  use model name
+            slug = instance._meta.module_name
+
+        assert slug, 'slug is defined before trying to ensure uniqueness'
+
+        # ensure the slug is unique (if required)
+        if self.unique or self.unique_with:
+            slug = self._generate_unique_slug(instance, slug)
+
+        assert slug, 'value is filled before saving'
+
+        setattr(instance, self.name, slug) # XXX do we need this?
+
+        return slug
+        #return super(AutoSlugField, self).pre_save(instance, add)
+
+    def _generate_unique_slug(self, instance, slug):
+        """
+        Generates unique slug by adding a number to given value until no model
+        is found with such slug. If unique_with (a tuple of field names) was
+        specified for the field, all these fields are included together
+        in the query when looking for a "rival" model instance.
+        """
+        def _get_lookups(instance):
+            "Returns a dict'able tuple of lookups to ensure slug uniqueness"
+            for field_name in self.unique_with:
+                field = instance._meta.get_field(field_name)
+                value = getattr(instance, field_name)
+                if not value:
+                    raise ValueError, 'Could not check uniqueness of %s.%s with'\
+                        ' respect to %s.%s because the latter is empty. Please'\
+                        ' ensure that "%s" is declared *after* all fields it'\
+                        ' depends on (i.e. "%s"), and that they are not blank.'\
+                        % (instance._meta.object_name, self.name,
+                           instance._meta.object_name, field_name,
+                           self.name, '", "'.join(self.unique_with))
+                if isinstance(field, DateField):    # DateTimeField is a DateField subclass
+                    for part in 'year', 'month', 'day':
+                        lookup = '%s__%s' % (field_name, part)
+                        yield (lookup, getattr(value, part))
+                else:
+                    yield (field_name, value)
+        lookups = tuple(_get_lookups(instance))
+        model = instance.__class__
+        field_name = self.name
+        index = 1
+        orig_slug = slug
+        # keep changing the slug until it is unique
+        while True:
+            try:
+                # raise model.DoesNotExist if current slug is unique
+                rival = model.objects.get(**dict(lookups + ((self.name, slug),) ))
+                # not unique, but maybe the "rival" is the instance itself?
+                if rival.id == instance.id:
+                    raise model.DoesNotExist
+                # the slug is not unique; change once more
+                index += 1
+                slug = '%s-%d' % (orig_slug, index)
+            except model.DoesNotExist:
+                # slug is unique, no model uses it
+                return slug
diff --git a/lib/autoslug/settings.py b/lib/autoslug/settings.py
new file mode 100644
index 0000000..fd8c6af
--- /dev/null
+++ b/lib/autoslug/settings.py
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+#
+#  Copyright (c) 2008â??2009 Andy Mikhailenko
+#
+#  This file is part of django-autoslug.
+#
+#  django-autoslug is free software under terms of the GNU Lesser
+#  General Public License version 3 (LGPLv3) as published by the Free
+#  Software Foundation. See the file README for copying conditions.
+#
+
+from django.conf import settings
+
+""" the AUTOSLUG_SLUGIFY_FUNCTION setting allows to define
+a custom slugifying function. Value can be a string or a callable.
+Default value is 'django.template.defaultfilters.slugify'.
+"""
+
+# use custom slugifying function if any 
+slugify = getattr(settings, 'AUTOSLUG_SLUGIFY_FUNCTION', None)
+
+if not slugify:
+    try:
+        # more i18n-friendly slugify function (supports Russian transliteration)
+        from pytils.translit import slugify
+    except ImportError:
+        # fall back to Django's default one
+        slugify = 'django.template.defaultfilters.slugify'
+
+# find callable by string
+if isinstance(slugify, str):
+    from django.core.urlresolvers import get_callable
+    slugify = get_callable(slugify)
diff --git a/notes/models.py b/notes/models.py
index 1d496e9..44d747a 100644
--- a/notes/models.py
+++ b/notes/models.py
@@ -19,6 +19,8 @@ from django.db import models
 from django.contrib.auth.models import User
 from django.db.models.signals import post_save
 
+from autoslug.fields import AutoSlugField
+
 class Note(models.Model):
     NOTE_PERMISSIONS = (
         (0, 'Private'), (1, 'Public'), 
@@ -33,7 +35,8 @@ class Note(models.Model):
     user_modified = models.DateTimeField(auto_now_add=True)
 
     title = models.CharField(max_length=128, blank=True)
-    slug = models.SlugField(blank=True)
+    slug = AutoSlugField(unique_with='author', populate_from='title',
+                         editable=True)
     content = models.TextField(blank=True)
     content_version = models.CharField(max_length=10, blank=True)
 
diff --git a/settings.py b/settings.py
index e0a9be7..9afbaf7 100644
--- a/settings.py
+++ b/settings.py
@@ -98,6 +98,7 @@ INSTALLED_APPS = (
     'django_evolution',
     'reversion',
     'gravatar',
+    'autoslug',
 
     # Local apps
     'notes',



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]