Run test_tasklistdb.py
(provided in the code distribution for this chapter). The output should be a list of test results:
python test_tasklistdb.py
......
----------------------------------------------------------------------
Ran 6 tests in 1.312s
OK
Let us look at one of the classes defined in test_tasklistdb.py, DBentityTest. DBentityTest
contains a number of methods starting with test_
. These are the actual tests and they verify whether some common operations like retrieving or deleting tasks behave as expected.
Chapter4/test_tasklistdb.py
from tasklistdb import TaskDB, Task, AuthenticationError,
DatabaseError
import unittest
from os import unlink,close
from tempfile import mkstemp
(fileno,database) = mkstemp()
close(fileno)
class DBentityTest(unittest.TestCase):
def setUp(self):
try:
unlink(database)
except:
pass
self.t=TaskDB(database)
self.t.connect()
self.description='testtask'
self.task = self.t.create(user='testuser',description=self.
description)
def tearDown(self):
self.t.close()
try:
unlink(database)
except:
pass
def test_retrieve(self):
task = self.t.retrieve('testuser',self.task.id)
self.assertEqual(task.id,self.task.id)
self.assertEqual(task.description,self.task.description)
self.assertEqual(task.user,self.task.user)
def test_list(self):
ids = self.t.list('testuser')
self.assertListEqual(ids,[self.task.id])
def test_update(self):
newdescription='updated description' self.task.
description=newdescription
self.task.update('testuser')
task = self.t.retrieve('testuser',self.task.id)
self.assertEqual(task.id,self.task.id)
self.assertEqual(task.duedate,self.task.duedate)
self.assertEqual(task.completed,self.task.completed)
self.assertEqual(task.description,newdescription)
def test_delete(self):
task = self.t.create('testuser',description='second task')
ids = self.t.list('testuser')
self.assertListEqual(sorted(ids),sorted([self.task.id,task.id]))
task.delete('testuser')
ids = self.t.list('testuser')
self.assertListEqual(sorted(ids),sorted([self.task.id]))
with self.assertRaises(DatabaseError):
task = self.t.create('testuser',id='short')
task.delete('testuser')
if __name__ == '__main__':
unittest.main(exit=False)
All these test_
methods depend on an initialized database containing at least one task and an open connection to this database. Instead of repeating this setup for each test, DBentityTest
contains the special method setUp()
(highlighted) that removes any test database lingering around from a previous test and then instantiates a TestDB
object. This will initialize the database with proper table definitions. Then it connects to this new database and creates a single task object. All tests now can rely upon their initial environment to be the same. The corresponding tearDown()
method is provided to close the database connection and remove the database file.
The file that is used to store the temporary database is created with the mkstemp()
function from Python's tempfile
module and stored in the global variable database. (mkstemp() returns the number of the file handle of the opened as well, which is immediately used to close the file as we are only interested in the name of the file.)
The test_list()
and test_delete()
methods feature a new assertion: assertListEqual()
. This assertion checks whether two lists have the same items (and in the same order, hence the sorted()
calls). The unittest
module contains a whole host of specialized assertions that can be applied for specific comparisons. Check Python's online documentation for the unittest
module for more details (http://docs.python.org/py3k/library/unittest.html).
Using AJAX to retrieve data not only has the potential to make the tasklist application more responsive, but it will also make it simpler. This is achieved because the HTML will be simpler as there will be no need for the many<form>
elements we created to accommodate the various delete and done buttons. Instead, we will simply act on click events bound to buttons and call small methods in our CherryPy application. All these functions have to do is perform the action and return ok, whereas in the previous version of our application, we would have to return a completely new page.
In fact, apart from a number of<script>
elements in the<head>
, the core HTML in the body is rather short (the<header>
element and the extra elements in the<div>
element with a taskheader
class are omitted for brevity):
<body id="itemlist"> <div id="content"> <div class="header"></div> <div class="taskheader"></div> <div id="items"></div> <div class="item newitem"> <input type="text" class="duedate left editable-date tooltip" name="duedate" title="click for a date" /> <input type="text" class="description middle tooltip" title="click to enter a description" name="description"/> <button type="submit" class="add-button" name="add" value="Add" >Add</button> </div> </div> </body>
The<div>
element containing the input fields and a submit button takes up most of the space. It structures the elements that make up the line that allows the user to add new tasks. The<div>
element with the ID items
will hold a list of tasks and will be initialized and managed by the JavaScript code using AJAX calls.
The JavaScript code in tasklistajax.js
serves a number of goals:
datepicker)
Let's have a look at tasklistajax.js
.
Chapter4/static/js/tasklistajax.js
$.ajaxSetup({cache:false});$.ajaxSetup({cache:false}); function itemmakeup(data,status,req){ $(".done-button").button( {icons: {primary: 'ui-icon-check' }, text:false}); $(".del-button").button( {icons: {primary: 'ui-icon-trash' }, text:false}); $("#items input.duedate").sort( function(a,b){return $(a).val() > $(b).val() ? 1 : -1;}, function(){ return this.parentNode; }).addClass("just-sorted"); // disable input fields and done button on items that are already marked as completed $(".done .done-button").button( "option", "disabled", true ); $(".done input").attr("disabled","disabled"); $( "#items .editable-date" ).datepicker({ dateFormat: $.datepicker.ISO_8601, onClose: function(dateText,datePicker){ if(dateText != '') {$(this).removeClass("inline-label");}} }); }; $(document).ready(function(){ $(".header").addClass("ui-widget ui-widget-header"); $(".add-button").button( {icons: {primary: 'ui-icon-plusthick' }, text:false}).click(function(){ $(".inline-label").each(function() { if($(this).val() === $(this).attr('title')) { $(this).val(''), }; }) var dd=$(this).siblings(".duedate").val(); var ds=$(this).siblings(".description").val(); $.get("add",{description:ds, duedate:dd},function(data,status,req) { $("#items").load("list",itemmakeup); }); return false; // prevent the normal action of the button click }); $(".logoff-button").button({icons: {primary: 'ui-icon- closethick'}, text:false}).click(function(){ location.href = $(this).val(); return false; }); $(".login-button").button( {icons: {primary: 'ui-icon-play' }, text:false}); $(":text").addClass("textinput"); $(":password").addClass("textinput"); $( ".editable-date" ).datepicker({ dateFormat: $.datepicker.ISO_8601, onClose: function(dateText,datePicker){ if(dateText != '') {$(this).removeClass("inline-label");}} }); // give username field focus (only if it's there) $("#username").focus(); $(".newitem input").addClass("ui-state-highlight"); $(".done-button").live("click",function(){ var item=$(this).siblings("[name='id']").val(); var done=$(this).siblings(".completed").val(); $.get("done",{id:item, completed:done},function(data,status,req) { $("#items").load("list",itemmakeup); }); return false; }); $(".del-button").live("click",function(){ var item=$(this).siblings("[name='id']").val(); $.get("delete",{id:item},function(data,status,req){ $("#items").load("list",itemmakeup); }); return false; }); $("#items").load("list",itemmakeup); // get the individual task items });
The first line establishes the defaults for all AJAX calls that we will use. It makes sure that the browser will not cache any results.
Initializing the list of items once the page is loaded is done in the final highlighted line of code. It calls the load()
method with a URL that will be handled by our application and will return a list of tasks. If the call to load()
is successful, it will not only insert this data in the selected<div>
element, but also call the function itemmakeup()
passed to it as a second argument. That function, itemmakeup()
, is defined in the beginning of the file. It will style any<button>
element with a done-button
or del-button
class with a suitable icon. We do not add any event handlers to those buttons here, which is done elsewhere as we will see shortly.
Next, we use the sort
plugin to sort the items (highlighted), that is, we select any input field with the duedate
class that are children of the<div>
element with the ID items
(we do not want to consider input fields that are part of the new item div for example).
The sort plugin is available as sort.js and is based on code by James Padolsey: http://james.padolsey.com/javascript/sorting-elements-with-jquery/. The plugin will sort any list of HTML elements and takes two arguments. The first argument is a comparison function that will return either 1 or -1 and the second argument is a function that when given an element will return the element that should actually be moved around. This allows us to compare the values of child elements while swapping the parent elements they are contained in.
For example, here we compare the due dates. That is, the content of the selected<input>
elements, as retrieved by their val()
method, but we sort not the actual input fields but their parents, the<div>
elements containing all elements that make up a task.
Finally, itemmakeup()
makes sure any button marked with a done
class is disabled as is any input element with that class to prevent completed tasks from being altered and changes any input element with an editable-date
class into a datapicker widget to allow the user to choose a completion date before marking a task as done.
Besides styling elements, the $(document).ready()
function adds click handlers to the add, done, and delete buttons (highlighted).
Only one add button is created when the page is created, so we can add a click handler with the click()
method. However, new done and delete buttons may appear each time the list of items is refreshed. To ensure that freshly appearing buttons that match the same selection criteria receive the same event handler as the ones present now, we call the live()
method.
jQuery's live()
method will make sure any event handler is attached to any element that matches some criterion, now or in the future. More on jQuery's event methods can be found at http://api.jquery.com/category/events/.
Apart from the way we bind an event handler to a button, the actions associated with a click are similar for all buttons. We retrieve the data we want to pass to the server by selecting the appropriate input elements from among the button's siblings with the siblings()
method. As each task is represented by its own<div>
element in the list and the<button>
and<input>
elements are all children of that<div>
element, so selecting sibling input elements only ensures that we refer to elements of a single task only.
To get a better understanding of what we are selecting with the siblings()
method, take a look at some of the (simplified) HTML that is generated for the list of items:
<div id="items"> <div class="item"><input name=""/> … <button name="done"></div <div class="item"><input name=""/> … <button name="done"></div … </div>
So each<div>
that represents a task contains a number of<input>
elements and some<button>
elements. The siblings of any<button>
element are the elements within the same<div>
(without the button itself).
When we have gathered the relevant data from the input elements, this data is then passed to a get()
call. The get()
function is another AJAX shortcut that will make an HTTP GET request to the URL given as its first argument (a different URL for each button type). The data passed to the get()
function is appended to the GET request as parameters. Upon success, the function passed as the third argument to get()
is called. This is the same itemmakeup()
function that refreshes the list of items that was used when the page was first loaded.
With the JavaScript to implement the interactivity and the means to access the database in place, we still have to define a class that can act as a CherryPy application. It is available as taskapp.py
and here we show the relevant bits only (Its index()
method is omitted because it simply delivers the HTML shown earlier).
Chapter4/taskapp.py
class TaskApp(object):
def __init__(self,dbpath,logon,logoffpath):
self.logon=logon
self.logoffpath=logoffpath
self.taskdb=TaskDB(dbpath)
def connect(self):
self.taskdb.connect()
The constructor for TaskApp
stores a reference to a LogonDB
instance in order to be able to call its checkauth()
method in exposed methods to authenticate a user. It also stores the logoffpath
, a URL to a page that will end the user's session. The dbpath
argument is the filename of the file that holds the tasklist database. It is used to create an instance of TaskDB
, used in subsequent methods to access the data (highlighted).
The connect()
method should be called for each new CherryPy thread and simply calls the corresponding method on the TaskDB
instance.
To service the AJAX calls of the application, TaskApp
exposes four short methods: list()
to generate a list of tasks, add()
to add a new task, and done()
and delete()
to mark a task as done or to remove a task respectively. All take a dummy argument named _
(a single underscore) that is ignored. It is added by the AJAX call in the browser to prevent caching of the results.
list()
is the longer one and starts out with authenticating the user making the request (highlighted). If the user is logged in, this will yield the username. This username is then passed as an argument to the taskdb.list()
method to retrieve a list of task IDs belonging to this user.
With each ID, a Task
instance is created that holds all information for that task (highlighted). This information is used to construct the HTML that makes up the task as visualized on screen. Finally, all HTML of the individual tasks is joined and returned to the browser.
Chapter4/taskapp.py
@cherrypy.expose def list(self,_=None): username = self.logon.checkauth() tasks = [] for t in self.taskdb.list(username): task=self.taskdb.retrieve(username,t) tasks.append('''<div class="item %s"> <input type="text" class="duedate left" name="duedate" value="%s" readonly="readonly" /> <input type="text" class="description middle" name="description" value="%s" readonly="readonly" /> <input type="text" class="completed right editable-date tooltip" title="click to select a date, then click done" name="completed" value="%s" /> <input type="hidden" name="id" value="%s" /> <button type="submit" class="done-button" name="done" value="Done" >Done</button> <button type="submit" class="del-button" name="delete" value="Del" >Del</button> </div>'''%('notdone' if task.completed==None else 'done',task. duedate,task.description,task.completed,task.id)) return ' '.join(tasks)
The other methods are quite similar to each other. add()
takes description
and duedate
as arguments and passes them together with the username it got after authentication of the user to the create()
method of the TaskDB
instance. It returns 'ok' to indicate success. (Note that an empty string would do just as well: it's the return code that matters, but this makes it more obvious to anyone reading the code).
The delete()
method (highlighted) has one relevant argument, id
. This ID is used together with the username to retrieve a Task
instance. This instance's delete()
method is then called to remove this task from the database.
The done()
method (highlighted) also takes an id
argument together with completed
. The latter either holds a date or is empty, in which case it is set to today's date. A Task
instance is retrieved in the same manner as for the delete()
method, but now its completed
attribute is set with the contents of the argument of the same name and its update()
method is called to synchronize this update with the database.
Chapter4/taskapp.py
@cherrypy.expose def add(self,description,duedate,_=None): username = self.logon.checkauth() task=self.taskdb.create(user=username, description=description, duedate=duedate) return 'ok' @cherrypy.expose def delete(self,id,_=None): username = self.logon.checkauth() task=self.taskdb.retrieve(username,id) task.delete(username) return 'ok' @cherrypy.expose def done(self,id,completed,_=None): username = self.logon.checkauth() task=self.taskdb.retrieve(username,id) if completed == "" or completed == "None": completed = date.today().isoformat() task.completed=completed task.update(username) return 'ok'
3.142.199.181