Trigger Django functions with Javascript sendBeacon

While working on a recent project a couple of requirements required the front page posting back to a django function while retaining data or triggering a function only if certain criteria are met.

In order to do this we looked at standard Ajax post functions and found a better fit with navigator.sendBeacon, which was the first time we had actually utilised this function.

Full details can be found here:
https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon

.

navigator.sendBeacon

First of all the project required a function to cleanup data if the user failed to submit data and navigated away from the page, so the best fit for this was navigator.sendBeacon inside the onunload event.

.

1: onunload Event:

This onload script allowed us to detect if the page was being left and trigger a Django function if this event was detected.

<script language="JavaScript">
    window.onunload = function(evt) {
        const endpoint = 'leavepost/';
        var formData = new FormData();
  		//Required by Django for form post
        formData.append('csrfmiddlewaretoken', '{{ csrf_token }}');
        navigator.sendBeacon(endpoint, formData);
    };
</script>

2: Django Function:

This was used to carry out due diligence on the trigger and ensure we clean up if a user failed to submit data to our Django function or to bypass the cleanup if the submission was in an error state.

@login_required
def clean_post_data(request):
    if request.method == "POST":
        #CARRY OUT DUE DILIGENCE 
        #AND EXECUTE FUNCTIONS
		print("Triggered from Javascript")
        return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/'))
    return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/'))

This allowed logic in the clean_post_data function to detect if a form submit was in error or if the data was abandoned and therefore required a cleanup, this worked flawlessly and will definitely be utilised again for other projects.


navigator.sendBeacon in Dropzone Uploader

Again this can be utilised to call functions in php or Django so nothing new here from what has been outlined above, but useful for anyone using dropzone.js and wanting to trigger posts based on add or remove of files.

The following allowed the platform to detect files that were added but not submitted and allowed a cleanup of stale data once the remove link in dropzone was selected.

.

1: Triggering Django from dropzone removedfile function.

While selecting the remove link on the dropzone box it was required that the data was removed from the platform and not tracked, the navigator.sendBeacon was again utilised to trigger a Django function in order to check and clean this data.

this.on("removedfile", function (file) {
  	// Remove the file.size from the totalsize tracker
	totalsize -= parseFloat((file.size / (1024*1024)).toFixed(2)); 
  	var formData = new FormData();
  	formData.append('csrfmiddlewaretoken', '{{ csrf_token }}');
  	formData.append('dzitem', file.name);
  	navigator.sendBeacon(removedz, formData);
});

2: Django remove from Dropzone function.

Below shows the generic makeup of the function triggered from dropzone and allows cleanup logic based on the request data from the navigator.sendBeacon function.

@login_required
def remove_dz_item(request):
    if request.method == 'POST':
        try:
          	# Grab the file item from the formData item
            # sent by sendBeacon
            dzitem = request._post['dzitem']
           	# Carry out due DILIGENCE for cleanup
            print("Executed from Dropzone removedfile")
        except Exception as dz_exception:
          	#handle error condition
            pass
        return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/'))

As it can be seen the request._post is used in order to take the named item in the formData posted by navigator.sendBeacon, more data can be added using the formData.append function and retrieved in a similar way.


Entire Dropzone upload script

Hopefully this can help someone out if trying to tie django and javascript functions together and especially combining this with a popular library such as dropzone.js

<script>

var totalsize = 0.0; //Used to track total size of uploaded files
const MaxTotalSize = 50; //Max aggregated filesize in MB
const removedz = 'some_url/'; //Django url for function

Dropzone.autoDiscover = false; 
//form id
$('#mydropzoneformid').dropzone({
  //Django upload file url
  url: "some_django_upload_url/",
  crossDomain: false,
  paramName: "file",
  parallelUploads: 1,
  autoProcessQueue: true,
  filesizeBase: 1024,
  maxFilesize: 25, //MB
  dictRemoveFileConfirmation: "Are you sure you want to remove this File?",  
  addRemoveLinks: true, //allow for remove links in dz
  //accept function to track file sizes uploaded
  accept: function(file, done) {
    if (totalsize >= MaxTotalSize) {
 //aborts the upload if the file pushes past the totalsize tracker
      file.status = Dropzone.CANCELED;
      this._errorProcessing([file],  "Max limit reached", null);
      alert("Total Maximum Upload limit reached, total size used: " + totalsize + " out of: " + MaxTotalSize + " File failed to upload: " + file.name);
    }else { 
      done();
    }
  },
  init: function () {
	//upload progress
    this.on("uploadprogress", function (file, progress, bytesSent) {
      progress = bytesSent / file.size * 100;
    });
    this.on("maxfilesexceeded", function (data) {
      var res = eval('(' + data.xhr.responseText + ')');
    });
    this.on("addedfile", function (file) {
      var _this = this;
      //track the file upload size
      totalsize += parseFloat((file.size / (1024*1024)).toFixed(2));
    });
    this.on("removedfile", function (file) {
      //remove the filesize from the total size variable
      totalsize -= parseFloat((file.size / (1024*1024)).toFixed(2));
      //initialize new formData
      var formData = new FormData();
      //add django csrf_token
      formData.append('csrfmiddlewaretoken', '{{ csrf_token }}');
      //add file item into formData
      formData.append('dzitem', file.name);
      //trigger the django removedz function
      navigator.sendBeacon(removedz, formData);
    });
    this.on("error", function (file, message) {
      //console.log(message);
      this.removeFile(file);
    });
    this.on('sending', function (file, xhr, formData) {
      xhr.setRequestHeader("X-CSRFToken", '{{ csrf_token }}');
    });

  }
});

Dropzone.prototype.filesize = function (size) {
  filesizecalculation(size)
};

function filesizecalculation(size) {

  if (size < 1024 * 1024) {
    return "<strong>" + (Math.round(Math.round(size / 1024) * 10) / 10) + " KB</strong>";
  } else if (size < 1024 * 1024 * 1024) {
    return "<strong>" + (Math.round((size / 1024 / 1024) * 10) / 10) + " MB</strong>";
  } else if (size < 1024 * 1024 * 1024 * 1024) {
    return "<strong>" + (Math.round((size / 1024 / 1024 / 1024) * 10) / 10) + " GB</strong>";
  }
}
</script>