mirror of
				https://github.com/SectorLabs/django-localized-fields.git
				synced 2025-10-30 18:48:56 +03:00 
			
		
		
		
	LocalizedUniqueSlugField refactored
This commit is contained in:
		| @@ -194,11 +194,10 @@ Besides ``LocalizedField``, there's also: | ||||
|           .. code-block:: python | ||||
|  | ||||
|               from localized_fields import (LocalizedModel, | ||||
|                                             AtomicSlugRetryMixin, | ||||
|                                             LocalizedField, | ||||
|                                             LocalizedUniqueSlugField) | ||||
|  | ||||
|               class MyModel(AtomicSlugRetryMixin, LocalizedModel): | ||||
|               class MyModel(LocalizedModel): | ||||
|                    title = LocalizedField() | ||||
|                    slug = LocalizedUniqueSlugField(populate_from='title') | ||||
|  | ||||
|   | ||||
| @@ -1,18 +1,19 @@ | ||||
| from datetime import datetime | ||||
|  | ||||
| from django.conf import settings | ||||
| from django import forms | ||||
| 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 ..mixins import AtomicSlugRetryMixin | ||||
| from ..localized_value import LocalizedValue | ||||
| from .localized_autoslug_field import LocalizedAutoSlugField | ||||
| from .localized_field import LocalizedField | ||||
|  | ||||
|  | ||||
| class LocalizedUniqueSlugField(LocalizedAutoSlugField): | ||||
|     """Automatically provides slugs for a localized | ||||
|     field upon saving." | ||||
| class LocalizedUniqueSlugField(LocalizedField): | ||||
|     """Automatically provides slugs for a localized field upon saving." | ||||
|  | ||||
|     An improved version of :see:LocalizedAutoSlugField, | ||||
|     which adds: | ||||
| @@ -21,8 +22,6 @@ class LocalizedUniqueSlugField(LocalizedAutoSlugField): | ||||
|         - Improved performance | ||||
|  | ||||
|     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): | ||||
| @@ -30,14 +29,11 @@ class LocalizedUniqueSlugField(LocalizedAutoSlugField): | ||||
|  | ||||
|         kwargs['uniqueness'] = kwargs.pop('uniqueness', get_language_codes()) | ||||
|  | ||||
|         super(LocalizedUniqueSlugField, self).__init__( | ||||
|             *args, | ||||
|             **kwargs | ||||
|         ) | ||||
|  | ||||
|         self.populate_from = kwargs.pop('populate_from') | ||||
|         self.include_time = kwargs.pop('include_time', False) | ||||
|  | ||||
|         super().__init__(*args, **kwargs) | ||||
|  | ||||
|     def deconstruct(self): | ||||
|         """Deconstructs the field into something the database | ||||
|         can store.""" | ||||
| @@ -49,36 +45,88 @@ class LocalizedUniqueSlugField(LocalizedAutoSlugField): | ||||
|         kwargs['include_time'] = self.include_time | ||||
|         return name, path, args, kwargs | ||||
|  | ||||
|     def pre_save(self, instance, add: bool): | ||||
|         """Ran just before the model is saved, allows us to built | ||||
|         the slug. | ||||
|     def formfield(self, **kwargs): | ||||
|         """Gets the form field associated with this field. | ||||
|  | ||||
|         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: | ||||
|             instance: | ||||
|                 The model that is being saved. | ||||
|  | ||||
|             add: | ||||
|                 Indicates whether this is a new entry | ||||
|                 to the database or an update. | ||||
|             retries: | ||||
|                 The value of the current attempt. | ||||
|  | ||||
|         Returns: | ||||
|             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() | ||||
|  | ||||
|         populates_slugs = getattr(instance, self.populate_from, {}) | ||||
|         for lang_code, _ in settings.LANGUAGES: | ||||
|             value = self._get_populate_from_value( | ||||
|                 instance, | ||||
|                 self.populate_from, | ||||
|                 lang_code | ||||
|             ) | ||||
|  | ||||
|             value = populates_slugs.get(lang_code) | ||||
|  | ||||
|             if not value: | ||||
|                 continue | ||||
| @@ -98,13 +146,11 @@ class LocalizedUniqueSlugField(LocalizedAutoSlugField): | ||||
|             if self.include_time: | ||||
|                 slug += '-%d' % datetime.now().microsecond | ||||
|  | ||||
|             if instance.retries > 0: | ||||
|             if retries > 0: | ||||
|                 # do not add another - if we already added time | ||||
|                 if not self.include_time: | ||||
|                     slug += '-' | ||||
|                 slug += '%d' % instance.retries | ||||
|                 slug += '%d' % retries | ||||
|  | ||||
|             slugs.set(lang_code, slug) | ||||
|  | ||||
|         setattr(instance, self.name, slugs) | ||||
|         return slugs | ||||
|   | ||||
| @@ -1,38 +1,17 @@ | ||||
| from django.db import transaction | ||||
| from django.conf import settings | ||||
| from django.db.utils import IntegrityError | ||||
|  | ||||
| from django.core.checks import Warning | ||||
|  | ||||
| class AtomicSlugRetryMixin: | ||||
|     """Makes :see:LocalizedUniqueSlugField work by retrying upon | ||||
|     violation of the UNIQUE constraint.""" | ||||
|     """A Mixin keeped for backwards compatibility""" | ||||
|  | ||||
|     def save(self, *args, **kwargs): | ||||
|         """Saves this model instance to the database.""" | ||||
|  | ||||
|         max_retries = getattr( | ||||
|             settings, | ||||
|             'LOCALIZED_FIELDS_MAX_RETRIES', | ||||
|             100 | ||||
|     @classmethod | ||||
|     def check(cls, **kwargs): | ||||
|         errors = super().check(**kwargs) | ||||
|         errors.append( | ||||
|             Warning( | ||||
|                 'localized_fields.AtomicSlugRetryMixin is deprecated', | ||||
|                 hint='There is no need to use ' | ||||
|                      'localized_fields.AtomicSlugRetryMixin', | ||||
|                 obj=cls | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
|         if not hasattr(self, 'retries'): | ||||
|             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() | ||||
|         return errors | ||||
|   | ||||
| @@ -14,7 +14,7 @@ def define_fake_model(name='TestModel', fields=None): | ||||
|  | ||||
|     if fields: | ||||
|         attributes.update(fields) | ||||
|     model = type(name, (AtomicSlugRetryMixin,LocalizedModel,), attributes) | ||||
|     model = type(name, (LocalizedModel,), attributes) | ||||
|  | ||||
|     return model | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user