Our second—and final—example, is a simple To Do list application that runs on the Web called todolist.pl. You can add, delete, and change list items, sort by date, priority, or descriptions, and mark items as done. Figure 21.3 shows an example of the To Do list at work:
The To Do list application consists of a large table containing the To Do items. Each item has a check box indicating whether it is done or not, a priority, a due date, and a description. All the data in this table is editable through form elements; the changes are applied when the user chooses the Update button. Also affected with Update is whether an item is to be removed (the check boxes in the right-most column of the table), how to sort the data (the Sort By menu just below the table), and whether to display done items (the Show Done check box under the table as well).
In addition to the table and the display preferences, there is also an area for adding new items to the list. By filling out the boxes in that part of the application and choosing Add Item, new items are added (and all other changes applied as well).
As with the stock portfolio program, the script that runs the To Do list is a CGI script, and runs directly as a URL, no initial form needed. The script generates its own content, including the forms that enable you to change the items in the list and how they are displayed. It's all the same script. The only other part is a data file—listdata.txt—which stores the To Do data, and which is read and written by the To Do script.
This script is twice the size of anything we've looked at in this book, so, as with the last example, I'm not going to go over it line by line. The complete script is at the end of this section, in Listing 21.3, and you can get it from the Web site for this book as well (www.typerl.com). In this section, I'll describe the flow and general structure of how the script works and, if you're still curious, you can check out the code for yourself.
As with most of the examples we've looked at this week, this script has a data file that it reads and writes to that keeps track of the scripts data. The data file for this script, called listdata.txt, stores the To Do item data. The todolist.pl script reads this file at each iteration and writes new data to it whenever anything is changed. It looks much like the data files you've seen previously in other examples:
id=1 desc=Finish Chapter 20 date=3/1/2002 prior=1 done=1 --- id=2 desc=Finish Chapter 21 date=3/14/2002 prior=1 done=0 --- id=3 desc=Lunch with Eric date=3/16/2002 prior=2 done=1 ---
Each record is separated in the file by three dashes (---). Each field of the record has a key and a value, separated by an equal sign.
When the CGI script for the To Do list is installed, an initial data file must also be installed somewhere the Web server can read (and write) it. The initial data file can be empty—the script will simply generate a Web page with no items in it—but the file must exist for the script to work.
The todolist.pl is large, but pretty straightforward. There aren't many confusing regular expressions, and the flow from function to function is fairly straightforward. In fact, much of the bulk of the script is taken up by print statements to generate the HTML for the To Do List and its various form elements—and customizing those elements to behave differently in different situations.
The two starting subroutines for the todolist.pl script are &init() and &process(). The &init() determines the current date, calls the &read_data() subroutine, and prints the top part of the HTML file to be generated by the script. Let's start from there and work down.
The initialization subroutine is responsible primarily for calling &read_data() to open the data file and read each of its elements into a single data structure. That data structure is an array of hashes, with each hash containing the data for each To Do item. The keys in the hash are
date— The due date for the item. The date is of the format MM/DD/YYYY (this is enforced by the script).
In addition to these keys, which come from the data file, each item in the list also has an ID. The ID is assigned when the data file is read, from 0 to the number of elements. The ID will be used later to keep track of the various form elements for each list item.
The &process() routine is where the major work of the script takes place. In this subroutine there are two main branches, based on the param() function from the CGI.pm module. Back on Day 16 you learned about param(), and how it can be used to get the values of form elements. Another way of using param() is without any arguments, in which case it returns all the names of all the form elements—or, if the script wasn't called with a form, param() returns undefined (undef). In the &process() subroutine, we take advantage of that behavior to produce two different results:
The first time the script is called, there are no parameters, so we simply display the current To Do list (by using the &display_all() subroutine).
All other times the script is called, there are potential changes to be managed, either changes to how the existing items are or changes and additions to the items. If the Update button was selected, we remove the items to be deleted (the &remove_selected() subroutine), update all the data (&update_data()), write the data back out to the file (&write_data()), and display it over again (&display_all()).
If the Add Items button was pressed, we do all the same steps, except that in between updating the data and writing it out, we call &add_item() to add the new To Do Item to the list of items.
As all the updating and adding is happening, we're also checking for formatting errors in the dates. More about that later when we talk about updating the data and adding list items.
The largest part of the To Do list script, number-of-lines-of-code-wise, is contained in the &display_all() and &display_data() script. These subroutines don't just generate HTML for the data; the data table is also a form, and all the elements need to be generated automatically. In addition, a lot of the HTML that is generated is conditional on various states of the table. Priority 1 items are displayed in red, for example, and the menus for each priority are set to their current values based on the data. So rather than just using an enormous “here” document for this part of the script, we need to work through line by line to generate it.
The &display_all() subroutine is the main one that gets called first. It starts the table, prints the headers, sorts the main array based on the current sort order, and then calls &display_data() inside a for loop to display each element in the To Do list. It also generates the elements below the data itself: the Sort By menu, the Show Done check box, the form elements for adding an item, and both of the buttons to submit the form. Along the way, it also prints and manages warnings if there's an error processing the date. All this involves a lot of conditional statements and prints, as well as a whole lot of lines of HTML.
The &display_data() subroutine has a similar task for each specific element of the To Do list. Each row of the table has five columns, each of which contains a form element. Each element needs a unique name, and many form elements change appearance based on the data they reflect (a check box is checked if an item is done, for example). &display_data() also handles NOT displaying some items—if the Show Done check box is not selected, it won't display any item that is marked Done—but it will generate a hidden form element with some of that item's data so that the updates work right.
As with &display_all(), this involves a lot of if statements and a lot of HTML. The result is a gigantic form with each form element attached to the item to which it refers, and which is already filled in with the current To Do list data. Change any part of that data and, when the form is submitted, those changes will make it back to the original data set.
Speaking of updating the changes, let's move onto the &update_data() subroutine. This subroutine is called regardless of whether the user chooses the Update or Add Item buttons to make sure that any changes made to the data get made in either case. What &update_data() does is loop through all the form elements on the page—each element for the list items, as well as the Sort By and Show Done form elements—and change the data or global settings to reflect the changes that were made on the Web page.
Let's focus on the data itself. Each part of the HTML form that is generated by &display_data() has a unique name, generated from the name of the field (description, priority, and so on) and that item's ID number. By picking apart those form element names that come back when the form is submitted, we can match each name to each part of the data set, compare the values, and if they differ, update the data set with the new value. Each time the form is submitted, every single element is checked. This isn't the most efficient way to keep track of the data, but it does let us keep everything on one page.
The other thing the &update_data() subroutine does is check for bad dates in the existing data. If you tried to change a date from its normal format (“10/9/1998” or something like that), &update_data() will catch that and report an error, which will then be displayed along with the data set when &display_all() is called.
To remove items from the list, you select the check boxes in the Remove column for the data and choose Update. To add an item to the list, you enter its data in the form at the bottom of the page and choose Add Item. In either case, &remove_selected() is called; for the latter case, &add_item() is also called.
The &remove_selected() subroutine is responsible for updating the data to delete any records that have been chosen by the user to be removed. In this case, because all our data is stored in an array of references, removing those items is easy—we just build another array of references, minus the ones we want to delete, and then put that new array back in the old one's variable. Because it's an array of references, all the data referred to by those references stays put and doesn't need to be recopied or reconstructed anywhere. At the end of the subroutine, renumber all the records so that there aren't any gaps that could cause problems when the form is processed.
The &add_item() subroutine is equally easy; with all the data from the form elements, all we need to do is stuff it into a hash and put a reference to that hash in the data array. We also assign this new item a new ID, one larger than the current largest ID.
All that's left are a few minor supporting subroutines: &write_data() to write the data back out to the listdata.txt file, and two subroutines to manage date formats and comparisons.
The &write_data() subroutine is easy; here all we do is open the listdata.txt file for writing, and then loop over the data set to write out each of the records. Because this subroutine is called once each time the script is run and after any changes have been made to the data, we can be close to certain that the data will never be corrupted or items lost. Note here that the item IDs are not written to the data file with the rest of the data; those IDs are generated when the data is initially read and used only to keep track of form elements, so they don't need to be preserved between calls to the script.
The final two sets of subroutines relate to date management. Dates, as I mentioned earlier, are of the format MM/DD/YYYY. Using a single consistent format is important because it enables the list of items to be sorted by the date—which is a form of numeric sort. To convert the data format into a number that can be compared to some other number, the formatting must be correct. For this reason, whenever a date is changed in the existing data or added to a new item, its format is checked with the &check_date() subroutine and errors are reported if the format doesn't match up or the numbers used are clearly out of bounds (both a large red message at the top of the Web page and by asterisks added to the wrong date itself).
Sorting the list by date happens in the &display_all() subroutine, if the value of the Sort By menu is Date. To convert the dates into something that can be compared against something else, we use the Time::Local module, a built-in module that can be used to convert various parts of a date and time into time format—that is, number of seconds since 1900 (the value returned by the time function). The &date2time() subroutine is used for just this purpose, to split up a correctly formatted date into its elements and return the time value. The &date2time() subroutine also watches for dates in error format—with leading asterisks—and sorts those values to the top.
Listing 21.3 contains the (very) complete code for the todolist.pl script. Start from the top and read down. The only tricky parts are those that deal with attaching the IDs to the form elements, and handling the data errors (watch for the &check_date() subroutine). And, as with all CGI scripts, it helps to have an understanding of HTML and of how forms and CGI.pm interact with each other.