From 789e49921bb7efac43ca0370f8eb5f17bd524760 Mon Sep 17 00:00:00 2001 From: tmig Date: Wed, 5 Feb 2020 14:14:42 +0100 Subject: [PATCH] add nested multipart support --- rest_framework/renderers.py | 8 +++++ rest_framework/settings.py | 1 + rest_framework/utils/encoders.py | 59 +++++++++++++++++++++++++++++++- 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index a96fa6e65..3061d2c8e 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -914,6 +914,14 @@ class MultiPartRenderer(BaseRenderer): return encode_multipart(self.BOUNDARY, data) +class NestedMultiPartRenderer(MultiPartRenderer): + format = 'nestedmultipart' + + def render(self, data, media_type=None, renderer_context=None): + encoder = encoders.NestedMultiPartEncoder() + return encoder.encode(self.BOUNDARY, data) + + class CoreJSONRenderer(BaseRenderer): media_type = 'application/coreapi+json' charset = None diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 9eb4c5653..209faba78 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -87,6 +87,7 @@ DEFAULTS = { # Testing 'TEST_REQUEST_RENDERER_CLASSES': [ + 'rest_framework.renderers.NestedMultiPartRenderer', 'rest_framework.renderers.MultiPartRenderer', 'rest_framework.renderers.JSONRenderer' ], diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py index 27293b725..e7548d8a3 100644 --- a/rest_framework/utils/encoders.py +++ b/rest_framework/utils/encoders.py @@ -6,10 +6,14 @@ import decimal import json # noqa import uuid +from django.conf import settings from django.db.models.query import QuerySet +from django.test.client import encode_file from django.utils import timezone -from django.utils.encoding import force_str +from django.utils.encoding import force_str, force_bytes from django.utils.functional import Promise +from django.utils.itercompat import is_iterable + from rest_framework.compat import coreapi @@ -65,3 +69,56 @@ class JSONEncoder(json.JSONEncoder): elif hasattr(obj, '__iter__'): return tuple(item for item in obj) return super().default(obj) + + +class NestedMultiPartEncoder: + def encode(self, boundary, data): + lines = [] + + def to_bytes(s): + return force_bytes(s, settings.DEFAULT_CHARSET) + + def is_file(thing): + return hasattr(thing, "read") and callable(thing.read) + + def to_lines(d, prefix='', dot='.'): + for (key, value) in d.items(): + key = f'{prefix}{dot}{key}' if prefix else key + + if value is None: + raise TypeError( + 'Cannot encode None as POST data. Did you mean to pass an ' + 'empty string or omit the value?' + ) + elif isinstance(value, dict): + to_lines(value, key) + elif is_file(value): + lines.extend(encode_file(boundary, key, value)) + elif not isinstance(value, str) and is_iterable(value): + for index, item in enumerate(value): + if isinstance(item, dict): + to_lines(item, f'{key}[{index}]', '') + elif is_file(item): + lines.extend(encode_file(boundary, f'{key}{[index]}', item)) + else: + lines.extend(to_bytes(val) for val in [ + f'--{boundary}', + f'Content-Disposition: form-data; name="{key}{[index]}"', + '', + item + ]) + else: + lines.extend(to_bytes(val) for val in [ + '--%s' % boundary, + 'Content-Disposition: form-data; name="%s"' % key, + '', + value + ]) + + to_lines(data) + + lines.extend([ + to_bytes('--%s--' % boundary), + b'', + ]) + return b'\r\n'.join(lines)