mirror of
				https://github.com/SectorLabs/django-localized-fields.git
				synced 2025-10-25 00:28:57 +03:00 
			
		
		
		
	Compare commits
	
		
			58 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 909ebfee69 | ||
|  | 95284e6fd0 | ||
|  | e84a5e4ff1 | ||
|  | 472c7bbc41 | ||
|  | 8cc50889ec | ||
|  | 8494615d05 | ||
|  | f0541c047b | ||
|  | 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] | [run] | ||||||
| include = localized_fields/* | include = localized_fields/* | ||||||
| omit = *migrations*, *tests* | omit = *migrations*, *tests* | ||||||
| plugins = |  | ||||||
|     django_coverage_plugin |  | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,5 +1,6 @@ | |||||||
| # Ignore virtual environments | # Ignore virtual environments | ||||||
| env/ | env/ | ||||||
|  | .env/ | ||||||
|  |  | ||||||
| # Ignore Python byte code cache | # Ignore Python byte code cache | ||||||
| *.pyc | *.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' |  | ||||||
							
								
								
									
										18
									
								
								.travis.yml
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								.travis.yml
									
									
									
									
									
								
							| @@ -1,18 +0,0 @@ | |||||||
| env: |  | ||||||
|  - DJANGO_SETTINGS_MODULE=settings |  | ||||||
| sudo: true |  | ||||||
| before_install: |  | ||||||
|   - sudo apt-get update -qq |  | ||||||
|   - sudo apt-get install -qq build-essential gettext python-dev zlib1g-dev libpq-dev xvfb |  | ||||||
|   - sudo apt-get install -qq libtiff4-dev libjpeg8-dev libfreetype6-dev liblcms1-dev libwebp-dev |  | ||||||
|   - sudo apt-get install -qq graphviz-dev python-setuptools python3-dev python-virtualenv python-pip |  | ||||||
|   - sudo apt-get install -qq firefox automake libtool libreadline6 libreadline6-dev libreadline-dev |  | ||||||
|   - sudo apt-get install -qq libsqlite3-dev libxml2 libxml2-dev libssl-dev libbz2-dev wget curl llvm |  | ||||||
| language: python |  | ||||||
| python: |  | ||||||
| - "3.5" |  | ||||||
| services: |  | ||||||
|  - postgresql |  | ||||||
| install: "pip install -r requirements/test.txt" |  | ||||||
| script: |  | ||||||
|     - coverage run manage.py test |  | ||||||
							
								
								
									
										42
									
								
								README.rst
									
									
									
									
									
								
							
							
						
						
									
										42
									
								
								README.rst
									
									
									
									
									
								
							| @@ -1,11 +1,8 @@ | |||||||
| django-localized-fields | django-localized-fields | ||||||
| ======================= | ======================= | ||||||
|  |  | ||||||
| .. image:: https://scrutinizer-ci.com/g/SectorLabs/django-localized-fields/badges/quality-score.png | .. image:: https://circleci.com/gh/SectorLabs/django-localized-fields.svg?style=svg | ||||||
|     :target: https://scrutinizer-ci.com/g/SectorLabs/django-localized-fields/ |     :target: https://circleci.com/gh/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://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 |     :target: https://github.com/SectorLabs/django-localized-fields/blob/master/LICENSE.md | ||||||
| @@ -36,7 +33,7 @@ Installation | |||||||
|      .. code-block:: bash |      .. code-block:: bash | ||||||
|  |  | ||||||
|         INSTALLED_APPS = [ |         INSTALLED_APPS = [ | ||||||
|             .... |             ... | ||||||
|  |  | ||||||
|             'django.contrib.postgres', |             'django.contrib.postgres', | ||||||
|             'localized_fields.apps.LocalizedFieldsConfig' |             'localized_fields.apps.LocalizedFieldsConfig' | ||||||
| @@ -64,6 +61,13 @@ Installation | |||||||
|              ('ro', 'Romanian') |              ('ro', 'Romanian') | ||||||
|          ) |          ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 4. Apply migrations to enable the HStore extension: | ||||||
|  |  | ||||||
|  |      .. code-block:: bash | ||||||
|  |  | ||||||
|  | 	 python manage.py migrate | ||||||
|  |  | ||||||
| Usage | Usage | ||||||
| ----- | ----- | ||||||
|  |  | ||||||
| @@ -121,6 +125,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: | ||||||
|  |  | ||||||
| @@ -136,7 +141,7 @@ Constraints | |||||||
|  |  | ||||||
| **Required/Optional** | **Required/Optional** | ||||||
|  |  | ||||||
| Constraints is enforced on a database level. | Constraints are enforced on a database level. | ||||||
|  |  | ||||||
| * Optional filling | * Optional filling | ||||||
|  |  | ||||||
| @@ -274,8 +279,24 @@ 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 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`` | * ``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. |     ``LocalizedCharField`` uses ``TextInput`` (``<input type="text">``) widget for render. | ||||||
|  |  | ||||||
|     Example usage: |     Example usage: | ||||||
| @@ -331,6 +352,7 @@ Experimental feature | |||||||
| ^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^ | ||||||
| Enables the following experimental features: | Enables the following experimental features: | ||||||
|     * ``LocalizedField`` will return ``None`` instead of an empty ``LocalizedValue`` if there is no database value. |     * ``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 | .. code-block:: python | ||||||
|  |  | ||||||
| @@ -354,6 +376,10 @@ To enable widgets in the admin, you need to inherit from ``LocalizedFieldsAdminM | |||||||
|     admin.site.register(MyLocalizedModel, MyLocalizedModelAdmin) |     admin.site.register(MyLocalizedModel, MyLocalizedModelAdmin) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | .. image:: ./images/admin-widget.png | ||||||
|  |     :alt: The appearance of admin widget | ||||||
|  |  | ||||||
|  |  | ||||||
| Frequently asked questions (FAQ) | 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 = ( |         js = ( | ||||||
|  |             'admin/js/jquery.init.js', | ||||||
|             'localized_fields/localized-fields-admin.js', |             'localized_fields/localized-fields-admin.js', | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,5 +1,21 @@ | |||||||
|  | import inspect | ||||||
|  |  | ||||||
| from django.apps import AppConfig | from django.apps import AppConfig | ||||||
|  | from django.conf import settings | ||||||
|  |  | ||||||
|  | from . import lookups | ||||||
|  | from .fields import LocalizedField | ||||||
|  | from .lookups import LocalizedLookupMixin | ||||||
|  |  | ||||||
|  |  | ||||||
| class LocalizedFieldsConfig(AppConfig): | class LocalizedFieldsConfig(AppConfig): | ||||||
|     name = 'localized_fields' |     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__: |         if self.field.name in instance.__dict__: | ||||||
|             value = instance.__dict__[self.field.name] |             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]) |             instance.refresh_from_db(fields=[self.field.name]) | ||||||
|             value = getattr(instance, self.field.name) |             value = getattr(instance, self.field.name) | ||||||
|         else: |         else: | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ from .uniqueslug_field import LocalizedUniqueSlugField | |||||||
| from .char_field import LocalizedCharField | from .char_field import LocalizedCharField | ||||||
| from .text_field import LocalizedTextField | from .text_field import LocalizedTextField | ||||||
| from .file_field import LocalizedFileField | from .file_field import LocalizedFileField | ||||||
|  | from .integer_field import LocalizedIntegerField | ||||||
|  |  | ||||||
|  |  | ||||||
| __all__ = [ | __all__ = [ | ||||||
| @@ -12,7 +13,8 @@ __all__ = [ | |||||||
|     'LocalizedUniqueSlugField', |     'LocalizedUniqueSlugField', | ||||||
|     'LocalizedCharField', |     'LocalizedCharField', | ||||||
|     'LocalizedTextField', |     'LocalizedTextField', | ||||||
|     'LocalizedFileField' |     'LocalizedFileField', | ||||||
|  |     'LocalizedIntegerField' | ||||||
| ] | ] | ||||||
|  |  | ||||||
| try: | try: | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import json | import json | ||||||
|  |  | ||||||
| from typing import Union, List | 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 | ||||||
| @@ -53,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. | ||||||
|  |  | ||||||
| @@ -165,7 +165,7 @@ class LocalizedField(HStoreField): | |||||||
|         can store in the database. |         can store in the database. | ||||||
|  |  | ||||||
|         For example, when all the language fields are |         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. |         we will store None instead of empty keys. | ||||||
|  |  | ||||||
|         Arguments: |         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 django.forms.widgets import FILE_INPUT_CONTRADICTION | ||||||
|  |  | ||||||
| from .value import LocalizedValue, LocalizedStringValue, \ | from .value import LocalizedValue, LocalizedStringValue, \ | ||||||
|     LocalizedFileValue |     LocalizedFileValue, LocalizedIntegerValue | ||||||
| from .widgets import LocalizedFieldWidget, LocalizedCharFieldWidget, \ | from .widgets import LocalizedFieldWidget, LocalizedCharFieldWidget, \ | ||||||
|     LocalizedFileWidget |     LocalizedFileWidget, AdminLocalizedIntegerFieldWidget | ||||||
|  |  | ||||||
|  |  | ||||||
| class LocalizedFieldForm(forms.MultiValueField): | class LocalizedFieldForm(forms.MultiValueField): | ||||||
| @@ -79,6 +79,14 @@ class LocalizedTextFieldForm(LocalizedFieldForm): | |||||||
|     value_class = LocalizedStringValue |     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): | class LocalizedFileFieldForm(LocalizedFieldForm, forms.FileField): | ||||||
|     """Form for a localized file field, allows editing |     """Form for a localized file field, allows editing | ||||||
|     the field in multiple languages.""" |     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 @@ | |||||||
|  | {% 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 %} |     {% for widget in widget.subwidgets %} | ||||||
| <label for="{{ widget.attrs.id }}">{{ widget.lang_name }}</label> |         <li class="localized-fields-widget tab"> | ||||||
| {% include widget.template_name %} |             <label for="{{ widget_id }}_{{ widget.lang_code }}">{{ widget.lang_name|capfirst }}</label> | ||||||
|  |         </li> | ||||||
|     {% endfor %} |     {% 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 | 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 | ||||||
|  |  | ||||||
| @@ -36,7 +38,8 @@ class LocalizedValue(dict): | |||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         language = language or settings.LANGUAGE_CODE |         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): |     def set(self, language: str, value: str): | ||||||
|         """Sets the value in the specified language. |         """Sets the value in the specified language. | ||||||
| @@ -98,10 +101,10 @@ class LocalizedValue(dict): | |||||||
|             for val in value: |             for val in value: | ||||||
|                 self._interpret_value(val) |                 self._interpret_value(val) | ||||||
|  |  | ||||||
|     def __str__(self) -> str: |     def translate(self) -> Optional[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.""" | ||||||
|  |  | ||||||
|         fallbacks = getattr(settings, 'LOCALIZED_FIELDS_FALLBACKS', {}) |         fallbacks = getattr(settings, 'LOCALIZED_FIELDS_FALLBACKS', {}) | ||||||
|  |  | ||||||
| @@ -112,9 +115,16 @@ class LocalizedValue(dict): | |||||||
|         for lang_code in languages: |         for lang_code in languages: | ||||||
|             value = self.get(lang_code) |             value = self.get(lang_code) | ||||||
|             if value: |             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): |     def __eq__(self, other): | ||||||
|         """Compares :paramref:self to :paramref:other for |         """Compares :paramref:self to :paramref:other for | ||||||
| @@ -183,8 +193,44 @@ class LocalizedFileValue(LocalizedValue): | |||||||
|  |  | ||||||
|     def __str__(self) -> str: |     def __str__(self) -> str: | ||||||
|         """Returns string representation of value""" |         """Returns string representation of value""" | ||||||
|  |  | ||||||
|         return str(super().__str__()) |         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): |     def localized(self): | ||||||
|         """Returns value for current language""" |         """Returns value for current language""" | ||||||
|  |  | ||||||
|         return self.get(translation.get_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 typing import List | ||||||
|  |  | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| @@ -16,7 +18,7 @@ class LocalizedFieldWidget(forms.MultiWidget): | |||||||
|         """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 | ||||||
|         ] |         ] | ||||||
|  |  | ||||||
| @@ -118,3 +120,7 @@ class AdminLocalizedCharFieldWidget(AdminLocalizedFieldWidget): | |||||||
|  |  | ||||||
| class AdminLocalizedFileFieldWidget(AdminLocalizedFieldWidget): | class AdminLocalizedFileFieldWidget(AdminLocalizedFieldWidget): | ||||||
|     widget = widgets.AdminFileWidget |     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-autoslug==1.9.3 | ||||||
| django-bleach==0.3.0 | django-bleach==0.3.0 | ||||||
| django-coverage-plugin==1.3.1 |  | ||||||
| psycopg2==2.7.3.2 | 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 | coverage==4.4.2 | ||||||
| django-coverage-plugin==1.3.1 | flake8==3.6.0 | ||||||
| flake8==3.5.0 |  | ||||||
| pep8==1.7.1 | pep8==1.7.1 | ||||||
| dj-database-url==0.4.2 | dj-database-url==0.4.2 | ||||||
| tox==2.9.1 | tox==2.9.1 | ||||||
|   | |||||||
| @@ -21,6 +21,9 @@ LANGUAGES = ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| INSTALLED_APPS = ( | INSTALLED_APPS = ( | ||||||
|  |     'django.contrib.auth', | ||||||
|  |     'django.contrib.contenttypes', | ||||||
|  |     'django.contrib.admin', | ||||||
|     'localized_fields', |     'localized_fields', | ||||||
|     'tests', |     'tests', | ||||||
| ) | ) | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| [flake8] | [flake8] | ||||||
| max-line-length = 120 | max-line-length = 120 | ||||||
|  | ignore = E252, W605 | ||||||
| exclude = env,.tox,.git,config/settings,*/migrations/*,*/static/CACHE/*,docs,node_modules | exclude = env,.tox,.git,config/settings,*/migrations/*,*/static/CACHE/*,docs,node_modules | ||||||
|  |  | ||||||
| [pep8] | [pep8] | ||||||
|   | |||||||
							
								
								
									
										41
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										41
									
								
								setup.py
									
									
									
									
									
								
							| @@ -1,13 +1,39 @@ | |||||||
| import os | import os | ||||||
|  | import distutils.cmd | ||||||
|  | import subprocess | ||||||
|  |  | ||||||
| from setuptools import find_packages, setup | 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: | 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.6a2', |     version='5.2', | ||||||
|     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,8 +44,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.22', | ||||||
|         'Django>=1.11' |         'Django>=1.11', | ||||||
|  |         'deprecation==2.0.3' | ||||||
|     ], |     ], | ||||||
|     classifiers=[ |     classifiers=[ | ||||||
|         'Environment :: Web Environment', |         'Environment :: Web Environment', | ||||||
| @@ -31,5 +58,11 @@ setup( | |||||||
|         'Programming Language :: Python :: 3.5', |         'Programming Language :: Python :: 3.5', | ||||||
|         'Topic :: Internet :: WWW/HTTP', |         'Topic :: Internet :: WWW/HTTP', | ||||||
|         'Topic :: Internet :: WWW/HTTP :: Dynamic Content', |         '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 | import json | ||||||
|  |  | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
|  | from django.db import models | ||||||
| from django.db.utils import IntegrityError | from django.db.utils import IntegrityError | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
|  |  | ||||||
| @@ -33,7 +34,6 @@ class LocalizedFieldTestCase(TestCase): | |||||||
|         field = LocalizedField(required=False) |         field = LocalizedField(required=False) | ||||||
|         assert field.required == [] |         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 | ||||||
| @@ -205,6 +205,16 @@ class LocalizedFieldTestCase(TestCase): | |||||||
|         for field in field.formfield().fields: |         for field in field.formfield().fields: | ||||||
|             assert field.required |             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): |     def test_required_all(self): | ||||||
|         """Tests whether passing required=True properly validates |         """Tests whether passing required=True properly validates | ||||||
|         that all languages are filled in.""" |         that all languages are filled in.""" | ||||||
|   | |||||||
| @@ -40,7 +40,6 @@ class LocalizedFieldFormTestCase(TestCase): | |||||||
|         for field in form.fields: |         for field in form.fields: | ||||||
|             assert not field.required |             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 | ||||||
							
								
								
									
										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.test import TestCase | ||||||
| from django.utils import translation |  | ||||||
|  |  | ||||||
| from localized_fields.fields import LocalizedField | 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.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 | from localized_fields.value import LocalizedValue | ||||||
|  |  | ||||||
| @@ -90,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 | ||||||
| @@ -116,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' | ||||||
| @@ -131,13 +131,13 @@ 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 | ||||||
| @@ -145,12 +145,35 @@ class LocalizedValueTestCase(TestCase): | |||||||
|         assert localized_value.get(other_language) != test_value |         assert localized_value.get(other_language) != test_value | ||||||
|  |  | ||||||
|     @staticmethod |     @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 |         """Tests whether the :see:LocalizedValue class's | ||||||
|         __str__'s fallback functionality properly respects |         translate()'s fallback functionality properly respects | ||||||
|         the LOCALIZED_FIELDS_FALLBACKS setting.""" |         the LOCALIZED_FIELDS_FALLBACKS setting.""" | ||||||
|  |  | ||||||
|         settings.LOCALIZED_FIELDS_FALLBACKS = { |         fallbacks = { | ||||||
|             'nl': ['ro'] |             'nl': ['ro'] | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -159,8 +182,9 @@ class LocalizedValueTestCase(TestCase): | |||||||
|             'ro': 'ro' |             'ro': 'ro' | ||||||
|         }) |         }) | ||||||
|  |  | ||||||
|  |         with override_settings(LOCALIZED_FIELDS_FALLBACKS=fallbacks): | ||||||
|             with translation.override('nl'): |             with translation.override('nl'): | ||||||
|             assert str(localized_value) == 'ro' |                 assert localized_value.translate() == 'ro' | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def test_deconstruct(): |     def test_deconstruct(): | ||||||
|   | |||||||
| @@ -16,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(): | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								tox.ini
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								tox.ini
									
									
									
									
									
								
							| @@ -1,10 +1,12 @@ | |||||||
| [tox] | [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] | [testenv] | ||||||
| deps = | deps = | ||||||
|     dj111: Django>=1.11,<1.12 |     dj111: Django>=1.11,<1.12 | ||||||
|     dj20: Django>=2.0,<2.1 |     dj20: Django>=2.0,<2.1 | ||||||
|  |     dj21: Django>=2.1,<2.2 | ||||||
|  |     dj22: Django>=2.2,<2.3 | ||||||
|     -rrequirements/test.txt |     -rrequirements/test.txt | ||||||
| setenv = | setenv = | ||||||
|     DJANGO_SETTINGS_MODULE=settings |     DJANGO_SETTINGS_MODULE=settings | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user