A Web-Based To Do List (todolist.pl)

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:

Figure 21.1. To Do List Web application.


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.

The Data File

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.

How the Script Works

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.

Data Initialization with &init()

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

  • id— The unique ID of the item.

  • desc— The description of the item.

  • date— The due date for the item. The date is of the format MM/DD/YYYY (this is enforced by the script).

  • prior— The priority of the item, from 1 (highest) to 5.

  • done— Whether or not this item is complete.

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.

Process the Form and the Data with &process()

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.

Displaying the Data with &display_all() and &display_data()

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.

Updating Changes with &update_data()

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.

Adding and Removing items with &add_item() and &remove_selected()

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.

Other Subroutines: Writing Data and Checking for Errors

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.

The Code

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.

Listing 21.3. The Code for todolist.pl
1:   #!/usr/local/bin/perl -w
2:   use strict;
3:   use CGI qw(:standard);
4:   use CGI::Carp qw(fatalsToBrowser);
5:   use Time::Local;
6:
7:   my $listdata = 'listdata.txt';  # data file
8:   my @data = ();                  # array of hashes
9:
10:  # global default settings
11:  my $sortby = 'prior';           # order to sort list
12:  my $showdone = 1;               # show done items?  (1 == yes)
13:
14:  &init();
15:  &process();
16:
17:  sub init {
18:      # get the current date, put in in MM/DD/YY format
19:      my ($day,$month,$year) = 0;
20:      (undef,undef,undef,$day,$month,$year) = localtime(time);
21:      $month++;                     # months start from 0
22:      $year += 1900;                # Perl years are years since 1900;
23:      # this keep us from getting bit by Y2K
24:      my $date = "$month/$day/$year";
25:      # open & read data file
26:      &read_data();
27:
28:      # start HTML
29:      print header;
30:      print start_html('My To Do List'),
31:      print "<h1 align="center"><font face="Helvetica,Arial">";
32:
33:      print "To Do List</font></h1>
";
34:      print "<h2 align="center"><font face="Helvetica,Arial">";
35:      print "$date</font></h2>
";
36:      print "<hr />
";
37:      print "<form method="post">
";
38:  }
39:
40:  sub process {
41:      my $dateerror = 0;                # error in date format in old list
42:      my $newerror = 0;                # error in date format in new item
43:
44:      # main switching point.  There are 2 choices:
45:      # no parameters, for displaying defaults
46:      # any parameters: update, add item if necessary, write and display
47:      if (!param()) {                # first time only
48:          &display_all();
49:      }  else {                        # handle buttons
50:          &remove_selected();
51:          $dateerror = &update_data(); # update existing changes, if any
52:
53:          # add items
54:          if (defined param('additems')) {
55:              $newerror = &check_date(param('newdate'));
56:              if (!$newerror) {
57:                  &add_item();
58:              }
59:          }
60:
61:          &write_data();
62:          &display_all($dateerror,$newerror);
63:      }
64:
65:      print end_html;
66:  }
67:
68:  # read data file into array of hashes
69:  sub read_data {
70:      open(DATA, $listdata) or die "Can't open data file: $!";
71:      my %rec = ();
72:      while (<DATA>) {
73:          chomp;
74:          if ($_ =~ /^#/) {
75:              next;
76:          }
77:          if ($_ ne '---' and $_ ne '') { # build the record
78:              my ($key, $val) = split(/=/,$_,2);
79:              $rec{$key}  = $val;
80:          }  else {                # end of record
81:              push @data, { %rec } ;
82:              %rec = ();
83:          }
84:      }
85:      close(DATA);
86:  }
87:
88:  sub display_all {
89:      my $olderror = shift;        # has an error occurred?
90:      my $newerror = shift;
91:
92:      if ($olderror or $newerror) {
93:          print "<p><font color="red"><b>Error:  Dates marked with *** ";
94:          print "not in right format (use MM/DD/YYYY)</b></font></p>
";
95:      }
96:
97:      print "<table width="75%" align="center">
";
98:      print "<tr bgcolor="silver"><th>Done?</th><th>Priority</th>";
99:      print "<th>Date Due</th><th align="left">Description</th>";
100:     print "<th>Remove?</th></tr>
";
101:
102:     # determine sort type (numeric or string) based on $sortby
103:     my @sdata = ();
104:
105:     # sort the array of hashes based on value of $sortby
106:     if ($sortby eq 'date') {        # special date sort
107:         @sdata = sort {&date2time($a->{'date'} ) <=>
108:                        &date2time($b->{'date'} )}  @data;
109:     }  else {                        # regular text/priority sort
110:         @sdata = sort {$a->{$sortby}  cmp $b->{$sortby} }  @data;
111:     }
112:
113:     # print each item in order
114:     foreach (@sdata) {
115:         &display_data(%$_);        # pass in record
116:     }
117:
118:     print "</table>
";
119:
120:     # preference table, with state preserved
121:     print "<p><table width="75%" align="center">
";
122:     print "<tr><td align="center"><b>Sort By:</b>";
123:     print "<select name="sortby">
";
124:
125:     my @sort_options = ('prior', 'date', 'desc'),
126:     my @sort_option_names = ('Priority', 'Date', 'Description'),
127:
128:     for (my $i = 0; $i < @sort_options; $i++)
129:     {
130:         # get current val of sortby, show menu
131:         print "<option value="$sort_options[$i]" ";
132:         if ($sortby eq $sort_options[$i]) {
133:             print "selected>";
134:         }  else {
135:             print">";
136:         }
137:         print "$sort_option_names[$i]</option>
";
138:     }
139:
140:     print "</select></td>
";
141:
142:     # get current val of showdone, show check boxn
143:     print "<td align="center" width="50%"><b>Show Done?<b>
";
144:     my $checked = '';
145:     if ($showdone == 1) {
146:         $checked = 'checked';
147:     }
148:     print "<input type="checkbox" name="showdone" value="showdone"";
149:     print " $checked /> </td>
";
150:
151:     # print submit button and start of add items table
152:     print <<EOF;
153:     </tr></table>
154:     <p><table align="center">
155:     <tr><td align="center" valign="center">
156:     <input type="submit" value="   Update   " name="update"></td></tr>
157:     </table><hr />
158:     <table align="center">
159:     <tr><th>Priority</th><th>Date</th><th align="left">Description</th>
160: EOF
161:     # print priority menu;
162:     print "<tr><td><select name="newprior">
";
163:     my $i;
164:     foreach $i (1..5) {                # priorities 1 to 5
165:         if ($newerror and param('newprior') == $i) {
166:             $checked = 'selected';
167:         }
168:         print "<option $checked>$i</option>
";
169:     }
170:     print "</select></td>
";
171:
172:     # print date and description cells; may be different in case of
173:     # errors
174:     my $newdate = '';
175:     my $newdesc = '';
176:     print "<td align="center"><input type="text" name="newdate"";
177:     if ($newerror) {                # has an error occurred?
178:         $newdate = "***" . param('newdate'),
179:         $newdesc = param('newdesc'),
180:     }
181:     print "value="$newdate" size="10"></td> 
";
182:     # description cell; preserve old value if error
183:     print "<td><input type="text" name="newdesc" value="$newdesc"";
184:     print "size="50"></td></tr></table><table align="center">
";
185:
186:     # and finish up
187:     print <<EOF;
188:     <tr><td align="center" valign="center">
189:     <input type="submit" value="Add New Item" name="additems" /></td></tr>
190:     </table></form>
191: EOF
192: }
193:
194: # display each line of the data.  Data is already sorted; this just
195: # prints an inidividual record
196: sub display_data {
197:     my %rec = @_;                # record to print
198:
199:     # don't show done items if Show Done is unchecked
200:     # BUT include their settings anyhow (otherwise its too
201:     # difficult to figure out what's shown and changed versus
202:     # what's hidden
203:     if ($showdone == 0 and $rec{'done'} ) {
204:         print "<input type="hidden" name="done", $rec{'id'} ;
205:         print "" />
";
206:         next;
207:     }
208:     # make 1 priority items print in red
209:     my $bgcolor = '';               # priority items are red, all others ''
210:     if ($rec{'prior'}  == 1) {
211:         $bgcolor = "bgcolor="red"";
212:     }
213:
214:     # Is it done or not?
215:     my $checked = '';                # done items are checked
216:     if ($rec{'done'} ) {
217:         $checked = 'checked="checked"';
218:     }
219:
220:     print "<!-- ID: ", $rec{id} , " -->
";
221:
222:     print "<tr>
";                # start row
223:
224:     # done boxes
225:     print "<td width="10%" align="center" $bgcolor>";
226:     print "<input type="checkbox" name="done", $rec{'id'} ;
227:     print "" $checked /></td>
";
228:
229:     # priority menus
230:     print "<td width="10%" align="center" $bgcolor>";
231:     print "<select name="prior", $rec{'id'} , "">
";
232:     my $select = '';
233:     my $i;
234:     foreach $i (1..5) {                # priorities 1 to 5
235:         $checked = '';
236:         if ($rec{'prior'}  == $i) {
237:             $checked = 'selected';
238:         }
239:         print "<option value="$i" $checked>$i</option>
";
240:         $select = '';
241:     }
242:     print "</select></td>
";
243:
244:     # dates
245:     print "<td $bgcolor width="10%" align="checked">";
246:     print "<input type="text" size="10" name="date", $rec{'id'} , "" ";
247:     print "value="", $rec{'date'} , "" /></td>
";
248:
249:     # descriptions
250:     print "<td $bgcolor><input type="text" name="desc", $rec{'id'} ;
251:     print "" size="50" value="", $rec{'desc'} , "" /></td>
";
252:
253:     # Remove boxes
254:     print "<td $bgcolor align="center">";
255:     print "<input type="checkbox" name="r", $rec{'id'} , "" /></td>";
256:
257:     # end row
258:     print "</tr>

";
259: }
260:
261: # update all values in case changes were made
262: sub update_data {
263:     my $error = 0;                # error checking
264:     # check to see if showdone is selected;
265:     if (defined param('showdone')) {
266:         $showdone = 1;
267:     }  else {
268:         $showdone = 0;
269:     }
270:
271:     # get currrent sortby value
272:     $sortby = param('sortby'),
273:
274:     foreach (@data) {
275:         my $id = $_->{'id'} ;        # not the global $id
276:
277:         # Entries that are marked done cannot be changed (usability
278:         # assumption).  So if an entry is marked done, and hasn't
279:         # been changed to not-done, we can skip checking any of the
280:         # rest of its data.
281:         if ($_->{'done'}  == 1 && defined param('done' . $id)) {
282:             next;
283:         }
284:
285:         # All newly done items.
286:         if (defined param('done' . $id)) {
287:             $_->{'done'}  = 1;
288:         }  else {
289:             $_->{'done'}  = 0;
290:         }
291:         # dates.  check for weird date
292:         if (param('date' . $id) ne $_->{'date'} ) {
293:             $error = check_date(param('date' . $id));
294:             if ($error) {
295:                 $_->{'date'}  = "*** " . param('date' . $id);
296:             }  else {
297:                 $_->{'date'}  = param('date' . $id);
298:             }
299:         }
300:
301:         # priorities, descriptions, change only if different
302:         my $thing;
303:         foreach $thing ('prior', 'desc') {
304:             if (param($thing . $id) ne $_->{$thing} ) {
305:                 $_->{$thing}  = param($thing . $id);
306:             }
307:         }
308:     }
309:     return $error;
310: }
311:
312: # remove items by redoing the @data list
313: sub remove_selected {
314:     my @newdata = ();
315:     foreach (@data) {
316:         my $id = $_->{'id'} ;        # also not the global id
317:
318:         if (!defined param('r' . $id)) {
319:             push @newdata, $_;        # $_ is the reference
320:         }
321:     }
322:     @data = @newdata;                # get rid of removed items
323: }
324:
325: # add a new item.  This is only called if check_date has already said OK
326: sub add_item {
327:     my %newrec = ();
328:
329:     $newrec{'desc'}  = param('newdesc'),
330:     $newrec{'date'}  = param('newdate'),
331:     $newrec{'prior'}  = param('newprior'),
332:     $newrec{'done'}  = 0;
333:
334:     my $max_id = 0;
335:
336:     foreach my $datum (@data) {
337:         if ($datum->{'id'}  > $max_id) {
338:             $max_id = $datum->{'id'} ;
339:         }
340:     }
341:
342:     $newrec{'id'}  = ++$max_id;        # global ID + 1
343:     push @data, { %newrec } ;
344: }
345:
346: # dates must be in XX/XX/XX format
347: sub check_date {
348:     my $date = shift;
349:     # MM/DD/YYYY, MM and DD can be 0 or 1 char, but YYYY must be four
350:     # ending whitespace is OK.
351:     if ($date !~ /^(d{1,2} )/(d{1,2} )/(d{4} )s*$/) {
352:         return 1;                # error!
353:     }
354:     return 1 if ($1 > 12);
355:     return 1 if ($2 > 31);
356:
357:     return 0;                    # OK date
358: }
359:
360: # rewrite data file
361: sub write_data {
362:     open(DATA,">$listdata") or die "Can't open list data: $!.";
363:     foreach (@data) {
364:         my %rec = %$_;
365:
366:         foreach ('id', 'desc', 'date','prior','done') {
367:             print DATA "$_=$rec{$_} 
";
368:         }
369:         print DATA "---
";
370:     }
371:     close(DATA);
372: }
373:
374: # I use MM/DD/YY format for dates.  To sort by date you need to
375: # convert this format back into Perl's seconds-since-1900 format.
376: # the Time::Local module and the timelocal func do this.
377: sub date2time {
378:     my $date = shift;
379:     if ($date =~ /^***/) {        # error formatting, sort to top
380:         return 0;
381:     }  else {
382:         my ($m,$d,$y) = split(///,$date);
383:         $m--;                   # months start from 0 in perl's time format
384:         return timelocal(0,0,0,$d,$m,$y);
385:     }
386: }
						

..................Content has been hidden....................

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