From 0d9ec6385ca305d7d59bb0bc03b0fe2ddadcefa9 Mon Sep 17 00:00:00 2001 From: Alexandru Arnautu Date: Mon, 9 Mar 2020 12:12:18 +0200 Subject: [PATCH 1/5] Add LocalizedFloatValue --- localized_fields/value.py | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/localized_fields/value.py b/localized_fields/value.py index ef2685d..19014db 100644 --- a/localized_fields/value.py +++ b/localized_fields/value.py @@ -257,4 +257,39 @@ class LocalizedIntegerValue(LocalizedValue): """Returns string representation of value.""" value = self.translate() - return str(value) if value is not None else "" + return str(value) if value is not None else '' + + +class LocalizedFloatValue(LocalizedValue): + """All values are floats""" + + default_value = None + + def translate(self): + """ + Gets the value in the current language, or in the configured + fallback language. + """ + + value = super().translate() + if value is None or (isinstance(value, str) and value.strip() == ''): + return None + + return float(value) + + def __float__(self): + """ + Gets the value in the current language as a float + """ + value = self.translate() + if value is None: + return self.default_value + + return float(value) + + def __str__(self) -> str: + """ + Returns the string repsentation of value + """ + value = self.translate + return str(value) if value is not None else '' From 5ed1a1219deae1f57eaa196e32a319c69d1e584a Mon Sep 17 00:00:00 2001 From: Alexandru Arnautu Date: Mon, 9 Mar 2020 12:12:45 +0200 Subject: [PATCH 2/5] Add LocalizedFloatField --- localized_fields/fields/__init__.py | 2 + localized_fields/fields/float_field.py | 91 ++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 localized_fields/fields/float_field.py diff --git a/localized_fields/fields/__init__.py b/localized_fields/fields/__init__.py index 6bd3580..390a43c 100644 --- a/localized_fields/fields/__init__.py +++ b/localized_fields/fields/__init__.py @@ -5,6 +5,7 @@ from .file_field import LocalizedFileField from .integer_field import LocalizedIntegerField from .text_field import LocalizedTextField from .uniqueslug_field import LocalizedUniqueSlugField +from .float_field import LocalizedFloatField __all__ = [ "LocalizedField", @@ -14,6 +15,7 @@ __all__ = [ "LocalizedTextField", "LocalizedFileField", "LocalizedIntegerField", + "LocalizedFloatField" ] try: diff --git a/localized_fields/fields/float_field.py b/localized_fields/fields/float_field.py new file mode 100644 index 0000000..87d2d91 --- /dev/null +++ b/localized_fields/fields/float_field.py @@ -0,0 +1,91 @@ +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, LocalizedFloatValue +from ..forms import LocalizedIntegerFieldForm + + +class LocalizedFloatField(LocalizedField): + """Stores float as a localized value.""" + + attr_class = LocalizedFloatValue + + @classmethod + def from_db_value(cls, value, *_) -> Optional[LocalizedFloatValue]: + 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:LocalizedFloatValue + 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]) -> LocalizedFloatValue: + """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: LocalizedFloatValue) -> dict: + """Gets the value in a format to store into the database.""" + + # apply default values + default_values = LocalizedFloatValue(self.default) + if isinstance(value, LocalizedFloatValue): + 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 floats + for lang_code, _ in settings.LANGUAGES: + local_value = prepped_value[lang_code] + try: + if local_value is not None: + float(local_value) + except (TypeError, ValueError): + raise IntegrityError('non-float value in column "%s.%s" violates ' + 'float 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 + + def formfield(self, **kwargs): + """Gets the form field associated with this field.""" + defaults = { + 'form_class': LocalizedIntegerFieldForm + } + + defaults.update(kwargs) + return super().formfield(**defaults) + + @staticmethod + def _convert_localized_value(value: LocalizedValue) -> LocalizedFloatValue: + """Converts from :see:LocalizedValue to :see:LocalizedFloatValue.""" + + float_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: + float_values[lang_code] = float(local_value) + except (ValueError, TypeError): + float_values[lang_code] = None + + return LocalizedFloatValue(float_values) From 0f1d6636f6a78799b1834f3a0606cf5cdeac8e86 Mon Sep 17 00:00:00 2001 From: Alexandru Arnautu Date: Mon, 9 Mar 2020 12:21:40 +0200 Subject: [PATCH 3/5] Add tests for float field --- tests/test_float_field.py | 176 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 tests/test_float_field.py diff --git a/tests/test_float_field.py b/tests/test_float_field.py new file mode 100644 index 0000000..60a620c --- /dev/null +++ b/tests/test_float_field.py @@ -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 LocalizedFloatField + +from .fake_model import get_fake_model + + +class LocalizedFloatFieldTestCase(TestCase): + """Tests whether the :see:LocalizedFloatField + and :see:LocalizedFloatValue works properly.""" + + TestModel = None + + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.TestModel = get_fake_model({ + 'score': LocalizedFloatField() + }) + + def test_basic(self): + """Tests the basics of storing float values.""" + + obj = self.TestModel() + for index, (lang_code, _) in enumerate(settings.LANGUAGES): + obj.score.set(lang_code, index + 1.0) + obj.save() + + obj = self.TestModel.objects.all().first() + for index, (lang_code, _) in enumerate(settings.LANGUAGES): + assert obj.score.get(lang_code) == index + 1.0 + + 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.0) + 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.0) + 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 float + results in the value being returned in the currently + active language as an float.""" + + obj = self.TestModel() + for index, (lang_code, _) in enumerate(settings.LANGUAGES): + obj.score.set(lang_code, index + 1.0) + obj.save() + + obj.refresh_from_db() + for index, (lang_code, _) in enumerate(settings.LANGUAGES): + with translation.override(lang_code): + assert float(obj.score) == index + 1.0 + assert obj.score.translate() == index + 1.0 + + def test_translate_primary_fallback(self): + """Tests whether casting the value to an float + 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.0) + + secondary_language = settings.LANGUAGES[-1][0] + assert obj.score.get(secondary_language) is None + + with translation.override(secondary_language): + assert obj.score.translate() == 25.0 + assert float(obj.score) == 25.0 + + 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.0) + + secondary_language = settings.LANGUAGES[-1][0] + assert obj.score.get(secondary_language) is None + assert obj.score.get(secondary_language, 1337.0) == 1337.0 + + def test_completely_optional(self): + """Tests whether having all languages optional + works properly.""" + + model = get_fake_model({ + 'score': LocalizedFloatField(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-float.""" + + 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 + float.""" + + obj = self.TestModel() + obj.score.set(settings.LANGUAGE_CODE, 25.0) + 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': LocalizedFloatField(default={settings.LANGUAGE_CODE: 75.0}) + }) + + obj = model.objects.create() + assert obj.score.get(settings.LANGUAGE_CODE) == 75.0 + + 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.0 + else: + assert obj.score.get(lang_code) is None From 5e1d46669ca1b5a88f2ade36bf14e587ee4672db Mon Sep 17 00:00:00 2001 From: Alexandru Arnautu Date: Mon, 9 Mar 2020 15:36:51 +0200 Subject: [PATCH 4/5] Create LocalizedNumericValue with __int__ and __float__ methods --- localized_fields/value.py | 61 +++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 35 deletions(-) diff --git a/localized_fields/value.py b/localized_fields/value.py index 19014db..b06dbd0 100644 --- a/localized_fields/value.py +++ b/localized_fields/value.py @@ -229,24 +229,9 @@ class LocalizedFileValue(LocalizedValue): 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) - +class LocalizedNumericValue(LocalizedValue): def __int__(self): """Gets the value in the current language as an integer.""" - value = self.translate() if value is None: return self.default_value @@ -259,8 +244,32 @@ class LocalizedIntegerValue(LocalizedValue): value = self.translate() return str(value) if value is not None else '' + def __float__(self): + """Gets the value in the current language as a float""" + value = self.translate() + if value is None: + return self.default_value -class LocalizedFloatValue(LocalizedValue): + return float(value) + + +class LocalizedIntegerValue(LocalizedNumericValue): + """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) + + +class LocalizedFloatValue(LocalizedNumericValue): """All values are floats""" default_value = None @@ -270,26 +279,8 @@ class LocalizedFloatValue(LocalizedValue): Gets the value in the current language, or in the configured fallback language. """ - value = super().translate() if value is None or (isinstance(value, str) and value.strip() == ''): return None return float(value) - - def __float__(self): - """ - Gets the value in the current language as a float - """ - value = self.translate() - if value is None: - return self.default_value - - return float(value) - - def __str__(self) -> str: - """ - Returns the string repsentation of value - """ - value = self.translate - return str(value) if value is not None else '' From 8968b0c7a89908cfb20309485525483eea80b2fd Mon Sep 17 00:00:00 2001 From: Alexandru Arnautu Date: Tue, 10 Mar 2020 09:33:34 +0200 Subject: [PATCH 5/5] Format and lint code --- localized_fields/fields/__init__.py | 4 +- localized_fields/fields/float_field.py | 26 ++++++---- localized_fields/value.py | 20 ++++---- tests/test_float_field.py | 70 ++++++++++++-------------- 4 files changed, 59 insertions(+), 61 deletions(-) diff --git a/localized_fields/fields/__init__.py b/localized_fields/fields/__init__.py index 390a43c..e6066df 100644 --- a/localized_fields/fields/__init__.py +++ b/localized_fields/fields/__init__.py @@ -2,10 +2,10 @@ from .autoslug_field import LocalizedAutoSlugField from .char_field import LocalizedCharField from .field import LocalizedField from .file_field import LocalizedFileField +from .float_field import LocalizedFloatField from .integer_field import LocalizedIntegerField from .text_field import LocalizedTextField from .uniqueslug_field import LocalizedUniqueSlugField -from .float_field import LocalizedFloatField __all__ = [ "LocalizedField", @@ -15,7 +15,7 @@ __all__ = [ "LocalizedTextField", "LocalizedFileField", "LocalizedIntegerField", - "LocalizedFloatField" + "LocalizedFloatField", ] try: diff --git a/localized_fields/fields/float_field.py b/localized_fields/fields/float_field.py index 87d2d91..721f7aa 100644 --- a/localized_fields/fields/float_field.py +++ b/localized_fields/fields/float_field.py @@ -1,11 +1,11 @@ -from typing import Optional, Union, Dict +from typing import Dict, Optional, Union from django.conf import settings from django.db.utils import IntegrityError -from .field import LocalizedField -from ..value import LocalizedValue, LocalizedFloatValue from ..forms import LocalizedIntegerFieldForm +from ..value import LocalizedFloatValue, LocalizedValue +from .field import LocalizedField class LocalizedFloatField(LocalizedField): @@ -27,7 +27,9 @@ class LocalizedFloatField(LocalizedField): return cls._convert_localized_value(db_value) - def to_python(self, value: Union[Dict[str, int], int, None]) -> LocalizedFloatValue: + def to_python( + self, value: Union[Dict[str, int], int, None] + ) -> LocalizedFloatValue: """Converts the value from a database value into a Python value.""" db_value = super().to_python(value) @@ -55,20 +57,22 @@ class LocalizedFloatField(LocalizedField): if local_value is not None: float(local_value) except (TypeError, ValueError): - raise IntegrityError('non-float value in column "%s.%s" violates ' - 'float constraint' % (self.name, lang_code)) + raise IntegrityError( + 'non-float value in column "%s.%s" violates ' + "float 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 + prepped_value[lang_code] = ( + str(local_value) if local_value is not None else None + ) return prepped_value def formfield(self, **kwargs): """Gets the form field associated with this field.""" - defaults = { - 'form_class': LocalizedIntegerFieldForm - } + defaults = {"form_class": LocalizedIntegerFieldForm} defaults.update(kwargs) return super().formfield(**defaults) @@ -80,7 +84,7 @@ class LocalizedFloatField(LocalizedField): float_values = {} for lang_code, _ in settings.LANGUAGES: local_value = value.get(lang_code, None) - if local_value is None or local_value.strip() == '': + if local_value is None or local_value.strip() == "": local_value = None try: diff --git a/localized_fields/value.py b/localized_fields/value.py index b06dbd0..c318454 100644 --- a/localized_fields/value.py +++ b/localized_fields/value.py @@ -242,10 +242,10 @@ class LocalizedNumericValue(LocalizedValue): """Returns string representation of value.""" value = self.translate() - return str(value) if value is not None else '' + return str(value) if value is not None else "" def __float__(self): - """Gets the value in the current language as a float""" + """Gets the value in the current language as a float.""" value = self.translate() if value is None: return self.default_value @@ -259,28 +259,26 @@ class LocalizedIntegerValue(LocalizedNumericValue): default_value = None def translate(self): - """Gets the value in the current language, or - in the configured fallbck language.""" + """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() == ''): + if value is None or (isinstance(value, str) and value.strip() == ""): return None return int(value) class LocalizedFloatValue(LocalizedNumericValue): - """All values are floats""" + """All values are floats.""" default_value = None def translate(self): - """ - Gets the value in the current language, or in the configured - fallback language. - """ + """Gets the value in the current language, or in the configured + fallback language.""" value = super().translate() - if value is None or (isinstance(value, str) and value.strip() == ''): + if value is None or (isinstance(value, str) and value.strip() == ""): return None return float(value) diff --git a/tests/test_float_field.py b/tests/test_float_field.py index 60a620c..9dd8110 100644 --- a/tests/test_float_field.py +++ b/tests/test_float_field.py @@ -1,7 +1,7 @@ -from django.test import TestCase -from django.db.utils import IntegrityError from django.conf import settings from django.db import connection +from django.db.utils import IntegrityError +from django.test import TestCase from django.utils import translation from localized_fields.fields import LocalizedFloatField @@ -10,8 +10,8 @@ from .fake_model import get_fake_model class LocalizedFloatFieldTestCase(TestCase): - """Tests whether the :see:LocalizedFloatField - and :see:LocalizedFloatValue works properly.""" + """Tests whether the :see:LocalizedFloatField and :see:LocalizedFloatValue + works properly.""" TestModel = None @@ -19,9 +19,7 @@ class LocalizedFloatFieldTestCase(TestCase): def setUpClass(cls): super().setUpClass() - cls.TestModel = get_fake_model({ - 'score': LocalizedFloatField() - }) + cls.TestModel = get_fake_model({"score": LocalizedFloatField()}) def test_basic(self): """Tests the basics of storing float values.""" @@ -36,8 +34,8 @@ class LocalizedFloatFieldTestCase(TestCase): assert obj.score.get(lang_code) == index + 1.0 def test_primary_language_required(self): - """Tests whether the primary language is required by - default and all other languages are optiona.""" + """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 @@ -56,8 +54,8 @@ class LocalizedFloatFieldTestCase(TestCase): obj.save() def test_default_value_none(self): - """Tests whether the default value for optional languages - is NoneType.""" + """Tests whether the default value for optional languages is + NoneType.""" obj = self.TestModel() obj.score.set(settings.LANGUAGE_CODE, 1234.0) @@ -70,9 +68,8 @@ class LocalizedFloatFieldTestCase(TestCase): assert obj.score.get(lang_code) is None def test_translate(self): - """Tests whether casting the value to an float - results in the value being returned in the currently - active language as an float.""" + """Tests whether casting the value to an float results in the value + being returned in the currently active language as an float.""" obj = self.TestModel() for index, (lang_code, _) in enumerate(settings.LANGUAGES): @@ -86,10 +83,9 @@ class LocalizedFloatFieldTestCase(TestCase): assert obj.score.translate() == index + 1.0 def test_translate_primary_fallback(self): - """Tests whether casting the value to an float - 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.""" + """Tests whether casting the value to an float 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.0) @@ -102,9 +98,8 @@ class LocalizedFloatFieldTestCase(TestCase): assert float(obj.score) == 25.0 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.""" + """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.0) @@ -114,12 +109,11 @@ class LocalizedFloatFieldTestCase(TestCase): assert obj.score.get(secondary_language, 1337.0) == 1337.0 def test_completely_optional(self): - """Tests whether having all languages optional - works properly.""" + """Tests whether having all languages optional works properly.""" - model = get_fake_model({ - 'score': LocalizedFloatField(null=True, required=[], blank=True) - }) + model = get_fake_model( + {"score": LocalizedFloatField(null=True, required=[], blank=True)} + ) obj = model() obj.save() @@ -128,19 +122,18 @@ class LocalizedFloatFieldTestCase(TestCase): 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-float.""" + """Tests whether the field properly raises an error when trying to + store a non-float.""" for lang_code, _ in settings.LANGUAGES: obj = self.TestModel() with self.assertRaises(IntegrityError): - obj.score.set(lang_code, 'haha') + 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 - float.""" + """Tests whether None is returned for a language if the value stored in + the database is not an float.""" obj = self.TestModel() obj.score.set(settings.LANGUAGE_CODE, 25.0) @@ -154,12 +147,15 @@ class LocalizedFloatFieldTestCase(TestCase): assert obj.score.get(settings.LANGUAGE_CODE) is None def test_default_value(self): - """Tests whether a default is properly set - when specified.""" + """Tests whether a default is properly set when specified.""" - model = get_fake_model({ - 'score': LocalizedFloatField(default={settings.LANGUAGE_CODE: 75.0}) - }) + model = get_fake_model( + { + "score": LocalizedFloatField( + default={settings.LANGUAGE_CODE: 75.0} + ) + } + ) obj = model.objects.create() assert obj.score.get(settings.LANGUAGE_CODE) == 75.0