--- loncom/interface/Attic/lonchart.pm 2001/12/18 20:06:05 1.24 +++ loncom/interface/Attic/lonchart.pm 2002/07/17 12:36:17 1.60 @@ -1,4 +1,30 @@ # The LearningOnline Network with CAPA +# (Publication Handler +# +# $Id: lonchart.pm,v 1.60 2002/07/17 12:36:17 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,360 +32,1200 @@ # (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/18/01, 10/19/01 Behrouz Minaei -# 11/17/01, 11/22/01, 11/24/01, 11/28/01 Behrouz Minaei -# 12/18/01 Behrouz Minaei +# 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 +# +### + +=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 ExtractStudentData { - my ($index,$coid)=@_; - my ($sname,$sdom) = split( /\:/, $students[$index] ); - my $shome=&Apache::lonnet::homeserver( $sname,$sdom ); - my $reply=&Apache::lonnet::reply('dump:'.$sdom.':'.$sname.':'.$coid,$shome ); - my %result=(); - my $ResId; - my $Code; - my $Tries; - my $Wrongs; - my %TempHash; +#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=''; + my $data; + + for(my $index=0; $index<(scalar @$studentInformation); $index++) { + if(!&ShouldShowColumn($cache, 'ChartHeading'.$index)) { + next; + } + $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; +} + +=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,$studentInformation,$spacePadding,$ChartDB)=@_; + my ($sname,$sdom) = split(/\:/,$name); + my $Str; + my %CacheData; + + unless(tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_READER,0640)) { + return ''; + } + +# my $section = &Apache::loncoursedata::CheckStatus($name, \%CacheData, +# $CacheData{'form.Status'}); +# if($section eq 'not found') { +# untie(%CacheData); +# return; +# } + + # Handle Student information ------------------------------------------ + # Handle user data + $Str=&FormatStudentInformation(\%CacheData, $name, $studentInformation, + $spacePadding); + + # Handle errors + if($CacheData{$name.':error'} =~ /environment/) { + $Str .= '
'; + untie(%CacheData); + return $Str; + } + + if($CacheData{$name.':error'} =~ /course/) { + $Str .= '
'; + untie(%CacheData); + return $Str; + } + + # Handle problem data ------------------------------------------------ my $Version; - my $ProbNo; - my $PrTotal; - my $LatestVersion; - my $Str=substr($students[$index]. - ' ',0,14).' ! '. - substr($rowlabels[$index]. - ' ',0,45).' ! '; - unless ($reply=~/^error\:/) { - map { - my ($name,$value)=split(/\=/,&Apache::lonnet::unescape($_)); - $result{$name}=$value; - } split(/\&/,$reply); - $ProbNo = 0; - $PrTotal = 0; - my $IterationNo = 0; - foreach $ResId (@cols) { - if ($IterationNo == 0) {$IterationNo++; next;} - if (!$ResId) { - my $PrNo = sprintf( "%3d", $ProbNo ); - $Str .= ' '.''.$PrNo.' '; - $PrTotal += $ProbNo; - $ProbNo=0; - next; - } - $ResId=~/(\d+)\.(\d+)/; - my $meta=$hash{'src_'.$ResId}; - my $PartNo = 0; - undef %TempHash; - map { - if ($_=~/^stores\_(\d+)\_tries$/) { - my $Part=&Apache::lonnet::metadata($meta,$_.'.part'); - if ( $TempHash{"$Part"} eq '' ) { - $TempHash{"$Part"} = $Part; - $TempHash{$PartNo}=$Part; - $TempHash{"$Part.Code"} = ' '; - $PartNo++; - } - } - } split(/\,/,&Apache::lonnet::metadata($meta,'keys')); - - my $Prob = &Apache::lonnet::declutter( $hash{'map_id_'.$1} ). - '___'.$2.'___'. - &Apache::lonnet::declutter( $hash{'src_'.$ResId} ); - $Code=' '; - $Tries = 0; - $LatestVersion = $result{"version:$Prob"}; - - if ( $LatestVersion ) { - for ( my $Version=1; $Version<=$LatestVersion; $Version++ ) { - my $vkeys = $result{"$Version:keys:$Prob"}; - my @keys = split(/\:/,$vkeys); - - foreach my $Key (@keys) { - if (($Key=~/\.(\w+)\.solved$/) && ($Key!~/^\d+\:/)) { - my $Part = $1; - $Tries = $result{"$Version:$Prob:resource.$Part.tries"}; - $TempHash{"$Part.Tries"}=($Tries) ? $Tries : 0; - my $Val = $result{"$Version:$Prob: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 = ' ';} - $TempHash{"$Part.Code"} = $Code; - } - } - } - for ( my $n = 0; $n < $PartNo; $n++ ) { - my $part = $TempHash{$n}; - if ($TempHash{"$part.Code"} eq '*') { - $ProbNo++; - if (($TempHash{"$part.Tries"}<10) || - ($TempHash{"$part.Tries"} eq '')) { - $TempHash{"$part.Code"}=$TempHash{"$part.Tries"}; - } + 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.=''; + foreach(split(/\:/,$CacheData{$sequence.':'.$problemID. + ':parts'})) { + if($partData{$_.':code'} eq '*') { + $problemsCorrect++; + if (($partData{$_.':tries'}<10) && + ($partData{$_.':tries'} ne '')) { + $partData{$_.':code'}=$partData{$_.':tries'}; } - $Str .= $TempHash{"$part.Code"}; - } - } - else {for(my $n=0; $n<$PartNo; $n++) {$Str.=' ';}} - } - } - my $PrTot = sprintf( "%5d", $PrTotal ); - $Str .= ' '.''.$PrTot.' '; - - return $Str ; + } elsif($partData{$_.':code'} eq '+') { + $problemsCorrect++; + } + + $Str .= $partData{$_.':code'}; + $characterCount++; + + if($partData{$_.':code'} ne 'x') { + $totalProblems++; + } + } + $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); + + my $outputProblemsCorrect = sprintf( "%3d", $problemsCorrect ); + $Str .= ''.$outputProblemsCorrect.''; + $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 .= ''.$outputProblemsSolved. + ' / '.$outputTotalProblems.'
'; + + untie(%CacheData); + return $Str; } +=pod + +=item &CreateTableHeadings() -# ------------------------------------------------------------ Build page table - -sub tracetable { - my ($rid,$beenhere)=@_; - unless ($beenhere=~/\&$rid\&/) { - $beenhere.=$rid.'&'; -# new ... updating the map according to sequence and page - if (defined($hash{'is_map_'.$rid})) { - my $cmap=$hash{'map_type_'.$hash{'map_pc_'.$hash{'src_'.$rid}}}; - if ( $cmap eq 'sequence' || $cmap eq 'page' ) { - $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}); - } +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=''; + + for(my $index=0; $index<(scalar @$headings); $index++) { + if(!&ShouldShowColumn($CacheData, 'ChartHeading'.$index)) { + next; + } + + $Str .= '
';
+	my $data=$$headings[$index];
+	$Str .= $data;
+
+	my @dataLength=split(//,$data);
+	my $length=scalar @dataLength;
+	$Str .= (' 'x($CacheData->{$$studentInformation[$index].'Length'}-
+                      $length));
+	$Str .= $spacePadding;
+        $Str .= '
'; } + + foreach my $sequence (split(/\:/,$CacheData->{'orderedSequences'})) { + if(!&ShouldShowColumn($CacheData, 'ChartSequence'.$sequence)) { + next; + } + + $Str .= '
';
+        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 .= '
'; + } + + $Str .= '
Total Solved/Total Problems
'; + $Str .= ''; + + return $Str; } -# ================================================================ Main Handler +=pod -sub handler { +=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='Select column to view:'; + my $name; + $notThere .= ''; + $notThere .= ''; + } else { + $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,$headings)=@_; + + my $found=0; + my ($name, $length, $position); + + my $present = ''; + for(my $index=0; $index<(scalar @$headings); $index++) { + if(!&ShouldShowColumn($CacheData, 'ChartHeading'.$index)) { + next; + } + $present .= ''; + $present .= ''; + $present .= ''; + $found++; + } + + foreach my $sequence (split(/\:/,$CacheData->{'orderedSequences'})) { + if(!&ShouldShowColumn($CacheData, 'ChartSequence'.$sequence)) { + next; + } + $present .= ''; + $present .= ''; + $present .= ''; + $found++; + } - undef %hash; - undef @students; - undef @cols; - undef @rowlabels; - - my $r=shift; - - if (&Apache::lonnet::allowed('vgr',$ENV{'request.course.id'})) { -# ------------------------------------------- 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'); - } - $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( - 'LON-CAPA Assessment Chart'); - - $r->print(''. - ''. - ''. - '

Assessment Chart

'); - -# ---------------------------------------------------------------- Course title - - $r->print('

'. - $ENV{'course.'.$ENV{'request.course.id'}.'.description'}.'

'. - localtime()."

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

"); - -# ------------------------------- 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 - - 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:lastname&generation&firstname&middlename', - &Apache::lonnet::homeserver($sname,$sdom)); - $rowlabels[$thisindex]= - sprintf('%3s',$ssec).' '.$reply{$sname}.' '; - my $i=0; - map { - $i++; - if ( $_ ne '') { - $rowlabels[$thisindex].=&Apache::lonnet::unescape($_).' '; - } - if ($i == 2) { - chop($rowlabels[$thisindex]); - $rowlabels[$thisindex].=', '; - } - } split(/\&/,$reply); + if(!$found) { + $present = ''; + } + + 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=''; + my $OpSel2=''; + my $OpSel3=''; + my $Status = $CacheData->{'form.Status'}; + if ( $Status eq 'Any' ) { $OpSel3='selected'; } + elsif ($Status eq 'Expired' ) { $OpSel2 = 'selected'; } + else { $OpSel1 = 'selected'; } + + my $Ptr .= '

'; + $Ptr .= "\n"; + $Ptr .= ''; + $Ptr .= ''; + $Ptr .= ''. + ' '."\n"; + $Ptr .= ''; + + 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 = "

".
+              "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".
+              "

"; + return $Str; +} + +=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 .= ''; + $Str .= ''; + $Str .= $title.''; + $Str .= ''; + $Str .= ''; + $Str .= ''; + $Str .= '

'.$header.'

'; + $Str .= '

'.$ENV{'course.'.$ENV{'request.course.id'}.'.description'}; + $Str .= '

'; + + 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.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\.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'}.=":::".$_; } } - } sort split(/\&/,$classlst); + } - } else { - $r->print('

Could not access course data

'); + # !$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.Status'}='Active'; + $CacheData{'form.ChartSort'}='username'; + $CacheData{'form.ChartHeadings'}='ALLHEADINGS'; + $CacheData{'form.ChartSequences'}='ALLSEQUENCES'; + } else { + $CacheData{'form.ChartReset'}='false'; + } + + untie(%CacheData); } - my $allstudents=$#students+1; - $r->print('

'.$allstudents.' students

'); - $r->rflush(); + 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 -# --------------- Find all assessments and put them into some linear-like order +sub SpaceColumns { + my ($students,$studentInformation,$headings,$cache)=@_; - &tracetable($firstres,'&'.$lastres.'&'); + # Initialize Lengths + for(my $index=0; $index<(scalar @$headings); $index++) { + my @titleLength=split(//,$$headings[$index]); + $cache->{$$studentInformation[$index].'Length'}= + scalar @titleLength; + } -# ----------------------------------------------------------------- Start table + foreach my $name (@$students) { + foreach (@$studentInformation) { + my @dataLength=split(//,$cache->{$name.':'.$_}); + my $length=scalar @dataLength; + if($length > $cache->{$_.'Length'}) { + $cache->{$_.'Length'}=$length; + } + } + } - $r->print('

');
- 			  my $index;
-                           for ($index=0;$index<=$#students;$index++) {
-                              $r->print(&ExtractStudentData($index,$cid).'
'); - $r->rflush(); - } - $r->print('
'); - - } else { - $r->print('

Undefined course sequence

'); - } - - $r->print(''); - -# ------------------------------------------------------------- End render page - } else { - $r->content_type('text/html'); - $r->send_http_header; - $r->print('Coursemap undefined.'); - } -# ------------------------------------------------------------------ Untie hash - unless (untie(%hash)) { - &Apache::lonnet::logthis("WARNING: ". - "Could not untie coursemap $fn (browse)."); - } - -# -------------------------------------------------------------------- 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; + return; } -} else { - $ENV{'user.error.msg'}= - $r->uri.":vgr:0:0:Cannot view grades for complete course"; - return HTTP_NOT_ACCEPTABLE; +=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.Status'}; + $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{$sorted1Students[$index].':fullname'}}= + $sorted1Students[$index]; + } + } elsif($Pos eq 'Section') { + for(my $index=0; $index{$sorted1Students[$index].':section'}. + $sorted1Students[$index]}=$sorted1Students[$index]; + } + } else { + # Sort by user name + for(my $index=0; $index{'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; } -1; -__END__ +# ----- 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"); + $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"); + $r->rflush(); + return; + } + + my $processTopResourceMapReturn= + &Apache::loncoursedata::ProcessTopResourceMap(\%CacheData,$c); + if($processTopResourceMapReturn ne 'OK') { + $r->print($processTopResourceMapReturn.''); + untie(%CacheData); + return; + } + + if($c->aborted()) { + untie(%CacheData); + $r->print(''); + 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."); + $r->rflush(); + untie(%CacheData); + return; + } + + if($c->aborted()) { + untie(%CacheData); + $r->print(''); + return; + } + + + @students=&Apache::loncoursedata::ProcessClassList(\%CacheData, + $classlist, $cid, + $CacheData{'form.Status'}, + $c); + + if($c->aborted()) { + untie(%CacheData); + $r->print(''); + return; + } + + &SpaceColumns(\@students,\@studentInformation,\@headings, + \%CacheData); + + if($c->aborted()) { + untie(%CacheData); + $r->print(''); + 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('

'.$downloadTime.'

'); } + if(!$c->aborted()) { $r->print('

'.(scalar @students). + ' 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, + \@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, + \@headings)); } + if(!$c->aborted()) { $r->print(&CreateTableHeadings( + \%CacheData, + \@studentInformation, + \@headings, + $spacePadding)); } + if(!$c->aborted()) { $r->print('
'); } + 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('
');
+    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('
'); + $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__