29 Commits
4.2 ... v4.4

Author SHA1 Message Date
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
Swen Kooij
3ce57ed4cc Bump version number to 4.3 2017-06-26 13:37:02 +03:00
Swen Kooij
7316d312b4 Add simple test for LOCALIZED_FIELDS_FALLBACKS setting 2017-06-26 13:36:21 +03:00
Swen Kooij
16e23963cc Add support for LOCALIZED_FIELDS_FALLBACKS 2017-06-26 13:27:52 +03:00
Swen Kooij
b10472d3e9 Officially deprecate LocalizedAutoSlugField 2017-06-26 13:10:21 +03:00
Swen Kooij
833ceb849c Update docs on enhanced LocalizedUniqueSlugField 2017-06-26 13:07:18 +03:00
Swen Kooij
d7382fbf30 Add support for using a callable to populate slug with 2017-06-26 13:03:41 +03:00
Swen Kooij
8ad9268426 Remove tests for LocalizedAutoSlug 2017-06-26 12:52:42 +03:00
Swen Kooij
96ddc77cfc Fake models now have generated names 2017-06-26 12:44:09 +03:00
Swen Kooij
51fc6959d2 Support for slugging from multiple fields 2017-06-26 12:34:50 +03:00
Swen Kooij
3b28a5e707 Fix PEP8 violations 2017-06-26 11:33:25 +03:00
Swen Kooij
06873afbda Merge pull request #15 from MELScience/extra-fields
Extra fields
2017-06-21 11:33:30 +03:00
seroy
e5d7cd25e2 Shorten names for everything 2017-06-19 21:58:48 +03:00
seroy
236ce1648c Upgrade django-postgres-extra to 1.11 2017-06-19 21:49:24 +03:00
seroy
aacc712195 Merge branch 'master' of https://github.com/SectorLabs/django-localized-fields into extra-fields 2017-06-19 21:40:11 +03:00
seroy
d1790f1fc1 added missing 'r' in type LocalizedStringValue 2017-06-19 17:17:24 +03:00
seroy
db93b93046 added test for LocalizedFileField and LocalizedFileFieldForm 2017-04-24 20:30:44 +03:00
seroy
a352388243 refactored LocalizedFieldFile.save method 2017-04-24 20:29:09 +03:00
seroy
8ba08c389c changed indentation 2017-04-13 16:15:13 +03:00
seroy
24079a2fcb added description of LocalizedCharField, LocalizedTextField and LocalizedFileField 2017-04-13 11:11:52 +03:00
seroy
0f4c74a9b2 added comments and deleted extra code 2017-04-13 11:07:40 +03:00
seroy
5b93c5ec8f added style for AdminLocalizedFileFieldWidget 2017-04-12 22:10:12 +03:00
seroy
817c7e13fe added new LocalizedCharField, LocalizedTextField and LocalizedFileField fields 2017-04-12 21:34:19 +03:00
23 changed files with 865 additions and 132 deletions

1
.gitignore vendored
View File

@@ -4,6 +4,7 @@ env/
# Ignore Python byte code cache
*.pyc
__pycache__
.cache/
# Ignore coverage reports
.coverage

View File

@@ -199,6 +199,29 @@ Besides ``LocalizedField``, there's also:
title = LocalizedField()
slug = LocalizedUniqueSlugField(populate_from='title')
``populate_from`` can be:
- The name of a field.
.. code-block:: python
slug = LocalizedUniqueSlugField(populate_from='name', include_time=True)
- A callable.
.. code-block:: python
def generate_slug(instance):
return instance.title
slug = LocalizedUniqueSlugField(populate_from=generate_slug, include_time=True)
- A tuple of names of fields.
.. code-block:: python
slug = LocalizedUniqueSlugField(populate_from=('name', 'beer') include_time=True)
By setting the option ``include_time=True``
.. code-block:: python
@@ -208,21 +231,6 @@ Besides ``LocalizedField``, there's also:
You can instruct the field to include a part of the current time into
the resulting slug. This is useful if you're running into a lot of collisions.
* ``LocalizedAutoSlugField``
Automatically creates a slug for every language from the specified field.
Currently only supports ``populate_from``. Example usage:
.. code-block:: python
from localized_fields.fields import LocalizedField, LocalizedAutoSlugField
class MyModel(LocalizedModel):
title = LocalizedField()
slug = LocalizedAutoSlugField(populate_from='title')
This implementation is **NOT** concurrency safe, prefer ``LocalizedUniqueSlugField``.
* ``LocalizedBleachField``
Automatically bleaches the content of the field.
@@ -238,6 +246,60 @@ Besides ``LocalizedField``, there's also:
title = LocalizedField()
description = LocalizedBleachField()
* ``LocalizedCharField`` and ``LocalizedTextField``
This 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)
class MyModel(models.Model):
title = LocalizedCharField()
description = LocalizedTextField()
* ``LocalizedFileField``
A file-upload field
Parameter ``upload_to`` supports ``lang`` parameter for string formatting or as function argument (in case if ``upload_to`` is callable).
Example usage:
.. code-block:: python
from localized_fields import LocalizedFileField
def my_directory_path(instance, filename, lang):
# file will be uploaded to MEDIA_ROOT/<lang>/<id>_<filename>
return '{0}/{0}_{1}'.format(lang, instance.id, filename)
class MyModel(models.Model):
file1 = LocalizedFileField(upload_to='uploads/{lang}/')
file2 = LocalizedFileField(upload_to=my_directory_path)
In template you can access to file attributes:
.. code-block:: django
{# For current active language: #}
{{ model.file.url }} {# output file url #}
{{ model.file.name }} {# output file name #}
{# Or get it in a specific language: #}
{{ model.file.ro.url }} {# output file url for romanian language #}
{{ model.file.ro.name }} {# output file name for romanian language #}
To get access to file instance for current active language use ``localized`` method:
.. code-block:: python
model.file.localized()
Experimental feature
^^^^^^^^^^^^^^^^^^^^
Enables the following experimental features:

View File

@@ -1,11 +1,15 @@
from django.contrib.admin import ModelAdmin
from . import widgets
from .fields import LocalizedField
from .fields import LocalizedField, LocalizedCharField, LocalizedTextField, \
LocalizedFileField
FORMFIELD_FOR_LOCALIZED_FIELDS_DEFAULTS = {
LocalizedField: {'widget': widgets.AdminLocalizedFieldWidget},
LocalizedCharField: {'widget': widgets.AdminLocalizedCharFieldWidget},
LocalizedTextField: {'widget': widgets.AdminLocalizedFieldWidget},
LocalizedFileField: {'widget': widgets.AdminLocalizedFileFieldWidget},
}

View File

@@ -1,12 +1,18 @@
from .field import LocalizedField
from .autoslug_field import LocalizedAutoSlugField
from .uniqueslug_field import LocalizedUniqueSlugField
from .char_field import LocalizedCharField
from .text_field import LocalizedTextField
from .file_field import LocalizedFileField
__all__ = [
'LocalizedField',
'LocalizedAutoSlugField',
'LocalizedUniqueSlugField',
'LocalizedCharField',
'LocalizedTextField',
'LocalizedFileField'
]
try:

View File

@@ -1,12 +1,16 @@
from typing import Callable, Tuple
import warnings
from typing import Callable, Tuple, Union
from datetime import datetime
from django import forms
from django.conf import settings
from django.utils import translation
from django.utils.text import slugify
from .field import LocalizedField
from ..value import LocalizedValue
from ..util import resolve_object_property
class LocalizedAutoSlugField(LocalizedField):
@@ -19,6 +23,11 @@ class LocalizedAutoSlugField(LocalizedField):
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
@@ -147,7 +156,7 @@ class LocalizedAutoSlugField(LocalizedField):
]
@staticmethod
def _get_populate_from_value(instance, field_name: str, language: str):
def _get_populate_from_value(instance, field_name: Union[str, Tuple[str]], language: str):
"""Gets the value to create a slug from in the specified language.
Arguments:
@@ -164,5 +173,20 @@ class LocalizedAutoSlugField(LocalizedField):
The text to generate a slug for.
"""
value = getattr(instance, field_name, None)
return value.get(language)
if callable(field_name):
return field_name(instance)
def get_field_value(name):
value = resolve_object_property(instance, name)
with translation.override(language):
return str(value)
if isinstance(field_name, tuple) or isinstance(field_name, list):
value = '-'.join([
value
for value in [get_field_value(name) for name in field_name]
if value
])
return value
return get_field_value(field_name)

View File

@@ -0,0 +1,16 @@
from ..forms import LocalizedCharFieldForm
from .field import LocalizedField
from ..value import LocalizedStringValue
class LocalizedCharField(LocalizedField):
attr_class = LocalizedStringValue
def formfield(self, **kwargs):
"""Gets the form field associated with this field."""
defaults = {
'form_class': LocalizedCharFieldForm
}
defaults.update(kwargs)
return super().formfield(**defaults)

View File

@@ -170,7 +170,10 @@ class LocalizedField(HStoreField):
# are any of the language fiels None/empty?
is_all_null = True
for lang_code, _ in settings.LANGUAGES:
if value.get(lang_code):
# 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
@@ -198,7 +201,9 @@ class LocalizedField(HStoreField):
primary_lang_val = getattr(value, settings.LANGUAGE_CODE)
if not primary_lang_val:
# 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,

View File

@@ -0,0 +1,160 @@
import json
import datetime
import posixpath
from django.core.files import File
from django.db.models.fields.files import FieldFile
from django.utils import six
from django.core.files.storage import default_storage
from django.utils.encoding import force_str, force_text
from localized_fields.fields import LocalizedField
from localized_fields.fields.field import LocalizedValueDescriptor
from localized_fields.value import LocalizedValue
from ..value import LocalizedFileValue
from ..forms import LocalizedFileFieldForm
class LocalizedFieldFile(FieldFile):
def __init__(self, instance, field, name, lang):
super().__init__(instance, field, name)
self.lang = lang
def save(self, name, content, save=True):
name = self.field.generate_filename(self.instance, name, self.lang)
self.name = self.storage.save(name, content,
max_length=self.field.max_length)
self._committed = True
if save:
self.instance.save()
save.alters_data = True
def delete(self, save=True):
if not self:
return
if hasattr(self, '_file'):
self.close()
del self.file
self.storage.delete(self.name)
self.name = None
self._committed = False
if save:
self.instance.save()
delete.alters_data = True
class LocalizedFileValueDescriptor(LocalizedValueDescriptor):
def __get__(self, instance, cls=None):
value = super().__get__(instance, cls)
for lang, file in value.__dict__.items():
if isinstance(file, six.string_types) or file is None:
file = self.field.value_class(instance, self.field, file, lang)
value.set(lang, file)
elif isinstance(file, File) and \
not isinstance(file, LocalizedFieldFile):
file_copy = self.field.value_class(instance, self.field,
file.name, lang)
file_copy.file = file
file_copy._committed = False
value.set(lang, file_copy)
elif isinstance(file, LocalizedFieldFile) and \
not hasattr(file, 'field'):
file.instance = instance
file.field = self.field
file.storage = self.field.storage
file.lang = lang
# Make sure that the instance is correct.
elif isinstance(file, LocalizedFieldFile) \
and instance is not file.instance:
file.instance = instance
file.lang = lang
return value
class LocalizedFileField(LocalizedField):
descriptor_class = LocalizedFileValueDescriptor
attr_class = LocalizedFileValue
value_class = LocalizedFieldFile
def __init__(self, verbose_name=None, name=None, upload_to='', storage=None,
**kwargs):
self.storage = storage or default_storage
self.upload_to = upload_to
super().__init__(verbose_name, name, **kwargs)
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
kwargs['upload_to'] = self.upload_to
if self.storage is not default_storage:
kwargs['storage'] = self.storage
return name, path, args, kwargs
def get_prep_value(self, value):
"""Returns field's value prepared for saving into a database."""
if isinstance(value, LocalizedValue):
prep_value = LocalizedValue()
for k, v in value.__dict__.items():
if v is None:
prep_value.set(k, '')
else:
# Need to convert File objects provided via a form to
# unicode for database insertion
prep_value.set(k, six.text_type(v))
return super().get_prep_value(prep_value)
return super().get_prep_value(value)
def pre_save(self, model_instance, add):
"""Returns field's value just before saving."""
value = super().pre_save(model_instance, add)
if isinstance(value, LocalizedValue):
for file in value.__dict__.values():
if file and not file._committed:
file.save(file.name, file, save=False)
return value
def generate_filename(self, instance, filename, lang):
if callable(self.upload_to):
filename = self.upload_to(instance, filename, lang)
else:
now = datetime.datetime.now()
dirname = force_text(now.strftime(force_str(self.upload_to)))
dirname = dirname.format(lang=lang)
filename = posixpath.join(dirname, filename)
return self.storage.generate_filename(filename)
def save_form_data(self, instance, data):
if isinstance(data, LocalizedValue):
for k, v in data.__dict__.items():
if v is not None and not v:
data.set(k, '')
setattr(instance, self.name, data)
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,14 @@
from ..forms import LocalizedTextFieldForm
from .char_field import LocalizedCharField
class LocalizedTextField(LocalizedCharField):
def formfield(self, **kwargs):
"""Gets the form field associated with this field."""
defaults = {
'form_class': LocalizedTextFieldForm
}
defaults.update(kwargs)
return super().formfield(**defaults)

View File

@@ -2,9 +2,13 @@ from typing import List
from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
from django.forms.widgets import FILE_INPUT_CONTRADICTION
from .value import LocalizedValue
from .widgets import LocalizedFieldWidget
from .value import LocalizedValue, LocalizedStringValue, \
LocalizedFileValue
from .widgets import LocalizedFieldWidget, LocalizedCharFieldWidget, \
LocalizedFileWidget
class LocalizedFieldForm(forms.MultiValueField):
@@ -12,6 +16,7 @@ class LocalizedFieldForm(forms.MultiValueField):
the field in multiple languages."""
widget = LocalizedFieldWidget
field_class = forms.fields.CharField
value_class = LocalizedValue
def __init__(self, *args, **kwargs):
@@ -26,7 +31,7 @@ class LocalizedFieldForm(forms.MultiValueField):
field_options['required'] = kwargs.get('required', True)
field_options['label'] = lang_code
fields.append(forms.fields.CharField(**field_options))
fields.append(self.field_class(**field_options))
super(LocalizedFieldForm, self).__init__(
fields,
@@ -37,7 +42,7 @@ class LocalizedFieldForm(forms.MultiValueField):
for f, w in zip(self.fields, self.widget.widgets):
w.is_required = f.required
def compress(self, value: List[str]) -> LocalizedValue:
def compress(self, value: List[str]) -> value_class:
"""Compresses the values from individual fields
into a single :see:LocalizedValue instance.
@@ -56,3 +61,107 @@ class LocalizedFieldForm(forms.MultiValueField):
localized_value.set(lang_code, value)
return localized_value
class LocalizedCharFieldForm(LocalizedFieldForm):
"""Form for a localized char field, allows editing
the field in multiple languages."""
widget = LocalizedCharFieldWidget
value_class = LocalizedStringValue
class LocalizedTextFieldForm(LocalizedFieldForm):
"""Form for a localized text field, allows editing
the field in multiple languages."""
value_class = LocalizedStringValue
class LocalizedFileFieldForm(LocalizedFieldForm, forms.FileField):
"""Form for a localized file field, allows editing
the field in multiple languages."""
widget = LocalizedFileWidget
field_class = forms.fields.FileField
value_class = LocalizedFileValue
def clean(self, value, initial=None):
"""
Most part of this method is a copy of
django.forms.MultiValueField.clean, with the exception of initial
value handling (this need for correct processing FileField's).
All original comments saved.
"""
if initial is None:
initial = [None for x in range(0, len(value))]
else:
if not isinstance(initial, list):
initial = self.widget.decompress(initial)
clean_data = []
errors = []
if not value or isinstance(value, (list, tuple)):
if (not value or not [v for v in value if
v not in self.empty_values]) \
and (not initial or not [v for v in initial if
v not in self.empty_values]):
if self.required:
raise ValidationError(self.error_messages['required'],
code='required')
else:
raise ValidationError(self.error_messages['invalid'],
code='invalid')
for i, field in enumerate(self.fields):
try:
field_value = value[i]
except IndexError:
field_value = None
try:
field_initial = initial[i]
except IndexError:
field_initial = None
if field_value in self.empty_values and \
field_initial in self.empty_values:
if self.require_all_fields:
# Raise a 'required' error if the MultiValueField is
# required and any field is empty.
if self.required:
raise ValidationError(self.error_messages['required'],
code='required')
elif field.required:
# Otherwise, add an 'incomplete' error to the list of
# collected errors and skip field cleaning, if a required
# field is empty.
if field.error_messages['incomplete'] not in errors:
errors.append(field.error_messages['incomplete'])
continue
try:
clean_data.append(field.clean(field_value, field_initial))
except ValidationError as e:
# Collect all validation errors in a single list, which we'll
# raise at the end of clean(), rather than raising a single
# exception for the first error we encounter. Skip duplicates.
errors.extend(m for m in e.error_list if m not in errors)
if errors:
raise ValidationError(errors)
out = self.compress(clean_data)
self.validate(out)
self.run_validators(out)
return out
def bound_data(self, data, initial):
bound_data = []
if initial is None:
initial = [None for x in range(0, len(data))]
else:
if not isinstance(initial, list):
initial = self.widget.decompress(initial)
for d, i in zip(data, initial):
if d in (None, FILE_INPUT_CONTRADICTION):
bound_data.append(i)
else:
bound_data.append(d)
return bound_data

View File

@@ -45,3 +45,7 @@
border-color: #79aec8;
opacity: 1;
}
.localized-fields-widget p.file-upload {
margin-left: 0;
}

View File

@@ -19,3 +19,26 @@ def get_language_codes() -> List[str]:
lang_code
for lang_code, _ in settings.LANGUAGES
]
def resolve_object_property(obj, path: str):
"""Resolves the value of a property on an object.
Is able to resolve nested properties. For example,
a path can be specified:
'other.beer.name'
Raises:
AttributeError:
In case the property could not be resolved.
Returns:
The value of the specified property.
"""
value = obj
for path_part in path.split('.'):
value = getattr(value, path_part)
return value

View File

@@ -6,6 +6,7 @@ from django.utils import translation
class LocalizedValue(dict):
"""Represents the value of a :see:LocalizedField."""
default_value = None
def __init__(self, keys: dict=None):
"""Initializes a new instance of :see:LocalizedValue.
@@ -20,7 +21,7 @@ class LocalizedValue(dict):
super().__init__({})
self._interpret_value(keys)
def get(self, language: str=None) -> str:
def get(self, language: str=None, default: str=None) -> str:
"""Gets the underlying value in the specified or
primary language.
@@ -35,7 +36,7 @@ class LocalizedValue(dict):
"""
language = language or settings.LANGUAGE_CODE
return super().get(language, None)
return super().get(language, default)
def set(self, language: str, value: str):
"""Sets the value in the specified language.
@@ -60,7 +61,7 @@ class LocalizedValue(dict):
contained in this instance.
"""
path = 'localized_fields.fields.LocalizedValue'
path = 'localized_fields.localized_value.%s' % self.__class__.__name__
return path, [self.__dict__], {}
def _interpret_value(self, value):
@@ -83,14 +84,14 @@ class LocalizedValue(dict):
"""
for lang_code, _ in settings.LANGUAGES:
self.set(lang_code, None)
self.set(lang_code, self.default_value)
if isinstance(value, str):
self.set(settings.LANGUAGE_CODE, value)
elif isinstance(value, dict):
for lang_code, _ in settings.LANGUAGES:
lang_value = value.get(lang_code) or None
lang_value = value.get(lang_code, self.default_value)
self.set(lang_code, lang_value)
elif isinstance(value, collections.Iterable):
@@ -102,13 +103,19 @@ class LocalizedValue(dict):
back to the primary language if there's no value
in the current language."""
value = self.get(translation.get_language())
fallbacks = getattr(settings, 'LOCALIZED_FIELDS_FALLBACKS', {})
if not value:
value = self.get(settings.LANGUAGE_CODE)
language = translation.get_language() or settings.LANGUAGE_CODE
languages = fallbacks.get(language, [settings.LANGUAGE_CODE])[:]
languages.insert(0, language)
for lang_code in languages:
value = self.get(lang_code)
if value:
return value or ''
return ''
def __eq__(self, other):
"""Compares :paramref:self to :paramref:other for
equality.
@@ -156,4 +163,28 @@ class LocalizedValue(dict):
def __repr__(self): # pragma: no cover
"""Gets a textual representation of this object."""
return 'LocalizedValue<%s> 0x%s' % (dict(self), id(self))
return '%s<%s> 0x%s' % (self.__class__.__name__,
self.__dict__, id(self))
class LocalizedStringValue(LocalizedValue):
default_value = ''
class LocalizedFileValue(LocalizedValue):
def __getattr__(self, name: str):
"""Proxies access to attributes to attributes of LocalizedFile"""
value = self.get(translation.get_language())
if hasattr(value, name):
return getattr(value, name)
raise AttributeError("'{}' object has no attribute '{}'".
format(self.__class__.__name__, name))
def __str__(self) -> str:
"""Returns string representation of value"""
return str(super().__str__())
def localized(self):
"""Returns value for current language"""
return self.get(translation.get_language())

View File

@@ -45,6 +45,16 @@ class LocalizedFieldWidget(forms.MultiWidget):
return result
class LocalizedCharFieldWidget(LocalizedFieldWidget):
"""Widget that has an input box for every language."""
widget = forms.TextInput
class LocalizedFileWidget(LocalizedFieldWidget):
"""Widget that has an file input box for every language."""
widget = forms.ClearableFileInput
class AdminLocalizedFieldWidget(LocalizedFieldWidget):
widget = widgets.AdminTextareaWidget
template = 'localized_fields/admin/widget.html'
@@ -84,3 +94,11 @@ class AdminLocalizedFieldWidget(LocalizedFieldWidget):
and 'required' in attrs:
del attrs['required']
return attrs
class AdminLocalizedCharFieldWidget(AdminLocalizedFieldWidget):
widget = widgets.AdminTextInputWidget
class AdminLocalizedFileFieldWidget(AdminLocalizedFieldWidget):
widget = widgets.AdminFileWidget

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.2',
version='4.4',
packages=find_packages(exclude=['tests']),
include_package_data=True,
license='MIT License',

View File

@@ -1,3 +1,5 @@
import uuid
from django.db import connection, migrations
from django.db.migrations.executor import MigrationExecutor
from django.contrib.postgres.operations import HStoreExtension
@@ -5,24 +7,27 @@ from django.contrib.postgres.operations import HStoreExtension
from localized_fields.models import LocalizedModel
def define_fake_model(name='TestModel', fields=None):
def define_fake_model(fields=None, model_base=LocalizedModel, meta_options={}):
name = str(uuid.uuid4()).replace('-', '')[:8]
attributes = {
'app_label': 'localized_fields',
'app_label': 'tests',
'__module__': __name__,
'__name__': name
'__name__': name,
'Meta': type('Meta', (object,), meta_options)
}
if fields:
attributes.update(fields)
model = type(name, (model_base,), attributes)
model = type(name, (LocalizedModel,), attributes)
return model
def get_fake_model(name='TestModel', fields=None):
def get_fake_model(fields=None, model_base=LocalizedModel, meta_options={}):
"""Creates a fake model to use during unit tests."""
model = define_fake_model(name, fields)
model = define_fake_model(fields, model_base, meta_options)
class TestProject:
@@ -39,7 +44,7 @@ def get_fake_model(name='TestModel', fields=None):
with connection.schema_editor() as schema_editor:
migration_executor = MigrationExecutor(schema_editor.connection)
migration_executor.apply_migration(
TestProject(), TestMigration('eh', 'localized_fields'))
TestProject(), TestMigration('eh', 'postgres_extra'))
schema_editor.create_model(model)

View File

@@ -16,7 +16,6 @@ class LocalizedBulkTestCase(TestCase):
a :see:LocalizedUniqueSlugField in the model."""
model = get_fake_model(
'BulkSlugInsertModel',
{
'name': LocalizedField(),
'slug': LocalizedUniqueSlugField(populate_from='name', include_time=True),

View File

@@ -24,14 +24,12 @@ class LocalizedExpressionsTestCase(TestCase):
super(LocalizedExpressionsTestCase, cls).setUpClass()
cls.TestModel1 = get_fake_model(
'LocalizedExpressionsTestCase2',
{
'name': models.CharField(null=False, blank=False, max_length=255),
}
)
cls.TestModel2 = get_fake_model(
'LocalizedExpressionsTestCase1',
{
'text': LocalizedField(),
'other': models.ForeignKey(cls.TestModel1, related_name='features')

166
tests/test_file_field.py Normal file
View File

@@ -0,0 +1,166 @@
import os
import shutil
import tempfile as sys_tempfile
import pickle
import json
from django import forms
from django.test import TestCase, override_settings
from django.core.files.base import File, ContentFile
from django.core.files import temp as tempfile
from localized_fields.fields import LocalizedFileField
from localized_fields.value import LocalizedValue
from localized_fields.fields.file_field import LocalizedFieldFile
from localized_fields.forms import LocalizedFileFieldForm
from localized_fields.value import LocalizedFileValue
from localized_fields.widgets import LocalizedFileWidget
from .fake_model import get_fake_model
MEDIA_ROOT = sys_tempfile.mkdtemp()
@override_settings(MEDIA_ROOT=MEDIA_ROOT)
class LocalizedFileFieldTestCase(TestCase):
"""Tests the localized slug classes."""
@classmethod
def setUpClass(cls):
"""Creates the test models in the database."""
super().setUpClass()
cls.FileFieldModel = get_fake_model(
{
'file': LocalizedFileField(),
}
)
if not os.path.isdir(MEDIA_ROOT):
os.makedirs(MEDIA_ROOT)
@classmethod
def tearDownClass(cls):
super().tearDownClass()
shutil.rmtree(MEDIA_ROOT)
@classmethod
def test_assign(cls):
"""Tests whether the :see:LocalizedFileValueDescriptor works properly"""
temp_file = tempfile.NamedTemporaryFile(dir=MEDIA_ROOT)
instance = cls.FileFieldModel()
instance.file = {'en': temp_file.name}
assert isinstance(instance.file.en, LocalizedFieldFile)
assert instance.file.en.name == temp_file.name
field_dump = pickle.dumps(instance.file)
instance = cls.FileFieldModel()
instance.file = pickle.loads(field_dump)
assert instance.file.en.field == instance._meta.get_field('file')
assert instance.file.en.instance == instance
assert isinstance(instance.file.en, LocalizedFieldFile)
instance = cls.FileFieldModel()
instance.file = {'en': ContentFile("test", "testfilename")}
assert isinstance(instance.file.en, LocalizedFieldFile)
assert instance.file.en.name == "testfilename"
another_instance = cls.FileFieldModel()
another_instance.file = {'ro': instance.file.en}
assert another_instance == another_instance.file.ro.instance
assert another_instance.file.ro.lang == 'ro'
@classmethod
def test_save_form_data(cls):
"""Tests whether the :see:save_form_data function correctly set
a valid value."""
instance = cls.FileFieldModel()
data = LocalizedFileValue({'en': False})
instance._meta.get_field('file').save_form_data(instance, data)
assert instance.file.en == ''
@classmethod
def test_pre_save(cls):
"""Tests whether the :see:pre_save function works properly."""
instance = cls.FileFieldModel()
instance.file = {'en': ContentFile("test", "testfilename")}
instance._meta.get_field('file').pre_save(instance, False)
assert instance.file.en._committed == True
@classmethod
def test_file_methods(cls):
"""Tests whether the :see:LocalizedFieldFile.delete method works
correctly."""
temp_file = File(tempfile.NamedTemporaryFile())
instance = cls.FileFieldModel()
# Calling delete on an unset FileField should not call the file deletion
# process, but fail silently
instance.file.en.delete()
instance.file.en.save('testfilename', temp_file)
assert instance.file.en.name == 'testfilename'
instance.file.en.delete()
assert instance.file.en.name is None
@classmethod
def test_generate_filename(cls):
"""Tests whether the :see:LocalizedFieldFile.generate_filename method
works correctly."""
instance = cls.FileFieldModel()
field = instance._meta.get_field('file')
field.upload_to = '{lang}/'
filename = field.generate_filename(instance, 'test', 'en')
assert filename == 'en/test'
field.upload_to = lambda instance, filename, lang: \
'%s_%s' % (lang, filename)
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
value."""
value = LocalizedValue({'en': None})
assert LocalizedFileField().get_prep_value(None) == 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
a valid form."""
form_field = LocalizedFileField().formfield()
assert isinstance(form_field, LocalizedFileFieldForm)
assert isinstance(form_field, forms.FileField)
assert isinstance(form_field.widget, LocalizedFileWidget)
@staticmethod
def test_deconstruct():
"""Tests whether the :see:LocalizedFileField
class's :see:deconstruct function works properly."""
name, path, args, kwargs = LocalizedFileField().deconstruct()
assert 'upload_to' in kwargs
assert 'storage' not in kwargs
name, path, \
args, kwargs = LocalizedFileField(storage='test').deconstruct()
assert 'storage' in kwargs

View File

@@ -0,0 +1,41 @@
from django.conf import settings
from django.core.exceptions import ValidationError
from django.forms.widgets import FILE_INPUT_CONTRADICTION
from django.test import TestCase
from localized_fields.forms import LocalizedFileFieldForm
class LocalizedFileFieldFormTestCase(TestCase):
"""Tests the workings of the :see:LocalizedFileFieldForm class."""
def test_clean(self):
"""Tests whether the :see:clean function is working properly."""
formfield = LocalizedFileFieldForm(required=True)
with self.assertRaises(ValidationError):
formfield.clean([])
with self.assertRaises(ValidationError):
formfield.clean([], {'en': None})
with self.assertRaises(ValidationError):
formfield.clean("badvalue")
with self.assertRaises(ValidationError):
value = [FILE_INPUT_CONTRADICTION] * len(settings.LANGUAGES)
formfield.clean(value)
formfield = LocalizedFileFieldForm(required=False)
formfield.clean([''] * len(settings.LANGUAGES))
formfield.clean(['', ''], ['', ''])
def test_bound_data(self):
"""Tests whether the :see:bound_data function is returns correctly
value"""
formfield = LocalizedFileFieldForm()
assert formfield.bound_data([''], None) == ['']
initial = dict([(lang, '') for lang, _ in settings.LANGUAGES])
value = [None] * len(settings.LANGUAGES)
expected_value = [''] * len(settings.LANGUAGES)
assert formfield.bound_data(value, initial) == expected_value

View File

@@ -18,7 +18,6 @@ class LocalizedModelTestCase(TestCase):
super(LocalizedModelTestCase, cls).setUpClass()
cls.TestModel = get_fake_model(
'LocalizedModelTestCase',
{
'title': LocalizedField()
}

View File

@@ -1,6 +1,7 @@
import copy
from django import forms
from django.db import models
from django.conf import settings
from django.test import TestCase
from django.db.utils import IntegrityError
@@ -8,7 +9,6 @@ from django.utils.text import slugify
from localized_fields.fields import (
LocalizedField,
LocalizedAutoSlugField,
LocalizedUniqueSlugField
)
@@ -19,7 +19,7 @@ class LocalizedSlugFieldTestCase(TestCase):
"""Tests the localized slug classes."""
AutoSlugModel = None
MagicSlugModel = None
Model = None
@classmethod
def setUpClass(cls):
@@ -27,46 +27,14 @@ class LocalizedSlugFieldTestCase(TestCase):
super(LocalizedSlugFieldTestCase, cls).setUpClass()
cls.AutoSlugModel = get_fake_model(
'LocalizedAutoSlugFieldTestModel',
{
'title': LocalizedField(),
'slug': LocalizedAutoSlugField(populate_from='title')
}
)
cls.MagicSlugModel = get_fake_model(
'LocalizedUniqueSlugFieldTestModel',
cls.Model = get_fake_model(
{
'title': LocalizedField(),
'name': models.CharField(max_length=255),
'slug': LocalizedUniqueSlugField(populate_from='title')
}
)
@classmethod
def test_populate_auto(cls):
cls._test_populate(cls.AutoSlugModel)
@classmethod
def test_populate_unique(cls):
cls._test_populate(cls.MagicSlugModel)
@classmethod
def test_populate_multiple_languages_auto(cls):
cls._test_populate_multiple_languages(cls.AutoSlugModel)
@classmethod
def test_populate_multiple_languages_unique(cls):
cls._test_populate_multiple_languages(cls.MagicSlugModel)
@classmethod
def test_unique_slug_auto(cls):
cls._test_unique_slug(cls.AutoSlugModel)
@classmethod
def test_unique_slug_unique(cls):
cls._test_unique_slug(cls.MagicSlugModel)
@staticmethod
def test_unique_slug_with_time():
"""Tests whether the primary key is included in
@@ -75,7 +43,6 @@ class LocalizedSlugFieldTestCase(TestCase):
title = 'myuniquetitle'
PkModel = get_fake_model(
'PkModel',
{
'title': LocalizedField(),
'slug': LocalizedUniqueSlugField(populate_from='title', include_time=True)
@@ -93,7 +60,6 @@ class LocalizedSlugFieldTestCase(TestCase):
"""Tests whether slugs are not re-generated if not needed."""
NoChangeSlugModel = get_fake_model(
'NoChangeSlugModel',
{
'title': LocalizedField(),
'slug': LocalizedUniqueSlugField(populate_from='title', include_time=True)
@@ -115,62 +81,55 @@ class LocalizedSlugFieldTestCase(TestCase):
assert old_slug_en == obj.slug.en
assert old_slug_nl != obj.slug.nl
def test_unique_slug_unique_max_retries(self):
@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
try to find a slug forever and gives up after a while."""
title = 'myuniquetitle'
obj = self.MagicSlugModel()
obj = cls.Model()
obj.title.en = title
obj.save()
with self.assertRaises(IntegrityError):
with cls.assertRaises(cls, IntegrityError):
for _ in range(0, settings.LOCALIZED_FIELDS_MAX_RETRIES + 1):
another_obj = self.MagicSlugModel()
another_obj = cls.Model()
another_obj.title.en = title
another_obj.save()
@classmethod
def test_unique_slug_utf_auto(cls):
cls._test_unique_slug_utf(cls.AutoSlugModel)
@classmethod
def test_unique_slug_utf_unique(cls):
cls._test_unique_slug_utf(cls.MagicSlugModel)
@classmethod
def test_deconstruct_auto(cls):
cls._test_deconstruct(LocalizedAutoSlugField)
@classmethod
def test_deconstruct_unique(cls):
cls._test_deconstruct(LocalizedUniqueSlugField)
@classmethod
def test_formfield_auto(cls):
cls._test_formfield(LocalizedAutoSlugField)
@classmethod
def test_formfield_unique(cls):
cls._test_formfield(LocalizedUniqueSlugField)
@staticmethod
def _test_populate(model):
def test_populate(cls):
"""Tests whether the populating feature works correctly."""
obj = model()
obj = cls.Model()
obj.title.en = 'this is my title'
obj.save()
assert obj.slug.get('en') == slugify(obj.title)
@staticmethod
def _test_populate_multiple_languages(model):
"""Tests whether the populating feature correctly
works for all languages."""
@classmethod
def test_populate_callable(cls):
"""Tests whether the populating feature works correctly
when you specify a callable."""
obj = model()
def generate_slug(instance):
return instance.title
model = get_fake_model({
'title': LocalizedField(),
'slug': LocalizedUniqueSlugField(populate_from=generate_slug)
})
obj = cls.Model()
for lang_code, lang_name in settings.LANGUAGES:
obj.title.set(lang_code, 'title %s' % lang_name)
@@ -179,53 +138,122 @@ 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_unique_slug(model):
def test_populate_multiple_from_fields():
"""Tests whether populating the slug from multiple
fields works correctly."""
model = get_fake_model(
{
'title': LocalizedField(),
'name': models.CharField(max_length=255),
'slug': LocalizedUniqueSlugField(populate_from=('title', 'name'))
}
)
obj = model()
for lang_code, lang_name in settings.LANGUAGES:
obj.name = 'swen'
obj.title.set(lang_code, 'title %s' % lang_name)
obj.save()
for lang_code, lang_name in settings.LANGUAGES:
assert obj.slug.get(lang_code) == 'title-%s-swen' % lang_name.lower()
@staticmethod
def test_populate_multiple_from_fields_fk():
"""Tests whether populating the slug from multiple
fields works correctly."""
model_fk = get_fake_model(
{
'name': LocalizedField(),
}
)
model = get_fake_model(
{
'title': LocalizedField(),
'other': models.ForeignKey(model_fk),
'slug': LocalizedUniqueSlugField(populate_from=('title', 'other.name'))
}
)
other = model_fk.objects.create(name={settings.LANGUAGE_CODE: 'swen'})
obj = model()
for lang_code, lang_name in settings.LANGUAGES:
obj.other_id = other.id
obj.title.set(lang_code, 'title %s' % lang_name)
obj.save()
for lang_code, lang_name in settings.LANGUAGES:
assert obj.slug.get(lang_code) == 'title-%s-swen' % lang_name.lower()
@classmethod
def test_populate_multiple_languages(cls):
"""Tests whether the populating feature correctly
works for all languages."""
obj = cls.Model()
for lang_code, lang_name in settings.LANGUAGES:
obj.title.set(lang_code, 'title %s' % lang_name)
obj.save()
for lang_code, lang_name in settings.LANGUAGES:
assert obj.slug.get(lang_code) == 'title-%s' % lang_name.lower()
@classmethod
def test_unique_slug(cls):
"""Tests whether unique slugs are properly generated."""
title = 'myuniquetitle'
obj = model()
obj = cls.Model()
obj.title.en = title
obj.save()
for i in range(1, settings.LOCALIZED_FIELDS_MAX_RETRIES - 1):
another_obj = model()
another_obj = cls.Model()
another_obj.title.en = title
another_obj.save()
assert another_obj.slug.en == '%s-%d' % (title, i)
@staticmethod
def _test_unique_slug_utf(model):
@classmethod
def test_unique_slug_utf(cls):
"""Tests whether generating a slug works
when the value consists completely out
of non-ASCII characters."""
obj = model()
obj = cls.Model()
obj.title.en = 'مكاتب للايجار بشارع بورسعيد'
obj.save()
assert obj.slug.en == 'مكاتب-للايجار-بشارع-بورسعيد'
@staticmethod
def _test_deconstruct(field_type):
def test_deconstruct():
"""Tests whether the :see:deconstruct
function properly retains options
specified in the constructor."""
field = field_type(populate_from='title')
field = LocalizedUniqueSlugField(populate_from='title')
_, _, _, kwargs = field.deconstruct()
assert 'populate_from' in kwargs
assert kwargs['populate_from'] == field.populate_from
@staticmethod
def _test_formfield(field_type):
def test_formfield():
"""Tests whether the :see:formfield method
returns a valid form field that is hidden."""
form_field = field_type(populate_from='title').formfield()
form_field = LocalizedUniqueSlugField(populate_from='title').formfield()
assert isinstance(form_field, forms.CharField)
assert isinstance(form_field.widget, forms.HiddenInput)

View File

@@ -143,6 +143,26 @@ class LocalizedValueTestCase(TestCase):
# there's no actual value
assert localized_value.get(other_language) != test_value
@staticmethod
def test_str_fallback_custom_fallback():
"""Tests whether the :see:LocalizedValue class's
__str__'s fallback functionality properly respects
the LOCALIZED_FIELDS_FALLBACKS setting."""
test_value = 'myvalue'
settings.LOCALIZED_FIELDS_FALLBACKS = {
'nl': ['ro']
}
localized_value = LocalizedValue({
settings.LANGUAGE_CODE: settings.LANGUAGE_CODE,
'ro': 'ro'
})
with translation.override('nl'):
assert str(localized_value) == 'ro'
@staticmethod
def test_deconstruct():
"""Tests whether the :see:LocalizedValue