This commit is contained in:
Dan Stephenson 2013-09-10 08:21:45 -07:00
commit b359af72a9
5 changed files with 116 additions and 36 deletions

View File

@ -13,6 +13,8 @@ from django.http.multipartparser import MultiPartParserError, parse_header, Chun
from rest_framework.compat import yaml, etree from rest_framework.compat import yaml, etree
from rest_framework.exceptions import ParseError from rest_framework.exceptions import ParseError
from rest_framework.compat import six from rest_framework.compat import six
from rest_framework.utils.datastructures import TokenExpandedDict
from rest_framework.settings import api_settings
import json import json
import datetime import datetime
import decimal import decimal
@ -40,6 +42,16 @@ class BaseParser(object):
""" """
raise NotImplementedError(".parse() must be overridden.") raise NotImplementedError(".parse() must be overridden.")
def _parse_tokenization(self, data):
"""
Configuration dependant processing of input data, where character tokens
(such as periods '.') can be used to reshape for data into nested form,
for use with NestedModelSerializer.
"""
if api_settings.NESTED_FIELDS:
data = TokenExpandedDict(data)
return data
class JSONParser(BaseParser): class JSONParser(BaseParser):
""" """
@ -108,6 +120,8 @@ class FormParser(BaseParser):
parser_context = parser_context or {} parser_context = parser_context or {}
encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET) encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET)
data = QueryDict(stream.read(), encoding=encoding) data = QueryDict(stream.read(), encoding=encoding)
data = self._parse_tokenization(data)
return data return data
@ -134,6 +148,8 @@ class MultiPartParser(BaseParser):
try: try:
parser = DjangoMultiPartParser(meta, stream, upload_handlers, encoding) parser = DjangoMultiPartParser(meta, stream, upload_handlers, encoding)
data, files = parser.parse() data, files = parser.parse()
data = self._parse_tokenization(data)
return DataAndFiles(data, files) return DataAndFiles(data, files)
except MultiPartParserError as exc: except MultiPartParserError as exc:
raise ParseError('Multipart form parse error - %s' % six.u(exc)) raise ParseError('Multipart form parse error - %s' % six.u(exc))

View File

@ -150,8 +150,8 @@ class Request(object):
Similar to usual behaviour of `request.POST`, except that it handles Similar to usual behaviour of `request.POST`, except that it handles
arbitrary parsers, and also works on methods other than POST (eg PUT). arbitrary parsers, and also works on methods other than POST (eg PUT).
""" """
if not _hasattr(self, '_data'): #if not _hasattr(self, '_data'):
self._load_data_and_files() # self._load_data_and_files()
return self._data return self._data
@property @property
@ -277,8 +277,7 @@ class Request(object):
return return
# At this point we're committed to parsing the request as form data. # At this point we're committed to parsing the request as form data.
self._data = self._request.POST self._data, self._files = self._parse()
self._files = self._request.FILES
# Method overloading - change the method and remove the param from the content. # Method overloading - change the method and remove the param from the content.
if (self._METHOD_PARAM and if (self._METHOD_PARAM and

View File

@ -887,6 +887,10 @@ class ModelSerializer(Serializer):
""" """
Save the deserialized object and return it. Save the deserialized object and return it.
""" """
def save_nested_forward_relations(obj):
"""
Save nested nested forward relations
"""
if getattr(obj, '_nested_forward_relations', None): if getattr(obj, '_nested_forward_relations', None):
# Nested relationships need to be saved before we can save the # Nested relationships need to be saved before we can save the
# parent instance. # parent instance.
@ -897,11 +901,25 @@ class ModelSerializer(Serializer):
obj.save(**kwargs) obj.save(**kwargs)
if getattr(obj, '_m2m_data', None): def save_m2m(obj):
"""
Save nested ManyToMany relations
"""
if getattr(obj, '_m2m_data', None) and hasattr(obj._m2m_data, '__iter__'):
for accessor_name, object_list in obj._m2m_data.items(): for accessor_name, object_list in obj._m2m_data.items():
if hasattr(object_list, '__iter__'):
for m2m_object in object_list:
save_nested_forward_relations(m2m_object)
save_m2m(m2m_object)
save_related_data(m2m_object)
setattr(obj, accessor_name, object_list) setattr(obj, accessor_name, object_list)
m2m_object.save()
del(obj._m2m_data) del(obj._m2m_data)
def save_related_data(obj):
"""
Save nested nested related data
"""
if getattr(obj, '_related_data', None): if getattr(obj, '_related_data', None):
for accessor_name, related in obj._related_data.items(): for accessor_name, related in obj._related_data.items():
if isinstance(related, RelationsList): if isinstance(related, RelationsList):
@ -925,6 +943,11 @@ class ModelSerializer(Serializer):
setattr(obj, accessor_name, related) setattr(obj, accessor_name, related)
del(obj._related_data) del(obj._related_data)
# Save
save_nested_forward_relations(obj)
save_m2m(obj)
save_related_data(obj)
class HyperlinkedModelSerializerOptions(ModelSerializerOptions): class HyperlinkedModelSerializerOptions(ModelSerializerOptions):
""" """

View File

@ -65,6 +65,10 @@ DEFAULTS = {
'anon': None, 'anon': None,
}, },
# Nested (multi-dimensional) field names
'NESTED_FIELDS': False,
'NESTED_FIELD_TOKENIZER': '.',
# Pagination # Pagination
'PAGINATE_BY': None, 'PAGINATE_BY': None,
'PAGINATE_BY_PARAM': None, 'PAGINATE_BY_PARAM': None,

View File

@ -0,0 +1,38 @@
"""
Utility functions for reshaping datastructures
"""
from rest_framework.settings import api_settings
from django.http import QueryDict
class TokenExpandedDict(QueryDict):
"""
A special dictionary constructor that takes a dictionary in which the keys
may contain dots to specify inner dictionaries. It's confusing, but this
example should make sense.
>>> d = TokenExpandedDict({'person.1.firstname': ['Simon'], \
'person.1.lastname': ['Willison'], \
'person.2.firstname': ['Adrian'], \
'person.2.lastname': ['Holovaty']})
>>> d
{'person': {'1': {'lastname': ['Willison'], 'firstname': ['Simon']}, '2': {'lastname': ['Holovaty'], 'firstname': ['Adrian']}}}
>>> d['person']
{'1': {'lastname': ['Willison'], 'firstname': ['Simon']}, '2': {'lastname': ['Holovaty'], 'firstname': ['Adrian']}}
>>> d['person']['1']
{'lastname': ['Willison'], 'firstname': ['Simon']}
# Gotcha: Results are unpredictable if the dots are "uneven":
>>> TokenExpandedDict({'c.1': 2, 'c.2': 3, 'c': 1})
{'c': 1}
"""
def __init__(self, key_to_list_mapping):
for k, v in key_to_list_mapping.items():
current = self
bits = k.split(api_settings.NESTED_FIELD_TOKENIZER)
for bit in bits[:-1]:
current = current.setdefault(bit, {})
# Now assign value to current position
try:
current[bits[-1]] = v
except TypeError: # Special-case if current isn't a dict.
current = {bits[-1]: v}