Time for action writing unit tests for tasklistdb.py

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

What just happened?

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).

Note

Many of the modules we develop in this book come bundled with a suite of unit tests. We will not examine those tests in any detail, but it might be educational to check some of them. You should certainly use them if you experiment with the code as that is exactly what they are for.

Designing for AJAX

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:

  • Initializing the list of items
  • Styling and enhancing UI elements with interactive widgets (like a datepicker)
  • Maintaining and refreshing the list of tasks based on button clicks

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.

Click handlers

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.

Note

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.

The application

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'
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.146.155