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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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") |