mirror of
https://github.com/SectorLabs/django-localized-fields.git
synced 2025-10-20 06:48:56 +03:00
Compare commits
65 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
f7c14f897c | ||
|
e5e3068835 | ||
|
de7d3b9a21 | ||
|
fd47deaa2e | ||
|
4b6d997ddd | ||
|
68d6991608 | ||
|
c75c1764e2 | ||
|
6736b3b32d | ||
|
b97a7f3c23 | ||
|
1b036dc1de | ||
|
ea7733670d | ||
|
a8dc4fe837 | ||
|
3c8bea0fc3 | ||
|
2741a6a2a2 | ||
|
06f7ee15f0 | ||
|
e5214b07ae | ||
|
2d5fe0be05 | ||
|
4305696f1b | ||
|
8c73c9ab77 | ||
|
92a53bc3d7 | ||
|
5a4f449363 | ||
|
0fa79ddbb0 | ||
|
2205f9c6a4 | ||
|
84c267330f | ||
|
a1a02552b7 | ||
|
bb84d7577c | ||
|
5e0343801f | ||
|
2df2ec8b36 | ||
|
3fcaece894 | ||
|
093a9d58f2 | ||
|
cff22855c2 | ||
|
bf2995fd27 | ||
|
f1798b0cc6 | ||
|
fc80462ce7 | ||
|
366dceaddc | ||
|
8591af1f2a | ||
|
c55001ac12 | ||
|
23c6f975d8 | ||
|
a9037b603a | ||
|
8b08e5a467 | ||
|
bf90131e4f | ||
|
8bdfee0666 | ||
|
add3c1e1d4 | ||
|
8ba33695f9 | ||
|
78594541e1 | ||
|
52145ca7d3 | ||
|
5df44b0d62 | ||
|
5aef1d791b | ||
|
03df76d6d7 | ||
|
465e35ba8a | ||
|
9278e99b18 | ||
|
9754134298 | ||
|
f0c7a72078 | ||
|
7f4dfbae1f | ||
|
d07da55215 | ||
|
340dde18cd | ||
|
3951266747 | ||
|
b5f4c43d6b | ||
|
3d08475468 | ||
|
b3d7092b91 | ||
|
97a785e9b0 | ||
|
97c14fd2ba | ||
|
6cb4cdf52e | ||
|
bcb2ff0092 | ||
|
43a48403e9 |
@@ -1,2 +1,5 @@
|
||||
include LICENSE
|
||||
include README.rst
|
||||
recursive-include localized_fields/static *
|
||||
recursive-include localized_fields/templates *
|
||||
recursive-exclude tests *
|
||||
|
69
README.rst
69
README.rst
@@ -32,7 +32,7 @@ Installation
|
||||
....
|
||||
|
||||
'django.contrib.postgres',
|
||||
'localized_fields'
|
||||
'localized_fields.apps.LocalizedFieldsConfig'
|
||||
]
|
||||
|
||||
3. Set the database engine to ``psqlextra.backend``:
|
||||
@@ -62,15 +62,14 @@ Usage
|
||||
|
||||
Preparation
|
||||
^^^^^^^^^^^
|
||||
Inherit your model from ``LocalizedModel`` and declare fields on your model as ``LocalizedField``:
|
||||
Declare fields on your model as ``LocalizedField``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from localized_fields.models import LocalizedModel
|
||||
from localized_fields.fields import LocalizedField
|
||||
|
||||
|
||||
class MyModel(LocalizedModel):
|
||||
class MyModel(models.Model):
|
||||
title = LocalizedField()
|
||||
|
||||
``django-localized-fields`` integrates with Django's i18n system, in order for certain languages to be available you have to correctly configure the ``LANGUAGES`` and ``LANGUAGE_CODE`` settings:
|
||||
@@ -136,14 +135,14 @@ At the moment, it is not possible to select two languages to be marked as requir
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyModel(LocalizedModel):
|
||||
class MyModel(models.Model):
|
||||
title = LocalizedField(required=True)
|
||||
|
||||
* Make all languages optional:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyModel(LocalizedModel):
|
||||
class MyModel(models.Model):
|
||||
title = LocalizedField(null=True)
|
||||
|
||||
**Uniqueness**
|
||||
@@ -154,32 +153,32 @@ By default the values stored in a ``LocalizedField`` are *not unique*. You can e
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyModel(LocalizedModel):
|
||||
class MyModel(models.Model):
|
||||
title = LocalizedField(uniqueness=['en', 'ro'])
|
||||
|
||||
* Enforce uniqueness for **all** languages:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from localized_fields import get_language_codes
|
||||
from localized_fields.util import get_language_codes
|
||||
|
||||
class MyModel(LocalizedModel):
|
||||
class MyModel(models.Model):
|
||||
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(LocalizedModel):
|
||||
class MyModel(models.Model):
|
||||
title = LocalizedField(uniqueness=[('en', 'ro')])
|
||||
|
||||
* Enforce uniqueness for **all** languages **together**:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from localized_fields import get_language_codes
|
||||
from localized_fields.util import get_language_codes
|
||||
|
||||
class MyModel(LocalizedModel):
|
||||
class MyModel(models.Model):
|
||||
title = LocalizedField(uniqueness=[(*get_language_codes())])
|
||||
|
||||
|
||||
@@ -193,12 +192,10 @@ Besides ``LocalizedField``, there's also:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from localized_fields import (LocalizedModel,
|
||||
AtomicSlugRetryMixin,
|
||||
LocalizedField,
|
||||
LocalizedUniqueSlugField)
|
||||
from localized_fields.models import LocalizedModel
|
||||
from localized_fields.fields import LocalizedField, LocalizedUniqueSlugField
|
||||
|
||||
class MyModel(AtomicSlugRetryMixin, LocalizedModel):
|
||||
class MyModel(LocalizedModel):
|
||||
title = LocalizedField()
|
||||
slug = LocalizedUniqueSlugField(populate_from='title')
|
||||
|
||||
@@ -218,9 +215,7 @@ Besides ``LocalizedField``, there's also:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from localized_fields import (LocalizedModel,
|
||||
LocalizedField,
|
||||
LocalizedUniqueSlugField)
|
||||
from localized_fields.fields import LocalizedField, LocalizedAutoSlugField
|
||||
|
||||
class MyModel(LocalizedModel):
|
||||
title = LocalizedField()
|
||||
@@ -230,20 +225,46 @@ Besides ``LocalizedField``, there's also:
|
||||
|
||||
* ``LocalizedBleachField``
|
||||
Automatically bleaches the content of the field.
|
||||
|
||||
* django-bleach
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from localized_fields import (LocalizedModel,
|
||||
LocalizedField,
|
||||
LocalizedBleachField)
|
||||
from localized_fields.fields import LocalizedField, LocalizedBleachField
|
||||
|
||||
class MyModel(LocalizedModel):
|
||||
class MyModel(models.Model):
|
||||
title = LocalizedField()
|
||||
description = LocalizedBleachField()
|
||||
|
||||
Experimental feature
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
Enables the following experimental features:
|
||||
* ``LocalizedField`` will return ``None`` instead of an empty ``LocalizedValue`` if there is no database value.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
LOCALIZED_FIELDS_EXPERIMENTAL = True
|
||||
|
||||
|
||||
Django Admin Integration
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
To enable widgets in the admin, you need to inherit from ``LocalizedFieldsAdminMixin``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django.contrib import admin
|
||||
from myapp.models import MyLocalizedModel
|
||||
|
||||
from localized_fields.admin import LocalizedFieldsAdminMixin
|
||||
|
||||
class MyLocalizedModelAdmin(LocalizedFieldsAdminMixin, admin.ModelAdmin):
|
||||
"""Any admin options you need go here"""
|
||||
|
||||
admin.site.register(MyLocalizedModel, MyLocalizedModelAdmin)
|
||||
|
||||
|
||||
Frequently asked questions (FAQ)
|
||||
--------------------------------
|
||||
|
||||
|
@@ -1,20 +0,0 @@
|
||||
from .util import get_language_codes
|
||||
from .forms import LocalizedFieldForm, LocalizedFieldWidget
|
||||
from .fields import (LocalizedField, LocalizedBleachField,
|
||||
LocalizedAutoSlugField, LocalizedUniqueSlugField)
|
||||
from .mixins import AtomicSlugRetryMixin
|
||||
from .models import LocalizedModel
|
||||
from .localized_value import LocalizedValue
|
||||
|
||||
__all__ = [
|
||||
'get_language_codes',
|
||||
'LocalizedField',
|
||||
'LocalizedValue',
|
||||
'LocalizedAutoSlugField',
|
||||
'LocalizedUniqueSlugField',
|
||||
'LocalizedBleachField',
|
||||
'LocalizedFieldWidget',
|
||||
'LocalizedFieldForm',
|
||||
'LocalizedModel',
|
||||
'AtomicSlugRetryMixin'
|
||||
]
|
||||
|
32
localized_fields/admin.py
Normal file
32
localized_fields/admin.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from django.contrib.admin import ModelAdmin
|
||||
|
||||
from . import widgets
|
||||
from .fields import LocalizedField
|
||||
|
||||
|
||||
FORMFIELD_FOR_LOCALIZED_FIELDS_DEFAULTS = {
|
||||
LocalizedField: {'widget': widgets.AdminLocalizedFieldWidget},
|
||||
}
|
||||
|
||||
|
||||
class LocalizedFieldsAdminMixin(ModelAdmin):
|
||||
"""Mixin for making the fancy widgets work in Django Admin."""
|
||||
|
||||
class Media:
|
||||
css = {
|
||||
'all': (
|
||||
'localized_fields/localized-fields-admin.css',
|
||||
)
|
||||
}
|
||||
|
||||
js = (
|
||||
'localized_fields/localized-fields-admin.js',
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initializes a new instance of :see:LocalizedFieldsAdminMixin."""
|
||||
|
||||
super(LocalizedFieldsAdminMixin, self).__init__(*args, **kwargs)
|
||||
overrides = FORMFIELD_FOR_LOCALIZED_FIELDS_DEFAULTS.copy()
|
||||
overrides.update(self.formfield_overrides)
|
||||
self.formfield_overrides = 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,12 +1,18 @@
|
||||
from .localized_field import LocalizedField
|
||||
from .localized_autoslug_field import LocalizedAutoSlugField
|
||||
from .localized_uniqueslug_field import LocalizedUniqueSlugField
|
||||
from .localized_bleach_field import LocalizedBleachField
|
||||
from .field import LocalizedField
|
||||
from .autoslug_field import LocalizedAutoSlugField
|
||||
from .uniqueslug_field import LocalizedUniqueSlugField
|
||||
|
||||
|
||||
__all__ = [
|
||||
'LocalizedField',
|
||||
'LocalizedAutoSlugField',
|
||||
'LocalizedUniqueSlugField',
|
||||
'LocalizedBleachField',
|
||||
]
|
||||
|
||||
try:
|
||||
from .bleach_field import LocalizedBleachField
|
||||
__all__ += [
|
||||
'LocalizedBleachField'
|
||||
]
|
||||
except ImportError:
|
||||
pass
|
||||
|
@@ -1,12 +1,12 @@
|
||||
from typing import Callable
|
||||
from typing import Callable, Tuple
|
||||
from datetime import datetime
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.utils.text import slugify
|
||||
|
||||
from .localized_field import LocalizedField
|
||||
from ..localized_value import LocalizedValue
|
||||
from .field import LocalizedField
|
||||
from ..value import LocalizedValue
|
||||
|
||||
|
||||
class LocalizedAutoSlugField(LocalizedField):
|
||||
@@ -69,13 +69,7 @@ class LocalizedAutoSlugField(LocalizedField):
|
||||
|
||||
slugs = LocalizedValue()
|
||||
|
||||
for lang_code, _ in settings.LANGUAGES:
|
||||
value = self._get_populate_from_value(
|
||||
instance,
|
||||
self.populate_from,
|
||||
lang_code
|
||||
)
|
||||
|
||||
for lang_code, value in self._get_populate_values(instance):
|
||||
if not value:
|
||||
continue
|
||||
|
||||
@@ -128,6 +122,30 @@ class LocalizedAutoSlugField(LocalizedField):
|
||||
|
||||
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
|
||||
def _get_populate_from_value(instance, field_name: str, language: str):
|
||||
"""Gets the value to create a slug from in the specified language.
|
@@ -1,8 +1,9 @@
|
||||
import bleach
|
||||
|
||||
from django.conf import settings
|
||||
from django_bleach.utils import get_bleach_default_options
|
||||
|
||||
from .localized_field import LocalizedField
|
||||
from .field import LocalizedField
|
||||
|
||||
|
||||
class LocalizedBleachField(LocalizedField):
|
@@ -1,9 +1,15 @@
|
||||
import json
|
||||
|
||||
from typing import Union
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.utils import IntegrityError
|
||||
from localized_fields import LocalizedFieldForm
|
||||
|
||||
from psqlextra.fields import HStoreField
|
||||
|
||||
from ..localized_value import LocalizedValue
|
||||
from ..forms import LocalizedFieldForm
|
||||
from ..value import LocalizedValue
|
||||
from ..descriptor import LocalizedValueDescriptor
|
||||
|
||||
|
||||
class LocalizedField(HStoreField):
|
||||
@@ -14,14 +20,33 @@ class LocalizedField(HStoreField):
|
||||
|
||||
Meta = None
|
||||
|
||||
# The class to wrap instance attributes in. Accessing to field attribute in
|
||||
# model instance will always return an instance of attr_class.
|
||||
attr_class = LocalizedValue
|
||||
|
||||
# The descriptor to use for accessing the attribute off of the class.
|
||||
descriptor_class = LocalizedValueDescriptor
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initializes a new instance of :see:LocalizedField."""
|
||||
|
||||
super(LocalizedField, self).__init__(
|
||||
*args, required=[settings.LANGUAGE_CODE], **kwargs)
|
||||
super(LocalizedField, self).__init__(*args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def from_db_value(value, *_):
|
||||
def contribute_to_class(self, model, name, **kwargs):
|
||||
"""Adds this field to the specifed model.
|
||||
|
||||
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
|
||||
def from_db_value(cls, value, *_):
|
||||
"""Turns the specified database value into its Python
|
||||
equivalent.
|
||||
|
||||
@@ -36,11 +61,36 @@ class LocalizedField(HStoreField):
|
||||
"""
|
||||
|
||||
if not value:
|
||||
return LocalizedValue()
|
||||
if getattr(settings, 'LOCALIZED_FIELDS_EXPERIMENTAL', False):
|
||||
return None
|
||||
else:
|
||||
return cls.attr_class()
|
||||
|
||||
return LocalizedValue(value)
|
||||
# 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)
|
||||
|
||||
def to_python(self, value: dict) -> LocalizedValue:
|
||||
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)
|
||||
|
||||
def to_python(self, value: Union[dict, str, None]) -> LocalizedValue:
|
||||
"""Turns the specified database value into its Python
|
||||
equivalent.
|
||||
|
||||
@@ -54,10 +104,17 @@ class LocalizedField(HStoreField):
|
||||
data extracted from the database.
|
||||
"""
|
||||
|
||||
if not value or not isinstance(value, dict):
|
||||
return LocalizedValue()
|
||||
# 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
|
||||
|
||||
return LocalizedValue(value)
|
||||
if not deserialized_value:
|
||||
return self.attr_class()
|
||||
|
||||
return self.attr_class(deserialized_value)
|
||||
|
||||
def get_prep_value(self, value: LocalizedValue) -> dict:
|
||||
"""Turns the specified value into something the database
|
@@ -1,13 +1,12 @@
|
||||
from datetime import datetime
|
||||
|
||||
from django.conf import settings
|
||||
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 ..localized_value import LocalizedValue
|
||||
from .localized_autoslug_field import LocalizedAutoSlugField
|
||||
from ..value import LocalizedValue
|
||||
|
||||
|
||||
class LocalizedUniqueSlugField(LocalizedAutoSlugField):
|
||||
@@ -73,13 +72,7 @@ class LocalizedUniqueSlugField(LocalizedAutoSlugField):
|
||||
|
||||
slugs = LocalizedValue()
|
||||
|
||||
for lang_code, _ in settings.LANGUAGES:
|
||||
value = self._get_populate_from_value(
|
||||
instance,
|
||||
self.populate_from,
|
||||
lang_code
|
||||
)
|
||||
|
||||
for lang_code, value in self._get_populate_values(instance):
|
||||
if not value:
|
||||
continue
|
||||
|
||||
@@ -98,11 +91,12 @@ class LocalizedUniqueSlugField(LocalizedAutoSlugField):
|
||||
if self.include_time:
|
||||
slug += '-%d' % datetime.now().microsecond
|
||||
|
||||
if instance.retries > 0:
|
||||
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' % instance.retries
|
||||
slug += '%d' % retries
|
||||
|
||||
slugs.set(lang_code, slug)
|
||||
|
@@ -2,53 +2,17 @@ from typing import List
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.forms import MultiWidget
|
||||
|
||||
from .localized_value import LocalizedValue
|
||||
|
||||
|
||||
class LocalizedFieldWidget(MultiWidget):
|
||||
"""Widget that has an input box for every language."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initializes a new instance of :see:LocalizedFieldWidget."""
|
||||
|
||||
widgets = []
|
||||
|
||||
for _ in settings.LANGUAGES:
|
||||
widgets.append(forms.Textarea())
|
||||
|
||||
super(LocalizedFieldWidget, self).__init__(widgets, *args, **kwargs)
|
||||
|
||||
def decompress(self, value: LocalizedValue) -> List[str]:
|
||||
"""Decompresses the specified value so
|
||||
it can be spread over the internal widgets.
|
||||
|
||||
Arguments:
|
||||
value:
|
||||
The :see:LocalizedValue to display in this
|
||||
widget.
|
||||
|
||||
Returns:
|
||||
All values to display in the inner widgets.
|
||||
"""
|
||||
|
||||
result = []
|
||||
|
||||
for lang_code, _ in settings.LANGUAGES:
|
||||
if value:
|
||||
result.append(value.get(lang_code))
|
||||
else:
|
||||
result.append(None)
|
||||
|
||||
return result
|
||||
from .value import LocalizedValue
|
||||
from .widgets import LocalizedFieldWidget
|
||||
|
||||
|
||||
class LocalizedFieldForm(forms.MultiValueField):
|
||||
"""Form for a localized field, allows editing
|
||||
the field in multiple languages."""
|
||||
|
||||
widget = LocalizedFieldWidget()
|
||||
widget = LocalizedFieldWidget
|
||||
value_class = LocalizedValue
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initializes a new instance of :see:LocalizedFieldForm."""
|
||||
@@ -59,15 +23,19 @@ class LocalizedFieldForm(forms.MultiValueField):
|
||||
field_options = {'required': False}
|
||||
|
||||
if lang_code == settings.LANGUAGE_CODE:
|
||||
field_options['required'] = True
|
||||
field_options['required'] = kwargs.get('required', True)
|
||||
|
||||
field_options['label'] = lang_code
|
||||
fields.append(forms.fields.CharField(**field_options))
|
||||
|
||||
super(LocalizedFieldForm, self).__init__(
|
||||
fields,
|
||||
require_all_fields=False
|
||||
require_all_fields=False,
|
||||
*args, **kwargs
|
||||
)
|
||||
# set 'required' attribute for each widget separately
|
||||
for f, w in zip(self.fields, self.widget.widgets):
|
||||
w.is_required = f.required
|
||||
|
||||
def compress(self, value: List[str]) -> LocalizedValue:
|
||||
"""Compresses the values from individual fields
|
||||
@@ -82,7 +50,7 @@ class LocalizedFieldForm(forms.MultiValueField):
|
||||
the value in several languages.
|
||||
"""
|
||||
|
||||
localized_value = LocalizedValue()
|
||||
localized_value = self.value_class()
|
||||
|
||||
for (lang_code, _), value in zip(settings.LANGUAGES, value):
|
||||
localized_value.set(lang_code, value)
|
||||
|
@@ -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,36 +1,16 @@
|
||||
from django.db import models, transaction
|
||||
from django.db.utils import IntegrityError
|
||||
from django.conf import settings
|
||||
from psqlextra.models import PostgresModel
|
||||
|
||||
from .fields import LocalizedField
|
||||
from .localized_value import LocalizedValue
|
||||
from .mixins import AtomicSlugRetryMixin
|
||||
|
||||
|
||||
class LocalizedModel(models.Model):
|
||||
"""A model that contains localized fields."""
|
||||
class LocalizedModel(AtomicSlugRetryMixin, PostgresModel):
|
||||
"""Turns a model into a model that contains LocalizedField's.
|
||||
|
||||
For basic localisation functionality, it isn't needed to inherit
|
||||
from LocalizedModel. However, for certain features, this is required.
|
||||
|
||||
It is definitely needed for :see:LocalizedUniqueSlugField, unless you
|
||||
manually inherit from AtomicSlugRetryMixin."""
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initializes a new instance of :see:LocalizedModel.
|
||||
|
||||
Here we set all the fields that are of :see:LocalizedField
|
||||
to an instance of :see:LocalizedValue in case they are none
|
||||
so that the user doesn't explicitely have to do so."""
|
||||
|
||||
super(LocalizedModel, self).__init__(*args, **kwargs)
|
||||
|
||||
for field in self._meta.get_fields():
|
||||
if not isinstance(field, LocalizedField):
|
||||
continue
|
||||
|
||||
value = getattr(self, field.name, None)
|
||||
|
||||
if not isinstance(value, LocalizedValue):
|
||||
if isinstance(value, dict):
|
||||
value = LocalizedValue(value)
|
||||
else:
|
||||
value = LocalizedValue()
|
||||
|
||||
setattr(self, field.name, value)
|
||||
|
@@ -0,0 +1,47 @@
|
||||
.localized-fields-widget {
|
||||
margin-left: 160px;
|
||||
}
|
||||
|
||||
.localized-fields-widget.tabs {
|
||||
display: block;
|
||||
margin: 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.localized-fields-widget.tabs .localized-fields-widget.tab {
|
||||
display: inline-block;
|
||||
margin-left: 5px;
|
||||
border: 1px solid #79aec8;
|
||||
border-bottom: none;
|
||||
border-radius: 4px;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
background: #79aec8;
|
||||
color: #fff;
|
||||
font-weight: 400;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.localized-fields-widget.tabs .localized-fields-widget.tab:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.localized-fields-widget.tabs .localized-fields-widget.tab:hover {
|
||||
background: #417690;
|
||||
border-color: #417690;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.localized-fields-widget.tabs .localized-fields-widget.tab a {
|
||||
padding: 5px 10px;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.localized-fields-widget.tabs .localized-fields-widget.tab.active,
|
||||
.localized-fields-widget.tabs .localized-fields-widget.tab.active:hover {
|
||||
background: #79aec8;
|
||||
border-color: #79aec8;
|
||||
opacity: 1;
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
(function($) {
|
||||
var syncTabs = function(lang) {
|
||||
$('.localized-fields-widget.tab a:contains("'+lang+'")').each(function(){
|
||||
$(this).parents('.localized-fields-widget[role="tabs"]').find('.localized-fields-widget.tab').removeClass('active');
|
||||
$(this).parents('.localized-fields-widget.tab').addClass('active');
|
||||
$(this).parents('.localized-fields-widget[role="tabs"]').children('.localized-fields-widget [role="tabpanel"]').hide();
|
||||
$($(this).attr('href')).show();
|
||||
});
|
||||
}
|
||||
|
||||
$(function (){
|
||||
$('.localized-fields-widget [role="tabpanel"]').hide();
|
||||
// set first tab as active
|
||||
$('.localized-fields-widget[role="tabs"]').each(function () {
|
||||
$(this).find('.localized-fields-widget.tab:first').addClass('active');
|
||||
$($(this).find('.localized-fields-widget.tab:first a').attr('href')).show();
|
||||
});
|
||||
// try set active last selected tab
|
||||
if (window.sessionStorage) {
|
||||
var lang = window.sessionStorage.getItem('localized-field-lang');
|
||||
if (lang) {
|
||||
syncTabs(lang);
|
||||
}
|
||||
}
|
||||
|
||||
$('.localized-fields-widget.tab a').click(function(event) {
|
||||
event.preventDefault();
|
||||
syncTabs(this.innerText);
|
||||
if (window.sessionStorage) {
|
||||
window.sessionStorage.setItem('localized-field-lang', this.innerText);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
});
|
||||
})(django.jQuery)
|
@@ -0,0 +1,14 @@
|
||||
<div class="localized-fields-widget" role="tabs" data-synctabs="translation">
|
||||
<ul class="localized-fields-widget tabs">
|
||||
{% for key, lang in available_languages %}
|
||||
<li class="localized-fields-widget tab">
|
||||
<a href="#{{ id }}_{{ key }}">{{ lang|capfirst }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% for key, widget in widgets %}
|
||||
<div role="tabpanel" id="{{ id }}_{{ key }}">
|
||||
{{ widget }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
@@ -1,8 +1,10 @@
|
||||
import collections
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import translation
|
||||
|
||||
|
||||
class LocalizedValue:
|
||||
class LocalizedValue(dict):
|
||||
"""Represents the value of a :see:LocalizedField."""
|
||||
|
||||
def __init__(self, keys: dict=None):
|
||||
@@ -15,12 +17,8 @@ class LocalizedValue:
|
||||
different language.
|
||||
"""
|
||||
|
||||
if isinstance(keys, str):
|
||||
setattr(self, settings.LANGUAGE_CODE, keys)
|
||||
else:
|
||||
for lang_code, _ in settings.LANGUAGES:
|
||||
value = keys.get(lang_code) if keys else None
|
||||
setattr(self, lang_code, value)
|
||||
super().__init__({})
|
||||
self._interpret_value(keys)
|
||||
|
||||
def get(self, language: str=None) -> str:
|
||||
"""Gets the underlying value in the specified or
|
||||
@@ -37,7 +35,7 @@ class LocalizedValue:
|
||||
"""
|
||||
|
||||
language = language or settings.LANGUAGE_CODE
|
||||
return getattr(self, language, None)
|
||||
return super().get(language, None)
|
||||
|
||||
def set(self, language: str, value: str):
|
||||
"""Sets the value in the specified language.
|
||||
@@ -50,7 +48,8 @@ class LocalizedValue:
|
||||
The value to set.
|
||||
"""
|
||||
|
||||
setattr(self, language, value)
|
||||
self[language] = value
|
||||
self.__dict__.update(self)
|
||||
return self
|
||||
|
||||
def deconstruct(self) -> dict:
|
||||
@@ -64,6 +63,40 @@ class LocalizedValue:
|
||||
path = 'localized_fields.fields.LocalizedValue'
|
||||
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, None)
|
||||
|
||||
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) or None
|
||||
self.set(lang_code, lang_value)
|
||||
|
||||
elif isinstance(value, collections.Iterable):
|
||||
for val in value:
|
||||
self._interpret_value(val)
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Gets the value in the current language, or falls
|
||||
back to the primary language if there's no value
|
||||
@@ -85,13 +118,42 @@ class LocalizedValue:
|
||||
And False when they are not.
|
||||
"""
|
||||
|
||||
if not isinstance(other, type(self)):
|
||||
if isinstance(other, str):
|
||||
return self.__str__() == other
|
||||
return False
|
||||
|
||||
for lang_code, _ in settings.LANGUAGES:
|
||||
if self.get(lang_code) != other.get(lang_code):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def __ne__(self, other):
|
||||
"""Compares :paramref:self to :paramerf:other for
|
||||
in-equality.
|
||||
|
||||
Returns:
|
||||
True when :paramref:self is not equal to :paramref:other.
|
||||
And False when they are.
|
||||
"""
|
||||
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __setattr__(self, language: str, value: str):
|
||||
"""Sets the value for a language with the specified name.
|
||||
|
||||
Arguments:
|
||||
language:
|
||||
The language to set the value in.
|
||||
|
||||
value:
|
||||
The value to set.
|
||||
"""
|
||||
|
||||
self.set(language, value)
|
||||
|
||||
def __repr__(self): # pragma: no cover
|
||||
"""Gets a textual representation of this object."""
|
||||
|
||||
return 'LocalizedValue<%s> 0x%s' % (self.__dict__, id(self))
|
||||
return 'LocalizedValue<%s> 0x%s' % (dict(self), id(self))
|
86
localized_fields/widgets.py
Normal file
86
localized_fields/widgets.py
Normal file
@@ -0,0 +1,86 @@
|
||||
from typing import List
|
||||
|
||||
from django.conf import settings
|
||||
from django import forms
|
||||
from django.contrib.admin import widgets
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
from .value import LocalizedValue
|
||||
|
||||
|
||||
class LocalizedFieldWidget(forms.MultiWidget):
|
||||
"""Widget that has an input box for every language."""
|
||||
widget = forms.Textarea
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initializes a new instance of :see:LocalizedFieldWidget."""
|
||||
|
||||
initial_widgets = [
|
||||
self.widget
|
||||
for _ in settings.LANGUAGES
|
||||
]
|
||||
|
||||
super().__init__(initial_widgets, *args, **kwargs)
|
||||
|
||||
def decompress(self, value: LocalizedValue) -> List[str]:
|
||||
"""Decompresses the specified value so
|
||||
it can be spread over the internal widgets.
|
||||
|
||||
Arguments:
|
||||
value:
|
||||
The :see:LocalizedValue to display in this
|
||||
widget.
|
||||
|
||||
Returns:
|
||||
All values to display in the inner widgets.
|
||||
"""
|
||||
|
||||
result = []
|
||||
for lang_code, _ in settings.LANGUAGES:
|
||||
if value:
|
||||
result.append(value.get(lang_code))
|
||||
else:
|
||||
result.append(None)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class AdminLocalizedFieldWidget(LocalizedFieldWidget):
|
||||
widget = widgets.AdminTextareaWidget
|
||||
template = 'localized_fields/admin/widget.html'
|
||||
|
||||
def render(self, name, value, attrs=None):
|
||||
if self.is_localized:
|
||||
for widget in self.widgets:
|
||||
widget.is_localized = self.is_localized
|
||||
# value is a list of values, each corresponding to a widget
|
||||
# in self.widgets.
|
||||
if not isinstance(value, list):
|
||||
value = self.decompress(value)
|
||||
output = []
|
||||
final_attrs = self.build_attrs(attrs)
|
||||
id_ = final_attrs.get('id')
|
||||
for i, widget in enumerate(self.widgets):
|
||||
try:
|
||||
widget_value = value[i]
|
||||
except IndexError:
|
||||
widget_value = None
|
||||
if id_:
|
||||
final_attrs = dict(final_attrs, id='%s_%s' % (id_, i))
|
||||
widget_attrs = self.build_widget_attrs(widget, widget_value, final_attrs)
|
||||
output.append(widget.render(name + '_%s' % i, widget_value, widget_attrs))
|
||||
context = {
|
||||
'id': final_attrs.get('id'),
|
||||
'name': name,
|
||||
'widgets': zip([code for code, lang in settings.LANGUAGES], output),
|
||||
'available_languages': settings.LANGUAGES
|
||||
}
|
||||
return render_to_string(self.template, context)
|
||||
|
||||
@staticmethod
|
||||
def build_widget_attrs(widget, value, attrs):
|
||||
attrs = dict(attrs) # Copy attrs to avoid modifying the argument.
|
||||
if (not widget.use_required_attribute(value) or not widget.is_required) \
|
||||
and 'required' in attrs:
|
||||
del attrs['required']
|
||||
return attrs
|
@@ -1 +1 @@
|
||||
django-postgres-extra==1.2
|
||||
django-postgres-extra==1.11
|
||||
|
@@ -21,9 +21,12 @@ LANGUAGES = (
|
||||
)
|
||||
|
||||
INSTALLED_APPS = (
|
||||
'localized_fields',
|
||||
'tests',
|
||||
)
|
||||
|
||||
# set to a lower number than the default, since
|
||||
# we want the tests to be fast, default is 100
|
||||
LOCALIZED_FIELDS_MAX_RETRIES = 3
|
||||
|
||||
LOCALIZED_FIELDS_EXPERIMENTAL = False
|
||||
|
6
setup.py
6
setup.py
@@ -7,8 +7,8 @@ with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme:
|
||||
|
||||
setup(
|
||||
name='django-localized-fields',
|
||||
version='3.1',
|
||||
packages=find_packages(),
|
||||
version='4.2',
|
||||
packages=find_packages(exclude=['tests']),
|
||||
include_package_data=True,
|
||||
license='MIT License',
|
||||
description='Implementation of localized model fields using PostgreSQL HStore fields.',
|
||||
@@ -18,7 +18,7 @@ setup(
|
||||
author_email='open-source@sectorlabs.ro',
|
||||
keywords=['django', 'localized', 'language', 'models', 'fields'],
|
||||
install_requires=[
|
||||
'django-postgres-extra>=1.2'
|
||||
'django-postgres-extra>=1.11'
|
||||
],
|
||||
classifiers=[
|
||||
'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.contrib.postgres.operations import HStoreExtension
|
||||
|
||||
from localized_fields import LocalizedModel, AtomicSlugRetryMixin
|
||||
from localized_fields.models import LocalizedModel
|
||||
|
||||
|
||||
def define_fake_model(name='TestModel', fields=None):
|
||||
@@ -14,8 +14,8 @@ def define_fake_model(name='TestModel', fields=None):
|
||||
|
||||
if fields:
|
||||
attributes.update(fields)
|
||||
model = type(name, (AtomicSlugRetryMixin,LocalizedModel,), attributes)
|
||||
|
||||
model = type(name, (LocalizedModel,), attributes)
|
||||
return model
|
||||
|
||||
|
||||
|
@@ -1,9 +1,11 @@
|
||||
import bleach
|
||||
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
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:
|
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.test import TestCase
|
||||
|
||||
from localized_fields import LocalizedFieldForm
|
||||
from localized_fields.forms import LocalizedFieldForm
|
||||
|
||||
|
||||
class LocalizedFieldFormTestCase(TestCase):
|
@@ -1,6 +1,7 @@
|
||||
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
|
||||
|
||||
@@ -33,7 +34,6 @@ class LocalizedModelTestCase(TestCase):
|
||||
|
||||
assert isinstance(obj.title, LocalizedValue)
|
||||
|
||||
|
||||
@classmethod
|
||||
def test_model_init_kwargs(cls):
|
||||
"""Tests whether all :see:LocalizedField
|
@@ -4,10 +4,14 @@ from django import forms
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
from django.db.utils import IntegrityError
|
||||
from localized_fields import (LocalizedField, LocalizedAutoSlugField,
|
||||
LocalizedUniqueSlugField)
|
||||
from django.utils.text import slugify
|
||||
|
||||
from localized_fields.fields import (
|
||||
LocalizedField,
|
||||
LocalizedAutoSlugField,
|
||||
LocalizedUniqueSlugField
|
||||
)
|
||||
|
||||
from .fake_model import get_fake_model
|
||||
|
||||
|
@@ -1,21 +1,10 @@
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
from django.utils import translation
|
||||
from django.db.utils import IntegrityError
|
||||
|
||||
from localized_fields import LocalizedField, LocalizedValue, LocalizedFieldForm
|
||||
from localized_fields.value import 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
|
||||
from .data import get_init_values
|
||||
|
||||
|
||||
class LocalizedValueTestCase(TestCase):
|
||||
@@ -42,7 +31,7 @@ class LocalizedValueTestCase(TestCase):
|
||||
|
||||
@staticmethod
|
||||
def test_init_default_values():
|
||||
"""Tests wehther the __init__ function
|
||||
"""Tests whether the __init__ function
|
||||
of the :see:LocalizedValue accepts the
|
||||
default value or an empty dict properly."""
|
||||
|
||||
@@ -51,6 +40,18 @@ class LocalizedValueTestCase(TestCase):
|
||||
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
|
||||
@@ -161,118 +162,3 @@ class LocalizedValueTestCase(TestCase):
|
||||
|
||||
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_valuei 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
|
||||
|
||||
@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
|
||||
)
|
@@ -1,7 +1,8 @@
|
||||
from django.conf import settings
|
||||
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):
|
Reference in New Issue
Block a user