[LON-CAPA-cvs] cvs: loncom /interface lonchart.pm

stredwic lon-capa-cvs@mail.lon-capa.org
Mon, 08 Jul 2002 13:38:52 -0000


This is a MIME encoded message

--stredwic1026135532
Content-Type: text/plain

stredwic		Mon Jul  8 09:38:52 2002 EDT

  Modified files:              
    /loncom/interface	lonchart.pm 
  Log:
  When pressing the chrt button on the remote is supposed to refresh
  the chart with the current options selected.  This works, but there
  was a problem if the cache database didn't exist for the course.  Now
  that is fixed so that it does both a recalculate chart and a 
  reset selections.  Entering a chart for a course after the cache data
  has expired will display correctly.
  
  Add pod documentation for the all the functions but three.
  
  
--stredwic1026135532
Content-Type: text/plain
Content-Disposition: attachment; filename="stredwic-20020708093852.txt"

Index: loncom/interface/lonchart.pm
diff -u loncom/interface/lonchart.pm:1.54 loncom/interface/lonchart.pm:1.55
--- loncom/interface/lonchart.pm:1.54	Wed Jul  3 10:11:14 2002
+++ loncom/interface/lonchart.pm	Mon Jul  8 09:38:52 2002
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # (Publication Handler
 #
-# $Id: lonchart.pm,v 1.54 2002/07/03 14:11:14 stredwic Exp $
+# $Id: lonchart.pm,v 1.55 2002/07/08 13:38:52 stredwic Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -48,6 +48,55 @@
 
 =pod
 
+=head1 NAME
+
+lonchart
+
+=head1 SYNOPSIS
+
+Quick display of students grades for a course in a compressed table format.
+
+=head1 DESCRIPTION
+
+This module process all student grades for a course and turns them into a 
+table like structure.
+
+This is part of the LearningOnline Network with CAPA project
+described at http://www.lon-capa.org
+
+lonchart presents the user with a condensed view all a course's data.  The
+class title, the number of students, and the date for the last update of the
+displayed data.  There is also a legend that describes the chart values.  
+
+For each valid grade for a student is linked with a submission record for that
+problem.  The ability to add and remove columns of data from the chart was
+added for reducing the burden of having to scroll through large quantities
+of data.  The interface also allows for sorting of students by username,
+last name, and section number of class.  Active and expired students are
+also available.
+
+The interface is controlled by three primary buttons: Recalculate Chart, 
+Refresh Chart, and Reset Selections.  Recalculate Chart will update 
+the chart to the most recent data and keep the display settings for the chart
+the same.  Refresh Chart is used to redisplay the chart after selecting
+different output formatting.  Reset Selections is used to set the chart
+display options back to default values.
+
+=head1 CODE LAYOUT DESCRIPTION
+
+The code is broken down into five components: formatting data for printing,
+downloading data from servers, processing data, helper functions,
+and the central processing functions.  The module is broken into chunks
+for each component.
+
+=head1 PACKAGES USED
+
+ Apache::Constants qw(:common :http)
+ Apache::lonnet()
+ Apache::loncommon()
+ HTML::TokeParser
+ GDBM_File
+
 =cut
 
 package Apache::lonchart;
@@ -60,8 +109,44 @@
 use GDBM_File;
 
 #my $jr; 
+
+=pod
+
+=head1 FORMAT DATA FOR PRINTING
+
+=cut
+
 # ----- FORMAT PRINT DATA ----------------------------------------------
 
+=pod
+
+=item &FormatStudentInformation()
+
+This function produces a formatted string of the student's information:
+username, domain, section, full name, and PID.
+
+=over 4
+
+Input: $cache, $name, $studentInformation, $spacePadding
+
+$cache: This is a pointer to a hash that is tied to the cached data
+
+$name:  The name and domain of the current student in name:domain format
+
+$studentInformation: A pointer to an array holding the names used to
+
+remove data from the hash.  They represent the name of the data to be removed.
+
+$spacePadding: Extra spaces that represent the space between columns
+
+Output: $Str
+
+$Str: Formatted string.
+
+=back
+
+=cut
+
 sub FormatStudentInformation {
     my ($cache,$name,$studentInformation,$spacePadding)=@_;
     my $Str='';
@@ -83,8 +168,42 @@
     return $Str;
 }
 
+=pod
+
+=item &FormatStudentData()
+
+First, FormatStudentInformation is called and prefixes the course information.
+This function produces a formatted string of the student's course information.
+Each column of data represents all the problems for a given sequence.  For
+valid grade data, a link is created for that problem to a submission record
+for that problem.
+
+=over 4
+
+Input: $name, $studentInformation, $spacePadding, $ChartDB
+
+$name: The name and domain of the current student in name:domain format
+
+$studentInformation: A pointer to an array holding the names used to 
+remove data from the hash.  They represent 
+the name of the data to be removed.
+
+$spacePadding: Extra spaces that represent the space between columns
+
+$ChartDB: The name of the cached data database which will be tied to that 
+database.
+
+Output: $Str
+
+$Str: Formatted string that is an entire row of the chart.  It is a 
+concatenation of student information and student course information.
+
+=back
+
+=cut
+
 sub FormatStudentData {
-    my ($name,$coid,$studentInformation,$spacePadding,$ChartDB)=@_;
+    my ($name,$studentInformation,$spacePadding,$ChartDB)=@_;
     my ($sname,$sdom) = split(/\:/,$name);
     my $Str;
     my %CacheData;
@@ -221,6 +340,33 @@
     return $Str;
 }
 
+=pod
+
+=item &CreateTableHeadings()
+
+This function generates the column headings for the chart.
+
+=over 4
+
+Inputs: $CacheData, $studentInformation, $headings, $spacePadding
+
+$CacheData: pointer to a hash tied to the cached data database
+
+$studentInformation: a pointer to an array containing the names of the data 
+held in a column and is used as part of a key into $CacheData
+
+$headings: The names of the headings for the student information
+
+$spacePadding: The spaces to go between columns
+
+Output: $Str
+
+$Str: A formatted string of the table column headings.
+
+=back
+
+=cut
+
 sub CreateTableHeadings {
     my ($CacheData,$studentInformation,$headings,$spacePadding)=@_;
     my $Str='<tr>';
@@ -259,13 +405,42 @@
     }
 
     $Str .= '<td><pre>Total Solved/Total Problems</pre></td>';
-    $Str .= '</tr></tbody></table>';
+    $Str .= '</tr>';
 
     return $Str;
 }
 
+=pod
+
+=item &CreateColumnSelectionBox()
+
+If there are columns not being displayed then this selection box is created
+with a list of those columns.  When selections are made and the page 
+refreshed, the columns will be removed from this box and the column is
+put back in the chart.  If there is no columns to select, no row is added
+to the interface table.
+
+=over 4
+Input: $CacheData, $headings
+
+
+$CacheData: A pointer to a hash tied to the cached data
+
+$headings:  An array of the names of the columns for the student information.  
+They are used for displaying which columns are missing.
+
+Output: $notThere
+
+$notThere: The string contains one row of a table.  The first column has the 
+name of the selection box.  The second contains the selection box 
+which has a size of four.
+
+=back
+
+=cut
+
 sub CreateColumnSelectionBox {
-    my ($CacheData,$studentInformation,$headings,$spacePadding)=@_;
+    my ($CacheData,$headings)=@_;
 
     my $missing=0;
     my $notThere='<tr><td align="right"><b>Select column to view:</b>';
@@ -299,20 +474,42 @@
         $notThere='<tr><td>';
     }
 
-    return $notThere.'</td></tr></tbody></table>';
+    return $notThere.'</td></tr>';
 }
 
+=pod
+
+=item &CreateColumnSelectors()
+
+This function generates the checkboxes above the column headings.  The 
+column will be removed if the checkbox is unchecked.
+
+=over 4
+
+Input: $CacheData, $headings
+
+$CacheData: A pointer to a hash tied to the cached data
+
+$headings:  An array of the names of the columns for the student 
+information.  They are used to know what are the student information columns
+
+Output: $present
+
+$present: The string contains the first row of a table.  Each column contains
+a checkbox which is left justified.  Currently left justification is used
+for consistency of location over the column in which it presides.
+
+=back
+
+=cut
+
 sub CreateColumnSelectors {
-    my ($CacheData,$studentInformation,$headings,$spacePadding)=@_;
+    my ($CacheData,$headings)=@_;
 
     my $found=0;
     my ($name, $length, $position);
 
-    my $present='<pre><b>Note: Uncheck the boxes above a column to ';
-    $present .= 'remove that column from the display.</b></pre>'."\n";
-
-    $present .= '<table border="0" cellpadding="0" cellspacing="0">';
-    $present .= '<tbody><tr>';
+    my $present = '<tr>';
     for(my $index=0; $index<(scalar @$headings); $index++) {
         if(!&ShouldShowColumn($CacheData, 'heading'.$index)) {
             next;
@@ -342,6 +539,30 @@
     return $present.'<td></td></tr></form>'."\n";;
 }
 
+=pod
+
+=item &CreateForm()
+
+The interface for this module consists primarily of the controls in this
+function.  The student status selection (active, expired, any) is set here.
+The sort buttons: username, last name, and section are set here.  The
+other buttons are Recalculate Chart, Refresh Chart, and Reset Selections.
+These controls are in a table to clean up the interface.
+
+=over 4
+
+Input: $CacheData
+
+$CacheData is a hash pointer to tied database for cached data.
+
+Output: $Ptr
+
+$Ptr is a string containing all the html for the above mentioned buttons.
+
+=back
+
+=cut
+
 sub CreateForm {
     my ($CacheData)=@_;
     my $OpSel1='';
@@ -353,7 +574,6 @@
     else { $OpSel1 = 'selected'; }
 
     my $Ptr .= '<form name="stat" method="post" action="/adm/chart" >'."\n";
-    $Ptr .= '<table border="0"><tbody>';
     $Ptr .= '<tr><td align="right">';
     $Ptr .= '</td><td align="left">';
     $Ptr .= '<input type="submit" name="recalculate" ';
@@ -384,6 +604,16 @@
     return $Ptr;
 }
 
+=pod
+
+=item &CreateLegend()
+
+This function returns a formatted string containing the legend for the
+chart.  The legend describes the symbols used to represent grades for
+problems.
+
+=cut
+
 sub CreateLegend {
     my $Str = "<p><pre>".
               "1..9: correct by student in 1..9 tries\n".
@@ -398,6 +628,15 @@
     return $Str;
 }
 
+=pod
+
+=item &StartDocument()
+
+Returns a string containing the header information for the chart: title,
+logo, and course title.
+
+=cut
+
 sub StartDocument {
     my $Str = '';
     $Str .= '<html>';
@@ -415,8 +654,53 @@
 
 # ----- END FORMAT PRINT DATA ------------------------------------------
 
+=pod
+
+=head1 DOWNLOAD INFORMATION
+
+This section contains all the files that get data from other servers 
+and/or itself.  There is one function that has a call to get remote
+information but isn't included here which is ProcessTopLevelMap.  The
+usage was small enough to be ignored, but that portion may be moved
+here in the future.
+
+=cut
+
 # ----- DOWNLOAD INFORMATION -------------------------------------------
 
+=pod
+
+=item &DownloadPrerequisiteData()
+
+Collects lastname, generation, middlename, firstname PID, and section for each
+student from their environment database.  The list of students is built from
+collecting a classlist for the course that is to be displayed.
+
+=over 4
+
+Input: $courseID, $c
+
+$courseID:  The id of the course
+
+$c: The connection class that can determine if the browser has aborted.  It
+is used to short circuit this function so that it doesn't continue to 
+get information when there is no need.
+
+Output: \%classlist
+
+\%classlist: A pointer to a hash containing the following data:
+
+-A list of student name:domain (as keys) (known below as $name)
+
+-A hash pointer for each student containing lastname, generation, firstname,
+middlename, and PID : Key is $name.'studentInformation'
+
+-A hash pointer to each students section data : Key is $name.section
+
+=back
+
+=cut
+
 sub DownloadPrerequisiteData {
     my ($courseID, $c)=@_;
     my ($courseDomain,$courseNumber)=split(/\_/,$courseID);
@@ -457,6 +741,30 @@
     return \%classlist;
 }
 
+=pod
+
+=item &DownloadStudentCourseInformation()
+
+Dump of all the course information for a single student.  There is no
+pruning of data, it is all stored in a hash and returned.
+
+=over 4
+
+Input: $name, $courseID
+
+$name: student name:domain
+
+$courseID:  The id of the course
+
+Output: \%courseData
+
+\%courseData:  A hash pointer to the raw data from the student's course
+database.
+
+=back
+
+=cut
+
 sub DownloadStudentCourseInformation {
     my ($name,$courseID)=@_;
     my ($studentName,$studentDomain) = split(/\:/,$name);
@@ -469,7 +777,23 @@
 
 # ----- END DOWNLOAD INFORMATION ---------------------------------------
 
-# ----- END PROCESSING FUNCTIONS ---------------------------------------
+=pod
+
+=head1 PROCESSING FUNCTIONS
+
+These functions process all the data for all the students.  Also, they
+are the only functions that access the cache database for writing.  Thus
+they are the only functions that cache data.  The downloading and caching
+were separated to reduce problems with stopping downloading then can't
+tie hash to database later.
+
+=cut
+
+# ----- PROCESSING FUNCTIONS ---------------------------------------
+
+=pod
+
+=cut
 
 sub ProcessTopResourceMap {
     my ($ChartDB,$c)=@_;
@@ -776,9 +1100,147 @@
     return @names;
 }
 
-# ----- END PROCESSING FUNCTIONS ---------------------------------------
+sub ProcessStudentData {
+    my ($courseData, $name, $ChartDB)=@_;
 
-# ----- HELPER FUNCTIONS -----------------------------------------------
+    my %CacheData;
+    if(tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_WRCREAT,0640)) {
+        my ($checkForError) = keys(%$courseData);
+        if($checkForError =~ /^(con_lost|error|no_such_host)/i) {
+            $CacheData{$name.':error'}='Could not download course data.';
+        } else {
+            foreach my $key (keys (%$courseData)) {
+                $CacheData{$name.':'.$key}=$courseData->{$key};
+            }
+            if(defined($CacheData{'NamesOfStudents'})) {
+                $CacheData{'NamesOfStudents'}.=':::'.$name;
+            } else {
+                $CacheData{'NamesOfStudents'}=$name;
+            }
+        }
+        untie(%CacheData);
+    }
+
+    return;
+}
+
+=pod
+
+=item &ProcessFormData()
+
+Cache form data and set default form data (sort, status, heading.$number,
+sequence.$number, reselect, reset, recalculate, and refresh)
+
+=over 4
+
+Input: $ChartDB, $isCached
+
+$ChartDB: The name of the database for cached data
+
+$isCached: Is there already data for this course cached.  This is used in 
+conjunction with the absence of all form data to know to display all selection 
+types.
+
+Output: None
+
+=back
+
+=cut
+
+sub ProcessFormData {
+    my ($ChartDB, $isCached)=@_;
+    my %CacheData;
+
+    if(tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_WRCREAT,0640)) {
+        if(defined($ENV{'form.sort'})) {
+            $CacheData{'form.sort'}=$ENV{'form.sort'};
+        } elsif(!defined($CacheData{'form.sort'})) {
+            $CacheData{'form.sort'}='username';
+        }
+
+        # Ignore $ENV{'form.refresh'}
+        # Ignore $ENV{'form.recalculate'}
+
+        if(defined($ENV{'form.status'})) {
+            $CacheData{'form.status'}=$ENV{'form.status'};
+        } elsif(!defined($CacheData{'form.status'})) {
+            $CacheData{'form.status'}='Active';
+        }
+
+        my @headings=();
+        my @sequences=();
+        my $found=0;
+        foreach (keys(%ENV)) {
+            if(/form\.heading/) {
+                $found++;
+                push(@headings, $_);
+            } elsif(/form\.sequence/) {
+                $found++;
+                push(@sequences, $_);
+            } elsif(/form\./) {
+                $found++;
+            }
+        }
+
+        if($found) {
+            $CacheData{'form.headings'}=join(":::",@headings);
+            $CacheData{'form.sequences'}=join(":::",@sequences);
+        }
+
+        if(defined($ENV{'form.reselect'})) {
+            my @reselected = (ref($ENV{'form.reselect'}) ? 
+                              @{$ENV{'form.reselect'}}
+                              : ($ENV{'form.reselect'}));
+            foreach (@reselected) {
+                if(/heading/) {
+                    $CacheData{'form.headings'}.=":::".$_;
+                } elsif(/sequence/) {
+                    $CacheData{'form.sequences'}.=":::".$_;
+                }
+            }
+        }
+
+        if(defined($ENV{'form.reset'}) || (!$found && !$isCached)) {
+            $CacheData{'form.reset'}='true';
+            $CacheData{'form.status'}='Active';
+            $CacheData{'form.sort'}='username';
+            $CacheData{'form.headings'}='ALLHEADINGS';
+            $CacheData{'form.sequences'}='ALLSEQUENCES';
+        } else {
+            $CacheData{'form.reset'}='false';
+        }
+
+        untie(%CacheData);
+    }
+
+    return;
+}
+
+=pod
+
+=item &SpaceColumns()
+
+Determines the width of all the columns in the chart.  It is based on
+the max of the data for that column and its header.
+
+=over 4
+
+Input: $students, $studentInformation, $headings, $ChartDB
+
+$students: An array pointer to a list of students (username:domain)
+
+$studentInformatin: The type of data for the student information.  It is
+used as part of the key in $CacheData.
+
+$headings: The name of the student information columns.
+
+$ChartDB: The name of the cache database which is opened for read/write.
+
+Output: None - All data stored in cache.
+
+=back
+
+=cut
 
 sub SpaceColumns {
     my ($students,$studentInformation,$headings,$ChartDB)=@_;
@@ -807,6 +1269,28 @@
     return;
 }
 
+# ----- END PROCESSING FUNCTIONS ---------------------------------------
+
+=pod
+
+=head1 HELPER FUNCTIONS
+
+These are just a couple of functions do various odd and end 
+jobs.
+
+=cut
+
+# ----- HELPER FUNCTIONS -----------------------------------------------
+
+=pod
+
+=item &ProcessFullName()
+
+Takes lastname, generation, firstname, and middlename (or some partial
+set of this data) and returns the full name version as a string.
+
+=cut
+
 sub ProcessFullName {
     my ($lastname, $generation, $firstname, $middlename)=@_;
     my $Str = '';
@@ -847,6 +1331,31 @@
     return $Str;
 }
 
+=pod
+
+=item &SortStudents()
+
+Determines which students to display and in which order.  Which are 
+displayed are determined by their status(active/expired).  The order
+is determined by the sort button pressed (default to username).  The
+type of sorting is username, lastname, or section.
+
+=over 4
+
+Input: $students, $CacheData
+
+$students: A array pointer to a list of students (username:domain)
+
+$CacheData: A pointer to the hash tied to the cached data
+
+Output: @order
+
+@order: An ordered list of students (username:domain)
+
+=back
+
+=cut
+
 sub SortStudents {
     my ($students,$CacheData)=@_;
 
@@ -895,6 +1404,32 @@
     return @order;
 }
 
+=pod
+
+=item &TestCacheData()
+
+Determine if the cache database can be accessed with a tie.  It waits up to
+ten seconds before returning failure.  This function exists to help with
+the problems with stopping the data download.  When an abort occurs and the
+user quickly presses a form button and httpd child is created.  This
+child needs to wait for the other to finish (hopefully within ten seconds).
+
+=over 4
+
+Input: $ChartDB
+
+$ChartDB: The name of the cache database to be opened
+
+Output: -1, 0, 1
+
+-1: Couldn't tie database
+ 0: Use cached data
+ 1: New cache database created, use that.
+
+=back
+
+=cut
+
 sub TestCacheData {
     my ($ChartDB)=@_;
     my $isCached=-1;
@@ -929,29 +1464,26 @@
     return $isCached;
 }
 
-sub ExtractStudentData {
-    my ($courseData, $name, $ChartDB)=@_;
+=pod
 
-    my %CacheData;
-    if(tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_WRCREAT,0640)) {
-        my ($checkForError) = keys(%$courseData);
-        if($checkForError =~ /^(con_lost|error|no_such_host)/i) {
-            $CacheData{$name.':error'}='Could not download course data.';
-        } else {
-            foreach my $key (keys (%$courseData)) {
-                $CacheData{$name.':'.$key}=$courseData->{$key};
-            }
-            if(defined($CacheData{'NamesOfStudents'})) {
-                $CacheData{'NamesOfStudents'}.=':::'.$name;
-            } else {
-                $CacheData{'NamesOfStudents'}=$name;
-            }
-        }
-        untie(%CacheData);
-    }
+=item &ShouldShowColumn()
 
-    return;
-}
+Determine if a specified column should be shown on the chart.
+
+=over 4
+
+Input: $cache, $test
+
+$cache: A pointer to the hash tied to the cached data
+
+$test: The form name of the column (heading.$headingIndex) or 
+(sequence.$sequenceIndex)
+
+Output: 0 (false), 1 (true)
+
+=back
+
+=cut
 
 sub ShouldShowColumn {
     my ($cache,$test)=@_;
@@ -967,84 +1499,49 @@
         return 1;
     }
 
-#    my $reselected=$cache->{'form.reselect'};
-#    if($reselected=~/$test/) {
-#        return 1;
-#    }
-
     return 0;
 }
 
-sub ProcessFormData {
-    my ($ChartDB)=@_;
-    my %CacheData;
+# ----- END HELPER FUNCTIONS --------------------------------------------
 
-    if(tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_WRCREAT,0640)) {
-        if(defined($ENV{'form.sort'})) {
-            $CacheData{'form.sort'}=$ENV{'form.sort'};
-        } elsif(!defined($CacheData{'form.sort'})) {
-            $CacheData{'form.sort'}='username';
-        }
+=pod
 
-        # Ignore $ENV{'form.refresh'}
-        # Ignore $ENV{'form.recalculate'}
+=head1 Handler and main function(BuildChart)
 
-        if(defined($ENV{'form.status'})) {
-            $CacheData{'form.status'}=$ENV{'form.status'};
-        } elsif(!defined($CacheData{'form.status'})) {
-            $CacheData{'form.status'}='Active';
-        }
+The handler does some initial error checking and then passes the torch to
+BuildChart.  BuildChart calls all the appropriate functions to get the
+job done.  These are the only two functions that use print ($r).  All other
+functions return strings to BuildChart to be printed.
 
-        my @headings=();
-        my @sequences=();
-        my $found=0;
-        foreach (keys(%ENV)) {
-            if(/form\.heading/) {
-                $found++;
-                push(@headings, $_);
-            } elsif(/form\.sequence/) {
-                $found++;
-                push(@sequences, $_);
-            } elsif(/form\./) {
-                $found++;
-            }
-        }
+=cut
 
-        if($found) {
-            $CacheData{'form.headings'}=join(":::",@headings);
-            $CacheData{'form.sequences'}=join(":::",@sequences);
-        }
+=pod
 
-        if(defined($ENV{'form.reselect'})) {
-            my @reselected = (ref($ENV{'form.reselect'}) ? 
-                              @{$ENV{'form.reselect'}}
-                              : ($ENV{'form.reselect'}));
-            foreach (@reselected) {
-                if(/heading/) {
-                    $CacheData{'form.headings'}.=":::".$_;
-                } elsif(/sequence/) {
-                    $CacheData{'form.sequences'}.=":::".$_;
-                }
-            }
-        }
+=item &BuildChart()
 
-        if(defined($ENV{'form.reset'})) {
-            $CacheData{'form.reset'}='true';
-            $CacheData{'form.status'}='Active';
-            $CacheData{'form.sort'}='username';
-            $CacheData{'form.headings'}='ALLHEADINGS';
-            $CacheData{'form.sequences'}='ALLSEQUENCES';
-        } else {
-            $CacheData{'form.reset'}='false';
-        }
+ The following is the process that BuildChart goes through to create the
+  html document.
 
-        untie(%CacheData);
-    }
+ -Start the lonchart document
+ -Test for access to the CacheData
+ -Download class list information if not using cached data 
+ -Sort students and print out table desciptive data
+ -Output student data
+ -If recalculating, store a list of students, but only if all their data was 
+  downloaded.  Leave off the others.
+ -End document
 
-    return;
-}
+=over 4
 
-# ----- END HELPER FUNCTIONS --------------------------------------------
+Input: $r
+
+$r:  Used to print html
+
+Output: None
+
+=back
+
+=cut
 
 sub BuildChart {
     my ($r)=@_;
@@ -1068,7 +1565,7 @@
         $r->rflush();
         return;
     }
-    &ProcessFormData($ChartDB);
+    &ProcessFormData($ChartDB, $isCached);
 
     # Download class list information if not using cached data
     my %CacheData;
@@ -1103,9 +1600,9 @@
     }
 
     # Sort students and print out table desciptive data
+    my $downloadTime=0;
     if(tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_READER,0640)) {
         if(!$c->aborted()) { @students=&SortStudents(\@students,\%CacheData); }
-        my $downloadTime=0;
         if(defined($CacheData{'time'})) { $downloadTime=$CacheData{'time'}; }
         else { $downloadTime=localtime(); }
         if(!$c->aborted()) { $r->print('<h3>'.$downloadTime.'</h3>'); }
@@ -1113,22 +1610,26 @@
                                        ' students</h1>'); }
 	if(!$c->aborted()) { $r->rflush(); }
 	if(!$c->aborted()) { $r->print(&CreateLegend()); }
+        if(!$c->aborted()) { $r->print('<table border="0"><tbody>'); }
 	if(!$c->aborted()) { $r->print(&CreateForm(\%CacheData)); }
 	if(!$c->aborted()) { $r->print(&CreateColumnSelectionBox(
                                                        \%CacheData,
-                                                       \@studentInformation, 
-						       \@headings, 
-                                                       $spacePadding)); }
+                                                       \@headings)); }
+        if(!$c->aborted()) { $r->print('</tbody></table>'); }
+        if(!$c->aborted()) { $r->print('<b>Note: Uncheck the boxes above a'); }
+        if(!$c->aborted()) { $r->print(' column to remove that column from'); }
+        if(!$c->aborted()) { $r->print(' the display.</b></pre>'); }
+        if(!$c->aborted()) { $r->print('<table border="0" cellpadding="0" '); }
+        if(!$c->aborted()) { $r->print('cellspacing="0"><tbody>'); }
 	if(!$c->aborted()) { $r->print(&CreateColumnSelectors(
                                                        \%CacheData,
-                                                       \@studentInformation, 
-						       \@headings, 
-                                                       $spacePadding)); }
+                                                       \@headings)); }
 	if(!$c->aborted()) { $r->print(&CreateTableHeadings(
                                                          \%CacheData,
                                                          \@studentInformation, 
 							 \@headings, 
 							 $spacePadding)); }
+        if(!$c->aborted()) { $r->print('</tbody></table>'); }
 	if(!$c->aborted()) { $r->rflush(); }
 	untie(%CacheData);
     } else {
@@ -1136,6 +1637,7 @@
 	return;
     }
 
+    # Output student data
     my @updateStudentList = ();
     my $courseData;
     $r->print('<pre>');
@@ -1148,13 +1650,15 @@
             $courseData=&DownloadStudentCourseInformation($_, $cid);
             if($c->aborted()) { last; }
             push(@updateStudentList, $_);
-            &ExtractStudentData($courseData, $_, $ChartDB);
+            &ProcessStudentData($courseData, $_, $ChartDB);
         }
-        $r->print(&FormatStudentData($_, $cid, \@studentInformation,
+        $r->print(&FormatStudentData($_, \@studentInformation,
                                      $spacePadding, $ChartDB));
         $r->rflush();
     }
 
+    # If recalculating, store a list of students, but only if all their 
+    # data was downloaded.  Leave off the others.
     if(!$isCached && tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_WRCREAT,0640)) {
         $CacheData{'NamesOfStudents'}=join(":::", @updateStudentList);
 #		    $CacheData{'NamesOfStudents'}=
@@ -1162,6 +1666,7 @@
         untie(%CacheData);
     }
 
+    # End document
     $r->print('</pre></body></html>');
     $r->rflush();
 
@@ -1169,6 +1674,26 @@
 }
 
 # ================================================================ Main Handler
+
+=pod
+
+=item &handler()
+
+The handler checks for permission to access the course data and for 
+initial header problem.  Then it passes the torch to the work horse
+function BuildChart.
+
+=over 4
+
+Input: $r
+
+$r: This is the object that is used to print.
+
+Output: A Value (OK or HTTP_NOT_ACCEPTABLE)
+
+=back
+
+=cut
 
 sub handler {
     my $r=shift;

--stredwic1026135532--