mirror of
				https://github.com/SectorLabs/django-localized-fields.git
				synced 2025-10-28 17:58:58 +03:00 
			
		
		
		
	Compare commits
	
		
			87 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 3ce57ed4cc | ||
|  | 7316d312b4 | ||
|  | 16e23963cc | ||
|  | b10472d3e9 | ||
|  | 833ceb849c | ||
|  | d7382fbf30 | ||
|  | 8ad9268426 | ||
|  | 96ddc77cfc | ||
|  | 51fc6959d2 | ||
|  | 3b28a5e707 | ||
|  | 06873afbda | ||
|  | f7c14f897c | ||
|  | e5e3068835 | ||
|  | de7d3b9a21 | ||
|  | fd47deaa2e | ||
|  | 4b6d997ddd | ||
|  | 68d6991608 | ||
|  | e5d7cd25e2 | ||
|  | 236ce1648c | ||
|  | aacc712195 | ||
|  | d1790f1fc1 | ||
|  | c75c1764e2 | ||
|  | 6736b3b32d | ||
|  | b97a7f3c23 | ||
|  | 1b036dc1de | ||
|  | ea7733670d | ||
|  | a8dc4fe837 | ||
|  | 3c8bea0fc3 | ||
|  | 2741a6a2a2 | ||
|  | 06f7ee15f0 | ||
|  | e5214b07ae | ||
|  | 2d5fe0be05 | ||
|  | 4305696f1b | ||
|  | 8c73c9ab77 | ||
|  | 92a53bc3d7 | ||
|  | 5a4f449363 | ||
|  | 0fa79ddbb0 | ||
|  | 2205f9c6a4 | ||
|  | 84c267330f | ||
|  | a1a02552b7 | ||
|  | bb84d7577c | ||
|  | 5e0343801f | ||
|  | 2df2ec8b36 | ||
|  | 3fcaece894 | ||
|  | 093a9d58f2 | ||
|  | cff22855c2 | ||
|  | db93b93046 | ||
|  | a352388243 | ||
|  | bf2995fd27 | ||
|  | 8ba08c389c | ||
|  | f1798b0cc6 | ||
|  | fc80462ce7 | ||
|  | 24079a2fcb | ||
|  | 0f4c74a9b2 | ||
|  | 366dceaddc | ||
|  | 5b93c5ec8f | ||
|  | 817c7e13fe | ||
|  | 8591af1f2a | ||
|  | c55001ac12 | ||
|  | 23c6f975d8 | ||
|  | a9037b603a | ||
|  | 8b08e5a467 | ||
|  | bf90131e4f | ||
|  | 8bdfee0666 | ||
|  | add3c1e1d4 | ||
|  | 8ba33695f9 | ||
|  | 78594541e1 | ||
|  | 52145ca7d3 | ||
|  | 5df44b0d62 | ||
|  | 5aef1d791b | ||
|  | 03df76d6d7 | ||
|  | 465e35ba8a | ||
|  | 9278e99b18 | ||
|  | 9754134298 | ||
|  | f0c7a72078 | ||
|  | 7f4dfbae1f | ||
|  | d07da55215 | ||
|  | 340dde18cd | ||
|  | 3951266747 | ||
|  | b5f4c43d6b | ||
|  | 3d08475468 | ||
|  | b3d7092b91 | ||
|  | 97a785e9b0 | ||
|  | 97c14fd2ba | ||
|  | 6cb4cdf52e | ||
|  | bcb2ff0092 | ||
|  | 43a48403e9 | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -4,6 +4,7 @@ env/ | |||||||
| # Ignore Python byte code cache | # Ignore Python byte code cache | ||||||
| *.pyc | *.pyc | ||||||
| __pycache__ | __pycache__ | ||||||
|  | .cache/ | ||||||
|  |  | ||||||
| # Ignore coverage reports | # Ignore coverage reports | ||||||
| .coverage | .coverage | ||||||
|   | |||||||
| @@ -1,2 +1,5 @@ | |||||||
| include LICENSE | include LICENSE | ||||||
| include README.rst | include README.rst | ||||||
|  | recursive-include localized_fields/static * | ||||||
|  | recursive-include localized_fields/templates * | ||||||
|  | recursive-exclude tests * | ||||||
|   | |||||||
							
								
								
									
										159
									
								
								README.rst
									
									
									
									
									
								
							
							
						
						
									
										159
									
								
								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``: | ||||||
| @@ -62,15 +62,14 @@ Usage | |||||||
|  |  | ||||||
| Preparation | Preparation | ||||||
| ^^^^^^^^^^^ | ^^^^^^^^^^^ | ||||||
| Inherit your model from ``LocalizedModel`` and declare fields on your model as ``LocalizedField``: | Declare fields on your model as ``LocalizedField``: | ||||||
|  |  | ||||||
| .. code-block:: python | .. code-block:: python | ||||||
|  |  | ||||||
|      from localized_fields.models import LocalizedModel |  | ||||||
|      from localized_fields.fields import LocalizedField |      from localized_fields.fields import LocalizedField | ||||||
|  |  | ||||||
|  |  | ||||||
|      class MyModel(LocalizedModel): |      class MyModel(models.Model): | ||||||
|          title = LocalizedField() |          title = LocalizedField() | ||||||
|  |  | ||||||
| ``django-localized-fields`` integrates with Django's i18n system, in order for certain languages to be available you have to correctly configure the ``LANGUAGES`` and ``LANGUAGE_CODE`` settings: | ``django-localized-fields`` integrates with Django's i18n system, in order for certain languages to be available you have to correctly configure the ``LANGUAGES`` and ``LANGUAGE_CODE`` settings: | ||||||
| @@ -136,14 +135,14 @@ At the moment, it is not possible to select two languages to be marked as requir | |||||||
|  |  | ||||||
|     .. code-block:: python |     .. code-block:: python | ||||||
|  |  | ||||||
|         class MyModel(LocalizedModel): |         class MyModel(models.Model): | ||||||
|             title = LocalizedField(required=True) |             title = LocalizedField(required=True) | ||||||
|  |  | ||||||
| * Make all languages optional: | * Make all languages optional: | ||||||
|  |  | ||||||
|     .. code-block:: python |     .. code-block:: python | ||||||
|  |  | ||||||
|         class MyModel(LocalizedModel): |         class MyModel(models.Model): | ||||||
|             title = LocalizedField(null=True) |             title = LocalizedField(null=True) | ||||||
|  |  | ||||||
| **Uniqueness** | **Uniqueness** | ||||||
| @@ -154,32 +153,32 @@ By default the values stored in a ``LocalizedField`` are *not unique*. You can e | |||||||
|  |  | ||||||
|     .. code-block:: python |     .. code-block:: python | ||||||
|  |  | ||||||
|         class MyModel(LocalizedModel): |         class MyModel(models.Model): | ||||||
|             title = LocalizedField(uniqueness=['en', 'ro']) |             title = LocalizedField(uniqueness=['en', 'ro']) | ||||||
|  |  | ||||||
| * Enforce uniqueness for **all** languages: | * Enforce uniqueness for **all** languages: | ||||||
|  |  | ||||||
|     .. code-block:: python |     .. code-block:: python | ||||||
|  |  | ||||||
|         from localized_fields import get_language_codes |         from localized_fields.util import get_language_codes | ||||||
|  |  | ||||||
|         class MyModel(LocalizedModel): |         class MyModel(models.Model): | ||||||
|             title = LocalizedField(uniqueness=get_language_codes()) |             title = LocalizedField(uniqueness=get_language_codes()) | ||||||
|  |  | ||||||
| * Enforce uniqueness for one ore more languages **together** (similar to Django's ``unique_together``): | * Enforce uniqueness for one ore more languages **together** (similar to Django's ``unique_together``): | ||||||
|  |  | ||||||
|     .. code-block:: python |     .. code-block:: python | ||||||
|  |  | ||||||
|         class MyModel(LocalizedModel): |         class MyModel(models.Model): | ||||||
|             title = LocalizedField(uniqueness=[('en', 'ro')]) |             title = LocalizedField(uniqueness=[('en', 'ro')]) | ||||||
|  |  | ||||||
| * Enforce uniqueness for **all** languages **together**: | * Enforce uniqueness for **all** languages **together**: | ||||||
|  |  | ||||||
|     .. code-block:: python |     .. code-block:: python | ||||||
|  |  | ||||||
|         from localized_fields import get_language_codes |         from localized_fields.util import get_language_codes | ||||||
|  |  | ||||||
|         class MyModel(LocalizedModel): |         class MyModel(models.Model): | ||||||
|             title = LocalizedField(uniqueness=[(*get_language_codes())]) |             title = LocalizedField(uniqueness=[(*get_language_codes())]) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -193,15 +192,36 @@ Besides ``LocalizedField``, there's also: | |||||||
|  |  | ||||||
|           .. code-block:: python |           .. code-block:: python | ||||||
|  |  | ||||||
|               from localized_fields import (LocalizedModel, |               from localized_fields.models import LocalizedModel | ||||||
|                                             AtomicSlugRetryMixin, |               from localized_fields.fields import LocalizedField, LocalizedUniqueSlugField | ||||||
|                                             LocalizedField, |  | ||||||
|                                             LocalizedUniqueSlugField) |  | ||||||
|  |  | ||||||
|               class MyModel(AtomicSlugRetryMixin, LocalizedModel): |               class MyModel(LocalizedModel): | ||||||
|                    title = LocalizedField() |                    title = LocalizedField() | ||||||
|                    slug = LocalizedUniqueSlugField(populate_from='title') |                    slug = LocalizedUniqueSlugField(populate_from='title') | ||||||
|  |  | ||||||
|  |     ``populate_from`` can be: | ||||||
|  |  | ||||||
|  |         - The name of a field. | ||||||
|  |  | ||||||
|  |            .. code-block:: python | ||||||
|  |  | ||||||
|  |                slug = LocalizedUniqueSlugField(populate_from='name', include_time=True) | ||||||
|  |  | ||||||
|  |         - A callable. | ||||||
|  |  | ||||||
|  |            .. code-block:: python | ||||||
|  |  | ||||||
|  |                def generate_slug(instance): | ||||||
|  |                    return instance.title | ||||||
|  |  | ||||||
|  |                slug = LocalizedUniqueSlugField(populate_from=generate_slug, include_time=True) | ||||||
|  |  | ||||||
|  |         - A tuple of names of fields. | ||||||
|  |  | ||||||
|  |            .. code-block:: python | ||||||
|  |  | ||||||
|  |                slug = LocalizedUniqueSlugField(populate_from=('name', 'beer') include_time=True) | ||||||
|  |  | ||||||
|     By setting the option ``include_time=True`` |     By setting the option ``include_time=True`` | ||||||
|  |  | ||||||
|           .. code-block:: python |           .. code-block:: python | ||||||
| @@ -211,39 +231,102 @@ Besides ``LocalizedField``, there's also: | |||||||
|     You can instruct the field to include a part of the current time into |     You can instruct the field to include a part of the current time into | ||||||
|     the resulting slug. This is useful if you're running into a lot of collisions. |     the resulting slug. This is useful if you're running into a lot of collisions. | ||||||
|  |  | ||||||
| * ``LocalizedAutoSlugField`` |  | ||||||
|      Automatically creates a slug for every language from the specified field. |  | ||||||
|  |  | ||||||
|      Currently only supports ``populate_from``. Example usage: |  | ||||||
|  |  | ||||||
|           .. code-block:: python |  | ||||||
|  |  | ||||||
|               from localized_fields import (LocalizedModel, |  | ||||||
|                                             LocalizedField, |  | ||||||
|                                             LocalizedUniqueSlugField) |  | ||||||
|  |  | ||||||
|               class MyModel(LocalizedModel): |  | ||||||
|                    title = LocalizedField() |  | ||||||
|                    slug = LocalizedAutoSlugField(populate_from='title') |  | ||||||
|  |  | ||||||
|      This implementation is **NOT** concurrency safe, prefer ``LocalizedUniqueSlugField``. |  | ||||||
|  |  | ||||||
| * ``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 (LocalizedModel, |               from localized_fields.fields import LocalizedField, LocalizedBleachField | ||||||
|                                             LocalizedField, |  | ||||||
|                                             LocalizedBleachField) |  | ||||||
|  |  | ||||||
|               class MyModel(LocalizedModel): |               class MyModel(models.Model): | ||||||
|                    title = LocalizedField() |                    title = LocalizedField() | ||||||
|                    description = LocalizedBleachField() |                    description = LocalizedBleachField() | ||||||
|  |  | ||||||
|  | * ``LocalizedCharField`` and ``LocalizedTextField`` | ||||||
|  |     This fields following the Django convention for string-based fields use the empty string as value for “no data”, not NULL. | ||||||
|  |     ``LocalizedCharField`` uses ``TextInput`` (``<input type="text">``) widget for render. | ||||||
|  |  | ||||||
|  |     Example usage: | ||||||
|  |  | ||||||
|  |            .. code-block:: python | ||||||
|  |  | ||||||
|  |               from localized_fields import (LocalizedCharField, | ||||||
|  |                                             LocalizedTextField) | ||||||
|  |  | ||||||
|  |               class MyModel(models.Model): | ||||||
|  |                    title = LocalizedCharField() | ||||||
|  |                    description = LocalizedTextField() | ||||||
|  |  | ||||||
|  | * ``LocalizedFileField`` | ||||||
|  |     A file-upload field | ||||||
|  |  | ||||||
|  |     Parameter ``upload_to`` supports ``lang`` parameter for string formatting or as function argument (in case if ``upload_to`` is callable). | ||||||
|  |  | ||||||
|  |     Example usage: | ||||||
|  |  | ||||||
|  |            .. code-block:: python | ||||||
|  |  | ||||||
|  |               from localized_fields import LocalizedFileField | ||||||
|  |  | ||||||
|  |               def my_directory_path(instance, filename, lang): | ||||||
|  |                 # file will be uploaded to MEDIA_ROOT/<lang>/<id>_<filename> | ||||||
|  |                 return '{0}/{0}_{1}'.format(lang, instance.id, filename) | ||||||
|  |  | ||||||
|  |               class MyModel(models.Model): | ||||||
|  |                    file1 = LocalizedFileField(upload_to='uploads/{lang}/') | ||||||
|  |                    file2 = LocalizedFileField(upload_to=my_directory_path) | ||||||
|  |  | ||||||
|  |     In template you can access to file attributes: | ||||||
|  |  | ||||||
|  |             .. code-block:: django | ||||||
|  |  | ||||||
|  |               {# For current active language: #} | ||||||
|  |  | ||||||
|  |               {{ model.file.url }}  {# output file url #} | ||||||
|  |               {{ model.file.name }} {# output file name #} | ||||||
|  |  | ||||||
|  |               {# Or get it in a specific language: #} | ||||||
|  |  | ||||||
|  |               {{ model.file.ro.url }}  {# output file url for romanian language #} | ||||||
|  |               {{ model.file.ro.name }} {# output file name for romanian language #} | ||||||
|  |  | ||||||
|  |     To get access to file instance for current active language use ``localized`` method: | ||||||
|  |  | ||||||
|  |             .. code-block:: python | ||||||
|  |  | ||||||
|  |               model.file.localized() | ||||||
|  |  | ||||||
|  | Experimental feature | ||||||
|  | ^^^^^^^^^^^^^^^^^^^^ | ||||||
|  | Enables the following experimental features: | ||||||
|  |     * ``LocalizedField`` will return ``None`` instead of an empty ``LocalizedValue`` if there is no database value. | ||||||
|  |  | ||||||
|  | .. code-block:: python | ||||||
|  |  | ||||||
|  |      LOCALIZED_FIELDS_EXPERIMENTAL = True | ||||||
|  |  | ||||||
|  |  | ||||||
|  | Django Admin Integration | ||||||
|  | ^^^^^^^^^^^^^^^^^^^^^^^^ | ||||||
|  | To enable widgets in the admin, you need to inherit from ``LocalizedFieldsAdminMixin``: | ||||||
|  |  | ||||||
|  | .. code-block:: python | ||||||
|  |  | ||||||
|  |     from django.contrib import admin | ||||||
|  |     from myapp.models import MyLocalizedModel | ||||||
|  |  | ||||||
|  |     from localized_fields.admin import LocalizedFieldsAdminMixin | ||||||
|  |  | ||||||
|  |     class MyLocalizedModelAdmin(LocalizedFieldsAdminMixin, admin.ModelAdmin): | ||||||
|  |         """Any admin options you need go here""" | ||||||
|  |  | ||||||
|  |     admin.site.register(MyLocalizedModel, MyLocalizedModelAdmin) | ||||||
|  |  | ||||||
|  |  | ||||||
| Frequently asked questions (FAQ) | Frequently asked questions (FAQ) | ||||||
| -------------------------------- | -------------------------------- | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,20 +0,0 @@ | |||||||
| from .util import get_language_codes |  | ||||||
| from .forms import LocalizedFieldForm, LocalizedFieldWidget |  | ||||||
| from .fields import (LocalizedField, LocalizedBleachField, |  | ||||||
|                      LocalizedAutoSlugField, LocalizedUniqueSlugField) |  | ||||||
| from .mixins import AtomicSlugRetryMixin |  | ||||||
| from .models import LocalizedModel |  | ||||||
| from .localized_value import LocalizedValue |  | ||||||
|  |  | ||||||
| __all__ = [ |  | ||||||
|     'get_language_codes', |  | ||||||
|     'LocalizedField', |  | ||||||
|     'LocalizedValue', |  | ||||||
|     'LocalizedAutoSlugField', |  | ||||||
|     'LocalizedUniqueSlugField', |  | ||||||
|     'LocalizedBleachField', |  | ||||||
|     'LocalizedFieldWidget', |  | ||||||
|     'LocalizedFieldForm', |  | ||||||
|     'LocalizedModel', |  | ||||||
|     'AtomicSlugRetryMixin' |  | ||||||
| ] |  | ||||||
|   | |||||||
							
								
								
									
										36
									
								
								localized_fields/admin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								localized_fields/admin.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | |||||||
|  | from django.contrib.admin import ModelAdmin | ||||||
|  |  | ||||||
|  | from . import widgets | ||||||
|  | from .fields import LocalizedField, LocalizedCharField, LocalizedTextField, \ | ||||||
|  |     LocalizedFileField | ||||||
|  |  | ||||||
|  |  | ||||||
|  | FORMFIELD_FOR_LOCALIZED_FIELDS_DEFAULTS = { | ||||||
|  |     LocalizedField: {'widget': widgets.AdminLocalizedFieldWidget}, | ||||||
|  |     LocalizedCharField: {'widget': widgets.AdminLocalizedCharFieldWidget}, | ||||||
|  |     LocalizedTextField: {'widget': widgets.AdminLocalizedFieldWidget}, | ||||||
|  |     LocalizedFileField: {'widget': widgets.AdminLocalizedFileFieldWidget}, | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedFieldsAdminMixin(ModelAdmin): | ||||||
|  |     """Mixin for making the fancy widgets work in Django Admin.""" | ||||||
|  |  | ||||||
|  |     class Media: | ||||||
|  |         css = { | ||||||
|  |             'all': ( | ||||||
|  |                 'localized_fields/localized-fields-admin.css', | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         js = ( | ||||||
|  |             'localized_fields/localized-fields-admin.js', | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def __init__(self, *args, **kwargs): | ||||||
|  |         """Initializes a new instance of :see:LocalizedFieldsAdminMixin.""" | ||||||
|  |  | ||||||
|  |         super(LocalizedFieldsAdminMixin, self).__init__(*args, **kwargs) | ||||||
|  |         overrides = FORMFIELD_FOR_LOCALIZED_FIELDS_DEFAULTS.copy() | ||||||
|  |         overrides.update(self.formfield_overrides) | ||||||
|  |         self.formfield_overrides = 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,12 +1,24 @@ | |||||||
| 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_bleach_field import LocalizedBleachField | from .char_field import LocalizedCharField | ||||||
|  | from .text_field import LocalizedTextField | ||||||
|  | from .file_field import LocalizedFileField | ||||||
|  |  | ||||||
|  |  | ||||||
| __all__ = [ | __all__ = [ | ||||||
|     'LocalizedField', |     'LocalizedField', | ||||||
|     'LocalizedAutoSlugField', |     'LocalizedAutoSlugField', | ||||||
|     'LocalizedUniqueSlugField', |     'LocalizedUniqueSlugField', | ||||||
|     'LocalizedBleachField', |     'LocalizedCharField', | ||||||
|  |     'LocalizedTextField', | ||||||
|  |     'LocalizedFileField' | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | try: | ||||||
|  |     from .bleach_field import LocalizedBleachField | ||||||
|  |     __all__ += [ | ||||||
|  |         'LocalizedBleachField' | ||||||
|  |     ] | ||||||
|  | except ImportError: | ||||||
|  |     pass | ||||||
|   | |||||||
| @@ -1,12 +1,16 @@ | |||||||
| from typing import Callable | import warnings | ||||||
|  | 
 | ||||||
|  | from typing import Callable, Tuple, Union | ||||||
| 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 import translation | ||||||
| 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 | ||||||
|  | from ..util import resolve_object_property | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class LocalizedAutoSlugField(LocalizedField): | class LocalizedAutoSlugField(LocalizedField): | ||||||
| @@ -19,6 +23,11 @@ class LocalizedAutoSlugField(LocalizedField): | |||||||
|         self.populate_from = kwargs.pop('populate_from', None) |         self.populate_from = kwargs.pop('populate_from', None) | ||||||
|         self.include_time = kwargs.pop('include_time', False) |         self.include_time = kwargs.pop('include_time', False) | ||||||
| 
 | 
 | ||||||
|  |         warnings.warn( | ||||||
|  |             'LocalizedAutoSlug is deprecated and will be removed in the next major version.', | ||||||
|  |             DeprecationWarning | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|         super(LocalizedAutoSlugField, self).__init__( |         super(LocalizedAutoSlugField, self).__init__( | ||||||
|             *args, |             *args, | ||||||
|             **kwargs |             **kwargs | ||||||
| @@ -69,13 +78,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,8 +131,32 @@ 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: Union[str, Tuple[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. | ||||||
| 
 | 
 | ||||||
|         Arguments: |         Arguments: | ||||||
| @@ -146,5 +173,20 @@ class LocalizedAutoSlugField(LocalizedField): | |||||||
|             The text to generate a slug for. |             The text to generate a slug for. | ||||||
|         """ |         """ | ||||||
| 
 | 
 | ||||||
|         value = getattr(instance, field_name, None) |         if callable(field_name): | ||||||
|         return value.get(language) |             return field_name(instance) | ||||||
|  | 
 | ||||||
|  |         def get_field_value(name): | ||||||
|  |             value = resolve_object_property(instance, name) | ||||||
|  |             with translation.override(language): | ||||||
|  |                 return str(value) | ||||||
|  | 
 | ||||||
|  |         if isinstance(field_name, tuple) or isinstance(field_name, list): | ||||||
|  |             value = '-'.join([ | ||||||
|  |                 value | ||||||
|  |                 for value in [get_field_value(name) for name in field_name] | ||||||
|  |                 if value | ||||||
|  |             ]) | ||||||
|  |             return value | ||||||
|  | 
 | ||||||
|  |         return get_field_value(field_name) | ||||||
| @@ -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): | ||||||
							
								
								
									
										16
									
								
								localized_fields/fields/char_field.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								localized_fields/fields/char_field.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | from ..forms import LocalizedCharFieldForm | ||||||
|  | from .field import LocalizedField | ||||||
|  | from ..value import LocalizedStringValue | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedCharField(LocalizedField): | ||||||
|  |     attr_class = LocalizedStringValue | ||||||
|  |  | ||||||
|  |     def formfield(self, **kwargs): | ||||||
|  |         """Gets the form field associated with this field.""" | ||||||
|  |         defaults = { | ||||||
|  |             'form_class': LocalizedCharFieldForm | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         defaults.update(kwargs) | ||||||
|  |         return super().formfield(**defaults) | ||||||
| @@ -1,9 +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 localized_fields import LocalizedFieldForm | 
 | ||||||
| from psqlextra.fields import HStoreField | from psqlextra.fields import HStoreField | ||||||
| 
 | 
 | ||||||
| from ..localized_value import LocalizedValue | from ..forms import LocalizedFieldForm | ||||||
|  | from ..value import LocalizedValue | ||||||
|  | from ..descriptor import LocalizedValueDescriptor | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class LocalizedField(HStoreField): | class LocalizedField(HStoreField): | ||||||
| @@ -14,14 +20,33 @@ class LocalizedField(HStoreField): | |||||||
| 
 | 
 | ||||||
|     Meta = None |     Meta = None | ||||||
| 
 | 
 | ||||||
|  |     # The class to wrap instance attributes in. Accessing to field attribute in | ||||||
|  |     # model instance will always return an instance of attr_class. | ||||||
|  |     attr_class = LocalizedValue | ||||||
|  | 
 | ||||||
|  |     # The descriptor to use for accessing the attribute off of the class. | ||||||
|  |     descriptor_class = LocalizedValueDescriptor | ||||||
|  | 
 | ||||||
|     def __init__(self, *args, **kwargs): |     def __init__(self, *args, **kwargs): | ||||||
|         """Initializes a new instance of :see:LocalizedField.""" |         """Initializes a new instance of :see:LocalizedField.""" | ||||||
| 
 | 
 | ||||||
|         super(LocalizedField, self).__init__( |         super(LocalizedField, self).__init__(*args, **kwargs) | ||||||
|             *args, required=[settings.LANGUAGE_CODE], **kwargs) |  | ||||||
| 
 | 
 | ||||||
|     @staticmethod |     def contribute_to_class(self, model, name, **kwargs): | ||||||
|     def from_db_value(value, *_): |         """Adds this field to the specifed model. | ||||||
|  | 
 | ||||||
|  |         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 | ||||||
|  |     def from_db_value(cls, value, *_): | ||||||
|         """Turns the specified database value into its Python |         """Turns the specified database value into its Python | ||||||
|         equivalent. |         equivalent. | ||||||
| 
 | 
 | ||||||
| @@ -36,11 +61,36 @@ class LocalizedField(HStoreField): | |||||||
|         """ |         """ | ||||||
| 
 | 
 | ||||||
|         if not value: |         if not value: | ||||||
|             return LocalizedValue() |             if getattr(settings, 'LOCALIZED_FIELDS_EXPERIMENTAL', False): | ||||||
|  |                 return None | ||||||
|  |             else: | ||||||
|  |                 return cls.attr_class() | ||||||
| 
 | 
 | ||||||
|         return LocalizedValue(value) |         # 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) | ||||||
| 
 | 
 | ||||||
|     def to_python(self, value: dict) -> LocalizedValue: |             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) | ||||||
|  | 
 | ||||||
|  |     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. | ||||||
| 
 | 
 | ||||||
| @@ -54,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 | ||||||
|             return LocalizedValue() |         # get specified a json string representing a dict | ||||||
|  |         try: | ||||||
|  |             deserialized_value = super(LocalizedField, self).to_python(value) | ||||||
|  |         except json.JSONDecodeError: | ||||||
|  |             deserialized_value = value | ||||||
| 
 | 
 | ||||||
|         return LocalizedValue(value) |         if not deserialized_value: | ||||||
|  |             return self.attr_class() | ||||||
|  | 
 | ||||||
|  |         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 | ||||||
| @@ -113,7 +170,10 @@ class LocalizedField(HStoreField): | |||||||
|         # are any of the language fiels None/empty? |         # are any of the language fiels None/empty? | ||||||
|         is_all_null = True |         is_all_null = True | ||||||
|         for lang_code, _ in settings.LANGUAGES: |         for lang_code, _ in settings.LANGUAGES: | ||||||
|             if value.get(lang_code): |             # NOTE(seroy): use check for None, instead of | ||||||
|  |             # `bool(value.get(lang_code))==True` condition, cause in this way | ||||||
|  |             # we can not save '' value | ||||||
|  |             if value.get(lang_code) is not None: | ||||||
|                 is_all_null = False |                 is_all_null = False | ||||||
|                 break |                 break | ||||||
| 
 | 
 | ||||||
| @@ -141,7 +201,9 @@ class LocalizedField(HStoreField): | |||||||
| 
 | 
 | ||||||
|         primary_lang_val = getattr(value, settings.LANGUAGE_CODE) |         primary_lang_val = getattr(value, settings.LANGUAGE_CODE) | ||||||
| 
 | 
 | ||||||
|         if not primary_lang_val: |         # NOTE(seroy): use check for None, instead of `not primary_lang_val` | ||||||
|  |         # condition, cause in this way we can not save '' value | ||||||
|  |         if primary_lang_val is None: | ||||||
|             raise IntegrityError( |             raise IntegrityError( | ||||||
|                 'null value in column "%s.%s" violates not-null constraint' % ( |                 'null value in column "%s.%s" violates not-null constraint' % ( | ||||||
|                     self.name, |                     self.name, | ||||||
							
								
								
									
										151
									
								
								localized_fields/fields/file_field.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								localized_fields/fields/file_field.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,151 @@ | |||||||
|  | import datetime | ||||||
|  | import posixpath | ||||||
|  |  | ||||||
|  | from django.core.files import File | ||||||
|  | from django.db.models.fields.files import FieldFile | ||||||
|  | from django.utils import six | ||||||
|  | from django.core.files.storage import default_storage | ||||||
|  | from django.utils.encoding import force_str, force_text | ||||||
|  |  | ||||||
|  | from localized_fields.fields import LocalizedField | ||||||
|  | from localized_fields.fields.field import LocalizedValueDescriptor | ||||||
|  | from localized_fields.value import LocalizedValue | ||||||
|  |  | ||||||
|  | from ..value import LocalizedFileValue | ||||||
|  | from ..forms import LocalizedFileFieldForm | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedFieldFile(FieldFile): | ||||||
|  |  | ||||||
|  |     def __init__(self, instance, field, name, lang): | ||||||
|  |         super().__init__(instance, field, name) | ||||||
|  |         self.lang = lang | ||||||
|  |  | ||||||
|  |     def save(self, name, content, save=True): | ||||||
|  |         name = self.field.generate_filename(self.instance, name, self.lang) | ||||||
|  |         self.name = self.storage.save(name, content, | ||||||
|  |                                       max_length=self.field.max_length) | ||||||
|  |         self._committed = True | ||||||
|  |  | ||||||
|  |         if save: | ||||||
|  |             self.instance.save() | ||||||
|  |  | ||||||
|  |     save.alters_data = True | ||||||
|  |  | ||||||
|  |     def delete(self, save=True): | ||||||
|  |         if not self: | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         if hasattr(self, '_file'): | ||||||
|  |             self.close() | ||||||
|  |             del self.file | ||||||
|  |  | ||||||
|  |         self.storage.delete(self.name) | ||||||
|  |  | ||||||
|  |         self.name = None | ||||||
|  |         self._committed = False | ||||||
|  |  | ||||||
|  |         if save: | ||||||
|  |             self.instance.save() | ||||||
|  |  | ||||||
|  |     delete.alters_data = True | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedFileValueDescriptor(LocalizedValueDescriptor): | ||||||
|  |     def __get__(self, instance, cls=None): | ||||||
|  |         value = super().__get__(instance, cls) | ||||||
|  |         for lang, file in value.__dict__.items(): | ||||||
|  |             if isinstance(file, six.string_types) or file is None: | ||||||
|  |                 file = self.field.value_class(instance, self.field, file, lang) | ||||||
|  |                 value.set(lang, file) | ||||||
|  |  | ||||||
|  |             elif isinstance(file, File) and \ | ||||||
|  |                     not isinstance(file, LocalizedFieldFile): | ||||||
|  |                 file_copy = self.field.value_class(instance, self.field, | ||||||
|  |                                                    file.name, lang) | ||||||
|  |                 file_copy.file = file | ||||||
|  |                 file_copy._committed = False | ||||||
|  |                 value.set(lang, file_copy) | ||||||
|  |  | ||||||
|  |             elif isinstance(file, LocalizedFieldFile) and \ | ||||||
|  |                     not hasattr(file, 'field'): | ||||||
|  |                 file.instance = instance | ||||||
|  |                 file.field = self.field | ||||||
|  |                 file.storage = self.field.storage | ||||||
|  |                 file.lang = lang | ||||||
|  |  | ||||||
|  |             # Make sure that the instance is correct. | ||||||
|  |             elif isinstance(file, LocalizedFieldFile) \ | ||||||
|  |                     and instance is not file.instance: | ||||||
|  |                 file.instance = instance | ||||||
|  |                 file.lang = lang | ||||||
|  |         return value | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedFileField(LocalizedField): | ||||||
|  |     descriptor_class = LocalizedFileValueDescriptor | ||||||
|  |     attr_class = LocalizedFileValue | ||||||
|  |     value_class = LocalizedFieldFile | ||||||
|  |  | ||||||
|  |     def __init__(self, verbose_name=None, name=None, upload_to='', storage=None, | ||||||
|  |                  **kwargs): | ||||||
|  |  | ||||||
|  |         self.storage = storage or default_storage | ||||||
|  |         self.upload_to = upload_to | ||||||
|  |  | ||||||
|  |         super().__init__(verbose_name, name, **kwargs) | ||||||
|  |  | ||||||
|  |     def deconstruct(self): | ||||||
|  |         name, path, args, kwargs = super().deconstruct() | ||||||
|  |         kwargs['upload_to'] = self.upload_to | ||||||
|  |         if self.storage is not default_storage: | ||||||
|  |             kwargs['storage'] = self.storage | ||||||
|  |         return name, path, args, kwargs | ||||||
|  |  | ||||||
|  |     def get_prep_value(self, value): | ||||||
|  |         """Returns field's value prepared for saving into a database.""" | ||||||
|  |  | ||||||
|  |         if isinstance(value, LocalizedValue): | ||||||
|  |             prep_value = LocalizedValue() | ||||||
|  |             for k, v in value.__dict__.items(): | ||||||
|  |                 if v is None: | ||||||
|  |                     prep_value.set(k, '') | ||||||
|  |                 else: | ||||||
|  |                     # Need to convert File objects provided via a form to | ||||||
|  |                     # unicode for database insertion | ||||||
|  |                     prep_value.set(k, six.text_type(v)) | ||||||
|  |             return super().get_prep_value(prep_value) | ||||||
|  |         return super().get_prep_value(value) | ||||||
|  |  | ||||||
|  |     def pre_save(self, model_instance, add): | ||||||
|  |         """Returns field's value just before saving.""" | ||||||
|  |         value = super().pre_save(model_instance, add) | ||||||
|  |         if isinstance(value, LocalizedValue): | ||||||
|  |             for file in value.__dict__.values(): | ||||||
|  |                 if file and not file._committed: | ||||||
|  |                     file.save(file.name, file, save=False) | ||||||
|  |         return value | ||||||
|  |  | ||||||
|  |     def generate_filename(self, instance, filename, lang): | ||||||
|  |         if callable(self.upload_to): | ||||||
|  |             filename = self.upload_to(instance, filename, lang) | ||||||
|  |         else: | ||||||
|  |             now = datetime.datetime.now() | ||||||
|  |             dirname = force_text(now.strftime(force_str(self.upload_to))) | ||||||
|  |             dirname = dirname.format(lang=lang) | ||||||
|  |             filename = posixpath.join(dirname, filename) | ||||||
|  |         return self.storage.generate_filename(filename) | ||||||
|  |  | ||||||
|  |     def save_form_data(self, instance, data): | ||||||
|  |         if isinstance(data, LocalizedValue): | ||||||
|  |             for k, v in data.__dict__.items(): | ||||||
|  |                 if v is not None and not v: | ||||||
|  |                     data.set(k, '') | ||||||
|  |             setattr(instance, self.name, data) | ||||||
|  |  | ||||||
|  |     def formfield(self, **kwargs): | ||||||
|  |         defaults = {'form_class': LocalizedFileFieldForm} | ||||||
|  |         if 'initial' in kwargs: | ||||||
|  |             defaults['required'] = False | ||||||
|  |         defaults.update(kwargs) | ||||||
|  |         return super().formfield(**defaults) | ||||||
							
								
								
									
										14
									
								
								localized_fields/fields/text_field.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								localized_fields/fields/text_field.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | from ..forms import LocalizedTextFieldForm | ||||||
|  | from .char_field import LocalizedCharField | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedTextField(LocalizedCharField): | ||||||
|  |     def formfield(self, **kwargs): | ||||||
|  |         """Gets the form field associated with this field.""" | ||||||
|  |  | ||||||
|  |         defaults = { | ||||||
|  |             'form_class': LocalizedTextFieldForm | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         defaults.update(kwargs) | ||||||
|  |         return super().formfield(**defaults) | ||||||
| @@ -1,13 +1,12 @@ | |||||||
| from datetime import datetime | from datetime import datetime | ||||||
| 
 | 
 | ||||||
| from django.conf import settings |  | ||||||
| from django.utils.text import slugify | from django.utils.text import slugify | ||||||
| from django.core.exceptions import ImproperlyConfigured | from django.core.exceptions import ImproperlyConfigured | ||||||
| 
 | 
 | ||||||
|  | from .autoslug_field import LocalizedAutoSlugField | ||||||
| from ..util import get_language_codes | from ..util import get_language_codes | ||||||
| from ..mixins import AtomicSlugRetryMixin | from ..mixins import AtomicSlugRetryMixin | ||||||
| from ..localized_value import LocalizedValue | from ..value import LocalizedValue | ||||||
| from .localized_autoslug_field import LocalizedAutoSlugField |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class LocalizedUniqueSlugField(LocalizedAutoSlugField): | class LocalizedUniqueSlugField(LocalizedAutoSlugField): | ||||||
| @@ -73,13 +72,7 @@ class LocalizedUniqueSlugField(LocalizedAutoSlugField): | |||||||
| 
 | 
 | ||||||
|         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 | ||||||
| 
 | 
 | ||||||
| @@ -98,11 +91,12 @@ class LocalizedUniqueSlugField(LocalizedAutoSlugField): | |||||||
|             if self.include_time: |             if self.include_time: | ||||||
|                 slug += '-%d' % datetime.now().microsecond |                 slug += '-%d' % datetime.now().microsecond | ||||||
| 
 | 
 | ||||||
|             if instance.retries > 0: |             retries = getattr(instance, 'retries', 0) | ||||||
|  |             if retries > 0: | ||||||
|                 # do not add another - if we already added time |                 # do not add another - if we already added time | ||||||
|                 if not self.include_time: |                 if not self.include_time: | ||||||
|                     slug += '-' |                     slug += '-' | ||||||
|                 slug += '%d' % instance.retries |                 slug += '%d' % retries | ||||||
| 
 | 
 | ||||||
|             slugs.set(lang_code, slug) |             slugs.set(lang_code, slug) | ||||||
| 
 | 
 | ||||||
| @@ -2,53 +2,22 @@ from typing import List | |||||||
|  |  | ||||||
| from django import forms | from django import forms | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.forms import MultiWidget | from django.core.exceptions import ValidationError | ||||||
|  | from django.forms.widgets import FILE_INPUT_CONTRADICTION | ||||||
|  |  | ||||||
| from .localized_value import LocalizedValue | from .value import LocalizedValue, LocalizedStringValue, \ | ||||||
|  |     LocalizedFileValue | ||||||
|  | from .widgets import LocalizedFieldWidget, LocalizedCharFieldWidget, \ | ||||||
| class LocalizedFieldWidget(MultiWidget): |     LocalizedFileWidget | ||||||
|     """Widget that has an input box for every language.""" |  | ||||||
|  |  | ||||||
|     def __init__(self, *args, **kwargs): |  | ||||||
|         """Initializes a new instance of :see:LocalizedFieldWidget.""" |  | ||||||
|  |  | ||||||
|         widgets = [] |  | ||||||
|  |  | ||||||
|         for _ in settings.LANGUAGES: |  | ||||||
|             widgets.append(forms.Textarea()) |  | ||||||
|  |  | ||||||
|         super(LocalizedFieldWidget, self).__init__(widgets, *args, **kwargs) |  | ||||||
|  |  | ||||||
|     def decompress(self, value: LocalizedValue) -> List[str]: |  | ||||||
|         """Decompresses the specified value so |  | ||||||
|         it can be spread over the internal widgets. |  | ||||||
|  |  | ||||||
|         Arguments: |  | ||||||
|             value: |  | ||||||
|                 The :see:LocalizedValue to display in this |  | ||||||
|                 widget. |  | ||||||
|  |  | ||||||
|         Returns: |  | ||||||
|             All values to display in the inner widgets. |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         result = [] |  | ||||||
|  |  | ||||||
|         for lang_code, _ in settings.LANGUAGES: |  | ||||||
|             if value: |  | ||||||
|                 result.append(value.get(lang_code)) |  | ||||||
|             else: |  | ||||||
|                 result.append(None) |  | ||||||
|  |  | ||||||
|         return result |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class LocalizedFieldForm(forms.MultiValueField): | class LocalizedFieldForm(forms.MultiValueField): | ||||||
|     """Form for a localized field, allows editing |     """Form for a localized field, allows editing | ||||||
|     the field in multiple languages.""" |     the field in multiple languages.""" | ||||||
|  |  | ||||||
|     widget = LocalizedFieldWidget() |     widget = LocalizedFieldWidget | ||||||
|  |     field_class = forms.fields.CharField | ||||||
|  |     value_class = LocalizedValue | ||||||
|  |  | ||||||
|     def __init__(self, *args, **kwargs): |     def __init__(self, *args, **kwargs): | ||||||
|         """Initializes a new instance of :see:LocalizedFieldForm.""" |         """Initializes a new instance of :see:LocalizedFieldForm.""" | ||||||
| @@ -59,17 +28,21 @@ class LocalizedFieldForm(forms.MultiValueField): | |||||||
|             field_options = {'required': False} |             field_options = {'required': False} | ||||||
|  |  | ||||||
|             if lang_code == settings.LANGUAGE_CODE: |             if lang_code == settings.LANGUAGE_CODE: | ||||||
|                 field_options['required'] = True |                 field_options['required'] = kwargs.get('required', True) | ||||||
|  |  | ||||||
|             field_options['label'] = lang_code |             field_options['label'] = lang_code | ||||||
|             fields.append(forms.fields.CharField(**field_options)) |             fields.append(self.field_class(**field_options)) | ||||||
|  |  | ||||||
|         super(LocalizedFieldForm, self).__init__( |         super(LocalizedFieldForm, self).__init__( | ||||||
|             fields, |             fields, | ||||||
|             require_all_fields=False |             require_all_fields=False, | ||||||
|  |             *args, **kwargs | ||||||
|         ) |         ) | ||||||
|  |         # set 'required' attribute for each widget separately | ||||||
|  |         for f, w in zip(self.fields, self.widget.widgets): | ||||||
|  |             w.is_required = f.required | ||||||
|  |  | ||||||
|     def compress(self, value: List[str]) -> LocalizedValue: |     def compress(self, value: List[str]) -> value_class: | ||||||
|         """Compresses the values from individual fields |         """Compresses the values from individual fields | ||||||
|         into a single :see:LocalizedValue instance. |         into a single :see:LocalizedValue instance. | ||||||
|  |  | ||||||
| @@ -82,9 +55,113 @@ class LocalizedFieldForm(forms.MultiValueField): | |||||||
|             the value in several languages. |             the value in several languages. | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         localized_value = LocalizedValue() |         localized_value = self.value_class() | ||||||
|  |  | ||||||
|         for (lang_code, _), value in zip(settings.LANGUAGES, value): |         for (lang_code, _), value in zip(settings.LANGUAGES, value): | ||||||
|             localized_value.set(lang_code, value) |             localized_value.set(lang_code, value) | ||||||
|  |  | ||||||
|         return localized_value |         return localized_value | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedCharFieldForm(LocalizedFieldForm): | ||||||
|  |     """Form for a localized char field, allows editing | ||||||
|  |     the field in multiple languages.""" | ||||||
|  |  | ||||||
|  |     widget = LocalizedCharFieldWidget | ||||||
|  |     value_class = LocalizedStringValue | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedTextFieldForm(LocalizedFieldForm): | ||||||
|  |     """Form for a localized text field, allows editing | ||||||
|  |     the field in multiple languages.""" | ||||||
|  |  | ||||||
|  |     value_class = LocalizedStringValue | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedFileFieldForm(LocalizedFieldForm, forms.FileField): | ||||||
|  |     """Form for a localized file field, allows editing | ||||||
|  |     the field in multiple languages.""" | ||||||
|  |  | ||||||
|  |     widget = LocalizedFileWidget | ||||||
|  |     field_class = forms.fields.FileField | ||||||
|  |     value_class = LocalizedFileValue | ||||||
|  |  | ||||||
|  |     def clean(self, value, initial=None): | ||||||
|  |         """ | ||||||
|  |         Most part of this method is a copy of | ||||||
|  |         django.forms.MultiValueField.clean, with the exception of initial | ||||||
|  |         value handling (this need for correct processing FileField's). | ||||||
|  |         All original comments saved. | ||||||
|  |         """ | ||||||
|  |         if initial is None: | ||||||
|  |             initial = [None for x in range(0, len(value))] | ||||||
|  |         else: | ||||||
|  |             if not isinstance(initial, list): | ||||||
|  |                 initial = self.widget.decompress(initial) | ||||||
|  |  | ||||||
|  |         clean_data = [] | ||||||
|  |         errors = [] | ||||||
|  |         if not value or isinstance(value, (list, tuple)): | ||||||
|  |             if (not value or not [v for v in value if | ||||||
|  |                                   v not in self.empty_values]) \ | ||||||
|  |                     and (not initial or not [v for v in initial if | ||||||
|  |                                              v not in self.empty_values]): | ||||||
|  |                 if self.required: | ||||||
|  |                     raise ValidationError(self.error_messages['required'], | ||||||
|  |                                           code='required') | ||||||
|  |         else: | ||||||
|  |             raise ValidationError(self.error_messages['invalid'], | ||||||
|  |                                   code='invalid') | ||||||
|  |         for i, field in enumerate(self.fields): | ||||||
|  |             try: | ||||||
|  |                 field_value = value[i] | ||||||
|  |             except IndexError: | ||||||
|  |                 field_value = None | ||||||
|  |             try: | ||||||
|  |                 field_initial = initial[i] | ||||||
|  |             except IndexError: | ||||||
|  |                 field_initial = None | ||||||
|  |  | ||||||
|  |             if field_value in self.empty_values and \ | ||||||
|  |                     field_initial in self.empty_values: | ||||||
|  |                 if self.require_all_fields: | ||||||
|  |                     # Raise a 'required' error if the MultiValueField is | ||||||
|  |                     # required and any field is empty. | ||||||
|  |                     if self.required: | ||||||
|  |                         raise ValidationError(self.error_messages['required'], | ||||||
|  |                                               code='required') | ||||||
|  |                 elif field.required: | ||||||
|  |                     # Otherwise, add an 'incomplete' error to the list of | ||||||
|  |                     # collected errors and skip field cleaning, if a required | ||||||
|  |                     # field is empty. | ||||||
|  |                     if field.error_messages['incomplete'] not in errors: | ||||||
|  |                         errors.append(field.error_messages['incomplete']) | ||||||
|  |                     continue | ||||||
|  |             try: | ||||||
|  |                 clean_data.append(field.clean(field_value, field_initial)) | ||||||
|  |             except ValidationError as e: | ||||||
|  |                 # Collect all validation errors in a single list, which we'll | ||||||
|  |                 # raise at the end of clean(), rather than raising a single | ||||||
|  |                 # exception for the first error we encounter. Skip duplicates. | ||||||
|  |                 errors.extend(m for m in e.error_list if m not in errors) | ||||||
|  |         if errors: | ||||||
|  |             raise ValidationError(errors) | ||||||
|  |  | ||||||
|  |         out = self.compress(clean_data) | ||||||
|  |         self.validate(out) | ||||||
|  |         self.run_validators(out) | ||||||
|  |         return out | ||||||
|  |  | ||||||
|  |     def bound_data(self, data, initial): | ||||||
|  |         bound_data = [] | ||||||
|  |         if initial is None: | ||||||
|  |             initial = [None for x in range(0, len(data))] | ||||||
|  |         else: | ||||||
|  |             if not isinstance(initial, list): | ||||||
|  |                 initial = self.widget.decompress(initial) | ||||||
|  |         for d, i in zip(data, initial): | ||||||
|  |             if d in (None, FILE_INPUT_CONTRADICTION): | ||||||
|  |                 bound_data.append(i) | ||||||
|  |             else: | ||||||
|  |                 bound_data.append(d) | ||||||
|  |         return bound_data | ||||||
|   | |||||||
| @@ -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,97 +0,0 @@ | |||||||
| from django.conf import settings |  | ||||||
| from django.utils import translation |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class LocalizedValue: |  | ||||||
|     """Represents the value of a :see:LocalizedField.""" |  | ||||||
|  |  | ||||||
|     def __init__(self, keys: dict=None): |  | ||||||
|         """Initializes a new instance of :see:LocalizedValue. |  | ||||||
|  |  | ||||||
|         Arguments: |  | ||||||
|             keys: |  | ||||||
|                 The keys to initialize this value with. Every |  | ||||||
|                 key contains the value of this field in a |  | ||||||
|                 different language. |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         if isinstance(keys, str): |  | ||||||
|             setattr(self, settings.LANGUAGE_CODE, keys) |  | ||||||
|         else: |  | ||||||
|             for lang_code, _ in settings.LANGUAGES: |  | ||||||
|                 value = keys.get(lang_code) if keys else None |  | ||||||
|                 setattr(self, lang_code, value) |  | ||||||
|  |  | ||||||
|     def get(self, language: str=None) -> str: |  | ||||||
|         """Gets the underlying value in the specified or |  | ||||||
|         primary language. |  | ||||||
|  |  | ||||||
|         Arguments: |  | ||||||
|             language: |  | ||||||
|                 The language to get the value in. |  | ||||||
|  |  | ||||||
|         Returns: |  | ||||||
|             The value in the current language, or |  | ||||||
|             the primary language in case no language |  | ||||||
|             was specified. |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         language = language or settings.LANGUAGE_CODE |  | ||||||
|         return getattr(self, language, None) |  | ||||||
|  |  | ||||||
|     def set(self, language: str, value: str): |  | ||||||
|         """Sets the value in the specified language. |  | ||||||
|  |  | ||||||
|         Arguments: |  | ||||||
|             language: |  | ||||||
|                 The language to set the value in. |  | ||||||
|  |  | ||||||
|             value: |  | ||||||
|                 The value to set. |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         setattr(self, language, value) |  | ||||||
|         return self |  | ||||||
|  |  | ||||||
|     def deconstruct(self) -> dict: |  | ||||||
|         """Deconstructs this value into a primitive type. |  | ||||||
|  |  | ||||||
|         Returns: |  | ||||||
|             A dictionary with all the localized values |  | ||||||
|             contained in this instance. |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         path = 'localized_fields.fields.LocalizedValue' |  | ||||||
|         return path, [self.__dict__], {} |  | ||||||
|  |  | ||||||
|     def __str__(self) -> str: |  | ||||||
|         """Gets the value in the current language, or falls |  | ||||||
|         back to the primary language if there's no value |  | ||||||
|         in the current language.""" |  | ||||||
|  |  | ||||||
|         value = self.get(translation.get_language()) |  | ||||||
|  |  | ||||||
|         if not value: |  | ||||||
|             value = self.get(settings.LANGUAGE_CODE) |  | ||||||
|  |  | ||||||
|         return value or '' |  | ||||||
|  |  | ||||||
|     def __eq__(self, other): |  | ||||||
|         """Compares :paramref:self to :paramref:other for |  | ||||||
|         equality. |  | ||||||
|  |  | ||||||
|         Returns: |  | ||||||
|             True when :paramref:self is equal to :paramref:other. |  | ||||||
|             And False when they are not. |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         for lang_code, _ in settings.LANGUAGES: |  | ||||||
|             if self.get(lang_code) != other.get(lang_code): |  | ||||||
|                 return False |  | ||||||
|  |  | ||||||
|         return True |  | ||||||
|  |  | ||||||
|     def __repr__(self):  # pragma: no cover |  | ||||||
|         """Gets a textual representation of this object.""" |  | ||||||
|  |  | ||||||
|         return 'LocalizedValue<%s> 0x%s' % (self.__dict__, id(self)) |  | ||||||
| @@ -1,36 +1,16 @@ | |||||||
| from django.db import models, transaction | from psqlextra.models import PostgresModel | ||||||
| from django.db.utils import IntegrityError |  | ||||||
| from django.conf import settings |  | ||||||
|  |  | ||||||
| from .fields import LocalizedField | from .mixins import AtomicSlugRetryMixin | ||||||
| from .localized_value import LocalizedValue |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class LocalizedModel(models.Model): | class LocalizedModel(AtomicSlugRetryMixin, PostgresModel): | ||||||
|     """A model that contains localized fields.""" |     """Turns a model into a model that contains LocalizedField's. | ||||||
|  |  | ||||||
|  |     For basic localisation functionality, it isn't needed to inherit | ||||||
|  |     from LocalizedModel. However, for certain features, this is required. | ||||||
|  |  | ||||||
|  |     It is definitely needed for :see:LocalizedUniqueSlugField, unless you | ||||||
|  |     manually inherit from AtomicSlugRetryMixin.""" | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|         abstract = True |         abstract = True | ||||||
|  |  | ||||||
|     def __init__(self, *args, **kwargs): |  | ||||||
|         """Initializes a new instance of :see:LocalizedModel. |  | ||||||
|  |  | ||||||
|         Here we set all the fields that are of :see:LocalizedField |  | ||||||
|         to an instance of :see:LocalizedValue in case they are none |  | ||||||
|         so that the user doesn't explicitely have to do so.""" |  | ||||||
|  |  | ||||||
|         super(LocalizedModel, self).__init__(*args, **kwargs) |  | ||||||
|  |  | ||||||
|         for field in self._meta.get_fields(): |  | ||||||
|             if not isinstance(field, LocalizedField): |  | ||||||
|                 continue |  | ||||||
|  |  | ||||||
|             value = getattr(self, field.name, None) |  | ||||||
|  |  | ||||||
|             if not isinstance(value, LocalizedValue): |  | ||||||
|                 if isinstance(value, dict): |  | ||||||
|                     value = LocalizedValue(value) |  | ||||||
|                 else: |  | ||||||
|                     value = LocalizedValue() |  | ||||||
|  |  | ||||||
|             setattr(self, field.name, value) |  | ||||||
|   | |||||||
| @@ -0,0 +1,51 @@ | |||||||
|  | .localized-fields-widget { | ||||||
|  |     margin-left: 160px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .localized-fields-widget.tabs { | ||||||
|  |     display: block; | ||||||
|  |     margin: 0; | ||||||
|  |     border-bottom: 1px solid #eee; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .localized-fields-widget.tabs .localized-fields-widget.tab { | ||||||
|  |     display: inline-block; | ||||||
|  |     margin-left: 5px; | ||||||
|  |     border: 1px solid #79aec8; | ||||||
|  |     border-bottom: none; | ||||||
|  |     border-radius: 4px; | ||||||
|  |     border-bottom-left-radius: 0; | ||||||
|  |     border-bottom-right-radius: 0; | ||||||
|  |     background: #79aec8; | ||||||
|  |     color: #fff; | ||||||
|  |     font-weight: 400; | ||||||
|  |     opacity: 0.5; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .localized-fields-widget.tabs .localized-fields-widget.tab:first-child { | ||||||
|  |     margin-left: 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .localized-fields-widget.tabs .localized-fields-widget.tab:hover { | ||||||
|  |     background: #417690; | ||||||
|  |     border-color: #417690; | ||||||
|  |     opacity: 1; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .localized-fields-widget.tabs .localized-fields-widget.tab a { | ||||||
|  |     padding: 5px 10px; | ||||||
|  |     display: inline-block; | ||||||
|  |     text-decoration: none; | ||||||
|  |     color: #fff; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .localized-fields-widget.tabs .localized-fields-widget.tab.active, | ||||||
|  | .localized-fields-widget.tabs .localized-fields-widget.tab.active:hover { | ||||||
|  |     background: #79aec8; | ||||||
|  |     border-color: #79aec8; | ||||||
|  |     opacity: 1; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .localized-fields-widget p.file-upload { | ||||||
|  |     margin-left: 0; | ||||||
|  | } | ||||||
| @@ -0,0 +1,35 @@ | |||||||
|  | (function($) { | ||||||
|  |     var syncTabs = function(lang) { | ||||||
|  |         $('.localized-fields-widget.tab a:contains("'+lang+'")').each(function(){ | ||||||
|  |             $(this).parents('.localized-fields-widget[role="tabs"]').find('.localized-fields-widget.tab').removeClass('active'); | ||||||
|  |             $(this).parents('.localized-fields-widget.tab').addClass('active'); | ||||||
|  |             $(this).parents('.localized-fields-widget[role="tabs"]').children('.localized-fields-widget [role="tabpanel"]').hide(); | ||||||
|  |             $($(this).attr('href')).show(); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     $(function (){ | ||||||
|  |         $('.localized-fields-widget [role="tabpanel"]').hide(); | ||||||
|  |         // set first tab as active | ||||||
|  |         $('.localized-fields-widget[role="tabs"]').each(function () { | ||||||
|  |             $(this).find('.localized-fields-widget.tab:first').addClass('active'); | ||||||
|  |             $($(this).find('.localized-fields-widget.tab:first a').attr('href')).show(); | ||||||
|  |         }); | ||||||
|  |         // try set active last selected tab | ||||||
|  |         if (window.sessionStorage) { | ||||||
|  |             var lang = window.sessionStorage.getItem('localized-field-lang'); | ||||||
|  |             if (lang) { | ||||||
|  |                 syncTabs(lang); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $('.localized-fields-widget.tab a').click(function(event) { | ||||||
|  |             event.preventDefault(); | ||||||
|  |             syncTabs(this.innerText); | ||||||
|  |             if (window.sessionStorage) { | ||||||
|  |                 window.sessionStorage.setItem('localized-field-lang', this.innerText); | ||||||
|  |             } | ||||||
|  |             return false; | ||||||
|  |         }); | ||||||
|  |     }); | ||||||
|  | })(django.jQuery) | ||||||
| @@ -0,0 +1,14 @@ | |||||||
|  | <div class="localized-fields-widget" role="tabs" data-synctabs="translation"> | ||||||
|  |     <ul class="localized-fields-widget tabs"> | ||||||
|  |     {% for key, lang in available_languages %} | ||||||
|  |         <li class="localized-fields-widget tab"> | ||||||
|  |             <a href="#{{ id }}_{{ key }}">{{ lang|capfirst }}</a> | ||||||
|  |         </li> | ||||||
|  |     {% endfor %} | ||||||
|  |     </ul> | ||||||
|  | {% for key, widget in widgets %} | ||||||
|  |     <div role="tabpanel" id="{{ id }}_{{ key }}"> | ||||||
|  |         {{ widget }} | ||||||
|  |     </div> | ||||||
|  | {% endfor %} | ||||||
|  | </div> | ||||||
| @@ -19,3 +19,26 @@ def get_language_codes() -> List[str]: | |||||||
|         lang_code |         lang_code | ||||||
|         for lang_code, _ in settings.LANGUAGES |         for lang_code, _ in settings.LANGUAGES | ||||||
|     ] |     ] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def resolve_object_property(obj, path: str): | ||||||
|  |     """Resolves the value of a property on an object. | ||||||
|  |  | ||||||
|  |     Is able to resolve nested properties. For example, | ||||||
|  |     a path can be specified: | ||||||
|  |  | ||||||
|  |         'other.beer.name' | ||||||
|  |  | ||||||
|  |     Raises: | ||||||
|  |         AttributeError: | ||||||
|  |             In case the property could not be resolved. | ||||||
|  |  | ||||||
|  |     Returns: | ||||||
|  |         The value of the specified property. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     value = obj | ||||||
|  |     for path_part in path.split('.'): | ||||||
|  |         value = getattr(value, path_part) | ||||||
|  |  | ||||||
|  |     return value | ||||||
|   | |||||||
							
								
								
									
										190
									
								
								localized_fields/value.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										190
									
								
								localized_fields/value.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,190 @@ | |||||||
|  | import collections | ||||||
|  |  | ||||||
|  | from django.conf import settings | ||||||
|  | from django.utils import translation | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedValue(dict): | ||||||
|  |     """Represents the value of a :see:LocalizedField.""" | ||||||
|  |     default_value = None | ||||||
|  |  | ||||||
|  |     def __init__(self, keys: dict=None): | ||||||
|  |         """Initializes a new instance of :see:LocalizedValue. | ||||||
|  |  | ||||||
|  |         Arguments: | ||||||
|  |             keys: | ||||||
|  |                 The keys to initialize this value with. Every | ||||||
|  |                 key contains the value of this field in a | ||||||
|  |                 different language. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         super().__init__({}) | ||||||
|  |         self._interpret_value(keys) | ||||||
|  |  | ||||||
|  |     def get(self, language: str=None, default: str=None) -> str: | ||||||
|  |         """Gets the underlying value in the specified or | ||||||
|  |         primary language. | ||||||
|  |  | ||||||
|  |         Arguments: | ||||||
|  |             language: | ||||||
|  |                 The language to get the value in. | ||||||
|  |  | ||||||
|  |         Returns: | ||||||
|  |             The value in the current language, or | ||||||
|  |             the primary language in case no language | ||||||
|  |             was specified. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         language = language or settings.LANGUAGE_CODE | ||||||
|  |         return super().get(language, default) | ||||||
|  |  | ||||||
|  |     def set(self, language: str, value: str): | ||||||
|  |         """Sets the value in the specified language. | ||||||
|  |  | ||||||
|  |         Arguments: | ||||||
|  |             language: | ||||||
|  |                 The language to set the value in. | ||||||
|  |  | ||||||
|  |             value: | ||||||
|  |                 The value to set. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         self[language] = value | ||||||
|  |         self.__dict__.update(self) | ||||||
|  |         return self | ||||||
|  |  | ||||||
|  |     def deconstruct(self) -> dict: | ||||||
|  |         """Deconstructs this value into a primitive type. | ||||||
|  |  | ||||||
|  |         Returns: | ||||||
|  |             A dictionary with all the localized values | ||||||
|  |             contained in this instance. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         path = 'localized_fields.localized_value.%s' % self.__class__.__name__ | ||||||
|  |         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: | ||||||
|  |         """Gets the value in the current language, or falls | ||||||
|  |         back to the primary language if there's no value | ||||||
|  |         in the current language.""" | ||||||
|  |  | ||||||
|  |         fallbacks = getattr(settings, 'LOCALIZED_FIELDS_FALLBACKS', {}) | ||||||
|  |  | ||||||
|  |         language = translation.get_language() or settings.LANGUAGE_CODE | ||||||
|  |         languages = fallbacks.get(language, [settings.LANGUAGE_CODE])[:] | ||||||
|  |         languages.insert(0, language) | ||||||
|  |  | ||||||
|  |         for lang_code in languages: | ||||||
|  |             value = self.get(lang_code) | ||||||
|  |             if value: | ||||||
|  |                 return value or '' | ||||||
|  |  | ||||||
|  |         return '' | ||||||
|  |  | ||||||
|  |     def __eq__(self, other): | ||||||
|  |         """Compares :paramref:self to :paramref:other for | ||||||
|  |         equality. | ||||||
|  |  | ||||||
|  |         Returns: | ||||||
|  |             True when :paramref:self is equal to :paramref:other. | ||||||
|  |             And False when they are not. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         if not isinstance(other, type(self)): | ||||||
|  |             if isinstance(other, str): | ||||||
|  |                 return self.__str__() == other | ||||||
|  |             return False | ||||||
|  |  | ||||||
|  |         for lang_code, _ in settings.LANGUAGES: | ||||||
|  |             if self.get(lang_code) != other.get(lang_code): | ||||||
|  |                 return False | ||||||
|  |  | ||||||
|  |         return True | ||||||
|  |  | ||||||
|  |     def __ne__(self, other): | ||||||
|  |         """Compares :paramref:self to :paramerf:other for | ||||||
|  |         in-equality. | ||||||
|  |  | ||||||
|  |         Returns: | ||||||
|  |             True when :paramref:self is not equal to :paramref:other. | ||||||
|  |             And False when they are. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         return not self.__eq__(other) | ||||||
|  |  | ||||||
|  |     def __setattr__(self, language: str, value: str): | ||||||
|  |         """Sets the value for a language with the specified name. | ||||||
|  |  | ||||||
|  |         Arguments: | ||||||
|  |             language: | ||||||
|  |                 The language to set the value in. | ||||||
|  |  | ||||||
|  |             value: | ||||||
|  |                 The value to set. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         self.set(language, value) | ||||||
|  |  | ||||||
|  |     def __repr__(self):  # pragma: no cover | ||||||
|  |         """Gets a textual representation of this object.""" | ||||||
|  |  | ||||||
|  |         return '%s<%s> 0x%s' % (self.__class__.__name__, | ||||||
|  |                                 self.__dict__, id(self)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedStringValue(LocalizedValue): | ||||||
|  |     default_value = '' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedFileValue(LocalizedValue): | ||||||
|  |     def __getattr__(self, name: str): | ||||||
|  |         """Proxies access to attributes to attributes of LocalizedFile""" | ||||||
|  |  | ||||||
|  |         value = self.get(translation.get_language()) | ||||||
|  |         if hasattr(value, name): | ||||||
|  |             return getattr(value, name) | ||||||
|  |         raise AttributeError("'{}' object has no attribute '{}'". | ||||||
|  |                              format(self.__class__.__name__, name)) | ||||||
|  |  | ||||||
|  |     def __str__(self) -> str: | ||||||
|  |         """Returns string representation of value""" | ||||||
|  |         return str(super().__str__()) | ||||||
|  |  | ||||||
|  |     def localized(self): | ||||||
|  |         """Returns value for current language""" | ||||||
|  |         return self.get(translation.get_language()) | ||||||
							
								
								
									
										104
									
								
								localized_fields/widgets.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								localized_fields/widgets.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,104 @@ | |||||||
|  | from typing import List | ||||||
|  |  | ||||||
|  | from django.conf import settings | ||||||
|  | from django import forms | ||||||
|  | from django.contrib.admin import widgets | ||||||
|  | from django.template.loader import render_to_string | ||||||
|  |  | ||||||
|  | from .value import LocalizedValue | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedFieldWidget(forms.MultiWidget): | ||||||
|  |     """Widget that has an input box for every language.""" | ||||||
|  |     widget = forms.Textarea | ||||||
|  |  | ||||||
|  |     def __init__(self, *args, **kwargs): | ||||||
|  |         """Initializes a new instance of :see:LocalizedFieldWidget.""" | ||||||
|  |  | ||||||
|  |         initial_widgets = [ | ||||||
|  |             self.widget | ||||||
|  |             for _ in settings.LANGUAGES | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |         super().__init__(initial_widgets, *args, **kwargs) | ||||||
|  |  | ||||||
|  |     def decompress(self, value: LocalizedValue) -> List[str]: | ||||||
|  |         """Decompresses the specified value so | ||||||
|  |         it can be spread over the internal widgets. | ||||||
|  |  | ||||||
|  |         Arguments: | ||||||
|  |             value: | ||||||
|  |                 The :see:LocalizedValue to display in this | ||||||
|  |                 widget. | ||||||
|  |  | ||||||
|  |         Returns: | ||||||
|  |             All values to display in the inner widgets. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         result = [] | ||||||
|  |         for lang_code, _ in settings.LANGUAGES: | ||||||
|  |             if value: | ||||||
|  |                 result.append(value.get(lang_code)) | ||||||
|  |             else: | ||||||
|  |                 result.append(None) | ||||||
|  |  | ||||||
|  |         return result | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedCharFieldWidget(LocalizedFieldWidget): | ||||||
|  |     """Widget that has an input box for every language.""" | ||||||
|  |     widget = forms.TextInput | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedFileWidget(LocalizedFieldWidget): | ||||||
|  |     """Widget that has an file input box for every language.""" | ||||||
|  |     widget = forms.ClearableFileInput | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AdminLocalizedFieldWidget(LocalizedFieldWidget): | ||||||
|  |     widget = widgets.AdminTextareaWidget | ||||||
|  |     template = 'localized_fields/admin/widget.html' | ||||||
|  |  | ||||||
|  |     def render(self, name, value, attrs=None): | ||||||
|  |         if self.is_localized: | ||||||
|  |             for widget in self.widgets: | ||||||
|  |                 widget.is_localized = self.is_localized | ||||||
|  |         # value is a list of values, each corresponding to a widget | ||||||
|  |         # in self.widgets. | ||||||
|  |         if not isinstance(value, list): | ||||||
|  |             value = self.decompress(value) | ||||||
|  |         output = [] | ||||||
|  |         final_attrs = self.build_attrs(attrs) | ||||||
|  |         id_ = final_attrs.get('id') | ||||||
|  |         for i, widget in enumerate(self.widgets): | ||||||
|  |             try: | ||||||
|  |                 widget_value = value[i] | ||||||
|  |             except IndexError: | ||||||
|  |                 widget_value = None | ||||||
|  |             if id_: | ||||||
|  |                 final_attrs = dict(final_attrs, id='%s_%s' % (id_, i)) | ||||||
|  |             widget_attrs = self.build_widget_attrs(widget, widget_value, final_attrs) | ||||||
|  |             output.append(widget.render(name + '_%s' % i, widget_value, widget_attrs)) | ||||||
|  |         context = { | ||||||
|  |             'id': final_attrs.get('id'), | ||||||
|  |             'name': name, | ||||||
|  |             'widgets': zip([code for code, lang in settings.LANGUAGES], output), | ||||||
|  |             'available_languages': settings.LANGUAGES | ||||||
|  |         } | ||||||
|  |         return render_to_string(self.template, context) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def build_widget_attrs(widget, value, attrs): | ||||||
|  |         attrs = dict(attrs)  # Copy attrs to avoid modifying the argument. | ||||||
|  |         if (not widget.use_required_attribute(value) or not widget.is_required) \ | ||||||
|  |                 and 'required' in attrs: | ||||||
|  |             del attrs['required'] | ||||||
|  |         return attrs | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AdminLocalizedCharFieldWidget(AdminLocalizedFieldWidget): | ||||||
|  |     widget = widgets.AdminTextInputWidget | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AdminLocalizedFileFieldWidget(AdminLocalizedFieldWidget): | ||||||
|  |     widget = widgets.AdminFileWidget | ||||||
| @@ -1 +1 @@ | |||||||
| django-postgres-extra==1.2 | django-postgres-extra==1.11 | ||||||
|   | |||||||
| @@ -21,9 +21,12 @@ LANGUAGES = ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| INSTALLED_APPS = ( | INSTALLED_APPS = ( | ||||||
|  |     'localized_fields', | ||||||
|     'tests', |     'tests', | ||||||
| ) | ) | ||||||
|  |  | ||||||
| # set to a lower number than the default, since | # set to a lower number than the default, since | ||||||
| # we want the tests to be fast, default is 100 | # we want the tests to be fast, default is 100 | ||||||
| LOCALIZED_FIELDS_MAX_RETRIES = 3 | LOCALIZED_FIELDS_MAX_RETRIES = 3 | ||||||
|  |  | ||||||
|  | LOCALIZED_FIELDS_EXPERIMENTAL = False | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								setup.py
									
									
									
									
									
								
							| @@ -7,8 +7,8 @@ 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.1', |     version='4.3', | ||||||
|     packages=find_packages(), |     packages=find_packages(exclude=['tests']), | ||||||
|     include_package_data=True, |     include_package_data=True, | ||||||
|     license='MIT License', |     license='MIT License', | ||||||
|     description='Implementation of localized model fields using PostgreSQL HStore fields.', |     description='Implementation of localized model fields using PostgreSQL HStore fields.', | ||||||
| @@ -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.2' |         '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 | ||||||
| @@ -1,28 +1,33 @@ | |||||||
|  | import uuid | ||||||
|  |  | ||||||
| from django.db import connection, migrations | 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, AtomicSlugRetryMixin | from localized_fields.models import LocalizedModel | ||||||
|  |  | ||||||
|  |  | ||||||
| def define_fake_model(name='TestModel', fields=None): | def define_fake_model(fields=None, model_base=LocalizedModel, meta_options={}): | ||||||
|  |     name = str(uuid.uuid4()).replace('-', '')[:8] | ||||||
|  |  | ||||||
|     attributes = { |     attributes = { | ||||||
|         'app_label': 'localized_fields', |         'app_label': 'tests', | ||||||
|         '__module__': __name__, |         '__module__': __name__, | ||||||
|         '__name__': name |         '__name__': name, | ||||||
|  |         'Meta': type('Meta', (object,), meta_options) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if fields: |     if fields: | ||||||
|         attributes.update(fields) |         attributes.update(fields) | ||||||
|     model = type(name, (AtomicSlugRetryMixin,LocalizedModel,), attributes) |     model = type(name, (model_base,), attributes) | ||||||
|  |  | ||||||
|     return model |     return model | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_fake_model(name='TestModel', fields=None): | def get_fake_model(fields=None, model_base=LocalizedModel, meta_options={}): | ||||||
|     """Creates a fake model to use during unit tests.""" |     """Creates a fake model to use during unit tests.""" | ||||||
|  |  | ||||||
|     model = define_fake_model(name, fields) |     model = define_fake_model(fields, model_base, meta_options) | ||||||
|  |  | ||||||
|     class TestProject: |     class TestProject: | ||||||
|  |  | ||||||
| @@ -39,7 +44,7 @@ def get_fake_model(name='TestModel', fields=None): | |||||||
|     with connection.schema_editor() as schema_editor: |     with connection.schema_editor() as schema_editor: | ||||||
|         migration_executor = MigrationExecutor(schema_editor.connection) |         migration_executor = MigrationExecutor(schema_editor.connection) | ||||||
|         migration_executor.apply_migration( |         migration_executor.apply_migration( | ||||||
|             TestProject(), TestMigration('eh', 'localized_fields')) |             TestProject(), TestMigration('eh', 'postgres_extra')) | ||||||
|  |  | ||||||
|         schema_editor.create_model(model) |         schema_editor.create_model(model) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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: | ||||||
							
								
								
									
										44
									
								
								tests/test_bulk.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								tests/test_bulk.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | |||||||
|  | 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( | ||||||
|  |             { | ||||||
|  |                 '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) | ||||||
							
								
								
									
										84
									
								
								tests/test_expressions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								tests/test_expressions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,84 @@ | |||||||
|  | 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( | ||||||
|  |             { | ||||||
|  |                 'name': models.CharField(null=False, blank=False, max_length=255), | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         cls.TestModel2 = get_fake_model( | ||||||
|  |             { | ||||||
|  |                 '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 | ||||||
|  |         ) | ||||||
							
								
								
									
										151
									
								
								tests/test_file_field.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								tests/test_file_field.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,151 @@ | |||||||
|  | import os | ||||||
|  | import shutil | ||||||
|  | import tempfile as sys_tempfile | ||||||
|  | import pickle | ||||||
|  |  | ||||||
|  | from django import forms | ||||||
|  | from django.test import TestCase, override_settings | ||||||
|  | from django.core.files.base import File, ContentFile | ||||||
|  | from django.core.files import temp as tempfile | ||||||
|  | from localized_fields.fields import LocalizedFileField | ||||||
|  | from localized_fields.value import LocalizedValue | ||||||
|  | from localized_fields.fields.file_field import LocalizedFieldFile | ||||||
|  | from localized_fields.forms import LocalizedFileFieldForm | ||||||
|  | from localized_fields.value import LocalizedFileValue | ||||||
|  | from localized_fields.widgets import LocalizedFileWidget | ||||||
|  | from .fake_model import get_fake_model | ||||||
|  |  | ||||||
|  |  | ||||||
|  | MEDIA_ROOT = sys_tempfile.mkdtemp() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @override_settings(MEDIA_ROOT=MEDIA_ROOT) | ||||||
|  | class LocalizedFileFieldTestCase(TestCase): | ||||||
|  |     """Tests the localized slug classes.""" | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def setUpClass(cls): | ||||||
|  |         """Creates the test models in the database.""" | ||||||
|  |  | ||||||
|  |         super().setUpClass() | ||||||
|  |  | ||||||
|  |         cls.FileFieldModel = get_fake_model( | ||||||
|  |             { | ||||||
|  |                 'file': LocalizedFileField(), | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |         if not os.path.isdir(MEDIA_ROOT): | ||||||
|  |             os.makedirs(MEDIA_ROOT) | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def tearDownClass(cls): | ||||||
|  |         super().tearDownClass() | ||||||
|  |         shutil.rmtree(MEDIA_ROOT) | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def test_assign(cls): | ||||||
|  |         """Tests whether the :see:LocalizedFileValueDescriptor works properly""" | ||||||
|  |  | ||||||
|  |         temp_file = tempfile.NamedTemporaryFile(dir=MEDIA_ROOT) | ||||||
|  |         instance = cls.FileFieldModel() | ||||||
|  |         instance.file = {'en': temp_file.name} | ||||||
|  |         assert isinstance(instance.file.en, LocalizedFieldFile) | ||||||
|  |         assert instance.file.en.name == temp_file.name | ||||||
|  |  | ||||||
|  |         field_dump = pickle.dumps(instance.file) | ||||||
|  |         instance = cls.FileFieldModel() | ||||||
|  |         instance.file = pickle.loads(field_dump) | ||||||
|  |         assert instance.file.en.field == instance._meta.get_field('file') | ||||||
|  |         assert instance.file.en.instance == instance | ||||||
|  |         assert isinstance(instance.file.en, LocalizedFieldFile) | ||||||
|  |  | ||||||
|  |         instance = cls.FileFieldModel() | ||||||
|  |         instance.file = {'en': ContentFile("test", "testfilename")} | ||||||
|  |         assert isinstance(instance.file.en, LocalizedFieldFile) | ||||||
|  |         assert instance.file.en.name == "testfilename" | ||||||
|  |  | ||||||
|  |         another_instance = cls.FileFieldModel() | ||||||
|  |         another_instance.file = {'ro': instance.file.en} | ||||||
|  |         assert another_instance == another_instance.file.ro.instance | ||||||
|  |         assert another_instance.file.ro.lang == 'ro' | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def test_save_form_data(cls): | ||||||
|  |         """Tests whether the :see:save_form_data function correctly set  | ||||||
|  |         a valid value.""" | ||||||
|  |  | ||||||
|  |         instance = cls.FileFieldModel() | ||||||
|  |         data = LocalizedFileValue({'en': False}) | ||||||
|  |         instance._meta.get_field('file').save_form_data(instance, data) | ||||||
|  |         assert instance.file.en == '' | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def test_pre_save(cls): | ||||||
|  |         """Tests whether the :see:pre_save function works properly.""" | ||||||
|  |  | ||||||
|  |         instance = cls.FileFieldModel() | ||||||
|  |         instance.file = {'en': ContentFile("test", "testfilename")} | ||||||
|  |         instance._meta.get_field('file').pre_save(instance, False) | ||||||
|  |         assert instance.file.en._committed == True | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def test_file_methods(cls): | ||||||
|  |         """Tests whether the :see:LocalizedFieldFile.delete method works | ||||||
|  |         correctly.""" | ||||||
|  |  | ||||||
|  |         temp_file = File(tempfile.NamedTemporaryFile()) | ||||||
|  |         instance = cls.FileFieldModel() | ||||||
|  |         # Calling delete on an unset FileField should not call the file deletion | ||||||
|  |         # process, but fail silently | ||||||
|  |         instance.file.en.delete() | ||||||
|  |         instance.file.en.save('testfilename', temp_file) | ||||||
|  |         assert instance.file.en.name == 'testfilename' | ||||||
|  |         instance.file.en.delete() | ||||||
|  |         assert instance.file.en.name is None | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def test_generate_filename(cls): | ||||||
|  |         """Tests whether the :see:LocalizedFieldFile.generate_filename method | ||||||
|  |         works correctly.""" | ||||||
|  |  | ||||||
|  |         instance = cls.FileFieldModel() | ||||||
|  |         field = instance._meta.get_field('file') | ||||||
|  |         field.upload_to = '{lang}/' | ||||||
|  |         filename = field.generate_filename(instance, 'test', 'en') | ||||||
|  |         assert filename == 'en/test' | ||||||
|  |         field.upload_to = lambda instance, filename, lang: \ | ||||||
|  |             '%s_%s' % (lang, filename) | ||||||
|  |         filename = field.generate_filename(instance, 'test', 'en') | ||||||
|  |         assert filename == 'en_test' | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_get_prep_value(): | ||||||
|  |         """Tests whether the :see:get_prep_value function returns correctly  | ||||||
|  |         value.""" | ||||||
|  |  | ||||||
|  |         value = LocalizedValue({'en': None}) | ||||||
|  |         assert LocalizedFileField().get_prep_value(None) == None | ||||||
|  |         assert isinstance(LocalizedFileField().get_prep_value(value), dict) | ||||||
|  |         assert LocalizedFileField().get_prep_value(value)['en'] == '' | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_formfield(): | ||||||
|  |         """Tests whether the :see:formfield function correctly returns  | ||||||
|  |         a valid form.""" | ||||||
|  |  | ||||||
|  |         form_field = LocalizedFileField().formfield() | ||||||
|  |         assert isinstance(form_field, LocalizedFileFieldForm) | ||||||
|  |         assert isinstance(form_field, forms.FileField) | ||||||
|  |         assert isinstance(form_field.widget, LocalizedFileWidget) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_deconstruct(): | ||||||
|  |         """Tests whether the :see:LocalizedFileField | ||||||
|  |         class's :see:deconstruct function works properly.""" | ||||||
|  |  | ||||||
|  |         name, path, args, kwargs = LocalizedFileField().deconstruct() | ||||||
|  |         assert 'upload_to' in kwargs | ||||||
|  |         assert 'storage' not in kwargs | ||||||
|  |         name, path, \ | ||||||
|  |         args, kwargs = LocalizedFileField(storage='test').deconstruct() | ||||||
|  |         assert 'storage' in kwargs | ||||||
							
								
								
									
										41
									
								
								tests/test_file_field_form.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								tests/test_file_field_form.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | |||||||
|  | from django.conf import settings | ||||||
|  | from django.core.exceptions import ValidationError | ||||||
|  | from django.forms.widgets import FILE_INPUT_CONTRADICTION | ||||||
|  | from django.test import TestCase | ||||||
|  |  | ||||||
|  | from localized_fields.forms import LocalizedFileFieldForm | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedFileFieldFormTestCase(TestCase): | ||||||
|  |     """Tests the workings of the :see:LocalizedFileFieldForm class.""" | ||||||
|  |  | ||||||
|  |     def test_clean(self): | ||||||
|  |         """Tests whether the :see:clean function is working properly.""" | ||||||
|  |  | ||||||
|  |         formfield = LocalizedFileFieldForm(required=True) | ||||||
|  |         with self.assertRaises(ValidationError): | ||||||
|  |             formfield.clean([]) | ||||||
|  |         with self.assertRaises(ValidationError): | ||||||
|  |             formfield.clean([], {'en': None}) | ||||||
|  |         with self.assertRaises(ValidationError): | ||||||
|  |             formfield.clean("badvalue") | ||||||
|  |         with self.assertRaises(ValidationError): | ||||||
|  |             value = [FILE_INPUT_CONTRADICTION] * len(settings.LANGUAGES) | ||||||
|  |             formfield.clean(value) | ||||||
|  |  | ||||||
|  |         formfield = LocalizedFileFieldForm(required=False) | ||||||
|  |         formfield.clean([''] * len(settings.LANGUAGES)) | ||||||
|  |         formfield.clean(['', ''], ['', '']) | ||||||
|  |  | ||||||
|  |     def test_bound_data(self): | ||||||
|  |         """Tests whether the :see:bound_data function is returns correctly  | ||||||
|  |         value""" | ||||||
|  |  | ||||||
|  |         formfield = LocalizedFileFieldForm() | ||||||
|  |         assert formfield.bound_data([''], None) == [''] | ||||||
|  |  | ||||||
|  |         initial = dict([(lang, '') for lang, _ in settings.LANGUAGES]) | ||||||
|  |         value = [None] * len(settings.LANGUAGES) | ||||||
|  |         expected_value = [''] * len(settings.LANGUAGES) | ||||||
|  |         assert formfield.bound_data(value, initial) == expected_value | ||||||
|  |  | ||||||
| @@ -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,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 | ||||||
| 
 | 
 | ||||||
| @@ -17,7 +18,6 @@ class LocalizedModelTestCase(TestCase): | |||||||
|         super(LocalizedModelTestCase, cls).setUpClass() |         super(LocalizedModelTestCase, cls).setUpClass() | ||||||
| 
 | 
 | ||||||
|         cls.TestModel = get_fake_model( |         cls.TestModel = get_fake_model( | ||||||
|             'LocalizedModelTestCase', |  | ||||||
|             { |             { | ||||||
|                 'title': LocalizedField() |                 'title': LocalizedField() | ||||||
|             } |             } | ||||||
| @@ -33,7 +33,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 | ||||||
| @@ -1,13 +1,17 @@ | |||||||
| import copy | import copy | ||||||
| 
 | 
 | ||||||
| from django import forms | from django import forms | ||||||
|  | from django.db import models | ||||||
| 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, | ||||||
|  |     LocalizedUniqueSlugField | ||||||
|  | ) | ||||||
|  | 
 | ||||||
| from .fake_model import get_fake_model | from .fake_model import get_fake_model | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @@ -15,7 +19,7 @@ class LocalizedSlugFieldTestCase(TestCase): | |||||||
|     """Tests the localized slug classes.""" |     """Tests the localized slug classes.""" | ||||||
| 
 | 
 | ||||||
|     AutoSlugModel = None |     AutoSlugModel = None | ||||||
|     MagicSlugModel = None |     Model = None | ||||||
| 
 | 
 | ||||||
|     @classmethod |     @classmethod | ||||||
|     def setUpClass(cls): |     def setUpClass(cls): | ||||||
| @@ -23,46 +27,14 @@ class LocalizedSlugFieldTestCase(TestCase): | |||||||
| 
 | 
 | ||||||
|         super(LocalizedSlugFieldTestCase, cls).setUpClass() |         super(LocalizedSlugFieldTestCase, cls).setUpClass() | ||||||
| 
 | 
 | ||||||
|         cls.AutoSlugModel = get_fake_model( |         cls.Model = get_fake_model( | ||||||
|             'LocalizedAutoSlugFieldTestModel', |  | ||||||
|             { |  | ||||||
|                 'title': LocalizedField(), |  | ||||||
|                 'slug': LocalizedAutoSlugField(populate_from='title') |  | ||||||
|             } |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
|         cls.MagicSlugModel = get_fake_model( |  | ||||||
|             'LocalizedUniqueSlugFieldTestModel', |  | ||||||
|             { |             { | ||||||
|                 'title': LocalizedField(), |                 'title': LocalizedField(), | ||||||
|  |                 'name': models.CharField(max_length=255), | ||||||
|                 'slug': LocalizedUniqueSlugField(populate_from='title') |                 'slug': LocalizedUniqueSlugField(populate_from='title') | ||||||
|             } |             } | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|     @classmethod |  | ||||||
|     def test_populate_auto(cls): |  | ||||||
|         cls._test_populate(cls.AutoSlugModel) |  | ||||||
| 
 |  | ||||||
|     @classmethod |  | ||||||
|     def test_populate_unique(cls): |  | ||||||
|         cls._test_populate(cls.MagicSlugModel) |  | ||||||
| 
 |  | ||||||
|     @classmethod |  | ||||||
|     def test_populate_multiple_languages_auto(cls): |  | ||||||
|         cls._test_populate_multiple_languages(cls.AutoSlugModel) |  | ||||||
| 
 |  | ||||||
|     @classmethod |  | ||||||
|     def test_populate_multiple_languages_unique(cls): |  | ||||||
|         cls._test_populate_multiple_languages(cls.MagicSlugModel) |  | ||||||
| 
 |  | ||||||
|     @classmethod |  | ||||||
|     def test_unique_slug_auto(cls): |  | ||||||
|         cls._test_unique_slug(cls.AutoSlugModel) |  | ||||||
| 
 |  | ||||||
|     @classmethod |  | ||||||
|     def test_unique_slug_unique(cls): |  | ||||||
|         cls._test_unique_slug(cls.MagicSlugModel) |  | ||||||
| 
 |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def test_unique_slug_with_time(): |     def test_unique_slug_with_time(): | ||||||
|         """Tests whether the primary key is included in |         """Tests whether the primary key is included in | ||||||
| @@ -71,7 +43,6 @@ class LocalizedSlugFieldTestCase(TestCase): | |||||||
|         title = 'myuniquetitle' |         title = 'myuniquetitle' | ||||||
| 
 | 
 | ||||||
|         PkModel = get_fake_model( |         PkModel = get_fake_model( | ||||||
|             'PkModel', |  | ||||||
|             { |             { | ||||||
|                 'title': LocalizedField(), |                 'title': LocalizedField(), | ||||||
|                 'slug': LocalizedUniqueSlugField(populate_from='title', include_time=True) |                 'slug': LocalizedUniqueSlugField(populate_from='title', include_time=True) | ||||||
| @@ -89,7 +60,6 @@ class LocalizedSlugFieldTestCase(TestCase): | |||||||
|         """Tests whether slugs are not re-generated if not needed.""" |         """Tests whether slugs are not re-generated if not needed.""" | ||||||
| 
 | 
 | ||||||
|         NoChangeSlugModel = get_fake_model( |         NoChangeSlugModel = get_fake_model( | ||||||
|             'NoChangeSlugModel', |  | ||||||
|             { |             { | ||||||
|                 'title': LocalizedField(), |                 'title': LocalizedField(), | ||||||
|                 'slug': LocalizedUniqueSlugField(populate_from='title', include_time=True) |                 'slug': LocalizedUniqueSlugField(populate_from='title', include_time=True) | ||||||
| @@ -111,62 +81,47 @@ class LocalizedSlugFieldTestCase(TestCase): | |||||||
|         assert old_slug_en == obj.slug.en |         assert old_slug_en == obj.slug.en | ||||||
|         assert old_slug_nl != obj.slug.nl |         assert old_slug_nl != obj.slug.nl | ||||||
| 
 | 
 | ||||||
|     def test_unique_slug_unique_max_retries(self): |     @classmethod | ||||||
|  |     def test_unique_slug_unique_max_retries(cls): | ||||||
|         """Tests whether the unique slug implementation doesn't |         """Tests whether the unique slug implementation doesn't | ||||||
|         try to find a slug forever and gives up after a while.""" |         try to find a slug forever and gives up after a while.""" | ||||||
| 
 | 
 | ||||||
|         title = 'myuniquetitle' |         title = 'myuniquetitle' | ||||||
| 
 | 
 | ||||||
|         obj = self.MagicSlugModel() |         obj = cls.Model() | ||||||
|         obj.title.en = title |         obj.title.en = title | ||||||
|         obj.save() |         obj.save() | ||||||
| 
 | 
 | ||||||
|         with self.assertRaises(IntegrityError): |         with cls.assertRaises(cls, IntegrityError): | ||||||
|             for _ in range(0, settings.LOCALIZED_FIELDS_MAX_RETRIES + 1): |             for _ in range(0, settings.LOCALIZED_FIELDS_MAX_RETRIES + 1): | ||||||
|                 another_obj = self.MagicSlugModel() |                 another_obj = cls.Model() | ||||||
|                 another_obj.title.en = title |                 another_obj.title.en = title | ||||||
|                 another_obj.save() |                 another_obj.save() | ||||||
| 
 | 
 | ||||||
|     @classmethod |     @classmethod | ||||||
|     def test_unique_slug_utf_auto(cls): |     def test_populate(cls): | ||||||
|         cls._test_unique_slug_utf(cls.AutoSlugModel) |  | ||||||
| 
 |  | ||||||
|     @classmethod |  | ||||||
|     def test_unique_slug_utf_unique(cls): |  | ||||||
|         cls._test_unique_slug_utf(cls.MagicSlugModel) |  | ||||||
| 
 |  | ||||||
|     @classmethod |  | ||||||
|     def test_deconstruct_auto(cls): |  | ||||||
|         cls._test_deconstruct(LocalizedAutoSlugField) |  | ||||||
| 
 |  | ||||||
|     @classmethod |  | ||||||
|     def test_deconstruct_unique(cls): |  | ||||||
|         cls._test_deconstruct(LocalizedUniqueSlugField) |  | ||||||
| 
 |  | ||||||
|     @classmethod |  | ||||||
|     def test_formfield_auto(cls): |  | ||||||
|         cls._test_formfield(LocalizedAutoSlugField) |  | ||||||
| 
 |  | ||||||
|     @classmethod |  | ||||||
|     def test_formfield_unique(cls): |  | ||||||
|         cls._test_formfield(LocalizedUniqueSlugField) |  | ||||||
| 
 |  | ||||||
|     @staticmethod |  | ||||||
|     def _test_populate(model): |  | ||||||
|         """Tests whether the populating feature works correctly.""" |         """Tests whether the populating feature works correctly.""" | ||||||
| 
 | 
 | ||||||
|         obj = model() |         obj = cls.Model() | ||||||
|         obj.title.en = 'this is my title' |         obj.title.en = 'this is my title' | ||||||
|         obj.save() |         obj.save() | ||||||
| 
 | 
 | ||||||
|         assert obj.slug.get('en') == slugify(obj.title) |         assert obj.slug.get('en') == slugify(obj.title) | ||||||
| 
 | 
 | ||||||
|     @staticmethod |     @classmethod | ||||||
|     def _test_populate_multiple_languages(model): |     def test_populate_callable(cls): | ||||||
|         """Tests whether the populating feature correctly |         """Tests whether the populating feature works correctly | ||||||
|         works for all languages.""" |         when you specify a callable.""" | ||||||
| 
 | 
 | ||||||
|         obj = model() |         def generate_slug(instance): | ||||||
|  |             return instance.title | ||||||
|  | 
 | ||||||
|  |         model = get_fake_model({ | ||||||
|  |             'title': LocalizedField(), | ||||||
|  |             'slug': LocalizedUniqueSlugField(populate_from=generate_slug) | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         obj = cls.Model() | ||||||
|         for lang_code, lang_name in settings.LANGUAGES: |         for lang_code, lang_name in settings.LANGUAGES: | ||||||
|             obj.title.set(lang_code, 'title %s' % lang_name) |             obj.title.set(lang_code, 'title %s' % lang_name) | ||||||
| 
 | 
 | ||||||
| @@ -175,53 +130,122 @@ class LocalizedSlugFieldTestCase(TestCase): | |||||||
|         for lang_code, lang_name in settings.LANGUAGES: |         for lang_code, lang_name in settings.LANGUAGES: | ||||||
|             assert obj.slug.get(lang_code) == 'title-%s' % lang_name.lower() |             assert obj.slug.get(lang_code) == 'title-%s' % lang_name.lower() | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def _test_unique_slug(model): |     def test_populate_multiple_from_fields(): | ||||||
|  |         """Tests whether populating the slug from multiple | ||||||
|  |         fields works correctly.""" | ||||||
|  | 
 | ||||||
|  |         model = get_fake_model( | ||||||
|  |             { | ||||||
|  |                 'title': LocalizedField(), | ||||||
|  |                 'name': models.CharField(max_length=255), | ||||||
|  |                 'slug': LocalizedUniqueSlugField(populate_from=('title', 'name')) | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         obj = model() | ||||||
|  |         for lang_code, lang_name in settings.LANGUAGES: | ||||||
|  |             obj.name = 'swen' | ||||||
|  |             obj.title.set(lang_code, 'title %s' % lang_name) | ||||||
|  | 
 | ||||||
|  |         obj.save() | ||||||
|  | 
 | ||||||
|  |         for lang_code, lang_name in settings.LANGUAGES: | ||||||
|  |             assert obj.slug.get(lang_code) == 'title-%s-swen' % lang_name.lower() | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def test_populate_multiple_from_fields_fk(): | ||||||
|  |         """Tests whether populating the slug from multiple | ||||||
|  |         fields works correctly.""" | ||||||
|  | 
 | ||||||
|  |         model_fk = get_fake_model( | ||||||
|  |             { | ||||||
|  |                 'name': LocalizedField(), | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         model = get_fake_model( | ||||||
|  |             { | ||||||
|  |                 'title': LocalizedField(), | ||||||
|  |                 'other': models.ForeignKey(model_fk), | ||||||
|  |                 'slug': LocalizedUniqueSlugField(populate_from=('title', 'other.name')) | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         other = model_fk.objects.create(name={settings.LANGUAGE_CODE: 'swen'}) | ||||||
|  | 
 | ||||||
|  |         obj = model() | ||||||
|  |         for lang_code, lang_name in settings.LANGUAGES: | ||||||
|  |             obj.other_id = other.id | ||||||
|  |             obj.title.set(lang_code, 'title %s' % lang_name) | ||||||
|  | 
 | ||||||
|  |         obj.save() | ||||||
|  | 
 | ||||||
|  |         for lang_code, lang_name in settings.LANGUAGES: | ||||||
|  |             assert obj.slug.get(lang_code) == 'title-%s-swen' % lang_name.lower() | ||||||
|  | 
 | ||||||
|  |     @classmethod | ||||||
|  |     def test_populate_multiple_languages(cls): | ||||||
|  |         """Tests whether the populating feature correctly | ||||||
|  |         works for all languages.""" | ||||||
|  | 
 | ||||||
|  |         obj = cls.Model() | ||||||
|  |         for lang_code, lang_name in settings.LANGUAGES: | ||||||
|  |             obj.title.set(lang_code, 'title %s' % lang_name) | ||||||
|  | 
 | ||||||
|  |         obj.save() | ||||||
|  | 
 | ||||||
|  |         for lang_code, lang_name in settings.LANGUAGES: | ||||||
|  |             assert obj.slug.get(lang_code) == 'title-%s' % lang_name.lower() | ||||||
|  | 
 | ||||||
|  |     @classmethod | ||||||
|  |     def test_unique_slug(cls): | ||||||
|         """Tests whether unique slugs are properly generated.""" |         """Tests whether unique slugs are properly generated.""" | ||||||
| 
 | 
 | ||||||
|         title = 'myuniquetitle' |         title = 'myuniquetitle' | ||||||
| 
 | 
 | ||||||
|         obj = model() |         obj = cls.Model() | ||||||
|         obj.title.en = title |         obj.title.en = title | ||||||
|         obj.save() |         obj.save() | ||||||
| 
 | 
 | ||||||
|         for i in range(1, settings.LOCALIZED_FIELDS_MAX_RETRIES - 1): |         for i in range(1, settings.LOCALIZED_FIELDS_MAX_RETRIES - 1): | ||||||
|             another_obj = model() |             another_obj = cls.Model() | ||||||
|             another_obj.title.en = title |             another_obj.title.en = title | ||||||
|             another_obj.save() |             another_obj.save() | ||||||
| 
 | 
 | ||||||
|             assert another_obj.slug.en == '%s-%d' % (title, i) |             assert another_obj.slug.en == '%s-%d' % (title, i) | ||||||
| 
 | 
 | ||||||
|     @staticmethod |     @classmethod | ||||||
|     def _test_unique_slug_utf(model): |     def test_unique_slug_utf(cls): | ||||||
|         """Tests whether generating a slug works |         """Tests whether generating a slug works | ||||||
|         when the value consists completely out |         when the value consists completely out | ||||||
|         of non-ASCII characters.""" |         of non-ASCII characters.""" | ||||||
| 
 | 
 | ||||||
|         obj = model() |         obj = cls.Model() | ||||||
|         obj.title.en = 'مكاتب للايجار بشارع بورسعيد' |         obj.title.en = 'مكاتب للايجار بشارع بورسعيد' | ||||||
|         obj.save() |         obj.save() | ||||||
| 
 | 
 | ||||||
|         assert obj.slug.en == 'مكاتب-للايجار-بشارع-بورسعيد' |         assert obj.slug.en == 'مكاتب-للايجار-بشارع-بورسعيد' | ||||||
| 
 | 
 | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def _test_deconstruct(field_type): |     def test_deconstruct(): | ||||||
|         """Tests whether the :see:deconstruct |         """Tests whether the :see:deconstruct | ||||||
|         function properly retains options |         function properly retains options | ||||||
|         specified in the constructor.""" |         specified in the constructor.""" | ||||||
| 
 | 
 | ||||||
|         field = field_type(populate_from='title') |         field = LocalizedUniqueSlugField(populate_from='title') | ||||||
|         _, _, _, kwargs = field.deconstruct() |         _, _, _, kwargs = field.deconstruct() | ||||||
| 
 | 
 | ||||||
|         assert 'populate_from' in kwargs |         assert 'populate_from' in kwargs | ||||||
|         assert kwargs['populate_from'] == field.populate_from |         assert kwargs['populate_from'] == field.populate_from | ||||||
| 
 | 
 | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def _test_formfield(field_type): |     def test_formfield(): | ||||||
|         """Tests whether the :see:formfield method |         """Tests whether the :see:formfield method | ||||||
|         returns a valid form field that is hidden.""" |         returns a valid form field that is hidden.""" | ||||||
| 
 | 
 | ||||||
|         form_field = field_type(populate_from='title').formfield() |         form_field = LocalizedUniqueSlugField(populate_from='title').formfield() | ||||||
| 
 | 
 | ||||||
|         assert isinstance(form_field, forms.CharField) |         assert isinstance(form_field, forms.CharField) | ||||||
|         assert isinstance(form_field.widget, forms.HiddenInput) |         assert isinstance(form_field.widget, forms.HiddenInput) | ||||||
| @@ -1,21 +1,10 @@ | |||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
| from django.utils import translation | from django.utils import translation | ||||||
| from django.db.utils import IntegrityError |  | ||||||
| 
 | 
 | ||||||
| from localized_fields import LocalizedField, LocalizedValue, LocalizedFieldForm | from localized_fields.value import LocalizedValue | ||||||
| 
 | 
 | ||||||
| 
 | from .data import get_init_values | ||||||
| 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): | class LocalizedValueTestCase(TestCase): | ||||||
| @@ -42,7 +31,7 @@ class LocalizedValueTestCase(TestCase): | |||||||
| 
 | 
 | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def test_init_default_values(): |     def test_init_default_values(): | ||||||
|         """Tests wehther the __init__ function |         """Tests whether the __init__ function | ||||||
|         of the :see:LocalizedValue accepts the |         of the :see:LocalizedValue accepts the | ||||||
|         default value or an empty dict properly.""" |         default value or an empty dict properly.""" | ||||||
| 
 | 
 | ||||||
| @@ -51,6 +40,18 @@ class LocalizedValueTestCase(TestCase): | |||||||
|         for lang_code, _ in settings.LANGUAGES: |         for lang_code, _ in settings.LANGUAGES: | ||||||
|             assert getattr(value, lang_code) is None |             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 |     @staticmethod | ||||||
|     def test_get_explicit(): |     def test_get_explicit(): | ||||||
|         """Tests whether the the :see:LocalizedValue |         """Tests whether the the :see:LocalizedValue | ||||||
| @@ -142,6 +143,26 @@ class LocalizedValueTestCase(TestCase): | |||||||
|         # there's no actual value |         # there's no actual value | ||||||
|         assert localized_value.get(other_language) != test_value |         assert localized_value.get(other_language) != test_value | ||||||
| 
 | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def test_str_fallback_custom_fallback(): | ||||||
|  |         """Tests whether the :see:LocalizedValue class's | ||||||
|  |         __str__'s fallback functionality properly respects | ||||||
|  |         the LOCALIZED_FIELDS_FALLBACKS setting.""" | ||||||
|  | 
 | ||||||
|  |         test_value = 'myvalue' | ||||||
|  | 
 | ||||||
|  |         settings.LOCALIZED_FIELDS_FALLBACKS = { | ||||||
|  |             'nl': ['ro'] | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         localized_value = LocalizedValue({ | ||||||
|  |             settings.LANGUAGE_CODE: settings.LANGUAGE_CODE, | ||||||
|  |             'ro': 'ro' | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         with translation.override('nl'): | ||||||
|  |             assert str(localized_value) == 'ro' | ||||||
|  | 
 | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def test_deconstruct(): |     def test_deconstruct(): | ||||||
|         """Tests whether the :see:LocalizedValue |         """Tests whether the :see:LocalizedValue | ||||||
| @@ -161,118 +182,3 @@ class LocalizedValueTestCase(TestCase): | |||||||
| 
 | 
 | ||||||
|         value = LocalizedValue('beer') |         value = LocalizedValue('beer') | ||||||
|         assert value.get(settings.LANGUAGE_CODE) == '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_valuei 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 |  | ||||||
| 
 |  | ||||||
|     @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 |  | ||||||
|         ) |  | ||||||
| @@ -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