Commits

Andy Mikhailenko committed 666d546

Refactored (moved service functions from fields.py to utils.py). Added tests.

  • Participants
  • Parent commits d2099dd

Comments (0)

Files changed (4)

File autoslug/fields.py

 from warnings import warn
 
 # django
-from django.db.models.fields import FieldDoesNotExist, DateField, SlugField
+from django.db.models.fields import SlugField
 
 # app
 from autoslug.settings import slugify
+import utils
 
 
 __all__ = ['AutoSlugField']
 
         # backward compatibility
         if kwargs.get('unique_with_date'):
-            warn('Using unique_with_date="foo" in AutoSlugField is deprecated, '\
+            warn('Using unique_with_date="foo" in AutoSlugField is deprecated, '
                  'use unique_with=("foo",) instead.', DeprecationWarning)
             self.unique_with += (kwargs['unique_with_date'],)
 
 
         # autopopulate (unless the field is editable and has some value)
         if self.populate_from and not value: # and not self.editable:
-            value = self._get_prepopulated_value(instance)
+            value = utils.get_prepopulated_value(self, instance)
 
             if __debug__ and not value:
                 print 'Failed to populate slug %s.%s from %s' % \
 
         assert slug, 'slug is defined before trying to ensure uniqueness'
 
-        slug = self._crop_slug(slug)
+        slug = utils.crop_slug(self, slug)
 
         # ensure the slug is unique (if required)
         if self.unique or self.unique_with:
-            slug = self._generate_unique_slug(instance, slug)
+            slug = utils.generate_unique_slug(self, instance, slug)
 
         assert slug, 'value is filled before saving'
 
         setattr(instance, self.name, slug)
 
         return slug
-
-    def _get_prepopulated_value(self, instance):
-        """Returns preliminary value based on `populate_from`."""
-        if callable(self.populate_from):
-            # AutoSlugField(populate_from=lambda instance: ...)
-            return self.populate_from(instance)
-        else:
-            # AutoSlugField(populate_from='foo')
-            attr = getattr(instance, self.populate_from)
-            return callable(attr) and attr() or attr
-
-    def _generate_unique_slug(self, instance, slug):
-        """
-        Generates unique slug by adding a number to given value until no model
-        instance can be 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.
-        """
-        base_instance = instance
-
-        def _get_lookups(instance, unique_with):
-            "Returns a dict'able tuple of lookups to ensure slug uniqueness"
-            for _field_name in unique_with:
-                if '__' in _field_name:
-                    field_name, inner = _field_name.split('__', 1)
-                else:
-                    field_name, inner = _field_name, None
-
-                if not hasattr(instance, '_meta'):
-                    raise ValueError('Could not resolve lookup "...%s" in %s.%s'
-                                     ' `unique_with`.'
-                                     % (_field_name, base_instance._meta.object_name,
-                                        self.name))
-
-                try:
-                    field = instance._meta.get_field(field_name)
-                except FieldDoesNotExist:
-                    raise ValueError('Could not find attribute %s.%s referenced'
-                                     ' by %s.%s (see constraint `unique_with`)'
-                                     % (instance._meta.object_name, field_name,
-                                        base_instance._meta.object_name, self.name))
-
-                value = getattr(instance, field_name)
-                if not value:
-                    if field.blank:
-                        break
-                    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.'
-                                     % (base_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
-                    inner = inner or 'day'
-
-                    if '__' in inner:
-                        raise ValueError('The `unique_with` constraint in %s.%s'
-                                         ' is set to "%s", but AutoSlugField only'
-                                         ' accepts one level of nesting for dates'
-                                         ' (e.g. "date__month").'
-                                         % (base_instance._meta.object_name, self.name,
-                                            _field_name))
-
-                    parts = ['year', 'month', 'day']
-                    try:
-                        granularity = parts.index(inner) + 1
-                    except ValueError:
-                        raise ValueError('expected one of %s, got "%s" in "%s"'
-                                         % (parts, inner, _field_name))
-                    else:
-                        for part in parts[:granularity]:
-                            lookup = '%s__%s' % (field_name, part)
-                            yield lookup, getattr(value, part)
-                else:
-                    if inner:
-                        for res in _get_lookups(value, [inner]):
-                            yield _field_name, res[1]
-                    else:
-                        yield field_name, value
-
-        lookups = tuple(_get_lookups(instance, self.unique_with))
-        model = instance.__class__
-        field_name = self.name
-        index = 1
-        slug = self._crop_slug(slug)
-        orig_slug = slug
-        # keep changing the slug until it is unique
-        while True:
-            rivals = model.objects\
-                          .filter(**dict(lookups + ((self.name, slug),) ))\
-                          .exclude(pk=instance.pk)
-            if not rivals:
-                # the slug is unique, no model uses it
-                return slug
-
-            # the slug is not unique; change once more
-            index += 1
-            # ensure the resulting string is not too long
-            tail_length = len(self.index_sep) + len(str(index))
-            combined_length = len(orig_slug) + tail_length
-            if self.max_length < combined_length:
-                orig_slug = orig_slug[:self.max_length - tail_length]
-            # re-generate the slug
-            data = dict(slug=orig_slug, sep=self.index_sep, index=index)
-            slug = '%(slug)s%(sep)s%(index)d' % data
-
-    def _crop_slug(self, slug):
-        if self.max_length < len(slug):
-            return slug[:self.max_length]
-        return slug

File autoslug/tests.py

     slug = AutoSlugField(unique=True, sep='_')
 
 
+class ModelWithReferenceToItself(Model):
+    """
+    >>> a = ModelWithReferenceToItself(slug='test')
+    >>> a.save()
+    Traceback (most recent call last):
+    ...
+    ValueError: Attribute ModelWithReferenceToItself.slug references itself \
+    in `unique_with`. Please use "unique=True" for this case.
+    """
+    slug = AutoSlugField(unique_with='slug')
+
+
+class ModelWithWrongReferencedField(Model):
+    """
+    >>> a = ModelWithWrongReferencedField(slug='test')
+    >>> a.save()
+    Traceback (most recent call last):
+    ...
+    ValueError: Could not find attribute ModelWithWrongReferencedField.wrong_field \
+    referenced by ModelWithWrongReferencedField.slug (see constraint `unique_with`)
+    """
+    slug = AutoSlugField(unique_with='wrong_field')
+
+
+class ModelWithWrongLookupInUniqueWith(Model):
+    """
+    >>> a = ModelWithWrongLookupInUniqueWith(name='test', slug='test')
+    >>> a.save()
+    Traceback (most recent call last):
+    ...
+    ValueError: Could not resolve lookup "name__foo" in `unique_with` of \
+    ModelWithWrongLookupInUniqueWith.slug
+    """
+    slug = AutoSlugField(unique_with='name__foo')
+    name = CharField(max_length=10)
+
+
 class ModelWithWrongFieldOrder(Model):
     """
     >>> a = ModelWithWrongFieldOrder(slug='test')
     ...
     ValueError: Could not check uniqueness of ModelWithWrongFieldOrder.slug with \
     respect to ModelWithWrongFieldOrder.date because the latter is empty. Please \
-    ensure that "slug" is declared *after* all fields it depends on (i.e. "date"), \
-    and that they are not blank.
+    ensure that "slug" is declared *after* all fields listed in unique_with.
     """
     slug = AutoSlugField(unique_with='date')
     date = DateField(blank=False, null=False)

File autoslug/utils.py

+# -*- coding: utf-8 -*-
+
+# django
+from django.db.models.fields import FieldDoesNotExist, DateField
+
+
+def get_prepopulated_value(field, instance):
+    """
+    Returns preliminary value based on `populate_from`.
+    """
+    if hasattr(field.populate_from, '__call__'):
+        # AutoSlugField(populate_from=lambda instance: ...)
+        return field.populate_from(instance)
+    else:
+        # AutoSlugField(populate_from='foo')
+        attr = getattr(instance, field.populate_from)
+        return callable(attr) and attr() or attr
+
+def generate_unique_slug(field, instance, slug):
+    """
+    Generates unique slug by adding a number to given value until no model
+    instance can be 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.
+    """
+
+    original_slug = slug = crop_slug(field, slug)
+
+    default_lookups = tuple(get_uniqueness_lookups(field, instance, field.unique_with))
+
+    index = 1
+
+    # keep changing the slug until it is unique
+    while True:
+        # find instances with same slug
+        lookups = dict(default_lookups, **{field.name: slug})
+        rivals = type(instance).objects.filter(**lookups).exclude(pk=instance.pk)
+
+        if not rivals:
+            # the slug is unique, no model uses it
+            return slug
+
+        # the slug is not unique; change once more
+        index += 1
+
+        # ensure the resulting string is not too long
+        tail_length = len(field.index_sep) + len(str(index))
+        combined_length = len(original_slug) + tail_length
+        if field.max_length < combined_length:
+            original_slug = original_slug[:field.max_length - tail_length]
+
+        # re-generate the slug
+        data = dict(slug=original_slug, sep=field.index_sep, index=index)
+        slug = '%(slug)s%(sep)s%(index)d' % data
+
+        # ...next iteration...
+
+def get_uniqueness_lookups(field, instance, unique_with):
+    """
+    Returns a dict'able tuple of lookups to ensure uniqueness of a slug.
+    """
+    for original_lookup_name in unique_with:
+        if '__' in original_lookup_name:
+            field_name, inner_lookup = original_lookup_name.split('__', 1)
+        else:
+            field_name, inner_lookup = original_lookup_name, None
+
+        try:
+            other_field = instance._meta.get_field(field_name)
+        except FieldDoesNotExist:
+            raise ValueError('Could not find attribute %s.%s referenced'
+                             ' by %s.%s (see constraint `unique_with`)'
+                             % (instance._meta.object_name, field_name,
+                                instance._meta.object_name, field.name))
+
+        if field == other_field:
+            raise ValueError('Attribute %s.%s references itself in `unique_with`.'
+                             ' Please use "unique=True" for this case.'
+                             % (instance._meta.object_name, field_name))
+
+        value = getattr(instance, field_name)
+        if not value:
+            if other_field.blank:
+                break
+            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 listed in unique_with.'
+                             % (instance._meta.object_name, field.name,
+                                instance._meta.object_name, field_name,
+                                field.name))
+        if isinstance(other_field, DateField):    # DateTimeField is a DateField subclass
+            inner_lookup = inner_lookup or 'day'
+
+            if '__' in inner_lookup:
+                raise ValueError('The `unique_with` constraint in %s.%s'
+                                 ' is set to "%s", but AutoSlugField only'
+                                 ' accepts one level of nesting for dates'
+                                 ' (e.g. "date__month").'
+                                 % (instance._meta.object_name, field.name,
+                                    original_lookup_name))
+
+            parts = ['year', 'month', 'day']
+            try:
+                granularity = parts.index(inner_lookup) + 1
+            except ValueError:
+                raise ValueError('expected one of %s, got "%s" in "%s"'
+                                    % (parts, inner_lookup, original_lookup_name))
+            else:
+                for part in parts[:granularity]:
+                    lookup = '%s__%s' % (field_name, part)
+                    yield lookup, getattr(value, part)
+        else:
+            # TODO: this part should be documented as it involves recursion
+            if inner_lookup:
+                if not hasattr(value, '_meta'):
+                    raise ValueError('Could not resolve lookup "%s" in `unique_with` of %s.%s'
+                                     % (original_lookup_name, instance._meta.object_name, field.name))
+                for inner_name, inner_value in get_uniqueness_lookups(field, value, [inner_lookup]):
+                    yield original_lookup_name, inner_value
+            else:
+                yield field_name, value
+
+def crop_slug(field, slug):
+    if field.max_length < len(slug):
+        return slug[:field.max_length]
+    return slug
 
 setup(
     name     = 'django-autoslug',
-    version  = '1.3.6',
+    version  = '1.3.7',
     packages = ['autoslug'],
 
     requires = ['python (>= 2.4)', 'django (>= 1.0)'],