Time for action implementing the task module

Have a look at the Python code in task.py:

Chapter3/task.py

import cherrypy
import json
import os
import os.path
import glob
from configparser import RawConfigParser as configparser
from uuid import uuid4 as uuid
from datetime import date
import logon

This first part illustrates Python's "batteries included" philosophy nicely: besides the cherrypy module and our own logon module, we need quite a bit of specific functionality. For example, to generate unique identifiers, we use the uuid module and to manipulate dates, we use the datetime module. All of this functionality is already bundled with Python, saving us an enormous amount of development time. The next part is the definition of the basic HTML structure that will hold our task list:

Chapter3/task.py

base_page = '''
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<script type="text/javascript" src="/jquery.js" ></script>
<script type="text/javascript" src="/jquery-ui.js" ></script>
<style type="text/css" title="currentStyle">
	@import "/static/css/tasklist.css";
	@import "/jquerytheme.css";
</style>
<script type="text/javascript" src="/static/js/sort.js" ></script>
<script type="text/javascript" src="/static/js/tooltip.js" ></script>
<script type="text/javascript" src="/static/js/tasklist.js" ></script>
</head>
<body id="%s">
<div id="content">
%s
</div>
</body>
</html>
'''

Again the structure is simple, but besides the themed stylesheet needed by jQuery UI (and reused by the elements we add to the page), we need an additional stylesheet specific to our task list application. It defines specific layout properties for the elements that make up our task list (first highlight). The highlighted<script> elements show that besides the jQuery and jQuery UI libraries, we need some additional libraries. Each of them deserves some explanation.

What just happened?

The first JavaScript library is sort.js, a code snippet from James Padolsey (http://james.padolsey.com/tag/plugins/) that provides us with a plugin that allows us to sort HTML elements. We need this to present the list of tasks sorted by their due date.

The second is tooltip.js that combines a number of techniques from various sources to implement tooltips for our buttons and inline labels for our<input> elements. There are a number of tooltip plugins available for jQuery, but writing our own provides us with some valuable insights so we will examine this file in depth in a later section.

The last one is tasklist.js. It employs all the JavaScript libraries and plugins to actually style and sort the elements in the task list.

The next part of task.py determines the directory we're running the application from. We will need this bit of information because we store individual tasks as files located relative to this directory. The gettaskdir() function takes care of determining the exact path for a given username (highlighted). It also creates the taskdir directory and a sub directory with a name equal to username, if these do not yet exist with the os.makedirs() function (notice the final 's' in the function name: this one will create all intermediate directories as well if they do not yet exist):

Chapter3/task.py

current_dir = os.path.dirname(os.path.abspath(__file__))
def gettaskdir(username):
	taskdir = os.path.join(current_dir,'taskdir',username)
	# fails if name exists but is a file instead of a directory
	if not os.path.exists(taskdir):
			os.makedirs(taskdir)
	return taskdir

The Task class is where the handlers are defined that CherryPy may use to show and manipulate the task list. The __init__() method stores a path to a location that provides the user with a possibility to end a session. This path is used by other methods to create a suitable logoff button.

The index() method will present the user with an overview of all his/her tasks plus an extra line where a new task can be defined. As we have seen, each task is adorned with buttons to delete a task or mark it as done. The first thing we do is check whether the user is authenticated by calling the checkauth() function from our logon module (highlighted). If this call returns, we have a valid username, and with that username, we figure out where to store the tasks for this user.

Once we know this directory, we use the glob() function from the Python glob module to retrieve a list of files with a .task extension. We store that list in the tasklist variable:

Chapter3/task.py

class Task(object):
def __init__(self,logoffpath="/logoff"):
	self.logoffpath=logoffpath
@cherrypy.expose
def index(self):
	username = logon.checkauth()
	taskdir = gettaskdir(username)
	tasklist = glob.glob(os.path.join(taskdir,'*.task'))

Next, we create a tasks variable that will hold a list of strings that we will construct when we iterate over the list of tasks. It is initialized with some elements that together form the header of our task list. It contains, for example, a small form with a logoff button and the headers for the columns above the list of tasks. The next step is to iterate over all files that represent a task (highlighted) and create a form with suitable content together with delete and done buttons.

Each .task file is structured in a way that is consistent with Microsoft Windows .ini files. Such files can be manipulated with Python's configparser module. The .task file is structured as a single [task] section with three possible keys. This is an example of the format:

[task]
description = something
duedate = 2010-08-26
completed = 2010-08-25

When we initialize a configparser object, we pass it a dictionary with default values in case any of these keys is missing. The configparser will read a file when we pass an open file descriptor to its readfp() method. The value associated with any key in a given section may then be retrieved with the get() method that will take a section and a key as parameters. If the key is missing, it supplies the default if that was provided upon initialization. The second highlighted line shows how this is used to retrieve the values for the description key.

Next, we construct a form for each .task file. It contains read-only<input> elements to display the Due date, Description, and the completion date plus buttons to delete the task or mark it as done. When these buttons are clicked the contents of the form are passed to the /task/mark URL (handled by the mark() method). The method needs to know which file to update. Therefore, it is passed a hidden value: the basename of the file. That is, the filename without any leading directories and stripped of its .task extension:

Chapter3/task.py

				tasks = [
'''
<div class="header">
Tasklist for user <span class="highlight">%s</span>
	<form class="logoff" action="%s" method="GET">
		<button type="submit" name="logoffurl"
				class="logoff-button" value="/">Log off
		</button>
	</form>
</div>
'''%(username,self.logoffpath),
'''
<div class="taskheader">
	<div class="left">Due date</div>
	<div class="middle">Description</div>
	<div class="right">Completed</div>
</div>
''','<div id="items" class="ui-widget-content">']
		for filename in tasklist:
			d = configparser(
			defaults={'description':'',
					'duedate':'',
					'completed':None})
			id = os.path.splitext(os.path.basename(filename))[0]
			d.readfp(open(filename))
			description = d.get('task','description')
			duedate = d.get('task','duedate')
			completed = d.get('task','completed')
			tasks.append(
'''
<form class="%s" action="mark" method="GET">
	<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>
</form>
'''%('notdone' if completed==None else 'done',
	duedate,description,completed,id))
		tasks.append(
'''
<form class="add" action="add" method="GET">
	<input type="text" class="duedate left editable-date tooltip"
			name="duedate" title="click to select 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>
</form>
</div>
''')
		return base_page%('itemlist',"".join(tasks))

Finally, we append one extra form with the same type of input fields for Due date and Description but this time, not marked as read-only. This form has a single button that will submit the contents to the /task/add URL. These will be handled by the add() method. The actual content returned by the index() method consists of all these generated lines joined together and embedded in the HTML of the base_page variable.

Adding new tasks

New tasks are created by the add() method. Besides the value of the add button (which is not relevant), it will take a description and a duedate as parameters. To prevent accidents, it first checks if the user is authenticated, and if so, it determines what the taskdir for this user is.

We are adding a new task so we want to create a new file in this directory. To guarantee that it has a unique name, we construct this filename from the path to this directory and a globally unique ID object provided by Python's uuid() function from the uuid module. The .hex() method of a uuid object returns the ID as a long string of hexadecimal numbers that we may use as a valid filename. To make the file recognizable to us as a task file, we append the .task extension (highlighted).

Because we want our file to be readable by a configparser object, we will create it with a configparser object to which we add a task section with the add_section() method and description and duedate keys with the set() method. Then we open a file for writing and use the open file handle to this file within a context manager (the with clause), thereby ensuring that if anything goes wrong when accessing this file, it will be closed and we will proceed to redirect the user to that list of tasks again. Note that we use a relative URL consisting of a single dot to get us the index page. Because the add() method handles a URL like /task/add redirecting to '.' (the single dot), will mean the user is redirected to /task/, which is handled by the index() method:

Chapter3/task.py

@cherrypy.expose
def add(self,add,description,duedate):
	username = logon.checkauth()
	taskdir = gettaskdir(username)
	filename = os.path.join(taskdir,uuid().hex+'.task')
	d=configparser()
	d.add_section('task')
	d.set('task','description',description)
	d.set('task','duedate',duedate)
	with open(filename,"w") as file:
		d.write(file)
	raise cherrypy.InternalRedirect(".")

Deleting a task

Deleting or marking a task as done are both handled by the mark() method. Besides an ID (the basename of an existing .task file), it takes duedate, description, and completed parameters. It also takes optional done and delete parameters, which are set depending on whether the done or delete buttons are clicked respectively.

Again, the first actions are to establish whether the user is authenticated and what the corresponding task directory is. With this information, we can construct the filename we will act on. We take care to check the validity of the id argument. We expect it to be a string of hexadecimal characters only and one way to verify this is to convert it using the int() function with 16 as the base argument. This way, we prevent malicious users from passing a file path to another user's directory. Even though it is unlikely that a 32 character random string can be guessed, it never hurts to be careful.

The next step is to see if we are acting on a click on the done button (highlighted in the following code). If we are, we read the file with a configparser object and update its completed key.

The completed key is either the date that we were passed as the completed parameter or the current date if that parameter was either empty or None. Once we have updated the configparser object, we write it back again to the file with the write() method.

Another possibility is that we are acting on a click on the delete button; in that case, the delete parameter is set. If so, we simply delete the file with the unlink() function from Python's os module:

Chapter3/task.py

@cherrypy.expose
def mark(self,id,duedate,description,
			completed,done=None,delete=None):
	username = logon.checkauth()
	taskdir = gettaskdir(username)
	try:
			int(id,16)
	except ValueError:
			raise cherrypy.InternalRedirect(self.logoffpath)
	filename = os.path.join(taskdir,id+'.task')
if done=="Done":
			d=configparser()
			with open(filename,"r") as file:
				d.readfp(file)
			if completed == "" or completed == "None":
				completed = date.today().isoformat()
			d.set('task','completed',completed)
			with open(filename,"w") as file:
				d.write(file)
elif delete=="Del":
			os.unlink(filename)
raise cherrypy.InternalRedirect(".")
..................Content has been hidden....................

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