Monday, February 28, 2011

AJAX and Python on Google App Engine

 I recently thought it would be fun to learn more about Google App Engine (GAE) by creating a simple app. Although my app could have been implemented entirely in plain JavaScript, I wanted to learn more about AJAX, so I had to figure out how to make RPC calls from JavaScript to the python server code. There are lots of good reasons to use AJAX, including:

  • coding your app logic in python (or whatever the server part of your app is written in) instead of JavaScript
  • hiding your app logic from the world

Of course the first step to learning a new platform is usually to read a few docs and find a working example of what you want to do. Finding examples to work from turned out to be harder than I thought.

In the GAE documentation, there was only one article on this topic that I could find: "Using AJAX to Enable Client RPC Requests". On the whole, it was pretty good, but I think it makes the problem more complicated than it needs to be. Here are the two simplifications I made:
  1. Use a read-made and popular library (jQuery) to manage the client side AJAX call.  This removes all that Request-setup JavaScript in the Google article.
  2. Simplify the server-side post handler to look at an "action" string, rather than use python's getattr() function.
Let's look at the same app implemented by the original article, but with these simplifications.

Here's the client-side "index.html:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
  <head>
    <title>index</title>
    <script type="text/javascript" src="scripts/jquery-1.5.js">
    </script>
    <script type="text/javascript">
      $(document).ready( function() {
        $("#add").click( function() {
          var formdata = $("form").serialize();
          $.post("/rpc", formdata, function(data) {
            var res = jQuery.parseJSON(data);
            $("#result").val(res['result']);
          });
        });
      });
    </script>
  </head>
  <body>
    <center>
      <h1>{{title}}</h1>
      <form>
        <input type="hidden" name="action" value="add"/>
        <table>
          <tr>
            <td align="right">Number 1:
            <input name="num1" type="text" value="1" />
            <br />
            Number 2:
            <input name="num2" type="text" value="2" />
            <br />
            <input type="button" value="Add" id="add" style="width:100%" />
            <br>
            Result:
            <input id="result" type="text" value="" readonly="true" disabled="true" /></td>
          </tr>
        </table>
      </form>
    </center>
  </body>
</html>


Some notes on what changed and how its' working:

  • $(document).ready() is a jQuery function that fires when the document DOM is ready (loaded by the browser).  It calls $("#add").click() to add an onClick() function to the input button with an id="add".
  • The onClick() function calls another jQuery function, .serialize() on the <form> element in the document. This is a very convenient helper function that takes all the elements in the form, and creates a JSON string from them, in the form of "name":"value".  So in our case, it creates a string that looks like {"action":"add", "num1":1,"num2":2}.
  • The onClick() function then calls the jQuery .post() function.  Again, this saves you a lot of heavy-lifting.  It sets up the request, and makes a post request to our /rpc url, passing the serialized form data, and a callback function that gets called when the request returns.
  • The callback uses jQuery.parseJSON() to translate the returned JSON string into a JavaScript object. This is a lot safer than doing something like eval('('+data+')', although it will fail if the returned string isn't correctly encoded (eg, contains single quotes ' instead of double quotes ").  The returned string is converted to a JavaScript property map.
  • Finally, we set the value of the input with the id="result" to the value of the result.
Note that we didn't have to return a property map, since we're only interested in a single return value, but this example shows how you could return a lot of data.  The jQuery AJAX documentation contains a lot more information.

Ok, here's our simplified server-side:

import os
from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import run_wsgi_app
from google.appengine.ext.webapp import template
from django.utils import simplejson as json

class MainPage(webapp.RequestHandler):
    """ Renders the main template."""
    def get(self):
        template_values = { 'title':'AJAX Add (via GET)', }
        path = os.path.join(os.path.dirname(__file__), "index.html")
        self.response.out.write(template.render(path, template_values))

class RPCHandler(webapp.RequestHandler):
    """ Will handle the RPC requests."""
    def post(self):
        result = 0        
        action = self.request.get('action') 
        if action == 'add':
            num1 = int(self.request.get('num1'))
            num2 = int(self.request.get('num2'))
            result = {'result':num1+num2}
        self.response.out.write(json.dumps(result))


def main():
    application = webapp.WSGIApplication(
                                         [('/', MainPage),
                                          ('/rpc', RPCHandler)],
                                          debug=True)
    run_wsgi_app(application)

if __name__ == "__main__":
    main()

Some notes on this code.

  • You'll notice this is a lot shorter than the original.
  • Since we know the requested action ("add"), we know the names of the other parameters.  This may not always be the case, in which case you'll need to iterate through the parameters and deal with them that way.
  • We have to cast the parameters to int, because everything in the request string is a string.
  • The result is put into a Python dict, which is then translated into a proper JSON string with json.dumps().
Hopefully this will help you out!