Merge remote-tracking branch 'beer/model_inheritance_free'

# Conflicts:
#	localized_fields/fields/localized_field.py
#	localized_fields/models.py
#	tests/fake_model.py
This commit is contained in:
Swen Kooij 2017-04-03 14:49:27 +03:00
commit 8b08e5a467
5 changed files with 114 additions and 61 deletions

View File

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

View File

@ -1,12 +1,72 @@
from django.conf import settings
from django.db.utils import IntegrityError
from django.utils import six, translation
from psqlextra.fields import HStoreField
from localized_fields import LocalizedFieldForm
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
class LocalizedField(HStoreField):
"""A field that has the same value in multiple languages.
@ -15,13 +75,24 @@ 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))
@classmethod
def from_db_value(cls, value, *_):
"""Turns the specified database value into its Python
equivalent.
@ -39,12 +110,11 @@ class LocalizedField(HStoreField):
if getattr(settings, 'LOCALIZED_FIELDS_EXPERIMENTAL', False):
return None
else:
return LocalizedValue()
return cls.attr_class()
return LocalizedValue(value)
return cls.attr_class(value)
@staticmethod
def to_python(value: dict) -> LocalizedValue:
def to_python(self, value: dict) -> LocalizedValue:
"""Turns the specified database value into its Python
equivalent.
@ -59,9 +129,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

View File

@ -1,34 +1,21 @@
from psqlextra.models import PostgresModel
from .fields import LocalizedField
from .localized_value import LocalizedValue
from django.db import models
from django.core.checks import Warning
class LocalizedModel(PostgresModel):
"""A model that contains localized fields."""
class LocalizedModel(models.Model):
"""A model keeped for backwards compatibility"""
@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
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 explicitly 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,8 @@
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.models import LocalizedModel
from localized_fields import AtomicSlugRetryMixin
@ -15,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

View File

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