mirror of
https://github.com/SectorLabs/django-localized-fields.git
synced 2025-06-27 13:53:19 +03:00
Merge branch 'master' of https://github.com/SectorLabs/django-localized-fields into extra-fields
This commit is contained in:
commit
aacc712195
20
README.rst
20
README.rst
@ -32,7 +32,7 @@ Installation
|
|||||||
....
|
....
|
||||||
|
|
||||||
'django.contrib.postgres',
|
'django.contrib.postgres',
|
||||||
'localized_fields'
|
'localized_fields.apps.LocalizedFieldsConfig'
|
||||||
]
|
]
|
||||||
|
|
||||||
3. Set the database engine to ``psqlextra.backend``:
|
3. Set the database engine to ``psqlextra.backend``:
|
||||||
@ -160,7 +160,7 @@ By default the values stored in a ``LocalizedField`` are *not unique*. You can e
|
|||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from localized_fields import get_language_codes
|
from localized_fields.util import get_language_codes
|
||||||
|
|
||||||
class MyModel(models.Model):
|
class MyModel(models.Model):
|
||||||
title = LocalizedField(uniqueness=get_language_codes())
|
title = LocalizedField(uniqueness=get_language_codes())
|
||||||
@ -176,7 +176,7 @@ By default the values stored in a ``LocalizedField`` are *not unique*. You can e
|
|||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from localized_fields import get_language_codes
|
from localized_fields.util import get_language_codes
|
||||||
|
|
||||||
class MyModel(models.Model):
|
class MyModel(models.Model):
|
||||||
title = LocalizedField(uniqueness=[(*get_language_codes())])
|
title = LocalizedField(uniqueness=[(*get_language_codes())])
|
||||||
@ -192,9 +192,8 @@ Besides ``LocalizedField``, there's also:
|
|||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from localized_fields import (LocalizedModel,
|
from localized_fields.models import LocalizedModel
|
||||||
LocalizedField,
|
from localized_fields.fields import LocalizedField, LocalizedUniqueSlugField
|
||||||
LocalizedUniqueSlugField)
|
|
||||||
|
|
||||||
class MyModel(LocalizedModel):
|
class MyModel(LocalizedModel):
|
||||||
title = LocalizedField()
|
title = LocalizedField()
|
||||||
@ -216,10 +215,9 @@ Besides ``LocalizedField``, there's also:
|
|||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from localized_fields import (LocalizedField,
|
from localized_fields.fields import LocalizedField, LocalizedAutoSlugField
|
||||||
LocalizedUniqueSlugField)
|
|
||||||
|
|
||||||
class MyModel(models.Model):
|
class MyModel(LocalizedModel):
|
||||||
title = LocalizedField()
|
title = LocalizedField()
|
||||||
slug = LocalizedAutoSlugField(populate_from='title')
|
slug = LocalizedAutoSlugField(populate_from='title')
|
||||||
|
|
||||||
@ -227,14 +225,14 @@ Besides ``LocalizedField``, there's also:
|
|||||||
|
|
||||||
* ``LocalizedBleachField``
|
* ``LocalizedBleachField``
|
||||||
Automatically bleaches the content of the field.
|
Automatically bleaches the content of the field.
|
||||||
|
|
||||||
* django-bleach
|
* django-bleach
|
||||||
|
|
||||||
Example usage:
|
Example usage:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from localized_fields import (LocalizedField,
|
from localized_fields.fields import LocalizedField, LocalizedBleachField
|
||||||
LocalizedBleachField)
|
|
||||||
|
|
||||||
class MyModel(models.Model):
|
class MyModel(models.Model):
|
||||||
title = LocalizedField()
|
title = LocalizedField()
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
from .forms import LocalizedFieldForm, LocalizedFieldWidget
|
|
||||||
from .fields import (LocalizedAutoSlugField, LocalizedField,
|
|
||||||
LocalizedUniqueSlugField, LocalizedCharField,
|
|
||||||
LocalizedTextField, LocalizedFileField)
|
|
||||||
from .localized_value import LocalizedValue
|
|
||||||
from .mixins import AtomicSlugRetryMixin
|
|
||||||
from .models import LocalizedModel
|
|
||||||
from .util import get_language_codes
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'get_language_codes',
|
|
||||||
'LocalizedField',
|
|
||||||
'LocalizedModel',
|
|
||||||
'LocalizedValue',
|
|
||||||
'LocalizedAutoSlugField',
|
|
||||||
'LocalizedUniqueSlugField',
|
|
||||||
'LocalizedBleachField',
|
|
||||||
'LocalizedCharField',
|
|
||||||
'LocalizedTextField',
|
|
||||||
'LocalizedFileField',
|
|
||||||
'LocalizedFieldWidget',
|
|
||||||
'LocalizedFieldForm',
|
|
||||||
'AtomicSlugRetryMixin'
|
|
||||||
]
|
|
||||||
|
|
||||||
try:
|
|
||||||
from .fields import LocalizedBleachField
|
|
||||||
__all__ += [
|
|
||||||
'LocalizedBleachField'
|
|
||||||
]
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
@ -1,8 +1,9 @@
|
|||||||
from django.contrib.admin import ModelAdmin
|
from django.contrib.admin import ModelAdmin
|
||||||
|
|
||||||
|
from . import widgets
|
||||||
from .fields import LocalizedField, LocalizedCharField, LocalizedTextField, \
|
from .fields import LocalizedField, LocalizedCharField, LocalizedTextField, \
|
||||||
LocalizedFileField
|
LocalizedFileField
|
||||||
from . import widgets
|
|
||||||
|
|
||||||
|
|
||||||
FORMFIELD_FOR_LOCALIZED_FIELDS_DEFAULTS = {
|
FORMFIELD_FOR_LOCALIZED_FIELDS_DEFAULTS = {
|
||||||
@ -14,17 +15,22 @@ FORMFIELD_FOR_LOCALIZED_FIELDS_DEFAULTS = {
|
|||||||
|
|
||||||
|
|
||||||
class LocalizedFieldsAdminMixin(ModelAdmin):
|
class LocalizedFieldsAdminMixin(ModelAdmin):
|
||||||
|
"""Mixin for making the fancy widgets work in Django Admin."""
|
||||||
|
|
||||||
class Media:
|
class Media:
|
||||||
css = {
|
css = {
|
||||||
'all': (
|
'all': (
|
||||||
'localized_fields/localized-fields-admin.css',
|
'localized_fields/localized-fields-admin.css',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
js = (
|
js = (
|
||||||
'localized_fields/localized-fields-admin.js',
|
'localized_fields/localized-fields-admin.js',
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""Initializes a new instance of :see:LocalizedFieldsAdminMixin."""
|
||||||
|
|
||||||
super(LocalizedFieldsAdminMixin, self).__init__(*args, **kwargs)
|
super(LocalizedFieldsAdminMixin, self).__init__(*args, **kwargs)
|
||||||
overrides = FORMFIELD_FOR_LOCALIZED_FIELDS_DEFAULTS.copy()
|
overrides = FORMFIELD_FOR_LOCALIZED_FIELDS_DEFAULTS.copy()
|
||||||
overrides.update(self.formfield_overrides)
|
overrides.update(self.formfield_overrides)
|
||||||
|
65
localized_fields/descriptor.py
Normal file
65
localized_fields/descriptor.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
from django.utils import six, translation
|
||||||
|
|
||||||
|
|
||||||
|
class LocalizedValueDescriptor:
|
||||||
|
"""
|
||||||
|
The descriptor for the localized value attribute on the model instance.
|
||||||
|
Returns a :see:LocalizedValue when accessed so you can do stuff like::
|
||||||
|
|
||||||
|
>>> from myapp.models import MyModel
|
||||||
|
>>> instance = MyModel()
|
||||||
|
>>> instance.value.en = 'English value'
|
||||||
|
|
||||||
|
Assigns a strings to active language key in :see:LocalizedValue on
|
||||||
|
assignment so you can do::
|
||||||
|
|
||||||
|
>>> from django.utils import translation
|
||||||
|
>>> from myapp.models import MyModel
|
||||||
|
|
||||||
|
>>> translation.activate('nl')
|
||||||
|
>>> instance = MyModel()
|
||||||
|
>>> instance.title = 'dutch title'
|
||||||
|
>>> print(instance.title.nl) # prints 'dutch title'
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, field):
|
||||||
|
"""Initializes a new instance of :see:LocalizedValueDescriptor."""
|
||||||
|
|
||||||
|
self.field = field
|
||||||
|
|
||||||
|
def __get__(self, instance, cls=None):
|
||||||
|
if instance is None:
|
||||||
|
return self
|
||||||
|
|
||||||
|
# This is slightly complicated, so worth an explanation.
|
||||||
|
# `instance.localizedvalue` needs to ultimately return some instance of
|
||||||
|
# `LocalizedValue`, probably a subclass.
|
||||||
|
|
||||||
|
# The instance dict contains whatever was originally assigned
|
||||||
|
# in __set__.
|
||||||
|
|
||||||
|
if self.field.name in instance.__dict__:
|
||||||
|
value = instance.__dict__[self.field.name]
|
||||||
|
elif instance.pk is not None:
|
||||||
|
instance.refresh_from_db(fields=[self.field.name])
|
||||||
|
value = getattr(instance, self.field.name)
|
||||||
|
else:
|
||||||
|
value = None
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
attr = self.field.attr_class()
|
||||||
|
instance.__dict__[self.field.name] = attr
|
||||||
|
|
||||||
|
if isinstance(value, dict):
|
||||||
|
attr = self.field.attr_class(value)
|
||||||
|
instance.__dict__[self.field.name] = attr
|
||||||
|
|
||||||
|
return instance.__dict__[self.field.name]
|
||||||
|
|
||||||
|
def __set__(self, instance, value):
|
||||||
|
if isinstance(value, six.string_types):
|
||||||
|
language = translation.get_language() or settings.LANGUAGE_CODE
|
||||||
|
self.__get__(instance).set(language, value) # pylint: disable=no-member
|
||||||
|
else:
|
||||||
|
instance.__dict__[self.field.name] = value
|
25
localized_fields/expressions.py
Normal file
25
localized_fields/expressions.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
from django.utils import translation
|
||||||
|
|
||||||
|
from psqlextra import expressions
|
||||||
|
|
||||||
|
|
||||||
|
class LocalizedRef(expressions.HStoreRef):
|
||||||
|
"""Expression that selects the value in a field only in
|
||||||
|
the currently active language."""
|
||||||
|
|
||||||
|
def __init__(self, name: str, lang: str=None):
|
||||||
|
"""Initializes a new instance of :see:LocalizedRef.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
name:
|
||||||
|
The field/column to select from.
|
||||||
|
|
||||||
|
lang:
|
||||||
|
The language to get the field/column in.
|
||||||
|
If not specified, the currently active language
|
||||||
|
is used.
|
||||||
|
"""
|
||||||
|
|
||||||
|
language = lang or translation.get_language() or settings.LANGUAGE_CODE
|
||||||
|
super().__init__(name, language)
|
@ -1,6 +1,6 @@
|
|||||||
from .localized_field import LocalizedField
|
from .field import LocalizedField
|
||||||
from .localized_autoslug_field import LocalizedAutoSlugField
|
from .autoslug_field import LocalizedAutoSlugField
|
||||||
from .localized_uniqueslug_field import LocalizedUniqueSlugField
|
from .uniqueslug_field import LocalizedUniqueSlugField
|
||||||
from .localized_char_field import LocalizedCharField
|
from .localized_char_field import LocalizedCharField
|
||||||
from .localized_text_field import LocalizedTextField
|
from .localized_text_field import LocalizedTextField
|
||||||
from .localized_file_field import LocalizedFileField
|
from .localized_file_field import LocalizedFileField
|
||||||
@ -16,7 +16,7 @@ __all__ = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from .localized_bleach_field import LocalizedBleachField
|
from .bleach_field import LocalizedBleachField
|
||||||
__all__ += [
|
__all__ += [
|
||||||
'LocalizedBleachField'
|
'LocalizedBleachField'
|
||||||
]
|
]
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
from typing import Callable
|
from typing import Callable, Tuple
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
|
|
||||||
from .localized_field import LocalizedField
|
from .field import LocalizedField
|
||||||
from ..localized_value import LocalizedValue
|
from ..value import LocalizedValue
|
||||||
|
|
||||||
|
|
||||||
class LocalizedAutoSlugField(LocalizedField):
|
class LocalizedAutoSlugField(LocalizedField):
|
||||||
@ -69,13 +69,7 @@ class LocalizedAutoSlugField(LocalizedField):
|
|||||||
|
|
||||||
slugs = LocalizedValue()
|
slugs = LocalizedValue()
|
||||||
|
|
||||||
for lang_code, _ in settings.LANGUAGES:
|
for lang_code, value in self._get_populate_values(instance):
|
||||||
value = self._get_populate_from_value(
|
|
||||||
instance,
|
|
||||||
self.populate_from,
|
|
||||||
lang_code
|
|
||||||
)
|
|
||||||
|
|
||||||
if not value:
|
if not value:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -128,6 +122,30 @@ class LocalizedAutoSlugField(LocalizedField):
|
|||||||
|
|
||||||
return unique_slug
|
return unique_slug
|
||||||
|
|
||||||
|
def _get_populate_values(self, instance) -> Tuple[str, str]:
|
||||||
|
"""Gets all values (for each language) from the
|
||||||
|
specified's instance's `populate_from` field.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
instance:
|
||||||
|
The instance to get the values from.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of (lang_code, value) tuples.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return [
|
||||||
|
(
|
||||||
|
lang_code,
|
||||||
|
self._get_populate_from_value(
|
||||||
|
instance,
|
||||||
|
self.populate_from,
|
||||||
|
lang_code
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for lang_code, _ in settings.LANGUAGES
|
||||||
|
]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_populate_from_value(instance, field_name: str, language: str):
|
def _get_populate_from_value(instance, field_name: str, language: str):
|
||||||
"""Gets the value to create a slug from in the specified language.
|
"""Gets the value to create a slug from in the specified language.
|
@ -1,8 +1,9 @@
|
|||||||
import bleach
|
import bleach
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django_bleach.utils import get_bleach_default_options
|
from django_bleach.utils import get_bleach_default_options
|
||||||
|
|
||||||
from .localized_field import LocalizedField
|
from .field import LocalizedField
|
||||||
|
|
||||||
|
|
||||||
class LocalizedBleachField(LocalizedField):
|
class LocalizedBleachField(LocalizedField):
|
@ -1,70 +1,15 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
from django.utils import six, translation
|
|
||||||
|
|
||||||
from psqlextra.fields import HStoreField
|
from psqlextra.fields import HStoreField
|
||||||
|
|
||||||
from ..forms import LocalizedFieldForm
|
from ..forms import LocalizedFieldForm
|
||||||
from ..localized_value import LocalizedValue
|
from ..value import LocalizedValue
|
||||||
|
from ..descriptor import LocalizedValueDescriptor
|
||||||
|
|
||||||
class LocalizedValueDescriptor(object):
|
|
||||||
"""
|
|
||||||
The descriptor for the localized value attribute on the model instance.
|
|
||||||
Returns a :see:LocalizedValue when accessed so you can do stuff like::
|
|
||||||
|
|
||||||
>>> from myapp.models import MyModel
|
|
||||||
>>> instance = MyModel()
|
|
||||||
>>> instance.value.en = 'English value'
|
|
||||||
|
|
||||||
Assigns a strings to active language key in :see:LocalizedValue on
|
|
||||||
assignment so you can do::
|
|
||||||
|
|
||||||
>>> from django.utils import translation
|
|
||||||
>>> from myapp.models import MyModel
|
|
||||||
|
|
||||||
>>> translation.activate('nl')
|
|
||||||
>>> instance = MyModel()
|
|
||||||
>>> instance.title = 'dutch title'
|
|
||||||
>>> print(instance.title.nl) # prints 'dutch title'
|
|
||||||
"""
|
|
||||||
def __init__(self, field):
|
|
||||||
self.field = field
|
|
||||||
|
|
||||||
def __get__(self, instance, cls=None):
|
|
||||||
if instance is None:
|
|
||||||
return self
|
|
||||||
|
|
||||||
# This is slightly complicated, so worth an explanation.
|
|
||||||
# `instance.localizedvalue` needs to ultimately return some instance of
|
|
||||||
# `LocalizedValue`, probably a subclass.
|
|
||||||
|
|
||||||
# The instance dict contains whatever was originally assigned
|
|
||||||
# in __set__.
|
|
||||||
if self.field.name in instance.__dict__:
|
|
||||||
value = instance.__dict__[self.field.name]
|
|
||||||
elif instance.pk is not None:
|
|
||||||
instance.refresh_from_db(fields=[self.field.name])
|
|
||||||
value = getattr(instance, self.field.name)
|
|
||||||
else:
|
|
||||||
value = None
|
|
||||||
|
|
||||||
if value is None:
|
|
||||||
attr = self.field.attr_class()
|
|
||||||
instance.__dict__[self.field.name] = attr
|
|
||||||
|
|
||||||
if isinstance(value, dict):
|
|
||||||
attr = self.field.attr_class(value)
|
|
||||||
instance.__dict__[self.field.name] = attr
|
|
||||||
|
|
||||||
return instance.__dict__[self.field.name]
|
|
||||||
|
|
||||||
def __set__(self, instance, value):
|
|
||||||
if isinstance(value, six.string_types):
|
|
||||||
self.__get__(instance).set(translation.get_language() or
|
|
||||||
settings.LANGUAGE_CODE, value)
|
|
||||||
else:
|
|
||||||
instance.__dict__[self.field.name] = value
|
|
||||||
|
|
||||||
|
|
||||||
class LocalizedField(HStoreField):
|
class LocalizedField(HStoreField):
|
||||||
@ -87,9 +32,18 @@ class LocalizedField(HStoreField):
|
|||||||
|
|
||||||
super(LocalizedField, self).__init__(*args, **kwargs)
|
super(LocalizedField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def contribute_to_class(self, cls, name, **kwargs):
|
def contribute_to_class(self, model, name, **kwargs):
|
||||||
super(LocalizedField, self).contribute_to_class(cls, name, **kwargs)
|
"""Adds this field to the specifed model.
|
||||||
setattr(cls, self.name, self.descriptor_class(self))
|
|
||||||
|
Arguments:
|
||||||
|
cls:
|
||||||
|
The model to add the field to.
|
||||||
|
|
||||||
|
name:
|
||||||
|
The name of the field to add.
|
||||||
|
"""
|
||||||
|
super(LocalizedField, self).contribute_to_class(model, name, **kwargs)
|
||||||
|
setattr(model, self.name, self.descriptor_class(self))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_db_value(cls, value, *_):
|
def from_db_value(cls, value, *_):
|
||||||
@ -112,9 +66,31 @@ class LocalizedField(HStoreField):
|
|||||||
else:
|
else:
|
||||||
return cls.attr_class()
|
return cls.attr_class()
|
||||||
|
|
||||||
|
# we can get a list if an aggregation expression was used..
|
||||||
|
# if we the expression was flattened when only one key was selected
|
||||||
|
# then we don't wrap each value in a localized value, otherwise we do
|
||||||
|
if isinstance(value, list):
|
||||||
|
result = []
|
||||||
|
for inner_val in value:
|
||||||
|
if isinstance(inner_val, dict):
|
||||||
|
if inner_val is None:
|
||||||
|
result.append(None)
|
||||||
|
else:
|
||||||
|
result.append(cls.attr_class(inner_val))
|
||||||
|
else:
|
||||||
|
result.append(inner_val)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
# this is for when you select an individual key, it will be string,
|
||||||
|
# not a dictionary, we'll give it to you as a flat value, not as a
|
||||||
|
# localized value instance
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
return value
|
||||||
|
|
||||||
return cls.attr_class(value)
|
return cls.attr_class(value)
|
||||||
|
|
||||||
def to_python(self, value: dict) -> LocalizedValue:
|
def to_python(self, value: Union[dict, str, None]) -> LocalizedValue:
|
||||||
"""Turns the specified database value into its Python
|
"""Turns the specified database value into its Python
|
||||||
equivalent.
|
equivalent.
|
||||||
|
|
||||||
@ -128,10 +104,17 @@ class LocalizedField(HStoreField):
|
|||||||
data extracted from the database.
|
data extracted from the database.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not value or not isinstance(value, dict):
|
# first let the base class handle the deserialization, this is in case we
|
||||||
|
# get specified a json string representing a dict
|
||||||
|
try:
|
||||||
|
deserialized_value = super(LocalizedField, self).to_python(value)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
deserialized_value = value
|
||||||
|
|
||||||
|
if not deserialized_value:
|
||||||
return self.attr_class()
|
return self.attr_class()
|
||||||
|
|
||||||
return self.attr_class(value)
|
return self.attr_class(deserialized_value)
|
||||||
|
|
||||||
def get_prep_value(self, value: LocalizedValue) -> dict:
|
def get_prep_value(self, value: LocalizedValue) -> dict:
|
||||||
"""Turns the specified value into something the database
|
"""Turns the specified value into something the database
|
@ -1,6 +1,6 @@
|
|||||||
from ..forms import LocalizedCharFieldForm
|
from ..forms import LocalizedCharFieldForm
|
||||||
from .localized_field import LocalizedField
|
from .field import LocalizedField
|
||||||
from ..localized_value import LocalizedStringValue
|
from ..value import LocalizedStringValue
|
||||||
|
|
||||||
|
|
||||||
class LocalizedCharField(LocalizedField):
|
class LocalizedCharField(LocalizedField):
|
||||||
|
@ -8,10 +8,10 @@ from django.core.files.storage import default_storage
|
|||||||
from django.utils.encoding import force_str, force_text
|
from django.utils.encoding import force_str, force_text
|
||||||
|
|
||||||
from localized_fields.fields import LocalizedField
|
from localized_fields.fields import LocalizedField
|
||||||
from localized_fields.fields.localized_field import LocalizedValueDescriptor
|
from localized_fields.fields.field import LocalizedValueDescriptor
|
||||||
from localized_fields.localized_value import LocalizedValue
|
from localized_fields.value import LocalizedValue
|
||||||
|
|
||||||
from ..localized_value import LocalizedFileValue
|
from ..value import LocalizedFileValue
|
||||||
from ..forms import LocalizedFileFieldForm
|
from ..forms import LocalizedFileFieldForm
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,156 +0,0 @@
|
|||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django import forms
|
|
||||||
from django.utils.text import slugify
|
|
||||||
from django.db import transaction
|
|
||||||
from django.db.utils import IntegrityError
|
|
||||||
|
|
||||||
|
|
||||||
from ..util import get_language_codes
|
|
||||||
from ..localized_value import LocalizedValue
|
|
||||||
from .localized_field import LocalizedField
|
|
||||||
|
|
||||||
|
|
||||||
class LocalizedUniqueSlugField(LocalizedField):
|
|
||||||
"""Automatically provides slugs for a localized field upon saving."
|
|
||||||
|
|
||||||
An improved version of :see:LocalizedAutoSlugField,
|
|
||||||
which adds:
|
|
||||||
|
|
||||||
- Concurrency safety
|
|
||||||
- Improved performance
|
|
||||||
|
|
||||||
When in doubt, use this over :see:LocalizedAutoSlugField.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
"""Initializes a new instance of :see:LocalizedUniqueSlugField."""
|
|
||||||
|
|
||||||
kwargs['uniqueness'] = kwargs.pop('uniqueness', get_language_codes())
|
|
||||||
|
|
||||||
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."""
|
|
||||||
|
|
||||||
name, path, args, kwargs = super(
|
|
||||||
LocalizedUniqueSlugField, self).deconstruct()
|
|
||||||
|
|
||||||
kwargs['populate_from'] = self.populate_from
|
|
||||||
kwargs['include_time'] = self.include_time
|
|
||||||
return name, path, args, kwargs
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
retries:
|
|
||||||
The value of the current attempt.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The localized slug that was generated.
|
|
||||||
"""
|
|
||||||
slugs = LocalizedValue()
|
|
||||||
populates_slugs = getattr(instance, self.populate_from, {})
|
|
||||||
for lang_code, _ in settings.LANGUAGES:
|
|
||||||
|
|
||||||
value = populates_slugs.get(lang_code)
|
|
||||||
|
|
||||||
if not value:
|
|
||||||
continue
|
|
||||||
|
|
||||||
slug = slugify(value, allow_unicode=True)
|
|
||||||
|
|
||||||
# verify whether it's needed to re-generate a slug,
|
|
||||||
# if not, re-use the same slug
|
|
||||||
if instance.pk is not None:
|
|
||||||
current_slug = getattr(instance, self.name).get(lang_code)
|
|
||||||
if current_slug is not None:
|
|
||||||
stripped_slug = current_slug[0:current_slug.rfind('-')]
|
|
||||||
if slug == stripped_slug:
|
|
||||||
slugs.set(lang_code, current_slug)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if self.include_time:
|
|
||||||
slug += '-%d' % datetime.now().microsecond
|
|
||||||
|
|
||||||
if retries > 0:
|
|
||||||
# do not add another - if we already added time
|
|
||||||
if not self.include_time:
|
|
||||||
slug += '-'
|
|
||||||
slug += '%d' % retries
|
|
||||||
|
|
||||||
slugs.set(lang_code, slug)
|
|
||||||
return slugs
|
|
104
localized_fields/fields/uniqueslug_field.py
Normal file
104
localized_fields/fields/uniqueslug_field.py
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from django.utils.text import slugify
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
|
||||||
|
from .autoslug_field import LocalizedAutoSlugField
|
||||||
|
from ..util import get_language_codes
|
||||||
|
from ..mixins import AtomicSlugRetryMixin
|
||||||
|
from ..value import LocalizedValue
|
||||||
|
|
||||||
|
|
||||||
|
class LocalizedUniqueSlugField(LocalizedAutoSlugField):
|
||||||
|
"""Automatically provides slugs for a localized
|
||||||
|
field upon saving."
|
||||||
|
|
||||||
|
An improved version of :see:LocalizedAutoSlugField,
|
||||||
|
which adds:
|
||||||
|
|
||||||
|
- Concurrency safety
|
||||||
|
- 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:LocalizedUniqueSlugField."""
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
def deconstruct(self):
|
||||||
|
"""Deconstructs the field into something the database
|
||||||
|
can store."""
|
||||||
|
|
||||||
|
name, path, args, kwargs = super(
|
||||||
|
LocalizedUniqueSlugField, self).deconstruct()
|
||||||
|
|
||||||
|
kwargs['populate_from'] = self.populate_from
|
||||||
|
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.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
instance:
|
||||||
|
The model that is being saved.
|
||||||
|
|
||||||
|
add:
|
||||||
|
Indicates whether this is a new entry
|
||||||
|
to the database or an update.
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
for lang_code, value in self._get_populate_values(instance):
|
||||||
|
if not value:
|
||||||
|
continue
|
||||||
|
|
||||||
|
slug = slugify(value, allow_unicode=True)
|
||||||
|
|
||||||
|
# verify whether it's needed to re-generate a slug,
|
||||||
|
# if not, re-use the same slug
|
||||||
|
if instance.pk is not None:
|
||||||
|
current_slug = getattr(instance, self.name).get(lang_code)
|
||||||
|
if current_slug is not None:
|
||||||
|
stripped_slug = current_slug[0:current_slug.rfind('-')]
|
||||||
|
if slug == stripped_slug:
|
||||||
|
slugs.set(lang_code, current_slug)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if self.include_time:
|
||||||
|
slug += '-%d' % datetime.now().microsecond
|
||||||
|
|
||||||
|
retries = getattr(instance, 'retries', 0)
|
||||||
|
if retries > 0:
|
||||||
|
# do not add another - if we already added time
|
||||||
|
if not self.include_time:
|
||||||
|
slug += '-'
|
||||||
|
slug += '%d' % retries
|
||||||
|
|
||||||
|
slugs.set(lang_code, slug)
|
||||||
|
|
||||||
|
setattr(instance, self.name, slugs)
|
||||||
|
return slugs
|
@ -5,7 +5,7 @@ from django.conf import settings
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.forms.widgets import FILE_INPUT_CONTRADICTION
|
from django.forms.widgets import FILE_INPUT_CONTRADICTION
|
||||||
|
|
||||||
from .localized_value import LocalizedValue, LocalizedStringValue, \
|
from .value import LocalizedValue, LocalizedStringValue, \
|
||||||
LocalizedFileValue
|
LocalizedFileValue
|
||||||
from .widgets import LocalizedFieldWidget, LocalizedCharFieldWidget, \
|
from .widgets import LocalizedFieldWidget, LocalizedCharFieldWidget, \
|
||||||
LocalizedFileWidget
|
LocalizedFileWidget
|
||||||
|
@ -1,132 +0,0 @@
|
|||||||
"""This module is unused, but should be contributed to Django."""
|
|
||||||
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from django.db import models
|
|
||||||
|
|
||||||
|
|
||||||
class HStoreIndex(models.Index):
|
|
||||||
"""Allows creating a index on a specific HStore index.
|
|
||||||
|
|
||||||
Note: pieces of code in this class have been copied
|
|
||||||
from the base class. There was no way around this."""
|
|
||||||
|
|
||||||
def __init__(self, field: str, keys: List[str], unique: bool=False,
|
|
||||||
name: str=''):
|
|
||||||
"""Initializes a new instance of :see:HStoreIndex.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
field:
|
|
||||||
Name of the hstore field for
|
|
||||||
which's keys to create a index for.
|
|
||||||
|
|
||||||
keys:
|
|
||||||
The name of the hstore keys to
|
|
||||||
create the index on.
|
|
||||||
|
|
||||||
unique:
|
|
||||||
Whether this index should
|
|
||||||
be marked as UNIQUE.
|
|
||||||
|
|
||||||
name:
|
|
||||||
The name of the index. If left
|
|
||||||
empty, one will be generated.
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.field = field
|
|
||||||
self.keys = keys
|
|
||||||
self.unique = unique
|
|
||||||
|
|
||||||
# this will eventually set self.name
|
|
||||||
super(HStoreIndex, self).__init__(
|
|
||||||
fields=[field],
|
|
||||||
name=name
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_sql_create_template_values(self, model, schema_editor, using):
|
|
||||||
"""Gets the values for the SQL template.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
model:
|
|
||||||
The model this index applies to.
|
|
||||||
|
|
||||||
schema_editor:
|
|
||||||
The schema editor to modify the schema.
|
|
||||||
|
|
||||||
using:
|
|
||||||
Optional: "USING" statement.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dictionary of keys to pass into the SQL template.
|
|
||||||
"""
|
|
||||||
|
|
||||||
fields = [model._meta.get_field(field_name) for field_name, order in self.fields_orders]
|
|
||||||
tablespace_sql = schema_editor._get_index_tablespace_sql(model, fields)
|
|
||||||
quote_name = schema_editor.quote_name
|
|
||||||
|
|
||||||
columns = [
|
|
||||||
'(%s->\'%s\')' % (self.field, key)
|
|
||||||
for key in self.keys
|
|
||||||
]
|
|
||||||
|
|
||||||
return {
|
|
||||||
'table': quote_name(model._meta.db_table),
|
|
||||||
'name': quote_name(self.name),
|
|
||||||
'columns': ', '.join(columns),
|
|
||||||
'using': using,
|
|
||||||
'extra': tablespace_sql,
|
|
||||||
}
|
|
||||||
|
|
||||||
def create_sql(self, model, schema_editor, using=''):
|
|
||||||
"""Gets the SQL to execute when creating the index.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
model:
|
|
||||||
The model this index applies to.
|
|
||||||
|
|
||||||
schema_editor:
|
|
||||||
The schema editor to modify the schema.
|
|
||||||
|
|
||||||
using:
|
|
||||||
Optional: "USING" statement.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
SQL string to execute to create this index.
|
|
||||||
"""
|
|
||||||
|
|
||||||
sql_create_index = schema_editor.sql_create_index
|
|
||||||
if self.unique:
|
|
||||||
sql_create_index = sql_create_index.replace('CREATE', 'CREATE UNIQUE')
|
|
||||||
sql_parameters = self.get_sql_create_template_values(model, schema_editor, using)
|
|
||||||
return sql_create_index % sql_parameters
|
|
||||||
|
|
||||||
def remove_sql(self, model, schema_editor):
|
|
||||||
"""Gets the SQL to execute to remove this index.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
model:
|
|
||||||
The model this index applies to.
|
|
||||||
|
|
||||||
schema_editor:
|
|
||||||
The schema editor to modify the schema.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
SQL string to execute to remove this index.
|
|
||||||
"""
|
|
||||||
quote_name = schema_editor.quote_name
|
|
||||||
return schema_editor.sql_delete_index % {
|
|
||||||
'table': quote_name(model._meta.db_table),
|
|
||||||
'name': quote_name(self.name),
|
|
||||||
}
|
|
||||||
|
|
||||||
def deconstruct(self):
|
|
||||||
"""Gets the values to pass to :see:__init__ when
|
|
||||||
re-creating this object."""
|
|
||||||
|
|
||||||
path = '%s.%s' % (self.__class__.__module__, self.__class__.__name__)
|
|
||||||
return (path, (), {
|
|
||||||
'field': self.field,
|
|
||||||
'keys': self.keys,
|
|
||||||
'unique': self.unique,
|
|
||||||
'name': self.name
|
|
||||||
})
|
|
@ -1,17 +1,38 @@
|
|||||||
from django.core.checks import Warning
|
from django.db import transaction
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db.utils import IntegrityError
|
||||||
|
|
||||||
|
|
||||||
class AtomicSlugRetryMixin:
|
class AtomicSlugRetryMixin:
|
||||||
"""A Mixin keeped for backwards compatibility"""
|
"""Makes :see:LocalizedUniqueSlugField work by retrying upon
|
||||||
|
violation of the UNIQUE constraint."""
|
||||||
|
|
||||||
@classmethod
|
def save(self, *args, **kwargs):
|
||||||
def check(cls, **kwargs):
|
"""Saves this model instance to the database."""
|
||||||
errors = super().check(**kwargs)
|
|
||||||
errors.append(
|
max_retries = getattr(
|
||||||
Warning(
|
settings,
|
||||||
'localized_fields.AtomicSlugRetryMixin is deprecated',
|
'LOCALIZED_FIELDS_MAX_RETRIES',
|
||||||
hint='There is no need to use '
|
100
|
||||||
'localized_fields.AtomicSlugRetryMixin',
|
|
||||||
obj=cls
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
return errors
|
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()
|
||||||
|
@ -1,21 +1,16 @@
|
|||||||
from django.db import models
|
from psqlextra.models import PostgresModel
|
||||||
from django.core.checks import Warning
|
|
||||||
|
from .mixins import AtomicSlugRetryMixin
|
||||||
|
|
||||||
|
|
||||||
class LocalizedModel(models.Model):
|
class LocalizedModel(AtomicSlugRetryMixin, PostgresModel):
|
||||||
"""A model keeped for backwards compatibility"""
|
"""Turns a model into a model that contains LocalizedField's.
|
||||||
|
|
||||||
@classmethod
|
For basic localisation functionality, it isn't needed to inherit
|
||||||
def check(cls, **kwargs):
|
from LocalizedModel. However, for certain features, this is required.
|
||||||
errors = super().check(**kwargs)
|
|
||||||
errors.append(
|
It is definitely needed for :see:LocalizedUniqueSlugField, unless you
|
||||||
Warning(
|
manually inherit from AtomicSlugRetryMixin."""
|
||||||
'localized_fields.LocalizedModel is deprecated',
|
|
||||||
hint='There is no need to use localized_fields.LocalizedModel',
|
|
||||||
obj=cls
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return errors
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
import collections
|
||||||
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import translation
|
from django.utils import translation
|
||||||
|
|
||||||
@ -16,17 +19,10 @@ class LocalizedValue(dict):
|
|||||||
different language.
|
different language.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# NOTE(seroy): First fill all the keys with default value,
|
super().__init__({})
|
||||||
# in order to attributes will be for each language
|
self._interpret_value(keys)
|
||||||
for lang_code, _ in settings.LANGUAGES:
|
|
||||||
value = keys.get(lang_code) if isinstance(keys, dict) else \
|
|
||||||
self.default_value
|
|
||||||
self.set(lang_code, value)
|
|
||||||
|
|
||||||
if isinstance(keys, str):
|
def get(self, language: str=None, default: str=None) -> str:
|
||||||
setattr(self, settings.LANGUAGE_CODE, keys)
|
|
||||||
|
|
||||||
def get(self, language: str=None) -> str:
|
|
||||||
"""Gets the underlying value in the specified or
|
"""Gets the underlying value in the specified or
|
||||||
primary language.
|
primary language.
|
||||||
|
|
||||||
@ -41,7 +37,7 @@ class LocalizedValue(dict):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
language = language or settings.LANGUAGE_CODE
|
language = language or settings.LANGUAGE_CODE
|
||||||
return super().get(language, None)
|
return super().get(language, default)
|
||||||
|
|
||||||
def set(self, language: str, value: str):
|
def set(self, language: str, value: str):
|
||||||
"""Sets the value in the specified language.
|
"""Sets the value in the specified language.
|
||||||
@ -69,6 +65,40 @@ class LocalizedValue(dict):
|
|||||||
path = 'localized_fields.localized_value.%s' % self.__class__.__name__
|
path = 'localized_fields.localized_value.%s' % self.__class__.__name__
|
||||||
return path, [self.__dict__], {}
|
return path, [self.__dict__], {}
|
||||||
|
|
||||||
|
def _interpret_value(self, value):
|
||||||
|
"""Interprets a value passed in the constructor as
|
||||||
|
a :see:LocalizedValue.
|
||||||
|
|
||||||
|
If string:
|
||||||
|
Assumes it's the default language.
|
||||||
|
|
||||||
|
If dict:
|
||||||
|
Each key is a language and the value a string
|
||||||
|
in that language.
|
||||||
|
|
||||||
|
If list:
|
||||||
|
Recurse into to apply rules above.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
value:
|
||||||
|
The value to interpret.
|
||||||
|
"""
|
||||||
|
|
||||||
|
for lang_code, _ in settings.LANGUAGES:
|
||||||
|
self.set(lang_code, self.default_value)
|
||||||
|
|
||||||
|
if isinstance(value, str):
|
||||||
|
self.set(settings.LANGUAGE_CODE, value)
|
||||||
|
|
||||||
|
elif isinstance(value, dict):
|
||||||
|
for lang_code, _ in settings.LANGUAGES:
|
||||||
|
lang_value = value.get(lang_code, self.default_value)
|
||||||
|
self.set(lang_code, lang_value)
|
||||||
|
|
||||||
|
elif isinstance(value, collections.Iterable):
|
||||||
|
for val in value:
|
||||||
|
self._interpret_value(val)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
"""Gets the value in the current language, or falls
|
"""Gets the value in the current language, or falls
|
||||||
back to the primary language if there's no value
|
back to the primary language if there's no value
|
@ -5,7 +5,7 @@ from django import forms
|
|||||||
from django.contrib.admin import widgets
|
from django.contrib.admin import widgets
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
|
|
||||||
from .localized_value import LocalizedValue
|
from .value import LocalizedValue
|
||||||
|
|
||||||
|
|
||||||
class LocalizedFieldWidget(forms.MultiWidget):
|
class LocalizedFieldWidget(forms.MultiWidget):
|
||||||
@ -15,12 +15,12 @@ class LocalizedFieldWidget(forms.MultiWidget):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Initializes a new instance of :see:LocalizedFieldWidget."""
|
"""Initializes a new instance of :see:LocalizedFieldWidget."""
|
||||||
|
|
||||||
widgets = []
|
initial_widgets = [
|
||||||
|
self.widget
|
||||||
|
for _ in settings.LANGUAGES
|
||||||
|
]
|
||||||
|
|
||||||
for _ in settings.LANGUAGES:
|
super().__init__(initial_widgets, *args, **kwargs)
|
||||||
widgets.append(self.widget)
|
|
||||||
|
|
||||||
super(LocalizedFieldWidget, self).__init__(widgets, *args, **kwargs)
|
|
||||||
|
|
||||||
def decompress(self, value: LocalizedValue) -> List[str]:
|
def decompress(self, value: LocalizedValue) -> List[str]:
|
||||||
"""Decompresses the specified value so
|
"""Decompresses the specified value so
|
||||||
@ -36,7 +36,6 @@ class LocalizedFieldWidget(forms.MultiWidget):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
|
|
||||||
for lang_code, _ in settings.LANGUAGES:
|
for lang_code, _ in settings.LANGUAGES:
|
||||||
if value:
|
if value:
|
||||||
result.append(value.get(lang_code))
|
result.append(value.get(lang_code))
|
||||||
@ -88,7 +87,8 @@ class AdminLocalizedFieldWidget(LocalizedFieldWidget):
|
|||||||
}
|
}
|
||||||
return render_to_string(self.template, context)
|
return render_to_string(self.template, context)
|
||||||
|
|
||||||
def build_widget_attrs(self, widget, value, attrs):
|
@staticmethod
|
||||||
|
def build_widget_attrs(widget, value, attrs):
|
||||||
attrs = dict(attrs) # Copy attrs to avoid modifying the argument.
|
attrs = dict(attrs) # Copy attrs to avoid modifying the argument.
|
||||||
if (not widget.use_required_attribute(value) or not widget.is_required) \
|
if (not widget.use_required_attribute(value) or not widget.is_required) \
|
||||||
and 'required' in attrs:
|
and 'required' in attrs:
|
||||||
|
@ -21,6 +21,7 @@ LANGUAGES = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
INSTALLED_APPS = (
|
INSTALLED_APPS = (
|
||||||
|
'localized_fields',
|
||||||
'tests',
|
'tests',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
4
setup.py
4
setup.py
@ -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='3.6',
|
version='4.1',
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
license='MIT License',
|
license='MIT License',
|
||||||
@ -18,7 +18,7 @@ setup(
|
|||||||
author_email='open-source@sectorlabs.ro',
|
author_email='open-source@sectorlabs.ro',
|
||||||
keywords=['django', 'localized', 'language', 'models', 'fields'],
|
keywords=['django', 'localized', 'language', 'models', 'fields'],
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'django-postgres-extra>=1.4'
|
'django-postgres-extra>=1.11'
|
||||||
],
|
],
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Environment :: Web Environment',
|
'Environment :: Web Environment',
|
||||||
|
13
tests/data.py
Normal file
13
tests/data.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
def get_init_values() -> dict:
|
||||||
|
"""Gets a test dictionary containing a key
|
||||||
|
for every language."""
|
||||||
|
|
||||||
|
keys = {}
|
||||||
|
|
||||||
|
for lang_code, lang_name in settings.LANGUAGES:
|
||||||
|
keys[lang_code] = 'value in %s' % lang_name
|
||||||
|
|
||||||
|
return keys
|
@ -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.models import LocalizedModel
|
||||||
|
|
||||||
|
|
||||||
def define_fake_model(name='TestModel', fields=None):
|
def define_fake_model(name='TestModel', fields=None):
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
|
import bleach
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django_bleach.utils import get_bleach_default_options
|
from django_bleach.utils import get_bleach_default_options
|
||||||
import bleach
|
|
||||||
|
|
||||||
from localized_fields import LocalizedBleachField, LocalizedValue
|
from localized_fields.fields import LocalizedBleachField
|
||||||
|
from localized_fields.value import LocalizedValue
|
||||||
|
|
||||||
|
|
||||||
class TestModel:
|
class TestModel:
|
45
tests/test_bulk.py
Normal file
45
tests/test_bulk.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from localized_fields.fields import LocalizedField, LocalizedUniqueSlugField
|
||||||
|
|
||||||
|
from .fake_model import get_fake_model
|
||||||
|
|
||||||
|
|
||||||
|
class LocalizedBulkTestCase(TestCase):
|
||||||
|
"""Tests bulk operations with data structures provided
|
||||||
|
by the django-localized-fields library."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def test_localized_bulk_insert():
|
||||||
|
"""Tests whether bulk inserts work properly when using
|
||||||
|
a :see:LocalizedUniqueSlugField in the model."""
|
||||||
|
|
||||||
|
model = get_fake_model(
|
||||||
|
'BulkSlugInsertModel',
|
||||||
|
{
|
||||||
|
'name': LocalizedField(),
|
||||||
|
'slug': LocalizedUniqueSlugField(populate_from='name', include_time=True),
|
||||||
|
'score': models.IntegerField()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
to_create = [
|
||||||
|
model(name={'en': 'english name 1', 'ro': 'romanian name 1'}, score=1),
|
||||||
|
model(name={'en': 'english name 2', 'ro': 'romanian name 2'}, score=2),
|
||||||
|
model(name={'en': 'english name 3', 'ro': 'romanian name 3'}, score=3)
|
||||||
|
]
|
||||||
|
|
||||||
|
model.objects.bulk_create(to_create)
|
||||||
|
assert model.objects.all().count() == 3
|
||||||
|
|
||||||
|
for obj in to_create:
|
||||||
|
obj_db = model.objects.filter(
|
||||||
|
name__en=obj.name.en,
|
||||||
|
name__ro=obj.name.ro,
|
||||||
|
score=obj.score
|
||||||
|
).first()
|
||||||
|
|
||||||
|
assert obj_db
|
||||||
|
assert len(obj_db.slug.en) >= len(obj_db.name.en)
|
||||||
|
assert len(obj_db.slug.ro) >= len(obj_db.name.ro)
|
86
tests/test_expressions.py
Normal file
86
tests/test_expressions.py
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
from django.db import models
|
||||||
|
from django.utils import translation
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.postgres.aggregates import ArrayAgg
|
||||||
|
|
||||||
|
from localized_fields.fields import LocalizedField
|
||||||
|
from localized_fields.value import LocalizedValue
|
||||||
|
from localized_fields.expressions import LocalizedRef
|
||||||
|
|
||||||
|
from .fake_model import get_fake_model
|
||||||
|
|
||||||
|
|
||||||
|
class LocalizedExpressionsTestCase(TestCase):
|
||||||
|
"""Tests whether expressions properly work with :see:LocalizedField."""
|
||||||
|
|
||||||
|
TestModel1 = None
|
||||||
|
TestModel2 = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
"""Creates the test model in the database."""
|
||||||
|
|
||||||
|
super(LocalizedExpressionsTestCase, cls).setUpClass()
|
||||||
|
|
||||||
|
cls.TestModel1 = get_fake_model(
|
||||||
|
'LocalizedExpressionsTestCase2',
|
||||||
|
{
|
||||||
|
'name': models.CharField(null=False, blank=False, max_length=255),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
cls.TestModel2 = get_fake_model(
|
||||||
|
'LocalizedExpressionsTestCase1',
|
||||||
|
{
|
||||||
|
'text': LocalizedField(),
|
||||||
|
'other': models.ForeignKey(cls.TestModel1, related_name='features')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def test_localized_ref(cls):
|
||||||
|
"""Tests whether the :see:LocalizedRef expression properly works."""
|
||||||
|
|
||||||
|
obj = cls.TestModel1.objects.create(name='bla bla')
|
||||||
|
for i in range(0, 10):
|
||||||
|
cls.TestModel2.objects.create(
|
||||||
|
text=LocalizedValue(dict(en='text_%d_en' % i, ro='text_%d_ro' % i, nl='text_%d_nl' % i)),
|
||||||
|
other=obj
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_queryset(ref):
|
||||||
|
return (
|
||||||
|
cls.TestModel1.objects
|
||||||
|
.annotate(mytexts=ref)
|
||||||
|
.values_list('mytexts', flat=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert that it properly selects the currently active language
|
||||||
|
for lang_code, _ in settings.LANGUAGES:
|
||||||
|
translation.activate(lang_code)
|
||||||
|
queryset = create_queryset(LocalizedRef('features__text'))
|
||||||
|
|
||||||
|
for index, value in enumerate(queryset):
|
||||||
|
assert translation.get_language() in value
|
||||||
|
assert str(index) in value
|
||||||
|
|
||||||
|
# ensure that the default language is used in case no
|
||||||
|
# language is active at all
|
||||||
|
translation.deactivate_all()
|
||||||
|
queryset = create_queryset(LocalizedRef('features__text'))
|
||||||
|
for index, value in enumerate(queryset):
|
||||||
|
assert settings.LANGUAGE_CODE in value
|
||||||
|
assert str(index) in value
|
||||||
|
|
||||||
|
# ensures that overriding the language works properly
|
||||||
|
queryset = create_queryset(LocalizedRef('features__text', 'ro'))
|
||||||
|
for index, value in enumerate(queryset):
|
||||||
|
assert 'ro' in value
|
||||||
|
assert str(index) in value
|
||||||
|
|
||||||
|
# ensures that using this in combination with ArrayAgg works properly
|
||||||
|
queryset = create_queryset(ArrayAgg(LocalizedRef('features__text', 'ro'))).first()
|
||||||
|
assert isinstance(queryset, list)
|
||||||
|
for value in queryset:
|
||||||
|
assert 'ro' in value
|
158
tests/test_field.py
Normal file
158
tests/test_field.py
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db.utils import IntegrityError
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from localized_fields.fields import LocalizedField
|
||||||
|
from localized_fields.forms import LocalizedFieldForm
|
||||||
|
from localized_fields.value import LocalizedValue
|
||||||
|
|
||||||
|
from .data import get_init_values
|
||||||
|
|
||||||
|
|
||||||
|
class LocalizedFieldTestCase(TestCase):
|
||||||
|
"""Tests the :see:LocalizedField class."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def test_from_db_value():
|
||||||
|
"""Tests whether the :see:from_db_value function
|
||||||
|
produces the expected :see:LocalizedValue."""
|
||||||
|
|
||||||
|
input_data = get_init_values()
|
||||||
|
localized_value = LocalizedField().from_db_value(input_data)
|
||||||
|
|
||||||
|
for lang_code, _ in settings.LANGUAGES:
|
||||||
|
assert getattr(localized_value, lang_code) == input_data[lang_code]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def test_from_db_value_none():
|
||||||
|
"""Tests whether the :see:from_db_value function
|
||||||
|
correctly handles None values."""
|
||||||
|
|
||||||
|
localized_value = LocalizedField().from_db_value(None)
|
||||||
|
|
||||||
|
for lang_code, _ in settings.LANGUAGES:
|
||||||
|
assert localized_value.get(lang_code) is None
|
||||||
|
|
||||||
|
def test_from_db_value_none_return_none(self):
|
||||||
|
"""Tests whether the :see:from_db_value function
|
||||||
|
correctly handles None values when LOCALIZED_FIELDS_EXPERIMENTAL
|
||||||
|
is set to True."""
|
||||||
|
|
||||||
|
with self.settings(LOCALIZED_FIELDS_EXPERIMENTAL=True):
|
||||||
|
localized_value = LocalizedField.from_db_value(None)
|
||||||
|
|
||||||
|
assert localized_value is None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def test_to_python():
|
||||||
|
"""Tests whether the :see:to_python function
|
||||||
|
produces the expected :see:LocalizedValue."""
|
||||||
|
|
||||||
|
input_data = get_init_values()
|
||||||
|
localized_value = LocalizedField().to_python(input_data)
|
||||||
|
|
||||||
|
for language, value in input_data.items():
|
||||||
|
assert localized_value.get(language) == value
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def test_to_python_non_json():
|
||||||
|
"""Tests whether the :see:to_python function
|
||||||
|
properly handles a string that is not JSON."""
|
||||||
|
|
||||||
|
localized_value = LocalizedField().to_python('my value')
|
||||||
|
assert localized_value.get() == 'my value'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def test_to_python_none():
|
||||||
|
"""Tests whether the :see:to_python function
|
||||||
|
produces the expected :see:LocalizedValue
|
||||||
|
instance when it is passes None."""
|
||||||
|
|
||||||
|
localized_value = LocalizedField().to_python(None)
|
||||||
|
assert localized_value
|
||||||
|
|
||||||
|
for lang_code, _ in settings.LANGUAGES:
|
||||||
|
assert localized_value.get(lang_code) is None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def test_to_python_non_dict():
|
||||||
|
"""Tests whether the :see:to_python function produces
|
||||||
|
the expected :see:LocalizedValue when it is
|
||||||
|
passed a non-dictionary value."""
|
||||||
|
|
||||||
|
localized_value = LocalizedField().to_python(list())
|
||||||
|
assert localized_value
|
||||||
|
|
||||||
|
for lang_code, _ in settings.LANGUAGES:
|
||||||
|
assert localized_value.get(lang_code) is None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def test_to_python_str():
|
||||||
|
"""Tests whether the :see:to_python function produces
|
||||||
|
the expected :see:LocalizedValue when it is
|
||||||
|
passed serialized string value."""
|
||||||
|
|
||||||
|
serialized_str = json.dumps(get_init_values())
|
||||||
|
localized_value = LocalizedField().to_python(serialized_str)
|
||||||
|
assert isinstance(localized_value, LocalizedValue)
|
||||||
|
|
||||||
|
for language, value in get_init_values().items():
|
||||||
|
assert localized_value.get(language) == value
|
||||||
|
assert getattr(localized_value, language) == value
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def test_get_prep_value():
|
||||||
|
""""Tests whether the :see:get_prep_value function
|
||||||
|
produces the expected dictionary."""
|
||||||
|
|
||||||
|
input_data = get_init_values()
|
||||||
|
localized_value = LocalizedValue(input_data)
|
||||||
|
|
||||||
|
output_data = LocalizedField().get_prep_value(localized_value)
|
||||||
|
|
||||||
|
for language, value in input_data.items():
|
||||||
|
assert language in output_data
|
||||||
|
assert output_data.get(language) == value
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def test_get_prep_value_none():
|
||||||
|
"""Tests whether the :see:get_prep_value function
|
||||||
|
produces the expected output when it is passed None."""
|
||||||
|
|
||||||
|
output_data = LocalizedField().get_prep_value(None)
|
||||||
|
assert not output_data
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def test_get_prep_value_no_localized_value():
|
||||||
|
"""Tests whether the :see:get_prep_value function
|
||||||
|
produces the expected output when it is passed a
|
||||||
|
non-LocalizedValue value."""
|
||||||
|
|
||||||
|
output_data = LocalizedField().get_prep_value(['huh'])
|
||||||
|
assert not output_data
|
||||||
|
|
||||||
|
def test_get_prep_value_clean(self):
|
||||||
|
"""Tests whether the :see:get_prep_value produces
|
||||||
|
None as the output when it is passed an empty, but
|
||||||
|
valid LocalizedValue value but, only when null=True."""
|
||||||
|
|
||||||
|
localized_value = LocalizedValue()
|
||||||
|
|
||||||
|
with self.assertRaises(IntegrityError):
|
||||||
|
LocalizedField(null=False).get_prep_value(localized_value)
|
||||||
|
|
||||||
|
assert not LocalizedField(null=True).get_prep_value(localized_value)
|
||||||
|
assert not LocalizedField().clean(None)
|
||||||
|
assert not LocalizedField().clean(['huh'])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def test_formfield():
|
||||||
|
"""Tests whether the :see:formfield function
|
||||||
|
correctly returns a valid form."""
|
||||||
|
|
||||||
|
assert isinstance(
|
||||||
|
LocalizedField().formfield(),
|
||||||
|
LocalizedFieldForm
|
||||||
|
)
|
@ -1,7 +1,7 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from localized_fields import LocalizedFieldForm
|
from localized_fields.forms import LocalizedFieldForm
|
||||||
|
|
||||||
|
|
||||||
class LocalizedFieldFormTestCase(TestCase):
|
class LocalizedFieldFormTestCase(TestCase):
|
@ -1,288 +0,0 @@
|
|||||||
from django.conf import settings
|
|
||||||
from django.db.utils import IntegrityError
|
|
||||||
from django.test import TestCase
|
|
||||||
from django.utils import translation
|
|
||||||
|
|
||||||
from localized_fields import LocalizedField, LocalizedFieldForm, LocalizedValue
|
|
||||||
|
|
||||||
|
|
||||||
def get_init_values() -> dict:
|
|
||||||
"""Gets a test dictionary containing a key
|
|
||||||
for every language."""
|
|
||||||
|
|
||||||
keys = {}
|
|
||||||
|
|
||||||
for lang_code, lang_name in settings.LANGUAGES:
|
|
||||||
keys[lang_code] = 'value in %s' % lang_name
|
|
||||||
|
|
||||||
return keys
|
|
||||||
|
|
||||||
|
|
||||||
class LocalizedValueTestCase(TestCase):
|
|
||||||
"""Tests the :see:LocalizedValue class."""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def tearDown():
|
|
||||||
"""Assures that the current language
|
|
||||||
is set back to the default."""
|
|
||||||
|
|
||||||
translation.activate(settings.LANGUAGE_CODE)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def test_init():
|
|
||||||
"""Tests whether the __init__ function
|
|
||||||
of the :see:LocalizedValue class works
|
|
||||||
as expected."""
|
|
||||||
|
|
||||||
keys = get_init_values()
|
|
||||||
value = LocalizedValue(keys)
|
|
||||||
|
|
||||||
for lang_code, _ in settings.LANGUAGES:
|
|
||||||
assert getattr(value, lang_code, None) == keys[lang_code]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def test_init_default_values():
|
|
||||||
"""Tests wehther the __init__ function
|
|
||||||
of the :see:LocalizedValue accepts the
|
|
||||||
default value or an empty dict properly."""
|
|
||||||
|
|
||||||
value = LocalizedValue()
|
|
||||||
|
|
||||||
for lang_code, _ in settings.LANGUAGES:
|
|
||||||
assert getattr(value, lang_code) is None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def test_get_explicit():
|
|
||||||
"""Tests whether the the :see:LocalizedValue
|
|
||||||
class's :see:get function works properly
|
|
||||||
when specifying an explicit value."""
|
|
||||||
|
|
||||||
keys = get_init_values()
|
|
||||||
localized_value = LocalizedValue(keys)
|
|
||||||
|
|
||||||
for language, value in keys.items():
|
|
||||||
assert localized_value.get(language) == value
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def test_get_default_language():
|
|
||||||
"""Tests whether the :see:LocalizedValue
|
|
||||||
class's see:get function properly
|
|
||||||
gets the value in the default language."""
|
|
||||||
|
|
||||||
keys = get_init_values()
|
|
||||||
localized_value = LocalizedValue(keys)
|
|
||||||
|
|
||||||
for language, _ in keys.items():
|
|
||||||
translation.activate(language)
|
|
||||||
assert localized_value.get() == keys[settings.LANGUAGE_CODE]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def test_set():
|
|
||||||
"""Tests whether the :see:LocalizedValue
|
|
||||||
class's see:set function works properly."""
|
|
||||||
|
|
||||||
localized_value = LocalizedValue()
|
|
||||||
|
|
||||||
for language, value in get_init_values():
|
|
||||||
localized_value.set(language, value)
|
|
||||||
assert localized_value.get(language) == value
|
|
||||||
assert getattr(localized_value, language) == value
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def test_str():
|
|
||||||
"""Tests whether the :see:LocalizedValue
|
|
||||||
class's __str__ works properly."""
|
|
||||||
|
|
||||||
keys = get_init_values()
|
|
||||||
localized_value = LocalizedValue(keys)
|
|
||||||
|
|
||||||
for language, value in keys.items():
|
|
||||||
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
|
|
||||||
class's __str__'s fallback functionality
|
|
||||||
works properly."""
|
|
||||||
|
|
||||||
test_value = 'myvalue'
|
|
||||||
|
|
||||||
localized_value = LocalizedValue({
|
|
||||||
settings.LANGUAGE_CODE: test_value
|
|
||||||
})
|
|
||||||
|
|
||||||
other_language = settings.LANGUAGES[-1][0]
|
|
||||||
|
|
||||||
# make sure that, by default it returns
|
|
||||||
# the value in the default language
|
|
||||||
assert str(localized_value) == test_value
|
|
||||||
|
|
||||||
# make sure that it falls back to the
|
|
||||||
# primary language when there's no value
|
|
||||||
# available in the current language
|
|
||||||
translation.activate(other_language)
|
|
||||||
assert str(localized_value) == test_value
|
|
||||||
|
|
||||||
# make sure that it's just __str__ falling
|
|
||||||
# back and that for the other language
|
|
||||||
# there's no actual value
|
|
||||||
assert localized_value.get(other_language) != test_value
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def test_deconstruct():
|
|
||||||
"""Tests whether the :see:LocalizedValue
|
|
||||||
class's :see:deconstruct function works properly."""
|
|
||||||
|
|
||||||
keys = get_init_values()
|
|
||||||
value = LocalizedValue(keys)
|
|
||||||
|
|
||||||
path, args, kwargs = value.deconstruct()
|
|
||||||
|
|
||||||
assert args[0] == keys
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def test_construct_string():
|
|
||||||
"""Tests whether the :see:LocalizedValue's constructor
|
|
||||||
assumes the primary language when passing a single string."""
|
|
||||||
|
|
||||||
value = LocalizedValue('beer')
|
|
||||||
assert value.get(settings.LANGUAGE_CODE) == 'beer'
|
|
||||||
|
|
||||||
|
|
||||||
class LocalizedFieldTestCase(TestCase):
|
|
||||||
"""Tests the :see:LocalizedField class."""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def test_from_db_value():
|
|
||||||
"""Tests whether the :see:from_db_value function
|
|
||||||
produces the expected :see:LocalizedValue."""
|
|
||||||
|
|
||||||
input_data = get_init_values()
|
|
||||||
localized_value = LocalizedField().from_db_value(input_data)
|
|
||||||
|
|
||||||
for lang_code, _ in settings.LANGUAGES:
|
|
||||||
assert getattr(localized_value, lang_code) == input_data[lang_code]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def test_from_db_value_none():
|
|
||||||
"""Tests whether the :see:from_db_value function
|
|
||||||
correctly handles None values."""
|
|
||||||
|
|
||||||
localized_value = LocalizedField().from_db_value(None)
|
|
||||||
|
|
||||||
for lang_code, _ in settings.LANGUAGES:
|
|
||||||
assert localized_value.get(lang_code) is None
|
|
||||||
|
|
||||||
def test_from_db_value_none_return_none(self):
|
|
||||||
"""Tests whether the :see:from_db_value function
|
|
||||||
correctly handles None values when LOCALIZED_FIELDS_EXPERIMENTAL
|
|
||||||
is set to True."""
|
|
||||||
|
|
||||||
with self.settings(LOCALIZED_FIELDS_EXPERIMENTAL=True):
|
|
||||||
localized_value = LocalizedField.from_db_value(None)
|
|
||||||
|
|
||||||
assert localized_value is None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def test_to_python():
|
|
||||||
"""Tests whether the :see:to_python function
|
|
||||||
produces the expected :see:LocalizedValue."""
|
|
||||||
|
|
||||||
input_data = get_init_values()
|
|
||||||
localized_value = LocalizedField().to_python(input_data)
|
|
||||||
|
|
||||||
for language, value in input_data.items():
|
|
||||||
assert localized_value.get(language) == value
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def test_to_python_none():
|
|
||||||
"""Tests whether the :see:to_python function
|
|
||||||
produces the expected :see:LocalizedValue
|
|
||||||
instance when it is passes None."""
|
|
||||||
|
|
||||||
localized_value = LocalizedField().to_python(None)
|
|
||||||
assert localized_value
|
|
||||||
|
|
||||||
for lang_code, _ in settings.LANGUAGES:
|
|
||||||
assert localized_value.get(lang_code) is None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def test_to_python_non_dict():
|
|
||||||
"""Tests whether the :see:to_python function produces
|
|
||||||
the expected :see:LocalizedValue when it is
|
|
||||||
passed a non-dictionary value."""
|
|
||||||
|
|
||||||
localized_value = LocalizedField().to_python(list())
|
|
||||||
assert localized_value
|
|
||||||
|
|
||||||
for lang_code, _ in settings.LANGUAGES:
|
|
||||||
assert localized_value.get(lang_code) is None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def test_get_prep_value():
|
|
||||||
""""Tests whether the :see:get_prep_value function
|
|
||||||
produces the expected dictionary."""
|
|
||||||
|
|
||||||
input_data = get_init_values()
|
|
||||||
localized_value = LocalizedValue(input_data)
|
|
||||||
|
|
||||||
output_data = LocalizedField().get_prep_value(localized_value)
|
|
||||||
|
|
||||||
for language, value in input_data.items():
|
|
||||||
assert language in output_data
|
|
||||||
assert output_data.get(language) == value
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def test_get_prep_value_none():
|
|
||||||
"""Tests whether the :see:get_prep_value function
|
|
||||||
produces the expected output when it is passed None."""
|
|
||||||
|
|
||||||
output_data = LocalizedField().get_prep_value(None)
|
|
||||||
assert not output_data
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def test_get_prep_value_no_localized_value():
|
|
||||||
"""Tests whether the :see:get_prep_value function
|
|
||||||
produces the expected output when it is passed a
|
|
||||||
non-LocalizedValue value."""
|
|
||||||
|
|
||||||
output_data = LocalizedField().get_prep_value(['huh'])
|
|
||||||
assert not output_data
|
|
||||||
|
|
||||||
def test_get_prep_value_clean(self):
|
|
||||||
"""Tests whether the :see:get_prep_value produces
|
|
||||||
None as the output when it is passed an empty, but
|
|
||||||
valid LocalizedValue value but, only when null=True."""
|
|
||||||
|
|
||||||
localized_value = LocalizedValue()
|
|
||||||
|
|
||||||
with self.assertRaises(IntegrityError):
|
|
||||||
LocalizedField(null=False).get_prep_value(localized_value)
|
|
||||||
|
|
||||||
assert not LocalizedField(null=True).get_prep_value(localized_value)
|
|
||||||
assert not LocalizedField().clean(None)
|
|
||||||
assert not LocalizedField().clean(['huh'])
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def test_formfield():
|
|
||||||
"""Tests whether the :see:formfield function
|
|
||||||
correctly returns a valid form."""
|
|
||||||
|
|
||||||
assert isinstance(
|
|
||||||
LocalizedField().formfield(),
|
|
||||||
LocalizedFieldForm
|
|
||||||
)
|
|
@ -7,10 +7,11 @@ from django import forms
|
|||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
from django.core.files.base import File, ContentFile
|
from django.core.files.base import File, ContentFile
|
||||||
from django.core.files import temp as tempfile
|
from django.core.files import temp as tempfile
|
||||||
from localized_fields import LocalizedFileField, LocalizedValue
|
from localized_fields.fields import LocalizedFileField
|
||||||
|
from localized_fields.value import LocalizedValue
|
||||||
from localized_fields.fields.localized_file_field import LocalizedFieldFile
|
from localized_fields.fields.localized_file_field import LocalizedFieldFile
|
||||||
from localized_fields.forms import LocalizedFileFieldForm
|
from localized_fields.forms import LocalizedFileFieldForm
|
||||||
from localized_fields.localized_value import LocalizedFileValue
|
from localized_fields.value import LocalizedFileValue
|
||||||
from localized_fields.widgets import LocalizedFileWidget
|
from localized_fields.widgets import LocalizedFileWidget
|
||||||
from .fake_model import get_fake_model
|
from .fake_model import get_fake_model
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from localized_fields import LocalizedField, LocalizedValue
|
from localized_fields.fields import LocalizedField
|
||||||
|
from localized_fields.value import LocalizedValue
|
||||||
|
|
||||||
from .fake_model import get_fake_model
|
from .fake_model import get_fake_model
|
||||||
|
|
||||||
@ -33,7 +34,6 @@ class LocalizedModelTestCase(TestCase):
|
|||||||
|
|
||||||
assert isinstance(obj.title, LocalizedValue)
|
assert isinstance(obj.title, LocalizedValue)
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def test_model_init_kwargs(cls):
|
def test_model_init_kwargs(cls):
|
||||||
"""Tests whether all :see:LocalizedField
|
"""Tests whether all :see:LocalizedField
|
@ -4,10 +4,14 @@ from django import forms
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
from localized_fields import (LocalizedField, LocalizedAutoSlugField,
|
|
||||||
LocalizedUniqueSlugField)
|
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
|
|
||||||
|
from localized_fields.fields import (
|
||||||
|
LocalizedField,
|
||||||
|
LocalizedAutoSlugField,
|
||||||
|
LocalizedUniqueSlugField
|
||||||
|
)
|
||||||
|
|
||||||
from .fake_model import get_fake_model
|
from .fake_model import get_fake_model
|
||||||
|
|
||||||
|
|
164
tests/test_value.py
Normal file
164
tests/test_value.py
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.utils import translation
|
||||||
|
|
||||||
|
from localized_fields.value import LocalizedValue
|
||||||
|
|
||||||
|
from .data import get_init_values
|
||||||
|
|
||||||
|
|
||||||
|
class LocalizedValueTestCase(TestCase):
|
||||||
|
"""Tests the :see:LocalizedValue class."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def tearDown():
|
||||||
|
"""Assures that the current language
|
||||||
|
is set back to the default."""
|
||||||
|
|
||||||
|
translation.activate(settings.LANGUAGE_CODE)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def test_init():
|
||||||
|
"""Tests whether the __init__ function
|
||||||
|
of the :see:LocalizedValue class works
|
||||||
|
as expected."""
|
||||||
|
|
||||||
|
keys = get_init_values()
|
||||||
|
value = LocalizedValue(keys)
|
||||||
|
|
||||||
|
for lang_code, _ in settings.LANGUAGES:
|
||||||
|
assert getattr(value, lang_code, None) == keys[lang_code]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def test_init_default_values():
|
||||||
|
"""Tests whether the __init__ function
|
||||||
|
of the :see:LocalizedValue accepts the
|
||||||
|
default value or an empty dict properly."""
|
||||||
|
|
||||||
|
value = LocalizedValue()
|
||||||
|
|
||||||
|
for lang_code, _ in settings.LANGUAGES:
|
||||||
|
assert getattr(value, lang_code) is None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def test_init_array():
|
||||||
|
"""Tests whether the __init__ function
|
||||||
|
of :see:LocalizedValue properly handles an
|
||||||
|
array.
|
||||||
|
|
||||||
|
Arrays can be passed to LocalizedValue as
|
||||||
|
a result of a ArrayAgg operation."""
|
||||||
|
|
||||||
|
value = LocalizedValue(['my value'])
|
||||||
|
assert value.get(settings.LANGUAGE_CODE) == 'my value'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def test_get_explicit():
|
||||||
|
"""Tests whether the the :see:LocalizedValue
|
||||||
|
class's :see:get function works properly
|
||||||
|
when specifying an explicit value."""
|
||||||
|
|
||||||
|
keys = get_init_values()
|
||||||
|
localized_value = LocalizedValue(keys)
|
||||||
|
|
||||||
|
for language, value in keys.items():
|
||||||
|
assert localized_value.get(language) == value
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def test_get_default_language():
|
||||||
|
"""Tests whether the :see:LocalizedValue
|
||||||
|
class's see:get function properly
|
||||||
|
gets the value in the default language."""
|
||||||
|
|
||||||
|
keys = get_init_values()
|
||||||
|
localized_value = LocalizedValue(keys)
|
||||||
|
|
||||||
|
for language, _ in keys.items():
|
||||||
|
translation.activate(language)
|
||||||
|
assert localized_value.get() == keys[settings.LANGUAGE_CODE]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def test_set():
|
||||||
|
"""Tests whether the :see:LocalizedValue
|
||||||
|
class's see:set function works properly."""
|
||||||
|
|
||||||
|
localized_value = LocalizedValue()
|
||||||
|
|
||||||
|
for language, value in get_init_values():
|
||||||
|
localized_value.set(language, value)
|
||||||
|
assert localized_value.get(language) == value
|
||||||
|
assert getattr(localized_value, language) == value
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def test_str():
|
||||||
|
"""Tests whether the :see:LocalizedValue
|
||||||
|
class's __str__ works properly."""
|
||||||
|
|
||||||
|
keys = get_init_values()
|
||||||
|
localized_value = LocalizedValue(keys)
|
||||||
|
|
||||||
|
for language, value in keys.items():
|
||||||
|
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
|
||||||
|
class's __str__'s fallback functionality
|
||||||
|
works properly."""
|
||||||
|
|
||||||
|
test_value = 'myvalue'
|
||||||
|
|
||||||
|
localized_value = LocalizedValue({
|
||||||
|
settings.LANGUAGE_CODE: test_value
|
||||||
|
})
|
||||||
|
|
||||||
|
other_language = settings.LANGUAGES[-1][0]
|
||||||
|
|
||||||
|
# make sure that, by default it returns
|
||||||
|
# the value in the default language
|
||||||
|
assert str(localized_value) == test_value
|
||||||
|
|
||||||
|
# make sure that it falls back to the
|
||||||
|
# primary language when there's no value
|
||||||
|
# available in the current language
|
||||||
|
translation.activate(other_language)
|
||||||
|
assert str(localized_value) == test_value
|
||||||
|
|
||||||
|
# make sure that it's just __str__ falling
|
||||||
|
# back and that for the other language
|
||||||
|
# there's no actual value
|
||||||
|
assert localized_value.get(other_language) != test_value
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def test_deconstruct():
|
||||||
|
"""Tests whether the :see:LocalizedValue
|
||||||
|
class's :see:deconstruct function works properly."""
|
||||||
|
|
||||||
|
keys = get_init_values()
|
||||||
|
value = LocalizedValue(keys)
|
||||||
|
|
||||||
|
path, args, kwargs = value.deconstruct()
|
||||||
|
|
||||||
|
assert args[0] == keys
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def test_construct_string():
|
||||||
|
"""Tests whether the :see:LocalizedValue's constructor
|
||||||
|
assumes the primary language when passing a single string."""
|
||||||
|
|
||||||
|
value = LocalizedValue('beer')
|
||||||
|
assert value.get(settings.LANGUAGE_CODE) == 'beer'
|
@ -1,7 +1,8 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from localized_fields import LocalizedFieldWidget, LocalizedValue
|
from localized_fields.value import LocalizedValue
|
||||||
|
from localized_fields.widgets import LocalizedFieldWidget
|
||||||
|
|
||||||
|
|
||||||
class LocalizedFieldWidgetTestCase(TestCase):
|
class LocalizedFieldWidgetTestCase(TestCase):
|
Loading…
x
Reference in New Issue
Block a user