Saturday, July 30, 2011

Project: Basic Web Application List with Real-time Updating (Part 3)

Introduction

In this post, we will setup the basic list item addition and display. We will be covering basic App Engine techniques.

List Item Data Model

To be able to store our list item to the App Engine Datastore, we need to define a data model for the item. The data model is similar to a database model; we indicate the fields and data types. We write all the code for defining the data models in a separate script file, 'model.py', to make our project organized. Create the file, 'model.py' in the root directory of the project folder and put the following code.

from google.appengine.ext import db
from google.appengine.ext.db import polymodel


class Item(polymodel.PolyModel):
    title = db.StringProperty()
    bodytext = db.StringProperty(multiline=True)
    createdate = db.DateTimeProperty(auto_now_add=True)

The data model is very, very basic as our intention is to focus on experimenting with the Channel API. In addition, the above data model can easily be extended if needed.

Handlers to Save and Display Items

We go back now to 'main.py'. We modify it as follows.

#!/usr/bin/env python


from google.appengine.ext import webapp
from google.appengine.ext.webapp import util


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


from model import *


class MainHandler(webapp.RequestHandler):
    def get(self):
        path = os.path.join(os.path.dirname(__file__), 'templates/main.html')
        self.response.out.write(template.render(path, {}))


class SaveHandler(webapp.RequestHandler):
    def post(self):
        itemtitle = self.request.get('itemtitle')
        itemtext = self.request.get('itemtext')
        
        self.response.headers['Content-Type'] = 'text/json'
        
        if itemtext and itemtitle:
            item = Item(title=itemtitle, bodytext=itemtext)
            item.put()
            self.response.out.write(json.dumps({'result': 'success'}))
        else:
            self.response.out.write(json.dumps({'result': 'failure'}))
            
class ListHandler(webapp.RequestHandler):
    def get(self):
        items = Item.all()
        items.order("-createdate")
        
        d = {'rows': []}
        for item in items.fetch(20):
            d['rows'].append({'title': item.title, 'bodytext': item.bodytext})
                
        self.response.headers['Content-Type'] = 'text/json'
        self.response.out.write(json.dumps(d))


def main():
    application = webapp.WSGIApplication([('/', MainHandler),
                                         ('/save', SaveHandler),
                                         ('/list', ListHandler),],
                                         debug=True)
    util.run_wsgi_app(application)




if __name__ == '__main__':
    main()

Highlighted above are the changes. At the top we import all the objects from the 'model.py' script file to give as access to the data model we defined. We also added two request handlers, 'SaveHandler' and 'ListHandler'. The 'SaveHandler' saves a new list item. The 'ListHandler' returns, in JSON representation, the latest 20 items. Lastly, at the bottom, we make the two request handlers accessible through the '/save' and '/list' path.

Changes to main.html Template


We are done with the back-end and would now work on the front-end.

First let us modify the 'main.html' template as follows.

{% extends "base.html" %}


{% block content %}
<div id="itemform" title="Item Form">
  <form id ="itemformmain" action="/save" method="post">
    <ul>
      <li>
        <div class="formlabel">
          <label for="itemtitle">Title</label>
        </div>
        <div class="formwidget">
          <input name="itemtitle"
                 type="text"
                 placeholder="Place Title Here"
                 size="50"
                 required>
        </div>
      </li>
      <li>
        <div class="formlabel">
          <label for="itemtext">Main Text</label>
        </div>
        <div class="formwidget">
          <textarea name="itemtext"
                    cols="50"
                    rows="5"
                    required></textarea>
        </div>
      </li>
    </ul>
  </form>
</div>
<input type="button" name="additem" value="Add New Item"/>
<div id="mainlist">
</div>
{% endblock %}


{% block addscript %}
<script src="/js/main.js"></script>
{% endblock %}

In the content block, we added two div containers. The first div container is going to be used by Jqueryui to instantiate a dialog box.  The dialog box contains a form which will get the information for creating a new list item. The second div container is an empty div that would  contain the Evently widget for displaying the list of items. In between the two div containers is an 'Add New Item' button. This button will open the dialog.

At the bottom, we also added a new script tag inside the 'addscript' block. The script tag references a new Javascript file.

New Item Dialog

To enable the dialog box we defined, we need to do some Javascript. We will be putting all of our Javascript in 'main.js'. So create the 'main.js' file inside the 'js' folder and put the following code inside.

$(function () {
$("#itemform").dialog({
autoOpen: false,
modal: true,
width: 450,
buttons: {
"Save": function() {
var itemtitle = $("#itemformmain input[name='itemtitle']").val();
var itemtext = $("#itemformmain textarea[name='itemtext']").val();
var that = this;
$.post('/save',
      {itemtitle: itemtitle, itemtext: itemtext},
      function () {
      $(that).dialog("close");
      }, 'json');
}, 
"Cancel": function() { 
$(this).dialog("close"); 

}
});

$("input[name='additem']").bind('click',
function () {
$("#itemform").dialog("open");
}
);
});

The code above does two things:

  1. configure the dialog box
  2. and bind a function that opens the dialog box to the Click event of the "Add New Item" button.
In the event handler function for the 'Save' button, we simply make an AJAX POST request to '/save' which we defined to create a new item and save it to the datastore. The other parts of the code is quite simple and is self explanatory.

You can now try accesssing the site and adding new items. The only problem is that we still don't have any code to display the items added. For the meantime, you could just manually check the Datastore through the administrator interface, http://localhost:8080/_ah/admin/datastore.

Some Aesthetics

Notice that our dialog box looks very bad. Aesthetics is important so lets fix the styling a bit. In the 'style' directory, create the 'main.css' file and put the following code.

body {
font-size: 10px;
}

#itemform ul {
padding-left: 0;
}

#itemform li {
display: block;
}

#itemform .formlabel {
float: left;
text-align: right;
width: 60px;
}

#itemform .formwidget {
margin-left: 70px;
}

After creating the CSS file, we need to link it to our HTML page. We do this by inserting a 'link' tag in our 'base.html' template.

<!doctype html>
<html>
<head>
  <meta charset="utf-8"/>
  <title></title>

  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"/>
  <link type="text/css" href="/style/smoothness/jquery-ui-1.8.14.custom.css" rel="stylesheet" />
  <link type="text/css" href="/style/main.css" rel="stylesheet" />
</head>

<body>
  <header>
  {% block header %}{% endblock %}
  </header>

  <div id="contentwrapper">
  {% block content %}{% endblock %}
  </div>

  <footer>
  {% block footer %}{% endblock %}
  </footer>

  <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script>
  <script src="/js/jquery.evently.js"></script>
  <script src="/js/jquery.mustache.js"></script>
  <script src="/js/jquery-ui-1.8.14.custom.min.js"></script>
  {% block addscript %}{% endblock %}
</body>

</html>

Highlighted above are the changes.

List Items Display

Now, lets add some Javascript to display the list items. Modify the 'main.js' file as follows.

$(function () {
$("#itemform").dialog({
autoOpen: false,
modal: true,
width: 450,
buttons: {
"Save": function() {
var itemtitle = $("#itemformmain input[name='itemtitle']").val();
var itemtext = $("#itemformmain textarea[name='itemtext']").val();
var that = this;
$.post('/save',
      {itemtitle: itemtitle, itemtext: itemtext},
      function () {
      $(that).dialog("close");
      }, 'json');
}, 
"Cancel": function() { 
$(this).dialog("close"); 
}
});
$("input[name='additem']").bind('click',
function () {
$("#itemform").dialog("open");
}
);

        $("#mainlist").evently({
_init: {
async: function (cb) {
$.getJSON('/list',
function (data) {
cb(data);
});
},
data: function (data) {
return {
items: data['rows']
};
},
mustache: '\
{{#items}}\
<div class="itementry">\
<h2>{{title}}</h2>\
<div>\
{{bodytext}}\
</div>\
</div>\
{{/items}}'
}
});
});

In the above code, we configured the 'mainlist' div container into an Evently Widget. We configured it to display the list items upon loading. The Evently Widget above responds to only one event, the special '_init' event, which is called automatically upon loading. Inside the '_init' object are three methods.

  • async - a function that makes an AJAX request to '/list'. '/list' returns a JSON representation of the 20 latest items.
  • data - callback function for the AJAX request in async. It does some pre-processing for the mustache template.
  • mustache - string template for displaying the object returned by the data method.
Assuming that you already added some items, opening the url, 'http://localhost:8080/' now displays the items in the datastore. However, adding a new list item, the new item is not displayed in the list. This because the widget only requests information upon loading.

Adding New Items to the List

To complete this post, let us finish by completing the basic functionality of displaying new items. The strategy used in displaying new items is chosen in such a way that it would be easy to integrate the Channel API functionality later on.

Modify the 'main.js' file as follows.

$(function () {
$("#itemform").dialog({
autoOpen: false,
modal: true,
width: 450,
buttons: {
"Save": function() {
var itemtitle = $("#itemformmain input[name='itemtitle']").val();
var itemtext = $("#itemformmain textarea[name='itemtext']").val();
var that = this;
$.post('/save',
      {itemtitle: itemtitle, itemtext: itemtext},
      function () {
                                        $("#mainlist").trigger('prependitem', {rows:[{title: itemtitle, bodytext: itemtext}]});
      $(that).dialog("close");
      }, 'json');
}, 
"Cancel": function() { 
$(this).dialog("close"); 
}
});
$("input[name='additem']").bind('click',
function () {
$("#itemform").dialog("open");
}
);

        $("#mainlist").evently({
_init: {
async: function (cb) {
$.getJSON('/list',
function (data) {
cb(data);
});
},
data: function (data) {
return {
items: data['rows']
};
},
mustache: '\
{{#items}}\
<div class="itementry">\
<h2>{{title}}</h2>\
<div>\
{{bodytext}}\
</div>\
</div>\
{{/items}}'
},
                prependitem: {
data: function (e, data) {
return {
items: data['rows']
};
},
mustache: '\
{{#items}}\
<div class="itementry">\
<h2>{{title}}</h2>\
<div>\
{{bodytext}}\
</div>\
</div>\
{{/items}}',
render: 'prepend'
}
});
});

In the above code, we added a 'prependitem' item custom event to the Evently widget. This event inserts at the start of the widget (why its prepend) items passed to the event. By now the 'data' method and 'mustache' property in the 'prepend' event is already quite self-explanatory. Hower, the 'render' property is new. The 'render' property indicates how the mustache template is added to the widget. Its default value is 'replace', meaning it replaces the contents of the widget. Other values are 'prepend' and 'append'.

Near the top of the code above, there is a statement that triggers the 'prependitem' event upon saving a new item. Upon triggering it also passes the information about the new item for display. We will be replacing this trigger statement later on when we integrate the Channel API functionality.

Next Post

In the next post, we will start working with the Channel API.

No comments:

Post a Comment