Commit 4b1bc750875f2de02f0c6702ae925e5b9f0c492a
1 parent
f34a20a8
Exists in
master
and in
5 other branches
Biblioteca para gerar o slug automaticamente já no model
Showing
5 changed files
with
625 additions
and
0 deletions
Show diff stats
| ... | ... | @@ -0,0 +1,15 @@ |
| 1 | +# coding: utf-8 | |
| 2 | +# | |
| 3 | +# Copyright (c) 2008—2015 Andy Mikhailenko | |
| 4 | +# | |
| 5 | +# This file is part of django-autoslug. | |
| 6 | +# | |
| 7 | +# django-autoslug is free software under terms of the GNU Lesser | |
| 8 | +# General Public License version 3 (LGPLv3) as published by the Free | |
| 9 | +# Software Foundation. See the file README for copying conditions. | |
| 10 | +# | |
| 11 | +from autoslug.fields import AutoSlugField | |
| 12 | + | |
| 13 | + | |
| 14 | +__version__ = '1.9.3' | |
| 15 | +__all__ = ['AutoSlugField'] | ... | ... |
| ... | ... | @@ -0,0 +1,343 @@ |
| 1 | +# coding: utf-8 | |
| 2 | +# | |
| 3 | +# Copyright (c) 2008—2015 Andy Mikhailenko | |
| 4 | +# | |
| 5 | +# This file is part of django-autoslug. | |
| 6 | +# | |
| 7 | +# django-autoslug is free software under terms of the GNU Lesser | |
| 8 | +# General Public License version 3 (LGPLv3) as published by the Free | |
| 9 | +# Software Foundation. See the file README for copying conditions. | |
| 10 | +# | |
| 11 | + | |
| 12 | +# django | |
| 13 | +from django.conf import settings | |
| 14 | +from django.db.models.fields import SlugField | |
| 15 | +from django.db.models.signals import post_save | |
| 16 | + | |
| 17 | +# 3rd-party | |
| 18 | +try: | |
| 19 | + from south.modelsinspector import introspector | |
| 20 | +except ImportError: | |
| 21 | + introspector = lambda self: [], {} | |
| 22 | + | |
| 23 | +try: | |
| 24 | + from modeltranslation import utils as modeltranslation_utils | |
| 25 | +except ImportError: | |
| 26 | + modeltranslation_utils = None | |
| 27 | + | |
| 28 | +# this app | |
| 29 | +from autoslug.settings import slugify, autoslug_modeltranslation_enable | |
| 30 | +from autoslug import utils | |
| 31 | + | |
| 32 | +__all__ = ['AutoSlugField'] | |
| 33 | + | |
| 34 | +SLUG_INDEX_SEPARATOR = '-' # the "-" in "foo-2" | |
| 35 | + | |
| 36 | +try: # pragma: nocover | |
| 37 | + # Python 2.x | |
| 38 | + basestring | |
| 39 | +except NameError: # pragma: nocover | |
| 40 | + # Python 3.x | |
| 41 | + basestring = str | |
| 42 | + | |
| 43 | + | |
| 44 | +class AutoSlugField(SlugField): | |
| 45 | + """ | |
| 46 | + AutoSlugField is an extended SlugField able to automatically resolve name | |
| 47 | + clashes. | |
| 48 | + | |
| 49 | + AutoSlugField can also perform the following tasks on save: | |
| 50 | + | |
| 51 | + - populate itself from another field (using `populate_from`), | |
| 52 | + - use custom `slugify` function (using `slugify` or :doc:`settings`), and | |
| 53 | + - preserve uniqueness of the value (using `unique` or `unique_with`). | |
| 54 | + | |
| 55 | + None of the tasks is mandatory, i.e. you can have auto-populated non-unique | |
| 56 | + fields, manually entered unique ones (absolutely unique or within a given | |
| 57 | + date) or both. | |
| 58 | + | |
| 59 | + Uniqueness is preserved by checking if the slug is unique with given constraints | |
| 60 | + (`unique_with`) or globally (`unique`) and adding a number to the slug to make | |
| 61 | + it unique. | |
| 62 | + | |
| 63 | + :param always_update: boolean: if True, the slug is updated each time the | |
| 64 | + model instance is saved. Use with care because `cool URIs don't | |
| 65 | + change`_ (and the slug is usually a part of object's URI). Note that | |
| 66 | + even if the field is editable, any manual changes will be lost when | |
| 67 | + this option is activated. | |
| 68 | + :param populate_from: string or callable: if string is given, it is considered | |
| 69 | + as the name of attribute from which to fill the slug. If callable is given, | |
| 70 | + it should accept `instance` parameter and return a value to fill the slug | |
| 71 | + with. | |
| 72 | + :param sep: string: if defined, overrides default separator for automatically | |
| 73 | + incremented slug index (i.e. the "-" in "foo-2"). | |
| 74 | + :param slugify: callable: if defined, overrides `AUTOSLUG_SLUGIFY_FUNCTION` | |
| 75 | + defined in :doc:`settings`. | |
| 76 | + :param unique: boolean: ensure total slug uniqueness (unless more precise | |
| 77 | + `unique_with` is defined). | |
| 78 | + :param unique_with: string or tuple of strings: name or names of attributes | |
| 79 | + to check for "partial uniqueness", i.e. there will not be two objects | |
| 80 | + with identical slugs if these objects share the same values of given | |
| 81 | + attributes. For instance, ``unique_with='pub_date'`` tells AutoSlugField | |
| 82 | + to enforce slug uniqueness of all items published on given date. The | |
| 83 | + slug, however, may reappear on another date. If more than one field is | |
| 84 | + given, e.g. ``unique_with=('pub_date', 'author')``, then the same slug may | |
| 85 | + reappear within a day or within some author's articles but never within | |
| 86 | + a day for the same author. Foreign keys are also supported, i.e. not only | |
| 87 | + `unique_with='author'` will do, but also `unique_with='author__name'`. | |
| 88 | + | |
| 89 | + .. _cool URIs don't change: http://w3.org/Provider/Style/URI.html | |
| 90 | + | |
| 91 | + .. note:: always place any slug attribute *after* attributes referenced | |
| 92 | + by it (i.e. those from which you wish to `populate_from` or check | |
| 93 | + `unique_with`). The reasoning is that autosaved dates and other such | |
| 94 | + fields must be already processed before using them in the AutoSlugField. | |
| 95 | + | |
| 96 | + Example usage: | |
| 97 | + | |
| 98 | + .. code-block:: python | |
| 99 | + | |
| 100 | + from django.db import models | |
| 101 | + from autoslug import AutoSlugField | |
| 102 | + | |
| 103 | + class Article(models.Model): | |
| 104 | + '''An article with title, date and slug. The slug is not totally | |
| 105 | + unique but there will be no two articles with the same slug within | |
| 106 | + any month. | |
| 107 | + ''' | |
| 108 | + title = models.CharField(max_length=200) | |
| 109 | + pub_date = models.DateField(auto_now_add=True) | |
| 110 | + slug = AutoSlugField(populate_from='title', unique_with='pub_date__month') | |
| 111 | + | |
| 112 | + | |
| 113 | + More options: | |
| 114 | + | |
| 115 | + .. code-block:: python | |
| 116 | + | |
| 117 | + # slugify but allow non-unique slugs | |
| 118 | + slug = AutoSlugField() | |
| 119 | + | |
| 120 | + # globally unique, silently fix on conflict ("foo" --> "foo-1".."foo-n") | |
| 121 | + slug = AutoSlugField(unique=True) | |
| 122 | + | |
| 123 | + # autoslugify value from attribute named "title"; editable defaults to False | |
| 124 | + slug = AutoSlugField(populate_from='title') | |
| 125 | + | |
| 126 | + # same as above but force editable=True | |
| 127 | + slug = AutoSlugField(populate_from='title', editable=True) | |
| 128 | + | |
| 129 | + # ensure that slug is unique with given date (not globally) | |
| 130 | + slug = AutoSlugField(unique_with='pub_date') | |
| 131 | + | |
| 132 | + # ensure that slug is unique with given date AND category | |
| 133 | + slug = AutoSlugField(unique_with=('pub_date','category')) | |
| 134 | + | |
| 135 | + # ensure that slug in unique with an external object | |
| 136 | + # assuming that author=ForeignKey(Author) | |
| 137 | + slug = AutoSlugField(unique_with='author') | |
| 138 | + | |
| 139 | + # ensure that slug in unique with a subset of external objects (by lookups) | |
| 140 | + # assuming that author=ForeignKey(Author) | |
| 141 | + slug = AutoSlugField(unique_with='author__name') | |
| 142 | + | |
| 143 | + # mix above-mentioned behaviour bits | |
| 144 | + slug = AutoSlugField(populate_from='title', unique_with='pub_date') | |
| 145 | + | |
| 146 | + # minimum date granularity is shifted from day to month | |
| 147 | + slug = AutoSlugField(populate_from='title', unique_with='pub_date__month') | |
| 148 | + | |
| 149 | + # autoslugify value from a dynamic attribute (i.e. a method) | |
| 150 | + slug = AutoSlugField(populate_from='get_full_name') | |
| 151 | + | |
| 152 | + # autoslugify value from a custom callable | |
| 153 | + # (ex. usage: user profile models) | |
| 154 | + slug = AutoSlugField(populate_from=lambda instance: instance.user.get_full_name()) | |
| 155 | + | |
| 156 | + # specify model manager for looking up slugs shared by subclasses | |
| 157 | + | |
| 158 | + class Article(models.Model): | |
| 159 | + '''An article with title, date and slug. The slug is not totally | |
| 160 | + unique but there will be no two articles with the same slug within | |
| 161 | + any month. | |
| 162 | + ''' | |
| 163 | + objects = models.Manager() | |
| 164 | + title = models.CharField(max_length=200) | |
| 165 | + slug = AutoSlugField(populate_from='title', unique_with='pub_date__month', manager=objects) | |
| 166 | + | |
| 167 | + class NewsArticle(Article): | |
| 168 | + pass | |
| 169 | + | |
| 170 | + # autoslugify value using custom `slugify` function | |
| 171 | + from autoslug.settings import slugify as default_slugify | |
| 172 | + def custom_slugify(value): | |
| 173 | + return default_slugify(value).replace('-', '_') | |
| 174 | + slug = AutoSlugField(slugify=custom_slugify) | |
| 175 | + | |
| 176 | + """ | |
| 177 | + def __init__(self, *args, **kwargs): | |
| 178 | + kwargs['max_length'] = kwargs.get('max_length', 50) | |
| 179 | + | |
| 180 | + # autopopulated slug is not editable unless told so | |
| 181 | + self.populate_from = kwargs.pop('populate_from', None) | |
| 182 | + if self.populate_from: | |
| 183 | + kwargs.setdefault('editable', False) | |
| 184 | + | |
| 185 | + # unique_with value can be string or tuple | |
| 186 | + self.unique_with = kwargs.pop('unique_with', ()) | |
| 187 | + if isinstance(self.unique_with, basestring): | |
| 188 | + self.unique_with = (self.unique_with,) | |
| 189 | + | |
| 190 | + self.slugify = kwargs.pop('slugify', slugify) | |
| 191 | + assert hasattr(self.slugify, '__call__') | |
| 192 | + | |
| 193 | + self.index_sep = kwargs.pop('sep', SLUG_INDEX_SEPARATOR) | |
| 194 | + | |
| 195 | + if self.unique_with: | |
| 196 | + # we will do "manual" granular check below | |
| 197 | + kwargs['unique'] = False | |
| 198 | + | |
| 199 | + # Set db_index=True unless it's been set manually. | |
| 200 | + if 'db_index' not in kwargs: | |
| 201 | + kwargs['db_index'] = True | |
| 202 | + | |
| 203 | + # A boolean instructing the field to accept Unicode letters in | |
| 204 | + # addition to ASCII letters. Defaults to False. | |
| 205 | + self.allow_unicode = kwargs.pop('allow_unicode', False) | |
| 206 | + | |
| 207 | + # When using model inheritence, set manager to search for matching | |
| 208 | + # slug values | |
| 209 | + self.manager = kwargs.pop('manager', None) | |
| 210 | + | |
| 211 | + self.always_update = kwargs.pop('always_update', False) | |
| 212 | + super(SlugField, self).__init__(*args, **kwargs) | |
| 213 | + | |
| 214 | + def deconstruct(self): | |
| 215 | + name, path, args, kwargs = super(AutoSlugField, self).deconstruct() | |
| 216 | + | |
| 217 | + if self.max_length == 50: | |
| 218 | + kwargs.pop('max_length', None) | |
| 219 | + | |
| 220 | + if self.populate_from is not None: | |
| 221 | + kwargs['populate_from'] = self.populate_from | |
| 222 | + if self.editable is not False: | |
| 223 | + kwargs['editable'] = self.editable | |
| 224 | + | |
| 225 | + if self.unique_with != (): | |
| 226 | + kwargs['unique_with'] = self.unique_with | |
| 227 | + kwargs.pop('unique', None) | |
| 228 | + | |
| 229 | + if self.slugify != slugify: | |
| 230 | + kwargs['slugify'] = self.slugify | |
| 231 | + | |
| 232 | + if self.index_sep != SLUG_INDEX_SEPARATOR: | |
| 233 | + kwargs['sep'] = self.index_sep | |
| 234 | + | |
| 235 | + kwargs.pop('db_index', None) | |
| 236 | + | |
| 237 | + if self.manager is not None: | |
| 238 | + kwargs['manager'] = self.manager | |
| 239 | + | |
| 240 | + if self.always_update: | |
| 241 | + kwargs['always_update'] = self.always_update | |
| 242 | + | |
| 243 | + return name, path, args, kwargs | |
| 244 | + | |
| 245 | + def pre_save(self, instance, add): | |
| 246 | + | |
| 247 | + # get currently entered slug | |
| 248 | + value = self.value_from_object(instance) | |
| 249 | + | |
| 250 | + manager = self.manager | |
| 251 | + | |
| 252 | + # autopopulate | |
| 253 | + if self.always_update or (self.populate_from and not value): | |
| 254 | + value = utils.get_prepopulated_value(self, instance) | |
| 255 | + | |
| 256 | + # pragma: nocover | |
| 257 | + if __debug__ and not value and not self.blank: | |
| 258 | + print('Failed to populate slug %s.%s from %s' % \ | |
| 259 | + (instance._meta.object_name, self.name, self.populate_from)) | |
| 260 | + | |
| 261 | + if value: | |
| 262 | + slug = self.slugify(value) | |
| 263 | + else: | |
| 264 | + slug = None | |
| 265 | + | |
| 266 | + if not self.blank: | |
| 267 | + slug = instance._meta.model_name | |
| 268 | + elif not self.null: | |
| 269 | + slug = '' | |
| 270 | + | |
| 271 | + if not self.blank: | |
| 272 | + assert slug, 'slug is defined before trying to ensure uniqueness' | |
| 273 | + | |
| 274 | + if slug: | |
| 275 | + slug = utils.crop_slug(self, slug) | |
| 276 | + | |
| 277 | + # ensure the slug is unique (if required) | |
| 278 | + if self.unique or self.unique_with: | |
| 279 | + slug = utils.generate_unique_slug(self, instance, slug, manager) | |
| 280 | + | |
| 281 | + assert slug, 'value is filled before saving' | |
| 282 | + | |
| 283 | + # make the updated slug available as instance attribute | |
| 284 | + setattr(instance, self.name, slug) | |
| 285 | + | |
| 286 | + # modeltranslation support | |
| 287 | + if 'modeltranslation' in settings.INSTALLED_APPS \ | |
| 288 | + and not hasattr(self.populate_from, '__call__') \ | |
| 289 | + and autoslug_modeltranslation_enable: | |
| 290 | + post_save.connect(modeltranslation_update_slugs, sender=type(instance)) | |
| 291 | + | |
| 292 | + return slug | |
| 293 | + | |
| 294 | + | |
| 295 | + def south_field_triple(self): | |
| 296 | + "Returns a suitable description of this field for South." | |
| 297 | + args, kwargs = introspector(self) | |
| 298 | + kwargs.update({ | |
| 299 | + 'populate_from': 'None' if callable(self.populate_from) else repr(self.populate_from), | |
| 300 | + 'unique_with': repr(self.unique_with) | |
| 301 | + }) | |
| 302 | + return ('autoslug.fields.AutoSlugField', args, kwargs) | |
| 303 | + | |
| 304 | + | |
| 305 | +def modeltranslation_update_slugs(sender, **kwargs): | |
| 306 | + # https://bitbucket.org/neithere/django-autoslug/pull-request/11/modeltranslation-support-fix-issue-19/ | |
| 307 | + # http://django-modeltranslation.readthedocs.org | |
| 308 | + # | |
| 309 | + # TODO: tests | |
| 310 | + # | |
| 311 | + if not modeltranslation_utils: | |
| 312 | + return | |
| 313 | + | |
| 314 | + instance = kwargs['instance'] | |
| 315 | + slugs = {} | |
| 316 | + | |
| 317 | + for field in instance._meta.fields: | |
| 318 | + if type(field) != AutoSlugField: | |
| 319 | + continue | |
| 320 | + if not field.populate_from: | |
| 321 | + continue | |
| 322 | + for lang in settings.LANGUAGES: | |
| 323 | + lang_code, _ = lang | |
| 324 | + lang_code = lang_code.replace('-', '_') | |
| 325 | + | |
| 326 | + populate_from_localized = modeltranslation_utils.build_localized_fieldname(field.populate_from, lang_code) | |
| 327 | + field_name_localized = modeltranslation_utils.build_localized_fieldname(field.name, lang_code) | |
| 328 | + | |
| 329 | + # The source field or the slug field itself may not be registered | |
| 330 | + # with translator | |
| 331 | + if not hasattr(instance, populate_from_localized): | |
| 332 | + continue | |
| 333 | + if not hasattr(instance, field_name_localized): | |
| 334 | + continue | |
| 335 | + | |
| 336 | + populate_from_value = getattr(instance, populate_from_localized) | |
| 337 | + field_value = getattr(instance, field_name_localized) | |
| 338 | + | |
| 339 | + if not field_value or field.always_update: | |
| 340 | + slug = field.slugify(populate_from_value) | |
| 341 | + slugs[field_name_localized] = slug | |
| 342 | + | |
| 343 | + sender.objects.filter(pk=instance.pk).update(**slugs) | ... | ... |
| ... | ... | @@ -0,0 +1,68 @@ |
| 1 | +# coding: utf-8 | |
| 2 | +# | |
| 3 | +# Copyright (c) 2008—2015 Andy Mikhailenko | |
| 4 | +# | |
| 5 | +# This file is part of django-autoslug. | |
| 6 | +# | |
| 7 | +# django-autoslug is free software under terms of the GNU Lesser | |
| 8 | +# General Public License version 3 (LGPLv3) as published by the Free | |
| 9 | +# Software Foundation. See the file README for copying conditions. | |
| 10 | +# | |
| 11 | +""" | |
| 12 | +Django settings that affect django-autoslug: | |
| 13 | + | |
| 14 | +`AUTOSLUG_SLUGIFY_FUNCTION` | |
| 15 | + Allows to define a custom slugifying function. | |
| 16 | + | |
| 17 | + The function can be repsesented as string or callable, e.g.:: | |
| 18 | + | |
| 19 | + # custom function, path as string: | |
| 20 | + AUTOSLUG_SLUGIFY_FUNCTION = 'some_app.slugify_func' | |
| 21 | + | |
| 22 | + # custom function, callable: | |
| 23 | + AUTOSLUG_SLUGIFY_FUNCTION = some_app.slugify_func | |
| 24 | + | |
| 25 | + # custom function, defined inline: | |
| 26 | + AUTOSLUG_SLUGIFY_FUNCTION = lambda slug: 'can i haz %s?' % slug | |
| 27 | + | |
| 28 | + If no value is given, default value is used. | |
| 29 | + | |
| 30 | + Default value is one of these depending on availability in given order: | |
| 31 | + | |
| 32 | + * `unidecode.unidecode()` if Unidecode_ is available; | |
| 33 | + * `pytils.translit.slugify()` if pytils_ is available; | |
| 34 | + * `django.template.defaultfilters.slugify()` bundled with Django. | |
| 35 | + | |
| 36 | + django-autoslug also ships a couple of slugify functions that use | |
| 37 | + the translitcodec_ Python library, e.g.:: | |
| 38 | + | |
| 39 | + # using as many characters as needed to make a natural replacement | |
| 40 | + AUTOSLUG_SLUGIFY_FUNCTION = 'autoslug.utils.translit_long' | |
| 41 | + | |
| 42 | + # using the minimum number of characters to make a replacement | |
| 43 | + AUTOSLUG_SLUGIFY_FUNCTION = 'autoslug.utils.translit_short' | |
| 44 | + | |
| 45 | + # only performing single character replacements | |
| 46 | + AUTOSLUG_SLUGIFY_FUNCTION = 'autoslug.utils.translit_one' | |
| 47 | + | |
| 48 | +.. _Unidecode: http://pypi.python.org/pypi/Unidecode | |
| 49 | +.. _pytils: http://pypi.python.org/pypi/pytils | |
| 50 | +.. _translitcodec: http://pypi.python.org/pypi/translitcodec | |
| 51 | + | |
| 52 | +`AUTOSLUG_MODELTRANSLATION_ENABLE` | |
| 53 | + Django-autoslug support of modeltranslation_ is still experimental. | |
| 54 | + If you wish to enable it, please set this option to `True` in your project | |
| 55 | + settings. Default is `False`. | |
| 56 | + | |
| 57 | +.. _modeltranslation: http://django-modeltranslation.readthedocs.org | |
| 58 | + | |
| 59 | +""" | |
| 60 | +from django.conf import settings | |
| 61 | +from django.core.urlresolvers import get_callable | |
| 62 | + | |
| 63 | +# use custom slugifying function if any | |
| 64 | +slugify_function_path = getattr(settings, 'AUTOSLUG_SLUGIFY_FUNCTION', 'autoslug.utils.slugify') | |
| 65 | +slugify = get_callable(slugify_function_path) | |
| 66 | + | |
| 67 | +# enable/disable modeltranslation support | |
| 68 | +autoslug_modeltranslation_enable = getattr(settings, 'AUTOSLUG_MODELTRANSLATION_ENABLE', False) | ... | ... |
| ... | ... | @@ -0,0 +1,199 @@ |
| 1 | +# coding: utf-8 | |
| 2 | +# | |
| 3 | +# Copyright (c) 2008—2015 Andy Mikhailenko | |
| 4 | +# | |
| 5 | +# This file is part of django-autoslug. | |
| 6 | +# | |
| 7 | +# django-autoslug is free software under terms of the GNU Lesser | |
| 8 | +# General Public License version 3 (LGPLv3) as published by the Free | |
| 9 | +# Software Foundation. See the file README for copying conditions. | |
| 10 | +# | |
| 11 | + | |
| 12 | +# django | |
| 13 | +from django.core.exceptions import ImproperlyConfigured | |
| 14 | +from django.db.models import ForeignKey | |
| 15 | +from django.db.models.fields import FieldDoesNotExist, DateField | |
| 16 | +from django.template.defaultfilters import slugify as django_slugify | |
| 17 | + | |
| 18 | +try: | |
| 19 | + # i18n-friendly approach | |
| 20 | + from unidecode import unidecode | |
| 21 | +except ImportError: | |
| 22 | + try: | |
| 23 | + # Cyrillic transliteration (primarily Russian) | |
| 24 | + from pytils.translit import slugify | |
| 25 | + except ImportError: | |
| 26 | + # fall back to Django's default method | |
| 27 | + slugify = django_slugify | |
| 28 | +else: | |
| 29 | + # Use Django's default method over decoded string | |
| 30 | + def slugify(value): | |
| 31 | + return django_slugify(unidecode(value)) | |
| 32 | + | |
| 33 | + | |
| 34 | +def get_prepopulated_value(field, instance): | |
| 35 | + """ | |
| 36 | + Returns preliminary value based on `populate_from`. | |
| 37 | + """ | |
| 38 | + if hasattr(field.populate_from, '__call__'): | |
| 39 | + # AutoSlugField(populate_from=lambda instance: ...) | |
| 40 | + return field.populate_from(instance) | |
| 41 | + else: | |
| 42 | + # AutoSlugField(populate_from='foo') | |
| 43 | + attr = getattr(instance, field.populate_from) | |
| 44 | + return callable(attr) and attr() or attr | |
| 45 | + | |
| 46 | + | |
| 47 | +def generate_unique_slug(field, instance, slug, manager): | |
| 48 | + """ | |
| 49 | + Generates unique slug by adding a number to given value until no model | |
| 50 | + instance can be found with such slug. If ``unique_with`` (a tuple of field | |
| 51 | + names) was specified for the field, all these fields are included together | |
| 52 | + in the query when looking for a "rival" model instance. | |
| 53 | + """ | |
| 54 | + | |
| 55 | + original_slug = slug = crop_slug(field, slug) | |
| 56 | + | |
| 57 | + default_lookups = tuple(get_uniqueness_lookups(field, instance, field.unique_with)) | |
| 58 | + | |
| 59 | + index = 1 | |
| 60 | + | |
| 61 | + if not manager: | |
| 62 | + manager = field.model._default_manager | |
| 63 | + | |
| 64 | + | |
| 65 | + # keep changing the slug until it is unique | |
| 66 | + while True: | |
| 67 | + # find instances with same slug | |
| 68 | + lookups = dict(default_lookups, **{field.name: slug}) | |
| 69 | + rivals = manager.filter(**lookups) | |
| 70 | + if instance.pk: | |
| 71 | + rivals = rivals.exclude(pk=instance.pk) | |
| 72 | + | |
| 73 | + if not rivals: | |
| 74 | + # the slug is unique, no model uses it | |
| 75 | + return slug | |
| 76 | + | |
| 77 | + # the slug is not unique; change once more | |
| 78 | + index += 1 | |
| 79 | + | |
| 80 | + # ensure the resulting string is not too long | |
| 81 | + tail_length = len(field.index_sep) + len(str(index)) | |
| 82 | + combined_length = len(original_slug) + tail_length | |
| 83 | + if field.max_length < combined_length: | |
| 84 | + original_slug = original_slug[:field.max_length - tail_length] | |
| 85 | + | |
| 86 | + # re-generate the slug | |
| 87 | + data = dict(slug=original_slug, sep=field.index_sep, index=index) | |
| 88 | + slug = '%(slug)s%(sep)s%(index)d' % data | |
| 89 | + | |
| 90 | + # ...next iteration... | |
| 91 | + | |
| 92 | + | |
| 93 | +def get_uniqueness_lookups(field, instance, unique_with): | |
| 94 | + """ | |
| 95 | + Returns a dict'able tuple of lookups to ensure uniqueness of a slug. | |
| 96 | + """ | |
| 97 | + for original_lookup_name in unique_with: | |
| 98 | + if '__' in original_lookup_name: | |
| 99 | + field_name, inner_lookup = original_lookup_name.split('__', 1) | |
| 100 | + else: | |
| 101 | + field_name, inner_lookup = original_lookup_name, None | |
| 102 | + | |
| 103 | + try: | |
| 104 | + other_field = instance._meta.get_field(field_name) | |
| 105 | + except FieldDoesNotExist: | |
| 106 | + raise ValueError('Could not find attribute %s.%s referenced' | |
| 107 | + ' by %s.%s (see constraint `unique_with`)' | |
| 108 | + % (instance._meta.object_name, field_name, | |
| 109 | + instance._meta.object_name, field.name)) | |
| 110 | + | |
| 111 | + if field == other_field: | |
| 112 | + raise ValueError('Attribute %s.%s references itself in `unique_with`.' | |
| 113 | + ' Please use "unique=True" for this case.' | |
| 114 | + % (instance._meta.object_name, field_name)) | |
| 115 | + | |
| 116 | + value = getattr(instance, field_name) | |
| 117 | + if not value: | |
| 118 | + if other_field.blank: | |
| 119 | + field_object, model, direct, m2m = instance._meta.get_field_by_name(field_name) | |
| 120 | + if isinstance(field_object, ForeignKey): | |
| 121 | + lookup = '%s__isnull' % field_name | |
| 122 | + yield lookup, True | |
| 123 | + break | |
| 124 | + raise ValueError('Could not check uniqueness of %s.%s with' | |
| 125 | + ' respect to %s.%s because the latter is empty.' | |
| 126 | + ' Please ensure that "%s" is declared *after*' | |
| 127 | + ' all fields listed in unique_with.' | |
| 128 | + % (instance._meta.object_name, field.name, | |
| 129 | + instance._meta.object_name, field_name, | |
| 130 | + field.name)) | |
| 131 | + if isinstance(other_field, DateField): # DateTimeField is a DateField subclass | |
| 132 | + inner_lookup = inner_lookup or 'day' | |
| 133 | + | |
| 134 | + if '__' in inner_lookup: | |
| 135 | + raise ValueError('The `unique_with` constraint in %s.%s' | |
| 136 | + ' is set to "%s", but AutoSlugField only' | |
| 137 | + ' accepts one level of nesting for dates' | |
| 138 | + ' (e.g. "date__month").' | |
| 139 | + % (instance._meta.object_name, field.name, | |
| 140 | + original_lookup_name)) | |
| 141 | + | |
| 142 | + parts = ['year', 'month', 'day'] | |
| 143 | + try: | |
| 144 | + granularity = parts.index(inner_lookup) + 1 | |
| 145 | + except ValueError: | |
| 146 | + raise ValueError('expected one of %s, got "%s" in "%s"' | |
| 147 | + % (parts, inner_lookup, original_lookup_name)) | |
| 148 | + else: | |
| 149 | + for part in parts[:granularity]: | |
| 150 | + lookup = '%s__%s' % (field_name, part) | |
| 151 | + yield lookup, getattr(value, part) | |
| 152 | + else: | |
| 153 | + # TODO: this part should be documented as it involves recursion | |
| 154 | + if inner_lookup: | |
| 155 | + if not hasattr(value, '_meta'): | |
| 156 | + raise ValueError('Could not resolve lookup "%s" in `unique_with` of %s.%s' | |
| 157 | + % (original_lookup_name, instance._meta.object_name, field.name)) | |
| 158 | + for inner_name, inner_value in get_uniqueness_lookups(field, value, [inner_lookup]): | |
| 159 | + yield original_lookup_name, inner_value | |
| 160 | + else: | |
| 161 | + yield field_name, value | |
| 162 | + | |
| 163 | + | |
| 164 | +def crop_slug(field, slug): | |
| 165 | + if field.max_length < len(slug): | |
| 166 | + return slug[:field.max_length] | |
| 167 | + return slug | |
| 168 | + | |
| 169 | + | |
| 170 | +try: | |
| 171 | + import translitcodec | |
| 172 | +except ImportError: | |
| 173 | + pass | |
| 174 | +else: | |
| 175 | + import re | |
| 176 | + PUNCT_RE = re.compile(r'[\t !"#$%&\'()*\-/<=>?@\[\\\]^_`{|},.]+') | |
| 177 | + | |
| 178 | + def translitcodec_slugify(codec): | |
| 179 | + def _slugify(value, delim='-', encoding=''): | |
| 180 | + """ | |
| 181 | + Generates an ASCII-only slug. | |
| 182 | + | |
| 183 | + Borrowed from http://flask.pocoo.org/snippets/5/ | |
| 184 | + """ | |
| 185 | + if encoding: | |
| 186 | + encoder = "%s/%s" % (codec, encoding) | |
| 187 | + else: | |
| 188 | + encoder = codec | |
| 189 | + result = [] | |
| 190 | + for word in PUNCT_RE.split(value.lower()): | |
| 191 | + word = word.encode(encoder) | |
| 192 | + if word: | |
| 193 | + result.append(word) | |
| 194 | + return unicode(delim.join(result)) | |
| 195 | + return _slugify | |
| 196 | + | |
| 197 | + translit_long = translitcodec_slugify("translit/long") | |
| 198 | + translit_short = translitcodec_slugify("translit/short") | |
| 199 | + translit_one = translitcodec_slugify("translit/one") | ... | ... |