File:  [LON-CAPA] / loncom / interface / spreadsheet / Spreadsheet.pm
Revision 1.38: download - view: text, annotated - select for diffs
Thu Feb 3 23:59:41 2005 UTC (19 years, 6 months ago) by matthew
Branches: MAIN
CVS tags: HEAD
Safe space changes
Fix &EXT routine by including all appropriate context information (symb,
name, domain, section).
Fixed bug in which the symb was not actually available in the safe space
(used 'usymb' instead of 'symb' to denote the symb).

    1: #
    2: # $Id: Spreadsheet.pm,v 1.38 2005/02/03 23:59:41 matthew Exp $
    3: #
    4: # Copyright Michigan State University Board of Trustees
    5: #
    6: # This file is part of the LearningOnline Network with CAPA (LON-CAPA).
    7: #
    8: # LON-CAPA is free software; you can redistribute it and/or modify
    9: # it under the terms of the GNU General Public License as published by
   10: # the Free Software Foundation; either version 2 of the License, or
   11: # (at your option) any later version.
   12: #
   13: # LON-CAPA is distributed in the hope that it will be useful,
   14: # but WITHOUT ANY WARRANTY; without even the implied warranty of
   15: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   16: # GNU General Public License for more details.
   17: #
   18: # You should have received a copy of the GNU General Public License
   19: # along with LON-CAPA; if not, write to the Free Software
   20: # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
   21: #
   22: # /home/httpd/html/adm/gpl.txt
   23: #
   24: # http://www.lon-capa.org/
   25: #
   26: # The LearningOnline Network with CAPA
   27: # Spreadsheet/Grades Display Handler
   28: #
   29: # POD required stuff:
   30: 
   31: =head1 NAME
   32: 
   33: Spreadsheet
   34: 
   35: =head1 SYNOPSIS
   36: 
   37: =head1 DESCRIPTION
   38: 
   39: =over 4
   40: 
   41: =cut
   42: 
   43: ###################################################
   44: ###################################################
   45: ###                 Spreadsheet                 ###
   46: ###################################################
   47: ###################################################
   48: package Apache::Spreadsheet;
   49: 
   50: use strict;
   51: #use warnings FATAL=>'all';
   52: #no warnings 'uninitialized';
   53: use Apache::Constants qw(:common :http);
   54: use Apache::lonnet;
   55: use Safe;
   56: use Safe::Hole;
   57: use Opcode;
   58: use HTML::Entities();
   59: use HTML::TokeParser;
   60: use Spreadsheet::WriteExcel;
   61: use Time::HiRes;
   62: use Apache::lonlocal;
   63: 
   64: ##
   65: ## Package Variables
   66: ##
   67: my %expiredates;
   68: 
   69: my @UC_Columns = split(//,'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
   70: my @LC_Columns = split(//,'abcdefghijklmnopqrstuvwxyz');
   71: 
   72: ######################################################
   73: 
   74: =pod
   75: 
   76: =item &new
   77: 
   78: Returns a new spreadsheet object.
   79: 
   80: =cut
   81: 
   82: ######################################################
   83: sub new {
   84:     my $this = shift;
   85:     my $class = ref($this) || $this;
   86:     my ($stype) = ($class =~ /Apache::(.*)$/);
   87:     #
   88:     my ($name,$domain,$filename,$usymb)=@_;
   89:     if (! defined($name) || $name eq '') {
   90:         $name = $ENV{'user.name'};
   91:     }
   92:     if (! defined($domain) || $domain eq '') {
   93:         $domain = $ENV{'user.domain'};
   94:     }
   95:     #
   96:     my $self = {
   97:         name     => $name,
   98:         domain   => $domain,
   99:         type     => $stype,
  100:         symb     => $usymb,
  101:         errorlog => '',
  102:         maxrow   => 0,
  103:         cid      => $ENV{'request.course.id'},
  104:         cnum     => $ENV{'course.'.$ENV{'request.course.id'}.'.num'},
  105:         cdom     => $ENV{'course.'.$ENV{'request.course.id'}.'.domain'},
  106:         chome    => $ENV{'course.'.$ENV{'request.course.id'}.'.home'},
  107:         coursedesc => $ENV{'course.'.$ENV{'request.course.id'}.'.description'},
  108:         coursefilename => $ENV{'request.course.fn'},
  109:         #
  110:         # Flags
  111:         temporary => 0,  # true if this sheet has been modified but not saved
  112:         new_rows  => 0, # true if this sheet has new rows
  113:         #
  114:         # blackout is used to determine if any data needs to be hidden from the
  115:         # student.
  116:         blackout => 0,
  117:         #
  118:         # Data storage
  119:         formulas    => {},
  120:         constants   => {},
  121:         rows        => [],
  122:         row_source  => {}, 
  123:         othersheets => [],
  124:     };
  125:     #
  126:     $self->{'uhome'} = &Apache::lonnet::homeserver($name,$domain);
  127:     #
  128:     bless($self,$class);
  129:     #
  130:     # Load in the spreadsheet definition
  131:     $self->filename($filename);
  132:     if (exists($ENV{'form.workcopy'}) && 
  133:         $self->{'type'} eq $ENV{'form.workcopy'}) {
  134:         $self->load_tmp();
  135:     } else {
  136:         $self->load();
  137:     }
  138:     return $self;
  139: }
  140: 
  141: ######################################################
  142: 
  143: =pod
  144: 
  145: =item &filename
  146: 
  147: get or set the filename for a spreadsheet.
  148: 
  149: =cut
  150: 
  151: ######################################################
  152: sub filename {
  153:     my $self = shift();
  154:     if (@_) {
  155:         my ($newfilename) = @_;
  156:         if (! defined($newfilename) || $newfilename eq 'Default' ||
  157:             $newfilename !~ /\w/ || $newfilename eq '') {
  158:             my $key = 'course.'.$self->{'cid'}.'.spreadsheet_default_'.
  159:                 $self->{'type'};
  160:             if (exists($ENV{$key}) && $ENV{$key} ne '') {
  161:                 $newfilename = $ENV{$key};
  162:             } else {
  163:                 $newfilename = 'default_'.$self->{'type'};
  164:             }
  165:         }
  166:         if ($newfilename !~ /\w/ || $newfilename =~ /^\W*$/) {
  167:             $newfilename = 'default_'.$self->{'type'};
  168:         }
  169:         if ($newfilename !~ /^default\.$self->{'type'}$/ &&
  170:             $newfilename !~ /^\/res\/(.*)spreadsheet$/) {
  171:             if ($newfilename !~ /_$self->{'type'}$/) {
  172:                 $newfilename =~ s/[\s_]*$//;
  173:                 $newfilename .= '_'.$self->{'type'};
  174:             }
  175:         }
  176:         $self->{'filename'} = $newfilename;
  177:         return;
  178:     }
  179:     return $self->{'filename'};
  180: }
  181: 
  182: ######################################################
  183: 
  184: =pod
  185: 
  186: =item &make_default()
  187: 
  188: Make the current spreadsheet file the default for the course.  Expires all the
  189: default spreadsheets.......!
  190: 
  191: =cut
  192: 
  193: ######################################################
  194: sub make_default {
  195:     my $self = shift();
  196:     my $result = &Apache::lonnet::put('environment',
  197:             {'spreadsheet_default_'.$self->{'type'} => $self->filename()},
  198:                                      $self->{'cdom'},$self->{'cnum'});
  199:     return $result if ($result ne 'ok');
  200:     my $symb = $self->{'symb'};
  201:     $symb = '' if (! defined($symb));
  202:     &Apache::lonnet::expirespread('','',$self->{'type'},$symb);    
  203: }
  204: 
  205: ######################################################
  206: 
  207: =pod
  208: 
  209: =item &is_default()
  210: 
  211: Returns 1 if the current spreadsheet is the default as specified in the
  212: course environment.  Returns 0 otherwise.
  213: 
  214: =cut
  215: 
  216: ######################################################
  217: sub is_default {
  218:     my $self = shift;
  219:     # Check to find out if we are the default spreadsheet (filenames match)
  220:     my $default_filename = '';
  221:     my %tmphash = &Apache::lonnet::get('environment',
  222:                                        ['spreadsheet_default_'.
  223:                                         $self->{'type'}],
  224:                                        $self->{'cdom'},
  225:                                        $self->{'cnum'});
  226:     my ($tmp) = keys(%tmphash);
  227:     if ($tmp !~ /^(con_lost|error|no_such_host)/i) {
  228:         $default_filename = $tmphash{'spreadsheet_default_'.$self->{'type'}};
  229:     }
  230:     if ($default_filename =~ /^\s*$/) {
  231:         $default_filename = 'default_'.$self->{'type'};
  232:     }
  233:     return 1 if ($self->filename() eq $default_filename);
  234:     return 0;
  235: }
  236: 
  237: sub initialize {
  238:     # This method is here to remind you that it will be overridden by
  239:     # the descendents of the spreadsheet class.
  240: }
  241: 
  242: sub clear_package {
  243:     # This method is here to remind you that it will be overridden by
  244:     # the descendents of the spreadsheet class.
  245: }
  246: 
  247: sub cleanup {
  248:     my $self = shift();
  249:     $self->clear_package();
  250: }
  251: 
  252: sub initialize_spreadsheet_package {
  253:     &load_spreadsheet_expirationdates();
  254:     &clear_spreadsheet_definition_cache();
  255: }
  256: 
  257: sub load_spreadsheet_expirationdates {
  258:     undef %expiredates;
  259:     my $cid=$ENV{'request.course.id'};
  260:     my @tmp = &Apache::lonnet::dump('nohist_expirationdates',
  261:                                     $ENV{'course.'.$cid.'.domain'},
  262:                                     $ENV{'course.'.$cid.'.num'});
  263:     if (lc($tmp[0]) !~ /^error/){
  264:         %expiredates = @tmp;
  265:     }
  266: }
  267: 
  268: sub check_expiration_time {
  269:     my $self = shift;
  270:     my ($time)=@_;
  271:     return 0 if (! defined($time));
  272:     my ($key1,$key2,$key3,$key4,$key5);
  273:     # Description of keys
  274:     #
  275:     # key1: all sheets of this type have expired
  276:     # key2: all sheets of this type for this student
  277:     # key3: all sheets of this type in this map for this student
  278:     # key4: this assessment sheet for this student
  279:     # key5: this assessment sheet for all students
  280:     $key1 = '::'.$self->{'type'}.':';
  281:     $key2 = $self->{'name'}.':'.$self->{'domain'}.':'.$self->{'type'}.':';
  282:     $key3 = $key2.$self->{'container'} if (defined($self->{'container'}));
  283:     $key4 = $key2.$self->{'symb'} if (defined($self->{'symb'}));
  284:     $key5 = $key1.$self->{'symb'} if (defined($self->{'symb'}));
  285:     my $returnvalue = 1; # default to okay
  286:     foreach my $key ($key1,$key2,$key3,$key4,$key5) {
  287:         next if (! defined($key));
  288:         if (exists($expiredates{$key}) && $expiredates{$key} > $time) {
  289:             $returnvalue = 0; # need to recompute
  290:         }
  291:     }
  292:     return $returnvalue;
  293: }
  294: 
  295: ######################################################
  296: 
  297: =pod
  298: 
  299: =item &initialize_safe_space
  300: 
  301: Returns the safe space required by a Spreadsheet object.
  302: 
  303: =head 2 Safe Space Functions
  304: 
  305: =over 4
  306: 
  307: =cut
  308: 
  309: ######################################################
  310: { 
  311: 
  312:     my $safeeval;
  313: 
  314: sub initialize_safe_space {
  315:   my $self = shift;
  316:   my $usection = &Apache::lonnet::getsection($self->{'domain'},
  317:                                              $self->{'name'},
  318:                                              $ENV{'request.course.id'});
  319:   if (! defined($safeeval)) {
  320:       $safeeval = new Safe(shift);
  321:       my $safehole = new Safe::Hole;
  322:       $safeeval->permit("entereval");
  323:       $safeeval->permit(":base_math");
  324:       $safeeval->permit("sort");
  325:       $safeeval->deny(":base_io");
  326:       $safehole->wrap(\&Apache::lonnet::EXT,$safeeval,'&Apache::lonnet::EXT');
  327:       $safehole->wrap(\&mask,$safeeval,'&mask');
  328:       $safeeval->share('$@');
  329:       my $code=<<'ENDDEFS';
  330: # ---------------------------------------------------- Inside of the safe space
  331: #
  332: # f: formulas
  333: # t: intermediate format (variable references expanded)
  334: # v: output values
  335: # c: preloaded constants (A-column)
  336: # rl: row label
  337: # os: other spreadsheets (for student spreadsheet only)
  338: undef %sheet_values;   # Holds the (computed, final) values for the sheet
  339:     # This is only written to by &calc, the spreadsheet computation routine.
  340:     # It is read by many functions
  341: undef %t; # Holds the values of the spreadsheet temporarily. Set in &sett, 
  342:     # which does the translation of strings like C5 into the value in C5.
  343:     # Used in &calc - %t holds the values that are actually eval'd.
  344: undef %f;    # Holds the formulas for each cell.  This is the users
  345:     # (spreadsheet authors) data for each cell.
  346: undef %c; # Holds the constants for a sheet.  In the assessment
  347:     # sheets, this is the A column.  Used in &MINPARM, &MAXPARM, &expandnamed,
  348:     # &sett, and &constants.  There is no &getconstants.
  349:     # &constants is called by &loadstudent, &loadcourse, &load assessment,
  350: undef @os;  # Holds the names of other spreadsheets - this is used to specify
  351:     # the spreadsheets that are available for the assessment sheet.
  352:     # Set by &setothersheets.  &setothersheets is called by &handler.  A
  353:     # related subroutine is &othersheets.
  354: $errorlog = '';
  355: #
  356: $maxrow = 0;
  357: $type = '';
  358: #
  359: # filename/reference of the sheet
  360: $filename = '';
  361: #
  362: # user data
  363: $name = '';
  364: $uhome = '';
  365: $domain  = '';
  366: #
  367: # course data
  368: $csec = '';
  369: $chome= '';
  370: $cnum = '';
  371: $cdom = '';
  372: $cid  = '';
  373: $coursefilename  = '';
  374: #
  375: # symb
  376: $usymb = '';
  377: #
  378: # error messages
  379: $errormsg = '';
  380: #
  381: #-------------------------------------------------------
  382: 
  383: =pod
  384: 
  385: =item EXT(parameter)
  386: 
  387: Calls the system EXT function to determine the value of the given parameter.
  388: 
  389: =cut
  390: 
  391: #-------------------------------------------------------
  392: sub EXT {
  393:     my ($parameter) = @_;
  394:     return '' if (! defined($parameter) || $parameter eq '');
  395:     $parameter =~ s/^parameter\./resource\./;
  396:     my $value = &Apache::lonnet::EXT($parameter,$symb,$domain,$name,$usection);
  397:     return $value;
  398: }
  399: 
  400: #-------------------------------------------------------
  401: 
  402: =pod
  403: 
  404: =item NUM(range)
  405: 
  406: returns the number of items in the range.
  407: 
  408: =cut
  409: 
  410: #-------------------------------------------------------
  411: sub NUM {
  412:     my $mask=&mask(@_);
  413:     my $num= $#{@{grep(eval("/$mask/"),keys(%sheet_values))}}+1;
  414:     return $num;   
  415: }
  416: 
  417: #-------------------------------------------------------
  418: 
  419: =pod
  420: 
  421: =item BIN(low,high,lower,upper)
  422: 
  423: =cut
  424: 
  425: #-------------------------------------------------------
  426: sub BIN {
  427:     my ($low,$high,$lower,$upper)=@_;
  428:     my $mask=&mask($lower,$upper);
  429:     my $num=0;
  430:     foreach (grep eval("/$mask/"),keys(%sheet_values)) {
  431:         if (($sheet_values{$_}>=$low) && ($sheet_values{$_}<=$high)) {
  432:             $num++;
  433:         }
  434:     }
  435:     return $num;   
  436: }
  437: 
  438: #-------------------------------------------------------
  439: 
  440: =pod
  441: 
  442: =item SUM(range)
  443: 
  444: returns the sum of items in the range.
  445: 
  446: =cut
  447: 
  448: #-------------------------------------------------------
  449: sub SUM {
  450:     my $mask=&mask(@_);
  451:     my $sum=0;
  452:     foreach (grep eval("/$mask/"),keys(%sheet_values)) {
  453:         $sum+=$sheet_values{$_};
  454:     }
  455:     return $sum;   
  456: }
  457: 
  458: #-------------------------------------------------------
  459: 
  460: =pod
  461: 
  462: =item MEAN(range)
  463: 
  464: compute the average of the items in the range.
  465: 
  466: =cut
  467: 
  468: #-------------------------------------------------------
  469: sub MEAN {
  470:     my $mask=&mask(@_);
  471:     my $sum=0; 
  472:     my $num=0;
  473:     foreach (grep eval("/$mask/"),keys(%sheet_values)) {
  474:         $sum+=$sheet_values{$_};
  475:         $num++;
  476:     }
  477:     if ($num) {
  478:        return $sum/$num;
  479:     } else {
  480:        return undef;
  481:     }   
  482: }
  483: 
  484: #-------------------------------------------------------
  485: 
  486: =pod
  487: 
  488: =item STDDEV(range)
  489: 
  490: compute the standard deviation of the items in the range.
  491: 
  492: =cut
  493: 
  494: #-------------------------------------------------------
  495: sub STDDEV {
  496:     my $mask=&mask(@_);
  497:     my $sum=0; my $num=0;
  498:     foreach (grep eval("/$mask/"),keys(%sheet_values)) {
  499:         $sum+=$sheet_values{$_};
  500:         $num++;
  501:     }
  502:     unless ($num>1) { return undef; }
  503:     my $mean=$sum/$num;
  504:     $sum=0;
  505:     foreach (grep eval("/$mask/"),keys(%sheet_values)) {
  506:         $sum+=($sheet_values{$_}-$mean)**2;
  507:     }
  508:     return sqrt($sum/($num-1));    
  509: }
  510: 
  511: #-------------------------------------------------------
  512: 
  513: =pod
  514: 
  515: =item PROD(range)
  516: 
  517: compute the product of the items in the range.
  518: 
  519: =cut
  520: 
  521: #-------------------------------------------------------
  522: sub PROD {
  523:     my $mask=&mask(@_);
  524:     my $prod=1;
  525:     foreach (grep eval("/$mask/"),keys(%sheet_values)) {
  526:         $prod*=$sheet_values{$_};
  527:     }
  528:     return $prod;   
  529: }
  530: 
  531: #-------------------------------------------------------
  532: 
  533: =pod
  534: 
  535: =item MAX(range)
  536: 
  537: compute the maximum of the items in the range.
  538: 
  539: =cut
  540: 
  541: #-------------------------------------------------------
  542: sub MAX {
  543:     my $mask=&mask(@_);
  544:     my $max='-';
  545:     foreach (grep eval("/$mask/"),keys(%sheet_values)) {
  546:         unless ($max) { $max=$sheet_values{$_}; }
  547:         if (($sheet_values{$_}>$max) || ($max eq '-')) { 
  548:             $max=$sheet_values{$_}; 
  549:         }
  550:     } 
  551:     return $max;   
  552: }
  553: 
  554: #-------------------------------------------------------
  555: 
  556: =pod
  557: 
  558: =item MIN(range)
  559: 
  560: compute the minimum of the items in the range.
  561: 
  562: =cut
  563: 
  564: #-------------------------------------------------------
  565: sub MIN {
  566:     my $mask=&mask(@_);
  567:     my $min='-';
  568:     foreach (grep eval("/$mask/"),keys(%sheet_values)) {
  569:         unless ($max) { $max=$sheet_values{$_}; }
  570:         if (($sheet_values{$_}<$min) || ($min eq '-')) { 
  571:             $min=$sheet_values{$_}; 
  572:         }
  573:     }
  574:     return $min;   
  575: }
  576: 
  577: #-------------------------------------------------------
  578: 
  579: =pod
  580: 
  581: =item SUMMAX(num,lower,upper)
  582: 
  583: compute the sum of the largest 'num' items in the range from
  584: 'lower' to 'upper'
  585: 
  586: =cut
  587: 
  588: #-------------------------------------------------------
  589: sub SUMMAX {
  590:     my ($num,$lower,$upper)=@_;
  591:     my $mask=&mask($lower,$upper);
  592:     my @inside=();
  593:     foreach (grep eval("/$mask/"),keys(%sheet_values)) {
  594: 	push (@inside,$sheet_values{$_});
  595:     }
  596:     @inside=sort(@inside);
  597:     my $sum=0; my $i;
  598:     for ($i=$#inside;(($i>$#inside-$num) && ($i>=0));$i--) { 
  599:         $sum+=$inside[$i];
  600:     }
  601:     return $sum;   
  602: }
  603: 
  604: #-------------------------------------------------------
  605: 
  606: =pod
  607: 
  608: =item SUMMIN(num,lower,upper)
  609: 
  610: compute the sum of the smallest 'num' items in the range from
  611: 'lower' to 'upper'
  612: 
  613: =cut
  614: 
  615: #-------------------------------------------------------
  616: sub SUMMIN {
  617:     my ($num,$lower,$upper)=@_;
  618:     my $mask=&mask($lower,$upper);
  619:     my @inside=();
  620:     foreach (grep eval("/$mask/"),keys(%sheet_values)) {
  621: 	$inside[$#inside+1]=$sheet_values{$_};
  622:     }
  623:     @inside=sort(@inside);
  624:     my $sum=0; my $i;
  625:     for ($i=0;(($i<$num) && ($i<=$#inside));$i++) { 
  626:         $sum+=$inside[$i];
  627:     }
  628:     return $sum;   
  629: }
  630: 
  631: #-------------------------------------------------------
  632: 
  633: =pod
  634: 
  635: =item MINPARM(parametername)
  636: 
  637: Returns the minimum value of the parameters matching the parametername.
  638: parametername should be a string such as 'duedate'.
  639: 
  640: =cut
  641: 
  642: #-------------------------------------------------------
  643: sub MINPARM {
  644:     my ($expression) = @_;
  645:     my $min = undef;
  646:     foreach $parameter (keys(%c)) {
  647:         next if ($parameter !~ /$expression/);
  648:         if ((! defined($min)) || ($min > $c{$parameter})) {
  649:             $min = $c{$parameter} 
  650:         }
  651:     }
  652:     return $min;
  653: }
  654: 
  655: #-------------------------------------------------------
  656: 
  657: =pod
  658: 
  659: =item MAXPARM(parametername)
  660: 
  661: Returns the maximum value of the parameters matching the input parameter name.
  662: parametername should be a string such as 'duedate'.
  663: 
  664: =cut
  665: 
  666: #-------------------------------------------------------
  667: sub MAXPARM {
  668:     my ($expression) = @_;
  669:     my $max = undef;
  670:     foreach $parameter (keys(%c)) {
  671:         next if ($parameter !~ /$expression/);
  672:         if ((! defined($min)) || ($max < $c{$parameter})) {
  673:             $max = $c{$parameter} 
  674:         }
  675:     }
  676:     return $max;
  677: }
  678: 
  679: 
  680: sub calc {
  681:     %sheet_values = %t;
  682:     my $notfinished = 1;
  683:     my $lastcalc = '';
  684:     my $depth = 0;
  685:     while ($notfinished) {
  686: 	$notfinished=0;
  687:         while (my ($cell,$value) = each(%t)) {
  688:             my $old=$sheet_values{$cell};
  689:             $sheet_values{$cell}=eval $value;
  690: #            $errorlog .= $cell.' = '.$old.'->'.$sheet_values{$cell}."\n";
  691: 	    if ($@) {
  692: 		undef %sheet_values;
  693:                 return $cell.': '.$@;
  694:             }
  695: 	    if ($sheet_values{$cell} ne $old) { 
  696:                 $notfinished=1; 
  697:                 $lastcalc=$cell; 
  698:             }
  699:         }
  700: #        $errorlog.="------------------------------------------------";
  701: 
  702:         $depth++;
  703:         if ($depth>100) {
  704: 	    undef %sheet_values;
  705:             return $lastcalc.': Maximum calculation depth exceeded';
  706:         }
  707:     }
  708:     return 'okay';
  709: }
  710: 
  711: # ------------------------------------------- End of "Inside of the safe space"
  712: ENDDEFS
  713:         $safeeval->reval($code);
  714:     }
  715:     $self->{'safe'} = $safeeval;
  716:     $self->{'root'} = $self->{'safe'}->root();
  717:     #
  718:     # Place some of the %$self  items into the safe space except the safe space
  719:     # itself
  720:     my $initstring = '';
  721:     foreach (qw/name domain type symb cid csec coursefilename
  722:              cnum cdom chome uhome/) {
  723:         $initstring.= qq{\$$_="$self->{$_}";};
  724:     }
  725:     $initstring.=qq{\$usection="$usection";};
  726:     $self->{'safe'}->reval($initstring);
  727:     return $self;
  728: }
  729: 
  730: }
  731: 
  732: ######################################################
  733: 
  734: =pod
  735: 
  736: =back
  737: 
  738: =cut
  739: 
  740: ######################################################
  741: 
  742: 
  743: ######################################################
  744: 
  745: =pod
  746: 
  747: =item  &mask($lower,$upper)
  748: 
  749: Inputs: $lower and $upper, cell names ("X12" or "a150") or globs ("X*").
  750: 
  751: Returns:  Regular expression matching spreadsheet cells that are within
  752: the rectangle defined by $lower and $upper.  Due to the nature of the
  753: regular expression this result must be used inside an eval().
  754: 
  755: =cut
  756: 
  757: ######################################################
  758: {
  759: 
  760: my %memoizer;
  761: 
  762: sub mask {
  763:     my ($lower,$upper)=@_;
  764:     my $key = $lower.'_'.$upper;
  765:     if (exists($memoizer{$key})) {
  766:         return $memoizer{$key};
  767:     }
  768:     $upper = $lower if (! defined($upper));
  769:     #
  770:     my ($la,$ld) = ($lower=~/([A-z]|\*)(\d+|\*)/);
  771:     my ($ua,$ud) = ($upper=~/([A-z]|\*)(\d+|\*)/);
  772:     #
  773:     my $alpha='';
  774:     my $num='';
  775:     #
  776:     # Do not put parenthases around $alpha.
  777:     # $num depends on the value in $1.
  778:     if (($la eq '*') || ($ua eq '*')) {
  779:         $alpha='[A-z]';
  780:     } else {
  781:         if ($la gt $ua) {
  782:             my $tmp = $ua;
  783:             $ua = $la;
  784:             $la = $ua;
  785:         }
  786:         $alpha=qq/[$la-$ua]/;
  787:     }
  788:     if ($ld ne '*' && $ud ne '*') {
  789:         # Make sure $ld <= $ud
  790:         if ($ld > $ud) {
  791:             my $tmp = $ud;
  792:             $ud = $ld;
  793:             $ld = $tmp;
  794:         }
  795:         # Here we make a regular expression using some advanced regexp
  796:         # abilities.
  797:         # (\d+) will match the digits of the cell name and dump them in
  798:         #     to $1
  799:         # (?(?{ ... code ...} pattern_if_true | pattern_if_false)) will
  800:         #     choose pattern_if_true if { ... code ... } is true and
  801:         #     pattern_if_false if { ... code ... } is false.
  802:         # In this case, pattern_if_true is empty.  pattern_if_false is 
  803:         #     'donotmatch' and will not match our cells because none of 
  804:         #     them end with donotmatch.  
  805:         # Unfortunately, the use of this type of regular expression 
  806:         #     requires that each match be wrapped in an eval().  Search for
  807:         #     $mask in this module for examples
  808:         $num = '(\d+)(?(?{$1>= '.$ld.' && $1<='.$ud.'})|donotmatch)';
  809:     } else {
  810:         $num = '(\d+)';
  811:     }
  812:     my $expression = '^'.$alpha.$num.'$';
  813:     $memoizer{$key} = $expression;
  814:     return $expression;
  815: }
  816: 
  817: #
  818: # Debugging routine
  819: sub dump_memoized_values {
  820:     while (my ($key,$value) = each(%memoizer)) {
  821:         &Apache::lonnet::logthis('memoizer: '.$key.' = '.$value);
  822:     }
  823:     return;
  824: }
  825: 
  826: }
  827: 
  828: ##
  829: ## sub add_hash_to_safe {} # spreadsheet, would like to destroy
  830: ##
  831: 
  832: #
  833: # expandnamed used to reside in the safe space
  834: #
  835: sub expandnamed {
  836:     my $self = shift;
  837:     my $expression=shift;
  838:     if ($expression=~/^\&/) {
  839: 	my ($func,$var,$formula)=($expression=~/^\&(\w+)\(([^\;]+)\;(.*)\)/);
  840: 	my @vars=split(/\W+/,$formula);
  841:         my %values=();
  842: 	foreach my $varname ( @vars ) {
  843:             if ($varname=~/^(parameter|stores|timestamp)/) {
  844:                 $formula=~s/$varname/'$c{\''.$varname.'\'}'/ge;
  845:                $varname=~s/$var/\([\\w:\\- ]\+\)/g;
  846: 	       foreach (keys(%{$self->{'constants'}})) {
  847: 		  if ($_=~/$varname/) {
  848: 		      $values{$1}=1;
  849:                   }
  850:                }
  851: 	    }
  852:         }
  853:         if ($func eq 'EXPANDSUM') {
  854:             my $result='';
  855: 	    foreach (keys(%values)) {
  856:                 my $thissum=$formula;
  857:                 $thissum=~s/$var/$_/g;
  858:                 $result.=$thissum.'+';
  859:             } 
  860:             $result=~s/\+$//;
  861:             return $result;
  862:         } else {
  863: 	    return 0;
  864:         }
  865:     } else {
  866:         # it is not a function, so it is a parameter name
  867:         # We should do the following:
  868:         #    1. Take the list of parameter names
  869:         #    2. look through the list for ones that match the parameter we want
  870:         #    3. If there are no collisions, return the one that matches
  871:         #    4. If there is a collision, return 'bad parameter name error'
  872:         my $returnvalue = '';
  873:         my @matches = ();
  874:         my @values = ();
  875:         $#matches = -1;
  876:         while (my($parameter,$value) = each(%{$self->{'constants'}})) {
  877:             next if ($parameter !~ /$expression/);
  878:             push(@matches,$parameter);
  879:             push(@values,$value);
  880:         }
  881:         if (scalar(@matches) == 0) {
  882:             $returnvalue = '""';#'"unmatched parameter: '.$parameter.'"';
  883:         } elsif (scalar(@matches) == 1) {
  884:             # why do we not do this lookup here, instead of delaying it?
  885:             $returnvalue = $values[0];
  886:         } elsif (scalar(@matches) > 0) {
  887:             # more than one match.  Look for a concise one
  888:             $returnvalue =  "'non-unique parameter name : $expression'";
  889:             for (my $i=0; $i<=$#matches;$i++) {
  890:                 if ($matches[$i] =~ /^$expression$/) {
  891:                     # why do we not do this lookup here?
  892:                     $returnvalue = $values[$i];
  893:                 }
  894:             }
  895:         } else {
  896:             # There was a negative number of matches, which indicates 
  897:             # something is wrong with reality.  Better warn the user.
  898:             $returnvalue = '"bizzare parameter: '.$expression.'"';
  899:         }
  900:         return $returnvalue;
  901:     }
  902: }
  903: 
  904: sub sett {
  905:     my $self = shift;
  906:     my %t=();
  907:     #
  908:     # Deal with the template row
  909:     foreach my $col ($self->template_cells()) {
  910:         next if ($col=~/^[A-Z]/);
  911:         foreach my $row ($self->rows()) {
  912:             # Get the name of this cell
  913:             my $cell=$col.$row;
  914:             # Grab the template declaration
  915:             $t{$cell}=$self->formula('template_'.$col);
  916:             # Replace '#' with the row number
  917:             $t{$cell}=~s/\#/$row/g;
  918:             # Replace '....' with ','
  919:             $t{$cell}=~s/\.\.+/\,/g;
  920:             # Replace 'A0' with the value from 'A0'
  921:             $t{$cell}=~s/(^|[^\"\'])([A-Za-z]\d+)/$1\$sheet_values\{\'$2\'\}/g;
  922:             # Replace parameters
  923:             $t{$cell}=~s/(^|[^\"\'])\[([^\]]+)\]/$1.$self->expandnamed($2)/ge;
  924:         }
  925:     }
  926:     #
  927:     # Deal with the normal cells
  928:     while (my($cell,$formula) = each(%{$self->{'formulas'}})) {
  929: 	next if ($_=~/^template\_/);
  930:         my ($col,$row) = ($cell =~ /^([A-z])(\d+)$/);
  931:         if ($row eq '0') {
  932:             $t{$cell}=$formula;
  933:             $t{$cell}=~s/\.\.+/\,/g;
  934:             $t{$cell}=~s/(^|[^\"\'])([A-Za-z]\d+)/$1\$sheet_values\{\'$2\'\}/g;
  935:             $t{$cell}=~s/(^|[^\"\'])\[([^\]]+)\]/$1.$self->expandnamed($2)/ge;
  936:         } elsif  ( $col  =~ /^[A-Z]$/  ) {
  937:             if ($formula !~ /^\!/ && exists($self->{'constants'}->{$cell})) {
  938:                 my $data = $self->{'constants'}->{$cell};
  939:                 $t{$cell} = $data;
  940:             }
  941:         } else { # $row > 1 and $col =~ /[a-z]
  942:             $t{$cell}=$formula;
  943:             $t{$cell}=~s/\.\.+/\,/g;
  944:             $t{$cell}=~s/(^|[^\"\'])([A-Za-z]\d+)/$1\$sheet_values\{\'$2\'\}/g;
  945:             $t{$cell}=~s/(^|[^\"\'])\[([^\]]+)\]/$1.$self->expandnamed($2)/ge;
  946:         }
  947:     }
  948:     %{$self->{'safe'}->varglob('t')}=%t;
  949: }
  950: 
  951: ##
  952: ## sync_safe_space:  Called by calcsheet to make sure all the data we 
  953: #  need to calculate is placed into the safe space
  954: ##
  955: sub sync_safe_space {
  956:     my $self = shift;
  957:     # Inside the safe space 'formulas' has a diabolical alter-ego named 'f'.
  958:     %{$self->{'safe'}->varglob('f')}=%{$self->{'formulas'}};
  959:     # 'constants' leads a peaceful hidden life of 'c'.
  960:     %{$self->{'safe'}->varglob('c')}=%{$self->{'constants'}};
  961:     # 'othersheets' hides as 'os', a disguise few can penetrate.
  962:     @{$self->{'safe'}->varglob('os')}=@{$self->{'othersheets'}};
  963: }
  964: 
  965: ##
  966: ## Retrieve the error log from the safe space (used for debugging)
  967: ##
  968: sub get_errorlog {
  969:     my $self = shift;
  970:     $self->{'errorlog'} = $ { $self->{'safe'}->varglob('errorlog') };
  971:     return $self->{'errorlog'};
  972: }
  973: 
  974: ##
  975: ## Clear the error log inside the safe space
  976: ##
  977: sub clear_errorlog {
  978:     my $self = shift;
  979:     $ {$self->{'safe'}->varglob('errorlog')} = '';
  980:     $self->{'errorlog'} = '';
  981: }
  982: 
  983: ##
  984: ## constants:  either set or get the constants
  985: ##
  986: sub constants {
  987:     my $self=shift;
  988:     my ($constants) = @_;
  989:     if (defined($constants)) {
  990:         if (! ref($constants)) {
  991:             my %tmp = @_;
  992:             $constants = \%tmp;
  993:         }
  994:         $self->{'constants'} = $constants;
  995:         return;
  996:     } else {
  997:         return %{$self->{'constants'}};
  998:     }
  999: }
 1000: 
 1001: ##
 1002: ## formulas: either set or get the formulas
 1003: ##
 1004: sub formulas {
 1005:     my $self=shift;
 1006:     my ($formulas) = @_;
 1007:     if (defined($formulas)) {
 1008:         if (! ref($formulas)) {
 1009:             my %tmp = @_;
 1010:             $formulas = \%tmp;
 1011:         }
 1012:         $self->{'formulas'} = $formulas;
 1013:         $self->{'rows'} = [];
 1014:         $self->{'template_cells'} = [];
 1015:         return;
 1016:     } else {
 1017:         return %{$self->{'formulas'}};
 1018:     }
 1019: }
 1020: 
 1021: sub set_formula {
 1022:     my $self = shift;
 1023:     my ($cell,$formula) = @_;
 1024:     $self->{'formulas'}->{$cell}=$formula;
 1025:     return;
 1026: }
 1027: 
 1028: ##
 1029: ## formulas_keys:  Return the keys to the formulas hash.
 1030: ##
 1031: sub formulas_keys {
 1032:     my $self = shift;
 1033:     my @keys = keys(%{$self->{'formulas'}});
 1034:     return keys(%{$self->{'formulas'}});
 1035: }
 1036: 
 1037: ##
 1038: ## formula:  Return the formula for a given cell in the spreadsheet
 1039: ## returns '' if the cell does not have a formula or does not exist
 1040: ##
 1041: sub formula {
 1042:     my $self = shift;
 1043:     my $cell = shift;
 1044:     if (defined($cell) && exists($self->{'formulas'}->{$cell})) {
 1045:         return $self->{'formulas'}->{$cell};
 1046:     }
 1047:     return '';
 1048: }
 1049: 
 1050: ##
 1051: ## logthis: write the input to lonnet.log
 1052: ##
 1053: sub logthis {
 1054:     my $self = shift;
 1055:     my $message = shift;
 1056:     &Apache::lonnet::logthis($self->{'type'}.':'.
 1057:                              $self->{'name'}.':'.$self->{'domain'}.':'.
 1058:                              $message);
 1059:     return;
 1060: }
 1061: 
 1062: ##
 1063: ## dump_formulas_to_log: makes lonnet.log huge...
 1064: ##
 1065: sub dump_formulas_to_log {
 1066:     my $self =shift;
 1067:     $self->logthis("Spreadsheet formulas");
 1068:     $self->logthis("--------------------------------------------------------");
 1069:     while (my ($cell, $formula) = each(%{$self->{'formulas'}})) {
 1070:         $self->logthis('    '.$cell.' = '.$formula);
 1071:     }
 1072:     $self->logthis("--------------------------------------------------------");}
 1073: 
 1074: ##
 1075: ## value: returns the computed value of a particular cell
 1076: ##
 1077: sub value {
 1078:     my $self = shift;
 1079:     my $cell = shift;
 1080:     if (defined($cell) && exists($self->{'values'}->{$cell})) {
 1081:         return $self->{'values'}->{$cell};
 1082:     }
 1083:     return '';
 1084: }
 1085: 
 1086: ##
 1087: ## dump_values_to_log: makes lonnet.log huge...
 1088: ##
 1089: sub dump_values_to_log {
 1090:     my $self =shift;
 1091:     $self->logthis("Spreadsheet Values");
 1092:     $self->logthis("------------------------------------------------------");
 1093:     while (my ($cell, $value) = each(%{$self->{'values'}})) {
 1094:         $self->logthis('    '.$cell.' = '.$value);
 1095:     }
 1096:     $self->logthis("------------------------------------------------------");
 1097: }
 1098: 
 1099: ##
 1100: ## Yet another debugging function
 1101: ##
 1102: sub dump_hash_to_log {
 1103:     my $self= shift();
 1104:     my %tmp = @_;
 1105:     if (@_<2) {
 1106:         %tmp = %{$_[0]};
 1107:     }
 1108:     $self->logthis('---------------------------- (begin hash dump)');
 1109:     while (my ($key,$val) = each (%tmp)) {
 1110:         $self->logthis(' '.$key.' = '.$val.':');
 1111:     }
 1112:     $self->logthis('---------------------------- (finished hash dump)');
 1113: }
 1114: 
 1115: ##
 1116: ## rebuild_stats: rebuilds the rows and template_cells arrays
 1117: ##
 1118: sub rebuild_stats {
 1119:     my $self = shift;
 1120:     $self->{'rows'}=[];
 1121:     $self->{'template_cells'}=[];
 1122:     while (my ($cell,$formula) = each(%{$self->{'formulas'}})) {
 1123:         push(@{$self->{'rows'}},$1) if ($cell =~ /^A(\d+)/ && $1 != 0);
 1124:         push(@{$self->{'template_cells'}},$1) if ($cell =~ /^template_(\w+)/);
 1125:     }
 1126:     return;
 1127: }
 1128: 
 1129: ##
 1130: ## template_cells returns a list of the cells defined in the template row
 1131: ##
 1132: sub template_cells {
 1133:     my $self = shift;
 1134:     $self->rebuild_stats() if (! defined($self->{'template_cells'}) ||
 1135:                                ! @{$self->{'template_cells'}});
 1136:     return @{$self->{'template_cells'}};
 1137: }
 1138: 
 1139: ##
 1140: ## Sigh.... 
 1141: ##
 1142: sub setothersheets {
 1143:     my $self = shift;
 1144:     my @othersheets = @_;
 1145:     $self->{'othersheets'} = \@othersheets;
 1146: }
 1147: 
 1148: ##
 1149: ## rows returns a list of the names of cells defined in the A column
 1150: ##
 1151: sub rows {
 1152:     my $self = shift;
 1153:     $self->rebuild_stats() if (!@{$self->{'rows'}});
 1154:     return @{$self->{'rows'}};
 1155: }
 1156: 
 1157: #
 1158: # calcsheet: makes all the calls to compute the spreadsheet.
 1159: #
 1160: sub calcsheet {
 1161:     my $self = shift;
 1162:     $self->sync_safe_space();
 1163:     $self->clear_errorlog();
 1164:     $self->sett();
 1165:     my $result =  $self->{'safe'}->reval('&calc();');
 1166: #    $self->logthis($self->get_errorlog());
 1167:     %{$self->{'values'}} = %{$self->{'safe'}->varglob('sheet_values')};
 1168: #    $self->logthis($self->get_errorlog());
 1169:     if ($result ne 'okay') {
 1170:         $self->set_calcerror($result);
 1171:     }
 1172:     return $result;
 1173: }
 1174: 
 1175: sub set_badcalc {
 1176:     my $self = shift();
 1177:     $self->{'badcalc'} =1;
 1178:     return;
 1179: }
 1180: 
 1181: sub badcalc {
 1182:     my $self = shift;
 1183:     if (exists($self->{'badcalc'}) && $self->{'badcalc'}) {
 1184:         return 1;
 1185:     } else {
 1186:         return 0;
 1187:     }
 1188: }
 1189: 
 1190: sub set_calcerror {
 1191:     my $self = shift;
 1192:     if (@_) {
 1193:         $self->set_badcalc();
 1194:         if (exists($self->{'calcerror'})) {
 1195:             $self->{'calcerror'}.="\n".$_[0];
 1196:         } else {
 1197:             $self->{'calcerror'}.=$_[0];
 1198:         }
 1199:     }
 1200: }
 1201: 
 1202: sub calcerror {
 1203:     my $self = shift;
 1204:     if ($self->badcalc()) {
 1205:         if (exists($self->{'calcerror'})) {
 1206:             return $self->{'calcerror'};
 1207:         }
 1208:     }
 1209:     return;
 1210: }
 1211: 
 1212: ###########################################################
 1213: ##
 1214: ## Output Helpers
 1215: ##
 1216: ###########################################################
 1217: sub display {
 1218:     my $self = shift;
 1219:     my ($r) = @_;
 1220:     my $outputmode = 'html';
 1221:     foreach ($self->output_options()) {
 1222:         if ($ENV{'form.output_format'} eq $_->{'value'}) {
 1223:             $outputmode = $_->{'value'};
 1224:             last;
 1225:         }
 1226:     }
 1227:     if ($outputmode eq 'html') {
 1228:         $self->compute($r);
 1229:         $self->outsheet_html($r);
 1230:     } elsif ($outputmode eq 'htmlclasslist') {
 1231:         # No computation neccessary...  This is kludgy
 1232:         $self->outsheet_htmlclasslist($r);
 1233:     } elsif ($outputmode eq 'excel') {
 1234:         $self->compute($r);
 1235:         $self->outsheet_excel($r);
 1236:     } elsif ($outputmode eq 'csv') {
 1237:         $self->compute($r);
 1238:         $self->outsheet_csv($r);
 1239:     } elsif ($outputmode eq 'xml') {
 1240: #        $self->compute($r);
 1241:         $self->outsheet_xml($r);
 1242:     }
 1243:     $self->cleanup();
 1244:     return;
 1245: }
 1246: 
 1247: ############################################
 1248: ##         HTML output routines           ##
 1249: ############################################
 1250: sub html_report_error {
 1251:     my $self = shift();
 1252:     my $Str = '';
 1253:     if ($self->badcalc()) {
 1254:         $Str = '<h3 style="color:red">'.
 1255:             &mt('An error occurred while calculating this spreadsheet').
 1256:             "</h3>\n".
 1257:             '<pre>'.$self->calcerror()."</pre>\n";
 1258:     }
 1259:     return $Str;
 1260: }
 1261: 
 1262: sub html_export_row {
 1263:     my $self = shift();
 1264:     my ($color) = @_;
 1265:     $color = '#CCCCFF' if (! defined($color));
 1266:     my $allowed = &Apache::lonnet::allowed('mgr',$ENV{'request.course.id'});
 1267:     my $row_html;
 1268:     my @rowdata = $self->get_row(0);
 1269:     foreach my $cell (@rowdata) {
 1270:         if ($cell->{'name'} =~ /^[A-Z]/) {
 1271: 	    $row_html .= '<td bgcolor="'.$color.'">'.
 1272:                 &html_editable_cell($cell,$color,$allowed).'</td>';
 1273:         } else {
 1274: 	    $row_html .= '<td bgcolor="#DDCCFF">'.
 1275:                 &html_editable_cell($cell,'#DDCCFF',$allowed).'</td>';
 1276:         }
 1277:     }
 1278:     return $row_html;
 1279: }
 1280: 
 1281: sub html_template_row {
 1282:     my $self = shift();
 1283:     my $allowed = &Apache::lonnet::allowed('mgr',$ENV{'request.course.id'});
 1284:     my ($num_uneditable,$importcolor) = @_;
 1285:     my $row_html;
 1286:     my @rowdata = $self->get_template_row();
 1287:     my $count = 0;
 1288:     for (my $i = 0; $i<=$#rowdata; $i++) {
 1289:         my $cell = $rowdata[$i];
 1290:         if ($i < $num_uneditable) {
 1291: 	    $row_html .= '<td bgcolor="'.$importcolor.'">'.
 1292:                 &html_uneditable_cell($cell,'#FFDDDD',$allowed).'</td>';
 1293:         } else {
 1294: 	    $row_html .= '<td bgcolor="#EOFFDD">'.
 1295:                 &html_editable_cell($cell,'#EOFFDD',$allowed).'</td>';
 1296:         }
 1297:     }
 1298:     return $row_html;
 1299: }
 1300: 
 1301: sub html_editable_cell {
 1302:     my ($cell,$bgcolor,$allowed) = @_;
 1303:     my $result;
 1304:     my ($name,$formula,$value);
 1305:     if (defined($cell)) {
 1306:         $name    = $cell->{'name'};
 1307:         $formula = $cell->{'formula'};
 1308:         $value   = $cell->{'value'};
 1309:     }
 1310:     $name    = '' if (! defined($name));
 1311:     $formula = '' if (! defined($formula));
 1312:     if (! defined($value)) {
 1313:         $value = '<font color="'.$bgcolor.'">#</font>';
 1314:         if ($formula ne '') {
 1315:             $value = '<i>undefined value</i>';
 1316:         }
 1317:     } elsif ($value =~ /^\s*$/ ) {
 1318:         $value = '<font color="'.$bgcolor.'">#</font>';
 1319:     } else {
 1320:         $value = &HTML::Entities::encode($value,'<>&"') if ($value !~/&nbsp;/);
 1321:     }
 1322:     return $value if (! $allowed);
 1323:     #
 1324:     # The formula will be parsed by the browser twice before being 
 1325:     # displayed to the user for editing. 
 1326:     #
 1327:     # The encoding string "^A-blah" is placed in []'s inside a regexp, so 
 1328:     # we specify the characters we want left alone by putting a '^' in front.
 1329:     $formula = &HTML::Entities::encode($formula,'^A-z0-9 !#$%-;=?~');
 1330:     # HTML::Entities::encode does not catch everything - we need '\' encoded
 1331:     $formula =~ s/\\/&\#092/g;
 1332:     # Escape it again - this time the only encodable character is '&'
 1333:     $formula =~ s/\&/\&amp;/g;
 1334:     # Glue everything together
 1335:     $result .= "<a href=\"javascript:celledit(\'".
 1336:         $name."','".$formula."');\">".$value."</a>";
 1337:     return $result;
 1338: }
 1339: 
 1340: sub html_uneditable_cell {
 1341:     my ($cell,$bgcolor) = @_;
 1342:     my $value = (defined($cell) ? $cell->{'value'} : '');
 1343:     $value = &HTML::Entities::encode($value,'<>&"') if ($value !~/&nbsp;/);
 1344:     return '&nbsp;'.$value.'&nbsp;';
 1345: }
 1346: 
 1347: sub html_row {
 1348:     my $self = shift();
 1349:     my ($num_uneditable,$row,$exportcolor,$importcolor) = @_;
 1350:     my $allowed = &Apache::lonnet::allowed('mgr',$ENV{'request.course.id'});
 1351:     my @rowdata = $self->get_row($row);
 1352:     my $num_cols_output = 0;
 1353:     my $row_html;
 1354:     my $color = $importcolor;
 1355:     if ($row == 0) {
 1356:         $color = $exportcolor;
 1357:     }
 1358:     $color = '#FFDDDD' if (! defined($color));
 1359:     foreach my $cell (@rowdata) {
 1360: 	if ($num_cols_output++ < $num_uneditable) {
 1361: 	    $row_html .= '<td bgcolor="'.$color.'">';
 1362: 	    $row_html .= &html_uneditable_cell($cell,'#FFDDDD');
 1363: 	} else {
 1364: 	    $row_html .= '<td bgcolor="#EOFFDD">';
 1365: 	    $row_html .= &html_editable_cell($cell,'#E0FFDD',$allowed);
 1366: 	}
 1367: 	$row_html .= '</td>';
 1368:     }
 1369:     return $row_html;
 1370: }
 1371: 
 1372: sub html_header {
 1373:     my $self = shift;
 1374:     return '' if (! $ENV{'request.role.adv'});
 1375:     return "<table>\n".
 1376:         '<tr><th align="center">'.&mt('Output Format').'</th></tr>'."\n".
 1377:         '<tr><td>'.$self->output_selector()."</td></tr>\n".
 1378:         "</table>\n";
 1379: }
 1380: 
 1381: ##
 1382: ## Default output types are HTML, Excel, and CSV
 1383: sub output_options {
 1384:     my $self = shift();
 1385:     return  ({value       => 'html',
 1386:               description => 'HTML'},
 1387:              {value       => 'excel',
 1388:               description => 'Excel'},
 1389: #             {value       => 'xml',
 1390: #              description => 'XML'},
 1391:              {value       => 'csv',
 1392:               description => 'Comma Separated Values'},);
 1393: }
 1394: 
 1395: sub output_selector {
 1396:     my $self = shift();
 1397:     my $output_selector = '<select name="output_format" size="3">'."\n";
 1398:     my $default = 'html';
 1399:     if (exists($ENV{'form.output_format'})) {
 1400:         $default = $ENV{'form.output_format'} 
 1401:     } else {
 1402:         $ENV{'form.output_format'} = $default;
 1403:     }
 1404:     foreach  ($self->output_options()) {
 1405:         $output_selector.='<option value="'.$_->{'value'}.'"';
 1406:         if ($_->{'value'} eq $default) {
 1407:             $output_selector .= ' selected';
 1408:         }
 1409:         $output_selector .= ">".&mt($_->{'description'})."</option>\n";
 1410:     }
 1411:     $output_selector .= "</select>\n";
 1412:     return $output_selector;
 1413: }
 1414: 
 1415: ################################################
 1416: ##          Excel output routines             ##
 1417: ################################################
 1418: sub excel_output_row {
 1419:     my $self = shift;
 1420:     my ($worksheet,$rownum,$rows_output,@prepend) = @_;
 1421:     my $cols_output = 0;
 1422:     #
 1423:     my @rowdata = $self->get_row($rownum);
 1424:     foreach my $cell (@prepend,@rowdata) {
 1425:         my $value = $cell;
 1426:         $value = $cell->{'value'} if (ref($value));
 1427:         $value =~ s/\&nbsp;/ /gi;
 1428:         $worksheet->write($rows_output,$cols_output++,$value);
 1429:     }
 1430:     return;
 1431: }
 1432: 
 1433: sub create_excel_spreadsheet {
 1434:     my $self = shift;
 1435:     my ($r) = @_;
 1436:     my $filename = '/prtspool/'.
 1437:         $ENV{'user.name'}.'_'.$ENV{'user.domain'}.'_'.
 1438:         time.'_'.rand(1000000000).'.xls';
 1439:     my $workbook  = Spreadsheet::WriteExcel->new('/home/httpd'.$filename);
 1440:     if (! defined($workbook)) {
 1441:         $r->log_error("Error creating excel spreadsheet $filename: $!");
 1442:         $r->print(&mt("Problems creating new Excel file.  ".
 1443:                   "This error has been logged.  ".
 1444:                   "Please alert your LON-CAPA administrator"));
 1445:         return undef;
 1446:     }
 1447:     #
 1448:     # The excel spreadsheet stores temporary data in files, then put them
 1449:     # together.  If needed we should be able to disable this (memory only).
 1450:     # The temporary directory must be specified before calling 'addworksheet'.
 1451:     # File::Temp is used to determine the temporary directory.
 1452:     $workbook->set_tempdir('/home/httpd/perl/tmp');
 1453:     #
 1454:     # Determine the name to give the worksheet
 1455:     return ($workbook,$filename);
 1456: }
 1457: 
 1458: #
 1459: # This routine is just a stub 
 1460: sub outsheet_htmlclasslist {
 1461:     my $self = shift;
 1462:     my ($r) = @_;
 1463:     $r->print('<h2>'.&mt("This output is not supported").'</h2>');
 1464:     $r->rflush();
 1465:     return;
 1466: }
 1467: 
 1468: sub outsheet_excel {
 1469:     my $self = shift;
 1470:     my ($r) = @_;
 1471:     my $connection = $r->connection();
 1472:     #
 1473:     $r->print($self->html_report_error());
 1474:     $r->rflush();
 1475:     #
 1476:     $r->print("<h2>".&mt('Preparing Excel Spreadsheet')."</h2>");
 1477:     #
 1478:     # Create excel worksheet
 1479:     my ($workbook,$filename) = $self->create_excel_spreadsheet($r);
 1480:     return if (! defined($workbook));
 1481:     #
 1482:     # Create main worksheet
 1483:     my $worksheet = $workbook->addworksheet('main');
 1484:     my $rows_output = 0;
 1485:     my $cols_output = 0;
 1486:     #
 1487:     # Write excel header
 1488:     foreach my $value ($self->get_title()) {
 1489:         $cols_output = 0;
 1490:         $worksheet->write($rows_output++,$cols_output,$value);
 1491:     }
 1492:     $rows_output++;    # skip a line
 1493:     #
 1494:     # Write summary/export row
 1495:     $cols_output = 0;
 1496:     $self->excel_output_row($worksheet,0,$rows_output++,'Summary');
 1497:     $rows_output++;    # skip a line
 1498:     #
 1499:     $self->excel_rows($connection,$worksheet,$cols_output,$rows_output);
 1500:     #
 1501:     #
 1502:     # Close the excel file
 1503:     $workbook->close();
 1504:     #
 1505:     # Write a link to allow them to download it
 1506:     $r->print('<br />'.
 1507:               '<a href="'.$filename.'">Your Excel spreadsheet.</a>'."\n");
 1508:     return;
 1509: }
 1510: 
 1511: #################################
 1512: ## CSV output routines         ##
 1513: #################################
 1514: sub outsheet_csv   {
 1515:     my $self = shift;
 1516:     my ($r) = @_;
 1517:     my $connection = $r->connection();
 1518:     #
 1519:     $r->print($self->html_report_error());
 1520:     $r->rflush();
 1521:     #
 1522:     my $csvdata = '';
 1523:     my @Values;
 1524:     #
 1525:     # Open the csv file
 1526:     my $filename = '/prtspool/'.
 1527:         $ENV{'user.name'}.'_'.$ENV{'user.domain'}.'_'.
 1528:         time.'_'.rand(1000000000).'.csv';
 1529:     my $file;
 1530:     unless ($file = Apache::File->new('>'.'/home/httpd'.$filename)) {
 1531:         $r->log_error("Couldn't open $filename for output $!");
 1532:         $r->print(&mt("Problems occured in writing the csv file.  ".
 1533:                   "This error has been logged.  ".
 1534:                   "Please alert your LON-CAPA administrator."));
 1535:         $r->print("<pre>\n".$csvdata."</pre>\n");
 1536:         return 0;
 1537:     }
 1538:     #
 1539:     # Output the title information
 1540:     foreach my $value ($self->get_title()) {
 1541:         print $file "'".&Apache::loncommon::csv_translate($value)."'\n";
 1542:     }
 1543:     #
 1544:     # Output the body of the spreadsheet
 1545:     $self->csv_rows($connection,$file);
 1546:     #
 1547:     # Close the csv file
 1548:     close($file);
 1549:     $r->print('<br /><br />'.
 1550:               '<a href="'.$filename.'">'.&mt('Your CSV spreadsheet.').'</a>'."\n");
 1551:     #
 1552:     return 1;
 1553: }
 1554: 
 1555: sub csv_output_row {
 1556:     my $self = shift;
 1557:     my ($filehandle,$rownum,@prepend) = @_;
 1558:     #
 1559:     my @rowdata = ();
 1560:     if (defined($rownum)) {
 1561:         @rowdata = $self->get_row($rownum);
 1562:     }
 1563:     my @output = ();
 1564:     foreach my $cell (@prepend,@rowdata) {
 1565:         my $value = $cell;
 1566:         $value = $cell->{'value'} if (ref($value));
 1567:         $value =~ s/\&nbsp;/ /gi;
 1568:         $value = "'".$value."'";
 1569:         push (@output,$value);
 1570:     }
 1571:     print $filehandle join(',',@output )."\n";
 1572:     return;
 1573: }
 1574: 
 1575: ############################################
 1576: ##          XML output routines           ##
 1577: ############################################
 1578: sub outsheet_xml   {
 1579:     my $self = shift;
 1580:     my ($r) = @_;
 1581:     ## Someday XML
 1582:     ## Will be rendered for the user
 1583:     ## But not on this day
 1584:     my $Str = '<spreadsheet type="'.$self->{'type'}.'">'."\n";
 1585:     while (my ($cell,$formula) = each(%{$self->{'formulas'}})) {
 1586:         if ($cell =~ /^template_(\w+)/) {
 1587:             my $col = $1;
 1588:             $Str .= '<template col="'.$col.'">'.$formula.'</template>'."\n";
 1589:         } else {
 1590:             my ($col,$row) = ($cell =~ /^([A-z])(\d+)/);
 1591:             next if (! defined($row) || ! defined($col));
 1592:             next if ($row != 0);
 1593:             $Str .= 
 1594:                 '<field row="'.$row.'" col="'.$col.'" >'.$formula.'</field>'
 1595:                 ."\n";
 1596:         }
 1597:     }
 1598:     $Str.="</spreadsheet>";
 1599:     $r->print("<pre>\n\n\n".$Str."\n\n\n</pre>");
 1600:     return $Str;
 1601: }
 1602: 
 1603: ############################################
 1604: ###        Filesystem routines           ###
 1605: ############################################
 1606: sub parse_sheet {
 1607:     # $sheetxml is a scalar reference or a scalar
 1608:     my ($sheetxml) = @_;
 1609:     if (! ref($sheetxml)) {
 1610:         my $tmp = $sheetxml;
 1611:         $sheetxml = \$tmp;
 1612:     }
 1613:     my %formulas;
 1614:     my %sources;
 1615:     my $parser=HTML::TokeParser->new($sheetxml);
 1616:     my $token;
 1617:     while ($token=$parser->get_token) {
 1618:         if ($token->[0] eq 'S') {
 1619:             if ($token->[1] eq 'field') {
 1620:                 my $cell = $token->[2]->{'col'}.$token->[2]->{'row'};
 1621:                 my $source = $token->[2]->{'source'};
 1622:                 my $formula = $parser->get_text('/field');
 1623:                 $formulas{$cell} = $formula;
 1624:                 $sources{$cell}  = $source if (defined($source));
 1625:                 $parser->get_text('/field');
 1626:             } elsif ($token->[1] eq 'template') {
 1627:                 $formulas{'template_'.$token->[2]->{'col'}}=
 1628:                     $parser->get_text('/template');
 1629:             }
 1630:         }
 1631:     }
 1632:     return (\%formulas,\%sources);
 1633: }
 1634: 
 1635: {
 1636: 
 1637: my %spreadsheets;
 1638: 
 1639: sub clear_spreadsheet_definition_cache {
 1640:     undef(%spreadsheets);
 1641: }
 1642: 
 1643: sub load_system_default_sheet {
 1644:     my $self = shift;
 1645:     my $includedir = $Apache::lonnet::perlvar{'lonIncludes'};
 1646:     # load in the default defined spreadsheet
 1647:     my $sheetxml='';
 1648:     my $fh;
 1649:     if ($fh=Apache::File->new($includedir.'/default_'.$self->{'type'})) {
 1650:         $sheetxml=join('',<$fh>);
 1651:         $fh->close();
 1652:     } else {
 1653:         # $sheetxml='<field row="0" col="A">"Error"</field>';
 1654:         $sheetxml='<field row="0" col="A"></field>';
 1655:     }
 1656:     $self->filename('default_');
 1657:     my ($formulas,undef) = &parse_sheet(\$sheetxml);
 1658:     return $formulas;
 1659: }
 1660: 
 1661: sub load {
 1662:     my $self = shift;
 1663:     #
 1664:     my $stype = $self->{'type'};
 1665:     my $cnum  = $self->{'cnum'};
 1666:     my $cdom  = $self->{'cdom'};
 1667:     my $chome = $self->{'chome'};
 1668:     #
 1669:     my $filename = $self->filename();
 1670:     my $cachekey = join('_',($cnum,$cdom,$stype,$filename));
 1671:     #
 1672:     # see if sheet is cached
 1673:     my ($formulas);
 1674:     if (exists($spreadsheets{$cachekey})) {
 1675:         $formulas = $spreadsheets{$cachekey}->{'formulas'};
 1676:     } else {
 1677:         # Not cached, need to read
 1678:         if (! defined($filename)) {
 1679:             $formulas = $self->load_system_default_sheet();
 1680:         } elsif($filename =~ /^\/res\/.*\.spreadsheet$/) {
 1681:             # Load a spreadsheet definition file
 1682:             my $sheetxml=&Apache::lonnet::getfile
 1683:                 (&Apache::lonnet::filelocation('',$filename));
 1684:             if ($sheetxml == -1) {
 1685:                 $sheetxml='<field row="0" col="A">"Error loading spreadsheet '
 1686:                     .$self->filename().'"</field>';
 1687:             }
 1688:             ($formulas,undef) = &parse_sheet(\$sheetxml);
 1689:             # Get just the filename and set the sheets filename
 1690:             my ($newfilename) = ($filename =~ /\/([^\/]*)\.spreadsheet$/);
 1691:             if ($self->is_default()) {
 1692:                 $self->filename($newfilename);
 1693:                 $self->make_default();
 1694:             } else {
 1695:                 $self->filename($newfilename);
 1696:             }
 1697:         } else {
 1698:             # Load the spreadsheet definition file from the save file
 1699:             my %tmphash = &Apache::lonnet::dump($filename,$cdom,$cnum);
 1700:             my ($tmp) = keys(%tmphash);
 1701:             if ($tmp !~ /^(con_lost|error|no_such_host)/i) {
 1702:                 while (my ($cell,$formula) = each(%tmphash)) {
 1703:                     $formulas->{$cell}=$formula;
 1704:                 }
 1705:             } else {
 1706:                 $formulas = $self->load_system_default_sheet();
 1707:             }
 1708:         }
 1709:         $filename=$self->filename(); # filename may have changed
 1710:         $cachekey = join('_',($cnum,$cdom,$stype,$filename));
 1711:         %{$spreadsheets{$cachekey}->{'formulas'}} = %{$formulas};
 1712:     }
 1713:     $self->formulas($formulas);
 1714:     $self->set_row_sources();
 1715:     $self->set_row_numbers();
 1716: }
 1717: 
 1718: sub set_row_sources {
 1719:     my $self = shift;
 1720:     while (my ($cell,$value) = each(%{$self->{'formulas'}})) {
 1721:         next if ($cell !~ /^A(\d+)/ || $1 < 1);
 1722:         my $row = $1;
 1723:         $self->{'row_source'}->{$row} = $value;
 1724:     }
 1725:     return;
 1726: }
 1727: 
 1728: sub set_row_numbers {
 1729:     my $self = shift;
 1730:     while (my ($cell,$value) = each(%{$self->{'formulas'}})) {
 1731: 	next if ($cell !~ /^A(\d+)$/);
 1732:         next if (! defined($value));
 1733: 	$self->{'row_numbers'}->{$value} = $1;
 1734:         $self->{'maxrow'} = $1 if ($1 > $self->{'maxrow'});
 1735:     }
 1736: }
 1737: 
 1738: ##
 1739: ## exportrow is *not* used to get the export row from a computed sub-sheet.
 1740: ##
 1741: sub exportrow {
 1742:     my $self = shift;
 1743:     if (exists($self->{'badcalc'}) && $self->{'badcalc'}) {
 1744:         return ();
 1745:     }
 1746:     my @exportarray;
 1747:     foreach my $column (@UC_Columns) {
 1748:         push(@exportarray,$self->value($column.'0'));
 1749:     }
 1750:     return @exportarray;
 1751: }
 1752: 
 1753: sub save {
 1754:     my $self = shift;
 1755:     my ($makedef)=@_;
 1756:     my $cid=$self->{'cid'};
 1757:     # If we are saving it, it must not be temporary
 1758:     $self->temporary(0);
 1759:     if (&Apache::lonnet::allowed('opa',$cid)) {
 1760:         my %f=$self->formulas();
 1761:         my $stype = $self->{'type'};
 1762:         my $cnum  = $self->{'cnum'};
 1763:         my $cdom  = $self->{'cdom'};
 1764:         my $chome = $self->{'chome'};
 1765:         my $filename    = $self->{'filename'};
 1766:         my $cachekey = join('_',($cnum,$cdom,$stype,$filename));
 1767:         # Cache new sheet
 1768:         %{$spreadsheets{$cachekey}->{'formulas'}}=%f;
 1769:         # Write sheet
 1770:         foreach (keys(%f)) {
 1771:             delete($f{$_}) if ($f{$_} eq 'import');
 1772:         }
 1773:         my $reply = &Apache::lonnet::put($filename,\%f,$cdom,$cnum);
 1774:         return $reply if ($reply ne 'ok');
 1775:         $reply = &Apache::lonnet::put($stype.'_spreadsheets',
 1776:                      {$filename => $ENV{'user.name'}.'@'.$ENV{'user.domain'}},
 1777:                                       $cdom,$cnum);
 1778:         return $reply if ($reply ne 'ok');
 1779:         if ($makedef) { 
 1780:             $reply = &Apache::lonnet::put('environment',
 1781:                                 {'spreadsheet_default_'.$stype => $filename },
 1782:                                           $cdom,$cnum);
 1783:             return $reply if ($reply ne 'ok');
 1784:         } 
 1785:         if ($self->is_default()) {
 1786:             if ($self->{'type'} eq 'studentcalc') {
 1787:                 &Apache::lonnet::expirespread('','','studentcalc','');
 1788:             } elsif ($self->{'type'} eq 'assesscalc') {
 1789:                 &Apache::lonnet::expirespread('','','assesscalc','');
 1790:                 &Apache::lonnet::expirespread('','','studentcalc','');
 1791:             }
 1792:         }
 1793:         return $reply;
 1794:     }
 1795:     return 'unauthorized';
 1796: }
 1797: 
 1798: } # end of scope for %spreadsheets
 1799: 
 1800: sub save_tmp {
 1801:     my $self = shift;
 1802:     my $filename=$ENV{'user.name'}.'_'.
 1803:         $ENV{'user.domain'}.'_spreadsheet_'.$self->{'symb'}.'_'.
 1804:            $self->{'filename'};
 1805:     $filename=~s/\W/\_/g;
 1806:     $filename=$Apache::lonnet::tmpdir.$filename.'.tmp';
 1807:     $self->temporary(1);
 1808:     my $fh;
 1809:     if ($fh=Apache::File->new('>'.$filename)) {
 1810:         my %f = $self->formulas();
 1811:         while( my ($cell,$formula) = each(%f)) {
 1812:             next if ($formula eq 'import');
 1813:             print $fh &Apache::lonnet::escape($cell)."=".
 1814:                 &Apache::lonnet::escape($formula)."\n";
 1815:         }
 1816:         $fh->close();
 1817:     }
 1818: }
 1819: 
 1820: sub load_tmp {
 1821:     my $self = shift;
 1822:     my $filename=$ENV{'user.name'}.'_'.
 1823:         $ENV{'user.domain'}.'_spreadsheet_'.$self->{'symb'}.'_'.
 1824:             $self->{'filename'};
 1825:     $filename=~s/\W/\_/g;
 1826:     $filename=$Apache::lonnet::tmpdir.$filename.'.tmp';
 1827:     my %formulas = ();
 1828:     if (my $spreadsheet_file = Apache::File->new($filename)) {
 1829:         while (<$spreadsheet_file>) {
 1830: 	    chomp;
 1831:             my ($cell,$formula) = split(/=/);
 1832:             $cell    = &Apache::lonnet::unescape($cell);
 1833:             $formula = &Apache::lonnet::unescape($formula);
 1834:             $formulas{$cell} = $formula;
 1835:         }
 1836:         $spreadsheet_file->close();
 1837:     }
 1838:     # flag the sheet as temporary
 1839:     $self->temporary(1);
 1840:     $self->formulas(\%formulas);
 1841:     $self->set_row_sources();
 1842:     $self->set_row_numbers();
 1843:     return;
 1844: }
 1845: 
 1846: sub temporary {
 1847:     my $self=shift;
 1848:     if (@_) {
 1849:         ($self->{'temporary'})= @_;
 1850:     }
 1851:     return $self->{'temporary'};
 1852: }
 1853: 
 1854: sub modify_cell {
 1855:     # studentcalc overrides this
 1856:     my $self = shift;
 1857:     my ($cell,$formula) = @_;
 1858:     if ($cell =~ /([A-z])\-/) {
 1859:         $cell = 'template_'.$1;
 1860:     } elsif ($cell !~ /^([A-z](\d+)|template_[A-z])$/) {
 1861:         return;
 1862:     }
 1863:     $self->set_formula($cell,$formula);
 1864:     $self->rebuild_stats();
 1865:     return;
 1866: }
 1867: 
 1868: ###########################################
 1869: # othersheets: Returns the list of other spreadsheets available 
 1870: ###########################################
 1871: sub othersheets {
 1872:     my $self = shift(); 
 1873:     my ($stype) = @_;
 1874:     $stype = $self->{'type'} if (! defined($stype) || $stype !~ /calc$/);
 1875:     #
 1876:     my @alternatives=();
 1877:     my %results=&Apache::lonnet::dump($stype.'_spreadsheets',
 1878:                                       $self->{'cdom'}, $self->{'cnum'});
 1879:     my ($tmp) = keys(%results);
 1880:     if ($tmp =~ /^(con_lost|error|no_such_host)/i ) {
 1881:         @alternatives = (&mt('Default'));
 1882:     } else {
 1883:         @alternatives = (&mt('Default'), sort (keys(%results)));
 1884:     }
 1885:     return @alternatives; 
 1886: }
 1887: 
 1888: sub blackout {
 1889:     my $self = shift;
 1890:     $self->{'blackout'} = $_[0] if (@_);
 1891:     return $self->{'blackout'};
 1892: }
 1893: 
 1894: sub get_row {
 1895:     my $self = shift;
 1896:     my ($n)=@_;
 1897:     my @cols=();
 1898:     foreach my $col (@UC_Columns,@LC_Columns) {
 1899:         my $cell = $col.$n;
 1900:         push(@cols,{ name    => $cell,
 1901:                      formula => $self->formula($cell),
 1902:                      value   => $self->value($cell)});
 1903:     }
 1904:     return @cols;
 1905: }
 1906: 
 1907: sub get_template_row {
 1908:     my $self = shift;
 1909:     my @cols=();
 1910:     foreach my $col (@UC_Columns,@LC_Columns) {
 1911:         my $cell = 'template_'.$col;
 1912:         push(@cols,{ name    => $cell,
 1913:                      formula => $self->formula($cell),
 1914:                      value   => $self->formula($cell) });
 1915:     }
 1916:     return @cols;
 1917: }
 1918: 
 1919: sub need_to_save {
 1920:     my $self = shift;
 1921:     if ($self->{'new_rows'} && ! $self->temporary()) {
 1922:         return 1;
 1923:     }
 1924:     return 0;
 1925: }
 1926: 
 1927: sub get_row_number_from_key {
 1928:     my $self = shift;
 1929:     my ($key) = @_;
 1930:     if (! exists($self->{'row_numbers'}->{$key}) ||
 1931:         ! defined($self->{'row_numbers'}->{$key})) {
 1932:         # I used to set $f here to the new value, but the key passed for lookup
 1933:         # may not be the key we need to save
 1934: 	$self->{'maxrow'}++;
 1935: 	$self->{'row_numbers'}->{$key} = $self->{'maxrow'};
 1936: #        $self->logthis('added row '.$self->{'row_numbers'}->{$key}.
 1937: #                       ' for '.$key);
 1938:         $self->{'new_rows'} = 1;
 1939:     }
 1940:     return $self->{'row_numbers'}->{$key};
 1941: }
 1942: 
 1943: 1;
 1944: 
 1945: __END__

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