36 Commits

Author SHA1 Message Date
Swen Kooij
a21ff53cb9 Bump version number to 5.4.2 2020-03-10 13:16:03 +02:00
Alexandru Arnăutu
e4bd26ece2 Add support for storing Float values (#80)
* Add LocalizedFloatValue

* Add LocalizedFloatField

* Add tests for float field

* Create LocalizedNumericValue with __int__ and __float__ methods
2020-03-10 09:21:11 +02:00
Swen Kooij
a198440a64 Remove annoying quotes around 'master' in README 2019-10-20 18:20:27 +03:00
Swen Kooij
5945c3f531 Update pip install command for 5.4.1 2019-10-20 18:19:07 +03:00
Swen Kooij
3fd862ce4d Bump version number to 5.4.1 2019-10-20 18:15:05 +03:00
Swen Kooij
dad7f164cd Pin dependency down to django-postgres-extra <2.0 2019-10-20 18:14:12 +03:00
Swen Kooij
6490f3908c Add warning about v6 2019-10-20 18:00:29 +03:00
Uma Zalakain
54ad6eb434 The deprecation package removes its unnecessary unittest2 dependency 2019-09-13 21:03:31 +02:00
Swen Kooij
893fe0f5ab Bump version to 5.4 2019-08-14 08:58:08 +03:00
Swen Kooij
3de1492a58 Use collections.abc.Iterable instead of collections.Iterable
The latter is going to be removed after Python 3.8
2019-08-14 08:57:05 +03:00
Swen Kooij
946e9a67c4 Bump version to 5.3 2019-06-28 08:31:31 +03:00
Swen Kooij
36f6e946b0 Attempt at reducing deprecation warning spam 2019-06-27 15:15:44 +03:00
Swen Kooij
909ebfee69 Bump version number to 5.2 2019-06-27 14:53:58 +03:00
Swen Kooij
95284e6fd0 Add extra step to set up instructions about applying migrations 2019-06-27 13:42:42 +03:00
Swen Kooij
e84a5e4ff1 Bump version to 5.1 2019-06-27 13:37:28 +03:00
Swen Kooij
472c7bbc41 Remove dead travis-ci configuration 2019-06-27 13:36:23 +03:00
Swen Kooij
8cc50889ec Set version back to 5.0a11, it was never released 2019-06-27 13:30:19 +03:00
Swen Kooij
8494615d05 Upgrade django-postgres-extra to 1.22 2019-06-27 13:28:29 +03:00
Swen Kooij
f0541c047b Bump version number to 5.0a12 2019-06-27 13:27:59 +03:00
Swen Kooij
84658f6010 Bump version number to 5.0a10 2019-06-05 23:26:47 +03:00
Swen Kooij
0e29871458 Merge pull request #62 from GabLeRoux/patch-1
Remove extra dot in example integration code
2019-06-05 23:12:43 +03:00
Swen Kooij
ffe235d3ac Merge pull request #66 from martinsvoboda/admin-widget-image
Added the image of admin widget
2019-06-05 23:12:25 +03:00
Swen Kooij
d7db2c58c0 Merge pull request #68 from MELScience/fix-django-22
update to support Django 2.2
2019-06-05 23:11:38 +03:00
Dmitry Groshev
6a7545a910 update to support Django 2.2 2019-05-30 13:11:33 +01:00
Martin Svoboda
11bf4ee88a Added the image of admin widget 2019-05-26 11:12:45 +02:00
Gabriel Le Breton
d3223eca53 Remove extra dot in example integration code 2019-04-23 11:12:29 -04:00
Swen Kooij
f0ac0f7f25 Merge branch 'bump-version' 2019-02-21 12:54:04 +02:00
Swen Kooij
93ffce557c Bump version number to 5.0a9 2019-02-21 12:50:58 +02:00
Swen Kooij
7c432baec7 Upgrade django-postgres-extra to 1.21a16 2019-02-21 12:50:46 +02:00
Swen Kooij
476a20ba88 Merge pull request #61 from SectorLabs/bump-version
Bump version number to 5.0a8
2019-02-21 12:47:15 +02:00
Swen Kooij
ad99b77bcd Bump version number to 5.0a8 2019-02-21 12:34:22 +02:00
Swen Kooij
151250505d Merge pull request #60 from AdrianMuntean/error_on_empty_localized_integer_field
Return empty string in case the LocalizedIntegerField is null
2019-02-21 12:31:50 +02:00
Adrian Muntean
d8b872758c Return empty string in case of None 2019-02-20 12:26:04 +02:00
Adrian Muntean
a0ca977cab Set None in case the LocalizedIntegerField is null
In case the LocalizedIntegerField is null in the DB then it must explicitly be set to None,
otherwise it will yield TypeError: __str__ returned non-string
2019-02-14 14:48:38 +02:00
Swen Kooij
eb2cb6b244 Bump version number to 5.0a7 2019-01-14 11:00:20 +02:00
Swen Kooij
ed15fb0079 Upgrade django-postgres-extra>=1.21a15 2019-01-14 11:00:12 +02:00
13 changed files with 369 additions and 49 deletions

View File

@@ -1,18 +0,0 @@
env:
- DJANGO_SETTINGS_MODULE=settings
sudo: true
before_install:
- sudo apt-get update -qq
- sudo apt-get install -qq build-essential gettext python-dev zlib1g-dev libpq-dev xvfb
- sudo apt-get install -qq libtiff4-dev libjpeg8-dev libfreetype6-dev liblcms1-dev libwebp-dev
- sudo apt-get install -qq graphviz-dev python-setuptools python3-dev python-virtualenv python-pip
- sudo apt-get install -qq firefox automake libtool libreadline6 libreadline6-dev libreadline-dev
- sudo apt-get install -qq libsqlite3-dev libxml2 libxml2-dev libssl-dev libbz2-dev wget curl llvm
language: python
python:
- "3.5"
services:
- postgresql
install: "pip install -r requirements/test.txt"
script:
- coverage run manage.py test

View File

@@ -1,8 +1,8 @@
django-localized-fields
=======================
.. image:: https://circleci.com/gh/SectorLabs/django-localized-fields.svg?style=svg
:target: https://circleci.com/gh/SectorLabs/django-localized-fields
.. image:: https://circleci.com/gh/SectorLabs/django-localized-fields/tree/v5.x.svg?style=svg
:target: https://circleci.com/gh/SectorLabs/django-localized-fields/tree/v5.x
.. image:: https://img.shields.io/github/license/SectorLabs/django-localized-fields.svg
:target: https://github.com/SectorLabs/django-localized-fields/blob/master/LICENSE.md
@@ -14,11 +14,17 @@ django-localized-fields
This package requires Python 3.5 or newer, Django 1.11 or newer and PostgreSQL 9.6 or newer.
----
**This README is for v5.x. Check out the master branch for v6.x and newer.**
----
Contributors
------------
* `seroy <https://github.com/seroy/>`_
* `unaizalakain <https://github.com/unaizalakain/>`_
* `umazalakain <https://github.com/umazalakain/>`_
Installation
------------
@@ -26,14 +32,14 @@ Installation
.. code-block:: bash
$ pip install django-localized-fields
$ pip install django-localized-fields==5.4.1
2. Add ``localized_fields`` and ``django.contrib.postgres`` to your ``INSTALLED_APPS``:
.. code-block:: bash
INSTALLED_APPS = [
....
...
'django.contrib.postgres',
'localized_fields.apps.LocalizedFieldsConfig'
@@ -61,6 +67,13 @@ Installation
('ro', 'Romanian')
)
4. Apply migrations to enable the HStore extension:
.. code-block:: bash
python manage.py migrate
Usage
-----
@@ -279,7 +292,7 @@ Besides ``LocalizedField``, there's also:
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``
the backing field type is ``hstore``, which only allows storing strings. 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:
@@ -369,6 +382,10 @@ To enable widgets in the admin, you need to inherit from ``LocalizedFieldsAdminM
admin.site.register(MyLocalizedModel, MyLocalizedModelAdmin)
.. image:: ./images/admin-widget.png
:alt: The appearance of admin widget
Frequently asked questions (FAQ)
--------------------------------

BIN
images/admin-widget.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View File

@@ -22,6 +22,7 @@ class LocalizedFieldsAdminMixin:
}
js = (
'admin/js/jquery.init.js',
'localized_fields/localized-fields-admin.js',
)

View File

@@ -5,6 +5,7 @@ from .char_field import LocalizedCharField
from .text_field import LocalizedTextField
from .file_field import LocalizedFileField
from .integer_field import LocalizedIntegerField
from .float_field import LocalizedFloatField
__all__ = [
@@ -14,7 +15,8 @@ __all__ = [
'LocalizedCharField',
'LocalizedTextField',
'LocalizedFileField',
'LocalizedIntegerField'
'LocalizedIntegerField',
'LocalizedFloatField'
]
try:

View File

@@ -17,17 +17,17 @@ class LocalizedAutoSlugField(LocalizedField):
"""Automatically provides slugs for a localized
field upon saving."""
warnings.warn(
'LocalizedAutoSlug is deprecated and will be removed in the next major version.',
DeprecationWarning
)
def __init__(self, *args, **kwargs):
"""Initializes a new instance of :see:LocalizedAutoSlugField."""
self.populate_from = kwargs.pop('populate_from', None)
self.include_time = kwargs.pop('include_time', False)
warnings.warn(
'LocalizedAutoSlug is deprecated and will be removed in the next major version.',
DeprecationWarning
)
super(LocalizedAutoSlugField, self).__init__(
*args,
**kwargs

View File

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

View File

@@ -1,7 +1,8 @@
import deprecation
import collections
from typing import Optional
from collections.abc import Iterable
from django.conf import settings
from django.utils import translation
@@ -97,7 +98,7 @@ class LocalizedValue(dict):
lang_value = value.get(lang_code, self.default_value)
self.set(lang_code, lang_value)
elif isinstance(value, collections.Iterable):
elif isinstance(value, Iterable):
for val in value:
self._interpret_value(val)
@@ -205,7 +206,31 @@ class LocalizedFileValue(LocalizedValue):
return self.get(translation.get_language())
class LocalizedIntegerValue(LocalizedValue):
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
return int(value)
def __str__(self) -> str:
"""Returns string representation of value"""
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
return float(value)
class LocalizedIntegerValue(LocalizedNumericValue):
"""All values are integers."""
default_value = None
@@ -220,17 +245,19 @@ class LocalizedIntegerValue(LocalizedValue):
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
class LocalizedFloatValue(LocalizedNumericValue):
"""All values are floats"""
return int(value)
default_value = None
def __str__(self) -> str:
"""Returns string representation of value"""
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
value = self.translate()
return str(value) if value is not None else None
return float(value)

View File

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

View File

@@ -24,10 +24,33 @@ INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.admin',
'django.contrib.messages',
'localized_fields',
'tests',
)
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
MIDDLEWARE = [
'django.contrib.messages.middleware.MessageMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
]
# set to a lower number than the default, since
# we want the tests to be fast, default is 100
LOCALIZED_FIELDS_MAX_RETRIES = 3

View File

@@ -33,7 +33,7 @@ with open(os.path.join(os.path.dirname(__file__), 'README.rst'), encoding='utf-8
setup(
name='django-localized-fields',
version='5.0a6',
version='5.4.2',
packages=find_packages(exclude=['tests']),
include_package_data=True,
license='MIT License',
@@ -44,9 +44,9 @@ setup(
author_email='open-source@sectorlabs.ro',
keywords=['django', 'localized', 'language', 'models', 'fields'],
install_requires=[
'django-postgres-extra>=1.21a14',
'django-postgres-extra>=1.22,<2.0',
'Django>=1.11',
'deprecation==2.0.3'
'deprecation==2.0.7'
],
classifiers=[
'Environment :: Web Environment',

176
tests/test_float_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 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

View File

@@ -1,11 +1,12 @@
[tox]
envlist = py35-dj{111,20,21}, py36-dj{111,20,21}, py37-dj{111,20,21}
envlist = py35-dj{111,20,21,22}, py36-dj{111,20,21,22}, py37-dj{111,20,21,22}
[testenv]
deps =
dj111: Django>=1.11,<1.12
dj20: Django>=2.0,<2.1
dj21: Django>=2.1,<2.2
dj22: Django>=2.2,<2.3
-rrequirements/test.txt
setenv =
DJANGO_SETTINGS_MODULE=settings