Chris Chris - 4 months ago 64
JSON Question

Create a unit test using Django's test client post method passing parameters and requesting JSON from rest_framework

I want to instantiate a

django.test.client.Client()
or
rest_framework.test.APIClient()
, POST a simple set of parameters, and request a JSON format response from a djangorestframework class-based view.

The documentation suggests I just instantiate APIClient() and post with the parameter
format='json'
:

rest_framework.test import APIClient
apiclient = APIClient()
response = apiclient.post('/api/v1/model/1/run',
data=request_params, format='json')


However then my view (a DRF viewset custom method) does not receive the request parameters. Tracing this to the view, the POST parameters do make it to
request.data
as a dict, but
request.POST.items()
returns an empty list. When I use the code below to make a POST request over AJAX from a browser,
request.POST.items()
returns all the parameters correctly. It is only when using the unit test
APIClient()
post()
method that the parameter values aren't in
request.POST.items()
.

If I use the
.get()
method of
APIClient()
, the request parameters are not in
request.data
when it reaches the view, but they are in
request.GET.items()
, passed down in
QUERY_STRING
. The values are moved from query string to the WSGIRequest GET QueryDict by ClientHandler.call in django.test.client line 115
request = WSGIRequest(environ)
(Django 1.9.7). This doesn't seem to be happening for
APIClient()
post()
.

I tried the following:


  • Passing
    json.dumps(request_params)
    to the data parameter, but same response - my view doesn't see any parameters in the request (ref).

  • Using the Django Client, passing
    content_type='application/json'
    , with and without json.dumps, but same response.

  • Using Django Client, setting post **extra parameter to
    HTTP_ACCEPT='application/json'
    (with and without json.dumps) - same response.

  • Initializing the Django Client with
    HTTP_ACCEPT='application/json'
    (with and without json.dumps) - same response.

  • Leaving the
    Accept
    HTTP header, post's content_type parameter, and APIClient's format parameter undefined, and adding
    {'format':'json'}
    to the request_params - which works for
    Client.get
    requests
    , my code sees request parameters, but rest_framework returns HTML. The JSON rendered in this HTML shows the code is working correctly (returns status 202 and a polling URL, as it should).

  • Appending
    .json
    to the URL in the unit test and leaving content type, etc, at their defaults, but I get
    Not Found: /api/v1/model/1/run/.json
    from get_response.



My code works fine accepting AJAX POST requests through the browser, and my unit tests were working fine when I was using client.get(). It is only the combination of using client.post() and needing JSON back that I cannot get working.

I extract the request values with:

if request.method == 'POST':
form_values = ((key, value) for key, value in request.POST.items())
else:
form_values = ((key, value) for key, value in request.GET.items())


The Javascript that sends the AJAX request, that succeeds and returns JSON, is as follows:

// Setup at the bottom of the HTML body
$(document).ready(function(){
$.ajaxSetup({
data: {csrfmiddlewaretoken: "{{ csrf_token }}", format: "json" }
});
$.ajaxSetup({
beforeSend: function (xhr, settings) {
xhr.setRequestHeader("Accept", "application/json");
if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken'));
}
}
});
});

// Code that makes the request url=/api/v1/model/1/run, method=post
// Only POST is permitted on the view method by decorator @detail_route(methods=['post']))
function run_model(event)
{
var form = $(event.target);

$.ajax({
type: form.attr('method'),
url: form.attr('action'),
data: $("#" + form.attr('id')).serialize() + "&format=json&csrfmiddlewaretoken={{ csrf_token }}"
})
.done(function (data, status, jqXHR) {
poll_instance(data.instance_id, data.model_id);
})
.fail(function (jqXHR, status, err) {
var status_div = $("." + construct_div_class("model", "div", jqXHR.responseJSON.model_id)).children("div.status");
if (catch_ajax_error(status_div, failed_tries, jqXHR, status, err)) {
setTimeout(run_model, 3000, event);
};
});

event.preventDefault();
};


The Accept header was what got this working, format=json didn't work.

This is the receiving view:

class ModelViewSet(viewsets.ModelViewSet):
@detail_route(methods=['post'])
def run(self, request, *args, **kwargs):
"""
Runs a model and redirects to the URL that will return the output results when ready.
"""
try:
instance_id = run_model(request, self.get_object().id)

except ParameterValidationError as e:

# ...

return Response(data={'instance_id': instance_id, 'model_id': self.get_object().id},
status=status.HTTP_202_ACCEPTED)


The form, whose submit is tied to run_model() above:

<form method="POST" action="/api/v1/model/3/run/" id="model-form-3">
<table class="table table-striped table-bordered table-hover">
<tbody><tr>
<th>
Model
</th>
<th>
Parameter
</th>
<th>
Value
</th>
</tr>
<tr>
<td>
Source source model of Composite (model #2)
</td>
<td>
GUI dim value in for POC model #89
</td>
<td>
<select name="5_77" id="5_77">
<option value="13">
Dimension description #17
</option>
<option value="14">
Dimension description #18
</option>
</select>
</td>
</tr>
<tr>
<td>
Source model of Composite (model #1)
</td>
<td>
Decimal GUI value in for POC model #64
</td>
<td>
<input name="4_52" id="4_52" value="123456789" type="text">
</td>
</tr>
<tr>
<td>
Second source model of Composite (model #3)
</td>
<td>
GUI dim value in for POC model #112
</td>
<td>
<select name="6_100" id="6_100">
<option value="16">
Dimension description #20
</option>

<option value="17">
Dimension description #21
</option>
</select>
</td>
</tr>
<tr>
<td>
Dependent of Composite (model #0)
</td>
<td>
GUI dim value in for POC model #45
</td>
<td>
<select name="3_33" id="3_33">
<option value="7">
Dimension description #11
</option>
<option value="8">
Dimension description #12
</option>
</select>
</td>
</tr>
<tr>
<td>
Dependent of Composite (model #0)
</td>
<td>
Decimal GUI value in for POC model #43
</td>
<td>
<input name="3_31" id="3_31" value="123456789" type="text">
</td>
</tr>
</tbody></table>
<input value="Run model" type="submit"><br><br>
</form>


I'm on Python 3.5, Django 1.9.7, djangorestframework 3.4.0 (also happened in 3.2.1), djangorestframework-xml 1.3.0, debugging in PyCharm 2016.1

Answer

Turns out the AJAX data is supposed to appear in request.data, and I was using the wrong approach to submit the data via AJAX from the browser. Django rest_framework (DRF) assumes that data from the request will be passed in the same format as data returned to the client - in this case JSON both ways. As it assumes that for an Accept=application/json request, incoming data will be in JSON format, it automatically parses it and populates request.data for you, and request.GET and request.POST are empty by the time the request reaches your DRF view.

To pass a form of data in the AJAX request I use the jquery form plugin's .formSerialize() method.

I did just have a .map() compile a dictionary from a form, but this won't work for radios and other instances where you might have several values for a single key/form id.

Credit for this answer should really go to @dhke, who pointed out my fundamental error. Although perhaps this question should be deleted.

Comments