diff --git a/README.rst b/README.rst index f81a963..490305d 100644 --- a/README.rst +++ b/README.rst @@ -192,11 +192,11 @@ Besides ``LocalizedField``, there's also: .. code-block:: python - from localized_fields import (AtomicSlugRetryMixin, + from localized_fields import (LocalizedModel, LocalizedField, LocalizedUniqueSlugField) - class MyModel(AtomicSlugRetryMixin, models.Model): + class MyModel(LocalizedModel): title = LocalizedField() slug = LocalizedUniqueSlugField(populate_from='title') diff --git a/localized_fields/fields/localized_uniqueslug_field.py b/localized_fields/fields/localized_uniqueslug_field.py index a5ac993..120089a 100644 --- a/localized_fields/fields/localized_uniqueslug_field.py +++ b/localized_fields/fields/localized_uniqueslug_field.py @@ -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 diff --git a/localized_fields/mixins.py b/localized_fields/mixins.py index 1402715..dc3b2b2 100644 --- a/localized_fields/mixins.py +++ b/localized_fields/mixins.py @@ -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 diff --git a/tests/fake_model.py b/tests/fake_model.py index b5fcc21..d79e217 100644 --- a/tests/fake_model.py +++ b/tests/fake_model.py @@ -1,9 +1,8 @@ from django.db import connection, migrations -from django.db import models from django.db.migrations.executor import MigrationExecutor from django.contrib.postgres.operations import HStoreExtension -from localized_fields import AtomicSlugRetryMixin +from localized_fields import LocalizedModel def define_fake_model(name='TestModel', fields=None): @@ -15,8 +14,8 @@ def define_fake_model(name='TestModel', fields=None): if fields: attributes.update(fields) - model = type(name, (AtomicSlugRetryMixin, models.Model), attributes) + model = type(name, (LocalizedModel,), attributes) return model