mirror of
				https://github.com/SectorLabs/django-localized-fields.git
				synced 2025-10-26 17:08:58 +03:00 
			
		
		
		
	Compare commits
	
		
			105 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 88e2d29596 | ||
|  | 588f32086b | ||
|  | 13e2666a51 | ||
|  | 2393539b44 | ||
|  | e322b3d63b | ||
|  | 1b1d24a460 | ||
|  | fb233e8f25 | ||
|  | bca94a3508 | ||
|  | 8c83fa6b49 | ||
|  | 90597da8fd | ||
|  | 752e17064d | ||
|  | def7dae640 | ||
|  | db4324fbf3 | ||
|  | 9ff0b781ab | ||
|  | a76101c9ad | ||
|  | 04f15363bb | ||
|  | a849866bcb | ||
|  | 5e58bc6e24 | ||
|  | 04a750053f | ||
|  | d25b1b92fe | ||
|  | ccc46e1899 | ||
|  | 9063509817 | ||
|  | 8fbe3e8680 | ||
|  | b9f14322be | ||
|  | 24974b2f6b | ||
|  | af02593ebd | ||
|  | f161b5d047 | ||
|  | 77f626fc69 | ||
|  | d7889b0601 | ||
|  | 09cd0a8177 | ||
|  | 2a29efdf14 | ||
|  | d8eb3394d0 | ||
|  | b27f1535ee | ||
|  | cd844fccec | ||
|  | 6a4beca193 | ||
|  | bf0383d742 | ||
|  | 879b857aa5 | ||
|  | 59e8e18b2a | ||
|  | 11cfe5b6e1 | ||
|  | 084f5dd0b6 | ||
|  | 83605cde13 | ||
|  | 7437b58f20 | ||
|  | db6664c84f | ||
|  | 928c4c624d | ||
|  | 3d4f9c413e | ||
|  | f434566338 | ||
|  | 2807ed10c8 | ||
|  | d5f43c783a | ||
|  | 5347db6d8f | ||
|  | 1accee0b59 | ||
|  | 05bcd84a88 | ||
|  | 1c2e013695 | ||
|  | 8b76948328 | ||
|  | 940587d748 | ||
|  | 6522e38f18 | ||
|  | 69cf0df166 | ||
|  | d8c5544e91 | ||
|  | ccc41f9143 | ||
|  | 073846d74b | ||
|  | 0f08eb8280 | ||
|  | 60d14069d8 | ||
|  | 33e9709373 | ||
|  | c4bf151938 | ||
|  | 7d629c186d | ||
|  | cff388cae3 | ||
|  | 08690ab361 | ||
|  | 5ac05efbd0 | ||
|  | aff41f671a | ||
|  | a38d53b609 | ||
|  | 968840188d | ||
|  | e8e044f6e2 | ||
|  | aaf49614f2 | ||
|  | d4c24dea97 | ||
|  | b38ded0cc3 | ||
|  | 226d47e766 | ||
|  | cc4bfb48b9 | ||
|  | d14859a45b | ||
|  | cb7fda5abc | ||
|  | 9000635f1f | ||
|  | dabeb3b79f | ||
|  | 0b4bb7295e | ||
|  | 2b34b6751e | ||
|  | 32696f4e1e | ||
|  | 3ce57ed4cc | ||
|  | 7316d312b4 | ||
|  | 16e23963cc | ||
|  | b10472d3e9 | ||
|  | 833ceb849c | ||
|  | d7382fbf30 | ||
|  | 8ad9268426 | ||
|  | 96ddc77cfc | ||
|  | 51fc6959d2 | ||
|  | 3b28a5e707 | ||
|  | 06873afbda | ||
|  | e5d7cd25e2 | ||
|  | 236ce1648c | ||
|  | aacc712195 | ||
|  | d1790f1fc1 | ||
|  | db93b93046 | ||
|  | a352388243 | ||
|  | 8ba08c389c | ||
|  | 24079a2fcb | ||
|  | 0f4c74a9b2 | ||
|  | 5b93c5ec8f | ||
|  | 817c7e13fe | 
| @@ -1,6 +0,0 @@ | |||||||
| # Ignore virtual environments |  | ||||||
| env/ |  | ||||||
|  |  | ||||||
| # Ignore Python byte code cache |  | ||||||
| *.pyc |  | ||||||
| __pycache__ |  | ||||||
							
								
								
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.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 | ||||||
| @@ -15,3 +16,9 @@ dist/ | |||||||
|  |  | ||||||
| # Ignore stupid .DS_Store | # Ignore stupid .DS_Store | ||||||
| .DS_Store | .DS_Store | ||||||
|  |  | ||||||
|  | # Ignore PyCharm | ||||||
|  | .idea/ | ||||||
|  |  | ||||||
|  | # Ignore tox environments | ||||||
|  | .tox/ | ||||||
|   | |||||||
| @@ -13,19 +13,22 @@ filter: | |||||||
| build: | build: | ||||||
|     environment: |     environment: | ||||||
|         python: '3.5.0' |         python: '3.5.0' | ||||||
|         node: 'v6.2.0' |  | ||||||
|         variables: |         variables: | ||||||
|             DJANGO_SETTINGS_MODULES: settings |             DJANGO_SETTINGS_MODULES: settings | ||||||
|             DATABASE_URL: postgres://scrutinizer:scrutinizer@localhost:5434/localized_fields |             DATABASE_URL: postgres://scrutinizer:scrutinizer@localhost:5434/localized_fields | ||||||
|         postgresql: true |         postgresql: true | ||||||
|         redis: true |  | ||||||
|     dependencies: |     dependencies: | ||||||
|         override: |         override: | ||||||
|             - 'pip install -r requirements/test.txt' |             - 'pip install -r requirements/test.txt' | ||||||
|     tests: |     tests: | ||||||
|         override: |         override: | ||||||
|             - |             - | ||||||
|                 command: coverage run manage.py test |                 command: pep8 ./localized_fields/ | ||||||
|  |             - | ||||||
|  |                 command: flake8 ./localized_fields/ | ||||||
|  |             - | ||||||
|  |                 command: tox | ||||||
|                 coverage: |                 coverage: | ||||||
|                     file: '.coverage' |                     file: '.coverage' | ||||||
|                     format: 'py-cc' |                     format: 'py-cc' | ||||||
|   | |||||||
							
								
								
									
										154
									
								
								README.rst
									
									
									
									
									
								
							
							
						
						
									
										154
									
								
								README.rst
									
									
									
									
									
								
							| @@ -8,13 +8,20 @@ django-localized-fields | |||||||
|     :target: https://scrutinizer-ci.com/g/SectorLabs/django-localized-fields/ |     :target: https://scrutinizer-ci.com/g/SectorLabs/django-localized-fields/ | ||||||
|  |  | ||||||
| .. image:: https://img.shields.io/github/license/SectorLabs/django-localized-fields.svg | .. image:: https://img.shields.io/github/license/SectorLabs/django-localized-fields.svg | ||||||
|  |     :target: https://github.com/SectorLabs/django-localized-fields/blob/master/LICENSE.md | ||||||
|  |  | ||||||
| .. image:: https://badge.fury.io/py/django-localized-fields.svg | .. image:: https://badge.fury.io/py/django-localized-fields.svg | ||||||
|     :target: https://pypi.python.org/pypi/django-localized-fields |     :target: https://pypi.python.org/pypi/django-localized-fields | ||||||
|  |  | ||||||
| ``django-localized-fields`` is an implementation of a field class for Django models that allows the field's value to be set in multiple languages. It does this by utilizing the ``hstore`` type (PostgreSQL specific), which is available as ``models.HStoreField`` in Django 1.10. | ``django-localized-fields`` is an implementation of a field class for Django models that allows the field's value to be set in multiple languages. It does this by utilizing the ``hstore`` type (PostgreSQL specific), which is available as ``models.HStoreField`` since Django 1.10. | ||||||
|  |  | ||||||
| This package requires Python 3.5 or newer, Django 1.10 or newer and PostgreSQL 9.6 or newer. | This package requires Python 3.5 or newer, Django 1.11 or newer and PostgreSQL 9.6 or newer. | ||||||
|  |  | ||||||
|  | Contributors | ||||||
|  | ------------ | ||||||
|  |  | ||||||
|  | * `seroy <https://github.com/seroy/>`_ | ||||||
|  | * `unaizalakain <https://github.com/unaizalakain/>`_ | ||||||
|  |  | ||||||
| Installation | Installation | ||||||
| ------------ | ------------ | ||||||
| @@ -114,6 +121,7 @@ Or get it in a specific language: | |||||||
|      print(new.title.get('en')) # prints 'english title' |      print(new.title.get('en')) # prints 'english title' | ||||||
|      print(new.title.get('ro')) # prints 'romanian title' |      print(new.title.get('ro')) # prints 'romanian title' | ||||||
|      print(new.title.get()) # whatever language is the primary one |      print(new.title.get()) # whatever language is the primary one | ||||||
|  |      print(new.title.get('ar', 'haha')) # prints 'haha' if there is no value in arabic | ||||||
|  |  | ||||||
| You can also explicitly set a value in a certain language: | You can also explicitly set a value in a certain language: | ||||||
|  |  | ||||||
| @@ -129,21 +137,42 @@ Constraints | |||||||
|  |  | ||||||
| **Required/Optional** | **Required/Optional** | ||||||
|  |  | ||||||
| At the moment, it is not possible to select two languages to be marked as required. The constraint is **not** enforced on a database level. | Constraints are enforced on a database level. | ||||||
|  |  | ||||||
| * Make the primary language **required** and the others optional (this is the **default**): | * Optional filling | ||||||
|  |  | ||||||
|     .. code-block:: python |     .. code-block:: python | ||||||
|  |  | ||||||
|         class MyModel(models.Model): |         class MyModel(models.Model): | ||||||
|             title = LocalizedField(required=True) |             title = LocalizedField(blank=True, null=True, required=False) | ||||||
|  |  | ||||||
| * Make all languages optional: | * Make translation required for any language | ||||||
|  |  | ||||||
|     .. code-block:: python |     .. code-block:: python | ||||||
|  |  | ||||||
|         class MyModel(models.Model): |         class MyModel(models.Model): | ||||||
|             title = LocalizedField(null=True) |             title = LocalizedField(blank=False, null=False, required=False) | ||||||
|  |  | ||||||
|  | * Make translation required for specific languages | ||||||
|  |  | ||||||
|  |     .. code-block:: python | ||||||
|  |  | ||||||
|  |         class MyModel(models.Model): | ||||||
|  |             title = LocalizedField(blank=False, null=False, required=['en', 'ro']) | ||||||
|  |  | ||||||
|  | * Make translation required for all languages | ||||||
|  |  | ||||||
|  |     .. code-block:: python | ||||||
|  |  | ||||||
|  |         class MyModel(models.Model): | ||||||
|  |             title = LocalizedField(blank=False, null=False, required=True) | ||||||
|  |  | ||||||
|  | * By default the primary language **required** and the others optional: | ||||||
|  |  | ||||||
|  |     .. code-block:: python | ||||||
|  |  | ||||||
|  |         class MyModel(models.Model): | ||||||
|  |             title = LocalizedField() | ||||||
|  |  | ||||||
| **Uniqueness** | **Uniqueness** | ||||||
|  |  | ||||||
| @@ -199,6 +228,29 @@ Besides ``LocalizedField``, there's also: | |||||||
|                    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 | ||||||
| @@ -208,21 +260,6 @@ 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.fields import LocalizedField, LocalizedAutoSlugField |  | ||||||
|  |  | ||||||
|               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. | ||||||
|  |  | ||||||
| @@ -238,6 +275,75 @@ Besides ``LocalizedField``, there's also: | |||||||
|                    title = LocalizedField() |                    title = LocalizedField() | ||||||
|                    description = LocalizedBleachField() |                    description = LocalizedBleachField() | ||||||
|  |  | ||||||
|  | * ``LocalizedIntegerField`` | ||||||
|  |     This is an experimental field type introduced in version 5.0 and is subject to change. It also has | ||||||
|  |     some pretty major downsides due to the fact that values are stored as strings and are converted | ||||||
|  |     back and forth. | ||||||
|  |  | ||||||
|  |     Allows storing integers in multiple languages. This works exactly like ``LocalizedField`` except that | ||||||
|  |     all values must be integers. Do note that values are stored as strings in your database because | ||||||
|  |     the backing field type is ``hstore``, which only allows storing integers. The ``LocalizedIntegerField`` | ||||||
|  |     takes care of ensuring that all values are integers and converts the stored strings back to integers | ||||||
|  |     when retrieving them from the database. Do not expect to be able to do queries such as: | ||||||
|  |  | ||||||
|  |         .. code-block:: python | ||||||
|  |  | ||||||
|  |             MyModel.objects.filter(score__en__gt=1) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | * ``LocalizedCharField`` and ``LocalizedTextField`` | ||||||
|  |     These 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.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.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 | Experimental feature | ||||||
| ^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^ | ||||||
| Enables the following experimental features: | Enables the following experimental features: | ||||||
| @@ -272,9 +378,9 @@ Frequently asked questions (FAQ) | |||||||
|  |  | ||||||
|     No. Only Python 3.5 or newer is supported. We're using type hints. These do not work well under older versions of Python. |     No. Only Python 3.5 or newer is supported. We're using type hints. These do not work well under older versions of Python. | ||||||
|  |  | ||||||
| 2. Does this package work with Django 1.X? | 2. With what Django versions does this package work? | ||||||
|  |  | ||||||
|     No. Only Django 1.10 or newer is supported. This is because we rely on Django's ``HStoreField``. |     Only Django 1.11 or newer is supported, this includes Django 2.X. This is because we rely on Django's ``HStoreField`` and template-based widget rendering. | ||||||
|  |  | ||||||
| 3. Does this package come with support for Django Admin? | 3. Does this package come with support for Django Admin? | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,11 +1,15 @@ | |||||||
| from django.contrib.admin import ModelAdmin | from django.contrib.admin import ModelAdmin | ||||||
|  |  | ||||||
| from . import widgets | from . import widgets | ||||||
| from .fields import LocalizedField | from .fields import LocalizedField, LocalizedCharField, LocalizedTextField, \ | ||||||
|  |     LocalizedFileField | ||||||
|  |  | ||||||
|  |  | ||||||
| FORMFIELD_FOR_LOCALIZED_FIELDS_DEFAULTS = { | FORMFIELD_FOR_LOCALIZED_FIELDS_DEFAULTS = { | ||||||
|     LocalizedField: {'widget': widgets.AdminLocalizedFieldWidget}, |     LocalizedField: {'widget': widgets.AdminLocalizedFieldWidget}, | ||||||
|  |     LocalizedCharField: {'widget': widgets.AdminLocalizedCharFieldWidget}, | ||||||
|  |     LocalizedTextField: {'widget': widgets.AdminLocalizedFieldWidget}, | ||||||
|  |     LocalizedFileField: {'widget': widgets.AdminLocalizedFileFieldWidget}, | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -26,7 +30,7 @@ class LocalizedFieldsAdminMixin(ModelAdmin): | |||||||
|     def __init__(self, *args, **kwargs): |     def __init__(self, *args, **kwargs): | ||||||
|         """Initializes a new instance of :see:LocalizedFieldsAdminMixin.""" |         """Initializes a new instance of :see:LocalizedFieldsAdminMixin.""" | ||||||
|  |  | ||||||
|         super(LocalizedFieldsAdminMixin, self).__init__(*args, **kwargs) |         super().__init__(*args, **kwargs) | ||||||
|         overrides = FORMFIELD_FOR_LOCALIZED_FIELDS_DEFAULTS.copy() |         overrides = FORMFIELD_FOR_LOCALIZED_FIELDS_DEFAULTS.copy() | ||||||
|         overrides.update(self.formfield_overrides) |         overrides.update(self.formfield_overrides) | ||||||
|         self.formfield_overrides = overrides |         self.formfield_overrides = overrides | ||||||
|   | |||||||
| @@ -1,12 +1,20 @@ | |||||||
| from .field import LocalizedField | from .field import LocalizedField | ||||||
| from .autoslug_field import LocalizedAutoSlugField | from .autoslug_field import LocalizedAutoSlugField | ||||||
| from .uniqueslug_field import LocalizedUniqueSlugField | from .uniqueslug_field import LocalizedUniqueSlugField | ||||||
|  | from .char_field import LocalizedCharField | ||||||
|  | from .text_field import LocalizedTextField | ||||||
|  | from .file_field import LocalizedFileField | ||||||
|  | from .integer_field import LocalizedIntegerField | ||||||
|  |  | ||||||
|  |  | ||||||
| __all__ = [ | __all__ = [ | ||||||
|     'LocalizedField', |     'LocalizedField', | ||||||
|     'LocalizedAutoSlugField', |     'LocalizedAutoSlugField', | ||||||
|     'LocalizedUniqueSlugField', |     'LocalizedUniqueSlugField', | ||||||
|  |     'LocalizedCharField', | ||||||
|  |     'LocalizedTextField', | ||||||
|  |     'LocalizedFileField', | ||||||
|  |     'LocalizedIntegerField' | ||||||
| ] | ] | ||||||
|  |  | ||||||
| try: | try: | ||||||
|   | |||||||
| @@ -1,12 +1,16 @@ | |||||||
| from typing import Callable, Tuple | 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 .field import LocalizedField | from .field import LocalizedField | ||||||
| from ..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 | ||||||
| @@ -147,7 +156,7 @@ class LocalizedAutoSlugField(LocalizedField): | |||||||
|         ] |         ] | ||||||
|  |  | ||||||
|     @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: | ||||||
| @@ -164,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) | ||||||
|   | |||||||
							
								
								
									
										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,6 +1,6 @@ | |||||||
| import json | import json | ||||||
|  |  | ||||||
| from typing import Union | from typing import Union, List, Optional | ||||||
|  |  | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.db.utils import IntegrityError | from django.db.utils import IntegrityError | ||||||
| @@ -27,10 +27,17 @@ class LocalizedField(HStoreField): | |||||||
|     # The descriptor to use for accessing the attribute off of the class. |     # The descriptor to use for accessing the attribute off of the class. | ||||||
|     descriptor_class = LocalizedValueDescriptor |     descriptor_class = LocalizedValueDescriptor | ||||||
|  |  | ||||||
|     def __init__(self, *args, **kwargs): |     def __init__(self, *args, required: Union[bool, List[str]]=None, **kwargs): | ||||||
|         """Initializes a new instance of :see:LocalizedField.""" |         """Initializes a new instance of :see:LocalizedField.""" | ||||||
|  |  | ||||||
|         super(LocalizedField, self).__init__(*args, **kwargs) |         super(LocalizedField, self).__init__(*args, required=required, **kwargs) | ||||||
|  |  | ||||||
|  |         if (self.required is None and self.blank) or self.required is False: | ||||||
|  |             self.required = [] | ||||||
|  |         elif self.required is None and not self.blank: | ||||||
|  |             self.required = [settings.LANGUAGE_CODE] | ||||||
|  |         elif self.required is True: | ||||||
|  |             self.required = [lang_code for lang_code, _ in settings.LANGUAGES] | ||||||
|  |  | ||||||
|     def contribute_to_class(self, model, name, **kwargs): |     def contribute_to_class(self, model, name, **kwargs): | ||||||
|         """Adds this field to the specifed model. |         """Adds this field to the specifed model. | ||||||
| @@ -46,7 +53,7 @@ class LocalizedField(HStoreField): | |||||||
|         setattr(model, self.name, self.descriptor_class(self)) |         setattr(model, self.name, self.descriptor_class(self)) | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def from_db_value(cls, value, *_): |     def from_db_value(cls, value, *_) -> Optional[LocalizedValue]: | ||||||
|         """Turns the specified database value into its Python |         """Turns the specified database value into its Python | ||||||
|         equivalent. |         equivalent. | ||||||
|  |  | ||||||
| @@ -124,6 +131,8 @@ class LocalizedField(HStoreField): | |||||||
|         specified, we'll treat it as an empty :see:LocalizedValue |         specified, we'll treat it as an empty :see:LocalizedValue | ||||||
|         instance, on which the validation will fail. |         instance, on which the validation will fail. | ||||||
|  |  | ||||||
|  |         Dictonaries are converted into :see:LocalizedValue instances. | ||||||
|  |  | ||||||
|         Arguments: |         Arguments: | ||||||
|             value: |             value: | ||||||
|                 The :see:LocalizedValue instance to serialize |                 The :see:LocalizedValue instance to serialize | ||||||
| @@ -134,6 +143,9 @@ class LocalizedField(HStoreField): | |||||||
|             extracted from the specified value. |             extracted from the specified value. | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|  |         if isinstance(value, dict): | ||||||
|  |             value = LocalizedValue(value) | ||||||
|  |  | ||||||
|         # default to None if this is an unknown type |         # default to None if this is an unknown type | ||||||
|         if not isinstance(value, LocalizedValue) and value: |         if not isinstance(value, LocalizedValue) and value: | ||||||
|             value = None |             value = None | ||||||
| @@ -170,7 +182,7 @@ 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): |             if value.get(lang_code) is not None: | ||||||
|                 is_all_null = False |                 is_all_null = False | ||||||
|                 break |                 break | ||||||
|  |  | ||||||
| @@ -182,8 +194,8 @@ class LocalizedField(HStoreField): | |||||||
|         return value |         return value | ||||||
|  |  | ||||||
|     def validate(self, value: LocalizedValue, *_): |     def validate(self, value: LocalizedValue, *_): | ||||||
|         """Validates that the value for the primary language |         """Validates that the values has been filled in for all required | ||||||
|         has been filled in. |         languages | ||||||
|  |  | ||||||
|         Exceptions are raises in order to notify the user |         Exceptions are raises in order to notify the user | ||||||
|         of invalid values. |         of invalid values. | ||||||
| @@ -196,34 +208,19 @@ class LocalizedField(HStoreField): | |||||||
|         if self.null: |         if self.null: | ||||||
|             return |             return | ||||||
|  |  | ||||||
|         primary_lang_val = getattr(value, settings.LANGUAGE_CODE) |         for lang in self.required: | ||||||
|  |             lang_val = getattr(value, settings.LANGUAGE_CODE) | ||||||
|  |  | ||||||
|         if not primary_lang_val: |             if lang_val is None: | ||||||
|             raise IntegrityError( |                 raise IntegrityError('null value in column "%s.%s" violates ' | ||||||
|                 'null value in column "%s.%s" violates not-null constraint' % ( |                                      'not-null constraint' % (self.name, lang)) | ||||||
|                     self.name, |  | ||||||
|                     settings.LANGUAGE_CODE |  | ||||||
|                 ) |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|     def formfield(self, **kwargs): |     def formfield(self, **kwargs): | ||||||
|         """Gets the form field associated with this field.""" |         """Gets the form field associated with this field.""" | ||||||
|  |  | ||||||
|         defaults = { |         defaults = dict( | ||||||
|             'form_class': LocalizedFieldForm |             form_class=LocalizedFieldForm, | ||||||
|         } |             required=False if self.blank else self.required | ||||||
|  |         ) | ||||||
|         defaults.update(kwargs) |         defaults.update(kwargs) | ||||||
|         return super().formfield(**defaults) |         return super().formfield(**defaults) | ||||||
|  |  | ||||||
|     def deconstruct(self): |  | ||||||
|         """Gets the values to pass to :see:__init__ when |  | ||||||
|         re-creating this object.""" |  | ||||||
|  |  | ||||||
|         name, path, args, kwargs = super( |  | ||||||
|             LocalizedField, self).deconstruct() |  | ||||||
|  |  | ||||||
|         if self.uniqueness: |  | ||||||
|             kwargs['uniqueness'] = self.uniqueness |  | ||||||
|  |  | ||||||
|         return name, path, args, kwargs |  | ||||||
|   | |||||||
							
								
								
									
										158
									
								
								localized_fields/fields/file_field.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								localized_fields/fields/file_field.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,158 @@ | |||||||
|  | import json | ||||||
|  | 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} | ||||||
|  |         defaults.update(kwargs) | ||||||
|  |         return super().formfield(**defaults) | ||||||
|  |  | ||||||
|  |     def value_to_string(self, obj): | ||||||
|  |         value = self.value_from_object(obj) | ||||||
|  |         if isinstance(value, LocalizedFileValue): | ||||||
|  |             return json.dumps({k: v.name for k, v | ||||||
|  |                                in value.__dict__.items()}) | ||||||
|  |         else: | ||||||
|  |             return super().value_to_string(obj) | ||||||
							
								
								
									
										81
									
								
								localized_fields/fields/integer_field.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								localized_fields/fields/integer_field.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | |||||||
|  | from typing import Optional, Union, Dict | ||||||
|  |  | ||||||
|  | from django.conf import settings | ||||||
|  | from django.db.utils import IntegrityError | ||||||
|  |  | ||||||
|  | from .field import LocalizedField | ||||||
|  | from ..value import LocalizedValue, LocalizedIntegerValue | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedIntegerField(LocalizedField): | ||||||
|  |     """Stores integers as a localized value.""" | ||||||
|  |  | ||||||
|  |     attr_class = LocalizedIntegerValue | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def from_db_value(cls, value, *_) -> Optional[LocalizedIntegerValue]: | ||||||
|  |         db_value = super().from_db_value(value) | ||||||
|  |         if db_value is None: | ||||||
|  |             return db_value | ||||||
|  |  | ||||||
|  |         # if we were used in an expression somehow then it might be | ||||||
|  |         # that we're returning an individual value or an array, so | ||||||
|  |         # we should not convert that into an :see:LocalizedIntegerValue | ||||||
|  |         if not isinstance(db_value, LocalizedValue): | ||||||
|  |             return db_value | ||||||
|  |  | ||||||
|  |         return cls._convert_localized_value(db_value) | ||||||
|  |  | ||||||
|  |     def to_python(self, value: Union[Dict[str, int], int, None]) -> LocalizedIntegerValue: | ||||||
|  |         """Converts the value from a database value into a Python value.""" | ||||||
|  |  | ||||||
|  |         db_value = super().to_python(value) | ||||||
|  |         return self._convert_localized_value(db_value) | ||||||
|  |  | ||||||
|  |     def get_prep_value(self, value: LocalizedIntegerValue) -> dict: | ||||||
|  |         """Gets the value in a format to store into the database.""" | ||||||
|  |  | ||||||
|  |         # apply default values | ||||||
|  |         default_values = LocalizedIntegerValue(self.default) | ||||||
|  |         if isinstance(value, LocalizedIntegerValue): | ||||||
|  |             for lang_code, _ in settings.LANGUAGES: | ||||||
|  |                 local_value = value.get(lang_code) | ||||||
|  |                 if local_value is None: | ||||||
|  |                     value.set(lang_code, default_values.get(lang_code, None)) | ||||||
|  |  | ||||||
|  |         prepped_value = super().get_prep_value(value) | ||||||
|  |         if prepped_value is None: | ||||||
|  |             return None | ||||||
|  |  | ||||||
|  |         # make sure all values are proper integers | ||||||
|  |         for lang_code, _ in settings.LANGUAGES: | ||||||
|  |             local_value = prepped_value[lang_code] | ||||||
|  |             try: | ||||||
|  |                 if local_value is not None: | ||||||
|  |                     int(local_value) | ||||||
|  |             except (TypeError, ValueError): | ||||||
|  |                 raise IntegrityError('non-integer value in column "%s.%s" violates ' | ||||||
|  |                                      'integer constraint' % (self.name, lang_code)) | ||||||
|  |  | ||||||
|  |             # convert to a string before saving because the underlying | ||||||
|  |             # type is hstore, which only accept strings | ||||||
|  |             prepped_value[lang_code] = str(local_value) if local_value is not None else None | ||||||
|  |  | ||||||
|  |         return prepped_value | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def _convert_localized_value(value: LocalizedValue) -> LocalizedIntegerValue: | ||||||
|  |         """Converts from :see:LocalizedValue to :see:LocalizedIntegerValue.""" | ||||||
|  |  | ||||||
|  |         integer_values = {} | ||||||
|  |         for lang_code, _ in settings.LANGUAGES: | ||||||
|  |             local_value = value.get(lang_code, None) | ||||||
|  |             if local_value is None or local_value.strip() == '': | ||||||
|  |                 local_value = None | ||||||
|  |  | ||||||
|  |             try: | ||||||
|  |                 integer_values[lang_code] = int(local_value) | ||||||
|  |             except (ValueError, TypeError): | ||||||
|  |                 integer_values[lang_code] = None | ||||||
|  |  | ||||||
|  |         return LocalizedIntegerValue(integer_values) | ||||||
							
								
								
									
										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,10 +1,14 @@ | |||||||
| from typing import List | from typing import List, Union | ||||||
|  |  | ||||||
| from django import forms | from django import forms | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
|  | from django.core.exceptions import ValidationError | ||||||
|  | from django.forms.widgets import FILE_INPUT_CONTRADICTION | ||||||
|  |  | ||||||
| from .value import LocalizedValue | from .value import LocalizedValue, LocalizedStringValue, \ | ||||||
| from .widgets import LocalizedFieldWidget |     LocalizedFileValue | ||||||
|  | from .widgets import LocalizedFieldWidget, LocalizedCharFieldWidget, \ | ||||||
|  |     LocalizedFileWidget | ||||||
|  |  | ||||||
|  |  | ||||||
| class LocalizedFieldForm(forms.MultiValueField): | class LocalizedFieldForm(forms.MultiValueField): | ||||||
| @@ -12,32 +16,34 @@ class LocalizedFieldForm(forms.MultiValueField): | |||||||
|     the field in multiple languages.""" |     the field in multiple languages.""" | ||||||
|  |  | ||||||
|     widget = LocalizedFieldWidget |     widget = LocalizedFieldWidget | ||||||
|  |     field_class = forms.fields.CharField | ||||||
|     value_class = LocalizedValue |     value_class = LocalizedValue | ||||||
|  |  | ||||||
|     def __init__(self, *args, **kwargs): |     def __init__(self, *args, required: Union[bool, List[str]]=False, **kwargs): | ||||||
|         """Initializes a new instance of :see:LocalizedFieldForm.""" |         """Initializes a new instance of :see:LocalizedFieldForm.""" | ||||||
|  |  | ||||||
|         fields = [] |         fields = [] | ||||||
|  |  | ||||||
|         for lang_code, _ in settings.LANGUAGES: |         for lang_code, _ in settings.LANGUAGES: | ||||||
|             field_options = {'required': False} |             field_options = dict( | ||||||
|  |                 required=required if type(required) is bool else (lang_code in | ||||||
|             if lang_code == settings.LANGUAGE_CODE: |                                                                   required), | ||||||
|                 field_options['required'] = kwargs.get('required', True) |                 label=lang_code | ||||||
|  |             ) | ||||||
|             field_options['label'] = lang_code |             fields.append(self.field_class(**field_options)) | ||||||
|             fields.append(forms.fields.CharField(**field_options)) |  | ||||||
|  |  | ||||||
|         super(LocalizedFieldForm, self).__init__( |         super(LocalizedFieldForm, self).__init__( | ||||||
|             fields, |             fields, | ||||||
|  |             required=required if type(required) is bool else True, | ||||||
|             require_all_fields=False, |             require_all_fields=False, | ||||||
|             *args, **kwargs |             *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: |         # set 'required' attribute for each widget separately | ||||||
|  |         for field, widget in zip(self.fields, self.widget.widgets): | ||||||
|  |             widget.is_required = field.required | ||||||
|  |  | ||||||
|  |     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. | ||||||
|  |  | ||||||
| @@ -56,3 +62,107 @@ class LocalizedFieldForm(forms.MultiValueField): | |||||||
|             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,5 +1,5 @@ | |||||||
| .localized-fields-widget { | .localized-fields-widget { | ||||||
|     margin-left: 160px; |     display: inline-block; | ||||||
| } | } | ||||||
|  |  | ||||||
| .localized-fields-widget.tabs { | .localized-fields-widget.tabs { | ||||||
| @@ -32,11 +32,12 @@ | |||||||
|     opacity: 1; |     opacity: 1; | ||||||
| } | } | ||||||
|  |  | ||||||
| .localized-fields-widget.tabs .localized-fields-widget.tab a { | .localized-fields-widget.tabs .localized-fields-widget.tab label { | ||||||
|     padding: 5px 10px; |     padding: 5px 10px; | ||||||
|     display: inline-block; |     display: inline-block; | ||||||
|     text-decoration: none; |     text-decoration: none; | ||||||
|     color: #fff; |     color: #fff; | ||||||
|  |     width: initial; | ||||||
| } | } | ||||||
|  |  | ||||||
| .localized-fields-widget.tabs .localized-fields-widget.tab.active, | .localized-fields-widget.tabs .localized-fields-widget.tab.active, | ||||||
| @@ -45,3 +46,7 @@ | |||||||
|     border-color: #79aec8; |     border-color: #79aec8; | ||||||
|     opacity: 1; |     opacity: 1; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .localized-fields-widget p.file-upload { | ||||||
|  |     margin-left: 0; | ||||||
|  | } | ||||||
|   | |||||||
| @@ -1,10 +1,10 @@ | |||||||
| (function($) { | (function($) { | ||||||
|     var syncTabs = function(lang) { |     var syncTabs = function(lang) { | ||||||
|         $('.localized-fields-widget.tab a:contains("'+lang+'")').each(function(){ |         $('.localized-fields-widget.tab label:contains("'+lang+'")').each(function(){ | ||||||
|             $(this).parents('.localized-fields-widget[role="tabs"]').find('.localized-fields-widget.tab').removeClass('active'); |             $(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.tab').addClass('active'); | ||||||
|             $(this).parents('.localized-fields-widget[role="tabs"]').children('.localized-fields-widget [role="tabpanel"]').hide(); |             $(this).parents('.localized-fields-widget[role="tabs"]').children('.localized-fields-widget [role="tabpanel"]').hide(); | ||||||
|             $($(this).attr('href')).show(); |             $('#'+$(this).attr('for')).show(); | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -13,7 +13,7 @@ | |||||||
|         // set first tab as active |         // set first tab as active | ||||||
|         $('.localized-fields-widget[role="tabs"]').each(function () { |         $('.localized-fields-widget[role="tabs"]').each(function () { | ||||||
|             $(this).find('.localized-fields-widget.tab:first').addClass('active'); |             $(this).find('.localized-fields-widget.tab:first').addClass('active'); | ||||||
|             $($(this).find('.localized-fields-widget.tab:first a').attr('href')).show(); |             $('#'+$(this).find('.localized-fields-widget.tab:first label').attr('for')).show(); | ||||||
|         }); |         }); | ||||||
|         // try set active last selected tab |         // try set active last selected tab | ||||||
|         if (window.sessionStorage) { |         if (window.sessionStorage) { | ||||||
| @@ -23,7 +23,7 @@ | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         $('.localized-fields-widget.tab a').click(function(event) { |         $('.localized-fields-widget.tab label').click(function(event) { | ||||||
|             event.preventDefault(); |             event.preventDefault(); | ||||||
|             syncTabs(this.innerText); |             syncTabs(this.innerText); | ||||||
|             if (window.sessionStorage) { |             if (window.sessionStorage) { | ||||||
|   | |||||||
| @@ -1,14 +1,16 @@ | |||||||
|  | {% with widget_id=widget.attrs.id %} | ||||||
| <div class="localized-fields-widget" role="tabs" data-synctabs="translation"> | <div class="localized-fields-widget" role="tabs" data-synctabs="translation"> | ||||||
|     <ul class="localized-fields-widget tabs"> |     <ul class="localized-fields-widget tabs"> | ||||||
|     {% for key, lang in available_languages %} |     {% for widget in widget.subwidgets %} | ||||||
|         <li class="localized-fields-widget tab"> |         <li class="localized-fields-widget tab"> | ||||||
|             <a href="#{{ id }}_{{ key }}">{{ lang|capfirst }}</a> |             <label for="{{ widget_id }}_{{ widget.lang_code }}">{{ widget.lang_name|capfirst }}</label> | ||||||
|         </li> |         </li> | ||||||
|     {% endfor %} |     {% endfor %} | ||||||
|     </ul> |     </ul> | ||||||
| {% for key, widget in widgets %} | {% for widget in widget.subwidgets %} | ||||||
|     <div role="tabpanel" id="{{ id }}_{{ key }}"> |     <div role="tabpanel" id="{{ widget_id }}_{{ widget.lang_code }}"> | ||||||
|         {{ widget }} |         {% include widget.template_name %} | ||||||
|     </div> |     </div> | ||||||
| {% endfor %} | {% endfor %} | ||||||
| </div> | </div> | ||||||
|  | {% endwith %} | ||||||
|   | |||||||
							
								
								
									
										16
									
								
								localized_fields/templates/localized_fields/multiwidget.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								localized_fields/templates/localized_fields/multiwidget.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | {% with widget_id=widget.attrs.id %} | ||||||
|  | <div class="localized-fields-widget" role="tabs" data-synctabs="translation"> | ||||||
|  |     <ul class="localized-fields-widget tabs"> | ||||||
|  |     {% for widget in widget.subwidgets %} | ||||||
|  |         <li class="localized-fields-widget tab"> | ||||||
|  |             <label for="{{ widget_id }}_{{ widget.lang_code }}">{{ widget.lang_name|capfirst }}</label> | ||||||
|  |         </li> | ||||||
|  |     {% endfor %} | ||||||
|  |     </ul> | ||||||
|  |     {% for widget in widget.subwidgets %} | ||||||
|  |         <div role="tabpanel" id="{{ widget_id }}_{{ widget.lang_code }}"> | ||||||
|  |             {% include widget.template_name %} | ||||||
|  |         </div> | ||||||
|  |     {% endfor %} | ||||||
|  | </div> | ||||||
|  | {% endwith %} | ||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -1,11 +1,14 @@ | |||||||
|  | import deprecation | ||||||
| import collections | import collections | ||||||
|  |  | ||||||
|  | from typing import Optional | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.utils import translation | from django.utils import translation | ||||||
|  |  | ||||||
|  |  | ||||||
| class LocalizedValue(dict): | class LocalizedValue(dict): | ||||||
|     """Represents the value of a :see:LocalizedField.""" |     """Represents the value of a :see:LocalizedField.""" | ||||||
|  |     default_value = None | ||||||
|  |  | ||||||
|     def __init__(self, keys: dict=None): |     def __init__(self, keys: dict=None): | ||||||
|         """Initializes a new instance of :see:LocalizedValue. |         """Initializes a new instance of :see:LocalizedValue. | ||||||
| @@ -20,7 +23,7 @@ class LocalizedValue(dict): | |||||||
|         super().__init__({}) |         super().__init__({}) | ||||||
|         self._interpret_value(keys) |         self._interpret_value(keys) | ||||||
|  |  | ||||||
|     def get(self, language: str=None) -> str: |     def get(self, language: str=None, default: str=None) -> str: | ||||||
|         """Gets the underlying value in the specified or |         """Gets the underlying value in the specified or | ||||||
|         primary language. |         primary language. | ||||||
|  |  | ||||||
| @@ -35,7 +38,8 @@ class LocalizedValue(dict): | |||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         language = language or settings.LANGUAGE_CODE |         language = language or settings.LANGUAGE_CODE | ||||||
|         return super().get(language, None) |         value = super().get(language, default) | ||||||
|  |         return value if value is not None else default | ||||||
|  |  | ||||||
|     def set(self, language: str, value: str): |     def set(self, language: str, value: str): | ||||||
|         """Sets the value in the specified language. |         """Sets the value in the specified language. | ||||||
| @@ -60,7 +64,7 @@ class LocalizedValue(dict): | |||||||
|             contained in this instance. |             contained in this instance. | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         path = 'localized_fields.fields.LocalizedValue' |         path = 'localized_fields.value.%s' % self.__class__.__name__ | ||||||
|         return path, [self.__dict__], {} |         return path, [self.__dict__], {} | ||||||
|  |  | ||||||
|     def _interpret_value(self, value): |     def _interpret_value(self, value): | ||||||
| @@ -83,31 +87,44 @@ class LocalizedValue(dict): | |||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         for lang_code, _ in settings.LANGUAGES: |         for lang_code, _ in settings.LANGUAGES: | ||||||
|             self.set(lang_code, None) |             self.set(lang_code, self.default_value) | ||||||
|  |  | ||||||
|         if isinstance(value, str): |         if isinstance(value, str): | ||||||
|             self.set(settings.LANGUAGE_CODE, value) |             self.set(settings.LANGUAGE_CODE, value) | ||||||
|  |  | ||||||
|         elif isinstance(value, dict): |         elif isinstance(value, dict): | ||||||
|             for lang_code, _ in settings.LANGUAGES: |             for lang_code, _ in settings.LANGUAGES: | ||||||
|                 lang_value = value.get(lang_code) or None |                 lang_value = value.get(lang_code, self.default_value) | ||||||
|                 self.set(lang_code, lang_value) |                 self.set(lang_code, lang_value) | ||||||
|  |  | ||||||
|         elif isinstance(value, collections.Iterable): |         elif isinstance(value, collections.Iterable): | ||||||
|             for val in value: |             for val in value: | ||||||
|                 self._interpret_value(val) |                 self._interpret_value(val) | ||||||
|  |  | ||||||
|  |     def translate(self) -> Optional[str]: | ||||||
|  |         """Gets the value in the current language or falls | ||||||
|  |         back to the next 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 None | ||||||
|  |  | ||||||
|  |         return None | ||||||
|  |  | ||||||
|     def __str__(self) -> str: |     def __str__(self) -> str: | ||||||
|         """Gets the value in the current language, or falls |         """Gets the value in the current language or falls | ||||||
|         back to the primary language if there's no value |         back to the next language if there's no value in the | ||||||
|         in the current language.""" |         current language.""" | ||||||
|  |  | ||||||
|         value = self.get(translation.get_language()) |         return self.translate() or '' | ||||||
|  |  | ||||||
|         if not value: |  | ||||||
|             value = self.get(settings.LANGUAGE_CODE) |  | ||||||
|  |  | ||||||
|         return value or '' |  | ||||||
|  |  | ||||||
|     def __eq__(self, other): |     def __eq__(self, other): | ||||||
|         """Compares :paramref:self to :paramref:other for |         """Compares :paramref:self to :paramref:other for | ||||||
| @@ -156,4 +173,58 @@ class LocalizedValue(dict): | |||||||
|     def __repr__(self):  # pragma: no cover |     def __repr__(self):  # pragma: no cover | ||||||
|         """Gets a textual representation of this object.""" |         """Gets a textual representation of this object.""" | ||||||
|  |  | ||||||
|         return 'LocalizedValue<%s> 0x%s' % (dict(self), id(self)) |         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__()) | ||||||
|  |  | ||||||
|  |     @deprecation.deprecated(deprecated_in='4.6', removed_in='5.0', | ||||||
|  |                             current_version='4.6', | ||||||
|  |                             details='Use the translate() function instead.') | ||||||
|  |     def localized(self): | ||||||
|  |         """Returns value for current language""" | ||||||
|  |  | ||||||
|  |         return self.get(translation.get_language()) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedIntegerValue(LocalizedValue): | ||||||
|  |     """All values are integers.""" | ||||||
|  |  | ||||||
|  |     default_value = None | ||||||
|  |  | ||||||
|  |     def translate(self): | ||||||
|  |         """Gets the value in the current language, or | ||||||
|  |         in the configured fallbck language.""" | ||||||
|  |  | ||||||
|  |         value = super().translate() | ||||||
|  |         if value is None or (isinstance(value, str) and value.strip() == ''): | ||||||
|  |             return None | ||||||
|  |  | ||||||
|  |         return int(value) | ||||||
|  |  | ||||||
|  |     def __int__(self): | ||||||
|  |         """Gets the value in the current language as an integer.""" | ||||||
|  |  | ||||||
|  |         value = self.translate() | ||||||
|  |         if value is None: | ||||||
|  |             return self.default_value | ||||||
|  |  | ||||||
|  |         return int(value) | ||||||
|   | |||||||
| @@ -1,27 +1,34 @@ | |||||||
|  | import copy | ||||||
|  |  | ||||||
| from typing import List | from typing import List | ||||||
|  |  | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django import forms | from django import forms | ||||||
| from django.contrib.admin import widgets | from django.contrib.admin import widgets | ||||||
| from django.template.loader import render_to_string |  | ||||||
|  |  | ||||||
| from .value import LocalizedValue | from .value import LocalizedValue | ||||||
|  |  | ||||||
|  |  | ||||||
| class LocalizedFieldWidget(forms.MultiWidget): | class LocalizedFieldWidget(forms.MultiWidget): | ||||||
|     """Widget that has an input box for every language.""" |     """Widget that has an input box for every language.""" | ||||||
|  |     template_name = 'localized_fields/multiwidget.html' | ||||||
|     widget = forms.Textarea |     widget = forms.Textarea | ||||||
|  |  | ||||||
|     def __init__(self, *args, **kwargs): |     def __init__(self, *args, **kwargs): | ||||||
|         """Initializes a new instance of :see:LocalizedFieldWidget.""" |         """Initializes a new instance of :see:LocalizedFieldWidget.""" | ||||||
|  |  | ||||||
|         initial_widgets = [ |         initial_widgets = [ | ||||||
|             self.widget |             copy.copy(self.widget) | ||||||
|             for _ in settings.LANGUAGES |             for _ in settings.LANGUAGES | ||||||
|         ] |         ] | ||||||
|  |  | ||||||
|         super().__init__(initial_widgets, *args, **kwargs) |         super().__init__(initial_widgets, *args, **kwargs) | ||||||
|  |  | ||||||
|  |         for ((lang_code, lang_name), widget) in zip(settings.LANGUAGES, self.widgets): | ||||||
|  |             widget.attrs['lang'] = lang_code | ||||||
|  |             widget.lang_code = lang_code | ||||||
|  |             widget.lang_name = lang_name | ||||||
|  |  | ||||||
|     def decompress(self, value: LocalizedValue) -> List[str]: |     def decompress(self, value: LocalizedValue) -> List[str]: | ||||||
|         """Decompresses the specified value so |         """Decompresses the specified value so | ||||||
|         it can be spread over the internal widgets. |         it can be spread over the internal widgets. | ||||||
| @@ -44,12 +51,8 @@ class LocalizedFieldWidget(forms.MultiWidget): | |||||||
|  |  | ||||||
|         return result |         return result | ||||||
|  |  | ||||||
|  |     def get_context(self, name, value, attrs): | ||||||
| class AdminLocalizedFieldWidget(LocalizedFieldWidget): |         context = super(forms.MultiWidget, self).get_context(name, value, attrs) | ||||||
|     widget = widgets.AdminTextareaWidget |  | ||||||
|     template = 'localized_fields/admin/widget.html' |  | ||||||
|  |  | ||||||
|     def render(self, name, value, attrs=None): |  | ||||||
|         if self.is_localized: |         if self.is_localized: | ||||||
|             for widget in self.widgets: |             for widget in self.widgets: | ||||||
|                 widget.is_localized = self.is_localized |                 widget.is_localized = self.is_localized | ||||||
| @@ -57,30 +60,63 @@ class AdminLocalizedFieldWidget(LocalizedFieldWidget): | |||||||
|         # in self.widgets. |         # in self.widgets. | ||||||
|         if not isinstance(value, list): |         if not isinstance(value, list): | ||||||
|             value = self.decompress(value) |             value = self.decompress(value) | ||||||
|         output = [] |  | ||||||
|         final_attrs = self.build_attrs(attrs) |         final_attrs = context['widget']['attrs'] | ||||||
|  |         input_type = final_attrs.pop('type', None) | ||||||
|         id_ = final_attrs.get('id') |         id_ = final_attrs.get('id') | ||||||
|  |         subwidgets = [] | ||||||
|         for i, widget in enumerate(self.widgets): |         for i, widget in enumerate(self.widgets): | ||||||
|  |             if input_type is not None: | ||||||
|  |                 widget.input_type = input_type | ||||||
|  |             widget_name = '%s_%s' % (name, i) | ||||||
|             try: |             try: | ||||||
|                 widget_value = value[i] |                 widget_value = value[i] | ||||||
|             except IndexError: |             except IndexError: | ||||||
|                 widget_value = None |                 widget_value = None | ||||||
|             if id_: |             if id_: | ||||||
|                 final_attrs = dict(final_attrs, id='%s_%s' % (id_, i)) |                 widget_attrs = final_attrs.copy() | ||||||
|             widget_attrs = self.build_widget_attrs(widget, widget_value, final_attrs) |                 widget_attrs['id'] = '%s_%s' % (id_, i) | ||||||
|             output.append(widget.render(name + '_%s' % i, widget_value, widget_attrs)) |             else: | ||||||
|         context = { |                 widget_attrs = final_attrs | ||||||
|             'id': final_attrs.get('id'), |             widget_attrs = self.build_widget_attrs(widget, widget_value, widget_attrs) | ||||||
|             'name': name, |             widget_context = widget.get_context(widget_name, widget_value, widget_attrs)['widget'] | ||||||
|             'widgets': zip([code for code, lang in settings.LANGUAGES], output), |             widget_context.update(dict( | ||||||
|             'available_languages': settings.LANGUAGES |                 lang_code=widget.lang_code, | ||||||
|         } |                 lang_name=widget.lang_name | ||||||
|         return render_to_string(self.template, context) |             )) | ||||||
|  |             subwidgets.append(widget_context) | ||||||
|  |         context['widget']['subwidgets'] = subwidgets | ||||||
|  |         return context | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def build_widget_attrs(widget, value, attrs): |     def build_widget_attrs(widget, value, attrs): | ||||||
|         attrs = dict(attrs)  # Copy attrs to avoid modifying the argument. |         attrs = dict(attrs)  # Copy attrs to avoid modifying the argument. | ||||||
|  |  | ||||||
|         if (not widget.use_required_attribute(value) or not widget.is_required) \ |         if (not widget.use_required_attribute(value) or not widget.is_required) \ | ||||||
|                 and 'required' in attrs: |                 and 'required' in attrs: | ||||||
|             del attrs['required'] |             del attrs['required'] | ||||||
|  |  | ||||||
|         return attrs |         return attrs | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 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): | ||||||
|  |     template_name = 'localized_fields/admin/widget.html' | ||||||
|  |     widget = widgets.AdminTextareaWidget | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AdminLocalizedCharFieldWidget(AdminLocalizedFieldWidget): | ||||||
|  |     widget = widgets.AdminTextInputWidget | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AdminLocalizedFileFieldWidget(AdminLocalizedFieldWidget): | ||||||
|  |     widget = widgets.AdminFileWidget | ||||||
|   | |||||||
| @@ -1 +1,2 @@ | |||||||
| django-postgres-extra==1.11 | django-postgres-extra==1.21a9 | ||||||
|  | deprecation==2.0.3 | ||||||
|   | |||||||
| @@ -1,17 +1,16 @@ | |||||||
| -r base.txt | -r base.txt | ||||||
|  |  | ||||||
| coverage==4.2 |  | ||||||
| Django==1.10.2 |  | ||||||
| django-autoslug==1.9.3 | django-autoslug==1.9.3 | ||||||
| django-bleach==0.3.0 | django-bleach==0.3.0 | ||||||
| django-coverage-plugin==1.3.1 | django-coverage-plugin==1.3.1 | ||||||
| psycopg2==2.6.2 | psycopg2==2.7.3.2 | ||||||
| pylint==1.6.4 | pylint==1.8.1 | ||||||
| pylint-common==0.2.2 | pylint-common==0.2.5 | ||||||
| pylint-django==0.7.2 | pylint-django==0.8.0 | ||||||
| pylint-plugin-utils==0.2.4 | pylint-plugin-utils==0.2.6 | ||||||
| coverage==4.2 | coverage==4.4.2 | ||||||
| django-coverage-plugin==1.3.1 | django-coverage-plugin==1.3.1 | ||||||
| flake8==3.0.4 | flake8==3.5.0 | ||||||
| pep8==1.7.0 | pep8==1.7.1 | ||||||
| dj-database-url==0.4.1 | dj-database-url==0.4.2 | ||||||
|  | tox==2.9.1 | ||||||
|   | |||||||
							
								
								
									
										8
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								setup.py
									
									
									
									
									
								
							| @@ -2,12 +2,12 @@ import os | |||||||
|  |  | ||||||
| from setuptools import find_packages, setup | from setuptools import find_packages, setup | ||||||
|  |  | ||||||
| with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme: | with open(os.path.join(os.path.dirname(__file__), 'README.rst'), encoding='utf-8') as readme: | ||||||
|     README = readme.read() |     README = readme.read() | ||||||
|  |  | ||||||
| setup( | setup( | ||||||
|     name='django-localized-fields', |     name='django-localized-fields', | ||||||
|     version='4.2', |     version='5.0a3', | ||||||
|     packages=find_packages(exclude=['tests']), |     packages=find_packages(exclude=['tests']), | ||||||
|     include_package_data=True, |     include_package_data=True, | ||||||
|     license='MIT License', |     license='MIT License', | ||||||
| @@ -18,7 +18,9 @@ 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.11' |         'django-postgres-extra>=1.21a11', | ||||||
|  |         'Django>=1.11', | ||||||
|  |         'deprecation==2.0.3' | ||||||
|     ], |     ], | ||||||
|     classifiers=[ |     classifiers=[ | ||||||
|         'Environment :: Web Environment', |         'Environment :: Web Environment', | ||||||
|   | |||||||
| @@ -1,3 +1,5 @@ | |||||||
|  | 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 | ||||||
| @@ -5,24 +7,27 @@ from django.contrib.postgres.operations import HStoreExtension | |||||||
| from localized_fields.models import LocalizedModel | 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, (model_base,), attributes) | ||||||
|  |  | ||||||
|     model = type(name, (LocalizedModel,), 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) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -16,7 +16,6 @@ class LocalizedBulkTestCase(TestCase): | |||||||
|         a :see:LocalizedUniqueSlugField in the model.""" |         a :see:LocalizedUniqueSlugField in the model.""" | ||||||
|  |  | ||||||
|         model = get_fake_model( |         model = get_fake_model( | ||||||
|             'BulkSlugInsertModel', |  | ||||||
|             { |             { | ||||||
|                 'name': LocalizedField(), |                 'name': LocalizedField(), | ||||||
|                 'slug': LocalizedUniqueSlugField(populate_from='name', include_time=True), |                 'slug': LocalizedUniqueSlugField(populate_from='name', include_time=True), | ||||||
|   | |||||||
| @@ -24,17 +24,15 @@ class LocalizedExpressionsTestCase(TestCase): | |||||||
|         super(LocalizedExpressionsTestCase, cls).setUpClass() |         super(LocalizedExpressionsTestCase, cls).setUpClass() | ||||||
|  |  | ||||||
|         cls.TestModel1 = get_fake_model( |         cls.TestModel1 = get_fake_model( | ||||||
|             'LocalizedExpressionsTestCase2', |  | ||||||
|             { |             { | ||||||
|                 'name': models.CharField(null=False, blank=False, max_length=255), |                 'name': models.CharField(null=False, blank=False, max_length=255), | ||||||
|             } |             } | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         cls.TestModel2 = get_fake_model( |         cls.TestModel2 = get_fake_model( | ||||||
|             'LocalizedExpressionsTestCase1', |  | ||||||
|             { |             { | ||||||
|                 'text': LocalizedField(), |                 'text': LocalizedField(), | ||||||
|                 'other': models.ForeignKey(cls.TestModel1, related_name='features') |                 'other': models.ForeignKey(cls.TestModel1, related_name='features', on_delete=models.CASCADE) | ||||||
|             } |             } | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -9,11 +9,31 @@ from localized_fields.forms import LocalizedFieldForm | |||||||
| from localized_fields.value import LocalizedValue | from localized_fields.value import LocalizedValue | ||||||
|  |  | ||||||
| from .data import get_init_values | from .data import get_init_values | ||||||
|  | from .fake_model import get_fake_model | ||||||
|  |  | ||||||
|  |  | ||||||
| class LocalizedFieldTestCase(TestCase): | class LocalizedFieldTestCase(TestCase): | ||||||
|     """Tests the :see:LocalizedField class.""" |     """Tests the :see:LocalizedField class.""" | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_init(): | ||||||
|  |         """Tests whether the :see:__init__ function | ||||||
|  |         correctly handles parameters""" | ||||||
|  |  | ||||||
|  |         field = LocalizedField(blank=True) | ||||||
|  |         assert field.required == [] | ||||||
|  |  | ||||||
|  |         field = LocalizedField(blank=False) | ||||||
|  |         assert field.required == [settings.LANGUAGE_CODE] | ||||||
|  |  | ||||||
|  |         field = LocalizedField(required=True) | ||||||
|  |         assert field.required == [lang_code for lang_code, _ in | ||||||
|  |                                   settings.LANGUAGES] | ||||||
|  |  | ||||||
|  |         field = LocalizedField(required=False) | ||||||
|  |         assert field.required == [] | ||||||
|  |  | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def test_from_db_value(): |     def test_from_db_value(): | ||||||
|         """Tests whether the :see:from_db_value function |         """Tests whether the :see:from_db_value function | ||||||
| @@ -156,3 +176,90 @@ class LocalizedFieldTestCase(TestCase): | |||||||
|             LocalizedField().formfield(), |             LocalizedField().formfield(), | ||||||
|             LocalizedFieldForm |             LocalizedFieldForm | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |         # case optional filling | ||||||
|  |         field = LocalizedField(blank=True, required=[]) | ||||||
|  |         assert not field.formfield().required | ||||||
|  |         for field in field.formfield().fields: | ||||||
|  |             assert not field.required | ||||||
|  |  | ||||||
|  |         # case required for any language | ||||||
|  |         field = LocalizedField(blank=False, required=[]) | ||||||
|  |         assert field.formfield().required | ||||||
|  |         for field in field.formfield().fields: | ||||||
|  |             assert not field.required | ||||||
|  |  | ||||||
|  |         # case required for specific languages | ||||||
|  |         required_langs = ['ro', 'nl'] | ||||||
|  |         field = LocalizedField(blank=False, required=required_langs) | ||||||
|  |         assert field.formfield().required | ||||||
|  |         for field in field.formfield().fields: | ||||||
|  |             if field.label in required_langs: | ||||||
|  |                 assert field.required | ||||||
|  |             else: | ||||||
|  |                 assert not field.required | ||||||
|  |  | ||||||
|  |         # case required for all languages | ||||||
|  |         field = LocalizedField(blank=False, required=True) | ||||||
|  |         assert field.formfield().required | ||||||
|  |         for field in field.formfield().fields: | ||||||
|  |             assert field.required | ||||||
|  |  | ||||||
|  |     def test_required_all(self): | ||||||
|  |         """Tests whether passing required=True properly validates | ||||||
|  |         that all languages are filled in.""" | ||||||
|  |  | ||||||
|  |         model = get_fake_model(dict( | ||||||
|  |             title=LocalizedField(required=True) | ||||||
|  |         )) | ||||||
|  |  | ||||||
|  |         with self.assertRaises(IntegrityError): | ||||||
|  |             model.objects.create(title=dict(ro='romanian', nl='dutch')) | ||||||
|  |  | ||||||
|  |         with self.assertRaises(IntegrityError): | ||||||
|  |             model.objects.create(title=dict(nl='dutch')) | ||||||
|  |  | ||||||
|  |         with self.assertRaises(IntegrityError): | ||||||
|  |             model.objects.create(title=dict(random='random')) | ||||||
|  |  | ||||||
|  |         with self.assertRaises(IntegrityError): | ||||||
|  |             model.objects.create(title=dict()) | ||||||
|  |  | ||||||
|  |         with self.assertRaises(IntegrityError): | ||||||
|  |             model.objects.create(title=None) | ||||||
|  |  | ||||||
|  |         with self.assertRaises(IntegrityError): | ||||||
|  |             model.objects.create(title='') | ||||||
|  |  | ||||||
|  |         with self.assertRaises(IntegrityError): | ||||||
|  |             model.objects.create(title='         ') | ||||||
|  |  | ||||||
|  |     def test_required_some(self): | ||||||
|  |         """Tests whether passing an array to required, | ||||||
|  |         properly validates whether the specified languages | ||||||
|  |         are marked as required.""" | ||||||
|  |  | ||||||
|  |         model = get_fake_model(dict( | ||||||
|  |             title=LocalizedField(required=['nl', 'ro']) | ||||||
|  |         )) | ||||||
|  |  | ||||||
|  |         with self.assertRaises(IntegrityError): | ||||||
|  |             model.objects.create(title=dict(ro='romanian', nl='dutch')) | ||||||
|  |  | ||||||
|  |         with self.assertRaises(IntegrityError): | ||||||
|  |             model.objects.create(title=dict(nl='dutch')) | ||||||
|  |  | ||||||
|  |         with self.assertRaises(IntegrityError): | ||||||
|  |             model.objects.create(title=dict(random='random')) | ||||||
|  |  | ||||||
|  |         with self.assertRaises(IntegrityError): | ||||||
|  |             model.objects.create(title=dict()) | ||||||
|  |  | ||||||
|  |         with self.assertRaises(IntegrityError): | ||||||
|  |             model.objects.create(title=None) | ||||||
|  |  | ||||||
|  |         with self.assertRaises(IntegrityError): | ||||||
|  |             model.objects.create(title='') | ||||||
|  |  | ||||||
|  |         with self.assertRaises(IntegrityError): | ||||||
|  |             model.objects.create(title='         ') | ||||||
|   | |||||||
							
								
								
									
										165
									
								
								tests/test_file_field.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								tests/test_file_field.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,165 @@ | |||||||
|  | import os | ||||||
|  | import shutil | ||||||
|  | import tempfile as sys_tempfile | ||||||
|  | import pickle | ||||||
|  | import json | ||||||
|  |  | ||||||
|  | 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 is 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' | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     @override_settings(LANGUAGES=(('en', 'English'),)) | ||||||
|  |     def test_value_to_string(cls): | ||||||
|  |         """Tests whether the :see:LocalizedFileField | ||||||
|  |         class's :see:value_to_string function works properly.""" | ||||||
|  |  | ||||||
|  |         temp_file = File(tempfile.NamedTemporaryFile()) | ||||||
|  |         instance = cls.FileFieldModel() | ||||||
|  |         field = cls.FileFieldModel._meta.get_field('file') | ||||||
|  |         field.upload_to = '' | ||||||
|  |         instance.file.en.save('testfilename', temp_file) | ||||||
|  |         expected_value_to_string = json.dumps({'en': 'testfilename'}) | ||||||
|  |         assert field.value_to_string(instance) == expected_value_to_string | ||||||
|  |  | ||||||
|  |     @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) is 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 | ||||||
							
								
								
									
										40
									
								
								tests/test_file_field_form.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								tests/test_file_field_form.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | |||||||
|  | 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 | ||||||
							
								
								
									
										24
									
								
								tests/test_file_widget.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								tests/test_file_widget.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | |||||||
|  | from django.test import TestCase | ||||||
|  |  | ||||||
|  | from localized_fields.value import LocalizedFileValue | ||||||
|  | from localized_fields.widgets import LocalizedFileWidget | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedFileWidgetTestCase(TestCase): | ||||||
|  |     """Tests the workings of the :see:LocalizedFiledWidget class.""" | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_get_context(): | ||||||
|  |         """Tests whether the :see:get_context correctly | ||||||
|  |         handles 'required' attribute, separately for each subwidget.""" | ||||||
|  |  | ||||||
|  |         widget = LocalizedFileWidget() | ||||||
|  |         widget.widgets[0].is_required = True | ||||||
|  |         widget.widgets[1].is_required = True | ||||||
|  |         widget.widgets[2].is_required = False | ||||||
|  |         context = widget.get_context(name='test', | ||||||
|  |                                      value=LocalizedFileValue(dict(en='test')), | ||||||
|  |                                      attrs=dict(required=True)) | ||||||
|  |         assert 'required' not in context['widget']['subwidgets'][0]['attrs'] | ||||||
|  |         assert context['widget']['subwidgets'][1]['attrs']['required'] | ||||||
|  |         assert 'required' not in context['widget']['subwidgets'][2]['attrs'] | ||||||
| @@ -11,8 +11,8 @@ class LocalizedFieldFormTestCase(TestCase): | |||||||
|     def test_init(): |     def test_init(): | ||||||
|         """Tests whether the constructor correctly |         """Tests whether the constructor correctly | ||||||
|         creates a field for every language.""" |         creates a field for every language.""" | ||||||
|  |         # case required for specific language | ||||||
|         form = LocalizedFieldForm() |         form = LocalizedFieldForm(required=[settings.LANGUAGE_CODE]) | ||||||
|  |  | ||||||
|         for (lang_code, _), field in zip(settings.LANGUAGES, form.fields): |         for (lang_code, _), field in zip(settings.LANGUAGES, form.fields): | ||||||
|             assert field.label == lang_code |             assert field.label == lang_code | ||||||
| @@ -22,6 +22,25 @@ class LocalizedFieldFormTestCase(TestCase): | |||||||
|             else: |             else: | ||||||
|                 assert not field.required |                 assert not field.required | ||||||
|  |  | ||||||
|  |         # case required for all languages | ||||||
|  |         form = LocalizedFieldForm(required=True) | ||||||
|  |         assert form.required | ||||||
|  |         for field in form.fields: | ||||||
|  |             assert field.required | ||||||
|  |  | ||||||
|  |         # case optional filling | ||||||
|  |         form = LocalizedFieldForm(required=False) | ||||||
|  |         assert not form.required | ||||||
|  |         for field in form.fields: | ||||||
|  |             assert not field.required | ||||||
|  |  | ||||||
|  |         # case required for any language | ||||||
|  |         form = LocalizedFieldForm(required=[]) | ||||||
|  |         assert form.required | ||||||
|  |         for field in form.fields: | ||||||
|  |             assert not field.required | ||||||
|  |  | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def test_compress(): |     def test_compress(): | ||||||
|         """Tests whether the :see:compress function |         """Tests whether the :see:compress function | ||||||
|   | |||||||
							
								
								
									
										176
									
								
								tests/test_integer_field.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								tests/test_integer_field.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,176 @@ | |||||||
|  | from django.test import TestCase | ||||||
|  | from django.db.utils import IntegrityError | ||||||
|  | from django.conf import settings | ||||||
|  | from django.db import connection | ||||||
|  | from django.utils import translation | ||||||
|  |  | ||||||
|  | from localized_fields.fields import LocalizedIntegerField | ||||||
|  |  | ||||||
|  | from .fake_model import get_fake_model | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedIntegerFieldTestCase(TestCase): | ||||||
|  |     """Tests whether the :see:LocalizedIntegerField | ||||||
|  |     and :see:LocalizedIntegerValue works properly.""" | ||||||
|  |  | ||||||
|  |     TestModel = None | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def setUpClass(cls): | ||||||
|  |         super().setUpClass() | ||||||
|  |  | ||||||
|  |         cls.TestModel = get_fake_model({ | ||||||
|  |             'score': LocalizedIntegerField() | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |     def test_basic(self): | ||||||
|  |         """Tests the basics of storing integer values.""" | ||||||
|  |  | ||||||
|  |         obj = self.TestModel() | ||||||
|  |         for index, (lang_code, _) in enumerate(settings.LANGUAGES): | ||||||
|  |             obj.score.set(lang_code, index + 1) | ||||||
|  |         obj.save() | ||||||
|  |  | ||||||
|  |         obj = self.TestModel.objects.all().first() | ||||||
|  |         for index, (lang_code, _) in enumerate(settings.LANGUAGES): | ||||||
|  |             assert obj.score.get(lang_code) == index + 1 | ||||||
|  |  | ||||||
|  |     def test_primary_language_required(self): | ||||||
|  |         """Tests whether the primary language is required by | ||||||
|  |         default and all other languages are optiona.""" | ||||||
|  |  | ||||||
|  |         # not filling in anything should raise IntegrityError, | ||||||
|  |         # the primary language is required | ||||||
|  |         with self.assertRaises(IntegrityError): | ||||||
|  |             obj = self.TestModel() | ||||||
|  |             obj.save() | ||||||
|  |  | ||||||
|  |         # when filling all other languages besides the primary language | ||||||
|  |         # should still raise an error because the primary is always required | ||||||
|  |         with self.assertRaises(IntegrityError): | ||||||
|  |             obj = self.TestModel() | ||||||
|  |             for lang_code, _ in settings.LANGUAGES: | ||||||
|  |                 if lang_code == settings.LANGUAGE_CODE: | ||||||
|  |                     continue | ||||||
|  |                 obj.score.set(lang_code, 23) | ||||||
|  |             obj.save() | ||||||
|  |  | ||||||
|  |     def test_default_value_none(self): | ||||||
|  |         """Tests whether the default value for optional languages | ||||||
|  |         is NoneType.""" | ||||||
|  |  | ||||||
|  |         obj = self.TestModel() | ||||||
|  |         obj.score.set(settings.LANGUAGE_CODE, 1234) | ||||||
|  |         obj.save() | ||||||
|  |  | ||||||
|  |         for lang_code, _ in settings.LANGUAGES: | ||||||
|  |             if lang_code == settings.LANGUAGE_CODE: | ||||||
|  |                 continue | ||||||
|  |  | ||||||
|  |             assert obj.score.get(lang_code) is None | ||||||
|  |  | ||||||
|  |     def test_translate(self): | ||||||
|  |         """Tests whether casting the value to an integer | ||||||
|  |         results in the value being returned in the currently | ||||||
|  |         active language as an integer.""" | ||||||
|  |  | ||||||
|  |         obj = self.TestModel() | ||||||
|  |         for index, (lang_code, _) in enumerate(settings.LANGUAGES): | ||||||
|  |             obj.score.set(lang_code, index + 1) | ||||||
|  |         obj.save() | ||||||
|  |  | ||||||
|  |         obj.refresh_from_db() | ||||||
|  |         for index, (lang_code, _) in enumerate(settings.LANGUAGES): | ||||||
|  |             with translation.override(lang_code): | ||||||
|  |                 assert int(obj.score) == index + 1 | ||||||
|  |                 assert obj.score.translate() == index + 1 | ||||||
|  |  | ||||||
|  |     def test_translate_primary_fallback(self): | ||||||
|  |         """Tests whether casting the value to an integer | ||||||
|  |         results in the value begin returned in the active | ||||||
|  |         language and falls back to the primary language | ||||||
|  |         if there is no value in that language.""" | ||||||
|  |  | ||||||
|  |         obj = self.TestModel() | ||||||
|  |         obj.score.set(settings.LANGUAGE_CODE, 25) | ||||||
|  |  | ||||||
|  |         secondary_language = settings.LANGUAGES[-1][0] | ||||||
|  |         assert obj.score.get(secondary_language) is None | ||||||
|  |  | ||||||
|  |         with translation.override(secondary_language): | ||||||
|  |             assert obj.score.translate() == 25 | ||||||
|  |             assert int(obj.score) == 25 | ||||||
|  |  | ||||||
|  |     def test_get_default_value(self): | ||||||
|  |         """Tests whether getting the value in a specific | ||||||
|  |         language properly returns the specified default | ||||||
|  |         in case it is not available.""" | ||||||
|  |  | ||||||
|  |         obj = self.TestModel() | ||||||
|  |         obj.score.set(settings.LANGUAGE_CODE, 25) | ||||||
|  |  | ||||||
|  |         secondary_language = settings.LANGUAGES[-1][0] | ||||||
|  |         assert obj.score.get(secondary_language) is None | ||||||
|  |         assert obj.score.get(secondary_language, 1337) == 1337 | ||||||
|  |  | ||||||
|  |     def test_completely_optional(self): | ||||||
|  |         """Tests whether having all languages optional | ||||||
|  |         works properly.""" | ||||||
|  |  | ||||||
|  |         model = get_fake_model({ | ||||||
|  |             'score': LocalizedIntegerField(null=True, required=[], blank=True) | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |         obj = model() | ||||||
|  |         obj.save() | ||||||
|  |  | ||||||
|  |         for lang_code, _ in settings.LANGUAGES: | ||||||
|  |             assert getattr(obj.score, lang_code) is None | ||||||
|  |  | ||||||
|  |     def test_store_string(self): | ||||||
|  |         """Tests whether the field properly raises | ||||||
|  |         an error when trying to store a non-integer.""" | ||||||
|  |  | ||||||
|  |         for lang_code, _ in settings.LANGUAGES: | ||||||
|  |             obj = self.TestModel() | ||||||
|  |             with self.assertRaises(IntegrityError): | ||||||
|  |                 obj.score.set(lang_code, 'haha') | ||||||
|  |                 obj.save() | ||||||
|  |  | ||||||
|  |     def test_none_if_illegal_value_stored(self): | ||||||
|  |         """Tests whether None is returned for a language | ||||||
|  |         if the value stored in the database is not an | ||||||
|  |         integer.""" | ||||||
|  |  | ||||||
|  |         obj = self.TestModel() | ||||||
|  |         obj.score.set(settings.LANGUAGE_CODE, 25) | ||||||
|  |         obj.save() | ||||||
|  |  | ||||||
|  |         with connection.cursor() as cursor: | ||||||
|  |             table_name = self.TestModel._meta.db_table | ||||||
|  |             cursor.execute("update %s set score = 'en=>haha'" % table_name); | ||||||
|  |  | ||||||
|  |         obj.refresh_from_db() | ||||||
|  |         assert obj.score.get(settings.LANGUAGE_CODE) is None | ||||||
|  |  | ||||||
|  |     def test_default_value(self): | ||||||
|  |         """Tests whether a default is properly set | ||||||
|  |         when specified.""" | ||||||
|  |  | ||||||
|  |         model = get_fake_model({ | ||||||
|  |             'score': LocalizedIntegerField(default={settings.LANGUAGE_CODE: 75}) | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |         obj = model.objects.create() | ||||||
|  |         assert obj.score.get(settings.LANGUAGE_CODE) == 75 | ||||||
|  |  | ||||||
|  |         obj = model() | ||||||
|  |         for lang_code, _ in settings.LANGUAGES: | ||||||
|  |             obj.score.set(lang_code, None) | ||||||
|  |         obj.save() | ||||||
|  |  | ||||||
|  |         for lang_code, _ in settings.LANGUAGES: | ||||||
|  |             if lang_code == settings.LANGUAGE_CODE: | ||||||
|  |                 assert obj.score.get(lang_code) == 75 | ||||||
|  |             else: | ||||||
|  |                 assert obj.score.get(lang_code) is None | ||||||
| @@ -18,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() | ||||||
|             } |             } | ||||||
|   | |||||||
							
								
								
									
										47
									
								
								tests/test_query_set.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								tests/test_query_set.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | |||||||
|  | from django.conf import settings | ||||||
|  | from django.test import TestCase | ||||||
|  | from django.utils import translation | ||||||
|  |  | ||||||
|  | from localized_fields.fields import LocalizedField | ||||||
|  |  | ||||||
|  | from .fake_model import get_fake_model | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedQuerySetTestCase(TestCase): | ||||||
|  |     """Tests query sets with models containing :see:LocalizedField.""" | ||||||
|  |  | ||||||
|  |     Model = None | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def setUpClass(cls): | ||||||
|  |         """Creates the test models in the database.""" | ||||||
|  |  | ||||||
|  |         super(LocalizedQuerySetTestCase, cls).setUpClass() | ||||||
|  |  | ||||||
|  |         cls.Model = get_fake_model( | ||||||
|  |             { | ||||||
|  |                 'title': LocalizedField(), | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def test_assign_raw_dict(cls): | ||||||
|  |         inst = cls.Model() | ||||||
|  |         inst.title = dict(en='Bread', ro='Paine') | ||||||
|  |         inst.save() | ||||||
|  |  | ||||||
|  |         inst = cls.Model.objects.get(pk=inst.pk) | ||||||
|  |         assert inst.title.en == 'Bread' | ||||||
|  |         assert inst.title.ro == 'Paine' | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def test_assign_raw_dict_update(cls): | ||||||
|  |         inst = cls.Model.objects.create( | ||||||
|  |             title=dict(en='Bread', ro='Paine')) | ||||||
|  |  | ||||||
|  |         cls.Model.objects.update( | ||||||
|  |             title=dict(en='Beer', ro='Bere')) | ||||||
|  |  | ||||||
|  |         inst = cls.Model.objects.get(pk=inst.pk) | ||||||
|  |         assert inst.title.en == 'Beer' | ||||||
|  |         assert inst.title.ro == 'Bere' | ||||||
| @@ -1,6 +1,7 @@ | |||||||
| 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 | ||||||
| @@ -8,7 +9,6 @@ from django.utils.text import slugify | |||||||
|  |  | ||||||
| from localized_fields.fields import ( | from localized_fields.fields import ( | ||||||
|     LocalizedField, |     LocalizedField, | ||||||
|     LocalizedAutoSlugField, |  | ||||||
|     LocalizedUniqueSlugField |     LocalizedUniqueSlugField | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -19,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): | ||||||
| @@ -27,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 | ||||||
| @@ -75,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) | ||||||
| @@ -93,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) | ||||||
| @@ -115,62 +81,55 @@ 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_update(cls): | ||||||
|  |         obj = cls.Model.objects.create(title={settings.LANGUAGE_CODE: 'mytitle'}) | ||||||
|  |         assert obj.slug.get() == 'mytitle' | ||||||
|  |         obj.title.set(settings.LANGUAGE_CODE, 'othertitle') | ||||||
|  |         obj.save() | ||||||
|  |         assert obj.slug.get() == 'othertitle' | ||||||
|  |  | ||||||
|  |     @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 | ||||||
|  |  | ||||||
|  |         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) | ||||||
|  |  | ||||||
| @@ -180,52 +139,120 @@ class LocalizedSlugFieldTestCase(TestCase): | |||||||
|             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, on_delete=models.CASCADE), | ||||||
|  |                 '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,6 +1,7 @@ | |||||||
|  | from django.db.models import F | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.test import TestCase |  | ||||||
| from django.utils import translation | from django.utils import translation | ||||||
|  | from django.test import TestCase, override_settings | ||||||
|  |  | ||||||
| from localized_fields.value import LocalizedValue | from localized_fields.value import LocalizedValue | ||||||
|  |  | ||||||
| @@ -89,18 +90,6 @@ class LocalizedValueTestCase(TestCase): | |||||||
|             assert localized_value.get(language) == value |             assert localized_value.get(language) == value | ||||||
|             assert getattr(localized_value, language) == value |             assert getattr(localized_value, language) == value | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_str(): |  | ||||||
|         """Tests whether the :see:LocalizedValue |  | ||||||
|         class's __str__ works properly.""" |  | ||||||
|  |  | ||||||
|         keys = get_init_values() |  | ||||||
|         localized_value = LocalizedValue(keys) |  | ||||||
|  |  | ||||||
|         for language, value in keys.items(): |  | ||||||
|             translation.activate(language) |  | ||||||
|             assert str(localized_value) == value |  | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def test_eq(): |     def test_eq(): | ||||||
|         """Tests whether the __eq__ operator |         """Tests whether the __eq__ operator | ||||||
| @@ -115,9 +104,21 @@ class LocalizedValueTestCase(TestCase): | |||||||
|         assert a != b |         assert a != b | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def test_str_fallback(): |     def test_translate(): | ||||||
|         """Tests whether the :see:LocalizedValue |         """Tests whether the :see:LocalizedValue | ||||||
|         class's __str__'s fallback functionality |         class's __str__ works properly.""" | ||||||
|  |  | ||||||
|  |         keys = get_init_values() | ||||||
|  |         localized_value = LocalizedValue(keys) | ||||||
|  |  | ||||||
|  |         for language, value in keys.items(): | ||||||
|  |             translation.activate(language) | ||||||
|  |             assert localized_value.translate() == value | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_translate_fallback(): | ||||||
|  |         """Tests whether the :see:LocalizedValue | ||||||
|  |         class's translate()'s fallback functionality | ||||||
|         works properly.""" |         works properly.""" | ||||||
|  |  | ||||||
|         test_value = 'myvalue' |         test_value = 'myvalue' | ||||||
| @@ -130,19 +131,61 @@ class LocalizedValueTestCase(TestCase): | |||||||
|  |  | ||||||
|         # make sure that, by default it returns |         # make sure that, by default it returns | ||||||
|         # the value in the default language |         # the value in the default language | ||||||
|         assert str(localized_value) == test_value |         assert localized_value.translate() == test_value | ||||||
|  |  | ||||||
|         # make sure that it falls back to the |         # make sure that it falls back to the | ||||||
|         # primary language when there's no value |         # primary language when there's no value | ||||||
|         # available in the current language |         # available in the current language | ||||||
|         translation.activate(other_language) |         translation.activate(other_language) | ||||||
|         assert str(localized_value) == test_value |         assert localized_value.translate() == test_value | ||||||
|  |  | ||||||
|         # make sure that it's just __str__ falling |         # make sure that it's just __str__ falling | ||||||
|         # back and that for the other language |         # back and that for the other language | ||||||
|         # 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_translate_none(): | ||||||
|  |         """Tests whether the :see:LocalizedValue | ||||||
|  |         class's translate() method properly returns | ||||||
|  |         None when there is no value.""" | ||||||
|  |  | ||||||
|  |         # with no value, we always expect it to return None | ||||||
|  |         localized_value = LocalizedValue() | ||||||
|  |         assert localized_value.translate() == None | ||||||
|  |         assert str(localized_value) == '' | ||||||
|  |  | ||||||
|  |         # with no value for the default language, the default | ||||||
|  |         # behavior is to return None, unless a custom fallback | ||||||
|  |         # chain is configured, which there is not for this test | ||||||
|  |         other_language = settings.LANGUAGES[-1][0] | ||||||
|  |         localized_value = LocalizedValue({ | ||||||
|  |             other_language: 'hey' | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |         translation.activate(settings.LANGUAGE_CODE) | ||||||
|  |         assert localized_value.translate() == None | ||||||
|  |         assert str(localized_value) == '' | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_translate_fallback_custom_fallback(): | ||||||
|  |         """Tests whether the :see:LocalizedValue class's | ||||||
|  |         translate()'s fallback functionality properly respects | ||||||
|  |         the LOCALIZED_FIELDS_FALLBACKS setting.""" | ||||||
|  |  | ||||||
|  |         fallbacks = { | ||||||
|  |             'nl': ['ro'] | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         localized_value = LocalizedValue({ | ||||||
|  |             settings.LANGUAGE_CODE: settings.LANGUAGE_CODE, | ||||||
|  |             'ro': 'ro' | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |         with override_settings(LOCALIZED_FIELDS_FALLBACKS=fallbacks): | ||||||
|  |             with translation.override('nl'): | ||||||
|  |                 assert localized_value.translate() == 'ro' | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def test_deconstruct(): |     def test_deconstruct(): | ||||||
|         """Tests whether the :see:LocalizedValue |         """Tests whether the :see:LocalizedValue | ||||||
| @@ -162,3 +205,11 @@ class LocalizedValueTestCase(TestCase): | |||||||
|  |  | ||||||
|         value = LocalizedValue('beer') |         value = LocalizedValue('beer') | ||||||
|         assert value.get(settings.LANGUAGE_CODE) == 'beer' |         assert value.get(settings.LANGUAGE_CODE) == 'beer' | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_construct_expression(): | ||||||
|  |         """Tests whether passing expressions as values | ||||||
|  |         works properly and are not converted to string.""" | ||||||
|  |  | ||||||
|  |         value = LocalizedValue(dict(en=F('other'))) | ||||||
|  |         assert isinstance(value.en, F) | ||||||
|   | |||||||
| @@ -1,3 +1,4 @@ | |||||||
|  | import re | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
|  |  | ||||||
| @@ -15,6 +16,7 @@ class LocalizedFieldWidgetTestCase(TestCase): | |||||||
|  |  | ||||||
|         widget = LocalizedFieldWidget() |         widget = LocalizedFieldWidget() | ||||||
|         assert len(widget.widgets) == len(settings.LANGUAGES) |         assert len(widget.widgets) == len(settings.LANGUAGES) | ||||||
|  |         assert len(set(widget.widgets)) == len(widget.widgets) | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def test_decompress(): |     def test_decompress(): | ||||||
| @@ -42,3 +44,40 @@ class LocalizedFieldWidgetTestCase(TestCase): | |||||||
|  |  | ||||||
|         for _, value in zip(settings.LANGUAGES, decompressed_values): |         for _, value in zip(settings.LANGUAGES, decompressed_values): | ||||||
|             assert not value |             assert not value | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_get_context_required(): | ||||||
|  |         """Tests whether the :see:get_context correctly | ||||||
|  |         handles 'required' attribute, separately for each subwidget.""" | ||||||
|  |  | ||||||
|  |         widget = LocalizedFieldWidget() | ||||||
|  |         widget.widgets[0].is_required = True | ||||||
|  |         widget.widgets[1].is_required = False | ||||||
|  |         context = widget.get_context(name='test', value=LocalizedValue(), | ||||||
|  |                                      attrs=dict(required=True)) | ||||||
|  |         assert context['widget']['subwidgets'][0]['attrs']['required'] | ||||||
|  |         assert 'required' not in context['widget']['subwidgets'][1]['attrs'] | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_get_context_langs(): | ||||||
|  |         """Tests whether the :see:get_context contains 'lang_code' and | ||||||
|  |         'lang_name' attribute for each subwidget.""" | ||||||
|  |  | ||||||
|  |         widget = LocalizedFieldWidget() | ||||||
|  |         context = widget.get_context(name='test', value=LocalizedValue(), | ||||||
|  |                                      attrs=dict()) | ||||||
|  |         subwidgets_context = context['widget']['subwidgets'] | ||||||
|  |         for widget, context in zip(widget.widgets, subwidgets_context): | ||||||
|  |             assert 'lang_code' in context | ||||||
|  |             assert 'lang_name' in context | ||||||
|  |             assert widget.lang_code == context['lang_code'] | ||||||
|  |             assert widget.lang_name == context['lang_name'] | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_render(): | ||||||
|  |         """Tests whether the :see:LocalizedFieldWidget correctly | ||||||
|  |         render.""" | ||||||
|  |  | ||||||
|  |         widget = LocalizedFieldWidget() | ||||||
|  |         output = widget.render(name='title', value=None) | ||||||
|  |         assert bool(re.search('<label (.|\n|\t)*>\w+<\/label>', output)) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user