0

My Django project has a number of buttons on the web page that do POST requests to the main view.py which in turn handles the action and returns a 204 No content response. The results of the action show up asynchronously later on the web page (at the time the response is generated there's nothing new to show). On any non-iOS based browser the 204 response works fine and the web page remains in the browser, as expected from RFC 7231. Unfortunately all iOS based browsers I've tried (Safari, Firefox, Chrome) navigate to a blank page after the POST, which is not what I want (see this question). Apparently this is a long standing bug in WebKit.

Is there any way to achieve the same thing across all browsers? ie. Click button, POST, web page stays as is, change appears later. I've looked at this but no sure it's really what I'm after. Changing the response code is possible but I don't see a viable alternative that doesn't navigate away from the current page. My current hacky fix is to reload the whole page for an iOS device, but this moves the page if the user had scrolled down earlier so looks pretty janky.

Community
  • 1
  • 1
Paul
  • 360
  • 3
  • 14

1 Answers1

0

The solution for this is contained in this question and the answer by Terry. Plus some other info about handling the CSRF protection in Django.

The short answer is to override the regular form submit with event.preventDefault() in your own onsubmit function for each form. Then do the POST asynchronously with JQuery. This avoids the server having to return an HttpResponse with status 204, or anything really, beyond an empty JsonResponse.

There are several forms on my web page so I don't have a single function assigned to each button, like in the above linked answer, but several that change individual button behaviour. Also for Django there's the added consideration of including the CSRF token in the POST request.

So, for example, some html with two buttons;

<form id="form-a" onsubmit="submit_form_a(event)">{% csrf_token %}
    <input type="hidden" name="button-a" value="some_value_for_a">
    <button type="submit" class="w3-button">Button A</button>
</form>

<form id="form-z" onsubmit="submit_form_z(event)">{% csrf_token %}
    <input type="hidden" name="button-z" value="some_value_for_z">
    <button type="submit" class="w3-button">Button Z</button>
</form>

Then Javascript, where the first form submit callback, for example, disables the button;

function submit_form_a(e){
    e.preventDefault();
    var url = "/main/a"
    var formData = $(e.target).serialize();
    var btn = $(e.target).find("button");
    btn.prop("disabled", true);
    myPost(url, formData);
}

And the second submit callback that does some animation, using the excellent W3.CSS;

function submit_form_z(e){
   e.preventDefault();
   var url = "/main/z"
   var formData = $(e.target).serialize();
   var btn = $(e.target).find("button");
   btn.toggleClass("w3-animate-fading").toggleClass("w3-text-red")
   myPost(url, formData);
}

Both of those scripts call a common script mypost, which sets up the XMLHttpResponse header with the CSRF token, then POSTs it;

// Make sure any AJAX POST requests are not going off-site. Prevents
// leaking the CSRF token.

function csrfSafeMethod(method) {
// these HTTP methods do not require CSRF protection
    return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); } 

function myPost(url, data) {
    var csrftoken = $("[name=csrfmiddlewaretoken]").val();
    // Set the header on the AJAX POST request with the CSRF token
    $.ajaxSetup({beforeSend: function(xhr, settings) {
        if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
            xhr.setRequestHeader("X-CSRFToken", csrftoken);
        }
    }});
    $.post(url, data);
}

Back at the Django views.py side of things, look at the <input> data from the form and do what you need to do. I only just discovered on iOS the buttons themselves don't get included in the POST form data, like they seem to with other platforms, making hidden text input elements necessary;

if request.is_ajax() and request.method == 'POST':
    if 'button-a' in request.POST:
        # do something for A
    elif 'button-z' in request.POST:
      # do something for Z
    return JsonResponse({})

And voila! Buttons can be clicked, background stuff happens and the page doesn't move. You could return something other than an empty JSON response and handle it back in the form callbacks, such as re-enabling the button, but I don't need that. See earlier linked answer for details there.

Paul
  • 360
  • 3
  • 14