mirror of
				https://github.com/SectorLabs/django-localized-fields.git
				synced 2025-10-31 02:48:56 +03:00 
			
		
		
		
	LocalizedUniqueSlugField refactored
This commit is contained in:
		| @@ -194,11 +194,10 @@ Besides ``LocalizedField``, there's also: | |||||||
|           .. code-block:: python |           .. code-block:: python | ||||||
|  |  | ||||||
|               from localized_fields import (LocalizedModel, |               from localized_fields import (LocalizedModel, | ||||||
|                                             AtomicSlugRetryMixin, |  | ||||||
|                                             LocalizedField, |                                             LocalizedField, | ||||||
|                                             LocalizedUniqueSlugField) |                                             LocalizedUniqueSlugField) | ||||||
|  |  | ||||||
|               class MyModel(AtomicSlugRetryMixin, LocalizedModel): |               class MyModel(LocalizedModel): | ||||||
|                    title = LocalizedField() |                    title = LocalizedField() | ||||||
|                    slug = LocalizedUniqueSlugField(populate_from='title') |                    slug = LocalizedUniqueSlugField(populate_from='title') | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,18 +1,19 @@ | |||||||
| from datetime import datetime | from datetime import datetime | ||||||
|  |  | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
|  | from django import forms | ||||||
| from django.utils.text import slugify | from django.utils.text import slugify | ||||||
| from django.core.exceptions import ImproperlyConfigured | from django.db import transaction | ||||||
|  | from django.db.utils import IntegrityError | ||||||
|  |  | ||||||
|  |  | ||||||
| from ..util import get_language_codes | from ..util import get_language_codes | ||||||
| from ..mixins import AtomicSlugRetryMixin |  | ||||||
| from ..localized_value import LocalizedValue | from ..localized_value import LocalizedValue | ||||||
| from .localized_autoslug_field import LocalizedAutoSlugField | from .localized_field import LocalizedField | ||||||
|  |  | ||||||
|  |  | ||||||
| class LocalizedUniqueSlugField(LocalizedAutoSlugField): | class LocalizedUniqueSlugField(LocalizedField): | ||||||
|     """Automatically provides slugs for a localized |     """Automatically provides slugs for a localized field upon saving." | ||||||
|     field upon saving." |  | ||||||
|  |  | ||||||
|     An improved version of :see:LocalizedAutoSlugField, |     An improved version of :see:LocalizedAutoSlugField, | ||||||
|     which adds: |     which adds: | ||||||
| @@ -21,8 +22,6 @@ class LocalizedUniqueSlugField(LocalizedAutoSlugField): | |||||||
|         - Improved performance |         - Improved performance | ||||||
|  |  | ||||||
|     When in doubt, use this over :see:LocalizedAutoSlugField. |     When in doubt, use this over :see:LocalizedAutoSlugField. | ||||||
|     Inherit from :see:AtomicSlugRetryMixin in your model to |  | ||||||
|     make this field work properly. |  | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     def __init__(self, *args, **kwargs): |     def __init__(self, *args, **kwargs): | ||||||
| @@ -30,14 +29,11 @@ class LocalizedUniqueSlugField(LocalizedAutoSlugField): | |||||||
|  |  | ||||||
|         kwargs['uniqueness'] = kwargs.pop('uniqueness', get_language_codes()) |         kwargs['uniqueness'] = kwargs.pop('uniqueness', get_language_codes()) | ||||||
|  |  | ||||||
|         super(LocalizedUniqueSlugField, self).__init__( |  | ||||||
|             *args, |  | ||||||
|             **kwargs |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         self.populate_from = kwargs.pop('populate_from') |         self.populate_from = kwargs.pop('populate_from') | ||||||
|         self.include_time = kwargs.pop('include_time', False) |         self.include_time = kwargs.pop('include_time', False) | ||||||
|  |  | ||||||
|  |         super().__init__(*args, **kwargs) | ||||||
|  |  | ||||||
|     def deconstruct(self): |     def deconstruct(self): | ||||||
|         """Deconstructs the field into something the database |         """Deconstructs the field into something the database | ||||||
|         can store.""" |         can store.""" | ||||||
| @@ -49,36 +45,88 @@ class LocalizedUniqueSlugField(LocalizedAutoSlugField): | |||||||
|         kwargs['include_time'] = self.include_time |         kwargs['include_time'] = self.include_time | ||||||
|         return name, path, args, kwargs |         return name, path, args, kwargs | ||||||
|  |  | ||||||
|     def pre_save(self, instance, add: bool): |     def formfield(self, **kwargs): | ||||||
|         """Ran just before the model is saved, allows us to built |         """Gets the form field associated with this field. | ||||||
|         the slug. |  | ||||||
|  |         Because this is a slug field which is automatically | ||||||
|  |         populated, it should be hidden from the form. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         defaults = { | ||||||
|  |             'form_class': forms.CharField, | ||||||
|  |             'required': False | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         defaults.update(kwargs) | ||||||
|  |  | ||||||
|  |         form_field = super().formfield(**defaults) | ||||||
|  |         form_field.widget = forms.HiddenInput() | ||||||
|  |  | ||||||
|  |         return form_field | ||||||
|  |  | ||||||
|  |     def contribute_to_class(self, cls, name, *args, **kwargs): | ||||||
|  |         """Hook that allow us to operate with model class. We overwrite save() | ||||||
|  |         method to run retry logic. | ||||||
|  |  | ||||||
|  |         Arguments: | ||||||
|  |             cls: | ||||||
|  |                 Model class. | ||||||
|  |  | ||||||
|  |             name: | ||||||
|  |                 Name of field in model. | ||||||
|  |         """ | ||||||
|  |         # apparently in inheritance cases, contribute_to_class is called more | ||||||
|  |         # than once, so we have to be careful not to overwrite the original | ||||||
|  |         # save method. | ||||||
|  |         if not hasattr(cls, '_orig_save'): | ||||||
|  |             cls._orig_save = cls.save | ||||||
|  |             max_retries = getattr( | ||||||
|  |                 settings, | ||||||
|  |                 'LOCALIZED_FIELDS_MAX_RETRIES', | ||||||
|  |                 100 | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |             def _new_save(instance, *args_, **kwargs_): | ||||||
|  |                 retries = 0 | ||||||
|  |                 while True: | ||||||
|  |                     with transaction.atomic(): | ||||||
|  |                         try: | ||||||
|  |                             slugs = self.populate_slugs(instance, retries) | ||||||
|  |                             setattr(instance, name, slugs) | ||||||
|  |                             instance._orig_save(*args_, **kwargs_) | ||||||
|  |                             break | ||||||
|  |                         except IntegrityError as e: | ||||||
|  |                             if retries >= max_retries: | ||||||
|  |                                 raise e | ||||||
|  |                             # check to be sure a slug fight caused | ||||||
|  |                             # the IntegrityError | ||||||
|  |                             s_e = str(e) | ||||||
|  |                             if name in s_e and 'unique' in s_e: | ||||||
|  |                                 retries += 1 | ||||||
|  |                             else: | ||||||
|  |                                 raise e | ||||||
|  |  | ||||||
|  |             cls.save = _new_save | ||||||
|  |         super().contribute_to_class(cls, name, *args, **kwargs) | ||||||
|  |  | ||||||
|  |     def populate_slugs(self, instance, retries=0): | ||||||
|  |         """Built the slug from populate_from field. | ||||||
|  |  | ||||||
|         Arguments: |         Arguments: | ||||||
|             instance: |             instance: | ||||||
|                 The model that is being saved. |                 The model that is being saved. | ||||||
|  |  | ||||||
|             add: |             retries: | ||||||
|                 Indicates whether this is a new entry |                 The value of the current attempt. | ||||||
|                 to the database or an update. |  | ||||||
|  |  | ||||||
|         Returns: |         Returns: | ||||||
|             The localized slug that was generated. |             The localized slug that was generated. | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         if not isinstance(instance, AtomicSlugRetryMixin): |  | ||||||
|             raise ImproperlyConfigured(( |  | ||||||
|                 'Model \'%s\' does not inherit from AtomicSlugRetryMixin. ' |  | ||||||
|                 'Without this, the LocalizedUniqueSlugField will not work.' |  | ||||||
|             ) % type(instance).__name__) |  | ||||||
|  |  | ||||||
|         slugs = LocalizedValue() |         slugs = LocalizedValue() | ||||||
|  |         populates_slugs = getattr(instance, self.populate_from, {}) | ||||||
|         for lang_code, _ in settings.LANGUAGES: |         for lang_code, _ in settings.LANGUAGES: | ||||||
|             value = self._get_populate_from_value( |  | ||||||
|                 instance, |             value = populates_slugs.get(lang_code) | ||||||
|                 self.populate_from, |  | ||||||
|                 lang_code |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|             if not value: |             if not value: | ||||||
|                 continue |                 continue | ||||||
| @@ -98,13 +146,11 @@ class LocalizedUniqueSlugField(LocalizedAutoSlugField): | |||||||
|             if self.include_time: |             if self.include_time: | ||||||
|                 slug += '-%d' % datetime.now().microsecond |                 slug += '-%d' % datetime.now().microsecond | ||||||
|  |  | ||||||
|             if instance.retries > 0: |             if retries > 0: | ||||||
|                 # do not add another - if we already added time |                 # do not add another - if we already added time | ||||||
|                 if not self.include_time: |                 if not self.include_time: | ||||||
|                     slug += '-' |                     slug += '-' | ||||||
|                 slug += '%d' % instance.retries |                 slug += '%d' % retries | ||||||
|  |  | ||||||
|             slugs.set(lang_code, slug) |             slugs.set(lang_code, slug) | ||||||
|  |  | ||||||
|         setattr(instance, self.name, slugs) |  | ||||||
|         return slugs |         return slugs | ||||||
|   | |||||||
| @@ -1,38 +1,17 @@ | |||||||
| from django.db import transaction | from django.core.checks import Warning | ||||||
| from django.conf import settings |  | ||||||
| from django.db.utils import IntegrityError |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class AtomicSlugRetryMixin: | class AtomicSlugRetryMixin: | ||||||
|     """Makes :see:LocalizedUniqueSlugField work by retrying upon |     """A Mixin keeped for backwards compatibility""" | ||||||
|     violation of the UNIQUE constraint.""" |  | ||||||
|  |  | ||||||
|     def save(self, *args, **kwargs): |     @classmethod | ||||||
|         """Saves this model instance to the database.""" |     def check(cls, **kwargs): | ||||||
|  |         errors = super().check(**kwargs) | ||||||
|         max_retries = getattr( |         errors.append( | ||||||
|             settings, |             Warning( | ||||||
|             'LOCALIZED_FIELDS_MAX_RETRIES', |                 'localized_fields.AtomicSlugRetryMixin is deprecated', | ||||||
|             100 |                 hint='There is no need to use ' | ||||||
|  |                      'localized_fields.AtomicSlugRetryMixin', | ||||||
|  |                 obj=cls | ||||||
|             ) |             ) | ||||||
|  |         ) | ||||||
|         if not hasattr(self, 'retries'): |         return errors | ||||||
|             self.retries = 0 |  | ||||||
|  |  | ||||||
|         with transaction.atomic(): |  | ||||||
|             try: |  | ||||||
|                 return super().save(*args, **kwargs) |  | ||||||
|             except IntegrityError as ex: |  | ||||||
|                 # this is as retarded as it looks, there's no |  | ||||||
|                 # way we can put the retry logic inside the slug |  | ||||||
|                 # field class... we can also not only catch exceptions |  | ||||||
|                 # that apply to slug fields... so yea.. this is as |  | ||||||
|                 # retarded as it gets... i am sorry :( |  | ||||||
|                 if 'slug' not in str(ex): |  | ||||||
|                     raise ex |  | ||||||
|  |  | ||||||
|                 if self.retries >= max_retries: |  | ||||||
|                     raise ex |  | ||||||
|  |  | ||||||
|         self.retries += 1 |  | ||||||
|         return self.save() |  | ||||||
|   | |||||||
| @@ -14,7 +14,7 @@ def define_fake_model(name='TestModel', fields=None): | |||||||
|  |  | ||||||
|     if fields: |     if fields: | ||||||
|         attributes.update(fields) |         attributes.update(fields) | ||||||
|     model = type(name, (AtomicSlugRetryMixin,LocalizedModel,), attributes) |     model = type(name, (LocalizedModel,), attributes) | ||||||
|  |  | ||||||
|     return model |     return model | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user