83 Commits
v4.3 ... v5.0a3

Author SHA1 Message Date
Swen Kooij
88e2d29596 Bump version number to 5.0a3 2018-06-28 12:39:25 +03:00
Swen Kooij
588f32086b Merge pull request #51 from SectorLabs/multiwidget
Copy the widget for each language
2018-06-28 12:39:02 +03:00
Cristi Ingineru
13e2666a51 Copy the widget for each language 2018-06-28 12:33:44 +03:00
Swen Kooij
2393539b44 Bump version number to 5.0a2 2018-06-19 12:30:04 +03:00
Swen Kooij
e322b3d63b Upgrade django-postgres-extra to 1.21a11 2018-06-19 12:29:55 +03:00
Swen Kooij
1b1d24a460 Make defaults work for LocalizedIntegerField 2018-06-15 17:07:12 +03:00
Swen Kooij
fb233e8f25 Make sure values are strings before saving LocalizedIntegerValue 2018-06-15 16:19:32 +03:00
Swen Kooij
bca94a3508 Add quick docs on LocalizedIntegerField 2018-06-15 13:04:00 +03:00
Swen Kooij
8c83fa6b49 Bump version number to 5.0a1 2018-06-15 12:58:27 +03:00
Swen Kooij
90597da8fd Add a LocalizedIntegerField 2018-06-15 12:58:01 +03:00
Swen Kooij
752e17064d Deprecate LocalizedFileValue.localized() 2018-06-14 08:01:10 +03:00
Swen Kooij
def7dae640 Add LocalizedValue.translate()
LocalizedValue.translate() behaves the exact same as the str(..) cast
works, with the exception that it returns None if there is no value
instead of an empty string. This makes it easier to implement custom
value classes on top of the LocalizedValue class.

Behavior for str(..) stays the same as it was.
2018-06-14 07:57:02 +03:00
Swen Kooij
db4324fbf3 Bump version number to 4.6a3 2018-04-02 15:59:42 +03:00
Swen Kooij
9ff0b781ab Upgrade django-postgres-extra to 1.21a9 2018-04-02 15:59:27 +03:00
Swen Kooij
a76101c9ad Fix LocalizedFieldsAdminMixin not having a base class
This was a breaking change and broke a lot of projects.
2018-04-02 15:59:16 +03:00
Swen Kooij
04f15363bb Upgrade django-postgres-extra to 1.21a8 2018-04-01 18:28:43 +03:00
Swen Kooij
a849866bcb Make it more clear Django 2.X is supported 2018-03-31 17:05:39 +03:00
Swen Kooij
5e58bc6e24 Revert "Use standard postgres on Scrutinizer"
This reverts commit 04a750053f.
2018-03-31 16:59:11 +03:00
Swen Kooij
04a750053f Use standard postgres on Scrutinizer 2018-03-31 16:56:01 +03:00
Swen Kooij
d25b1b92fe Merge branch 'master' of https://github.com/SectorLabs/django-localized-fields 2018-03-31 16:53:48 +03:00
Swen Kooij
ccc46e1899 Allow raw dicts to be used in update statements 2018-03-31 16:53:10 +03:00
Swen Kooij
9063509817 Merge pull request #47 from MELScience/fix-admin-widget-inline-tabs
fix admin widget tabs width in inline formset
2018-03-31 16:47:36 +03:00
Swen Kooij
8fbe3e8680 Merge branch 'master' of https://github.com/SectorLabs/django-localized-fields 2018-03-31 16:42:08 +03:00
Swen Kooij
b9f14322be Bump version to 4.6a2 2018-03-31 16:40:50 +03:00
Swen Kooij
24974b2f6b Upgrade django-postgres-extra to 1.21a7 2018-03-31 16:40:29 +03:00
Swen Kooij
af02593ebd Add test to make sure LocalizedValue doesn't cast values to string 2018-03-31 08:52:12 +03:00
seroy
f161b5d047 fix admin widget tabs width in inline formset 2018-03-01 13:22:54 +03:00
Swen Kooij
77f626fc69 Merge pull request #46 from MELScience/fix-admin-widget-inline
fix admin widget work with inline formsets
2018-02-20 09:14:40 +02:00
seroy
d7889b0601 use label tag + for attribute instead of a + href for properly work with inline formsets 2018-02-20 00:58:08 +03:00
Swen Kooij
09cd0a8177 Merge pull request #44 from si14/patch-4
fix LocalizedValue.deconstruct (wrong module name)
2018-02-04 18:00:48 +02:00
Dmitry Groshev
2a29efdf14 fix LocalizedValue.deconstruct (wrong module name) 2018-01-31 23:59:48 +00:00
Dmitry Groshev
d8eb3394d0 LocalizedFieldsAdminMixin can be used with inlines
LocalizedFieldsAdminMixin was inheriting from ModelAdmin. This means in a code like this

```python
class FooInline(LocalizedFieldsAdminMixin, admin.TabularMixin):
    pass
```

`__init__` was being resolved to `ModelAdmin.__init__` which is clearly wrong.
2018-01-29 13:39:47 +02:00
Swen Kooij
b27f1535ee Merge pull request #43 from SectorLabs/fix-test-on-django20
Fix test on django20
2018-01-29 13:39:09 +02:00
Swen Kooij
cd844fccec Add support for tox to run tests against Django 1.X/2.0 2018-01-29 13:34:40 +02:00
seroy
6a4beca193 fix tests on Django 2.0 2018-01-29 13:30:43 +02:00
Swen Kooij
bf0383d742 Upgrade to alpha version of django-postgres-extra (1.21a4) 2018-01-29 12:37:44 +02:00
Swen Kooij
879b857aa5 Merge pull request #41 from MELScience/update-requirements
update requirements
2018-01-24 23:53:02 +02:00
seroy
59e8e18b2a update requirements 2018-01-22 01:19:23 +03:00
Swen Kooij
11cfe5b6e1 Add unaizalakain to the contributor list 2017-07-18 15:47:07 +03:00
Swen Kooij
084f5dd0b6 Merge pull request #36 from MELScience/issue35
Fix non-valid HTML tags attributes
2017-07-18 14:37:11 +03:00
Swen Kooij
83605cde13 Merge pull request #37 from MELScience/gitignore
Gitignore
2017-07-18 14:36:46 +03:00
seroy
7437b58f20 Ignore PyCharm 2017-07-18 14:17:45 +03:00
seroy
db6664c84f Remove wrong filename 2017-07-18 14:16:46 +03:00
seroy
928c4c624d Add test whether get_context contains 'lang_code' and 'lang_name' attribute 2017-07-18 13:54:53 +03:00
seroy
3d4f9c413e Fix non-valid HTML tags attributes 2017-07-18 13:52:17 +03:00
Swen Kooij
f434566338 Bump version number to 4.5 2017-07-18 09:40:55 +03:00
Swen Kooij
2807ed10c8 Add target link for the license Badge 2017-07-18 09:40:29 +03:00
Swen Kooij
d5f43c783a Merge pull request #34 from MELScience/widget_refactor
Use template-based widget rendering in AdminLocalizedFieldWidget
2017-07-18 09:39:00 +03:00
Swen Kooij
5347db6d8f Add contributors list to README 2017-07-18 09:38:51 +03:00
Swen Kooij
1accee0b59 Merge branch 'master' into widget_refactor 2017-07-18 09:33:59 +03:00
Swen Kooij
05bcd84a88 Merge pull request #29 from MELScience/required
Improved functionality of required parameter
2017-07-18 09:33:24 +03:00
Swen Kooij
1c2e013695 Add Django 1.11 as a dependency to setup.py
Fixes #32
2017-07-18 09:31:47 +03:00
Swen Kooij
8b76948328 Merge pull request #33 from MELScience/issue32
Django requirement updated
2017-07-18 09:29:09 +03:00
seroy
940587d748 Updated django requirement 2017-07-18 01:06:49 +03:00
seroy
6522e38f18 Added tests for LocalizedFieldWidget.get_context method 2017-07-18 01:03:50 +03:00
seroy
69cf0df166 Use template-based widget rendering in AdminLocalizedFieldWidget 2017-07-18 00:29:44 +03:00
seroy
d8c5544e91 Use template-based widget rendering in AdminLocalizedFieldWidget 2017-07-17 22:53:47 +03:00
seroy
ccc41f9143 Updated django requirement in documentation 2017-07-17 22:16:40 +03:00
seroy
073846d74b Updated django requirement 2017-07-17 22:10:19 +03:00
seroy
0f08eb8280 Add test on render method 2017-07-17 22:05:31 +03:00
seroy
60d14069d8 Updated 'required' attribute in documentation 2017-07-17 20:57:59 +03:00
seroy
33e9709373 Add tests for 'required' attribute 2017-07-17 20:55:13 +03:00
seroy
c4bf151938 Refactor required_langs into required 2017-07-17 20:53:50 +03:00
Swen Kooij
7d629c186d Add basic tests for 'required' attribute 2017-07-16 12:22:48 +03:00
Swen Kooij
cff388cae3 Add PEP8 and Flake8 as part of Scrutinizer test 2017-07-15 14:13:51 +03:00
Swen Kooij
08690ab361 Fix outstanding PEP8 and Flake8 issues 2017-07-15 14:06:50 +03:00
Swen Kooij
5ac05efbd0 Slight clean up, use dict instead of {} 2017-07-15 14:04:09 +03:00
Swen Kooij
aff41f671a Implement code review suggestions for #29 2017-07-15 13:59:39 +03:00
Swen Kooij
a38d53b609 Merge pull request #30 from SectorLabs/subwidgets-labels
Subwidgets labels
2017-07-15 13:56:29 +03:00
Swen Kooij
968840188d Formatting cleanup in widgets.py 2017-07-15 13:48:54 +03:00
Unai Zalakain
e8e044f6e2 Add labels to localized subwidgets 2017-07-15 13:45:10 +03:00
seroy
aaf49614f2 improve functionality of required parameter 2017-07-14 15:07:48 +03:00
Swen Kooij
d4c24dea97 Merge pull request #28 from MELScience/fix_imports_in_doc
fix imports in README
2017-07-11 11:21:57 +03:00
seroy
b38ded0cc3 fix imports for LocalizedCharField, LocalizedTextField and LocalizedFileField 2017-07-11 11:03:21 +03:00
Swen Kooij
226d47e766 Merge pull request #27 from MELScience/issue26
Fix incorrect indentation
2017-07-11 10:40:48 +03:00
seroy
cc4bfb48b9 Fix incorrect indentation 2017-07-11 10:27:07 +03:00
Swen Kooij
d14859a45b Bump version number to 4.4 2017-06-29 18:56:33 +03:00
Swen Kooij
cb7fda5abc Merge branch 'master' of https://github.com/SectorLabs/django-localized-fields 2017-06-29 18:52:23 +03:00
Swen Kooij
9000635f1f Open README as UTF-8 2017-06-29 18:51:53 +03:00
Swen Kooij
dabeb3b79f Merge pull request #24 from MELScience/ff_value_to_string
Added `value_to_string` method
2017-06-27 10:49:17 +03:00
seroy
0b4bb7295e Added value_to_string method 2017-06-26 18:27:03 +03:00
seroy
2b34b6751e Added test for value_to_string method 2017-06-26 18:08:15 +03:00
Swen Kooij
32696f4e1e Add test that confirms slug is re-computed when value changes 2017-06-26 14:01:25 +03:00
31 changed files with 867 additions and 183 deletions

View File

@@ -1,6 +0,0 @@
# Ignore virtual environments
env/
# Ignore Python byte code cache
*.pyc
__pycache__

6
.gitignore vendored
View File

@@ -16,3 +16,9 @@ dist/
# Ignore stupid .DS_Store
.DS_Store
# Ignore PyCharm
.idea/
# Ignore tox environments
.tox/

View File

@@ -13,19 +13,22 @@ filter:
build:
environment:
python: '3.5.0'
node: 'v6.2.0'
variables:
DJANGO_SETTINGS_MODULES: settings
DATABASE_URL: postgres://scrutinizer:scrutinizer@localhost:5434/localized_fields
postgresql: true
redis: true
dependencies:
override:
- 'pip install -r requirements/test.txt'
tests:
override:
-
command: coverage run manage.py test
command: pep8 ./localized_fields/
-
command: flake8 ./localized_fields/
-
command: tox
coverage:
file: '.coverage'
format: 'py-cc'

View File

@@ -8,13 +8,20 @@ django-localized-fields
:target: https://scrutinizer-ci.com/g/SectorLabs/django-localized-fields/
.. image:: https://img.shields.io/github/license/SectorLabs/django-localized-fields.svg
:target: https://github.com/SectorLabs/django-localized-fields/blob/master/LICENSE.md
.. image:: https://badge.fury.io/py/django-localized-fields.svg
:target: https://pypi.python.org/pypi/django-localized-fields
``django-localized-fields`` is an implementation of a field class for Django models that allows the field's value to be set in multiple languages. It does this by utilizing the ``hstore`` type (PostgreSQL specific), which is available as ``models.HStoreField`` in Django 1.10.
``django-localized-fields`` is an implementation of a field class for Django models that allows the field's value to be set in multiple languages. It does this by utilizing the ``hstore`` type (PostgreSQL specific), which is available as ``models.HStoreField`` since Django 1.10.
This package requires Python 3.5 or newer, Django 1.10 or newer and PostgreSQL 9.6 or newer.
This package requires Python 3.5 or newer, Django 1.11 or newer and PostgreSQL 9.6 or newer.
Contributors
------------
* `seroy <https://github.com/seroy/>`_
* `unaizalakain <https://github.com/unaizalakain/>`_
Installation
------------
@@ -114,6 +121,7 @@ Or get it in a specific language:
print(new.title.get('en')) # prints 'english title'
print(new.title.get('ro')) # prints 'romanian title'
print(new.title.get()) # whatever language is the primary one
print(new.title.get('ar', 'haha')) # prints 'haha' if there is no value in arabic
You can also explicitly set a value in a certain language:
@@ -129,21 +137,42 @@ Constraints
**Required/Optional**
At the moment, it is not possible to select two languages to be marked as required. The constraint is **not** enforced on a database level.
Constraints are enforced on a database level.
* Make the primary language **required** and the others optional (this is the **default**):
* Optional filling
.. code-block:: python
class MyModel(models.Model):
title = LocalizedField(required=True)
title = LocalizedField(blank=True, null=True, required=False)
* Make all languages optional:
* Make translation required for any language
.. code-block:: python
class MyModel(models.Model):
title = LocalizedField(null=True)
title = LocalizedField(blank=False, null=False, required=False)
* Make translation required for specific languages
.. code-block:: python
class MyModel(models.Model):
title = LocalizedField(blank=False, null=False, required=['en', 'ro'])
* Make translation required for all languages
.. code-block:: python
class MyModel(models.Model):
title = LocalizedField(blank=False, null=False, required=True)
* By default the primary language **required** and the others optional:
.. code-block:: python
class MyModel(models.Model):
title = LocalizedField()
**Uniqueness**
@@ -246,16 +275,31 @@ Besides ``LocalizedField``, there's also:
title = LocalizedField()
description = LocalizedBleachField()
* ``LocalizedIntegerField``
This is an experimental field type introduced in version 5.0 and is subject to change. It also has
some pretty major downsides due to the fact that values are stored as strings and are converted
back and forth.
Allows storing integers in multiple languages. This works exactly like ``LocalizedField`` except that
all values must be integers. Do note that values are stored as strings in your database because
the backing field type is ``hstore``, which only allows storing integers. The ``LocalizedIntegerField``
takes care of ensuring that all values are integers and converts the stored strings back to integers
when retrieving them from the database. Do not expect to be able to do queries such as:
.. code-block:: python
MyModel.objects.filter(score__en__gt=1)
* ``LocalizedCharField`` and ``LocalizedTextField``
This fields following the Django convention for string-based fields use the empty string as value for “no data”, not NULL.
These fields following the Django convention for string-based fields use the empty string as value for “no data”, not NULL.
``LocalizedCharField`` uses ``TextInput`` (``<input type="text">``) widget for render.
Example usage:
.. code-block:: python
from localized_fields import (LocalizedCharField,
LocalizedTextField)
from localized_fields.fields import LocalizedCharField, LocalizedTextField
class MyModel(models.Model):
title = LocalizedCharField()
@@ -270,7 +314,7 @@ Besides ``LocalizedField``, there's also:
.. code-block:: python
from localized_fields import LocalizedFileField
from localized_fields.fields import LocalizedFileField
def my_directory_path(instance, filename, lang):
# file will be uploaded to MEDIA_ROOT/<lang>/<id>_<filename>
@@ -334,9 +378,9 @@ Frequently asked questions (FAQ)
No. Only Python 3.5 or newer is supported. We're using type hints. These do not work well under older versions of Python.
2. Does this package work with Django 1.X?
2. With what Django versions does this package work?
No. Only Django 1.10 or newer is supported. This is because we rely on Django's ``HStoreField``.
Only Django 1.11 or newer is supported, this includes Django 2.X. This is because we rely on Django's ``HStoreField`` and template-based widget rendering.
3. Does this package come with support for Django Admin?

View File

@@ -30,7 +30,7 @@ class LocalizedFieldsAdminMixin(ModelAdmin):
def __init__(self, *args, **kwargs):
"""Initializes a new instance of :see:LocalizedFieldsAdminMixin."""
super(LocalizedFieldsAdminMixin, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
overrides = FORMFIELD_FOR_LOCALIZED_FIELDS_DEFAULTS.copy()
overrides.update(self.formfield_overrides)
self.formfield_overrides = overrides

View File

@@ -4,6 +4,7 @@ from .uniqueslug_field import LocalizedUniqueSlugField
from .char_field import LocalizedCharField
from .text_field import LocalizedTextField
from .file_field import LocalizedFileField
from .integer_field import LocalizedIntegerField
__all__ = [
@@ -12,7 +13,8 @@ __all__ = [
'LocalizedUniqueSlugField',
'LocalizedCharField',
'LocalizedTextField',
'LocalizedFileField'
'LocalizedFileField',
'LocalizedIntegerField'
]
try:

View File

@@ -1,6 +1,6 @@
import json
from typing import Union
from typing import Union, List, Optional
from django.conf import settings
from django.db.utils import IntegrityError
@@ -27,10 +27,17 @@ class LocalizedField(HStoreField):
# The descriptor to use for accessing the attribute off of the class.
descriptor_class = LocalizedValueDescriptor
def __init__(self, *args, **kwargs):
def __init__(self, *args, required: Union[bool, List[str]]=None, **kwargs):
"""Initializes a new instance of :see:LocalizedField."""
super(LocalizedField, self).__init__(*args, **kwargs)
super(LocalizedField, self).__init__(*args, required=required, **kwargs)
if (self.required is None and self.blank) or self.required is False:
self.required = []
elif self.required is None and not self.blank:
self.required = [settings.LANGUAGE_CODE]
elif self.required is True:
self.required = [lang_code for lang_code, _ in settings.LANGUAGES]
def contribute_to_class(self, model, name, **kwargs):
"""Adds this field to the specifed model.
@@ -46,7 +53,7 @@ class LocalizedField(HStoreField):
setattr(model, self.name, self.descriptor_class(self))
@classmethod
def from_db_value(cls, value, *_):
def from_db_value(cls, value, *_) -> Optional[LocalizedValue]:
"""Turns the specified database value into its Python
equivalent.
@@ -124,6 +131,8 @@ class LocalizedField(HStoreField):
specified, we'll treat it as an empty :see:LocalizedValue
instance, on which the validation will fail.
Dictonaries are converted into :see:LocalizedValue instances.
Arguments:
value:
The :see:LocalizedValue instance to serialize
@@ -134,6 +143,9 @@ class LocalizedField(HStoreField):
extracted from the specified value.
"""
if isinstance(value, dict):
value = LocalizedValue(value)
# default to None if this is an unknown type
if not isinstance(value, LocalizedValue) and value:
value = None
@@ -170,9 +182,6 @@ class LocalizedField(HStoreField):
# are any of the language fiels None/empty?
is_all_null = True
for lang_code, _ in settings.LANGUAGES:
# NOTE(seroy): use check for None, instead of
# `bool(value.get(lang_code))==True` condition, cause in this way
# we can not save '' value
if value.get(lang_code) is not None:
is_all_null = False
break
@@ -185,8 +194,8 @@ class LocalizedField(HStoreField):
return value
def validate(self, value: LocalizedValue, *_):
"""Validates that the value for the primary language
has been filled in.
"""Validates that the values has been filled in for all required
languages
Exceptions are raises in order to notify the user
of invalid values.
@@ -199,36 +208,19 @@ class LocalizedField(HStoreField):
if self.null:
return
primary_lang_val = getattr(value, settings.LANGUAGE_CODE)
for lang in self.required:
lang_val = getattr(value, settings.LANGUAGE_CODE)
# NOTE(seroy): use check for None, instead of `not primary_lang_val`
# condition, cause in this way we can not save '' value
if primary_lang_val is None:
raise IntegrityError(
'null value in column "%s.%s" violates not-null constraint' % (
self.name,
settings.LANGUAGE_CODE
)
)
if lang_val is None:
raise IntegrityError('null value in column "%s.%s" violates '
'not-null constraint' % (self.name, lang))
def formfield(self, **kwargs):
"""Gets the form field associated with this field."""
defaults = {
'form_class': LocalizedFieldForm
}
defaults = dict(
form_class=LocalizedFieldForm,
required=False if self.blank else self.required
)
defaults.update(kwargs)
return super().formfield(**defaults)
def deconstruct(self):
"""Gets the values to pass to :see:__init__ when
re-creating this object."""
name, path, args, kwargs = super(
LocalizedField, self).deconstruct()
if self.uniqueness:
kwargs['uniqueness'] = self.uniqueness
return name, path, args, kwargs

View File

@@ -1,3 +1,4 @@
import json
import datetime
import posixpath
@@ -145,7 +146,13 @@ class LocalizedFileField(LocalizedField):
def formfield(self, **kwargs):
defaults = {'form_class': LocalizedFileFieldForm}
if 'initial' in kwargs:
defaults['required'] = False
defaults.update(kwargs)
return super().formfield(**defaults)
def value_to_string(self, obj):
value = self.value_from_object(obj)
if isinstance(value, LocalizedFileValue):
return json.dumps({k: v.name for k, v
in value.__dict__.items()})
else:
return super().value_to_string(obj)

View File

@@ -0,0 +1,81 @@
from typing import Optional, Union, Dict
from django.conf import settings
from django.db.utils import IntegrityError
from .field import LocalizedField
from ..value import LocalizedValue, LocalizedIntegerValue
class LocalizedIntegerField(LocalizedField):
"""Stores integers as a localized value."""
attr_class = LocalizedIntegerValue
@classmethod
def from_db_value(cls, value, *_) -> Optional[LocalizedIntegerValue]:
db_value = super().from_db_value(value)
if db_value is None:
return db_value
# if we were used in an expression somehow then it might be
# that we're returning an individual value or an array, so
# we should not convert that into an :see:LocalizedIntegerValue
if not isinstance(db_value, LocalizedValue):
return db_value
return cls._convert_localized_value(db_value)
def to_python(self, value: Union[Dict[str, int], int, None]) -> LocalizedIntegerValue:
"""Converts the value from a database value into a Python value."""
db_value = super().to_python(value)
return self._convert_localized_value(db_value)
def get_prep_value(self, value: LocalizedIntegerValue) -> dict:
"""Gets the value in a format to store into the database."""
# apply default values
default_values = LocalizedIntegerValue(self.default)
if isinstance(value, LocalizedIntegerValue):
for lang_code, _ in settings.LANGUAGES:
local_value = value.get(lang_code)
if local_value is None:
value.set(lang_code, default_values.get(lang_code, None))
prepped_value = super().get_prep_value(value)
if prepped_value is None:
return None
# make sure all values are proper integers
for lang_code, _ in settings.LANGUAGES:
local_value = prepped_value[lang_code]
try:
if local_value is not None:
int(local_value)
except (TypeError, ValueError):
raise IntegrityError('non-integer value in column "%s.%s" violates '
'integer constraint' % (self.name, lang_code))
# convert to a string before saving because the underlying
# type is hstore, which only accept strings
prepped_value[lang_code] = str(local_value) if local_value is not None else None
return prepped_value
@staticmethod
def _convert_localized_value(value: LocalizedValue) -> LocalizedIntegerValue:
"""Converts from :see:LocalizedValue to :see:LocalizedIntegerValue."""
integer_values = {}
for lang_code, _ in settings.LANGUAGES:
local_value = value.get(lang_code, None)
if local_value is None or local_value.strip() == '':
local_value = None
try:
integer_values[lang_code] = int(local_value)
except (ValueError, TypeError):
integer_values[lang_code] = None
return LocalizedIntegerValue(integer_values)

View File

@@ -1,4 +1,4 @@
from typing import List
from typing import List, Union
from django import forms
from django.conf import settings
@@ -19,28 +19,29 @@ class LocalizedFieldForm(forms.MultiValueField):
field_class = forms.fields.CharField
value_class = LocalizedValue
def __init__(self, *args, **kwargs):
def __init__(self, *args, required: Union[bool, List[str]]=False, **kwargs):
"""Initializes a new instance of :see:LocalizedFieldForm."""
fields = []
for lang_code, _ in settings.LANGUAGES:
field_options = {'required': False}
if lang_code == settings.LANGUAGE_CODE:
field_options['required'] = kwargs.get('required', True)
field_options['label'] = lang_code
field_options = dict(
required=required if type(required) is bool else (lang_code in
required),
label=lang_code
)
fields.append(self.field_class(**field_options))
super(LocalizedFieldForm, self).__init__(
fields,
required=required if type(required) is bool else True,
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
for field, widget in zip(self.fields, self.widget.widgets):
widget.is_required = field.required
def compress(self, value: List[str]) -> value_class:
"""Compresses the values from individual fields

View File

@@ -1,5 +1,5 @@
.localized-fields-widget {
margin-left: 160px;
display: inline-block;
}
.localized-fields-widget.tabs {
@@ -32,11 +32,12 @@
opacity: 1;
}
.localized-fields-widget.tabs .localized-fields-widget.tab a {
.localized-fields-widget.tabs .localized-fields-widget.tab label {
padding: 5px 10px;
display: inline-block;
text-decoration: none;
color: #fff;
width: initial;
}
.localized-fields-widget.tabs .localized-fields-widget.tab.active,

View File

@@ -1,10 +1,10 @@
(function($) {
var syncTabs = function(lang) {
$('.localized-fields-widget.tab a:contains("'+lang+'")').each(function(){
$('.localized-fields-widget.tab label: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();
$('#'+$(this).attr('for')).show();
});
}
@@ -13,7 +13,7 @@
// 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();
$('#'+$(this).find('.localized-fields-widget.tab:first label').attr('for')).show();
});
// try set active last selected tab
if (window.sessionStorage) {
@@ -23,7 +23,7 @@
}
}
$('.localized-fields-widget.tab a').click(function(event) {
$('.localized-fields-widget.tab label').click(function(event) {
event.preventDefault();
syncTabs(this.innerText);
if (window.sessionStorage) {

View File

@@ -1,14 +1,16 @@
{% with widget_id=widget.attrs.id %}
<div class="localized-fields-widget" role="tabs" data-synctabs="translation">
<ul class="localized-fields-widget tabs">
{% for key, lang in available_languages %}
{% for widget in widget.subwidgets %}
<li class="localized-fields-widget tab">
<a href="#{{ id }}_{{ key }}">{{ lang|capfirst }}</a>
<label for="{{ widget_id }}_{{ widget.lang_code }}">{{ widget.lang_name|capfirst }}</label>
</li>
{% endfor %}
</ul>
{% for key, widget in widgets %}
<div role="tabpanel" id="{{ id }}_{{ key }}">
{{ widget }}
{% for widget in widget.subwidgets %}
<div role="tabpanel" id="{{ widget_id }}_{{ widget.lang_code }}">
{% include widget.template_name %}
</div>
{% endfor %}
</div>
{% endwith %}

View File

@@ -0,0 +1,16 @@
{% with widget_id=widget.attrs.id %}
<div class="localized-fields-widget" role="tabs" data-synctabs="translation">
<ul class="localized-fields-widget tabs">
{% for widget in widget.subwidgets %}
<li class="localized-fields-widget tab">
<label for="{{ widget_id }}_{{ widget.lang_code }}">{{ widget.lang_name|capfirst }}</label>
</li>
{% endfor %}
</ul>
{% for widget in widget.subwidgets %}
<div role="tabpanel" id="{{ widget_id }}_{{ widget.lang_code }}">
{% include widget.template_name %}
</div>
{% endfor %}
</div>
{% endwith %}

View File

@@ -1,5 +1,7 @@
import deprecation
import collections
from typing import Optional
from django.conf import settings
from django.utils import translation
@@ -36,7 +38,8 @@ class LocalizedValue(dict):
"""
language = language or settings.LANGUAGE_CODE
return super().get(language, default)
value = super().get(language, default)
return value if value is not None else default
def set(self, language: str, value: str):
"""Sets the value in the specified language.
@@ -61,7 +64,7 @@ class LocalizedValue(dict):
contained in this instance.
"""
path = 'localized_fields.localized_value.%s' % self.__class__.__name__
path = 'localized_fields.value.%s' % self.__class__.__name__
return path, [self.__dict__], {}
def _interpret_value(self, value):
@@ -98,10 +101,10 @@ class LocalizedValue(dict):
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
in the current language."""
def translate(self) -> Optional[str]:
"""Gets the value in the current language or falls
back to the next language if there's no value in the
current language."""
fallbacks = getattr(settings, 'LOCALIZED_FIELDS_FALLBACKS', {})
@@ -112,9 +115,16 @@ class LocalizedValue(dict):
for lang_code in languages:
value = self.get(lang_code)
if value:
return value or ''
return value or None
return ''
return None
def __str__(self) -> str:
"""Gets the value in the current language or falls
back to the next language if there's no value in the
current language."""
return self.translate() or ''
def __eq__(self, other):
"""Compares :paramref:self to :paramref:other for
@@ -183,8 +193,38 @@ class LocalizedFileValue(LocalizedValue):
def __str__(self) -> str:
"""Returns string representation of value"""
return str(super().__str__())
@deprecation.deprecated(deprecated_in='4.6', removed_in='5.0',
current_version='4.6',
details='Use the translate() function instead.')
def localized(self):
"""Returns value for current language"""
return self.get(translation.get_language())
class LocalizedIntegerValue(LocalizedValue):
"""All values are integers."""
default_value = None
def translate(self):
"""Gets the value in the current language, or
in the configured fallbck language."""
value = super().translate()
if value is None or (isinstance(value, str) and value.strip() == ''):
return None
return int(value)
def __int__(self):
"""Gets the value in the current language as an integer."""
value = self.translate()
if value is None:
return self.default_value
return int(value)

View File

@@ -1,27 +1,34 @@
import copy
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."""
template_name = 'localized_fields/multiwidget.html'
widget = forms.Textarea
def __init__(self, *args, **kwargs):
"""Initializes a new instance of :see:LocalizedFieldWidget."""
initial_widgets = [
self.widget
copy.copy(self.widget)
for _ in settings.LANGUAGES
]
super().__init__(initial_widgets, *args, **kwargs)
for ((lang_code, lang_name), widget) in zip(settings.LANGUAGES, self.widgets):
widget.attrs['lang'] = lang_code
widget.lang_code = lang_code
widget.lang_name = lang_name
def decompress(self, value: LocalizedValue) -> List[str]:
"""Decompresses the specified value so
it can be spread over the internal widgets.
@@ -44,6 +51,53 @@ class LocalizedFieldWidget(forms.MultiWidget):
return result
def get_context(self, name, value, attrs):
context = super(forms.MultiWidget, self).get_context(name, value, attrs)
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)
final_attrs = context['widget']['attrs']
input_type = final_attrs.pop('type', None)
id_ = final_attrs.get('id')
subwidgets = []
for i, widget in enumerate(self.widgets):
if input_type is not None:
widget.input_type = input_type
widget_name = '%s_%s' % (name, i)
try:
widget_value = value[i]
except IndexError:
widget_value = None
if id_:
widget_attrs = final_attrs.copy()
widget_attrs['id'] = '%s_%s' % (id_, i)
else:
widget_attrs = final_attrs
widget_attrs = self.build_widget_attrs(widget, widget_value, widget_attrs)
widget_context = widget.get_context(widget_name, widget_value, widget_attrs)['widget']
widget_context.update(dict(
lang_code=widget.lang_code,
lang_name=widget.lang_name
))
subwidgets.append(widget_context)
context['widget']['subwidgets'] = subwidgets
return 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
class LocalizedCharFieldWidget(LocalizedFieldWidget):
"""Widget that has an input box for every language."""
@@ -56,44 +110,8 @@ class LocalizedFileWidget(LocalizedFieldWidget):
class AdminLocalizedFieldWidget(LocalizedFieldWidget):
template_name = 'localized_fields/admin/widget.html'
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
class AdminLocalizedCharFieldWidget(AdminLocalizedFieldWidget):

View File

@@ -1 +1,2 @@
django-postgres-extra==1.11
django-postgres-extra==1.21a9
deprecation==2.0.3

View File

@@ -1,17 +1,16 @@
-r base.txt
coverage==4.2
Django==1.10.2
django-autoslug==1.9.3
django-bleach==0.3.0
django-coverage-plugin==1.3.1
psycopg2==2.6.2
pylint==1.6.4
pylint-common==0.2.2
pylint-django==0.7.2
pylint-plugin-utils==0.2.4
coverage==4.2
psycopg2==2.7.3.2
pylint==1.8.1
pylint-common==0.2.5
pylint-django==0.8.0
pylint-plugin-utils==0.2.6
coverage==4.4.2
django-coverage-plugin==1.3.1
flake8==3.0.4
pep8==1.7.0
dj-database-url==0.4.1
flake8==3.5.0
pep8==1.7.1
dj-database-url==0.4.2
tox==2.9.1

View File

@@ -2,12 +2,12 @@ import os
from setuptools import find_packages, setup
with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme:
with open(os.path.join(os.path.dirname(__file__), 'README.rst'), encoding='utf-8') as readme:
README = readme.read()
setup(
name='django-localized-fields',
version='4.3',
version='5.0a3',
packages=find_packages(exclude=['tests']),
include_package_data=True,
license='MIT License',
@@ -18,7 +18,9 @@ setup(
author_email='open-source@sectorlabs.ro',
keywords=['django', 'localized', 'language', 'models', 'fields'],
install_requires=[
'django-postgres-extra>=1.11'
'django-postgres-extra>=1.21a11',
'Django>=1.11',
'deprecation==2.0.3'
],
classifiers=[
'Environment :: Web Environment',

View File

@@ -32,7 +32,7 @@ class LocalizedExpressionsTestCase(TestCase):
cls.TestModel2 = get_fake_model(
{
'text': LocalizedField(),
'other': models.ForeignKey(cls.TestModel1, related_name='features')
'other': models.ForeignKey(cls.TestModel1, related_name='features', on_delete=models.CASCADE)
}
)

View File

@@ -9,11 +9,31 @@ from localized_fields.forms import LocalizedFieldForm
from localized_fields.value import LocalizedValue
from .data import get_init_values
from .fake_model import get_fake_model
class LocalizedFieldTestCase(TestCase):
"""Tests the :see:LocalizedField class."""
@staticmethod
def test_init():
"""Tests whether the :see:__init__ function
correctly handles parameters"""
field = LocalizedField(blank=True)
assert field.required == []
field = LocalizedField(blank=False)
assert field.required == [settings.LANGUAGE_CODE]
field = LocalizedField(required=True)
assert field.required == [lang_code for lang_code, _ in
settings.LANGUAGES]
field = LocalizedField(required=False)
assert field.required == []
@staticmethod
def test_from_db_value():
"""Tests whether the :see:from_db_value function
@@ -156,3 +176,90 @@ class LocalizedFieldTestCase(TestCase):
LocalizedField().formfield(),
LocalizedFieldForm
)
# case optional filling
field = LocalizedField(blank=True, required=[])
assert not field.formfield().required
for field in field.formfield().fields:
assert not field.required
# case required for any language
field = LocalizedField(blank=False, required=[])
assert field.formfield().required
for field in field.formfield().fields:
assert not field.required
# case required for specific languages
required_langs = ['ro', 'nl']
field = LocalizedField(blank=False, required=required_langs)
assert field.formfield().required
for field in field.formfield().fields:
if field.label in required_langs:
assert field.required
else:
assert not field.required
# case required for all languages
field = LocalizedField(blank=False, required=True)
assert field.formfield().required
for field in field.formfield().fields:
assert field.required
def test_required_all(self):
"""Tests whether passing required=True properly validates
that all languages are filled in."""
model = get_fake_model(dict(
title=LocalizedField(required=True)
))
with self.assertRaises(IntegrityError):
model.objects.create(title=dict(ro='romanian', nl='dutch'))
with self.assertRaises(IntegrityError):
model.objects.create(title=dict(nl='dutch'))
with self.assertRaises(IntegrityError):
model.objects.create(title=dict(random='random'))
with self.assertRaises(IntegrityError):
model.objects.create(title=dict())
with self.assertRaises(IntegrityError):
model.objects.create(title=None)
with self.assertRaises(IntegrityError):
model.objects.create(title='')
with self.assertRaises(IntegrityError):
model.objects.create(title=' ')
def test_required_some(self):
"""Tests whether passing an array to required,
properly validates whether the specified languages
are marked as required."""
model = get_fake_model(dict(
title=LocalizedField(required=['nl', 'ro'])
))
with self.assertRaises(IntegrityError):
model.objects.create(title=dict(ro='romanian', nl='dutch'))
with self.assertRaises(IntegrityError):
model.objects.create(title=dict(nl='dutch'))
with self.assertRaises(IntegrityError):
model.objects.create(title=dict(random='random'))
with self.assertRaises(IntegrityError):
model.objects.create(title=dict())
with self.assertRaises(IntegrityError):
model.objects.create(title=None)
with self.assertRaises(IntegrityError):
model.objects.create(title='')
with self.assertRaises(IntegrityError):
model.objects.create(title=' ')

View File

@@ -2,6 +2,7 @@ import os
import shutil
import tempfile as sys_tempfile
import pickle
import json
from django import forms
from django.test import TestCase, override_settings
@@ -71,7 +72,7 @@ class LocalizedFileFieldTestCase(TestCase):
@classmethod
def test_save_form_data(cls):
"""Tests whether the :see:save_form_data function correctly set
"""Tests whether the :see:save_form_data function correctly set
a valid value."""
instance = cls.FileFieldModel()
@@ -86,7 +87,7 @@ class LocalizedFileFieldTestCase(TestCase):
instance = cls.FileFieldModel()
instance.file = {'en': ContentFile("test", "testfilename")}
instance._meta.get_field('file').pre_save(instance, False)
assert instance.file.en._committed == True
assert instance.file.en._committed is True
@classmethod
def test_file_methods(cls):
@@ -118,19 +119,33 @@ class LocalizedFileFieldTestCase(TestCase):
filename = field.generate_filename(instance, 'test', 'en')
assert filename == 'en_test'
@classmethod
@override_settings(LANGUAGES=(('en', 'English'),))
def test_value_to_string(cls):
"""Tests whether the :see:LocalizedFileField
class's :see:value_to_string function works properly."""
temp_file = File(tempfile.NamedTemporaryFile())
instance = cls.FileFieldModel()
field = cls.FileFieldModel._meta.get_field('file')
field.upload_to = ''
instance.file.en.save('testfilename', temp_file)
expected_value_to_string = json.dumps({'en': 'testfilename'})
assert field.value_to_string(instance) == expected_value_to_string
@staticmethod
def test_get_prep_value():
"""Tests whether the :see:get_prep_value function returns correctly
"""Tests whether the :see:get_prep_value function returns correctly
value."""
value = LocalizedValue({'en': None})
assert LocalizedFileField().get_prep_value(None) == None
assert LocalizedFileField().get_prep_value(None) is None
assert isinstance(LocalizedFileField().get_prep_value(value), dict)
assert LocalizedFileField().get_prep_value(value)['en'] == ''
@staticmethod
def test_formfield():
"""Tests whether the :see:formfield function correctly returns
"""Tests whether the :see:formfield function correctly returns
a valid form."""
form_field = LocalizedFileField().formfield()
@@ -146,6 +161,5 @@ class LocalizedFileFieldTestCase(TestCase):
name, path, args, kwargs = LocalizedFileField().deconstruct()
assert 'upload_to' in kwargs
assert 'storage' not in kwargs
name, path, \
args, kwargs = LocalizedFileField(storage='test').deconstruct()
name, path, args, kwargs = LocalizedFileField(storage='test').deconstruct()
assert 'storage' in kwargs

View File

@@ -28,7 +28,7 @@ class LocalizedFileFieldFormTestCase(TestCase):
formfield.clean(['', ''], ['', ''])
def test_bound_data(self):
"""Tests whether the :see:bound_data function is returns correctly
"""Tests whether the :see:bound_data function is returns correctly
value"""
formfield = LocalizedFileFieldForm()
@@ -38,4 +38,3 @@ class LocalizedFileFieldFormTestCase(TestCase):
value = [None] * len(settings.LANGUAGES)
expected_value = [''] * len(settings.LANGUAGES)
assert formfield.bound_data(value, initial) == expected_value

24
tests/test_file_widget.py Normal file
View File

@@ -0,0 +1,24 @@
from django.test import TestCase
from localized_fields.value import LocalizedFileValue
from localized_fields.widgets import LocalizedFileWidget
class LocalizedFileWidgetTestCase(TestCase):
"""Tests the workings of the :see:LocalizedFiledWidget class."""
@staticmethod
def test_get_context():
"""Tests whether the :see:get_context correctly
handles 'required' attribute, separately for each subwidget."""
widget = LocalizedFileWidget()
widget.widgets[0].is_required = True
widget.widgets[1].is_required = True
widget.widgets[2].is_required = False
context = widget.get_context(name='test',
value=LocalizedFileValue(dict(en='test')),
attrs=dict(required=True))
assert 'required' not in context['widget']['subwidgets'][0]['attrs']
assert context['widget']['subwidgets'][1]['attrs']['required']
assert 'required' not in context['widget']['subwidgets'][2]['attrs']

View File

@@ -11,8 +11,8 @@ class LocalizedFieldFormTestCase(TestCase):
def test_init():
"""Tests whether the constructor correctly
creates a field for every language."""
form = LocalizedFieldForm()
# case required for specific language
form = LocalizedFieldForm(required=[settings.LANGUAGE_CODE])
for (lang_code, _), field in zip(settings.LANGUAGES, form.fields):
assert field.label == lang_code
@@ -22,6 +22,25 @@ class LocalizedFieldFormTestCase(TestCase):
else:
assert not field.required
# case required for all languages
form = LocalizedFieldForm(required=True)
assert form.required
for field in form.fields:
assert field.required
# case optional filling
form = LocalizedFieldForm(required=False)
assert not form.required
for field in form.fields:
assert not field.required
# case required for any language
form = LocalizedFieldForm(required=[])
assert form.required
for field in form.fields:
assert not field.required
@staticmethod
def test_compress():
"""Tests whether the :see:compress function

176
tests/test_integer_field.py Normal file
View File

@@ -0,0 +1,176 @@
from django.test import TestCase
from django.db.utils import IntegrityError
from django.conf import settings
from django.db import connection
from django.utils import translation
from localized_fields.fields import LocalizedIntegerField
from .fake_model import get_fake_model
class LocalizedIntegerFieldTestCase(TestCase):
"""Tests whether the :see:LocalizedIntegerField
and :see:LocalizedIntegerValue works properly."""
TestModel = None
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.TestModel = get_fake_model({
'score': LocalizedIntegerField()
})
def test_basic(self):
"""Tests the basics of storing integer values."""
obj = self.TestModel()
for index, (lang_code, _) in enumerate(settings.LANGUAGES):
obj.score.set(lang_code, index + 1)
obj.save()
obj = self.TestModel.objects.all().first()
for index, (lang_code, _) in enumerate(settings.LANGUAGES):
assert obj.score.get(lang_code) == index + 1
def test_primary_language_required(self):
"""Tests whether the primary language is required by
default and all other languages are optiona."""
# not filling in anything should raise IntegrityError,
# the primary language is required
with self.assertRaises(IntegrityError):
obj = self.TestModel()
obj.save()
# when filling all other languages besides the primary language
# should still raise an error because the primary is always required
with self.assertRaises(IntegrityError):
obj = self.TestModel()
for lang_code, _ in settings.LANGUAGES:
if lang_code == settings.LANGUAGE_CODE:
continue
obj.score.set(lang_code, 23)
obj.save()
def test_default_value_none(self):
"""Tests whether the default value for optional languages
is NoneType."""
obj = self.TestModel()
obj.score.set(settings.LANGUAGE_CODE, 1234)
obj.save()
for lang_code, _ in settings.LANGUAGES:
if lang_code == settings.LANGUAGE_CODE:
continue
assert obj.score.get(lang_code) is None
def test_translate(self):
"""Tests whether casting the value to an integer
results in the value being returned in the currently
active language as an integer."""
obj = self.TestModel()
for index, (lang_code, _) in enumerate(settings.LANGUAGES):
obj.score.set(lang_code, index + 1)
obj.save()
obj.refresh_from_db()
for index, (lang_code, _) in enumerate(settings.LANGUAGES):
with translation.override(lang_code):
assert int(obj.score) == index + 1
assert obj.score.translate() == index + 1
def test_translate_primary_fallback(self):
"""Tests whether casting the value to an integer
results in the value begin returned in the active
language and falls back to the primary language
if there is no value in that language."""
obj = self.TestModel()
obj.score.set(settings.LANGUAGE_CODE, 25)
secondary_language = settings.LANGUAGES[-1][0]
assert obj.score.get(secondary_language) is None
with translation.override(secondary_language):
assert obj.score.translate() == 25
assert int(obj.score) == 25
def test_get_default_value(self):
"""Tests whether getting the value in a specific
language properly returns the specified default
in case it is not available."""
obj = self.TestModel()
obj.score.set(settings.LANGUAGE_CODE, 25)
secondary_language = settings.LANGUAGES[-1][0]
assert obj.score.get(secondary_language) is None
assert obj.score.get(secondary_language, 1337) == 1337
def test_completely_optional(self):
"""Tests whether having all languages optional
works properly."""
model = get_fake_model({
'score': LocalizedIntegerField(null=True, required=[], blank=True)
})
obj = model()
obj.save()
for lang_code, _ in settings.LANGUAGES:
assert getattr(obj.score, lang_code) is None
def test_store_string(self):
"""Tests whether the field properly raises
an error when trying to store a non-integer."""
for lang_code, _ in settings.LANGUAGES:
obj = self.TestModel()
with self.assertRaises(IntegrityError):
obj.score.set(lang_code, 'haha')
obj.save()
def test_none_if_illegal_value_stored(self):
"""Tests whether None is returned for a language
if the value stored in the database is not an
integer."""
obj = self.TestModel()
obj.score.set(settings.LANGUAGE_CODE, 25)
obj.save()
with connection.cursor() as cursor:
table_name = self.TestModel._meta.db_table
cursor.execute("update %s set score = 'en=>haha'" % table_name);
obj.refresh_from_db()
assert obj.score.get(settings.LANGUAGE_CODE) is None
def test_default_value(self):
"""Tests whether a default is properly set
when specified."""
model = get_fake_model({
'score': LocalizedIntegerField(default={settings.LANGUAGE_CODE: 75})
})
obj = model.objects.create()
assert obj.score.get(settings.LANGUAGE_CODE) == 75
obj = model()
for lang_code, _ in settings.LANGUAGES:
obj.score.set(lang_code, None)
obj.save()
for lang_code, _ in settings.LANGUAGES:
if lang_code == settings.LANGUAGE_CODE:
assert obj.score.get(lang_code) == 75
else:
assert obj.score.get(lang_code) is None

47
tests/test_query_set.py Normal file
View File

@@ -0,0 +1,47 @@
from django.conf import settings
from django.test import TestCase
from django.utils import translation
from localized_fields.fields import LocalizedField
from .fake_model import get_fake_model
class LocalizedQuerySetTestCase(TestCase):
"""Tests query sets with models containing :see:LocalizedField."""
Model = None
@classmethod
def setUpClass(cls):
"""Creates the test models in the database."""
super(LocalizedQuerySetTestCase, cls).setUpClass()
cls.Model = get_fake_model(
{
'title': LocalizedField(),
}
)
@classmethod
def test_assign_raw_dict(cls):
inst = cls.Model()
inst.title = dict(en='Bread', ro='Paine')
inst.save()
inst = cls.Model.objects.get(pk=inst.pk)
assert inst.title.en == 'Bread'
assert inst.title.ro == 'Paine'
@classmethod
def test_assign_raw_dict_update(cls):
inst = cls.Model.objects.create(
title=dict(en='Bread', ro='Paine'))
cls.Model.objects.update(
title=dict(en='Beer', ro='Bere'))
inst = cls.Model.objects.get(pk=inst.pk)
assert inst.title.en == 'Beer'
assert inst.title.ro == 'Bere'

View File

@@ -81,6 +81,14 @@ class LocalizedSlugFieldTestCase(TestCase):
assert old_slug_en == obj.slug.en
assert old_slug_nl != obj.slug.nl
@classmethod
def test_unique_slug_update(cls):
obj = cls.Model.objects.create(title={settings.LANGUAGE_CODE: 'mytitle'})
assert obj.slug.get() == 'mytitle'
obj.title.set(settings.LANGUAGE_CODE, 'othertitle')
obj.save()
assert obj.slug.get() == 'othertitle'
@classmethod
def test_unique_slug_unique_max_retries(cls):
"""Tests whether the unique slug implementation doesn't
@@ -116,7 +124,7 @@ class LocalizedSlugFieldTestCase(TestCase):
def generate_slug(instance):
return instance.title
model = get_fake_model({
get_fake_model({
'title': LocalizedField(),
'slug': LocalizedUniqueSlugField(populate_from=generate_slug)
})
@@ -130,7 +138,6 @@ class LocalizedSlugFieldTestCase(TestCase):
for lang_code, lang_name in settings.LANGUAGES:
assert obj.slug.get(lang_code) == 'title-%s' % lang_name.lower()
@staticmethod
def test_populate_multiple_from_fields():
"""Tests whether populating the slug from multiple
@@ -168,7 +175,7 @@ class LocalizedSlugFieldTestCase(TestCase):
model = get_fake_model(
{
'title': LocalizedField(),
'other': models.ForeignKey(model_fk),
'other': models.ForeignKey(model_fk, on_delete=models.CASCADE),
'slug': LocalizedUniqueSlugField(populate_from=('title', 'other.name'))
}
)

View File

@@ -1,6 +1,7 @@
from django.db.models import F
from django.conf import settings
from django.test import TestCase
from django.utils import translation
from django.test import TestCase, override_settings
from localized_fields.value import LocalizedValue
@@ -89,18 +90,6 @@ class LocalizedValueTestCase(TestCase):
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
@@ -115,9 +104,21 @@ class LocalizedValueTestCase(TestCase):
assert a != b
@staticmethod
def test_str_fallback():
def test_translate():
"""Tests whether the :see:LocalizedValue
class's __str__'s fallback functionality
class's __str__ works properly."""
keys = get_init_values()
localized_value = LocalizedValue(keys)
for language, value in keys.items():
translation.activate(language)
assert localized_value.translate() == value
@staticmethod
def test_translate_fallback():
"""Tests whether the :see:LocalizedValue
class's translate()'s fallback functionality
works properly."""
test_value = 'myvalue'
@@ -130,13 +131,13 @@ class LocalizedValueTestCase(TestCase):
# make sure that, by default it returns
# the value in the default language
assert str(localized_value) == test_value
assert localized_value.translate() == 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
assert localized_value.translate() == test_value
# make sure that it's just __str__ falling
# back and that for the other language
@@ -144,14 +145,35 @@ class LocalizedValueTestCase(TestCase):
assert localized_value.get(other_language) != test_value
@staticmethod
def test_str_fallback_custom_fallback():
def test_translate_none():
"""Tests whether the :see:LocalizedValue
class's translate() method properly returns
None when there is no value."""
# with no value, we always expect it to return None
localized_value = LocalizedValue()
assert localized_value.translate() == None
assert str(localized_value) == ''
# with no value for the default language, the default
# behavior is to return None, unless a custom fallback
# chain is configured, which there is not for this test
other_language = settings.LANGUAGES[-1][0]
localized_value = LocalizedValue({
other_language: 'hey'
})
translation.activate(settings.LANGUAGE_CODE)
assert localized_value.translate() == None
assert str(localized_value) == ''
@staticmethod
def test_translate_fallback_custom_fallback():
"""Tests whether the :see:LocalizedValue class's
__str__'s fallback functionality properly respects
translate()'s fallback functionality properly respects
the LOCALIZED_FIELDS_FALLBACKS setting."""
test_value = 'myvalue'
settings.LOCALIZED_FIELDS_FALLBACKS = {
fallbacks = {
'nl': ['ro']
}
@@ -160,8 +182,9 @@ class LocalizedValueTestCase(TestCase):
'ro': 'ro'
})
with translation.override('nl'):
assert str(localized_value) == 'ro'
with override_settings(LOCALIZED_FIELDS_FALLBACKS=fallbacks):
with translation.override('nl'):
assert localized_value.translate() == 'ro'
@staticmethod
def test_deconstruct():
@@ -182,3 +205,11 @@ class LocalizedValueTestCase(TestCase):
value = LocalizedValue('beer')
assert value.get(settings.LANGUAGE_CODE) == 'beer'
@staticmethod
def test_construct_expression():
"""Tests whether passing expressions as values
works properly and are not converted to string."""
value = LocalizedValue(dict(en=F('other')))
assert isinstance(value.en, F)

View File

@@ -1,3 +1,4 @@
import re
from django.conf import settings
from django.test import TestCase
@@ -15,6 +16,7 @@ class LocalizedFieldWidgetTestCase(TestCase):
widget = LocalizedFieldWidget()
assert len(widget.widgets) == len(settings.LANGUAGES)
assert len(set(widget.widgets)) == len(widget.widgets)
@staticmethod
def test_decompress():
@@ -42,3 +44,40 @@ class LocalizedFieldWidgetTestCase(TestCase):
for _, value in zip(settings.LANGUAGES, decompressed_values):
assert not value
@staticmethod
def test_get_context_required():
"""Tests whether the :see:get_context correctly
handles 'required' attribute, separately for each subwidget."""
widget = LocalizedFieldWidget()
widget.widgets[0].is_required = True
widget.widgets[1].is_required = False
context = widget.get_context(name='test', value=LocalizedValue(),
attrs=dict(required=True))
assert context['widget']['subwidgets'][0]['attrs']['required']
assert 'required' not in context['widget']['subwidgets'][1]['attrs']
@staticmethod
def test_get_context_langs():
"""Tests whether the :see:get_context contains 'lang_code' and
'lang_name' attribute for each subwidget."""
widget = LocalizedFieldWidget()
context = widget.get_context(name='test', value=LocalizedValue(),
attrs=dict())
subwidgets_context = context['widget']['subwidgets']
for widget, context in zip(widget.widgets, subwidgets_context):
assert 'lang_code' in context
assert 'lang_name' in context
assert widget.lang_code == context['lang_code']
assert widget.lang_name == context['lang_name']
@staticmethod
def test_render():
"""Tests whether the :see:LocalizedFieldWidget correctly
render."""
widget = LocalizedFieldWidget()
output = widget.render(name='title', value=None)
assert bool(re.search('<label (.|\n|\t)*>\w+<\/label>', output))

12
tox.ini Normal file
View File

@@ -0,0 +1,12 @@
[tox]
envlist = py35-dj{111,20}
[testenv]
deps =
dj111: Django>=1.11,<1.12
dj20: Django>=2.0,<2.1
-rrequirements/test.txt
setenv =
DJANGO_SETTINGS_MODULE=settings
passenv = DATABASE_URL
commands = coverage run manage.py test