--- loncom/interface/Attic/lonchart.pm 2002/06/05 05:05:38 1.43 +++ 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.43 2002/06/05 05:05:38 stredwic Exp $ +# $Id: lonchart.pm,v 1.58 2002/07/08 16:50:03 stredwic Exp $ # # Copyright Michigan State University Board of Trustees # @@ -46,6 +46,59 @@ # ### +=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; use strict; @@ -55,288 +108,967 @@ use Apache::loncommon(); use HTML::TokeParser; use GDBM_File; -# -------------------------------------------------------------- Module Globals -my %hash; -my %CachData; -my @cols; -my $r; -my $c; - -# ------------------------------------------------------------- Find out status +#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 -sub ExtractStudentData { - my ($name,$coid)=@_; +$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, 'heading'.$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; +} + +=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 $ResId; - my $Code; - my $Tries; - my $Wrongs; - my %TempHash; - my $Version; - my $problemsCorrect; - my $problemsSolved; - my $totalProblems; - my $LatestVersion; 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($CachData{$name.':error'} =~ /environment/) { -# my $errorMessage = $CachData{$name.':error'}; -# return ''.$sname.''.$sdom. -# ''.$errorMessage.''; -# } + if($CacheData{$name.':error'} =~ /environment/) { + $Str .= '
'; + untie(%CacheData); + return $Str; + } - # Handle user data - $Str = '
'.$sname.'
'.$sdom;
-    $Str .= '
'.$CachData{$name.':section'};
-    $Str .= '
'.$CachData{$name.':id'};
-    $Str .= '
'.$CachData{$name.':fullname'};
-    $Str .= '
'; - - if($CachData{$name.':error'} =~ /course/) { - return $Str; -# my $errorMessage = 'May have no course data or '. -# $CachData{$name.':error'}; -# return ''.$sname.''.$sdom. -# ''.$errorMessage.''; + if($CacheData{$name.':error'} =~ /course/) { + $Str .= '
'; + untie(%CacheData); + return $Str; } # Handle problem data ------------------------------------------------ - $Str .= '
';
-    $problemsCorrect = 0;
-    $totalProblems = 0;
-    $problemsSolved = 0;
-    my $IterationNo = 0;
-    foreach $ResId (@cols) {
-	if ($IterationNo == 0) {
-	    # Looks to be skipping start resource
-	    $IterationNo++; 
-	    next;
-	}
-
-	# ResId is 0 for sequences and pages, 
-	# please check tracetable for changes
-	if (!$ResId) {
-	    my $outputProblemsCorrect = sprintf( "%3d", $problemsCorrect );
-	    $Str .= ''.$outputProblemsCorrect.
-		    '
'; - $Str .= '
';
-	    $problemsSolved += $problemsCorrect;
-	    $problemsCorrect=0;
-	    next; 
-	}
-
-	# Set $1 and $2
-	$ResId=~/(\d+)\.(\d+)/;
-	my $meta=$hash{'src_'.$ResId};
-	my $numberOfParts = 0;
-	undef %TempHash;
-	foreach (split(/\,/,&Apache::lonnet::metadata($meta,'keys'))) {
-#----------- Overwrite $1 in next statement ---------------------------------
-	    if ($_=~/^stores\_(\d+)\_tries$/) {
-		my $Part=&Apache::lonnet::metadata($meta,$_.'.part');
-		if ( $TempHash{"$Part"} eq '' ) { 
-		    $TempHash{"$Part"} = $Part;
-		    $TempHash{$numberOfParts}=$Part;
-		    $TempHash{"$Part.Code"} = ' ';  
-		    $numberOfParts++;
-		}
-	    }
-	}
+    my $Version;
+    my $problemsCorrect = 0;
+    my $totalProblems   = 0;
+    my $problemsSolved  = 0;
+    my $numberOfParts   = 0;
+    foreach my $sequence (split(/\:/,$CacheData{'orderedSequences'})) {
+        if(!&ShouldShowColumn(\%CacheData, 'sequence'.$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'};
+                    }
+                } 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.'
'; -#----------- Using $1 and $2 ----------------------------------------------- - my $Prob = &Apache::lonnet::symbclean( - &Apache::lonnet::declutter($hash{'map_id_'.$1} ). - '___'.$2.'___'. - &Apache::lonnet::declutter( $hash{'src_'.$ResId} )); - $Code=' '; - $Tries = 0; - $LatestVersion = $CachData{$name.":version:$Prob"}; - - if ( $LatestVersion ) { - for ( my $Version=1; $Version<=$LatestVersion; $Version++ ) { - my $vkeys = $CachData{$name.":$Version:keys:$Prob"}; - my @keys = split(/\:/,$vkeys); - - foreach my $Key (@keys) { -#---------------------- Changing $1 ------------------------------------------- - if (($Key=~/\.(\w+)\.solved$/) && ($Key!~/^\d+\:/)) { -#---------------------- Using $1 ----------------------------------------------- - my $Part = $1; - $Tries = $CachData{$name.":$Version:$Prob". - ":resource.$Part.tries"}; - $TempHash{"$Part.Tries"}=($Tries) ? $Tries : 0; - my $Val = $CachData{$name.":$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 = ' ';} + untie(%CacheData); + return $Str; +} - $TempHash{"$Part.Code"} = $Code; - } - } - } -# Actually append problem to output (all parts) - $Str.=''; - for(my $n = 0; $n < $numberOfParts; $n++) { - my $part = $TempHash{$n}; - my $code2 = $TempHash{"$part.Code"}; - if($code2 eq '*') { - $problemsCorrect++; -# !!!!!!!!!!!------------------------- Should 10 not be maxtries? ------------ - if (($TempHash{"$part.Tries"}<10) || - ($TempHash{"$part.Tries"} eq '')) { - $TempHash{"$part.Code"}=$TempHash{"$part.Tries"}; - } - } elsif($code2 eq '+') { - $problemsCorrect++; - } +=pod - $Str .= $TempHash{"$part.Code"}; +=item &CreateTableHeadings() - if($code2 ne 'x') { - $totalProblems++; - } - } - $Str.=''; - } else { - for(my $n=0; $n<$numberOfParts; $n++) { - $Str.=' '; - $totalProblems++; - } - } +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, 'heading'.$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, 'sequence'.$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 .= '
'.$problemsSolved.
-	    ' / '.$totalProblems.'
'; + $Str .= '
Total Solved/Total Problems
'; + $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,$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, 'heading'.$index)) { + next; + } + $present .= ''; + $present .= ''; + $present .= ''; + $found++; + } + + foreach my $sequence (split(/\:/,$CacheData->{'orderedSequences'})) { + if(!&ShouldShowColumn($CacheData, 'sequence'.$sequence)) { + next; + } + $present .= ''; + $present .= ''; + $present .= ''; + $found++; + } + + 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 = $ENV{'form.status'}; + my $Status = $CacheData->{'form.status'}; if ( $Status eq 'Any' ) { $OpSel3='selected'; } elsif ($Status eq 'Expired' ) { $OpSel2 = 'selected'; } else { $OpSel1 = 'selected'; } - my $Ptr = '
'."\n"; - $Ptr .= ' Sort by:   '."\n"; + my $Ptr .= ''."\n"; + $Ptr .= ''; + $Ptr .= ''; + $Ptr .= ''. ' '."\n"; - $Ptr .= '   '; - $Ptr .= ''."\n"; - $Ptr .= '
'."\n"; - $r->print( $Ptr ); + $Ptr .= ''; + + return $Ptr; } -sub CreateTableHeadings { - $r->print(''); - $r->print('User Name'); - $r->print('Domain'); - $r->print('Section'); - $r->print('PID'); - $r->print('Full Name'); - - my $ResId; - my $IterationNo = 0; - foreach $ResId (@cols) { - if ($IterationNo == 0) {$IterationNo++; next;} - if (!$ResId) { -# my $PrNo = sprintf( "%3d", $ProbNo ); -# $Str .= 'Chapter '.$PrNo.''; - $r->print('Chapter '.'0'.''); - } - } +=pod - $r->print(''); - $r->rflush(); +=item &CreateLegend() - return; +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; } -# ------------------------------------------------------------ Build page table +=pod -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})) { - foreach (split(/\,/,$hash{'to_'.$rid})){ - &tracetable($hash{'goesto_'.$_},$beenhere); - } - } - } +=item &StartDocument() + +Returns a string containing the header information for the chart: title, +logo, and course title. + +=cut + +sub StartDocument { + my $Str = ''; + $Str .= ''; + $Str .= ''; + $Str .= 'LON-CAPA Assessment Chart'; + $Str .= ''; + $Str .= ''; + $Str .= ''; + $Str .= '

Assessment Chart

'; + $Str .= '

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

'; + + return $Str; } -sub usection { - my ($udom,$unam,$courseid,$ActiveFlag)=@_; - $courseid=~s/\_/\//g; - $courseid=~s/^(\w)/\/$1/; +# ----- END FORMAT PRINT DATA ------------------------------------------ + +=pod + +=head1 DOWNLOAD INFORMATION + +This section contains all the files that get data from other servers +and/or itself. There is one function that has a call to get remote +information but isn't included here which is ProcessTopLevelMap. The +usage was small enough to be ignored, but that portion may be moved +here in the future. + +=cut + +# ----- DOWNLOAD INFORMATION ------------------------------------------- + +=pod + +=item &DownloadPrerequisiteData() + +Collects lastname, generation, middlename, firstname, PID, and section for each +student from their environment database. The list of students is built from +collecting a classlist for the course that is to be displayed. + +=over 4 + +Input: $courseID, $c + +$courseID: The id of the course + +$c: The connection class that can determine if the browser has aborted. It +is used to short circuit this function so that it doesn't continue to +get information when there is no need. + +Output: \%classlist + +\%classlist: A pointer to a hash containing the following data: + +-A list of student name:domain (as keys) (known below as $name) + +-A hash pointer for each student containing lastname, generation, firstname, +middlename, and PID : Key is $name.'studentInformation' + +-A hash pointer to each students section data : Key is $name.section + +=back - my %result=&Apache::lonnet::dump('roles',$udom,$unam); +=cut - my($checkForError)=keys (%result); +sub DownloadPrerequisiteData { + my ($courseID, $c)=@_; + my ($courseDomain,$courseNumber)=split(/\_/,$courseID); + + my %classlist=&Apache::lonnet::dump('classlist',$courseDomain, + $courseNumber); + my ($checkForError)=keys (%classlist); if($checkForError =~ /^(con_lost|error|no_such_host)/i) { - return -1; + return \%classlist; + } + + foreach my $name (keys(%classlist)) { + if($c->aborted()) { + $classlist{'error'}='aborted'; + return \%classlist; + } + + my ($studentName,$studentDomain) = split(/\:/,$name); + # Download student environment data, specifically the full name and id. + my %studentInformation=&Apache::lonnet::get('environment', + ['lastname','generation', + 'firstname','middlename', + 'id'], + $studentDomain, + $studentName); + $classlist{$name.':studentInformation'}=\%studentInformation; + + if($c->aborted()) { + $classlist{'error'}='aborted'; + return \%classlist; + } + + #Section + my %section=&Apache::lonnet::dump('roles',$studentDomain,$studentName); + $classlist{$name.':section'}=\%section; } + return \%classlist; +} + +=pod + +=item &DownloadStudentCourseInformation() + +Dump of all the course information for a single student. There is no +pruning of data, it is all stored in a hash and returned. + +=over 4 + +Input: $name, $courseID + +$name: student name:domain + +$courseID: The id of the course + +Output: \%courseData + +\%courseData: A hash pointer to the raw data from the student's course +database. + +=back + +=cut + +sub DownloadStudentCourseInformation { + my ($name,$courseID)=@_; + my ($studentName,$studentDomain) = split(/\:/,$name); + + # Download student course data + my %courseData=&Apache::lonnet::dump($courseID,$studentDomain, + $studentName); + return \%courseData; +} + +# ----- END DOWNLOAD INFORMATION --------------------------------------- + +=pod + +=head1 PROCESSING FUNCTIONS + +These functions process all the data for all the students. Also, they +are the only functions that access the cache database for writing. Thus +they are the only functions that cache data. The downloading and caching +were separated to reduce problems with stopping downloading then can't +tie hash to database later. + +=cut + +# ----- PROCESSING FUNCTIONS --------------------------------------- + +=pod + +=item &ProcessTopResourceMap() + +Trace through the "big hash" created in rat/lonuserstate.pm::loadmap. +Basically, this function organizes a subset of the data and stores it in +cached data. The data stored is the problems, sequences, sequence titles, +parts of problems, and their ordering. Column width information is also +partially handled here on a per sequence basis. + +=over 4 + +Input: $ChartDB, $c + +$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: A string that contains an error message or "OK" if everything went +smoothly. + +=back + +=cut + +sub ProcessTopResourceMap { + my ($ChartDB,$c)=@_; + my %hash; + my $fn=$ENV{'request.course.fn'}; + if(-e "$fn.db") { + my $tieTries=0; + while($tieTries < 3) { + if(tie(%hash,'GDBM_File',"$fn.db",&GDBM_READER,0640)) { + last; + } + $tieTries++; + sleep 1; + } + if($tieTries >= 3) { + return 'Coursemap undefined.'; + } + } else { + return 'Can not open Coursemap.'; + } + + my %CacheData; + unless(tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_WRCREAT,0640)) { + untie(%hash); + return 'Could not tie cache hash.'; + } + + # Initialize state machine. Set information pointing to top level map. + my (@sequences, @currentResource, @finishResource); + my ($currentSequence, $currentResourceID, $lastResourceID); + + $currentResourceID=$hash{'ids_/res/'.$ENV{'request.course.uri'}}; + push(@currentResource, $currentResourceID); + $lastResourceID=-1; + $currentSequence=-1; + my $topLevelSequenceNumber = $currentSequence; + + while(1) { + if($c->aborted()) { + last; + } + # HANDLE NEW SEQUENCE! + #if page || sequence + if(defined($hash{'map_pc_'.$hash{'src_'.$currentResourceID}})) { + push(@sequences, $currentSequence); + push(@currentResource, $currentResourceID); + push(@finishResource, $lastResourceID); + + $currentSequence=$hash{'map_pc_'.$hash{'src_'.$currentResourceID}}; + + # Mark sequence as containing problems. If it doesn't, then + # it will be removed when processing for this sequence is + # complete. This allows the problems in a sequence + # to be outputed before problems in the subsequences + if(!defined($CacheData{'orderedSequences'})) { + $CacheData{'orderedSequences'}=$currentSequence; + } else { + $CacheData{'orderedSequences'}.=':'.$currentSequence; + } + + $lastResourceID=$hash{'map_finish_'. + $hash{'src_'.$currentResourceID}}; + $currentResourceID=$hash{'map_start_'. + $hash{'src_'.$currentResourceID}}; + + if(!($currentResourceID) || !($lastResourceID)) { + $currentSequence=pop(@sequences); + $currentResourceID=pop(@currentResource); + $lastResourceID=pop(@finishResource); + if($currentSequence eq $topLevelSequenceNumber) { + last; + } + } + } + + # Handle gradable resources: exams, problems, etc + $currentResourceID=~/(\d+)\.(\d+)/; + my $partA=$1; + my $partB=$2; + if($hash{'src_'.$currentResourceID}=~ + /\.(problem|exam|quiz|assess|survey|form)$/ && + $partA eq $currentSequence) { + my $Problem = &Apache::lonnet::symbclean( + &Apache::lonnet::declutter($hash{'map_id_'.$partA}). + '___'.$partB.'___'. + &Apache::lonnet::declutter($hash{'src_'. + $currentResourceID})); + + $CacheData{$currentResourceID.':problem'}=$Problem; + if(!defined($CacheData{$currentSequence.':problems'})) { + $CacheData{$currentSequence.':problems'}=$currentResourceID; + } else { + $CacheData{$currentSequence.':problems'}.= + ':'.$currentResourceID; + } + + # Get Parts for problem + my $meta=$hash{'src_'.$currentResourceID}; + foreach (split(/\,/,&Apache::lonnet::metadata($meta,'keys'))) { + if($_=~/^stores\_(\d+)\_tries$/) { + my $Part=&Apache::lonnet::metadata($meta,$_.'.part'); + if(!defined($CacheData{$currentSequence.':'. + $currentResourceID.':parts'})) { + $CacheData{$currentSequence.':'.$currentResourceID. + ':parts'}=$Part; + } else { + $CacheData{$currentSequence.':'.$currentResourceID. + ':parts'}.=':'.$Part; + } + } + } + } + + # if resource == finish resource, then it is the end of a sequence/page + if($currentResourceID eq $lastResourceID) { + # pop off last resource of sequence + $currentResourceID=pop(@currentResource); + $lastResourceID=pop(@finishResource); + + if(defined($CacheData{$currentSequence.':problems'})) { + # Capture sequence information here + $CacheData{$currentSequence.':title'}= + $hash{'title_'.$currentResourceID}; + + my $totalProblems=0; + foreach my $currentProblem (split(/\:/, + $CacheData{$currentSequence. + ':problems'})) { + foreach (split(/\:/,$CacheData{$currentSequence.':'. + $currentProblem. + ':parts'})) { + $totalProblems++; + } + } + my @titleLength=split(//,$CacheData{$currentSequence. + ':title'}); + # $extra is 3 for problems correct and 3 for space + # between problems correct and problem output + my $extra = 6; + if(($totalProblems + $extra) > (scalar @titleLength)) { + $CacheData{$currentSequence.':columnWidth'}= + $totalProblems + $extra; + } else { + $CacheData{$currentSequence.':columnWidth'}= + (scalar @titleLength); + } + } else { + # Remove sequence from list, if it contains no problems to + # display. + $CacheData{'orderedSequences'}=~s/$currentSequence//; + $CacheData{'orderedSequences'}=~s/::/:/g; + $CacheData{'orderedSequences'}=~s/^:|:$//g; + } + + $currentSequence=pop(@sequences); + if($currentSequence eq $topLevelSequenceNumber) { + last; + } + } + + # MOVE!!! + # move to next resource + unless(defined($hash{'to_'.$currentResourceID})) { + # big problem, need to handle. Next is probably wrong + last; + } + my @nextResources=(); + foreach (split(/\,/,$hash{'to_'.$currentResourceID})) { + push(@nextResources, $hash{'goesto_'.$_}); + } + push(@currentResource, @nextResources); + # Set the next resource to be processed + $currentResourceID=pop(@currentResource); + } + + unless (untie(%hash)) { + &Apache::lonnet::logthis("WARNING: ". + "Could not untie coursemap $fn (browse)". + "."); + } + + unless (untie(%CacheData)) { + &Apache::lonnet::logthis("WARNING: ". + "Could not untie Cache Hash (browse)". + "."); + } + + return 'OK'; +} + +=pod + +=item &ProcessSection() + +Determine the section number for a student for the class. A student can have +multiple sections for the same class. The correct one is chosen. + +=over 4 + +Input: $sectionData, $courseid, $ActiveFlag + +$sectionData: A pointer to a hash containing all section data for this +student for the class + +$courseid: The course ID. + +$ActiveFlag: The student's active status (Active/Expired) + +Output: $oldsection, $cursection, or -1 + +$oldsection and $cursection and sections number that will be displayed in the +chart. + +-1 is returned if an error occurs. + +=back + +=cut + +sub ProcessSection { + my ($sectionData, $courseid,$ActiveFlag)=@_; + $courseid=~s/\_/\//g; + $courseid=~s/^(\w)/\/$1/; + my $cursection='-1'; my $oldsection='-1'; my $status='Expired'; - foreach my $key (keys (%result)) { - my $value = $result{$key}; + my $section=''; + foreach my $key (keys (%$sectionData)) { + my $value = $sectionData->{$key}; if ($key=~/^$courseid(?:\/)*(\w+)*\_st$/) { - my $section=$1; - if ($key eq $courseid.'_st') { $section=''; } + $section=$1; + if($key eq $courseid.'_st') { + $section=''; + } my ($dummy,$end,$start)=split(/\_/,$value); my $now=time; my $notactive=0; @@ -353,6 +1085,7 @@ sub usection { if($notactive == 0) { $status='Active'; $cursection=$section; + last; } if($notactive == 1) { $oldsection=$section; @@ -374,38 +1107,404 @@ sub usection { return '-1'; } +=pod + +=item &ProcessStudentInformation() + +Takes data downloaded for a student and breaks it up into managable pieces and +stored in cache data. The username, domain, class related date, PID, +full name, and section are all processed here. + +=over 4 + +Input: $CacheData, $studentInformation, $section, $date, $name, $courseID + +$CacheData: A hash pointer to the cached data + +$studentInformation: Student information is what was requested in +&DownloadPrerequistedData(). See that function for what data is requested. + +$section: A hash pointer to class section related information. + +$date: A composite of the start and end date for this class for this +student. Format: end:start + +$name: the username:domain information + +$courseID: The course ID + +Output: None + +*NOTE: There is no return value, but if an error occurs a key is added to +the cache data with the value being the error message. The key is +username:domain:error. It will only exist if an error occurs. + +=back + +=cut + +sub ProcessStudentInformation { + my ($CacheData,$studentInformation,$section,$date,$name,$courseID)=@_; + my ($studentName,$studentDomain) = split(/\:/,$name); + + $CacheData->{$name.':username'}=$studentName; + $CacheData->{$name.':domain'}=$studentDomain; + $CacheData->{$name.':date'}=$date; + + my ($checkForError)=keys(%$studentInformation); + if($checkForError =~ /^(con_lost|error|no_such_host)/i) { + $CacheData->{$name.':error'}= + 'Could not download student environment data.'; + $CacheData->{$name.':fullname'}=''; + $CacheData->{$name.':id'}=''; + } else { + $CacheData->{$name.':fullname'}=&ProcessFullName( + $studentInformation->{'lastname'}, + $studentInformation->{'generation'}, + $studentInformation->{'firstname'}, + $studentInformation->{'middlename'}); + $CacheData->{$name.':id'}=$studentInformation->{'id'}; + } + + # Get student's section number + my $sec=&ProcessSection($section, $courseID, $CacheData->{'form.status'}); + if($sec != -1) { + $CacheData->{$name.':section'}=$sec; + } else { + $CacheData->{$name.':section'}=''; + } + + 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=(); + + my %CacheData; + if(tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_WRCREAT,0640)) { + foreach my $name (keys(%$classlist)) { + if($name =~ /\:section/ || $name =~ /\:studentInformation/ || + $name eq '') { + next; + } + if($c->aborted()) { + last; + } + push(@names,$name); + &ProcessStudentInformation( + \%CacheData, + $classlist->{$name.':studentInformation'}, + $classlist->{$name.':section'}, + $classlist->{$name}, + $name,$courseID); + } + + # Time of download + $CacheData{'time'}=localtime(); + untie(%CacheData); + } + + return @names; +} + +=pod + +=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)=@_; + + my %CacheData; + if(tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_WRCREAT,0640)) { + # Initialize Lengths + for(my $index=0; $index<(scalar @$headings); $index++) { + my @titleLength=split(//,$$headings[$index]); + $CacheData{$$studentInformation[$index].'Length'}= + scalar @titleLength; + } + + foreach my $name (@$students) { + foreach (@$studentInformation) { + my @dataLength=split(//,$CacheData{$name.':'.$_}); + my $length=scalar @dataLength; + if($length > $CacheData{$_.'Length'}) { + $CacheData{$_.'Length'}=$length; + } + } + } + untie(%CacheData); + } + + 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 ($name)=@_; + my ($lastname, $generation, $firstname, $middlename)=@_; my $Str = ''; - if($CachData{$name.':lastname'} ne '') { - $Str .= $CachData{$name.':lastname'}.' '; - if($CachData{$name.':generation'} ne '') { - $Str .= $CachData{$name.':generation'}; + if($lastname ne '') { + $Str .= $lastname.' '; + if($generation ne '') { + $Str .= $generation; } else { chop($Str); } $Str .= ', '; - if($CachData{$name.':firstname'} ne '') { - $Str .= $CachData{$name.':firstname'}.' '; + if($firstname ne '') { + $Str .= $firstname.' '; } - if($CachData{$name.':middlename'} ne '') { - $Str .= $CachData{$name.':middlename'}; + if($middlename ne '') { + $Str .= $middlename; } else { chop($Str); - if($CachData{$name.'firstname'} eq '') { + if($firstname eq '') { chop($Str); } } } else { - if($CachData{$name.':firstname'} ne '') { - $Str .= $CachData{$name.':firstname'}.' '; + if($firstname ne '') { + $Str .= $firstname.' '; } - if($CachData{$name.':middlename'} ne '') { - $Str .= $CachData{$name.':middlename'}.' '; + if($middlename ne '') { + $Str .= $middlename.' '; } - if($CachData{$name.':generation'} ne '') { - $Str .= $CachData{$name.':generation'}; + if($generation ne '') { + $Str .= $generation; } else { chop($Str); } @@ -414,300 +1513,400 @@ sub ProcessFullName { return $Str; } -sub DownloadStudentInformation { - my ($name,$courseID)=@_; - my ($studentName,$studentDomain) = split(/\:/,$name); - my $checkForError; - my $key; - my $Status=$CachData{$name.':Status'}; - -#----------------------------------------------------------------- - # Download student environment data, specifically the full name and id. - my %studentInformation=&Apache::lonnet::get('environment', - ['lastname','generation', - 'firstname','middlename', - 'id'], - $studentDomain,$studentName); - if($c->aborted()) { - return; - } - ($checkForError)=keys (%studentInformation); - if($checkForError =~ /^(con_lost|error|no_such_host)/i) { - $CachData{$name.':error'}= - 'Could not download student environment data.'; -# return; - $CachData{$name.':lastname'}=''; - $CachData{$name.':generation'}=''; - $CachData{$name.':firstname'}=''; - $CachData{$name.':middlename'}=''; - $CachData{$name.':fullname'}=''; - $CachData{$name.':id'}=''; - } else { - $CachData{$name.':lastname'}=$studentInformation{'lastname'}; - $CachData{$name.':generation'}=$studentInformation{'generation'}; - $CachData{$name.':firstname'}=$studentInformation{'firstname'}; - $CachData{$name.':middlename'}=$studentInformation{'middlename'}; - $CachData{$name.':fullname'}=&ProcessFullName($name); - $CachData{$name.':id'}=$studentInformation{'id'}; - } +=pod - # Download student course data - my %courseData=&Apache::lonnet::dump($courseID,$studentDomain, - $studentName); - if($c->aborted()) { - return; - } - ($checkForError)=keys (%courseData); - if($checkForError =~ /^(con_lost|error|no_such_host)/i) { - $CachData{$name.':error'}='Could not download course data.'; -# return; - } else { - foreach $key (keys (%courseData)) { - $CachData{$name.':'.$key}=$courseData{$key}; - } - } +=item &SortStudents() - # Get student's section number - my $sec=&usection($studentDomain, $studentName, $courseID, $Status); - if($sec != -1) { - $CachData{$name.':section'}=sprintf('%3s',$sec); - } else { - $CachData{$name.':section'}=''; - } +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. - return; -} +=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 { -# --------------------------------------------------------------- Sort Students - my $Pos = $ENV{'form.sort'}; - my @students = split(/:::/,$CachData{'NamesOfStudents'}); - my %sortData; + 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.sort'}; + my %sortData; if($Pos eq 'Last Name') { - for(my $index=0; $index<$#students+1; $index++) { - $sortData{$CachData{$students[$index].':fullname'}}= - $students[$index]; + for(my $index=0; $index{$sorted1Students[$index].':fullname'}}= + $sorted1Students[$index]; } } elsif($Pos eq 'Section') { - for(my $index=0; $index<$#students+1; $index++) { - $sortData{$CachData{$students[$index].':section'}. - $students[$index]}=$students[$index]; + for(my $index=0; $index{$sorted1Students[$index].':section'}. + $sorted1Students[$index]}=$sorted1Students[$index]; } } else { # Sort by user name - for(my $index=0; $index<$#students+1; $index++) { - $sortData{$students[$index]}=$students[$index]; + for(my $index=0; $indexprint('

Could not access course data

'); - push (@names, 'error'); - return @names; +=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; + my %testData; + my $tieTries=0; + + if ((-e "$ChartDB") && (!defined($ENV{'form.recalculate'}))) { + $isCached = 1; + } else { + $isCached = 0; } -# ------------------------------------- Calculate Status and number of students - my $now=time; - foreach my $name (sort(keys(%classlist))) { - my $value=$classlist{$name}; - my ($end,$start)=split(/\:/,$value); - my $active=1; - my $Status=$ENV{'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(@names,$name); - $CachData{$name.':Status'}=$Status; - } + while($tieTries < 10) { + my $result=0; + if($isCached) { + $result=tie(%testData,'GDBM_File',$ChartDB,&GDBM_READER,0640); + } else { + $result=tie(%testData,'GDBM_File',$ChartDB,&GDBM_NEWDB,0640); + } + if($result) { + last; + } + $tieTries++; + sleep 1; + } + if($tieTries >= 10) { + return -1; } - $CachData{'NamesOfStudents'}=join(":::",@names); + untie(%testData); - return @names; + return $isCached; } -sub BuildChart { -# ----------------------- 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)) { - $r->print('

Undefined course sequence

'); - return; +=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.reset'} eq 'true') { + return 1; + } + + my $headings=$cache->{'form.headings'}; + my $sequences=$cache->{'form.sequences'}; + if($headings eq 'ALLHEADINGS' || $sequences eq 'ALLSEQUENCES' || + $headings=~/$test/ || $sequences=~/$test/) { + return 1; } -# --------------- Find all assessments and put them into some linear-like order - &tracetable($firstres,'&'.$lastres.'&'); + return 0; +} + +# ----- END HELPER FUNCTIONS -------------------------------------------- + +=pod -# ----------------------------------------------------------------- Render page - &CreateForm(); +=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()); + $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 $isCached = 0; - my @students; - if ((-e "$ChartDB") && ($ENV{'form.sort'} ne 'Recalculate Chart')) { - if (tie(%CachData,'GDBM_File',"$ChartDB",&GDBM_READER,0640)) { - $isCached = 1; - @students=&SortStudents(); - } else { - $r->print("Unable to tie hash to db file"); - $r->rflush(); - return; - } + + $isCached=&TestCacheData($ChartDB); + 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) { + my $processTopResourceMapReturn=&ProcessTopResourceMap($ChartDB,$c); + if($processTopResourceMapReturn ne 'OK') { + $r->print($processTopResourceMapReturn); + return; + } + if($c->aborted()) { return; } + my $classlist=&DownloadPrerequisiteData($cid, $c); + my ($checkForError)=keys(%$classlist); + if($checkForError =~ /^(con_lost|error|no_such_host)/i || + defined($classlist->{'error'})) { + return; + } + if($c->aborted()) { return; } + @students=&ProcessClassList($classlist,$cid,$ChartDB,$c); + if($c->aborted()) { return; } + &SpaceColumns(\@students,\@studentInformation,\@headings, + $ChartDB); + if($c->aborted()) { return; } } else { - if (tie(%CachData,'GDBM_File',$ChartDB,&GDBM_NEWDB,0640)) { - $isCached = 0; - @students=&CollectClasslist(); - if($students[0] eq 'error') { - return; - } - } else { - $r->print("Unable to tie hash to db file"); - return; - } + 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; } - $r->print('

'.($#students+1).' students

'); - $r->rflush(); - -# ----------------------------------------------------------------- Start table - $r->print(''); -# &CreateTableHeadings(); + # Output student data my @updateStudentList = (); - foreach my $student (@students) { - if($c->aborted()) { - if($isCached == 0) { - $CachData{'NamesOfStudents'}=join(":::",@updateStudentList); - } - last; - } - if($isCached == 0) { - &DownloadStudentInformation($student,$cid); - push (@updateStudentList, $student); - } - my $Str=&ExtractStudentData($student,$cid); - $r->print(''.$Str.''); + my $courseData; + $r->print('
');
+    foreach (@students) {
+        if($c->aborted()) {
+            last;
+        }
+
+        if(!$isCached) {
+            $courseData=&DownloadStudentCourseInformation($_, $cid);
+            if($c->aborted()) { last; }
+            push(@updateStudentList, $_);
+            &ProcessStudentData($courseData, $_, $ChartDB);
+        }
+        $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);
     }
-    $r->print('
'); - untie(%CachData); + # End document + $r->print(''); + $r->rflush(); return; } -sub Start { - $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(); +# ================================================================ Main Handler - &BuildChart(); +=pod - $r->print(''); +=item &handler() - return; -} +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. -# ================================================================ Main Handler +=over 4 -sub handler { - undef %hash; - undef %CachData; - undef @cols; - - $r=shift; - $c = $r->connection; - 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'); - } - &Apache::loncommon::no_cache($r); - $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 -# ---------------------------------------------------------------- Send headers - $r->content_type('text/html'); - $r->send_http_header; - $r->print(''); - &Start(); - $r->print(''); - $r->rflush(); -# ------------------------------------------------------------- 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)."); - } +Input: $r -# -------------------------------------------------------------------- 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; - } - } else { +$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__