From 340dde18cde72fd7169b620a1032e7083171d8df Mon Sep 17 00:00:00 2001 From: seroy Date: Mon, 13 Mar 2017 00:50:34 +0300 Subject: [PATCH] no need inheritance from LocalizedModel anymore. Introduction of LocalizedValueDescriptor --- README.rst | 32 ++++----- localized_fields/__init__.py | 2 - localized_fields/fields/localized_field.py | 80 ++++++++++++++++++++-- localized_fields/models.py | 33 --------- tests/fake_model.py | 5 +- tests/test_localized_field.py | 4 +- 6 files changed, 93 insertions(+), 63 deletions(-) diff --git a/README.rst b/README.rst index 577b359..0bd2cce 100644 --- a/README.rst +++ b/README.rst @@ -62,15 +62,14 @@ Usage Preparation ^^^^^^^^^^^ -Inherit your model from ``LocalizedModel`` and declare fields on your model as ``LocalizedField``: +Declare fields on your model as ``LocalizedField``: .. code-block:: python - from localized_fields.models import LocalizedModel from localized_fields.fields import LocalizedField - class MyModel(LocalizedModel): + class MyModel(models.Model): 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: @@ -136,14 +135,14 @@ At the moment, it is not possible to select two languages to be marked as requir .. code-block:: python - class MyModel(LocalizedModel): + class MyModel(models.Model): title = LocalizedField(required=True) * Make all languages optional: .. code-block:: python - class MyModel(LocalizedModel): + class MyModel(models.Model): title = LocalizedField(null=True) **Uniqueness** @@ -154,7 +153,7 @@ By default the values stored in a ``LocalizedField`` are *not unique*. You can e .. code-block:: python - class MyModel(LocalizedModel): + class MyModel(models.Model): title = LocalizedField(uniqueness=['en', 'ro']) * 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 - class MyModel(LocalizedModel): + class MyModel(models.Model): title = LocalizedField(uniqueness=get_language_codes()) * Enforce uniqueness for one ore more languages **together** (similar to Django's ``unique_together``): .. code-block:: python - class MyModel(LocalizedModel): + class MyModel(models.Model): title = LocalizedField(uniqueness=[('en', 'ro')]) * 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 - class MyModel(LocalizedModel): + class MyModel(models.Model): title = LocalizedField(uniqueness=[(*get_language_codes())]) @@ -193,12 +192,11 @@ Besides ``LocalizedField``, there's also: .. code-block:: python - from localized_fields import (LocalizedModel, - AtomicSlugRetryMixin, + from localized_fields import (AtomicSlugRetryMixin, LocalizedField, LocalizedUniqueSlugField) - class MyModel(AtomicSlugRetryMixin, LocalizedModel): + class MyModel(AtomicSlugRetryMixin, models.Model): title = LocalizedField() slug = LocalizedUniqueSlugField(populate_from='title') @@ -218,11 +216,10 @@ Besides ``LocalizedField``, there's also: .. code-block:: python - from localized_fields import (LocalizedModel, - LocalizedField, + from localized_fields import (LocalizedField, LocalizedUniqueSlugField) - class MyModel(LocalizedModel): + class MyModel(models.Model): title = LocalizedField() slug = LocalizedAutoSlugField(populate_from='title') @@ -236,11 +233,10 @@ Besides ``LocalizedField``, there's also: .. code-block:: python - from localized_fields import (LocalizedModel, - LocalizedField, + from localized_fields import (LocalizedField, LocalizedBleachField) - class MyModel(LocalizedModel): + class MyModel(models.Model): title = LocalizedField() description = LocalizedBleachField() diff --git a/localized_fields/__init__.py b/localized_fields/__init__.py index 1385f00..cf3b547 100644 --- a/localized_fields/__init__.py +++ b/localized_fields/__init__.py @@ -3,7 +3,6 @@ from .forms import LocalizedFieldForm, LocalizedFieldWidget from .fields import (LocalizedField, LocalizedBleachField, LocalizedAutoSlugField, LocalizedUniqueSlugField) from .mixins import AtomicSlugRetryMixin -from .models import LocalizedModel from .localized_value import LocalizedValue __all__ = [ @@ -15,6 +14,5 @@ __all__ = [ 'LocalizedBleachField', 'LocalizedFieldWidget', 'LocalizedFieldForm', - 'LocalizedModel', 'AtomicSlugRetryMixin' ] diff --git a/localized_fields/fields/localized_field.py b/localized_fields/fields/localized_field.py index 34ae876..a957cd7 100644 --- a/localized_fields/fields/localized_field.py +++ b/localized_fields/fields/localized_field.py @@ -1,5 +1,6 @@ from django.conf import settings from django.db.utils import IntegrityError +from django.utils import six, translation from localized_fields import LocalizedFieldForm from psqlextra.fields import HStoreField @@ -7,6 +8,63 @@ from psqlextra.fields import HStoreField 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): """A field that has the same value in multiple languages. @@ -15,13 +73,23 @@ class LocalizedField(HStoreField): 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): """Initializes a new instance of :see:LocalizedField.""" super(LocalizedField, self).__init__(*args, **kwargs) - @staticmethod - def from_db_value(value, *_): + 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 from_db_value(self, value, *_): """Turns the specified database value into its Python equivalent. @@ -36,9 +104,9 @@ class LocalizedField(HStoreField): """ if not value: - return LocalizedValue() + return self.attr_class() - return LocalizedValue(value) + return self.attr_class(value) def to_python(self, value: dict) -> LocalizedValue: """Turns the specified database value into its Python @@ -55,9 +123,9 @@ class LocalizedField(HStoreField): """ 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: """Turns the specified value into something the database diff --git a/localized_fields/models.py b/localized_fields/models.py index 2f681f8..8b13789 100644 --- a/localized_fields/models.py +++ b/localized_fields/models.py @@ -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) diff --git a/tests/fake_model.py b/tests/fake_model.py index 128d77e..b5fcc21 100644 --- a/tests/fake_model.py +++ b/tests/fake_model.py @@ -1,8 +1,9 @@ from django.db import connection, migrations +from django.db import models from django.db.migrations.executor import MigrationExecutor 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): @@ -14,7 +15,7 @@ def define_fake_model(name='TestModel', fields=None): if fields: attributes.update(fields) - model = type(name, (AtomicSlugRetryMixin,LocalizedModel,), attributes) + model = type(name, (AtomicSlugRetryMixin, models.Model), attributes) return model diff --git a/tests/test_localized_field.py b/tests/test_localized_field.py index f557422..ce2e0c5 100644 --- a/tests/test_localized_field.py +++ b/tests/test_localized_field.py @@ -172,7 +172,7 @@ class LocalizedFieldTestCase(TestCase): produces the expected :see:LocalizedValue.""" 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: 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 correctly handles None values.""" - localized_value = LocalizedField.from_db_value(None) + localized_value = LocalizedField().from_db_value(None) for lang_code, _ in settings.LANGUAGES: assert localized_value.get(lang_code) is None