Add a LocalizedIntegerField

This commit is contained in:
Swen Kooij 2018-06-15 12:58:01 +03:00
parent 752e17064d
commit 90597da8fd
5 changed files with 256 additions and 4 deletions

View File

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

View File

@ -1,6 +1,6 @@
import json import json
from typing import Union, List from typing import Union, List, Optional
from django.conf import settings from django.conf import settings
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
@ -53,7 +53,7 @@ class LocalizedField(HStoreField):
setattr(model, self.name, self.descriptor_class(self)) setattr(model, self.name, self.descriptor_class(self))
@classmethod @classmethod
def from_db_value(cls, value, *_): def from_db_value(cls, value, *_) -> Optional[LocalizedValue]:
"""Turns the specified database value into its Python """Turns the specified database value into its Python
equivalent. equivalent.

View File

@ -0,0 +1,68 @@
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: LocalizedValue) -> dict:
"""Gets the value in a format to store into the database."""
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:
try:
if prepped_value[lang_code] is not None:
int(prepped_value[lang_code])
except (TypeError, ValueError):
raise IntegrityError('non-integer value in column "%s.%s" violates '
'integer constraint' % (self.name, lang_code))
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

@ -38,7 +38,8 @@ class LocalizedValue(dict):
""" """
language = language or settings.LANGUAGE_CODE 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): def set(self, language: str, value: str):
"""Sets the value in the specified language. """Sets the value in the specified language.
@ -192,6 +193,7 @@ class LocalizedFileValue(LocalizedValue):
def __str__(self) -> str: def __str__(self) -> str:
"""Returns string representation of value""" """Returns string representation of value"""
return str(super().__str__()) return str(super().__str__())
@deprecation.deprecated(deprecated_in='4.6', removed_in='5.0', @deprecation.deprecated(deprecated_in='4.6', removed_in='5.0',
@ -199,4 +201,30 @@ class LocalizedFileValue(LocalizedValue):
details='Use the translate() function instead.') details='Use the translate() function instead.')
def localized(self): def localized(self):
"""Returns value for current language""" """Returns value for current language"""
return self.get(translation.get_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)

154
tests/test_integer_field.py Normal file
View File

@ -0,0 +1,154 @@
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