mirror of
https://github.com/SectorLabs/django-localized-fields.git
synced 2025-04-25 03:32:55 +03:00
no need inheritance from LocalizedModel anymore. Introduction of LocalizedValueDescriptor
This commit is contained in:
parent
3951266747
commit
340dde18cd
32
README.rst
32
README.rst
@ -62,15 +62,14 @@ Usage
|
|||||||
|
|
||||||
Preparation
|
Preparation
|
||||||
^^^^^^^^^^^
|
^^^^^^^^^^^
|
||||||
Inherit your model from ``LocalizedModel`` and declare fields on your model as ``LocalizedField``:
|
Declare fields on your model as ``LocalizedField``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from localized_fields.models import LocalizedModel
|
|
||||||
from localized_fields.fields import LocalizedField
|
from localized_fields.fields import LocalizedField
|
||||||
|
|
||||||
|
|
||||||
class MyModel(LocalizedModel):
|
class MyModel(models.Model):
|
||||||
title = LocalizedField()
|
title = LocalizedField()
|
||||||
|
|
||||||
``django-localized-fields`` integrates with Django's i18n system, in order for certain languages to be available you have to correctly configure the ``LANGUAGES`` and ``LANGUAGE_CODE`` settings:
|
``django-localized-fields`` integrates with Django's i18n system, in order for certain languages to be available you have to correctly configure the ``LANGUAGES`` and ``LANGUAGE_CODE`` settings:
|
||||||
@ -136,14 +135,14 @@ At the moment, it is not possible to select two languages to be marked as requir
|
|||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
class MyModel(LocalizedModel):
|
class MyModel(models.Model):
|
||||||
title = LocalizedField(required=True)
|
title = LocalizedField(required=True)
|
||||||
|
|
||||||
* Make all languages optional:
|
* Make all languages optional:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
class MyModel(LocalizedModel):
|
class MyModel(models.Model):
|
||||||
title = LocalizedField(null=True)
|
title = LocalizedField(null=True)
|
||||||
|
|
||||||
**Uniqueness**
|
**Uniqueness**
|
||||||
@ -154,7 +153,7 @@ By default the values stored in a ``LocalizedField`` are *not unique*. You can e
|
|||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
class MyModel(LocalizedModel):
|
class MyModel(models.Model):
|
||||||
title = LocalizedField(uniqueness=['en', 'ro'])
|
title = LocalizedField(uniqueness=['en', 'ro'])
|
||||||
|
|
||||||
* Enforce uniqueness for **all** languages:
|
* Enforce uniqueness for **all** languages:
|
||||||
@ -163,14 +162,14 @@ By default the values stored in a ``LocalizedField`` are *not unique*. You can e
|
|||||||
|
|
||||||
from localized_fields import get_language_codes
|
from localized_fields import get_language_codes
|
||||||
|
|
||||||
class MyModel(LocalizedModel):
|
class MyModel(models.Model):
|
||||||
title = LocalizedField(uniqueness=get_language_codes())
|
title = LocalizedField(uniqueness=get_language_codes())
|
||||||
|
|
||||||
* Enforce uniqueness for one ore more languages **together** (similar to Django's ``unique_together``):
|
* Enforce uniqueness for one ore more languages **together** (similar to Django's ``unique_together``):
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
class MyModel(LocalizedModel):
|
class MyModel(models.Model):
|
||||||
title = LocalizedField(uniqueness=[('en', 'ro')])
|
title = LocalizedField(uniqueness=[('en', 'ro')])
|
||||||
|
|
||||||
* Enforce uniqueness for **all** languages **together**:
|
* Enforce uniqueness for **all** languages **together**:
|
||||||
@ -179,7 +178,7 @@ By default the values stored in a ``LocalizedField`` are *not unique*. You can e
|
|||||||
|
|
||||||
from localized_fields import get_language_codes
|
from localized_fields import get_language_codes
|
||||||
|
|
||||||
class MyModel(LocalizedModel):
|
class MyModel(models.Model):
|
||||||
title = LocalizedField(uniqueness=[(*get_language_codes())])
|
title = LocalizedField(uniqueness=[(*get_language_codes())])
|
||||||
|
|
||||||
|
|
||||||
@ -193,12 +192,11 @@ Besides ``LocalizedField``, there's also:
|
|||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from localized_fields import (LocalizedModel,
|
from localized_fields import (AtomicSlugRetryMixin,
|
||||||
AtomicSlugRetryMixin,
|
|
||||||
LocalizedField,
|
LocalizedField,
|
||||||
LocalizedUniqueSlugField)
|
LocalizedUniqueSlugField)
|
||||||
|
|
||||||
class MyModel(AtomicSlugRetryMixin, LocalizedModel):
|
class MyModel(AtomicSlugRetryMixin, models.Model):
|
||||||
title = LocalizedField()
|
title = LocalizedField()
|
||||||
slug = LocalizedUniqueSlugField(populate_from='title')
|
slug = LocalizedUniqueSlugField(populate_from='title')
|
||||||
|
|
||||||
@ -218,11 +216,10 @@ Besides ``LocalizedField``, there's also:
|
|||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from localized_fields import (LocalizedModel,
|
from localized_fields import (LocalizedField,
|
||||||
LocalizedField,
|
|
||||||
LocalizedUniqueSlugField)
|
LocalizedUniqueSlugField)
|
||||||
|
|
||||||
class MyModel(LocalizedModel):
|
class MyModel(models.Model):
|
||||||
title = LocalizedField()
|
title = LocalizedField()
|
||||||
slug = LocalizedAutoSlugField(populate_from='title')
|
slug = LocalizedAutoSlugField(populate_from='title')
|
||||||
|
|
||||||
@ -236,11 +233,10 @@ Besides ``LocalizedField``, there's also:
|
|||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from localized_fields import (LocalizedModel,
|
from localized_fields import (LocalizedField,
|
||||||
LocalizedField,
|
|
||||||
LocalizedBleachField)
|
LocalizedBleachField)
|
||||||
|
|
||||||
class MyModel(LocalizedModel):
|
class MyModel(models.Model):
|
||||||
title = LocalizedField()
|
title = LocalizedField()
|
||||||
description = LocalizedBleachField()
|
description = LocalizedBleachField()
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@ from .forms import LocalizedFieldForm, LocalizedFieldWidget
|
|||||||
from .fields import (LocalizedField, LocalizedBleachField,
|
from .fields import (LocalizedField, LocalizedBleachField,
|
||||||
LocalizedAutoSlugField, LocalizedUniqueSlugField)
|
LocalizedAutoSlugField, LocalizedUniqueSlugField)
|
||||||
from .mixins import AtomicSlugRetryMixin
|
from .mixins import AtomicSlugRetryMixin
|
||||||
from .models import LocalizedModel
|
|
||||||
from .localized_value import LocalizedValue
|
from .localized_value import LocalizedValue
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@ -15,6 +14,5 @@ __all__ = [
|
|||||||
'LocalizedBleachField',
|
'LocalizedBleachField',
|
||||||
'LocalizedFieldWidget',
|
'LocalizedFieldWidget',
|
||||||
'LocalizedFieldForm',
|
'LocalizedFieldForm',
|
||||||
'LocalizedModel',
|
|
||||||
'AtomicSlugRetryMixin'
|
'AtomicSlugRetryMixin'
|
||||||
]
|
]
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
|
from django.utils import six, translation
|
||||||
|
|
||||||
from localized_fields import LocalizedFieldForm
|
from localized_fields import LocalizedFieldForm
|
||||||
from psqlextra.fields import HStoreField
|
from psqlextra.fields import HStoreField
|
||||||
@ -7,6 +8,63 @@ from psqlextra.fields import HStoreField
|
|||||||
from ..localized_value import LocalizedValue
|
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]
|
||||||
|
else:
|
||||||
|
instance.refresh_from_db(fields=[self.field.name])
|
||||||
|
value = getattr(instance, self.field.name)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
class LocalizedField(HStoreField):
|
class LocalizedField(HStoreField):
|
||||||
"""A field that has the same value in multiple languages.
|
"""A field that has the same value in multiple languages.
|
||||||
|
|
||||||
@ -15,13 +73,23 @@ class LocalizedField(HStoreField):
|
|||||||
|
|
||||||
Meta = None
|
Meta = None
|
||||||
|
|
||||||
|
# The class to wrap instance attributes in. Accessing to field attribute in
|
||||||
|
# model instance will always return an instance of attr_class.
|
||||||
|
attr_class = LocalizedValue
|
||||||
|
|
||||||
|
# The descriptor to use for accessing the attribute off of the class.
|
||||||
|
descriptor_class = LocalizedValueDescriptor
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Initializes a new instance of :see:LocalizedField."""
|
"""Initializes a new instance of :see:LocalizedField."""
|
||||||
|
|
||||||
super(LocalizedField, self).__init__(*args, **kwargs)
|
super(LocalizedField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
@staticmethod
|
def contribute_to_class(self, cls, name, **kwargs):
|
||||||
def from_db_value(value, *_):
|
super(LocalizedField, self).contribute_to_class(cls, name, **kwargs)
|
||||||
|
setattr(cls, self.name, self.descriptor_class(self))
|
||||||
|
|
||||||
|
def from_db_value(self, value, *_):
|
||||||
"""Turns the specified database value into its Python
|
"""Turns the specified database value into its Python
|
||||||
equivalent.
|
equivalent.
|
||||||
|
|
||||||
@ -36,9 +104,9 @@ class LocalizedField(HStoreField):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if not value:
|
if not value:
|
||||||
return LocalizedValue()
|
return self.attr_class()
|
||||||
|
|
||||||
return LocalizedValue(value)
|
return self.attr_class(value)
|
||||||
|
|
||||||
def to_python(self, value: dict) -> LocalizedValue:
|
def to_python(self, value: dict) -> LocalizedValue:
|
||||||
"""Turns the specified database value into its Python
|
"""Turns the specified database value into its Python
|
||||||
@ -55,9 +123,9 @@ class LocalizedField(HStoreField):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if not value or not isinstance(value, dict):
|
if not value or not isinstance(value, dict):
|
||||||
return LocalizedValue()
|
return self.attr_class()
|
||||||
|
|
||||||
return LocalizedValue(value)
|
return self.attr_class(value)
|
||||||
|
|
||||||
def get_prep_value(self, value: LocalizedValue) -> dict:
|
def get_prep_value(self, value: LocalizedValue) -> dict:
|
||||||
"""Turns the specified value into something the database
|
"""Turns the specified value into something the database
|
||||||
|
@ -1,34 +1 @@
|
|||||||
from psqlextra.models import PostgresModel
|
|
||||||
|
|
||||||
from .fields import LocalizedField
|
|
||||||
from .localized_value import LocalizedValue
|
|
||||||
|
|
||||||
|
|
||||||
class LocalizedModel(PostgresModel):
|
|
||||||
"""A model that contains localized fields."""
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
abstract = True
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
"""Initializes a new instance of :see:LocalizedModel.
|
|
||||||
|
|
||||||
Here we set all the fields that are of :see:LocalizedField
|
|
||||||
to an instance of :see:LocalizedValue in case they are none
|
|
||||||
so that the user doesn't explicitely have to do so."""
|
|
||||||
|
|
||||||
super(LocalizedModel, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
for field in self._meta.get_fields():
|
|
||||||
if not isinstance(field, LocalizedField):
|
|
||||||
continue
|
|
||||||
|
|
||||||
value = getattr(self, field.name, None)
|
|
||||||
|
|
||||||
if not isinstance(value, LocalizedValue):
|
|
||||||
if isinstance(value, dict):
|
|
||||||
value = LocalizedValue(value)
|
|
||||||
else:
|
|
||||||
value = LocalizedValue()
|
|
||||||
|
|
||||||
setattr(self, field.name, value)
|
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
from django.db import connection, migrations
|
from django.db import connection, migrations
|
||||||
|
from django.db import models
|
||||||
from django.db.migrations.executor import MigrationExecutor
|
from django.db.migrations.executor import MigrationExecutor
|
||||||
from django.contrib.postgres.operations import HStoreExtension
|
from django.contrib.postgres.operations import HStoreExtension
|
||||||
|
|
||||||
from localized_fields import LocalizedModel, AtomicSlugRetryMixin
|
from localized_fields import AtomicSlugRetryMixin
|
||||||
|
|
||||||
|
|
||||||
def define_fake_model(name='TestModel', fields=None):
|
def define_fake_model(name='TestModel', fields=None):
|
||||||
@ -14,7 +15,7 @@ def define_fake_model(name='TestModel', fields=None):
|
|||||||
|
|
||||||
if fields:
|
if fields:
|
||||||
attributes.update(fields)
|
attributes.update(fields)
|
||||||
model = type(name, (AtomicSlugRetryMixin,LocalizedModel,), attributes)
|
model = type(name, (AtomicSlugRetryMixin, models.Model), attributes)
|
||||||
|
|
||||||
return model
|
return model
|
||||||
|
|
||||||
|
@ -172,7 +172,7 @@ class LocalizedFieldTestCase(TestCase):
|
|||||||
produces the expected :see:LocalizedValue."""
|
produces the expected :see:LocalizedValue."""
|
||||||
|
|
||||||
input_data = get_init_values()
|
input_data = get_init_values()
|
||||||
localized_value = LocalizedField.from_db_value(input_data)
|
localized_value = LocalizedField().from_db_value(input_data)
|
||||||
|
|
||||||
for lang_code, _ in settings.LANGUAGES:
|
for lang_code, _ in settings.LANGUAGES:
|
||||||
assert getattr(localized_value, lang_code) == input_data[lang_code]
|
assert getattr(localized_value, lang_code) == input_data[lang_code]
|
||||||
@ -182,7 +182,7 @@ class LocalizedFieldTestCase(TestCase):
|
|||||||
"""Tests whether the :see:from_db_valuei function
|
"""Tests whether the :see:from_db_valuei function
|
||||||
correctly handles None values."""
|
correctly handles None values."""
|
||||||
|
|
||||||
localized_value = LocalizedField.from_db_value(None)
|
localized_value = LocalizedField().from_db_value(None)
|
||||||
|
|
||||||
for lang_code, _ in settings.LANGUAGES:
|
for lang_code, _ in settings.LANGUAGES:
|
||||||
assert localized_value.get(lang_code) is None
|
assert localized_value.get(lang_code) is None
|
||||||
|
Loading…
x
Reference in New Issue
Block a user