summaryrefslogtreecommitdiff
path: root/django/forms
diff options
context:
space:
mode:
Diffstat (limited to 'django/forms')
-rw-r--r--django/forms/fields.py71
1 files changed, 68 insertions, 3 deletions
diff --git a/django/forms/fields.py b/django/forms/fields.py
index c5374c7e9d..36dad72704 100644
--- a/django/forms/fields.py
+++ b/django/forms/fields.py
@@ -4,6 +4,7 @@ Field classes.
import copy
import datetime
+import json
import math
import operator
import os
@@ -21,8 +22,8 @@ from django.forms.widgets import (
FILE_INPUT_CONTRADICTION, CheckboxInput, ClearableFileInput, DateInput,
DateTimeInput, EmailInput, FileInput, HiddenInput, MultipleHiddenInput,
NullBooleanSelect, NumberInput, Select, SelectMultiple,
- SplitDateTimeWidget, SplitHiddenDateTimeWidget, TextInput, TimeInput,
- URLInput,
+ SplitDateTimeWidget, SplitHiddenDateTimeWidget, Textarea, TextInput,
+ TimeInput, URLInput,
)
from django.utils import formats
from django.utils.dateparse import parse_datetime, parse_duration
@@ -38,7 +39,8 @@ __all__ = (
'BooleanField', 'NullBooleanField', 'ChoiceField', 'MultipleChoiceField',
'ComboField', 'MultiValueField', 'FloatField', 'DecimalField',
'SplitDateTimeField', 'GenericIPAddressField', 'FilePathField',
- 'SlugField', 'TypedChoiceField', 'TypedMultipleChoiceField', 'UUIDField',
+ 'JSONField', 'SlugField', 'TypedChoiceField', 'TypedMultipleChoiceField',
+ 'UUIDField',
)
@@ -1211,3 +1213,66 @@ class UUIDField(CharField):
except ValueError:
raise ValidationError(self.error_messages['invalid'], code='invalid')
return value
+
+
+class InvalidJSONInput(str):
+ pass
+
+
+class JSONString(str):
+ pass
+
+
+class JSONField(CharField):
+ default_error_messages = {
+ 'invalid': _('Enter a valid JSON.'),
+ }
+ widget = Textarea
+
+ def __init__(self, encoder=None, decoder=None, **kwargs):
+ self.encoder = encoder
+ self.decoder = decoder
+ super().__init__(**kwargs)
+
+ def to_python(self, value):
+ if self.disabled:
+ return value
+ if value in self.empty_values:
+ return None
+ elif isinstance(value, (list, dict, int, float, JSONString)):
+ return value
+ try:
+ converted = json.loads(value, cls=self.decoder)
+ except json.JSONDecodeError:
+ raise ValidationError(
+ self.error_messages['invalid'],
+ code='invalid',
+ params={'value': value},
+ )
+ if isinstance(converted, str):
+ return JSONString(converted)
+ else:
+ return converted
+
+ def bound_data(self, data, initial):
+ if self.disabled:
+ return initial
+ try:
+ return json.loads(data, cls=self.decoder)
+ except json.JSONDecodeError:
+ return InvalidJSONInput(data)
+
+ def prepare_value(self, value):
+ if isinstance(value, InvalidJSONInput):
+ return value
+ return json.dumps(value, cls=self.encoder)
+
+ def has_changed(self, initial, data):
+ if super().has_changed(initial, data):
+ return True
+ # For purposes of seeing whether something has changed, True isn't the
+ # same as 1 and the order of keys doesn't matter.
+ return (
+ json.dumps(initial, sort_keys=True, cls=self.encoder) !=
+ json.dumps(self.to_python(data), sort_keys=True, cls=self.encoder)
+ )