mirror of
				https://github.com/SectorLabs/django-localized-fields.git
				synced 2025-10-31 02:48:56 +03:00 
			
		
		
		
	Merge branch 'master' of https://github.com/SectorLabs/django-localized-fields into extra-fields
This commit is contained in:
		
							
								
								
									
										20
									
								
								README.rst
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								README.rst
									
									
									
									
									
								
							| @@ -32,7 +32,7 @@ Installation | |||||||
|             .... |             .... | ||||||
|  |  | ||||||
|             'django.contrib.postgres', |             'django.contrib.postgres', | ||||||
|             'localized_fields' |             'localized_fields.apps.LocalizedFieldsConfig' | ||||||
|         ] |         ] | ||||||
|  |  | ||||||
| 3. Set the database engine to ``psqlextra.backend``: | 3. Set the database engine to ``psqlextra.backend``: | ||||||
| @@ -160,7 +160,7 @@ By default the values stored in a ``LocalizedField`` are *not unique*. You can e | |||||||
|  |  | ||||||
|     .. code-block:: python |     .. code-block:: python | ||||||
|  |  | ||||||
|         from localized_fields import get_language_codes |         from localized_fields.util import get_language_codes | ||||||
|  |  | ||||||
|         class MyModel(models.Model): |         class MyModel(models.Model): | ||||||
|             title = LocalizedField(uniqueness=get_language_codes()) |             title = LocalizedField(uniqueness=get_language_codes()) | ||||||
| @@ -176,7 +176,7 @@ By default the values stored in a ``LocalizedField`` are *not unique*. You can e | |||||||
|  |  | ||||||
|     .. code-block:: python |     .. code-block:: python | ||||||
|  |  | ||||||
|         from localized_fields import get_language_codes |         from localized_fields.util import get_language_codes | ||||||
|  |  | ||||||
|         class MyModel(models.Model): |         class MyModel(models.Model): | ||||||
|             title = LocalizedField(uniqueness=[(*get_language_codes())]) |             title = LocalizedField(uniqueness=[(*get_language_codes())]) | ||||||
| @@ -192,9 +192,8 @@ Besides ``LocalizedField``, there's also: | |||||||
|  |  | ||||||
|           .. code-block:: python |           .. code-block:: python | ||||||
|  |  | ||||||
|               from localized_fields import (LocalizedModel, |               from localized_fields.models import LocalizedModel | ||||||
|                                             LocalizedField, |               from localized_fields.fields import LocalizedField, LocalizedUniqueSlugField | ||||||
|                                             LocalizedUniqueSlugField) |  | ||||||
|  |  | ||||||
|               class MyModel(LocalizedModel): |               class MyModel(LocalizedModel): | ||||||
|                    title = LocalizedField() |                    title = LocalizedField() | ||||||
| @@ -216,10 +215,9 @@ Besides ``LocalizedField``, there's also: | |||||||
|  |  | ||||||
|           .. code-block:: python |           .. code-block:: python | ||||||
|  |  | ||||||
|               from localized_fields import (LocalizedField, |               from localized_fields.fields import LocalizedField, LocalizedAutoSlugField | ||||||
|                                             LocalizedUniqueSlugField) |  | ||||||
|  |  | ||||||
|               class MyModel(models.Model): |               class MyModel(LocalizedModel): | ||||||
|                    title = LocalizedField() |                    title = LocalizedField() | ||||||
|                    slug = LocalizedAutoSlugField(populate_from='title') |                    slug = LocalizedAutoSlugField(populate_from='title') | ||||||
|  |  | ||||||
| @@ -227,14 +225,14 @@ Besides ``LocalizedField``, there's also: | |||||||
|  |  | ||||||
| * ``LocalizedBleachField`` | * ``LocalizedBleachField`` | ||||||
|      Automatically bleaches the content of the field. |      Automatically bleaches the content of the field. | ||||||
|  |  | ||||||
|           * django-bleach |           * django-bleach | ||||||
|  |  | ||||||
|      Example usage: |      Example usage: | ||||||
|  |  | ||||||
|            .. code-block:: python |            .. code-block:: python | ||||||
|  |  | ||||||
|               from localized_fields import (LocalizedField, |               from localized_fields.fields import LocalizedField, LocalizedBleachField | ||||||
|                                             LocalizedBleachField) |  | ||||||
|  |  | ||||||
|               class MyModel(models.Model): |               class MyModel(models.Model): | ||||||
|                    title = LocalizedField() |                    title = LocalizedField() | ||||||
|   | |||||||
| @@ -1,32 +0,0 @@ | |||||||
| from .forms import LocalizedFieldForm, LocalizedFieldWidget |  | ||||||
| from .fields import (LocalizedAutoSlugField, LocalizedField, |  | ||||||
|                      LocalizedUniqueSlugField, LocalizedCharField, |  | ||||||
|                      LocalizedTextField, LocalizedFileField) |  | ||||||
| from .localized_value import LocalizedValue |  | ||||||
| from .mixins import AtomicSlugRetryMixin |  | ||||||
| from .models import LocalizedModel |  | ||||||
| from .util import get_language_codes |  | ||||||
|  |  | ||||||
| __all__ = [ |  | ||||||
|     'get_language_codes', |  | ||||||
|     'LocalizedField', |  | ||||||
|     'LocalizedModel', |  | ||||||
|     'LocalizedValue', |  | ||||||
|     'LocalizedAutoSlugField', |  | ||||||
|     'LocalizedUniqueSlugField', |  | ||||||
|     'LocalizedBleachField', |  | ||||||
|     'LocalizedCharField', |  | ||||||
|     'LocalizedTextField', |  | ||||||
|     'LocalizedFileField', |  | ||||||
|     'LocalizedFieldWidget', |  | ||||||
|     'LocalizedFieldForm', |  | ||||||
|     'AtomicSlugRetryMixin' |  | ||||||
| ] |  | ||||||
|  |  | ||||||
| try: |  | ||||||
|     from .fields import LocalizedBleachField |  | ||||||
|     __all__ += [ |  | ||||||
|         'LocalizedBleachField' |  | ||||||
|     ] |  | ||||||
| except ImportError: |  | ||||||
|     pass |  | ||||||
|   | |||||||
| @@ -1,8 +1,9 @@ | |||||||
| from django.contrib.admin import ModelAdmin | from django.contrib.admin import ModelAdmin | ||||||
|  |  | ||||||
|  | from . import widgets | ||||||
| from .fields import LocalizedField, LocalizedCharField, LocalizedTextField, \ | from .fields import LocalizedField, LocalizedCharField, LocalizedTextField, \ | ||||||
|     LocalizedFileField |     LocalizedFileField | ||||||
| from . import widgets |  | ||||||
|  |  | ||||||
|  |  | ||||||
| FORMFIELD_FOR_LOCALIZED_FIELDS_DEFAULTS = { | FORMFIELD_FOR_LOCALIZED_FIELDS_DEFAULTS = { | ||||||
| @@ -14,17 +15,22 @@ FORMFIELD_FOR_LOCALIZED_FIELDS_DEFAULTS = { | |||||||
|  |  | ||||||
|  |  | ||||||
| class LocalizedFieldsAdminMixin(ModelAdmin): | class LocalizedFieldsAdminMixin(ModelAdmin): | ||||||
|  |     """Mixin for making the fancy widgets work in Django Admin.""" | ||||||
|  |  | ||||||
|     class Media: |     class Media: | ||||||
|         css = { |         css = { | ||||||
|             'all': ( |             'all': ( | ||||||
|                 'localized_fields/localized-fields-admin.css', |                 'localized_fields/localized-fields-admin.css', | ||||||
|             ) |             ) | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         js = ( |         js = ( | ||||||
|             'localized_fields/localized-fields-admin.js', |             'localized_fields/localized-fields-admin.js', | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def __init__(self, *args, **kwargs): |     def __init__(self, *args, **kwargs): | ||||||
|  |         """Initializes a new instance of :see:LocalizedFieldsAdminMixin.""" | ||||||
|  |  | ||||||
|         super(LocalizedFieldsAdminMixin, self).__init__(*args, **kwargs) |         super(LocalizedFieldsAdminMixin, self).__init__(*args, **kwargs) | ||||||
|         overrides = FORMFIELD_FOR_LOCALIZED_FIELDS_DEFAULTS.copy() |         overrides = FORMFIELD_FOR_LOCALIZED_FIELDS_DEFAULTS.copy() | ||||||
|         overrides.update(self.formfield_overrides) |         overrides.update(self.formfield_overrides) | ||||||
|   | |||||||
							
								
								
									
										65
									
								
								localized_fields/descriptor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								localized_fields/descriptor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | |||||||
|  | from django.conf import settings | ||||||
|  | from django.utils import six, translation | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedValueDescriptor: | ||||||
|  |     """ | ||||||
|  |     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): | ||||||
|  |         """Initializes a new instance of :see:LocalizedValueDescriptor.""" | ||||||
|  |  | ||||||
|  |         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): | ||||||
|  |             language = translation.get_language() or settings.LANGUAGE_CODE | ||||||
|  |             self.__get__(instance).set(language, value)  # pylint: disable=no-member | ||||||
|  |         else: | ||||||
|  |             instance.__dict__[self.field.name] = value | ||||||
							
								
								
									
										25
									
								
								localized_fields/expressions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								localized_fields/expressions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | from django.conf import settings | ||||||
|  | from django.utils import translation | ||||||
|  |  | ||||||
|  | from psqlextra import expressions | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedRef(expressions.HStoreRef): | ||||||
|  |     """Expression that selects the value in a field only in | ||||||
|  |     the currently active language.""" | ||||||
|  |  | ||||||
|  |     def __init__(self, name: str, lang: str=None): | ||||||
|  |         """Initializes a new instance of :see:LocalizedRef. | ||||||
|  |  | ||||||
|  |         Arguments: | ||||||
|  |             name: | ||||||
|  |                 The field/column to select from. | ||||||
|  |  | ||||||
|  |             lang: | ||||||
|  |                 The language to get the field/column in. | ||||||
|  |                 If not specified, the currently active language | ||||||
|  |                 is used. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         language = lang or translation.get_language() or settings.LANGUAGE_CODE | ||||||
|  |         super().__init__(name, language) | ||||||
| @@ -1,6 +1,6 @@ | |||||||
| from .localized_field import LocalizedField | from .field import LocalizedField | ||||||
| from .localized_autoslug_field import LocalizedAutoSlugField | from .autoslug_field import LocalizedAutoSlugField | ||||||
| from .localized_uniqueslug_field import LocalizedUniqueSlugField | from .uniqueslug_field import LocalizedUniqueSlugField | ||||||
| from .localized_char_field import LocalizedCharField | from .localized_char_field import LocalizedCharField | ||||||
| from .localized_text_field import LocalizedTextField | from .localized_text_field import LocalizedTextField | ||||||
| from .localized_file_field import LocalizedFileField | from .localized_file_field import LocalizedFileField | ||||||
| @@ -16,7 +16,7 @@ __all__ = [ | |||||||
| ] | ] | ||||||
|  |  | ||||||
| try: | try: | ||||||
|     from .localized_bleach_field import LocalizedBleachField |     from .bleach_field import LocalizedBleachField | ||||||
|     __all__ += [ |     __all__ += [ | ||||||
|         'LocalizedBleachField' |         'LocalizedBleachField' | ||||||
|     ] |     ] | ||||||
|   | |||||||
| @@ -1,12 +1,12 @@ | |||||||
| from typing import Callable | from typing import Callable, Tuple | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| 
 | 
 | ||||||
| from django import forms | from django import forms | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.utils.text import slugify | from django.utils.text import slugify | ||||||
| 
 | 
 | ||||||
| from .localized_field import LocalizedField | from .field import LocalizedField | ||||||
| from ..localized_value import LocalizedValue | from ..value import LocalizedValue | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class LocalizedAutoSlugField(LocalizedField): | class LocalizedAutoSlugField(LocalizedField): | ||||||
| @@ -69,13 +69,7 @@ class LocalizedAutoSlugField(LocalizedField): | |||||||
| 
 | 
 | ||||||
|         slugs = LocalizedValue() |         slugs = LocalizedValue() | ||||||
| 
 | 
 | ||||||
|         for lang_code, _ in settings.LANGUAGES: |         for lang_code, value in self._get_populate_values(instance): | ||||||
|             value = self._get_populate_from_value( |  | ||||||
|                 instance, |  | ||||||
|                 self.populate_from, |  | ||||||
|                 lang_code |  | ||||||
|             ) |  | ||||||
| 
 |  | ||||||
|             if not value: |             if not value: | ||||||
|                 continue |                 continue | ||||||
| 
 | 
 | ||||||
| @@ -128,6 +122,30 @@ class LocalizedAutoSlugField(LocalizedField): | |||||||
| 
 | 
 | ||||||
|         return unique_slug |         return unique_slug | ||||||
| 
 | 
 | ||||||
|  |     def _get_populate_values(self, instance) -> Tuple[str, str]: | ||||||
|  |         """Gets all values (for each language) from the | ||||||
|  |         specified's instance's `populate_from` field. | ||||||
|  | 
 | ||||||
|  |         Arguments: | ||||||
|  |             instance: | ||||||
|  |                 The instance to get the values from. | ||||||
|  | 
 | ||||||
|  |         Returns: | ||||||
|  |             A list of (lang_code, value) tuples. | ||||||
|  |         """ | ||||||
|  | 
 | ||||||
|  |         return [ | ||||||
|  |             ( | ||||||
|  |                 lang_code, | ||||||
|  |                 self._get_populate_from_value( | ||||||
|  |                     instance, | ||||||
|  |                     self.populate_from, | ||||||
|  |                     lang_code | ||||||
|  |                 ), | ||||||
|  |             ) | ||||||
|  |             for lang_code, _ in settings.LANGUAGES | ||||||
|  |         ] | ||||||
|  | 
 | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def _get_populate_from_value(instance, field_name: str, language: str): |     def _get_populate_from_value(instance, field_name: str, language: str): | ||||||
|         """Gets the value to create a slug from in the specified language. |         """Gets the value to create a slug from in the specified language. | ||||||
| @@ -1,8 +1,9 @@ | |||||||
| import bleach | import bleach | ||||||
|  | 
 | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django_bleach.utils import get_bleach_default_options | from django_bleach.utils import get_bleach_default_options | ||||||
| 
 | 
 | ||||||
| from .localized_field import LocalizedField | from .field import LocalizedField | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class LocalizedBleachField(LocalizedField): | class LocalizedBleachField(LocalizedField): | ||||||
| @@ -1,70 +1,15 @@ | |||||||
|  | import json | ||||||
|  | 
 | ||||||
|  | from typing import Union | ||||||
|  | 
 | ||||||
| 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 psqlextra.fields import HStoreField | from psqlextra.fields import HStoreField | ||||||
| 
 | 
 | ||||||
| from ..forms import LocalizedFieldForm | from ..forms import LocalizedFieldForm | ||||||
| from ..localized_value import LocalizedValue | from ..value import LocalizedValue | ||||||
| 
 | from ..descriptor import LocalizedValueDescriptor | ||||||
| 
 |  | ||||||
| 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): | class LocalizedField(HStoreField): | ||||||
| @@ -87,9 +32,18 @@ class LocalizedField(HStoreField): | |||||||
| 
 | 
 | ||||||
|         super(LocalizedField, self).__init__(*args, **kwargs) |         super(LocalizedField, self).__init__(*args, **kwargs) | ||||||
| 
 | 
 | ||||||
|     def contribute_to_class(self, cls, name, **kwargs): |     def contribute_to_class(self, model, name, **kwargs): | ||||||
|         super(LocalizedField, self).contribute_to_class(cls, name, **kwargs) |         """Adds this field to the specifed model. | ||||||
|         setattr(cls, self.name, self.descriptor_class(self)) | 
 | ||||||
|  |         Arguments: | ||||||
|  |             cls: | ||||||
|  |                 The model to add the field to. | ||||||
|  | 
 | ||||||
|  |             name: | ||||||
|  |                 The name of the field to add. | ||||||
|  |         """ | ||||||
|  |         super(LocalizedField, self).contribute_to_class(model, name, **kwargs) | ||||||
|  |         setattr(model, self.name, self.descriptor_class(self)) | ||||||
| 
 | 
 | ||||||
|     @classmethod |     @classmethod | ||||||
|     def from_db_value(cls, value, *_): |     def from_db_value(cls, value, *_): | ||||||
| @@ -112,9 +66,31 @@ class LocalizedField(HStoreField): | |||||||
|             else: |             else: | ||||||
|                 return cls.attr_class() |                 return cls.attr_class() | ||||||
| 
 | 
 | ||||||
|  |         # we can get a list if an aggregation expression was used.. | ||||||
|  |         # if we the expression was flattened when only one key was selected | ||||||
|  |         # then we don't wrap each value in a localized value, otherwise we do | ||||||
|  |         if isinstance(value, list): | ||||||
|  |             result = [] | ||||||
|  |             for inner_val in value: | ||||||
|  |                 if isinstance(inner_val, dict): | ||||||
|  |                     if inner_val is None: | ||||||
|  |                         result.append(None) | ||||||
|  |                     else: | ||||||
|  |                         result.append(cls.attr_class(inner_val)) | ||||||
|  |                 else: | ||||||
|  |                     result.append(inner_val) | ||||||
|  | 
 | ||||||
|  |             return result | ||||||
|  | 
 | ||||||
|  |         # this is for when you select an individual key, it will be string, | ||||||
|  |         # not a dictionary, we'll give it to you as a flat value, not as a | ||||||
|  |         # localized value instance | ||||||
|  |         if not isinstance(value, dict): | ||||||
|  |             return value | ||||||
|  | 
 | ||||||
|         return cls.attr_class(value) |         return cls.attr_class(value) | ||||||
| 
 | 
 | ||||||
|     def to_python(self, value: dict) -> LocalizedValue: |     def to_python(self, value: Union[dict, str, None]) -> LocalizedValue: | ||||||
|         """Turns the specified database value into its Python |         """Turns the specified database value into its Python | ||||||
|         equivalent. |         equivalent. | ||||||
| 
 | 
 | ||||||
| @@ -128,10 +104,17 @@ class LocalizedField(HStoreField): | |||||||
|             data extracted from the database. |             data extracted from the database. | ||||||
|         """ |         """ | ||||||
| 
 | 
 | ||||||
|         if not value or not isinstance(value, dict): |         # first let the base class  handle the deserialization, this is in case we | ||||||
|  |         # get specified a json string representing a dict | ||||||
|  |         try: | ||||||
|  |             deserialized_value = super(LocalizedField, self).to_python(value) | ||||||
|  |         except json.JSONDecodeError: | ||||||
|  |             deserialized_value = value | ||||||
|  | 
 | ||||||
|  |         if not deserialized_value: | ||||||
|             return self.attr_class() |             return self.attr_class() | ||||||
| 
 | 
 | ||||||
|         return self.attr_class(value) |         return self.attr_class(deserialized_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,6 +1,6 @@ | |||||||
| from ..forms import LocalizedCharFieldForm | from ..forms import LocalizedCharFieldForm | ||||||
| from .localized_field import LocalizedField | from .field import LocalizedField | ||||||
| from ..localized_value import LocalizedStringValue | from ..value import LocalizedStringValue | ||||||
|  |  | ||||||
|  |  | ||||||
| class LocalizedCharField(LocalizedField): | class LocalizedCharField(LocalizedField): | ||||||
|   | |||||||
| @@ -8,10 +8,10 @@ from django.core.files.storage import default_storage | |||||||
| from django.utils.encoding import force_str, force_text | from django.utils.encoding import force_str, force_text | ||||||
|  |  | ||||||
| from localized_fields.fields import LocalizedField | from localized_fields.fields import LocalizedField | ||||||
| from localized_fields.fields.localized_field import LocalizedValueDescriptor | from localized_fields.fields.field import LocalizedValueDescriptor | ||||||
| from localized_fields.localized_value import LocalizedValue | from localized_fields.value import LocalizedValue | ||||||
|  |  | ||||||
| from ..localized_value import LocalizedFileValue | from ..value import LocalizedFileValue | ||||||
| from ..forms import LocalizedFileFieldForm | from ..forms import LocalizedFileFieldForm | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,156 +0,0 @@ | |||||||
| from datetime import datetime |  | ||||||
|  |  | ||||||
| from django.conf import settings |  | ||||||
| from django import forms |  | ||||||
| from django.utils.text import slugify |  | ||||||
| from django.db import transaction |  | ||||||
| from django.db.utils import IntegrityError |  | ||||||
|  |  | ||||||
|  |  | ||||||
| from ..util import get_language_codes |  | ||||||
| from ..localized_value import LocalizedValue |  | ||||||
| from .localized_field import LocalizedField |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class LocalizedUniqueSlugField(LocalizedField): |  | ||||||
|     """Automatically provides slugs for a localized field upon saving." |  | ||||||
|  |  | ||||||
|     An improved version of :see:LocalizedAutoSlugField, |  | ||||||
|     which adds: |  | ||||||
|  |  | ||||||
|         - Concurrency safety |  | ||||||
|         - Improved performance |  | ||||||
|  |  | ||||||
|     When in doubt, use this over :see:LocalizedAutoSlugField. |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     def __init__(self, *args, **kwargs): |  | ||||||
|         """Initializes a new instance of :see:LocalizedUniqueSlugField.""" |  | ||||||
|  |  | ||||||
|         kwargs['uniqueness'] = kwargs.pop('uniqueness', get_language_codes()) |  | ||||||
|  |  | ||||||
|         self.populate_from = kwargs.pop('populate_from') |  | ||||||
|         self.include_time = kwargs.pop('include_time', False) |  | ||||||
|  |  | ||||||
|         super().__init__(*args, **kwargs) |  | ||||||
|  |  | ||||||
|     def deconstruct(self): |  | ||||||
|         """Deconstructs the field into something the database |  | ||||||
|         can store.""" |  | ||||||
|  |  | ||||||
|         name, path, args, kwargs = super( |  | ||||||
|             LocalizedUniqueSlugField, self).deconstruct() |  | ||||||
|  |  | ||||||
|         kwargs['populate_from'] = self.populate_from |  | ||||||
|         kwargs['include_time'] = self.include_time |  | ||||||
|         return name, path, args, kwargs |  | ||||||
|  |  | ||||||
|     def formfield(self, **kwargs): |  | ||||||
|         """Gets the form field associated with this field. |  | ||||||
|  |  | ||||||
|         Because this is a slug field which is automatically |  | ||||||
|         populated, it should be hidden from the form. |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         defaults = { |  | ||||||
|             'form_class': forms.CharField, |  | ||||||
|             'required': False |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         defaults.update(kwargs) |  | ||||||
|  |  | ||||||
|         form_field = super().formfield(**defaults) |  | ||||||
|         form_field.widget = forms.HiddenInput() |  | ||||||
|  |  | ||||||
|         return form_field |  | ||||||
|  |  | ||||||
|     def contribute_to_class(self, cls, name, *args, **kwargs): |  | ||||||
|         """Hook that allow us to operate with model class. We overwrite save() |  | ||||||
|         method to run retry logic. |  | ||||||
|  |  | ||||||
|         Arguments: |  | ||||||
|             cls: |  | ||||||
|                 Model class. |  | ||||||
|  |  | ||||||
|             name: |  | ||||||
|                 Name of field in model. |  | ||||||
|         """ |  | ||||||
|         # apparently in inheritance cases, contribute_to_class is called more |  | ||||||
|         # than once, so we have to be careful not to overwrite the original |  | ||||||
|         # save method. |  | ||||||
|         if not hasattr(cls, '_orig_save'): |  | ||||||
|             cls._orig_save = cls.save |  | ||||||
|             max_retries = getattr( |  | ||||||
|                 settings, |  | ||||||
|                 'LOCALIZED_FIELDS_MAX_RETRIES', |  | ||||||
|                 100 |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|             def _new_save(instance, *args_, **kwargs_): |  | ||||||
|                 retries = 0 |  | ||||||
|                 while True: |  | ||||||
|                     with transaction.atomic(): |  | ||||||
|                         try: |  | ||||||
|                             slugs = self.populate_slugs(instance, retries) |  | ||||||
|                             setattr(instance, name, slugs) |  | ||||||
|                             instance._orig_save(*args_, **kwargs_) |  | ||||||
|                             break |  | ||||||
|                         except IntegrityError as e: |  | ||||||
|                             if retries >= max_retries: |  | ||||||
|                                 raise e |  | ||||||
|                             # check to be sure a slug fight caused |  | ||||||
|                             # the IntegrityError |  | ||||||
|                             s_e = str(e) |  | ||||||
|                             if name in s_e and 'unique' in s_e: |  | ||||||
|                                 retries += 1 |  | ||||||
|                             else: |  | ||||||
|                                 raise e |  | ||||||
|  |  | ||||||
|             cls.save = _new_save |  | ||||||
|         super().contribute_to_class(cls, name, *args, **kwargs) |  | ||||||
|  |  | ||||||
|     def populate_slugs(self, instance, retries=0): |  | ||||||
|         """Built the slug from populate_from field. |  | ||||||
|  |  | ||||||
|         Arguments: |  | ||||||
|             instance: |  | ||||||
|                 The model that is being saved. |  | ||||||
|  |  | ||||||
|             retries: |  | ||||||
|                 The value of the current attempt. |  | ||||||
|  |  | ||||||
|         Returns: |  | ||||||
|             The localized slug that was generated. |  | ||||||
|         """ |  | ||||||
|         slugs = LocalizedValue() |  | ||||||
|         populates_slugs = getattr(instance, self.populate_from, {}) |  | ||||||
|         for lang_code, _ in settings.LANGUAGES: |  | ||||||
|  |  | ||||||
|             value = populates_slugs.get(lang_code) |  | ||||||
|  |  | ||||||
|             if not value: |  | ||||||
|                 continue |  | ||||||
|  |  | ||||||
|             slug = slugify(value, allow_unicode=True) |  | ||||||
|  |  | ||||||
|             # verify whether it's needed to re-generate a slug, |  | ||||||
|             # if not, re-use the same slug |  | ||||||
|             if instance.pk is not None: |  | ||||||
|                 current_slug = getattr(instance, self.name).get(lang_code) |  | ||||||
|                 if current_slug is not None: |  | ||||||
|                     stripped_slug = current_slug[0:current_slug.rfind('-')] |  | ||||||
|                     if slug == stripped_slug: |  | ||||||
|                         slugs.set(lang_code, current_slug) |  | ||||||
|                         continue |  | ||||||
|  |  | ||||||
|             if self.include_time: |  | ||||||
|                 slug += '-%d' % datetime.now().microsecond |  | ||||||
|  |  | ||||||
|             if retries > 0: |  | ||||||
|                 # do not add another - if we already added time |  | ||||||
|                 if not self.include_time: |  | ||||||
|                     slug += '-' |  | ||||||
|                 slug += '%d' % retries |  | ||||||
|  |  | ||||||
|             slugs.set(lang_code, slug) |  | ||||||
|         return slugs |  | ||||||
							
								
								
									
										104
									
								
								localized_fields/fields/uniqueslug_field.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								localized_fields/fields/uniqueslug_field.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,104 @@ | |||||||
|  | from datetime import datetime | ||||||
|  |  | ||||||
|  | from django.utils.text import slugify | ||||||
|  | from django.core.exceptions import ImproperlyConfigured | ||||||
|  |  | ||||||
|  | from .autoslug_field import LocalizedAutoSlugField | ||||||
|  | from ..util import get_language_codes | ||||||
|  | from ..mixins import AtomicSlugRetryMixin | ||||||
|  | from ..value import LocalizedValue | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedUniqueSlugField(LocalizedAutoSlugField): | ||||||
|  |     """Automatically provides slugs for a localized | ||||||
|  |     field upon saving." | ||||||
|  |  | ||||||
|  |     An improved version of :see:LocalizedAutoSlugField, | ||||||
|  |     which adds: | ||||||
|  |  | ||||||
|  |         - Concurrency safety | ||||||
|  |         - Improved performance | ||||||
|  |  | ||||||
|  |     When in doubt, use this over :see:LocalizedAutoSlugField. | ||||||
|  |     Inherit from :see:AtomicSlugRetryMixin in your model to | ||||||
|  |     make this field work properly. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def __init__(self, *args, **kwargs): | ||||||
|  |         """Initializes a new instance of :see:LocalizedUniqueSlugField.""" | ||||||
|  |  | ||||||
|  |         kwargs['uniqueness'] = kwargs.pop('uniqueness', get_language_codes()) | ||||||
|  |  | ||||||
|  |         super(LocalizedUniqueSlugField, self).__init__( | ||||||
|  |             *args, | ||||||
|  |             **kwargs | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         self.populate_from = kwargs.pop('populate_from') | ||||||
|  |         self.include_time = kwargs.pop('include_time', False) | ||||||
|  |  | ||||||
|  |     def deconstruct(self): | ||||||
|  |         """Deconstructs the field into something the database | ||||||
|  |         can store.""" | ||||||
|  |  | ||||||
|  |         name, path, args, kwargs = super( | ||||||
|  |             LocalizedUniqueSlugField, self).deconstruct() | ||||||
|  |  | ||||||
|  |         kwargs['populate_from'] = self.populate_from | ||||||
|  |         kwargs['include_time'] = self.include_time | ||||||
|  |         return name, path, args, kwargs | ||||||
|  |  | ||||||
|  |     def pre_save(self, instance, add: bool): | ||||||
|  |         """Ran just before the model is saved, allows us to built | ||||||
|  |         the slug. | ||||||
|  |  | ||||||
|  |         Arguments: | ||||||
|  |             instance: | ||||||
|  |                 The model that is being saved. | ||||||
|  |  | ||||||
|  |             add: | ||||||
|  |                 Indicates whether this is a new entry | ||||||
|  |                 to the database or an update. | ||||||
|  |  | ||||||
|  |         Returns: | ||||||
|  |             The localized slug that was generated. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         if not isinstance(instance, AtomicSlugRetryMixin): | ||||||
|  |             raise ImproperlyConfigured(( | ||||||
|  |                 'Model \'%s\' does not inherit from AtomicSlugRetryMixin. ' | ||||||
|  |                 'Without this, the LocalizedUniqueSlugField will not work.' | ||||||
|  |             ) % type(instance).__name__) | ||||||
|  |  | ||||||
|  |         slugs = LocalizedValue() | ||||||
|  |  | ||||||
|  |         for lang_code, value in self._get_populate_values(instance): | ||||||
|  |             if not value: | ||||||
|  |                 continue | ||||||
|  |  | ||||||
|  |             slug = slugify(value, allow_unicode=True) | ||||||
|  |  | ||||||
|  |             # verify whether it's needed to re-generate a slug, | ||||||
|  |             # if not, re-use the same slug | ||||||
|  |             if instance.pk is not None: | ||||||
|  |                 current_slug = getattr(instance, self.name).get(lang_code) | ||||||
|  |                 if current_slug is not None: | ||||||
|  |                     stripped_slug = current_slug[0:current_slug.rfind('-')] | ||||||
|  |                     if slug == stripped_slug: | ||||||
|  |                         slugs.set(lang_code, current_slug) | ||||||
|  |                         continue | ||||||
|  |  | ||||||
|  |             if self.include_time: | ||||||
|  |                 slug += '-%d' % datetime.now().microsecond | ||||||
|  |  | ||||||
|  |             retries = getattr(instance, 'retries', 0) | ||||||
|  |             if retries > 0: | ||||||
|  |                 # do not add another - if we already added time | ||||||
|  |                 if not self.include_time: | ||||||
|  |                     slug += '-' | ||||||
|  |                 slug += '%d' % retries | ||||||
|  |  | ||||||
|  |             slugs.set(lang_code, slug) | ||||||
|  |  | ||||||
|  |         setattr(instance, self.name, slugs) | ||||||
|  |         return slugs | ||||||
| @@ -5,7 +5,7 @@ from django.conf import settings | |||||||
| from django.core.exceptions import ValidationError | from django.core.exceptions import ValidationError | ||||||
| from django.forms.widgets import FILE_INPUT_CONTRADICTION | from django.forms.widgets import FILE_INPUT_CONTRADICTION | ||||||
|  |  | ||||||
| from .localized_value import LocalizedValue, LocalizedStringValue, \ | from .value import LocalizedValue, LocalizedStringValue, \ | ||||||
|     LocalizedFileValue |     LocalizedFileValue | ||||||
| from .widgets import LocalizedFieldWidget, LocalizedCharFieldWidget, \ | from .widgets import LocalizedFieldWidget, LocalizedCharFieldWidget, \ | ||||||
|     LocalizedFileWidget |     LocalizedFileWidget | ||||||
|   | |||||||
| @@ -1,132 +0,0 @@ | |||||||
| """This module is unused, but should be contributed to Django.""" |  | ||||||
|  |  | ||||||
| from typing import List |  | ||||||
|  |  | ||||||
| from django.db import models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class HStoreIndex(models.Index): |  | ||||||
|     """Allows creating a index on a specific HStore index. |  | ||||||
|  |  | ||||||
|     Note: pieces of code in this class have been copied |  | ||||||
|     from the base class. There was no way around this.""" |  | ||||||
|  |  | ||||||
|     def __init__(self, field: str, keys: List[str], unique: bool=False, |  | ||||||
|                  name: str=''): |  | ||||||
|         """Initializes a new instance of :see:HStoreIndex. |  | ||||||
|  |  | ||||||
|         Arguments: |  | ||||||
|             field: |  | ||||||
|                 Name of the hstore field for |  | ||||||
|                 which's keys to create a index for. |  | ||||||
|  |  | ||||||
|             keys: |  | ||||||
|                 The name of the hstore keys to |  | ||||||
|                 create the index on. |  | ||||||
|  |  | ||||||
|             unique: |  | ||||||
|                 Whether this index should |  | ||||||
|                 be marked as UNIQUE. |  | ||||||
|  |  | ||||||
|             name: |  | ||||||
|                 The name of the index. If left |  | ||||||
|                 empty, one will be generated. |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         self.field = field |  | ||||||
|         self.keys = keys |  | ||||||
|         self.unique = unique |  | ||||||
|  |  | ||||||
|         # this will eventually set self.name |  | ||||||
|         super(HStoreIndex, self).__init__( |  | ||||||
|             fields=[field], |  | ||||||
|             name=name |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     def get_sql_create_template_values(self, model, schema_editor, using): |  | ||||||
|         """Gets the values for the SQL template. |  | ||||||
|  |  | ||||||
|         Arguments: |  | ||||||
|             model: |  | ||||||
|                 The model this index applies to. |  | ||||||
|  |  | ||||||
|             schema_editor: |  | ||||||
|                 The schema editor to modify the schema. |  | ||||||
|  |  | ||||||
|             using: |  | ||||||
|                 Optional: "USING" statement. |  | ||||||
|  |  | ||||||
|         Returns: |  | ||||||
|             Dictionary of keys to pass into the SQL template. |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         fields = [model._meta.get_field(field_name) for field_name, order in self.fields_orders] |  | ||||||
|         tablespace_sql = schema_editor._get_index_tablespace_sql(model, fields) |  | ||||||
|         quote_name = schema_editor.quote_name |  | ||||||
|  |  | ||||||
|         columns = [ |  | ||||||
|             '(%s->\'%s\')' % (self.field, key) |  | ||||||
|             for key in self.keys |  | ||||||
|         ] |  | ||||||
|  |  | ||||||
|         return { |  | ||||||
|             'table': quote_name(model._meta.db_table), |  | ||||||
|             'name': quote_name(self.name), |  | ||||||
|             'columns': ', '.join(columns), |  | ||||||
|             'using': using, |  | ||||||
|             'extra': tablespace_sql, |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|     def create_sql(self, model, schema_editor, using=''): |  | ||||||
|         """Gets the SQL to execute when creating the index. |  | ||||||
|  |  | ||||||
|         Arguments: |  | ||||||
|             model: |  | ||||||
|                 The model this index applies to. |  | ||||||
|  |  | ||||||
|             schema_editor: |  | ||||||
|                 The schema editor to modify the schema. |  | ||||||
|  |  | ||||||
|             using: |  | ||||||
|                 Optional: "USING" statement. |  | ||||||
|  |  | ||||||
|         Returns: |  | ||||||
|             SQL string to execute to create this index. |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         sql_create_index = schema_editor.sql_create_index |  | ||||||
|         if self.unique: |  | ||||||
|             sql_create_index = sql_create_index.replace('CREATE', 'CREATE UNIQUE') |  | ||||||
|         sql_parameters = self.get_sql_create_template_values(model, schema_editor, using) |  | ||||||
|         return sql_create_index % sql_parameters |  | ||||||
|  |  | ||||||
|     def remove_sql(self, model, schema_editor): |  | ||||||
|         """Gets the SQL to execute to remove this index. |  | ||||||
|  |  | ||||||
|         Arguments: |  | ||||||
|             model: |  | ||||||
|                 The model this index applies to. |  | ||||||
|  |  | ||||||
|             schema_editor: |  | ||||||
|                 The schema editor to modify the schema. |  | ||||||
|  |  | ||||||
|         Returns: |  | ||||||
|             SQL string to execute to remove this index. |  | ||||||
|         """ |  | ||||||
|         quote_name = schema_editor.quote_name |  | ||||||
|         return schema_editor.sql_delete_index % { |  | ||||||
|             'table': quote_name(model._meta.db_table), |  | ||||||
|             'name': quote_name(self.name), |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|     def deconstruct(self): |  | ||||||
|         """Gets the values to pass to :see:__init__ when |  | ||||||
|         re-creating this object.""" |  | ||||||
|  |  | ||||||
|         path = '%s.%s' % (self.__class__.__module__, self.__class__.__name__) |  | ||||||
|         return (path, (), { |  | ||||||
|             'field': self.field, |  | ||||||
|             'keys': self.keys, |  | ||||||
|             'unique': self.unique, |  | ||||||
|             'name': self.name |  | ||||||
|         }) |  | ||||||
| @@ -1,17 +1,38 @@ | |||||||
| from django.core.checks import Warning | from django.db import transaction | ||||||
|  | from django.conf import settings | ||||||
|  | from django.db.utils import IntegrityError | ||||||
|  |  | ||||||
|  |  | ||||||
| class AtomicSlugRetryMixin: | class AtomicSlugRetryMixin: | ||||||
|     """A Mixin keeped for backwards compatibility""" |     """Makes :see:LocalizedUniqueSlugField work by retrying upon | ||||||
|  |     violation of the UNIQUE constraint.""" | ||||||
|  |  | ||||||
|     @classmethod |     def save(self, *args, **kwargs): | ||||||
|     def check(cls, **kwargs): |         """Saves this model instance to the database.""" | ||||||
|         errors = super().check(**kwargs) |  | ||||||
|         errors.append( |         max_retries = getattr( | ||||||
|             Warning( |             settings, | ||||||
|                 'localized_fields.AtomicSlugRetryMixin is deprecated', |             'LOCALIZED_FIELDS_MAX_RETRIES', | ||||||
|                 hint='There is no need to use ' |             100 | ||||||
|                      'localized_fields.AtomicSlugRetryMixin', |  | ||||||
|                 obj=cls |  | ||||||
|             ) |  | ||||||
|         ) |         ) | ||||||
|         return errors |  | ||||||
|  |         if not hasattr(self, 'retries'): | ||||||
|  |             self.retries = 0 | ||||||
|  |  | ||||||
|  |         with transaction.atomic(): | ||||||
|  |             try: | ||||||
|  |                 return super().save(*args, **kwargs) | ||||||
|  |             except IntegrityError as ex: | ||||||
|  |                 # this is as retarded as it looks, there's no | ||||||
|  |                 # way we can put the retry logic inside the slug | ||||||
|  |                 # field class... we can also not only catch exceptions | ||||||
|  |                 # that apply to slug fields... so yea.. this is as | ||||||
|  |                 # retarded as it gets... i am sorry :( | ||||||
|  |                 if 'slug' not in str(ex): | ||||||
|  |                     raise ex | ||||||
|  |  | ||||||
|  |                 if self.retries >= max_retries: | ||||||
|  |                     raise ex | ||||||
|  |  | ||||||
|  |         self.retries += 1 | ||||||
|  |         return self.save() | ||||||
|   | |||||||
| @@ -1,21 +1,16 @@ | |||||||
| from django.db import models | from psqlextra.models import PostgresModel | ||||||
| from django.core.checks import Warning |  | ||||||
|  | from .mixins import AtomicSlugRetryMixin | ||||||
|  |  | ||||||
|  |  | ||||||
| class LocalizedModel(models.Model): | class LocalizedModel(AtomicSlugRetryMixin, PostgresModel): | ||||||
|     """A model keeped for backwards compatibility""" |     """Turns a model into a model that contains LocalizedField's. | ||||||
|  |  | ||||||
|     @classmethod |     For basic localisation functionality, it isn't needed to inherit | ||||||
|     def check(cls, **kwargs): |     from LocalizedModel. However, for certain features, this is required. | ||||||
|         errors = super().check(**kwargs) |  | ||||||
|         errors.append( |     It is definitely needed for :see:LocalizedUniqueSlugField, unless you | ||||||
|             Warning( |     manually inherit from AtomicSlugRetryMixin.""" | ||||||
|                 'localized_fields.LocalizedModel is deprecated', |  | ||||||
|                 hint='There is no need to use localized_fields.LocalizedModel', |  | ||||||
|                 obj=cls |  | ||||||
|             ) |  | ||||||
|         ) |  | ||||||
|         return errors |  | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|         abstract = True |         abstract = True | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | import collections | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.utils import translation | from django.utils import translation | ||||||
| 
 | 
 | ||||||
| @@ -16,17 +19,10 @@ class LocalizedValue(dict): | |||||||
|                 different language. |                 different language. | ||||||
|         """ |         """ | ||||||
| 
 | 
 | ||||||
|         # NOTE(seroy): First fill all the keys with default value, |         super().__init__({}) | ||||||
|         # in order to attributes will be for each language |         self._interpret_value(keys) | ||||||
|         for lang_code, _ in settings.LANGUAGES: |  | ||||||
|             value = keys.get(lang_code) if isinstance(keys, dict) else \ |  | ||||||
|                 self.default_value |  | ||||||
|             self.set(lang_code, value) |  | ||||||
| 
 | 
 | ||||||
|         if isinstance(keys, str): |     def get(self, language: str=None, default: str=None) -> str: | ||||||
|             setattr(self, settings.LANGUAGE_CODE, keys) |  | ||||||
| 
 |  | ||||||
|     def get(self, language: str=None) -> str: |  | ||||||
|         """Gets the underlying value in the specified or |         """Gets the underlying value in the specified or | ||||||
|         primary language. |         primary language. | ||||||
| 
 | 
 | ||||||
| @@ -41,7 +37,7 @@ class LocalizedValue(dict): | |||||||
|         """ |         """ | ||||||
| 
 | 
 | ||||||
|         language = language or settings.LANGUAGE_CODE |         language = language or settings.LANGUAGE_CODE | ||||||
|         return super().get(language, None) |         return super().get(language, default) | ||||||
| 
 | 
 | ||||||
|     def set(self, language: str, value: str): |     def set(self, language: str, value: str): | ||||||
|         """Sets the value in the specified language. |         """Sets the value in the specified language. | ||||||
| @@ -69,6 +65,40 @@ class LocalizedValue(dict): | |||||||
|         path = 'localized_fields.localized_value.%s' % self.__class__.__name__ |         path = 'localized_fields.localized_value.%s' % self.__class__.__name__ | ||||||
|         return path, [self.__dict__], {} |         return path, [self.__dict__], {} | ||||||
| 
 | 
 | ||||||
|  |     def _interpret_value(self, value): | ||||||
|  |         """Interprets a value passed in the constructor as | ||||||
|  |         a :see:LocalizedValue. | ||||||
|  | 
 | ||||||
|  |         If string: | ||||||
|  |             Assumes it's the default language. | ||||||
|  | 
 | ||||||
|  |         If dict: | ||||||
|  |             Each key is a language and the value a string | ||||||
|  |             in that language. | ||||||
|  | 
 | ||||||
|  |         If list: | ||||||
|  |             Recurse into to apply rules above. | ||||||
|  | 
 | ||||||
|  |         Arguments: | ||||||
|  |             value: | ||||||
|  |                 The value to interpret. | ||||||
|  |         """ | ||||||
|  | 
 | ||||||
|  |         for lang_code, _ in settings.LANGUAGES: | ||||||
|  |             self.set(lang_code, self.default_value) | ||||||
|  | 
 | ||||||
|  |         if isinstance(value, str): | ||||||
|  |             self.set(settings.LANGUAGE_CODE, value) | ||||||
|  | 
 | ||||||
|  |         elif isinstance(value, dict): | ||||||
|  |             for lang_code, _ in settings.LANGUAGES: | ||||||
|  |                 lang_value = value.get(lang_code, self.default_value) | ||||||
|  |                 self.set(lang_code, lang_value) | ||||||
|  | 
 | ||||||
|  |         elif isinstance(value, collections.Iterable): | ||||||
|  |             for val in value: | ||||||
|  |                 self._interpret_value(val) | ||||||
|  | 
 | ||||||
|     def __str__(self) -> str: |     def __str__(self) -> str: | ||||||
|         """Gets the value in the current language, or falls |         """Gets the value in the current language, or falls | ||||||
|         back to the primary language if there's no value |         back to the primary language if there's no value | ||||||
| @@ -5,7 +5,7 @@ from django import forms | |||||||
| from django.contrib.admin import widgets | from django.contrib.admin import widgets | ||||||
| from django.template.loader import render_to_string | from django.template.loader import render_to_string | ||||||
|  |  | ||||||
| from .localized_value import LocalizedValue | from .value import LocalizedValue | ||||||
|  |  | ||||||
|  |  | ||||||
| class LocalizedFieldWidget(forms.MultiWidget): | class LocalizedFieldWidget(forms.MultiWidget): | ||||||
| @@ -15,12 +15,12 @@ class LocalizedFieldWidget(forms.MultiWidget): | |||||||
|     def __init__(self, *args, **kwargs): |     def __init__(self, *args, **kwargs): | ||||||
|         """Initializes a new instance of :see:LocalizedFieldWidget.""" |         """Initializes a new instance of :see:LocalizedFieldWidget.""" | ||||||
|  |  | ||||||
|         widgets = [] |         initial_widgets = [ | ||||||
|  |             self.widget | ||||||
|  |             for _ in settings.LANGUAGES | ||||||
|  |         ] | ||||||
|  |  | ||||||
|         for _ in settings.LANGUAGES: |         super().__init__(initial_widgets, *args, **kwargs) | ||||||
|             widgets.append(self.widget) |  | ||||||
|  |  | ||||||
|         super(LocalizedFieldWidget, self).__init__(widgets, *args, **kwargs) |  | ||||||
|  |  | ||||||
|     def decompress(self, value: LocalizedValue) -> List[str]: |     def decompress(self, value: LocalizedValue) -> List[str]: | ||||||
|         """Decompresses the specified value so |         """Decompresses the specified value so | ||||||
| @@ -36,7 +36,6 @@ class LocalizedFieldWidget(forms.MultiWidget): | |||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         result = [] |         result = [] | ||||||
|  |  | ||||||
|         for lang_code, _ in settings.LANGUAGES: |         for lang_code, _ in settings.LANGUAGES: | ||||||
|             if value: |             if value: | ||||||
|                 result.append(value.get(lang_code)) |                 result.append(value.get(lang_code)) | ||||||
| @@ -88,7 +87,8 @@ class AdminLocalizedFieldWidget(LocalizedFieldWidget): | |||||||
|         } |         } | ||||||
|         return render_to_string(self.template, context) |         return render_to_string(self.template, context) | ||||||
|  |  | ||||||
|     def build_widget_attrs(self, widget, value, attrs): |     @staticmethod | ||||||
|  |     def build_widget_attrs(widget, value, attrs): | ||||||
|         attrs = dict(attrs)  # Copy attrs to avoid modifying the argument. |         attrs = dict(attrs)  # Copy attrs to avoid modifying the argument. | ||||||
|         if (not widget.use_required_attribute(value) or not widget.is_required) \ |         if (not widget.use_required_attribute(value) or not widget.is_required) \ | ||||||
|                 and 'required' in attrs: |                 and 'required' in attrs: | ||||||
|   | |||||||
| @@ -21,6 +21,7 @@ LANGUAGES = ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| INSTALLED_APPS = ( | INSTALLED_APPS = ( | ||||||
|  |     'localized_fields', | ||||||
|     'tests', |     'tests', | ||||||
| ) | ) | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								setup.py
									
									
									
									
									
								
							| @@ -7,7 +7,7 @@ with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme: | |||||||
|  |  | ||||||
| setup( | setup( | ||||||
|     name='django-localized-fields', |     name='django-localized-fields', | ||||||
|     version='3.6', |     version='4.1', | ||||||
|     packages=find_packages(), |     packages=find_packages(), | ||||||
|     include_package_data=True, |     include_package_data=True, | ||||||
|     license='MIT License', |     license='MIT License', | ||||||
| @@ -18,7 +18,7 @@ setup( | |||||||
|     author_email='open-source@sectorlabs.ro', |     author_email='open-source@sectorlabs.ro', | ||||||
|     keywords=['django', 'localized', 'language', 'models', 'fields'], |     keywords=['django', 'localized', 'language', 'models', 'fields'], | ||||||
|     install_requires=[ |     install_requires=[ | ||||||
|         'django-postgres-extra>=1.4' |         'django-postgres-extra>=1.11' | ||||||
|     ], |     ], | ||||||
|     classifiers=[ |     classifiers=[ | ||||||
|         'Environment :: Web Environment', |         'Environment :: Web Environment', | ||||||
|   | |||||||
							
								
								
									
										13
									
								
								tests/data.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								tests/data.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | from django.conf import settings | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_init_values() -> dict: | ||||||
|  |     """Gets a test dictionary containing a key | ||||||
|  |     for every language.""" | ||||||
|  |  | ||||||
|  |     keys = {} | ||||||
|  |  | ||||||
|  |     for lang_code, lang_name in settings.LANGUAGES: | ||||||
|  |         keys[lang_code] = 'value in %s' % lang_name | ||||||
|  |  | ||||||
|  |     return keys | ||||||
| @@ -2,7 +2,7 @@ from django.db import connection, migrations | |||||||
| 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 | from localized_fields.models import LocalizedModel | ||||||
|  |  | ||||||
|  |  | ||||||
| def define_fake_model(name='TestModel', fields=None): | def define_fake_model(name='TestModel', fields=None): | ||||||
|   | |||||||
| @@ -1,9 +1,11 @@ | |||||||
|  | import bleach | ||||||
|  | 
 | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
| from django_bleach.utils import get_bleach_default_options | from django_bleach.utils import get_bleach_default_options | ||||||
| import bleach |  | ||||||
| 
 | 
 | ||||||
| from localized_fields import LocalizedBleachField, LocalizedValue | from localized_fields.fields import LocalizedBleachField | ||||||
|  | from localized_fields.value import LocalizedValue | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class TestModel: | class TestModel: | ||||||
							
								
								
									
										45
									
								
								tests/test_bulk.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								tests/test_bulk.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | |||||||
|  | from django.db import models | ||||||
|  | from django.test import TestCase | ||||||
|  |  | ||||||
|  | from localized_fields.fields import LocalizedField, LocalizedUniqueSlugField | ||||||
|  |  | ||||||
|  | from .fake_model import get_fake_model | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedBulkTestCase(TestCase): | ||||||
|  |     """Tests bulk operations with data structures provided | ||||||
|  |     by the django-localized-fields library.""" | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_localized_bulk_insert(): | ||||||
|  |         """Tests whether bulk inserts work properly when using | ||||||
|  |         a :see:LocalizedUniqueSlugField in the model.""" | ||||||
|  |  | ||||||
|  |         model = get_fake_model( | ||||||
|  |             'BulkSlugInsertModel', | ||||||
|  |             { | ||||||
|  |                 'name': LocalizedField(), | ||||||
|  |                 'slug': LocalizedUniqueSlugField(populate_from='name', include_time=True), | ||||||
|  |                 'score': models.IntegerField() | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         to_create = [ | ||||||
|  |             model(name={'en': 'english name 1', 'ro': 'romanian name 1'}, score=1), | ||||||
|  |             model(name={'en': 'english name 2', 'ro': 'romanian name 2'}, score=2), | ||||||
|  |             model(name={'en': 'english name 3', 'ro': 'romanian name 3'}, score=3) | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |         model.objects.bulk_create(to_create) | ||||||
|  |         assert model.objects.all().count() == 3 | ||||||
|  |  | ||||||
|  |         for obj in to_create: | ||||||
|  |             obj_db = model.objects.filter( | ||||||
|  |                 name__en=obj.name.en, | ||||||
|  |                 name__ro=obj.name.ro, | ||||||
|  |                 score=obj.score | ||||||
|  |             ).first() | ||||||
|  |  | ||||||
|  |             assert obj_db | ||||||
|  |             assert len(obj_db.slug.en) >= len(obj_db.name.en) | ||||||
|  |             assert len(obj_db.slug.ro) >= len(obj_db.name.ro) | ||||||
							
								
								
									
										86
									
								
								tests/test_expressions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								tests/test_expressions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,86 @@ | |||||||
|  | from django.test import TestCase | ||||||
|  | from django.db import models | ||||||
|  | from django.utils import translation | ||||||
|  | from django.conf import settings | ||||||
|  | from django.contrib.postgres.aggregates import ArrayAgg | ||||||
|  |  | ||||||
|  | from localized_fields.fields import LocalizedField | ||||||
|  | from localized_fields.value import LocalizedValue | ||||||
|  | from localized_fields.expressions import LocalizedRef | ||||||
|  |  | ||||||
|  | from .fake_model import get_fake_model | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedExpressionsTestCase(TestCase): | ||||||
|  |     """Tests whether expressions properly work with :see:LocalizedField.""" | ||||||
|  |  | ||||||
|  |     TestModel1 = None | ||||||
|  |     TestModel2 = None | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def setUpClass(cls): | ||||||
|  |         """Creates the test model in the database.""" | ||||||
|  |  | ||||||
|  |         super(LocalizedExpressionsTestCase, cls).setUpClass() | ||||||
|  |  | ||||||
|  |         cls.TestModel1 = get_fake_model( | ||||||
|  |             'LocalizedExpressionsTestCase2', | ||||||
|  |             { | ||||||
|  |                 'name': models.CharField(null=False, blank=False, max_length=255), | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         cls.TestModel2 = get_fake_model( | ||||||
|  |             'LocalizedExpressionsTestCase1', | ||||||
|  |             { | ||||||
|  |                 'text': LocalizedField(), | ||||||
|  |                 'other': models.ForeignKey(cls.TestModel1, related_name='features') | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def test_localized_ref(cls): | ||||||
|  |         """Tests whether the :see:LocalizedRef expression properly works.""" | ||||||
|  |  | ||||||
|  |         obj = cls.TestModel1.objects.create(name='bla bla') | ||||||
|  |         for i in range(0, 10): | ||||||
|  |             cls.TestModel2.objects.create( | ||||||
|  |                 text=LocalizedValue(dict(en='text_%d_en' % i, ro='text_%d_ro' % i, nl='text_%d_nl' % i)), | ||||||
|  |                 other=obj | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         def create_queryset(ref): | ||||||
|  |             return ( | ||||||
|  |                 cls.TestModel1.objects | ||||||
|  |                 .annotate(mytexts=ref) | ||||||
|  |                 .values_list('mytexts', flat=True) | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         # assert that it properly selects the currently active language | ||||||
|  |         for lang_code, _ in settings.LANGUAGES: | ||||||
|  |             translation.activate(lang_code) | ||||||
|  |             queryset = create_queryset(LocalizedRef('features__text')) | ||||||
|  |  | ||||||
|  |             for index, value in enumerate(queryset): | ||||||
|  |                 assert translation.get_language() in value | ||||||
|  |                 assert str(index) in value | ||||||
|  |  | ||||||
|  |         # ensure that the default language is used in case no | ||||||
|  |         # language is active at all | ||||||
|  |         translation.deactivate_all() | ||||||
|  |         queryset = create_queryset(LocalizedRef('features__text')) | ||||||
|  |         for index, value in enumerate(queryset): | ||||||
|  |             assert settings.LANGUAGE_CODE in value | ||||||
|  |             assert str(index) in value | ||||||
|  |  | ||||||
|  |         # ensures that overriding the language works properly | ||||||
|  |         queryset = create_queryset(LocalizedRef('features__text', 'ro')) | ||||||
|  |         for index, value in enumerate(queryset): | ||||||
|  |             assert 'ro' in value | ||||||
|  |             assert str(index) in value | ||||||
|  |  | ||||||
|  |         # ensures that using this in combination with ArrayAgg works properly | ||||||
|  |         queryset = create_queryset(ArrayAgg(LocalizedRef('features__text', 'ro'))).first() | ||||||
|  |         assert isinstance(queryset, list) | ||||||
|  |         for value in queryset: | ||||||
|  |             assert 'ro' in value | ||||||
							
								
								
									
										158
									
								
								tests/test_field.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								tests/test_field.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,158 @@ | |||||||
|  | import json | ||||||
|  |  | ||||||
|  | from django.conf import settings | ||||||
|  | from django.db.utils import IntegrityError | ||||||
|  | from django.test import TestCase | ||||||
|  |  | ||||||
|  | from localized_fields.fields import LocalizedField | ||||||
|  | from localized_fields.forms import LocalizedFieldForm | ||||||
|  | from localized_fields.value import LocalizedValue | ||||||
|  |  | ||||||
|  | from .data import get_init_values | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedFieldTestCase(TestCase): | ||||||
|  |     """Tests the :see:LocalizedField class.""" | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_from_db_value(): | ||||||
|  |         """Tests whether the :see:from_db_value function | ||||||
|  |         produces the expected :see:LocalizedValue.""" | ||||||
|  |  | ||||||
|  |         input_data = get_init_values() | ||||||
|  |         localized_value = LocalizedField().from_db_value(input_data) | ||||||
|  |  | ||||||
|  |         for lang_code, _ in settings.LANGUAGES: | ||||||
|  |             assert getattr(localized_value, lang_code) == input_data[lang_code] | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_from_db_value_none(): | ||||||
|  |         """Tests whether the :see:from_db_value function | ||||||
|  |         correctly handles None values.""" | ||||||
|  |  | ||||||
|  |         localized_value = LocalizedField().from_db_value(None) | ||||||
|  |  | ||||||
|  |         for lang_code, _ in settings.LANGUAGES: | ||||||
|  |             assert localized_value.get(lang_code) is None | ||||||
|  |  | ||||||
|  |     def test_from_db_value_none_return_none(self): | ||||||
|  |         """Tests whether the :see:from_db_value function | ||||||
|  |         correctly handles None values when LOCALIZED_FIELDS_EXPERIMENTAL | ||||||
|  |         is set to True.""" | ||||||
|  |  | ||||||
|  |         with self.settings(LOCALIZED_FIELDS_EXPERIMENTAL=True): | ||||||
|  |             localized_value = LocalizedField.from_db_value(None) | ||||||
|  |  | ||||||
|  |         assert localized_value is None | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_to_python(): | ||||||
|  |         """Tests whether the :see:to_python function | ||||||
|  |         produces the expected :see:LocalizedValue.""" | ||||||
|  |  | ||||||
|  |         input_data = get_init_values() | ||||||
|  |         localized_value = LocalizedField().to_python(input_data) | ||||||
|  |  | ||||||
|  |         for language, value in input_data.items(): | ||||||
|  |             assert localized_value.get(language) == value | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_to_python_non_json(): | ||||||
|  |         """Tests whether the :see:to_python function | ||||||
|  |         properly handles a string that is not JSON.""" | ||||||
|  |  | ||||||
|  |         localized_value = LocalizedField().to_python('my value') | ||||||
|  |         assert localized_value.get() == 'my value' | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_to_python_none(): | ||||||
|  |         """Tests whether the :see:to_python function | ||||||
|  |         produces the expected :see:LocalizedValue | ||||||
|  |         instance when it is passes None.""" | ||||||
|  |  | ||||||
|  |         localized_value = LocalizedField().to_python(None) | ||||||
|  |         assert localized_value | ||||||
|  |  | ||||||
|  |         for lang_code, _ in settings.LANGUAGES: | ||||||
|  |             assert localized_value.get(lang_code) is None | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_to_python_non_dict(): | ||||||
|  |         """Tests whether the :see:to_python function produces | ||||||
|  |         the expected :see:LocalizedValue when it is | ||||||
|  |         passed a non-dictionary value.""" | ||||||
|  |  | ||||||
|  |         localized_value = LocalizedField().to_python(list()) | ||||||
|  |         assert localized_value | ||||||
|  |  | ||||||
|  |         for lang_code, _ in settings.LANGUAGES: | ||||||
|  |             assert localized_value.get(lang_code) is None | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_to_python_str(): | ||||||
|  |         """Tests whether the :see:to_python function produces | ||||||
|  |         the expected :see:LocalizedValue when it is | ||||||
|  |         passed serialized string value.""" | ||||||
|  |  | ||||||
|  |         serialized_str = json.dumps(get_init_values()) | ||||||
|  |         localized_value = LocalizedField().to_python(serialized_str) | ||||||
|  |         assert isinstance(localized_value, LocalizedValue) | ||||||
|  |  | ||||||
|  |         for language, value in get_init_values().items(): | ||||||
|  |             assert localized_value.get(language) == value | ||||||
|  |             assert getattr(localized_value, language) == value | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_get_prep_value(): | ||||||
|  |         """"Tests whether the :see:get_prep_value function | ||||||
|  |         produces the expected dictionary.""" | ||||||
|  |  | ||||||
|  |         input_data = get_init_values() | ||||||
|  |         localized_value = LocalizedValue(input_data) | ||||||
|  |  | ||||||
|  |         output_data = LocalizedField().get_prep_value(localized_value) | ||||||
|  |  | ||||||
|  |         for language, value in input_data.items(): | ||||||
|  |             assert language in output_data | ||||||
|  |             assert output_data.get(language) == value | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_get_prep_value_none(): | ||||||
|  |         """Tests whether the :see:get_prep_value function | ||||||
|  |         produces the expected output when it is passed None.""" | ||||||
|  |  | ||||||
|  |         output_data = LocalizedField().get_prep_value(None) | ||||||
|  |         assert not output_data | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_get_prep_value_no_localized_value(): | ||||||
|  |         """Tests whether the :see:get_prep_value function | ||||||
|  |         produces the expected output when it is passed a | ||||||
|  |         non-LocalizedValue value.""" | ||||||
|  |  | ||||||
|  |         output_data = LocalizedField().get_prep_value(['huh']) | ||||||
|  |         assert not output_data | ||||||
|  |  | ||||||
|  |     def test_get_prep_value_clean(self): | ||||||
|  |         """Tests whether the :see:get_prep_value produces | ||||||
|  |         None as the output when it is passed an empty, but | ||||||
|  |         valid LocalizedValue value but, only when null=True.""" | ||||||
|  |  | ||||||
|  |         localized_value = LocalizedValue() | ||||||
|  |  | ||||||
|  |         with self.assertRaises(IntegrityError): | ||||||
|  |             LocalizedField(null=False).get_prep_value(localized_value) | ||||||
|  |  | ||||||
|  |         assert not LocalizedField(null=True).get_prep_value(localized_value) | ||||||
|  |         assert not LocalizedField().clean(None) | ||||||
|  |         assert not LocalizedField().clean(['huh']) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_formfield(): | ||||||
|  |         """Tests whether the :see:formfield function | ||||||
|  |         correctly returns a valid form.""" | ||||||
|  |  | ||||||
|  |         assert isinstance( | ||||||
|  |             LocalizedField().formfield(), | ||||||
|  |             LocalizedFieldForm | ||||||
|  |         ) | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
| 
 | 
 | ||||||
| from localized_fields import LocalizedFieldForm | from localized_fields.forms import LocalizedFieldForm | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class LocalizedFieldFormTestCase(TestCase): | class LocalizedFieldFormTestCase(TestCase): | ||||||
| @@ -1,288 +0,0 @@ | |||||||
| from django.conf import settings |  | ||||||
| from django.db.utils import IntegrityError |  | ||||||
| from django.test import TestCase |  | ||||||
| from django.utils import translation |  | ||||||
|  |  | ||||||
| from localized_fields import LocalizedField, LocalizedFieldForm, LocalizedValue |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_init_values() -> dict: |  | ||||||
|     """Gets a test dictionary containing a key |  | ||||||
|     for every language.""" |  | ||||||
|  |  | ||||||
|     keys = {} |  | ||||||
|  |  | ||||||
|     for lang_code, lang_name in settings.LANGUAGES: |  | ||||||
|         keys[lang_code] = 'value in %s' % lang_name |  | ||||||
|  |  | ||||||
|     return keys |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class LocalizedValueTestCase(TestCase): |  | ||||||
|     """Tests the :see:LocalizedValue class.""" |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def tearDown(): |  | ||||||
|         """Assures that the current language |  | ||||||
|         is set back to the default.""" |  | ||||||
|  |  | ||||||
|         translation.activate(settings.LANGUAGE_CODE) |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_init(): |  | ||||||
|         """Tests whether the __init__ function |  | ||||||
|         of the :see:LocalizedValue class works |  | ||||||
|         as expected.""" |  | ||||||
|  |  | ||||||
|         keys = get_init_values() |  | ||||||
|         value = LocalizedValue(keys) |  | ||||||
|  |  | ||||||
|         for lang_code, _ in settings.LANGUAGES: |  | ||||||
|             assert getattr(value, lang_code, None) == keys[lang_code] |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_init_default_values(): |  | ||||||
|         """Tests wehther the __init__ function |  | ||||||
|         of the :see:LocalizedValue accepts the |  | ||||||
|         default value or an empty dict properly.""" |  | ||||||
|  |  | ||||||
|         value = LocalizedValue() |  | ||||||
|  |  | ||||||
|         for lang_code, _ in settings.LANGUAGES: |  | ||||||
|             assert getattr(value, lang_code) is None |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_get_explicit(): |  | ||||||
|         """Tests whether the the :see:LocalizedValue |  | ||||||
|         class's :see:get function works properly |  | ||||||
|         when specifying an explicit value.""" |  | ||||||
|  |  | ||||||
|         keys = get_init_values() |  | ||||||
|         localized_value = LocalizedValue(keys) |  | ||||||
|  |  | ||||||
|         for language, value in keys.items(): |  | ||||||
|             assert localized_value.get(language) == value |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_get_default_language(): |  | ||||||
|         """Tests whether the :see:LocalizedValue |  | ||||||
|         class's see:get function properly |  | ||||||
|         gets the value in the default language.""" |  | ||||||
|  |  | ||||||
|         keys = get_init_values() |  | ||||||
|         localized_value = LocalizedValue(keys) |  | ||||||
|  |  | ||||||
|         for language, _ in keys.items(): |  | ||||||
|             translation.activate(language) |  | ||||||
|             assert localized_value.get() == keys[settings.LANGUAGE_CODE] |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_set(): |  | ||||||
|         """Tests whether the :see:LocalizedValue |  | ||||||
|         class's see:set function works properly.""" |  | ||||||
|  |  | ||||||
|         localized_value = LocalizedValue() |  | ||||||
|  |  | ||||||
|         for language, value in get_init_values(): |  | ||||||
|             localized_value.set(language, value) |  | ||||||
|             assert localized_value.get(language) == value |  | ||||||
|             assert getattr(localized_value, language) == value |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_str(): |  | ||||||
|         """Tests whether the :see:LocalizedValue |  | ||||||
|         class's __str__ works properly.""" |  | ||||||
|  |  | ||||||
|         keys = get_init_values() |  | ||||||
|         localized_value = LocalizedValue(keys) |  | ||||||
|  |  | ||||||
|         for language, value in keys.items(): |  | ||||||
|             translation.activate(language) |  | ||||||
|             assert str(localized_value) == value |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_eq(): |  | ||||||
|         """Tests whether the __eq__ operator |  | ||||||
|         of :see:LocalizedValue works properly.""" |  | ||||||
|  |  | ||||||
|         a = LocalizedValue({'en': 'a', 'ar': 'b'}) |  | ||||||
|         b = LocalizedValue({'en': 'a', 'ar': 'b'}) |  | ||||||
|  |  | ||||||
|         assert a == b |  | ||||||
|  |  | ||||||
|         b.en = 'b' |  | ||||||
|         assert a != b |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_str_fallback(): |  | ||||||
|         """Tests whether the :see:LocalizedValue |  | ||||||
|         class's __str__'s fallback functionality |  | ||||||
|         works properly.""" |  | ||||||
|  |  | ||||||
|         test_value = 'myvalue' |  | ||||||
|  |  | ||||||
|         localized_value = LocalizedValue({ |  | ||||||
|             settings.LANGUAGE_CODE: test_value |  | ||||||
|         }) |  | ||||||
|  |  | ||||||
|         other_language = settings.LANGUAGES[-1][0] |  | ||||||
|  |  | ||||||
|         # make sure that, by default it returns |  | ||||||
|         # the value in the default language |  | ||||||
|         assert str(localized_value) == test_value |  | ||||||
|  |  | ||||||
|         # make sure that it falls back to the |  | ||||||
|         # primary language when there's no value |  | ||||||
|         # available in the current language |  | ||||||
|         translation.activate(other_language) |  | ||||||
|         assert str(localized_value) == test_value |  | ||||||
|  |  | ||||||
|         # make sure that it's just __str__ falling |  | ||||||
|         # back and that for the other language |  | ||||||
|         # there's no actual value |  | ||||||
|         assert localized_value.get(other_language) != test_value |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_deconstruct(): |  | ||||||
|         """Tests whether the :see:LocalizedValue |  | ||||||
|         class's :see:deconstruct function works properly.""" |  | ||||||
|  |  | ||||||
|         keys = get_init_values() |  | ||||||
|         value = LocalizedValue(keys) |  | ||||||
|  |  | ||||||
|         path, args, kwargs = value.deconstruct() |  | ||||||
|  |  | ||||||
|         assert args[0] == keys |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_construct_string(): |  | ||||||
|         """Tests whether the :see:LocalizedValue's constructor |  | ||||||
|         assumes the primary language when passing a single string.""" |  | ||||||
|  |  | ||||||
|         value = LocalizedValue('beer') |  | ||||||
|         assert value.get(settings.LANGUAGE_CODE) == 'beer' |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class LocalizedFieldTestCase(TestCase): |  | ||||||
|     """Tests the :see:LocalizedField class.""" |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_from_db_value(): |  | ||||||
|         """Tests whether the :see:from_db_value function |  | ||||||
|         produces the expected :see:LocalizedValue.""" |  | ||||||
|  |  | ||||||
|         input_data = get_init_values() |  | ||||||
|         localized_value = LocalizedField().from_db_value(input_data) |  | ||||||
|  |  | ||||||
|         for lang_code, _ in settings.LANGUAGES: |  | ||||||
|             assert getattr(localized_value, lang_code) == input_data[lang_code] |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_from_db_value_none(): |  | ||||||
|         """Tests whether the :see:from_db_value function |  | ||||||
|         correctly handles None values.""" |  | ||||||
|  |  | ||||||
|         localized_value = LocalizedField().from_db_value(None) |  | ||||||
|  |  | ||||||
|         for lang_code, _ in settings.LANGUAGES: |  | ||||||
|             assert localized_value.get(lang_code) is None |  | ||||||
|  |  | ||||||
|     def test_from_db_value_none_return_none(self): |  | ||||||
|         """Tests whether the :see:from_db_value function |  | ||||||
|         correctly handles None values when LOCALIZED_FIELDS_EXPERIMENTAL |  | ||||||
|         is set to True.""" |  | ||||||
|  |  | ||||||
|         with self.settings(LOCALIZED_FIELDS_EXPERIMENTAL=True): |  | ||||||
|             localized_value = LocalizedField.from_db_value(None) |  | ||||||
|  |  | ||||||
|         assert localized_value is None |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_to_python(): |  | ||||||
|         """Tests whether the :see:to_python function |  | ||||||
|         produces the expected :see:LocalizedValue.""" |  | ||||||
|  |  | ||||||
|         input_data = get_init_values() |  | ||||||
|         localized_value = LocalizedField().to_python(input_data) |  | ||||||
|  |  | ||||||
|         for language, value in input_data.items(): |  | ||||||
|             assert localized_value.get(language) == value |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_to_python_none(): |  | ||||||
|         """Tests whether the :see:to_python function |  | ||||||
|         produces the expected :see:LocalizedValue |  | ||||||
|         instance when it is passes None.""" |  | ||||||
|  |  | ||||||
|         localized_value = LocalizedField().to_python(None) |  | ||||||
|         assert localized_value |  | ||||||
|  |  | ||||||
|         for lang_code, _ in settings.LANGUAGES: |  | ||||||
|             assert localized_value.get(lang_code) is None |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_to_python_non_dict(): |  | ||||||
|         """Tests whether the :see:to_python function produces |  | ||||||
|         the expected :see:LocalizedValue when it is |  | ||||||
|         passed a non-dictionary value.""" |  | ||||||
|  |  | ||||||
|         localized_value = LocalizedField().to_python(list()) |  | ||||||
|         assert localized_value |  | ||||||
|  |  | ||||||
|         for lang_code, _ in settings.LANGUAGES: |  | ||||||
|             assert localized_value.get(lang_code) is None |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_get_prep_value(): |  | ||||||
|         """"Tests whether the :see:get_prep_value function |  | ||||||
|         produces the expected dictionary.""" |  | ||||||
|  |  | ||||||
|         input_data = get_init_values() |  | ||||||
|         localized_value = LocalizedValue(input_data) |  | ||||||
|  |  | ||||||
|         output_data = LocalizedField().get_prep_value(localized_value) |  | ||||||
|  |  | ||||||
|         for language, value in input_data.items(): |  | ||||||
|             assert language in output_data |  | ||||||
|             assert output_data.get(language) == value |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_get_prep_value_none(): |  | ||||||
|         """Tests whether the :see:get_prep_value function |  | ||||||
|         produces the expected output when it is passed None.""" |  | ||||||
|  |  | ||||||
|         output_data = LocalizedField().get_prep_value(None) |  | ||||||
|         assert not output_data |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_get_prep_value_no_localized_value(): |  | ||||||
|         """Tests whether the :see:get_prep_value function |  | ||||||
|         produces the expected output when it is passed a |  | ||||||
|         non-LocalizedValue value.""" |  | ||||||
|  |  | ||||||
|         output_data = LocalizedField().get_prep_value(['huh']) |  | ||||||
|         assert not output_data |  | ||||||
|  |  | ||||||
|     def test_get_prep_value_clean(self): |  | ||||||
|         """Tests whether the :see:get_prep_value produces |  | ||||||
|         None as the output when it is passed an empty, but |  | ||||||
|         valid LocalizedValue value but, only when null=True.""" |  | ||||||
|  |  | ||||||
|         localized_value = LocalizedValue() |  | ||||||
|  |  | ||||||
|         with self.assertRaises(IntegrityError): |  | ||||||
|             LocalizedField(null=False).get_prep_value(localized_value) |  | ||||||
|  |  | ||||||
|         assert not LocalizedField(null=True).get_prep_value(localized_value) |  | ||||||
|         assert not LocalizedField().clean(None) |  | ||||||
|         assert not LocalizedField().clean(['huh']) |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_formfield(): |  | ||||||
|         """Tests whether the :see:formfield function |  | ||||||
|         correctly returns a valid form.""" |  | ||||||
|  |  | ||||||
|         assert isinstance( |  | ||||||
|             LocalizedField().formfield(), |  | ||||||
|             LocalizedFieldForm |  | ||||||
|         ) |  | ||||||
| @@ -7,10 +7,11 @@ from django import forms | |||||||
| from django.test import TestCase, override_settings | from django.test import TestCase, override_settings | ||||||
| from django.core.files.base import File, ContentFile | from django.core.files.base import File, ContentFile | ||||||
| from django.core.files import temp as tempfile | from django.core.files import temp as tempfile | ||||||
| from localized_fields import LocalizedFileField, LocalizedValue | from localized_fields.fields import LocalizedFileField | ||||||
|  | from localized_fields.value import LocalizedValue | ||||||
| from localized_fields.fields.localized_file_field import LocalizedFieldFile | from localized_fields.fields.localized_file_field import LocalizedFieldFile | ||||||
| from localized_fields.forms import LocalizedFileFieldForm | from localized_fields.forms import LocalizedFileFieldForm | ||||||
| from localized_fields.localized_value import LocalizedFileValue | from localized_fields.value import LocalizedFileValue | ||||||
| from localized_fields.widgets import LocalizedFileWidget | from localized_fields.widgets import LocalizedFileWidget | ||||||
| from .fake_model import get_fake_model | from .fake_model import get_fake_model | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
| 
 | 
 | ||||||
| from localized_fields import LocalizedField, LocalizedValue | from localized_fields.fields import LocalizedField | ||||||
|  | from localized_fields.value import LocalizedValue | ||||||
| 
 | 
 | ||||||
| from .fake_model import get_fake_model | from .fake_model import get_fake_model | ||||||
| 
 | 
 | ||||||
| @@ -33,7 +34,6 @@ class LocalizedModelTestCase(TestCase): | |||||||
| 
 | 
 | ||||||
|         assert isinstance(obj.title, LocalizedValue) |         assert isinstance(obj.title, LocalizedValue) | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def test_model_init_kwargs(cls): |     def test_model_init_kwargs(cls): | ||||||
|         """Tests whether all :see:LocalizedField |         """Tests whether all :see:LocalizedField | ||||||
| @@ -4,10 +4,14 @@ from django import forms | |||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
| from django.db.utils import IntegrityError | from django.db.utils import IntegrityError | ||||||
| from localized_fields import (LocalizedField, LocalizedAutoSlugField, |  | ||||||
|                               LocalizedUniqueSlugField) |  | ||||||
| from django.utils.text import slugify | from django.utils.text import slugify | ||||||
| 
 | 
 | ||||||
|  | from localized_fields.fields import ( | ||||||
|  |     LocalizedField, | ||||||
|  |     LocalizedAutoSlugField, | ||||||
|  |     LocalizedUniqueSlugField | ||||||
|  | ) | ||||||
|  | 
 | ||||||
| from .fake_model import get_fake_model | from .fake_model import get_fake_model | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
							
								
								
									
										164
									
								
								tests/test_value.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								tests/test_value.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,164 @@ | |||||||
|  | from django.conf import settings | ||||||
|  | from django.test import TestCase | ||||||
|  | from django.utils import translation | ||||||
|  |  | ||||||
|  | from localized_fields.value import LocalizedValue | ||||||
|  |  | ||||||
|  | from .data import get_init_values | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedValueTestCase(TestCase): | ||||||
|  |     """Tests the :see:LocalizedValue class.""" | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def tearDown(): | ||||||
|  |         """Assures that the current language | ||||||
|  |         is set back to the default.""" | ||||||
|  |  | ||||||
|  |         translation.activate(settings.LANGUAGE_CODE) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_init(): | ||||||
|  |         """Tests whether the __init__ function | ||||||
|  |         of the :see:LocalizedValue class works | ||||||
|  |         as expected.""" | ||||||
|  |  | ||||||
|  |         keys = get_init_values() | ||||||
|  |         value = LocalizedValue(keys) | ||||||
|  |  | ||||||
|  |         for lang_code, _ in settings.LANGUAGES: | ||||||
|  |             assert getattr(value, lang_code, None) == keys[lang_code] | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_init_default_values(): | ||||||
|  |         """Tests whether the __init__ function | ||||||
|  |         of the :see:LocalizedValue accepts the | ||||||
|  |         default value or an empty dict properly.""" | ||||||
|  |  | ||||||
|  |         value = LocalizedValue() | ||||||
|  |  | ||||||
|  |         for lang_code, _ in settings.LANGUAGES: | ||||||
|  |             assert getattr(value, lang_code) is None | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_init_array(): | ||||||
|  |         """Tests whether the __init__ function | ||||||
|  |         of :see:LocalizedValue properly handles an | ||||||
|  |         array. | ||||||
|  |  | ||||||
|  |         Arrays can be passed to LocalizedValue as | ||||||
|  |         a result of a ArrayAgg operation.""" | ||||||
|  |  | ||||||
|  |         value = LocalizedValue(['my value']) | ||||||
|  |         assert value.get(settings.LANGUAGE_CODE) == 'my value' | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_get_explicit(): | ||||||
|  |         """Tests whether the the :see:LocalizedValue | ||||||
|  |         class's :see:get function works properly | ||||||
|  |         when specifying an explicit value.""" | ||||||
|  |  | ||||||
|  |         keys = get_init_values() | ||||||
|  |         localized_value = LocalizedValue(keys) | ||||||
|  |  | ||||||
|  |         for language, value in keys.items(): | ||||||
|  |             assert localized_value.get(language) == value | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_get_default_language(): | ||||||
|  |         """Tests whether the :see:LocalizedValue | ||||||
|  |         class's see:get function properly | ||||||
|  |         gets the value in the default language.""" | ||||||
|  |  | ||||||
|  |         keys = get_init_values() | ||||||
|  |         localized_value = LocalizedValue(keys) | ||||||
|  |  | ||||||
|  |         for language, _ in keys.items(): | ||||||
|  |             translation.activate(language) | ||||||
|  |             assert localized_value.get() == keys[settings.LANGUAGE_CODE] | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_set(): | ||||||
|  |         """Tests whether the :see:LocalizedValue | ||||||
|  |         class's see:set function works properly.""" | ||||||
|  |  | ||||||
|  |         localized_value = LocalizedValue() | ||||||
|  |  | ||||||
|  |         for language, value in get_init_values(): | ||||||
|  |             localized_value.set(language, value) | ||||||
|  |             assert localized_value.get(language) == value | ||||||
|  |             assert getattr(localized_value, language) == value | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_str(): | ||||||
|  |         """Tests whether the :see:LocalizedValue | ||||||
|  |         class's __str__ works properly.""" | ||||||
|  |  | ||||||
|  |         keys = get_init_values() | ||||||
|  |         localized_value = LocalizedValue(keys) | ||||||
|  |  | ||||||
|  |         for language, value in keys.items(): | ||||||
|  |             translation.activate(language) | ||||||
|  |             assert str(localized_value) == value | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_eq(): | ||||||
|  |         """Tests whether the __eq__ operator | ||||||
|  |         of :see:LocalizedValue works properly.""" | ||||||
|  |  | ||||||
|  |         a = LocalizedValue({'en': 'a', 'ar': 'b'}) | ||||||
|  |         b = LocalizedValue({'en': 'a', 'ar': 'b'}) | ||||||
|  |  | ||||||
|  |         assert a == b | ||||||
|  |  | ||||||
|  |         b.en = 'b' | ||||||
|  |         assert a != b | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_str_fallback(): | ||||||
|  |         """Tests whether the :see:LocalizedValue | ||||||
|  |         class's __str__'s fallback functionality | ||||||
|  |         works properly.""" | ||||||
|  |  | ||||||
|  |         test_value = 'myvalue' | ||||||
|  |  | ||||||
|  |         localized_value = LocalizedValue({ | ||||||
|  |             settings.LANGUAGE_CODE: test_value | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |         other_language = settings.LANGUAGES[-1][0] | ||||||
|  |  | ||||||
|  |         # make sure that, by default it returns | ||||||
|  |         # the value in the default language | ||||||
|  |         assert str(localized_value) == test_value | ||||||
|  |  | ||||||
|  |         # make sure that it falls back to the | ||||||
|  |         # primary language when there's no value | ||||||
|  |         # available in the current language | ||||||
|  |         translation.activate(other_language) | ||||||
|  |         assert str(localized_value) == test_value | ||||||
|  |  | ||||||
|  |         # make sure that it's just __str__ falling | ||||||
|  |         # back and that for the other language | ||||||
|  |         # there's no actual value | ||||||
|  |         assert localized_value.get(other_language) != test_value | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_deconstruct(): | ||||||
|  |         """Tests whether the :see:LocalizedValue | ||||||
|  |         class's :see:deconstruct function works properly.""" | ||||||
|  |  | ||||||
|  |         keys = get_init_values() | ||||||
|  |         value = LocalizedValue(keys) | ||||||
|  |  | ||||||
|  |         path, args, kwargs = value.deconstruct() | ||||||
|  |  | ||||||
|  |         assert args[0] == keys | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_construct_string(): | ||||||
|  |         """Tests whether the :see:LocalizedValue's constructor | ||||||
|  |         assumes the primary language when passing a single string.""" | ||||||
|  |  | ||||||
|  |         value = LocalizedValue('beer') | ||||||
|  |         assert value.get(settings.LANGUAGE_CODE) == 'beer' | ||||||
| @@ -1,7 +1,8 @@ | |||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
| 
 | 
 | ||||||
| from localized_fields import LocalizedFieldWidget, LocalizedValue | from localized_fields.value import LocalizedValue | ||||||
|  | from localized_fields.widgets import LocalizedFieldWidget | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class LocalizedFieldWidgetTestCase(TestCase): | class LocalizedFieldWidgetTestCase(TestCase): | ||||||
		Reference in New Issue
	
	Block a user