diff --git a/src/wirecloud/commons/fields.py b/src/wirecloud/commons/fields.py index 3aad07daab..2f0e630d1d 100644 --- a/src/wirecloud/commons/fields.py +++ b/src/wirecloud/commons/fields.py @@ -2,9 +2,7 @@ import json from django.core.exceptions import ValidationError -from django.db import connection, models - -from django.utils.encoding import smart_text +from django.db import models class JSONField(models.TextField): @@ -24,9 +22,8 @@ def get_default(self): if callable(self.default): return copy.deepcopy(self.default()) return copy.deepcopy(self.default) - if not self.empty_strings_allowed or (self.null and not connection.features.interprets_empty_strings_as_nulls): - return None - return "" + else: + return None if self.null else {} def from_db_value(self, value, expression, connection, context): return self.to_python(value) @@ -36,11 +33,10 @@ def to_python(self, value): Convert the input JSON value into python structures, raises django.core.exceptions.ValidationError if the data can't be converted. """ - if self.blank and not value: - return {} + if self.null and value is None: + return None + value = value or '{}' - if isinstance(value, bytes): - value = str(value, 'utf-8') if isinstance(value, str): try: return json.loads(value) @@ -49,16 +45,6 @@ def to_python(self, value): else: return value - def validate(self, value, model_instance): - """Check value is a valid JSON string, raise ValidationError on - error.""" - if isinstance(value, str): - super(JSONField, self).validate(value, model_instance) - try: - json.loads(value) - except Exception as err: - raise ValidationError(str(err)) - def get_prep_value(self, value): """Convert value to JSON string before save""" try: @@ -67,9 +53,9 @@ def get_prep_value(self, value): raise ValidationError(str(err)) def value_to_string(self, obj): - """Return value from object converted to string properly""" - return smart_text(self.get_prep_value(self._get_val_from_obj(obj))) + """Converts obj to a string. Used to serialize the value of the field.""" + return self.value_from_object(obj) def value_from_object(self, obj): """Return value dumped to string.""" - return self.get_prep_value(self._get_val_from_obj(obj)) + return self.get_prep_value(super(JSONField, self).value_from_object(obj)) diff --git a/src/wirecloud/commons/tests/__init__.py b/src/wirecloud/commons/tests/__init__.py index cffd0dc0bb..73fbb7ff7a 100644 --- a/src/wirecloud/commons/tests/__init__.py +++ b/src/wirecloud/commons/tests/__init__.py @@ -1,14 +1,17 @@ from wirecloud.commons.tests.admin_commands import BaseAdminCommandTestCase, ConvertCommandTestCase, StartprojectCommandTestCase from wirecloud.commons.tests.basic_views import BasicViewTestCase from wirecloud.commons.tests.commands import CreateOrganizationCommandTestCase +from wirecloud.commons.tests.fields import JSONFieldTestCase from wirecloud.commons.tests.search_indexes import QueryParserTestCase, SearchAPITestCase, GroupIndexTestCase, UserIndexTestCase from wirecloud.commons.tests.template import TemplateUtilsTestCase from wirecloud.commons.tests.utils import GeneralUtilsTestCase, HTMLCleanupTestCase, WGTTestCase, HTTPUtilsTestCase __all__ = ( - "BaseAdminCommandTestCase", "ConvertCommandTestCase", - "StartprojectCommandTestCase", "BasicViewTestCase", + "BaseAdminCommandTestCase", "BasicViewTestCase", "ConvertCommandTestCase", + "CreateOrganizationCommandTestCase", "GeneralUtilsTestCase", + "GroupIndexTestCase", "HTMLCleanupTestCase", "HTTPUtilsTestCase", + "JSONFieldTestCase", "QueryParserTestCase", "ResetSearchIndexesCommandTestCase", "SearchAPITestCase", - "TemplateUtilsTestCase", "GeneralUtilsTestCase", - "HTMLCleanupTestCase", "WGTTestCase", "HTTPUtilsTestCase" + "StartprojectCommandTestCase", "TemplateUtilsTestCase", "UserIndexTestCase", + "WGTTestCase" ) diff --git a/src/wirecloud/commons/tests/fields.py b/src/wirecloud/commons/tests/fields.py new file mode 100644 index 0000000000..247858524f --- /dev/null +++ b/src/wirecloud/commons/tests/fields.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2019 Future Internet Consulting and Development Solutions S.L. + +# This file is part of Wirecloud. + +# Wirecloud is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# Wirecloud is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with Wirecloud. If not, see . + +from unittest.mock import patch, Mock + +from django.core.exceptions import ValidationError +from django.db.models.fields import NOT_PROVIDED +from django.test import TestCase + +from wirecloud.commons.fields import JSONField + + +# Avoid nose to repeat these tests (they are run through wirecloud/commons/tests/__init__.py) +__test__ = False + + +class JSONFieldTestCase(TestCase): + + tags = ('wirecloud-fields', 'wirecloud-noselenium') + + def test_get_default_none(self): + field = JSONField() + + default = field.get_default() + self.assertEqual(default, {}) + + def test_get_default_not_provided(self): + field = JSONField(null=False, default=NOT_PROVIDED) + + self.assertEqual(field.get_default(), {}) + + def test_get_default_not_provided_null(self): + field = JSONField(null=True, default=NOT_PROVIDED) + + self.assertEqual(field.get_default(), None) + + def test_get_default_static_value(self): + obj = {"h": "w"} + field = JSONField(default=obj) + + default = field.get_default() + self.assertEqual(default, obj) + default["h"] = "a" + self.assertNotEqual(default, obj) + + def test_get_default_callable(self): + obj = {"h": "w"} + field = JSONField(default=lambda: obj) + + default = field.get_default() + self.assertEqual(default, obj) + default["h"] = "a" + self.assertNotEqual(default, obj) + + def test_from_db_value(self): + field = JSONField() + field.to_python = Mock() + value = '{"a": "b"}' + + self.assertEqual(field.from_db_value(value, None, None, None), field.to_python(value)) + + field.to_python.assert_called_with(value) + + def test_to_python_null_none(self): + field = JSONField(null=True) + self.assertEqual(field.to_python(None), None) + + def test_to_python_none(self): + field = JSONField(null=False) + self.assertEqual(field.to_python(None), {}) + + def test_to_python_str(self): + field = JSONField() + obj = {"a": "c"} + self.assertEqual(field.to_python('{"a": "c"}'), obj) + + def test_to_python_str_exception(self): + field = JSONField() + self.assertRaises(ValidationError, field.to_python, 'invalid json') + + def test_to_python_json(self): + field = JSONField() + obj = {"a": "c"} + self.assertEqual(field.to_python(obj), obj) + + def test_get_prep_value_exception(self): + field = JSONField() + self.assertRaises(ValidationError, field.get_prep_value, set()) + + @patch("wirecloud.commons.fields.json") + def test_implements_value_from_object(self, json): + field = JSONField() + field.set_attributes_from_name("myfield") + obj = Mock(myfield={"a": "b"}) + json.dumps.return_value = "serialized json" + + self.assertEqual(field.value_from_object(obj), "serialized json") + + json.dumps.assert_called_once_with({"a": "b"}) + + @patch("wirecloud.commons.fields.json") + def test_implements_value_to_string(self, json): + field = JSONField() + field.set_attributes_from_name("myfield") + obj = Mock(myfield={"a": "b"}) + json.dumps.return_value = "serialized json" + + self.assertEqual(field.value_to_string(obj), "serialized json") + + json.dumps.assert_called_once_with({"a": "b"})