Re-format all files

This commit is contained in:
Swen Kooij
2019-10-19 12:43:17 +03:00
parent 4ee1a5f487
commit 7cdd1f4490
41 changed files with 836 additions and 812 deletions

View File

@@ -1 +1 @@
default_app_config = 'localized_fields.apps.LocalizedFieldsConfig'
default_app_config = "localized_fields.apps.LocalizedFieldsConfig"

View File

@@ -1,13 +1,16 @@
from . import widgets
from .fields import LocalizedField, LocalizedCharField, LocalizedTextField, \
LocalizedFileField
from .fields import (
LocalizedCharField,
LocalizedField,
LocalizedFileField,
LocalizedTextField,
)
FORMFIELD_FOR_LOCALIZED_FIELDS_DEFAULTS = {
LocalizedField: {'widget': widgets.AdminLocalizedFieldWidget},
LocalizedCharField: {'widget': widgets.AdminLocalizedCharFieldWidget},
LocalizedTextField: {'widget': widgets.AdminLocalizedFieldWidget},
LocalizedFileField: {'widget': widgets.AdminLocalizedFileFieldWidget},
LocalizedField: {"widget": widgets.AdminLocalizedFieldWidget},
LocalizedCharField: {"widget": widgets.AdminLocalizedCharFieldWidget},
LocalizedTextField: {"widget": widgets.AdminLocalizedFieldWidget},
LocalizedFileField: {"widget": widgets.AdminLocalizedFileFieldWidget},
}
@@ -15,15 +18,11 @@ class LocalizedFieldsAdminMixin:
"""Mixin for making the fancy widgets work in Django Admin."""
class Media:
css = {
'all': (
'localized_fields/localized-fields-admin.css',
)
}
css = {"all": ("localized_fields/localized-fields-admin.css",)}
js = (
'admin/js/jquery.init.js',
'localized_fields/localized-fields-admin.js',
"admin/js/jquery.init.js",
"localized_fields/localized-fields-admin.js",
)
def __init__(self, *args, **kwargs):

View File

@@ -9,10 +9,10 @@ from .lookups import LocalizedLookupMixin
class LocalizedFieldsConfig(AppConfig):
name = 'localized_fields'
name = "localized_fields"
def ready(self):
if getattr(settings, 'LOCALIZED_FIELDS_EXPERIMENTAL', False):
if getattr(settings, "LOCALIZED_FIELDS_EXPERIMENTAL", False):
for _, clazz in inspect.getmembers(lookups):
if not inspect.isclass(clazz) or clazz is LocalizedLookupMixin:
continue

View File

@@ -3,8 +3,7 @@ from django.utils import six, translation
class LocalizedValueDescriptor:
"""
The descriptor for the localized value attribute on the model instance.
"""The descriptor for the localized value attribute on the model instance.
Returns a :see:LocalizedValue when accessed so you can do stuff like::
>>> from myapp.models import MyModel
@@ -60,6 +59,8 @@ class LocalizedValueDescriptor:
def __set__(self, instance, value):
if isinstance(value, six.string_types):
language = translation.get_language() or settings.LANGUAGE_CODE
self.__get__(instance).set(language, value) # pylint: disable=no-member
self.__get__(instance).set(
language, value
) # pylint: disable=no-member
else:
instance.__dict__[self.field.name] = value

View File

@@ -1,14 +1,13 @@
from django.conf import settings
from django.utils import translation
from psqlextra import expressions
class LocalizedRef(expressions.HStoreRef):
"""Expression that selects the value in a field only in
the currently active language."""
"""Expression that selects the value in a field only in the currently
active language."""
def __init__(self, name: str, lang: str=None):
def __init__(self, name: str, lang: str = None):
"""Initializes a new instance of :see:LocalizedRef.
Arguments:

View File

@@ -1,26 +1,24 @@
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 .field import LocalizedField
from .file_field import LocalizedFileField
from .integer_field import LocalizedIntegerField
from .text_field import LocalizedTextField
from .uniqueslug_field import LocalizedUniqueSlugField
__all__ = [
'LocalizedField',
'LocalizedAutoSlugField',
'LocalizedUniqueSlugField',
'LocalizedCharField',
'LocalizedTextField',
'LocalizedFileField',
'LocalizedIntegerField'
"LocalizedField",
"LocalizedAutoSlugField",
"LocalizedUniqueSlugField",
"LocalizedCharField",
"LocalizedTextField",
"LocalizedFileField",
"LocalizedIntegerField",
]
try:
from .bleach_field import LocalizedBleachField
__all__ += [
'LocalizedBleachField'
]
__all__ += ["LocalizedBleachField"]
except ImportError:
pass

View File

@@ -1,60 +1,53 @@
import warnings
from typing import Callable, Tuple, Union
from datetime import datetime
from typing import Callable, Tuple, Union
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
from ..value import LocalizedValue
from .field import LocalizedField
class LocalizedAutoSlugField(LocalizedField):
"""Automatically provides slugs for a localized
field upon saving."""
"""Automatically provides slugs for a localized field upon saving."""
warnings.warn(
'LocalizedAutoSlug is deprecated and will be removed in the next major version.',
DeprecationWarning
"LocalizedAutoSlug is deprecated and will be removed in the next major version.",
DeprecationWarning,
)
def __init__(self, *args, **kwargs):
"""Initializes a new instance of :see:LocalizedAutoSlugField."""
self.populate_from = kwargs.pop('populate_from', None)
self.include_time = kwargs.pop('include_time', False)
self.populate_from = kwargs.pop("populate_from", None)
self.include_time = kwargs.pop("include_time", False)
super(LocalizedAutoSlugField, self).__init__(
*args,
**kwargs
)
super(LocalizedAutoSlugField, self).__init__(*args, **kwargs)
def deconstruct(self):
"""Deconstructs the field into something the database
can store."""
"""Deconstructs the field into something the database can store."""
name, path, args, kwargs = super(
LocalizedAutoSlugField, self).deconstruct()
LocalizedAutoSlugField, self
).deconstruct()
kwargs['populate_from'] = self.populate_from
kwargs['include_time'] = self.include_time
kwargs["populate_from"] = self.populate_from
kwargs["include_time"] = self.include_time
return name, path, args, kwargs
def formfield(self, **kwargs):
"""Gets the form field associated with this field.
Because this is a slug field which is automatically
populated, it should be hidden from the form.
Because this is a slug field which is automatically populated,
it should be hidden from the form.
"""
defaults = {
'form_class': forms.CharField,
'required': False
}
defaults = {"form_class": forms.CharField, "required": False}
defaults.update(kwargs)
@@ -64,8 +57,7 @@ class LocalizedAutoSlugField(LocalizedField):
return form_field
def pre_save(self, instance, add: bool):
"""Ran just before the model is saved, allows us to built
the slug.
"""Ran just before the model is saved, allows us to built the slug.
Arguments:
instance:
@@ -83,21 +75,19 @@ class LocalizedAutoSlugField(LocalizedField):
continue
if self.include_time:
value += '-%s' % datetime.now().microsecond
value += "-%s" % datetime.now().microsecond
def is_unique(slug: str, language: str) -> bool:
"""Gets whether the specified slug is unique."""
unique_filter = {
'%s__%s' % (self.name, language): slug
}
unique_filter = {"%s__%s" % (self.name, language): slug}
return not type(instance).objects.filter(**unique_filter).exists()
return (
not type(instance).objects.filter(**unique_filter).exists()
)
slug = self._make_unique_slug(
slugify(value, allow_unicode=True),
lang_code,
is_unique
slugify(value, allow_unicode=True), lang_code, is_unique
)
slugs.set(lang_code, slug)
@@ -106,9 +96,11 @@ class LocalizedAutoSlugField(LocalizedField):
return slugs
@staticmethod
def _make_unique_slug(slug: str, language: str, is_unique: Callable[[str], bool]) -> str:
"""Guarentees that the specified slug is unique by appending
a number until it is unique.
def _make_unique_slug(
slug: str, language: str, is_unique: Callable[[str], bool]
) -> str:
"""Guarentees that the specified slug is unique by appending a number
until it is unique.
Arguments:
slug:
@@ -126,14 +118,14 @@ class LocalizedAutoSlugField(LocalizedField):
unique_slug = slug
while not is_unique(unique_slug, language):
unique_slug = '%s-%d' % (slug, index)
unique_slug = "%s-%d" % (slug, index)
index += 1
return unique_slug
def _get_populate_values(self, instance) -> Tuple[str, str]:
"""Gets all values (for each language) from the
specified's instance's `populate_from` field.
"""Gets all values (for each language) from the specified's instance's
`populate_from` field.
Arguments:
instance:
@@ -147,16 +139,16 @@ class LocalizedAutoSlugField(LocalizedField):
(
lang_code,
self._get_populate_from_value(
instance,
self.populate_from,
lang_code
instance, self.populate_from, lang_code
),
)
for lang_code, _ in settings.LANGUAGES
]
@staticmethod
def _get_populate_from_value(instance, field_name: Union[str, Tuple[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:
@@ -182,11 +174,13 @@ class LocalizedAutoSlugField(LocalizedField):
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
])
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

@@ -7,12 +7,11 @@ from .field import LocalizedField
class LocalizedBleachField(LocalizedField):
"""Custom version of :see:BleachField that
is actually a :see:LocalizedField."""
"""Custom version of :see:BleachField that is actually a
:see:LocalizedField."""
def pre_save(self, instance, add: bool):
"""Ran just before the model is saved, allows us to built
the slug.
"""Ran just before the model is saved, allows us to built the slug.
Arguments:
instance:
@@ -33,8 +32,7 @@ class LocalizedBleachField(LocalizedField):
continue
localized_value.set(
lang_code,
bleach.clean(value, **get_bleach_default_options())
lang_code, bleach.clean(value, **get_bleach_default_options())
)
return localized_value

View File

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

View File

@@ -1,22 +1,22 @@
import json
from typing import Union, List, Optional
from typing import List, Optional, Union
from django.conf import settings
from django.db.utils import IntegrityError
from psqlextra.fields import HStoreField
from ..descriptor import LocalizedValueDescriptor
from ..forms import LocalizedFieldForm
from ..value import LocalizedValue
from ..descriptor import LocalizedValueDescriptor
class LocalizedField(HStoreField):
"""A field that has the same value in multiple languages.
Internally this is stored as a :see:HStoreField where there
is a key for every language."""
Internally this is stored as a :see:HStoreField where there is a key
for every language.
"""
Meta = None
@@ -27,7 +27,9 @@ class LocalizedField(HStoreField):
# The descriptor to use for accessing the attribute off of the class.
descriptor_class = LocalizedValueDescriptor
def __init__(self, *args, required: Union[bool, List[str]]=None, **kwargs):
def __init__(
self, *args, required: Union[bool, List[str]] = None, **kwargs
):
"""Initializes a new instance of :see:LocalizedField."""
super(LocalizedField, self).__init__(*args, required=required, **kwargs)
@@ -54,8 +56,7 @@ class LocalizedField(HStoreField):
@classmethod
def from_db_value(cls, value, *_) -> Optional[LocalizedValue]:
"""Turns the specified database value into its Python
equivalent.
"""Turns the specified database value into its Python equivalent.
Arguments:
value:
@@ -68,7 +69,7 @@ class LocalizedField(HStoreField):
"""
if not value:
if getattr(settings, 'LOCALIZED_FIELDS_EXPERIMENTAL', False):
if getattr(settings, "LOCALIZED_FIELDS_EXPERIMENTAL", False):
return None
else:
return cls.attr_class()
@@ -98,8 +99,7 @@ class LocalizedField(HStoreField):
return cls.attr_class(value)
def to_python(self, value: Union[dict, str, None]) -> LocalizedValue:
"""Turns the specified database value into its Python
equivalent.
"""Turns the specified database value into its Python equivalent.
Arguments:
value:
@@ -124,8 +124,7 @@ class LocalizedField(HStoreField):
return self.attr_class(deserialized_value)
def get_prep_value(self, value: LocalizedValue) -> dict:
"""Turns the specified value into something the database
can store.
"""Turns the specified value into something the database can store.
If an illegal value (non-LocalizedValue instance) is
specified, we'll treat it as an empty :see:LocalizedValue
@@ -161,8 +160,8 @@ class LocalizedField(HStoreField):
)
def clean(self, value, *_):
"""Cleans the specified value into something we
can store in the database.
"""Cleans the specified value into something we can store in the
database.
For example, when all the language fields are
left empty, and the field is allowed to be null,
@@ -195,7 +194,7 @@ class LocalizedField(HStoreField):
def validate(self, value: LocalizedValue, *_):
"""Validates that the values has been filled in for all required
languages
languages.
Exceptions are raises in order to notify the user
of invalid values.
@@ -212,15 +211,17 @@ class LocalizedField(HStoreField):
lang_val = getattr(value, settings.LANGUAGE_CODE)
if lang_val is None:
raise IntegrityError('null value in column "%s.%s" violates '
'not-null constraint' % (self.name, lang))
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 = dict(
form_class=LocalizedFieldForm,
required=False if self.blank else self.required
required=False if self.blank else self.required,
)
defaults.update(kwargs)
return super().formfield(**defaults)

View File

@@ -1,31 +1,31 @@
import json
import datetime
import json
import posixpath
from django.core.files import File
from django.core.files.storage import default_storage
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
from ..value import LocalizedFileValue
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.name = self.storage.save(
name, content, max_length=self.field.max_length
)
self._committed = True
if save:
@@ -37,7 +37,7 @@ class LocalizedFieldFile(FieldFile):
if not self:
return
if hasattr(self, '_file'):
if hasattr(self, "_file"):
self.close()
del self.file
@@ -60,24 +60,29 @@ class LocalizedFileValueDescriptor(LocalizedValueDescriptor):
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)
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'):
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:
elif (
isinstance(file, LocalizedFieldFile)
and instance is not file.instance
):
file.instance = instance
file.lang = lang
return value
@@ -88,8 +93,9 @@ class LocalizedFileField(LocalizedField):
attr_class = LocalizedFileValue
value_class = LocalizedFieldFile
def __init__(self, verbose_name=None, name=None, upload_to='', storage=None,
**kwargs):
def __init__(
self, verbose_name=None, name=None, upload_to="", storage=None, **kwargs
):
self.storage = storage or default_storage
self.upload_to = upload_to
@@ -98,9 +104,9 @@ class LocalizedFileField(LocalizedField):
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
kwargs['upload_to'] = self.upload_to
kwargs["upload_to"] = self.upload_to
if self.storage is not default_storage:
kwargs['storage'] = self.storage
kwargs["storage"] = self.storage
return name, path, args, kwargs
def get_prep_value(self, value):
@@ -110,7 +116,7 @@ class LocalizedFileField(LocalizedField):
prep_value = LocalizedValue()
for k, v in value.__dict__.items():
if v is None:
prep_value.set(k, '')
prep_value.set(k, "")
else:
# Need to convert File objects provided via a form to
# unicode for database insertion
@@ -141,18 +147,17 @@ class LocalizedFileField(LocalizedField):
if isinstance(data, LocalizedValue):
for k, v in data.__dict__.items():
if v is not None and not v:
data.set(k, '')
data.set(k, "")
setattr(instance, self.name, data)
def formfield(self, **kwargs):
defaults = {'form_class': LocalizedFileFieldForm}
defaults = {"form_class": LocalizedFileFieldForm}
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()})
return json.dumps({k: v.name for k, v in value.__dict__.items()})
else:
return super().value_to_string(obj)

View File

@@ -1,11 +1,11 @@
from typing import Optional, Union, Dict
from typing import Dict, Optional, Union
from django.conf import settings
from django.db.utils import IntegrityError
from .field import LocalizedField
from ..value import LocalizedValue, LocalizedIntegerValue
from ..forms import LocalizedIntegerFieldForm
from ..value import LocalizedIntegerValue, LocalizedValue
from .field import LocalizedField
class LocalizedIntegerField(LocalizedField):
@@ -27,7 +27,9 @@ class LocalizedIntegerField(LocalizedField):
return cls._convert_localized_value(db_value)
def to_python(self, value: Union[Dict[str, int], int, None]) -> LocalizedIntegerValue:
def to_python(
self, value: Union[Dict[str, int], int, None]
) -> LocalizedIntegerValue:
"""Converts the value from a database value into a Python value."""
db_value = super().to_python(value)
@@ -55,32 +57,36 @@ class LocalizedIntegerField(LocalizedField):
if local_value is not None:
int(local_value)
except (TypeError, ValueError):
raise IntegrityError('non-integer value in column "%s.%s" violates '
'integer constraint' % (self.name, lang_code))
raise IntegrityError(
'non-integer value in column "%s.%s" violates '
"integer constraint" % (self.name, lang_code)
)
# convert to a string before saving because the underlying
# type is hstore, which only accept strings
prepped_value[lang_code] = str(local_value) if local_value is not None else None
prepped_value[lang_code] = (
str(local_value) if local_value is not None else None
)
return prepped_value
def formfield(self, **kwargs):
"""Gets the form field associated with this field."""
defaults = {
'form_class': LocalizedIntegerFieldForm
}
defaults = {"form_class": LocalizedIntegerFieldForm}
defaults.update(kwargs)
return super().formfield(**defaults)
@staticmethod
def _convert_localized_value(value: LocalizedValue) -> LocalizedIntegerValue:
def _convert_localized_value(
value: LocalizedValue
) -> LocalizedIntegerValue:
"""Converts from :see:LocalizedValue to :see:LocalizedIntegerValue."""
integer_values = {}
for lang_code, _ in settings.LANGUAGES:
local_value = value.get(lang_code, None)
if local_value is None or local_value.strip() == '':
if local_value is None or local_value.strip() == "":
local_value = None
try:

View File

@@ -6,9 +6,7 @@ class LocalizedTextField(LocalizedCharField):
def formfield(self, **kwargs):
"""Gets the form field associated with this field."""
defaults = {
'form_class': LocalizedTextFieldForm
}
defaults = {"form_class": LocalizedTextFieldForm}
defaults.update(kwargs)
return super().formfield(**defaults)

View File

@@ -1,17 +1,16 @@
from datetime import datetime
from django.utils.text import slugify
from django.core.exceptions import ImproperlyConfigured
from django.utils.text import slugify
from .autoslug_field import LocalizedAutoSlugField
from ..util import get_language_codes
from ..mixins import AtomicSlugRetryMixin
from ..util import get_language_codes
from ..value import LocalizedValue
from .autoslug_field import LocalizedAutoSlugField
class LocalizedUniqueSlugField(LocalizedAutoSlugField):
"""Automatically provides slugs for a localized
field upon saving."
"""Automatically provides slugs for a localized field upon saving.".
An improved version of :see:LocalizedAutoSlugField,
which adds:
@@ -27,30 +26,26 @@ class LocalizedUniqueSlugField(LocalizedAutoSlugField):
def __init__(self, *args, **kwargs):
"""Initializes a new instance of :see:LocalizedUniqueSlugField."""
kwargs['uniqueness'] = kwargs.pop('uniqueness', get_language_codes())
kwargs["uniqueness"] = kwargs.pop("uniqueness", get_language_codes())
super(LocalizedUniqueSlugField, self).__init__(
*args,
**kwargs
)
super(LocalizedUniqueSlugField, self).__init__(*args, **kwargs)
self.populate_from = kwargs.pop('populate_from')
self.include_time = kwargs.pop('include_time', False)
self.populate_from = kwargs.pop("populate_from")
self.include_time = kwargs.pop("include_time", False)
def deconstruct(self):
"""Deconstructs the field into something the database
can store."""
"""Deconstructs the field into something the database can store."""
name, path, args, kwargs = super(
LocalizedUniqueSlugField, self).deconstruct()
LocalizedUniqueSlugField, self
).deconstruct()
kwargs['populate_from'] = self.populate_from
kwargs['include_time'] = self.include_time
kwargs["populate_from"] = self.populate_from
kwargs["include_time"] = self.include_time
return name, path, args, kwargs
def pre_save(self, instance, add: bool):
"""Ran just before the model is saved, allows us to built
the slug.
"""Ran just before the model is saved, allows us to built the slug.
Arguments:
instance:
@@ -65,10 +60,13 @@ class LocalizedUniqueSlugField(LocalizedAutoSlugField):
"""
if not isinstance(instance, AtomicSlugRetryMixin):
raise ImproperlyConfigured((
'Model \'%s\' does not inherit from AtomicSlugRetryMixin. '
'Without this, the LocalizedUniqueSlugField will not work.'
) % type(instance).__name__)
raise ImproperlyConfigured(
(
"Model '%s' does not inherit from AtomicSlugRetryMixin. "
"Without this, the LocalizedUniqueSlugField will not work."
)
% type(instance).__name__
)
slugs = LocalizedValue()
@@ -83,20 +81,21 @@ class LocalizedUniqueSlugField(LocalizedAutoSlugField):
if instance.pk is not None:
current_slug = getattr(instance, self.name).get(lang_code)
if current_slug is not None:
stripped_slug = current_slug[0:current_slug.rfind('-')]
current_slug_end_index = current_slug.rfind("-")
stripped_slug = current_slug[0:current_slug_end_index]
if slug == stripped_slug:
slugs.set(lang_code, current_slug)
continue
if self.include_time:
slug += '-%d' % datetime.now().microsecond
slug += "-%d" % datetime.now().microsecond
retries = getattr(instance, 'retries', 0)
retries = getattr(instance, "retries", 0)
if retries > 0:
# do not add another - if we already added time
if not self.include_time:
slug += '-'
slug += '%d' % retries
slug += "-"
slug += "%d" % retries
slugs.set(lang_code, slug)

View File

@@ -5,30 +5,41 @@ from django.conf import settings
from django.core.exceptions import ValidationError
from django.forms.widgets import FILE_INPUT_CONTRADICTION
from .value import LocalizedValue, LocalizedStringValue, \
LocalizedFileValue, LocalizedIntegerValue
from .widgets import LocalizedFieldWidget, LocalizedCharFieldWidget, \
LocalizedFileWidget, AdminLocalizedIntegerFieldWidget
from .value import (
LocalizedFileValue,
LocalizedIntegerValue,
LocalizedStringValue,
LocalizedValue,
)
from .widgets import (
AdminLocalizedIntegerFieldWidget,
LocalizedCharFieldWidget,
LocalizedFieldWidget,
LocalizedFileWidget,
)
class LocalizedFieldForm(forms.MultiValueField):
"""Form for a localized field, allows editing
the field in multiple languages."""
"""Form for a localized field, allows editing the field in multiple
languages."""
widget = LocalizedFieldWidget
field_class = forms.fields.CharField
value_class = LocalizedValue
def __init__(self, *args, required: Union[bool, List[str]]=False, **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 = dict(
required=required if type(required) is bool else (lang_code in
required),
label=lang_code
required=required
if type(required) is bool
else (lang_code in required),
label=lang_code,
)
fields.append(self.field_class(**field_options))
@@ -36,7 +47,8 @@ class LocalizedFieldForm(forms.MultiValueField):
fields,
required=required if type(required) is bool else True,
require_all_fields=False,
*args, **kwargs
*args,
**kwargs
)
# set 'required' attribute for each widget separately
@@ -44,8 +56,8 @@ class LocalizedFieldForm(forms.MultiValueField):
widget.is_required = field.required
def compress(self, value: List[str]) -> value_class:
"""Compresses the values from individual fields
into a single :see:LocalizedValue instance.
"""Compresses the values from individual fields into a single
:see:LocalizedValue instance.
Arguments:
value:
@@ -65,41 +77,41 @@ class LocalizedFieldForm(forms.MultiValueField):
class LocalizedCharFieldForm(LocalizedFieldForm):
"""Form for a localized char field, allows editing
the field in multiple languages."""
"""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."""
"""Form for a localized text field, allows editing the field in multiple
languages."""
value_class = LocalizedStringValue
class LocalizedIntegerFieldForm(LocalizedFieldForm):
"""Form for a localized integer field, allows editing
the field in multiple languages."""
"""Form for a localized integer field, allows editing the field in multiple
languages."""
widget = AdminLocalizedIntegerFieldWidget
value_class = LocalizedIntegerValue
class LocalizedFileFieldForm(LocalizedFieldForm, forms.FileField):
"""Form for a localized file field, allows editing
the field in multiple languages."""
"""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).
"""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:
@@ -111,16 +123,21 @@ class LocalizedFileFieldForm(LocalizedFieldForm, forms.FileField):
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 (
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')
raise ValidationError(
self.error_messages["required"], code="required"
)
else:
raise ValidationError(self.error_messages['invalid'],
code='invalid')
raise ValidationError(
self.error_messages["invalid"], code="invalid"
)
for i, field in enumerate(self.fields):
try:
field_value = value[i]
@@ -131,20 +148,23 @@ class LocalizedFileFieldForm(LocalizedFieldForm, forms.FileField):
except IndexError:
field_initial = None
if field_value in self.empty_values and \
field_initial in self.empty_values:
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')
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'])
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))

View File

@@ -1,15 +1,29 @@
from django.conf import settings
from django.contrib.postgres.fields.hstore import KeyTransform
from django.contrib.postgres.lookups import (SearchLookup, TrigramSimilar,
Unaccent)
from django.contrib.postgres.lookups import (
SearchLookup,
TrigramSimilar,
Unaccent,
)
from django.db.models.expressions import Col
from django.db.models.lookups import (Contains, EndsWith, Exact, IContains,
IEndsWith, IExact, In, IRegex, IsNull,
IStartsWith, Regex, StartsWith)
from django.db.models.lookups import (
Contains,
EndsWith,
Exact,
IContains,
IEndsWith,
IExact,
In,
IRegex,
IsNull,
IStartsWith,
Regex,
StartsWith,
)
from django.utils import translation
class LocalizedLookupMixin():
class LocalizedLookupMixin:
def process_lhs(self, qn, connection):
if isinstance(self.lhs, Col):
language = translation.get_language() or settings.LANGUAGE_CODE

View File

@@ -6,9 +6,6 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
]
dependencies = []
operations = [
HStoreExtension(),
]
operations = [HStoreExtension()]

View File

@@ -1,22 +1,18 @@
from django.db import transaction
from django.conf import settings
from django.db import transaction
from django.db.utils import IntegrityError
class AtomicSlugRetryMixin:
"""Makes :see:LocalizedUniqueSlugField work by retrying upon
violation of the UNIQUE constraint."""
"""Makes :see:LocalizedUniqueSlugField work by retrying upon violation of
the UNIQUE constraint."""
def save(self, *args, **kwargs):
"""Saves this model instance to the database."""
max_retries = getattr(
settings,
'LOCALIZED_FIELDS_MAX_RETRIES',
100
)
max_retries = getattr(settings, "LOCALIZED_FIELDS_MAX_RETRIES", 100)
if not hasattr(self, 'retries'):
if not hasattr(self, "retries"):
self.retries = 0
with transaction.atomic():
@@ -28,7 +24,7 @@ class AtomicSlugRetryMixin:
# field class... we can also not only catch exceptions
# that apply to slug fields... so yea.. this is as
# retarded as it gets... i am sorry :(
if 'slug' not in str(ex):
if "slug" not in str(ex):
raise ex
if self.retries >= max_retries:

View File

@@ -10,7 +10,8 @@ class LocalizedModel(AtomicSlugRetryMixin, PostgresModel):
from LocalizedModel. However, for certain features, this is required.
It is definitely needed for :see:LocalizedUniqueSlugField, unless you
manually inherit from AtomicSlugRetryMixin."""
manually inherit from AtomicSlugRetryMixin.
"""
class Meta:
abstract = True

View File

@@ -15,10 +15,7 @@ def get_language_codes() -> List[str]:
in your project.
"""
return [
lang_code
for lang_code, _ in settings.LANGUAGES
]
return [lang_code for lang_code, _ in settings.LANGUAGES]
def resolve_object_property(obj, path: str):
@@ -38,7 +35,7 @@ def resolve_object_property(obj, path: str):
"""
value = obj
for path_part in path.split('.'):
for path_part in path.split("."):
value = getattr(value, path_part)
return value

View File

@@ -1,7 +1,7 @@
import deprecation
from typing import Optional
from collections.abc import Iterable
from typing import Optional
import deprecation
from django.conf import settings
from django.utils import translation
@@ -9,9 +9,10 @@ from django.utils import translation
class LocalizedValue(dict):
"""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.
Arguments:
@@ -24,9 +25,8 @@ class LocalizedValue(dict):
super().__init__({})
self._interpret_value(keys)
def get(self, language: str=None, default: str=None) -> str:
"""Gets the underlying value in the specified or
primary language.
def get(self, language: str = None, default: str = None) -> str:
"""Gets the underlying value in the specified or primary language.
Arguments:
language:
@@ -65,12 +65,12 @@ class LocalizedValue(dict):
contained in this instance.
"""
path = 'localized_fields.value.%s' % self.__class__.__name__
path = "localized_fields.value.%s" % self.__class__.__name__
return path, [self.__dict__], {}
def _interpret_value(self, value):
"""Interprets a value passed in the constructor as
a :see:LocalizedValue.
"""Interprets a value passed in the constructor as a
:see:LocalizedValue.
If string:
Assumes it's the default language.
@@ -103,11 +103,10 @@ class LocalizedValue(dict):
self._interpret_value(val)
def translate(self) -> Optional[str]:
"""Gets the value in the current language or falls
back to the next language if there's no value in the
current language."""
"""Gets the value in the current language or falls back to the next
language if there's no value in the current language."""
fallbacks = getattr(settings, 'LOCALIZED_FIELDS_FALLBACKS', {})
fallbacks = getattr(settings, "LOCALIZED_FIELDS_FALLBACKS", {})
language = translation.get_language() or settings.LANGUAGE_CODE
languages = fallbacks.get(language, [settings.LANGUAGE_CODE])[:]
@@ -121,15 +120,13 @@ class LocalizedValue(dict):
return None
def __str__(self) -> str:
"""Gets the value in the current language or falls
back to the next language if there's no value in the
current language."""
"""Gets the value in the current language or falls back to the next
language if there's no value in the current language."""
return self.translate() or ''
return self.translate() or ""
def __eq__(self, other):
"""Compares :paramref:self to :paramref:other for
equality.
"""Compares :paramref:self to :paramref:other for equality.
Returns:
True when :paramref:self is equal to :paramref:other.
@@ -148,8 +145,7 @@ class LocalizedValue(dict):
return True
def __ne__(self, other):
"""Compares :paramref:self to :paramerf:other for
in-equality.
"""Compares :paramref:self to :paramerf:other for in-equality.
Returns:
True when :paramref:self is not equal to :paramref:other.
@@ -174,34 +170,43 @@ class LocalizedValue(dict):
def __repr__(self): # pragma: no cover
"""Gets a textual representation of this object."""
return '%s<%s> 0x%s' % (self.__class__.__name__,
self.__dict__, id(self))
return "%s<%s> 0x%s" % (
self.__class__.__name__,
self.__dict__,
id(self),
)
class LocalizedStringValue(LocalizedValue):
default_value = ''
default_value = ""
class LocalizedFileValue(LocalizedValue):
def __getattr__(self, name: str):
"""Proxies access to attributes to attributes of LocalizedFile"""
"""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))
raise AttributeError(
"'{}' object has no attribute '{}'".format(
self.__class__.__name__, name
)
)
def __str__(self) -> str:
"""Returns string representation of value"""
"""Returns string representation of value."""
return str(super().__str__())
@deprecation.deprecated(deprecated_in='4.6', removed_in='5.0',
current_version='4.6',
details='Use the translate() function instead.')
@deprecation.deprecated(
deprecated_in="4.6",
removed_in="5.0",
current_version="4.6",
details="Use the translate() function instead.",
)
def localized(self):
"""Returns value for current language"""
"""Returns value for current language."""
return self.get(translation.get_language())
@@ -212,11 +217,11 @@ class LocalizedIntegerValue(LocalizedValue):
default_value = None
def translate(self):
"""Gets the value in the current language, or
in the configured fallbck language."""
"""Gets the value in the current language, or in the configured fallbck
language."""
value = super().translate()
if value is None or (isinstance(value, str) and value.strip() == ''):
if value is None or (isinstance(value, str) and value.strip() == ""):
return None
return int(value)
@@ -231,7 +236,7 @@ class LocalizedIntegerValue(LocalizedValue):
return int(value)
def __str__(self) -> str:
"""Returns string representation of value"""
"""Returns string representation of value."""
value = self.translate()
return str(value) if value is not None else ''
return str(value) if value is not None else ""

View File

@@ -2,8 +2,8 @@ import copy
from typing import List
from django.conf import settings
from django import forms
from django.conf import settings
from django.contrib.admin import widgets
from .value import LocalizedValue
@@ -11,27 +11,27 @@ from .value import LocalizedValue
class LocalizedFieldWidget(forms.MultiWidget):
"""Widget that has an input box for every language."""
template_name = 'localized_fields/multiwidget.html'
template_name = "localized_fields/multiwidget.html"
widget = forms.Textarea
def __init__(self, *args, **kwargs):
"""Initializes a new instance of :see:LocalizedFieldWidget."""
initial_widgets = [
copy.copy(self.widget)
for _ in settings.LANGUAGES
]
initial_widgets = [copy.copy(self.widget) for _ in settings.LANGUAGES]
super().__init__(initial_widgets, *args, **kwargs)
for ((lang_code, lang_name), widget) in zip(settings.LANGUAGES, self.widgets):
widget.attrs['lang'] = lang_code
for ((lang_code, lang_name), widget) in zip(
settings.LANGUAGES, self.widgets
):
widget.attrs["lang"] = lang_code
widget.lang_code = lang_code
widget.lang_name = lang_name
def decompress(self, value: LocalizedValue) -> List[str]:
"""Decompresses the specified value so
it can be spread over the internal widgets.
"""Decompresses the specified value so it can be spread over the
internal widgets.
Arguments:
value:
@@ -61,56 +61,62 @@ class LocalizedFieldWidget(forms.MultiWidget):
if not isinstance(value, list):
value = self.decompress(value)
final_attrs = context['widget']['attrs']
input_type = final_attrs.pop('type', None)
id_ = final_attrs.get('id')
final_attrs = context["widget"]["attrs"]
input_type = final_attrs.pop("type", None)
id_ = final_attrs.get("id")
subwidgets = []
for i, widget in enumerate(self.widgets):
if input_type is not None:
widget.input_type = input_type
widget_name = '%s_%s' % (name, i)
widget_name = "%s_%s" % (name, i)
try:
widget_value = value[i]
except IndexError:
widget_value = None
if id_:
widget_attrs = final_attrs.copy()
widget_attrs['id'] = '%s_%s' % (id_, i)
widget_attrs["id"] = "%s_%s" % (id_, i)
else:
widget_attrs = final_attrs
widget_attrs = self.build_widget_attrs(widget, widget_value, widget_attrs)
widget_context = widget.get_context(widget_name, widget_value, widget_attrs)['widget']
widget_context.update(dict(
lang_code=widget.lang_code,
lang_name=widget.lang_name
))
widget_attrs = self.build_widget_attrs(
widget, widget_value, widget_attrs
)
widget_context = widget.get_context(
widget_name, widget_value, widget_attrs
)["widget"]
widget_context.update(
dict(lang_code=widget.lang_code, lang_name=widget.lang_name)
)
subwidgets.append(widget_context)
context['widget']['subwidgets'] = subwidgets
context["widget"]["subwidgets"] = subwidgets
return context
@staticmethod
def build_widget_attrs(widget, value, attrs):
attrs = dict(attrs) # Copy attrs to avoid modifying the argument.
if (not widget.use_required_attribute(value) or not widget.is_required) \
and 'required' in attrs:
del attrs['required']
if (
not widget.use_required_attribute(value) or not widget.is_required
) and "required" in attrs:
del attrs["required"]
return attrs
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):
template_name = 'localized_fields/admin/widget.html'
template_name = "localized_fields/admin/widget.html"
widget = widgets.AdminTextareaWidget