diff --git a/README.rst b/README.rst index 5b6c327..0ad18e4 100644 --- a/README.rst +++ b/README.rst @@ -238,6 +238,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`` (````) 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//_ + 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: diff --git a/localized_fields/admin.py b/localized_fields/admin.py index 4c54303..98597ea 100644 --- a/localized_fields/admin.py +++ b/localized_fields/admin.py @@ -1,11 +1,16 @@ 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}, } diff --git a/localized_fields/fields/__init__.py b/localized_fields/fields/__init__.py index d23188e..9c9889c 100644 --- a/localized_fields/fields/__init__.py +++ b/localized_fields/fields/__init__.py @@ -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: diff --git a/localized_fields/fields/char_field.py b/localized_fields/fields/char_field.py new file mode 100644 index 0000000..6799b4f --- /dev/null +++ b/localized_fields/fields/char_field.py @@ -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) diff --git a/localized_fields/fields/field.py b/localized_fields/fields/field.py index 28d9e62..321a8d7 100644 --- a/localized_fields/fields/field.py +++ b/localized_fields/fields/field.py @@ -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, diff --git a/localized_fields/fields/file_field.py b/localized_fields/fields/file_field.py new file mode 100644 index 0000000..76980d7 --- /dev/null +++ b/localized_fields/fields/file_field.py @@ -0,0 +1,151 @@ +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) diff --git a/localized_fields/fields/text_field.py b/localized_fields/fields/text_field.py new file mode 100644 index 0000000..72dd984 --- /dev/null +++ b/localized_fields/fields/text_field.py @@ -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) diff --git a/localized_fields/forms.py b/localized_fields/forms.py index 386f71a..4e4d2c5 100644 --- a/localized_fields/forms.py +++ b/localized_fields/forms.py @@ -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,106 @@ 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 diff --git a/localized_fields/static/localized_fields/localized-fields-admin.css b/localized_fields/static/localized_fields/localized-fields-admin.css index df66ca6..6413fe0 100644 --- a/localized_fields/static/localized_fields/localized-fields-admin.css +++ b/localized_fields/static/localized_fields/localized-fields-admin.css @@ -45,3 +45,7 @@ border-color: #79aec8; opacity: 1; } + +.localized-fields-widget p.file-upload { + margin-left: 0; +} diff --git a/localized_fields/value.py b/localized_fields/value.py index 68fbbe0..cb864b9 100644 --- a/localized_fields/value.py +++ b/localized_fields/value.py @@ -1,11 +1,13 @@ import collections + from django.conf import settings 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 +22,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 +37,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 +62,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 +85,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): @@ -156,4 +158,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()) diff --git a/localized_fields/widgets.py b/localized_fields/widgets.py index 0d6883d..1005e02 100644 --- a/localized_fields/widgets.py +++ b/localized_fields/widgets.py @@ -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 diff --git a/tests/test_file_field.py b/tests/test_file_field.py new file mode 100644 index 0000000..7bab6d7 --- /dev/null +++ b/tests/test_file_field.py @@ -0,0 +1,152 @@ +import os +import shutil +import tempfile as sys_tempfile +import pickle + +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( + 'LocalizedFileFieldTestModel', + { + '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' + + @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 diff --git a/tests/test_file_field_form.py b/tests/test_file_field_form.py new file mode 100644 index 0000000..3b33ba4 --- /dev/null +++ b/tests/test_file_field_form.py @@ -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 +