Revert "LocalizedUniqueSlugField refactored"

This reverts commit 03df76d6d795f4453629bf526cc04a86c6393adc.
This commit is contained in:
Swen Kooij 2017-05-25 17:23:39 +03:00
parent bf2995fd27
commit cff22855c2
4 changed files with 75 additions and 98 deletions

View File

@ -193,10 +193,11 @@ 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(LocalizedModel): class MyModel(AtomicSlugRetryMixin, LocalizedModel):
title = LocalizedField() title = LocalizedField()
slug = LocalizedUniqueSlugField(populate_from='title') slug = LocalizedUniqueSlugField(populate_from='title')

View File

@ -1,19 +1,18 @@
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.db import transaction from django.core.exceptions import ImproperlyConfigured
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_field import LocalizedField from .localized_autoslug_field import LocalizedAutoSlugField
class LocalizedUniqueSlugField(LocalizedField): class LocalizedUniqueSlugField(LocalizedAutoSlugField):
"""Automatically provides slugs for a localized field upon saving." """Automatically provides slugs for a localized
field upon saving."
An improved version of :see:LocalizedAutoSlugField, An improved version of :see:LocalizedAutoSlugField,
which adds: which adds:
@ -22,6 +21,8 @@ class LocalizedUniqueSlugField(LocalizedField):
- 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):
@ -29,11 +30,14 @@ class LocalizedUniqueSlugField(LocalizedField):
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."""
@ -45,88 +49,36 @@ class LocalizedUniqueSlugField(LocalizedField):
kwargs['include_time'] = self.include_time kwargs['include_time'] = self.include_time
return name, path, args, kwargs return name, path, args, kwargs
def formfield(self, **kwargs): def pre_save(self, instance, add: bool):
"""Gets the form field associated with this field. """Ran just before the model is saved, allows us to built
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.
retries: add:
The value of the current attempt. Indicates whether this is a new entry
to the database or an update.
Returns: Returns:
The localized slug that was generated. The localized slug that was generated.
""" """
slugs = LocalizedValue()
populates_slugs = getattr(instance, self.populate_from, {})
for lang_code, _ in settings.LANGUAGES:
value = populates_slugs.get(lang_code) 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()
for lang_code, _ in settings.LANGUAGES:
value = self._get_populate_from_value(
instance,
self.populate_from,
lang_code
)
if not value: if not value:
continue continue
@ -146,11 +98,13 @@ class LocalizedUniqueSlugField(LocalizedField):
if self.include_time: if self.include_time:
slug += '-%d' % datetime.now().microsecond slug += '-%d' % datetime.now().microsecond
if retries > 0: if instance.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' % retries slug += '%d' % instance.retries
slugs.set(lang_code, slug) slugs.set(lang_code, slug)
setattr(instance, self.name, slugs)
return slugs return slugs

View File

@ -1,17 +1,38 @@
from django.core.checks import Warning from django.db import transaction
from django.conf import settings
from django.db.utils import IntegrityError
class AtomicSlugRetryMixin: class AtomicSlugRetryMixin:
"""A Mixin keeped for backwards compatibility""" """Makes :see:LocalizedUniqueSlugField work by retrying upon
violation of the UNIQUE constraint."""
@classmethod def save(self, *args, **kwargs):
def check(cls, **kwargs): """Saves this model instance to the database."""
errors = super().check(**kwargs)
errors.append( max_retries = getattr(
Warning( settings,
'localized_fields.AtomicSlugRetryMixin is deprecated', 'LOCALIZED_FIELDS_MAX_RETRIES',
hint='There is no need to use ' 100
'localized_fields.AtomicSlugRetryMixin',
obj=cls
) )
)
return errors 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()

View File

@ -3,6 +3,7 @@ from django.db.migrations.executor import MigrationExecutor
from django.contrib.postgres.operations import HStoreExtension from django.contrib.postgres.operations import HStoreExtension
from localized_fields.models import LocalizedModel from localized_fields.models import LocalizedModel
from localized_fields.mixins import AtomicSlugRetryMixin
def define_fake_model(name='TestModel', fields=None): def define_fake_model(name='TestModel', fields=None):
@ -15,7 +16,7 @@ def define_fake_model(name='TestModel', fields=None):
if fields: if fields:
attributes.update(fields) attributes.update(fields)
model = type(name, (LocalizedModel,), attributes) model = type(name, (AtomicSlugRetryMixin,LocalizedModel,), attributes)
return model return model