mirror of
				https://github.com/SectorLabs/django-localized-fields.git
				synced 2025-10-28 01:48:57 +03:00 
			
		
		
		
	Compare commits
	
		
			51 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 84658f6010 | ||
|  | 0e29871458 | ||
|  | ffe235d3ac | ||
|  | d7db2c58c0 | ||
|  | 6a7545a910 | ||
|  | 11bf4ee88a | ||
|  | d3223eca53 | ||
|  | f0ac0f7f25 | ||
|  | 93ffce557c | ||
|  | 7c432baec7 | ||
|  | 476a20ba88 | ||
|  | ad99b77bcd | ||
|  | 151250505d | ||
|  | d8b872758c | ||
|  | a0ca977cab | ||
|  | eb2cb6b244 | ||
|  | ed15fb0079 | ||
|  | f59904f8ea | ||
|  | f20966d6d2 | ||
|  | 25417b5815 | ||
|  | abd1587ca0 | ||
|  | 60fc79e9ff | ||
|  | ca470fc577 | ||
|  | fac1a595aa | ||
|  | d66f5085a8 | ||
|  | c6e8321ae7 | ||
|  | b2f50ec82b | ||
|  | ff836836bf | ||
|  | acf8867974 | ||
|  | 4922a1b93f | ||
|  | d308e773cf | ||
|  | b3b88d6d28 | ||
|  | 6c902229ce | ||
|  | 4f83cbf4ed | ||
|  | 88e2d29596 | ||
|  | 588f32086b | ||
|  | 13e2666a51 | ||
|  | 2393539b44 | ||
|  | e322b3d63b | ||
|  | 1b1d24a460 | ||
|  | fb233e8f25 | ||
|  | bca94a3508 | ||
|  | 8c83fa6b49 | ||
|  | 90597da8fd | ||
|  | 752e17064d | ||
|  | def7dae640 | ||
|  | 8ba0540237 | ||
|  | b3624916b2 | ||
|  | db4324fbf3 | ||
|  | 9ff0b781ab | ||
|  | a76101c9ad | 
							
								
								
									
										92
									
								
								.circleci/config.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								.circleci/config.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | ||||
| version: 2 | ||||
| jobs: | ||||
|     test-python35: | ||||
|         docker: | ||||
|             - image: python:3.5-alpine | ||||
|             - image: postgres:11.0 | ||||
|               environment: | ||||
|                   POSTGRES_DB: 'localizedfields' | ||||
|                   POSTGRES_USER: 'localizedfields' | ||||
|                   POSTGRES_PASSWORD: 'localizedfields' | ||||
|         steps: | ||||
|             - checkout | ||||
|             - run: | ||||
|                 name: Install packages | ||||
|                 command: apk add postgresql-libs gcc musl-dev postgresql-dev git | ||||
|             - run: | ||||
|                 name: Install Python packages | ||||
|                 command: pip install -r requirements/test.txt | ||||
|             - run: | ||||
|                 name: Run tests | ||||
|                 command: tox -e 'py35-dj{111,20,21}' | ||||
|                 environment: | ||||
|                     DATABASE_URL: 'postgres://localizedfields:localizedfields@localhost:5432/localizedfields' | ||||
|  | ||||
|     test-python36: | ||||
|         docker: | ||||
|             - image: python:3.6-alpine | ||||
|             - image: postgres:11.0 | ||||
|               environment: | ||||
|                   POSTGRES_DB: 'localizedfields' | ||||
|                   POSTGRES_USER: 'localizedfields' | ||||
|                   POSTGRES_PASSWORD: 'localizedfields' | ||||
|         steps: | ||||
|             - checkout | ||||
|             - run: | ||||
|                 name: Install packages | ||||
|                 command: apk add postgresql-libs gcc musl-dev postgresql-dev git | ||||
|             - run: | ||||
|                 name: Install Python packages | ||||
|                 command: pip install -r requirements/test.txt | ||||
|             - run: | ||||
|                 name: Run tests | ||||
|                 command: tox -e 'py36-dj{111,20,21}' | ||||
|                 environment: | ||||
|                     DATABASE_URL: 'postgres://localizedfields:localizedfields@localhost:5432/localizedfields' | ||||
|  | ||||
|     test-python37: | ||||
|         docker: | ||||
|             - image: python:3.7-alpine | ||||
|             - image: postgres:11.0 | ||||
|               environment: | ||||
|                   POSTGRES_DB: 'localizedfields' | ||||
|                   POSTGRES_USER: 'localizedfields' | ||||
|                   POSTGRES_PASSWORD: 'localizedfields' | ||||
|         steps: | ||||
|             - checkout | ||||
|             - run: | ||||
|                 name: Install packages | ||||
|                 command: apk add postgresql-libs gcc musl-dev postgresql-dev git | ||||
|             - run: | ||||
|                 name: Install Python packages | ||||
|                 command: pip install -r requirements/test.txt | ||||
|             - run: | ||||
|                 name: Run tests | ||||
|                 command: tox -e 'py37-dj{111,20,21}' | ||||
|                 environment: | ||||
|                     DATABASE_URL: 'postgres://localizedfields:localizedfields@localhost:5432/localizedfields' | ||||
|  | ||||
|     lint: | ||||
|         docker: | ||||
|             - image: python:3.5-alpine | ||||
|         steps: | ||||
|             - checkout | ||||
|             - run: | ||||
|                 name: Install packages | ||||
|                 command: apk add postgresql-libs gcc musl-dev postgresql-dev git | ||||
|             - run: | ||||
|                 name: Install Python packages | ||||
|                 command: pip install -r requirements/test.txt | ||||
|             - run: | ||||
|                 name: Lint code | ||||
|                 command: python setup.py lint | ||||
|  | ||||
|  | ||||
| workflows: | ||||
|     version: 2 | ||||
|     build: | ||||
|         jobs: | ||||
|             - test-python35 | ||||
|             - test-python36 | ||||
|             - test-python37 | ||||
|             - lint | ||||
| @@ -1,5 +1,3 @@ | ||||
| [run] | ||||
| include = localized_fields/* | ||||
| omit = *migrations*, *tests* | ||||
| plugins = | ||||
|     django_coverage_plugin | ||||
|   | ||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,5 +1,6 @@ | ||||
| # Ignore virtual environments | ||||
| env/ | ||||
| .env/ | ||||
|  | ||||
| # Ignore Python byte code cache | ||||
| *.pyc | ||||
|   | ||||
| @@ -1,34 +0,0 @@ | ||||
| checks: | ||||
|     python: | ||||
|         code_rating: true | ||||
|         duplicate_code: true | ||||
| tools: | ||||
|     pylint: | ||||
|         python_version: '3' | ||||
|         config_file: .pylintrc | ||||
| filter: | ||||
|     excluded_paths: | ||||
|         - '*/tests/*' | ||||
|         - '*/migrations/*' | ||||
| build: | ||||
|     environment: | ||||
|         python: '3.5.0' | ||||
|         variables: | ||||
|             DJANGO_SETTINGS_MODULES: settings | ||||
|             DATABASE_URL: postgres://scrutinizer:scrutinizer@localhost:5434/localized_fields | ||||
|         postgresql: true | ||||
|  | ||||
|     dependencies: | ||||
|         override: | ||||
|             - 'pip install -r requirements/test.txt' | ||||
|     tests: | ||||
|         override: | ||||
|             - | ||||
|                 command: pep8 ./localized_fields/ | ||||
|             - | ||||
|                 command: flake8 ./localized_fields/ | ||||
|             - | ||||
|                 command: tox | ||||
|                 coverage: | ||||
|                     file: '.coverage' | ||||
|                     format: 'py-cc' | ||||
							
								
								
									
										35
									
								
								README.rst
									
									
									
									
									
								
							
							
						
						
									
										35
									
								
								README.rst
									
									
									
									
									
								
							| @@ -1,11 +1,8 @@ | ||||
| django-localized-fields | ||||
| ======================= | ||||
|  | ||||
| .. image:: https://scrutinizer-ci.com/g/SectorLabs/django-localized-fields/badges/quality-score.png | ||||
|     :target: https://scrutinizer-ci.com/g/SectorLabs/django-localized-fields/ | ||||
|  | ||||
| .. image:: https://scrutinizer-ci.com/g/SectorLabs/django-localized-fields/badges/coverage.png | ||||
|     :target: https://scrutinizer-ci.com/g/SectorLabs/django-localized-fields/ | ||||
| .. image:: https://circleci.com/gh/SectorLabs/django-localized-fields.svg?style=svg | ||||
|     :target: https://circleci.com/gh/SectorLabs/django-localized-fields | ||||
|  | ||||
| .. image:: https://img.shields.io/github/license/SectorLabs/django-localized-fields.svg | ||||
|     :target: https://github.com/SectorLabs/django-localized-fields/blob/master/LICENSE.md | ||||
| @@ -36,7 +33,7 @@ Installation | ||||
|      .. code-block:: bash | ||||
|  | ||||
|         INSTALLED_APPS = [ | ||||
|             .... | ||||
|             ... | ||||
|  | ||||
|             'django.contrib.postgres', | ||||
|             'localized_fields.apps.LocalizedFieldsConfig' | ||||
| @@ -121,6 +118,7 @@ Or get it in a specific language: | ||||
|      print(new.title.get('en')) # prints 'english title' | ||||
|      print(new.title.get('ro')) # prints 'romanian title' | ||||
|      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: | ||||
|  | ||||
| @@ -136,7 +134,7 @@ Constraints | ||||
|  | ||||
| **Required/Optional** | ||||
|  | ||||
| Constraints is enforced on a database level. | ||||
| Constraints are enforced on a database level. | ||||
|  | ||||
| * Optional filling | ||||
|  | ||||
| @@ -274,8 +272,24 @@ Besides ``LocalizedField``, there's also: | ||||
|                    title = LocalizedField() | ||||
|                    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 strings. 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`` | ||||
|     This fields following the Django convention for string-based fields use the empty string as value for “no data”, not NULL. | ||||
|     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: | ||||
| @@ -331,6 +345,7 @@ Experimental feature | ||||
| ^^^^^^^^^^^^^^^^^^^^ | ||||
| Enables the following experimental features: | ||||
|     * ``LocalizedField`` will return ``None`` instead of an empty ``LocalizedValue`` if there is no database value. | ||||
|     * ``LocalizedField`` lookups will lookup by currently active language instead of HStoreField | ||||
|  | ||||
| .. code-block:: python | ||||
|  | ||||
| @@ -354,6 +369,10 @@ To enable widgets in the admin, you need to inherit from ``LocalizedFieldsAdminM | ||||
|     admin.site.register(MyLocalizedModel, MyLocalizedModelAdmin) | ||||
|  | ||||
|  | ||||
| .. image:: ./images/admin-widget.png | ||||
|     :alt: The appearance of admin widget | ||||
|  | ||||
|  | ||||
| Frequently asked questions (FAQ) | ||||
| -------------------------------- | ||||
|  | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								images/admin-widget.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								images/admin-widget.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 57 KiB | 
| @@ -0,0 +1 @@ | ||||
| default_app_config = 'localized_fields.apps.LocalizedFieldsConfig' | ||||
|   | ||||
| @@ -22,6 +22,7 @@ class LocalizedFieldsAdminMixin: | ||||
|         } | ||||
|  | ||||
|         js = ( | ||||
|             'admin/js/jquery.init.js', | ||||
|             'localized_fields/localized-fields-admin.js', | ||||
|         ) | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,21 @@ | ||||
| import inspect | ||||
|  | ||||
| from django.apps import AppConfig | ||||
| from django.conf import settings | ||||
|  | ||||
| from . import lookups | ||||
| from .fields import LocalizedField | ||||
| from .lookups import LocalizedLookupMixin | ||||
|  | ||||
|  | ||||
| class LocalizedFieldsConfig(AppConfig): | ||||
|     name = 'localized_fields' | ||||
|  | ||||
|     def ready(self): | ||||
|         if getattr(settings, 'LOCALIZED_FIELDS_EXPERIMENTAL', False): | ||||
|             for _, clazz in inspect.getmembers(lookups): | ||||
|                 if not inspect.isclass(clazz) or clazz is LocalizedLookupMixin: | ||||
|                     continue | ||||
|  | ||||
|                 if issubclass(clazz, LocalizedLookupMixin): | ||||
|                     LocalizedField.register_lookup(clazz) | ||||
|   | ||||
| @@ -41,7 +41,7 @@ class LocalizedValueDescriptor: | ||||
|  | ||||
|         if self.field.name in instance.__dict__: | ||||
|             value = instance.__dict__[self.field.name] | ||||
|         elif instance.pk is not None: | ||||
|         elif not instance._state.adding: | ||||
|             instance.refresh_from_db(fields=[self.field.name]) | ||||
|             value = getattr(instance, self.field.name) | ||||
|         else: | ||||
|   | ||||
| @@ -4,6 +4,7 @@ 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__ = [ | ||||
| @@ -12,7 +13,8 @@ __all__ = [ | ||||
|     'LocalizedUniqueSlugField', | ||||
|     'LocalizedCharField', | ||||
|     'LocalizedTextField', | ||||
|     'LocalizedFileField' | ||||
|     'LocalizedFileField', | ||||
|     'LocalizedIntegerField' | ||||
| ] | ||||
|  | ||||
| try: | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import json | ||||
|  | ||||
| from typing import Union, List | ||||
| from typing import Union, List, Optional | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.db.utils import IntegrityError | ||||
| @@ -53,7 +53,7 @@ class LocalizedField(HStoreField): | ||||
|         setattr(model, self.name, self.descriptor_class(self)) | ||||
|  | ||||
|     @classmethod | ||||
|     def from_db_value(cls, value, *_): | ||||
|     def from_db_value(cls, value, *_) -> Optional[LocalizedValue]: | ||||
|         """Turns the specified database value into its Python | ||||
|         equivalent. | ||||
|  | ||||
| @@ -165,7 +165,7 @@ class LocalizedField(HStoreField): | ||||
|         can store in the database. | ||||
|  | ||||
|         For example, when all the language fields are | ||||
|         left empty, and the field is allows to be null, | ||||
|         left empty, and the field is allowed to be null, | ||||
|         we will store None instead of empty keys. | ||||
|  | ||||
|         Arguments: | ||||
|   | ||||
							
								
								
									
										91
									
								
								localized_fields/fields/integer_field.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								localized_fields/fields/integer_field.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,91 @@ | ||||
| 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 | ||||
| from ..forms import LocalizedIntegerFieldForm | ||||
|  | ||||
|  | ||||
| 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 | ||||
|  | ||||
|     def formfield(self, **kwargs): | ||||
|         """Gets the form field associated with this field.""" | ||||
|         defaults = { | ||||
|             'form_class': LocalizedIntegerFieldForm | ||||
|         } | ||||
|  | ||||
|         defaults.update(kwargs) | ||||
|         return super().formfield(**defaults) | ||||
|  | ||||
|     @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) | ||||
| @@ -6,9 +6,9 @@ from django.core.exceptions import ValidationError | ||||
| from django.forms.widgets import FILE_INPUT_CONTRADICTION | ||||
|  | ||||
| from .value import LocalizedValue, LocalizedStringValue, \ | ||||
|     LocalizedFileValue | ||||
|     LocalizedFileValue, LocalizedIntegerValue | ||||
| from .widgets import LocalizedFieldWidget, LocalizedCharFieldWidget, \ | ||||
|     LocalizedFileWidget | ||||
|     LocalizedFileWidget, AdminLocalizedIntegerFieldWidget | ||||
|  | ||||
|  | ||||
| class LocalizedFieldForm(forms.MultiValueField): | ||||
| @@ -79,6 +79,14 @@ class LocalizedTextFieldForm(LocalizedFieldForm): | ||||
|     value_class = LocalizedStringValue | ||||
|  | ||||
|  | ||||
| class LocalizedIntegerFieldForm(LocalizedFieldForm): | ||||
|     """Form for a localized integer field, allows editing | ||||
|     the field in multiple languages.""" | ||||
|  | ||||
|     widget = AdminLocalizedIntegerFieldWidget | ||||
|     value_class = LocalizedIntegerValue | ||||
|  | ||||
|  | ||||
| class LocalizedFileFieldForm(LocalizedFieldForm, forms.FileField): | ||||
|     """Form for a localized file field, allows editing | ||||
|     the field in multiple languages.""" | ||||
|   | ||||
							
								
								
									
										80
									
								
								localized_fields/lookups.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								localized_fields/lookups.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,80 @@ | ||||
| from django.conf import settings | ||||
| from django.contrib.postgres.fields.hstore import KeyTransform | ||||
| from django.contrib.postgres.lookups import (SearchLookup, TrigramSimilar, | ||||
|                                              Unaccent) | ||||
| from django.db.models.expressions import Col | ||||
| from django.db.models.lookups import (Contains, EndsWith, Exact, IContains, | ||||
|                                       IEndsWith, IExact, In, IRegex, IsNull, | ||||
|                                       IStartsWith, Regex, StartsWith) | ||||
| from django.utils import translation | ||||
|  | ||||
|  | ||||
| class LocalizedLookupMixin(): | ||||
|     def process_lhs(self, qn, connection): | ||||
|         if isinstance(self.lhs, Col): | ||||
|             language = translation.get_language() or settings.LANGUAGE_CODE | ||||
|             self.lhs = KeyTransform(language, self.lhs) | ||||
|         return super().process_lhs(qn, connection) | ||||
|  | ||||
|     def get_prep_lookup(self): | ||||
|         return str(self.rhs) | ||||
|  | ||||
|  | ||||
| class LocalizedSearchLookup(LocalizedLookupMixin, SearchLookup): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class LocalizedUnaccent(LocalizedLookupMixin, Unaccent): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class LocalizedTrigramSimilair(LocalizedLookupMixin, TrigramSimilar): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class LocalizedExact(LocalizedLookupMixin, Exact): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class LocalizedIExact(LocalizedLookupMixin, IExact): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class LocalizedIn(LocalizedLookupMixin, In): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class LocalizedContains(LocalizedLookupMixin, Contains): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class LocalizedIContains(LocalizedLookupMixin, IContains): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class LocalizedStartsWith(LocalizedLookupMixin, StartsWith): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class LocalizedIStartsWith(LocalizedLookupMixin, IStartsWith): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class LocalizedEndsWith(LocalizedLookupMixin, EndsWith): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class LocalizedIEndsWith(LocalizedLookupMixin, IEndsWith): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class LocalizedIsNullWith(LocalizedLookupMixin, IsNull): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class LocalizedRegexWith(LocalizedLookupMixin, Regex): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class LocalizedIRegexWith(LocalizedLookupMixin, IRegex): | ||||
|     pass | ||||
							
								
								
									
										14
									
								
								localized_fields/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								localized_fields/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| # Generated by Django 2.1 on 2018-08-27 08:05 | ||||
|  | ||||
| from django.contrib.postgres.operations import HStoreExtension | ||||
| from django.db import migrations | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         HStoreExtension(), | ||||
|     ] | ||||
							
								
								
									
										0
									
								
								localized_fields/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								localized_fields/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -1,4 +1,16 @@ | ||||
| {% for widget in widget.subwidgets %} | ||||
| <label for="{{ widget.attrs.id }}">{{ widget.lang_name }}</label> | ||||
| {% include widget.template_name %} | ||||
| {% endfor %} | ||||
| {% 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 %} | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| import deprecation | ||||
| import collections | ||||
|  | ||||
| from typing import Optional | ||||
| from django.conf import settings | ||||
| from django.utils import translation | ||||
|  | ||||
| @@ -36,7 +38,8 @@ class LocalizedValue(dict): | ||||
|         """ | ||||
|  | ||||
|         language = language or settings.LANGUAGE_CODE | ||||
|         return super().get(language, default) | ||||
|         value = super().get(language, default) | ||||
|         return value if value is not None else default | ||||
|  | ||||
|     def set(self, language: str, value: str): | ||||
|         """Sets the value in the specified language. | ||||
| @@ -98,10 +101,10 @@ class LocalizedValue(dict): | ||||
|             for val in value: | ||||
|                 self._interpret_value(val) | ||||
|  | ||||
|     def __str__(self) -> str: | ||||
|         """Gets the value in the current language, or falls | ||||
|         back to the primary language if there's no value | ||||
|         in the current language.""" | ||||
|     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', {}) | ||||
|  | ||||
| @@ -112,9 +115,16 @@ class LocalizedValue(dict): | ||||
|         for lang_code in languages: | ||||
|             value = self.get(lang_code) | ||||
|             if value: | ||||
|                 return value or '' | ||||
|                 return value or None | ||||
|  | ||||
|         return '' | ||||
|         return None | ||||
|  | ||||
|     def __str__(self) -> str: | ||||
|         """Gets the value in the current language or falls | ||||
|         back to the next language if there's no value in the | ||||
|         current language.""" | ||||
|  | ||||
|         return self.translate() or '' | ||||
|  | ||||
|     def __eq__(self, other): | ||||
|         """Compares :paramref:self to :paramref:other for | ||||
| @@ -183,8 +193,44 @@ class LocalizedFileValue(LocalizedValue): | ||||
|  | ||||
|     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) | ||||
|  | ||||
|     def __str__(self) -> str: | ||||
|         """Returns string representation of value""" | ||||
|  | ||||
|         value = self.translate() | ||||
|         return str(value) if value is not None else '' | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| import copy | ||||
|  | ||||
| from typing import List | ||||
|  | ||||
| from django.conf import settings | ||||
| @@ -16,7 +18,7 @@ class LocalizedFieldWidget(forms.MultiWidget): | ||||
|         """Initializes a new instance of :see:LocalizedFieldWidget.""" | ||||
|  | ||||
|         initial_widgets = [ | ||||
|             self.widget | ||||
|             copy.copy(self.widget) | ||||
|             for _ in settings.LANGUAGES | ||||
|         ] | ||||
|  | ||||
| @@ -118,3 +120,7 @@ class AdminLocalizedCharFieldWidget(AdminLocalizedFieldWidget): | ||||
|  | ||||
| class AdminLocalizedFileFieldWidget(AdminLocalizedFieldWidget): | ||||
|     widget = widgets.AdminFileWidget | ||||
|  | ||||
|  | ||||
| class AdminLocalizedIntegerFieldWidget(AdminLocalizedFieldWidget): | ||||
|     widget = widgets.AdminIntegerFieldWidget | ||||
|   | ||||
| @@ -1 +1,2 @@ | ||||
| django-postgres-extra==1.21a8 | ||||
| django-postgres-extra==1.21a9 | ||||
| deprecation==2.0.3 | ||||
|   | ||||
| @@ -2,15 +2,9 @@ | ||||
|  | ||||
| django-autoslug==1.9.3 | ||||
| django-bleach==0.3.0 | ||||
| django-coverage-plugin==1.3.1 | ||||
| psycopg2==2.7.3.2 | ||||
| pylint==1.8.1 | ||||
| pylint-common==0.2.5 | ||||
| pylint-django==0.8.0 | ||||
| pylint-plugin-utils==0.2.6 | ||||
| coverage==4.4.2 | ||||
| django-coverage-plugin==1.3.1 | ||||
| flake8==3.5.0 | ||||
| flake8==3.6.0 | ||||
| pep8==1.7.1 | ||||
| dj-database-url==0.4.2 | ||||
| tox==2.9.1 | ||||
|   | ||||
| @@ -21,6 +21,9 @@ LANGUAGES = ( | ||||
| ) | ||||
|  | ||||
| INSTALLED_APPS = ( | ||||
|     'django.contrib.auth', | ||||
|     'django.contrib.contenttypes', | ||||
|     'django.contrib.admin', | ||||
|     'localized_fields', | ||||
|     'tests', | ||||
| ) | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| [flake8] | ||||
| max-line-length = 120 | ||||
| ignore = E252, W605 | ||||
| exclude = env,.tox,.git,config/settings,*/migrations/*,*/static/CACHE/*,docs,node_modules | ||||
|  | ||||
| [pep8] | ||||
|   | ||||
							
								
								
									
										41
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										41
									
								
								setup.py
									
									
									
									
									
								
							| @@ -1,13 +1,39 @@ | ||||
| import os | ||||
| import distutils.cmd | ||||
| import subprocess | ||||
|  | ||||
| from setuptools import find_packages, setup | ||||
|  | ||||
|  | ||||
| class BaseCommand(distutils.cmd.Command): | ||||
|     user_options = [] | ||||
|  | ||||
|     def initialize_options(self): | ||||
|         pass | ||||
|  | ||||
|     def finalize_options(self): | ||||
|         pass | ||||
|  | ||||
|  | ||||
| def create_command(text, commands): | ||||
|     """Creates a custom setup.py command.""" | ||||
|  | ||||
|     class CustomCommand(BaseCommand): | ||||
|         description = text | ||||
|  | ||||
|         def run(self): | ||||
|             for cmd in commands: | ||||
|                 subprocess.check_call(cmd) | ||||
|  | ||||
|     return CustomCommand | ||||
|  | ||||
|  | ||||
| with open(os.path.join(os.path.dirname(__file__), 'README.rst'), encoding='utf-8') as readme: | ||||
|     README = readme.read() | ||||
|  | ||||
| setup( | ||||
|     name='django-localized-fields', | ||||
|     version='4.6a2', | ||||
|     version='5.0a10', | ||||
|     packages=find_packages(exclude=['tests']), | ||||
|     include_package_data=True, | ||||
|     license='MIT License', | ||||
| @@ -18,8 +44,9 @@ setup( | ||||
|     author_email='open-source@sectorlabs.ro', | ||||
|     keywords=['django', 'localized', 'language', 'models', 'fields'], | ||||
|     install_requires=[ | ||||
|         'django-postgres-extra>=1.11', | ||||
|         'Django>=1.11' | ||||
|         'django-postgres-extra>=1.21a16', | ||||
|         'Django>=1.11', | ||||
|         'deprecation==2.0.3' | ||||
|     ], | ||||
|     classifiers=[ | ||||
|         'Environment :: Web Environment', | ||||
| @@ -31,5 +58,11 @@ setup( | ||||
|         'Programming Language :: Python :: 3.5', | ||||
|         'Topic :: Internet :: WWW/HTTP', | ||||
|         'Topic :: Internet :: WWW/HTTP :: Dynamic Content', | ||||
|     ] | ||||
|     ], | ||||
|     cmdclass={ | ||||
|         'lint': create_command( | ||||
|             'Lints the code', | ||||
|             [['flake8', 'setup.py', 'localized_fields', 'tests']], | ||||
|         ), | ||||
|     }, | ||||
| ) | ||||
|   | ||||
							
								
								
									
										87
									
								
								tests/test_admin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								tests/test_admin.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | ||||
| from django.apps import apps | ||||
| from django.contrib import admin | ||||
| from django.contrib.admin.checks import check_admin_app | ||||
| from django.db import models | ||||
| from django.test import TestCase | ||||
|  | ||||
| from localized_fields.fields import LocalizedField | ||||
| from localized_fields.admin import LocalizedFieldsAdminMixin | ||||
|  | ||||
| from tests.fake_model import get_fake_model | ||||
|  | ||||
|  | ||||
| class LocalizedFieldsAdminMixinTestCase(TestCase): | ||||
|     """Tests the :see:LocalizedFieldsAdminMixin class.""" | ||||
|  | ||||
|     TestModel = None | ||||
|     TestRelModel = None | ||||
|  | ||||
|     @classmethod | ||||
|     def setUpClass(cls): | ||||
|         """Creates the test model in the database.""" | ||||
|  | ||||
|         super(LocalizedFieldsAdminMixinTestCase, cls).setUpClass() | ||||
|  | ||||
|         cls.TestRelModel = get_fake_model( | ||||
|             { | ||||
|                 'description': LocalizedField() | ||||
|             } | ||||
|         ) | ||||
|         cls.TestModel = get_fake_model( | ||||
|             { | ||||
|                 'title': LocalizedField(), | ||||
|                 'rel': models.ForeignKey(cls.TestRelModel, | ||||
|                                          on_delete=models.CASCADE) | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|     def tearDown(self): | ||||
|         if admin.site.is_registered(self.TestModel): | ||||
|             admin.site.unregister(self.TestModel) | ||||
|         if admin.site.is_registered(self.TestRelModel): | ||||
|             admin.site.unregister(self.TestRelModel) | ||||
|  | ||||
|     @classmethod | ||||
|     def test_model_admin(cls): | ||||
|         """Tests whether :see:LocalizedFieldsAdminMixin | ||||
|         mixin are works with admin.ModelAdmin""" | ||||
|  | ||||
|         @admin.register(cls.TestModel) | ||||
|         class TestModelAdmin(LocalizedFieldsAdminMixin, admin.ModelAdmin): | ||||
|             pass | ||||
|  | ||||
|         assert len(check_admin_app(apps.get_app_configs())) == 0 | ||||
|  | ||||
|     @classmethod | ||||
|     def test_stackedmodel_admin(cls): | ||||
|         """Tests whether :see:LocalizedFieldsAdminMixin mixin are works | ||||
|         with admin.StackedInline""" | ||||
|  | ||||
|         class TestModelStackedInline(LocalizedFieldsAdminMixin, | ||||
|                                      admin.StackedInline): | ||||
|             model = cls.TestModel | ||||
|  | ||||
|         @admin.register(cls.TestRelModel) | ||||
|         class TestRelModelAdmin(admin.ModelAdmin): | ||||
|             inlines = [ | ||||
|                 TestModelStackedInline, | ||||
|             ] | ||||
|  | ||||
|         assert len(check_admin_app(apps.get_app_configs())) == 0 | ||||
|  | ||||
|     @classmethod | ||||
|     def test_tabularmodel_admin(cls): | ||||
|         """Tests whether :see:LocalizedFieldsAdminMixin mixin are works | ||||
|         with admin.TabularInline""" | ||||
|  | ||||
|         class TestModelTabularInline(LocalizedFieldsAdminMixin, | ||||
|                                      admin.TabularInline): | ||||
|             model = cls.TestModel | ||||
|  | ||||
|         @admin.register(cls.TestRelModel) | ||||
|         class TestRelModelAdmin(admin.ModelAdmin): | ||||
|             inlines = [ | ||||
|                 TestModelTabularInline, | ||||
|             ] | ||||
|  | ||||
|         assert len(check_admin_app(apps.get_app_configs())) == 0 | ||||
| @@ -1,6 +1,7 @@ | ||||
| import json | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.db import models | ||||
| from django.db.utils import IntegrityError | ||||
| from django.test import TestCase | ||||
|  | ||||
| @@ -33,7 +34,6 @@ class LocalizedFieldTestCase(TestCase): | ||||
|         field = LocalizedField(required=False) | ||||
|         assert field.required == [] | ||||
|  | ||||
|  | ||||
|     @staticmethod | ||||
|     def test_from_db_value(): | ||||
|         """Tests whether the :see:from_db_value function | ||||
| @@ -205,6 +205,16 @@ class LocalizedFieldTestCase(TestCase): | ||||
|         for field in field.formfield().fields: | ||||
|             assert field.required | ||||
|  | ||||
|     def test_descriptor_user_defined_primary_key(self): | ||||
|         """Tests that descriptor works even when primary key is user defined.""" | ||||
|         model = get_fake_model(dict( | ||||
|             slug=models.SlugField(primary_key=True), | ||||
|             title=LocalizedField() | ||||
|         )) | ||||
|  | ||||
|         obj = model.objects.create(slug='test', title='test') | ||||
|         assert obj.title == 'test' | ||||
|  | ||||
|     def test_required_all(self): | ||||
|         """Tests whether passing required=True properly validates | ||||
|         that all languages are filled in.""" | ||||
|   | ||||
| @@ -40,7 +40,6 @@ class LocalizedFieldFormTestCase(TestCase): | ||||
|         for field in form.fields: | ||||
|             assert not field.required | ||||
|  | ||||
|  | ||||
|     @staticmethod | ||||
|     def test_compress(): | ||||
|         """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 | ||||
							
								
								
									
										51
									
								
								tests/test_lookups.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								tests/test_lookups.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| from django.apps import apps | ||||
| from django.conf import settings | ||||
| from django.test import TestCase, override_settings | ||||
| from django.utils import translation | ||||
|  | ||||
| from localized_fields.fields import LocalizedField | ||||
| from localized_fields.value import LocalizedValue | ||||
|  | ||||
| from .fake_model import get_fake_model | ||||
|  | ||||
|  | ||||
| @override_settings(LOCALIZED_FIELDS_EXPERIMENTAL=True) | ||||
| class LocalizedLookupsTestCase(TestCase): | ||||
|     """Tests whether localized lookups properly work with.""" | ||||
|     TestModel1 = None | ||||
|  | ||||
|     @classmethod | ||||
|     def setUpClass(cls): | ||||
|         """Creates the test model in the database.""" | ||||
|  | ||||
|         super(LocalizedLookupsTestCase, cls).setUpClass() | ||||
|  | ||||
|         # reload app as setting has changed | ||||
|         config = apps.get_app_config('localized_fields') | ||||
|         config.ready() | ||||
|  | ||||
|         cls.TestModel = get_fake_model( | ||||
|             { | ||||
|                 'text': LocalizedField(), | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|     def test_localized_lookup(self): | ||||
|         """Tests whether localized lookup properly works.""" | ||||
|  | ||||
|         self.TestModel.objects.create( | ||||
|             text=LocalizedValue(dict(en='text_en', ro='text_ro', nl='text_nl')), | ||||
|         ) | ||||
|  | ||||
|         # assert that it properly lookups the currently active language | ||||
|         for lang_code, _ in settings.LANGUAGES: | ||||
|             translation.activate(lang_code) | ||||
|             assert self.TestModel.objects.filter(text='text_' + lang_code).exists() | ||||
|  | ||||
|         # ensure that the default language is used in case no | ||||
|         # language is active at all | ||||
|         translation.deactivate_all() | ||||
|         assert self.TestModel.objects.filter(text='text_en').exists() | ||||
|  | ||||
|         # ensure that hstore lookups still work | ||||
|         assert self.TestModel.objects.filter(text__ro='text_ro').exists() | ||||
| @@ -1,6 +1,4 @@ | ||||
| from django.conf import settings | ||||
| from django.test import TestCase | ||||
| from django.utils import translation | ||||
|  | ||||
| from localized_fields.fields import LocalizedField | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| from django.conf import settings | ||||
| from django.test import TestCase | ||||
| from django.utils import translation | ||||
| from django.db.models import F | ||||
| from django.conf import settings | ||||
| from django.utils import translation | ||||
| from django.test import TestCase, override_settings | ||||
|  | ||||
| from localized_fields.value import LocalizedValue | ||||
|  | ||||
| @@ -90,18 +90,6 @@ class LocalizedValueTestCase(TestCase): | ||||
|             assert localized_value.get(language) == value | ||||
|             assert getattr(localized_value, language) == value | ||||
|  | ||||
|     @staticmethod | ||||
|     def test_str(): | ||||
|         """Tests whether the :see:LocalizedValue | ||||
|         class's __str__ works properly.""" | ||||
|  | ||||
|         keys = get_init_values() | ||||
|         localized_value = LocalizedValue(keys) | ||||
|  | ||||
|         for language, value in keys.items(): | ||||
|             translation.activate(language) | ||||
|             assert str(localized_value) == value | ||||
|  | ||||
|     @staticmethod | ||||
|     def test_eq(): | ||||
|         """Tests whether the __eq__ operator | ||||
| @@ -116,9 +104,21 @@ class LocalizedValueTestCase(TestCase): | ||||
|         assert a != b | ||||
|  | ||||
|     @staticmethod | ||||
|     def test_str_fallback(): | ||||
|     def test_translate(): | ||||
|         """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.""" | ||||
|  | ||||
|         test_value = 'myvalue' | ||||
| @@ -131,13 +131,13 @@ class LocalizedValueTestCase(TestCase): | ||||
|  | ||||
|         # make sure that, by default it returns | ||||
|         # 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 | ||||
|         # primary language when there's no value | ||||
|         # available in the current 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 | ||||
|         # back and that for the other language | ||||
| @@ -145,12 +145,35 @@ class LocalizedValueTestCase(TestCase): | ||||
|         assert localized_value.get(other_language) != test_value | ||||
|  | ||||
|     @staticmethod | ||||
|     def test_str_fallback_custom_fallback(): | ||||
|     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() is 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() is None | ||||
|         assert str(localized_value) == '' | ||||
|  | ||||
|     @staticmethod | ||||
|     def test_translate_fallback_custom_fallback(): | ||||
|         """Tests whether the :see:LocalizedValue class's | ||||
|         __str__'s fallback functionality properly respects | ||||
|         translate()'s fallback functionality properly respects | ||||
|         the LOCALIZED_FIELDS_FALLBACKS setting.""" | ||||
|  | ||||
|         settings.LOCALIZED_FIELDS_FALLBACKS = { | ||||
|         fallbacks = { | ||||
|             'nl': ['ro'] | ||||
|         } | ||||
|  | ||||
| @@ -159,8 +182,9 @@ class LocalizedValueTestCase(TestCase): | ||||
|             'ro': 'ro' | ||||
|         }) | ||||
|  | ||||
|         with override_settings(LOCALIZED_FIELDS_FALLBACKS=fallbacks): | ||||
|             with translation.override('nl'): | ||||
|             assert str(localized_value) == 'ro' | ||||
|                 assert localized_value.translate() == 'ro' | ||||
|  | ||||
|     @staticmethod | ||||
|     def test_deconstruct(): | ||||
|   | ||||
| @@ -16,6 +16,7 @@ class LocalizedFieldWidgetTestCase(TestCase): | ||||
|  | ||||
|         widget = LocalizedFieldWidget() | ||||
|         assert len(widget.widgets) == len(settings.LANGUAGES) | ||||
|         assert len(set(widget.widgets)) == len(widget.widgets) | ||||
|  | ||||
|     @staticmethod | ||||
|     def test_decompress(): | ||||
|   | ||||
							
								
								
									
										4
									
								
								tox.ini
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								tox.ini
									
									
									
									
									
								
							| @@ -1,10 +1,12 @@ | ||||
| [tox] | ||||
| envlist = py35-dj{111,20} | ||||
| envlist = py35-dj{111,20,21,22}, py36-dj{111,20,21,22}, py37-dj{111,20,21,22} | ||||
|  | ||||
| [testenv] | ||||
| deps = | ||||
|     dj111: Django>=1.11,<1.12 | ||||
|     dj20: Django>=2.0,<2.1 | ||||
|     dj21: Django>=2.1,<2.2 | ||||
|     dj22: Django>=2.2,<2.3 | ||||
|     -rrequirements/test.txt | ||||
| setenv = | ||||
|     DJANGO_SETTINGS_MODULE=settings | ||||
|   | ||||
		Reference in New Issue
	
	Block a user