--- loncom/interface/Attic/lonchart.pm 2001/01/31 23:28:06 1.2 +++ loncom/interface/Attic/lonchart.pm 2002/07/09 15:43:49 1.59 @@ -1,4 +1,30 @@ # The LearningOnline Network with CAPA +# (Publication Handler +# +# $Id: lonchart.pm,v 1.59 2002/07/09 15:43:49 stredwic Exp $ +# +# Copyright Michigan State University Board of Trustees +# +# This file is part of the LearningOnline Network with CAPA (LON-CAPA). +# +# LON-CAPA is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# LON-CAPA is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with LON-CAPA; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# /home/httpd/html/adm/gpl.txt +# +# http://www.lon-capa.org/ +# # Homework Performance Chart # # (Navigate Maps Handler @@ -6,294 +32,1191 @@ # (Page Handler # # (TeX Content Handler -# +# YEAR=2000 # 05/29/00,05/30 Gerd Kortemeyer) # 08/30,08/31,09/06,09/14,09/15,09/16,09/19,09/20,09/21,09/23, # 10/02,10/10,10/14,10/16,10/18,10/19,10/31,11/6,11/14,11/16 Gerd Kortemeyer) +# YEAR=2001 +# 3/1/1,6/1,17/1,29/1,30/1,31/1 Gerd Kortemeyer) +# 7/10/01 Behrouz Minaei +# 9/8 Gerd Kortemeyer +# 10/1, 10/19, 11/17, 11/22, 11/24, 11/28 12/18 Behrouz Minaei +# YEAR=2002 +# 2/1, 2/6, 2/19, 2/28 Behrouz Minaei # -# 3/1/1,6/1,17/1,29/1,30/1 Gerd Kortemeyer) -# -# 1/31 Gerd Kortemeyer +### + +=pod + +=head1 NAME + +lonchart + +=head1 SYNOPSIS + +Quick display of students grades for a course in a compressed table format. + +=head1 DESCRIPTION + +This module process all student grades for a course and turns them into a +table like structure. + +This is part of the LearningOnline Network with CAPA project +described at http://www.lon-capa.org + +lonchart presents the user with a condensed view all a course's data. The +class title, the number of students, and the date for the last update of the +displayed data. There is also a legend that describes the chart values. + +For each valid grade for a student is linked with a submission record for that +problem. The ability to add and remove columns of data from the chart was +added for reducing the burden of having to scroll through large quantities +of data. The interface also allows for sorting of students by username, +last name, and section number of class. Active and expired students are +also available. + +The interface is controlled by three primary buttons: Recalculate Chart, +Refresh Chart, and Reset Selections. Recalculate Chart will update +the chart to the most recent data and keep the display settings for the chart +the same. Refresh Chart is used to redisplay the chart after selecting +different output formatting. Reset Selections is used to set the chart +display options back to default values. + +=head1 CODE LAYOUT DESCRIPTION + +The code is broken down into three components: formatting data for printing, +helper functions, and the central processing functions. The module is broken +into chunks for each component. + +=head1 PACKAGES USED + + Apache::Constants qw(:common :http) + Apache::lonnet() + Apache::loncommon() + HTML::TokeParser + GDBM_File + +=cut package Apache::lonchart; use strict; use Apache::Constants qw(:common :http); use Apache::lonnet(); +use Apache::loncommon(); +use Apache::loncoursedata(); use HTML::TokeParser; use GDBM_File; -# -------------------------------------------------------------- Module Globals -my %hash; -my @cols; -my @rowlabels; -my @students; - -# ------------------------------------------------------------- Find out status - -sub astatus { - my ($rid,$student)=@_; - my ($uname,$udom)=split(/\:/,$student); - my $code=' '; - $rid=~/(\d+)\.(\d+)/; - my $symb=&Apache::lonnet::declutter($hash{'map_id_'.$1}).'___'.$2.'___'. - &Apache::lonnet::declutter($hash{'src_'.$rid}); - my $answer=&Apache::lonnet::reply( - "restore:$udom:$uname:". - $ENV{'request.course.id'}.':'. - &Apache::lonnet::escape($symb), - &Apache::lonnet::homeserver($uname,$udom)); - my %returnhash=(); - map { - my ($name,$value)=split(/\=/,$_); - $returnhash{&Apache::lonnet::unescape($name)}= - &Apache::lonnet::unescape($value); - } split(/\&/,$answer); - if ($returnhash{'version'}) { - my $version; - for ($version=1;$version<=$returnhash{'version'};$version++) { - map { - $returnhash{$_}=$returnhash{$version.':'.$_}; - } split(/\:/,$returnhash{$version.':keys'}); - } - my $totaltries=0; - map { - if (($_=~/\.(\w+)\.solved$/) && ($_!~/^\d+\:/)) { - my $part=$1; - if ($returnhash{$_} eq 'correct_by_student') { - unless (($code eq '.') || ($code eq '-')) { $code='*'; } - $totaltries+=$returnhash{'resource.'.$part.'.tries'}; - } elsif ($returnhash{$_} eq 'correct_by_override') { - unless (($code eq '.') || ($code eq '-')) { $code='+'; } - } elsif ($returnhash{$_} eq 'incorrect_attempted') { - $code='.'; - } elsif ($returnhash{$_} eq 'incorrect_by_override') { - $code='-'; - } elsif ($returnhash{$_} eq 'excused') { - unless (($code eq '.') || ($code eq '-')) { $code='x'; } - } - } - } keys %returnhash; - if (($code eq '*') && ($totaltries<10)) { $code="$totaltries"; } - } - return $code; -} - -# ------------------------------------------------------------ Build page table - -sub tracetable { - my ($rid,$beenhere)=@_; - unless ($beenhere=~/\&$rid\&/) { - $beenhere.=$rid.'&'; - if (defined($hash{'is_map_'.$rid})) { - if ($hash{'map_type_'.$hash{'map_pc_'.$hash{'src_'.$rid}}} - eq 'sequence') { - $cols[$#cols+1]=0; - } - if ((defined($hash{'map_start_'.$hash{'src_'.$rid}})) && - (defined($hash{'map_finish_'.$hash{'src_'.$rid}}))) { - my $frid=$hash{'map_finish_'.$hash{'src_'.$rid}}; - - &tracetable($hash{'map_start_'.$hash{'src_'.$rid}}, - '&'.$frid.'&'); - - if ($hash{'src_'.$frid}) { - if ($hash{'src_'.$frid}=~ - /\.(problem|exam|quiz|assess|survey|form)$/) { - $cols[$#cols+1]=$frid; - } - } - - } - } else { - if ($hash{'src_'.$rid}) { - if ($hash{'src_'.$rid}=~ - /\.(problem|exam|quiz|assess|survey|form)$/) { - $cols[$#cols+1]=$rid; - } - } - } - if (defined($hash{'to_'.$rid})) { - map { - &tracetable($hash{'goesto_'.$_},$beenhere); - } split(/\,/,$hash{'to_'.$rid}); - } +#my $jr; + +=pod + +=head1 FORMAT DATA FOR PRINTING + +=cut + +# ----- FORMAT PRINT DATA ---------------------------------------------- + +=pod + +=item &FormatStudentInformation() + +This function produces a formatted string of the student's information: +username, domain, section, full name, and PID. + +=over 4 + +Input: $cache, $name, $studentInformation, $spacePadding + +$cache: This is a pointer to a hash that is tied to the cached data + +$name: The name and domain of the current student in name:domain format + +$studentInformation: A pointer to an array holding the names used to + +remove data from the hash. They represent the name of the data to be removed. + +$spacePadding: Extra spaces that represent the space between columns + +Output: $Str + +$Str: Formatted string. + +=back + +=cut + +sub FormatStudentInformation { + my ($cache,$name,$studentInformation,$spacePadding)=@_; + my $Str=''; + + for(my $index=0; $index<(scalar @$studentInformation); $index++) { + if(!&ShouldShowColumn($cache, 'ChartHeading'.$index)) { + next; + } + my $data=$cache->{$name.':'.$studentInformation->[$index]}; + $Str .= $data; + + my @dataLength=split(//,$data); + my $length=scalar @dataLength; + $Str .= (' 'x($cache->{$studentInformation->[$index].'Length'}- + $length)); + $Str .= $spacePadding; } + + return $Str; } -# ================================================================ Main Handler +=pod -sub handler { - my $r=shift; +=item &FormatStudentData() + +First, FormatStudentInformation is called and prefixes the course information. +This function produces a formatted string of the student's course information. +Each column of data represents all the problems for a given sequence. For +valid grade data, a link is created for that problem to a submission record +for that problem. + +=over 4 + +Input: $name, $studentInformation, $spacePadding, $ChartDB + +$name: The name and domain of the current student in name:domain format + +$studentInformation: A pointer to an array holding the names used to +remove data from the hash. They represent +the name of the data to be removed. + +$spacePadding: Extra spaces that represent the space between columns + +$ChartDB: The name of the cached data database which will be tied to that +database. + +Output: $Str + +$Str: Formatted string that is an entire row of the chart. It is a +concatenation of student information and student course information. + +=back + +=cut + +sub FormatStudentData { + my ($name,$studentInformation,$spacePadding,$ChartDB)=@_; + my ($sname,$sdom) = split(/\:/,$name); + my $Str; + my %CacheData; + + unless(tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_READER,0640)) { + return ''; + } + # Handle Student information ------------------------------------------ + # Handle user data + $Str=&FormatStudentInformation(\%CacheData, $name, $studentInformation, + $spacePadding); + + # Handle errors + if($CacheData{$name.':error'} =~ /environment/) { + $Str .= '<br>'; + untie(%CacheData); + return $Str; + } + + if($CacheData{$name.':error'} =~ /course/) { + $Str .= '<br>'; + untie(%CacheData); + return $Str; + } + + # Handle problem data ------------------------------------------------ + my $Version; + my $problemsCorrect = 0; + my $totalProblems = 0; + my $problemsSolved = 0; + my $numberOfParts = 0; + foreach my $sequence (split(/\:/,$CacheData{'orderedSequences'})) { + if(!&ShouldShowColumn(\%CacheData, 'ChartSequence'.$sequence)) { + next; + } + + my $characterCount=0; + foreach my $problemID (split(/\:/,$CacheData{$sequence.':problems'})) { + my $problem = $CacheData{$problemID.':problem'}; + my $LatestVersion = $CacheData{$name.":version:$problem"}; + + # Output blanks for all the parts of this problem if there + # is no version information about the current problem. + if(!$LatestVersion) { + foreach my $part (split(/\:/,$CacheData{$sequence.':'. + $problemID. + ':parts'})) { + $Str .= ' '; + $totalProblems++; + $characterCount++; + } + next; + } + + my %partData=undef; + # Initialize part data, display skips correctly + # Skip refers to when a student made no submissions on that + # part/problem. + foreach my $part (split(/\:/,$CacheData{$sequence.':'. + $problemID. + ':parts'})) { + $partData{$part.':tries'}=0; + $partData{$part.':code'}=' '; + } + + # Looping through all the versions of each part, starting with the + # oldest version. Basically, it gets the most recent + # set of grade data for each part. + for(my $Version=1; $Version<=$LatestVersion; $Version++) { + foreach my $part (split(/\:/,$CacheData{$sequence.':'. + $problemID. + ':parts'})) { + + if(!defined($CacheData{$name.":$Version:$problem". + ":resource.$part.solved"})) { + # No grade for this submission, so skip + next; + } + + my $tries=0; + my $code=' '; + + $tries = $CacheData{$name.":$Version:$problem". + ":resource.$part.tries"}; + $partData{$part.':tries'}=($tries) ? $tries : 0; + + my $val = $CacheData{$name.":$Version:$problem". + ":resource.$part.solved"}; + if ($val eq 'correct_by_student') {$code = '*';} + elsif ($val eq 'correct_by_override') {$code = '+';} + elsif ($val eq 'incorrect_attempted') {$code = '.';} + elsif ($val eq 'incorrect_by_override'){$code = '-';} + elsif ($val eq 'excused') {$code = 'x';} + elsif ($val eq 'ungraded_attempted') {$code = '#';} + else {$code = ' ';} + $partData{$part.':code'}=$code; + } + } + + # All grades (except for versionless parts) are displayed as links + # to their submission record. Loop through all the parts for the + # current problem in the correct order and prepare the output links + $Str.='<a href="/adm/grades?symb='. + &Apache::lonnet::escape($problem). + '&student='.$sname.'&domain='.$sdom.'&command=submission">'; + foreach(split(/\:/,$CacheData{$sequence.':'.$problemID. + ':parts'})) { + if($partData{$_.':code'} eq '*') { + $problemsCorrect++; + if (($partData{$_.':tries'}<10) && + ($partData{$_.':tries'} ne '')) { + $partData{$_.':code'}=$partData{$_.':tries'}; + } + } elsif($partData{$_.':code'} eq '+') { + $problemsCorrect++; + } - if (&Apache::lonnet::allowed('vgr',$ENV{'request.course.id'})) { -# ------------------------------------------- Set document type for header only + $Str .= $partData{$_.':code'}; + $characterCount++; - if ($r->header_only) { - if ($ENV{'browser.mathml'}) { - $r->content_type('text/xml'); - } else { - $r->content_type('text/html'); - } - $r->send_http_header; - return OK; - } - - my $requrl=$r->uri; -# ----------------------------------------------------------------- Tie db file - if ($ENV{'request.course.fn'}) { - my $fn=$ENV{'request.course.fn'}; - if (-e "$fn.db") { - if (tie(%hash,'GDBM_File',"$fn.db",&GDBM_READER,0640)) { -# ------------------------------------------------------------------- Hash tied - - -# ------------------------------------------------------------------ Build page - -# ---------------------------------------------------------------- Send headers - - $r->content_type('text/html'); - $r->send_http_header; - $r->print( - '<html><head><title>LON-CAPA Assessment Chart</title></head>'); - - $r->print('<body bgcolor="#FFFFFF">'. - '<script>window.focus();</script>'. - '<img align=right src=/adm/lonIcons/lonlogos.gif>'. - '<h1>Assessment Chart</h1>'); - -# ---------------------------------------------------------------- Course title - - $r->print('<h1>'. - $ENV{'course.'.$ENV{'request.course.id'}.'.description'}.'</h1>'); - - -# ------------------------------- This is going to take a while, produce output - - $r->rflush(); - -# ----------------------- Get first and last resource, see if there is anything - - - my $firstres=$hash{'map_start_/res/'.$ENV{'request.course.uri'}}; - my $lastres=$hash{'map_finish_/res/'.$ENV{'request.course.uri'}}; - if (($firstres) && ($lastres)) { -# ----------------------------------------------------------------- Render page - - my $cid=$ENV{'request.course.id'}; - my $chome=$ENV{'course.'.$cid.'.home'}; - my ($cdom,$cnum)=split(/\_/,$cid); - -# ---------------------------------------------- Read class list and row labels - - undef @rowlabels; - undef @students; - - my $classlst=&Apache::lonnet::reply - ('dump:'.$cdom.':'.$cnum.':classlist',$chome); - my $now=time; - unless ($classlst=~/^error\:/) { - map { - my ($name,$value)=split(/\=/,$_); - my ($end,$start)=split(/\:/,&Apache::lonnet::unescape($value)); - my $active=1; - if (($end) && ($now>$end)) { $active=0; } - if ($active) { - my $thisindex=$#students+1; - $name=&Apache::lonnet::unescape($name); - $students[$thisindex]=$name; - my ($sname,$sdom)=split(/\:/,$name); - my $ssec=&Apache::lonnet::usection($sdom,$sname,$cid); - if ($ssec==-1) { - $rowlabels[$thisindex]= - 'Data not available: '.$name; - } else { - my %reply=&Apache::lonnet::idrget($sdom,$sname); - my $reply=&Apache::lonnet::reply('get:'.$sdom.':'.$sname. - ':environment:firstname&middlename&lastname&generation', - &Apache::lonnet::homeserver($sname,$sdom)); - $rowlabels[$thisindex]= - $ssec.' '.$reply{$sname}.' '; - map { - $rowlabels[$thisindex].=&Apache::lonnet::unescape($_).' '; - } split(/\&/,$reply); + if($partData{$_.':code'} ne 'x') { + $totalProblems++; } } - } sort split(/\&/,$classlst); + $Str.='</a>'; + } + + # Output the number of correct answers for the current sequence. + # This part takes up 6 character slots, but is formated right + # justified. + my $spacesNeeded=$CacheData{$sequence.':columnWidth'}-$characterCount; + $spacesNeeded -= 3; + $Str .= (' 'x$spacesNeeded); + + my $outputProblemsCorrect = sprintf( "%3d", $problemsCorrect ); + $Str .= '<font color="#007700">'.$outputProblemsCorrect.'</font>'; + $problemsSolved += $problemsCorrect; + $problemsCorrect=0; + + $Str .= $spacePadding; + } + + # Output the total correct problems over the total number of problems. + # I don't like this type of formatting, but it is a solution. Need + # a way to dynamically determine the space requirements. + my $outputProblemsSolved = sprintf( "%4d", $problemsSolved ); + my $outputTotalProblems = sprintf( "%4d", $totalProblems ); + $Str .= '<font color="#000088">'.$outputProblemsSolved. + ' / '.$outputTotalProblems.'</font><br>'; + + untie(%CacheData); + return $Str; +} + +=pod + +=item &CreateTableHeadings() + +This function generates the column headings for the chart. + +=over 4 + +Inputs: $CacheData, $studentInformation, $headings, $spacePadding + +$CacheData: pointer to a hash tied to the cached data database + +$studentInformation: a pointer to an array containing the names of the data +held in a column and is used as part of a key into $CacheData + +$headings: The names of the headings for the student information + +$spacePadding: The spaces to go between columns + +Output: $Str + +$Str: A formatted string of the table column headings. + +=back + +=cut + +sub CreateTableHeadings { + my ($CacheData,$studentInformation,$headings,$spacePadding)=@_; + my $Str='<tr>'; + + for(my $index=0; $index<(scalar @$headings); $index++) { + if(!&ShouldShowColumn($CacheData, 'ChartHeading'.$index)) { + next; + } + + $Str .= '<td align="left"><pre>'; + my $data=$$headings[$index]; + $Str .= $data; + + my @dataLength=split(//,$data); + my $length=scalar @dataLength; + $Str .= (' 'x($CacheData->{$$studentInformation[$index].'Length'}- + $length)); + $Str .= $spacePadding; + $Str .= '</pre></td>'; + } + + foreach my $sequence (split(/\:/,$CacheData->{'orderedSequences'})) { + if(!&ShouldShowColumn($CacheData, 'ChartSequence'.$sequence)) { + next; + } + + $Str .= '<td align="left"><pre>'; + my $name = $CacheData->{$sequence.':title'}; + $Str .= $name; + my @titleLength=split(//,$CacheData->{$sequence.':title'}); + my $leftover=$CacheData->{$sequence.':columnWidth'}- + (scalar @titleLength); + $Str .= (' 'x$leftover); + $Str .= $spacePadding; + $Str .= '</pre></td>'; + } + + $Str .= '<td><pre>Total Solved/Total Problems</pre></td>'; + $Str .= '</tr>'; + + return $Str; +} + +=pod + +=item &CreateColumnSelectionBox() + +If there are columns not being displayed then this selection box is created +with a list of those columns. When selections are made and the page +refreshed, the columns will be removed from this box and the column is +put back in the chart. If there is no columns to select, no row is added +to the interface table. + +=over 4 +Input: $CacheData, $headings + + +$CacheData: A pointer to a hash tied to the cached data +$headings: An array of the names of the columns for the student information. +They are used for displaying which columns are missing. + +Output: $notThere + +$notThere: The string contains one row of a table. The first column has the +name of the selection box. The second contains the selection box +which has a size of four. + +=back + +=cut + +sub CreateColumnSelectionBox { + my ($CacheData,$headings)=@_; + + my $missing=0; + my $notThere='<tr><td align="right"><b>Select column to view:</b>'; + my $name; + $notThere .= '<td align="left">'; + $notThere .= '<select name="ChartReselect" size="4" multiple="true">'."\n"; + + for(my $index=0; $index<(scalar @$headings); $index++) { + if(&ShouldShowColumn($CacheData, 'ChartHeading'.$index)) { + next; + } + $name = $headings->[$index]; + $notThere .= '<option value="ChartHeading'.$index.'">'; + $notThere .= $name.'</option>'."\n"; + $missing++; + } + + foreach my $sequence (split(/\:/,$CacheData->{'orderedSequences'})) { + if(&ShouldShowColumn($CacheData, 'ChartSequence'.$sequence)) { + next; + } + $name = $CacheData->{$sequence.':title'}; + $notThere .= '<option value="ChartSequence'.$sequence.'">'; + $notThere .= $name.'</option>'."\n"; + $missing++; + } + + if($missing) { + $notThere .= '</select>'; } else { - $r->print('<h1>Could not access course data</h1>'); + $notThere='<tr><td>'; } - my $allstudents=$#students+1; - $r->print('<h3>'.$allstudents.' students</h3>'); - $r->rflush(); + return $notThere.'</td></tr>'; +} + +=pod + +=item &CreateColumnSelectors() + +This function generates the checkboxes above the column headings. The +column will be removed if the checkbox is unchecked. + +=over 4 + +Input: $CacheData, $headings + +$CacheData: A pointer to a hash tied to the cached data -# --------------- Find all assessments and put them into some linear-like order +$headings: An array of the names of the columns for the student +information. They are used to know what are the student information columns - &tracetable($firstres,'&'.$lastres.'&'); +Output: $present -# ----------------------------------------------------------------- Start table +$present: The string contains the first row of a table. Each column contains +a checkbox which is left justified. Currently left justification is used +for consistency of location over the column in which it presides. - $r->print('<p><pre>'); - my $index; - for ($index=0;$index<=$#students;$index++) { - $r->print( - substr($students[$index]. - ' ',0,14).' ! '. - substr($rowlabels[$index]. - ' ',0,45).' ! '); - map { - if ($_) { - $r->print(&astatus($_,$students[$index])); - } else { - $r->print(' ! '); - } - } @cols; - $r->print("\n"); - $r->rflush(); - } - $r->print('</pre>'); - - } else { - $r->print('<h3>Undefined course sequence</h3>'); - } - - $r->print('</body></html>'); - -# ------------------------------------------------------------- End render page - } else { - $r->content_type('text/html'); - $r->send_http_header; - $r->print('<html><body>Coursemap undefined.</body></html>'); - } -# ------------------------------------------------------------------ Untie hash - unless (untie(%hash)) { - &Apache::lonnet::logthis("<font color=blue>WARNING: ". - "Could not untie coursemap $fn (browse).</font>"); - } - -# -------------------------------------------------------------------- All done - return OK; -# ----------------------------------------------- Errors, hash could no be tied - } - } else { - $ENV{'user.error.msg'}="$requrl:bre:0:0:Course not initialized"; - return HTTP_NOT_ACCEPTABLE; +=back + +=cut + +sub CreateColumnSelectors { + my ($CacheData,$headings)=@_; + + my $found=0; + my ($name, $length, $position); + + my $present = '<tr>'; + for(my $index=0; $index<(scalar @$headings); $index++) { + if(!&ShouldShowColumn($CacheData, 'ChartHeading'.$index)) { + next; + } + $present .= '<td align="left">'; + $present .= '<input type="checkbox" checked="on" '; + $present .= 'name="ChartHeading'.$index.'" />'; + $present .= '</td>'; + $found++; + } + + foreach my $sequence (split(/\:/,$CacheData->{'orderedSequences'})) { + if(!&ShouldShowColumn($CacheData, 'ChartSequence'.$sequence)) { + next; + } + $present .= '<td align="left">'; + $present .= '<input type="checkbox" checked="on" '; + $present .= 'name="ChartSequence'.$sequence.'" />'; + $present .= '</td>'; + $found++; + } + + if(!$found) { + $present = ''; + } + + return $present.'<td></td></tr></form>'."\n";; } -} else { - $ENV{'user.error.msg'}= - $r->uri.":vgr:0:0:Cannot view grades for complete course"; - return HTTP_NOT_ACCEPTABLE; +=pod + +=item &CreateForm() + +The interface for this module consists primarily of the controls in this +function. The student status selection (active, expired, any) is set here. +The sort buttons: username, last name, and section are set here. The +other buttons are Recalculate Chart, Refresh Chart, and Reset Selections. +These controls are in a table to clean up the interface. + +=over 4 + +Input: $CacheData + +$CacheData is a hash pointer to tied database for cached data. + +Output: $Ptr + +$Ptr is a string containing all the html for the above mentioned buttons. + +=back + +=cut + +sub CreateForm { + my ($CacheData)=@_; + my $OpSel1=''; + my $OpSel2=''; + my $OpSel3=''; + my $Status = $CacheData->{'form.ChartStatus'}; + if ( $Status eq 'Any' ) { $OpSel3='selected'; } + elsif ($Status eq 'Expired' ) { $OpSel2 = 'selected'; } + else { $OpSel1 = 'selected'; } + + my $Ptr .= '<form name="ChartStat" method="post" action="/adm/chart" >'; + $Ptr .= "\n"; + $Ptr .= '<tr><td align="right">'; + $Ptr .= '</td><td align="left">'; + $Ptr .= '<input type="submit" name="ChartRecalculate" '; + $Ptr .= 'value="Recalculate Chart"/>'."\n"; + $Ptr .= ' '; + $Ptr .= '<input type="submit" name="ChartRefresh" '; + $Ptr .= 'value="Refresh Chart"/>'."\n"; + $Ptr .= ' '; + $Ptr .= '<input type="submit" name="ChartReset" '; + $Ptr .= 'value="Reset Selections"/></td>'."\n"; + $Ptr .= '</tr><tr><td align="right">'; + $Ptr .= '<b> Sort by: </b>'."\n"; + $Ptr .= '</td><td align="left">'; + $Ptr .= '<input type="submit" name="ChartSort" value="User Name" />'."\n"; + $Ptr .= ' '; + $Ptr .= '<input type="submit" name="ChartSort" value="Last Name" />'."\n"; + $Ptr .= ' '; + $Ptr .= '<input type="submit" name="ChartSort" value="Section"/>'."\n"; + $Ptr .= '</td></tr><tr><td align="right">'; + $Ptr .= '<b> Student Status: </b>'."\n". + '</td><td align="left">'. + '<select name="ChartStatus">'. + '<option '.$OpSel1.' >Active</option>'."\n". + '<option '.$OpSel2.' >Expired</option>'."\n". + '<option '.$OpSel3.' >Any</option> </select> '."\n"; + $Ptr .= '</td></tr>'; + + return $Ptr; } + +=pod + +=item &CreateLegend() + +This function returns a formatted string containing the legend for the +chart. The legend describes the symbols used to represent grades for +problems. + +=cut + +sub CreateLegend { + my $Str = "<p><pre>". + "1..9: correct by student in 1..9 tries\n". + " *: correct by student in more than 9 tries\n". + " +: correct by override\n". + " -: incorrect by override\n". + " .: incorrect attempted\n". + " #: ungraded attempted\n". + " : not attempted\n". + " x: excused". + "</pre><p>"; + return $Str; } -1; -__END__ +=pod + +=item &StartDocument() + +Returns a string containing the header information for the chart: title, +logo, and course title. + +=cut + +sub StartDocument { + my ($title, $header)=@_; + my $Str = ''; + $Str .= '<html>'; + $Str .= '<head><title>'; + $Str .= $title.'</title></head>'; + $Str .= '<body bgcolor="#FFFFFF">'; + $Str .= '<script>window.focus();</script>'; + $Str .= '<img align=right src=/adm/lonIcons/lonlogos.gif>'; + $Str .= '<h1>'.$header.'</h1>'; + $Str .= '<h1>'.$ENV{'course.'.$ENV{'request.course.id'}.'.description'}; + $Str .= '</h1>'; + + return $Str; +} + +# ----- END FORMAT PRINT DATA ------------------------------------------ + +=pod + +=head1 HELPER FUNCTIONS + +These are just a couple of functions do various odd and end +jobs. + +=cut + +# ----- HELPER FUNCTIONS ----------------------------------------------- + +=pod + +=item &ProcessFormData() + +Cache form data and set default form data (sort, status, heading.$number, +sequence.$number, reselect, reset, recalculate, and refresh) + +=over 4 + +Input: $ChartDB, $isCached + +$ChartDB: The name of the database for cached data + +$isCached: Is there already data for this course cached. This is used in +conjunction with the absence of all form data to know to display all selection +types. + +Output: None + +=back + +=cut + +# For all data, if ENV data doesn't exist for it, default values is used. +sub ProcessFormData { + my ($ChartDB, $isCached)=@_; + my %CacheData; + + if(tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_WRCREAT,0640)) { + # Ignore $ENV{'form.ChartRefresh'} + # Ignore $ENV{'form.ChartRecalculate'} + + if(defined($ENV{'form.ChartSort'})) { + $CacheData{'form.ChartSort'}=$ENV{'form.ChartSort'}; + } elsif(!defined($CacheData{'form.ChartSort'})) { + $CacheData{'form.ChartSort'}='username'; + } + + if(defined($ENV{'form.ChartStatus'})) { + $CacheData{'form.ChartStatus'}=$ENV{'form.ChartStatus'}; + } elsif(!defined($CacheData{'form.ChartStatus'})) { + $CacheData{'form.ChartStatus'}='Active'; + } + + # $found checks for any instances of form data in the ENV. If it is + # missing I assume the chrt button on the remote has been pressed. + my @headings=(); + my @sequences=(); + my $found=0; + foreach (keys(%ENV)) { + if(/form\.ChartHeading/) { + $found++; + push(@headings, $_); + } elsif(/form\.ChartSequence/) { + $found++; + push(@sequences, $_); + } elsif(/form\./) { + $found++; + } + } + + if($found) { + $CacheData{'form.ChartHeadings'}=join(":::",@headings); + $CacheData{'form.ChartSequences'}=join(":::",@sequences); + } + + if(defined($ENV{'form.ChartReselect'})) { + my @reselected = (ref($ENV{'form.ChartReselect'}) ? + @{$ENV{'form.ChartReselect'}} + : ($ENV{'form.ChartReselect'})); + foreach (@reselected) { + if(/ChartHeading/) { + $CacheData{'form.ChartHeadings'}.=":::".$_; + } elsif(/ChartSequence/) { + $CacheData{'form.ChartSequences'}.=":::".$_; + } + } + } + + # !$found and !$isCached are how I determine if the chrt button + # on the remote was pressed and needs to reset all the selections + if(defined($ENV{'form.ChartReset'}) || (!$found && !$isCached)) { + $CacheData{'form.ChartReset'}='true'; + $CacheData{'form.ChartStatus'}='Active'; + $CacheData{'form.ChartSort'}='username'; + $CacheData{'form.ChartHeadings'}='ALLHEADINGS'; + $CacheData{'form.ChartSequences'}='ALLSEQUENCES'; + } else { + $CacheData{'form.ChartReset'}='false'; + } + + untie(%CacheData); + } + + return; +} + +=pod + +=item &SpaceColumns() + +Determines the width of all the columns in the chart. It is based on +the max of the data for that column and its header. + +=over 4 + +Input: $students, $studentInformation, $headings, $ChartDB + +$students: An array pointer to a list of students (username:domain) + +$studentInformatin: The type of data for the student information. It is +used as part of the key in $CacheData. + +$headings: The name of the student information columns. + +$ChartDB: The name of the cache database which is opened for read/write. + +Output: None - All data stored in cache. + +=back + +=cut + +sub SpaceColumns { + my ($students,$studentInformation,$headings,$cache)=@_; + + # Initialize Lengths + for(my $index=0; $index<(scalar @$headings); $index++) { + my @titleLength=split(//,$$headings[$index]); + $cache->{$$studentInformation[$index].'Length'}= + scalar @titleLength; + } + + foreach my $name (@$students) { + foreach (@$studentInformation) { + my @dataLength=split(//,$cache->{$name.':'.$_}); + my $length=scalar @dataLength; + if($length > $cache->{$_.'Length'}) { + $cache->{$_.'Length'}=$length; + } + } + } + + return; +} + +=pod + +=item &SortStudents() + +Determines which students to display and in which order. Which are +displayed are determined by their status(active/expired). The order +is determined by the sort button pressed (default to username). The +type of sorting is username, lastname, or section. + +=over 4 + +Input: $students, $CacheData + +$students: A array pointer to a list of students (username:domain) + +$CacheData: A pointer to the hash tied to the cached data + +Output: @order + +@order: An ordered list of students (username:domain) + +=back + +=cut + +sub SortStudents { + my ($students,$CacheData)=@_; + + my @sorted1Students=(); + foreach (@$students) { + my ($end,$start)=split(/\:/,$CacheData->{$_.':date'}); + my $active=1; + my $now=time; + my $Status=$CacheData->{'form.ChartStatus'}; + $Status = ($Status) ? $Status : 'Active'; + if((($end) && $now > $end) && (($Status eq 'Active'))) { + $active=0; + } + if(($Status eq 'Expired') && ($end == 0 || $now < $end)) { + $active=0; + } + if($active) { + push(@sorted1Students, $_); + } + } + + my $Pos = $CacheData->{'form.ChartSort'}; + my %sortData; + if($Pos eq 'Last Name') { + for(my $index=0; $index<scalar @sorted1Students; $index++) { + $sortData{$CacheData->{$sorted1Students[$index].':fullname'}}= + $sorted1Students[$index]; + } + } elsif($Pos eq 'Section') { + for(my $index=0; $index<scalar @sorted1Students; $index++) { + $sortData{$CacheData->{$sorted1Students[$index].':section'}. + $sorted1Students[$index]}=$sorted1Students[$index]; + } + } else { + # Sort by user name + for(my $index=0; $index<scalar @sorted1Students; $index++) { + $sortData{$sorted1Students[$index]}=$sorted1Students[$index]; + } + } + + my @order = (); + foreach my $key (sort(keys(%sortData))) { + push (@order,$sortData{$key}); + } + + return @order; +} + +=pod + +=item &ShouldShowColumn() + +Determine if a specified column should be shown on the chart. + +=over 4 + +Input: $cache, $test + +$cache: A pointer to the hash tied to the cached data + +$test: The form name of the column (heading.$headingIndex) or +(sequence.$sequenceIndex) + +Output: 0 (false), 1 (true) + +=back + +=cut + +sub ShouldShowColumn { + my ($cache,$test)=@_; + + if($cache->{'form.ChartReset'} eq 'true') { + return 1; + } + + my $headings=$cache->{'form.ChartHeadings'}; + my $sequences=$cache->{'form.ChartSequences'}; + if($headings eq 'ALLHEADINGS' || $sequences eq 'ALLSEQUENCES' || + $headings=~/$test/ || $sequences=~/$test/) { + return 1; + } + + return 0; +} + +# ----- END HELPER FUNCTIONS -------------------------------------------- + +=pod + +=head1 Handler and main function(BuildChart) + +The handler does some initial error checking and then passes the torch to +BuildChart. BuildChart calls all the appropriate functions to get the +job done. These are the only two functions that use print ($r). All other +functions return strings to BuildChart to be printed. + +=cut + +=pod + +=item &BuildChart() + + The following is the process that BuildChart goes through to + create the html document. + + -Start the lonchart document + -Test for access to the CacheData + -Download class list information if not using cached data + -Sort students and print out table desciptive data + -Output student data + -If recalculating, store a list of students, but only if all + their data was downloaded. Leave off the others. + -End document +=over 4 +Input: $r +$r: Used to print html +Output: None +=back +=cut + +sub BuildChart { + my ($r)=@_; + my $c = $r->connection; + + # Start the lonchart document + $r->content_type('text/html'); + $r->send_http_header; + $r->print(&StartDocument('LON-CAPA Assessment Chart', 'Assessment Chart')); + $r->rflush(); + + # Test for access to the CacheData + my $isCached=0; + my $cid=$ENV{'request.course.id'}; + my $ChartDB = "/home/httpd/perl/tmp/$ENV{'user.name'}". + "_$ENV{'user.domain'}_$cid\_chart.db"; + my $isRecalculate=0; + if(defined($ENV{'form.ChartRecalculate'})) { + $isRecalculate=1; + } + $isCached=&Apache::loncoursedata::TestCacheData($ChartDB, $isRecalculate); + if($isCached < 0) { + $r->print("Unable to tie hash to db file</body></html>"); + $r->rflush(); + return; + } + &ProcessFormData($ChartDB, $isCached); + + # Download class list information if not using cached data + my %CacheData; + my @students=(); + my @studentInformation=('username','domain','section','id','fullname'); + my @headings=('User Name','Domain','Section','PID','Full Name'); + my $spacePadding=' '; + if(!$isCached) { + unless(tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_WRCREAT,0640)) { + $r->print("Unable to tie hash to db file</body></html>"); + $r->rflush(); + return; + } + + my $processTopResourceMapReturn= + &Apache::loncoursedata::ProcessTopResourceMap(\%CacheData,$c); + if($processTopResourceMapReturn ne 'OK') { + $r->print($processTopResourceMapReturn.'</body></html>'); + untie(%CacheData); + return; + } + + if($c->aborted()) { + untie(%CacheData); + $r->print('</body></html>'); + return; + } + + my $classlist=&Apache::loncoursedata::DownloadStudentNamePIDSection( + $cid, $c); + my ($checkForError)=keys(%$classlist); + if($checkForError =~ /^(con_lost|error|no_such_host)/i || + defined($classlist->{'error'})) { + $r->print("Error getting student data.</body></html>"); + $r->rflush(); + untie(%CacheData); + return; + } + + if($c->aborted()) { + untie(%CacheData); + $r->print('</body></html>'); + return; + } + + + @students=&Apache::loncoursedata::ProcessClassList(\%CacheData, + $classlist, $cid, + $CacheData{'form.ChartStatus'}, + $c); + + if($c->aborted()) { + untie(%CacheData); + $r->print('</body></html>'); + return; + } + + &SpaceColumns(\@students,\@studentInformation,\@headings, + \%CacheData); + + if($c->aborted()) { + untie(%CacheData); + $r->print('</body></html>'); + return; + } + + untie(%CacheData); + } else { + if(!$c->aborted() && tie(%CacheData,'GDBM_File',$ChartDB, + &GDBM_READER,0640)) { + @students=split(/:::/,$CacheData{'NamesOfStudents'}); + } + } + + # Sort students and print out table desciptive data + my $downloadTime=0; + if(tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_READER,0640)) { + if(!$c->aborted()) { @students=&SortStudents(\@students,\%CacheData); } + if(defined($CacheData{'time'})) { $downloadTime=$CacheData{'time'}; } + else { $downloadTime=localtime(); } + if(!$c->aborted()) { $r->print('<h3>'.$downloadTime.'</h3>'); } + if(!$c->aborted()) { $r->print('<h1>'.(scalar @students). + ' students</h1>'); } + if(!$c->aborted()) { $r->rflush(); } + if(!$c->aborted()) { $r->print(&CreateLegend()); } + if(!$c->aborted()) { $r->print('<table border="0"><tbody>'); } + if(!$c->aborted()) { $r->print(&CreateForm(\%CacheData)); } + if(!$c->aborted()) { $r->print(&CreateColumnSelectionBox( + \%CacheData, + \@headings)); } + if(!$c->aborted()) { $r->print('</tbody></table>'); } + if(!$c->aborted()) { $r->print('<b>Note: Uncheck the boxes above a'); } + if(!$c->aborted()) { $r->print(' column to remove that column from'); } + if(!$c->aborted()) { $r->print(' the display.</b></pre>'); } + if(!$c->aborted()) { $r->print('<table border="0" cellpadding="0" '); } + if(!$c->aborted()) { $r->print('cellspacing="0"><tbody>'); } + if(!$c->aborted()) { $r->print(&CreateColumnSelectors( + \%CacheData, + \@headings)); } + if(!$c->aborted()) { $r->print(&CreateTableHeadings( + \%CacheData, + \@studentInformation, + \@headings, + $spacePadding)); } + if(!$c->aborted()) { $r->print('</tbody></table>'); } + if(!$c->aborted()) { $r->rflush(); } + untie(%CacheData); + } else { + $r->print("Init2: Unable to tie hash to db file"); + return; + } + + # Output student data + my @updateStudentList = (); + my $courseData; + $r->print('<pre>'); + foreach (@students) { + if($c->aborted()) { + last; + } + + if(!$isCached) { + $courseData= + &Apache::loncoursedata::DownloadStudentCourseInformation($_, + $cid); + if($c->aborted()) { last; } + if(tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_WRCREAT,0640)) { + &Apache::loncoursedata::ProcessStudentData(\%CacheData, + $courseData, $_); + push(@updateStudentList, $_); + untie(%CacheData); + } else { + next; + } + } + $r->print(&FormatStudentData($_, \@studentInformation, + $spacePadding, $ChartDB)); + $r->rflush(); + } + + # If recalculating, store a list of students, but only if all their + # data was downloaded. Leave off the others. + if(!$isCached && tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_WRCREAT,0640)) { + $CacheData{'NamesOfStudents'}=join(":::", @updateStudentList); +# $CacheData{'NamesOfStudents'}= +# &Apache::lonnet::arrayref2str(\@updateStudentList); + untie(%CacheData); + } + + # End document + $r->print('</pre></body></html>'); + $r->rflush(); + + return; +} + +# ================================================================ Main Handler + +=pod + +=item &handler() + +The handler checks for permission to access the course data and for +initial header problem. Then it passes the torch to the work horse +function BuildChart. + +=over 4 + +Input: $r + +$r: This is the object that is used to print. + +Output: A Value (OK or HTTP_NOT_ACCEPTABLE) + +=back + +=cut + +sub handler { + my $r=shift; +# $jr=$r; + unless(&Apache::lonnet::allowed('vgr',$ENV{'request.course.id'})) { + $ENV{'user.error.msg'}= + $r->uri.":vgr:0:0:Cannot view grades for complete course"; + return HTTP_NOT_ACCEPTABLE; + } + + # Set document type for header only + if ($r->header_only) { + if($ENV{'browser.mathml'}) { + $r->content_type('text/xml'); + } else { + $r->content_type('text/html'); + } + &Apache::loncommon::no_cache($r); + $r->send_http_header; + return OK; + } + + unless($ENV{'request.course.fn'}) { + my $requrl=$r->uri; + $ENV{'user.error.msg'}="$requrl:bre:0:0:Course not initialized"; + return HTTP_NOT_ACCEPTABLE; + } + + &BuildChart($r); + + return OK; +} +1; +__END__