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.
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.
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 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(".")
18.226.163.229