--- loncom/interface/Attic/lonchart.pm 2002/07/03 14:11:14 1.54 +++ loncom/interface/Attic/lonchart.pm 2002/07/08 16:50:03 1.58 @@ -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.58 2002/07/08 16:50:03 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 HTML::TokeParser; 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 @@ sub FormatStudentInformation { 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; @@ -126,6 +245,8 @@ sub FormatStudentData { my $problem = $CacheData{$problemID.':problem'}; my $LatestVersion = $CacheData{$name.":version:$problem"}; + # Output blanks for all the parts of this problem if there + # is no version information about the current problem. if(!$LatestVersion) { foreach my $part (split(/\:/,$CacheData{$sequence.':'. $problemID. @@ -138,13 +259,19 @@ sub FormatStudentData { } my %partData=undef; - #initialize data, displays skips correctly + # Initialize part data, display skips correctly + # Skip refers to when a student made no submissions on that + # part/problem. foreach my $part (split(/\:/,$CacheData{$sequence.':'. $problemID. ':parts'})) { $partData{$part.':tries'}=0; $partData{$part.':code'}=' '; } + + # Looping through all the versions of each part, starting with the + # oldest version. Basically, it gets the most recent + # set of grade data for each part. for(my $Version=1; $Version<=$LatestVersion; $Version++) { foreach my $part (split(/\:/,$CacheData{$sequence.':'. $problemID. @@ -152,6 +279,7 @@ sub FormatStudentData { if(!defined($CacheData{$name.":$Version:$problem". ":resource.$part.solved"})) { + # No grade for this submission, so skip next; } @@ -175,6 +303,9 @@ sub FormatStudentData { } } + # All grades (except for versionless parts) are displayed as links + # to their submission record. Loop through all the parts for the + # current problem in the correct order and prepare the output links $Str.=''; @@ -200,6 +331,9 @@ sub FormatStudentData { $Str.=''; } + # Output the number of correct answers for the current sequence. + # This part takes up 6 character slots, but is formated right + # justified. my $spacesNeeded=$CacheData{$sequence.':columnWidth'}-$characterCount; $spacesNeeded -= 3; $Str .= (' 'x$spacesNeeded); @@ -212,6 +346,9 @@ sub FormatStudentData { $Str .= $spacePadding; } + # Output the total correct problems over the total number of problems. + # I don't like this type of formatting, but it is a solution. Need + # a way to dynamically determine the space requirements. my $outputProblemsSolved = sprintf( "%4d", $problemsSolved ); my $outputTotalProblems = sprintf( "%4d", $totalProblems ); $Str .= ''.$outputProblemsSolved. @@ -221,6 +358,33 @@ sub FormatStudentData { 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=''; @@ -259,13 +423,42 @@ sub CreateTableHeadings { } $Str .= '
Total Solved/Total Problems
'; - $Str .= ''; + $Str .= ''; 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='Select column to view:'; @@ -299,20 +492,42 @@ sub CreateColumnSelectionBox { $notThere=''; } - return $notThere.''; + return $notThere.''; } +=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='
Note: Uncheck the boxes above a column to ';
-    $present .= 'remove that column from the display.
'."\n"; - - $present .= ''; - $present .= ''; + my $present = ''; for(my $index=0; $index<(scalar @$headings); $index++) { if(!&ShouldShowColumn($CacheData, 'heading'.$index)) { next; @@ -342,6 +557,30 @@ sub CreateColumnSelectors { return $present.''."\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 +592,6 @@ sub CreateForm { else { $OpSel1 = 'selected'; } my $Ptr .= ''."\n"; - $Ptr .= '
'; $Ptr .= '
'; $Ptr .= ''; $Ptr .= '{$name.':username'}=$studentName; @@ -742,9 +1174,43 @@ sub ProcessStudentInformation { $CacheData->{$name.':section'}=''; } - return 0; + return; } +=pod + +=item &ProcessClassList() + +Taking the class list dumped from &DownloadPrerequisiteData(), all the +students and their non-class information is processed using the +&ProcessStudentInformation() function. A date stamp is also recorded for +when the data was processed. + +=over 4 + +Input: $classlist, $courseID, $ChartDB, $c + +$classlist: The hash of data collected about a student from +&DownloadPrerequisteData(). The hash contains a list of students, a pointer +to a hash of student information for each student, and each student's section +number. + +$courseID: The course ID + +$ChartDB: The name of the cache database file. + +$c: The connection class used to determine if an abort has been sent to the +browser + +Output: @names + +@names: An array of students whose information has been processed, and are to +be considered in an arbitrary order. + +=back + +=cut + sub ProcessClassList { my ($classlist,$courseID,$ChartDB,$c)=@_; my @names=(); @@ -765,7 +1231,7 @@ sub ProcessClassList { $classlist->{$name.':studentInformation'}, $classlist->{$name.':section'}, $classlist->{$name}, - $name,$courseID,$c); + $name,$courseID); } # Time of download @@ -776,9 +1242,186 @@ sub ProcessClassList { return @names; } -# ----- END PROCESSING FUNCTIONS --------------------------------------- +=pod -# ----- HELPER FUNCTIONS ----------------------------------------------- +=item &ProcessStudentData() + +Takes the course data downloaded for a student in +&DownloadStudentCourseInformation() and breaks it up into key value pairs +to be stored in the cached data. The keys are comprised of the +$username:$domain:$keyFromCourseDatabase. The student username:domain is +stored away signifying that the student's information has been downloaded and +can be reused from cached data. + +=over 4 + +Input: $courseData, $name, $ChartDB + +$courseData: A hash pointer that points to the course data downloaded for a +student. + +$name: username:domain + +$ChartDB: The name of the cache database file which will allow the data to +be written to the cache. + +Output: None + +*NOTE: There is no output, but an error message is stored away in the cache +data. This is checked in &FormatStudentData(). The key username:domain:error +will only exist if an error occured. The error is an error from +&DownloadStudentCourseInformation(). + +=back + +=cut + +sub ProcessStudentData { + my ($courseData, $name, $ChartDB)=@_; + + 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 + +# For all data, if ENV data doesn't exist for it, default values is used. +sub ProcessFormData { + my ($ChartDB, $isCached)=@_; + my %CacheData; + + if(tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_WRCREAT,0640)) { + # Ignore $ENV{'form.refresh'} + # Ignore $ENV{'form.recalculate'} + + if(defined($ENV{'form.sort'})) { + $CacheData{'form.sort'}=$ENV{'form.sort'}; + } elsif(!defined($CacheData{'form.sort'})) { + $CacheData{'form.sort'}='username'; + } + + if(defined($ENV{'form.status'})) { + $CacheData{'form.status'}=$ENV{'form.status'}; + } elsif(!defined($CacheData{'form.status'})) { + $CacheData{'form.status'}='Active'; + } + + # $found checks for any instances of form data in the ENV. If it is + # missing I assume the chrt button on the remote has been pressed. + 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'}.=":::".$_; + } + } + } + + # !$found and !$isCached are how I determine if the chrt button + # on the remote was pressed and needs to reset all the selections + 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 +1450,29 @@ sub SpaceColumns { 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. Format +is Lastname generation, firstname middlename or a subset of this. + +=cut + sub ProcessFullName { my ($lastname, $generation, $firstname, $middlename)=@_; my $Str = ''; @@ -847,6 +1513,31 @@ sub ProcessFullName { 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 +1586,32 @@ sub SortStudents { 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 +1646,26 @@ sub TestCacheData { 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 +1681,49 @@ sub ShouldShowColumn { 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 +1747,7 @@ sub BuildChart { $r->rflush(); return; } - &ProcessFormData($ChartDB); + &ProcessFormData($ChartDB, $isCached); # Download class list information if not using cached data my %CacheData; @@ -1103,9 +1782,9 @@ sub BuildChart { } # 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('

'.$downloadTime.'

'); } @@ -1113,22 +1792,26 @@ sub BuildChart { ' students'); } if(!$c->aborted()) { $r->rflush(); } if(!$c->aborted()) { $r->print(&CreateLegend()); } + if(!$c->aborted()) { $r->print(''); } if(!$c->aborted()) { $r->print(&CreateForm(\%CacheData)); } if(!$c->aborted()) { $r->print(&CreateColumnSelectionBox( \%CacheData, - \@studentInformation, - \@headings, - $spacePadding)); } + \@headings)); } + if(!$c->aborted()) { $r->print('
'); } + if(!$c->aborted()) { $r->print('Note: Uncheck the boxes above a'); } + if(!$c->aborted()) { $r->print(' column to remove that column from'); } + if(!$c->aborted()) { $r->print(' the display.'); } + if(!$c->aborted()) { $r->print('aborted()) { $r->print('cellspacing="0">'); } 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('
'); } if(!$c->aborted()) { $r->rflush(); } untie(%CacheData); } else { @@ -1136,6 +1819,7 @@ sub BuildChart { return; } + # Output student data my @updateStudentList = (); my $courseData; $r->print('
');
@@ -1148,13 +1832,15 @@ sub BuildChart {
             $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 +1848,7 @@ sub BuildChart {
         untie(%CacheData);
     }
 
+    # End document
     $r->print('
'); $r->rflush(); @@ -1170,6 +1857,26 @@ sub BuildChart { # ================================================================ 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; # $jr=$r; @@ -1190,7 +1897,7 @@ sub handler { $r->send_http_header; return OK; } - + unless($ENV{'request.course.fn'}) { my $requrl=$r->uri; $ENV{'user.error.msg'}="$requrl:bre:0:0:Course not initialized";