9 Commits
2.4 ... v2.7

Author SHA1 Message Date
Swen Kooij
64c3c06612 Bumped version number to 2.7 2017-02-09 14:57:25 +02:00
Swen Kooij
b121dfc2d7 Added __eq__ operator to LocalizedValue 2017-02-09 14:57:08 +02:00
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
Swen Kooij
759d03133b Bump version number to 2.5 2017-02-02 17:09:15 +02:00
Swen Kooij
d5ed3ced40 Raise IntegrityError if it's not about the slug 2017-02-02 17:08:06 +02:00
12 changed files with 120 additions and 68 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
class MyModel(models.Model):
class MyModel(LocalizedModel):
title = LocalizedField(required=True)
* Make all languages optional:
.. code-block:: python
class MyModel(models.Model):
class MyModel(LocalizedModel):
title = LocalizedField(null=True)
**Uniqueness**
@@ -164,7 +164,7 @@ By default the values stored in a ``LocalizedField`` are *not unique*. You can e
.. code-block:: python
class MyModel(models.Model):
class MyModel(LocalizedModel):
title = LocalizedField(uniqueness=['en', 'ro'])
* 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
class MyModel(models.Model):
class MyModel(LocalizedModel):
title = LocalizedField(uniqueness=get_language_codes())
* Enforce uniqueness for one ore more languages **together** (similar to Django's ``unique_together``):
.. code-block:: python
class MyModel(models.Model):
class MyModel(LocalizedModel):
title = LocalizedField(uniqueness=[('en', 'ro')])
* 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
class MyModel(models.Model):
class MyModel(LocalizedModel):
title = LocalizedField(uniqueness=[(*get_language_codes())])
@@ -197,19 +197,20 @@ Other fields
^^^^^^^^^^^^
Besides ``LocalizedField``, there's also:
* ``LocalizedMagicSlugField``
* ``LocalizedUniqueSlugField``
Successor of ``LocalizedAutoSlugField`` that fixes concurrency issues and enforces
uniqueness of slugs on a database level. Usage is the exact same:
.. code-block:: python
from localized_fields.models import LocalizedModel
from localized_fields.fields import (LocalizedField,
LocalizedMagicSlugField)
from localized_fields import (LocalizedModel,
AtomicSlugRetryMixin,
LocalizedField,
LocalizedUniqueSlugField)
class MyModel(LocalizedModel):
class MyModel(AtomicSlugRetryMixin, LocalizedModel):
title = LocalizedField()
slug = LocalizedMagicSlugField(populate_from='title')
slug = LocalizedUniqueSlugField(populate_from='title')
* ``LocalizedAutoSlugField``
Automatically creates a slug for every language from the specified field.
@@ -218,15 +219,15 @@ Besides ``LocalizedField``, there's also:
.. code-block:: python
from localized_fields.models import LocalizedModel
from localized_fields.fields import (LocalizedField,
LocalizedAutoSlugField)
from localized_fields import (LocalizedModel,
LocalizedField,
LocalizedUniqueSlugField)
class MyModel(LocalizedModel):
title = LocalizedField()
slug = LocalizedAutoSlugField(populate_from='title')
This implementation is **NOT** concurrency safe, prefer ``LocalizedMagicSlugField``.
This implementation is **NOT** concurrency safe, prefer ``LocalizedUniqueSlugField``.
* ``LocalizedBleachField``
Automatically bleaches the content of the field.
@@ -236,9 +237,9 @@ Besides ``LocalizedField``, there's also:
.. code-block:: python
from localized_fields.models import LocalizedModel
from localized_fields.fields import (LocalizedField,
LocalizedBleachField)
from localized_fields import (LocalizedModel,
LocalizedField,
LocalizedBleachField)
class MyModel(LocalizedModel):
title = LocalizedField()

View File

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

View File

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

View File

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

View File

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

View File

@@ -76,6 +76,21 @@ class LocalizedValue:
return value or ''
def __eq__(self, other):
"""Compares :paramref:self to :paramref:other for
equality.
Returns:
True when :paramref:self is equal to :paramref:other.
And False when they are not.
"""
for lang_code, _ in settings.LANGUAGES:
if self.get(lang_code) != other.get(lang_code):
return False
return True
def __repr__(self): # pragma: no cover
"""Gets a textual representation of this object."""

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,31 +34,3 @@ class LocalizedModel(models.Model):
value = LocalizedValue()
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' in str(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(
name='django-localized-fields',
version='2.4',
version='2.7',
packages=find_packages(),
include_package_data=True,
license='MIT License',

View File

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

View File

@@ -100,6 +100,19 @@ class LocalizedValueTestCase(TestCase):
translation.activate(language)
assert str(localized_value) == value
@staticmethod
def test_eq():
"""Tests whether the __eq__ operator
of :see:LocalizedValue works properly."""
a = LocalizedValue({'en': 'a', 'ar': 'b'})
b = LocalizedValue({'en': 'a', 'ar': 'b'})
assert a == b
b.en = 'b'
assert a != b
@staticmethod
def test_str_fallback():
"""Tests whether the :see:LocalizedValue

View File

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