This commit is contained in:
seroy 2017-06-19 21:40:11 +03:00
commit aacc712195
34 changed files with 881 additions and 768 deletions

View File

@ -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()

View File

@ -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

View File

@ -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)

View 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

View 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)

View File

@ -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'
] ]

View File

@ -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.

View File

@ -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):

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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

View 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

View File

@ -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

View File

@ -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
})

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -21,6 +21,7 @@ LANGUAGES = (
) )
INSTALLED_APPS = ( INSTALLED_APPS = (
'localized_fields',
'tests', 'tests',
) )

View File

@ -7,7 +7,7 @@ with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme:
setup( setup(
name='django-localized-fields', name='django-localized-fields',
version='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
View 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

View File

@ -2,7 +2,7 @@ from django.db import connection, migrations
from django.db.migrations.executor import MigrationExecutor from django.db.migrations.executor import MigrationExecutor
from django.contrib.postgres.operations import HStoreExtension from django.contrib.postgres.operations import HStoreExtension
from localized_fields import LocalizedModel from localized_fields.models import LocalizedModel
def define_fake_model(name='TestModel', fields=None): def define_fake_model(name='TestModel', fields=None):

View File

@ -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
View 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
View 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
View 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
)

View File

@ -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):

View File

@ -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
)

View File

@ -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

View File

@ -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

View File

@ -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
View 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'

View File

@ -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):