--- loncom/interface/Attic/lonchart.pm	2001/01/31 23:28:06	1.2
+++ loncom/interface/Attic/lonchart.pm	2002/07/09 15:43:49	1.59
@@ -1,4 +1,30 @@
 # The LearningOnline Network with CAPA
+# (Publication Handler
+#
+# $Id: lonchart.pm,v 1.59 2002/07/09 15:43:49 stredwic Exp $
+#
+# Copyright Michigan State University Board of Trustees
+#
+# This file is part of the LearningOnline Network with CAPA (LON-CAPA).
+#
+# LON-CAPA is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# LON-CAPA is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with LON-CAPA; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+# /home/httpd/html/adm/gpl.txt
+#
+# http://www.lon-capa.org/
+#
 # Homework Performance Chart
 #
 # (Navigate Maps Handler
@@ -6,294 +32,1191 @@
 # (Page Handler
 #
 # (TeX Content Handler
-#
+# YEAR=2000
 # 05/29/00,05/30 Gerd Kortemeyer)
 # 08/30,08/31,09/06,09/14,09/15,09/16,09/19,09/20,09/21,09/23,
 # 10/02,10/10,10/14,10/16,10/18,10/19,10/31,11/6,11/14,11/16 Gerd Kortemeyer)
+# YEAR=2001
+# 3/1/1,6/1,17/1,29/1,30/1,31/1 Gerd Kortemeyer)
+# 7/10/01 Behrouz Minaei
+# 9/8 Gerd Kortemeyer
+# 10/1, 10/19, 11/17, 11/22, 11/24, 11/28 12/18 Behrouz Minaei
+# YEAR=2002
+# 2/1, 2/6, 2/19, 2/28 Behrouz Minaei
 #
-# 3/1/1,6/1,17/1,29/1,30/1 Gerd Kortemeyer)
-#
-# 1/31 Gerd Kortemeyer
+###
+
+=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 three components: formatting data for printing,
+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;
 
 use strict;
 use Apache::Constants qw(:common :http);
 use Apache::lonnet();
+use Apache::loncommon();
+use Apache::loncoursedata();
 use HTML::TokeParser;
 use GDBM_File;
 
-# -------------------------------------------------------------- Module Globals
-my %hash;
-my @cols;
-my @rowlabels;
-my @students;
-
-# ------------------------------------------------------------- Find out status
-
-sub astatus {
-    my ($rid,$student)=@_;
-    my ($uname,$udom)=split(/\:/,$student);
-    my $code=' ';
-    $rid=~/(\d+)\.(\d+)/;
-    my $symb=&Apache::lonnet::declutter($hash{'map_id_'.$1}).'___'.$2.'___'.
-	     &Apache::lonnet::declutter($hash{'src_'.$rid});
-    my $answer=&Apache::lonnet::reply(
-              "restore:$udom:$uname:".
-              $ENV{'request.course.id'}.':'.
-              &Apache::lonnet::escape($symb),
-              &Apache::lonnet::homeserver($uname,$udom));
-    my %returnhash=();
-    map {
-	my ($name,$value)=split(/\=/,$_);
-        $returnhash{&Apache::lonnet::unescape($name)}=
-                    &Apache::lonnet::unescape($value);
-    } split(/\&/,$answer);
-    if ($returnhash{'version'}) {
-       my $version;
-       for ($version=1;$version<=$returnhash{'version'};$version++) {
-          map {
-             $returnhash{$_}=$returnhash{$version.':'.$_};
-          } split(/\:/,$returnhash{$version.':keys'});
-       }
-       my $totaltries=0;
-       map {
-           if (($_=~/\.(\w+)\.solved$/) && ($_!~/^\d+\:/)) {
-               my $part=$1;
-	       if ($returnhash{$_} eq 'correct_by_student') {
-                   unless (($code eq '.') || ($code eq '-')) { $code='*'; }
-                   $totaltries+=$returnhash{'resource.'.$part.'.tries'};
-               } elsif ($returnhash{$_} eq 'correct_by_override') {
-                   unless (($code eq '.') || ($code eq '-')) { $code='+'; }
-               } elsif ($returnhash{$_} eq 'incorrect_attempted') {
-                   $code='.';
-               } elsif ($returnhash{$_} eq 'incorrect_by_override') {
-                   $code='-';
-               } elsif ($returnhash{$_} eq 'excused') {
-                   unless (($code eq '.') || ($code eq '-')) { $code='x'; }
-               }
-           }
-       } keys %returnhash;
-       if (($code eq '*') && ($totaltries<10)) { $code="$totaltries"; }
-    }
-    return $code;
-}
-
-# ------------------------------------------------------------ Build page table
-
-sub tracetable {
-    my ($rid,$beenhere)=@_;
-    unless ($beenhere=~/\&$rid\&/) {
-       $beenhere.=$rid.'&';  
-       if (defined($hash{'is_map_'.$rid})) {
-           if ($hash{'map_type_'.$hash{'map_pc_'.$hash{'src_'.$rid}}} 
-            eq 'sequence') { 
-               $cols[$#cols+1]=0; 
-           }
-           if ((defined($hash{'map_start_'.$hash{'src_'.$rid}})) &&
-               (defined($hash{'map_finish_'.$hash{'src_'.$rid}}))) {
-              my $frid=$hash{'map_finish_'.$hash{'src_'.$rid}};
-
-                &tracetable($hash{'map_start_'.$hash{'src_'.$rid}},
-                '&'.$frid.'&');
-
-              if ($hash{'src_'.$frid}) {
-                 if ($hash{'src_'.$frid}=~
-                                 /\.(problem|exam|quiz|assess|survey|form)$/) {
-		     $cols[$#cols+1]=$frid;
-                 }
-	      }
-
-	   }
-       } else {
-          if ($hash{'src_'.$rid}) {
-             if ($hash{'src_'.$rid}=~
-                                 /\.(problem|exam|quiz|assess|survey|form)$/) {
-	         $cols[$#cols+1]=$rid;
-             }
-          }
-       }
-       if (defined($hash{'to_'.$rid})) {
-          map {
-              &tracetable($hash{'goesto_'.$_},$beenhere);
-          } split(/\,/,$hash{'to_'.$rid});
-       }
+#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='';
+
+    for(my $index=0; $index<(scalar @$studentInformation); $index++) {
+        if(!&ShouldShowColumn($cache, 'ChartHeading'.$index)) {
+            next;
+        }
+	my $data=$cache->{$name.':'.$studentInformation->[$index]};
+	$Str .= $data;
+
+	my @dataLength=split(//,$data);
+	my $length=scalar @dataLength;
+	$Str .= (' 'x($cache->{$studentInformation->[$index].'Length'}-
+                      $length));
+	$Str .= $spacePadding;
     }
+
+    return $Str;
 }
 
-# ================================================================ Main Handler
+=pod
 
-sub handler {
-  my $r=shift;
+=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,$studentInformation,$spacePadding,$ChartDB)=@_;
+    my ($sname,$sdom) = split(/\:/,$name);
+    my $Str;
+    my %CacheData;
+
+    unless(tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_READER,0640)) {
+        return '';
+    }
+    # Handle Student information ------------------------------------------
+    # Handle user data
+    $Str=&FormatStudentInformation(\%CacheData, $name, $studentInformation, 
+                                   $spacePadding);
+
+    # Handle errors
+    if($CacheData{$name.':error'} =~ /environment/) {
+        $Str .= '<br>';
+        untie(%CacheData);
+        return $Str;
+    }
+
+    if($CacheData{$name.':error'} =~ /course/) {
+        $Str .= '<br>';
+        untie(%CacheData);
+        return $Str;
+    }
+
+    # Handle problem data ------------------------------------------------
+    my $Version;
+    my $problemsCorrect = 0;
+    my $totalProblems   = 0;
+    my $problemsSolved  = 0;
+    my $numberOfParts   = 0;
+    foreach my $sequence (split(/\:/,$CacheData{'orderedSequences'})) {
+        if(!&ShouldShowColumn(\%CacheData, 'ChartSequence'.$sequence)) {
+            next;
+        }
+
+	my $characterCount=0;
+	foreach my $problemID (split(/\:/,$CacheData{$sequence.':problems'})) {
+	    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.
+                                                        ':parts'})) {
+                    $Str .= ' ';
+                    $totalProblems++;
+                    $characterCount++;
+                }
+                next;
+            }
+
+            my %partData=undef;
+            # 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.
+                                                        ':parts'})) {
+
+                    if(!defined($CacheData{$name.":$Version:$problem".
+                                               ":resource.$part.solved"})) {
+                        # No grade for this submission, so skip
+                        next;
+                    }
+
+                    my $tries=0;
+                    my $code=' ';
+
+                    $tries = $CacheData{$name.":$Version:$problem".
+                                        ":resource.$part.tries"};
+                    $partData{$part.':tries'}=($tries) ? $tries : 0;
+
+                    my $val = $CacheData{$name.":$Version:$problem".
+                                         ":resource.$part.solved"};
+                    if    ($val eq 'correct_by_student')   {$code = '*';} 
+                    elsif ($val eq 'correct_by_override')  {$code = '+';}
+                    elsif ($val eq 'incorrect_attempted')  {$code = '.';} 
+                    elsif ($val eq 'incorrect_by_override'){$code = '-';}
+                    elsif ($val eq 'excused')              {$code = 'x';}
+                    elsif ($val eq 'ungraded_attempted')   {$code = '#';}
+                    else                                   {$code = ' ';}
+                    $partData{$part.':code'}=$code;
+                }
+            }
+
+            # 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.='<a href="/adm/grades?symb='.
+                &Apache::lonnet::escape($problem).
+                '&student='.$sname.'&domain='.$sdom.'&command=submission">'; 
+            foreach(split(/\:/,$CacheData{$sequence.':'.$problemID.
+                                          ':parts'})) {
+                if($partData{$_.':code'} eq '*') {
+                    $problemsCorrect++;
+                    if (($partData{$_.':tries'}<10) &&
+                        ($partData{$_.':tries'} ne '')) {
+                        $partData{$_.':code'}=$partData{$_.':tries'};
+                    }
+                } elsif($partData{$_.':code'} eq '+') {
+                    $problemsCorrect++;
+                }
 
-  if (&Apache::lonnet::allowed('vgr',$ENV{'request.course.id'})) {
-# ------------------------------------------- Set document type for header only
+                $Str .= $partData{$_.':code'};
+                $characterCount++;
 
-  if ($r->header_only) {
-       if ($ENV{'browser.mathml'}) {
-           $r->content_type('text/xml');
-       } else {
-           $r->content_type('text/html');
-       }
-       $r->send_http_header;
-       return OK;
-   }
-
-  my $requrl=$r->uri;
-# ----------------------------------------------------------------- Tie db file
-  if ($ENV{'request.course.fn'}) {
-      my $fn=$ENV{'request.course.fn'};
-      if (-e "$fn.db") {
-          if (tie(%hash,'GDBM_File',"$fn.db",&GDBM_READER,0640)) {
-# ------------------------------------------------------------------- Hash tied
-
-
-# ------------------------------------------------------------------ Build page
-
-# ---------------------------------------------------------------- Send headers
-
-             $r->content_type('text/html');
-             $r->send_http_header;
-             $r->print(
-  '<html><head><title>LON-CAPA Assessment Chart</title></head>');
-
-	     $r->print('<body bgcolor="#FFFFFF">'.
-                                    '<script>window.focus();</script>'.
-                           '<img align=right src=/adm/lonIcons/lonlogos.gif>'.
-                                    '<h1>Assessment Chart</h1>');
-
-# ---------------------------------------------------------------- Course title
-
-    $r->print('<h1>'.
-            $ENV{'course.'.$ENV{'request.course.id'}.'.description'}.'</h1>');
-
-
-# ------------------------------- This is going to take a while, produce output
-
-             $r->rflush();
-
-# ----------------------- Get first and last resource, see if there is anything
-
-
-              my $firstres=$hash{'map_start_/res/'.$ENV{'request.course.uri'}};
-              my $lastres=$hash{'map_finish_/res/'.$ENV{'request.course.uri'}};
-              if (($firstres) && ($lastres)) {
-# ----------------------------------------------------------------- Render page
-
-                 my $cid=$ENV{'request.course.id'};
-                 my $chome=$ENV{'course.'.$cid.'.home'};
-                 my ($cdom,$cnum)=split(/\_/,$cid);
-
-# ---------------------------------------------- Read class list and row labels
-
-    undef @rowlabels;
-    undef @students;
-
-    my $classlst=&Apache::lonnet::reply
-                                 ('dump:'.$cdom.':'.$cnum.':classlist',$chome);
-    my $now=time;
-    unless ($classlst=~/^error\:/) {
-        map {
-            my ($name,$value)=split(/\=/,$_);
-            my ($end,$start)=split(/\:/,&Apache::lonnet::unescape($value));
-            my $active=1;
-            if (($end) && ($now>$end)) { $active=0; }
-            if ($active) {
-                my $thisindex=$#students+1;
-                $name=&Apache::lonnet::unescape($name);
-                $students[$thisindex]=$name;
-                my ($sname,$sdom)=split(/\:/,$name);
-                my $ssec=&Apache::lonnet::usection($sdom,$sname,$cid);
-                if ($ssec==-1) {
-                    $rowlabels[$thisindex]=
-                      'Data not available: '.$name;
-                } else {
-                    my %reply=&Apache::lonnet::idrget($sdom,$sname);
-                    my $reply=&Apache::lonnet::reply('get:'.$sdom.':'.$sname.
-		      ':environment:firstname&middlename&lastname&generation',
-                      &Apache::lonnet::homeserver($sname,$sdom));
-                    $rowlabels[$thisindex]=
-                      $ssec.' '.$reply{$sname}.' ';
-                    map {
-                     $rowlabels[$thisindex].=&Apache::lonnet::unescape($_).' ';
-                    } split(/\&/,$reply);
+                if($partData{$_.':code'} ne 'x') {
+                    $totalProblems++;
                 }
             }
-        } sort split(/\&/,$classlst);
+            $Str.='</a>';
+        }
+
+        # 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);
+
+	my $outputProblemsCorrect = sprintf( "%3d", $problemsCorrect );
+	$Str .= '<font color="#007700">'.$outputProblemsCorrect.'</font>';
+	$problemsSolved += $problemsCorrect;
+	$problemsCorrect=0;
+
+        $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 .= '<font color="#000088">'.$outputProblemsSolved.
+	    ' / '.$outputTotalProblems.'</font><br>';
+
+    untie(%CacheData);
+    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>';
+
+    for(my $index=0; $index<(scalar @$headings); $index++) {
+        if(!&ShouldShowColumn($CacheData, 'ChartHeading'.$index)) {
+            next;
+        }
+
+        $Str .= '<td align="left"><pre>';
+	my $data=$$headings[$index];
+	$Str .= $data;
+
+	my @dataLength=split(//,$data);
+	my $length=scalar @dataLength;
+	$Str .= (' 'x($CacheData->{$$studentInformation[$index].'Length'}-
+                      $length));
+	$Str .= $spacePadding;
+        $Str .= '</pre></td>';
+    }
+
+    foreach my $sequence (split(/\:/,$CacheData->{'orderedSequences'})) {
+        if(!&ShouldShowColumn($CacheData, 'ChartSequence'.$sequence)) {
+            next;
+        }
+
+        $Str .= '<td align="left"><pre>';
+        my $name = $CacheData->{$sequence.':title'};
+	$Str .= $name;
+	my @titleLength=split(//,$CacheData->{$sequence.':title'});
+	my $leftover=$CacheData->{$sequence.':columnWidth'}-
+                     (scalar @titleLength);
+	$Str .= (' 'x$leftover);
+	$Str .= $spacePadding;
+        $Str .= '</pre></td>';
+    }
+
+    $Str .= '<td><pre>Total Solved/Total Problems</pre></td>';
+    $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,$headings)=@_;
+
+    my $missing=0;
+    my $notThere='<tr><td align="right"><b>Select column to view:</b>';
+    my $name;
+    $notThere .= '<td align="left">';
+    $notThere .= '<select name="ChartReselect" size="4" multiple="true">'."\n";
+
+    for(my $index=0; $index<(scalar @$headings); $index++) {
+        if(&ShouldShowColumn($CacheData, 'ChartHeading'.$index)) {
+            next;
+        }
+        $name = $headings->[$index];
+        $notThere .= '<option value="ChartHeading'.$index.'">';
+        $notThere .= $name.'</option>'."\n";
+        $missing++;
+    }
+
+    foreach my $sequence (split(/\:/,$CacheData->{'orderedSequences'})) {
+        if(&ShouldShowColumn($CacheData, 'ChartSequence'.$sequence)) {
+            next;
+        }
+        $name = $CacheData->{$sequence.':title'};
+        $notThere .= '<option value="ChartSequence'.$sequence.'">';
+        $notThere .= $name.'</option>'."\n";
+        $missing++;
+    }
+
+    if($missing) {
+        $notThere .= '</select>';
     } else {
-        $r->print('<h1>Could not access course data</h1>');
+        $notThere='<tr><td>';
     }
 
-    my $allstudents=$#students+1;
-    $r->print('<h3>'.$allstudents.' students</h3>');
-    $r->rflush();
+    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
 
-# --------------- Find all assessments and put them into some linear-like order
+$headings:  An array of the names of the columns for the student 
+information.  They are used to know what are the student information columns
 
-   &tracetable($firstres,'&'.$lastres.'&');
+Output: $present
 
-# ----------------------------------------------------------------- Start table
+$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.
 
-                          $r->print('<p><pre>');
- 			  my $index;
-                           for ($index=0;$index<=$#students;$index++) {
-                              $r->print(
-                                       substr($students[$index].
-       '                                                        ',0,14).' ! '.
-        			       substr($rowlabels[$index].
-       '                                                        ',0,45).' ! ');
-                              map {
-                                  if ($_) {
-                                     $r->print(&astatus($_,$students[$index]));
-                                  } else {
-                                     $r->print(' ! ');
-                                  }
-                              } @cols;
-                              $r->print("\n");
-                              $r->rflush();
-                          }
-                          $r->print('</pre>');
-
-	     } else {
-                 $r->print('<h3>Undefined course sequence</h3>');
-             }
-
-                      $r->print('</body></html>');
-                                     
-# ------------------------------------------------------------- End render page
-              } else {
-                  $r->content_type('text/html');
-                  $r->send_http_header;
-		  $r->print('<html><body>Coursemap undefined.</body></html>');
-              }
-# ------------------------------------------------------------------ Untie hash
-              unless (untie(%hash)) {
-                   &Apache::lonnet::logthis("<font color=blue>WARNING: ".
-                       "Could not untie coursemap $fn (browse).</font>"); 
-              }
-
-# -------------------------------------------------------------------- All done
-	      return OK;
-# ----------------------------------------------- Errors, hash could no be tied
-      }
-  } else {
-  $ENV{'user.error.msg'}="$requrl:bre:0:0:Course not initialized";
-  return HTTP_NOT_ACCEPTABLE; 
+=back
+
+=cut
+
+sub CreateColumnSelectors {
+    my ($CacheData,$headings)=@_;
+
+    my $found=0;
+    my ($name, $length, $position);
+
+    my $present = '<tr>';
+    for(my $index=0; $index<(scalar @$headings); $index++) {
+        if(!&ShouldShowColumn($CacheData, 'ChartHeading'.$index)) {
+            next;
+        }
+        $present .= '<td align="left">';
+        $present .= '<input type="checkbox" checked="on" ';
+        $present .= 'name="ChartHeading'.$index.'" />';
+        $present .= '</td>';
+        $found++;
+    }
+
+    foreach my $sequence (split(/\:/,$CacheData->{'orderedSequences'})) {
+        if(!&ShouldShowColumn($CacheData, 'ChartSequence'.$sequence)) {
+            next;
+        }
+        $present .= '<td align="left">';
+        $present .= '<input type="checkbox" checked="on" ';
+        $present .= 'name="ChartSequence'.$sequence.'" />';
+        $present .= '</td>';
+        $found++;
+    }
+
+    if(!$found) {
+        $present = '';
+    }
+
+    return $present.'<td></td></tr></form>'."\n";;
 }
-} else {
-      $ENV{'user.error.msg'}=
-        $r->uri.":vgr:0:0:Cannot view grades for complete course";
-      return HTTP_NOT_ACCEPTABLE; 
 
+=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='';
+    my $OpSel2='';
+    my $OpSel3='';
+    my $Status = $CacheData->{'form.ChartStatus'};
+    if ( $Status eq 'Any' ) { $OpSel3='selected'; }
+    elsif ($Status eq 'Expired' ) { $OpSel2 = 'selected'; }
+    else { $OpSel1 = 'selected'; }
+
+    my $Ptr .= '<form name="ChartStat" method="post" action="/adm/chart" >';
+    $Ptr .= "\n";
+    $Ptr .= '<tr><td align="right">';
+    $Ptr .= '</td><td align="left">';
+    $Ptr .= '<input type="submit" name="ChartRecalculate" ';
+    $Ptr .= 'value="Recalculate Chart"/>'."\n";
+    $Ptr .= '&nbsp;&nbsp;&nbsp;';
+    $Ptr .= '<input type="submit" name="ChartRefresh" ';
+    $Ptr .= 'value="Refresh Chart"/>'."\n";
+    $Ptr .= '&nbsp;&nbsp;&nbsp;';
+    $Ptr .= '<input type="submit" name="ChartReset" ';
+    $Ptr .= 'value="Reset Selections"/></td>'."\n";
+    $Ptr .= '</tr><tr><td align="right">';
+    $Ptr .= '<b> Sort by: </b>'."\n";
+    $Ptr .= '</td><td align="left">';
+    $Ptr .= '<input type="submit" name="ChartSort" value="User Name" />'."\n";
+    $Ptr .= '&nbsp;&nbsp;&nbsp;';
+    $Ptr .= '<input type="submit" name="ChartSort" value="Last Name" />'."\n";
+    $Ptr .= '&nbsp;&nbsp;&nbsp;';
+    $Ptr .= '<input type="submit" name="ChartSort" value="Section"/>'."\n";
+    $Ptr .= '</td></tr><tr><td align="right">';
+    $Ptr .= '<b> Student Status: &nbsp; </b>'."\n".
+            '</td><td align="left">'.
+            '<select name="ChartStatus">'. 
+            '<option '.$OpSel1.' >Active</option>'."\n".
+            '<option '.$OpSel2.' >Expired</option>'."\n".
+	    '<option '.$OpSel3.' >Any</option> </select> '."\n";
+    $Ptr .= '</td></tr>';
+
+    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".
+              "   *: correct by student in more than 9 tries\n".
+	      "   +: correct by override\n".
+              "   -: incorrect by override\n".
+	      "   .: incorrect attempted\n".
+	      "   #: ungraded attempted\n".
+              "    : not attempted\n".
+	      "   x: excused".
+              "</pre><p>"; 
+    return $Str;
 }
-1;
-__END__
 
+=pod
+
+=item &StartDocument()
+
+Returns a string containing the header information for the chart: title,
+logo, and course title.
+
+=cut
+
+sub StartDocument {
+    my ($title, $header)=@_;
+    my $Str = '';
+    $Str .= '<html>';
+    $Str .= '<head><title>';
+    $Str .= $title.'</title></head>';
+    $Str .= '<body bgcolor="#FFFFFF">';
+    $Str .= '<script>window.focus();</script>';
+    $Str .= '<img align=right src=/adm/lonIcons/lonlogos.gif>';
+    $Str .= '<h1>'.$header.'</h1>';
+    $Str .= '<h1>'.$ENV{'course.'.$ENV{'request.course.id'}.'.description'};
+    $Str .= '</h1>';
+
+    return $Str;
+}
+
+# ----- END FORMAT PRINT DATA ------------------------------------------
+
+=pod
+
+=head1 HELPER FUNCTIONS
+
+These are just a couple of functions do various odd and end 
+jobs.
+
+=cut
+
+# ----- HELPER FUNCTIONS -----------------------------------------------
+
+=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.ChartRefresh'}
+        # Ignore $ENV{'form.ChartRecalculate'}
+
+        if(defined($ENV{'form.ChartSort'})) {
+            $CacheData{'form.ChartSort'}=$ENV{'form.ChartSort'};
+        } elsif(!defined($CacheData{'form.ChartSort'})) {
+            $CacheData{'form.ChartSort'}='username';
+        }
+
+        if(defined($ENV{'form.ChartStatus'})) {
+            $CacheData{'form.ChartStatus'}=$ENV{'form.ChartStatus'};
+        } elsif(!defined($CacheData{'form.ChartStatus'})) {
+            $CacheData{'form.ChartStatus'}='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\.ChartHeading/) {
+                $found++;
+                push(@headings, $_);
+            } elsif(/form\.ChartSequence/) {
+                $found++;
+                push(@sequences, $_);
+            } elsif(/form\./) {
+                $found++;
+            }
+        }
+
+        if($found) {
+            $CacheData{'form.ChartHeadings'}=join(":::",@headings);
+            $CacheData{'form.ChartSequences'}=join(":::",@sequences);
+        }
+
+        if(defined($ENV{'form.ChartReselect'})) {
+            my @reselected = (ref($ENV{'form.ChartReselect'}) ? 
+                              @{$ENV{'form.ChartReselect'}}
+                              : ($ENV{'form.ChartReselect'}));
+            foreach (@reselected) {
+                if(/ChartHeading/) {
+                    $CacheData{'form.ChartHeadings'}.=":::".$_;
+                } elsif(/ChartSequence/) {
+                    $CacheData{'form.ChartSequences'}.=":::".$_;
+                }
+            }
+        }
+
+        # !$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.ChartReset'}) || (!$found && !$isCached)) {
+            $CacheData{'form.ChartReset'}='true';
+            $CacheData{'form.ChartStatus'}='Active';
+            $CacheData{'form.ChartSort'}='username';
+            $CacheData{'form.ChartHeadings'}='ALLHEADINGS';
+            $CacheData{'form.ChartSequences'}='ALLSEQUENCES';
+        } else {
+            $CacheData{'form.ChartReset'}='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,$cache)=@_;
+
+    # Initialize Lengths
+    for(my $index=0; $index<(scalar @$headings); $index++) {
+        my @titleLength=split(//,$$headings[$index]);
+        $cache->{$$studentInformation[$index].'Length'}=
+            scalar @titleLength;
+    }
+
+    foreach my $name (@$students) {
+        foreach (@$studentInformation) {
+            my @dataLength=split(//,$cache->{$name.':'.$_});
+            my $length=scalar @dataLength;
+            if($length > $cache->{$_.'Length'}) {
+                $cache->{$_.'Length'}=$length;
+            }
+        }
+    }
+
+    return;
+}
+
+=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)=@_;
+
+    my @sorted1Students=();
+    foreach (@$students) {
+        my ($end,$start)=split(/\:/,$CacheData->{$_.':date'});
+        my $active=1;
+        my $now=time;
+        my $Status=$CacheData->{'form.ChartStatus'};
+        $Status = ($Status) ? $Status : 'Active';
+        if((($end) && $now > $end) && (($Status eq 'Active'))) { 
+            $active=0; 
+        }
+        if(($Status eq 'Expired') && ($end == 0 || $now < $end)) {
+            $active=0;
+        }
+        if($active) {
+            push(@sorted1Students, $_);
+        }
+    }
+
+    my $Pos = $CacheData->{'form.ChartSort'};
+    my %sortData;
+    if($Pos eq 'Last Name') {
+	for(my $index=0; $index<scalar @sorted1Students; $index++) {
+	    $sortData{$CacheData->{$sorted1Students[$index].':fullname'}}=
+		$sorted1Students[$index];
+	}
+    } elsif($Pos eq 'Section') {
+	for(my $index=0; $index<scalar @sorted1Students; $index++) {
+	    $sortData{$CacheData->{$sorted1Students[$index].':section'}.
+		      $sorted1Students[$index]}=$sorted1Students[$index];
+	}
+    } else {
+	# Sort by user name
+	for(my $index=0; $index<scalar @sorted1Students; $index++) {
+	    $sortData{$sorted1Students[$index]}=$sorted1Students[$index];
+	}
+    }
+
+    my @order = ();
+    foreach my $key (sort(keys(%sortData))) {
+	push (@order,$sortData{$key});
+    }
+
+    return @order;
+}
+
+=pod
+
+=item &ShouldShowColumn()
+
+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)=@_;
+
+    if($cache->{'form.ChartReset'} eq 'true') {
+        return 1;
+    }
+
+    my $headings=$cache->{'form.ChartHeadings'};
+    my $sequences=$cache->{'form.ChartSequences'};
+    if($headings eq 'ALLHEADINGS' || $sequences eq 'ALLSEQUENCES' ||
+       $headings=~/$test/ || $sequences=~/$test/) {
+        return 1;
+    }
+
+    return 0;
+}
+
+# ----- END HELPER FUNCTIONS --------------------------------------------
+
+=pod
+
+=head1 Handler and main function(BuildChart)
+
+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.
+
+=cut
+
+=pod
+
+=item &BuildChart()
+
+ The following is the process that BuildChart goes through to 
+  create the html document.
+
+ -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
 
+=over 4
 
+Input: $r
 
+$r:  Used to print html
 
+Output: None
 
+=back
 
+=cut
+
+sub BuildChart {
+    my ($r)=@_;
+    my $c = $r->connection;
+
+    # Start the lonchart document
+    $r->content_type('text/html');
+    $r->send_http_header;
+    $r->print(&StartDocument('LON-CAPA Assessment Chart', 'Assessment Chart'));
+    $r->rflush();
+
+    # Test for access to the CacheData
+    my $isCached=0;
+    my $cid=$ENV{'request.course.id'};
+    my $ChartDB = "/home/httpd/perl/tmp/$ENV{'user.name'}".
+                  "_$ENV{'user.domain'}_$cid\_chart.db";
+    my $isRecalculate=0;
+    if(defined($ENV{'form.ChartRecalculate'})) {
+        $isRecalculate=1;
+    }
+    $isCached=&Apache::loncoursedata::TestCacheData($ChartDB, $isRecalculate);
+    if($isCached < 0) {
+        $r->print("Unable to tie hash to db file</body></html>");
+        $r->rflush();
+        return;
+    }
+    &ProcessFormData($ChartDB, $isCached);
+
+    # Download class list information if not using cached data
+    my %CacheData;
+    my @students=();
+    my @studentInformation=('username','domain','section','id','fullname');
+    my @headings=('User Name','Domain','Section','PID','Full Name');
+    my $spacePadding='   ';
+    if(!$isCached) {
+        unless(tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_WRCREAT,0640)) {
+            $r->print("Unable to tie hash to db file</body></html>");
+            $r->rflush();
+            return;
+        }
+
+        my $processTopResourceMapReturn=
+            &Apache::loncoursedata::ProcessTopResourceMap(\%CacheData,$c);
+        if($processTopResourceMapReturn ne 'OK') {
+            $r->print($processTopResourceMapReturn.'</body></html>');
+            untie(%CacheData);
+            return;
+        }
+
+        if($c->aborted()) {
+            untie(%CacheData);
+            $r->print('</body></html>'); 
+            return;
+        }
+
+        my $classlist=&Apache::loncoursedata::DownloadStudentNamePIDSection(
+                                                                   $cid, $c);
+        my ($checkForError)=keys(%$classlist);
+        if($checkForError =~ /^(con_lost|error|no_such_host)/i ||
+           defined($classlist->{'error'})) {
+            $r->print("Error getting student data.</body></html>");
+            $r->rflush();
+            untie(%CacheData);
+            return;
+        }
+
+        if($c->aborted()) {
+            untie(%CacheData);
+            $r->print('</body></html>'); 
+            return;
+        }
+
+
+        @students=&Apache::loncoursedata::ProcessClassList(\%CacheData, 
+                                              $classlist, $cid, 
+                                              $CacheData{'form.ChartStatus'},
+                                              $c);
+
+        if($c->aborted()) {
+            untie(%CacheData);
+            $r->print('</body></html>'); 
+            return;
+        }
+
+        &SpaceColumns(\@students,\@studentInformation,\@headings,
+                      \%CacheData);
+
+        if($c->aborted()) {
+            untie(%CacheData);
+            $r->print('</body></html>'); 
+            return;
+        }
+
+        untie(%CacheData);
+    } else {
+        if(!$c->aborted() && tie(%CacheData,'GDBM_File',$ChartDB,
+                                 &GDBM_READER,0640)) {
+            @students=split(/:::/,$CacheData{'NamesOfStudents'});
+        }
+    }
+
+    # 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); }
+        if(defined($CacheData{'time'})) { $downloadTime=$CacheData{'time'}; }
+        else { $downloadTime=localtime(); }
+        if(!$c->aborted()) { $r->print('<h3>'.$downloadTime.'</h3>'); }
+        if(!$c->aborted()) { $r->print('<h1>'.(scalar @students).
+                                       ' 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,
+                                                       \@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,
+                                                       \@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 {
+	$r->print("Init2: Unable to tie hash to db file");
+	return;
+    }
+
+    # Output student data
+    my @updateStudentList = ();
+    my $courseData;
+    $r->print('<pre>');
+    foreach (@students) {
+        if($c->aborted()) {
+            last;
+        }
+
+        if(!$isCached) {
+            $courseData=
+                &Apache::loncoursedata::DownloadStudentCourseInformation($_, 
+                                                                         $cid);
+            if($c->aborted()) { last; }
+            if(tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_WRCREAT,0640)) {
+                &Apache::loncoursedata::ProcessStudentData(\%CacheData, 
+                                                           $courseData, $_);
+                push(@updateStudentList, $_);
+                untie(%CacheData);
+            } else {
+                next;
+            }
+        }
+        $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'}=
+#		            &Apache::lonnet::arrayref2str(\@updateStudentList);
+        untie(%CacheData);
+    }
+
+    # End document
+    $r->print('</pre></body></html>');
+    $r->rflush();
+
+    return;
+}
+
+# ================================================================ 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;
+    unless(&Apache::lonnet::allowed('vgr',$ENV{'request.course.id'})) {
+	$ENV{'user.error.msg'}=
+        $r->uri.":vgr:0:0:Cannot view grades for complete course";
+	return HTTP_NOT_ACCEPTABLE; 
+    }
+
+    # Set document type for header only
+    if ($r->header_only) {
+        if($ENV{'browser.mathml'}) {
+            $r->content_type('text/xml');
+        } else {
+            $r->content_type('text/html');
+        }
+        &Apache::loncommon::no_cache($r);
+        $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";
+        return HTTP_NOT_ACCEPTABLE; 
+    }
+
+    &BuildChart($r);
+
+    return OK;
+}
+1;
+__END__