mirror of
https://github.com/SectorLabs/django-localized-fields.git
synced 2025-10-29 18:18:57 +03:00
Merge branch 'master' of https://github.com/SectorLabs/django-localized-fields into extra-fields
This commit is contained in:
@@ -1,32 +0,0 @@
|
||||
from .forms import LocalizedFieldForm, LocalizedFieldWidget
|
||||
from .fields import (LocalizedAutoSlugField, LocalizedField,
|
||||
LocalizedUniqueSlugField, LocalizedCharField,
|
||||
LocalizedTextField, LocalizedFileField)
|
||||
from .localized_value import LocalizedValue
|
||||
from .mixins import AtomicSlugRetryMixin
|
||||
from .models import LocalizedModel
|
||||
from .util import get_language_codes
|
||||
|
||||
__all__ = [
|
||||
'get_language_codes',
|
||||
'LocalizedField',
|
||||
'LocalizedModel',
|
||||
'LocalizedValue',
|
||||
'LocalizedAutoSlugField',
|
||||
'LocalizedUniqueSlugField',
|
||||
'LocalizedBleachField',
|
||||
'LocalizedCharField',
|
||||
'LocalizedTextField',
|
||||
'LocalizedFileField',
|
||||
'LocalizedFieldWidget',
|
||||
'LocalizedFieldForm',
|
||||
'AtomicSlugRetryMixin'
|
||||
]
|
||||
|
||||
try:
|
||||
from .fields import LocalizedBleachField
|
||||
__all__ += [
|
||||
'LocalizedBleachField'
|
||||
]
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from django.contrib.admin import ModelAdmin
|
||||
|
||||
from . import widgets
|
||||
from .fields import LocalizedField, LocalizedCharField, LocalizedTextField, \
|
||||
LocalizedFileField
|
||||
from . import widgets
|
||||
|
||||
|
||||
|
||||
FORMFIELD_FOR_LOCALIZED_FIELDS_DEFAULTS = {
|
||||
@@ -14,17 +15,22 @@ FORMFIELD_FOR_LOCALIZED_FIELDS_DEFAULTS = {
|
||||
|
||||
|
||||
class LocalizedFieldsAdminMixin(ModelAdmin):
|
||||
"""Mixin for making the fancy widgets work in Django Admin."""
|
||||
|
||||
class Media:
|
||||
css = {
|
||||
'all': (
|
||||
'localized_fields/localized-fields-admin.css',
|
||||
)
|
||||
}
|
||||
|
||||
js = (
|
||||
'localized_fields/localized-fields-admin.js',
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initializes a new instance of :see:LocalizedFieldsAdminMixin."""
|
||||
|
||||
super(LocalizedFieldsAdminMixin, self).__init__(*args, **kwargs)
|
||||
overrides = FORMFIELD_FOR_LOCALIZED_FIELDS_DEFAULTS.copy()
|
||||
overrides.update(self.formfield_overrides)
|
||||
|
||||
65
localized_fields/descriptor.py
Normal file
65
localized_fields/descriptor.py
Normal file
@@ -0,0 +1,65 @@
|
||||
from django.conf import settings
|
||||
from django.utils import six, translation
|
||||
|
||||
|
||||
class LocalizedValueDescriptor:
|
||||
"""
|
||||
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
|
||||
>>> instance = MyModel()
|
||||
>>> instance.value.en = 'English value'
|
||||
|
||||
Assigns a strings to active language key in :see:LocalizedValue on
|
||||
assignment so you can do::
|
||||
|
||||
>>> from django.utils import translation
|
||||
>>> from myapp.models import MyModel
|
||||
|
||||
>>> translation.activate('nl')
|
||||
>>> instance = MyModel()
|
||||
>>> instance.title = 'dutch title'
|
||||
>>> print(instance.title.nl) # prints 'dutch title'
|
||||
"""
|
||||
|
||||
def __init__(self, field):
|
||||
"""Initializes a new instance of :see:LocalizedValueDescriptor."""
|
||||
|
||||
self.field = field
|
||||
|
||||
def __get__(self, instance, cls=None):
|
||||
if instance is None:
|
||||
return self
|
||||
|
||||
# This is slightly complicated, so worth an explanation.
|
||||
# `instance.localizedvalue` needs to ultimately return some instance of
|
||||
# `LocalizedValue`, probably a subclass.
|
||||
|
||||
# The instance dict contains whatever was originally assigned
|
||||
# in __set__.
|
||||
|
||||
if self.field.name in instance.__dict__:
|
||||
value = instance.__dict__[self.field.name]
|
||||
elif instance.pk is not None:
|
||||
instance.refresh_from_db(fields=[self.field.name])
|
||||
value = getattr(instance, self.field.name)
|
||||
else:
|
||||
value = None
|
||||
|
||||
if value is None:
|
||||
attr = self.field.attr_class()
|
||||
instance.__dict__[self.field.name] = attr
|
||||
|
||||
if isinstance(value, dict):
|
||||
attr = self.field.attr_class(value)
|
||||
instance.__dict__[self.field.name] = attr
|
||||
|
||||
return instance.__dict__[self.field.name]
|
||||
|
||||
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
|
||||
else:
|
||||
instance.__dict__[self.field.name] = value
|
||||
25
localized_fields/expressions.py
Normal file
25
localized_fields/expressions.py
Normal file
@@ -0,0 +1,25 @@
|
||||
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."""
|
||||
|
||||
def __init__(self, name: str, lang: str=None):
|
||||
"""Initializes a new instance of :see:LocalizedRef.
|
||||
|
||||
Arguments:
|
||||
name:
|
||||
The field/column to select from.
|
||||
|
||||
lang:
|
||||
The language to get the field/column in.
|
||||
If not specified, the currently active language
|
||||
is used.
|
||||
"""
|
||||
|
||||
language = lang or translation.get_language() or settings.LANGUAGE_CODE
|
||||
super().__init__(name, language)
|
||||
@@ -1,6 +1,6 @@
|
||||
from .localized_field import LocalizedField
|
||||
from .localized_autoslug_field import LocalizedAutoSlugField
|
||||
from .localized_uniqueslug_field import LocalizedUniqueSlugField
|
||||
from .field import LocalizedField
|
||||
from .autoslug_field import LocalizedAutoSlugField
|
||||
from .uniqueslug_field import LocalizedUniqueSlugField
|
||||
from .localized_char_field import LocalizedCharField
|
||||
from .localized_text_field import LocalizedTextField
|
||||
from .localized_file_field import LocalizedFileField
|
||||
@@ -16,7 +16,7 @@ __all__ = [
|
||||
]
|
||||
|
||||
try:
|
||||
from .localized_bleach_field import LocalizedBleachField
|
||||
from .bleach_field import LocalizedBleachField
|
||||
__all__ += [
|
||||
'LocalizedBleachField'
|
||||
]
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
from typing import Callable
|
||||
from typing import Callable, Tuple
|
||||
from datetime import datetime
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.utils.text import slugify
|
||||
|
||||
from .localized_field import LocalizedField
|
||||
from ..localized_value import LocalizedValue
|
||||
from .field import LocalizedField
|
||||
from ..value import LocalizedValue
|
||||
|
||||
|
||||
class LocalizedAutoSlugField(LocalizedField):
|
||||
@@ -69,13 +69,7 @@ class LocalizedAutoSlugField(LocalizedField):
|
||||
|
||||
slugs = LocalizedValue()
|
||||
|
||||
for lang_code, _ in settings.LANGUAGES:
|
||||
value = self._get_populate_from_value(
|
||||
instance,
|
||||
self.populate_from,
|
||||
lang_code
|
||||
)
|
||||
|
||||
for lang_code, value in self._get_populate_values(instance):
|
||||
if not value:
|
||||
continue
|
||||
|
||||
@@ -128,6 +122,30 @@ class LocalizedAutoSlugField(LocalizedField):
|
||||
|
||||
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.
|
||||
|
||||
Arguments:
|
||||
instance:
|
||||
The instance to get the values from.
|
||||
|
||||
Returns:
|
||||
A list of (lang_code, value) tuples.
|
||||
"""
|
||||
|
||||
return [
|
||||
(
|
||||
lang_code,
|
||||
self._get_populate_from_value(
|
||||
instance,
|
||||
self.populate_from,
|
||||
lang_code
|
||||
),
|
||||
)
|
||||
for lang_code, _ in settings.LANGUAGES
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _get_populate_from_value(instance, field_name: str, language: str):
|
||||
"""Gets the value to create a slug from in the specified language.
|
||||
@@ -1,8 +1,9 @@
|
||||
import bleach
|
||||
|
||||
from django.conf import settings
|
||||
from django_bleach.utils import get_bleach_default_options
|
||||
|
||||
from .localized_field import LocalizedField
|
||||
from .field import LocalizedField
|
||||
|
||||
|
||||
class LocalizedBleachField(LocalizedField):
|
||||
@@ -1,70 +1,15 @@
|
||||
import json
|
||||
|
||||
from typing import Union
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.utils import IntegrityError
|
||||
from django.utils import six, translation
|
||||
|
||||
from psqlextra.fields import HStoreField
|
||||
|
||||
from ..forms import LocalizedFieldForm
|
||||
from ..localized_value import LocalizedValue
|
||||
|
||||
|
||||
class LocalizedValueDescriptor(object):
|
||||
"""
|
||||
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
|
||||
>>> instance = MyModel()
|
||||
>>> instance.value.en = 'English value'
|
||||
|
||||
Assigns a strings to active language key in :see:LocalizedValue on
|
||||
assignment so you can do::
|
||||
|
||||
>>> from django.utils import translation
|
||||
>>> from myapp.models import MyModel
|
||||
|
||||
>>> translation.activate('nl')
|
||||
>>> instance = MyModel()
|
||||
>>> instance.title = 'dutch title'
|
||||
>>> print(instance.title.nl) # prints 'dutch title'
|
||||
"""
|
||||
def __init__(self, field):
|
||||
self.field = field
|
||||
|
||||
def __get__(self, instance, cls=None):
|
||||
if instance is None:
|
||||
return self
|
||||
|
||||
# This is slightly complicated, so worth an explanation.
|
||||
# `instance.localizedvalue` needs to ultimately return some instance of
|
||||
# `LocalizedValue`, probably a subclass.
|
||||
|
||||
# The instance dict contains whatever was originally assigned
|
||||
# in __set__.
|
||||
if self.field.name in instance.__dict__:
|
||||
value = instance.__dict__[self.field.name]
|
||||
elif instance.pk is not None:
|
||||
instance.refresh_from_db(fields=[self.field.name])
|
||||
value = getattr(instance, self.field.name)
|
||||
else:
|
||||
value = None
|
||||
|
||||
if value is None:
|
||||
attr = self.field.attr_class()
|
||||
instance.__dict__[self.field.name] = attr
|
||||
|
||||
if isinstance(value, dict):
|
||||
attr = self.field.attr_class(value)
|
||||
instance.__dict__[self.field.name] = attr
|
||||
|
||||
return instance.__dict__[self.field.name]
|
||||
|
||||
def __set__(self, instance, value):
|
||||
if isinstance(value, six.string_types):
|
||||
self.__get__(instance).set(translation.get_language() or
|
||||
settings.LANGUAGE_CODE, value)
|
||||
else:
|
||||
instance.__dict__[self.field.name] = value
|
||||
from ..value import LocalizedValue
|
||||
from ..descriptor import LocalizedValueDescriptor
|
||||
|
||||
|
||||
class LocalizedField(HStoreField):
|
||||
@@ -87,9 +32,18 @@ class LocalizedField(HStoreField):
|
||||
|
||||
super(LocalizedField, self).__init__(*args, **kwargs)
|
||||
|
||||
def contribute_to_class(self, cls, name, **kwargs):
|
||||
super(LocalizedField, self).contribute_to_class(cls, name, **kwargs)
|
||||
setattr(cls, self.name, self.descriptor_class(self))
|
||||
def contribute_to_class(self, model, name, **kwargs):
|
||||
"""Adds this field to the specifed model.
|
||||
|
||||
Arguments:
|
||||
cls:
|
||||
The model to add the field to.
|
||||
|
||||
name:
|
||||
The name of the field to add.
|
||||
"""
|
||||
super(LocalizedField, self).contribute_to_class(model, name, **kwargs)
|
||||
setattr(model, self.name, self.descriptor_class(self))
|
||||
|
||||
@classmethod
|
||||
def from_db_value(cls, value, *_):
|
||||
@@ -112,9 +66,31 @@ class LocalizedField(HStoreField):
|
||||
else:
|
||||
return cls.attr_class()
|
||||
|
||||
# we can get a list if an aggregation expression was used..
|
||||
# if we the expression was flattened when only one key was selected
|
||||
# then we don't wrap each value in a localized value, otherwise we do
|
||||
if isinstance(value, list):
|
||||
result = []
|
||||
for inner_val in value:
|
||||
if isinstance(inner_val, dict):
|
||||
if inner_val is None:
|
||||
result.append(None)
|
||||
else:
|
||||
result.append(cls.attr_class(inner_val))
|
||||
else:
|
||||
result.append(inner_val)
|
||||
|
||||
return result
|
||||
|
||||
# this is for when you select an individual key, it will be string,
|
||||
# not a dictionary, we'll give it to you as a flat value, not as a
|
||||
# localized value instance
|
||||
if not isinstance(value, dict):
|
||||
return value
|
||||
|
||||
return cls.attr_class(value)
|
||||
|
||||
def to_python(self, value: dict) -> LocalizedValue:
|
||||
def to_python(self, value: Union[dict, str, None]) -> LocalizedValue:
|
||||
"""Turns the specified database value into its Python
|
||||
equivalent.
|
||||
|
||||
@@ -128,10 +104,17 @@ class LocalizedField(HStoreField):
|
||||
data extracted from the database.
|
||||
"""
|
||||
|
||||
if not value or not isinstance(value, dict):
|
||||
# first let the base class handle the deserialization, this is in case we
|
||||
# get specified a json string representing a dict
|
||||
try:
|
||||
deserialized_value = super(LocalizedField, self).to_python(value)
|
||||
except json.JSONDecodeError:
|
||||
deserialized_value = value
|
||||
|
||||
if not deserialized_value:
|
||||
return self.attr_class()
|
||||
|
||||
return self.attr_class(value)
|
||||
return self.attr_class(deserialized_value)
|
||||
|
||||
def get_prep_value(self, value: LocalizedValue) -> dict:
|
||||
"""Turns the specified value into something the database
|
||||
@@ -1,6 +1,6 @@
|
||||
from ..forms import LocalizedCharFieldForm
|
||||
from .localized_field import LocalizedField
|
||||
from ..localized_value import LocalizedStringValue
|
||||
from .field import LocalizedField
|
||||
from ..value import LocalizedStringValue
|
||||
|
||||
|
||||
class LocalizedCharField(LocalizedField):
|
||||
|
||||
@@ -8,10 +8,10 @@ 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_fields.fields.field import LocalizedValueDescriptor
|
||||
from localized_fields.value import LocalizedValue
|
||||
|
||||
from ..localized_value import LocalizedFileValue
|
||||
from ..value import LocalizedFileValue
|
||||
from ..forms import LocalizedFileFieldForm
|
||||
|
||||
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
from datetime import datetime
|
||||
|
||||
from django.conf import settings
|
||||
from django import forms
|
||||
from django.utils.text import slugify
|
||||
from django.db import transaction
|
||||
from django.db.utils import IntegrityError
|
||||
|
||||
|
||||
from ..util import get_language_codes
|
||||
from ..localized_value import LocalizedValue
|
||||
from .localized_field import LocalizedField
|
||||
|
||||
|
||||
class LocalizedUniqueSlugField(LocalizedField):
|
||||
"""Automatically provides slugs for a localized field upon saving."
|
||||
|
||||
An improved version of :see:LocalizedAutoSlugField,
|
||||
which adds:
|
||||
|
||||
- Concurrency safety
|
||||
- Improved performance
|
||||
|
||||
When in doubt, use this over :see:LocalizedAutoSlugField.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initializes a new instance of :see:LocalizedUniqueSlugField."""
|
||||
|
||||
kwargs['uniqueness'] = kwargs.pop('uniqueness', get_language_codes())
|
||||
|
||||
self.populate_from = kwargs.pop('populate_from')
|
||||
self.include_time = kwargs.pop('include_time', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def deconstruct(self):
|
||||
"""Deconstructs the field into something the database
|
||||
can store."""
|
||||
|
||||
name, path, args, kwargs = super(
|
||||
LocalizedUniqueSlugField, self).deconstruct()
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
defaults = {
|
||||
'form_class': forms.CharField,
|
||||
'required': False
|
||||
}
|
||||
|
||||
defaults.update(kwargs)
|
||||
|
||||
form_field = super().formfield(**defaults)
|
||||
form_field.widget = forms.HiddenInput()
|
||||
|
||||
return form_field
|
||||
|
||||
def contribute_to_class(self, cls, name, *args, **kwargs):
|
||||
"""Hook that allow us to operate with model class. We overwrite save()
|
||||
method to run retry logic.
|
||||
|
||||
Arguments:
|
||||
cls:
|
||||
Model class.
|
||||
|
||||
name:
|
||||
Name of field in model.
|
||||
"""
|
||||
# apparently in inheritance cases, contribute_to_class is called more
|
||||
# than once, so we have to be careful not to overwrite the original
|
||||
# save method.
|
||||
if not hasattr(cls, '_orig_save'):
|
||||
cls._orig_save = cls.save
|
||||
max_retries = getattr(
|
||||
settings,
|
||||
'LOCALIZED_FIELDS_MAX_RETRIES',
|
||||
100
|
||||
)
|
||||
|
||||
def _new_save(instance, *args_, **kwargs_):
|
||||
retries = 0
|
||||
while True:
|
||||
with transaction.atomic():
|
||||
try:
|
||||
slugs = self.populate_slugs(instance, retries)
|
||||
setattr(instance, name, slugs)
|
||||
instance._orig_save(*args_, **kwargs_)
|
||||
break
|
||||
except IntegrityError as e:
|
||||
if retries >= max_retries:
|
||||
raise e
|
||||
# check to be sure a slug fight caused
|
||||
# the IntegrityError
|
||||
s_e = str(e)
|
||||
if name in s_e and 'unique' in s_e:
|
||||
retries += 1
|
||||
else:
|
||||
raise e
|
||||
|
||||
cls.save = _new_save
|
||||
super().contribute_to_class(cls, name, *args, **kwargs)
|
||||
|
||||
def populate_slugs(self, instance, retries=0):
|
||||
"""Built the slug from populate_from field.
|
||||
|
||||
Arguments:
|
||||
instance:
|
||||
The model that is being saved.
|
||||
|
||||
retries:
|
||||
The value of the current attempt.
|
||||
|
||||
Returns:
|
||||
The localized slug that was generated.
|
||||
"""
|
||||
slugs = LocalizedValue()
|
||||
populates_slugs = getattr(instance, self.populate_from, {})
|
||||
for lang_code, _ in settings.LANGUAGES:
|
||||
|
||||
value = populates_slugs.get(lang_code)
|
||||
|
||||
if not value:
|
||||
continue
|
||||
|
||||
slug = slugify(value, allow_unicode=True)
|
||||
|
||||
# verify whether it's needed to re-generate a slug,
|
||||
# if not, re-use the same slug
|
||||
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('-')]
|
||||
if slug == stripped_slug:
|
||||
slugs.set(lang_code, current_slug)
|
||||
continue
|
||||
|
||||
if self.include_time:
|
||||
slug += '-%d' % datetime.now().microsecond
|
||||
|
||||
if retries > 0:
|
||||
# do not add another - if we already added time
|
||||
if not self.include_time:
|
||||
slug += '-'
|
||||
slug += '%d' % retries
|
||||
|
||||
slugs.set(lang_code, slug)
|
||||
return slugs
|
||||
104
localized_fields/fields/uniqueslug_field.py
Normal file
104
localized_fields/fields/uniqueslug_field.py
Normal file
@@ -0,0 +1,104 @@
|
||||
from datetime import datetime
|
||||
|
||||
from django.utils.text import slugify
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
from .autoslug_field import LocalizedAutoSlugField
|
||||
from ..util import get_language_codes
|
||||
from ..mixins import AtomicSlugRetryMixin
|
||||
from ..value import LocalizedValue
|
||||
|
||||
|
||||
class LocalizedUniqueSlugField(LocalizedAutoSlugField):
|
||||
"""Automatically provides slugs for a localized
|
||||
field upon saving."
|
||||
|
||||
An improved version of :see:LocalizedAutoSlugField,
|
||||
which adds:
|
||||
|
||||
- Concurrency safety
|
||||
- Improved performance
|
||||
|
||||
When in doubt, use this over :see:LocalizedAutoSlugField.
|
||||
Inherit from :see:AtomicSlugRetryMixin in your model to
|
||||
make this field work properly.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initializes a new instance of :see:LocalizedUniqueSlugField."""
|
||||
|
||||
kwargs['uniqueness'] = kwargs.pop('uniqueness', get_language_codes())
|
||||
|
||||
super(LocalizedUniqueSlugField, self).__init__(
|
||||
*args,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
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."""
|
||||
|
||||
name, path, args, kwargs = super(
|
||||
LocalizedUniqueSlugField, self).deconstruct()
|
||||
|
||||
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.
|
||||
|
||||
Arguments:
|
||||
instance:
|
||||
The model that is being saved.
|
||||
|
||||
add:
|
||||
Indicates whether this is a new entry
|
||||
to the database or an update.
|
||||
|
||||
Returns:
|
||||
The localized slug that was generated.
|
||||
"""
|
||||
|
||||
if not isinstance(instance, AtomicSlugRetryMixin):
|
||||
raise ImproperlyConfigured((
|
||||
'Model \'%s\' does not inherit from AtomicSlugRetryMixin. '
|
||||
'Without this, the LocalizedUniqueSlugField will not work.'
|
||||
) % type(instance).__name__)
|
||||
|
||||
slugs = LocalizedValue()
|
||||
|
||||
for lang_code, value in self._get_populate_values(instance):
|
||||
if not value:
|
||||
continue
|
||||
|
||||
slug = slugify(value, allow_unicode=True)
|
||||
|
||||
# verify whether it's needed to re-generate a slug,
|
||||
# if not, re-use the same slug
|
||||
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('-')]
|
||||
if slug == stripped_slug:
|
||||
slugs.set(lang_code, current_slug)
|
||||
continue
|
||||
|
||||
if self.include_time:
|
||||
slug += '-%d' % datetime.now().microsecond
|
||||
|
||||
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
|
||||
|
||||
slugs.set(lang_code, slug)
|
||||
|
||||
setattr(instance, self.name, slugs)
|
||||
return slugs
|
||||
@@ -5,7 +5,7 @@ from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.forms.widgets import FILE_INPUT_CONTRADICTION
|
||||
|
||||
from .localized_value import LocalizedValue, LocalizedStringValue, \
|
||||
from .value import LocalizedValue, LocalizedStringValue, \
|
||||
LocalizedFileValue
|
||||
from .widgets import LocalizedFieldWidget, LocalizedCharFieldWidget, \
|
||||
LocalizedFileWidget
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
"""This module is unused, but should be contributed to Django."""
|
||||
|
||||
from typing import List
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
class HStoreIndex(models.Index):
|
||||
"""Allows creating a index on a specific HStore index.
|
||||
|
||||
Note: pieces of code in this class have been copied
|
||||
from the base class. There was no way around this."""
|
||||
|
||||
def __init__(self, field: str, keys: List[str], unique: bool=False,
|
||||
name: str=''):
|
||||
"""Initializes a new instance of :see:HStoreIndex.
|
||||
|
||||
Arguments:
|
||||
field:
|
||||
Name of the hstore field for
|
||||
which's keys to create a index for.
|
||||
|
||||
keys:
|
||||
The name of the hstore keys to
|
||||
create the index on.
|
||||
|
||||
unique:
|
||||
Whether this index should
|
||||
be marked as UNIQUE.
|
||||
|
||||
name:
|
||||
The name of the index. If left
|
||||
empty, one will be generated.
|
||||
"""
|
||||
|
||||
self.field = field
|
||||
self.keys = keys
|
||||
self.unique = unique
|
||||
|
||||
# this will eventually set self.name
|
||||
super(HStoreIndex, self).__init__(
|
||||
fields=[field],
|
||||
name=name
|
||||
)
|
||||
|
||||
def get_sql_create_template_values(self, model, schema_editor, using):
|
||||
"""Gets the values for the SQL template.
|
||||
|
||||
Arguments:
|
||||
model:
|
||||
The model this index applies to.
|
||||
|
||||
schema_editor:
|
||||
The schema editor to modify the schema.
|
||||
|
||||
using:
|
||||
Optional: "USING" statement.
|
||||
|
||||
Returns:
|
||||
Dictionary of keys to pass into the SQL template.
|
||||
"""
|
||||
|
||||
fields = [model._meta.get_field(field_name) for field_name, order in self.fields_orders]
|
||||
tablespace_sql = schema_editor._get_index_tablespace_sql(model, fields)
|
||||
quote_name = schema_editor.quote_name
|
||||
|
||||
columns = [
|
||||
'(%s->\'%s\')' % (self.field, key)
|
||||
for key in self.keys
|
||||
]
|
||||
|
||||
return {
|
||||
'table': quote_name(model._meta.db_table),
|
||||
'name': quote_name(self.name),
|
||||
'columns': ', '.join(columns),
|
||||
'using': using,
|
||||
'extra': tablespace_sql,
|
||||
}
|
||||
|
||||
def create_sql(self, model, schema_editor, using=''):
|
||||
"""Gets the SQL to execute when creating the index.
|
||||
|
||||
Arguments:
|
||||
model:
|
||||
The model this index applies to.
|
||||
|
||||
schema_editor:
|
||||
The schema editor to modify the schema.
|
||||
|
||||
using:
|
||||
Optional: "USING" statement.
|
||||
|
||||
Returns:
|
||||
SQL string to execute to create this index.
|
||||
"""
|
||||
|
||||
sql_create_index = schema_editor.sql_create_index
|
||||
if self.unique:
|
||||
sql_create_index = sql_create_index.replace('CREATE', 'CREATE UNIQUE')
|
||||
sql_parameters = self.get_sql_create_template_values(model, schema_editor, using)
|
||||
return sql_create_index % sql_parameters
|
||||
|
||||
def remove_sql(self, model, schema_editor):
|
||||
"""Gets the SQL to execute to remove this index.
|
||||
|
||||
Arguments:
|
||||
model:
|
||||
The model this index applies to.
|
||||
|
||||
schema_editor:
|
||||
The schema editor to modify the schema.
|
||||
|
||||
Returns:
|
||||
SQL string to execute to remove this index.
|
||||
"""
|
||||
quote_name = schema_editor.quote_name
|
||||
return schema_editor.sql_delete_index % {
|
||||
'table': quote_name(model._meta.db_table),
|
||||
'name': quote_name(self.name),
|
||||
}
|
||||
|
||||
def deconstruct(self):
|
||||
"""Gets the values to pass to :see:__init__ when
|
||||
re-creating this object."""
|
||||
|
||||
path = '%s.%s' % (self.__class__.__module__, self.__class__.__name__)
|
||||
return (path, (), {
|
||||
'field': self.field,
|
||||
'keys': self.keys,
|
||||
'unique': self.unique,
|
||||
'name': self.name
|
||||
})
|
||||
@@ -1,17 +1,38 @@
|
||||
from django.core.checks import Warning
|
||||
from django.db import transaction
|
||||
from django.conf import settings
|
||||
from django.db.utils import IntegrityError
|
||||
|
||||
|
||||
class AtomicSlugRetryMixin:
|
||||
"""A Mixin keeped for backwards compatibility"""
|
||||
"""Makes :see:LocalizedUniqueSlugField work by retrying upon
|
||||
violation of the UNIQUE constraint."""
|
||||
|
||||
@classmethod
|
||||
def check(cls, **kwargs):
|
||||
errors = super().check(**kwargs)
|
||||
errors.append(
|
||||
Warning(
|
||||
'localized_fields.AtomicSlugRetryMixin is deprecated',
|
||||
hint='There is no need to use '
|
||||
'localized_fields.AtomicSlugRetryMixin',
|
||||
obj=cls
|
||||
)
|
||||
def save(self, *args, **kwargs):
|
||||
"""Saves this model instance to the database."""
|
||||
|
||||
max_retries = getattr(
|
||||
settings,
|
||||
'LOCALIZED_FIELDS_MAX_RETRIES',
|
||||
100
|
||||
)
|
||||
return errors
|
||||
|
||||
if not hasattr(self, 'retries'):
|
||||
self.retries = 0
|
||||
|
||||
with transaction.atomic():
|
||||
try:
|
||||
return super().save(*args, **kwargs)
|
||||
except IntegrityError as ex:
|
||||
# this is as retarded as it looks, there's no
|
||||
# way we can put the retry logic inside the slug
|
||||
# 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):
|
||||
raise ex
|
||||
|
||||
if self.retries >= max_retries:
|
||||
raise ex
|
||||
|
||||
self.retries += 1
|
||||
return self.save()
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
from django.db import models
|
||||
from django.core.checks import Warning
|
||||
from psqlextra.models import PostgresModel
|
||||
|
||||
from .mixins import AtomicSlugRetryMixin
|
||||
|
||||
|
||||
class LocalizedModel(models.Model):
|
||||
"""A model keeped for backwards compatibility"""
|
||||
class LocalizedModel(AtomicSlugRetryMixin, PostgresModel):
|
||||
"""Turns a model into a model that contains LocalizedField's.
|
||||
|
||||
@classmethod
|
||||
def check(cls, **kwargs):
|
||||
errors = super().check(**kwargs)
|
||||
errors.append(
|
||||
Warning(
|
||||
'localized_fields.LocalizedModel is deprecated',
|
||||
hint='There is no need to use localized_fields.LocalizedModel',
|
||||
obj=cls
|
||||
)
|
||||
)
|
||||
return errors
|
||||
For basic localisation functionality, it isn't needed to inherit
|
||||
from LocalizedModel. However, for certain features, this is required.
|
||||
|
||||
It is definitely needed for :see:LocalizedUniqueSlugField, unless you
|
||||
manually inherit from AtomicSlugRetryMixin."""
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import collections
|
||||
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import translation
|
||||
|
||||
@@ -16,17 +19,10 @@ class LocalizedValue(dict):
|
||||
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)
|
||||
super().__init__({})
|
||||
self._interpret_value(keys)
|
||||
|
||||
if isinstance(keys, str):
|
||||
setattr(self, settings.LANGUAGE_CODE, 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.
|
||||
|
||||
@@ -41,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.
|
||||
@@ -69,6 +65,40 @@ class LocalizedValue(dict):
|
||||
path = 'localized_fields.localized_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.
|
||||
|
||||
If string:
|
||||
Assumes it's the default language.
|
||||
|
||||
If dict:
|
||||
Each key is a language and the value a string
|
||||
in that language.
|
||||
|
||||
If list:
|
||||
Recurse into to apply rules above.
|
||||
|
||||
Arguments:
|
||||
value:
|
||||
The value to interpret.
|
||||
"""
|
||||
|
||||
for lang_code, _ in settings.LANGUAGES:
|
||||
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, self.default_value)
|
||||
self.set(lang_code, lang_value)
|
||||
|
||||
elif isinstance(value, collections.Iterable):
|
||||
for val in value:
|
||||
self._interpret_value(val)
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Gets the value in the current language, or falls
|
||||
back to the primary language if there's no value
|
||||
@@ -5,7 +5,7 @@ from django import forms
|
||||
from django.contrib.admin import widgets
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
from .localized_value import LocalizedValue
|
||||
from .value import LocalizedValue
|
||||
|
||||
|
||||
class LocalizedFieldWidget(forms.MultiWidget):
|
||||
@@ -15,12 +15,12 @@ class LocalizedFieldWidget(forms.MultiWidget):
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initializes a new instance of :see:LocalizedFieldWidget."""
|
||||
|
||||
widgets = []
|
||||
initial_widgets = [
|
||||
self.widget
|
||||
for _ in settings.LANGUAGES
|
||||
]
|
||||
|
||||
for _ in settings.LANGUAGES:
|
||||
widgets.append(self.widget)
|
||||
|
||||
super(LocalizedFieldWidget, self).__init__(widgets, *args, **kwargs)
|
||||
super().__init__(initial_widgets, *args, **kwargs)
|
||||
|
||||
def decompress(self, value: LocalizedValue) -> List[str]:
|
||||
"""Decompresses the specified value so
|
||||
@@ -36,7 +36,6 @@ class LocalizedFieldWidget(forms.MultiWidget):
|
||||
"""
|
||||
|
||||
result = []
|
||||
|
||||
for lang_code, _ in settings.LANGUAGES:
|
||||
if value:
|
||||
result.append(value.get(lang_code))
|
||||
@@ -88,7 +87,8 @@ class AdminLocalizedFieldWidget(LocalizedFieldWidget):
|
||||
}
|
||||
return render_to_string(self.template, context)
|
||||
|
||||
def build_widget_attrs(self, widget, value, attrs):
|
||||
@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:
|
||||
|
||||
Reference in New Issue
Block a user