5 Commits
2.5 ... v2.6

Author SHA1 Message Date
Swen Kooij
d529da8886 Fixed bug with with missing populate_from 2017-02-03 11:14:37 +02:00
Swen Kooij
ca879087ea Bumped version to 2.6 2017-02-03 10:41:05 +02:00
Swen Kooij
302a64a02c Updated base classes in documentation 2017-02-03 10:40:37 +02:00
Swen Kooij
bb11253207 Moved retry mechanism to mixin 2017-02-03 10:35:39 +02:00
Swen Kooij
5db87763fb Rename LocalizedMagicSlugField to LocalizedUniqueSlugField 2017-02-03 10:27:30 +02:00
10 changed files with 92 additions and 70 deletions

View File

@@ -146,14 +146,14 @@ At the moment, it is not possible to select two languages to be marked as requir
.. code-block:: python .. code-block:: python
class MyModel(models.Model): class MyModel(LocalizedModel):
title = LocalizedField(required=True) title = LocalizedField(required=True)
* Make all languages optional: * Make all languages optional:
.. code-block:: python .. code-block:: python
class MyModel(models.Model): class MyModel(LocalizedModel):
title = LocalizedField(null=True) title = LocalizedField(null=True)
**Uniqueness** **Uniqueness**
@@ -164,7 +164,7 @@ By default the values stored in a ``LocalizedField`` are *not unique*. You can e
.. code-block:: python .. code-block:: python
class MyModel(models.Model): class MyModel(LocalizedModel):
title = LocalizedField(uniqueness=['en', 'ro']) title = LocalizedField(uniqueness=['en', 'ro'])
* Enforce uniqueness for **all** languages: * Enforce uniqueness for **all** languages:
@@ -173,14 +173,14 @@ By default the values stored in a ``LocalizedField`` are *not unique*. You can e
from localized_fields import get_language_codes from localized_fields import get_language_codes
class MyModel(models.Model): class MyModel(LocalizedModel):
title = LocalizedField(uniqueness=get_language_codes()) title = LocalizedField(uniqueness=get_language_codes())
* Enforce uniqueness for one ore more languages **together** (similar to Django's ``unique_together``): * Enforce uniqueness for one ore more languages **together** (similar to Django's ``unique_together``):
.. code-block:: python .. code-block:: python
class MyModel(models.Model): class MyModel(LocalizedModel):
title = LocalizedField(uniqueness=[('en', 'ro')]) title = LocalizedField(uniqueness=[('en', 'ro')])
* Enforce uniqueness for **all** languages **together**: * Enforce uniqueness for **all** languages **together**:
@@ -189,7 +189,7 @@ By default the values stored in a ``LocalizedField`` are *not unique*. You can e
from localized_fields import get_language_codes from localized_fields import get_language_codes
class MyModel(models.Model): class MyModel(LocalizedModel):
title = LocalizedField(uniqueness=[(*get_language_codes())]) title = LocalizedField(uniqueness=[(*get_language_codes())])
@@ -197,19 +197,20 @@ Other fields
^^^^^^^^^^^^ ^^^^^^^^^^^^
Besides ``LocalizedField``, there's also: Besides ``LocalizedField``, there's also:
* ``LocalizedMagicSlugField`` * ``LocalizedUniqueSlugField``
Successor of ``LocalizedAutoSlugField`` that fixes concurrency issues and enforces Successor of ``LocalizedAutoSlugField`` that fixes concurrency issues and enforces
uniqueness of slugs on a database level. Usage is the exact same: uniqueness of slugs on a database level. Usage is the exact same:
.. code-block:: python .. code-block:: python
from localized_fields.models import LocalizedModel from localized_fields import (LocalizedModel,
from localized_fields.fields import (LocalizedField, AtomicSlugRetryMixin,
LocalizedMagicSlugField) LocalizedField,
LocalizedUniqueSlugField)
class MyModel(LocalizedModel): class MyModel(AtomicSlugRetryMixin, LocalizedModel):
title = LocalizedField() title = LocalizedField()
slug = LocalizedMagicSlugField(populate_from='title') slug = LocalizedUniqueSlugField(populate_from='title')
* ``LocalizedAutoSlugField`` * ``LocalizedAutoSlugField``
Automatically creates a slug for every language from the specified field. Automatically creates a slug for every language from the specified field.
@@ -218,15 +219,15 @@ Besides ``LocalizedField``, there's also:
.. code-block:: python .. code-block:: python
from localized_fields.models import LocalizedModel from localized_fields import (LocalizedModel,
from localized_fields.fields import (LocalizedField, LocalizedField,
LocalizedAutoSlugField) LocalizedUniqueSlugField)
class MyModel(LocalizedModel): class MyModel(LocalizedModel):
title = LocalizedField() title = LocalizedField()
slug = LocalizedAutoSlugField(populate_from='title') slug = LocalizedAutoSlugField(populate_from='title')
This implementation is **NOT** concurrency safe, prefer ``LocalizedMagicSlugField``. This implementation is **NOT** concurrency safe, prefer ``LocalizedUniqueSlugField``.
* ``LocalizedBleachField`` * ``LocalizedBleachField``
Automatically bleaches the content of the field. Automatically bleaches the content of the field.
@@ -236,9 +237,9 @@ Besides ``LocalizedField``, there's also:
.. code-block:: python .. code-block:: python
from localized_fields.models import LocalizedModel from localized_fields import (LocalizedModel,
from localized_fields.fields import (LocalizedField, LocalizedField,
LocalizedBleachField) LocalizedBleachField)
class MyModel(LocalizedModel): class MyModel(LocalizedModel):
title = LocalizedField() title = LocalizedField()

View File

@@ -1,18 +1,20 @@
from .util import get_language_codes from .util import get_language_codes
from .forms import LocalizedFieldForm, LocalizedFieldWidget from .forms import LocalizedFieldForm, LocalizedFieldWidget
from .fields import (LocalizedField, LocalizedBleachField, from .fields import (LocalizedField, LocalizedBleachField,
LocalizedAutoSlugField, LocalizedMagicSlugField) LocalizedAutoSlugField, LocalizedUniqueSlugField)
from .localized_value import LocalizedValue from .mixins import AtomicSlugRetryMixin
from .models import LocalizedModel from .models import LocalizedModel
from .localized_value import LocalizedValue
__all__ = [ __all__ = [
'get_language_codes', 'get_language_codes',
'LocalizedField', 'LocalizedField',
'LocalizedValue', 'LocalizedValue',
'LocalizedAutoSlugField', 'LocalizedAutoSlugField',
'LocalizedMagicSlugField', 'LocalizedUniqueSlugField',
'LocalizedBleachField', 'LocalizedBleachField',
'LocalizedFieldWidget', 'LocalizedFieldWidget',
'LocalizedFieldForm', 'LocalizedFieldForm',
'LocalizedModel' 'LocalizedModel',
'AtomicSlugRetryMixin'
] ]

View File

@@ -34,14 +34,14 @@ def _get_backend_base():
'\'%s\' is not a valid database back-end.' '\'%s\' is not a valid database back-end.'
' The module does not define a DatabaseWrapper class.' ' The module does not define a DatabaseWrapper class.'
' Check the value of LOCALIZED_FIELDS_DB_BACKEND_BASE.' ' Check the value of LOCALIZED_FIELDS_DB_BACKEND_BASE.'
)) ) % base_class_name)
if isinstance(base_class, Psycopg2DatabaseWrapper): if isinstance(base_class, Psycopg2DatabaseWrapper):
raise ImproperlyConfigured(( raise ImproperlyConfigured((
'\'%s\' is not a valid database back-end.' '\'%s\' is not a valid database back-end.'
' It does inherit from the PostgreSQL back-end.' ' It does inherit from the PostgreSQL back-end.'
' Check the value of LOCALIZED_FIELDS_DB_BACKEND_BASE.' ' Check the value of LOCALIZED_FIELDS_DB_BACKEND_BASE.'
)) ) % base_class_name)
return base_class return base_class

View File

@@ -1,12 +1,12 @@
from .localized_field import LocalizedField from .localized_field import LocalizedField
from .localized_autoslug_field import LocalizedAutoSlugField from .localized_autoslug_field import LocalizedAutoSlugField
from .localized_magicslug_field import LocalizedMagicSlugField from .localized_uniqueslug_field import LocalizedUniqueSlugField
from .localized_bleach_field import LocalizedBleachField from .localized_bleach_field import LocalizedBleachField
__all__ = [ __all__ = [
'LocalizedField', 'LocalizedField',
'LocalizedAutoSlugField', 'LocalizedAutoSlugField',
'LocalizedMagicSlugField', 'LocalizedUniqueSlugField',
'LocalizedBleachField', 'LocalizedBleachField',
] ]

View File

@@ -1,12 +1,14 @@
from django.conf import settings from django.conf import settings
from django.utils.text import slugify from django.utils.text import slugify
from django.core.exceptions import ImproperlyConfigured
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_autoslug_field import LocalizedAutoSlugField
from ..util import get_language_codes
class LocalizedMagicSlugField(LocalizedAutoSlugField): class LocalizedUniqueSlugField(LocalizedAutoSlugField):
"""Automatically provides slugs for a localized """Automatically provides slugs for a localized
field upon saving." field upon saving."
@@ -17,19 +19,22 @@ class LocalizedMagicSlugField(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):
"""Initializes a new instance of :see:LocalizedMagicSlugField.""" """Initializes a new instance of :see:LocalizedUniqueSlugField."""
self.populate_from = kwargs.pop('populate_from')
kwargs['uniqueness'] = kwargs.pop('uniqueness', get_language_codes()) kwargs['uniqueness'] = kwargs.pop('uniqueness', get_language_codes())
super(LocalizedAutoSlugField, self).__init__( super(LocalizedUniqueSlugField, self).__init__(
*args, *args,
**kwargs **kwargs
) )
self.populate_from = kwargs.pop('populate_from')
def pre_save(self, instance, add: bool): def pre_save(self, instance, add: bool):
"""Ran just before the model is saved, allows us to built """Ran just before the model is saved, allows us to built
the slug. the slug.
@@ -46,6 +51,12 @@ class LocalizedMagicSlugField(LocalizedAutoSlugField):
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()
for lang_code, _ in settings.LANGUAGES: for lang_code, _ in settings.LANGUAGES:

View File

@@ -0,0 +1,38 @@
from django.db import transaction
from django.conf import settings
from django.db.utils import IntegrityError
class AtomicSlugRetryMixin:
"""Makes :see:LocalizedUniqueSlugField work by retrying upon
violation of the UNIQUE constraint."""
def save(self, *args, **kwargs):
"""Saves this model instance to the database."""
max_retries = getattr(
settings,
'LOCALIZED_FIELDS_MAX_RETRIES',
100
)
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

@@ -34,33 +34,3 @@ class LocalizedModel(models.Model):
value = LocalizedValue() value = LocalizedValue()
setattr(self, field.name, value) setattr(self, field.name, value)
def save(self, *args, **kwargs):
"""Saves this model instance to the database."""
max_retries = getattr(
settings,
'LOCALIZED_FIELDS_MAX_RETRIES',
100
)
if not hasattr(self, 'retries'):
self.retries = 0
with transaction.atomic():
try:
return super(LocalizedModel, self).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

@@ -7,7 +7,7 @@ with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme:
setup( setup(
name='django-localized-fields', name='django-localized-fields',
version='2.5', version='2.6',
packages=find_packages(), packages=find_packages(),
include_package_data=True, include_package_data=True,
license='MIT License', license='MIT License',

View File

@@ -2,7 +2,7 @@ from django.db import connection, migrations
from django.db.migrations.executor import MigrationExecutor from django.db.migrations.executor import MigrationExecutor
from django.contrib.postgres.operations import HStoreExtension from django.contrib.postgres.operations import HStoreExtension
from localized_fields import LocalizedModel from localized_fields import LocalizedModel, AtomicSlugRetryMixin
def define_fake_model(name='TestModel', fields=None): def define_fake_model(name='TestModel', fields=None):
@@ -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, (LocalizedModel,), attributes) model = type(name, (AtomicSlugRetryMixin,LocalizedModel,), attributes)
return model return model

View File

@@ -5,7 +5,7 @@ from django.db.utils import IntegrityError
from django.utils.text import slugify from django.utils.text import slugify
from localized_fields import (LocalizedField, LocalizedAutoSlugField, from localized_fields import (LocalizedField, LocalizedAutoSlugField,
LocalizedMagicSlugField) LocalizedUniqueSlugField)
from .fake_model import get_fake_model from .fake_model import get_fake_model
@@ -31,10 +31,10 @@ class LocalizedSlugFieldTestCase(TestCase):
) )
cls.MagicSlugModel = get_fake_model( cls.MagicSlugModel = get_fake_model(
'LocalizedMagicSlugFieldTestModel', 'LocalizedUniqueSlugFieldTestModel',
{ {
'title': LocalizedField(), 'title': LocalizedField(),
'slug': LocalizedMagicSlugField(populate_from='title') 'slug': LocalizedUniqueSlugField(populate_from='title')
} }
) )
@@ -92,7 +92,7 @@ class LocalizedSlugFieldTestCase(TestCase):
@classmethod @classmethod
def test_deconstruct_magic(cls): def test_deconstruct_magic(cls):
cls._test_deconstruct(LocalizedMagicSlugField) cls._test_deconstruct(LocalizedUniqueSlugField)
@classmethod @classmethod
def test_formfield_auto(cls): def test_formfield_auto(cls):
@@ -100,7 +100,7 @@ class LocalizedSlugFieldTestCase(TestCase):
@classmethod @classmethod
def test_formfield_magic(cls): def test_formfield_magic(cls):
cls._test_formfield(LocalizedMagicSlugField) cls._test_formfield(LocalizedUniqueSlugField)
@staticmethod @staticmethod
def _test_populate(model): def _test_populate(model):