185 Commits
v3.4 ... v5.0a7

Author SHA1 Message Date
Swen Kooij
eb2cb6b244 Bump version number to 5.0a7 2019-01-14 11:00:20 +02:00
Swen Kooij
ed15fb0079 Upgrade django-postgres-extra>=1.21a15 2019-01-14 11:00:12 +02:00
Swen Kooij
f59904f8ea Bump version number to 5.0a6 2019-01-11 15:03:19 +02:00
Swen Kooij
f20966d6d2 Fix tests not passing for Django 2.X 2019-01-11 15:02:22 +02:00
Swen Kooij
25417b5815 Merge pull request #56 from sliverc/user_defined_pk_descriptor
Avoid DoesNotExist error when creating model with user defined pk
2019-01-11 14:47:33 +02:00
Swen Kooij
abd1587ca0 Merge pull request #54 from sliverc/query_by_active_lang
Add support for localized query lookups
2019-01-11 14:47:18 +02:00
Swen Kooij
60fc79e9ff Merge pull request #57 from velrest/master
Fix typo in documentation for clean
2019-01-11 14:46:21 +02:00
Swen Kooij
ca470fc577 Merge pull request #49 from MELScience/admin-fix
Add tests for LocalizedFieldsAdminMixin
2019-01-11 14:46:10 +02:00
Swen Kooij
fac1a595aa Merge pull request #55 from sliverc/hstore_extension
Enable HStore extension for localized fields to work
2019-01-11 14:44:05 +02:00
Swen Kooij
d66f5085a8 Merge branch 'master' into hstore_extension 2019-01-11 14:40:38 +02:00
Swen Kooij
c6e8321ae7 Add CircleCI badge to README 2019-01-11 14:40:27 +02:00
Swen Kooij
b2f50ec82b Convert to use CircleCI and run tests against Django 2.1/Python 3.7 2019-01-11 14:37:03 +02:00
Oliver Sauder
ff836836bf Add support for localized query look ups 2018-12-03 09:45:08 +01:00
Swen Kooij
acf8867974 Merge pull request #52 from SectorLabs/localized-integer-field-widget
Add LocalizedIntegerFieldWidget
2018-09-24 15:16:55 +03:00
Cristi Ingineru
4922a1b93f self.translate() 2018-09-24 12:57:46 +03:00
Jonas Cosandey
d308e773cf fix typo 2018-09-12 14:27:44 +02:00
Oliver Sauder
b3b88d6d28 Avoid does not exist error when creating model with user defined pk 2018-09-10 12:05:46 +02:00
Oliver Sauder
6c902229ce Enable HStore extension for localized fields to work 2018-08-27 15:07:41 +02:00
Cristi Ingineru
4f83cbf4ed Add LocalizedIntegerFieldWidget 2018-08-16 14:27:38 +03:00
Swen Kooij
88e2d29596 Bump version number to 5.0a3 2018-06-28 12:39:25 +03:00
Swen Kooij
588f32086b Merge pull request #51 from SectorLabs/multiwidget
Copy the widget for each language
2018-06-28 12:39:02 +03:00
Cristi Ingineru
13e2666a51 Copy the widget for each language 2018-06-28 12:33:44 +03:00
Swen Kooij
2393539b44 Bump version number to 5.0a2 2018-06-19 12:30:04 +03:00
Swen Kooij
e322b3d63b Upgrade django-postgres-extra to 1.21a11 2018-06-19 12:29:55 +03:00
Swen Kooij
1b1d24a460 Make defaults work for LocalizedIntegerField 2018-06-15 17:07:12 +03:00
Swen Kooij
fb233e8f25 Make sure values are strings before saving LocalizedIntegerValue 2018-06-15 16:19:32 +03:00
Swen Kooij
bca94a3508 Add quick docs on LocalizedIntegerField 2018-06-15 13:04:00 +03:00
Swen Kooij
8c83fa6b49 Bump version number to 5.0a1 2018-06-15 12:58:27 +03:00
Swen Kooij
90597da8fd Add a LocalizedIntegerField 2018-06-15 12:58:01 +03:00
Swen Kooij
752e17064d Deprecate LocalizedFileValue.localized() 2018-06-14 08:01:10 +03:00
Swen Kooij
def7dae640 Add LocalizedValue.translate()
LocalizedValue.translate() behaves the exact same as the str(..) cast
works, with the exception that it returns None if there is no value
instead of an empty string. This makes it easier to implement custom
value classes on top of the LocalizedValue class.

Behavior for str(..) stays the same as it was.
2018-06-14 07:57:02 +03:00
seroy
8ba0540237 Fix using LocalizedFieldsAdminMixin with inlines 2018-04-20 02:33:15 +03:00
seroy
b3624916b2 Add tests for LocalizedFieldsAdminMixin 2018-04-20 02:27:57 +03:00
Swen Kooij
db4324fbf3 Bump version number to 4.6a3 2018-04-02 15:59:42 +03:00
Swen Kooij
9ff0b781ab Upgrade django-postgres-extra to 1.21a9 2018-04-02 15:59:27 +03:00
Swen Kooij
a76101c9ad Fix LocalizedFieldsAdminMixin not having a base class
This was a breaking change and broke a lot of projects.
2018-04-02 15:59:16 +03:00
Swen Kooij
04f15363bb Upgrade django-postgres-extra to 1.21a8 2018-04-01 18:28:43 +03:00
Swen Kooij
a849866bcb Make it more clear Django 2.X is supported 2018-03-31 17:05:39 +03:00
Swen Kooij
5e58bc6e24 Revert "Use standard postgres on Scrutinizer"
This reverts commit 04a750053f.
2018-03-31 16:59:11 +03:00
Swen Kooij
04a750053f Use standard postgres on Scrutinizer 2018-03-31 16:56:01 +03:00
Swen Kooij
d25b1b92fe Merge branch 'master' of https://github.com/SectorLabs/django-localized-fields 2018-03-31 16:53:48 +03:00
Swen Kooij
ccc46e1899 Allow raw dicts to be used in update statements 2018-03-31 16:53:10 +03:00
Swen Kooij
9063509817 Merge pull request #47 from MELScience/fix-admin-widget-inline-tabs
fix admin widget tabs width in inline formset
2018-03-31 16:47:36 +03:00
Swen Kooij
8fbe3e8680 Merge branch 'master' of https://github.com/SectorLabs/django-localized-fields 2018-03-31 16:42:08 +03:00
Swen Kooij
b9f14322be Bump version to 4.6a2 2018-03-31 16:40:50 +03:00
Swen Kooij
24974b2f6b Upgrade django-postgres-extra to 1.21a7 2018-03-31 16:40:29 +03:00
Swen Kooij
af02593ebd Add test to make sure LocalizedValue doesn't cast values to string 2018-03-31 08:52:12 +03:00
seroy
f161b5d047 fix admin widget tabs width in inline formset 2018-03-01 13:22:54 +03:00
Swen Kooij
77f626fc69 Merge pull request #46 from MELScience/fix-admin-widget-inline
fix admin widget work with inline formsets
2018-02-20 09:14:40 +02:00
seroy
d7889b0601 use label tag + for attribute instead of a + href for properly work with inline formsets 2018-02-20 00:58:08 +03:00
Swen Kooij
09cd0a8177 Merge pull request #44 from si14/patch-4
fix LocalizedValue.deconstruct (wrong module name)
2018-02-04 18:00:48 +02:00
Dmitry Groshev
2a29efdf14 fix LocalizedValue.deconstruct (wrong module name) 2018-01-31 23:59:48 +00:00
Dmitry Groshev
d8eb3394d0 LocalizedFieldsAdminMixin can be used with inlines
LocalizedFieldsAdminMixin was inheriting from ModelAdmin. This means in a code like this

```python
class FooInline(LocalizedFieldsAdminMixin, admin.TabularMixin):
    pass
```

`__init__` was being resolved to `ModelAdmin.__init__` which is clearly wrong.
2018-01-29 13:39:47 +02:00
Swen Kooij
b27f1535ee Merge pull request #43 from SectorLabs/fix-test-on-django20
Fix test on django20
2018-01-29 13:39:09 +02:00
Swen Kooij
cd844fccec Add support for tox to run tests against Django 1.X/2.0 2018-01-29 13:34:40 +02:00
seroy
6a4beca193 fix tests on Django 2.0 2018-01-29 13:30:43 +02:00
Swen Kooij
bf0383d742 Upgrade to alpha version of django-postgres-extra (1.21a4) 2018-01-29 12:37:44 +02:00
Swen Kooij
879b857aa5 Merge pull request #41 from MELScience/update-requirements
update requirements
2018-01-24 23:53:02 +02:00
seroy
59e8e18b2a update requirements 2018-01-22 01:19:23 +03:00
Swen Kooij
11cfe5b6e1 Add unaizalakain to the contributor list 2017-07-18 15:47:07 +03:00
Swen Kooij
084f5dd0b6 Merge pull request #36 from MELScience/issue35
Fix non-valid HTML tags attributes
2017-07-18 14:37:11 +03:00
Swen Kooij
83605cde13 Merge pull request #37 from MELScience/gitignore
Gitignore
2017-07-18 14:36:46 +03:00
seroy
7437b58f20 Ignore PyCharm 2017-07-18 14:17:45 +03:00
seroy
db6664c84f Remove wrong filename 2017-07-18 14:16:46 +03:00
seroy
928c4c624d Add test whether get_context contains 'lang_code' and 'lang_name' attribute 2017-07-18 13:54:53 +03:00
seroy
3d4f9c413e Fix non-valid HTML tags attributes 2017-07-18 13:52:17 +03:00
Swen Kooij
f434566338 Bump version number to 4.5 2017-07-18 09:40:55 +03:00
Swen Kooij
2807ed10c8 Add target link for the license Badge 2017-07-18 09:40:29 +03:00
Swen Kooij
d5f43c783a Merge pull request #34 from MELScience/widget_refactor
Use template-based widget rendering in AdminLocalizedFieldWidget
2017-07-18 09:39:00 +03:00
Swen Kooij
5347db6d8f Add contributors list to README 2017-07-18 09:38:51 +03:00
Swen Kooij
1accee0b59 Merge branch 'master' into widget_refactor 2017-07-18 09:33:59 +03:00
Swen Kooij
05bcd84a88 Merge pull request #29 from MELScience/required
Improved functionality of required parameter
2017-07-18 09:33:24 +03:00
Swen Kooij
1c2e013695 Add Django 1.11 as a dependency to setup.py
Fixes #32
2017-07-18 09:31:47 +03:00
Swen Kooij
8b76948328 Merge pull request #33 from MELScience/issue32
Django requirement updated
2017-07-18 09:29:09 +03:00
seroy
940587d748 Updated django requirement 2017-07-18 01:06:49 +03:00
seroy
6522e38f18 Added tests for LocalizedFieldWidget.get_context method 2017-07-18 01:03:50 +03:00
seroy
69cf0df166 Use template-based widget rendering in AdminLocalizedFieldWidget 2017-07-18 00:29:44 +03:00
seroy
d8c5544e91 Use template-based widget rendering in AdminLocalizedFieldWidget 2017-07-17 22:53:47 +03:00
seroy
ccc41f9143 Updated django requirement in documentation 2017-07-17 22:16:40 +03:00
seroy
073846d74b Updated django requirement 2017-07-17 22:10:19 +03:00
seroy
0f08eb8280 Add test on render method 2017-07-17 22:05:31 +03:00
seroy
60d14069d8 Updated 'required' attribute in documentation 2017-07-17 20:57:59 +03:00
seroy
33e9709373 Add tests for 'required' attribute 2017-07-17 20:55:13 +03:00
seroy
c4bf151938 Refactor required_langs into required 2017-07-17 20:53:50 +03:00
Swen Kooij
7d629c186d Add basic tests for 'required' attribute 2017-07-16 12:22:48 +03:00
Swen Kooij
cff388cae3 Add PEP8 and Flake8 as part of Scrutinizer test 2017-07-15 14:13:51 +03:00
Swen Kooij
08690ab361 Fix outstanding PEP8 and Flake8 issues 2017-07-15 14:06:50 +03:00
Swen Kooij
5ac05efbd0 Slight clean up, use dict instead of {} 2017-07-15 14:04:09 +03:00
Swen Kooij
aff41f671a Implement code review suggestions for #29 2017-07-15 13:59:39 +03:00
Swen Kooij
a38d53b609 Merge pull request #30 from SectorLabs/subwidgets-labels
Subwidgets labels
2017-07-15 13:56:29 +03:00
Swen Kooij
968840188d Formatting cleanup in widgets.py 2017-07-15 13:48:54 +03:00
Unai Zalakain
e8e044f6e2 Add labels to localized subwidgets 2017-07-15 13:45:10 +03:00
seroy
aaf49614f2 improve functionality of required parameter 2017-07-14 15:07:48 +03:00
Swen Kooij
d4c24dea97 Merge pull request #28 from MELScience/fix_imports_in_doc
fix imports in README
2017-07-11 11:21:57 +03:00
seroy
b38ded0cc3 fix imports for LocalizedCharField, LocalizedTextField and LocalizedFileField 2017-07-11 11:03:21 +03:00
Swen Kooij
226d47e766 Merge pull request #27 from MELScience/issue26
Fix incorrect indentation
2017-07-11 10:40:48 +03:00
seroy
cc4bfb48b9 Fix incorrect indentation 2017-07-11 10:27:07 +03:00
Swen Kooij
d14859a45b Bump version number to 4.4 2017-06-29 18:56:33 +03:00
Swen Kooij
cb7fda5abc Merge branch 'master' of https://github.com/SectorLabs/django-localized-fields 2017-06-29 18:52:23 +03:00
Swen Kooij
9000635f1f Open README as UTF-8 2017-06-29 18:51:53 +03:00
Swen Kooij
dabeb3b79f Merge pull request #24 from MELScience/ff_value_to_string
Added `value_to_string` method
2017-06-27 10:49:17 +03:00
seroy
0b4bb7295e Added value_to_string method 2017-06-26 18:27:03 +03:00
seroy
2b34b6751e Added test for value_to_string method 2017-06-26 18:08:15 +03:00
Swen Kooij
32696f4e1e Add test that confirms slug is re-computed when value changes 2017-06-26 14:01:25 +03:00
Swen Kooij
3ce57ed4cc Bump version number to 4.3 2017-06-26 13:37:02 +03:00
Swen Kooij
7316d312b4 Add simple test for LOCALIZED_FIELDS_FALLBACKS setting 2017-06-26 13:36:21 +03:00
Swen Kooij
16e23963cc Add support for LOCALIZED_FIELDS_FALLBACKS 2017-06-26 13:27:52 +03:00
Swen Kooij
b10472d3e9 Officially deprecate LocalizedAutoSlugField 2017-06-26 13:10:21 +03:00
Swen Kooij
833ceb849c Update docs on enhanced LocalizedUniqueSlugField 2017-06-26 13:07:18 +03:00
Swen Kooij
d7382fbf30 Add support for using a callable to populate slug with 2017-06-26 13:03:41 +03:00
Swen Kooij
8ad9268426 Remove tests for LocalizedAutoSlug 2017-06-26 12:52:42 +03:00
Swen Kooij
96ddc77cfc Fake models now have generated names 2017-06-26 12:44:09 +03:00
Swen Kooij
51fc6959d2 Support for slugging from multiple fields 2017-06-26 12:34:50 +03:00
Swen Kooij
3b28a5e707 Fix PEP8 violations 2017-06-26 11:33:25 +03:00
Swen Kooij
06873afbda Merge pull request #15 from MELScience/extra-fields
Extra fields
2017-06-21 11:33:30 +03:00
Swen Kooij
f7c14f897c Bump version number to 4.2 2017-06-20 18:04:44 +03:00
Swen Kooij
e5e3068835 Merge pull request #22 from MELScience/issue17
Added templates and static folders
2017-06-20 18:03:11 +03:00
seroy
de7d3b9a21 Tests excluded 2017-06-20 17:22:24 +03:00
seroy
fd47deaa2e Tests excluded 2017-06-20 16:37:49 +03:00
seroy
4b6d997ddd Upgrade django-postgres-extra to 1.11 2017-06-20 16:30:58 +03:00
seroy
68d6991608 Added templates and static folders 2017-06-20 14:52:54 +03:00
seroy
e5d7cd25e2 Shorten names for everything 2017-06-19 21:58:48 +03:00
seroy
236ce1648c Upgrade django-postgres-extra to 1.11 2017-06-19 21:49:24 +03:00
seroy
aacc712195 Merge branch 'master' of https://github.com/SectorLabs/django-localized-fields into extra-fields 2017-06-19 21:40:11 +03:00
seroy
d1790f1fc1 added missing 'r' in type LocalizedStringValue 2017-06-19 17:17:24 +03:00
Swen Kooij
c75c1764e2 Improve test case for bulk_create 2017-05-31 11:38:53 +03:00
Swen Kooij
6736b3b32d Simplify test case for bulk_create 2017-05-31 11:33:20 +03:00
Swen Kooij
b97a7f3c23 Fix crash when using LocalizedUniqueSlugField in a bulk_create 2017-05-31 11:31:04 +03:00
Swen Kooij
1b036dc1de Add simple test to verify LocalizedField can be used in bulk_create 2017-05-31 11:19:53 +03:00
Swen Kooij
ea7733670d Bump version to 4.1 2017-05-30 16:38:08 +03:00
Swen Kooij
a8dc4fe837 Fix bug/typo in LocalizedField.from_db_value 2017-05-30 13:48:43 +03:00
Swen Kooij
3c8bea0fc3 Add test for LocalizedRef in combination with ArrayAgg 2017-05-30 13:47:48 +03:00
Swen Kooij
2741a6a2a2 Add extra tests for LocalizedRef 2017-05-30 13:45:43 +03:00
Swen Kooij
06f7ee15f0 Add LocalizedRef expression for extracting the value in the current language 2017-05-30 13:38:27 +03:00
Swen Kooij
e5214b07ae Fix aggregation not expanding into a actual list 2017-05-30 13:07:07 +03:00
Swen Kooij
2d5fe0be05 Fix documentation on INSTALLED_APPS 2017-05-26 17:38:49 +03:00
Swen Kooij
4305696f1b Fix pep8 issue, use two spaces before inline comment 2017-05-26 17:05:52 +03:00
Swen Kooij
8c73c9ab77 Fix some mistakes in the README 2017-05-26 17:05:05 +03:00
Swen Kooij
92a53bc3d7 Fix various pep8/flake8/pylint errors 2017-05-25 19:40:03 +03:00
Swen Kooij
5a4f449363 Fix support for ArrayAgg 2017-05-25 19:23:52 +03:00
Swen Kooij
0fa79ddbb0 Bump version to 4.0 2017-05-25 19:16:35 +03:00
Swen Kooij
2205f9c6a4 Move LocalizedValueTest into dedicated file 2017-05-25 19:16:04 +03:00
Swen Kooij
84c267330f Update docs on new import style 2017-05-25 19:11:39 +03:00
Swen Kooij
a1a02552b7 Shorten names for everything 2017-05-25 19:06:44 +03:00
Swen Kooij
bb84d7577c BREAKING CHANGE: Empty out __init__
It is bad practice to do this in Django. If somebody imports something
from the package before Django is loaded, you get a 'Apps aren't loaded yet.'
exception.
2017-05-25 18:51:11 +03:00
Swen Kooij
5e0343801f Restore LocalizedModel to its former glory as an convient way to get all the mixins 2017-05-25 18:50:20 +03:00
Swen Kooij
2df2ec8b36 Move LocalizedValueDescriptor into its own file 2017-05-25 18:45:21 +03:00
Swen Kooij
3fcaece894 Add missing docs to LocalizedField.contribute_to_class 2017-05-25 18:14:41 +03:00
Swen Kooij
093a9d58f2 Fix to_python not working with non-json values 2017-05-25 18:11:58 +03:00
Swen Kooij
cff22855c2 Revert "LocalizedUniqueSlugField refactored"
This reverts commit 03df76d6d7.
2017-05-25 17:23:39 +03:00
seroy
db93b93046 added test for LocalizedFileField and LocalizedFileFieldForm 2017-04-24 20:30:44 +03:00
seroy
a352388243 refactored LocalizedFieldFile.save method 2017-04-24 20:29:09 +03:00
Swen Kooij
bf2995fd27 Merge pull request #16 from MELScience/deserialization
Ability to deserialize string value
2017-04-14 17:19:17 +03:00
seroy
8ba08c389c changed indentation 2017-04-13 16:15:13 +03:00
seroy
f1798b0cc6 added ability to deserialize string value 2017-04-13 11:53:56 +03:00
seroy
fc80462ce7 added test for str parameter to_python method 2017-04-13 11:41:24 +03:00
seroy
24079a2fcb added description of LocalizedCharField, LocalizedTextField and LocalizedFileField 2017-04-13 11:11:52 +03:00
seroy
0f4c74a9b2 added comments and deleted extra code 2017-04-13 11:07:40 +03:00
Swen Kooij
366dceaddc Merge pull request #14 from MELScience/app_registry_not_ready
fixed "Apps aren't loaded yet." exception
2017-04-13 09:35:38 +03:00
seroy
5b93c5ec8f added style for AdminLocalizedFileFieldWidget 2017-04-12 22:10:12 +03:00
seroy
817c7e13fe added new LocalizedCharField, LocalizedTextField and LocalizedFileField fields 2017-04-12 21:34:19 +03:00
seroy
8591af1f2a fixed "Apps aren't loaded yet." exception 2017-04-12 14:11:29 +03:00
seroy
c55001ac12 Added test for "Apps aren't loaded yet." exception 2017-04-12 13:55:42 +03:00
Swen Kooij
23c6f975d8 Merge remote-tracking branch 'beer/admin_integration' 2017-04-03 14:56:43 +03:00
Swen Kooij
a9037b603a Merge remote-tracking branch 'beer/uniqueslug'
# Conflicts:
#	README.rst
#	tests/fake_model.py
2017-04-03 14:54:25 +03:00
Swen Kooij
8b08e5a467 Merge remote-tracking branch 'beer/model_inheritance_free'
# Conflicts:
#	localized_fields/fields/localized_field.py
#	localized_fields/models.py
#	tests/fake_model.py
2017-04-03 14:49:27 +03:00
Swen Kooij
bf90131e4f Merge pull request #12 from SectorLabs/experimental
Add experimental features flag for LocalizedField
2017-03-24 15:13:55 +02:00
Bogdan Hopulele
8bdfee0666 Implement review suggestions 2017-03-24 14:48:33 +02:00
Bogdan Hopulele
add3c1e1d4 Bump up the library version 2017-03-24 14:39:06 +02:00
Bogdan Hopulele
8ba33695f9 Export LocalizedModel 2017-03-24 14:37:43 +02:00
seroy
78594541e1 fixed for new instance don't call refresh_from_db 2017-03-23 20:49:39 +03:00
Bogdan Hopulele
52145ca7d3 Add experimental features flag for LocalizedField
With the flag set, LocalizedField will return None if there is no
database value.
2017-03-23 17:32:37 +02:00
Swen Kooij
5df44b0d62 Merge pull request #7 from MELScience/import_error
fixed "Apps aren't loaded yet." exception
2017-03-22 17:48:18 +02:00
Swen Kooij
5aef1d791b Merge pull request #9 from MELScience/bleach_import
fixed ImportError if django-bleach not installed
2017-03-22 17:45:25 +02:00
seroy
03df76d6d7 LocalizedUniqueSlugField refactored 2017-03-18 22:58:11 +03:00
seroy
465e35ba8a fixed ImportError if django-bleach not installed 2017-03-18 19:04:51 +03:00
seroy
9278e99b18 import fixed 2017-03-18 18:44:26 +03:00
seroy
9754134298 fixed "Apps aren't loaded yet." exception 2017-03-18 18:26:33 +03:00
seroy
f0c7a72078 LocalizedModel keeped for backwards compatibility 2017-03-18 17:51:31 +03:00
seroy
7f4dfbae1f changed style of active and inactive tabs 2017-03-17 18:22:19 +03:00
seroy
d07da55215 Advanced django admin integration 2017-03-17 06:22:31 +03:00
seroy
340dde18cd no need inheritance from LocalizedModel anymore. Introduction of LocalizedValueDescriptor 2017-03-13 00:50:34 +03:00
Swen Kooij
3951266747 Bump version to 3.5 2017-03-09 14:32:48 +02:00
Swen Kooij
b5f4c43d6b LocalizedValue is now cast-able to dict 2017-03-09 14:32:33 +02:00
Swen Kooij
3d08475468 __eq__ should only compare same types
unless it's a string
2017-03-09 11:59:21 +02:00
62 changed files with 3277 additions and 1062 deletions

92
.circleci/config.yml Normal file
View 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

View File

@@ -1,5 +1,3 @@
[run]
include = localized_fields/*
omit = *migrations*, *tests*
plugins =
django_coverage_plugin

View File

@@ -1,6 +0,0 @@
# Ignore virtual environments
env/
# Ignore Python byte code cache
*.pyc
__pycache__

8
.gitignore vendored
View File

@@ -1,9 +1,11 @@
# Ignore virtual environments
env/
.env/
# Ignore Python byte code cache
*.pyc
__pycache__
.cache/
# Ignore coverage reports
.coverage
@@ -15,3 +17,9 @@ dist/
# Ignore stupid .DS_Store
.DS_Store
# Ignore PyCharm
.idea/
# Ignore tox environments
.tox/

View File

@@ -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'

View File

@@ -1,2 +1,5 @@
include LICENSE
include README.rst
recursive-include localized_fields/static *
recursive-include localized_fields/templates *
recursive-exclude tests *

View File

@@ -1,20 +1,24 @@
django-localized-fields
=======================
.. image:: https://scrutinizer-ci.com/g/SectorLabs/django-localized-fields/badges/quality-score.png
:target: https://scrutinizer-ci.com/g/SectorLabs/django-localized-fields/
.. image:: https://scrutinizer-ci.com/g/SectorLabs/django-localized-fields/badges/coverage.png
:target: https://scrutinizer-ci.com/g/SectorLabs/django-localized-fields/
.. image:: https://circleci.com/gh/SectorLabs/django-localized-fields.svg?style=svg
:target: https://circleci.com/gh/SectorLabs/django-localized-fields
.. image:: https://img.shields.io/github/license/SectorLabs/django-localized-fields.svg
:target: https://github.com/SectorLabs/django-localized-fields/blob/master/LICENSE.md
.. image:: https://badge.fury.io/py/django-localized-fields.svg
: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
------------
@@ -32,7 +36,7 @@ Installation
....
'django.contrib.postgres',
'localized_fields'
'localized_fields.apps.LocalizedFieldsConfig'
]
3. Set the database engine to ``psqlextra.backend``:
@@ -62,15 +66,14 @@ Usage
Preparation
^^^^^^^^^^^
Inherit your model from ``LocalizedModel`` and declare fields on your model as ``LocalizedField``:
Declare fields on your model as ``LocalizedField``:
.. code-block:: python
from localized_fields.models import LocalizedModel
from localized_fields.fields import LocalizedField
class MyModel(LocalizedModel):
class MyModel(models.Model):
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:
@@ -115,6 +118,7 @@ Or get it in a specific language:
print(new.title.get('en')) # prints 'english title'
print(new.title.get('ro')) # prints 'romanian title'
print(new.title.get()) # whatever language is the primary one
print(new.title.get('ar', 'haha')) # prints 'haha' if there is no value in arabic
You can also explicitly set a value in a certain language:
@@ -130,21 +134,42 @@ Constraints
**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
class MyModel(LocalizedModel):
title = LocalizedField(required=True)
class MyModel(models.Model):
title = LocalizedField(blank=True, null=True, required=False)
* Make all languages optional:
* Make translation required for any language
.. code-block:: python
class MyModel(LocalizedModel):
title = LocalizedField(null=True)
class MyModel(models.Model):
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**
@@ -154,32 +179,32 @@ By default the values stored in a ``LocalizedField`` are *not unique*. You can e
.. code-block:: python
class MyModel(LocalizedModel):
class MyModel(models.Model):
title = LocalizedField(uniqueness=['en', 'ro'])
* Enforce uniqueness for **all** languages:
.. 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())
* Enforce uniqueness for one ore more languages **together** (similar to Django's ``unique_together``):
.. code-block:: python
class MyModel(LocalizedModel):
class MyModel(models.Model):
title = LocalizedField(uniqueness=[('en', 'ro')])
* Enforce uniqueness for **all** languages **together**:
.. 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())])
@@ -193,15 +218,36 @@ Besides ``LocalizedField``, there's also:
.. code-block:: python
from localized_fields import (LocalizedModel,
AtomicSlugRetryMixin,
LocalizedField,
LocalizedUniqueSlugField)
from localized_fields.models import LocalizedModel
from localized_fields.fields import LocalizedField, LocalizedUniqueSlugField
class MyModel(AtomicSlugRetryMixin, LocalizedModel):
class MyModel(LocalizedModel):
title = LocalizedField()
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``
.. code-block:: python
@@ -211,39 +257,118 @@ Besides ``LocalizedField``, there's also:
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.
* ``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``
Automatically bleaches the content of the field.
* django-bleach
Example usage:
.. code-block:: python
from localized_fields import (LocalizedModel,
LocalizedField,
LocalizedBleachField)
from localized_fields.fields import LocalizedField, LocalizedBleachField
class MyModel(LocalizedModel):
class MyModel(models.Model):
title = LocalizedField()
description = LocalizedBleachField()
* ``LocalizedIntegerField``
This is an experimental field type introduced in version 5.0 and is subject to change. It also has
some pretty major downsides due to the fact that values are stored as strings and are converted
back and forth.
Allows storing integers in multiple languages. This works exactly like ``LocalizedField`` except that
all values must be integers. Do note that values are stored as strings in your database because
the backing field type is ``hstore``, which only allows storing integers. The ``LocalizedIntegerField``
takes care of ensuring that all values are integers and converts the stored strings back to integers
when retrieving them from the database. Do not expect to be able to do queries such as:
.. code-block:: python
MyModel.objects.filter(score__en__gt=1)
* ``LocalizedCharField`` and ``LocalizedTextField``
These fields following the Django convention for string-based fields use the empty string as value for “no data”, not NULL.
``LocalizedCharField`` uses ``TextInput`` (``<input type="text">``) widget for render.
Example usage:
.. code-block:: python
from localized_fields.fields import LocalizedCharField, LocalizedTextField
class MyModel(models.Model):
title = LocalizedCharField()
description = LocalizedTextField()
* ``LocalizedFileField``
A file-upload field
Parameter ``upload_to`` supports ``lang`` parameter for string formatting or as function argument (in case if ``upload_to`` is callable).
Example usage:
.. code-block:: python
from localized_fields.fields import LocalizedFileField
def my_directory_path(instance, filename, lang):
# file will be uploaded to MEDIA_ROOT/<lang>/<id>_<filename>
return '{0}/{0}_{1}'.format(lang, instance.id, filename)
class MyModel(models.Model):
file1 = LocalizedFileField(upload_to='uploads/{lang}/')
file2 = LocalizedFileField(upload_to=my_directory_path)
In template you can access to file attributes:
.. code-block:: django
{# For current active language: #}
{{ model.file.url }} {# output file url #}
{{ model.file.name }} {# output file name #}
{# Or get it in a specific language: #}
{{ model.file.ro.url }} {# output file url for romanian language #}
{{ model.file.ro.name }} {# output file name for romanian language #}
To get access to file instance for current active language use ``localized`` method:
.. code-block:: python
model.file.localized()
Experimental feature
^^^^^^^^^^^^^^^^^^^^
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)
Frequently asked questions (FAQ)
--------------------------------
@@ -251,9 +376,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.
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?

View File

@@ -1,20 +1 @@
from .util import get_language_codes
from .forms import LocalizedFieldForm, LocalizedFieldWidget
from .fields import (LocalizedField, LocalizedBleachField,
LocalizedAutoSlugField, LocalizedUniqueSlugField)
from .mixins import AtomicSlugRetryMixin
from .models import LocalizedModel
from .localized_value import LocalizedValue
__all__ = [
'get_language_codes',
'LocalizedField',
'LocalizedValue',
'LocalizedAutoSlugField',
'LocalizedUniqueSlugField',
'LocalizedBleachField',
'LocalizedFieldWidget',
'LocalizedFieldForm',
'LocalizedModel',
'AtomicSlugRetryMixin'
]
default_app_config = 'localized_fields.apps.LocalizedFieldsConfig'

34
localized_fields/admin.py Normal file
View File

@@ -0,0 +1,34 @@
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 = (
'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

View File

@@ -1,5 +1,21 @@
import inspect
from django.apps import AppConfig
from django.conf import settings
from . import lookups
from .fields import LocalizedField
from .lookups import LocalizedLookupMixin
class LocalizedFieldsConfig(AppConfig):
name = 'localized_fields'
def ready(self):
if getattr(settings, 'LOCALIZED_FIELDS_EXPERIMENTAL', False):
for _, clazz in inspect.getmembers(lookups):
if not inspect.isclass(clazz) or clazz is LocalizedLookupMixin:
continue
if issubclass(clazz, LocalizedLookupMixin):
LocalizedField.register_lookup(clazz)

View 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

View 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)

View File

@@ -1,12 +1,26 @@
from .localized_field import LocalizedField
from .localized_autoslug_field import LocalizedAutoSlugField
from .localized_uniqueslug_field import LocalizedUniqueSlugField
from .localized_bleach_field import LocalizedBleachField
from .field import LocalizedField
from .autoslug_field import LocalizedAutoSlugField
from .uniqueslug_field import LocalizedUniqueSlugField
from .char_field import LocalizedCharField
from .text_field import LocalizedTextField
from .file_field import LocalizedFileField
from .integer_field import LocalizedIntegerField
__all__ = [
'LocalizedField',
'LocalizedAutoSlugField',
'LocalizedUniqueSlugField',
'LocalizedBleachField',
'LocalizedCharField',
'LocalizedTextField',
'LocalizedFileField',
'LocalizedIntegerField'
]
try:
from .bleach_field import LocalizedBleachField
__all__ += [
'LocalizedBleachField'
]
except ImportError:
pass

View File

@@ -1,12 +1,16 @@
from typing import Callable
import warnings
from typing import Callable, Tuple, Union
from datetime import datetime
from django import forms
from django.conf import settings
from django.utils import translation
from django.utils.text import slugify
from .localized_field import LocalizedField
from ..localized_value import LocalizedValue
from .field import LocalizedField
from ..value import LocalizedValue
from ..util import resolve_object_property
class LocalizedAutoSlugField(LocalizedField):
@@ -19,6 +23,11 @@ class LocalizedAutoSlugField(LocalizedField):
self.populate_from = kwargs.pop('populate_from', None)
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__(
*args,
**kwargs
@@ -69,13 +78,7 @@ class LocalizedAutoSlugField(LocalizedField):
slugs = LocalizedValue()
for lang_code, _ in settings.LANGUAGES:
value = self._get_populate_from_value(
instance,
self.populate_from,
lang_code
)
for lang_code, value in self._get_populate_values(instance):
if not value:
continue
@@ -128,8 +131,32 @@ class LocalizedAutoSlugField(LocalizedField):
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
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.
Arguments:
@@ -146,5 +173,20 @@ class LocalizedAutoSlugField(LocalizedField):
The text to generate a slug for.
"""
value = getattr(instance, field_name, None)
return value.get(language)
if callable(field_name):
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)

View File

@@ -1,8 +1,9 @@
import bleach
from django.conf import settings
from django_bleach.utils import get_bleach_default_options
from .localized_field import LocalizedField
from .field import LocalizedField
class LocalizedBleachField(LocalizedField):

View 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)

View 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)

View 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)

View 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)

View File

@@ -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, **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

View 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)

View File

@@ -1,13 +1,12 @@
from datetime import datetime
from django.conf import settings
from django.utils.text import slugify
from django.core.exceptions import ImproperlyConfigured
from .autoslug_field import LocalizedAutoSlugField
from ..util import get_language_codes
from ..mixins import AtomicSlugRetryMixin
from ..localized_value import LocalizedValue
from .localized_autoslug_field import LocalizedAutoSlugField
from ..value import LocalizedValue
class LocalizedUniqueSlugField(LocalizedAutoSlugField):
@@ -73,13 +72,7 @@ class LocalizedUniqueSlugField(LocalizedAutoSlugField):
slugs = LocalizedValue()
for lang_code, _ in settings.LANGUAGES:
value = self._get_populate_from_value(
instance,
self.populate_from,
lang_code
)
for lang_code, value in self._get_populate_values(instance):
if not value:
continue
@@ -98,11 +91,12 @@ class LocalizedUniqueSlugField(LocalizedAutoSlugField):
if self.include_time:
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
if not self.include_time:
slug += '-'
slug += '%d' % instance.retries
slug += '%d' % retries
slugs.set(lang_code, slug)

View File

@@ -1,75 +1,49 @@
from typing import List
from typing import List, Union
from django import forms
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
class LocalizedFieldWidget(MultiWidget):
"""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
from .value import LocalizedValue, LocalizedStringValue, \
LocalizedFileValue, LocalizedIntegerValue
from .widgets import LocalizedFieldWidget, LocalizedCharFieldWidget, \
LocalizedFileWidget, AdminLocalizedIntegerFieldWidget
class LocalizedFieldForm(forms.MultiValueField):
"""Form for a localized field, allows editing
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."""
fields = []
for lang_code, _ in settings.LANGUAGES:
field_options = {'required': False}
if lang_code == settings.LANGUAGE_CODE:
field_options['required'] = True
field_options['label'] = lang_code
fields.append(forms.fields.CharField(**field_options))
field_options = dict(
required=required if type(required) is bool else (lang_code in
required),
label=lang_code
)
fields.append(self.field_class(**field_options))
super(LocalizedFieldForm, self).__init__(
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
into a single :see:LocalizedValue instance.
@@ -82,9 +56,121 @@ class LocalizedFieldForm(forms.MultiValueField):
the value in several languages.
"""
localized_value = LocalizedValue()
localized_value = self.value_class()
for (lang_code, _), value in zip(settings.LANGUAGES, value):
localized_value.set(lang_code, 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

View File

@@ -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
})

View File

@@ -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))

View 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

View 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(),
]

View File

View File

@@ -1,34 +1,16 @@
from psqlextra.models import PostgresModel
from .fields import LocalizedField
from .localized_value import LocalizedValue
from .mixins import AtomicSlugRetryMixin
class LocalizedModel(PostgresModel):
"""A model that contains localized fields."""
class LocalizedModel(AtomicSlugRetryMixin, PostgresModel):
"""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:
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)

View File

@@ -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;
}

View File

@@ -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)

View 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 %}

View 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 %}

View File

@@ -19,3 +19,26 @@ def get_language_codes() -> List[str]:
lang_code
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
View 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 None

126
localized_fields/widgets.py Normal file
View 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

View File

@@ -1 +1,2 @@
django-postgres-extra==1.4
django-postgres-extra==1.21a9
deprecation==2.0.3

View File

@@ -1,17 +1,10 @@
-r base.txt
coverage==4.2
Django==1.10.2
django-autoslug==1.9.3
django-bleach==0.3.0
django-coverage-plugin==1.3.1
psycopg2==2.6.2
pylint==1.6.4
pylint-common==0.2.2
pylint-django==0.7.2
pylint-plugin-utils==0.2.4
coverage==4.2
django-coverage-plugin==1.3.1
flake8==3.0.4
pep8==1.7.0
dj-database-url==0.4.1
psycopg2==2.7.3.2
coverage==4.4.2
flake8==3.6.0
pep8==1.7.1
dj-database-url==0.4.2
tox==2.9.1

View File

@@ -21,9 +21,15 @@ LANGUAGES = (
)
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.admin',
'localized_fields',
'tests',
)
# set to a lower number than the default, since
# we want the tests to be fast, default is 100
LOCALIZED_FIELDS_MAX_RETRIES = 3
LOCALIZED_FIELDS_EXPERIMENTAL = False

View File

@@ -1,5 +1,6 @@
[flake8]
max-line-length = 120
ignore = E252, W605
exclude = env,.tox,.git,config/settings,*/migrations/*,*/static/CACHE/*,docs,node_modules
[pep8]

View File

@@ -1,14 +1,40 @@
import os
import distutils.cmd
import subprocess
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()
setup(
name='django-localized-fields',
version='3.4',
packages=find_packages(),
version='5.0a7',
packages=find_packages(exclude=['tests']),
include_package_data=True,
license='MIT License',
description='Implementation of localized model fields using PostgreSQL HStore fields.',
@@ -18,7 +44,9 @@ setup(
author_email='open-source@sectorlabs.ro',
keywords=['django', 'localized', 'language', 'models', 'fields'],
install_requires=[
'django-postgres-extra>=1.4'
'django-postgres-extra>=1.21a15',
'Django>=1.11',
'deprecation==2.0.3'
],
classifiers=[
'Environment :: Web Environment',
@@ -30,5 +58,11 @@ setup(
'Programming Language :: Python :: 3.5',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
]
],
cmdclass={
'lint': create_command(
'Lints the code',
[['flake8', 'setup.py', 'localized_fields', 'tests']],
),
},
)

13
tests/data.py Normal file
View 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

View File

@@ -1,28 +1,33 @@
import uuid
from django.db import connection, migrations
from django.db.migrations.executor import MigrationExecutor
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 = {
'app_label': 'localized_fields',
'app_label': 'tests',
'__module__': __name__,
'__name__': name
'__name__': name,
'Meta': type('Meta', (object,), meta_options)
}
if fields:
attributes.update(fields)
model = type(name, (AtomicSlugRetryMixin,LocalizedModel,), attributes)
model = type(name, (model_base,), attributes)
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."""
model = define_fake_model(name, fields)
model = define_fake_model(fields, model_base, meta_options)
class TestProject:
@@ -39,7 +44,7 @@ def get_fake_model(name='TestModel', fields=None):
with connection.schema_editor() as schema_editor:
migration_executor = MigrationExecutor(schema_editor.connection)
migration_executor.apply_migration(
TestProject(), TestMigration('eh', 'localized_fields'))
TestProject(), TestMigration('eh', 'postgres_extra'))
schema_editor.create_model(model)

87
tests/test_admin.py Normal file
View 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

View File

@@ -1,9 +1,11 @@
import bleach
from django.conf import settings
from django.test import TestCase
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:

44
tests/test_bulk.py Normal file
View 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
View 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
View 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
View 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

View 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
View 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']

View File

@@ -1,7 +1,7 @@
from django.conf import settings
from django.test import TestCase
from localized_fields import LocalizedFieldForm
from localized_fields.forms import LocalizedFieldForm
class LocalizedFieldFormTestCase(TestCase):
@@ -11,8 +11,8 @@ class LocalizedFieldFormTestCase(TestCase):
def test_init():
"""Tests whether the constructor correctly
creates a field for every language."""
form = LocalizedFieldForm()
# case required for specific language
form = LocalizedFieldForm(required=[settings.LANGUAGE_CODE])
for (lang_code, _), field in zip(settings.LANGUAGES, form.fields):
assert field.label == lang_code
@@ -22,6 +22,24 @@ class LocalizedFieldFormTestCase(TestCase):
else:
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
def test_compress():
"""Tests whether the :see:compress function

176
tests/test_integer_field.py Normal file
View 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

View File

@@ -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
)

View File

@@ -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
View 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()

View File

@@ -1,6 +1,7 @@
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
@@ -17,7 +18,6 @@ class LocalizedModelTestCase(TestCase):
super(LocalizedModelTestCase, cls).setUpClass()
cls.TestModel = get_fake_model(
'LocalizedModelTestCase',
{
'title': LocalizedField()
}
@@ -33,7 +33,6 @@ class LocalizedModelTestCase(TestCase):
assert isinstance(obj.title, LocalizedValue)
@classmethod
def test_model_init_kwargs(cls):
"""Tests whether all :see:LocalizedField

45
tests/test_query_set.py Normal file
View 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'

View File

@@ -1,13 +1,17 @@
import copy
from django import forms
from django.db import models
from django.conf import settings
from django.test import TestCase
from django.db.utils import IntegrityError
from localized_fields import (LocalizedField, LocalizedAutoSlugField,
LocalizedUniqueSlugField)
from django.utils.text import slugify
from localized_fields.fields import (
LocalizedField,
LocalizedUniqueSlugField
)
from .fake_model import get_fake_model
@@ -15,7 +19,7 @@ class LocalizedSlugFieldTestCase(TestCase):
"""Tests the localized slug classes."""
AutoSlugModel = None
MagicSlugModel = None
Model = None
@classmethod
def setUpClass(cls):
@@ -23,46 +27,14 @@ class LocalizedSlugFieldTestCase(TestCase):
super(LocalizedSlugFieldTestCase, cls).setUpClass()
cls.AutoSlugModel = get_fake_model(
'LocalizedAutoSlugFieldTestModel',
{
'title': LocalizedField(),
'slug': LocalizedAutoSlugField(populate_from='title')
}
)
cls.MagicSlugModel = get_fake_model(
'LocalizedUniqueSlugFieldTestModel',
cls.Model = get_fake_model(
{
'title': LocalizedField(),
'name': models.CharField(max_length=255),
'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
def test_unique_slug_with_time():
"""Tests whether the primary key is included in
@@ -71,7 +43,6 @@ class LocalizedSlugFieldTestCase(TestCase):
title = 'myuniquetitle'
PkModel = get_fake_model(
'PkModel',
{
'title': LocalizedField(),
'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."""
NoChangeSlugModel = get_fake_model(
'NoChangeSlugModel',
{
'title': LocalizedField(),
'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_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
try to find a slug forever and gives up after a while."""
title = 'myuniquetitle'
obj = self.MagicSlugModel()
obj = cls.Model()
obj.title.en = title
obj.save()
with self.assertRaises(IntegrityError):
with cls.assertRaises(cls, IntegrityError):
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.save()
@classmethod
def test_unique_slug_utf_auto(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):
def test_populate(cls):
"""Tests whether the populating feature works correctly."""
obj = model()
obj = cls.Model()
obj.title.en = 'this is my title'
obj.save()
assert obj.slug.get('en') == slugify(obj.title)
@staticmethod
def _test_populate_multiple_languages(model):
"""Tests whether the populating feature correctly
works for all languages."""
@classmethod
def test_populate_callable(cls):
"""Tests whether the populating feature works correctly
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:
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()
@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."""
title = 'myuniquetitle'
obj = model()
obj = cls.Model()
obj.title.en = title
obj.save()
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.save()
assert another_obj.slug.en == '%s-%d' % (title, i)
@staticmethod
def _test_unique_slug_utf(model):
@classmethod
def test_unique_slug_utf(cls):
"""Tests whether generating a slug works
when the value consists completely out
of non-ASCII characters."""
obj = model()
obj = cls.Model()
obj.title.en = 'مكاتب للايجار بشارع بورسعيد'
obj.save()
assert obj.slug.en == 'مكاتب-للايجار-بشارع-بورسعيد'
@staticmethod
def _test_deconstruct(field_type):
def test_deconstruct():
"""Tests whether the :see:deconstruct
function properly retains options
specified in the constructor."""
field = field_type(populate_from='title')
field = LocalizedUniqueSlugField(populate_from='title')
_, _, _, kwargs = field.deconstruct()
assert 'populate_from' in kwargs
assert kwargs['populate_from'] == field.populate_from
@staticmethod
def _test_formfield(field_type):
def test_formfield():
"""Tests whether the :see:formfield method
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.widget, forms.HiddenInput)

215
tests/test_value.py Normal file
View 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
View 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))

13
tox.ini Normal file
View File

@@ -0,0 +1,13 @@
[tox]
envlist = py35-dj{111,20,21}, py36-dj{111,20,21}, py37-dj{111,20,21}
[testenv]
deps =
dj111: Django>=1.11,<1.12
dj20: Django>=2.0,<2.1
dj21: Django>=2.1,<2.2
-rrequirements/test.txt
setenv =
DJANGO_SETTINGS_MODULE=settings
passenv = DATABASE_URL
commands = coverage run manage.py test