--- loncom/interface/Attic/lonchart.pm 2002/07/02 21:48:36 1.52 +++ loncom/interface/Attic/lonchart.pm 2002/07/17 12:36:17 1.60 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # (Publication Handler # -# $Id: lonchart.pm,v 1.52 2002/07/02 21:48:36 stredwic Exp $ +# $Id: lonchart.pm,v 1.60 2002/07/17 12:36:17 stredwic Exp $ # # Copyright Michigan State University Board of Trustees # @@ -48,6 +48,54 @@ =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; @@ -56,21 +104,59 @@ use strict; use Apache::Constants qw(:common :http); use Apache::lonnet(); use Apache::loncommon(); +use Apache::loncoursedata(); use HTML::TokeParser; use GDBM_File; #my $jr; + +=pod + +=head1 FORMAT DATA FOR PRINTING + +=cut + # ----- FORMAT PRINT DATA ---------------------------------------------- +=pod + +=item &FormatStudentInformation() + +This function produces a formatted string of the student's information: +username, domain, section, full name, and PID. + +=over 4 + +Input: $cache, $name, $studentInformation, $spacePadding + +$cache: This is a pointer to a hash that is tied to the cached data + +$name: The name and domain of the current student in name:domain format + +$studentInformation: A pointer to an array holding the names used to + +remove data from the hash. They represent the name of the data to be removed. + +$spacePadding: Extra spaces that represent the space between columns + +Output: $Str + +$Str: Formatted string. + +=back + +=cut + sub FormatStudentInformation { my ($cache,$name,$studentInformation,$spacePadding)=@_; my $Str=''; + my $data; for(my $index=0; $index<(scalar @$studentInformation); $index++) { - if(!&ShouldShowColumn($cache, 'heading'.$index)) { + if(!&ShouldShowColumn($cache, 'ChartHeading'.$index)) { next; } - my $data=$cache->{$name.':'.$studentInformation->[$index]}; + $data=$cache->{$name.':'.$studentInformation->[$index]}; $Str .= $data; my @dataLength=split(//,$data); @@ -83,8 +169,42 @@ sub FormatStudentInformation { return $Str; } +=pod + +=item &FormatStudentData() + +First, FormatStudentInformation is called and prefixes the course information. +This function produces a formatted string of the student's course information. +Each column of data represents all the problems for a given sequence. For +valid grade data, a link is created for that problem to a submission record +for that problem. + +=over 4 + +Input: $name, $studentInformation, $spacePadding, $ChartDB + +$name: The name and domain of the current student in name:domain format + +$studentInformation: A pointer to an array holding the names used to +remove data from the hash. They represent +the name of the data to be removed. + +$spacePadding: Extra spaces that represent the space between columns + +$ChartDB: The name of the cached data database which will be tied to that +database. + +Output: $Str + +$Str: Formatted string that is an entire row of the chart. It is a +concatenation of student information and student course information. + +=back + +=cut + sub FormatStudentData { - my ($name,$coid,$studentInformation,$spacePadding,$ChartDB)=@_; + my ($name,$studentInformation,$spacePadding,$ChartDB)=@_; my ($sname,$sdom) = split(/\:/,$name); my $Str; my %CacheData; @@ -92,6 +212,14 @@ sub FormatStudentData { 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, @@ -117,7 +245,7 @@ sub FormatStudentData { my $problemsSolved = 0; my $numberOfParts = 0; foreach my $sequence (split(/\:/,$CacheData{'orderedSequences'})) { - if(!&ShouldShowColumn(\%CacheData, 'sequence'.$sequence)) { + if(!&ShouldShowColumn(\%CacheData, 'ChartSequence'.$sequence)) { next; } @@ -126,6 +254,8 @@ sub FormatStudentData { my $problem = $CacheData{$problemID.':problem'}; my $LatestVersion = $CacheData{$name.":version:$problem"}; + # Output blanks for all the parts of this problem if there + # is no version information about the current problem. if(!$LatestVersion) { foreach my $part (split(/\:/,$CacheData{$sequence.':'. $problemID. @@ -138,13 +268,19 @@ sub FormatStudentData { } my %partData=undef; - #initialize data, displays skips correctly + # Initialize part data, display skips correctly + # Skip refers to when a student made no submissions on that + # part/problem. foreach my $part (split(/\:/,$CacheData{$sequence.':'. $problemID. ':parts'})) { $partData{$part.':tries'}=0; $partData{$part.':code'}=' '; } + + # Looping through all the versions of each part, starting with the + # oldest version. Basically, it gets the most recent + # set of grade data for each part. for(my $Version=1; $Version<=$LatestVersion; $Version++) { foreach my $part (split(/\:/,$CacheData{$sequence.':'. $problemID. @@ -152,6 +288,7 @@ sub FormatStudentData { if(!defined($CacheData{$name.":$Version:$problem". ":resource.$part.solved"})) { + # No grade for this submission, so skip next; } @@ -175,6 +312,9 @@ sub FormatStudentData { } } + # All grades (except for versionless parts) are displayed as links + # to their submission record. Loop through all the parts for the + # current problem in the correct order and prepare the output links $Str.=''; @@ -200,6 +340,9 @@ sub FormatStudentData { $Str.=''; } + # Output the number of correct answers for the current sequence. + # This part takes up 6 character slots, but is formated right + # justified. my $spacesNeeded=$CacheData{$sequence.':columnWidth'}-$characterCount; $spacesNeeded -= 3; $Str .= (' 'x$spacesNeeded); @@ -212,6 +355,9 @@ sub FormatStudentData { $Str .= $spacePadding; } + # Output the total correct problems over the total number of problems. + # I don't like this type of formatting, but it is a solution. Need + # a way to dynamically determine the space requirements. my $outputProblemsSolved = sprintf( "%4d", $problemsSolved ); my $outputTotalProblems = sprintf( "%4d", $totalProblems ); $Str .= ''.$outputProblemsSolved. @@ -221,15 +367,43 @@ sub FormatStudentData { return $Str; } +=pod + +=item &CreateTableHeadings() + +This function generates the column headings for the chart. + +=over 4 + +Inputs: $CacheData, $studentInformation, $headings, $spacePadding + +$CacheData: pointer to a hash tied to the cached data database + +$studentInformation: a pointer to an array containing the names of the data +held in a column and is used as part of a key into $CacheData + +$headings: The names of the headings for the student information + +$spacePadding: The spaces to go between columns + +Output: $Str + +$Str: A formatted string of the table column headings. + +=back + +=cut + sub CreateTableHeadings { my ($CacheData,$studentInformation,$headings,$spacePadding)=@_; - my $Str='
';
+    my $Str='';
 
     for(my $index=0; $index<(scalar @$headings); $index++) {
-        if(!&ShouldShowColumn($CacheData, 'heading'.$index)) {
+        if(!&ShouldShowColumn($CacheData, 'ChartHeading'.$index)) {
             next;
         }
 
+        $Str .= '
';
 	my $data=$$headings[$index];
 	$Str .= $data;
 
@@ -238,13 +412,15 @@ sub CreateTableHeadings {
 	$Str .= (' 'x($CacheData->{$$studentInformation[$index].'Length'}-
                       $length));
 	$Str .= $spacePadding;
+        $Str .= '
'; } foreach my $sequence (split(/\:/,$CacheData->{'orderedSequences'})) { - if(!&ShouldShowColumn($CacheData, 'sequence'.$sequence)) { + if(!&ShouldShowColumn($CacheData, 'ChartSequence'.$sequence)) { next; } + $Str .= '
';
         my $name = $CacheData->{$sequence.':title'};
 	$Str .= $name;
 	my @titleLength=split(//,$CacheData->{$sequence.':title'});
@@ -252,39 +428,69 @@ sub CreateTableHeadings {
                      (scalar @titleLength);
 	$Str .= (' 'x$leftover);
 	$Str .= $spacePadding;
+        $Str .= '
'; } - $Str .= 'Total Solved/Total Problems'; - $Str .= '
'; + $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,$studentInformation,$headings,$spacePadding)=@_; + my ($CacheData,$headings)=@_; my $missing=0; my $notThere='Select column to view:'; my $name; $notThere .= ''; - $notThere .= ''."\n"; for(my $index=0; $index<(scalar @$headings); $index++) { - if(&ShouldShowColumn($CacheData, 'heading'.$index)) { + if(&ShouldShowColumn($CacheData, 'ChartHeading'.$index)) { next; } $name = $headings->[$index]; - $notThere .= ''."\n"; $missing++; } foreach my $sequence (split(/\:/,$CacheData->{'orderedSequences'})) { - if(&ShouldShowColumn($CacheData, 'sequence'.$sequence)) { + if(&ShouldShowColumn($CacheData, 'ChartSequence'.$sequence)) { next; } $name = $CacheData->{$sequence.':title'}; - $notThere .= ''."\n"; $missing++; } @@ -295,91 +501,129 @@ sub CreateColumnSelectionBox { $notThere=''; } - return $notThere.''; + return $notThere.''; } +=pod + +=item &CreateColumnSelectors() + +This function generates the checkboxes above the column headings. The +column will be removed if the checkbox is unchecked. + +=over 4 + +Input: $CacheData, $headings + +$CacheData: A pointer to a hash tied to the cached data + +$headings: An array of the names of the columns for the student +information. They are used to know what are the student information columns + +Output: $present + +$present: The string contains the first row of a table. Each column contains +a checkbox which is left justified. Currently left justification is used +for consistency of location over the column in which it presides. + +=back + +=cut + sub CreateColumnSelectors { - my ($CacheData,$studentInformation,$headings,$spacePadding)=@_; + my ($CacheData,$headings)=@_; my $found=0; my ($name, $length, $position); - my $present='
';
+
+    my $present = '';
     for(my $index=0; $index<(scalar @$headings); $index++) {
-        if(!&ShouldShowColumn($CacheData, 'heading'.$index)) {
+        if(!&ShouldShowColumn($CacheData, 'ChartHeading'.$index)) {
             next;
         }
-        $name = $headings->[$index];
-        $length=$CacheData->{$$studentInformation[$index].'Length'};
-        $position=int($length/2);
-	$present .= (' 'x($position));
+        $present .= '';
         $present .= '';
-        $position+=2;
-	$present .= (' 'x($length-$position));
-	$present .= $spacePadding;
+        $present .= 'name="ChartHeading'.$index.'" />';
+        $present .= '';
         $found++;
     }
 
     foreach my $sequence (split(/\:/,$CacheData->{'orderedSequences'})) {
-        if(!&ShouldShowColumn($CacheData, 'sequence'.$sequence)) {
+        if(!&ShouldShowColumn($CacheData, 'ChartSequence'.$sequence)) {
             next;
         }
-        $name = $CacheData->{$sequence.':title'};
-        $length=$CacheData->{$sequence.':columnWidth'};
-        $position=int($length/2);
-	$present .= (' 'x($position));
+        $present .= '';
         $present .= '';
-        $position+=2;
-	$present .= (' 'x($length-$position));
-	$present .= $spacePadding;
+        $present .= 'name="ChartSequence'.$sequence.'" />';
+        $present .= '';
         $found++;
     }
 
-    if($found) {
-        $present .= '
'; - $present = $present; - } else { + if(!$found) { $present = ''; } - return $present.''."\n";; + 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'}; + my $Status = $CacheData->{'form.Status'}; if ( $Status eq 'Any' ) { $OpSel3='selected'; } elsif ($Status eq 'Expired' ) { $OpSel2 = 'selected'; } else { $OpSel1 = 'selected'; } - my $Ptr .= '
'."\n"; - $Ptr .= ''; + my $Ptr .= ''; + $Ptr .= "\n"; $Ptr .= '
'; $Ptr .= ''; - $Ptr .= ''. - ''. ''."\n". ''."\n". ' '."\n"; @@ -388,6 +632,16 @@ sub CreateForm { 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".
@@ -402,16 +656,25 @@ sub CreateLegend {
     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 .= 'LON-CAPA Assessment Chart';
+    $Str .= $title.'';
     $Str .= '';
     $Str .= '';
     $Str .= '';
-    $Str .= '

Assessment Chart

'; - $Str .= '

'.localtime().'

'; + $Str .= '

'.$header.'

'; $Str .= '

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

'; @@ -420,435 +683,187 @@ sub StartDocument { # ----- END FORMAT PRINT DATA ------------------------------------------ -# ----- DOWNLOAD INFORMATION ------------------------------------------- +=pod -sub DownloadPrerequisiteData { - my ($courseID, $c)=@_; - my ($courseDomain,$courseNumber)=split(/\_/,$courseID); +=head1 HELPER FUNCTIONS - my %classlist=&Apache::lonnet::dump('classlist',$courseDomain, - $courseNumber); - my ($checkForError)=keys (%classlist); - if($checkForError =~ /^(con_lost|error|no_such_host)/i) { - return \%classlist; - } +These are just a couple of functions do various odd and end +jobs. - foreach my $name (keys(%classlist)) { - if($c->aborted()) { - $classlist{'error'}='aborted'; - return \%classlist; - } +=cut - 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; +# ----- HELPER FUNCTIONS ----------------------------------------------- - if($c->aborted()) { - $classlist{'error'}='aborted'; - return \%classlist; - } +=pod - #Section - my %section=&Apache::lonnet::dump('roles',$studentDomain,$studentName); - $classlist{$name.':section'}=\%section; - } +=item &ProcessFormData() - return \%classlist; -} +Cache form data and set default form data (sort, status, heading.$number, +sequence.$number, reselect, reset, recalculate, and refresh) -sub DownloadStudentCourseInformation { - my ($name,$courseID)=@_; - my ($studentName,$studentDomain) = split(/\:/,$name); - - # Download student course data - my %courseData=&Apache::lonnet::dump($courseID,$studentDomain, - $studentName); - return \%courseData; -} +=over 4 -# ----- END DOWNLOAD INFORMATION --------------------------------------- +Input: $ChartDB, $isCached -# ----- END PROCESSING FUNCTIONS --------------------------------------- +$ChartDB: The name of the database for cached data -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.'; - } +$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; - unless(tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_WRCREAT,0640)) { - untie(%hash); - return 'Could not tie cache hash.'; - } - my (@sequences, @currentResource, @finishResource); - my ($currentSequence, $currentResourceID, $lastResourceID); + if(tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_WRCREAT,0640)) { + # Ignore $ENV{'form.ChartRefresh'} + # Ignore $ENV{'form.ChartRecalculate'} - $currentResourceID=$hash{'ids_/res/'.$ENV{'request.course.uri'}}; - push(@currentResource, $currentResourceID); - $lastResourceID=-1; - $currentSequence=-1; - my $topLevelSequenceNumber = $currentSequence; + if(defined($ENV{'form.ChartSort'})) { + $CacheData{'form.ChartSort'}=$ENV{'form.ChartSort'}; + } elsif(!defined($CacheData{'form.ChartSort'})) { + $CacheData{'form.ChartSort'}='username'; + } - while(1) { - if($c->aborted()) { - last; + if(defined($ENV{'form.Status'})) { + $CacheData{'form.Status'}=$ENV{'form.Status'}; + } elsif(!defined($CacheData{'form.Status'})) { + $CacheData{'form.Status'}='Active'; } - # 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; - } - } - } + # $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++; + } + } - # 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($found) { + $CacheData{'form.ChartHeadings'}=join(":::",@headings); + $CacheData{'form.ChartSequences'}=join(":::",@sequences); + } - #if resource == finish resource - 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++; - } + 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'}.=":::".$_; } - 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 { - $CacheData{'orderedSequences'}=~s/$currentSequence//; - $CacheData{'orderedSequences'}=~s/::/:/g; - $CacheData{'orderedSequences'}=~s/^:|:$//g; } + } - $currentSequence=pop(@sequences); - if($currentSequence eq $topLevelSequenceNumber) { - last; - } - } + # !$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'; + } - # 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); + untie(%CacheData); } - unless (untie(%hash)) { - &Apache::lonnet::logthis("WARNING: ". - "Could not untie coursemap $fn (browse)". - "."); - } + return; +} - unless (untie(%CacheData)) { - &Apache::lonnet::logthis("WARNING: ". - "Could not untie Cache Hash (browse)". - "."); - } +=pod - return 'OK'; -} +=item &SpaceColumns() -sub ProcessSection { - my ($sectionData, $courseid,$ActiveFlag)=@_; - $courseid=~s/\_/\//g; - $courseid=~s/^(\w)/\/$1/; - - my $cursection='-1'; - my $oldsection='-1'; - my $status='Expired'; - my $section=''; - foreach my $key (keys (%$sectionData)) { - my $value = $sectionData->{$key}; - if ($key=~/^$courseid(?:\/)*(\w+)*\_st$/) { - $section=$1; - if($key eq $courseid.'_st') { - $section=''; - } - my ($dummy,$end,$start)=split(/\_/,$value); - my $now=time; - my $notactive=0; - if ($start) { - if($now<$start) { - $notactive=1; - } - } - if($end) { - if ($now>$end) { - $notactive=1; - } - } - if($notactive == 0) { - $status='Active'; - $cursection=$section; - last; - } - if($notactive == 1) { - $oldsection=$section; - } - } - } - if($status eq $ActiveFlag) { - if($cursection eq '-1') { - return $oldsection; - } - return $cursection; - } - if($ActiveFlag eq 'Any') { - if($cursection eq '-1') { - return $oldsection; - } - return $cursection; - } - return '-1'; -} +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. -sub ProcessStudentInformation { - my ($CacheData,$studentInformation,$section,$date,$name,$courseID,$c)=@_; - 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'}; - } +=over 4 - # Get student's section number - my $sec=&ProcessSection($section, $courseID, $CacheData->{'form.status'}); - if($sec != -1) { - $CacheData->{$name.':section'}=$sec; - } else { - $CacheData->{$name.':section'}=''; - } +Input: $students, $studentInformation, $headings, $ChartDB - return 0; -} +$students: An array pointer to a list of students (username:domain) -sub ProcessClassList { - my ($classlist,$courseID,$ChartDB,$c)=@_; - my @names=(); +$studentInformatin: The type of data for the student information. It is +used as part of the key in $CacheData. - 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,$c); - } +$headings: The name of the student information columns. - untie(%CacheData); - } +$ChartDB: The name of the cache database which is opened for read/write. - return @names; -} +Output: None - All data stored in cache. -# ----- END PROCESSING FUNCTIONS --------------------------------------- +=back -# ----- HELPER FUNCTIONS ----------------------------------------------- +=cut sub SpaceColumns { - my ($students,$studentInformation,$headings,$ChartDB)=@_; + my ($students,$studentInformation,$headings,$cache)=@_; - 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; - } + # Initialize Lengths + for(my $index=0; $index<(scalar @$headings); $index++) { + my @titleLength=split(//,$$headings[$index]); + $cache->{$$studentInformation[$index].'Length'}= + scalar @titleLength; + } - foreach my $name (@$students) { - foreach (@$studentInformation) { - my @dataLength=split(//,$CacheData{$name.':'.$_}); - my $length=scalar @dataLength; - if($length > $CacheData{$_.'Length'}) { - $CacheData{$_.'Length'}=$length; - } + foreach my $name (@$students) { + foreach (@$studentInformation) { + my @dataLength=split(//,$cache->{$name.':'.$_}); + my $length=scalar @dataLength; + if($length > $cache->{$_.'Length'}) { + $cache->{$_.'Length'}=$length; } } - untie(%CacheData); } return; } -sub ProcessFullName { - my ($lastname, $generation, $firstname, $middlename)=@_; - my $Str = ''; +=pod - if($lastname ne '') { - $Str .= $lastname.' '; - if($generation ne '') { - $Str .= $generation; - } else { - chop($Str); - } - $Str .= ', '; - if($firstname ne '') { - $Str .= $firstname.' '; - } - if($middlename ne '') { - $Str .= $middlename; - } else { - chop($Str); - if($firstname eq '') { - chop($Str); - } - } - } else { - if($firstname ne '') { - $Str .= $firstname.' '; - } - if($middlename ne '') { - $Str .= $middlename.' '; - } - if($generation ne '') { - $Str .= $generation; - } else { - chop($Str); - } - } +=item &SortStudents() - return $Str; -} +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)=@_; @@ -858,7 +873,7 @@ sub SortStudents { my ($end,$start)=split(/\:/,$CacheData->{$_.':date'}); my $active=1; my $now=time; - my $Status=$CacheData->{'form.status'}; + my $Status=$CacheData->{'form.Status'}; $Status = ($Status) ? $Status : 'Active'; if((($end) && $now > $end) && (($Status eq 'Active'))) { $active=0; @@ -871,7 +886,7 @@ sub SortStudents { } } - my $Pos = $CacheData->{'form.sort'}; + my $Pos = $CacheData->{'form.ChartSort'}; my %sortData; if($Pos eq 'Last Name') { for(my $index=0; $index= 10) { - return -1; - } +Determine if a specified column should be shown on the chart. - untie(%testData); +=over 4 - return $isCached; -} +Input: $cache, $test -sub ExtractStudentData { - my ($courseData, $name, $ChartDB)=@_; +$cache: A pointer to the hash tied to the cached data - 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); - } +$test: The form name of the column (heading.$headingIndex) or +(sequence.$sequenceIndex) - return; -} +Output: 0 (false), 1 (true) + +=back + +=cut sub ShouldShowColumn { my ($cache,$test)=@_; - if($cache->{'form.reset'} eq 'true') { + if($cache->{'form.ChartReset'} eq 'true') { return 1; } - my $headings=$cache->{'form.headings'}; - my $sequences=$cache->{'form.sequences'}; + my $headings=$cache->{'form.ChartHeadings'}; + my $sequences=$cache->{'form.ChartSequences'}; if($headings eq 'ALLHEADINGS' || $sequences eq 'ALLSEQUENCES' || $headings=~/$test/ || $sequences=~/$test/) { return 1; } -# my $reselected=$cache->{'form.reselect'}; -# if($reselected=~/$test/) { -# return 1; -# } - return 0; } -sub ProcessFormData { - my ($ChartDB)=@_; - my %CacheData; +# ----- END HELPER FUNCTIONS -------------------------------------------- - if(tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_WRCREAT,0640)) { - if(defined($ENV{'form.sort'})) { - $CacheData{'form.sort'}=$ENV{'form.sort'}; - } elsif(!defined($CacheData{'form.sort'})) { - $CacheData{'form.sort'}='username'; - } +=pod - # Ignore $ENV{'form.refresh'} - # Ignore $ENV{'form.recalculate'} - - if(defined($ENV{'form.status'})) { - $CacheData{'form.status'}=$ENV{'form.status'}; - } elsif(!defined($CacheData{'form.status'})) { - $CacheData{'form.status'}='Active'; - } +=head1 Handler and main function(BuildChart) - 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++; - } - } +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. - if($found) { - $CacheData{'form.headings'}=join(":::",@headings); - $CacheData{'form.sequences'}=join(":::",@sequences); - } +=cut - 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'}.=":::".$_; - } - } - } +=pod - if(defined($ENV{'form.reset'})) { - $CacheData{'form.reset'}='true'; - $CacheData{'form.status'}='Active'; - $CacheData{'form.sort'}='username'; - $CacheData{'form.headings'}='ALLHEADINGS'; - $CacheData{'form.sequences'}='ALLSEQUENCES'; - } else { - $CacheData{'form.reset'}='false'; - } +=item &BuildChart() - untie(%CacheData); - } + The following is the process that BuildChart goes through to + create the html document. - return; -} + -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 -# ----- END HELPER FUNCTIONS -------------------------------------------- +=over 4 + +Input: $r + +$r: Used to print html + +Output: None + +=back + +=cut sub BuildChart { my ($r)=@_; @@ -1056,7 +999,7 @@ sub BuildChart { # Start the lonchart document $r->content_type('text/html'); $r->send_http_header; - $r->print(&StartDocument()); + $r->print(&StartDocument('LON-CAPA Assessment Chart', 'Assessment Chart')); $r->rflush(); # Test for access to the CacheData @@ -1064,14 +1007,17 @@ sub BuildChart { my $cid=$ENV{'request.course.id'}; my $ChartDB = "/home/httpd/perl/tmp/$ENV{'user.name'}". "_$ENV{'user.domain'}_$cid\_chart.db"; - - $isCached=&TestCacheData($ChartDB); + 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->print("Unable to tie hash to db file"); $r->rflush(); return; } - &ProcessFormData($ChartDB); + &ProcessFormData($ChartDB, $isCached); # Download class list information if not using cached data my %CacheData; @@ -1080,24 +1026,65 @@ sub BuildChart { my @headings=('User Name','Domain','Section','PID','Full Name'); my $spacePadding=' '; if(!$isCached) { - my $processTopResourceMapReturn=&ProcessTopResourceMap($ChartDB,$c); + 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); + $r->print($processTopResourceMapReturn.''); + untie(%CacheData); return; } - if($c->aborted()) { return; } - my $classlist=&DownloadPrerequisiteData($cid, $c); + + 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()) { return; } - @students=&ProcessClassList($classlist,$cid,$ChartDB,$c); - if($c->aborted()) { 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, - $ChartDB); - if($c->aborted()) { return; } + \%CacheData); + + if($c->aborted()) { + untie(%CacheData); + $r->print(''); + return; + } + + untie(%CacheData); } else { if(!$c->aborted() && tie(%CacheData,'GDBM_File',$ChartDB, &GDBM_READER,0640)) { @@ -1106,28 +1093,36 @@ sub BuildChart { } # Sort students and print out table desciptive data + my $downloadTime=0; if(tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_READER,0640)) { if(!$c->aborted()) { @students=&SortStudents(\@students,\%CacheData); } + 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, - \@studentInformation, - \@headings, - $spacePadding)); } + \@headings)); } + if(!$c->aborted()) { $r->print('
'); } + if(!$c->aborted()) { $r->print('Note: Uncheck the boxes above a'); } + if(!$c->aborted()) { $r->print(' column to remove that column from'); } + if(!$c->aborted()) { $r->print(' the display.
'); } + if(!$c->aborted()) { $r->print('aborted()) { $r->print('cellspacing="0">'); } if(!$c->aborted()) { $r->print(&CreateColumnSelectors( \%CacheData, - \@studentInformation, - \@headings, - $spacePadding)); } + \@headings)); } if(!$c->aborted()) { $r->print(&CreateTableHeadings( \%CacheData, \@studentInformation, \@headings, $spacePadding)); } + if(!$c->aborted()) { $r->print('
'); } if(!$c->aborted()) { $r->rflush(); } untie(%CacheData); } else { @@ -1135,6 +1130,7 @@ sub BuildChart { return; } + # Output student data my @updateStudentList = (); my $courseData; $r->print('
');
@@ -1144,16 +1140,26 @@ sub BuildChart {
         }
 
         if(!$isCached) {
-            $courseData=&DownloadStudentCourseInformation($_, $cid);
+            $courseData=
+                &Apache::loncoursedata::DownloadStudentCourseInformation($_, 
+                                                                         $cid);
             if($c->aborted()) { last; }
-            push(@updateStudentList, $_);
-            &ExtractStudentData($courseData, $_, $ChartDB);
+            if(tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_WRCREAT,0640)) {
+                &Apache::loncoursedata::ProcessStudentData(\%CacheData, 
+                                                           $courseData, $_);
+                push(@updateStudentList, $_);
+                untie(%CacheData);
+            } else {
+                next;
+            }
         }
-        $r->print(&FormatStudentData($_, $cid, \@studentInformation,
+        $r->print(&FormatStudentData($_, \@studentInformation,
                                      $spacePadding, $ChartDB));
         $r->rflush();
     }
 
+    # If recalculating, store a list of students, but only if all their 
+    # data was downloaded.  Leave off the others.
     if(!$isCached && tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_WRCREAT,0640)) {
         $CacheData{'NamesOfStudents'}=join(":::", @updateStudentList);
 #		    $CacheData{'NamesOfStudents'}=
@@ -1161,6 +1167,7 @@ sub BuildChart {
         untie(%CacheData);
     }
 
+    # End document
     $r->print('
'); $r->rflush(); @@ -1169,6 +1176,26 @@ sub BuildChart { # ================================================================ Main Handler +=pod + +=item &handler() + +The handler checks for permission to access the course data and for +initial header problem. Then it passes the torch to the work horse +function BuildChart. + +=over 4 + +Input: $r + +$r: This is the object that is used to print. + +Output: A Value (OK or HTTP_NOT_ACCEPTABLE) + +=back + +=cut + sub handler { my $r=shift; # $jr=$r; @@ -1189,7 +1216,7 @@ sub handler { $r->send_http_header; return OK; } - + unless($ENV{'request.course.fn'}) { my $requrl=$r->uri; $ENV{'user.error.msg'}="$requrl:bre:0:0:Course not initialized";