added new LocalizedCharField, LocalizedTextField and LocalizedFileField fields

This commit is contained in:
seroy 2017-04-12 21:32:30 +03:00
parent 23c6f975d8
commit 817c7e13fe
10 changed files with 364 additions and 14 deletions

View File

@ -1,6 +1,7 @@
from .forms import LocalizedFieldForm, LocalizedFieldWidget from .forms import LocalizedFieldForm, LocalizedFieldWidget
from .fields import (LocalizedAutoSlugField, LocalizedField, from .fields import (LocalizedAutoSlugField, LocalizedField,
LocalizedUniqueSlugField) LocalizedUniqueSlugField, LocalizedCharField,
LocalizedTextField, LocalizedFileField)
from .localized_value import LocalizedValue from .localized_value import LocalizedValue
from .mixins import AtomicSlugRetryMixin from .mixins import AtomicSlugRetryMixin
from .models import LocalizedModel from .models import LocalizedModel
@ -14,6 +15,9 @@ __all__ = [
'LocalizedAutoSlugField', 'LocalizedAutoSlugField',
'LocalizedUniqueSlugField', 'LocalizedUniqueSlugField',
'LocalizedBleachField', 'LocalizedBleachField',
'LocalizedCharField',
'LocalizedTextField',
'LocalizedFileField',
'LocalizedFieldWidget', 'LocalizedFieldWidget',
'LocalizedFieldForm', 'LocalizedFieldForm',
'AtomicSlugRetryMixin' 'AtomicSlugRetryMixin'

View File

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

View File

@ -1,12 +1,18 @@
from .localized_field import LocalizedField from .localized_field import LocalizedField
from .localized_autoslug_field import LocalizedAutoSlugField from .localized_autoslug_field import LocalizedAutoSlugField
from .localized_uniqueslug_field import LocalizedUniqueSlugField from .localized_uniqueslug_field import LocalizedUniqueSlugField
from .localized_char_field import LocalizedCharField
from .localized_text_field import LocalizedTextField
from .localized_file_field import LocalizedFileField
__all__ = [ __all__ = [
'LocalizedField', 'LocalizedField',
'LocalizedAutoSlugField', 'LocalizedAutoSlugField',
'LocalizedUniqueSlugField', 'LocalizedUniqueSlugField',
'LocalizedCharField',
'LocalizedTextField',
'LocalizedFileField'
] ]
try: try:

View File

@ -0,0 +1,16 @@
from ..forms import LocalizedCharFieldForm
from .localized_field import LocalizedField
from ..localized_value import LocalizedStingValue
class LocalizedCharField(LocalizedField):
attr_class = LocalizedStingValue
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

@ -187,7 +187,10 @@ class LocalizedField(HStoreField):
# are any of the language fiels None/empty? # are any of the language fiels None/empty?
is_all_null = True is_all_null = True
for lang_code, _ in settings.LANGUAGES: 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 is_all_null = False
break break
@ -215,7 +218,9 @@ class LocalizedField(HStoreField):
primary_lang_val = getattr(value, settings.LANGUAGE_CODE) 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( raise IntegrityError(
'null value in column "%s.%s" violates not-null constraint' % ( 'null value in column "%s.%s" violates not-null constraint' % (
self.name, self.name,

View File

@ -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.localized_field import LocalizedValueDescriptor
from localized_fields.localized_value import LocalizedValue
from ..localized_value import LocalizedFileValue
from ..forms import LocalizedFileFieldForm
class LocalizedFieldFile(FieldFile):
def __init__(self, instance, field, name):
super(FieldFile, self).__init__(None, name)
self.instance = instance
self.field = field
self.storage = field.storage
self._committed = True
def save(self, name, content, lang, save=True):
name = self.field.generate_filename(self.instance, name, 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 k, file in value.__dict__.items():
if isinstance(file, six.string_types) or file is None:
file = self.field.value_class(instance, self.field, file)
value.set(k, file)
elif isinstance(file, File) and \
not isinstance(file, LocalizedFieldFile):
file_copy = self.field.value_class(instance, self.field,
file.name)
file_copy.file = file
file_copy._committed = False
value.set(k, file_copy)
elif isinstance(file, LocalizedFieldFile) and \
not hasattr(file, 'field'):
file.instance = instance
file.field = self.field
file.storage = self.field.storage
# Make sure that the instance is correct.
elif isinstance(file, LocalizedFieldFile) \
and instance is not file.instance:
file.instance = instance
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(LocalizedFileField, self).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 lang, file in value.__dict__.items():
if file and not file._committed:
file.save(file.name, file, lang, 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.attname, data)
def formfield(self, **kwargs):
defaults = {'form_class': LocalizedFileFieldForm}
if 'initial' in kwargs:
defaults['required'] = False
defaults.update(kwargs)
return super().formfield(**defaults)

View File

@ -0,0 +1,14 @@
from ..forms import LocalizedTextFieldForm
from .localized_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,10 +2,13 @@ from typing import List
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError
from django.forms.widgets import FILE_INPUT_CONTRADICTION
from .localized_value import LocalizedValue from .localized_value import LocalizedValue, LocalizedStingValue, \
from .widgets import LocalizedFieldWidget LocalizedFileValue
from .widgets import LocalizedFieldWidget, LocalizedCharFieldWidget, \
LocalizedFileWidget
class LocalizedFieldForm(forms.MultiValueField): class LocalizedFieldForm(forms.MultiValueField):
@ -13,6 +16,7 @@ class LocalizedFieldForm(forms.MultiValueField):
the field in multiple languages.""" the field in multiple languages."""
widget = LocalizedFieldWidget widget = LocalizedFieldWidget
field_class = forms.fields.CharField
value_class = LocalizedValue value_class = LocalizedValue
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -27,7 +31,7 @@ class LocalizedFieldForm(forms.MultiValueField):
field_options['required'] = kwargs.get('required', True) field_options['required'] = kwargs.get('required', True)
field_options['label'] = lang_code field_options['label'] = lang_code
fields.append(forms.fields.CharField(**field_options)) fields.append(self.field_class(**field_options))
super(LocalizedFieldForm, self).__init__( super(LocalizedFieldForm, self).__init__(
fields, fields,
@ -57,3 +61,106 @@ class LocalizedFieldForm(forms.MultiValueField):
localized_value.set(lang_code, value) localized_value.set(lang_code, value)
return localized_value return localized_value
class LocalizedCharFieldForm(LocalizedFieldForm):
"""Form for a localized char field, allows editing
the field in multiple languages."""
widget = LocalizedCharFieldWidget
value_class = LocalizedStingValue
class LocalizedTextFieldForm(LocalizedFieldForm):
"""Form for a localized text field, allows editing
the field in multiple languages."""
value_class = LocalizedStingValue
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

@ -4,6 +4,7 @@ from django.utils import translation
class LocalizedValue(dict): class LocalizedValue(dict):
"""Represents the value of a :see:LocalizedField.""" """Represents the value of a :see:LocalizedField."""
default_value = None
def __init__(self, keys: dict=None): def __init__(self, keys: dict=None):
"""Initializes a new instance of :see:LocalizedValue. """Initializes a new instance of :see:LocalizedValue.
@ -15,12 +16,15 @@ class LocalizedValue(dict):
different language. different language.
""" """
# NOTE(seroy): First fill all the keys with default value,
# in order to attributes will be for each language
for lang_code, _ in settings.LANGUAGES:
value = keys.get(lang_code) if isinstance(keys, dict) else \
self.default_value
self.set(lang_code, value)
if isinstance(keys, str): if isinstance(keys, str):
setattr(self, settings.LANGUAGE_CODE, keys) setattr(self, settings.LANGUAGE_CODE, keys)
else:
for lang_code, _ in settings.LANGUAGES:
value = keys.get(lang_code) if keys else None
self.set(lang_code, value)
def get(self, language: str=None) -> str: def get(self, language: str=None) -> str:
"""Gets the underlying value in the specified or """Gets the underlying value in the specified or
@ -62,7 +66,7 @@ class LocalizedValue(dict):
contained in this instance. contained in this instance.
""" """
path = 'localized_fields.fields.LocalizedValue' path = 'localized_fields.localized_value.%s' % self.__class__.__name__
return path, [self.__dict__], {} return path, [self.__dict__], {}
def __str__(self) -> str: def __str__(self) -> str:
@ -124,4 +128,25 @@ class LocalizedValue(dict):
def __repr__(self): # pragma: no cover def __repr__(self): # pragma: no cover
"""Gets a textual representation of this object.""" """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 LocalizedStingValue(LocalizedValue):
default_value = ''
class LocalizedFileValue(LocalizedValue):
def __getattr__(self, name):
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):
return str(super().__str__())
def localized(self):
return self.get(translation.get_language())

View File

@ -46,6 +46,16 @@ class LocalizedFieldWidget(forms.MultiWidget):
return result 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): class AdminLocalizedFieldWidget(LocalizedFieldWidget):
widget = widgets.AdminTextareaWidget widget = widgets.AdminTextareaWidget
template = 'localized_fields/admin/widget.html' template = 'localized_fields/admin/widget.html'
@ -84,3 +94,11 @@ class AdminLocalizedFieldWidget(LocalizedFieldWidget):
and 'required' in attrs: and 'required' in attrs:
del attrs['required'] del attrs['required']
return attrs return attrs
class AdminLocalizedCharFieldWidget(AdminLocalizedFieldWidget):
widget = widgets.AdminTextInputWidget
class AdminLocalizedFileFieldWidget(AdminLocalizedFieldWidget):
widget = widgets.AdminFileWidget