mirror of
https://github.com/SectorLabs/django-localized-fields.git
synced 2025-04-25 11:42:54 +03:00
Merge remote-tracking branch 'beer/uniqueslug'
# Conflicts: # README.rst # tests/fake_model.py
This commit is contained in:
commit
a9037b603a
@ -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')
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user