diff --git a/django/contrib/postgres/forms/jsonb.py b/django/contrib/postgres/forms/jsonb.py index 8eefc14993..415288dc90 100644 --- a/django/contrib/postgres/forms/jsonb.py +++ b/django/contrib/postgres/forms/jsonb.py @@ -1,11 +1,16 @@ import json from django import forms +from django.utils import six from django.utils.translation import ugettext_lazy as _ __all__ = ['JSONField'] +class InvalidJSONInput(six.text_type): + pass + + class JSONField(forms.CharField): default_error_messages = { 'invalid': _("'%(value)s' value must be valid JSON."), @@ -27,5 +32,15 @@ class JSONField(forms.CharField): params={'value': value}, ) + def bound_data(self, data, initial): + if self.disabled: + return initial + try: + return json.loads(data) + except ValueError: + return InvalidJSONInput(data) + def prepare_value(self, value): + if isinstance(value, InvalidJSONInput): + return value return json.dumps(value) diff --git a/django/forms/forms.py b/django/forms/forms.py index 7aceb905a7..0a15ec510f 100644 --- a/django/forms/forms.py +++ b/django/forms/forms.py @@ -365,13 +365,14 @@ class BaseForm(object): def _clean_fields(self): for name, field in self.fields.items(): + if field.disabled: + # Initial values are supposed to be clean + self.cleaned_data[name] = self.initial.get(name, field.initial) + continue # value_from_datadict() gets the data from the data dictionaries. # Each widget type knows how to retrieve its own data, because some # widgets split data over several HTML fields. - if field.disabled: - value = self.initial.get(name, field.initial) - else: - value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name)) + value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name)) try: if isinstance(field, FileField): initial = self.initial.get(name, field.initial) diff --git a/docs/releases/1.9.5.txt b/docs/releases/1.9.5.txt index fc2c6cea03..ddd5fd5551 100644 --- a/docs/releases/1.9.5.txt +++ b/docs/releases/1.9.5.txt @@ -46,3 +46,6 @@ Bugfixes * Fixed a migrations crash on SQLite when renaming the primary key of a model containing a ``ForeignKey`` to ``'self'`` (:ticket:`26384`). + +* Fixed ``JSONField`` inadvertently escaping its contents when displaying values + after failed form validation (:ticket:`25532`). diff --git a/tests/postgres_tests/test_json.py b/tests/postgres_tests/test_json.py index 59e5fe3fe7..11b6a349aa 100644 --- a/tests/postgres_tests/test_json.py +++ b/tests/postgres_tests/test_json.py @@ -3,7 +3,9 @@ import unittest from django.core import exceptions, serializers from django.db import connection +from django.forms import CharField, Form from django.test import TestCase +from django.utils.html import escape from . import PostgreSQLTestCase from .models import JSONModel @@ -258,7 +260,34 @@ class TestFormField(PostgreSQLTestCase): form_field = model_field.formfield() self.assertIsInstance(form_field, forms.JSONField) + def test_formfield_disabled(self): + class JsonForm(Form): + name = CharField() + jfield = forms.JSONField(disabled=True) + + form = JsonForm({'name': 'xyz', 'jfield': '["bar"]'}, initial={'jfield': ['foo']}) + self.assertIn('["foo"]', form.as_p()) + def test_prepare_value(self): field = forms.JSONField() self.assertEqual(field.prepare_value({'a': 'b'}), '{"a": "b"}') self.assertEqual(field.prepare_value(None), 'null') + self.assertEqual(field.prepare_value('foo'), '"foo"') + + def test_redisplay_wrong_input(self): + """ + When displaying a bound form (typically due to invalid input), the form + should not overquote JSONField inputs. + """ + class JsonForm(Form): + name = CharField(max_length=2) + jfield = forms.JSONField() + + # JSONField input is fine, name is too long + form = JsonForm({'name': 'xyz', 'jfield': '["foo"]'}) + self.assertIn('["foo"]', form.as_p()) + + # This time, the JSONField input is wrong + form = JsonForm({'name': 'xy', 'jfield': '{"foo"}'}) + # Appears once in the textarea and once in the error message + self.assertEqual(form.as_p().count(escape('{"foo"}')), 2)