mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-08-02 19:40:13 +03:00
Merge 5214615bff
into 315435d00a
This commit is contained in:
commit
b359af72a9
|
@ -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))
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -887,43 +887,66 @@ class ModelSerializer(Serializer):
|
||||||
"""
|
"""
|
||||||
Save the deserialized object and return it.
|
Save the deserialized object and return it.
|
||||||
"""
|
"""
|
||||||
if getattr(obj, '_nested_forward_relations', None):
|
def save_nested_forward_relations(obj):
|
||||||
# Nested relationships need to be saved before we can save the
|
"""
|
||||||
# parent instance.
|
Save nested nested forward relations
|
||||||
for field_name, sub_object in obj._nested_forward_relations.items():
|
"""
|
||||||
if sub_object:
|
if getattr(obj, '_nested_forward_relations', None):
|
||||||
self.save_object(sub_object)
|
# Nested relationships need to be saved before we can save the
|
||||||
setattr(obj, field_name, sub_object)
|
# parent instance.
|
||||||
|
for field_name, sub_object in obj._nested_forward_relations.items():
|
||||||
|
if sub_object:
|
||||||
|
self.save_object(sub_object)
|
||||||
|
setattr(obj, field_name, sub_object)
|
||||||
|
|
||||||
obj.save(**kwargs)
|
obj.save(**kwargs)
|
||||||
|
|
||||||
if getattr(obj, '_m2m_data', None):
|
def save_m2m(obj):
|
||||||
for accessor_name, object_list in obj._m2m_data.items():
|
"""
|
||||||
setattr(obj, accessor_name, object_list)
|
Save nested ManyToMany relations
|
||||||
del(obj._m2m_data)
|
"""
|
||||||
|
if getattr(obj, '_m2m_data', None) and hasattr(obj._m2m_data, '__iter__'):
|
||||||
|
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)
|
||||||
|
m2m_object.save()
|
||||||
|
del(obj._m2m_data)
|
||||||
|
|
||||||
if getattr(obj, '_related_data', None):
|
def save_related_data(obj):
|
||||||
for accessor_name, related in obj._related_data.items():
|
"""
|
||||||
if isinstance(related, RelationsList):
|
Save nested nested related data
|
||||||
# Nested reverse fk relationship
|
"""
|
||||||
for related_item in related:
|
if getattr(obj, '_related_data', None):
|
||||||
|
for accessor_name, related in obj._related_data.items():
|
||||||
|
if isinstance(related, RelationsList):
|
||||||
|
# Nested reverse fk relationship
|
||||||
|
for related_item in related:
|
||||||
|
fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name
|
||||||
|
setattr(related_item, fk_field, obj)
|
||||||
|
self.save_object(related_item)
|
||||||
|
|
||||||
|
# Delete any removed objects
|
||||||
|
if related._deleted:
|
||||||
|
[self.delete_object(item) for item in related._deleted]
|
||||||
|
|
||||||
|
elif isinstance(related, models.Model):
|
||||||
|
# Nested reverse one-one relationship
|
||||||
fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name
|
fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name
|
||||||
setattr(related_item, fk_field, obj)
|
setattr(related, fk_field, obj)
|
||||||
self.save_object(related_item)
|
self.save_object(related)
|
||||||
|
else:
|
||||||
|
# Reverse FK or reverse one-one
|
||||||
|
setattr(obj, accessor_name, related)
|
||||||
|
del(obj._related_data)
|
||||||
|
|
||||||
# Delete any removed objects
|
# Save
|
||||||
if related._deleted:
|
save_nested_forward_relations(obj)
|
||||||
[self.delete_object(item) for item in related._deleted]
|
save_m2m(obj)
|
||||||
|
save_related_data(obj)
|
||||||
elif isinstance(related, models.Model):
|
|
||||||
# Nested reverse one-one relationship
|
|
||||||
fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name
|
|
||||||
setattr(related, fk_field, obj)
|
|
||||||
self.save_object(related)
|
|
||||||
else:
|
|
||||||
# Reverse FK or reverse one-one
|
|
||||||
setattr(obj, accessor_name, related)
|
|
||||||
del(obj._related_data)
|
|
||||||
|
|
||||||
|
|
||||||
class HyperlinkedModelSerializerOptions(ModelSerializerOptions):
|
class HyperlinkedModelSerializerOptions(ModelSerializerOptions):
|
||||||
|
|
|
@ -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,
|
||||||
|
|
38
rest_framework/utils/datastructures.py
Executable file
38
rest_framework/utils/datastructures.py
Executable 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}
|
Loading…
Reference in New Issue
Block a user