LocalizedUniqueSlugField refactored

This commit is contained in:
seroy 2017-03-18 22:58:11 +03:00
parent 3951266747
commit 03df76d6d7
4 changed files with 97 additions and 73 deletions

View File

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

View File

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

View File

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

View File

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