diff --git a/horizon/static/framework/util/http/http.js b/horizon/static/framework/util/http/http.js index de7b90ac62..57202d8c3c 100644 --- a/horizon/static/framework/util/http/http.js +++ b/horizon/static/framework/util/http/http.js @@ -57,6 +57,9 @@ limitations under the License. for (var key in config.data) { if (config.data.hasOwnProperty(key) && uploadService.isFile(config.data[key])) { backend = uploadService.upload; + // NOTE(tsufiev): keeping the original JSON to not lose value types + // after sending them all as strings via multipart/form-data + config.data.$$originalJSON = JSON.stringify(config.data); break; } } diff --git a/horizon/static/framework/util/http/http.spec.js b/horizon/static/framework/util/http/http.spec.js index c26d0a4339..35dc1c2b4b 100644 --- a/horizon/static/framework/util/http/http.spec.js +++ b/horizon/static/framework/util/http/http.spec.js @@ -128,7 +128,10 @@ it('upload() is used when there is a File() blob inside data', function () { api.post('/good', {first: file, second: 'the data'}); expect(Upload.upload).toHaveBeenCalled(); - expect(called.config.data).toEqual({first: file, second: 'the data'}); + + var expected = {first: file, second: 'the data'}; + expected.$$originalJSON = JSON.stringify(expected); + expect(called.config.data).toEqual(expected); }); it('upload() is NOT used when a File() blob is passed as data', function () { diff --git a/openstack_dashboard/api/rest/glance.py b/openstack_dashboard/api/rest/glance.py index 95c1a7516d..935aab5237 100644 --- a/openstack_dashboard/api/rest/glance.py +++ b/openstack_dashboard/api/rest/glance.py @@ -170,14 +170,15 @@ class Images(generic.View): # note: not an AJAX request - the body will be raw file content mixed with # metadata + @rest_utils.post2data @csrf_exempt def post(self, request): - form = UploadObjectForm(request.POST, request.FILES) + form = UploadObjectForm(request.DATA, request.FILES) if not form.is_valid(): raise rest_utils.AjaxError(500, 'Invalid request') data = form.clean() - meta = create_image_metadata(request.POST) + meta = create_image_metadata(request.DATA) meta['data'] = data['data'] image = api.glance.image_create(request, **meta) diff --git a/openstack_dashboard/api/rest/utils.py b/openstack_dashboard/api/rest/utils.py index 6a0bd5fa49..0c18799ff1 100644 --- a/openstack_dashboard/api/rest/utils.py +++ b/openstack_dashboard/api/rest/utils.py @@ -163,3 +163,20 @@ def parse_filters_kwargs(request, client_keywords=None): else: filters[param] = request.GET[param] return filters, kwargs + + +def post2data(func): + """The sole purpose of this decorator is to restore original form values + along with their types stored on client-side under key $$originalJSON. + This in turn prevents the loss of field types when they are passed with + header 'Content-Type: multipart/form-data', which is needed to pass a + binary blob as a part of POST request. + """ + def wrapper(self, request): + request.DATA = request.POST + if '$$originalJSON' in request.POST: + request.DATA = json.loads(request.POST['$$originalJSON']) + + return func(self, request) + + return wrapper