mirror of
				https://github.com/SectorLabs/django-localized-fields.git
				synced 2025-10-25 08:38:56 +03:00 
			
		
		
		
	Compare commits
	
		
			211 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 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 | ||
|  | 04f15363bb | ||
|  | a849866bcb | ||
|  | 5e58bc6e24 | ||
|  | 04a750053f | ||
|  | d25b1b92fe | ||
|  | ccc46e1899 | ||
|  | 9063509817 | ||
|  | 8fbe3e8680 | ||
|  | b9f14322be | ||
|  | 24974b2f6b | ||
|  | af02593ebd | ||
|  | f161b5d047 | ||
|  | 77f626fc69 | ||
|  | d7889b0601 | ||
|  | 09cd0a8177 | ||
|  | 2a29efdf14 | ||
|  | d8eb3394d0 | ||
|  | b27f1535ee | ||
|  | cd844fccec | ||
|  | 6a4beca193 | ||
|  | bf0383d742 | ||
|  | 879b857aa5 | ||
|  | 59e8e18b2a | ||
|  | 11cfe5b6e1 | ||
|  | 084f5dd0b6 | ||
|  | 83605cde13 | ||
|  | 7437b58f20 | ||
|  | db6664c84f | ||
|  | 928c4c624d | ||
|  | 3d4f9c413e | ||
|  | f434566338 | ||
|  | 2807ed10c8 | ||
|  | d5f43c783a | ||
|  | 5347db6d8f | ||
|  | 1accee0b59 | ||
|  | 05bcd84a88 | ||
|  | 1c2e013695 | ||
|  | 8b76948328 | ||
|  | 940587d748 | ||
|  | 6522e38f18 | ||
|  | 69cf0df166 | ||
|  | d8c5544e91 | ||
|  | ccc41f9143 | ||
|  | 073846d74b | ||
|  | 0f08eb8280 | ||
|  | 60d14069d8 | ||
|  | 33e9709373 | ||
|  | c4bf151938 | ||
|  | 7d629c186d | ||
|  | cff388cae3 | ||
|  | 08690ab361 | ||
|  | 5ac05efbd0 | ||
|  | aff41f671a | ||
|  | a38d53b609 | ||
|  | 968840188d | ||
|  | e8e044f6e2 | ||
|  | aaf49614f2 | ||
|  | d4c24dea97 | ||
|  | b38ded0cc3 | ||
|  | 226d47e766 | ||
|  | cc4bfb48b9 | ||
|  | d14859a45b | ||
|  | cb7fda5abc | ||
|  | 9000635f1f | ||
|  | dabeb3b79f | ||
|  | 0b4bb7295e | ||
|  | 2b34b6751e | ||
|  | 32696f4e1e | ||
|  | 3ce57ed4cc | ||
|  | 7316d312b4 | ||
|  | 16e23963cc | ||
|  | b10472d3e9 | ||
|  | 833ceb849c | ||
|  | d7382fbf30 | ||
|  | 8ad9268426 | ||
|  | 96ddc77cfc | ||
|  | 51fc6959d2 | ||
|  | 3b28a5e707 | ||
|  | 06873afbda | ||
|  | f7c14f897c | ||
|  | e5e3068835 | ||
|  | de7d3b9a21 | ||
|  | fd47deaa2e | ||
|  | 4b6d997ddd | ||
|  | 68d6991608 | ||
|  | e5d7cd25e2 | ||
|  | 236ce1648c | ||
|  | aacc712195 | ||
|  | d1790f1fc1 | ||
|  | c75c1764e2 | ||
|  | 6736b3b32d | ||
|  | b97a7f3c23 | ||
|  | 1b036dc1de | ||
|  | ea7733670d | ||
|  | a8dc4fe837 | ||
|  | 3c8bea0fc3 | ||
|  | 2741a6a2a2 | ||
|  | 06f7ee15f0 | ||
|  | e5214b07ae | ||
|  | 2d5fe0be05 | ||
|  | 4305696f1b | ||
|  | 8c73c9ab77 | ||
|  | 92a53bc3d7 | ||
|  | 5a4f449363 | ||
|  | 0fa79ddbb0 | ||
|  | 2205f9c6a4 | ||
|  | 84c267330f | ||
|  | a1a02552b7 | ||
|  | bb84d7577c | ||
|  | 5e0343801f | ||
|  | 2df2ec8b36 | ||
|  | 3fcaece894 | ||
|  | 093a9d58f2 | ||
|  | cff22855c2 | ||
|  | db93b93046 | ||
|  | a352388243 | ||
|  | bf2995fd27 | ||
|  | 8ba08c389c | ||
|  | f1798b0cc6 | ||
|  | fc80462ce7 | ||
|  | 24079a2fcb | ||
|  | 0f4c74a9b2 | ||
|  | 366dceaddc | ||
|  | 5b93c5ec8f | ||
|  | 817c7e13fe | ||
|  | 8591af1f2a | ||
|  | c55001ac12 | ||
|  | 23c6f975d8 | ||
|  | a9037b603a | ||
|  | 8b08e5a467 | ||
|  | bf90131e4f | ||
|  | 8bdfee0666 | ||
|  | add3c1e1d4 | ||
|  | 8ba33695f9 | ||
|  | 78594541e1 | ||
|  | 52145ca7d3 | ||
|  | 5df44b0d62 | ||
|  | 5aef1d791b | ||
|  | 03df76d6d7 | ||
|  | 465e35ba8a | ||
|  | 9278e99b18 | ||
|  | 9754134298 | ||
|  | f0c7a72078 | ||
|  | 7f4dfbae1f | ||
|  | d07da55215 | ||
|  | 340dde18cd | ||
|  | 3951266747 | ||
|  | b5f4c43d6b | ||
|  | 3d08475468 | ||
|  | b3d7092b91 | ||
|  | 97a785e9b0 | ||
|  | 97c14fd2ba | ||
|  | 6cb4cdf52e | ||
|  | bcb2ff0092 | ||
|  | 43a48403e9 | 
							
								
								
									
										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,6 +0,0 @@ | |||||||
| # Ignore virtual environments |  | ||||||
| env/ |  | ||||||
|  |  | ||||||
| # Ignore Python byte code cache |  | ||||||
| *.pyc |  | ||||||
| __pycache__ |  | ||||||
							
								
								
									
										8
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,9 +1,11 @@ | |||||||
| # Ignore virtual environments | # Ignore virtual environments | ||||||
| env/ | env/ | ||||||
|  | .env/ | ||||||
|  |  | ||||||
| # Ignore Python byte code cache | # Ignore Python byte code cache | ||||||
| *.pyc | *.pyc | ||||||
| __pycache__ | __pycache__ | ||||||
|  | .cache/ | ||||||
|  |  | ||||||
| # Ignore coverage reports | # Ignore coverage reports | ||||||
| .coverage | .coverage | ||||||
| @@ -15,3 +17,9 @@ dist/ | |||||||
|  |  | ||||||
| # Ignore stupid .DS_Store | # Ignore stupid .DS_Store | ||||||
| .DS_Store | .DS_Store | ||||||
|  |  | ||||||
|  | # Ignore PyCharm | ||||||
|  | .idea/ | ||||||
|  |  | ||||||
|  | # Ignore tox environments | ||||||
|  | .tox/ | ||||||
|   | |||||||
| @@ -1,31 +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' |  | ||||||
|         node: 'v6.2.0' |  | ||||||
|         variables: |  | ||||||
|             DJANGO_SETTINGS_MODULES: settings |  | ||||||
|             DATABASE_URL: postgres://scrutinizer:scrutinizer@localhost:5434/localized_fields |  | ||||||
|         postgresql: true |  | ||||||
|         redis: true |  | ||||||
|     dependencies: |  | ||||||
|         override: |  | ||||||
|             - 'pip install -r requirements/test.txt' |  | ||||||
|     tests: |  | ||||||
|         override: |  | ||||||
|             - |  | ||||||
|                 command: coverage run manage.py test |  | ||||||
|                 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 |  | ||||||
| @@ -1,2 +1,5 @@ | |||||||
| include LICENSE | include LICENSE | ||||||
| include README.rst | include README.rst | ||||||
|  | recursive-include localized_fields/static * | ||||||
|  | recursive-include localized_fields/templates * | ||||||
|  | recursive-exclude tests * | ||||||
|   | |||||||
							
								
								
									
										235
									
								
								README.rst
									
									
									
									
									
								
							
							
						
						
									
										235
									
								
								README.rst
									
									
									
									
									
								
							| @@ -1,20 +1,24 @@ | |||||||
| 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 | ||||||
|  |  | ||||||
| .. image:: https://badge.fury.io/py/django-localized-fields.svg | .. image:: https://badge.fury.io/py/django-localized-fields.svg | ||||||
|     :target: https://pypi.python.org/pypi/django-localized-fields |     :target: https://pypi.python.org/pypi/django-localized-fields | ||||||
|  |  | ||||||
| ``django-localized-fields`` is an implementation of a field class for Django models that allows the field's value to be set in multiple languages. It does this by utilizing the ``hstore`` type (PostgreSQL specific), which is available as ``models.HStoreField`` in Django 1.10. | ``django-localized-fields`` is an implementation of a field class for Django models that allows the field's value to be set in multiple languages. It does this by utilizing the ``hstore`` type (PostgreSQL specific), which is available as ``models.HStoreField`` since Django 1.10. | ||||||
|  |  | ||||||
| This package requires Python 3.5 or newer, Django 1.10 or newer and PostgreSQL 9.6 or newer. | This package requires Python 3.5 or newer, Django 1.11 or newer and PostgreSQL 9.6 or newer. | ||||||
|  |  | ||||||
|  | Contributors | ||||||
|  | ------------ | ||||||
|  |  | ||||||
|  | * `seroy <https://github.com/seroy/>`_ | ||||||
|  | * `unaizalakain <https://github.com/unaizalakain/>`_ | ||||||
|  |  | ||||||
| Installation | Installation | ||||||
| ------------ | ------------ | ||||||
| @@ -29,10 +33,10 @@ Installation | |||||||
|      .. code-block:: bash |      .. code-block:: bash | ||||||
|  |  | ||||||
|         INSTALLED_APPS = [ |         INSTALLED_APPS = [ | ||||||
|             .... |             ... | ||||||
|  |  | ||||||
|             'django.contrib.postgres', |             'django.contrib.postgres', | ||||||
|             'localized_fields' |             'localized_fields.apps.LocalizedFieldsConfig' | ||||||
|         ] |         ] | ||||||
|  |  | ||||||
| 3. Set the database engine to ``psqlextra.backend``: | 3. Set the database engine to ``psqlextra.backend``: | ||||||
| @@ -62,15 +66,14 @@ Usage | |||||||
|  |  | ||||||
| Preparation | Preparation | ||||||
| ^^^^^^^^^^^ | ^^^^^^^^^^^ | ||||||
| Inherit your model from ``LocalizedModel`` and declare fields on your model as ``LocalizedField``: | Declare fields on your model as ``LocalizedField``: | ||||||
|  |  | ||||||
| .. code-block:: python | .. code-block:: python | ||||||
|  |  | ||||||
|      from localized_fields.models import LocalizedModel |  | ||||||
|      from localized_fields.fields import LocalizedField |      from localized_fields.fields import LocalizedField | ||||||
|  |  | ||||||
|  |  | ||||||
|      class MyModel(LocalizedModel): |      class MyModel(models.Model): | ||||||
|          title = LocalizedField() |          title = LocalizedField() | ||||||
|  |  | ||||||
| ``django-localized-fields`` integrates with Django's i18n system, in order for certain languages to be available you have to correctly configure the ``LANGUAGES`` and ``LANGUAGE_CODE`` settings: | ``django-localized-fields`` integrates with Django's i18n system, in order for certain languages to be available you have to correctly configure the ``LANGUAGES`` and ``LANGUAGE_CODE`` settings: | ||||||
| @@ -115,6 +118,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: | ||||||
|  |  | ||||||
| @@ -130,21 +134,42 @@ Constraints | |||||||
|  |  | ||||||
| **Required/Optional** | **Required/Optional** | ||||||
|  |  | ||||||
| At the moment, it is not possible to select two languages to be marked as required. The constraint is **not** enforced on a database level. | Constraints are enforced on a database level. | ||||||
|  |  | ||||||
| * Make the primary language **required** and the others optional (this is the **default**): | * Optional filling | ||||||
|  |  | ||||||
|     .. code-block:: python |     .. code-block:: python | ||||||
|  |  | ||||||
|         class MyModel(LocalizedModel): |         class MyModel(models.Model): | ||||||
|             title = LocalizedField(required=True) |             title = LocalizedField(blank=True, null=True, required=False) | ||||||
|  |  | ||||||
| * Make all languages optional: | * Make translation required for any language | ||||||
|  |  | ||||||
|     .. code-block:: python |     .. code-block:: python | ||||||
|  |  | ||||||
|         class MyModel(LocalizedModel): |         class MyModel(models.Model): | ||||||
|             title = LocalizedField(null=True) |             title = LocalizedField(blank=False, null=False, required=False) | ||||||
|  |  | ||||||
|  | * Make translation required for specific languages | ||||||
|  |  | ||||||
|  |     .. code-block:: python | ||||||
|  |  | ||||||
|  |         class MyModel(models.Model): | ||||||
|  |             title = LocalizedField(blank=False, null=False, required=['en', 'ro']) | ||||||
|  |  | ||||||
|  | * Make translation required for all languages | ||||||
|  |  | ||||||
|  |     .. code-block:: python | ||||||
|  |  | ||||||
|  |         class MyModel(models.Model): | ||||||
|  |             title = LocalizedField(blank=False, null=False, required=True) | ||||||
|  |  | ||||||
|  | * By default the primary language **required** and the others optional: | ||||||
|  |  | ||||||
|  |     .. code-block:: python | ||||||
|  |  | ||||||
|  |         class MyModel(models.Model): | ||||||
|  |             title = LocalizedField() | ||||||
|  |  | ||||||
| **Uniqueness** | **Uniqueness** | ||||||
|  |  | ||||||
| @@ -154,32 +179,32 @@ By default the values stored in a ``LocalizedField`` are *not unique*. You can e | |||||||
|  |  | ||||||
|     .. code-block:: python |     .. code-block:: python | ||||||
|  |  | ||||||
|         class MyModel(LocalizedModel): |         class MyModel(models.Model): | ||||||
|             title = LocalizedField(uniqueness=['en', 'ro']) |             title = LocalizedField(uniqueness=['en', 'ro']) | ||||||
|  |  | ||||||
| * Enforce uniqueness for **all** languages: | * Enforce uniqueness for **all** languages: | ||||||
|  |  | ||||||
|     .. code-block:: python |     .. code-block:: python | ||||||
|  |  | ||||||
|         from localized_fields import get_language_codes |         from localized_fields.util import get_language_codes | ||||||
|  |  | ||||||
|         class MyModel(LocalizedModel): |         class MyModel(models.Model): | ||||||
|             title = LocalizedField(uniqueness=get_language_codes()) |             title = LocalizedField(uniqueness=get_language_codes()) | ||||||
|  |  | ||||||
| * Enforce uniqueness for one ore more languages **together** (similar to Django's ``unique_together``): | * Enforce uniqueness for one ore more languages **together** (similar to Django's ``unique_together``): | ||||||
|  |  | ||||||
|     .. code-block:: python |     .. code-block:: python | ||||||
|  |  | ||||||
|         class MyModel(LocalizedModel): |         class MyModel(models.Model): | ||||||
|             title = LocalizedField(uniqueness=[('en', 'ro')]) |             title = LocalizedField(uniqueness=[('en', 'ro')]) | ||||||
|  |  | ||||||
| * Enforce uniqueness for **all** languages **together**: | * Enforce uniqueness for **all** languages **together**: | ||||||
|  |  | ||||||
|     .. code-block:: python |     .. code-block:: python | ||||||
|  |  | ||||||
|         from localized_fields import get_language_codes |         from localized_fields.util import get_language_codes | ||||||
|  |  | ||||||
|         class MyModel(LocalizedModel): |         class MyModel(models.Model): | ||||||
|             title = LocalizedField(uniqueness=[(*get_language_codes())]) |             title = LocalizedField(uniqueness=[(*get_language_codes())]) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -193,15 +218,36 @@ Besides ``LocalizedField``, there's also: | |||||||
|  |  | ||||||
|           .. code-block:: python |           .. code-block:: python | ||||||
|  |  | ||||||
|               from localized_fields import (LocalizedModel, |               from localized_fields.models import LocalizedModel | ||||||
|                                             AtomicSlugRetryMixin, |               from localized_fields.fields import LocalizedField, LocalizedUniqueSlugField | ||||||
|                                             LocalizedField, |  | ||||||
|                                             LocalizedUniqueSlugField) |  | ||||||
|  |  | ||||||
|               class MyModel(AtomicSlugRetryMixin, LocalizedModel): |               class MyModel(LocalizedModel): | ||||||
|                    title = LocalizedField() |                    title = LocalizedField() | ||||||
|                    slug = LocalizedUniqueSlugField(populate_from='title') |                    slug = LocalizedUniqueSlugField(populate_from='title') | ||||||
|  |  | ||||||
|  |     ``populate_from`` can be: | ||||||
|  |  | ||||||
|  |         - The name of a field. | ||||||
|  |  | ||||||
|  |            .. code-block:: python | ||||||
|  |  | ||||||
|  |                slug = LocalizedUniqueSlugField(populate_from='name', include_time=True) | ||||||
|  |  | ||||||
|  |         - A callable. | ||||||
|  |  | ||||||
|  |            .. code-block:: python | ||||||
|  |  | ||||||
|  |                def generate_slug(instance): | ||||||
|  |                    return instance.title | ||||||
|  |  | ||||||
|  |                slug = LocalizedUniqueSlugField(populate_from=generate_slug, include_time=True) | ||||||
|  |  | ||||||
|  |         - A tuple of names of fields. | ||||||
|  |  | ||||||
|  |            .. code-block:: python | ||||||
|  |  | ||||||
|  |                slug = LocalizedUniqueSlugField(populate_from=('name', 'beer') include_time=True) | ||||||
|  |  | ||||||
|     By setting the option ``include_time=True`` |     By setting the option ``include_time=True`` | ||||||
|  |  | ||||||
|           .. code-block:: python |           .. code-block:: python | ||||||
| @@ -211,39 +257,122 @@ Besides ``LocalizedField``, there's also: | |||||||
|     You can instruct the field to include a part of the current time into |     You can instruct the field to include a part of the current time into | ||||||
|     the resulting slug. This is useful if you're running into a lot of collisions. |     the resulting slug. This is useful if you're running into a lot of collisions. | ||||||
|  |  | ||||||
| * ``LocalizedAutoSlugField`` |  | ||||||
|      Automatically creates a slug for every language from the specified field. |  | ||||||
|  |  | ||||||
|      Currently only supports ``populate_from``. Example usage: |  | ||||||
|  |  | ||||||
|           .. code-block:: python |  | ||||||
|  |  | ||||||
|               from localized_fields import (LocalizedModel, |  | ||||||
|                                             LocalizedField, |  | ||||||
|                                             LocalizedUniqueSlugField) |  | ||||||
|  |  | ||||||
|               class MyModel(LocalizedModel): |  | ||||||
|                    title = LocalizedField() |  | ||||||
|                    slug = LocalizedAutoSlugField(populate_from='title') |  | ||||||
|  |  | ||||||
|      This implementation is **NOT** concurrency safe, prefer ``LocalizedUniqueSlugField``. |  | ||||||
|  |  | ||||||
| * ``LocalizedBleachField`` | * ``LocalizedBleachField`` | ||||||
|      Automatically bleaches the content of the field. |      Automatically bleaches the content of the field. | ||||||
|  |  | ||||||
|           * django-bleach |           * django-bleach | ||||||
|  |  | ||||||
|      Example usage: |      Example usage: | ||||||
|  |  | ||||||
|            .. code-block:: python |            .. code-block:: python | ||||||
|  |  | ||||||
|               from localized_fields import (LocalizedModel, |               from localized_fields.fields import LocalizedField, LocalizedBleachField | ||||||
|                                             LocalizedField, |  | ||||||
|                                             LocalizedBleachField) |  | ||||||
|  |  | ||||||
|               class MyModel(LocalizedModel): |               class MyModel(models.Model): | ||||||
|                    title = LocalizedField() |                    title = LocalizedField() | ||||||
|                    description = LocalizedBleachField() |                    description = LocalizedBleachField() | ||||||
|  |  | ||||||
|  | * ``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`` | ||||||
|  |     These fields following the Django convention for string-based fields use the empty string as value for “no data”, not NULL. | ||||||
|  |     ``LocalizedCharField`` uses ``TextInput`` (``<input type="text">``) widget for render. | ||||||
|  |  | ||||||
|  |     Example usage: | ||||||
|  |  | ||||||
|  |            .. code-block:: python | ||||||
|  |  | ||||||
|  |               from localized_fields.fields import LocalizedCharField, LocalizedTextField | ||||||
|  |  | ||||||
|  |               class MyModel(models.Model): | ||||||
|  |                    title = LocalizedCharField() | ||||||
|  |                    description = LocalizedTextField() | ||||||
|  |  | ||||||
|  | * ``LocalizedFileField`` | ||||||
|  |     A file-upload field | ||||||
|  |  | ||||||
|  |     Parameter ``upload_to`` supports ``lang`` parameter for string formatting or as function argument (in case if ``upload_to`` is callable). | ||||||
|  |  | ||||||
|  |     Example usage: | ||||||
|  |  | ||||||
|  |            .. code-block:: python | ||||||
|  |  | ||||||
|  |               from localized_fields.fields import LocalizedFileField | ||||||
|  |  | ||||||
|  |               def my_directory_path(instance, filename, lang): | ||||||
|  |                 # file will be uploaded to MEDIA_ROOT/<lang>/<id>_<filename> | ||||||
|  |                 return '{0}/{0}_{1}'.format(lang, instance.id, filename) | ||||||
|  |  | ||||||
|  |               class MyModel(models.Model): | ||||||
|  |                    file1 = LocalizedFileField(upload_to='uploads/{lang}/') | ||||||
|  |                    file2 = LocalizedFileField(upload_to=my_directory_path) | ||||||
|  |  | ||||||
|  |     In template you can access to file attributes: | ||||||
|  |  | ||||||
|  |             .. code-block:: django | ||||||
|  |  | ||||||
|  |               {# For current active language: #} | ||||||
|  |  | ||||||
|  |               {{ model.file.url }}  {# output file url #} | ||||||
|  |               {{ model.file.name }} {# output file name #} | ||||||
|  |  | ||||||
|  |               {# Or get it in a specific language: #} | ||||||
|  |  | ||||||
|  |               {{ model.file.ro.url }}  {# output file url for romanian language #} | ||||||
|  |               {{ model.file.ro.name }} {# output file name for romanian language #} | ||||||
|  |  | ||||||
|  |     To get access to file instance for current active language use ``localized`` method: | ||||||
|  |  | ||||||
|  |             .. code-block:: python | ||||||
|  |  | ||||||
|  |               model.file.localized() | ||||||
|  |  | ||||||
|  | Experimental feature | ||||||
|  | ^^^^^^^^^^^^^^^^^^^^ | ||||||
|  | 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 | ||||||
|  |  | ||||||
|  |      LOCALIZED_FIELDS_EXPERIMENTAL = True | ||||||
|  |  | ||||||
|  |  | ||||||
|  | Django Admin Integration | ||||||
|  | ^^^^^^^^^^^^^^^^^^^^^^^^ | ||||||
|  | To enable widgets in the admin, you need to inherit from ``LocalizedFieldsAdminMixin``: | ||||||
|  |  | ||||||
|  | .. code-block:: python | ||||||
|  |  | ||||||
|  |     from django.contrib import admin | ||||||
|  |     from myapp.models import MyLocalizedModel | ||||||
|  |  | ||||||
|  |     from localized_fields.admin import LocalizedFieldsAdminMixin | ||||||
|  |  | ||||||
|  |     class MyLocalizedModelAdmin(LocalizedFieldsAdminMixin, admin.ModelAdmin): | ||||||
|  |         """Any admin options you need go here""" | ||||||
|  |  | ||||||
|  |     admin.site.register(MyLocalizedModel, MyLocalizedModelAdmin) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | .. image:: ./images/admin-widget.png | ||||||
|  |     :alt: The appearance of admin widget | ||||||
|  |  | ||||||
|  |  | ||||||
| Frequently asked questions (FAQ) | Frequently asked questions (FAQ) | ||||||
| -------------------------------- | -------------------------------- | ||||||
|  |  | ||||||
| @@ -251,9 +380,9 @@ Frequently asked questions (FAQ) | |||||||
|  |  | ||||||
|     No. Only Python 3.5 or newer is supported. We're using type hints. These do not work well under older versions of Python. |     No. Only Python 3.5 or newer is supported. We're using type hints. These do not work well under older versions of Python. | ||||||
|  |  | ||||||
| 2. Does this package work with Django 1.X? | 2. With what Django versions does this package work? | ||||||
|  |  | ||||||
|     No. Only Django 1.10 or newer is supported. This is because we rely on Django's ``HStoreField``. |     Only Django 1.11 or newer is supported, this includes Django 2.X. This is because we rely on Django's ``HStoreField`` and template-based widget rendering. | ||||||
|  |  | ||||||
| 3. Does this package come with support for Django Admin? | 3. Does this package come with support for Django Admin? | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										
											BIN
										
									
								
								images/admin-widget.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								images/admin-widget.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 57 KiB | 
| @@ -1,20 +1 @@ | |||||||
| from .util import get_language_codes | default_app_config = 'localized_fields.apps.LocalizedFieldsConfig' | ||||||
| from .forms import LocalizedFieldForm, LocalizedFieldWidget |  | ||||||
| from .fields import (LocalizedField, LocalizedBleachField, |  | ||||||
|                      LocalizedAutoSlugField, LocalizedUniqueSlugField) |  | ||||||
| from .mixins import AtomicSlugRetryMixin |  | ||||||
| from .models import LocalizedModel |  | ||||||
| from .localized_value import LocalizedValue |  | ||||||
|  |  | ||||||
| __all__ = [ |  | ||||||
|     'get_language_codes', |  | ||||||
|     'LocalizedField', |  | ||||||
|     'LocalizedValue', |  | ||||||
|     'LocalizedAutoSlugField', |  | ||||||
|     'LocalizedUniqueSlugField', |  | ||||||
|     'LocalizedBleachField', |  | ||||||
|     'LocalizedFieldWidget', |  | ||||||
|     'LocalizedFieldForm', |  | ||||||
|     'LocalizedModel', |  | ||||||
|     'AtomicSlugRetryMixin' |  | ||||||
| ] |  | ||||||
|   | |||||||
							
								
								
									
										35
									
								
								localized_fields/admin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								localized_fields/admin.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | |||||||
|  | from . import widgets | ||||||
|  | from .fields import LocalizedField, LocalizedCharField, LocalizedTextField, \ | ||||||
|  |     LocalizedFileField | ||||||
|  |  | ||||||
|  |  | ||||||
|  | FORMFIELD_FOR_LOCALIZED_FIELDS_DEFAULTS = { | ||||||
|  |     LocalizedField: {'widget': widgets.AdminLocalizedFieldWidget}, | ||||||
|  |     LocalizedCharField: {'widget': widgets.AdminLocalizedCharFieldWidget}, | ||||||
|  |     LocalizedTextField: {'widget': widgets.AdminLocalizedFieldWidget}, | ||||||
|  |     LocalizedFileField: {'widget': widgets.AdminLocalizedFileFieldWidget}, | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedFieldsAdminMixin: | ||||||
|  |     """Mixin for making the fancy widgets work in Django Admin.""" | ||||||
|  |  | ||||||
|  |     class Media: | ||||||
|  |         css = { | ||||||
|  |             'all': ( | ||||||
|  |                 'localized_fields/localized-fields-admin.css', | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         js = ( | ||||||
|  |             'admin/js/jquery.init.js', | ||||||
|  |             'localized_fields/localized-fields-admin.js', | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def __init__(self, *args, **kwargs): | ||||||
|  |         """Initializes a new instance of :see:LocalizedFieldsAdminMixin.""" | ||||||
|  |  | ||||||
|  |         super().__init__(*args, **kwargs) | ||||||
|  |         overrides = FORMFIELD_FOR_LOCALIZED_FIELDS_DEFAULTS.copy() | ||||||
|  |         overrides.update(self.formfield_overrides) | ||||||
|  |         self.formfield_overrides = overrides | ||||||
| @@ -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) | ||||||
|   | |||||||
							
								
								
									
										65
									
								
								localized_fields/descriptor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								localized_fields/descriptor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | |||||||
|  | from django.conf import settings | ||||||
|  | from django.utils import six, translation | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedValueDescriptor: | ||||||
|  |     """ | ||||||
|  |     The descriptor for the localized value attribute on the model instance. | ||||||
|  |     Returns a :see:LocalizedValue when accessed so you can do stuff like:: | ||||||
|  |  | ||||||
|  |         >>> from myapp.models import MyModel | ||||||
|  |         >>> instance = MyModel() | ||||||
|  |         >>> instance.value.en = 'English value' | ||||||
|  |  | ||||||
|  |     Assigns a strings to active language key in :see:LocalizedValue on | ||||||
|  |     assignment so you can do:: | ||||||
|  |  | ||||||
|  |         >>> from django.utils import translation | ||||||
|  |         >>> from myapp.models import MyModel | ||||||
|  |  | ||||||
|  |         >>> translation.activate('nl') | ||||||
|  |         >>> instance = MyModel() | ||||||
|  |         >>> instance.title = 'dutch title' | ||||||
|  |         >>> print(instance.title.nl) # prints 'dutch title' | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def __init__(self, field): | ||||||
|  |         """Initializes a new instance of :see:LocalizedValueDescriptor.""" | ||||||
|  |  | ||||||
|  |         self.field = field | ||||||
|  |  | ||||||
|  |     def __get__(self, instance, cls=None): | ||||||
|  |         if instance is None: | ||||||
|  |             return self | ||||||
|  |  | ||||||
|  |         # This is slightly complicated, so worth an explanation. | ||||||
|  |         # `instance.localizedvalue` needs to ultimately return some instance of | ||||||
|  |         # `LocalizedValue`, probably a subclass. | ||||||
|  |  | ||||||
|  |         # The instance dict contains whatever was originally assigned | ||||||
|  |         # in __set__. | ||||||
|  |  | ||||||
|  |         if self.field.name in instance.__dict__: | ||||||
|  |             value = instance.__dict__[self.field.name] | ||||||
|  |         elif not instance._state.adding: | ||||||
|  |             instance.refresh_from_db(fields=[self.field.name]) | ||||||
|  |             value = getattr(instance, self.field.name) | ||||||
|  |         else: | ||||||
|  |             value = None | ||||||
|  |  | ||||||
|  |         if value is None: | ||||||
|  |             attr = self.field.attr_class() | ||||||
|  |             instance.__dict__[self.field.name] = attr | ||||||
|  |  | ||||||
|  |         if isinstance(value, dict): | ||||||
|  |             attr = self.field.attr_class(value) | ||||||
|  |             instance.__dict__[self.field.name] = attr | ||||||
|  |  | ||||||
|  |         return instance.__dict__[self.field.name] | ||||||
|  |  | ||||||
|  |     def __set__(self, instance, value): | ||||||
|  |         if isinstance(value, six.string_types): | ||||||
|  |             language = translation.get_language() or settings.LANGUAGE_CODE | ||||||
|  |             self.__get__(instance).set(language, value)  # pylint: disable=no-member | ||||||
|  |         else: | ||||||
|  |             instance.__dict__[self.field.name] = value | ||||||
							
								
								
									
										25
									
								
								localized_fields/expressions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								localized_fields/expressions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | from django.conf import settings | ||||||
|  | from django.utils import translation | ||||||
|  |  | ||||||
|  | from psqlextra import expressions | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedRef(expressions.HStoreRef): | ||||||
|  |     """Expression that selects the value in a field only in | ||||||
|  |     the currently active language.""" | ||||||
|  |  | ||||||
|  |     def __init__(self, name: str, lang: str=None): | ||||||
|  |         """Initializes a new instance of :see:LocalizedRef. | ||||||
|  |  | ||||||
|  |         Arguments: | ||||||
|  |             name: | ||||||
|  |                 The field/column to select from. | ||||||
|  |  | ||||||
|  |             lang: | ||||||
|  |                 The language to get the field/column in. | ||||||
|  |                 If not specified, the currently active language | ||||||
|  |                 is used. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         language = lang or translation.get_language() or settings.LANGUAGE_CODE | ||||||
|  |         super().__init__(name, language) | ||||||
| @@ -1,12 +1,26 @@ | |||||||
| from .localized_field import LocalizedField | from .field import LocalizedField | ||||||
| from .localized_autoslug_field import LocalizedAutoSlugField | from .autoslug_field import LocalizedAutoSlugField | ||||||
| from .localized_uniqueslug_field import LocalizedUniqueSlugField | from .uniqueslug_field import LocalizedUniqueSlugField | ||||||
| from .localized_bleach_field import LocalizedBleachField | from .char_field import LocalizedCharField | ||||||
|  | from .text_field import LocalizedTextField | ||||||
|  | from .file_field import LocalizedFileField | ||||||
|  | from .integer_field import LocalizedIntegerField | ||||||
|  |  | ||||||
|  |  | ||||||
| __all__ = [ | __all__ = [ | ||||||
|     'LocalizedField', |     'LocalizedField', | ||||||
|     'LocalizedAutoSlugField', |     'LocalizedAutoSlugField', | ||||||
|     'LocalizedUniqueSlugField', |     'LocalizedUniqueSlugField', | ||||||
|     'LocalizedBleachField', |     'LocalizedCharField', | ||||||
|  |     'LocalizedTextField', | ||||||
|  |     'LocalizedFileField', | ||||||
|  |     'LocalizedIntegerField' | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | try: | ||||||
|  |     from .bleach_field import LocalizedBleachField | ||||||
|  |     __all__ += [ | ||||||
|  |         'LocalizedBleachField' | ||||||
|  |     ] | ||||||
|  | except ImportError: | ||||||
|  |     pass | ||||||
|   | |||||||
| @@ -1,12 +1,16 @@ | |||||||
| from typing import Callable | import warnings | ||||||
|  | 
 | ||||||
|  | from typing import Callable, Tuple, Union | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| 
 | 
 | ||||||
| from django import forms | from django import forms | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
|  | from django.utils import translation | ||||||
| from django.utils.text import slugify | from django.utils.text import slugify | ||||||
| 
 | 
 | ||||||
| from .localized_field import LocalizedField | from .field import LocalizedField | ||||||
| from ..localized_value import LocalizedValue | from ..value import LocalizedValue | ||||||
|  | from ..util import resolve_object_property | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class LocalizedAutoSlugField(LocalizedField): | class LocalizedAutoSlugField(LocalizedField): | ||||||
| @@ -19,6 +23,11 @@ class LocalizedAutoSlugField(LocalizedField): | |||||||
|         self.populate_from = kwargs.pop('populate_from', None) |         self.populate_from = kwargs.pop('populate_from', None) | ||||||
|         self.include_time = kwargs.pop('include_time', False) |         self.include_time = kwargs.pop('include_time', False) | ||||||
| 
 | 
 | ||||||
|  |         warnings.warn( | ||||||
|  |             'LocalizedAutoSlug is deprecated and will be removed in the next major version.', | ||||||
|  |             DeprecationWarning | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|         super(LocalizedAutoSlugField, self).__init__( |         super(LocalizedAutoSlugField, self).__init__( | ||||||
|             *args, |             *args, | ||||||
|             **kwargs |             **kwargs | ||||||
| @@ -69,13 +78,7 @@ class LocalizedAutoSlugField(LocalizedField): | |||||||
| 
 | 
 | ||||||
|         slugs = LocalizedValue() |         slugs = LocalizedValue() | ||||||
| 
 | 
 | ||||||
|         for lang_code, _ in settings.LANGUAGES: |         for lang_code, value in self._get_populate_values(instance): | ||||||
|             value = self._get_populate_from_value( |  | ||||||
|                 instance, |  | ||||||
|                 self.populate_from, |  | ||||||
|                 lang_code |  | ||||||
|             ) |  | ||||||
| 
 |  | ||||||
|             if not value: |             if not value: | ||||||
|                 continue |                 continue | ||||||
| 
 | 
 | ||||||
| @@ -128,8 +131,32 @@ class LocalizedAutoSlugField(LocalizedField): | |||||||
| 
 | 
 | ||||||
|         return unique_slug |         return unique_slug | ||||||
| 
 | 
 | ||||||
|  |     def _get_populate_values(self, instance) -> Tuple[str, str]: | ||||||
|  |         """Gets all values (for each language) from the | ||||||
|  |         specified's instance's `populate_from` field. | ||||||
|  | 
 | ||||||
|  |         Arguments: | ||||||
|  |             instance: | ||||||
|  |                 The instance to get the values from. | ||||||
|  | 
 | ||||||
|  |         Returns: | ||||||
|  |             A list of (lang_code, value) tuples. | ||||||
|  |         """ | ||||||
|  | 
 | ||||||
|  |         return [ | ||||||
|  |             ( | ||||||
|  |                 lang_code, | ||||||
|  |                 self._get_populate_from_value( | ||||||
|  |                     instance, | ||||||
|  |                     self.populate_from, | ||||||
|  |                     lang_code | ||||||
|  |                 ), | ||||||
|  |             ) | ||||||
|  |             for lang_code, _ in settings.LANGUAGES | ||||||
|  |         ] | ||||||
|  | 
 | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def _get_populate_from_value(instance, field_name: str, language: str): |     def _get_populate_from_value(instance, field_name: Union[str, Tuple[str]], language: str): | ||||||
|         """Gets the value to create a slug from in the specified language. |         """Gets the value to create a slug from in the specified language. | ||||||
| 
 | 
 | ||||||
|         Arguments: |         Arguments: | ||||||
| @@ -146,5 +173,20 @@ class LocalizedAutoSlugField(LocalizedField): | |||||||
|             The text to generate a slug for. |             The text to generate a slug for. | ||||||
|         """ |         """ | ||||||
| 
 | 
 | ||||||
|         value = getattr(instance, field_name, None) |         if callable(field_name): | ||||||
|         return value.get(language) |             return field_name(instance) | ||||||
|  | 
 | ||||||
|  |         def get_field_value(name): | ||||||
|  |             value = resolve_object_property(instance, name) | ||||||
|  |             with translation.override(language): | ||||||
|  |                 return str(value) | ||||||
|  | 
 | ||||||
|  |         if isinstance(field_name, tuple) or isinstance(field_name, list): | ||||||
|  |             value = '-'.join([ | ||||||
|  |                 value | ||||||
|  |                 for value in [get_field_value(name) for name in field_name] | ||||||
|  |                 if value | ||||||
|  |             ]) | ||||||
|  |             return value | ||||||
|  | 
 | ||||||
|  |         return get_field_value(field_name) | ||||||
| @@ -1,8 +1,9 @@ | |||||||
| import bleach | import bleach | ||||||
|  | 
 | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django_bleach.utils import get_bleach_default_options | from django_bleach.utils import get_bleach_default_options | ||||||
| 
 | 
 | ||||||
| from .localized_field import LocalizedField | from .field import LocalizedField | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class LocalizedBleachField(LocalizedField): | class LocalizedBleachField(LocalizedField): | ||||||
							
								
								
									
										16
									
								
								localized_fields/fields/char_field.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								localized_fields/fields/char_field.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | from ..forms import LocalizedCharFieldForm | ||||||
|  | from .field import LocalizedField | ||||||
|  | from ..value import LocalizedStringValue | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedCharField(LocalizedField): | ||||||
|  |     attr_class = LocalizedStringValue | ||||||
|  |  | ||||||
|  |     def formfield(self, **kwargs): | ||||||
|  |         """Gets the form field associated with this field.""" | ||||||
|  |         defaults = { | ||||||
|  |             'form_class': LocalizedCharFieldForm | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         defaults.update(kwargs) | ||||||
|  |         return super().formfield(**defaults) | ||||||
							
								
								
									
										226
									
								
								localized_fields/fields/field.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										226
									
								
								localized_fields/fields/field.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,226 @@ | |||||||
|  | import json | ||||||
|  |  | ||||||
|  | from typing import Union, List, Optional | ||||||
|  |  | ||||||
|  | from django.conf import settings | ||||||
|  | from django.db.utils import IntegrityError | ||||||
|  |  | ||||||
|  | from psqlextra.fields import HStoreField | ||||||
|  |  | ||||||
|  | from ..forms import LocalizedFieldForm | ||||||
|  | from ..value import LocalizedValue | ||||||
|  | from ..descriptor import LocalizedValueDescriptor | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedField(HStoreField): | ||||||
|  |     """A field that has the same value in multiple languages. | ||||||
|  |  | ||||||
|  |     Internally this is stored as a :see:HStoreField where there | ||||||
|  |     is a key for every language.""" | ||||||
|  |  | ||||||
|  |     Meta = None | ||||||
|  |  | ||||||
|  |     # The class to wrap instance attributes in. Accessing to field attribute in | ||||||
|  |     # model instance will always return an instance of attr_class. | ||||||
|  |     attr_class = LocalizedValue | ||||||
|  |  | ||||||
|  |     # The descriptor to use for accessing the attribute off of the class. | ||||||
|  |     descriptor_class = LocalizedValueDescriptor | ||||||
|  |  | ||||||
|  |     def __init__(self, *args, required: Union[bool, List[str]]=None, **kwargs): | ||||||
|  |         """Initializes a new instance of :see:LocalizedField.""" | ||||||
|  |  | ||||||
|  |         super(LocalizedField, self).__init__(*args, required=required, **kwargs) | ||||||
|  |  | ||||||
|  |         if (self.required is None and self.blank) or self.required is False: | ||||||
|  |             self.required = [] | ||||||
|  |         elif self.required is None and not self.blank: | ||||||
|  |             self.required = [settings.LANGUAGE_CODE] | ||||||
|  |         elif self.required is True: | ||||||
|  |             self.required = [lang_code for lang_code, _ in settings.LANGUAGES] | ||||||
|  |  | ||||||
|  |     def contribute_to_class(self, model, name, **kwargs): | ||||||
|  |         """Adds this field to the specifed model. | ||||||
|  |  | ||||||
|  |         Arguments: | ||||||
|  |             cls: | ||||||
|  |                 The model to add the field to. | ||||||
|  |  | ||||||
|  |             name: | ||||||
|  |                 The name of the field to add. | ||||||
|  |         """ | ||||||
|  |         super(LocalizedField, self).contribute_to_class(model, name, **kwargs) | ||||||
|  |         setattr(model, self.name, self.descriptor_class(self)) | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def from_db_value(cls, value, *_) -> Optional[LocalizedValue]: | ||||||
|  |         """Turns the specified database value into its Python | ||||||
|  |         equivalent. | ||||||
|  |  | ||||||
|  |         Arguments: | ||||||
|  |             value: | ||||||
|  |                 The value that is stored in the database and | ||||||
|  |                 needs to be converted to its Python equivalent. | ||||||
|  |  | ||||||
|  |         Returns: | ||||||
|  |             A :see:LocalizedValue instance containing the | ||||||
|  |             data extracted from the database. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         if not value: | ||||||
|  |             if getattr(settings, 'LOCALIZED_FIELDS_EXPERIMENTAL', False): | ||||||
|  |                 return None | ||||||
|  |             else: | ||||||
|  |                 return cls.attr_class() | ||||||
|  |  | ||||||
|  |         # we can get a list if an aggregation expression was used.. | ||||||
|  |         # if we the expression was flattened when only one key was selected | ||||||
|  |         # then we don't wrap each value in a localized value, otherwise we do | ||||||
|  |         if isinstance(value, list): | ||||||
|  |             result = [] | ||||||
|  |             for inner_val in value: | ||||||
|  |                 if isinstance(inner_val, dict): | ||||||
|  |                     if inner_val is None: | ||||||
|  |                         result.append(None) | ||||||
|  |                     else: | ||||||
|  |                         result.append(cls.attr_class(inner_val)) | ||||||
|  |                 else: | ||||||
|  |                     result.append(inner_val) | ||||||
|  |  | ||||||
|  |             return result | ||||||
|  |  | ||||||
|  |         # this is for when you select an individual key, it will be string, | ||||||
|  |         # not a dictionary, we'll give it to you as a flat value, not as a | ||||||
|  |         # localized value instance | ||||||
|  |         if not isinstance(value, dict): | ||||||
|  |             return value | ||||||
|  |  | ||||||
|  |         return cls.attr_class(value) | ||||||
|  |  | ||||||
|  |     def to_python(self, value: Union[dict, str, None]) -> LocalizedValue: | ||||||
|  |         """Turns the specified database value into its Python | ||||||
|  |         equivalent. | ||||||
|  |  | ||||||
|  |         Arguments: | ||||||
|  |             value: | ||||||
|  |                 The value that is stored in the database and | ||||||
|  |                 needs to be converted to its Python equivalent. | ||||||
|  |  | ||||||
|  |         Returns: | ||||||
|  |             A :see:LocalizedValue instance containing the | ||||||
|  |             data extracted from the database. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         # first let the base class  handle the deserialization, this is in case we | ||||||
|  |         # get specified a json string representing a dict | ||||||
|  |         try: | ||||||
|  |             deserialized_value = super(LocalizedField, self).to_python(value) | ||||||
|  |         except json.JSONDecodeError: | ||||||
|  |             deserialized_value = value | ||||||
|  |  | ||||||
|  |         if not deserialized_value: | ||||||
|  |             return self.attr_class() | ||||||
|  |  | ||||||
|  |         return self.attr_class(deserialized_value) | ||||||
|  |  | ||||||
|  |     def get_prep_value(self, value: LocalizedValue) -> dict: | ||||||
|  |         """Turns the specified value into something the database | ||||||
|  |         can store. | ||||||
|  |  | ||||||
|  |         If an illegal value (non-LocalizedValue instance) is | ||||||
|  |         specified, we'll treat it as an empty :see:LocalizedValue | ||||||
|  |         instance, on which the validation will fail. | ||||||
|  |  | ||||||
|  |         Dictonaries are converted into :see:LocalizedValue instances. | ||||||
|  |  | ||||||
|  |         Arguments: | ||||||
|  |             value: | ||||||
|  |                 The :see:LocalizedValue instance to serialize | ||||||
|  |                 into a data type that the database can understand. | ||||||
|  |  | ||||||
|  |         Returns: | ||||||
|  |             A dictionary containing a key for every language, | ||||||
|  |             extracted from the specified value. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         if isinstance(value, dict): | ||||||
|  |             value = LocalizedValue(value) | ||||||
|  |  | ||||||
|  |         # default to None if this is an unknown type | ||||||
|  |         if not isinstance(value, LocalizedValue) and value: | ||||||
|  |             value = None | ||||||
|  |  | ||||||
|  |         if value: | ||||||
|  |             cleaned_value = self.clean(value) | ||||||
|  |             self.validate(cleaned_value) | ||||||
|  |         else: | ||||||
|  |             cleaned_value = value | ||||||
|  |  | ||||||
|  |         return super(LocalizedField, self).get_prep_value( | ||||||
|  |             cleaned_value.__dict__ if cleaned_value else None | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def clean(self, value, *_): | ||||||
|  |         """Cleans the specified value into something we | ||||||
|  |         can store in the database. | ||||||
|  |  | ||||||
|  |         For example, when all the language fields are | ||||||
|  |         left empty, and the field is allowed to be null, | ||||||
|  |         we will store None instead of empty keys. | ||||||
|  |  | ||||||
|  |         Arguments: | ||||||
|  |             value: | ||||||
|  |                 The value to clean. | ||||||
|  |  | ||||||
|  |         Returns: | ||||||
|  |             The cleaned value, ready for database storage. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         if not value or not isinstance(value, LocalizedValue): | ||||||
|  |             return None | ||||||
|  |  | ||||||
|  |         # are any of the language fiels None/empty? | ||||||
|  |         is_all_null = True | ||||||
|  |         for lang_code, _ in settings.LANGUAGES: | ||||||
|  |             if value.get(lang_code) is not None: | ||||||
|  |                 is_all_null = False | ||||||
|  |                 break | ||||||
|  |  | ||||||
|  |         # all fields have been left empty and we support | ||||||
|  |         # null values, let's return null to represent that | ||||||
|  |         if is_all_null and self.null: | ||||||
|  |             return None | ||||||
|  |  | ||||||
|  |         return value | ||||||
|  |  | ||||||
|  |     def validate(self, value: LocalizedValue, *_): | ||||||
|  |         """Validates that the values has been filled in for all required | ||||||
|  |         languages | ||||||
|  |  | ||||||
|  |         Exceptions are raises in order to notify the user | ||||||
|  |         of invalid values. | ||||||
|  |  | ||||||
|  |         Arguments: | ||||||
|  |             value: | ||||||
|  |                 The value to validate. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         if self.null: | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         for lang in self.required: | ||||||
|  |             lang_val = getattr(value, settings.LANGUAGE_CODE) | ||||||
|  |  | ||||||
|  |             if lang_val is None: | ||||||
|  |                 raise IntegrityError('null value in column "%s.%s" violates ' | ||||||
|  |                                      'not-null constraint' % (self.name, lang)) | ||||||
|  |  | ||||||
|  |     def formfield(self, **kwargs): | ||||||
|  |         """Gets the form field associated with this field.""" | ||||||
|  |  | ||||||
|  |         defaults = dict( | ||||||
|  |             form_class=LocalizedFieldForm, | ||||||
|  |             required=False if self.blank else self.required | ||||||
|  |         ) | ||||||
|  |         defaults.update(kwargs) | ||||||
|  |         return super().formfield(**defaults) | ||||||
							
								
								
									
										158
									
								
								localized_fields/fields/file_field.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								localized_fields/fields/file_field.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,158 @@ | |||||||
|  | import json | ||||||
|  | import datetime | ||||||
|  | import posixpath | ||||||
|  |  | ||||||
|  | from django.core.files import File | ||||||
|  | from django.db.models.fields.files import FieldFile | ||||||
|  | from django.utils import six | ||||||
|  | from django.core.files.storage import default_storage | ||||||
|  | from django.utils.encoding import force_str, force_text | ||||||
|  |  | ||||||
|  | from localized_fields.fields import LocalizedField | ||||||
|  | from localized_fields.fields.field import LocalizedValueDescriptor | ||||||
|  | from localized_fields.value import LocalizedValue | ||||||
|  |  | ||||||
|  | from ..value import LocalizedFileValue | ||||||
|  | from ..forms import LocalizedFileFieldForm | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedFieldFile(FieldFile): | ||||||
|  |  | ||||||
|  |     def __init__(self, instance, field, name, lang): | ||||||
|  |         super().__init__(instance, field, name) | ||||||
|  |         self.lang = lang | ||||||
|  |  | ||||||
|  |     def save(self, name, content, save=True): | ||||||
|  |         name = self.field.generate_filename(self.instance, name, self.lang) | ||||||
|  |         self.name = self.storage.save(name, content, | ||||||
|  |                                       max_length=self.field.max_length) | ||||||
|  |         self._committed = True | ||||||
|  |  | ||||||
|  |         if save: | ||||||
|  |             self.instance.save() | ||||||
|  |  | ||||||
|  |     save.alters_data = True | ||||||
|  |  | ||||||
|  |     def delete(self, save=True): | ||||||
|  |         if not self: | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         if hasattr(self, '_file'): | ||||||
|  |             self.close() | ||||||
|  |             del self.file | ||||||
|  |  | ||||||
|  |         self.storage.delete(self.name) | ||||||
|  |  | ||||||
|  |         self.name = None | ||||||
|  |         self._committed = False | ||||||
|  |  | ||||||
|  |         if save: | ||||||
|  |             self.instance.save() | ||||||
|  |  | ||||||
|  |     delete.alters_data = True | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedFileValueDescriptor(LocalizedValueDescriptor): | ||||||
|  |     def __get__(self, instance, cls=None): | ||||||
|  |         value = super().__get__(instance, cls) | ||||||
|  |         for lang, file in value.__dict__.items(): | ||||||
|  |             if isinstance(file, six.string_types) or file is None: | ||||||
|  |                 file = self.field.value_class(instance, self.field, file, lang) | ||||||
|  |                 value.set(lang, file) | ||||||
|  |  | ||||||
|  |             elif isinstance(file, File) and \ | ||||||
|  |                     not isinstance(file, LocalizedFieldFile): | ||||||
|  |                 file_copy = self.field.value_class(instance, self.field, | ||||||
|  |                                                    file.name, lang) | ||||||
|  |                 file_copy.file = file | ||||||
|  |                 file_copy._committed = False | ||||||
|  |                 value.set(lang, file_copy) | ||||||
|  |  | ||||||
|  |             elif isinstance(file, LocalizedFieldFile) and \ | ||||||
|  |                     not hasattr(file, 'field'): | ||||||
|  |                 file.instance = instance | ||||||
|  |                 file.field = self.field | ||||||
|  |                 file.storage = self.field.storage | ||||||
|  |                 file.lang = lang | ||||||
|  |  | ||||||
|  |             # Make sure that the instance is correct. | ||||||
|  |             elif isinstance(file, LocalizedFieldFile) \ | ||||||
|  |                     and instance is not file.instance: | ||||||
|  |                 file.instance = instance | ||||||
|  |                 file.lang = lang | ||||||
|  |         return value | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedFileField(LocalizedField): | ||||||
|  |     descriptor_class = LocalizedFileValueDescriptor | ||||||
|  |     attr_class = LocalizedFileValue | ||||||
|  |     value_class = LocalizedFieldFile | ||||||
|  |  | ||||||
|  |     def __init__(self, verbose_name=None, name=None, upload_to='', storage=None, | ||||||
|  |                  **kwargs): | ||||||
|  |  | ||||||
|  |         self.storage = storage or default_storage | ||||||
|  |         self.upload_to = upload_to | ||||||
|  |  | ||||||
|  |         super().__init__(verbose_name, name, **kwargs) | ||||||
|  |  | ||||||
|  |     def deconstruct(self): | ||||||
|  |         name, path, args, kwargs = super().deconstruct() | ||||||
|  |         kwargs['upload_to'] = self.upload_to | ||||||
|  |         if self.storage is not default_storage: | ||||||
|  |             kwargs['storage'] = self.storage | ||||||
|  |         return name, path, args, kwargs | ||||||
|  |  | ||||||
|  |     def get_prep_value(self, value): | ||||||
|  |         """Returns field's value prepared for saving into a database.""" | ||||||
|  |  | ||||||
|  |         if isinstance(value, LocalizedValue): | ||||||
|  |             prep_value = LocalizedValue() | ||||||
|  |             for k, v in value.__dict__.items(): | ||||||
|  |                 if v is None: | ||||||
|  |                     prep_value.set(k, '') | ||||||
|  |                 else: | ||||||
|  |                     # Need to convert File objects provided via a form to | ||||||
|  |                     # unicode for database insertion | ||||||
|  |                     prep_value.set(k, six.text_type(v)) | ||||||
|  |             return super().get_prep_value(prep_value) | ||||||
|  |         return super().get_prep_value(value) | ||||||
|  |  | ||||||
|  |     def pre_save(self, model_instance, add): | ||||||
|  |         """Returns field's value just before saving.""" | ||||||
|  |         value = super().pre_save(model_instance, add) | ||||||
|  |         if isinstance(value, LocalizedValue): | ||||||
|  |             for file in value.__dict__.values(): | ||||||
|  |                 if file and not file._committed: | ||||||
|  |                     file.save(file.name, file, save=False) | ||||||
|  |         return value | ||||||
|  |  | ||||||
|  |     def generate_filename(self, instance, filename, lang): | ||||||
|  |         if callable(self.upload_to): | ||||||
|  |             filename = self.upload_to(instance, filename, lang) | ||||||
|  |         else: | ||||||
|  |             now = datetime.datetime.now() | ||||||
|  |             dirname = force_text(now.strftime(force_str(self.upload_to))) | ||||||
|  |             dirname = dirname.format(lang=lang) | ||||||
|  |             filename = posixpath.join(dirname, filename) | ||||||
|  |         return self.storage.generate_filename(filename) | ||||||
|  |  | ||||||
|  |     def save_form_data(self, instance, data): | ||||||
|  |         if isinstance(data, LocalizedValue): | ||||||
|  |             for k, v in data.__dict__.items(): | ||||||
|  |                 if v is not None and not v: | ||||||
|  |                     data.set(k, '') | ||||||
|  |             setattr(instance, self.name, data) | ||||||
|  |  | ||||||
|  |     def formfield(self, **kwargs): | ||||||
|  |         defaults = {'form_class': LocalizedFileFieldForm} | ||||||
|  |         defaults.update(kwargs) | ||||||
|  |         return super().formfield(**defaults) | ||||||
|  |  | ||||||
|  |     def value_to_string(self, obj): | ||||||
|  |         value = self.value_from_object(obj) | ||||||
|  |         if isinstance(value, LocalizedFileValue): | ||||||
|  |             return json.dumps({k: v.name for k, v | ||||||
|  |                                in value.__dict__.items()}) | ||||||
|  |         else: | ||||||
|  |             return super().value_to_string(obj) | ||||||
							
								
								
									
										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) | ||||||
| @@ -1,172 +0,0 @@ | |||||||
| from django.conf import settings |  | ||||||
| from django.db.utils import IntegrityError |  | ||||||
| from localized_fields import LocalizedFieldForm |  | ||||||
| from psqlextra.fields import HStoreField |  | ||||||
|  |  | ||||||
| from ..localized_value import LocalizedValue |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class LocalizedField(HStoreField): |  | ||||||
|     """A field that has the same value in multiple languages. |  | ||||||
|  |  | ||||||
|     Internally this is stored as a :see:HStoreField where there |  | ||||||
|     is a key for every language.""" |  | ||||||
|  |  | ||||||
|     Meta = None |  | ||||||
|  |  | ||||||
|     def __init__(self, *args, **kwargs): |  | ||||||
|         """Initializes a new instance of :see:LocalizedField.""" |  | ||||||
|  |  | ||||||
|         super(LocalizedField, self).__init__( |  | ||||||
|             *args, required=[settings.LANGUAGE_CODE], **kwargs) |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def from_db_value(value, *_): |  | ||||||
|         """Turns the specified database value into its Python |  | ||||||
|         equivalent. |  | ||||||
|  |  | ||||||
|         Arguments: |  | ||||||
|             value: |  | ||||||
|                 The value that is stored in the database and |  | ||||||
|                 needs to be converted to its Python equivalent. |  | ||||||
|  |  | ||||||
|         Returns: |  | ||||||
|             A :see:LocalizedValue instance containing the |  | ||||||
|             data extracted from the database. |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         if not value: |  | ||||||
|             return LocalizedValue() |  | ||||||
|  |  | ||||||
|         return LocalizedValue(value) |  | ||||||
|  |  | ||||||
|     def to_python(self, value: dict) -> LocalizedValue: |  | ||||||
|         """Turns the specified database value into its Python |  | ||||||
|         equivalent. |  | ||||||
|  |  | ||||||
|         Arguments: |  | ||||||
|             value: |  | ||||||
|                 The value that is stored in the database and |  | ||||||
|                 needs to be converted to its Python equivalent. |  | ||||||
|  |  | ||||||
|         Returns: |  | ||||||
|             A :see:LocalizedValue instance containing the |  | ||||||
|             data extracted from the database. |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         if not value or not isinstance(value, dict): |  | ||||||
|             return LocalizedValue() |  | ||||||
|  |  | ||||||
|         return LocalizedValue(value) |  | ||||||
|  |  | ||||||
|     def get_prep_value(self, value: LocalizedValue) -> dict: |  | ||||||
|         """Turns the specified value into something the database |  | ||||||
|         can store. |  | ||||||
|  |  | ||||||
|         If an illegal value (non-LocalizedValue instance) is |  | ||||||
|         specified, we'll treat it as an empty :see:LocalizedValue |  | ||||||
|         instance, on which the validation will fail. |  | ||||||
|  |  | ||||||
|         Arguments: |  | ||||||
|             value: |  | ||||||
|                 The :see:LocalizedValue instance to serialize |  | ||||||
|                 into a data type that the database can understand. |  | ||||||
|  |  | ||||||
|         Returns: |  | ||||||
|             A dictionary containing a key for every language, |  | ||||||
|             extracted from the specified value. |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         # default to None if this is an unknown type |  | ||||||
|         if not isinstance(value, LocalizedValue) and value: |  | ||||||
|             value = None |  | ||||||
|  |  | ||||||
|         if value: |  | ||||||
|             cleaned_value = self.clean(value) |  | ||||||
|             self.validate(cleaned_value) |  | ||||||
|         else: |  | ||||||
|             cleaned_value = value |  | ||||||
|  |  | ||||||
|         return super(LocalizedField, self).get_prep_value( |  | ||||||
|             cleaned_value.__dict__ if cleaned_value else None |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     def clean(self, value, *_): |  | ||||||
|         """Cleans the specified value into something we |  | ||||||
|         can store in the database. |  | ||||||
|  |  | ||||||
|         For example, when all the language fields are |  | ||||||
|         left empty, and the field is allows to be null, |  | ||||||
|         we will store None instead of empty keys. |  | ||||||
|  |  | ||||||
|         Arguments: |  | ||||||
|             value: |  | ||||||
|                 The value to clean. |  | ||||||
|  |  | ||||||
|         Returns: |  | ||||||
|             The cleaned value, ready for database storage. |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         if not value or not isinstance(value, LocalizedValue): |  | ||||||
|             return None |  | ||||||
|  |  | ||||||
|         # are any of the language fiels None/empty? |  | ||||||
|         is_all_null = True |  | ||||||
|         for lang_code, _ in settings.LANGUAGES: |  | ||||||
|             if value.get(lang_code): |  | ||||||
|                 is_all_null = False |  | ||||||
|                 break |  | ||||||
|  |  | ||||||
|         # all fields have been left empty and we support |  | ||||||
|         # null values, let's return null to represent that |  | ||||||
|         if is_all_null and self.null: |  | ||||||
|             return None |  | ||||||
|  |  | ||||||
|         return value |  | ||||||
|  |  | ||||||
|     def validate(self, value: LocalizedValue, *_): |  | ||||||
|         """Validates that the value for the primary language |  | ||||||
|         has been filled in. |  | ||||||
|  |  | ||||||
|         Exceptions are raises in order to notify the user |  | ||||||
|         of invalid values. |  | ||||||
|  |  | ||||||
|         Arguments: |  | ||||||
|             value: |  | ||||||
|                 The value to validate. |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         if self.null: |  | ||||||
|             return |  | ||||||
|  |  | ||||||
|         primary_lang_val = getattr(value, settings.LANGUAGE_CODE) |  | ||||||
|  |  | ||||||
|         if not primary_lang_val: |  | ||||||
|             raise IntegrityError( |  | ||||||
|                 'null value in column "%s.%s" violates not-null constraint' % ( |  | ||||||
|                     self.name, |  | ||||||
|                     settings.LANGUAGE_CODE |  | ||||||
|                 ) |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|     def formfield(self, **kwargs): |  | ||||||
|         """Gets the form field associated with this field.""" |  | ||||||
|  |  | ||||||
|         defaults = { |  | ||||||
|             'form_class': LocalizedFieldForm |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         defaults.update(kwargs) |  | ||||||
|         return super().formfield(**defaults) |  | ||||||
|  |  | ||||||
|     def deconstruct(self): |  | ||||||
|         """Gets the values to pass to :see:__init__ when |  | ||||||
|         re-creating this object.""" |  | ||||||
|  |  | ||||||
|         name, path, args, kwargs = super( |  | ||||||
|             LocalizedField, self).deconstruct() |  | ||||||
|  |  | ||||||
|         if self.uniqueness: |  | ||||||
|             kwargs['uniqueness'] = self.uniqueness |  | ||||||
|  |  | ||||||
|         return name, path, args, kwargs |  | ||||||
							
								
								
									
										14
									
								
								localized_fields/fields/text_field.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								localized_fields/fields/text_field.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | from ..forms import LocalizedTextFieldForm | ||||||
|  | from .char_field import LocalizedCharField | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedTextField(LocalizedCharField): | ||||||
|  |     def formfield(self, **kwargs): | ||||||
|  |         """Gets the form field associated with this field.""" | ||||||
|  |  | ||||||
|  |         defaults = { | ||||||
|  |             'form_class': LocalizedTextFieldForm | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         defaults.update(kwargs) | ||||||
|  |         return super().formfield(**defaults) | ||||||
| @@ -1,13 +1,12 @@ | |||||||
| from datetime import datetime | from datetime import datetime | ||||||
| 
 | 
 | ||||||
| from django.conf import settings |  | ||||||
| from django.utils.text import slugify | from django.utils.text import slugify | ||||||
| from django.core.exceptions import ImproperlyConfigured | from django.core.exceptions import ImproperlyConfigured | ||||||
| 
 | 
 | ||||||
|  | from .autoslug_field import LocalizedAutoSlugField | ||||||
| from ..util import get_language_codes | from ..util import get_language_codes | ||||||
| from ..mixins import AtomicSlugRetryMixin | from ..mixins import AtomicSlugRetryMixin | ||||||
| from ..localized_value import LocalizedValue | from ..value import LocalizedValue | ||||||
| from .localized_autoslug_field import LocalizedAutoSlugField |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class LocalizedUniqueSlugField(LocalizedAutoSlugField): | class LocalizedUniqueSlugField(LocalizedAutoSlugField): | ||||||
| @@ -73,13 +72,7 @@ class LocalizedUniqueSlugField(LocalizedAutoSlugField): | |||||||
| 
 | 
 | ||||||
|         slugs = LocalizedValue() |         slugs = LocalizedValue() | ||||||
| 
 | 
 | ||||||
|         for lang_code, _ in settings.LANGUAGES: |         for lang_code, value in self._get_populate_values(instance): | ||||||
|             value = self._get_populate_from_value( |  | ||||||
|                 instance, |  | ||||||
|                 self.populate_from, |  | ||||||
|                 lang_code |  | ||||||
|             ) |  | ||||||
| 
 |  | ||||||
|             if not value: |             if not value: | ||||||
|                 continue |                 continue | ||||||
| 
 | 
 | ||||||
| @@ -98,11 +91,12 @@ class LocalizedUniqueSlugField(LocalizedAutoSlugField): | |||||||
|             if self.include_time: |             if self.include_time: | ||||||
|                 slug += '-%d' % datetime.now().microsecond |                 slug += '-%d' % datetime.now().microsecond | ||||||
| 
 | 
 | ||||||
|             if instance.retries > 0: |             retries = getattr(instance, 'retries', 0) | ||||||
|  |             if retries > 0: | ||||||
|                 # do not add another - if we already added time |                 # do not add another - if we already added time | ||||||
|                 if not self.include_time: |                 if not self.include_time: | ||||||
|                     slug += '-' |                     slug += '-' | ||||||
|                 slug += '%d' % instance.retries |                 slug += '%d' % retries | ||||||
| 
 | 
 | ||||||
|             slugs.set(lang_code, slug) |             slugs.set(lang_code, slug) | ||||||
| 
 | 
 | ||||||
| @@ -1,75 +1,49 @@ | |||||||
| from typing import List | from typing import List, Union | ||||||
|  |  | ||||||
| from django import forms | from django import forms | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.forms import MultiWidget | from django.core.exceptions import ValidationError | ||||||
|  | from django.forms.widgets import FILE_INPUT_CONTRADICTION | ||||||
|  |  | ||||||
| from .localized_value import LocalizedValue | from .value import LocalizedValue, LocalizedStringValue, \ | ||||||
|  |     LocalizedFileValue, LocalizedIntegerValue | ||||||
|  | from .widgets import LocalizedFieldWidget, LocalizedCharFieldWidget, \ | ||||||
| class LocalizedFieldWidget(MultiWidget): |     LocalizedFileWidget, AdminLocalizedIntegerFieldWidget | ||||||
|     """Widget that has an input box for every language.""" |  | ||||||
|  |  | ||||||
|     def __init__(self, *args, **kwargs): |  | ||||||
|         """Initializes a new instance of :see:LocalizedFieldWidget.""" |  | ||||||
|  |  | ||||||
|         widgets = [] |  | ||||||
|  |  | ||||||
|         for _ in settings.LANGUAGES: |  | ||||||
|             widgets.append(forms.Textarea()) |  | ||||||
|  |  | ||||||
|         super(LocalizedFieldWidget, self).__init__(widgets, *args, **kwargs) |  | ||||||
|  |  | ||||||
|     def decompress(self, value: LocalizedValue) -> List[str]: |  | ||||||
|         """Decompresses the specified value so |  | ||||||
|         it can be spread over the internal widgets. |  | ||||||
|  |  | ||||||
|         Arguments: |  | ||||||
|             value: |  | ||||||
|                 The :see:LocalizedValue to display in this |  | ||||||
|                 widget. |  | ||||||
|  |  | ||||||
|         Returns: |  | ||||||
|             All values to display in the inner widgets. |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         result = [] |  | ||||||
|  |  | ||||||
|         for lang_code, _ in settings.LANGUAGES: |  | ||||||
|             if value: |  | ||||||
|                 result.append(value.get(lang_code)) |  | ||||||
|             else: |  | ||||||
|                 result.append(None) |  | ||||||
|  |  | ||||||
|         return result |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class LocalizedFieldForm(forms.MultiValueField): | class LocalizedFieldForm(forms.MultiValueField): | ||||||
|     """Form for a localized field, allows editing |     """Form for a localized field, allows editing | ||||||
|     the field in multiple languages.""" |     the field in multiple languages.""" | ||||||
|  |  | ||||||
|     widget = LocalizedFieldWidget() |     widget = LocalizedFieldWidget | ||||||
|  |     field_class = forms.fields.CharField | ||||||
|  |     value_class = LocalizedValue | ||||||
|  |  | ||||||
|     def __init__(self, *args, **kwargs): |     def __init__(self, *args, required: Union[bool, List[str]]=False, **kwargs): | ||||||
|         """Initializes a new instance of :see:LocalizedFieldForm.""" |         """Initializes a new instance of :see:LocalizedFieldForm.""" | ||||||
|  |  | ||||||
|         fields = [] |         fields = [] | ||||||
|  |  | ||||||
|         for lang_code, _ in settings.LANGUAGES: |         for lang_code, _ in settings.LANGUAGES: | ||||||
|             field_options = {'required': False} |             field_options = dict( | ||||||
|  |                 required=required if type(required) is bool else (lang_code in | ||||||
|             if lang_code == settings.LANGUAGE_CODE: |                                                                   required), | ||||||
|                 field_options['required'] = True |                 label=lang_code | ||||||
|  |             ) | ||||||
|             field_options['label'] = lang_code |             fields.append(self.field_class(**field_options)) | ||||||
|             fields.append(forms.fields.CharField(**field_options)) |  | ||||||
|  |  | ||||||
|         super(LocalizedFieldForm, self).__init__( |         super(LocalizedFieldForm, self).__init__( | ||||||
|             fields, |             fields, | ||||||
|             require_all_fields=False |             required=required if type(required) is bool else True, | ||||||
|  |             require_all_fields=False, | ||||||
|  |             *args, **kwargs | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def compress(self, value: List[str]) -> LocalizedValue: |         # set 'required' attribute for each widget separately | ||||||
|  |         for field, widget in zip(self.fields, self.widget.widgets): | ||||||
|  |             widget.is_required = field.required | ||||||
|  |  | ||||||
|  |     def compress(self, value: List[str]) -> value_class: | ||||||
|         """Compresses the values from individual fields |         """Compresses the values from individual fields | ||||||
|         into a single :see:LocalizedValue instance. |         into a single :see:LocalizedValue instance. | ||||||
|  |  | ||||||
| @@ -82,9 +56,121 @@ class LocalizedFieldForm(forms.MultiValueField): | |||||||
|             the value in several languages. |             the value in several languages. | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         localized_value = LocalizedValue() |         localized_value = self.value_class() | ||||||
|  |  | ||||||
|         for (lang_code, _), value in zip(settings.LANGUAGES, value): |         for (lang_code, _), value in zip(settings.LANGUAGES, value): | ||||||
|             localized_value.set(lang_code, value) |             localized_value.set(lang_code, value) | ||||||
|  |  | ||||||
|         return localized_value |         return localized_value | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedCharFieldForm(LocalizedFieldForm): | ||||||
|  |     """Form for a localized char field, allows editing | ||||||
|  |     the field in multiple languages.""" | ||||||
|  |  | ||||||
|  |     widget = LocalizedCharFieldWidget | ||||||
|  |     value_class = LocalizedStringValue | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedTextFieldForm(LocalizedFieldForm): | ||||||
|  |     """Form for a localized text field, allows editing | ||||||
|  |     the field in multiple languages.""" | ||||||
|  |  | ||||||
|  |     value_class = LocalizedStringValue | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class 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.""" | ||||||
|  |  | ||||||
|  |     widget = LocalizedFileWidget | ||||||
|  |     field_class = forms.fields.FileField | ||||||
|  |     value_class = LocalizedFileValue | ||||||
|  |  | ||||||
|  |     def clean(self, value, initial=None): | ||||||
|  |         """ | ||||||
|  |         Most part of this method is a copy of | ||||||
|  |         django.forms.MultiValueField.clean, with the exception of initial | ||||||
|  |         value handling (this need for correct processing FileField's). | ||||||
|  |         All original comments saved. | ||||||
|  |         """ | ||||||
|  |         if initial is None: | ||||||
|  |             initial = [None for x in range(0, len(value))] | ||||||
|  |         else: | ||||||
|  |             if not isinstance(initial, list): | ||||||
|  |                 initial = self.widget.decompress(initial) | ||||||
|  |  | ||||||
|  |         clean_data = [] | ||||||
|  |         errors = [] | ||||||
|  |         if not value or isinstance(value, (list, tuple)): | ||||||
|  |             if (not value or not [v for v in value if | ||||||
|  |                                   v not in self.empty_values]) \ | ||||||
|  |                     and (not initial or not [v for v in initial if | ||||||
|  |                                              v not in self.empty_values]): | ||||||
|  |                 if self.required: | ||||||
|  |                     raise ValidationError(self.error_messages['required'], | ||||||
|  |                                           code='required') | ||||||
|  |         else: | ||||||
|  |             raise ValidationError(self.error_messages['invalid'], | ||||||
|  |                                   code='invalid') | ||||||
|  |         for i, field in enumerate(self.fields): | ||||||
|  |             try: | ||||||
|  |                 field_value = value[i] | ||||||
|  |             except IndexError: | ||||||
|  |                 field_value = None | ||||||
|  |             try: | ||||||
|  |                 field_initial = initial[i] | ||||||
|  |             except IndexError: | ||||||
|  |                 field_initial = None | ||||||
|  |  | ||||||
|  |             if field_value in self.empty_values and \ | ||||||
|  |                     field_initial in self.empty_values: | ||||||
|  |                 if self.require_all_fields: | ||||||
|  |                     # Raise a 'required' error if the MultiValueField is | ||||||
|  |                     # required and any field is empty. | ||||||
|  |                     if self.required: | ||||||
|  |                         raise ValidationError(self.error_messages['required'], | ||||||
|  |                                               code='required') | ||||||
|  |                 elif field.required: | ||||||
|  |                     # Otherwise, add an 'incomplete' error to the list of | ||||||
|  |                     # collected errors and skip field cleaning, if a required | ||||||
|  |                     # field is empty. | ||||||
|  |                     if field.error_messages['incomplete'] not in errors: | ||||||
|  |                         errors.append(field.error_messages['incomplete']) | ||||||
|  |                     continue | ||||||
|  |             try: | ||||||
|  |                 clean_data.append(field.clean(field_value, field_initial)) | ||||||
|  |             except ValidationError as e: | ||||||
|  |                 # Collect all validation errors in a single list, which we'll | ||||||
|  |                 # raise at the end of clean(), rather than raising a single | ||||||
|  |                 # exception for the first error we encounter. Skip duplicates. | ||||||
|  |                 errors.extend(m for m in e.error_list if m not in errors) | ||||||
|  |         if errors: | ||||||
|  |             raise ValidationError(errors) | ||||||
|  |  | ||||||
|  |         out = self.compress(clean_data) | ||||||
|  |         self.validate(out) | ||||||
|  |         self.run_validators(out) | ||||||
|  |         return out | ||||||
|  |  | ||||||
|  |     def bound_data(self, data, initial): | ||||||
|  |         bound_data = [] | ||||||
|  |         if initial is None: | ||||||
|  |             initial = [None for x in range(0, len(data))] | ||||||
|  |         else: | ||||||
|  |             if not isinstance(initial, list): | ||||||
|  |                 initial = self.widget.decompress(initial) | ||||||
|  |         for d, i in zip(data, initial): | ||||||
|  |             if d in (None, FILE_INPUT_CONTRADICTION): | ||||||
|  |                 bound_data.append(i) | ||||||
|  |             else: | ||||||
|  |                 bound_data.append(d) | ||||||
|  |         return bound_data | ||||||
|   | |||||||
| @@ -1,132 +0,0 @@ | |||||||
| """This module is unused, but should be contributed to Django.""" |  | ||||||
|  |  | ||||||
| from typing import List |  | ||||||
|  |  | ||||||
| from django.db import models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class HStoreIndex(models.Index): |  | ||||||
|     """Allows creating a index on a specific HStore index. |  | ||||||
|  |  | ||||||
|     Note: pieces of code in this class have been copied |  | ||||||
|     from the base class. There was no way around this.""" |  | ||||||
|  |  | ||||||
|     def __init__(self, field: str, keys: List[str], unique: bool=False, |  | ||||||
|                  name: str=''): |  | ||||||
|         """Initializes a new instance of :see:HStoreIndex. |  | ||||||
|  |  | ||||||
|         Arguments: |  | ||||||
|             field: |  | ||||||
|                 Name of the hstore field for |  | ||||||
|                 which's keys to create a index for. |  | ||||||
|  |  | ||||||
|             keys: |  | ||||||
|                 The name of the hstore keys to |  | ||||||
|                 create the index on. |  | ||||||
|  |  | ||||||
|             unique: |  | ||||||
|                 Whether this index should |  | ||||||
|                 be marked as UNIQUE. |  | ||||||
|  |  | ||||||
|             name: |  | ||||||
|                 The name of the index. If left |  | ||||||
|                 empty, one will be generated. |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         self.field = field |  | ||||||
|         self.keys = keys |  | ||||||
|         self.unique = unique |  | ||||||
|  |  | ||||||
|         # this will eventually set self.name |  | ||||||
|         super(HStoreIndex, self).__init__( |  | ||||||
|             fields=[field], |  | ||||||
|             name=name |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     def get_sql_create_template_values(self, model, schema_editor, using): |  | ||||||
|         """Gets the values for the SQL template. |  | ||||||
|  |  | ||||||
|         Arguments: |  | ||||||
|             model: |  | ||||||
|                 The model this index applies to. |  | ||||||
|  |  | ||||||
|             schema_editor: |  | ||||||
|                 The schema editor to modify the schema. |  | ||||||
|  |  | ||||||
|             using: |  | ||||||
|                 Optional: "USING" statement. |  | ||||||
|  |  | ||||||
|         Returns: |  | ||||||
|             Dictionary of keys to pass into the SQL template. |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         fields = [model._meta.get_field(field_name) for field_name, order in self.fields_orders] |  | ||||||
|         tablespace_sql = schema_editor._get_index_tablespace_sql(model, fields) |  | ||||||
|         quote_name = schema_editor.quote_name |  | ||||||
|  |  | ||||||
|         columns = [ |  | ||||||
|             '(%s->\'%s\')' % (self.field, key) |  | ||||||
|             for key in self.keys |  | ||||||
|         ] |  | ||||||
|  |  | ||||||
|         return { |  | ||||||
|             'table': quote_name(model._meta.db_table), |  | ||||||
|             'name': quote_name(self.name), |  | ||||||
|             'columns': ', '.join(columns), |  | ||||||
|             'using': using, |  | ||||||
|             'extra': tablespace_sql, |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|     def create_sql(self, model, schema_editor, using=''): |  | ||||||
|         """Gets the SQL to execute when creating the index. |  | ||||||
|  |  | ||||||
|         Arguments: |  | ||||||
|             model: |  | ||||||
|                 The model this index applies to. |  | ||||||
|  |  | ||||||
|             schema_editor: |  | ||||||
|                 The schema editor to modify the schema. |  | ||||||
|  |  | ||||||
|             using: |  | ||||||
|                 Optional: "USING" statement. |  | ||||||
|  |  | ||||||
|         Returns: |  | ||||||
|             SQL string to execute to create this index. |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         sql_create_index = schema_editor.sql_create_index |  | ||||||
|         if self.unique: |  | ||||||
|             sql_create_index = sql_create_index.replace('CREATE', 'CREATE UNIQUE') |  | ||||||
|         sql_parameters = self.get_sql_create_template_values(model, schema_editor, using) |  | ||||||
|         return sql_create_index % sql_parameters |  | ||||||
|  |  | ||||||
|     def remove_sql(self, model, schema_editor): |  | ||||||
|         """Gets the SQL to execute to remove this index. |  | ||||||
|  |  | ||||||
|         Arguments: |  | ||||||
|             model: |  | ||||||
|                 The model this index applies to. |  | ||||||
|  |  | ||||||
|             schema_editor: |  | ||||||
|                 The schema editor to modify the schema. |  | ||||||
|  |  | ||||||
|         Returns: |  | ||||||
|             SQL string to execute to remove this index. |  | ||||||
|         """ |  | ||||||
|         quote_name = schema_editor.quote_name |  | ||||||
|         return schema_editor.sql_delete_index % { |  | ||||||
|             'table': quote_name(model._meta.db_table), |  | ||||||
|             'name': quote_name(self.name), |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|     def deconstruct(self): |  | ||||||
|         """Gets the values to pass to :see:__init__ when |  | ||||||
|         re-creating this object.""" |  | ||||||
|  |  | ||||||
|         path = '%s.%s' % (self.__class__.__module__, self.__class__.__name__) |  | ||||||
|         return (path, (), { |  | ||||||
|             'field': self.field, |  | ||||||
|             'keys': self.keys, |  | ||||||
|             'unique': self.unique, |  | ||||||
|             'name': self.name |  | ||||||
|         }) |  | ||||||
| @@ -1,97 +0,0 @@ | |||||||
| from django.conf import settings |  | ||||||
| from django.utils import translation |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class LocalizedValue: |  | ||||||
|     """Represents the value of a :see:LocalizedField.""" |  | ||||||
|  |  | ||||||
|     def __init__(self, keys: dict=None): |  | ||||||
|         """Initializes a new instance of :see:LocalizedValue. |  | ||||||
|  |  | ||||||
|         Arguments: |  | ||||||
|             keys: |  | ||||||
|                 The keys to initialize this value with. Every |  | ||||||
|                 key contains the value of this field in a |  | ||||||
|                 different language. |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         if isinstance(keys, str): |  | ||||||
|             setattr(self, settings.LANGUAGE_CODE, keys) |  | ||||||
|         else: |  | ||||||
|             for lang_code, _ in settings.LANGUAGES: |  | ||||||
|                 value = keys.get(lang_code) if keys else None |  | ||||||
|                 setattr(self, lang_code, value) |  | ||||||
|  |  | ||||||
|     def get(self, language: str=None) -> str: |  | ||||||
|         """Gets the underlying value in the specified or |  | ||||||
|         primary language. |  | ||||||
|  |  | ||||||
|         Arguments: |  | ||||||
|             language: |  | ||||||
|                 The language to get the value in. |  | ||||||
|  |  | ||||||
|         Returns: |  | ||||||
|             The value in the current language, or |  | ||||||
|             the primary language in case no language |  | ||||||
|             was specified. |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         language = language or settings.LANGUAGE_CODE |  | ||||||
|         return getattr(self, language, None) |  | ||||||
|  |  | ||||||
|     def set(self, language: str, value: str): |  | ||||||
|         """Sets the value in the specified language. |  | ||||||
|  |  | ||||||
|         Arguments: |  | ||||||
|             language: |  | ||||||
|                 The language to set the value in. |  | ||||||
|  |  | ||||||
|             value: |  | ||||||
|                 The value to set. |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         setattr(self, language, value) |  | ||||||
|         return self |  | ||||||
|  |  | ||||||
|     def deconstruct(self) -> dict: |  | ||||||
|         """Deconstructs this value into a primitive type. |  | ||||||
|  |  | ||||||
|         Returns: |  | ||||||
|             A dictionary with all the localized values |  | ||||||
|             contained in this instance. |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         path = 'localized_fields.fields.LocalizedValue' |  | ||||||
|         return path, [self.__dict__], {} |  | ||||||
|  |  | ||||||
|     def __str__(self) -> str: |  | ||||||
|         """Gets the value in the current language, or falls |  | ||||||
|         back to the primary language if there's no value |  | ||||||
|         in the current language.""" |  | ||||||
|  |  | ||||||
|         value = self.get(translation.get_language()) |  | ||||||
|  |  | ||||||
|         if not value: |  | ||||||
|             value = self.get(settings.LANGUAGE_CODE) |  | ||||||
|  |  | ||||||
|         return value or '' |  | ||||||
|  |  | ||||||
|     def __eq__(self, other): |  | ||||||
|         """Compares :paramref:self to :paramref:other for |  | ||||||
|         equality. |  | ||||||
|  |  | ||||||
|         Returns: |  | ||||||
|             True when :paramref:self is equal to :paramref:other. |  | ||||||
|             And False when they are not. |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         for lang_code, _ in settings.LANGUAGES: |  | ||||||
|             if self.get(lang_code) != other.get(lang_code): |  | ||||||
|                 return False |  | ||||||
|  |  | ||||||
|         return True |  | ||||||
|  |  | ||||||
|     def __repr__(self):  # pragma: no cover |  | ||||||
|         """Gets a textual representation of this object.""" |  | ||||||
|  |  | ||||||
|         return 'LocalizedValue<%s> 0x%s' % (self.__dict__, id(self)) |  | ||||||
							
								
								
									
										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,36 +1,16 @@ | |||||||
| from django.db import models, transaction | from psqlextra.models import PostgresModel | ||||||
| from django.db.utils import IntegrityError |  | ||||||
| from django.conf import settings |  | ||||||
|  |  | ||||||
| from .fields import LocalizedField | from .mixins import AtomicSlugRetryMixin | ||||||
| from .localized_value import LocalizedValue |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class LocalizedModel(models.Model): | class LocalizedModel(AtomicSlugRetryMixin, PostgresModel): | ||||||
|     """A model that contains localized fields.""" |     """Turns a model into a model that contains LocalizedField's. | ||||||
|  |  | ||||||
|  |     For basic localisation functionality, it isn't needed to inherit | ||||||
|  |     from LocalizedModel. However, for certain features, this is required. | ||||||
|  |  | ||||||
|  |     It is definitely needed for :see:LocalizedUniqueSlugField, unless you | ||||||
|  |     manually inherit from AtomicSlugRetryMixin.""" | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|         abstract = True |         abstract = True | ||||||
|  |  | ||||||
|     def __init__(self, *args, **kwargs): |  | ||||||
|         """Initializes a new instance of :see:LocalizedModel. |  | ||||||
|  |  | ||||||
|         Here we set all the fields that are of :see:LocalizedField |  | ||||||
|         to an instance of :see:LocalizedValue in case they are none |  | ||||||
|         so that the user doesn't explicitely have to do so.""" |  | ||||||
|  |  | ||||||
|         super(LocalizedModel, self).__init__(*args, **kwargs) |  | ||||||
|  |  | ||||||
|         for field in self._meta.get_fields(): |  | ||||||
|             if not isinstance(field, LocalizedField): |  | ||||||
|                 continue |  | ||||||
|  |  | ||||||
|             value = getattr(self, field.name, None) |  | ||||||
|  |  | ||||||
|             if not isinstance(value, LocalizedValue): |  | ||||||
|                 if isinstance(value, dict): |  | ||||||
|                     value = LocalizedValue(value) |  | ||||||
|                 else: |  | ||||||
|                     value = LocalizedValue() |  | ||||||
|  |  | ||||||
|             setattr(self, field.name, value) |  | ||||||
|   | |||||||
| @@ -0,0 +1,52 @@ | |||||||
|  | .localized-fields-widget { | ||||||
|  |     display: inline-block; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .localized-fields-widget.tabs { | ||||||
|  |     display: block; | ||||||
|  |     margin: 0; | ||||||
|  |     border-bottom: 1px solid #eee; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .localized-fields-widget.tabs .localized-fields-widget.tab { | ||||||
|  |     display: inline-block; | ||||||
|  |     margin-left: 5px; | ||||||
|  |     border: 1px solid #79aec8; | ||||||
|  |     border-bottom: none; | ||||||
|  |     border-radius: 4px; | ||||||
|  |     border-bottom-left-radius: 0; | ||||||
|  |     border-bottom-right-radius: 0; | ||||||
|  |     background: #79aec8; | ||||||
|  |     color: #fff; | ||||||
|  |     font-weight: 400; | ||||||
|  |     opacity: 0.5; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .localized-fields-widget.tabs .localized-fields-widget.tab:first-child { | ||||||
|  |     margin-left: 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .localized-fields-widget.tabs .localized-fields-widget.tab:hover { | ||||||
|  |     background: #417690; | ||||||
|  |     border-color: #417690; | ||||||
|  |     opacity: 1; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .localized-fields-widget.tabs .localized-fields-widget.tab label { | ||||||
|  |     padding: 5px 10px; | ||||||
|  |     display: inline-block; | ||||||
|  |     text-decoration: none; | ||||||
|  |     color: #fff; | ||||||
|  |     width: initial; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .localized-fields-widget.tabs .localized-fields-widget.tab.active, | ||||||
|  | .localized-fields-widget.tabs .localized-fields-widget.tab.active:hover { | ||||||
|  |     background: #79aec8; | ||||||
|  |     border-color: #79aec8; | ||||||
|  |     opacity: 1; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .localized-fields-widget p.file-upload { | ||||||
|  |     margin-left: 0; | ||||||
|  | } | ||||||
| @@ -0,0 +1,35 @@ | |||||||
|  | (function($) { | ||||||
|  |     var syncTabs = function(lang) { | ||||||
|  |         $('.localized-fields-widget.tab label:contains("'+lang+'")').each(function(){ | ||||||
|  |             $(this).parents('.localized-fields-widget[role="tabs"]').find('.localized-fields-widget.tab').removeClass('active'); | ||||||
|  |             $(this).parents('.localized-fields-widget.tab').addClass('active'); | ||||||
|  |             $(this).parents('.localized-fields-widget[role="tabs"]').children('.localized-fields-widget [role="tabpanel"]').hide(); | ||||||
|  |             $('#'+$(this).attr('for')).show(); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     $(function (){ | ||||||
|  |         $('.localized-fields-widget [role="tabpanel"]').hide(); | ||||||
|  |         // set first tab as active | ||||||
|  |         $('.localized-fields-widget[role="tabs"]').each(function () { | ||||||
|  |             $(this).find('.localized-fields-widget.tab:first').addClass('active'); | ||||||
|  |             $('#'+$(this).find('.localized-fields-widget.tab:first label').attr('for')).show(); | ||||||
|  |         }); | ||||||
|  |         // try set active last selected tab | ||||||
|  |         if (window.sessionStorage) { | ||||||
|  |             var lang = window.sessionStorage.getItem('localized-field-lang'); | ||||||
|  |             if (lang) { | ||||||
|  |                 syncTabs(lang); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $('.localized-fields-widget.tab label').click(function(event) { | ||||||
|  |             event.preventDefault(); | ||||||
|  |             syncTabs(this.innerText); | ||||||
|  |             if (window.sessionStorage) { | ||||||
|  |                 window.sessionStorage.setItem('localized-field-lang', this.innerText); | ||||||
|  |             } | ||||||
|  |             return false; | ||||||
|  |         }); | ||||||
|  |     }); | ||||||
|  | })(django.jQuery) | ||||||
| @@ -0,0 +1,16 @@ | |||||||
|  | {% with widget_id=widget.attrs.id %} | ||||||
|  | <div class="localized-fields-widget" role="tabs" data-synctabs="translation"> | ||||||
|  |     <ul class="localized-fields-widget tabs"> | ||||||
|  |     {% for widget in widget.subwidgets %} | ||||||
|  |         <li class="localized-fields-widget tab"> | ||||||
|  |             <label for="{{ widget_id }}_{{ widget.lang_code }}">{{ widget.lang_name|capfirst }}</label> | ||||||
|  |         </li> | ||||||
|  |     {% endfor %} | ||||||
|  |     </ul> | ||||||
|  | {% for widget in widget.subwidgets %} | ||||||
|  |     <div role="tabpanel" id="{{ widget_id }}_{{ widget.lang_code }}"> | ||||||
|  |         {% include widget.template_name %} | ||||||
|  |     </div> | ||||||
|  | {% endfor %} | ||||||
|  | </div> | ||||||
|  | {% endwith %} | ||||||
							
								
								
									
										16
									
								
								localized_fields/templates/localized_fields/multiwidget.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								localized_fields/templates/localized_fields/multiwidget.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | {% with widget_id=widget.attrs.id %} | ||||||
|  | <div class="localized-fields-widget" role="tabs" data-synctabs="translation"> | ||||||
|  |     <ul class="localized-fields-widget tabs"> | ||||||
|  |     {% for widget in widget.subwidgets %} | ||||||
|  |         <li class="localized-fields-widget tab"> | ||||||
|  |             <label for="{{ widget_id }}_{{ widget.lang_code }}">{{ widget.lang_name|capfirst }}</label> | ||||||
|  |         </li> | ||||||
|  |     {% endfor %} | ||||||
|  |     </ul> | ||||||
|  |     {% for widget in widget.subwidgets %} | ||||||
|  |         <div role="tabpanel" id="{{ widget_id }}_{{ widget.lang_code }}"> | ||||||
|  |             {% include widget.template_name %} | ||||||
|  |         </div> | ||||||
|  |     {% endfor %} | ||||||
|  | </div> | ||||||
|  | {% endwith %} | ||||||
| @@ -19,3 +19,26 @@ def get_language_codes() -> List[str]: | |||||||
|         lang_code |         lang_code | ||||||
|         for lang_code, _ in settings.LANGUAGES |         for lang_code, _ in settings.LANGUAGES | ||||||
|     ] |     ] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def resolve_object_property(obj, path: str): | ||||||
|  |     """Resolves the value of a property on an object. | ||||||
|  |  | ||||||
|  |     Is able to resolve nested properties. For example, | ||||||
|  |     a path can be specified: | ||||||
|  |  | ||||||
|  |         'other.beer.name' | ||||||
|  |  | ||||||
|  |     Raises: | ||||||
|  |         AttributeError: | ||||||
|  |             In case the property could not be resolved. | ||||||
|  |  | ||||||
|  |     Returns: | ||||||
|  |         The value of the specified property. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     value = obj | ||||||
|  |     for path_part in path.split('.'): | ||||||
|  |         value = getattr(value, path_part) | ||||||
|  |  | ||||||
|  |     return value | ||||||
|   | |||||||
							
								
								
									
										236
									
								
								localized_fields/value.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										236
									
								
								localized_fields/value.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,236 @@ | |||||||
|  | import deprecation | ||||||
|  | import collections | ||||||
|  |  | ||||||
|  | from typing import Optional | ||||||
|  | from django.conf import settings | ||||||
|  | from django.utils import translation | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedValue(dict): | ||||||
|  |     """Represents the value of a :see:LocalizedField.""" | ||||||
|  |     default_value = None | ||||||
|  |  | ||||||
|  |     def __init__(self, keys: dict=None): | ||||||
|  |         """Initializes a new instance of :see:LocalizedValue. | ||||||
|  |  | ||||||
|  |         Arguments: | ||||||
|  |             keys: | ||||||
|  |                 The keys to initialize this value with. Every | ||||||
|  |                 key contains the value of this field in a | ||||||
|  |                 different language. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         super().__init__({}) | ||||||
|  |         self._interpret_value(keys) | ||||||
|  |  | ||||||
|  |     def get(self, language: str=None, default: str=None) -> str: | ||||||
|  |         """Gets the underlying value in the specified or | ||||||
|  |         primary language. | ||||||
|  |  | ||||||
|  |         Arguments: | ||||||
|  |             language: | ||||||
|  |                 The language to get the value in. | ||||||
|  |  | ||||||
|  |         Returns: | ||||||
|  |             The value in the current language, or | ||||||
|  |             the primary language in case no language | ||||||
|  |             was specified. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         language = language or settings.LANGUAGE_CODE | ||||||
|  |         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. | ||||||
|  |  | ||||||
|  |         Arguments: | ||||||
|  |             language: | ||||||
|  |                 The language to set the value in. | ||||||
|  |  | ||||||
|  |             value: | ||||||
|  |                 The value to set. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         self[language] = value | ||||||
|  |         self.__dict__.update(self) | ||||||
|  |         return self | ||||||
|  |  | ||||||
|  |     def deconstruct(self) -> dict: | ||||||
|  |         """Deconstructs this value into a primitive type. | ||||||
|  |  | ||||||
|  |         Returns: | ||||||
|  |             A dictionary with all the localized values | ||||||
|  |             contained in this instance. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         path = 'localized_fields.value.%s' % self.__class__.__name__ | ||||||
|  |         return path, [self.__dict__], {} | ||||||
|  |  | ||||||
|  |     def _interpret_value(self, value): | ||||||
|  |         """Interprets a value passed in the constructor as | ||||||
|  |         a :see:LocalizedValue. | ||||||
|  |  | ||||||
|  |         If string: | ||||||
|  |             Assumes it's the default language. | ||||||
|  |  | ||||||
|  |         If dict: | ||||||
|  |             Each key is a language and the value a string | ||||||
|  |             in that language. | ||||||
|  |  | ||||||
|  |         If list: | ||||||
|  |             Recurse into to apply rules above. | ||||||
|  |  | ||||||
|  |         Arguments: | ||||||
|  |             value: | ||||||
|  |                 The value to interpret. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         for lang_code, _ in settings.LANGUAGES: | ||||||
|  |             self.set(lang_code, self.default_value) | ||||||
|  |  | ||||||
|  |         if isinstance(value, str): | ||||||
|  |             self.set(settings.LANGUAGE_CODE, value) | ||||||
|  |  | ||||||
|  |         elif isinstance(value, dict): | ||||||
|  |             for lang_code, _ in settings.LANGUAGES: | ||||||
|  |                 lang_value = value.get(lang_code, self.default_value) | ||||||
|  |                 self.set(lang_code, lang_value) | ||||||
|  |  | ||||||
|  |         elif isinstance(value, collections.Iterable): | ||||||
|  |             for val in value: | ||||||
|  |                 self._interpret_value(val) | ||||||
|  |  | ||||||
|  |     def translate(self) -> Optional[str]: | ||||||
|  |         """Gets the value in the current language or falls | ||||||
|  |         back to the next language if there's no value in the | ||||||
|  |         current language.""" | ||||||
|  |  | ||||||
|  |         fallbacks = getattr(settings, 'LOCALIZED_FIELDS_FALLBACKS', {}) | ||||||
|  |  | ||||||
|  |         language = translation.get_language() or settings.LANGUAGE_CODE | ||||||
|  |         languages = fallbacks.get(language, [settings.LANGUAGE_CODE])[:] | ||||||
|  |         languages.insert(0, language) | ||||||
|  |  | ||||||
|  |         for lang_code in languages: | ||||||
|  |             value = self.get(lang_code) | ||||||
|  |             if value: | ||||||
|  |                 return value or None | ||||||
|  |  | ||||||
|  |         return None | ||||||
|  |  | ||||||
|  |     def __str__(self) -> str: | ||||||
|  |         """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 | ||||||
|  |         equality. | ||||||
|  |  | ||||||
|  |         Returns: | ||||||
|  |             True when :paramref:self is equal to :paramref:other. | ||||||
|  |             And False when they are not. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         if not isinstance(other, type(self)): | ||||||
|  |             if isinstance(other, str): | ||||||
|  |                 return self.__str__() == other | ||||||
|  |             return False | ||||||
|  |  | ||||||
|  |         for lang_code, _ in settings.LANGUAGES: | ||||||
|  |             if self.get(lang_code) != other.get(lang_code): | ||||||
|  |                 return False | ||||||
|  |  | ||||||
|  |         return True | ||||||
|  |  | ||||||
|  |     def __ne__(self, other): | ||||||
|  |         """Compares :paramref:self to :paramerf:other for | ||||||
|  |         in-equality. | ||||||
|  |  | ||||||
|  |         Returns: | ||||||
|  |             True when :paramref:self is not equal to :paramref:other. | ||||||
|  |             And False when they are. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         return not self.__eq__(other) | ||||||
|  |  | ||||||
|  |     def __setattr__(self, language: str, value: str): | ||||||
|  |         """Sets the value for a language with the specified name. | ||||||
|  |  | ||||||
|  |         Arguments: | ||||||
|  |             language: | ||||||
|  |                 The language to set the value in. | ||||||
|  |  | ||||||
|  |             value: | ||||||
|  |                 The value to set. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         self.set(language, value) | ||||||
|  |  | ||||||
|  |     def __repr__(self):  # pragma: no cover | ||||||
|  |         """Gets a textual representation of this object.""" | ||||||
|  |  | ||||||
|  |         return '%s<%s> 0x%s' % (self.__class__.__name__, | ||||||
|  |                                 self.__dict__, id(self)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedStringValue(LocalizedValue): | ||||||
|  |     default_value = '' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedFileValue(LocalizedValue): | ||||||
|  |     def __getattr__(self, name: str): | ||||||
|  |         """Proxies access to attributes to attributes of LocalizedFile""" | ||||||
|  |  | ||||||
|  |         value = self.get(translation.get_language()) | ||||||
|  |         if hasattr(value, name): | ||||||
|  |             return getattr(value, name) | ||||||
|  |         raise AttributeError("'{}' object has no attribute '{}'". | ||||||
|  |                              format(self.__class__.__name__, name)) | ||||||
|  |  | ||||||
|  |     def __str__(self) -> str: | ||||||
|  |         """Returns string representation of value""" | ||||||
|  |  | ||||||
|  |         return str(super().__str__()) | ||||||
|  |  | ||||||
|  |     @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 '' | ||||||
							
								
								
									
										126
									
								
								localized_fields/widgets.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								localized_fields/widgets.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,126 @@ | |||||||
|  | import copy | ||||||
|  |  | ||||||
|  | from typing import List | ||||||
|  |  | ||||||
|  | from django.conf import settings | ||||||
|  | from django import forms | ||||||
|  | from django.contrib.admin import widgets | ||||||
|  |  | ||||||
|  | from .value import LocalizedValue | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedFieldWidget(forms.MultiWidget): | ||||||
|  |     """Widget that has an input box for every language.""" | ||||||
|  |     template_name = 'localized_fields/multiwidget.html' | ||||||
|  |     widget = forms.Textarea | ||||||
|  |  | ||||||
|  |     def __init__(self, *args, **kwargs): | ||||||
|  |         """Initializes a new instance of :see:LocalizedFieldWidget.""" | ||||||
|  |  | ||||||
|  |         initial_widgets = [ | ||||||
|  |             copy.copy(self.widget) | ||||||
|  |             for _ in settings.LANGUAGES | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |         super().__init__(initial_widgets, *args, **kwargs) | ||||||
|  |  | ||||||
|  |         for ((lang_code, lang_name), widget) in zip(settings.LANGUAGES, self.widgets): | ||||||
|  |             widget.attrs['lang'] = lang_code | ||||||
|  |             widget.lang_code = lang_code | ||||||
|  |             widget.lang_name = lang_name | ||||||
|  |  | ||||||
|  |     def decompress(self, value: LocalizedValue) -> List[str]: | ||||||
|  |         """Decompresses the specified value so | ||||||
|  |         it can be spread over the internal widgets. | ||||||
|  |  | ||||||
|  |         Arguments: | ||||||
|  |             value: | ||||||
|  |                 The :see:LocalizedValue to display in this | ||||||
|  |                 widget. | ||||||
|  |  | ||||||
|  |         Returns: | ||||||
|  |             All values to display in the inner widgets. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         result = [] | ||||||
|  |         for lang_code, _ in settings.LANGUAGES: | ||||||
|  |             if value: | ||||||
|  |                 result.append(value.get(lang_code)) | ||||||
|  |             else: | ||||||
|  |                 result.append(None) | ||||||
|  |  | ||||||
|  |         return result | ||||||
|  |  | ||||||
|  |     def get_context(self, name, value, attrs): | ||||||
|  |         context = super(forms.MultiWidget, self).get_context(name, value, attrs) | ||||||
|  |         if self.is_localized: | ||||||
|  |             for widget in self.widgets: | ||||||
|  |                 widget.is_localized = self.is_localized | ||||||
|  |         # value is a list of values, each corresponding to a widget | ||||||
|  |         # in self.widgets. | ||||||
|  |         if not isinstance(value, list): | ||||||
|  |             value = self.decompress(value) | ||||||
|  |  | ||||||
|  |         final_attrs = context['widget']['attrs'] | ||||||
|  |         input_type = final_attrs.pop('type', None) | ||||||
|  |         id_ = final_attrs.get('id') | ||||||
|  |         subwidgets = [] | ||||||
|  |         for i, widget in enumerate(self.widgets): | ||||||
|  |             if input_type is not None: | ||||||
|  |                 widget.input_type = input_type | ||||||
|  |             widget_name = '%s_%s' % (name, i) | ||||||
|  |             try: | ||||||
|  |                 widget_value = value[i] | ||||||
|  |             except IndexError: | ||||||
|  |                 widget_value = None | ||||||
|  |             if id_: | ||||||
|  |                 widget_attrs = final_attrs.copy() | ||||||
|  |                 widget_attrs['id'] = '%s_%s' % (id_, i) | ||||||
|  |             else: | ||||||
|  |                 widget_attrs = final_attrs | ||||||
|  |             widget_attrs = self.build_widget_attrs(widget, widget_value, widget_attrs) | ||||||
|  |             widget_context = widget.get_context(widget_name, widget_value, widget_attrs)['widget'] | ||||||
|  |             widget_context.update(dict( | ||||||
|  |                 lang_code=widget.lang_code, | ||||||
|  |                 lang_name=widget.lang_name | ||||||
|  |             )) | ||||||
|  |             subwidgets.append(widget_context) | ||||||
|  |         context['widget']['subwidgets'] = subwidgets | ||||||
|  |         return context | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def build_widget_attrs(widget, value, attrs): | ||||||
|  |         attrs = dict(attrs)  # Copy attrs to avoid modifying the argument. | ||||||
|  |  | ||||||
|  |         if (not widget.use_required_attribute(value) or not widget.is_required) \ | ||||||
|  |                 and 'required' in attrs: | ||||||
|  |             del attrs['required'] | ||||||
|  |  | ||||||
|  |         return attrs | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedCharFieldWidget(LocalizedFieldWidget): | ||||||
|  |     """Widget that has an input box for every language.""" | ||||||
|  |     widget = forms.TextInput | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedFileWidget(LocalizedFieldWidget): | ||||||
|  |     """Widget that has an file input box for every language.""" | ||||||
|  |     widget = forms.ClearableFileInput | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AdminLocalizedFieldWidget(LocalizedFieldWidget): | ||||||
|  |     template_name = 'localized_fields/admin/widget.html' | ||||||
|  |     widget = widgets.AdminTextareaWidget | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AdminLocalizedCharFieldWidget(AdminLocalizedFieldWidget): | ||||||
|  |     widget = widgets.AdminTextInputWidget | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AdminLocalizedFileFieldWidget(AdminLocalizedFieldWidget): | ||||||
|  |     widget = widgets.AdminFileWidget | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AdminLocalizedIntegerFieldWidget(AdminLocalizedFieldWidget): | ||||||
|  |     widget = widgets.AdminIntegerFieldWidget | ||||||
| @@ -1 +1,2 @@ | |||||||
| django-postgres-extra==1.2 | django-postgres-extra==1.21a9 | ||||||
|  | deprecation==2.0.3 | ||||||
|   | |||||||
| @@ -1,17 +1,10 @@ | |||||||
| -r base.txt | -r base.txt | ||||||
|  |  | ||||||
| coverage==4.2 |  | ||||||
| Django==1.10.2 |  | ||||||
| django-autoslug==1.9.3 | django-autoslug==1.9.3 | ||||||
| django-bleach==0.3.0 | django-bleach==0.3.0 | ||||||
| django-coverage-plugin==1.3.1 | psycopg2==2.7.3.2 | ||||||
| psycopg2==2.6.2 | coverage==4.4.2 | ||||||
| pylint==1.6.4 | flake8==3.6.0 | ||||||
| pylint-common==0.2.2 | pep8==1.7.1 | ||||||
| pylint-django==0.7.2 | dj-database-url==0.4.2 | ||||||
| pylint-plugin-utils==0.2.4 | tox==2.9.1 | ||||||
| coverage==4.2 |  | ||||||
| django-coverage-plugin==1.3.1 |  | ||||||
| flake8==3.0.4 |  | ||||||
| pep8==1.7.0 |  | ||||||
| dj-database-url==0.4.1 |  | ||||||
|   | |||||||
| @@ -21,9 +21,15 @@ LANGUAGES = ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| INSTALLED_APPS = ( | INSTALLED_APPS = ( | ||||||
|  |     'django.contrib.auth', | ||||||
|  |     'django.contrib.contenttypes', | ||||||
|  |     'django.contrib.admin', | ||||||
|  |     'localized_fields', | ||||||
|     'tests', |     'tests', | ||||||
| ) | ) | ||||||
|  |  | ||||||
| # set to a lower number than the default, since | # set to a lower number than the default, since | ||||||
| # we want the tests to be fast, default is 100 | # we want the tests to be fast, default is 100 | ||||||
| LOCALIZED_FIELDS_MAX_RETRIES = 3 | LOCALIZED_FIELDS_MAX_RETRIES = 3 | ||||||
|  |  | ||||||
|  | LOCALIZED_FIELDS_EXPERIMENTAL = False | ||||||
|   | |||||||
| @@ -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] | ||||||
|   | |||||||
							
								
								
									
										44
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										44
									
								
								setup.py
									
									
									
									
									
								
							| @@ -1,14 +1,40 @@ | |||||||
| import os | import os | ||||||
|  | import distutils.cmd | ||||||
|  | import subprocess | ||||||
|  |  | ||||||
| from setuptools import find_packages, setup | from setuptools import find_packages, setup | ||||||
|  |  | ||||||
| with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme: |  | ||||||
|  | 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() |     README = readme.read() | ||||||
|  |  | ||||||
| setup( | setup( | ||||||
|     name='django-localized-fields', |     name='django-localized-fields', | ||||||
|     version='3.1', |     version='5.1', | ||||||
|     packages=find_packages(), |     packages=find_packages(exclude=['tests']), | ||||||
|     include_package_data=True, |     include_package_data=True, | ||||||
|     license='MIT License', |     license='MIT License', | ||||||
|     description='Implementation of localized model fields using PostgreSQL HStore fields.', |     description='Implementation of localized model fields using PostgreSQL HStore fields.', | ||||||
| @@ -18,7 +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.2' |         'django-postgres-extra>=1.22', | ||||||
|  |         'Django>=1.11', | ||||||
|  |         'deprecation==2.0.3' | ||||||
|     ], |     ], | ||||||
|     classifiers=[ |     classifiers=[ | ||||||
|         'Environment :: Web Environment', |         'Environment :: Web Environment', | ||||||
| @@ -30,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']], | ||||||
|  |         ), | ||||||
|  |     }, | ||||||
| ) | ) | ||||||
|   | |||||||
							
								
								
									
										13
									
								
								tests/data.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								tests/data.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | from django.conf import settings | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_init_values() -> dict: | ||||||
|  |     """Gets a test dictionary containing a key | ||||||
|  |     for every language.""" | ||||||
|  |  | ||||||
|  |     keys = {} | ||||||
|  |  | ||||||
|  |     for lang_code, lang_name in settings.LANGUAGES: | ||||||
|  |         keys[lang_code] = 'value in %s' % lang_name | ||||||
|  |  | ||||||
|  |     return keys | ||||||
| @@ -1,28 +1,33 @@ | |||||||
|  | import uuid | ||||||
|  |  | ||||||
| from django.db import connection, migrations | from django.db import connection, migrations | ||||||
| from django.db.migrations.executor import MigrationExecutor | from django.db.migrations.executor import MigrationExecutor | ||||||
| from django.contrib.postgres.operations import HStoreExtension | from django.contrib.postgres.operations import HStoreExtension | ||||||
|  |  | ||||||
| from localized_fields import LocalizedModel, AtomicSlugRetryMixin | from localized_fields.models import LocalizedModel | ||||||
|  |  | ||||||
|  |  | ||||||
| def define_fake_model(name='TestModel', fields=None): | def define_fake_model(fields=None, model_base=LocalizedModel, meta_options={}): | ||||||
|  |     name = str(uuid.uuid4()).replace('-', '')[:8] | ||||||
|  |  | ||||||
|     attributes = { |     attributes = { | ||||||
|         'app_label': 'localized_fields', |         'app_label': 'tests', | ||||||
|         '__module__': __name__, |         '__module__': __name__, | ||||||
|         '__name__': name |         '__name__': name, | ||||||
|  |         'Meta': type('Meta', (object,), meta_options) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if fields: |     if fields: | ||||||
|         attributes.update(fields) |         attributes.update(fields) | ||||||
|     model = type(name, (AtomicSlugRetryMixin,LocalizedModel,), attributes) |     model = type(name, (model_base,), attributes) | ||||||
|  |  | ||||||
|     return model |     return model | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_fake_model(name='TestModel', fields=None): | def get_fake_model(fields=None, model_base=LocalizedModel, meta_options={}): | ||||||
|     """Creates a fake model to use during unit tests.""" |     """Creates a fake model to use during unit tests.""" | ||||||
|  |  | ||||||
|     model = define_fake_model(name, fields) |     model = define_fake_model(fields, model_base, meta_options) | ||||||
|  |  | ||||||
|     class TestProject: |     class TestProject: | ||||||
|  |  | ||||||
| @@ -39,7 +44,7 @@ def get_fake_model(name='TestModel', fields=None): | |||||||
|     with connection.schema_editor() as schema_editor: |     with connection.schema_editor() as schema_editor: | ||||||
|         migration_executor = MigrationExecutor(schema_editor.connection) |         migration_executor = MigrationExecutor(schema_editor.connection) | ||||||
|         migration_executor.apply_migration( |         migration_executor.apply_migration( | ||||||
|             TestProject(), TestMigration('eh', 'localized_fields')) |             TestProject(), TestMigration('eh', 'postgres_extra')) | ||||||
|  |  | ||||||
|         schema_editor.create_model(model) |         schema_editor.create_model(model) | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										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,9 +1,11 @@ | |||||||
|  | import bleach | ||||||
|  | 
 | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
| from django_bleach.utils import get_bleach_default_options | from django_bleach.utils import get_bleach_default_options | ||||||
| import bleach |  | ||||||
| 
 | 
 | ||||||
| from localized_fields import LocalizedBleachField, LocalizedValue | from localized_fields.fields import LocalizedBleachField | ||||||
|  | from localized_fields.value import LocalizedValue | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class TestModel: | class TestModel: | ||||||
							
								
								
									
										44
									
								
								tests/test_bulk.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								tests/test_bulk.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | |||||||
|  | from django.db import models | ||||||
|  | from django.test import TestCase | ||||||
|  |  | ||||||
|  | from localized_fields.fields import LocalizedField, LocalizedUniqueSlugField | ||||||
|  |  | ||||||
|  | from .fake_model import get_fake_model | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedBulkTestCase(TestCase): | ||||||
|  |     """Tests bulk operations with data structures provided | ||||||
|  |     by the django-localized-fields library.""" | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_localized_bulk_insert(): | ||||||
|  |         """Tests whether bulk inserts work properly when using | ||||||
|  |         a :see:LocalizedUniqueSlugField in the model.""" | ||||||
|  |  | ||||||
|  |         model = get_fake_model( | ||||||
|  |             { | ||||||
|  |                 'name': LocalizedField(), | ||||||
|  |                 'slug': LocalizedUniqueSlugField(populate_from='name', include_time=True), | ||||||
|  |                 'score': models.IntegerField() | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         to_create = [ | ||||||
|  |             model(name={'en': 'english name 1', 'ro': 'romanian name 1'}, score=1), | ||||||
|  |             model(name={'en': 'english name 2', 'ro': 'romanian name 2'}, score=2), | ||||||
|  |             model(name={'en': 'english name 3', 'ro': 'romanian name 3'}, score=3) | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |         model.objects.bulk_create(to_create) | ||||||
|  |         assert model.objects.all().count() == 3 | ||||||
|  |  | ||||||
|  |         for obj in to_create: | ||||||
|  |             obj_db = model.objects.filter( | ||||||
|  |                 name__en=obj.name.en, | ||||||
|  |                 name__ro=obj.name.ro, | ||||||
|  |                 score=obj.score | ||||||
|  |             ).first() | ||||||
|  |  | ||||||
|  |             assert obj_db | ||||||
|  |             assert len(obj_db.slug.en) >= len(obj_db.name.en) | ||||||
|  |             assert len(obj_db.slug.ro) >= len(obj_db.name.ro) | ||||||
							
								
								
									
										84
									
								
								tests/test_expressions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								tests/test_expressions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,84 @@ | |||||||
|  | from django.test import TestCase | ||||||
|  | from django.db import models | ||||||
|  | from django.utils import translation | ||||||
|  | from django.conf import settings | ||||||
|  | from django.contrib.postgres.aggregates import ArrayAgg | ||||||
|  |  | ||||||
|  | from localized_fields.fields import LocalizedField | ||||||
|  | from localized_fields.value import LocalizedValue | ||||||
|  | from localized_fields.expressions import LocalizedRef | ||||||
|  |  | ||||||
|  | from .fake_model import get_fake_model | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedExpressionsTestCase(TestCase): | ||||||
|  |     """Tests whether expressions properly work with :see:LocalizedField.""" | ||||||
|  |  | ||||||
|  |     TestModel1 = None | ||||||
|  |     TestModel2 = None | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def setUpClass(cls): | ||||||
|  |         """Creates the test model in the database.""" | ||||||
|  |  | ||||||
|  |         super(LocalizedExpressionsTestCase, cls).setUpClass() | ||||||
|  |  | ||||||
|  |         cls.TestModel1 = get_fake_model( | ||||||
|  |             { | ||||||
|  |                 'name': models.CharField(null=False, blank=False, max_length=255), | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         cls.TestModel2 = get_fake_model( | ||||||
|  |             { | ||||||
|  |                 'text': LocalizedField(), | ||||||
|  |                 'other': models.ForeignKey(cls.TestModel1, related_name='features', on_delete=models.CASCADE) | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def test_localized_ref(cls): | ||||||
|  |         """Tests whether the :see:LocalizedRef expression properly works.""" | ||||||
|  |  | ||||||
|  |         obj = cls.TestModel1.objects.create(name='bla bla') | ||||||
|  |         for i in range(0, 10): | ||||||
|  |             cls.TestModel2.objects.create( | ||||||
|  |                 text=LocalizedValue(dict(en='text_%d_en' % i, ro='text_%d_ro' % i, nl='text_%d_nl' % i)), | ||||||
|  |                 other=obj | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         def create_queryset(ref): | ||||||
|  |             return ( | ||||||
|  |                 cls.TestModel1.objects | ||||||
|  |                 .annotate(mytexts=ref) | ||||||
|  |                 .values_list('mytexts', flat=True) | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         # assert that it properly selects the currently active language | ||||||
|  |         for lang_code, _ in settings.LANGUAGES: | ||||||
|  |             translation.activate(lang_code) | ||||||
|  |             queryset = create_queryset(LocalizedRef('features__text')) | ||||||
|  |  | ||||||
|  |             for index, value in enumerate(queryset): | ||||||
|  |                 assert translation.get_language() in value | ||||||
|  |                 assert str(index) in value | ||||||
|  |  | ||||||
|  |         # ensure that the default language is used in case no | ||||||
|  |         # language is active at all | ||||||
|  |         translation.deactivate_all() | ||||||
|  |         queryset = create_queryset(LocalizedRef('features__text')) | ||||||
|  |         for index, value in enumerate(queryset): | ||||||
|  |             assert settings.LANGUAGE_CODE in value | ||||||
|  |             assert str(index) in value | ||||||
|  |  | ||||||
|  |         # ensures that overriding the language works properly | ||||||
|  |         queryset = create_queryset(LocalizedRef('features__text', 'ro')) | ||||||
|  |         for index, value in enumerate(queryset): | ||||||
|  |             assert 'ro' in value | ||||||
|  |             assert str(index) in value | ||||||
|  |  | ||||||
|  |         # ensures that using this in combination with ArrayAgg works properly | ||||||
|  |         queryset = create_queryset(ArrayAgg(LocalizedRef('features__text', 'ro'))).first() | ||||||
|  |         assert isinstance(queryset, list) | ||||||
|  |         for value in queryset: | ||||||
|  |             assert 'ro' in value | ||||||
							
								
								
									
										275
									
								
								tests/test_field.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										275
									
								
								tests/test_field.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,275 @@ | |||||||
|  | import json | ||||||
|  |  | ||||||
|  | from django.conf import settings | ||||||
|  | from django.db import models | ||||||
|  | from django.db.utils import IntegrityError | ||||||
|  | from django.test import TestCase | ||||||
|  |  | ||||||
|  | from localized_fields.fields import LocalizedField | ||||||
|  | from localized_fields.forms import LocalizedFieldForm | ||||||
|  | from localized_fields.value import LocalizedValue | ||||||
|  |  | ||||||
|  | from .data import get_init_values | ||||||
|  | from .fake_model import get_fake_model | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedFieldTestCase(TestCase): | ||||||
|  |     """Tests the :see:LocalizedField class.""" | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_init(): | ||||||
|  |         """Tests whether the :see:__init__ function | ||||||
|  |         correctly handles parameters""" | ||||||
|  |  | ||||||
|  |         field = LocalizedField(blank=True) | ||||||
|  |         assert field.required == [] | ||||||
|  |  | ||||||
|  |         field = LocalizedField(blank=False) | ||||||
|  |         assert field.required == [settings.LANGUAGE_CODE] | ||||||
|  |  | ||||||
|  |         field = LocalizedField(required=True) | ||||||
|  |         assert field.required == [lang_code for lang_code, _ in | ||||||
|  |                                   settings.LANGUAGES] | ||||||
|  |  | ||||||
|  |         field = LocalizedField(required=False) | ||||||
|  |         assert field.required == [] | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_from_db_value(): | ||||||
|  |         """Tests whether the :see:from_db_value function | ||||||
|  |         produces the expected :see:LocalizedValue.""" | ||||||
|  |  | ||||||
|  |         input_data = get_init_values() | ||||||
|  |         localized_value = LocalizedField().from_db_value(input_data) | ||||||
|  |  | ||||||
|  |         for lang_code, _ in settings.LANGUAGES: | ||||||
|  |             assert getattr(localized_value, lang_code) == input_data[lang_code] | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_from_db_value_none(): | ||||||
|  |         """Tests whether the :see:from_db_value function | ||||||
|  |         correctly handles None values.""" | ||||||
|  |  | ||||||
|  |         localized_value = LocalizedField().from_db_value(None) | ||||||
|  |  | ||||||
|  |         for lang_code, _ in settings.LANGUAGES: | ||||||
|  |             assert localized_value.get(lang_code) is None | ||||||
|  |  | ||||||
|  |     def test_from_db_value_none_return_none(self): | ||||||
|  |         """Tests whether the :see:from_db_value function | ||||||
|  |         correctly handles None values when LOCALIZED_FIELDS_EXPERIMENTAL | ||||||
|  |         is set to True.""" | ||||||
|  |  | ||||||
|  |         with self.settings(LOCALIZED_FIELDS_EXPERIMENTAL=True): | ||||||
|  |             localized_value = LocalizedField.from_db_value(None) | ||||||
|  |  | ||||||
|  |         assert localized_value is None | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_to_python(): | ||||||
|  |         """Tests whether the :see:to_python function | ||||||
|  |         produces the expected :see:LocalizedValue.""" | ||||||
|  |  | ||||||
|  |         input_data = get_init_values() | ||||||
|  |         localized_value = LocalizedField().to_python(input_data) | ||||||
|  |  | ||||||
|  |         for language, value in input_data.items(): | ||||||
|  |             assert localized_value.get(language) == value | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_to_python_non_json(): | ||||||
|  |         """Tests whether the :see:to_python function | ||||||
|  |         properly handles a string that is not JSON.""" | ||||||
|  |  | ||||||
|  |         localized_value = LocalizedField().to_python('my value') | ||||||
|  |         assert localized_value.get() == 'my value' | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_to_python_none(): | ||||||
|  |         """Tests whether the :see:to_python function | ||||||
|  |         produces the expected :see:LocalizedValue | ||||||
|  |         instance when it is passes None.""" | ||||||
|  |  | ||||||
|  |         localized_value = LocalizedField().to_python(None) | ||||||
|  |         assert localized_value | ||||||
|  |  | ||||||
|  |         for lang_code, _ in settings.LANGUAGES: | ||||||
|  |             assert localized_value.get(lang_code) is None | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_to_python_non_dict(): | ||||||
|  |         """Tests whether the :see:to_python function produces | ||||||
|  |         the expected :see:LocalizedValue when it is | ||||||
|  |         passed a non-dictionary value.""" | ||||||
|  |  | ||||||
|  |         localized_value = LocalizedField().to_python(list()) | ||||||
|  |         assert localized_value | ||||||
|  |  | ||||||
|  |         for lang_code, _ in settings.LANGUAGES: | ||||||
|  |             assert localized_value.get(lang_code) is None | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_to_python_str(): | ||||||
|  |         """Tests whether the :see:to_python function produces | ||||||
|  |         the expected :see:LocalizedValue when it is | ||||||
|  |         passed serialized string value.""" | ||||||
|  |  | ||||||
|  |         serialized_str = json.dumps(get_init_values()) | ||||||
|  |         localized_value = LocalizedField().to_python(serialized_str) | ||||||
|  |         assert isinstance(localized_value, LocalizedValue) | ||||||
|  |  | ||||||
|  |         for language, value in get_init_values().items(): | ||||||
|  |             assert localized_value.get(language) == value | ||||||
|  |             assert getattr(localized_value, language) == value | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_get_prep_value(): | ||||||
|  |         """"Tests whether the :see:get_prep_value function | ||||||
|  |         produces the expected dictionary.""" | ||||||
|  |  | ||||||
|  |         input_data = get_init_values() | ||||||
|  |         localized_value = LocalizedValue(input_data) | ||||||
|  |  | ||||||
|  |         output_data = LocalizedField().get_prep_value(localized_value) | ||||||
|  |  | ||||||
|  |         for language, value in input_data.items(): | ||||||
|  |             assert language in output_data | ||||||
|  |             assert output_data.get(language) == value | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_get_prep_value_none(): | ||||||
|  |         """Tests whether the :see:get_prep_value function | ||||||
|  |         produces the expected output when it is passed None.""" | ||||||
|  |  | ||||||
|  |         output_data = LocalizedField().get_prep_value(None) | ||||||
|  |         assert not output_data | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_get_prep_value_no_localized_value(): | ||||||
|  |         """Tests whether the :see:get_prep_value function | ||||||
|  |         produces the expected output when it is passed a | ||||||
|  |         non-LocalizedValue value.""" | ||||||
|  |  | ||||||
|  |         output_data = LocalizedField().get_prep_value(['huh']) | ||||||
|  |         assert not output_data | ||||||
|  |  | ||||||
|  |     def test_get_prep_value_clean(self): | ||||||
|  |         """Tests whether the :see:get_prep_value produces | ||||||
|  |         None as the output when it is passed an empty, but | ||||||
|  |         valid LocalizedValue value but, only when null=True.""" | ||||||
|  |  | ||||||
|  |         localized_value = LocalizedValue() | ||||||
|  |  | ||||||
|  |         with self.assertRaises(IntegrityError): | ||||||
|  |             LocalizedField(null=False).get_prep_value(localized_value) | ||||||
|  |  | ||||||
|  |         assert not LocalizedField(null=True).get_prep_value(localized_value) | ||||||
|  |         assert not LocalizedField().clean(None) | ||||||
|  |         assert not LocalizedField().clean(['huh']) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_formfield(): | ||||||
|  |         """Tests whether the :see:formfield function | ||||||
|  |         correctly returns a valid form.""" | ||||||
|  |  | ||||||
|  |         assert isinstance( | ||||||
|  |             LocalizedField().formfield(), | ||||||
|  |             LocalizedFieldForm | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # case optional filling | ||||||
|  |         field = LocalizedField(blank=True, required=[]) | ||||||
|  |         assert not field.formfield().required | ||||||
|  |         for field in field.formfield().fields: | ||||||
|  |             assert not field.required | ||||||
|  |  | ||||||
|  |         # case required for any language | ||||||
|  |         field = LocalizedField(blank=False, required=[]) | ||||||
|  |         assert field.formfield().required | ||||||
|  |         for field in field.formfield().fields: | ||||||
|  |             assert not field.required | ||||||
|  |  | ||||||
|  |         # case required for specific languages | ||||||
|  |         required_langs = ['ro', 'nl'] | ||||||
|  |         field = LocalizedField(blank=False, required=required_langs) | ||||||
|  |         assert field.formfield().required | ||||||
|  |         for field in field.formfield().fields: | ||||||
|  |             if field.label in required_langs: | ||||||
|  |                 assert field.required | ||||||
|  |             else: | ||||||
|  |                 assert not field.required | ||||||
|  |  | ||||||
|  |         # case required for all languages | ||||||
|  |         field = LocalizedField(blank=False, required=True) | ||||||
|  |         assert field.formfield().required | ||||||
|  |         for field in field.formfield().fields: | ||||||
|  |             assert field.required | ||||||
|  |  | ||||||
|  |     def test_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.""" | ||||||
|  |  | ||||||
|  |         model = get_fake_model(dict( | ||||||
|  |             title=LocalizedField(required=True) | ||||||
|  |         )) | ||||||
|  |  | ||||||
|  |         with self.assertRaises(IntegrityError): | ||||||
|  |             model.objects.create(title=dict(ro='romanian', nl='dutch')) | ||||||
|  |  | ||||||
|  |         with self.assertRaises(IntegrityError): | ||||||
|  |             model.objects.create(title=dict(nl='dutch')) | ||||||
|  |  | ||||||
|  |         with self.assertRaises(IntegrityError): | ||||||
|  |             model.objects.create(title=dict(random='random')) | ||||||
|  |  | ||||||
|  |         with self.assertRaises(IntegrityError): | ||||||
|  |             model.objects.create(title=dict()) | ||||||
|  |  | ||||||
|  |         with self.assertRaises(IntegrityError): | ||||||
|  |             model.objects.create(title=None) | ||||||
|  |  | ||||||
|  |         with self.assertRaises(IntegrityError): | ||||||
|  |             model.objects.create(title='') | ||||||
|  |  | ||||||
|  |         with self.assertRaises(IntegrityError): | ||||||
|  |             model.objects.create(title='         ') | ||||||
|  |  | ||||||
|  |     def test_required_some(self): | ||||||
|  |         """Tests whether passing an array to required, | ||||||
|  |         properly validates whether the specified languages | ||||||
|  |         are marked as required.""" | ||||||
|  |  | ||||||
|  |         model = get_fake_model(dict( | ||||||
|  |             title=LocalizedField(required=['nl', 'ro']) | ||||||
|  |         )) | ||||||
|  |  | ||||||
|  |         with self.assertRaises(IntegrityError): | ||||||
|  |             model.objects.create(title=dict(ro='romanian', nl='dutch')) | ||||||
|  |  | ||||||
|  |         with self.assertRaises(IntegrityError): | ||||||
|  |             model.objects.create(title=dict(nl='dutch')) | ||||||
|  |  | ||||||
|  |         with self.assertRaises(IntegrityError): | ||||||
|  |             model.objects.create(title=dict(random='random')) | ||||||
|  |  | ||||||
|  |         with self.assertRaises(IntegrityError): | ||||||
|  |             model.objects.create(title=dict()) | ||||||
|  |  | ||||||
|  |         with self.assertRaises(IntegrityError): | ||||||
|  |             model.objects.create(title=None) | ||||||
|  |  | ||||||
|  |         with self.assertRaises(IntegrityError): | ||||||
|  |             model.objects.create(title='') | ||||||
|  |  | ||||||
|  |         with self.assertRaises(IntegrityError): | ||||||
|  |             model.objects.create(title='         ') | ||||||
							
								
								
									
										165
									
								
								tests/test_file_field.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								tests/test_file_field.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,165 @@ | |||||||
|  | import os | ||||||
|  | import shutil | ||||||
|  | import tempfile as sys_tempfile | ||||||
|  | import pickle | ||||||
|  | import json | ||||||
|  |  | ||||||
|  | from django import forms | ||||||
|  | from django.test import TestCase, override_settings | ||||||
|  | from django.core.files.base import File, ContentFile | ||||||
|  | from django.core.files import temp as tempfile | ||||||
|  | from localized_fields.fields import LocalizedFileField | ||||||
|  | from localized_fields.value import LocalizedValue | ||||||
|  | from localized_fields.fields.file_field import LocalizedFieldFile | ||||||
|  | from localized_fields.forms import LocalizedFileFieldForm | ||||||
|  | from localized_fields.value import LocalizedFileValue | ||||||
|  | from localized_fields.widgets import LocalizedFileWidget | ||||||
|  | from .fake_model import get_fake_model | ||||||
|  |  | ||||||
|  |  | ||||||
|  | MEDIA_ROOT = sys_tempfile.mkdtemp() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @override_settings(MEDIA_ROOT=MEDIA_ROOT) | ||||||
|  | class LocalizedFileFieldTestCase(TestCase): | ||||||
|  |     """Tests the localized slug classes.""" | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def setUpClass(cls): | ||||||
|  |         """Creates the test models in the database.""" | ||||||
|  |  | ||||||
|  |         super().setUpClass() | ||||||
|  |  | ||||||
|  |         cls.FileFieldModel = get_fake_model( | ||||||
|  |             { | ||||||
|  |                 'file': LocalizedFileField(), | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |         if not os.path.isdir(MEDIA_ROOT): | ||||||
|  |             os.makedirs(MEDIA_ROOT) | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def tearDownClass(cls): | ||||||
|  |         super().tearDownClass() | ||||||
|  |         shutil.rmtree(MEDIA_ROOT) | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def test_assign(cls): | ||||||
|  |         """Tests whether the :see:LocalizedFileValueDescriptor works properly""" | ||||||
|  |  | ||||||
|  |         temp_file = tempfile.NamedTemporaryFile(dir=MEDIA_ROOT) | ||||||
|  |         instance = cls.FileFieldModel() | ||||||
|  |         instance.file = {'en': temp_file.name} | ||||||
|  |         assert isinstance(instance.file.en, LocalizedFieldFile) | ||||||
|  |         assert instance.file.en.name == temp_file.name | ||||||
|  |  | ||||||
|  |         field_dump = pickle.dumps(instance.file) | ||||||
|  |         instance = cls.FileFieldModel() | ||||||
|  |         instance.file = pickle.loads(field_dump) | ||||||
|  |         assert instance.file.en.field == instance._meta.get_field('file') | ||||||
|  |         assert instance.file.en.instance == instance | ||||||
|  |         assert isinstance(instance.file.en, LocalizedFieldFile) | ||||||
|  |  | ||||||
|  |         instance = cls.FileFieldModel() | ||||||
|  |         instance.file = {'en': ContentFile("test", "testfilename")} | ||||||
|  |         assert isinstance(instance.file.en, LocalizedFieldFile) | ||||||
|  |         assert instance.file.en.name == "testfilename" | ||||||
|  |  | ||||||
|  |         another_instance = cls.FileFieldModel() | ||||||
|  |         another_instance.file = {'ro': instance.file.en} | ||||||
|  |         assert another_instance == another_instance.file.ro.instance | ||||||
|  |         assert another_instance.file.ro.lang == 'ro' | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def test_save_form_data(cls): | ||||||
|  |         """Tests whether the :see:save_form_data function correctly set | ||||||
|  |         a valid value.""" | ||||||
|  |  | ||||||
|  |         instance = cls.FileFieldModel() | ||||||
|  |         data = LocalizedFileValue({'en': False}) | ||||||
|  |         instance._meta.get_field('file').save_form_data(instance, data) | ||||||
|  |         assert instance.file.en == '' | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def test_pre_save(cls): | ||||||
|  |         """Tests whether the :see:pre_save function works properly.""" | ||||||
|  |  | ||||||
|  |         instance = cls.FileFieldModel() | ||||||
|  |         instance.file = {'en': ContentFile("test", "testfilename")} | ||||||
|  |         instance._meta.get_field('file').pre_save(instance, False) | ||||||
|  |         assert instance.file.en._committed is True | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def test_file_methods(cls): | ||||||
|  |         """Tests whether the :see:LocalizedFieldFile.delete method works | ||||||
|  |         correctly.""" | ||||||
|  |  | ||||||
|  |         temp_file = File(tempfile.NamedTemporaryFile()) | ||||||
|  |         instance = cls.FileFieldModel() | ||||||
|  |         # Calling delete on an unset FileField should not call the file deletion | ||||||
|  |         # process, but fail silently | ||||||
|  |         instance.file.en.delete() | ||||||
|  |         instance.file.en.save('testfilename', temp_file) | ||||||
|  |         assert instance.file.en.name == 'testfilename' | ||||||
|  |         instance.file.en.delete() | ||||||
|  |         assert instance.file.en.name is None | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def test_generate_filename(cls): | ||||||
|  |         """Tests whether the :see:LocalizedFieldFile.generate_filename method | ||||||
|  |         works correctly.""" | ||||||
|  |  | ||||||
|  |         instance = cls.FileFieldModel() | ||||||
|  |         field = instance._meta.get_field('file') | ||||||
|  |         field.upload_to = '{lang}/' | ||||||
|  |         filename = field.generate_filename(instance, 'test', 'en') | ||||||
|  |         assert filename == 'en/test' | ||||||
|  |         field.upload_to = lambda instance, filename, lang: \ | ||||||
|  |             '%s_%s' % (lang, filename) | ||||||
|  |         filename = field.generate_filename(instance, 'test', 'en') | ||||||
|  |         assert filename == 'en_test' | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     @override_settings(LANGUAGES=(('en', 'English'),)) | ||||||
|  |     def test_value_to_string(cls): | ||||||
|  |         """Tests whether the :see:LocalizedFileField | ||||||
|  |         class's :see:value_to_string function works properly.""" | ||||||
|  |  | ||||||
|  |         temp_file = File(tempfile.NamedTemporaryFile()) | ||||||
|  |         instance = cls.FileFieldModel() | ||||||
|  |         field = cls.FileFieldModel._meta.get_field('file') | ||||||
|  |         field.upload_to = '' | ||||||
|  |         instance.file.en.save('testfilename', temp_file) | ||||||
|  |         expected_value_to_string = json.dumps({'en': 'testfilename'}) | ||||||
|  |         assert field.value_to_string(instance) == expected_value_to_string | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_get_prep_value(): | ||||||
|  |         """Tests whether the :see:get_prep_value function returns correctly | ||||||
|  |         value.""" | ||||||
|  |  | ||||||
|  |         value = LocalizedValue({'en': None}) | ||||||
|  |         assert LocalizedFileField().get_prep_value(None) is None | ||||||
|  |         assert isinstance(LocalizedFileField().get_prep_value(value), dict) | ||||||
|  |         assert LocalizedFileField().get_prep_value(value)['en'] == '' | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_formfield(): | ||||||
|  |         """Tests whether the :see:formfield function correctly returns | ||||||
|  |         a valid form.""" | ||||||
|  |  | ||||||
|  |         form_field = LocalizedFileField().formfield() | ||||||
|  |         assert isinstance(form_field, LocalizedFileFieldForm) | ||||||
|  |         assert isinstance(form_field, forms.FileField) | ||||||
|  |         assert isinstance(form_field.widget, LocalizedFileWidget) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_deconstruct(): | ||||||
|  |         """Tests whether the :see:LocalizedFileField | ||||||
|  |         class's :see:deconstruct function works properly.""" | ||||||
|  |  | ||||||
|  |         name, path, args, kwargs = LocalizedFileField().deconstruct() | ||||||
|  |         assert 'upload_to' in kwargs | ||||||
|  |         assert 'storage' not in kwargs | ||||||
|  |         name, path, args, kwargs = LocalizedFileField(storage='test').deconstruct() | ||||||
|  |         assert 'storage' in kwargs | ||||||
							
								
								
									
										40
									
								
								tests/test_file_field_form.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								tests/test_file_field_form.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | |||||||
|  | from django.conf import settings | ||||||
|  | from django.core.exceptions import ValidationError | ||||||
|  | from django.forms.widgets import FILE_INPUT_CONTRADICTION | ||||||
|  | from django.test import TestCase | ||||||
|  |  | ||||||
|  | from localized_fields.forms import LocalizedFileFieldForm | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedFileFieldFormTestCase(TestCase): | ||||||
|  |     """Tests the workings of the :see:LocalizedFileFieldForm class.""" | ||||||
|  |  | ||||||
|  |     def test_clean(self): | ||||||
|  |         """Tests whether the :see:clean function is working properly.""" | ||||||
|  |  | ||||||
|  |         formfield = LocalizedFileFieldForm(required=True) | ||||||
|  |         with self.assertRaises(ValidationError): | ||||||
|  |             formfield.clean([]) | ||||||
|  |         with self.assertRaises(ValidationError): | ||||||
|  |             formfield.clean([], {'en': None}) | ||||||
|  |         with self.assertRaises(ValidationError): | ||||||
|  |             formfield.clean("badvalue") | ||||||
|  |         with self.assertRaises(ValidationError): | ||||||
|  |             value = [FILE_INPUT_CONTRADICTION] * len(settings.LANGUAGES) | ||||||
|  |             formfield.clean(value) | ||||||
|  |  | ||||||
|  |         formfield = LocalizedFileFieldForm(required=False) | ||||||
|  |         formfield.clean([''] * len(settings.LANGUAGES)) | ||||||
|  |         formfield.clean(['', ''], ['', '']) | ||||||
|  |  | ||||||
|  |     def test_bound_data(self): | ||||||
|  |         """Tests whether the :see:bound_data function is returns correctly | ||||||
|  |         value""" | ||||||
|  |  | ||||||
|  |         formfield = LocalizedFileFieldForm() | ||||||
|  |         assert formfield.bound_data([''], None) == [''] | ||||||
|  |  | ||||||
|  |         initial = dict([(lang, '') for lang, _ in settings.LANGUAGES]) | ||||||
|  |         value = [None] * len(settings.LANGUAGES) | ||||||
|  |         expected_value = [''] * len(settings.LANGUAGES) | ||||||
|  |         assert formfield.bound_data(value, initial) == expected_value | ||||||
							
								
								
									
										24
									
								
								tests/test_file_widget.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								tests/test_file_widget.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | |||||||
|  | from django.test import TestCase | ||||||
|  |  | ||||||
|  | from localized_fields.value import LocalizedFileValue | ||||||
|  | from localized_fields.widgets import LocalizedFileWidget | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedFileWidgetTestCase(TestCase): | ||||||
|  |     """Tests the workings of the :see:LocalizedFiledWidget class.""" | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_get_context(): | ||||||
|  |         """Tests whether the :see:get_context correctly | ||||||
|  |         handles 'required' attribute, separately for each subwidget.""" | ||||||
|  |  | ||||||
|  |         widget = LocalizedFileWidget() | ||||||
|  |         widget.widgets[0].is_required = True | ||||||
|  |         widget.widgets[1].is_required = True | ||||||
|  |         widget.widgets[2].is_required = False | ||||||
|  |         context = widget.get_context(name='test', | ||||||
|  |                                      value=LocalizedFileValue(dict(en='test')), | ||||||
|  |                                      attrs=dict(required=True)) | ||||||
|  |         assert 'required' not in context['widget']['subwidgets'][0]['attrs'] | ||||||
|  |         assert context['widget']['subwidgets'][1]['attrs']['required'] | ||||||
|  |         assert 'required' not in context['widget']['subwidgets'][2]['attrs'] | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
| 
 | 
 | ||||||
| from localized_fields import LocalizedFieldForm | from localized_fields.forms import LocalizedFieldForm | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class LocalizedFieldFormTestCase(TestCase): | class LocalizedFieldFormTestCase(TestCase): | ||||||
| @@ -11,8 +11,8 @@ class LocalizedFieldFormTestCase(TestCase): | |||||||
|     def test_init(): |     def test_init(): | ||||||
|         """Tests whether the constructor correctly |         """Tests whether the constructor correctly | ||||||
|         creates a field for every language.""" |         creates a field for every language.""" | ||||||
| 
 |         # case required for specific language | ||||||
|         form = LocalizedFieldForm() |         form = LocalizedFieldForm(required=[settings.LANGUAGE_CODE]) | ||||||
| 
 | 
 | ||||||
|         for (lang_code, _), field in zip(settings.LANGUAGES, form.fields): |         for (lang_code, _), field in zip(settings.LANGUAGES, form.fields): | ||||||
|             assert field.label == lang_code |             assert field.label == lang_code | ||||||
| @@ -22,6 +22,24 @@ class LocalizedFieldFormTestCase(TestCase): | |||||||
|             else: |             else: | ||||||
|                 assert not field.required |                 assert not field.required | ||||||
| 
 | 
 | ||||||
|  |         # case required for all languages | ||||||
|  |         form = LocalizedFieldForm(required=True) | ||||||
|  |         assert form.required | ||||||
|  |         for field in form.fields: | ||||||
|  |             assert field.required | ||||||
|  | 
 | ||||||
|  |         # case optional filling | ||||||
|  |         form = LocalizedFieldForm(required=False) | ||||||
|  |         assert not form.required | ||||||
|  |         for field in form.fields: | ||||||
|  |             assert not field.required | ||||||
|  | 
 | ||||||
|  |         # case required for any language | ||||||
|  |         form = LocalizedFieldForm(required=[]) | ||||||
|  |         assert form.required | ||||||
|  |         for field in form.fields: | ||||||
|  |             assert not field.required | ||||||
|  | 
 | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def test_compress(): |     def test_compress(): | ||||||
|         """Tests whether the :see:compress function |         """Tests whether the :see:compress function | ||||||
							
								
								
									
										176
									
								
								tests/test_integer_field.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								tests/test_integer_field.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,176 @@ | |||||||
|  | from django.test import TestCase | ||||||
|  | from django.db.utils import IntegrityError | ||||||
|  | from django.conf import settings | ||||||
|  | from django.db import connection | ||||||
|  | from django.utils import translation | ||||||
|  |  | ||||||
|  | from localized_fields.fields import LocalizedIntegerField | ||||||
|  |  | ||||||
|  | from .fake_model import get_fake_model | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedIntegerFieldTestCase(TestCase): | ||||||
|  |     """Tests whether the :see:LocalizedIntegerField | ||||||
|  |     and :see:LocalizedIntegerValue works properly.""" | ||||||
|  |  | ||||||
|  |     TestModel = None | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def setUpClass(cls): | ||||||
|  |         super().setUpClass() | ||||||
|  |  | ||||||
|  |         cls.TestModel = get_fake_model({ | ||||||
|  |             'score': LocalizedIntegerField() | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |     def test_basic(self): | ||||||
|  |         """Tests the basics of storing integer values.""" | ||||||
|  |  | ||||||
|  |         obj = self.TestModel() | ||||||
|  |         for index, (lang_code, _) in enumerate(settings.LANGUAGES): | ||||||
|  |             obj.score.set(lang_code, index + 1) | ||||||
|  |         obj.save() | ||||||
|  |  | ||||||
|  |         obj = self.TestModel.objects.all().first() | ||||||
|  |         for index, (lang_code, _) in enumerate(settings.LANGUAGES): | ||||||
|  |             assert obj.score.get(lang_code) == index + 1 | ||||||
|  |  | ||||||
|  |     def test_primary_language_required(self): | ||||||
|  |         """Tests whether the primary language is required by | ||||||
|  |         default and all other languages are optiona.""" | ||||||
|  |  | ||||||
|  |         # not filling in anything should raise IntegrityError, | ||||||
|  |         # the primary language is required | ||||||
|  |         with self.assertRaises(IntegrityError): | ||||||
|  |             obj = self.TestModel() | ||||||
|  |             obj.save() | ||||||
|  |  | ||||||
|  |         # when filling all other languages besides the primary language | ||||||
|  |         # should still raise an error because the primary is always required | ||||||
|  |         with self.assertRaises(IntegrityError): | ||||||
|  |             obj = self.TestModel() | ||||||
|  |             for lang_code, _ in settings.LANGUAGES: | ||||||
|  |                 if lang_code == settings.LANGUAGE_CODE: | ||||||
|  |                     continue | ||||||
|  |                 obj.score.set(lang_code, 23) | ||||||
|  |             obj.save() | ||||||
|  |  | ||||||
|  |     def test_default_value_none(self): | ||||||
|  |         """Tests whether the default value for optional languages | ||||||
|  |         is NoneType.""" | ||||||
|  |  | ||||||
|  |         obj = self.TestModel() | ||||||
|  |         obj.score.set(settings.LANGUAGE_CODE, 1234) | ||||||
|  |         obj.save() | ||||||
|  |  | ||||||
|  |         for lang_code, _ in settings.LANGUAGES: | ||||||
|  |             if lang_code == settings.LANGUAGE_CODE: | ||||||
|  |                 continue | ||||||
|  |  | ||||||
|  |             assert obj.score.get(lang_code) is None | ||||||
|  |  | ||||||
|  |     def test_translate(self): | ||||||
|  |         """Tests whether casting the value to an integer | ||||||
|  |         results in the value being returned in the currently | ||||||
|  |         active language as an integer.""" | ||||||
|  |  | ||||||
|  |         obj = self.TestModel() | ||||||
|  |         for index, (lang_code, _) in enumerate(settings.LANGUAGES): | ||||||
|  |             obj.score.set(lang_code, index + 1) | ||||||
|  |         obj.save() | ||||||
|  |  | ||||||
|  |         obj.refresh_from_db() | ||||||
|  |         for index, (lang_code, _) in enumerate(settings.LANGUAGES): | ||||||
|  |             with translation.override(lang_code): | ||||||
|  |                 assert int(obj.score) == index + 1 | ||||||
|  |                 assert obj.score.translate() == index + 1 | ||||||
|  |  | ||||||
|  |     def test_translate_primary_fallback(self): | ||||||
|  |         """Tests whether casting the value to an integer | ||||||
|  |         results in the value begin returned in the active | ||||||
|  |         language and falls back to the primary language | ||||||
|  |         if there is no value in that language.""" | ||||||
|  |  | ||||||
|  |         obj = self.TestModel() | ||||||
|  |         obj.score.set(settings.LANGUAGE_CODE, 25) | ||||||
|  |  | ||||||
|  |         secondary_language = settings.LANGUAGES[-1][0] | ||||||
|  |         assert obj.score.get(secondary_language) is None | ||||||
|  |  | ||||||
|  |         with translation.override(secondary_language): | ||||||
|  |             assert obj.score.translate() == 25 | ||||||
|  |             assert int(obj.score) == 25 | ||||||
|  |  | ||||||
|  |     def test_get_default_value(self): | ||||||
|  |         """Tests whether getting the value in a specific | ||||||
|  |         language properly returns the specified default | ||||||
|  |         in case it is not available.""" | ||||||
|  |  | ||||||
|  |         obj = self.TestModel() | ||||||
|  |         obj.score.set(settings.LANGUAGE_CODE, 25) | ||||||
|  |  | ||||||
|  |         secondary_language = settings.LANGUAGES[-1][0] | ||||||
|  |         assert obj.score.get(secondary_language) is None | ||||||
|  |         assert obj.score.get(secondary_language, 1337) == 1337 | ||||||
|  |  | ||||||
|  |     def test_completely_optional(self): | ||||||
|  |         """Tests whether having all languages optional | ||||||
|  |         works properly.""" | ||||||
|  |  | ||||||
|  |         model = get_fake_model({ | ||||||
|  |             'score': LocalizedIntegerField(null=True, required=[], blank=True) | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |         obj = model() | ||||||
|  |         obj.save() | ||||||
|  |  | ||||||
|  |         for lang_code, _ in settings.LANGUAGES: | ||||||
|  |             assert getattr(obj.score, lang_code) is None | ||||||
|  |  | ||||||
|  |     def test_store_string(self): | ||||||
|  |         """Tests whether the field properly raises | ||||||
|  |         an error when trying to store a non-integer.""" | ||||||
|  |  | ||||||
|  |         for lang_code, _ in settings.LANGUAGES: | ||||||
|  |             obj = self.TestModel() | ||||||
|  |             with self.assertRaises(IntegrityError): | ||||||
|  |                 obj.score.set(lang_code, 'haha') | ||||||
|  |                 obj.save() | ||||||
|  |  | ||||||
|  |     def test_none_if_illegal_value_stored(self): | ||||||
|  |         """Tests whether None is returned for a language | ||||||
|  |         if the value stored in the database is not an | ||||||
|  |         integer.""" | ||||||
|  |  | ||||||
|  |         obj = self.TestModel() | ||||||
|  |         obj.score.set(settings.LANGUAGE_CODE, 25) | ||||||
|  |         obj.save() | ||||||
|  |  | ||||||
|  |         with connection.cursor() as cursor: | ||||||
|  |             table_name = self.TestModel._meta.db_table | ||||||
|  |             cursor.execute("update %s set score = 'en=>haha'" % table_name) | ||||||
|  |  | ||||||
|  |         obj.refresh_from_db() | ||||||
|  |         assert obj.score.get(settings.LANGUAGE_CODE) is None | ||||||
|  |  | ||||||
|  |     def test_default_value(self): | ||||||
|  |         """Tests whether a default is properly set | ||||||
|  |         when specified.""" | ||||||
|  |  | ||||||
|  |         model = get_fake_model({ | ||||||
|  |             'score': LocalizedIntegerField(default={settings.LANGUAGE_CODE: 75}) | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |         obj = model.objects.create() | ||||||
|  |         assert obj.score.get(settings.LANGUAGE_CODE) == 75 | ||||||
|  |  | ||||||
|  |         obj = model() | ||||||
|  |         for lang_code, _ in settings.LANGUAGES: | ||||||
|  |             obj.score.set(lang_code, None) | ||||||
|  |         obj.save() | ||||||
|  |  | ||||||
|  |         for lang_code, _ in settings.LANGUAGES: | ||||||
|  |             if lang_code == settings.LANGUAGE_CODE: | ||||||
|  |                 assert obj.score.get(lang_code) == 75 | ||||||
|  |             else: | ||||||
|  |                 assert obj.score.get(lang_code) is None | ||||||
| @@ -1,278 +0,0 @@ | |||||||
| from django.conf import settings |  | ||||||
| from django.test import TestCase |  | ||||||
| from django.utils import translation |  | ||||||
| from django.db.utils import IntegrityError |  | ||||||
|  |  | ||||||
| from localized_fields import LocalizedField, LocalizedValue, LocalizedFieldForm |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_init_values() -> dict: |  | ||||||
|     """Gets a test dictionary containing a key |  | ||||||
|     for every language.""" |  | ||||||
|  |  | ||||||
|     keys = {} |  | ||||||
|  |  | ||||||
|     for lang_code, lang_name in settings.LANGUAGES: |  | ||||||
|         keys[lang_code] = 'value in %s' % lang_name |  | ||||||
|  |  | ||||||
|     return keys |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class LocalizedValueTestCase(TestCase): |  | ||||||
|     """Tests the :see:LocalizedValue class.""" |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def tearDown(): |  | ||||||
|         """Assures that the current language |  | ||||||
|         is set back to the default.""" |  | ||||||
|  |  | ||||||
|         translation.activate(settings.LANGUAGE_CODE) |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_init(): |  | ||||||
|         """Tests whether the __init__ function |  | ||||||
|         of the :see:LocalizedValue class works |  | ||||||
|         as expected.""" |  | ||||||
|  |  | ||||||
|         keys = get_init_values() |  | ||||||
|         value = LocalizedValue(keys) |  | ||||||
|  |  | ||||||
|         for lang_code, _ in settings.LANGUAGES: |  | ||||||
|             assert getattr(value, lang_code, None) == keys[lang_code] |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_init_default_values(): |  | ||||||
|         """Tests wehther the __init__ function |  | ||||||
|         of the :see:LocalizedValue accepts the |  | ||||||
|         default value or an empty dict properly.""" |  | ||||||
|  |  | ||||||
|         value = LocalizedValue() |  | ||||||
|  |  | ||||||
|         for lang_code, _ in settings.LANGUAGES: |  | ||||||
|             assert getattr(value, lang_code) is None |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_get_explicit(): |  | ||||||
|         """Tests whether the the :see:LocalizedValue |  | ||||||
|         class's :see:get function works properly |  | ||||||
|         when specifying an explicit value.""" |  | ||||||
|  |  | ||||||
|         keys = get_init_values() |  | ||||||
|         localized_value = LocalizedValue(keys) |  | ||||||
|  |  | ||||||
|         for language, value in keys.items(): |  | ||||||
|             assert localized_value.get(language) == value |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_get_default_language(): |  | ||||||
|         """Tests whether the :see:LocalizedValue |  | ||||||
|         class's see:get function properly |  | ||||||
|         gets the value in the default language.""" |  | ||||||
|  |  | ||||||
|         keys = get_init_values() |  | ||||||
|         localized_value = LocalizedValue(keys) |  | ||||||
|  |  | ||||||
|         for language, _ in keys.items(): |  | ||||||
|             translation.activate(language) |  | ||||||
|             assert localized_value.get() == keys[settings.LANGUAGE_CODE] |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_set(): |  | ||||||
|         """Tests whether the :see:LocalizedValue |  | ||||||
|         class's see:set function works properly.""" |  | ||||||
|  |  | ||||||
|         localized_value = LocalizedValue() |  | ||||||
|  |  | ||||||
|         for language, value in get_init_values(): |  | ||||||
|             localized_value.set(language, value) |  | ||||||
|             assert localized_value.get(language) == value |  | ||||||
|             assert getattr(localized_value, language) == value |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_str(): |  | ||||||
|         """Tests whether the :see:LocalizedValue |  | ||||||
|         class's __str__ works properly.""" |  | ||||||
|  |  | ||||||
|         keys = get_init_values() |  | ||||||
|         localized_value = LocalizedValue(keys) |  | ||||||
|  |  | ||||||
|         for language, value in keys.items(): |  | ||||||
|             translation.activate(language) |  | ||||||
|             assert str(localized_value) == value |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_eq(): |  | ||||||
|         """Tests whether the __eq__ operator |  | ||||||
|         of :see:LocalizedValue works properly.""" |  | ||||||
|  |  | ||||||
|         a = LocalizedValue({'en': 'a', 'ar': 'b'}) |  | ||||||
|         b = LocalizedValue({'en': 'a', 'ar': 'b'}) |  | ||||||
|  |  | ||||||
|         assert a == b |  | ||||||
|  |  | ||||||
|         b.en = 'b' |  | ||||||
|         assert a != b |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_str_fallback(): |  | ||||||
|         """Tests whether the :see:LocalizedValue |  | ||||||
|         class's __str__'s fallback functionality |  | ||||||
|         works properly.""" |  | ||||||
|  |  | ||||||
|         test_value = 'myvalue' |  | ||||||
|  |  | ||||||
|         localized_value = LocalizedValue({ |  | ||||||
|             settings.LANGUAGE_CODE: test_value |  | ||||||
|         }) |  | ||||||
|  |  | ||||||
|         other_language = settings.LANGUAGES[-1][0] |  | ||||||
|  |  | ||||||
|         # make sure that, by default it returns |  | ||||||
|         # the value in the default language |  | ||||||
|         assert str(localized_value) == test_value |  | ||||||
|  |  | ||||||
|         # make sure that it falls back to the |  | ||||||
|         # primary language when there's no value |  | ||||||
|         # available in the current language |  | ||||||
|         translation.activate(other_language) |  | ||||||
|         assert str(localized_value) == test_value |  | ||||||
|  |  | ||||||
|         # make sure that it's just __str__ falling |  | ||||||
|         # back and that for the other language |  | ||||||
|         # there's no actual value |  | ||||||
|         assert localized_value.get(other_language) != test_value |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_deconstruct(): |  | ||||||
|         """Tests whether the :see:LocalizedValue |  | ||||||
|         class's :see:deconstruct function works properly.""" |  | ||||||
|  |  | ||||||
|         keys = get_init_values() |  | ||||||
|         value = LocalizedValue(keys) |  | ||||||
|  |  | ||||||
|         path, args, kwargs = value.deconstruct() |  | ||||||
|  |  | ||||||
|         assert args[0] == keys |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_construct_string(): |  | ||||||
|         """Tests whether the :see:LocalizedValue's constructor |  | ||||||
|         assumes the primary language when passing a single string.""" |  | ||||||
|  |  | ||||||
|         value = LocalizedValue('beer') |  | ||||||
|         assert value.get(settings.LANGUAGE_CODE) == 'beer' |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class LocalizedFieldTestCase(TestCase): |  | ||||||
|     """Tests the :see:LocalizedField class.""" |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_from_db_value(): |  | ||||||
|         """Tests whether the :see:from_db_value function |  | ||||||
|         produces the expected :see:LocalizedValue.""" |  | ||||||
|  |  | ||||||
|         input_data = get_init_values() |  | ||||||
|         localized_value = LocalizedField.from_db_value(input_data) |  | ||||||
|  |  | ||||||
|         for lang_code, _ in settings.LANGUAGES: |  | ||||||
|             assert getattr(localized_value, lang_code) == input_data[lang_code] |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_from_db_value_none(): |  | ||||||
|         """Tests whether the :see:from_db_valuei function |  | ||||||
|         correctly handles None values.""" |  | ||||||
|  |  | ||||||
|         localized_value = LocalizedField.from_db_value(None) |  | ||||||
|  |  | ||||||
|         for lang_code, _ in settings.LANGUAGES: |  | ||||||
|             assert localized_value.get(lang_code) is None |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_to_python(): |  | ||||||
|         """Tests whether the :see:to_python function |  | ||||||
|         produces the expected :see:LocalizedValue.""" |  | ||||||
|  |  | ||||||
|         input_data = get_init_values() |  | ||||||
|         localized_value = LocalizedField().to_python(input_data) |  | ||||||
|  |  | ||||||
|         for language, value in input_data.items(): |  | ||||||
|             assert localized_value.get(language) == value |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_to_python_none(): |  | ||||||
|         """Tests whether the :see:to_python function |  | ||||||
|         produces the expected :see:LocalizedValue |  | ||||||
|         instance when it is passes None.""" |  | ||||||
|  |  | ||||||
|         localized_value = LocalizedField().to_python(None) |  | ||||||
|         assert localized_value |  | ||||||
|  |  | ||||||
|         for lang_code, _ in settings.LANGUAGES: |  | ||||||
|             assert localized_value.get(lang_code) is None |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_to_python_non_dict(): |  | ||||||
|         """Tests whether the :see:to_python function produces |  | ||||||
|         the expected :see:LocalizedValue when it is |  | ||||||
|         passed a non-dictionary value.""" |  | ||||||
|  |  | ||||||
|         localized_value = LocalizedField().to_python(list()) |  | ||||||
|         assert localized_value |  | ||||||
|  |  | ||||||
|         for lang_code, _ in settings.LANGUAGES: |  | ||||||
|             assert localized_value.get(lang_code) is None |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_get_prep_value(): |  | ||||||
|         """"Tests whether the :see:get_prep_value function |  | ||||||
|         produces the expected dictionary.""" |  | ||||||
|  |  | ||||||
|         input_data = get_init_values() |  | ||||||
|         localized_value = LocalizedValue(input_data) |  | ||||||
|  |  | ||||||
|         output_data = LocalizedField().get_prep_value(localized_value) |  | ||||||
|  |  | ||||||
|         for language, value in input_data.items(): |  | ||||||
|             assert language in output_data |  | ||||||
|             assert output_data.get(language) == value |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_get_prep_value_none(): |  | ||||||
|         """Tests whether the :see:get_prep_value function |  | ||||||
|         produces the expected output when it is passed None.""" |  | ||||||
|  |  | ||||||
|         output_data = LocalizedField().get_prep_value(None) |  | ||||||
|         assert not output_data |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_get_prep_value_no_localized_value(): |  | ||||||
|         """Tests whether the :see:get_prep_value function |  | ||||||
|         produces the expected output when it is passed a |  | ||||||
|         non-LocalizedValue value.""" |  | ||||||
|  |  | ||||||
|         output_data = LocalizedField().get_prep_value(['huh']) |  | ||||||
|         assert not output_data |  | ||||||
|  |  | ||||||
|     def test_get_prep_value_clean(self): |  | ||||||
|         """Tests whether the :see:get_prep_value produces |  | ||||||
|         None as the output when it is passed an empty, but |  | ||||||
|         valid LocalizedValue value but, only when null=True.""" |  | ||||||
|  |  | ||||||
|         localized_value = LocalizedValue() |  | ||||||
|  |  | ||||||
|         with self.assertRaises(IntegrityError): |  | ||||||
|             LocalizedField(null=False).get_prep_value(localized_value) |  | ||||||
|  |  | ||||||
|         assert not LocalizedField(null=True).get_prep_value(localized_value) |  | ||||||
|         assert not LocalizedField().clean(None) |  | ||||||
|         assert not LocalizedField().clean(['huh']) |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_formfield(): |  | ||||||
|         """Tests whether the :see:formfield function |  | ||||||
|         correctly returns a valid form.""" |  | ||||||
|  |  | ||||||
|         assert isinstance( |  | ||||||
|             LocalizedField().formfield(), |  | ||||||
|             LocalizedFieldForm |  | ||||||
|         ) |  | ||||||
| @@ -1,43 +0,0 @@ | |||||||
| from django.conf import settings |  | ||||||
| from django.test import TestCase |  | ||||||
|  |  | ||||||
| from localized_fields import LocalizedFieldWidget, LocalizedValue |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class LocalizedFieldWidgetTestCase(TestCase): |  | ||||||
|     """Tests the workings of the :see:LocalizedFieldWidget class.""" |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_widget_creation(): |  | ||||||
|         """Tests whether a widget is created for every |  | ||||||
|         language correctly.""" |  | ||||||
|  |  | ||||||
|         widget = LocalizedFieldWidget() |  | ||||||
|         assert len(widget.widgets) == len(settings.LANGUAGES) |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_decompress(): |  | ||||||
|         """Tests whether a :see:LocalizedValue instance |  | ||||||
|         can correctly be "decompressed" over the available |  | ||||||
|         widgets.""" |  | ||||||
|  |  | ||||||
|         localized_value = LocalizedValue() |  | ||||||
|         for lang_code, lang_name in settings.LANGUAGES: |  | ||||||
|             localized_value.set(lang_code, lang_name) |  | ||||||
|  |  | ||||||
|         widget = LocalizedFieldWidget() |  | ||||||
|         decompressed_values = widget.decompress(localized_value) |  | ||||||
|  |  | ||||||
|         for (lang_code, _), value in zip(settings.LANGUAGES, decompressed_values): |  | ||||||
|             assert localized_value.get(lang_code) == value |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def test_decompress_none(): |  | ||||||
|         """Tests whether the :see:LocalizedFieldWidget correctly |  | ||||||
|         handles :see:None.""" |  | ||||||
|  |  | ||||||
|         widget = LocalizedFieldWidget() |  | ||||||
|         decompressed_values = widget.decompress(None) |  | ||||||
|  |  | ||||||
|         for _, value in zip(settings.LANGUAGES, decompressed_values): |  | ||||||
|             assert not value |  | ||||||
							
								
								
									
										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,7 @@ | |||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
| 
 | 
 | ||||||
| from localized_fields import LocalizedField, LocalizedValue | from localized_fields.fields import LocalizedField | ||||||
|  | from localized_fields.value import LocalizedValue | ||||||
| 
 | 
 | ||||||
| from .fake_model import get_fake_model | from .fake_model import get_fake_model | ||||||
| 
 | 
 | ||||||
| @@ -17,7 +18,6 @@ class LocalizedModelTestCase(TestCase): | |||||||
|         super(LocalizedModelTestCase, cls).setUpClass() |         super(LocalizedModelTestCase, cls).setUpClass() | ||||||
| 
 | 
 | ||||||
|         cls.TestModel = get_fake_model( |         cls.TestModel = get_fake_model( | ||||||
|             'LocalizedModelTestCase', |  | ||||||
|             { |             { | ||||||
|                 'title': LocalizedField() |                 'title': LocalizedField() | ||||||
|             } |             } | ||||||
| @@ -33,7 +33,6 @@ class LocalizedModelTestCase(TestCase): | |||||||
| 
 | 
 | ||||||
|         assert isinstance(obj.title, LocalizedValue) |         assert isinstance(obj.title, LocalizedValue) | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def test_model_init_kwargs(cls): |     def test_model_init_kwargs(cls): | ||||||
|         """Tests whether all :see:LocalizedField |         """Tests whether all :see:LocalizedField | ||||||
							
								
								
									
										45
									
								
								tests/test_query_set.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								tests/test_query_set.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | |||||||
|  | from django.test import TestCase | ||||||
|  |  | ||||||
|  | from localized_fields.fields import LocalizedField | ||||||
|  |  | ||||||
|  | from .fake_model import get_fake_model | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedQuerySetTestCase(TestCase): | ||||||
|  |     """Tests query sets with models containing :see:LocalizedField.""" | ||||||
|  |  | ||||||
|  |     Model = None | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def setUpClass(cls): | ||||||
|  |         """Creates the test models in the database.""" | ||||||
|  |  | ||||||
|  |         super(LocalizedQuerySetTestCase, cls).setUpClass() | ||||||
|  |  | ||||||
|  |         cls.Model = get_fake_model( | ||||||
|  |             { | ||||||
|  |                 'title': LocalizedField(), | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def test_assign_raw_dict(cls): | ||||||
|  |         inst = cls.Model() | ||||||
|  |         inst.title = dict(en='Bread', ro='Paine') | ||||||
|  |         inst.save() | ||||||
|  |  | ||||||
|  |         inst = cls.Model.objects.get(pk=inst.pk) | ||||||
|  |         assert inst.title.en == 'Bread' | ||||||
|  |         assert inst.title.ro == 'Paine' | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def test_assign_raw_dict_update(cls): | ||||||
|  |         inst = cls.Model.objects.create( | ||||||
|  |             title=dict(en='Bread', ro='Paine')) | ||||||
|  |  | ||||||
|  |         cls.Model.objects.update( | ||||||
|  |             title=dict(en='Beer', ro='Bere')) | ||||||
|  |  | ||||||
|  |         inst = cls.Model.objects.get(pk=inst.pk) | ||||||
|  |         assert inst.title.en == 'Beer' | ||||||
|  |         assert inst.title.ro == 'Bere' | ||||||
| @@ -1,13 +1,17 @@ | |||||||
| import copy | import copy | ||||||
| 
 | 
 | ||||||
| from django import forms | from django import forms | ||||||
|  | from django.db import models | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
| from django.db.utils import IntegrityError | from django.db.utils import IntegrityError | ||||||
| from localized_fields import (LocalizedField, LocalizedAutoSlugField, |  | ||||||
|                               LocalizedUniqueSlugField) |  | ||||||
| from django.utils.text import slugify | from django.utils.text import slugify | ||||||
| 
 | 
 | ||||||
|  | from localized_fields.fields import ( | ||||||
|  |     LocalizedField, | ||||||
|  |     LocalizedUniqueSlugField | ||||||
|  | ) | ||||||
|  | 
 | ||||||
| from .fake_model import get_fake_model | from .fake_model import get_fake_model | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @@ -15,7 +19,7 @@ class LocalizedSlugFieldTestCase(TestCase): | |||||||
|     """Tests the localized slug classes.""" |     """Tests the localized slug classes.""" | ||||||
| 
 | 
 | ||||||
|     AutoSlugModel = None |     AutoSlugModel = None | ||||||
|     MagicSlugModel = None |     Model = None | ||||||
| 
 | 
 | ||||||
|     @classmethod |     @classmethod | ||||||
|     def setUpClass(cls): |     def setUpClass(cls): | ||||||
| @@ -23,46 +27,14 @@ class LocalizedSlugFieldTestCase(TestCase): | |||||||
| 
 | 
 | ||||||
|         super(LocalizedSlugFieldTestCase, cls).setUpClass() |         super(LocalizedSlugFieldTestCase, cls).setUpClass() | ||||||
| 
 | 
 | ||||||
|         cls.AutoSlugModel = get_fake_model( |         cls.Model = get_fake_model( | ||||||
|             'LocalizedAutoSlugFieldTestModel', |  | ||||||
|             { |  | ||||||
|                 'title': LocalizedField(), |  | ||||||
|                 'slug': LocalizedAutoSlugField(populate_from='title') |  | ||||||
|             } |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
|         cls.MagicSlugModel = get_fake_model( |  | ||||||
|             'LocalizedUniqueSlugFieldTestModel', |  | ||||||
|             { |             { | ||||||
|                 'title': LocalizedField(), |                 'title': LocalizedField(), | ||||||
|  |                 'name': models.CharField(max_length=255), | ||||||
|                 'slug': LocalizedUniqueSlugField(populate_from='title') |                 'slug': LocalizedUniqueSlugField(populate_from='title') | ||||||
|             } |             } | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|     @classmethod |  | ||||||
|     def test_populate_auto(cls): |  | ||||||
|         cls._test_populate(cls.AutoSlugModel) |  | ||||||
| 
 |  | ||||||
|     @classmethod |  | ||||||
|     def test_populate_unique(cls): |  | ||||||
|         cls._test_populate(cls.MagicSlugModel) |  | ||||||
| 
 |  | ||||||
|     @classmethod |  | ||||||
|     def test_populate_multiple_languages_auto(cls): |  | ||||||
|         cls._test_populate_multiple_languages(cls.AutoSlugModel) |  | ||||||
| 
 |  | ||||||
|     @classmethod |  | ||||||
|     def test_populate_multiple_languages_unique(cls): |  | ||||||
|         cls._test_populate_multiple_languages(cls.MagicSlugModel) |  | ||||||
| 
 |  | ||||||
|     @classmethod |  | ||||||
|     def test_unique_slug_auto(cls): |  | ||||||
|         cls._test_unique_slug(cls.AutoSlugModel) |  | ||||||
| 
 |  | ||||||
|     @classmethod |  | ||||||
|     def test_unique_slug_unique(cls): |  | ||||||
|         cls._test_unique_slug(cls.MagicSlugModel) |  | ||||||
| 
 |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def test_unique_slug_with_time(): |     def test_unique_slug_with_time(): | ||||||
|         """Tests whether the primary key is included in |         """Tests whether the primary key is included in | ||||||
| @@ -71,7 +43,6 @@ class LocalizedSlugFieldTestCase(TestCase): | |||||||
|         title = 'myuniquetitle' |         title = 'myuniquetitle' | ||||||
| 
 | 
 | ||||||
|         PkModel = get_fake_model( |         PkModel = get_fake_model( | ||||||
|             'PkModel', |  | ||||||
|             { |             { | ||||||
|                 'title': LocalizedField(), |                 'title': LocalizedField(), | ||||||
|                 'slug': LocalizedUniqueSlugField(populate_from='title', include_time=True) |                 'slug': LocalizedUniqueSlugField(populate_from='title', include_time=True) | ||||||
| @@ -89,7 +60,6 @@ class LocalizedSlugFieldTestCase(TestCase): | |||||||
|         """Tests whether slugs are not re-generated if not needed.""" |         """Tests whether slugs are not re-generated if not needed.""" | ||||||
| 
 | 
 | ||||||
|         NoChangeSlugModel = get_fake_model( |         NoChangeSlugModel = get_fake_model( | ||||||
|             'NoChangeSlugModel', |  | ||||||
|             { |             { | ||||||
|                 'title': LocalizedField(), |                 'title': LocalizedField(), | ||||||
|                 'slug': LocalizedUniqueSlugField(populate_from='title', include_time=True) |                 'slug': LocalizedUniqueSlugField(populate_from='title', include_time=True) | ||||||
| @@ -111,62 +81,55 @@ class LocalizedSlugFieldTestCase(TestCase): | |||||||
|         assert old_slug_en == obj.slug.en |         assert old_slug_en == obj.slug.en | ||||||
|         assert old_slug_nl != obj.slug.nl |         assert old_slug_nl != obj.slug.nl | ||||||
| 
 | 
 | ||||||
|     def test_unique_slug_unique_max_retries(self): |     @classmethod | ||||||
|  |     def test_unique_slug_update(cls): | ||||||
|  |         obj = cls.Model.objects.create(title={settings.LANGUAGE_CODE: 'mytitle'}) | ||||||
|  |         assert obj.slug.get() == 'mytitle' | ||||||
|  |         obj.title.set(settings.LANGUAGE_CODE, 'othertitle') | ||||||
|  |         obj.save() | ||||||
|  |         assert obj.slug.get() == 'othertitle' | ||||||
|  | 
 | ||||||
|  |     @classmethod | ||||||
|  |     def test_unique_slug_unique_max_retries(cls): | ||||||
|         """Tests whether the unique slug implementation doesn't |         """Tests whether the unique slug implementation doesn't | ||||||
|         try to find a slug forever and gives up after a while.""" |         try to find a slug forever and gives up after a while.""" | ||||||
| 
 | 
 | ||||||
|         title = 'myuniquetitle' |         title = 'myuniquetitle' | ||||||
| 
 | 
 | ||||||
|         obj = self.MagicSlugModel() |         obj = cls.Model() | ||||||
|         obj.title.en = title |         obj.title.en = title | ||||||
|         obj.save() |         obj.save() | ||||||
| 
 | 
 | ||||||
|         with self.assertRaises(IntegrityError): |         with cls.assertRaises(cls, IntegrityError): | ||||||
|             for _ in range(0, settings.LOCALIZED_FIELDS_MAX_RETRIES + 1): |             for _ in range(0, settings.LOCALIZED_FIELDS_MAX_RETRIES + 1): | ||||||
|                 another_obj = self.MagicSlugModel() |                 another_obj = cls.Model() | ||||||
|                 another_obj.title.en = title |                 another_obj.title.en = title | ||||||
|                 another_obj.save() |                 another_obj.save() | ||||||
| 
 | 
 | ||||||
|     @classmethod |     @classmethod | ||||||
|     def test_unique_slug_utf_auto(cls): |     def test_populate(cls): | ||||||
|         cls._test_unique_slug_utf(cls.AutoSlugModel) |  | ||||||
| 
 |  | ||||||
|     @classmethod |  | ||||||
|     def test_unique_slug_utf_unique(cls): |  | ||||||
|         cls._test_unique_slug_utf(cls.MagicSlugModel) |  | ||||||
| 
 |  | ||||||
|     @classmethod |  | ||||||
|     def test_deconstruct_auto(cls): |  | ||||||
|         cls._test_deconstruct(LocalizedAutoSlugField) |  | ||||||
| 
 |  | ||||||
|     @classmethod |  | ||||||
|     def test_deconstruct_unique(cls): |  | ||||||
|         cls._test_deconstruct(LocalizedUniqueSlugField) |  | ||||||
| 
 |  | ||||||
|     @classmethod |  | ||||||
|     def test_formfield_auto(cls): |  | ||||||
|         cls._test_formfield(LocalizedAutoSlugField) |  | ||||||
| 
 |  | ||||||
|     @classmethod |  | ||||||
|     def test_formfield_unique(cls): |  | ||||||
|         cls._test_formfield(LocalizedUniqueSlugField) |  | ||||||
| 
 |  | ||||||
|     @staticmethod |  | ||||||
|     def _test_populate(model): |  | ||||||
|         """Tests whether the populating feature works correctly.""" |         """Tests whether the populating feature works correctly.""" | ||||||
| 
 | 
 | ||||||
|         obj = model() |         obj = cls.Model() | ||||||
|         obj.title.en = 'this is my title' |         obj.title.en = 'this is my title' | ||||||
|         obj.save() |         obj.save() | ||||||
| 
 | 
 | ||||||
|         assert obj.slug.get('en') == slugify(obj.title) |         assert obj.slug.get('en') == slugify(obj.title) | ||||||
| 
 | 
 | ||||||
|     @staticmethod |     @classmethod | ||||||
|     def _test_populate_multiple_languages(model): |     def test_populate_callable(cls): | ||||||
|         """Tests whether the populating feature correctly |         """Tests whether the populating feature works correctly | ||||||
|         works for all languages.""" |         when you specify a callable.""" | ||||||
| 
 | 
 | ||||||
|         obj = model() |         def generate_slug(instance): | ||||||
|  |             return instance.title | ||||||
|  | 
 | ||||||
|  |         get_fake_model({ | ||||||
|  |             'title': LocalizedField(), | ||||||
|  |             'slug': LocalizedUniqueSlugField(populate_from=generate_slug) | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         obj = cls.Model() | ||||||
|         for lang_code, lang_name in settings.LANGUAGES: |         for lang_code, lang_name in settings.LANGUAGES: | ||||||
|             obj.title.set(lang_code, 'title %s' % lang_name) |             obj.title.set(lang_code, 'title %s' % lang_name) | ||||||
| 
 | 
 | ||||||
| @@ -176,52 +139,120 @@ class LocalizedSlugFieldTestCase(TestCase): | |||||||
|             assert obj.slug.get(lang_code) == 'title-%s' % lang_name.lower() |             assert obj.slug.get(lang_code) == 'title-%s' % lang_name.lower() | ||||||
| 
 | 
 | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def _test_unique_slug(model): |     def test_populate_multiple_from_fields(): | ||||||
|  |         """Tests whether populating the slug from multiple | ||||||
|  |         fields works correctly.""" | ||||||
|  | 
 | ||||||
|  |         model = get_fake_model( | ||||||
|  |             { | ||||||
|  |                 'title': LocalizedField(), | ||||||
|  |                 'name': models.CharField(max_length=255), | ||||||
|  |                 'slug': LocalizedUniqueSlugField(populate_from=('title', 'name')) | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         obj = model() | ||||||
|  |         for lang_code, lang_name in settings.LANGUAGES: | ||||||
|  |             obj.name = 'swen' | ||||||
|  |             obj.title.set(lang_code, 'title %s' % lang_name) | ||||||
|  | 
 | ||||||
|  |         obj.save() | ||||||
|  | 
 | ||||||
|  |         for lang_code, lang_name in settings.LANGUAGES: | ||||||
|  |             assert obj.slug.get(lang_code) == 'title-%s-swen' % lang_name.lower() | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def test_populate_multiple_from_fields_fk(): | ||||||
|  |         """Tests whether populating the slug from multiple | ||||||
|  |         fields works correctly.""" | ||||||
|  | 
 | ||||||
|  |         model_fk = get_fake_model( | ||||||
|  |             { | ||||||
|  |                 'name': LocalizedField(), | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         model = get_fake_model( | ||||||
|  |             { | ||||||
|  |                 'title': LocalizedField(), | ||||||
|  |                 'other': models.ForeignKey(model_fk, on_delete=models.CASCADE), | ||||||
|  |                 'slug': LocalizedUniqueSlugField(populate_from=('title', 'other.name')) | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         other = model_fk.objects.create(name={settings.LANGUAGE_CODE: 'swen'}) | ||||||
|  | 
 | ||||||
|  |         obj = model() | ||||||
|  |         for lang_code, lang_name in settings.LANGUAGES: | ||||||
|  |             obj.other_id = other.id | ||||||
|  |             obj.title.set(lang_code, 'title %s' % lang_name) | ||||||
|  | 
 | ||||||
|  |         obj.save() | ||||||
|  | 
 | ||||||
|  |         for lang_code, lang_name in settings.LANGUAGES: | ||||||
|  |             assert obj.slug.get(lang_code) == 'title-%s-swen' % lang_name.lower() | ||||||
|  | 
 | ||||||
|  |     @classmethod | ||||||
|  |     def test_populate_multiple_languages(cls): | ||||||
|  |         """Tests whether the populating feature correctly | ||||||
|  |         works for all languages.""" | ||||||
|  | 
 | ||||||
|  |         obj = cls.Model() | ||||||
|  |         for lang_code, lang_name in settings.LANGUAGES: | ||||||
|  |             obj.title.set(lang_code, 'title %s' % lang_name) | ||||||
|  | 
 | ||||||
|  |         obj.save() | ||||||
|  | 
 | ||||||
|  |         for lang_code, lang_name in settings.LANGUAGES: | ||||||
|  |             assert obj.slug.get(lang_code) == 'title-%s' % lang_name.lower() | ||||||
|  | 
 | ||||||
|  |     @classmethod | ||||||
|  |     def test_unique_slug(cls): | ||||||
|         """Tests whether unique slugs are properly generated.""" |         """Tests whether unique slugs are properly generated.""" | ||||||
| 
 | 
 | ||||||
|         title = 'myuniquetitle' |         title = 'myuniquetitle' | ||||||
| 
 | 
 | ||||||
|         obj = model() |         obj = cls.Model() | ||||||
|         obj.title.en = title |         obj.title.en = title | ||||||
|         obj.save() |         obj.save() | ||||||
| 
 | 
 | ||||||
|         for i in range(1, settings.LOCALIZED_FIELDS_MAX_RETRIES - 1): |         for i in range(1, settings.LOCALIZED_FIELDS_MAX_RETRIES - 1): | ||||||
|             another_obj = model() |             another_obj = cls.Model() | ||||||
|             another_obj.title.en = title |             another_obj.title.en = title | ||||||
|             another_obj.save() |             another_obj.save() | ||||||
| 
 | 
 | ||||||
|             assert another_obj.slug.en == '%s-%d' % (title, i) |             assert another_obj.slug.en == '%s-%d' % (title, i) | ||||||
| 
 | 
 | ||||||
|     @staticmethod |     @classmethod | ||||||
|     def _test_unique_slug_utf(model): |     def test_unique_slug_utf(cls): | ||||||
|         """Tests whether generating a slug works |         """Tests whether generating a slug works | ||||||
|         when the value consists completely out |         when the value consists completely out | ||||||
|         of non-ASCII characters.""" |         of non-ASCII characters.""" | ||||||
| 
 | 
 | ||||||
|         obj = model() |         obj = cls.Model() | ||||||
|         obj.title.en = 'مكاتب للايجار بشارع بورسعيد' |         obj.title.en = 'مكاتب للايجار بشارع بورسعيد' | ||||||
|         obj.save() |         obj.save() | ||||||
| 
 | 
 | ||||||
|         assert obj.slug.en == 'مكاتب-للايجار-بشارع-بورسعيد' |         assert obj.slug.en == 'مكاتب-للايجار-بشارع-بورسعيد' | ||||||
| 
 | 
 | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def _test_deconstruct(field_type): |     def test_deconstruct(): | ||||||
|         """Tests whether the :see:deconstruct |         """Tests whether the :see:deconstruct | ||||||
|         function properly retains options |         function properly retains options | ||||||
|         specified in the constructor.""" |         specified in the constructor.""" | ||||||
| 
 | 
 | ||||||
|         field = field_type(populate_from='title') |         field = LocalizedUniqueSlugField(populate_from='title') | ||||||
|         _, _, _, kwargs = field.deconstruct() |         _, _, _, kwargs = field.deconstruct() | ||||||
| 
 | 
 | ||||||
|         assert 'populate_from' in kwargs |         assert 'populate_from' in kwargs | ||||||
|         assert kwargs['populate_from'] == field.populate_from |         assert kwargs['populate_from'] == field.populate_from | ||||||
| 
 | 
 | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def _test_formfield(field_type): |     def test_formfield(): | ||||||
|         """Tests whether the :see:formfield method |         """Tests whether the :see:formfield method | ||||||
|         returns a valid form field that is hidden.""" |         returns a valid form field that is hidden.""" | ||||||
| 
 | 
 | ||||||
|         form_field = field_type(populate_from='title').formfield() |         form_field = LocalizedUniqueSlugField(populate_from='title').formfield() | ||||||
| 
 | 
 | ||||||
|         assert isinstance(form_field, forms.CharField) |         assert isinstance(form_field, forms.CharField) | ||||||
|         assert isinstance(form_field.widget, forms.HiddenInput) |         assert isinstance(form_field.widget, forms.HiddenInput) | ||||||
							
								
								
									
										215
									
								
								tests/test_value.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										215
									
								
								tests/test_value.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,215 @@ | |||||||
|  | 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 .data import get_init_values | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedValueTestCase(TestCase): | ||||||
|  |     """Tests the :see:LocalizedValue class.""" | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def tearDown(): | ||||||
|  |         """Assures that the current language | ||||||
|  |         is set back to the default.""" | ||||||
|  |  | ||||||
|  |         translation.activate(settings.LANGUAGE_CODE) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_init(): | ||||||
|  |         """Tests whether the __init__ function | ||||||
|  |         of the :see:LocalizedValue class works | ||||||
|  |         as expected.""" | ||||||
|  |  | ||||||
|  |         keys = get_init_values() | ||||||
|  |         value = LocalizedValue(keys) | ||||||
|  |  | ||||||
|  |         for lang_code, _ in settings.LANGUAGES: | ||||||
|  |             assert getattr(value, lang_code, None) == keys[lang_code] | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_init_default_values(): | ||||||
|  |         """Tests whether the __init__ function | ||||||
|  |         of the :see:LocalizedValue accepts the | ||||||
|  |         default value or an empty dict properly.""" | ||||||
|  |  | ||||||
|  |         value = LocalizedValue() | ||||||
|  |  | ||||||
|  |         for lang_code, _ in settings.LANGUAGES: | ||||||
|  |             assert getattr(value, lang_code) is None | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_init_array(): | ||||||
|  |         """Tests whether the __init__ function | ||||||
|  |         of :see:LocalizedValue properly handles an | ||||||
|  |         array. | ||||||
|  |  | ||||||
|  |         Arrays can be passed to LocalizedValue as | ||||||
|  |         a result of a ArrayAgg operation.""" | ||||||
|  |  | ||||||
|  |         value = LocalizedValue(['my value']) | ||||||
|  |         assert value.get(settings.LANGUAGE_CODE) == 'my value' | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_get_explicit(): | ||||||
|  |         """Tests whether the the :see:LocalizedValue | ||||||
|  |         class's :see:get function works properly | ||||||
|  |         when specifying an explicit value.""" | ||||||
|  |  | ||||||
|  |         keys = get_init_values() | ||||||
|  |         localized_value = LocalizedValue(keys) | ||||||
|  |  | ||||||
|  |         for language, value in keys.items(): | ||||||
|  |             assert localized_value.get(language) == value | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_get_default_language(): | ||||||
|  |         """Tests whether the :see:LocalizedValue | ||||||
|  |         class's see:get function properly | ||||||
|  |         gets the value in the default language.""" | ||||||
|  |  | ||||||
|  |         keys = get_init_values() | ||||||
|  |         localized_value = LocalizedValue(keys) | ||||||
|  |  | ||||||
|  |         for language, _ in keys.items(): | ||||||
|  |             translation.activate(language) | ||||||
|  |             assert localized_value.get() == keys[settings.LANGUAGE_CODE] | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_set(): | ||||||
|  |         """Tests whether the :see:LocalizedValue | ||||||
|  |         class's see:set function works properly.""" | ||||||
|  |  | ||||||
|  |         localized_value = LocalizedValue() | ||||||
|  |  | ||||||
|  |         for language, value in get_init_values(): | ||||||
|  |             localized_value.set(language, value) | ||||||
|  |             assert localized_value.get(language) == value | ||||||
|  |             assert getattr(localized_value, language) == value | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_eq(): | ||||||
|  |         """Tests whether the __eq__ operator | ||||||
|  |         of :see:LocalizedValue works properly.""" | ||||||
|  |  | ||||||
|  |         a = LocalizedValue({'en': 'a', 'ar': 'b'}) | ||||||
|  |         b = LocalizedValue({'en': 'a', 'ar': 'b'}) | ||||||
|  |  | ||||||
|  |         assert a == b | ||||||
|  |  | ||||||
|  |         b.en = 'b' | ||||||
|  |         assert a != b | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_translate(): | ||||||
|  |         """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 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' | ||||||
|  |  | ||||||
|  |         localized_value = LocalizedValue({ | ||||||
|  |             settings.LANGUAGE_CODE: test_value | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |         other_language = settings.LANGUAGES[-1][0] | ||||||
|  |  | ||||||
|  |         # make sure that, by default it returns | ||||||
|  |         # the value in the default language | ||||||
|  |         assert 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 localized_value.translate() == test_value | ||||||
|  |  | ||||||
|  |         # make sure that it's just __str__ falling | ||||||
|  |         # back and that for the other language | ||||||
|  |         # there's no actual value | ||||||
|  |         assert localized_value.get(other_language) != test_value | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_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 | ||||||
|  |         translate()'s fallback functionality properly respects | ||||||
|  |         the LOCALIZED_FIELDS_FALLBACKS setting.""" | ||||||
|  |  | ||||||
|  |         fallbacks = { | ||||||
|  |             'nl': ['ro'] | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         localized_value = LocalizedValue({ | ||||||
|  |             settings.LANGUAGE_CODE: settings.LANGUAGE_CODE, | ||||||
|  |             'ro': 'ro' | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |         with override_settings(LOCALIZED_FIELDS_FALLBACKS=fallbacks): | ||||||
|  |             with translation.override('nl'): | ||||||
|  |                 assert localized_value.translate() == 'ro' | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_deconstruct(): | ||||||
|  |         """Tests whether the :see:LocalizedValue | ||||||
|  |         class's :see:deconstruct function works properly.""" | ||||||
|  |  | ||||||
|  |         keys = get_init_values() | ||||||
|  |         value = LocalizedValue(keys) | ||||||
|  |  | ||||||
|  |         path, args, kwargs = value.deconstruct() | ||||||
|  |  | ||||||
|  |         assert args[0] == keys | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_construct_string(): | ||||||
|  |         """Tests whether the :see:LocalizedValue's constructor | ||||||
|  |         assumes the primary language when passing a single string.""" | ||||||
|  |  | ||||||
|  |         value = LocalizedValue('beer') | ||||||
|  |         assert value.get(settings.LANGUAGE_CODE) == 'beer' | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_construct_expression(): | ||||||
|  |         """Tests whether passing expressions as values | ||||||
|  |         works properly and are not converted to string.""" | ||||||
|  |  | ||||||
|  |         value = LocalizedValue(dict(en=F('other'))) | ||||||
|  |         assert isinstance(value.en, F) | ||||||
							
								
								
									
										83
									
								
								tests/test_widget.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								tests/test_widget.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | |||||||
|  | import re | ||||||
|  | from django.conf import settings | ||||||
|  | from django.test import TestCase | ||||||
|  |  | ||||||
|  | from localized_fields.value import LocalizedValue | ||||||
|  | from localized_fields.widgets import LocalizedFieldWidget | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalizedFieldWidgetTestCase(TestCase): | ||||||
|  |     """Tests the workings of the :see:LocalizedFieldWidget class.""" | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_widget_creation(): | ||||||
|  |         """Tests whether a widget is created for every | ||||||
|  |         language correctly.""" | ||||||
|  |  | ||||||
|  |         widget = LocalizedFieldWidget() | ||||||
|  |         assert len(widget.widgets) == len(settings.LANGUAGES) | ||||||
|  |         assert len(set(widget.widgets)) == len(widget.widgets) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_decompress(): | ||||||
|  |         """Tests whether a :see:LocalizedValue instance | ||||||
|  |         can correctly be "decompressed" over the available | ||||||
|  |         widgets.""" | ||||||
|  |  | ||||||
|  |         localized_value = LocalizedValue() | ||||||
|  |         for lang_code, lang_name in settings.LANGUAGES: | ||||||
|  |             localized_value.set(lang_code, lang_name) | ||||||
|  |  | ||||||
|  |         widget = LocalizedFieldWidget() | ||||||
|  |         decompressed_values = widget.decompress(localized_value) | ||||||
|  |  | ||||||
|  |         for (lang_code, _), value in zip(settings.LANGUAGES, decompressed_values): | ||||||
|  |             assert localized_value.get(lang_code) == value | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_decompress_none(): | ||||||
|  |         """Tests whether the :see:LocalizedFieldWidget correctly | ||||||
|  |         handles :see:None.""" | ||||||
|  |  | ||||||
|  |         widget = LocalizedFieldWidget() | ||||||
|  |         decompressed_values = widget.decompress(None) | ||||||
|  |  | ||||||
|  |         for _, value in zip(settings.LANGUAGES, decompressed_values): | ||||||
|  |             assert not value | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_get_context_required(): | ||||||
|  |         """Tests whether the :see:get_context correctly | ||||||
|  |         handles 'required' attribute, separately for each subwidget.""" | ||||||
|  |  | ||||||
|  |         widget = LocalizedFieldWidget() | ||||||
|  |         widget.widgets[0].is_required = True | ||||||
|  |         widget.widgets[1].is_required = False | ||||||
|  |         context = widget.get_context(name='test', value=LocalizedValue(), | ||||||
|  |                                      attrs=dict(required=True)) | ||||||
|  |         assert context['widget']['subwidgets'][0]['attrs']['required'] | ||||||
|  |         assert 'required' not in context['widget']['subwidgets'][1]['attrs'] | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_get_context_langs(): | ||||||
|  |         """Tests whether the :see:get_context contains 'lang_code' and | ||||||
|  |         'lang_name' attribute for each subwidget.""" | ||||||
|  |  | ||||||
|  |         widget = LocalizedFieldWidget() | ||||||
|  |         context = widget.get_context(name='test', value=LocalizedValue(), | ||||||
|  |                                      attrs=dict()) | ||||||
|  |         subwidgets_context = context['widget']['subwidgets'] | ||||||
|  |         for widget, context in zip(widget.widgets, subwidgets_context): | ||||||
|  |             assert 'lang_code' in context | ||||||
|  |             assert 'lang_name' in context | ||||||
|  |             assert widget.lang_code == context['lang_code'] | ||||||
|  |             assert widget.lang_name == context['lang_name'] | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def test_render(): | ||||||
|  |         """Tests whether the :see:LocalizedFieldWidget correctly | ||||||
|  |         render.""" | ||||||
|  |  | ||||||
|  |         widget = LocalizedFieldWidget() | ||||||
|  |         output = widget.render(name='title', value=None) | ||||||
|  |         assert bool(re.search('<label (.|\n|\t)*>\w+<\/label>', output)) | ||||||
							
								
								
									
										14
									
								
								tox.ini
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								tox.ini
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | [tox] | ||||||
|  | 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 | ||||||
|  | passenv = DATABASE_URL | ||||||
|  | commands = coverage run manage.py test | ||||||
		Reference in New Issue
	
	Block a user