no need inheritance from LocalizedModel anymore. Introduction of LocalizedValueDescriptor

This commit is contained in:
seroy 2017-03-13 00:50:34 +03:00
parent 3951266747
commit 340dde18cd
6 changed files with 93 additions and 63 deletions

View File

@ -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()

View File

@ -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'
] ]

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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