some background
Sunrise in Malvik, Norway 2018.

AJAX calls in Django 2.2

Introduction

AJAX (Asyncroneous JavaScript and XML) is a great way of updating client content without the need to reload the whole webpage. When working with Django, the front-end code is rendered form the backend as a part of template generation. So is any JavaScript code.

Although the problem of creating AJAX calls in Django has been solved in the past, the framework constantly evolves. At the time of writing of this post, the most recent version of Django was 2.2. Therefore, this post is written with an intention to outline a clear implementation recipe and bring it more up to date.

Basic scenario - GET

The most basic scenario assumes that we have want to request some data form a specific end-point. This tasks fits in very much with a GET method. To set it up, all we need to do is to:

  • define an end-point (in urls.py),
  • define the corresponding function (in views.py),
  • create a routine in JavaScript and include it in the template we generate.

Here is how we do it:

urls.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from django.urls import path
from django.conf import settings
from djamgo.cong.urls.static import static

from . import views


urlpatterns = [
    ...
    path('stuff/<int:item_id>', views.Stuff.as_view(), name='stuff')
    ...
]

if settings.DEBUG:
    urlpatterns += static(settings.STATIC_URL,
        document_root=settings.STATIC_ROOT)

Here, we choose to go on with class-based views, mainly to avoid having to branch the functions with conditions like if request.method == _GET_ and so on. Also, already at this stage, we account for designated paths for .js files we will include, instead of writing JavaScript code alongside with HTML. Although this approach works, it doesn’t scale.

views.py

1
2
3
4
5
6
7
8
9
10
11
12
from django.views.generic import ListView
from django.https import JsonResponse

from .models import StuffModel


class Stuff(ListView):
    model = StuffModel

    def get(self, request, item_id):
        stuff_item = StuffModel.objects.get(id=item_id)
        return JsonResponse({'item': stuff_item})

templates/yourapp/list.html

1
2
3
4
5
6
7
8
9
10
11
12
{% extends 'youapp/base.html' %}
{% load static %}


<!-- to trigger the call -->
<button id="42" onclick="getStuff(elem)">Get Stuff</button>

<!-- to fill out from AJAX response -->
<div id="something"></div>

<!-- include the call's logic in a separate script -->
<script src="{% static 'yourapp/js/calls.js' %}"></script>

static/yourapp/js/calls.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function getStuff(elem) {
  const id = $(elem).attr('id');
  
  $.ajax({
    url:            window.location.pathname + id,
    type:           'GET',
    contentType:    'application/json; charset=utf-8',
    dataType:       'json',
    success:        function (result) {
      $('#something').prepend(
        "<p>" + result.item + "</p>"
      );
    }
  });
} 

The essence of this logic is to trigger the call, synthesize the correct URL to hit out end point and do something with the result once response is received. Note that we rely on jQuery in this example (see: here).

Adding complexity - POST

GET methods are designed, as the name suggests, to fetch data from the servers, not to add or change anything. If we want to push some information, we should use other methods, such as POST (see later for PUT and DELETE). For obvious reasons, we would like to ensure that whatever is being POST-ed, comes from a legitimate source. The way Django handles it is that it enforces all POST, PUT and DELETE methods to present a so-called cross-site request forgery (CSRF) tokens (see here).

From the developer’s point of view, all you need to do when creating of a form that uses e.g. POST method, is to paste {% csrf_token %} inside <form></form> tags, which in the background, creates an additional hidden field into the form with the token it generates. Unfortunately, we are not working with forms here and the following snippets explain the process:

views.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...

class Stuff(ListView):
    model = StuffModel

    def get(self, request, item_id):
        ...

    def post(self, request, item_id):
        if request.is_ajax()
            new_stuff = StuffModel(item_id=item_id)
            new_stuff.save()
            return JsonResponse({'message': 'OK'})
        else:
            return jsonResponse({'message': 'Failed'})

Here, we have added a post function to support HTTP POST method. The logic is, however, somewhat arbitrary. It is really up to you what you want to achieve. In this example, all we do is to create a new database entry (implicitly assuming that item_id is a field).

templates/yourapp/list.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{% extends 'youapp/base.html' %}
{% load static %}


<!-- to trigger the call -->
<button id="42" onclick="getStuff(elem)">Get Stuff</button>

<!-- to fill out from AJAX response -->
<div id="something"></div>

<!-- include the call's logic in a separate script -->
<script type="text/javascript">
window.CSRF_TOKEN = "";
</script>
<script src="{% static 'yourapp/js/calls.js' %}"></script>

The double braces {{ ... }} (in the contary to {% ... %}) place the token in to the template literally. This value, we take using the tiny JavaScript snippet (lines 12-14) and save it into the DOM. Thanks to that, we can have it available for calls.js that is attached separately.

static/yourapp/js/calls.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function postStuff(elem) {
  const id = $(elem).attr('id');
  const csrftoken = window.CSRF_TOKEN;
  
  $.ajax({
    url:            window.location.pathname + id,
    type:           'POST',
    beforeSend:     function (request) {
      request.setRequestHeader("X-CSRFToken", csrftoken);
    },
    data:           {
      csrfmiddlewaretoken: csrftoken
    },
    contentType:    'application/json; charset=utf-8',
    dataType:       'json',
    success:        function (result) {
      $('#something').prepend(
        "<p>" + result.item + "</p>"
      );
    }
  });
} 

There are important modifications that we have made:

  • First, we retrieve the CSRF value from the DOM.
  • Change the type of the method to POST (obviously).
  • Next, add a specific header line X-CSRFToken.
  • Send the token also as normal data - just as if you were sending data from a form.

Unless Django receives the token in a correct way, it will reply with 403 code.

Other methods - DELETE, PUT

The HTTP protocol defines more specific methods for altering (PUT) or deleting (DELETE) of resources. Although from the logical point of view, the goal can be achieved with POST method only, we would need to create additional URLs.

If we use class-based views, implementing both DELETE and PUT methods can be done analogically to POST:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Stuff(ListView):
    model = StuffModel

    def get(self, request, item_id):
        ...

    def post(self, request, item_id):
        ...

    def delete(self, request, item_id):
        ...

    def put(self, request, item_id):
        ...

Similarly, the calls.js function can be modified with changing of the type attribute, essentially leaving the CSRF implementation the same.

Unfrotunately, handling of request objects for PUT and DELETE methods is more cumbersome in Django. According to the documentation, only HttpRequest.GET and HttpRequest.POST options are supported. For other methods, we cannot use this simple dictionary-like structure to get the content, only leaving us with manual parsing of the request.body. Since the intention of Django was to work with web browsers and accept content from forms (bsically done through POST request), it is perhaps not surprising. The following example illustrates the difference.

example

1
2
3
4
5
6
7
8
9
10
11
12
13
...
def post(self, request):
    param = request.POST.get('param')
    ...

def delete(self, request):
    body = request.body.decode('utf-8') # if utf-8 encoded...
    param_list = body.split('&')
    params = {}
    for p in param_list:
        params[p.split('=')[0]] = p.split('=')[1]
    param = params['param']
    ...

All in all, the exact implementation depends on the needs. Personally, I would prefer to use the action-specific methods, however the above delete function looks a bit ugly. Therefore, for consistency, I would define a separate URL and stick to using of POST method as it flows more naturally with Django. In the end, the most important part is to get the AJAX call work, which in this case it is no different for as soon as we implement the CSRF token correctly.