simo 1.7.20__py3-none-any.whl → 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of simo might be problematic. Click here for more details.
- simo/__pycache__/asgi.cpython-38.pyc +0 -0
- simo/__pycache__/settings.cpython-38.pyc +0 -0
- simo/__pycache__/urls.cpython-38.pyc +0 -0
- simo/__pycache__/wsgi.cpython-38.pyc +0 -0
- simo/core/__pycache__/admin.cpython-38.pyc +0 -0
- simo/core/__pycache__/api.cpython-38.pyc +0 -0
- simo/core/__pycache__/api_meta.cpython-38.pyc +0 -0
- simo/core/__pycache__/auto_urls.cpython-38.pyc +0 -0
- simo/core/__pycache__/autocomplete_views.cpython-38.pyc +0 -0
- simo/core/__pycache__/base_types.cpython-38.pyc +0 -0
- simo/core/__pycache__/context.cpython-38.pyc +0 -0
- simo/core/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/core/__pycache__/events.cpython-38.pyc +0 -0
- simo/core/__pycache__/forms.cpython-38.pyc +0 -0
- simo/core/__pycache__/gateways.cpython-38.pyc +0 -0
- simo/core/__pycache__/managers.cpython-38.pyc +0 -0
- simo/core/__pycache__/middleware.cpython-38.pyc +0 -0
- simo/core/__pycache__/models.cpython-38.pyc +0 -0
- simo/core/__pycache__/permissions.cpython-38.pyc +0 -0
- simo/core/__pycache__/serializers.cpython-38.pyc +0 -0
- simo/core/__pycache__/signal_receivers.cpython-38.pyc +0 -0
- simo/core/__pycache__/socket_consumers.cpython-38.pyc +0 -0
- simo/core/__pycache__/tasks.cpython-38.pyc +0 -0
- simo/core/__pycache__/views.cpython-38.pyc +0 -0
- simo/core/admin.py +28 -18
- simo/core/api.py +157 -16
- simo/core/api_meta.py +87 -0
- simo/core/auto_urls.py +4 -1
- simo/core/autocomplete_views.py +8 -4
- simo/core/base_types.py +1 -0
- simo/core/context.py +3 -1
- simo/core/controllers.py +112 -32
- simo/core/db_backend/base.py +7 -22
- simo/core/drf_braces/README +3 -0
- simo/core/drf_braces/__init__.py +7 -0
- simo/core/drf_braces/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/core/drf_braces/__pycache__/utils.cpython-38.pyc +0 -0
- simo/core/drf_braces/fields/__init__.py +5 -0
- simo/core/drf_braces/fields/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/core/drf_braces/fields/__pycache__/_fields.cpython-38.pyc +0 -0
- simo/core/drf_braces/fields/__pycache__/custom.cpython-38.pyc +0 -0
- simo/core/drf_braces/fields/__pycache__/mixins.cpython-38.pyc +0 -0
- simo/core/drf_braces/fields/__pycache__/modified.cpython-38.pyc +0 -0
- simo/core/drf_braces/fields/_fields.py +48 -0
- simo/core/drf_braces/fields/custom.py +107 -0
- simo/core/drf_braces/fields/mixins.py +58 -0
- simo/core/drf_braces/fields/modified.py +41 -0
- simo/core/drf_braces/forms/__init__.py +0 -0
- simo/core/drf_braces/forms/fields.py +20 -0
- simo/core/drf_braces/forms/serializer_form.py +156 -0
- simo/core/drf_braces/mixins.py +52 -0
- simo/core/drf_braces/models.py +0 -0
- simo/core/drf_braces/parsers.py +72 -0
- simo/core/drf_braces/renderers.py +37 -0
- simo/core/drf_braces/serializers/__init__.py +0 -0
- simo/core/drf_braces/serializers/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/core/drf_braces/serializers/__pycache__/form_serializer.cpython-38.pyc +0 -0
- simo/core/drf_braces/serializers/enforce_validation_serializer.py +214 -0
- simo/core/drf_braces/serializers/form_serializer.py +391 -0
- simo/core/drf_braces/serializers/swapping.py +48 -0
- simo/core/drf_braces/tests/__init__.py +0 -0
- simo/core/drf_braces/tests/fields/__init__.py +0 -0
- simo/core/drf_braces/tests/fields/test_custom.py +94 -0
- simo/core/drf_braces/tests/fields/test_fields.py +13 -0
- simo/core/drf_braces/tests/fields/test_mixins.py +96 -0
- simo/core/drf_braces/tests/fields/test_modified.py +40 -0
- simo/core/drf_braces/tests/forms/__init__.py +0 -0
- simo/core/drf_braces/tests/forms/test_fields.py +46 -0
- simo/core/drf_braces/tests/forms/test_serializer_form.py +256 -0
- simo/core/drf_braces/tests/serializers/__init__.py +0 -0
- simo/core/drf_braces/tests/serializers/test_enforce_validation_serializer.py +169 -0
- simo/core/drf_braces/tests/serializers/test_form_serializer.py +387 -0
- simo/core/drf_braces/tests/serializers/test_swapping.py +40 -0
- simo/core/drf_braces/tests/test_mixins.py +111 -0
- simo/core/drf_braces/tests/test_parsers.py +73 -0
- simo/core/drf_braces/tests/test_renderers.py +23 -0
- simo/core/drf_braces/tests/test_utils.py +73 -0
- simo/core/drf_braces/utils.py +209 -0
- simo/core/events.py +3 -3
- simo/core/forms.py +79 -37
- simo/core/gateways.py +31 -14
- simo/core/management/commands/gateways_manager.py +0 -1
- simo/core/managers.py +81 -0
- simo/core/middleware.py +25 -0
- simo/core/migrations/0026_category_instance.py +20 -0
- simo/core/migrations/0027_remove_component_tags.py +17 -0
- simo/core/migrations/0028_rename_subcomponents_component_slaves.py +18 -0
- simo/core/migrations/0029_auto_20240229_1331.py +33 -0
- simo/core/migrations/__pycache__/0026_category_instance.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0027_remove_component_tags.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0028_rename_subcomponents_component_slaves.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0029_auto_20240229_1331.cpython-38.pyc +0 -0
- simo/core/models.py +103 -66
- simo/core/permissions.py +28 -2
- simo/core/serializers.py +330 -26
- simo/core/socket_consumers.py +5 -14
- simo/core/tasks.py +11 -1
- simo/core/templates/admin/base.html +37 -10
- simo/core/templates/admin/wizard/discovery.html +188 -0
- simo/core/templates/admin/wizard/wizard_add.html +5 -5
- simo/core/utils/__pycache__/serialization.cpython-38.pyc +0 -0
- simo/core/utils/admin.py +9 -2
- simo/core/utils/formsets.py +17 -16
- simo/core/utils/helpers.py +1 -0
- simo/core/utils/serialization.py +56 -0
- simo/core/utils/type_constants.py +1 -1
- simo/core/utils/validators.py +14 -1
- simo/core/views.py +13 -0
- simo/fleet/__pycache__/admin.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/api.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/auto_urls.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/gateways.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/managers.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/models.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/serializers.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/socket_consumers.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/utils.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/views.cpython-38.pyc +0 -0
- simo/fleet/admin.py +54 -25
- simo/fleet/api.py +59 -3
- simo/fleet/auto_urls.py +2 -3
- simo/fleet/controllers.py +199 -16
- simo/fleet/forms.py +325 -483
- simo/fleet/gateways.py +44 -2
- simo/fleet/managers.py +32 -0
- simo/fleet/migrations/0025_auto_20240130_1334.py +27 -0
- simo/fleet/migrations/0026_rename_i2cinterface_scl_pin_and_more.py +64 -0
- simo/fleet/migrations/0027_auto_20240306_0802.py +170 -0
- simo/fleet/migrations/0028_remove_i2cinterface_scl_pin_no_and_more.py +21 -0
- simo/fleet/migrations/0029_alter_i2cinterface_scl_pin_and_more.py +24 -0
- simo/fleet/migrations/0030_colonelpin_label_alter_colonel_type.py +24 -0
- simo/fleet/migrations/0031_alter_colonel_type.py +18 -0
- simo/fleet/migrations/__pycache__/0025_auto_20240130_1334.cpython-38.pyc +0 -0
- simo/fleet/migrations/__pycache__/0026_rename_i2cinterface_scl_pin_and_more.cpython-38.pyc +0 -0
- simo/fleet/migrations/__pycache__/0027_auto_20240306_0802.cpython-38.pyc +0 -0
- simo/fleet/migrations/__pycache__/0028_remove_i2cinterface_scl_pin_no_and_more.cpython-38.pyc +0 -0
- simo/fleet/migrations/__pycache__/0029_alter_i2cinterface_scl_pin_and_more.cpython-38.pyc +0 -0
- simo/fleet/migrations/__pycache__/0030_colonelpin_label_alter_colonel_type.cpython-38.pyc +0 -0
- simo/fleet/migrations/__pycache__/0031_alter_colonel_type.cpython-38.pyc +0 -0
- simo/fleet/models.py +134 -82
- simo/fleet/serializers.py +35 -1
- simo/fleet/socket_consumers.py +239 -76
- simo/fleet/utils.py +15 -53
- simo/fleet/views.py +28 -14
- simo/generic/controllers.py +13 -89
- simo/generic/forms.py +29 -18
- simo/generic/gateways.py +73 -2
- simo/generic/models.py +3 -3
- simo/multimedia/controllers.py +9 -8
- simo/settings.py +7 -4
- simo/urls.py +4 -8
- simo/users/__pycache__/admin.cpython-38.pyc +0 -0
- simo/users/__pycache__/api.cpython-38.pyc +0 -0
- simo/users/__pycache__/auto_urls.cpython-38.pyc +0 -0
- simo/users/__pycache__/models.cpython-38.pyc +0 -0
- simo/users/__pycache__/serializers.cpython-38.pyc +0 -0
- simo/users/__pycache__/sso_urls.cpython-38.pyc +0 -0
- simo/users/admin.py +8 -1
- simo/users/api.py +38 -2
- simo/users/auto_urls.py +2 -2
- simo/users/migrations/0025_rename_name_fingerprint_type_and_more.py +22 -0
- simo/users/migrations/__pycache__/0025_rename_name_fingerprint_type_and_more.cpython-38.pyc +0 -0
- simo/users/models.py +2 -3
- simo/users/serializers.py +15 -1
- simo/users/sso_urls.py +3 -3
- simo/wsgi.py +7 -0
- {simo-1.7.20.dist-info → simo-2.0.0.dist-info}/METADATA +8 -9
- {simo-1.7.20.dist-info → simo-2.0.0.dist-info}/RECORD +173 -189
- {simo-1.7.20.dist-info → simo-2.0.0.dist-info}/WHEEL +1 -1
- simo/core/db_backend/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/core/db_backend/__pycache__/base.cpython-38.pyc +0 -0
- simo/core/management/commands/__pycache__/gateways_manager.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0001_initial.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0002_load_icons.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0003_create_default_zones_and_categories.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0004_create_generic.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0005_component_subcomponents.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0006_alter_component_subcomponents.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0007_component_change_init_to.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0008_alter_component_change_init_to.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0009_auto_20220707_1404.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0010_historyaggregate.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0011_component_last_change.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0012_instance.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0013_auto_20231003_0754.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0014_zone_instance.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0015_auto_20231004_1113.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0016_auto_20231004_1113.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0017_auto_20231004_1313.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0018_auto_20231005_0622.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0019_alter_gateway_type.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0020_component_meta.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0021_auto_20231020_1041.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/core/templatetags/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/core/templatetags/__pycache__/components_list.cpython-38.pyc +0 -0
- simo/core/utils/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/core/utils/__pycache__/admin.cpython-38.pyc +0 -0
- simo/core/utils/__pycache__/config_values.cpython-38.pyc +0 -0
- simo/core/utils/__pycache__/easing.cpython-38.pyc +0 -0
- simo/core/utils/__pycache__/form_fields.cpython-38.pyc +0 -0
- simo/core/utils/__pycache__/form_widgets.cpython-38.pyc +0 -0
- simo/core/utils/__pycache__/formsets.cpython-38.pyc +0 -0
- simo/core/utils/__pycache__/helpers.cpython-38.pyc +0 -0
- simo/core/utils/__pycache__/logs.cpython-38.pyc +0 -0
- simo/core/utils/__pycache__/mixins.cpython-38.pyc +0 -0
- simo/core/utils/__pycache__/model_helpers.cpython-38.pyc +0 -0
- simo/core/utils/__pycache__/relay.cpython-38.pyc +0 -0
- simo/core/utils/__pycache__/type_constants.cpython-38.pyc +0 -0
- simo/core/utils/__pycache__/validators.cpython-38.pyc +0 -0
- simo/fleet/tasks.py +0 -25
- simo/generic/__pycache__/__init__.cpython-37.pyc +0 -0
- simo/generic/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/generic/__pycache__/app_widgets.cpython-38.pyc +0 -0
- simo/generic/__pycache__/base_types.cpython-38.pyc +0 -0
- simo/generic/__pycache__/controllers.cpython-37.pyc +0 -0
- simo/generic/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/generic/__pycache__/forms.cpython-38.pyc +0 -0
- simo/generic/__pycache__/gateways.cpython-38.pyc +0 -0
- simo/generic/__pycache__/models.cpython-38.pyc +0 -0
- simo/generic/__pycache__/routing.cpython-38.pyc +0 -0
- simo/generic/__pycache__/socket_consumers.cpython-38.pyc +0 -0
- simo/generic/__pycache__/widgets.cpython-37.pyc +0 -0
- simo/multimedia/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/multimedia/__pycache__/admin.cpython-38.pyc +0 -0
- simo/multimedia/__pycache__/api.cpython-38.pyc +0 -0
- simo/multimedia/__pycache__/app_widgets.cpython-38.pyc +0 -0
- simo/multimedia/__pycache__/base_types.cpython-38.pyc +0 -0
- simo/multimedia/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/multimedia/__pycache__/forms.cpython-38.pyc +0 -0
- simo/multimedia/__pycache__/models.cpython-38.pyc +0 -0
- simo/multimedia/__pycache__/serializers.cpython-38.pyc +0 -0
- simo/multimedia/migrations/__pycache__/0001_initial.cpython-38.pyc +0 -0
- simo/multimedia/migrations/__pycache__/0002_sound_length.cpython-38.pyc +0 -0
- simo/multimedia/migrations/__pycache__/0003_alter_sound_length.cpython-38.pyc +0 -0
- simo/multimedia/migrations/__pycache__/0004_auto_20231023_1055.cpython-38.pyc +0 -0
- simo/multimedia/migrations/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/notifications/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/notifications/__pycache__/admin.cpython-38.pyc +0 -0
- simo/notifications/__pycache__/api.cpython-38.pyc +0 -0
- simo/notifications/__pycache__/models.cpython-38.pyc +0 -0
- simo/notifications/__pycache__/serializers.cpython-38.pyc +0 -0
- simo/notifications/__pycache__/utils.cpython-38.pyc +0 -0
- simo/notifications/migrations/__pycache__/0001_initial.cpython-38.pyc +0 -0
- simo/notifications/migrations/__pycache__/0002_notification_instance.cpython-38.pyc +0 -0
- simo/notifications/migrations/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0001_initial.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0002_componentpermission.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0003_create_roles_and_system_user.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0004_user_secret_key.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0005_permissionsrole_instance.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0006_auto_20231003_0850.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0007_auto_20231003_1228.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0008_auto_20231003_1229.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0009_remove_user_role.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0010_auto_20231004_1313.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0011_auto_20231004_1313.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0012_alter_userinstancerole_unique_together.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0013_remove_user_roles.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0014_user_roles.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0015_remove_user_at_home.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0016_auto_20231005_1050.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/__init__.cpython-38.pyc +0 -0
- {simo-1.7.20.dist-info → simo-2.0.0.dist-info}/LICENSE.md +0 -0
- {simo-1.7.20.dist-info → simo-2.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
from __future__ import absolute_import, print_function, unicode_literals
|
|
2
|
+
from collections import OrderedDict
|
|
3
|
+
|
|
4
|
+
import six
|
|
5
|
+
from django import forms
|
|
6
|
+
from rest_framework import serializers
|
|
7
|
+
|
|
8
|
+
from .. import fields
|
|
9
|
+
from ..utils import (
|
|
10
|
+
find_matching_class_kwargs,
|
|
11
|
+
get_attr_from_base_classes,
|
|
12
|
+
get_class_name_with_new_suffix,
|
|
13
|
+
reduce_attr_dict_from_instance,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FormSerializerFailure(object):
|
|
18
|
+
"""
|
|
19
|
+
Enum for the possible form validation failure modes.
|
|
20
|
+
|
|
21
|
+
'fail': validation failures should be added to self.errors
|
|
22
|
+
and `is_valid()` should return False.
|
|
23
|
+
|
|
24
|
+
'drop': validation failures for a given attribute will result in
|
|
25
|
+
that attribute being dropped from `cleaned_data`;
|
|
26
|
+
`is_valid()` will return True.
|
|
27
|
+
|
|
28
|
+
'ignore': validation failures will be ignored, and the (invalid)
|
|
29
|
+
data provided will be preserved in `cleaned_data`.
|
|
30
|
+
"""
|
|
31
|
+
fail = 'fail'
|
|
32
|
+
drop = 'drop'
|
|
33
|
+
ignore = 'ignore'
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class FormSerializerFieldMixin(object):
|
|
37
|
+
def run_validation(self, data):
|
|
38
|
+
try:
|
|
39
|
+
return super(FormSerializerFieldMixin, self).run_validation(data)
|
|
40
|
+
except (serializers.ValidationError, forms.ValidationError) as e:
|
|
41
|
+
# Only handle a ValidationError if the full validation is
|
|
42
|
+
# requested or if field is in minimum required in the case
|
|
43
|
+
# of partial validation.
|
|
44
|
+
if any([not self.parent.partial,
|
|
45
|
+
self.parent.Meta.failure_mode == FormSerializerFailure.fail,
|
|
46
|
+
self.field_name in self.parent.Meta.minimum_required]):
|
|
47
|
+
raise
|
|
48
|
+
self.capture_failed_field(self.field_name, data, e.detail)
|
|
49
|
+
raise serializers.SkipField
|
|
50
|
+
|
|
51
|
+
def capture_failed_field(self, field_name, field_data, error_msg):
|
|
52
|
+
"""
|
|
53
|
+
Hook for capturing invalid fields. This is used to track which fields have been skipped.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
field_name (str): the name of the field whose data failed to validate
|
|
57
|
+
field_data (object): the data of the field that failed validation
|
|
58
|
+
error_msg (str): validation error message
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Not meant to return anything.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def make_form_serializer_field(field_class, validation_form_serializer_field_mixin_class=FormSerializerFieldMixin):
|
|
66
|
+
return type(
|
|
67
|
+
get_class_name_with_new_suffix(field_class, 'Field', 'FormSerializerField'),
|
|
68
|
+
(validation_form_serializer_field_mixin_class, field_class,),
|
|
69
|
+
{}
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
FORM_SERIALIZER_FIELD_MAPPING = {
|
|
74
|
+
forms.CharField: make_form_serializer_field(fields.CharField),
|
|
75
|
+
forms.MultipleChoiceField: make_form_serializer_field(fields.ChoiceField),
|
|
76
|
+
forms.ChoiceField: make_form_serializer_field(fields.ChoiceField),
|
|
77
|
+
forms.BooleanField: make_form_serializer_field(fields.BooleanField),
|
|
78
|
+
forms.IntegerField: make_form_serializer_field(fields.IntegerField),
|
|
79
|
+
forms.EmailField: make_form_serializer_field(fields.EmailField),
|
|
80
|
+
forms.DateTimeField: make_form_serializer_field(fields.DateTimeField),
|
|
81
|
+
forms.DateField: make_form_serializer_field(fields.DateField),
|
|
82
|
+
forms.TimeField: make_form_serializer_field(fields.TimeField),
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class FormSerializerOptions(object):
|
|
87
|
+
"""
|
|
88
|
+
Defines what options FormSerializer can have in Meta.
|
|
89
|
+
|
|
90
|
+
:param form: The ``django.form.Form`` class to use as the base
|
|
91
|
+
for the serializer.
|
|
92
|
+
:param failure_mode: `FormSerializerFailure`
|
|
93
|
+
:param minimum_required: the minimum required fields that
|
|
94
|
+
must validate in order for validation to succeed.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def __init__(self, meta, class_name):
|
|
98
|
+
self.form = getattr(meta, 'form', None)
|
|
99
|
+
self.failure_mode = getattr(meta, 'failure_mode', FormSerializerFailure.fail)
|
|
100
|
+
self.minimum_required = getattr(meta, 'minimum_required', [])
|
|
101
|
+
self.field_mapping = getattr(meta, 'field_mapping', {})
|
|
102
|
+
self.exclude = getattr(meta, 'exclude', [])
|
|
103
|
+
|
|
104
|
+
assert self.form, (
|
|
105
|
+
'Class {serializer_class} missing "Meta.form" attribute'.format(
|
|
106
|
+
serializer_class=class_name
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
assert self.failure_mode in vars(FormSerializerFailure).values(), (
|
|
110
|
+
'Failure mode "{}" is not supported'.format(self.failure_mode)
|
|
111
|
+
)
|
|
112
|
+
if self.failure_mode == FormSerializerFailure.ignore:
|
|
113
|
+
raise NotImplementedError(
|
|
114
|
+
'Failure mode "{}" is not supported since it is not clear '
|
|
115
|
+
'what is an expected behavior'.format(self.failure_mode)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# copy all other custom keys
|
|
119
|
+
for k, v in vars(meta).items():
|
|
120
|
+
if hasattr(self, k):
|
|
121
|
+
continue
|
|
122
|
+
setattr(self, k, v)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class FormSerializerMeta(serializers.SerializerMetaclass):
|
|
126
|
+
def __new__(cls, name, bases, attrs):
|
|
127
|
+
try:
|
|
128
|
+
parents = [b for b in bases if issubclass(b, FormSerializer)]
|
|
129
|
+
except NameError:
|
|
130
|
+
# We are defining FormSerializer itself
|
|
131
|
+
parents = None
|
|
132
|
+
|
|
133
|
+
if not parents or attrs.pop('_is_base', False):
|
|
134
|
+
return super(FormSerializerMeta, cls).__new__(cls, name, bases, attrs)
|
|
135
|
+
|
|
136
|
+
assert 'Meta' in attrs, (
|
|
137
|
+
'Class {serializer_class} missing "Meta" attribute'.format(
|
|
138
|
+
serializer_class=name
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
options_class = get_attr_from_base_classes(
|
|
142
|
+
bases, attrs, '_options_class', default=FormSerializerOptions
|
|
143
|
+
)
|
|
144
|
+
attrs['Meta'] = options_class(attrs['Meta'], name)
|
|
145
|
+
|
|
146
|
+
return super(FormSerializerMeta, cls).__new__(cls, name, bases, attrs)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class FormSerializerBase(serializers.Serializer):
|
|
150
|
+
"""
|
|
151
|
+
The base Form serializer class.
|
|
152
|
+
When a subclassing serializer is validated or saved, this will
|
|
153
|
+
pass-through those operations to the mapped Form.
|
|
154
|
+
"""
|
|
155
|
+
_is_base = True
|
|
156
|
+
_options_class = FormSerializerOptions
|
|
157
|
+
|
|
158
|
+
def __init__(self, *args, **kwargs):
|
|
159
|
+
# We override partial validation handling, since for
|
|
160
|
+
# it to be properly implemented for a Form the caller
|
|
161
|
+
# must also choose whether or not to include the data
|
|
162
|
+
# that failed validation in the result cleaned_data.
|
|
163
|
+
# Unfortunately there is no way to prevent a caller from
|
|
164
|
+
# sending this param themselves, because of the way DRFv2
|
|
165
|
+
# serializers work internally.
|
|
166
|
+
if self.Meta.failure_mode != FormSerializerFailure.fail:
|
|
167
|
+
kwargs['partial'] = True
|
|
168
|
+
|
|
169
|
+
self.form_instance = None
|
|
170
|
+
|
|
171
|
+
super(FormSerializerBase, self).__init__(*args, **kwargs)
|
|
172
|
+
|
|
173
|
+
def get_form(self, data=None, **kwargs):
|
|
174
|
+
"""
|
|
175
|
+
Create an instance of configured form class.
|
|
176
|
+
|
|
177
|
+
:param data: optional initial data
|
|
178
|
+
:param kwargs: key args to pass to form instance
|
|
179
|
+
:return: instance of `self.opts.form`, bound if data was provided,
|
|
180
|
+
otherwise unbound.
|
|
181
|
+
"""
|
|
182
|
+
form_cls = self.Meta.form
|
|
183
|
+
|
|
184
|
+
instance = form_cls(data=data, **kwargs)
|
|
185
|
+
|
|
186
|
+
# Handle partial validation on the form side
|
|
187
|
+
if self.partial:
|
|
188
|
+
set_form_partial_validation(
|
|
189
|
+
instance, self.Meta.minimum_required
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
return instance
|
|
193
|
+
|
|
194
|
+
def get_fields(self):
|
|
195
|
+
"""
|
|
196
|
+
Return all the fields that should be serialized for the form.
|
|
197
|
+
This is a hook provided by parent class.
|
|
198
|
+
:return: dict of {'field_name': serializer_field_instance}
|
|
199
|
+
"""
|
|
200
|
+
ret = super(FormSerializerBase, self).get_fields()
|
|
201
|
+
|
|
202
|
+
field_mapping = reduce_attr_dict_from_instance(
|
|
203
|
+
self,
|
|
204
|
+
lambda i: getattr(getattr(i, 'Meta', None), 'field_mapping', {}),
|
|
205
|
+
FORM_SERIALIZER_FIELD_MAPPING
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Iterate over the form fields, creating an
|
|
209
|
+
# instance of serializer field for each.
|
|
210
|
+
form = self.Meta.form
|
|
211
|
+
for field_name, form_field in getattr(form, 'all_base_fields', form.base_fields).items():
|
|
212
|
+
# if field is specified as excluded field
|
|
213
|
+
if field_name in getattr(self.Meta, 'exclude', []):
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
# if field is already defined via declared fields
|
|
217
|
+
# skip mapping it from forms which then honors
|
|
218
|
+
# the custom validation defined on the DRF declared field
|
|
219
|
+
if field_name in ret:
|
|
220
|
+
continue
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
serializer_field_class = field_mapping[form_field.__class__]
|
|
224
|
+
except KeyError:
|
|
225
|
+
raise TypeError(
|
|
226
|
+
"{field} is not mapped to a serializer field. "
|
|
227
|
+
"Please add {field} to {serializer}.Meta.field_mapping. "
|
|
228
|
+
"Currently mapped fields: {mapped}".format(
|
|
229
|
+
field=form_field.__class__.__name__,
|
|
230
|
+
serializer=self.__class__.__name__,
|
|
231
|
+
mapped=', '.join(sorted([i.__name__ for i in field_mapping.keys()]))
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
else:
|
|
235
|
+
ret[field_name] = self._get_field(form_field, serializer_field_class)
|
|
236
|
+
|
|
237
|
+
return ret
|
|
238
|
+
|
|
239
|
+
def _get_field(self, form_field, serializer_field_class):
|
|
240
|
+
kwargs = self._get_field_kwargs(form_field, serializer_field_class)
|
|
241
|
+
|
|
242
|
+
field = serializer_field_class(**kwargs)
|
|
243
|
+
|
|
244
|
+
for kwarg, value in kwargs.items():
|
|
245
|
+
# set corresponding DRF attributes which don't have alternative
|
|
246
|
+
# in Django form fields
|
|
247
|
+
if kwarg == 'required':
|
|
248
|
+
field.allow_blank = not value
|
|
249
|
+
field.allow_null = not value
|
|
250
|
+
|
|
251
|
+
# ChoiceField natively uses choice_strings_to_values
|
|
252
|
+
# in the to_internal_value flow
|
|
253
|
+
elif kwarg == 'choices':
|
|
254
|
+
field.choice_strings_to_values = {
|
|
255
|
+
six.text_type(key): key for key in OrderedDict(value).keys()
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return field
|
|
259
|
+
|
|
260
|
+
def _get_field_kwargs(self, form_field, serializer_field_class):
|
|
261
|
+
"""
|
|
262
|
+
For a given Form field, determine what validation attributes
|
|
263
|
+
have been set. Includes things like max_length, required, etc.
|
|
264
|
+
These will be used to create an instance of ``rest_framework.fields.Field``.
|
|
265
|
+
|
|
266
|
+
:param form_field: a ``django.forms.field.Field`` instance
|
|
267
|
+
:return: dictionary of attributes to set
|
|
268
|
+
"""
|
|
269
|
+
attrs = find_matching_class_kwargs(form_field, serializer_field_class)
|
|
270
|
+
|
|
271
|
+
if 'choices' in attrs:
|
|
272
|
+
choices = OrderedDict(attrs['choices']).keys()
|
|
273
|
+
attrs['choices'] = OrderedDict(zip(choices, choices))
|
|
274
|
+
|
|
275
|
+
if getattr(form_field, 'initial', None):
|
|
276
|
+
attrs['default'] = form_field.initial
|
|
277
|
+
|
|
278
|
+
# avoid "May not set both `required` and `default`"
|
|
279
|
+
if attrs.get('required') and 'default' in attrs:
|
|
280
|
+
del attrs['required']
|
|
281
|
+
|
|
282
|
+
return attrs
|
|
283
|
+
|
|
284
|
+
def validate(self, data):
|
|
285
|
+
"""
|
|
286
|
+
Validate a form instance using the data that has been run through
|
|
287
|
+
the serializer field validation.
|
|
288
|
+
|
|
289
|
+
:param data: deserialized data to validate
|
|
290
|
+
:return: validated, cleaned form data
|
|
291
|
+
:raise: ``django.core.exceptions.ValidationError`` on failed
|
|
292
|
+
validation.
|
|
293
|
+
"""
|
|
294
|
+
self.form_instance = form = self.get_form(data=data)
|
|
295
|
+
|
|
296
|
+
if not form.is_valid():
|
|
297
|
+
_cleaned_data = getattr(form, 'cleaned_data', None) or {}
|
|
298
|
+
|
|
299
|
+
if self.Meta.failure_mode == FormSerializerFailure.fail:
|
|
300
|
+
raise serializers.ValidationError(form.errors)
|
|
301
|
+
|
|
302
|
+
else:
|
|
303
|
+
self.capture_failed_fields(data, form.errors)
|
|
304
|
+
cleaned_data = {k: v for k, v in data.items() if k not in form.errors}
|
|
305
|
+
# use any cleaned data form might of validated right until
|
|
306
|
+
# this moment even if validation failed
|
|
307
|
+
cleaned_data.update(_cleaned_data)
|
|
308
|
+
|
|
309
|
+
else:
|
|
310
|
+
cleaned_data = form.cleaned_data
|
|
311
|
+
|
|
312
|
+
return cleaned_data
|
|
313
|
+
|
|
314
|
+
def to_representation(self, instance):
|
|
315
|
+
"""
|
|
316
|
+
It doesn't make much sense to serialize a Form instance to JSON.
|
|
317
|
+
"""
|
|
318
|
+
raise NotImplementedError(
|
|
319
|
+
'{} does not currently serialize Form --> JSON'
|
|
320
|
+
''.format(self.__class__.__name__)
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
def capture_failed_fields(self, raw_data, form_errors):
|
|
324
|
+
"""
|
|
325
|
+
Hook for capturing all failed form data when the failure mode is not FormSerializerFailure.fail
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
raw_data (dict): raw form data
|
|
329
|
+
form_errors (dict): all form errors
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
Not meant to return anything.
|
|
333
|
+
"""
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class FormSerializer(six.with_metaclass(FormSerializerMeta, FormSerializerBase)):
|
|
337
|
+
pass
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class LazyLoadingValidationsMixin(object):
|
|
341
|
+
"""
|
|
342
|
+
Provides a method for re-evaluating the validations for
|
|
343
|
+
a form using an instance of it (whereas the FormSerializer
|
|
344
|
+
only uses the form class).
|
|
345
|
+
If your form class loads validations in `__init__()`, you
|
|
346
|
+
need this.
|
|
347
|
+
"""
|
|
348
|
+
|
|
349
|
+
def repopulate_form_fields(self):
|
|
350
|
+
"""
|
|
351
|
+
Repopulate the form fields, update choices.
|
|
352
|
+
The repopulation is required b/c some DT forms use a lazy-load approach
|
|
353
|
+
to populating choices of a ChoiceField, by putting the load
|
|
354
|
+
in the form's constructor. Also, the DT fields may require context_data,
|
|
355
|
+
which is unavailable when the fields are first constructed
|
|
356
|
+
(which happens during evaluation of the serializer classes).
|
|
357
|
+
:return: None
|
|
358
|
+
"""
|
|
359
|
+
instance = self.get_form()
|
|
360
|
+
|
|
361
|
+
for form_field_name, form_field in getattr(instance, 'all_fields', instance.fields).items():
|
|
362
|
+
if hasattr(form_field, 'choices'):
|
|
363
|
+
# let drf normalize choices down to key: key
|
|
364
|
+
# key:value is unsupported unlike in django form fields
|
|
365
|
+
self.fields[form_field_name].choices = OrderedDict(form_field.choices).keys()
|
|
366
|
+
self.fields[form_field_name].choice_strings_to_values = {
|
|
367
|
+
six.text_type(key): key for key in OrderedDict(form_field.choices).keys()
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
def to_internal_value(self, data):
|
|
371
|
+
"""
|
|
372
|
+
We have tons of "choices" loading in form `__init__()`,
|
|
373
|
+
(so that DB query is evaluated at last possible moment) so require the
|
|
374
|
+
use of ``common.common_json.serializers.LazyLoadingValidationsMixin``.
|
|
375
|
+
"""
|
|
376
|
+
self.repopulate_form_fields()
|
|
377
|
+
return super(LazyLoadingValidationsMixin, self).to_internal_value(data)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def set_form_partial_validation(form, minimum_required):
|
|
381
|
+
"""
|
|
382
|
+
Get a form ready for partial validation.
|
|
383
|
+
For all fields not in `minimum_required`, set
|
|
384
|
+
`Field.required` to False.
|
|
385
|
+
|
|
386
|
+
:param minimum_required: list of minimum required fields
|
|
387
|
+
:return: None
|
|
388
|
+
"""
|
|
389
|
+
for field_name, field in getattr(form, 'all_fields', form.fields).items():
|
|
390
|
+
if field_name not in minimum_required:
|
|
391
|
+
field.required = False
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from __future__ import absolute_import, print_function, unicode_literals
|
|
3
|
+
import copy
|
|
4
|
+
|
|
5
|
+
from rest_framework.serializers import BaseSerializer, ListSerializer
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SwappingSerializerMixin(BaseSerializer):
|
|
9
|
+
"""
|
|
10
|
+
Declaratively swap any of descendant fields.
|
|
11
|
+
|
|
12
|
+
Useful when any of child serializers need to be swapped to similar but slightly different
|
|
13
|
+
serializer. One use case is to swap normal serializer to hyperlinked one...
|
|
14
|
+
|
|
15
|
+
For example::
|
|
16
|
+
|
|
17
|
+
class SwappedSerializer(SwappingSerializerMixin, MyBaseSerializer):
|
|
18
|
+
class Meta(MyBaseSerializer.Meta):
|
|
19
|
+
swappable_fields = {
|
|
20
|
+
MySerializer: MyOtherSerializer,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.. note::
|
|
24
|
+
``MyOtherSerializer`` will be instantiated with same ``*args, **kwargs`` as given to ``MySerializer``.
|
|
25
|
+
This allows to swap fields but to leave state as is.
|
|
26
|
+
"""
|
|
27
|
+
def __init__(self, *args, **kwargs):
|
|
28
|
+
super(SwappingSerializerMixin, self).__init__(*args, **kwargs)
|
|
29
|
+
self.swap_fields(self)
|
|
30
|
+
|
|
31
|
+
def swap_fields(self, serializer):
|
|
32
|
+
for name, field in list(serializer.fields.items()):
|
|
33
|
+
new_field = self.swap_field(field)
|
|
34
|
+
if new_field is not field:
|
|
35
|
+
serializer.fields[name] = new_field
|
|
36
|
+
if isinstance(new_field, ListSerializer):
|
|
37
|
+
self.swap_fields(new_field.child)
|
|
38
|
+
elif isinstance(new_field, BaseSerializer):
|
|
39
|
+
self.swap_fields(new_field)
|
|
40
|
+
return serializer
|
|
41
|
+
|
|
42
|
+
def swap_field(self, field):
|
|
43
|
+
replacement = getattr(self.Meta, "swappable_fields", {}).get(field.__class__)
|
|
44
|
+
if replacement is None:
|
|
45
|
+
return field
|
|
46
|
+
|
|
47
|
+
field_copy = copy.deepcopy(field)
|
|
48
|
+
return replacement(*field_copy._args, **field_copy._kwargs)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from __future__ import absolute_import, print_function, unicode_literals
|
|
2
|
+
import unittest
|
|
3
|
+
from collections import OrderedDict
|
|
4
|
+
from decimal import ROUND_DOWN, Decimal
|
|
5
|
+
|
|
6
|
+
import mock
|
|
7
|
+
import pytz
|
|
8
|
+
|
|
9
|
+
from ...fields.custom import (
|
|
10
|
+
NonValidatingChoiceField,
|
|
11
|
+
PositiveIntegerField,
|
|
12
|
+
RoundedDecimalField,
|
|
13
|
+
UTCDateTimeField,
|
|
14
|
+
UnvalidatedField,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TestUnvalidatedField(unittest.TestCase):
|
|
19
|
+
def test_run_validators(self):
|
|
20
|
+
validator = mock.MagicMock()
|
|
21
|
+
field = UnvalidatedField(validators=[validator])
|
|
22
|
+
|
|
23
|
+
actual = field.run_validators(mock.sentinel.value)
|
|
24
|
+
|
|
25
|
+
self.assertIsNone(actual)
|
|
26
|
+
self.assertFalse(validator.called)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TestPositiveIntegerField(unittest.TestCase):
|
|
30
|
+
def test_init_default(self):
|
|
31
|
+
field = PositiveIntegerField()
|
|
32
|
+
self.assertEqual(field.min_value, 0)
|
|
33
|
+
|
|
34
|
+
def test_init_passed(self):
|
|
35
|
+
field = PositiveIntegerField(min_value=mock.sentinel.min_value)
|
|
36
|
+
self.assertEqual(field.min_value, mock.sentinel.min_value)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TestUTCDateTimeField(unittest.TestCase):
|
|
40
|
+
def test_init(self):
|
|
41
|
+
field = UTCDateTimeField()
|
|
42
|
+
|
|
43
|
+
self.assertEqual(
|
|
44
|
+
getattr(field, 'timezone', getattr(field, 'default_timezone', None)),
|
|
45
|
+
pytz.UTC
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TestNonValidatingChoiceField(unittest.TestCase):
|
|
50
|
+
def test_init(self):
|
|
51
|
+
field = NonValidatingChoiceField()
|
|
52
|
+
|
|
53
|
+
self.assertEqual(field.choices, OrderedDict())
|
|
54
|
+
|
|
55
|
+
def test_to_internal_value(self):
|
|
56
|
+
field = NonValidatingChoiceField(choices=['bar'])
|
|
57
|
+
|
|
58
|
+
self.assertEqual(field.to_internal_value('bar'), 'bar')
|
|
59
|
+
self.assertEqual(field.to_internal_value('haha'), 'haha')
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class TestRoundedDecimalField(unittest.TestCase):
|
|
63
|
+
def test_init(self):
|
|
64
|
+
field = RoundedDecimalField()
|
|
65
|
+
self.assertEqual(field.decimal_places, 2)
|
|
66
|
+
self.assertIsNone(field.rounding)
|
|
67
|
+
|
|
68
|
+
new_field = RoundedDecimalField(rounding=ROUND_DOWN)
|
|
69
|
+
self.assertEqual(new_field.rounding, ROUND_DOWN)
|
|
70
|
+
|
|
71
|
+
def test_to_internal_value(self):
|
|
72
|
+
field = RoundedDecimalField()
|
|
73
|
+
self.assertEqual(field.to_internal_value(5), Decimal('5'))
|
|
74
|
+
self.assertEqual(field.to_internal_value(5.2), Decimal('5.2'))
|
|
75
|
+
self.assertEqual(field.to_internal_value(5.23), Decimal('5.23'))
|
|
76
|
+
self.assertEqual(field.to_internal_value(5.2345), Decimal('5.23'))
|
|
77
|
+
self.assertEqual(field.to_internal_value(5.2356), Decimal('5.24'))
|
|
78
|
+
self.assertEqual(field.to_internal_value('5'), Decimal('5'))
|
|
79
|
+
self.assertEqual(field.to_internal_value('5.2'), Decimal('5.2'))
|
|
80
|
+
self.assertEqual(field.to_internal_value('5.23'), Decimal('5.23'))
|
|
81
|
+
self.assertEqual(field.to_internal_value('5.2345'), Decimal('5.23'))
|
|
82
|
+
self.assertEqual(field.to_internal_value('5.2356'), Decimal('5.24'))
|
|
83
|
+
self.assertEqual(field.to_internal_value(Decimal('5')), Decimal('5'))
|
|
84
|
+
self.assertEqual(field.to_internal_value(Decimal('5.2')), Decimal('5.2'))
|
|
85
|
+
self.assertEqual(field.to_internal_value(Decimal('5.23')), Decimal('5.23'))
|
|
86
|
+
self.assertEqual(field.to_internal_value(Decimal('5.2345')), Decimal('5.23'))
|
|
87
|
+
self.assertEqual(field.to_internal_value(Decimal('5.2356')), Decimal('5.24'))
|
|
88
|
+
self.assertEqual(field.to_internal_value(Decimal('4.2399')), Decimal('4.24'))
|
|
89
|
+
|
|
90
|
+
floored_field = RoundedDecimalField(rounding=ROUND_DOWN)
|
|
91
|
+
self.assertEqual(floored_field.to_internal_value(5.2345), Decimal('5.23'))
|
|
92
|
+
self.assertEqual(floored_field.to_internal_value(5.2356), Decimal('5.23'))
|
|
93
|
+
self.assertEqual(floored_field.to_internal_value(Decimal('5.2345')), Decimal('5.23'))
|
|
94
|
+
self.assertEqual(floored_field.to_internal_value(Decimal('5.2356')), Decimal('5.23'))
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import absolute_import, print_function, unicode_literals
|
|
2
|
+
import unittest
|
|
3
|
+
|
|
4
|
+
from ...fields import _fields
|
|
5
|
+
from ...fields.mixins import AllowBlankNullFieldMixin, EmptyStringFieldMixin
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestFields(unittest.TestCase):
|
|
9
|
+
def test_subclasses(self):
|
|
10
|
+
for f in _fields.FIELDS:
|
|
11
|
+
f = getattr(_fields, f)
|
|
12
|
+
self.assertTrue(issubclass(f, EmptyStringFieldMixin))
|
|
13
|
+
self.assertTrue(issubclass(f, AllowBlankNullFieldMixin))
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from __future__ import absolute_import, print_function, unicode_literals
|
|
2
|
+
import unittest
|
|
3
|
+
|
|
4
|
+
import mock
|
|
5
|
+
from rest_framework import fields
|
|
6
|
+
|
|
7
|
+
from ...fields.mixins import (
|
|
8
|
+
AllowBlankNullFieldMixin,
|
|
9
|
+
EmptyStringFieldMixin,
|
|
10
|
+
ValueAsTextFieldMixin,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestEmptyStringFieldMixin(unittest.TestCase):
|
|
15
|
+
def setUp(self):
|
|
16
|
+
super(TestEmptyStringFieldMixin, self).setUp()
|
|
17
|
+
|
|
18
|
+
class Field(EmptyStringFieldMixin, fields.IntegerField):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
self.field = Field()
|
|
22
|
+
|
|
23
|
+
def test_validate_empty_values_empty_string_required(self):
|
|
24
|
+
with self.assertRaises(fields.ValidationError):
|
|
25
|
+
self.field.validate_empty_values('')
|
|
26
|
+
|
|
27
|
+
def test_validate_empty_values_empty_string(self):
|
|
28
|
+
self.field.required = False
|
|
29
|
+
|
|
30
|
+
actual = self.field.validate_empty_values('')
|
|
31
|
+
|
|
32
|
+
self.assertTupleEqual(actual, (True, ''))
|
|
33
|
+
|
|
34
|
+
def test_validate_empty_values(self):
|
|
35
|
+
self.field.required = False
|
|
36
|
+
self.field.allow_null = True
|
|
37
|
+
|
|
38
|
+
actual = self.field.validate_empty_values(None)
|
|
39
|
+
|
|
40
|
+
self.assertTupleEqual(actual, (True, None))
|
|
41
|
+
|
|
42
|
+
def test_representation(self):
|
|
43
|
+
self.field.required = False
|
|
44
|
+
|
|
45
|
+
self.assertEqual(self.field.to_representation('50'), 50)
|
|
46
|
+
self.assertEqual(self.field.to_representation(''), '')
|
|
47
|
+
|
|
48
|
+
def test_representation_required(self):
|
|
49
|
+
self.field.required = True
|
|
50
|
+
|
|
51
|
+
self.assertEqual(self.field.to_representation('50'), 50)
|
|
52
|
+
with self.assertRaises(ValueError):
|
|
53
|
+
self.field.to_representation('')
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TestAllowBlankFieldMixin(unittest.TestCase):
|
|
57
|
+
def setUp(self):
|
|
58
|
+
super(TestAllowBlankFieldMixin, self).setUp()
|
|
59
|
+
|
|
60
|
+
class Field(AllowBlankNullFieldMixin, fields.CharField):
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
self.field_class = Field
|
|
64
|
+
|
|
65
|
+
def test_init(self):
|
|
66
|
+
field = self.field_class(required=False)
|
|
67
|
+
|
|
68
|
+
self.assertTrue(field.allow_blank)
|
|
69
|
+
self.assertTrue(field.allow_null)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class TestValueAsTextFieldMixin(unittest.TestCase):
|
|
73
|
+
def setUp(self):
|
|
74
|
+
super(TestValueAsTextFieldMixin, self).setUp()
|
|
75
|
+
|
|
76
|
+
class Field(ValueAsTextFieldMixin, fields.IntegerField):
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
self.field = Field(required=False, allow_null=True, max_value=100)
|
|
80
|
+
|
|
81
|
+
def test_to_string_value(self):
|
|
82
|
+
self.assertIsNone(self.field.to_string_value(None))
|
|
83
|
+
self.assertEqual(self.field.to_string_value(5), '5')
|
|
84
|
+
|
|
85
|
+
def test_prepare_value_for_validation(self):
|
|
86
|
+
self.assertEqual(
|
|
87
|
+
self.field.prepare_value_for_validation(mock.sentinel.value),
|
|
88
|
+
mock.sentinel.value
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def test_run_validation(self):
|
|
92
|
+
self.assertIsNone(self.field.run_validation(None))
|
|
93
|
+
|
|
94
|
+
self.assertEqual(self.field.run_validation(50), '50')
|
|
95
|
+
with self.assertRaises(fields.ValidationError):
|
|
96
|
+
self.field.run_validation(500)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import absolute_import, print_function, unicode_literals
|
|
2
|
+
import unittest
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
|
|
5
|
+
import pytz
|
|
6
|
+
from django.test.utils import override_settings
|
|
7
|
+
|
|
8
|
+
from ...fields.modified import BooleanField, DateTimeField, DecimalField
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestBooleanField(unittest.TestCase):
|
|
12
|
+
def test_init(self):
|
|
13
|
+
field = BooleanField(true_values=['Y', 'Yes'], false_values=['N', 'No'])
|
|
14
|
+
self.assertIn('Y', field.TRUE_VALUES)
|
|
15
|
+
self.assertIn('Yes', field.TRUE_VALUES)
|
|
16
|
+
self.assertIn('N', field.FALSE_VALUES)
|
|
17
|
+
self.assertIn('No', field.FALSE_VALUES)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestDecimalField(unittest.TestCase):
|
|
21
|
+
def test_init(self):
|
|
22
|
+
field = DecimalField()
|
|
23
|
+
self.assertIsNone(field.max_digits)
|
|
24
|
+
self.assertIsNone(field.decimal_places)
|
|
25
|
+
|
|
26
|
+
def test_quantize(self):
|
|
27
|
+
field = DecimalField()
|
|
28
|
+
self.assertIsNone(field.quantize(None))
|
|
29
|
+
|
|
30
|
+
field = DecimalField(max_digits=4, decimal_places=3)
|
|
31
|
+
self.assertEqual(field.quantize(Decimal('5.1234567')), Decimal('5.123'))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TestDateTimeField(unittest.TestCase):
|
|
35
|
+
@override_settings(USE_TZ=True)
|
|
36
|
+
def test_init(self):
|
|
37
|
+
value = '2015-01-02T16:00'
|
|
38
|
+
|
|
39
|
+
self.assertIsNotNone(DateTimeField(default_timezone=pytz.utc).run_validation(value).tzinfo)
|
|
40
|
+
self.assertIsNone(DateTimeField(default_timezone=None).run_validation(value).tzinfo)
|
|
File without changes
|