mirror of
https://github.com/SectorLabs/django-localized-fields.git
synced 2025-10-21 15:18:58 +03:00
Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
d529da8886 | ||
|
ca879087ea | ||
|
302a64a02c | ||
|
bb11253207 | ||
|
5db87763fb | ||
|
759d03133b | ||
|
d5ed3ced40 |
39
README.rst
39
README.rst
@@ -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()
|
||||
|
@@ -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'
|
||||
]
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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',
|
||||
]
|
||||
|
@@ -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:
|
38
localized_fields/mixins.py
Normal file
38
localized_fields/mixins.py
Normal 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()
|
@@ -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()
|
||||
|
2
setup.py
2
setup.py
@@ -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.6',
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
license='MIT License',
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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):
|
||||
|
Reference in New Issue
Block a user