File:  [LON-CAPA] / loncom / interface / Attic / lonchart.pm
Revision 1.58: download - view: text, annotated - select for diffs
Mon Jul 8 16:50:03 2002 UTC (22 years ago) by stredwic
Branches: MAIN
CVS tags: HEAD
Add some comments in the code.

    1: # The LearningOnline Network with CAPA
    2: # (Publication Handler
    3: #
    4: # $Id: lonchart.pm,v 1.58 2002/07/08 16:50:03 stredwic Exp $
    5: #
    6: # Copyright Michigan State University Board of Trustees
    7: #
    8: # This file is part of the LearningOnline Network with CAPA (LON-CAPA).
    9: #
   10: # LON-CAPA is free software; you can redistribute it and/or modify
   11: # it under the terms of the GNU General Public License as published by
   12: # the Free Software Foundation; either version 2 of the License, or
   13: # (at your option) any later version.
   14: #
   15: # LON-CAPA is distributed in the hope that it will be useful,
   16: # but WITHOUT ANY WARRANTY; without even the implied warranty of
   17: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   18: # GNU General Public License for more details.
   19: #
   20: # You should have received a copy of the GNU General Public License
   21: # along with LON-CAPA; if not, write to the Free Software
   22: # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
   23: #
   24: # /home/httpd/html/adm/gpl.txt
   25: #
   26: # http://www.lon-capa.org/
   27: #
   28: # Homework Performance Chart
   29: #
   30: # (Navigate Maps Handler
   31: #
   32: # (Page Handler
   33: #
   34: # (TeX Content Handler
   35: # YEAR=2000
   36: # 05/29/00,05/30 Gerd Kortemeyer)
   37: # 08/30,08/31,09/06,09/14,09/15,09/16,09/19,09/20,09/21,09/23,
   38: # 10/02,10/10,10/14,10/16,10/18,10/19,10/31,11/6,11/14,11/16 Gerd Kortemeyer)
   39: # YEAR=2001
   40: # 3/1/1,6/1,17/1,29/1,30/1,31/1 Gerd Kortemeyer)
   41: # 7/10/01 Behrouz Minaei
   42: # 9/8 Gerd Kortemeyer
   43: # 10/1, 10/19, 11/17, 11/22, 11/24, 11/28 12/18 Behrouz Minaei
   44: # YEAR=2002
   45: # 2/1, 2/6, 2/19, 2/28 Behrouz Minaei
   46: #
   47: ###
   48: 
   49: =pod
   50: 
   51: =head1 NAME
   52: 
   53: lonchart
   54: 
   55: =head1 SYNOPSIS
   56: 
   57: Quick display of students grades for a course in a compressed table format.
   58: 
   59: =head1 DESCRIPTION
   60: 
   61: This module process all student grades for a course and turns them into a 
   62: table like structure.
   63: 
   64: This is part of the LearningOnline Network with CAPA project
   65: described at http://www.lon-capa.org
   66: 
   67: lonchart presents the user with a condensed view all a course's data.  The
   68: class title, the number of students, and the date for the last update of the
   69: displayed data.  There is also a legend that describes the chart values.  
   70: 
   71: For each valid grade for a student is linked with a submission record for that
   72: problem.  The ability to add and remove columns of data from the chart was
   73: added for reducing the burden of having to scroll through large quantities
   74: of data.  The interface also allows for sorting of students by username,
   75: last name, and section number of class.  Active and expired students are
   76: also available.
   77: 
   78: The interface is controlled by three primary buttons: Recalculate Chart, 
   79: Refresh Chart, and Reset Selections.  Recalculate Chart will update 
   80: the chart to the most recent data and keep the display settings for the chart
   81: the same.  Refresh Chart is used to redisplay the chart after selecting
   82: different output formatting.  Reset Selections is used to set the chart
   83: display options back to default values.
   84: 
   85: =head1 CODE LAYOUT DESCRIPTION
   86: 
   87: The code is broken down into five components: formatting data for printing,
   88: downloading data from servers, processing data, helper functions,
   89: and the central processing functions.  The module is broken into chunks
   90: for each component.
   91: 
   92: =head1 PACKAGES USED
   93: 
   94:  Apache::Constants qw(:common :http)
   95:  Apache::lonnet()
   96:  Apache::loncommon()
   97:  HTML::TokeParser
   98:  GDBM_File
   99: 
  100: =cut
  101: 
  102: package Apache::lonchart;
  103: 
  104: use strict;
  105: use Apache::Constants qw(:common :http);
  106: use Apache::lonnet();
  107: use Apache::loncommon();
  108: use HTML::TokeParser;
  109: use GDBM_File;
  110: 
  111: #my $jr; 
  112: 
  113: =pod
  114: 
  115: =head1 FORMAT DATA FOR PRINTING
  116: 
  117: =cut
  118: 
  119: # ----- FORMAT PRINT DATA ----------------------------------------------
  120: 
  121: =pod
  122: 
  123: =item &FormatStudentInformation()
  124: 
  125: This function produces a formatted string of the student's information:
  126: username, domain, section, full name, and PID.
  127: 
  128: =over 4
  129: 
  130: Input: $cache, $name, $studentInformation, $spacePadding
  131: 
  132: $cache: This is a pointer to a hash that is tied to the cached data
  133: 
  134: $name:  The name and domain of the current student in name:domain format
  135: 
  136: $studentInformation: A pointer to an array holding the names used to
  137: 
  138: remove data from the hash.  They represent the name of the data to be removed.
  139: 
  140: $spacePadding: Extra spaces that represent the space between columns
  141: 
  142: Output: $Str
  143: 
  144: $Str: Formatted string.
  145: 
  146: =back
  147: 
  148: =cut
  149: 
  150: sub FormatStudentInformation {
  151:     my ($cache,$name,$studentInformation,$spacePadding)=@_;
  152:     my $Str='';
  153: 
  154:     for(my $index=0; $index<(scalar @$studentInformation); $index++) {
  155:         if(!&ShouldShowColumn($cache, 'heading'.$index)) {
  156:             next;
  157:         }
  158: 	my $data=$cache->{$name.':'.$studentInformation->[$index]};
  159: 	$Str .= $data;
  160: 
  161: 	my @dataLength=split(//,$data);
  162: 	my $length=scalar @dataLength;
  163: 	$Str .= (' 'x($cache->{$studentInformation->[$index].'Length'}-
  164:                       $length));
  165: 	$Str .= $spacePadding;
  166:     }
  167: 
  168:     return $Str;
  169: }
  170: 
  171: =pod
  172: 
  173: =item &FormatStudentData()
  174: 
  175: First, FormatStudentInformation is called and prefixes the course information.
  176: This function produces a formatted string of the student's course information.
  177: Each column of data represents all the problems for a given sequence.  For
  178: valid grade data, a link is created for that problem to a submission record
  179: for that problem.
  180: 
  181: =over 4
  182: 
  183: Input: $name, $studentInformation, $spacePadding, $ChartDB
  184: 
  185: $name: The name and domain of the current student in name:domain format
  186: 
  187: $studentInformation: A pointer to an array holding the names used to 
  188: remove data from the hash.  They represent 
  189: the name of the data to be removed.
  190: 
  191: $spacePadding: Extra spaces that represent the space between columns
  192: 
  193: $ChartDB: The name of the cached data database which will be tied to that 
  194: database.
  195: 
  196: Output: $Str
  197: 
  198: $Str: Formatted string that is an entire row of the chart.  It is a 
  199: concatenation of student information and student course information.
  200: 
  201: =back
  202: 
  203: =cut
  204: 
  205: sub FormatStudentData {
  206:     my ($name,$studentInformation,$spacePadding,$ChartDB)=@_;
  207:     my ($sname,$sdom) = split(/\:/,$name);
  208:     my $Str;
  209:     my %CacheData;
  210: 
  211:     unless(tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_READER,0640)) {
  212:         return '';
  213:     }
  214:     # Handle Student information ------------------------------------------
  215:     # Handle user data
  216:     $Str=&FormatStudentInformation(\%CacheData, $name, $studentInformation, 
  217:                                    $spacePadding);
  218: 
  219:     # Handle errors
  220:     if($CacheData{$name.':error'} =~ /environment/) {
  221:         $Str .= '<br>';
  222:         untie(%CacheData);
  223:         return $Str;
  224:     }
  225: 
  226:     if($CacheData{$name.':error'} =~ /course/) {
  227:         $Str .= '<br>';
  228:         untie(%CacheData);
  229:         return $Str;
  230:     }
  231: 
  232:     # Handle problem data ------------------------------------------------
  233:     my $Version;
  234:     my $problemsCorrect = 0;
  235:     my $totalProblems   = 0;
  236:     my $problemsSolved  = 0;
  237:     my $numberOfParts   = 0;
  238:     foreach my $sequence (split(/\:/,$CacheData{'orderedSequences'})) {
  239:         if(!&ShouldShowColumn(\%CacheData, 'sequence'.$sequence)) {
  240:             next;
  241:         }
  242: 
  243: 	my $characterCount=0;
  244: 	foreach my $problemID (split(/\:/,$CacheData{$sequence.':problems'})) {
  245: 	    my $problem = $CacheData{$problemID.':problem'};
  246: 	    my $LatestVersion = $CacheData{$name.":version:$problem"};
  247: 
  248:             # Output blanks for all the parts of this problem if there
  249:             # is no version information about the current problem.
  250:             if(!$LatestVersion) {
  251:                 foreach my $part (split(/\:/,$CacheData{$sequence.':'.
  252:                                                         $problemID.
  253:                                                         ':parts'})) {
  254:                     $Str .= ' ';
  255:                     $totalProblems++;
  256:                     $characterCount++;
  257:                 }
  258:                 next;
  259:             }
  260: 
  261:             my %partData=undef;
  262:             # Initialize part data, display skips correctly
  263:             # Skip refers to when a student made no submissions on that
  264:             # part/problem.
  265:             foreach my $part (split(/\:/,$CacheData{$sequence.':'.
  266:                                                     $problemID.
  267:                                                     ':parts'})) {
  268:                 $partData{$part.':tries'}=0;
  269:                 $partData{$part.':code'}=' ';
  270:             }
  271: 
  272:             # Looping through all the versions of each part, starting with the
  273:             # oldest version.  Basically, it gets the most recent 
  274:             # set of grade data for each part.
  275: 	    for(my $Version=1; $Version<=$LatestVersion; $Version++) {
  276:                 foreach my $part (split(/\:/,$CacheData{$sequence.':'.
  277:                                                         $problemID.
  278:                                                         ':parts'})) {
  279: 
  280:                     if(!defined($CacheData{$name.":$Version:$problem".
  281:                                                ":resource.$part.solved"})) {
  282:                         # No grade for this submission, so skip
  283:                         next;
  284:                     }
  285: 
  286:                     my $tries=0;
  287:                     my $code=' ';
  288: 
  289:                     $tries = $CacheData{$name.":$Version:$problem".
  290:                                         ":resource.$part.tries"};
  291:                     $partData{$part.':tries'}=($tries) ? $tries : 0;
  292: 
  293:                     my $val = $CacheData{$name.":$Version:$problem".
  294:                                          ":resource.$part.solved"};
  295:                     if    ($val eq 'correct_by_student')   {$code = '*';} 
  296:                     elsif ($val eq 'correct_by_override')  {$code = '+';}
  297:                     elsif ($val eq 'incorrect_attempted')  {$code = '.';} 
  298:                     elsif ($val eq 'incorrect_by_override'){$code = '-';}
  299:                     elsif ($val eq 'excused')              {$code = 'x';}
  300:                     elsif ($val eq 'ungraded_attempted')   {$code = '#';}
  301:                     else                                   {$code = ' ';}
  302:                     $partData{$part.':code'}=$code;
  303:                 }
  304:             }
  305: 
  306:             # All grades (except for versionless parts) are displayed as links
  307:             # to their submission record.  Loop through all the parts for the
  308:             # current problem in the correct order and prepare the output links
  309:             $Str.='<a href="/adm/grades?symb='.
  310:                 &Apache::lonnet::escape($problem).
  311:                 '&student='.$sname.'&domain='.$sdom.'&command=submission">'; 
  312:             foreach(split(/\:/,$CacheData{$sequence.':'.$problemID.
  313:                                           ':parts'})) {
  314:                 if($partData{$_.':code'} eq '*') {
  315:                     $problemsCorrect++;
  316:                     if (($partData{$_.':tries'}<10) &&
  317:                         ($partData{$_.':tries'} ne '')) {
  318:                         $partData{$_.':code'}=$partData{$_.':tries'};
  319:                     }
  320:                 } elsif($partData{$_.':code'} eq '+') {
  321:                     $problemsCorrect++;
  322:                 }
  323: 
  324:                 $Str .= $partData{$_.':code'};
  325:                 $characterCount++;
  326: 
  327:                 if($partData{$_.':code'} ne 'x') {
  328:                     $totalProblems++;
  329:                 }
  330:             }
  331:             $Str.='</a>';
  332:         }
  333: 
  334:         # Output the number of correct answers for the current sequence.
  335:         # This part takes up 6 character slots, but is formated right 
  336:         # justified.
  337:         my $spacesNeeded=$CacheData{$sequence.':columnWidth'}-$characterCount;
  338:         $spacesNeeded -= 3;
  339:         $Str .= (' 'x$spacesNeeded);
  340: 
  341: 	my $outputProblemsCorrect = sprintf( "%3d", $problemsCorrect );
  342: 	$Str .= '<font color="#007700">'.$outputProblemsCorrect.'</font>';
  343: 	$problemsSolved += $problemsCorrect;
  344: 	$problemsCorrect=0;
  345: 
  346:         $Str .= $spacePadding;
  347:     }
  348: 
  349:     # Output the total correct problems over the total number of problems.
  350:     # I don't like this type of formatting, but it is a solution.  Need
  351:     # a way to dynamically determine the space requirements.
  352:     my $outputProblemsSolved = sprintf( "%4d", $problemsSolved );
  353:     my $outputTotalProblems  = sprintf( "%4d", $totalProblems );
  354:     $Str .= '<font color="#000088">'.$outputProblemsSolved.
  355: 	    ' / '.$outputTotalProblems.'</font><br>';
  356: 
  357:     untie(%CacheData);
  358:     return $Str;
  359: }
  360: 
  361: =pod
  362: 
  363: =item &CreateTableHeadings()
  364: 
  365: This function generates the column headings for the chart.
  366: 
  367: =over 4
  368: 
  369: Inputs: $CacheData, $studentInformation, $headings, $spacePadding
  370: 
  371: $CacheData: pointer to a hash tied to the cached data database
  372: 
  373: $studentInformation: a pointer to an array containing the names of the data 
  374: held in a column and is used as part of a key into $CacheData
  375: 
  376: $headings: The names of the headings for the student information
  377: 
  378: $spacePadding: The spaces to go between columns
  379: 
  380: Output: $Str
  381: 
  382: $Str: A formatted string of the table column headings.
  383: 
  384: =back
  385: 
  386: =cut
  387: 
  388: sub CreateTableHeadings {
  389:     my ($CacheData,$studentInformation,$headings,$spacePadding)=@_;
  390:     my $Str='<tr>';
  391: 
  392:     for(my $index=0; $index<(scalar @$headings); $index++) {
  393:         if(!&ShouldShowColumn($CacheData, 'heading'.$index)) {
  394:             next;
  395:         }
  396: 
  397:         $Str .= '<td align="left"><pre>';
  398: 	my $data=$$headings[$index];
  399: 	$Str .= $data;
  400: 
  401: 	my @dataLength=split(//,$data);
  402: 	my $length=scalar @dataLength;
  403: 	$Str .= (' 'x($CacheData->{$$studentInformation[$index].'Length'}-
  404:                       $length));
  405: 	$Str .= $spacePadding;
  406:         $Str .= '</pre></td>';
  407:     }
  408: 
  409:     foreach my $sequence (split(/\:/,$CacheData->{'orderedSequences'})) {
  410:         if(!&ShouldShowColumn($CacheData, 'sequence'.$sequence)) {
  411:             next;
  412:         }
  413: 
  414:         $Str .= '<td align="left"><pre>';
  415:         my $name = $CacheData->{$sequence.':title'};
  416: 	$Str .= $name;
  417: 	my @titleLength=split(//,$CacheData->{$sequence.':title'});
  418: 	my $leftover=$CacheData->{$sequence.':columnWidth'}-
  419:                      (scalar @titleLength);
  420: 	$Str .= (' 'x$leftover);
  421: 	$Str .= $spacePadding;
  422:         $Str .= '</pre></td>';
  423:     }
  424: 
  425:     $Str .= '<td><pre>Total Solved/Total Problems</pre></td>';
  426:     $Str .= '</tr>';
  427: 
  428:     return $Str;
  429: }
  430: 
  431: =pod
  432: 
  433: =item &CreateColumnSelectionBox()
  434: 
  435: If there are columns not being displayed then this selection box is created
  436: with a list of those columns.  When selections are made and the page 
  437: refreshed, the columns will be removed from this box and the column is
  438: put back in the chart.  If there is no columns to select, no row is added
  439: to the interface table.
  440: 
  441: =over 4
  442: Input: $CacheData, $headings
  443: 
  444: 
  445: $CacheData: A pointer to a hash tied to the cached data
  446: 
  447: $headings:  An array of the names of the columns for the student information.  
  448: They are used for displaying which columns are missing.
  449: 
  450: Output: $notThere
  451: 
  452: $notThere: The string contains one row of a table.  The first column has the 
  453: name of the selection box.  The second contains the selection box 
  454: which has a size of four.
  455: 
  456: =back
  457: 
  458: =cut
  459: 
  460: sub CreateColumnSelectionBox {
  461:     my ($CacheData,$headings)=@_;
  462: 
  463:     my $missing=0;
  464:     my $notThere='<tr><td align="right"><b>Select column to view:</b>';
  465:     my $name;
  466:     $notThere .= '<td align="left">';
  467:     $notThere .= '<select name="reselect" size="4" multiple="true">'."\n";
  468: 
  469:     for(my $index=0; $index<(scalar @$headings); $index++) {
  470:         if(&ShouldShowColumn($CacheData, 'heading'.$index)) {
  471:             next;
  472:         }
  473:         $name = $headings->[$index];
  474:         $notThere .= '<option value="heading'.$index.'">';
  475:         $notThere .= $name.'</option>'."\n";
  476:         $missing++;
  477:     }
  478: 
  479:     foreach my $sequence (split(/\:/,$CacheData->{'orderedSequences'})) {
  480:         if(&ShouldShowColumn($CacheData, 'sequence'.$sequence)) {
  481:             next;
  482:         }
  483:         $name = $CacheData->{$sequence.':title'};
  484:         $notThere .= '<option value="sequence'.$sequence.'">';
  485:         $notThere .= $name.'</option>'."\n";
  486:         $missing++;
  487:     }
  488: 
  489:     if($missing) {
  490:         $notThere .= '</select>';
  491:     } else {
  492:         $notThere='<tr><td>';
  493:     }
  494: 
  495:     return $notThere.'</td></tr>';
  496: }
  497: 
  498: =pod
  499: 
  500: =item &CreateColumnSelectors()
  501: 
  502: This function generates the checkboxes above the column headings.  The 
  503: column will be removed if the checkbox is unchecked.
  504: 
  505: =over 4
  506: 
  507: Input: $CacheData, $headings
  508: 
  509: $CacheData: A pointer to a hash tied to the cached data
  510: 
  511: $headings:  An array of the names of the columns for the student 
  512: information.  They are used to know what are the student information columns
  513: 
  514: Output: $present
  515: 
  516: $present: The string contains the first row of a table.  Each column contains
  517: a checkbox which is left justified.  Currently left justification is used
  518: for consistency of location over the column in which it presides.
  519: 
  520: =back
  521: 
  522: =cut
  523: 
  524: sub CreateColumnSelectors {
  525:     my ($CacheData,$headings)=@_;
  526: 
  527:     my $found=0;
  528:     my ($name, $length, $position);
  529: 
  530:     my $present = '<tr>';
  531:     for(my $index=0; $index<(scalar @$headings); $index++) {
  532:         if(!&ShouldShowColumn($CacheData, 'heading'.$index)) {
  533:             next;
  534:         }
  535:         $present .= '<td align="left">';
  536:         $present .= '<input type="checkbox" checked="on" ';
  537:         $present .= 'name="heading'.$index.'" />';
  538:         $present .= '</td>';
  539:         $found++;
  540:     }
  541: 
  542:     foreach my $sequence (split(/\:/,$CacheData->{'orderedSequences'})) {
  543:         if(!&ShouldShowColumn($CacheData, 'sequence'.$sequence)) {
  544:             next;
  545:         }
  546:         $present .= '<td align="left">';
  547:         $present .= '<input type="checkbox" checked="on" ';
  548:         $present .= 'name="sequence'.$sequence.'" />';
  549:         $present .= '</td>';
  550:         $found++;
  551:     }
  552: 
  553:     if(!$found) {
  554:         $present = '';
  555:     }
  556: 
  557:     return $present.'<td></td></tr></form>'."\n";;
  558: }
  559: 
  560: =pod
  561: 
  562: =item &CreateForm()
  563: 
  564: The interface for this module consists primarily of the controls in this
  565: function.  The student status selection (active, expired, any) is set here.
  566: The sort buttons: username, last name, and section are set here.  The
  567: other buttons are Recalculate Chart, Refresh Chart, and Reset Selections.
  568: These controls are in a table to clean up the interface.
  569: 
  570: =over 4
  571: 
  572: Input: $CacheData
  573: 
  574: $CacheData is a hash pointer to tied database for cached data.
  575: 
  576: Output: $Ptr
  577: 
  578: $Ptr is a string containing all the html for the above mentioned buttons.
  579: 
  580: =back
  581: 
  582: =cut
  583: 
  584: sub CreateForm {
  585:     my ($CacheData)=@_;
  586:     my $OpSel1='';
  587:     my $OpSel2='';
  588:     my $OpSel3='';
  589:     my $Status = $CacheData->{'form.status'};
  590:     if ( $Status eq 'Any' ) { $OpSel3='selected'; }
  591:     elsif ($Status eq 'Expired' ) { $OpSel2 = 'selected'; }
  592:     else { $OpSel1 = 'selected'; }
  593: 
  594:     my $Ptr .= '<form name="stat" method="post" action="/adm/chart" >'."\n";
  595:     $Ptr .= '<tr><td align="right">';
  596:     $Ptr .= '</td><td align="left">';
  597:     $Ptr .= '<input type="submit" name="recalculate" ';
  598:     $Ptr .= 'value="Recalculate Chart"/>'."\n";
  599:     $Ptr .= '&nbsp;&nbsp;&nbsp;';
  600:     $Ptr .= '<input type="submit" name="refresh" ';
  601:     $Ptr .= 'value="Refresh Chart"/>'."\n";
  602:     $Ptr .= '&nbsp;&nbsp;&nbsp;';
  603:     $Ptr .= '<input type="submit" name="reset" ';
  604:     $Ptr .= 'value="Reset Selections"/></td>'."\n";
  605:     $Ptr .= '</tr><tr><td align="right">';
  606:     $Ptr .= '<b> Sort by: </b>'."\n";
  607:     $Ptr .= '</td><td align="left">';
  608:     $Ptr .= '<input type="submit" name="sort" value="User Name" />'."\n";
  609:     $Ptr .= '&nbsp;&nbsp;&nbsp;';
  610:     $Ptr .= '<input type="submit" name="sort" value="Last Name" />'."\n";
  611:     $Ptr .= '&nbsp;&nbsp;&nbsp;';
  612:     $Ptr .= '<input type="submit" name="sort" value="Section"/>'."\n";
  613:     $Ptr .= '</td></tr><tr><td align="right">';
  614:     $Ptr .= '<b> Student Status: &nbsp; </b>'."\n".
  615:             '</td><td align="left">'.
  616:             '<select name="status">'. 
  617:             '<option '.$OpSel1.' >Active</option>'."\n".
  618:             '<option '.$OpSel2.' >Expired</option>'."\n".
  619: 	    '<option '.$OpSel3.' >Any</option> </select> '."\n";
  620:     $Ptr .= '</td></tr>';
  621: 
  622:     return $Ptr;
  623: }
  624: 
  625: =pod
  626: 
  627: =item &CreateLegend()
  628: 
  629: This function returns a formatted string containing the legend for the
  630: chart.  The legend describes the symbols used to represent grades for
  631: problems.
  632: 
  633: =cut
  634: 
  635: sub CreateLegend {
  636:     my $Str = "<p><pre>".
  637:               "1..9: correct by student in 1..9 tries\n".
  638:               "   *: correct by student in more than 9 tries\n".
  639: 	      "   +: correct by override\n".
  640:               "   -: incorrect by override\n".
  641: 	      "   .: incorrect attempted\n".
  642: 	      "   #: ungraded attempted\n".
  643:               "    : not attempted\n".
  644: 	      "   x: excused".
  645:               "</pre><p>"; 
  646:     return $Str;
  647: }
  648: 
  649: =pod
  650: 
  651: =item &StartDocument()
  652: 
  653: Returns a string containing the header information for the chart: title,
  654: logo, and course title.
  655: 
  656: =cut
  657: 
  658: sub StartDocument {
  659:     my $Str = '';
  660:     $Str .= '<html>';
  661:     $Str .= '<head><title>';
  662:     $Str .= 'LON-CAPA Assessment Chart</title></head>';
  663:     $Str .= '<body bgcolor="#FFFFFF">';
  664:     $Str .= '<script>window.focus();</script>';
  665:     $Str .= '<img align=right src=/adm/lonIcons/lonlogos.gif>';
  666:     $Str .= '<h1>Assessment Chart</h1>';
  667:     $Str .= '<h1>'.$ENV{'course.'.$ENV{'request.course.id'}.'.description'};
  668:     $Str .= '</h1>';
  669: 
  670:     return $Str;
  671: }
  672: 
  673: # ----- END FORMAT PRINT DATA ------------------------------------------
  674: 
  675: =pod
  676: 
  677: =head1 DOWNLOAD INFORMATION
  678: 
  679: This section contains all the files that get data from other servers 
  680: and/or itself.  There is one function that has a call to get remote
  681: information but isn't included here which is ProcessTopLevelMap.  The
  682: usage was small enough to be ignored, but that portion may be moved
  683: here in the future.
  684: 
  685: =cut
  686: 
  687: # ----- DOWNLOAD INFORMATION -------------------------------------------
  688: 
  689: =pod
  690: 
  691: =item &DownloadPrerequisiteData()
  692: 
  693: Collects lastname, generation, middlename, firstname, PID, and section for each
  694: student from their environment database.  The list of students is built from
  695: collecting a classlist for the course that is to be displayed.
  696: 
  697: =over 4
  698: 
  699: Input: $courseID, $c
  700: 
  701: $courseID:  The id of the course
  702: 
  703: $c: The connection class that can determine if the browser has aborted.  It
  704: is used to short circuit this function so that it doesn't continue to 
  705: get information when there is no need.
  706: 
  707: Output: \%classlist
  708: 
  709: \%classlist: A pointer to a hash containing the following data:
  710: 
  711: -A list of student name:domain (as keys) (known below as $name)
  712: 
  713: -A hash pointer for each student containing lastname, generation, firstname,
  714: middlename, and PID : Key is $name.'studentInformation'
  715: 
  716: -A hash pointer to each students section data : Key is $name.section
  717: 
  718: =back
  719: 
  720: =cut
  721: 
  722: sub DownloadPrerequisiteData {
  723:     my ($courseID, $c)=@_;
  724:     my ($courseDomain,$courseNumber)=split(/\_/,$courseID);
  725: 
  726:     my %classlist=&Apache::lonnet::dump('classlist',$courseDomain,
  727:                                         $courseNumber);
  728:     my ($checkForError)=keys (%classlist);
  729:     if($checkForError =~ /^(con_lost|error|no_such_host)/i) {
  730:         return \%classlist;
  731:     }
  732: 
  733:     foreach my $name (keys(%classlist)) {
  734:         if($c->aborted()) {
  735:             $classlist{'error'}='aborted';
  736:             return \%classlist;
  737:         }
  738: 
  739:         my ($studentName,$studentDomain) = split(/\:/,$name);
  740:         # Download student environment data, specifically the full name and id.
  741:         my %studentInformation=&Apache::lonnet::get('environment',
  742:                                                     ['lastname','generation',
  743:                                                      'firstname','middlename',
  744:                                                      'id'],
  745:                                                     $studentDomain,
  746:                                                     $studentName);
  747:         $classlist{$name.':studentInformation'}=\%studentInformation;
  748: 
  749:         if($c->aborted()) {
  750:             $classlist{'error'}='aborted';
  751:             return \%classlist;
  752:         }
  753: 
  754:         #Section
  755:         my %section=&Apache::lonnet::dump('roles',$studentDomain,$studentName);
  756:         $classlist{$name.':section'}=\%section;
  757:     }
  758: 
  759:     return \%classlist;
  760: }
  761: 
  762: =pod
  763: 
  764: =item &DownloadStudentCourseInformation()
  765: 
  766: Dump of all the course information for a single student.  There is no
  767: pruning of data, it is all stored in a hash and returned.
  768: 
  769: =over 4
  770: 
  771: Input: $name, $courseID
  772: 
  773: $name: student name:domain
  774: 
  775: $courseID:  The id of the course
  776: 
  777: Output: \%courseData
  778: 
  779: \%courseData:  A hash pointer to the raw data from the student's course
  780: database.
  781: 
  782: =back
  783: 
  784: =cut
  785: 
  786: sub DownloadStudentCourseInformation {
  787:     my ($name,$courseID)=@_;
  788:     my ($studentName,$studentDomain) = split(/\:/,$name);
  789: 
  790:     # Download student course data
  791:     my %courseData=&Apache::lonnet::dump($courseID,$studentDomain,
  792: 					 $studentName);
  793:     return \%courseData;
  794: }
  795: 
  796: # ----- END DOWNLOAD INFORMATION ---------------------------------------
  797: 
  798: =pod
  799: 
  800: =head1 PROCESSING FUNCTIONS
  801: 
  802: These functions process all the data for all the students.  Also, they
  803: are the only functions that access the cache database for writing.  Thus
  804: they are the only functions that cache data.  The downloading and caching
  805: were separated to reduce problems with stopping downloading then can't
  806: tie hash to database later.
  807: 
  808: =cut
  809: 
  810: # ----- PROCESSING FUNCTIONS ---------------------------------------
  811: 
  812: =pod
  813: 
  814: =item &ProcessTopResourceMap()
  815: 
  816: Trace through the "big hash" created in rat/lonuserstate.pm::loadmap.  
  817: Basically, this function organizes a subset of the data and stores it in
  818: cached data.  The data stored is the problems, sequences, sequence titles,
  819: parts of problems, and their ordering.  Column width information is also 
  820: partially handled here on a per sequence basis.
  821: 
  822: =over 4
  823: 
  824: Input: $ChartDB, $c
  825: 
  826: $ChartDB:  The name of the cache database file
  827: 
  828: $c:  The connection class used to determine if an abort has been sent to the 
  829: browser
  830: 
  831: Output: A string that contains an error message or "OK" if everything went 
  832: smoothly.
  833: 
  834: =back
  835: 
  836: =cut
  837: 
  838: sub ProcessTopResourceMap {
  839:     my ($ChartDB,$c)=@_;
  840:     my %hash;
  841:     my $fn=$ENV{'request.course.fn'};
  842:     if(-e "$fn.db") {
  843: 	my $tieTries=0;
  844: 	while($tieTries < 3) {
  845: 	    if(tie(%hash,'GDBM_File',"$fn.db",&GDBM_READER,0640)) {
  846: 		last;
  847: 	    }
  848: 	    $tieTries++;
  849: 	    sleep 1;
  850: 	}
  851: 	if($tieTries >= 3) {
  852:             return 'Coursemap undefined.';
  853:         }
  854:     } else {
  855:         return 'Can not open Coursemap.';
  856:     }
  857: 
  858:     my %CacheData;
  859:     unless(tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_WRCREAT,0640)) {
  860:         untie(%hash);
  861: 	return 'Could not tie cache hash.';
  862:     }
  863: 
  864:     # Initialize state machine.  Set information pointing to top level map.
  865:     my (@sequences, @currentResource, @finishResource);
  866:     my ($currentSequence, $currentResourceID, $lastResourceID);
  867: 
  868:     $currentResourceID=$hash{'ids_/res/'.$ENV{'request.course.uri'}};
  869:     push(@currentResource, $currentResourceID);
  870:     $lastResourceID=-1;
  871:     $currentSequence=-1;
  872:     my $topLevelSequenceNumber = $currentSequence;
  873: 
  874:     while(1) {
  875:         if($c->aborted()) {
  876:             last;
  877:         }
  878: 	# HANDLE NEW SEQUENCE!
  879: 	#if page || sequence
  880: 	if(defined($hash{'map_pc_'.$hash{'src_'.$currentResourceID}})) {
  881: 	    push(@sequences, $currentSequence);
  882: 	    push(@currentResource, $currentResourceID);
  883: 	    push(@finishResource, $lastResourceID);
  884: 
  885: 	    $currentSequence=$hash{'map_pc_'.$hash{'src_'.$currentResourceID}};
  886: 
  887:             # Mark sequence as containing problems.  If it doesn't, then
  888:             # it will be removed when processing for this sequence is
  889:             # complete.  This allows the problems in a sequence
  890:             # to be outputed before problems in the subsequences
  891:             if(!defined($CacheData{'orderedSequences'})) {
  892:                 $CacheData{'orderedSequences'}=$currentSequence;
  893:             } else {
  894:                 $CacheData{'orderedSequences'}.=':'.$currentSequence;
  895:             }
  896: 
  897: 	    $lastResourceID=$hash{'map_finish_'.
  898: 				  $hash{'src_'.$currentResourceID}};
  899: 	    $currentResourceID=$hash{'map_start_'.
  900: 				     $hash{'src_'.$currentResourceID}};
  901: 
  902: 	    if(!($currentResourceID) || !($lastResourceID)) {
  903: 		$currentSequence=pop(@sequences);
  904: 		$currentResourceID=pop(@currentResource);
  905: 		$lastResourceID=pop(@finishResource);
  906: 		if($currentSequence eq $topLevelSequenceNumber) {
  907: 		    last;
  908: 		}
  909: 	    }
  910: 	}
  911: 
  912: 	# Handle gradable resources: exams, problems, etc
  913: 	$currentResourceID=~/(\d+)\.(\d+)/;
  914:         my $partA=$1;
  915:         my $partB=$2;
  916: 	if($hash{'src_'.$currentResourceID}=~
  917: 	   /\.(problem|exam|quiz|assess|survey|form)$/ &&
  918: 	   $partA eq $currentSequence) {
  919: 	    my $Problem = &Apache::lonnet::symbclean(
  920: 			  &Apache::lonnet::declutter($hash{'map_id_'.$partA}).
  921: 			  '___'.$partB.'___'.
  922: 			  &Apache::lonnet::declutter($hash{'src_'.
  923: 							 $currentResourceID}));
  924: 
  925: 	    $CacheData{$currentResourceID.':problem'}=$Problem;
  926: 	    if(!defined($CacheData{$currentSequence.':problems'})) {
  927: 		$CacheData{$currentSequence.':problems'}=$currentResourceID;
  928: 	    } else {
  929: 		$CacheData{$currentSequence.':problems'}.=
  930: 		    ':'.$currentResourceID;
  931: 	    }
  932: 
  933:             # Get Parts for problem
  934: 	    my $meta=$hash{'src_'.$currentResourceID};
  935: 	    foreach (split(/\,/,&Apache::lonnet::metadata($meta,'keys'))) {
  936: 		if($_=~/^stores\_(\d+)\_tries$/) {
  937: 		    my $Part=&Apache::lonnet::metadata($meta,$_.'.part');
  938:                     if(!defined($CacheData{$currentSequence.':'.
  939:                                           $currentResourceID.':parts'})) {
  940:                         $CacheData{$currentSequence.':'.$currentResourceID.
  941:                                    ':parts'}=$Part;
  942:                     } else {
  943:                         $CacheData{$currentSequence.':'.$currentResourceID.
  944:                                    ':parts'}.=':'.$Part;
  945:                     }
  946: 		}
  947: 	    }
  948: 	}
  949: 
  950: 	# if resource == finish resource, then it is the end of a sequence/page
  951: 	if($currentResourceID eq $lastResourceID) {
  952: 	    # pop off last resource of sequence
  953: 	    $currentResourceID=pop(@currentResource);
  954: 	    $lastResourceID=pop(@finishResource);
  955: 
  956: 	    if(defined($CacheData{$currentSequence.':problems'})) {
  957: 		# Capture sequence information here
  958: 		$CacheData{$currentSequence.':title'}=
  959: 		    $hash{'title_'.$currentResourceID};
  960: 
  961:                 my $totalProblems=0;
  962:                 foreach my $currentProblem (split(/\:/,
  963:                                                $CacheData{$currentSequence.
  964:                                                ':problems'})) {
  965:                     foreach (split(/\:/,$CacheData{$currentSequence.':'.
  966:                                                    $currentProblem.
  967:                                                    ':parts'})) {
  968:                         $totalProblems++;
  969:                     }
  970:                 }
  971: 		my @titleLength=split(//,$CacheData{$currentSequence.
  972:                                                     ':title'});
  973:                 # $extra is 3 for problems correct and 3 for space
  974:                 # between problems correct and problem output
  975:                 my $extra = 6;
  976: 		if(($totalProblems + $extra) > (scalar @titleLength)) {
  977: 		    $CacheData{$currentSequence.':columnWidth'}=
  978:                         $totalProblems + $extra;
  979: 		} else {
  980: 		    $CacheData{$currentSequence.':columnWidth'}=
  981:                         (scalar @titleLength);
  982: 		}
  983: 	    } else {
  984:                 # Remove sequence from list, if it contains no problems to
  985:                 # display.
  986:                 $CacheData{'orderedSequences'}=~s/$currentSequence//;
  987:                 $CacheData{'orderedSequences'}=~s/::/:/g;
  988:                 $CacheData{'orderedSequences'}=~s/^:|:$//g;
  989:             }
  990: 
  991: 	    $currentSequence=pop(@sequences);
  992: 	    if($currentSequence eq $topLevelSequenceNumber) {
  993: 		last;
  994: 	    }
  995: 	}
  996: 
  997: 	# MOVE!!!
  998: 	# move to next resource
  999: 	unless(defined($hash{'to_'.$currentResourceID})) {
 1000: 	    # big problem, need to handle.  Next is probably wrong
 1001: 	    last;
 1002: 	}
 1003: 	my @nextResources=();
 1004: 	foreach (split(/\,/,$hash{'to_'.$currentResourceID})) {
 1005: 	    push(@nextResources, $hash{'goesto_'.$_});
 1006: 	}
 1007: 	push(@currentResource, @nextResources);
 1008: 	# Set the next resource to be processed
 1009: 	$currentResourceID=pop(@currentResource);
 1010:     }
 1011: 
 1012:     unless (untie(%hash)) {
 1013:         &Apache::lonnet::logthis("<font color=blue>WARNING: ".
 1014:                                  "Could not untie coursemap $fn (browse)".
 1015:                                  ".</font>"); 
 1016:     }
 1017: 
 1018:     unless (untie(%CacheData)) {
 1019:         &Apache::lonnet::logthis("<font color=blue>WARNING: ".
 1020:                                  "Could not untie Cache Hash (browse)".
 1021:                                  ".</font>"); 
 1022:     }
 1023: 
 1024:     return 'OK';
 1025: }
 1026: 
 1027: =pod
 1028: 
 1029: =item &ProcessSection()
 1030: 
 1031: Determine the section number for a student for the class.  A student can have 
 1032: multiple sections for the same class.  The correct one is chosen.
 1033: 
 1034: =over 4
 1035: 
 1036: Input: $sectionData, $courseid, $ActiveFlag
 1037: 
 1038: $sectionData:  A pointer to a hash containing all section data for this 
 1039: student for the class
 1040: 
 1041: $courseid:  The course ID.
 1042: 
 1043: $ActiveFlag:  The student's active status (Active/Expired)
 1044: 
 1045: Output: $oldsection, $cursection, or -1
 1046: 
 1047: $oldsection and $cursection and sections number that will be displayed in the 
 1048: chart.
 1049: 
 1050: -1 is returned if an error occurs.
 1051: 
 1052: =back
 1053: 
 1054: =cut
 1055: 
 1056: sub ProcessSection {
 1057:     my ($sectionData, $courseid,$ActiveFlag)=@_;
 1058:     $courseid=~s/\_/\//g;
 1059:     $courseid=~s/^(\w)/\/$1/;
 1060: 
 1061:     my $cursection='-1';
 1062:     my $oldsection='-1';
 1063:     my $status='Expired';
 1064:     my $section='';
 1065:     foreach my $key (keys (%$sectionData)) {
 1066: 	my $value = $sectionData->{$key};
 1067:         if ($key=~/^$courseid(?:\/)*(\w+)*\_st$/) {
 1068: 	    $section=$1;
 1069: 	    if($key eq $courseid.'_st') {
 1070: 		$section='';
 1071: 	    }
 1072: 	    my ($dummy,$end,$start)=split(/\_/,$value);
 1073: 	    my $now=time;
 1074: 	    my $notactive=0;
 1075: 	    if ($start) {
 1076: 		if($now<$start) {
 1077: 		    $notactive=1;
 1078: 		}
 1079: 	    }
 1080: 	    if($end) {
 1081: 		if ($now>$end) {
 1082: 		    $notactive=1;
 1083: 		}
 1084: 	    }
 1085: 	    if($notactive == 0) {
 1086: 		$status='Active';
 1087: 		$cursection=$section;
 1088: 		last;
 1089: 	    }
 1090: 	    if($notactive == 1) {
 1091: 		$oldsection=$section;
 1092: 	    }
 1093: 	}
 1094:     }
 1095:     if($status eq $ActiveFlag) {
 1096: 	if($cursection eq '-1') {
 1097: 	    return $oldsection;
 1098: 	}
 1099: 	return $cursection;
 1100:     }
 1101:     if($ActiveFlag eq 'Any') {
 1102: 	if($cursection eq '-1') {
 1103: 	    return $oldsection;
 1104: 	}
 1105: 	return $cursection;
 1106:     }
 1107:     return '-1';
 1108: }
 1109: 
 1110: =pod
 1111: 
 1112: =item &ProcessStudentInformation()
 1113: 
 1114: Takes data downloaded for a student and breaks it up into managable pieces and 
 1115: stored in cache data.  The username, domain, class related date, PID, 
 1116: full name, and section are all processed here.
 1117: 
 1118: =over 4
 1119: 
 1120: Input: $CacheData, $studentInformation, $section, $date, $name, $courseID
 1121: 
 1122: $CacheData:  A hash pointer to the cached data
 1123: 
 1124: $studentInformation:  Student information is what was requested in 
 1125: &DownloadPrerequistedData().  See that function for what data is requested.
 1126: 
 1127: $section: A hash pointer to class section related information.
 1128: 
 1129: $date:  A composite of the start and end date for this class for this
 1130: student.  Format:  end:start
 1131: 
 1132: $name:  the username:domain information
 1133: 
 1134: $courseID: The course ID
 1135: 
 1136: Output: None
 1137: 
 1138: *NOTE:  There is no return value, but if an error occurs a key is added to 
 1139: the cache data with the value being the error message.  The key is 
 1140: username:domain:error.  It will only exist if an error occurs.
 1141: 
 1142: =back
 1143: 
 1144: =cut
 1145: 
 1146: sub ProcessStudentInformation {
 1147:     my ($CacheData,$studentInformation,$section,$date,$name,$courseID)=@_;
 1148:     my ($studentName,$studentDomain) = split(/\:/,$name);
 1149: 
 1150:     $CacheData->{$name.':username'}=$studentName;
 1151:     $CacheData->{$name.':domain'}=$studentDomain;
 1152:     $CacheData->{$name.':date'}=$date;
 1153: 
 1154:     my ($checkForError)=keys(%$studentInformation);
 1155:     if($checkForError =~ /^(con_lost|error|no_such_host)/i) {
 1156: 	$CacheData->{$name.':error'}=
 1157: 	    'Could not download student environment data.';
 1158: 	$CacheData->{$name.':fullname'}='';
 1159: 	$CacheData->{$name.':id'}='';
 1160:     } else {
 1161: 	$CacheData->{$name.':fullname'}=&ProcessFullName(
 1162:                                           $studentInformation->{'lastname'},
 1163: 				          $studentInformation->{'generation'},
 1164: 				          $studentInformation->{'firstname'},
 1165:                                           $studentInformation->{'middlename'});
 1166: 	$CacheData->{$name.':id'}=$studentInformation->{'id'};
 1167:     }
 1168: 
 1169:     # Get student's section number
 1170:     my $sec=&ProcessSection($section, $courseID, $CacheData->{'form.status'});
 1171:     if($sec != -1) {
 1172: 	$CacheData->{$name.':section'}=$sec;
 1173:     } else {
 1174: 	$CacheData->{$name.':section'}='';
 1175:     }
 1176: 
 1177:     return;
 1178: }
 1179: 
 1180: =pod
 1181: 
 1182: =item &ProcessClassList()
 1183: 
 1184: Taking the class list dumped from &DownloadPrerequisiteData(), all the 
 1185: students and their non-class information is processed using the 
 1186: &ProcessStudentInformation() function.  A date stamp is also recorded for
 1187: when the data was processed.
 1188: 
 1189: =over 4
 1190: 
 1191: Input: $classlist, $courseID, $ChartDB, $c
 1192: 
 1193: $classlist:  The hash of data collected about a student from 
 1194: &DownloadPrerequisteData().  The hash contains a list of students, a pointer 
 1195: to a hash of student information for each student, and each student's section 
 1196: number.
 1197: 
 1198: $courseID:  The course ID
 1199: 
 1200: $ChartDB:  The name of the cache database file.
 1201: 
 1202: $c:  The connection class used to determine if an abort has been sent to the 
 1203: browser
 1204: 
 1205: Output: @names
 1206: 
 1207: @names:  An array of students whose information has been processed, and are to 
 1208: be considered in an arbitrary order.
 1209: 
 1210: =back
 1211: 
 1212: =cut
 1213: 
 1214: sub ProcessClassList {
 1215:     my ($classlist,$courseID,$ChartDB,$c)=@_;
 1216:     my @names=();
 1217: 
 1218:     my %CacheData;
 1219:     if(tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_WRCREAT,0640)) {
 1220:         foreach my $name (keys(%$classlist)) {
 1221:             if($name =~ /\:section/ || $name =~ /\:studentInformation/ ||
 1222:                $name eq '') {
 1223:                 next;
 1224:             }
 1225:             if($c->aborted()) {
 1226:                 last;
 1227:             }
 1228:             push(@names,$name);
 1229:             &ProcessStudentInformation(
 1230:                                     \%CacheData,
 1231:                                     $classlist->{$name.':studentInformation'},
 1232:                                     $classlist->{$name.':section'},
 1233:                                     $classlist->{$name},
 1234:                                     $name,$courseID);
 1235:         }
 1236: 
 1237:         # Time of download
 1238:         $CacheData{'time'}=localtime();
 1239: 	untie(%CacheData);
 1240:     }
 1241: 
 1242:     return @names;
 1243: }
 1244: 
 1245: =pod
 1246: 
 1247: =item &ProcessStudentData()
 1248: 
 1249: Takes the course data downloaded for a student in 
 1250: &DownloadStudentCourseInformation() and breaks it up into key value pairs
 1251: to be stored in the cached data.  The keys are comprised of the 
 1252: $username:$domain:$keyFromCourseDatabase.  The student username:domain is
 1253: stored away signifying that the student's information has been downloaded and 
 1254: can be reused from cached data.
 1255: 
 1256: =over 4
 1257: 
 1258: Input: $courseData, $name, $ChartDB
 1259: 
 1260: $courseData:  A hash pointer that points to the course data downloaded for a 
 1261: student.
 1262: 
 1263: $name:  username:domain
 1264: 
 1265: $ChartDB:  The name of the cache database file which will allow the data to
 1266: be written to the cache.
 1267: 
 1268: Output: None
 1269: 
 1270: *NOTE:  There is no output, but an error message is stored away in the cache 
 1271: data.  This is checked in &FormatStudentData().  The key username:domain:error 
 1272: will only exist if an error occured.  The error is an error from 
 1273: &DownloadStudentCourseInformation().
 1274: 
 1275: =back
 1276: 
 1277: =cut
 1278: 
 1279: sub ProcessStudentData {
 1280:     my ($courseData, $name, $ChartDB)=@_;
 1281: 
 1282:     my %CacheData;
 1283:     if(tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_WRCREAT,0640)) {
 1284:         my ($checkForError) = keys(%$courseData);
 1285:         if($checkForError =~ /^(con_lost|error|no_such_host)/i) {
 1286:             $CacheData{$name.':error'}='Could not download course data.';
 1287:         } else {
 1288:             foreach my $key (keys (%$courseData)) {
 1289:                 $CacheData{$name.':'.$key}=$courseData->{$key};
 1290:             }
 1291:             if(defined($CacheData{'NamesOfStudents'})) {
 1292:                 $CacheData{'NamesOfStudents'}.=':::'.$name;
 1293:             } else {
 1294:                 $CacheData{'NamesOfStudents'}=$name;
 1295:             }
 1296:         }
 1297:         untie(%CacheData);
 1298:     }
 1299: 
 1300:     return;
 1301: }
 1302: 
 1303: =pod
 1304: 
 1305: =item &ProcessFormData()
 1306: 
 1307: Cache form data and set default form data (sort, status, heading.$number,
 1308: sequence.$number, reselect, reset, recalculate, and refresh)
 1309: 
 1310: =over 4
 1311: 
 1312: Input: $ChartDB, $isCached
 1313: 
 1314: $ChartDB: The name of the database for cached data
 1315: 
 1316: $isCached: Is there already data for this course cached.  This is used in 
 1317: conjunction with the absence of all form data to know to display all selection 
 1318: types.
 1319: 
 1320: Output: None
 1321: 
 1322: =back
 1323: 
 1324: =cut
 1325: 
 1326: # For all data, if ENV data doesn't exist for it, default values is used.
 1327: sub ProcessFormData {
 1328:     my ($ChartDB, $isCached)=@_;
 1329:     my %CacheData;
 1330: 
 1331:     if(tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_WRCREAT,0640)) {
 1332:         # Ignore $ENV{'form.refresh'}
 1333:         # Ignore $ENV{'form.recalculate'}
 1334: 
 1335:         if(defined($ENV{'form.sort'})) {
 1336:             $CacheData{'form.sort'}=$ENV{'form.sort'};
 1337:         } elsif(!defined($CacheData{'form.sort'})) {
 1338:             $CacheData{'form.sort'}='username';
 1339:         }
 1340: 
 1341:         if(defined($ENV{'form.status'})) {
 1342:             $CacheData{'form.status'}=$ENV{'form.status'};
 1343:         } elsif(!defined($CacheData{'form.status'})) {
 1344:             $CacheData{'form.status'}='Active';
 1345:         }
 1346: 
 1347:         # $found checks for any instances of form data in the ENV.  If it is
 1348:         # missing I assume the chrt button on the remote has been pressed.
 1349:         my @headings=();
 1350:         my @sequences=();
 1351:         my $found=0;
 1352:         foreach (keys(%ENV)) {
 1353:             if(/form\.heading/) {
 1354:                 $found++;
 1355:                 push(@headings, $_);
 1356:             } elsif(/form\.sequence/) {
 1357:                 $found++;
 1358:                 push(@sequences, $_);
 1359:             } elsif(/form\./) {
 1360:                 $found++;
 1361:             }
 1362:         }
 1363: 
 1364:         if($found) {
 1365:             $CacheData{'form.headings'}=join(":::",@headings);
 1366:             $CacheData{'form.sequences'}=join(":::",@sequences);
 1367:         }
 1368: 
 1369:         if(defined($ENV{'form.reselect'})) {
 1370:             my @reselected = (ref($ENV{'form.reselect'}) ? 
 1371:                               @{$ENV{'form.reselect'}}
 1372:                               : ($ENV{'form.reselect'}));
 1373:             foreach (@reselected) {
 1374:                 if(/heading/) {
 1375:                     $CacheData{'form.headings'}.=":::".$_;
 1376:                 } elsif(/sequence/) {
 1377:                     $CacheData{'form.sequences'}.=":::".$_;
 1378:                 }
 1379:             }
 1380:         }
 1381: 
 1382:         # !$found and !$isCached are how I determine if the chrt button
 1383:         # on the remote was pressed and needs to reset all the selections
 1384:         if(defined($ENV{'form.reset'}) || (!$found && !$isCached)) {
 1385:             $CacheData{'form.reset'}='true';
 1386:             $CacheData{'form.status'}='Active';
 1387:             $CacheData{'form.sort'}='username';
 1388:             $CacheData{'form.headings'}='ALLHEADINGS';
 1389:             $CacheData{'form.sequences'}='ALLSEQUENCES';
 1390:         } else {
 1391:             $CacheData{'form.reset'}='false';
 1392:         }
 1393: 
 1394:         untie(%CacheData);
 1395:     }
 1396: 
 1397:     return;
 1398: }
 1399: 
 1400: =pod
 1401: 
 1402: =item &SpaceColumns()
 1403: 
 1404: Determines the width of all the columns in the chart.  It is based on
 1405: the max of the data for that column and its header.
 1406: 
 1407: =over 4
 1408: 
 1409: Input: $students, $studentInformation, $headings, $ChartDB
 1410: 
 1411: $students: An array pointer to a list of students (username:domain)
 1412: 
 1413: $studentInformatin: The type of data for the student information.  It is
 1414: used as part of the key in $CacheData.
 1415: 
 1416: $headings: The name of the student information columns.
 1417: 
 1418: $ChartDB: The name of the cache database which is opened for read/write.
 1419: 
 1420: Output: None - All data stored in cache.
 1421: 
 1422: =back
 1423: 
 1424: =cut
 1425: 
 1426: sub SpaceColumns {
 1427:     my ($students,$studentInformation,$headings,$ChartDB)=@_;
 1428: 
 1429:     my %CacheData;
 1430:     if(tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_WRCREAT,0640)) {
 1431:         # Initialize Lengths
 1432:         for(my $index=0; $index<(scalar @$headings); $index++) {
 1433: 	    my @titleLength=split(//,$$headings[$index]);
 1434: 	    $CacheData{$$studentInformation[$index].'Length'}=
 1435:                 scalar @titleLength;
 1436: 	}
 1437: 
 1438:         foreach my $name (@$students) {
 1439:             foreach (@$studentInformation) {
 1440: 		my @dataLength=split(//,$CacheData{$name.':'.$_});
 1441: 		my $length=scalar @dataLength;
 1442: 		if($length > $CacheData{$_.'Length'}) {
 1443: 		    $CacheData{$_.'Length'}=$length;
 1444: 		}
 1445:             }
 1446:         }
 1447:         untie(%CacheData);
 1448:     }
 1449: 
 1450:     return;
 1451: }
 1452: 
 1453: # ----- END PROCESSING FUNCTIONS ---------------------------------------
 1454: 
 1455: =pod
 1456: 
 1457: =head1 HELPER FUNCTIONS
 1458: 
 1459: These are just a couple of functions do various odd and end 
 1460: jobs.
 1461: 
 1462: =cut
 1463: 
 1464: # ----- HELPER FUNCTIONS -----------------------------------------------
 1465: 
 1466: =pod
 1467: 
 1468: =item &ProcessFullName()
 1469: 
 1470: Takes lastname, generation, firstname, and middlename (or some partial
 1471: set of this data) and returns the full name version as a string.  Format
 1472: is Lastname generation, firstname middlename or a subset of this.
 1473: 
 1474: =cut
 1475: 
 1476: sub ProcessFullName {
 1477:     my ($lastname, $generation, $firstname, $middlename)=@_;
 1478:     my $Str = '';
 1479: 
 1480:     if($lastname ne '') {
 1481: 	$Str .= $lastname.' ';
 1482: 	if($generation ne '') {
 1483: 	    $Str .= $generation;
 1484: 	} else {
 1485: 	    chop($Str);
 1486: 	}
 1487: 	$Str .= ', ';
 1488: 	if($firstname ne '') {
 1489: 	    $Str .= $firstname.' ';
 1490: 	}
 1491: 	if($middlename ne '') {
 1492: 	    $Str .= $middlename;
 1493: 	} else {
 1494: 	    chop($Str);
 1495: 	    if($firstname eq '') {
 1496: 		chop($Str);
 1497: 	    }
 1498: 	}
 1499:     } else {
 1500: 	if($firstname ne '') {
 1501: 	    $Str .= $firstname.' ';
 1502: 	}
 1503: 	if($middlename ne '') {
 1504: 	    $Str .= $middlename.' ';
 1505: 	}
 1506: 	if($generation ne '') {
 1507: 	    $Str .= $generation;
 1508: 	} else {
 1509: 	    chop($Str);
 1510: 	}
 1511:     }
 1512: 
 1513:     return $Str;
 1514: }
 1515: 
 1516: =pod
 1517: 
 1518: =item &SortStudents()
 1519: 
 1520: Determines which students to display and in which order.  Which are 
 1521: displayed are determined by their status(active/expired).  The order
 1522: is determined by the sort button pressed (default to username).  The
 1523: type of sorting is username, lastname, or section.
 1524: 
 1525: =over 4
 1526: 
 1527: Input: $students, $CacheData
 1528: 
 1529: $students: A array pointer to a list of students (username:domain)
 1530: 
 1531: $CacheData: A pointer to the hash tied to the cached data
 1532: 
 1533: Output: @order
 1534: 
 1535: @order: An ordered list of students (username:domain)
 1536: 
 1537: =back
 1538: 
 1539: =cut
 1540: 
 1541: sub SortStudents {
 1542:     my ($students,$CacheData)=@_;
 1543: 
 1544:     my @sorted1Students=();
 1545:     foreach (@$students) {
 1546:         my ($end,$start)=split(/\:/,$CacheData->{$_.':date'});
 1547:         my $active=1;
 1548:         my $now=time;
 1549:         my $Status=$CacheData->{'form.status'};
 1550:         $Status = ($Status) ? $Status : 'Active';
 1551:         if((($end) && $now > $end) && (($Status eq 'Active'))) { 
 1552:             $active=0; 
 1553:         }
 1554:         if(($Status eq 'Expired') && ($end == 0 || $now < $end)) {
 1555:             $active=0;
 1556:         }
 1557:         if($active) {
 1558:             push(@sorted1Students, $_);
 1559:         }
 1560:     }
 1561: 
 1562:     my $Pos = $CacheData->{'form.sort'};
 1563:     my %sortData;
 1564:     if($Pos eq 'Last Name') {
 1565: 	for(my $index=0; $index<scalar @sorted1Students; $index++) {
 1566: 	    $sortData{$CacheData->{$sorted1Students[$index].':fullname'}}=
 1567: 		$sorted1Students[$index];
 1568: 	}
 1569:     } elsif($Pos eq 'Section') {
 1570: 	for(my $index=0; $index<scalar @sorted1Students; $index++) {
 1571: 	    $sortData{$CacheData->{$sorted1Students[$index].':section'}.
 1572: 		      $sorted1Students[$index]}=$sorted1Students[$index];
 1573: 	}
 1574:     } else {
 1575: 	# Sort by user name
 1576: 	for(my $index=0; $index<scalar @sorted1Students; $index++) {
 1577: 	    $sortData{$sorted1Students[$index]}=$sorted1Students[$index];
 1578: 	}
 1579:     }
 1580: 
 1581:     my @order = ();
 1582:     foreach my $key (sort(keys(%sortData))) {
 1583: 	push (@order,$sortData{$key});
 1584:     }
 1585: 
 1586:     return @order;
 1587: }
 1588: 
 1589: =pod
 1590: 
 1591: =item &TestCacheData()
 1592: 
 1593: Determine if the cache database can be accessed with a tie.  It waits up to
 1594: ten seconds before returning failure.  This function exists to help with
 1595: the problems with stopping the data download.  When an abort occurs and the
 1596: user quickly presses a form button and httpd child is created.  This
 1597: child needs to wait for the other to finish (hopefully within ten seconds).
 1598: 
 1599: =over 4
 1600: 
 1601: Input: $ChartDB
 1602: 
 1603: $ChartDB: The name of the cache database to be opened
 1604: 
 1605: Output: -1, 0, 1
 1606: 
 1607: -1: Couldn't tie database
 1608:  0: Use cached data
 1609:  1: New cache database created, use that.
 1610: 
 1611: =back
 1612: 
 1613: =cut
 1614: 
 1615: sub TestCacheData {
 1616:     my ($ChartDB)=@_;
 1617:     my $isCached=-1;
 1618:     my %testData;
 1619:     my $tieTries=0;
 1620: 
 1621:     if ((-e "$ChartDB") && (!defined($ENV{'form.recalculate'}))) {
 1622: 	$isCached = 1;
 1623:     } else {
 1624: 	$isCached = 0;
 1625:     }
 1626: 
 1627:     while($tieTries < 10) {
 1628:         my $result=0;
 1629:         if($isCached) {
 1630:             $result=tie(%testData,'GDBM_File',$ChartDB,&GDBM_READER,0640);
 1631:         } else {
 1632:             $result=tie(%testData,'GDBM_File',$ChartDB,&GDBM_NEWDB,0640);
 1633:         }
 1634:         if($result) {
 1635:             last;
 1636:         }
 1637:         $tieTries++;
 1638:         sleep 1;
 1639:     }
 1640:     if($tieTries >= 10) {
 1641:         return -1;
 1642:     }
 1643: 
 1644:     untie(%testData);
 1645: 
 1646:     return $isCached;
 1647: }
 1648: 
 1649: =pod
 1650: 
 1651: =item &ShouldShowColumn()
 1652: 
 1653: Determine if a specified column should be shown on the chart.
 1654: 
 1655: =over 4
 1656: 
 1657: Input: $cache, $test
 1658: 
 1659: $cache: A pointer to the hash tied to the cached data
 1660: 
 1661: $test: The form name of the column (heading.$headingIndex) or 
 1662: (sequence.$sequenceIndex)
 1663: 
 1664: Output: 0 (false), 1 (true)
 1665: 
 1666: =back
 1667: 
 1668: =cut
 1669: 
 1670: sub ShouldShowColumn {
 1671:     my ($cache,$test)=@_;
 1672: 
 1673:     if($cache->{'form.reset'} eq 'true') {
 1674:         return 1;
 1675:     }
 1676: 
 1677:     my $headings=$cache->{'form.headings'};
 1678:     my $sequences=$cache->{'form.sequences'};
 1679:     if($headings eq 'ALLHEADINGS' || $sequences eq 'ALLSEQUENCES' ||
 1680:        $headings=~/$test/ || $sequences=~/$test/) {
 1681:         return 1;
 1682:     }
 1683: 
 1684:     return 0;
 1685: }
 1686: 
 1687: # ----- END HELPER FUNCTIONS --------------------------------------------
 1688: 
 1689: =pod
 1690: 
 1691: =head1 Handler and main function(BuildChart)
 1692: 
 1693: The handler does some initial error checking and then passes the torch to
 1694: BuildChart.  BuildChart calls all the appropriate functions to get the
 1695: job done.  These are the only two functions that use print ($r).  All other
 1696: functions return strings to BuildChart to be printed.
 1697: 
 1698: =cut
 1699: 
 1700: =pod
 1701: 
 1702: =item &BuildChart()
 1703: 
 1704:  The following is the process that BuildChart goes through to 
 1705:   create the html document.
 1706: 
 1707:  -Start the lonchart document
 1708:  -Test for access to the CacheData
 1709:  -Download class list information if not using cached data 
 1710:  -Sort students and print out table desciptive data
 1711:  -Output student data
 1712:  -If recalculating, store a list of students, but only if all 
 1713:   their data was downloaded.  Leave off the others.
 1714:  -End document
 1715: 
 1716: =over 4
 1717: 
 1718: Input: $r
 1719: 
 1720: $r:  Used to print html
 1721: 
 1722: Output: None
 1723: 
 1724: =back
 1725: 
 1726: =cut
 1727: 
 1728: sub BuildChart {
 1729:     my ($r)=@_;
 1730:     my $c = $r->connection;
 1731: 
 1732:     # Start the lonchart document
 1733:     $r->content_type('text/html');
 1734:     $r->send_http_header;
 1735:     $r->print(&StartDocument());
 1736:     $r->rflush();
 1737: 
 1738:     # Test for access to the CacheData
 1739:     my $isCached=0;
 1740:     my $cid=$ENV{'request.course.id'};
 1741:     my $ChartDB = "/home/httpd/perl/tmp/$ENV{'user.name'}".
 1742:                   "_$ENV{'user.domain'}_$cid\_chart.db";
 1743: 
 1744:     $isCached=&TestCacheData($ChartDB);
 1745:     if($isCached < 0) {
 1746:         $r->print("Unable to tie hash to db file");
 1747:         $r->rflush();
 1748:         return;
 1749:     }
 1750:     &ProcessFormData($ChartDB, $isCached);
 1751: 
 1752:     # Download class list information if not using cached data
 1753:     my %CacheData;
 1754:     my @students=();
 1755:     my @studentInformation=('username','domain','section','id','fullname');
 1756:     my @headings=('User Name','Domain','Section','PID','Full Name');
 1757:     my $spacePadding='   ';
 1758:     if(!$isCached) {
 1759:         my $processTopResourceMapReturn=&ProcessTopResourceMap($ChartDB,$c);
 1760:         if($processTopResourceMapReturn ne 'OK') {
 1761:             $r->print($processTopResourceMapReturn);
 1762:             return;
 1763:         }
 1764:         if($c->aborted()) { return; }
 1765:         my $classlist=&DownloadPrerequisiteData($cid, $c);
 1766:         my ($checkForError)=keys(%$classlist);
 1767:         if($checkForError =~ /^(con_lost|error|no_such_host)/i ||
 1768:            defined($classlist->{'error'})) {
 1769:             return;
 1770:         }
 1771:         if($c->aborted()) { return; }
 1772:         @students=&ProcessClassList($classlist,$cid,$ChartDB,$c);
 1773:         if($c->aborted()) { return; }
 1774:         &SpaceColumns(\@students,\@studentInformation,\@headings,
 1775:                       $ChartDB);
 1776:         if($c->aborted()) { return; }
 1777:     } else {
 1778:         if(!$c->aborted() && tie(%CacheData,'GDBM_File',$ChartDB,
 1779:                                  &GDBM_READER,0640)) {
 1780:             @students=split(/:::/,$CacheData{'NamesOfStudents'});
 1781:         }
 1782:     }
 1783: 
 1784:     # Sort students and print out table desciptive data
 1785:     my $downloadTime=0;
 1786:     if(tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_READER,0640)) {
 1787:         if(!$c->aborted()) { @students=&SortStudents(\@students,\%CacheData); }
 1788:         if(defined($CacheData{'time'})) { $downloadTime=$CacheData{'time'}; }
 1789:         else { $downloadTime=localtime(); }
 1790:         if(!$c->aborted()) { $r->print('<h3>'.$downloadTime.'</h3>'); }
 1791:         if(!$c->aborted()) { $r->print('<h1>'.(scalar @students).
 1792:                                        ' students</h1>'); }
 1793: 	if(!$c->aborted()) { $r->rflush(); }
 1794: 	if(!$c->aborted()) { $r->print(&CreateLegend()); }
 1795:         if(!$c->aborted()) { $r->print('<table border="0"><tbody>'); }
 1796: 	if(!$c->aborted()) { $r->print(&CreateForm(\%CacheData)); }
 1797: 	if(!$c->aborted()) { $r->print(&CreateColumnSelectionBox(
 1798:                                                        \%CacheData,
 1799:                                                        \@headings)); }
 1800:         if(!$c->aborted()) { $r->print('</tbody></table>'); }
 1801:         if(!$c->aborted()) { $r->print('<b>Note: Uncheck the boxes above a'); }
 1802:         if(!$c->aborted()) { $r->print(' column to remove that column from'); }
 1803:         if(!$c->aborted()) { $r->print(' the display.</b></pre>'); }
 1804:         if(!$c->aborted()) { $r->print('<table border="0" cellpadding="0" '); }
 1805:         if(!$c->aborted()) { $r->print('cellspacing="0"><tbody>'); }
 1806: 	if(!$c->aborted()) { $r->print(&CreateColumnSelectors(
 1807:                                                        \%CacheData,
 1808:                                                        \@headings)); }
 1809: 	if(!$c->aborted()) { $r->print(&CreateTableHeadings(
 1810:                                                          \%CacheData,
 1811:                                                          \@studentInformation, 
 1812: 							 \@headings, 
 1813: 							 $spacePadding)); }
 1814:         if(!$c->aborted()) { $r->print('</tbody></table>'); }
 1815: 	if(!$c->aborted()) { $r->rflush(); }
 1816: 	untie(%CacheData);
 1817:     } else {
 1818: 	$r->print("Init2: Unable to tie hash to db file");
 1819: 	return;
 1820:     }
 1821: 
 1822:     # Output student data
 1823:     my @updateStudentList = ();
 1824:     my $courseData;
 1825:     $r->print('<pre>');
 1826:     foreach (@students) {
 1827:         if($c->aborted()) {
 1828:             last;
 1829:         }
 1830: 
 1831:         if(!$isCached) {
 1832:             $courseData=&DownloadStudentCourseInformation($_, $cid);
 1833:             if($c->aborted()) { last; }
 1834:             push(@updateStudentList, $_);
 1835:             &ProcessStudentData($courseData, $_, $ChartDB);
 1836:         }
 1837:         $r->print(&FormatStudentData($_, \@studentInformation,
 1838:                                      $spacePadding, $ChartDB));
 1839:         $r->rflush();
 1840:     }
 1841: 
 1842:     # If recalculating, store a list of students, but only if all their 
 1843:     # data was downloaded.  Leave off the others.
 1844:     if(!$isCached && tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_WRCREAT,0640)) {
 1845:         $CacheData{'NamesOfStudents'}=join(":::", @updateStudentList);
 1846: #		    $CacheData{'NamesOfStudents'}=
 1847: #		            &Apache::lonnet::arrayref2str(\@updateStudentList);
 1848:         untie(%CacheData);
 1849:     }
 1850: 
 1851:     # End document
 1852:     $r->print('</pre></body></html>');
 1853:     $r->rflush();
 1854: 
 1855:     return;
 1856: }
 1857: 
 1858: # ================================================================ Main Handler
 1859: 
 1860: =pod
 1861: 
 1862: =item &handler()
 1863: 
 1864: The handler checks for permission to access the course data and for 
 1865: initial header problem.  Then it passes the torch to the work horse
 1866: function BuildChart.
 1867: 
 1868: =over 4
 1869: 
 1870: Input: $r
 1871: 
 1872: $r: This is the object that is used to print.
 1873: 
 1874: Output: A Value (OK or HTTP_NOT_ACCEPTABLE)
 1875: 
 1876: =back
 1877: 
 1878: =cut
 1879: 
 1880: sub handler {
 1881:     my $r=shift;
 1882: #    $jr=$r;
 1883:     unless(&Apache::lonnet::allowed('vgr',$ENV{'request.course.id'})) {
 1884: 	$ENV{'user.error.msg'}=
 1885:         $r->uri.":vgr:0:0:Cannot view grades for complete course";
 1886: 	return HTTP_NOT_ACCEPTABLE; 
 1887:     }
 1888: 
 1889:     # Set document type for header only
 1890:     if ($r->header_only) {
 1891:         if($ENV{'browser.mathml'}) {
 1892:             $r->content_type('text/xml');
 1893:         } else {
 1894:             $r->content_type('text/html');
 1895:         }
 1896:         &Apache::loncommon::no_cache($r);
 1897:         $r->send_http_header;
 1898:         return OK;
 1899:     }
 1900: 
 1901:     unless($ENV{'request.course.fn'}) {
 1902:         my $requrl=$r->uri;
 1903:         $ENV{'user.error.msg'}="$requrl:bre:0:0:Course not initialized";
 1904:         return HTTP_NOT_ACCEPTABLE; 
 1905:     }
 1906: 
 1907:     &BuildChart($r);
 1908: 
 1909:     return OK;
 1910: }
 1911: 1;
 1912: __END__

FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>