Merge branch 'master' into widget_refactor

This commit is contained in:
Swen Kooij 2017-07-18 09:33:59 +03:00 committed by GitHub
commit 1accee0b59
13 changed files with 214 additions and 72 deletions

View File

@ -13,17 +13,20 @@ 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: pep8 ./localized_fields/
-
command: flake8 ./localized_fields/
-
command: coverage run manage.py test
coverage:

View File

@ -12,9 +12,9 @@ django-localized-fields
.. 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.
Installation
------------
@ -129,21 +129,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 is 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**
@ -335,7 +356,7 @@ Frequently asked questions (FAQ)
2. Does this package work with Django 1.X?
No. Only Django 1.10 or newer is supported. This is because we rely on Django's ``HStoreField``.
No. Only Django 1.11 or newer is supported. 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

@ -1,6 +1,6 @@
import json
from typing import Union
from typing import Union, List
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.
@ -170,9 +177,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 +189,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 +203,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

@ -146,8 +146,6 @@ 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)

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

@ -18,7 +18,8 @@ setup(
author_email='open-source@sectorlabs.ro',
keywords=['django', 'localized', 'language', 'models', 'fields'],
install_requires=[
'django-postgres-extra>=1.11'
'django-postgres-extra>=1.11',
'Django>=1.11'
],
classifiers=[
'Environment :: Web Environment',

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

@ -72,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()
@ -87,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):
@ -135,17 +135,17 @@ class LocalizedFileFieldTestCase(TestCase):
@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()
@ -161,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

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

View File

@ -124,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)
})
@ -138,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

View File

@ -149,8 +149,6 @@ class LocalizedValueTestCase(TestCase):
__str__'s fallback functionality properly respects
the LOCALIZED_FIELDS_FALLBACKS setting."""
test_value = 'myvalue'
settings.LOCALIZED_FIELDS_FALLBACKS = {
'nl': ['ro']
}

View File

@ -1,3 +1,4 @@
import re
from django.conf import settings
from django.test import TestCase
@ -55,3 +56,12 @@ class LocalizedFieldWidgetTestCase(TestCase):
attrs=dict(required=True))
assert context['widget']['subwidgets'][0]['attrs']['required']
assert 'required' not in context['widget']['subwidgets'][1]['attrs']
@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))