mirror of
				https://github.com/SectorLabs/django-localized-fields.git
				synced 2025-11-03 19:58:56 +03:00 
			
		
		
		
	no need inheritance from LocalizedModel anymore. Introduction of LocalizedValueDescriptor
This commit is contained in:
		
							
								
								
									
										32
									
								
								README.rst
									
									
									
									
									
								
							
							
						
						
									
										32
									
								
								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()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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'
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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 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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user