by David Harlan
You may have sensed that something important was missing from the preceding chapter, which explained in detail how to get and save data but did not explain how to get that information from your data files back to an HTML page. This chapter fills in those gaps.
Saving your experiment data as you did in Chapter 3is all well and good, but without a suitable output method, all that data is useless. You certainly don't want to waste all the work that you just finished. This section discusses some good ways to use the data that you're gathering from those scripts and forms.
The most obvious thing that you can to do with the experiment data is print an up-to-date summary of a user's data after that user enters a new weekly data set. The result might look something like figure 4.1.
Figure 4.1 : The user can view a summary of his or her survey data in this format.
This page is fairly simple. The first thing that you see is a simple thank-you/introduction line; then you see a table that shows all the data to date for the user. The script that creates this page, shown in Listing 4.1, is correspondingly simple.
Listing 4.1 Script to Print a Summary of User-Survey Data (PRINTDATA1.PL)
#!/usr/bin/perl require "process_cgi.pl"; $period[1]="Aug 5-11"; $period[2]="Aug 12-18"; $period[3]="Aug 19-25"; $period[4]="Aug 26-Sep 1"; $period[5]="Sep 1-7"; $period[6]="Sep 8-14"; $period[7]="Sep 15-21"; $period[8]="Sep 22-28"; $email=&path_info; &print_header; print "<title>Web Use for $email</title>\n"; print '<body bgcolor="#FFFFFF">'; print "Following is the up-to-date web use survey data for $email. Thank you for your continued support. See you in another week or so.<p>\n"; print "<table border=1><tr><td valign=bottom>Period<td>Total<br>Hours<td>Web<br> Hours<td>Phone <br>Hours<td>e-mail<br>Sent<td>e-mail<br>Received<td valign=bottom>Ways Used\n\n"; for ($i=1; $i<9; $i++) { open (data, "printdatasup period$i $email |"); $data=<data>; $data=~s/::::/::n\/a::/g; $data=~s/::::/::n\/a::/g; $data=~s/::$/::n\/a/; $data=~s/^::/n\/a::/; $data=~s/::/<td>/g; print "<tr><td>$period[$i]<td>",$data,"\n" if $data ne ''; } print "</table>";
The following paragraphs examine how printdata works. The first two lines constitute the familiar header, common to most of the scripts in this book, that tells the operating system where to find the interpreter and that tells the interpreter that you want to include some code from an outside file. The next section of code defines an array of strings that you'll use to put labels on the period data later in the script.
Next comes the line $email=&path_info. If you read chapters 2 and 3, you know that this statement is calling the subroutine &path_info and placing the result in the variable $email. As you see in Listing 4.1, this program has no path_info subroutine. I added another useful function to PROCESS_CGI.PL. As you learned in Chapter 2 "Introduction to CGI" (specifically, in Listing 2.5), the PATH_INFO environment variable is a useful way to pass information to a CGI script. I use this method with some frequency, so I decided that the code that gets this variable was a good candidate to reside in PROCESS_CGI.PL. As Listing 4.2 shows, the subroutine is as simple as subroutines come. This code contains no new Perl.
Listing 4.2 Subroutine to Return the PATH_INFO Environment Variable
sub path_info { $path=$ENV{'PATH_INFO'}; $path= s/\///; $return=$path; }
After assigning $ENV{'PATH_INFO'} to $path, the script removes the leading slash. The final assignment ensures that the appropriate information is returned to the script. This is easy enough that you could put this code into each script that required it, but why bother if you're going to be using the library anyway? Write the code once, and get the information that you need with one line of code instead of three.
In Listing 4.1, after getting the e-mail address from the &path_info subroutine, the script prints the HTTP header and the beginning of the page, setting the page background color to white, printing the introductory text, starting a table, and printing the first row of the table.
The next (and most important) section of code is a loop that prints the data that the user entered into the survey database. You may recall from the preceding chapter that some versions of Perl are limited to one dbmopen statement per script. Therefore, you have to call an outside program each time you want to access the DBM file for a new time period. Chapter 3used the system function for that purpose. Listing 4.1 uses a version of the open command that you saw in Chapter 2(refer to Listing 2.8). This syntax calls the listed program-in this case, printdatasup-and puts the resulting output in the file handle listed in the first argument (data).
printdatasup is not a built-in function; you probably have guessed that it's a Perl program that I wrote for this specific purpose. Listing 4.3 shows the code.
Listing 4.3 Listing of printdatasup (PRINTDATASUP.PL)
#!/usr/bin/perl dbmopen (%data, $ARGV[0], 0666); print $data{$ARGV[1]};
This program is as straightforward as it looks; all it does is open the DBM file provided in the first argument and then print the data from that file that corresponds to the key provided in the second argument. The first time through the loop, printdata calls this command with the arguments period1 and harlan@3rdplanet.com, so printdatasup simply prints the appropriate data from the period1 DBM file. printdata then saves this data in the variable $data (appropriately enough) for processing later in the script. Each time through the loop, $i is incremented, so each time through the loop, the next period's DBM file is queried for data for the given e-mail address.
The rest of the loop processes the information in $data before printing it. For the sake of clarity in output, I made sure that any empty fields were replaced by n/a. The successive s/::::/::n\/a::/g commands ensure that any lines of data that contain several null fields in succession (such as the last line of fig. 4.1) print with the appropriate number of n/a fields. With only one of these substitutions, alternating fields would be blank.
The following two lines make sure that blank fields at the beginning
or the end of the data also contain n/a. Finally, the
script substitutes <td> tags for the ::
field delimiters in $data.
TIP |
Tables and other complex HTML structures sometimes cause problems when you're trying to debug a CGI program. To make this process easier, I sometimes print an <xmp> tag after the HTML header during the debugging phase. When you run a script such as this, the HTML tags after the <xmp> tag are printed rather than interpreted, so you can quickly see what's going on. You can forgo this method and view the source every time, but this method is more convenient if you are doing extensive debugging. (Don't forget to remove the <xmp> tag before you put the script online.) |
With all the processing done, the script then prints the row in the table, beginning with the appropriate data from the @period array, followed by the fully processed information in $data. The conditional at the end of the line ensures that nothing will print if $data is empty. This variable is blank for any periods that the user did not supply data for. You can see that even though the script checked periods one through eight, only the first six periods have rows in the table. Data was not entered for the final two periods of the survey, so those rows don't appear in the table. When the loop is finished, the script simply prints the closing table tag and exits.
The preceding example is a good starting point for presenting the survey data, but it's really only the beginning of what you can do with it. You can imagine any number of ways that you may want to view this kind of data, and perhaps your users can, too, so give them an opportunity to see the data exactly as they want to. Consider the form shown in figure 4.2.
Figure 4.2 : The user uses this form to choose the data to be displayed.
As you can see, the form in figure 4.2 presents a multitude of choices. Figure 4.3 shows the page that was produced when I submitted the form as shown. The script that processed this form and created the page is by far the most complex that you've seen so far in this book.
Figure 4.3 : The page results from the submission of the form in figure 4.2.
You must deal with many issues when you design a script such as this to create a user-designed page. You might be tempted to use conditionals, hard-coding the form fields into the script. This method would work, but it would make the script large and difficult to adapt to changes in the data. Instead, through careful form design and the use of some interesting Perl, you can make this script relatively short and fairly adaptable. Listing 4.4 shows the code.
Listing 4.4 Part 1 of the printcustomdata Script (PRINTCUSTOMDATA.PL)
#!/usr/bin/perl require "process_cgi.pl"; require "check_pass.pl"; @fieldorder=('hours','webhours','webhourratio','phonehours', 'phonehourratio','sentmail','receivedmail','waysused'); @fieldtitles=('Total<br>Hours','Web<br>Hours', 'Web Hours /<br>Total Hours','Phone<br>Hours', 'Phone Hours /<br>Total Hours','E-mail<br>Sent', 'E-mail<br>Received','Ways Used'); @graphgifs=('hoursbar.gif','webhoursbar.gif','webhourratiobar.gif', 'phonehoursbar.gif','phonehourratiobar.gif','sentmailbar.gif', 'receivedmailbar.gif'); @period=('',"Aug 5-11","Aug 12-18","Aug 19-25", "Aug 26-Sep 1","Sep 1-7","Sep 8-14","Sep 15-21", "Sep 22-28"); &parse_input(*fields); &print_header;
The code shown in Listing 4.4 presents nothing new but is important nonetheless. After requiring two outside files (you should recognize one and should be able to guess the function of the other), the script defines some arrays. The @fieldorder array does exactly what its name suggests; it establishes a field order for the data from the form. Each of the strings in the array is the name of one of the fields in the table section of the form. Recall that a comma-separated list of scalar data enclosed in parentheses is the equivalent of an array. This syntax is similar to the shorthand used in previous examples to extract array-type data from functions; it just goes in the other direction. (The importance of the @fieldorder array will become clear soon.)
The next two arrays establish two additional sets of string data. Notice that the data in these arrays follows the same order as the data in @fieldorder. Again, this is quite important, as you soon will see. The definition of the @period array is functionally identical to the way that this array is defined in Listing 4.1; this method simply requires less typing. Notice that a null string is the first element of this array. This is necessary because Perl arrays begin at index 0, and you want your data to start at 1.
Finally, the script parses the form input into the %fields array and prints the page header. This section of code sets the stage for a little fancy footwork, as you see in the following sections.
Creating Code On-the-Fly The short section of code shown in Listing 4.5 begins the actual processing of the form. After setting $j (which will keep track of the script's progress through the loop in the following listing), the script defines a series of strings. You should notice that each string (up to $graphkey) is the beginning of a Perl statement.
Listing 4.5 Part 2 of the printcustomdata Script (PRINTCUSTOMDATA.PL)
$j=0; if (&check_pass($fields{'email'},$fields{'pass'})) { #Begin writing our 'mini-programs' $printtop='print "<tr><td>Period'; $printrow='print "<tr><td>$period[$i]'; $printtot='print "<tr><td><b>Total</b>'; $printavg='print "<tr><td><b>Average</b>'; $graph='$return=\''; $calchght='$return="'; $printgraph = 'print "<table border=0><tr>'; $graphkey='print "'; $tableit='n'; $graphit='n';
The comment near the top of this block in Listing 4.5 says, "Begin writing our 'mini-programs,'" which is exactly what the code in Listings 4.6, 4.7, 4.8, and 4.9 does. By the end of these sections, the script will have created several strings that contain Perl statements for use later in the program. Why you're doing this will become clear soon.
Also notice that the first line of Listing 4.5 checks the subroutine &check_pass before moving on. This routine, which resides in the file CHECK_PASS.PL, checks that the user information submitted with the form is correct. Listing 4.6 shows the code.
Listing 4.6 A Subroutine to Check User and Password Information (CHECK_PASS.PL)
#!/usr/bin/perl sub check_pass { $email=$_[0]; $pass=$_[1]; dbmopen (%users,"users",0666); if (!defined($users{$email})) { print "The email address you entered does not exist in our database. Please hit the back button on your browser, correct your entry and re-submit the form."; return 0; } else { $temp=$users{$email}; dbmclose(%users); $temp=~/([a-zA-Z0-9]{5,10})$/; $actualpass=$1; if ($actualpass eq '') { print "There is no password entered for this e-mail address. Please enter one <a href=/userpassword.html>here</a> before you view your data."; return 0; } elsif ($pass ne $actualpass) { print "The password you entered is incorrect. Please return to the previous screen and try again."; return 0; } else { return 1; } } } return 1;
CHECK_PASS.PL should look familiar; the code is nearly identical to the section of code that performs the same function in Listing 3.8 (refer to Chapter 3 "Advanced Form Processing and Data Storage"). The first difference that you should notice is that whenever the test fails-that is, when the address does not exist in the database, or when the password doesn't exist or doesn't match-the subroutine returns a value of 0. When the password test passes, the subroutine returns a value of 1. This allows you to use the syntax if (&check_pass()) { to make sure that the user is authorized to execute the rest of the script.
You should also notice that because &check_pass prints the appropriate error message, the script doesn't need to do anything if this test fails. In fact, the last line of printcustomdata is the bracket that closes the block that starts at the top of Listing 4.5. Other than initialization, nothing happens if the &check_pass test fails.
Now that the password checking is finished and the various strings are initialized, you can move on to the meat of the program, shown in Listing 4.7. This section of the script is the first section of a loop over the @fieldorder array. You'll recall that a foreach loop starts processing at the first item of the given list or array. Because this particular loop has no scalar variable before the array, the loop places each successive item in the Perl special variable $_.
Listing 4.7 Part 3 of the printcustomdata Script
foreach (@fieldorder) { $findmax .= "\$max$_=\$$_ if \$$_ > \$max$_;\n"; $calctotal .= "\$tot$_ += \$$_;\n"; if (/ratio/) { $calcavg .= "\$avg$_ = sprintf ('%.2f', \$tot$_ / \$rationum) if \$rationum != 0;\n"; } else { $calcavg .= "\$avg$_ = sprintf ('%.2f', \$tot$_ / \$num) if \$num != 0;\n"; }
The remainder of the loop occupies itself with appending appropriate text to the various strings that were initialized in Listing 4.5. Take a closer look at the first of those lines:
$findmax .= "\$max$_=\$$_ if \$$_ > \$max$_;\n";
What does this line do? You saw the .= assignment operator previously; it simply appends the string on the right to the variable on the left. What the string on the right will turn out to be may not be entirely clear at first; I'll explain.
Because the string is enclosed in double quotes, you are asking Perl to interpolate any variables or special escape sequences in the string. Right off the bat, though, you want the string to include a dollar sign before max. Putting a backslash before the dollar sign tells Perl that you want the actual dollar sign to be included in the string. Without the backslash, Perl would have looked for the variable $max and, finding nothing, replaced it with the null string.
Next comes the variable $_. Remember that this string is interpolated, so this variable is replaced by whatever $_ contains at that point in the execution of the script.
Following the equal sign, another \$ sequence tells the interpreter that you want another dollar sign. That sequence is followed by $_ (no backslash).
You should have the idea by now. Each time through the loop, another line is added to $findmax. At the end, the contents of the string would look something like this:
$maxhours=$hours if $hours > $maxhours; $maxwebhours=$webhours if $webhours > $maxwebhours; $maxwebhourratio=$webhourratio if $webhourratio > $maxwebhourratio; $maxphonehours=$phonehours if $phonehours > $maxphonehours; $maxphonehourratio=$phonehourratio if $phonehourratio > $maxphonehourratio; $maxsentmail=$sentmail if $sentmail > $maxsentmail; $maxreceivedmail=$receivedmail if $receivedmail > $maxreceivedmail; $maxwaysused=$waysused if $waysused > $maxwaysused;
You should be able to figure out what the next line does to its variable. One line of $calctotal's final form looks like $tothours += $hours;. Next, a conditional provides two options for $calcavg. As a result, two successive lines in the final contents of $calcavg will appear as follows:
$avgwebhours = sprintf ('%.2f', $totwebhourrs / $num) if $num != 0; $avgwebhourratio = sprintf ('%.2f', $totwebhourratio / $rationum) if $rationum != 0;
These statements will be executed later in the script. First, I'll explain the two new pieces of Perl that you see in the preceding examples.
The first new element is the += assignment operator. Much like the .= operator, += is a shortcut for a commonly performed task. In this case, $a += $b is the functional equivalent of $a = $a + $b. Perl has several "shortcut" assignment operators.
The second new element is the function sprintf, which is used to set the average variables. This function takes a string representing a format and a list of values. The format contains a list of symbols and flags that tell the function how to deal with the listed values. In this case, the format tells the script to print the first value in the list as a floating-point number rounded to two decimal places.
The next section of code from printcustomdata, shown in Listing 4.8, builds the actual statements that print the table shown in figure 4.3. The initial conditional ensures that the script will print data only for those fields that the user requested on the form. Remember that @fieldorder contains the exact names of the fields from the table section of the form shown in figure 4.2.
Listing 4.8 Part 4 of the printcustomdata Script
if ($fields{$_} eq 'y') { $tableit='y'; $printtop .= "<td>$fieldtitles[$j]"; $printrow .= "<td>\$$_"; $printavg .= "<td>\$avg$_"; if (/ratio/) { $printtot .= "<td>"; } else { $printtot .= "<td>\$tot$_"; } }
Each time through the loop, $_ contains one of these key fields. By checking to see whether $fields{$_} is y, the script is checking to see whether the user wanted that particular field in his table. After the loop finishes, $printrow contains the following:
print "<tr><td>$period[$i]<td>$hours<td>$webhours<td>$webhourratio<td>$sentmail <td>$receivedmail;
The purpose of this line should be clear: it prints one row of the table shown in figure 4.3. The line does not contain the variables $phonehours, $phonehourratio, and $waysused, because they were not checked on the form (the user didn't want them to be included in the resulting table).
$printtop, $printavg, and $printtot end up containing similar data. The major difference is that because a total would make no sense for a ratio, the conditional if (/ratio/) { is used to make the appropriate adjustments. If this conditional is true, the script appends the appropriate code to $printot so that it prints an empty table cell; otherwise, text is appended that puts the appropriate variable in the next cell.
The final section of the foreach(@fieldorder) loop shown in Listing 4.9 prepares the statements that print the graph that the user designed. These statements are somewhat more complex than the ones in the preceding section.
Listing 4.9 Part 5 of the printcustomdata Script
if ($fields{"gr$_"} eq 'y') { $graphit='y'; $graph .= "\$gr${_}['.\"\$i\".']=\"<td valign=bottom><img src=\/bars\/ ${_}bar.gif width=15 height=\$${_}hght['.\"\$i\".'] >\";"; $calchght .= "if (\\\$max${_} != 0) {\\\$${_}hght[\$i] = int((\$$_ / \\\$max${_})*200);} else {\\\$${_}hght[\$i] = 0;}\n"; $printgraph .= "\$gr${_}[\$i]\n"; $graphkey .= "<tr><td align=right>$fieldtitles[$j]=<td> <img src=\\\"\/bars\/$graphgifs[$j]\\\">"; } $j++; }
Like the code shown in Listing 4.8, Listing 4.9 first determines whether the user wants to display a given piece of data in the graph. This is where careful form design comes into play. When I created this form, I made sure to name the check boxes for the graph portion of the form identically to those in the table portion, but I appended gr to the beginning of each name. By doing this, I have to loop through only 8 values, rather than 15.
The opening conditional in Listing 4.8 checks to see whether $fields{"gr$_"}
is equal to y. If so, the user wants that data to be
used in the graph. The first time through the loop, $fields{"grhours"}
would be checked. Because grhours is a piece of data
that the user indicated on the form that he wanted to see on the
graph, $fields{"grhours"} does indeed equal
y, and we execute the block of statements.
TIP |
Form design is a key step in the creation of a CGI application that programmers often overlook. If you are working with a form that has poorly named variables or strange values for check boxes or menu items, writing the program is more difficult. I recommend that you spend significant time getting the form right before you get too deep into your programming; you'll save time in the long run. Also, it frequently makes more sense to change the form than to go through some programming magic to make the form do what you need it to do. |
The first thing that this block does is set $graphit to y. This variable tells later code that the script has at least one graph element. Then you reach what has to be one of the ugliest statements I've ever written in Perl:
$graph .= "\$gr${_}['.\"\$i\".']=\"<td valign=bottom><img src=\/bars\/${_}bar.gif width=15 height=\$${_}hght['.\"\$i\".'] >\";";
To make this statement a little easier to understand, examine what $graph will contain after the first time through the loop:
$return='$grhours['."$i".']="<td valign=bottom><img src=/bars/hoursbar.gif width=15 height=$hourshght['."$i".']>";
Okay, the statement probably isn't much more clear immediately, but let's press on. Remember that in Listing 4.5, $graph was initialized with the string $return= \'. When you append the string to $graph as shown, you have the start of a new assignment statement. As you can see, you're going to be assigning a big string to $return.
When this statement is executed, the script won't be just assigning one long string enclosed in quotes; it actually will perform several append operations, using the . operator. The two instances of $i are enclosed in double quotes; thus, they will be interpolated when this statement is executed. The rest of the strings are enclosed in single quotes specifically to prevent interpolation when this statement is executed. The reason is that this step is actually two steps away from the final piece of code. $calchght also is two steps away from the final piece of code.
The last two variables-$printgraph and $graphkey-are similar to the print variables in Listing 4.8 and should be easy for you to figure out.
Listing 4.10 shows the final piece of code needed to complete this section. This code completes all the strings that will be executed as statements later in the script.
Listing 4.10 Part 6 of the printcustomdata Script
$printtop .= '\n";'; $printrow .= '\n";'; $printtot .= '\n";'; $printavg .= '\n";'; $graph .= '\';'; $calchght .= '";'; $calctop .= '";'; $graphkey .= '\n";'; $printgraph .= '</table>";';
Using eval() to Execute Dynamic Code Sections By now, you're probably wondering how the script is going to use all the mini-programs discussed in the preceding sections. The listings in this section should answer that question nicely. Listing 4.11 begins the answering process.
Listing 4.11 Part 7 of the printcustomdata Script
#print out the top of the page. print "<title>Web Use for $fields{'email'}</title>"; print '<body bgcolor="#FFFFFF">'; print "Following is the data you requested. Thank you for your continued support. See you in another week or so.<p>"; #Run the first mini-program to print out the top of the table. if ($tableit eq 'y'){ print "<table border=1>\n"; eval($printtop); }
Listing 4.11 begins by printing the top of the page, starting with a title and the <body> tag; it then prints a brief greeting message before executing the conditional if ($tableit eq 'y') {. If the $tableit variable is y, you know that you have at least one field requested for the table (refer to Listing 4.8), and you want to initialize the table. After the print statement that outputs the tag to open a table, you see the command that is going to take care of all the mini-programs: eval().
Just as you would expect, this command takes the string that you give it and executes the string as though the lines that it contains were typed at that point in the program. In Listing 4.11, this eval() command prints the top row of the table, which contains the column titles for the user-requested data. Listing 4.12 continues the process of printing the table.
Listing 4.12 Part 8 of the printcustomdata Script
$num=0; $rationum=0; #Loop through the data. for ($i=1; $i<9; $i++) { open (data, "printdatasup period$i $fields{'email'} |"); $data=<data>; if ($data ne '') { $num++; $data=~s/::::/::0::/g; $data=~s/::::/::0::/g; $data=~s/^::/0::/; $data=~s/::$/::n\/a/; ($hours,$webhours,$phonehours, $sentmail,$receivedmail,$waysused)=split(/::/,$data); if ($hours != 0) { $rationum++; $webhourratio=sprintf('%.2f', $webhours/$hours); $phonehourratio=sprintf('%.2f', $phonehours/$hours); } else { $webhourratio='0'; $phonehourratio='0'; }
Listing 4.12 begins by initializing two counter variables-$num and $rationum-that will be used to calculate averages; then it starts a loop from 1 to 8. This loop is the same loop that was used to scan the data in Listing 4.1. The beginning of this loop uses the support script from Listing 4.2 to grab the appropriate data from the DBM file. After checking for data, the script performs a couple of substitutions on the data to make sure that the data contains zeroes instead of blank fields; then it splits the data into separate variables.
Notice that the variable names used here are the names of the fields in the original form. The mini-programs created earlier in the script depend on these variable names. After splitting the data, the script calculates the ratios (notice the use of sprintf()) for Web hours and phone hours. You could have created a string to run through an eval() to perform this calculation. You may want to see whether you can figure out what that would look like.
After the data is split into the appropriate variables and the ratio calculations are made, Listing 4.13 shows a series of eval() statements. As the comment at the top of this section of code indicates, this code performs several key steps.
Listing 4.13 Part 9 of the printcustomdata Script
#Run the mini-programs to print a row #recalculate totals and find the maximum #value of a column. eval ($printrow) if $tableit eq 'y'; eval ($calctotal); eval ($findmax); $finalgraph .= eval($graph); $finalcalchght .= eval($calchght); $graphbottom .= "<td align=center>$period[$i]"; } }
The first eval() prints a row of the table. Remember that $printrow contains the following:
print "<tr><td>$period[$i]<td>$hours<td>$webhours<td>$webhourratio<td>$sentmail <td>$receivedmail;
When the script evaluates this string the first time through the loop, it prints the following:
<tr><td>Aug 5-11<td>50<td>45<td>0.90<td>30<td>200
The next two statements evaluate two strings that amount to a series of assignment statements. The first string adds the current value of each variable to the appropriate running total; the second string makes sure that the maximum value for each variable is still the maximum.
The functionality of the next two statements isn't as clear. Earlier in this chapter, I said that $graph and $calchght are two steps away from the final code. The next few paragraphs discuss the first step. If you use eval() on the right side of an assignment, it works just like a subroutine. The last value of the last assignment statement in the block of statements (a string, in the case of an eval) is returned to the variable on the left side of the assignment.
If you look at Listing 4.5, you'll notice that $graph and $calchght each contain a single assignment statement. Thus, when the script executes the $finalgraph .= ... statement in Listing 4.13, it repeatedly appends the data from $graph.
Remember that parts of $graph were single-quoted and other parts were not. If you look at what $graph contained, you see that by the time the script gets through this loop, the final graph contains a series of assignment statements that look like the following:
$grhours[1]= "<td valign=bottom><img src=/bars/hoursbar.gif width=15 height=$hourshght[1]>"; $grwebhours[1]= "<td valign=bottom><img src=/bars/webhoursbar.gif width=15 height=$webhourshght[1]>";
Each variable that the user requested for the graph is represented by an array that contains as many members as the user has entries in the database. Similarly, $finalcalchght contains a series of statements that create the hght arrays for each variable. When each of these new strings is evaluated later (in Listing 4.15), the arrays are created, and all the appropriate data is assigned to the appropriate places.
This may not be entirely clear on first reading, but it's worth taking the time to wrap your brain around this concept. The eval() function is an extremely useful tool in high-end form processing.
After all that complexity, Listing 4.14 is a nice change of pace. This section of code starts with a conditional to make sure that the code executes only if some table data was selected in the original form. If the code passes that test, it prints a totals line, if the user selected it. The script then calculates the averages and prints the averages line, if needed. Then the script closes the table and prints a paragraph mark to separate the table from the graph that follows.
Listing 4.14 Part 10 of the printcustomdata Script
if ($tableit eq 'y') { eval ($printtot) if $fields{'total'} eq 'y'; eval ($calcavg); eval ($printavg) if $fields{'average'} eq 'y'; print "</table>"; print "<p>"; }
The last section of printcustomdata, shown in Listing 4.15, prints the graph portion of the page. Not surprisingly, the code first checks to make sure that the user actually requested a graph. With that task out of the way, the script opens the table. Then the script evaluates the $finalcalchght and $finalgraph strings, creating the arrays that the eval($printgraph) statement will use to print the graph.
Listing 4.15 Part 11 of the printcustomdata Script
if ($graphit eq 'y') { print "<table border=0><tr>"; eval ($finalcalchght); eval ($finalgraph); for ($i=1; $i<9; $i++) { print "\n<td valign=bottom align=center>"; eval ($printgraph); } print "\n<tr>$graphbottom"; print "</table><p>"; $graphkey=~s/<br>/ /g; print "<table>"; print "<tr><td colspan=2 align=center><b>Key</b><hr>"; eval ($graphkey); print "</table>"; } }
You may be curious about why I had to go through the bother of creating two evals for this section of code when everything else could be done in the loop started in Listing 4.12. The difficulty lay in the fact that I needed a maximum for each variable to set a scale for each section of the graph. Netscape scales images to the size indicated in the <img> tag to make the bars of the graph. The actual GIFs that make up the bars are 14 pixels high by 14 pixels wide. By calculating a height for each bar relative to the maximum for that variable over all of the periods, I am able to scale each bar appropriately. The Aug. 26-Sep. 1 total hours bar, for example, is all the way to the top. That period happened to have the highest number of hours, so all the rest of the hours bars were set relative to it. This method seemed to be the best way to present the data in one graph.
After finishing the loop, the script prints the labels for each period and closes the table. The final act that this script performs is to print a key so that the user knows what each bar means. By now, you should be able to figure out how this procedure works.
As you've seen, the eval() function is a useful tool for interpreting some kinds of form data into truly advanced dynamic pages. The following sections introduce some other high-end page-output options.
Beginning CGI programmers often get stuck in the mindset that forms are a data source, not an output option. This is not always the case. Almost certainly, a user will need to change some data that he entered at your site. The ability to take data from your data files and return it to a form for editing is an important tool to have in your CGI toolbox.
Consider the periodic data-entry field shown in figure 3.5. Not every user is going to get his data correct right off the bat, so you should allow users to select and edit a line of data. I changed two of the scripts that appear earlier in this chapter for this purpose. Now, after a user enters data, a screen like the one shown in figure 4.4 appears.
Figure 4.4 : This form enables the user to edit previously entered periodic data.
As you can see in the Location box in figure 4.4, this page was produced by a modified version of the printdata script shown in Listing 4.1. The new version of the script adds some code that makes the original table into an HTML form. The new code (PRINTDATA.PL) is on the CD-ROM that accompanies this book. To automatically direct the user to this script after entering data, I changed the postperioddata script (refer to Listing 3.8 in Chapter 3 to redirect the browser instead of printing a page directly; see POSTPERIODDATA.PL on the CD-ROM.
When a user selects one of the periods and clicks the Edit Data button in figure 4.4, a screen like the one shown in figure 4.5 appears. Notice that all the text boxes contain appropriate data and that the appropriate items in the Uses menu box are selected.
Listing 4.16 shows the first key part of the code that creates this form. This block follows a straightforward initialization section that prints the header and initializes some variables.
Listing 4.16 Partial Listing of editperiod (EDITPERIOD.PL)
if (&check_pass($fields{'email'},$fields{'pass'})) { open (data, "printdatasup period$fields{'period'} $fields{'email'} |"); $data=<data>; ($hours,$webhours,$phonehours, $sentmail,$receivedmail,$waysused)=split(/::/,$data); @waysused=split(/,/,$waysused); foreach (@waysused) { $dowaysused .= "\$${_}selected=\"SELECTED\";"; } eval ($dowaysused);
The first thing that you should notice about Listing 4.16 is that it again uses the &check_pass() subroutine and printdatasup program. When the script gets the data back from the DBM file, it splits the data into the now-familiar variables. This time, however, the script splits one of those variables-$waysused-again. The script uses the resulting array in a foreach loop that builds a string that is evaluated in the last statement of Listing 4.16.
A closer look at $dowaysused clarifies what's happening. After the loop is complete, $dowaysused looks similar to this:
$webselected="SELECTED"; $emailselected="SELECTED"; $ftpselected="SELECTED";
When this string is evaluated, all the Web uses that the user selected have a corresponding selected variable set to SELECTED. When this process is complete, the script prints the actual form, as shown in Listing 4.17.
Listing 4.17 Part 2 of editperiod
print <<EOF <title>Internet Use Survey Data Entry</title> <body bgcolor=#FFFFFF> <h2>Internet Use Survey</h2> <form method=post action=/cgi-bin/harlan/postperioddata> <table> <tr><td colspan=2><p>Please enter your password. <tr><td align=right>e-mail address:<td>$fields{'email'}<input type=hidden name=email value="$fields{'email'}" size=30> <tr><td align=right>password:<td><input type=password name=pass size=10 maxlength=10> <tr><td align=right>entry period:<td>$period[$fields{'period'}] <input type=hidden name=period value=$fields{'period'}> <tr><td colspan=2><p>How many hours did you spend on the Net during the indicated period? <tr><td align=right>hours:<td><input type=text name=hours value="$hours" size=4> <tr><td colspan=2><p>What did you use the Net for during the indicated period? (Shift or Ctrl+click to select more than one value.) <tr><td align=right valign=top>uses:<td><select name=uses multiple size=3> <option value=Web $webselected>World Wide Web <option value=e-mail $emailselected>email <option value=FTP $ftpselected>File Transfer <option value=Gopher $gopherselected>Gopher <option value=IRC $ircselected>Internet Relay Chat <option value=talk $talkselected>Talk (text based) <option value=phone $phoneselected>Internet Phone (voice) <option value=other $otherselected>Other uses </select> <tr><td colspan=2><p>If you used the Net for e-mail, how many messages did you receive and send during this period? <tr><td align=right>received:<td><input type=text name=receive value="$receivedmail" size=4> <tr><td align=right>sent:<td><input type=text name=send value="$sentmail" size=4> <tr><td colspan=2><p>If you used the Web, how many hours did you do so during this period? <tr><td align=right>hours:<td><input type=text name=webhours value="$webhours" size=4> <tr><td colspan=2><p>If you used an Internet phone, how many hours did you talk during this period? <tr><td align=right>hours:<td><input type=text name=phonehours value="$phonehours" size=4> <tr><td colspan=2><p><input type=submit value="submit data"> </table> </form> EOF }
This listing presents a new piece of Perl syntax. The <<EOF after the print command tells the interpreter to print everything that follows until it runs into the label EOF. The next few lines are then treated as though they were a string-in this case, a double-quoted string. The primary advantage of this syntax in this application is that you don't have to worry about putting backslashes before any double quotes that you want to print. Variables are still interpolated, however. You can print single-quoted strings with this syntax by enclosing the label in single quotes. Had the script used the command print <<'EOF' instead, the variable references would not be interpreted, and instead of getting numbers in the form, the script would have printed the variable names.
Putting the values in the appropriate places in the form is easy. I put the values of text boxes after the appropriate value= attribute. When you set a selected variable for each Web use that occurs in the data, preselecting the list items is as simple as putting each selected variable next in the appropriate tag. If a particular Web use occurs in the data, the <option> tag ends up containing SELECTED, so that use shows up in the browser as being selected.
Although this block of text looks as though it may have been copied directly from the form's original HTML file, the code contains some significant changes. First, the code took away the user's capability to change the e-mail address and the period for which the data is being entered. Notice that the user sees these two pieces of information as straight text. In this new form, the appropriate data is also coded into a hidden field. Had the code allowed users to change this information, it would have created potential for confusion and perhaps lost data.
Also notice that the code requires the user to type his e-mail
address again. Although I could have hard-coded the data into
the form in a hidden field, I felt that the small inconvenience
of having the user type the password again was offset by the added
security. If this form resided on a site that had user authentication,
I might have been able to prevent this repetitious password typing.
TIP |
One of the most important concepts for a CGI programmer to remember is the fact that Web applications are essentially stateless. The server makes no attempt to track a user's progress through a certain function, so if a function for your Web application is going to require three forms, you have to figure out a way to transmit the state of that function from one form to the next. The best CGI programmers understand this concept and develop an arsenal of tools (many of which are discussed in this chapter) to overcome this limitation. |
With some knowledge of editing previously entered data under your belt, now you can look for other kinds of data to store, retrieve, and edit. How about those user-designed pages that you created at the beginning of the chapter? Web users really appreciate sites that keep track of what they want to see. This section shows you a way to save a user's pages and enable the user to view and edit those pages.
Saving the Custom Layout As you can see in figures 4.6 and 4.7, I have created scripts to store, retrieve, and edit the custom layouts that you spent so much time looking at earlier in this chapter.
Figure 4.6 : The bottom of a user-designed page gives the user the option to save this layout.
Figure 4.7 : This listing of saved layouts is the result of selecting the link in figure 4.6.
Figure 4.6 shows a good way to allow the user to save a layout. This link was a simple addition to the printcustomdata script that you examined in detail in Listings 4.4 through 4.15. I added the following line at the bottom of the script:
print "<p><a href=\"/cgi-bin/harlan/savedataset/$fields{'email'}::$saveit\"> save this layout.</a>" if $direct ne 'y';
As you can see, the URL calls the script savedataset. You should also be able to tell that the script is going to get its data from the PATH_INFO variable. The variable $fields{'email'} should be familiar; $saveit should not. This new variable contains a list of the selections on the form. I built this variable in the same loop that built the dynamic code in printcustomdata. You can see exactly how this process works by looking at the code on the CD-ROM that accompanies this book (PRINTCUSTOMDATA.PL). I'll explain more about the conditional that controls this line later in the chapter.
When the user selects the link shown in figure 3.6, the savedataset script saves the layout and prints the page shown in figure 4.7. Listing 4.18 shows the code.
Listing 4.18 A Script to Save a User-Designed Page (SAVEDATASET.PL)
#!/usr/bin/perl require "process_cgi.pl"; if (&parse_input(*fields)) { $email=$fields{'email'}; $pass= $fields{'pass'}; $id= $fields{'layout'}; delete($fields{'email'}); delete($fields{'pass'}); delete($fields{'layout'}); foreach (keys(%fields)) { $data .= "$_,"; } $data=~s/,$//; $data="NULL" if $data eq ''; } else { $temp=&path_info; ($email,$data)=split (/::/,$temp); } dbmopen (%users,"users",0666); if (!defined($users{$email})) { &print_header; print "The email address you entered does not exist in our database. Someone may be trying to fool us..."; } else { system ("savegraph $email $data $id"); &print_header ("http://192.0.0.1/cgi-bin/harlan/showsaveddatasets/$email"); }
The first thing that you should notice in Listing 4.18 is the fact that this script checks for form input (if (&parse_input(*fields))...) before looking to the PATH_INFO variable for its data. I added this flexibility to avoid having to write another script to put edited data back into the database. When called from the link shown in figure 4.6, the script learns all that it needs to know from the PATH_INFO variable. The script splits the data into an e-mail address and the rest of the data: a list of fields that belong in this layout. The script then makes sure that the e-mail address exists in the user database. If the address doesn't exist, something has gone wrong, or someone is trying to fool the script.
You don't really need to check the password in this case, because the only way to get to this script is from scripts that check the password. A user could easily type some garbage to see what kind of result he would get. But because this data isn't very sensitive and the user can easily edit out any offending data, I didn't feel that the extra protection was necessary. By now, you probably can figure out how to add this protection on your own anyway.
After the script determines that the user exists, it makes a call to an outside program, savegraph (remember the limit of one dbmopen() call per script) to actually save the data. Listing 4.19 shows savegraph.
Listing 4.19 Script to Save User Page Data to a DBM File (SAVEGRAPH.PL)
#!/usr/bin/perl $email=$ARGV[0]; $data=$ARGV[1]; $id=$ARGV[2]; dbmopen (%graphs, savedgraphs, 0666); if ($id ne '') { if ($data ne 'NULL'){ $graphs{$email}=~s/\!\![\w,]+::$id/!!${data}::$id/; } else { $graphs{$email}=~s/\!\![\w,]+::$id//; } } else { $temp=$graphs{$email}; $temp=~/(\d+)$/; if ($1 ne '') { $id=$1+1; } else { $id=1; } $graphs{$email} .= "!!${data}::$id" }
Like its big brother in Listing 4.18, savegraph serves two purposes; it can deal with new data or edited data. The first thing that this script does is store the command-line arguments in $email, $data, and $id. The conditional if ($id eq '')... looks to see whether the script was called with a layout ID number on the command line. If not, the data that the script is given is for a new layout, and that processing takes place in the else section of the conditional.
This block begins by getting the current data for the given e-mail address from the DBM file savedgraphs, which was opened at the top of the script. The script then executes the command $temp=~/(\d+)$/;. This line of code places the number from the end of the data that the script just retrieved in the special variable $1. The script then checks for a number. If the script finds no number, this layout is the first saved layout, so the ID is set to 1. If the script does find a number, it adds 1 to that number to get an ID for the new saved layout. The script then appends the new data-with appropriate delimiters-to the data in the DBM file and exits, returning to savedataset.
As you can see in Listing 4.18, only one command is left in the parent script: a call to the &print_header subroutine, with a location to direct the browser. The script at this location finally prints the form that you see in figure 4.7. The script, showsaveddatasets, is available on the CD-ROM that comes with this book (SHOWSAVEDDATASETS.PL). This script is a simple piece of code that you should be able to decipher without difficulty.
Viewing and Editing the Saved Layouts With the data now safely stored, you need to act on the user's actions on the form shown in figure 4.7. Listing 4.20 shows the initial script that acts on the submission of this form.
Listing 4.20 Script to Edit or View Selected Saved Layouts (EDITSAVEDLAYOUT.PL)
#!/usr/bin/perl require "process_cgi.pl"; require "check_pass.pl"; &parse_input(*fields); &print_header; if (&check_pass($fields{'email'},$fields{'pass'})) { open (data, "getlayoutdata $fields{'email'} $fields{'layout'} |"); $data=<data>; if ($fields{'action'} eq 'edit') { open (out, "editlayout $fields{'email'} $fields{'layout'} $data |"); @out=<out>; print @out; } else { open (out, "printcustomdata $fields{'email'} $data |"); @out=<out>; print @out; } }
The basic format of the editsavedlayout script should be quite familiar to you by now. The script starts by requiring the CGI library and the password-checking script. The script then reads the input from the form, prints the header, and checks the password, just like many of the scripts in this chapter. When the password is confirmed, the script uses yet another support script (getlayoutdata, on the CD-ROM) to get the data for the chosen layout. If the user chose the Edit radio button, the script calls a new script called editlayout.
Listing 4.21 shows a partial listing of this script. Notice that the script again uses the eval function to create the variables that check the appropriate boxes on the form. Also, the script uses the print << syntax to ease the output of the on-the-fly form. The form that results from the submission of this form is almost identical to the original layout selection form shown in figure 4.2. The only differences are that the e-mail address is in plain text and that this version points to a different supporting script. Also, the layout number and the e-mail address are stored in hidden fields.
Listing 4.21 A Partial Listing of editlayout (EDITLAYOUT.PL)
#!/usr/bin/perl $email=$ARGV[0]; $number=$ARGV[1]; @data=split(/,/,$ARGV[2]); foreach (@data) { eval ("\$${_}checked='CHECKED';") } print <<EOF; <title>Data Display Choices</title>
When the user makes the desired changes to this layout, he can submit the form. The data is sent to the savedataset script (refer to Listing 4.18). I explained earlier how this script works with new data (and the PATH_INFO variable). With the edited data from this form submission, the information comes in through the normal POST-method channels, and the script accesses it through the &parse_input routine.
When the data is in the %fields hash, the script finds and saves the e-mail, password, and layout ID information. The script then uses a function that you haven't seen before-delete()-to delete those pieces from the %fields array. delete() does exactly what its name implies; it removes the indicated element from a hash. This function is useful in this case because you want to build your $data string from the remaining elements of the array. You use a foreach loop for this purpose, iterating over the keys of $fields. When $data is built, the script removes the extra comma from the end and then calls the savegraph script (refer to Listing 4.19).
Because the script calls savegraph with three command-line arguments this time, it knows that you are editing an existing layout. In most cases, the script performs a straight substitution, using the s/// operator. Notice, however, that if the $data string is NULL, the script actually deletes the layout. If you refer to Listing 4.18, you'll notice that $data is set to NULL if it is empty at the end of the foreach loop that builds the string. This syntax provides a simple means of deleting unwanted layouts from the data file.
Why do I explicitly set $data to NULL instead of just allowing it to be empty? The problem is that if $data is empty, the script ends up calling savegraph with only two command-line arguments. The program would think that the ID is the data; it would see no ID, so it would create a new layout in the data file with bogus data.
When the processing of the edited data is complete, the program sends the user back to the listing of saved layouts.
If the user chose the View radio button in figure 4.7, the script calls printcustomdata, with the e-mail address and layout data as command-line arguments. The original version of this script did not account for dealing with command-line arguments, so I had to modify it slightly from what you saw in "Returning Data to a Form for Further Revision" earlier in this chapter. Listing 4.22 shows the most significant change.
Listing 4.22 A Partial Listing of the New Version of printcustomdata
if (!(&parse_input(*fields))){ $fields{'email'}=$ARGV[0]; @layout=split(/,/,$ARGV[1]); foreach(@layout){ $fields{$_}='y'; } $pass="good"; $direct='y'; } else { &print_header; if (&check_pass($fields{'email'},$fields{'pass'})) { $pass="good"; } } $j=0; if ($pass eq 'good') {
As you can see in the listing, the modification was fairly simple. If &parse_input returned false, I knew that the script had been called directly. With that fact in mind, I put all the data in the appropriate places, building the %fields hash that would have come from &parse_input. Again, notice that careful form design and variable naming throughout this application helped immensely.
When %fields is built properly, the script sets two new variables. The first-$pass-substitutes for checking the password. Because the user has already authenticated to get this far, you needn't check again. The second variable- $direct-is used to prevent the Save this layout line from being printed at the end of the script. This option isn't necessary, and it would potentially cause confusion.
The else section of the conditional in Listing 4.22 performs password checking like the original version of the script and sets pass to good, if appropriate. The if ($pass eq 'good') { conditional replaces the password-checking line that you see at the top of Listing 4.5. If the password didn't pass muster, the script essentially stops at that point.
In the preceding two chapters, you got a detailed view of a relatively complex, user-driven Web application. Although you may not need a Web survey application, the concepts are applicable to a wide variety of programming chores. A good example is a companywide schedule book. Users would need to enter and edit data, and they would want to be able to view that data as flexibly as possible.
Now you're ready to find some new concepts to add to this knowledge. Following are some suggestions for further reading: