diff --git a/rest_framework/utils/html.py b/rest_framework/utils/html.py
index f5a915c25..77e1402d7 100644
--- a/rest_framework/utils/html.py
+++ b/rest_framework/utils/html.py
@@ -18,7 +18,7 @@ def parse_json_form(dictionary, prefix=''):
"""
# Step 1: Initialize output object
output = {}
- for name, value in dictionary.items():
+ for name, value in get_all_items(dictionary):
# TODO: implement is_file flag
# Step 2: Compute steps array
@@ -40,6 +40,8 @@ def parse_json_form(dictionary, prefix=''):
entry_value=value,
is_file=False,
)
+ # Convert any remaining Undefined array entries to None
+ output = clean_undefined(output)
# Account for DRF prefix (not part of JSON form spec)
result = get_value(output, prefix, Undefined())
@@ -101,7 +103,7 @@ def parse_json_path(path):
# Step 8 - Loop
while path:
# Step 8.1 - Check for single-item array
- if path[:1] == "[]":
+ if path[:2] == "[]":
steps[-1].append = True
path = path[2:]
if path:
@@ -153,8 +155,6 @@ def set_json_value(context, step, current_value, entry_value, is_file):
# TODO: handle is_file
# Add empty values to array so indexing works like JavaScript
- # TODO: According to spec, these should turn into nulls if they are never
- # overriden in a later step.
if isinstance(context, list) and isinstance(step.key, int):
while len(context) <= step.key:
context.append(Undefined())
@@ -163,13 +163,22 @@ def set_json_value(context, step, current_value, entry_value, is_file):
if step.last:
if isinstance(current_value, Undefined):
# Step 7.1: No existing value
+ key = step.key
+ if isinstance(context, dict) and isinstance(key, int):
+ key = str(key)
if step.append:
- context[step.key] = [entry_value]
+ context[key] = [entry_value]
else:
- context[step.key] = entry_value
+ context[key] = entry_value
elif isinstance(current_value, list):
- # Step 7.2: Existing value is an Array
+ # Step 7.2: Existing value is an Array, assume multi-valued field
+ # and add entry to end.
+
+ # FIXME: What if the other items in the array had explicit keys and
+ # this one is supposed to be the "" value?
+ # (See step 8.4 and Example 7)
context[step.key].append(entry_value)
+
elif isinstance(current_value, dict) and not is_file:
# Step 7.3: Existing value is an Object
return set_json_value(
@@ -195,20 +204,21 @@ def set_json_value(context, step, current_value, entry_value, is_file):
context[step.key] = {}
return context[step.key]
elif isinstance(current_value, dict):
- # Step 7.2: Existing value is an Object
+ # Step 8.2: Existing value is an Object
return get_value(context, step.key, Undefined())
elif isinstance(current_value, list):
- # Step 7.3: Existing value is an Array
+ # Step 8.3: Existing value is an Array
if step.next_type == "array":
return current_value
+ # Convert array to object to facilitate mixed keys
obj = {}
for i, item in enumerate(current_value):
if not isinstance(item, Undefined):
- obj[i] = item
- context[step.key] = obj
- return obj
+ obj[str(i)] = item
+ context[step.key] = obj
+ return obj
else:
- # 7.4: Existing value is a scalar; convert to Object, preserving
+ # 8.4: Existing value is a scalar; convert to Object, preserving
# current value via an empty key
obj = {'': current_value}
context[step.key] = obj
@@ -255,3 +265,32 @@ class JsonStep(object):
append = None
last = None
failed = None
+
+
+def clean_undefined(obj):
+ """
+ Convert Undefined array entries to None (null)
+ """
+ if isinstance(obj, list):
+ return [
+ None if isinstance(item, Undefined) else item
+ for item in obj
+ ]
+ if isinstance(obj, dict):
+ for key in obj:
+ obj[key] = clean_undefined(obj[key])
+ return obj
+
+
+def get_all_items(obj):
+ """
+ dict.items() but with a separate row for each value in a MultiValueDict
+ """
+ if hasattr(obj, 'getlist'):
+ items = []
+ for key in obj:
+ for value in obj.getlist(key):
+ items.append((key, value))
+ return items
+ else:
+ return obj.items()
diff --git a/tests/test_json_forms.py b/tests/test_json_forms.py
new file mode 100644
index 000000000..e98247b83
--- /dev/null
+++ b/tests/test_json_forms.py
@@ -0,0 +1,178 @@
+from rest_framework.utils import html
+from django.utils.datastructures import MultiValueDict
+
+
+class TestHtmlJsonExamples:
+ """
+ Tests for the HTML JSON form algorithm
+ Examples 1-10 from http://www.w3.org/TR/html-json-forms/
+ """
+
+ def test_basic_keys(self):
+ """
+ Example 1: Basic Keys
+ """
+ input_data = {
+ 'name': "Bender",
+ 'hind': "Bitable",
+ 'shiny': True
+ }
+ expected_output = input_data
+ result = html.parse_json_form(input_data)
+ assert result == expected_output
+
+ def test_multiple_values(self):
+ """
+ Example 2: Multiple Values
+ """
+ input_data = MultiValueDict()
+ input_data.setlist('bottle-on-wall', [1, 2, 3])
+ expected_output = {
+ 'bottle-on-wall': [1, 2, 3]
+ }
+ result = html.parse_json_form(input_data)
+ assert result == expected_output
+
+ def test_deeper_structure(self):
+ """
+ Example 3: Deeper Structure
+ """
+ input_data = {
+ 'pet[species]': "Dahut",
+ 'pet[name]': "Hypatia",
+ 'kids[1]': "Thelma",
+ 'kids[0]': "Ashley",
+ }
+ expected_output = {
+ 'pet': {
+ 'species': "Dahut",
+ 'name': "Hypatia",
+ },
+ 'kids': ["Ashley", "Thelma"],
+ }
+ result = html.parse_json_form(input_data)
+ assert result == expected_output
+
+ def test_sparse_arrays(self):
+ """
+ Example 4: Sparse Arrays
+ """
+ input_data = {
+ "hearbeat[0]": "thunk",
+ "hearbeat[2]": "thunk",
+ }
+ expected_output = {
+ "hearbeat": ["thunk", None, "thunk"]
+ }
+ result = html.parse_json_form(input_data)
+ assert result == expected_output
+
+ def test_even_deeper(self):
+ """
+ Example 5: Even Deeper
+ """
+ input_data = {
+ 'pet[0][species]': "Dahut",
+ 'pet[0][name]': "Hypatia",
+ 'pet[1][species]': "Felis Stultus",
+ 'pet[1][name]': "Billie",
+ }
+ expected_output = {
+ 'pet': [
+ {
+ 'species': "Dahut",
+ 'name': "Hypatia",
+ },
+ {
+ 'species': "Felis Stultus",
+ 'name': "Billie",
+ }
+ ]
+ }
+ result = html.parse_json_form(input_data)
+ assert result == expected_output
+
+ def test_such_deep(self):
+ """
+ Example 6: Such Deep
+ """
+ input_data = {
+ 'wow[such][deep][3][much][power][!]': "Amaze",
+ }
+ expected_output = {
+ 'wow': {
+ 'such': {
+ 'deep': [
+ None,
+ None,
+ None,
+ {
+ 'much': {
+ 'power': {
+ '!': "Amaze",
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ result = html.parse_json_form(input_data)
+ assert result == expected_output
+
+ def test_merge_behaviour(self):
+ """
+ Example 7: Merge Behaviour
+ """
+ # FIXME: Shouldn't this work regardless of key order?
+ from rest_framework.compat import OrderedDict
+ input_data = OrderedDict([
+ ('mix', "scalar"),
+ ('mix[0]', "array 1"),
+ ('mix[2]', "array 2"),
+ ('mix[key]', "key key"),
+ ('mix[car]', "car key"),
+ ])
+ expected_output = {
+ 'mix': {
+ '': "scalar",
+ '0': "array 1",
+ '2': "array 2",
+ 'key': "key key",
+ 'car': "car key",
+ }
+ }
+ result = html.parse_json_form(input_data)
+ assert result == expected_output
+
+ def test_append(self):
+ """
+ Example 8: Append
+ """
+ input_data = {
+ 'highlander[]': "one",
+ }
+ expected_output = {
+ 'highlander': ["one"],
+ }
+ result = html.parse_json_form(input_data)
+ assert result == expected_output
+
+ # TODO: Example 9: Files
+
+ def test_invalid(self):
+ """
+ Example 10: Invalid
+ """
+ input_data = {
+ "error[good]": "BOOM!",
+ "error[bad": "BOOM BOOM!",
+ }
+ expected_output = {
+ 'error': {
+ 'good': "BOOM!",
+ },
+ 'error[bad': "BOOM BOOM!",
+ }
+ result = html.parse_json_form(input_data)
+ assert result == expected_output