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