Leveraging jQuery and Django

July 3, 2018, 9:25 p.m.


There are many powerful frameworks to build robust, dynamic, and feature rich websites. Anyone reading this post most likely understands that browsers handle client-side presentation responsibilities while data management, complex logic, and other responsibilities are handled server side. The term "full stack" developer is a term used to describe developers who write both front-end and backend code.

Client Side (front-end)

It is necessary for anyone serious about client side programming to master HTML5, CSS, and JavaScript. JavaScript itself has a large number of frameworks (e.g. Angular, React, etc.) and libraries which are beyond the scope of this blog, but Asynchronous JavaScript and XML (AJAX) and jQuery are relevant. AJAX is a process/technique for to send/receive data via HTTP requests/responses. jQuery is a JavaScript library that makes working with the Document Object Model (DOM) much easier to include interacting with AJAX. These are the two client-side technologies we will work with in this blog.

Server Side (backend)

There are several popular and widely used frameworks for developing server side components includeing Node.js and Django. I like Node and use it on occasion, but I prefer Django due to its tremendous community support, versatility, ease of use, and avaialabiilty of plug-in applications including authentication mechanisms, filters, and much more. A very common design pattern is known as Model-View-Controller (MVC) and it is worth understanding. The model is a representation of data which is often saved in databases. Django has a powerful mechanism for modeling data objects designed as classes. Most django model classes inherit from models.Model. Django's object relational mapping (ORM) techniques allow very complicated relationships between model classes to be easily created (beyond the scope of this blog). The view is what users see and it is the presentation layer for models displayed in an appropriate manner. The controller controls the flow of information between the model and view and it is responsible for controlling requests and returning responses. That's really the essence of Django; it receives a request and returns a response.

Use Case

I recently wrote an application that required a signficant amount of interaction between the browser client and the backend database. A user is expected to select a customer from a drop-down list. In the example I typed in "storm" to narrow down the result set (shout out to Daniel Farrell and his bootstrap-combobox.js which is fantastic!). I needed a way to efficiently retrieve information from the server based on actions performed by a user. In this case the user is only required to select a customer and then a piece of equipment owned by that customer (filtered by the server) and the client and server side code handles taking care of customer address and equipment specific informaiton. This is done using jQuery/AJAX client-side and Django server-side.

After selecting the "Stormy Brewing Company" you can see that the address information has been filled in. Also note that all equipment owned by the Stormy Brewing Company has been returned and populated in the drop down box which previously stated "Select Customer."

Once a piece of equipment is selected you can see below that the model and serial number have been returned and populated as well.

Client -- jQuery and AJAX

JavaScript's event loop is synchronous and it relies on callbacks to handle events triggered via user interaction. In this case our company drop-down has an id of company and the equipment drop-down has an id of make. The JavaScript code below shows the jQuery actions triggered when a change event happens on either drop-down. Notice the code for these two events is almost identical with the only difference being the parameter passed into DjangoInfo.django...if it's a company then "NA" is passed; otherwise, the value of the currenly selected equipment is sent (we will see why later). The change events for both company and equipment creates a variable called companyPromise and when it's ready it's callback is invoked which in turn calls a method named setCompanyData passing the data returned by the server.

$('#company').change(function() {
        if ($('#company').val() != null) {
            // Request a Promise object using DjangoInfo
            var companyPromise = DjangoInfo.django("NA");
            companyPromise.done(function (data) {
                setCompanyData(data)
            }).fail(function (error) {
                console.log(error);
            }) // We can "chain" the fail callback to done; won't be called if there is no error
        }
    });
    $('#make').change(function() {
        var companyPromise = DjangoInfo.django($('#make').val());
        companyPromise.done(function(data) {
             setCompanyData(data)
        }).fail(function(error) {
            console.log(error);
        });
    });

Below is a variable named DjangoInfo which returns a JavaScript Promise object. A deferred object is an object than can create a promise and change its state to resolved or rejected which is what is implemented below. Deferreds are typically used if you write your own function and want to provide a promise to the calling code. You are the producer of the value.

A promise is, as the name says, a promise about a future value. You can attach callbacks to it to get that value. The promise is "given" to the invoking function and it is the receiver of the future value. Keep in mind the invoking function is the change event triggered when a user selects a company or equipment. The change event cannot modify the state of the promise. Only the code that created the promise can change its state. In this case, the callbacks are registered to the change events for the company and equipment drop down lists. Below is the code:

var DjangoInfo = {
    django: function(equip_pk) {
       var csrftoken = jQuery("[name=csrfmiddlewaretoken]").val();
       $.ajaxSetup({
           headers: { "X-CSRFToken": csrftoken }
       });            
       var url = "/company_info/";
       var promise = $.Deferred(); // jQuery method for creating a Promise object
       $.ajax({
           type: "POST",
           url: url,
           contentType: "application/json",
           data: {
               company_pk: $('#company').val(),
               equip_pk: equip_pk
           },
           dataType: "json",
           success: function(data) {
               promise.resolve(data); // Calls the done callback
           },
           error: function() {
               promise.reject("Oops!") // Calls the fail callback
           }
        });
        return promise;
    }
};

DRY Principle: We need to set the address and equipment attributes whenever company or equipment is selected

function setCompanyData(data) {
   $('#id_equip').text(data["equip_model"]+ " - " + data["equip_serial_num"])
   $('#id_address').text(data["address"]);
   $('#make').html(data["cust_equip"]);
   $('#model').val(data["equip_model"]);
   $('#serial_number').val(data["equip_serial_num"]);
   $('#id_street_address').val(data["street_address"]);
   $('#id_city').val(data["city"]);
   $('#id_region_code').val(data["region_code"]);
}

Server (Django)

You can see we are using an AJAX call to make an HttpRequest using the post method. The server expects two variables to be passed. We always send the selected company and the selected equipment if the change event was caused by the equipment drop-down. If not, "NA" is passed as the equipment attribute. Note the url variable set above (/company_info/). If you are thinking this must have something to do with controller in MVC you are correct. Django maps the requested URL within its URL configuration file and routes it to the appropriate Django view. Below is our URL mapping in urls.py:

from django.conf.urls import url
from django.contrib import admin
from collopack.auth.views import *
from django.conf.urls.static import static

urlpatterns = [   
    ...
    url(r'company_info/$', company_info, name='company_info'),
    ...
]

This will result in Django invoking the company_info method:

@csrf_exempt
def company_info(request):
    try:
        content = {}
        company_data = request.body.decode('utf-8').split('&')[0]
        comp_pk = company_data.split('=')[1]
        company = Company.objects.get(pk=comp_pk)
        equip_data = request.body.decode('utf-8').split('&')[1]
        equip_pk = equip_data.split('=')[1]
        if equip_pk != "NA":
            sel_equip = Equipment.objects.get(pk=equip_pk)
            content["equip_model"] = sel_equip.make
            content["equip_serial_num"] = sel_equip.serial_num

        add_equip = ""
        equip_list = Equipment.objects.all().filter(customer_id=company.pk)
        for equip in equip_list:
            if equip_list.count() == 1: # If customer only has 1 piece of equipment then send the model and serial number
                content["equip_model"] = equip.make
                content["equip_serial_num"] = equip.serial_num
            if equip_pk == str(equip.pk): # Set equipment to selected value in drop down
                add_equip += "<option value = " + str(equip.pk) + " selected>" + Equipment.objects.get(pk=equip.pk).equip_name + "</option>"
            else:
                add_equip += "<option value = " + str(equip.pk) + ">" + Equipment.objects.get(pk=equip.pk).equip_name + "</option>"
        if add_equip == "":
            add_equip = "<option value = 1>No Equipment</option>"
            content["equip_model"] = "Model"
            content["equip_serial_num"] = "Serial Number"

        content["street_address"] = company.address1
        content["city"] = company.city
        content["region_code"] = company.state
        content["cust_equip"] = add_equip
        content["address"] = "{0} {1} {2}".format(company.address1, company.city, company.state)
        return HttpResponse(json.dumps(content))
    except:
        pass

Django doesn't automatically handle JSON encoded data. When form-encoded data is received in a POST request, django will automatically populate its HttpRequest.POST dictionary object (and also HttpRequest.REQUEST). This doesn't happen with json data. The query dictionary in HttpRequest.POST will remain empty unless the the contentType and dataType parameters in the ajax call are removed or commented out. Since we set the contentType and dataType parameters as json (JavaScript Object Notation) we need to access the passed attributes by parsing the request body: company_data = request.body.decode('utf-8').split('&')[0] splits out the company and equipment attributes respectively with the key value pair separated by an = sign. That explains the code lines comp_pk = company_data.split('=')[1] and company = Company.objects.get(pk=comp_pk). I point this out because it's a good illustration of the model at work here; Company is the Django model and we retrieve the correct company using the primary key which was passed in as a value. A similar action is performed to retrieve equipment that belongs to a customer. All Equipment model objects are searched and filtered using a foreign key relationship defined between the Customer and Equipment models (a piece of equipment has a foreign key relationship to a company). We iterate through the equipment list to dynamically build the equipment drop-down list and add other attributes such as address, city, equipment model, etc. The response is prepared using a Python dictionary named content which is then converted to a string by json.dumps(content) and returned as the HttpResponse object. Hopefully, it's obvious the HttpResponse is the data returned in our promise object which can then be used by our view to display the appropriate data.

This post might be confusing to some, but we covered a lot of ground. One of my objectives was to demonstrate the power of JavaScript's Promise objects. We were able to re-use the JavaScript DjangoInfo variable and Django company-info method to process requests for both companies and equipment. This is consistent with Django's Don't Repeat Yourself (DRY) mantra.

Comment Enter a new comment: