--- loncom/interface/spreadsheet/Spreadsheet.pm	2003/05/23 19:36:04	1.5
+++ loncom/interface/spreadsheet/Spreadsheet.pm	2003/11/11 14:17:20	1.29
@@ -1,5 +1,5 @@
 #
-# $Id: Spreadsheet.pm,v 1.5 2003/05/23 19:36:04 matthew Exp $
+# $Id: Spreadsheet.pm,v 1.29 2003/11/11 14:17:20 matthew Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -48,6 +48,8 @@ Spreadsheet
 package Apache::Spreadsheet;
 
 use strict;
+#use warnings FATAL=>'all';
+#no warnings 'uninitialized';
 use Apache::Constants qw(:common :http);
 use Apache::lonnet;
 use Safe;
@@ -57,6 +59,7 @@ use HTML::Entities();
 use HTML::TokeParser;
 use Spreadsheet::WriteExcel;
 use Time::HiRes;
+use Apache::lonlocal;
 
 ##
 ## Package Variables
@@ -90,14 +93,17 @@ sub new {
         type     => $stype,
         symb     => $usymb,
         errorlog => '',
-        maxrow   => '',
+        maxrow   => 0,
         cid      => $ENV{'request.course.id'},
         cnum     => $ENV{'course.'.$ENV{'request.course.id'}.'.num'},
         cdom     => $ENV{'course.'.$ENV{'request.course.id'}.'.domain'},
         chome    => $ENV{'course.'.$ENV{'request.course.id'}.'.home'},
         coursedesc => $ENV{'course.'.$ENV{'request.course.id'}.'.description'},
         coursefilename => $ENV{'request.course.fn'},
-        temporary => '',
+        #
+        # Flags
+        temporary => 0,  # true if this sheet has been modified but not saved
+        new_rows  => 0, # true if this sheet has new rows
         #
         # blackout is used to determine if any data needs to be hidden from the
         # student.
@@ -142,24 +148,21 @@ sub filename {
     if (@_) {
         my ($newfilename) = @_;
         if (! defined($newfilename) || $newfilename eq 'Default' ||
-            $newfilename !~ /\w/    || $newfilename =~ /\W/) {
-            my %tmphash = &Apache::lonnet::get('environment',
-                                               ['spreadsheet_default_'.
-                                                $self->{'type'}],
-                                               $self->{'cdom'},
-                                               $self->{'cnum'});
-            my ($tmp) = keys(%tmphash);
-            if ($tmp !~ /^(con_lost|error|no_such_host)/i) {
-                $newfilename=$tmphash{'spreadsheet_default_'.$self->{'type'}};
+            $newfilename !~ /\w/ || $newfilename eq '') {
+            my $key = 'course.'.$self->{'cid'}.'.spreadsheet_default_'.
+                $self->{'type'};
+            if (exists($ENV{$key}) && $ENV{$key} ne '') {
+                $newfilename = $ENV{$key};
+            } else {
+                $newfilename = 'default_'.$self->{'type'};
             }
         }
-        if (! defined($newfilename) || 
-              $newfilename !~ /\w/   || 
-              $newfilename =~ /^\W*$/) {
-            $newfilename = 'default.'.$self->{'type'};
-        } else {
-            my $regexp = '_'.$self->{'type'}.'$';
-            if ($newfilename !~ /$regexp/) {
+        if ($newfilename !~ /\w/ || $newfilename =~ /^\W*$/) {
+            $newfilename = 'default_'.$self->{'type'};
+        }
+        if ($newfilename !~ /^default\.$self->{'type'}$/ ) {
+            if ($newfilename !~ /_$self->{'type'}$/) {
+                $newfilename =~ s/[\s_]*$//;
                 $newfilename .= '_'.$self->{'type'};
             }
         }
@@ -184,7 +187,7 @@ default spreadsheets.......!
 sub make_default {
     my $self = shift();
     my $result = &Apache::lonnet::put('environment',
-         {'spreadsheet_default_'.$self->{'type'} => $self->filename()},
+            {'spreadsheet_default_'.$self->{'type'} => $self->filename()},
                                      $self->{'cdom'},$self->{'cnum'});
     return $result if ($result ne 'ok');
     my $symb = $self->{'symb'};
@@ -217,10 +220,28 @@ sub is_default {
     if ($tmp !~ /^(con_lost|error|no_such_host)/i) {
         $default_filename = $tmphash{'spreadsheet_default_'.$self->{'type'}};
     }
+    if ($default_filename =~ /^\s*$/) {
+        $default_filename = 'default_'.$self->{'type'};
+    }
     return 1 if ($self->filename() eq $default_filename);
     return 0;
 }
 
+sub initialize {
+    # This method is here to remind you that it will be overridden by
+    # the descendents of the spreadsheet class.
+}
+
+sub clear_package {
+    # This method is here to remind you that it will be overridden by
+    # the descendents of the spreadsheet class.
+}
+
+sub cleanup {
+    my $self = shift();
+    $self->clear_package();
+}
+
 sub initialize_spreadsheet_package {
     &load_spreadsheet_expirationdates();
     &clear_spreadsheet_definition_cache();
@@ -240,18 +261,28 @@ sub load_spreadsheet_expirationdates {
 sub check_expiration_time {
     my $self = shift;
     my ($time)=@_;
-    my ($key1,$key2,$key3,$key4);
+    return 0 if (! defined($time));
+    my ($key1,$key2,$key3,$key4,$key5);
+    # Description of keys
+    #
+    # key1: all sheets of this type have expired
+    # key2: all sheets of this type for this student
+    # key3: all sheets of this type in this map for this student
+    # key4: this assessment sheet for this student
+    # key5: this assessment sheet for all students
     $key1 = '::'.$self->{'type'}.':';
     $key2 = $self->{'name'}.':'.$self->{'domain'}.':'.$self->{'type'}.':';
     $key3 = $key2.$self->{'container'} if (defined($self->{'container'}));
-    $key4 = $key2.$self->{'usymb'} if (defined($self->{'usymb'}));
-    foreach my $key ($key1,$key2,$key3,$key4) {
+    $key4 = $key2.$self->{'symb'} if (defined($self->{'symb'}));
+    $key5 = $key1.$self->{'symb'} if (defined($self->{'symb'}));
+    my $returnvalue = 1; # default to okay
+    foreach my $key ($key1,$key2,$key3,$key4,$key5) {
         next if (! defined($key));
-        if (exists($expiredates{$key}) &&$expiredates{$key} > $time) {
-            return 0;
+        if (exists($expiredates{$key}) && $expiredates{$key} > $time) {
+            $returnvalue = 0; # need to recompute
         }
     }
-    return 1;
+    return $returnvalue;
 }
 
 ######################################################
@@ -269,18 +300,23 @@ Returns the safe space required by a Spr
 =cut
 
 ######################################################
+{ 
+
+    my $safeeval;
+
 sub initialize_safe_space {
-    my $self = shift;
-    my $safeeval = new Safe(shift);
-    my $safehole = new Safe::Hole;
-    $safeeval->permit("entereval");
-    $safeeval->permit(":base_math");
-    $safeeval->permit("sort");
-    $safeeval->deny(":base_io");
-    $safehole->wrap(\&Apache::lonnet::EXT,$safeeval,'&EXT');
-    $safehole->wrap(\&mask,$safeeval,'&mask');
-    $safeeval->share('$@');
-    my $code=<<'ENDDEFS';
+  my $self = shift;
+  if (! defined($safeeval)) {
+      $safeeval = new Safe(shift);
+      my $safehole = new Safe::Hole;
+      $safeeval->permit("entereval");
+      $safeeval->permit(":base_math");
+      $safeeval->permit("sort");
+      $safeeval->deny(":base_io");
+      $safehole->wrap(\&Apache::lonnet::EXT,$safeeval,'&EXT');
+      $safehole->wrap(\&mask,$safeeval,'&mask');
+      $safeeval->share('$@');
+      my $code=<<'ENDDEFS';
 # ---------------------------------------------------- Inside of the safe space
 #
 # f: formulas
@@ -345,7 +381,7 @@ returns the number of items in the range
 #-------------------------------------------------------
 sub NUM {
     my $mask=&mask(@_);
-    my $num= $#{@{grep(/$mask/,keys(%sheet_values))}}+1;
+    my $num= $#{@{grep(eval("/$mask/"),keys(%sheet_values))}}+1;
     return $num;   
 }
 
@@ -362,7 +398,7 @@ sub BIN {
     my ($low,$high,$lower,$upper)=@_;
     my $mask=&mask($lower,$upper);
     my $num=0;
-    foreach (grep /$mask/,keys(%sheet_values)) {
+    foreach (grep eval("/$mask/"),keys(%sheet_values)) {
         if (($sheet_values{$_}>=$low) && ($sheet_values{$_}<=$high)) {
             $num++;
         }
@@ -384,7 +420,7 @@ returns the sum of items in the range.
 sub SUM {
     my $mask=&mask(@_);
     my $sum=0;
-    foreach (grep /$mask/,keys(%sheet_values)) {
+    foreach (grep eval("/$mask/"),keys(%sheet_values)) {
         $sum+=$sheet_values{$_};
     }
     return $sum;   
@@ -405,7 +441,7 @@ sub MEAN {
     my $mask=&mask(@_);
     my $sum=0; 
     my $num=0;
-    foreach (grep /$mask/,keys(%sheet_values)) {
+    foreach (grep eval("/$mask/"),keys(%sheet_values)) {
         $sum+=$sheet_values{$_};
         $num++;
     }
@@ -430,14 +466,14 @@ compute the standard deviation of the it
 sub STDDEV {
     my $mask=&mask(@_);
     my $sum=0; my $num=0;
-    foreach (grep /$mask/,keys(%sheet_values)) {
+    foreach (grep eval("/$mask/"),keys(%sheet_values)) {
         $sum+=$sheet_values{$_};
         $num++;
     }
     unless ($num>1) { return undef; }
     my $mean=$sum/$num;
     $sum=0;
-    foreach (grep /$mask/,keys(%sheet_values)) {
+    foreach (grep eval("/$mask/"),keys(%sheet_values)) {
         $sum+=($sheet_values{$_}-$mean)**2;
     }
     return sqrt($sum/($num-1));    
@@ -457,7 +493,7 @@ compute the product of the items in the
 sub PROD {
     my $mask=&mask(@_);
     my $prod=1;
-    foreach (grep /$mask/,keys(%sheet_values)) {
+    foreach (grep eval("/$mask/"),keys(%sheet_values)) {
         $prod*=$sheet_values{$_};
     }
     return $prod;   
@@ -477,7 +513,7 @@ compute the maximum of the items in the
 sub MAX {
     my $mask=&mask(@_);
     my $max='-';
-    foreach (grep /$mask/,keys(%sheet_values)) {
+    foreach (grep eval("/$mask/"),keys(%sheet_values)) {
         unless ($max) { $max=$sheet_values{$_}; }
         if (($sheet_values{$_}>$max) || ($max eq '-')) { 
             $max=$sheet_values{$_}; 
@@ -500,7 +536,7 @@ compute the minimum of the items in the
 sub MIN {
     my $mask=&mask(@_);
     my $min='-';
-    foreach (grep /$mask/,keys(%sheet_values)) {
+    foreach (grep eval("/$mask/"),keys(%sheet_values)) {
         unless ($max) { $max=$sheet_values{$_}; }
         if (($sheet_values{$_}<$min) || ($min eq '-')) { 
             $min=$sheet_values{$_}; 
@@ -525,7 +561,7 @@ sub SUMMAX {
     my ($num,$lower,$upper)=@_;
     my $mask=&mask($lower,$upper);
     my @inside=();
-    foreach (grep /$mask/,keys(%sheet_values)) {
+    foreach (grep eval("/$mask/"),keys(%sheet_values)) {
 	push (@inside,$sheet_values{$_});
     }
     @inside=sort(@inside);
@@ -552,7 +588,7 @@ sub SUMMIN {
     my ($num,$lower,$upper)=@_;
     my $mask=&mask($lower,$upper);
     my @inside=();
-    foreach (grep /$mask/,keys(%sheet_values)) {
+    foreach (grep eval("/$mask/"),keys(%sheet_values)) {
 	$inside[$#inside+1]=$sheet_values{$_};
     }
     @inside=sort(@inside);
@@ -647,7 +683,8 @@ sub calc {
 
 # ------------------------------------------- End of "Inside of the safe space"
 ENDDEFS
-    $safeeval->reval($code);
+        $safeeval->reval($code);
+    }
     $self->{'safe'} = $safeeval;
     $self->{'root'} = $self->{'safe'}->root();
     #
@@ -661,6 +698,9 @@ ENDDEFS
     $self->{'safe'}->reval($initstring);
     return $self;
 }
+
+}
+
 ######################################################
 
 =pod
@@ -674,6 +714,17 @@ ENDDEFS
 
 ######################################################
 
+=pod
+
+=item  &mask($lower,$upper)
+
+Inputs: $lower and $upper, cell names ("X12" or "a150") or globs ("X*").
+
+Returns:  Regular expression matching spreadsheet cells that are within
+the rectangle defined by $lower and $upper.  Due to the nature of the
+regular expression this result must be used inside an eval().
+
+=cut
 
 ######################################################
 {
@@ -688,78 +739,62 @@ sub mask {
     }
     $upper = $lower if (! defined($upper));
     #
-    my ($la,$ld) = ($lower=~/([A-Za-z]|\*)(\d+|\*)/);
-    my ($ua,$ud) = ($upper=~/([A-Za-z]|\*)(\d+|\*)/);
+    my ($la,$ld) = ($lower=~/([A-z]|\*)(\d+|\*)/);
+    my ($ua,$ud) = ($upper=~/([A-z]|\*)(\d+|\*)/);
     #
     my $alpha='';
     my $num='';
     #
+    # Do not put parenthases around $alpha.
+    # $num depends on the value in $1.
     if (($la eq '*') || ($ua eq '*')) {
-        $alpha='[A-Za-z]';
+        $alpha='[A-z]';
     } else {
-       if (($la=~/[A-Z]/) && ($ua=~/[A-Z]/) ||
-           ($la=~/[a-z]/) && ($ua=~/[a-z]/)) {
-          $alpha='['.$la.'-'.$ua.']';
-       } else {
-          $alpha='['.$la.'-Za-'.$ua.']';
-       }
-    }   
-    if (($ld eq '*') || ($ud eq '*')) {
-	$num='\d+';
+        if ($la gt $ua) {
+            my $tmp = $ua;
+            $ua = $la;
+            $la = $ua;
+        }
+        $alpha=qq/[$la-$ua]/;
+    }
+    if ($ld ne '*' && $ud ne '*') {
+        # Make sure $ld <= $ud
+        if ($ld > $ud) {
+            my $tmp = $ud;
+            $ud = $ld;
+            $ld = $tmp;
+        }
+        # Here we make a regular expression using some advanced regexp
+        # abilities.
+        # (\d+) will match the digits of the cell name and dump them in
+        #     to $1
+        # (?(?{ ... code ...} pattern_if_true | pattern_if_false)) will
+        #     choose pattern_if_true if { ... code ... } is true and
+        #     pattern_if_false if { ... code ... } is false.
+        # In this case, pattern_if_true is empty.  pattern_if_false is 
+        #     'donotmatch' and will not match our cells because none of 
+        #     them end with donotmatch.  
+        # Unfortunately, the use of this type of regular expression 
+        #     requires that each match be wrapped in an eval().  Search for
+        #     $mask in this module for examples
+        $num = '(\d+)(?(?{$1>= '.$ld.' && $1<='.$ud.'})|donotmatch)';
     } else {
-        if (length($ld)!=length($ud)) {
-           $num.='(';
-	   foreach ($ld=~m/\d/g) {
-              $num.='['.$_.'-9]';
-	   }
-           if (length($ud)-length($ld)>1) {
-              $num.='|\d{'.(length($ld)+1).','.(length($ud)-1).'}';
-	   }
-           $num.='|';
-           foreach ($ud=~m/\d/g) {
-               $num.='[0-'.$_.']';
-           }
-           $num.=')';
-       } else {
-           my @lda=($ld=~m/\d/g);
-           my @uda=($ud=~m/\d/g);
-           my $i; 
-           my $j=0; 
-           my $notdone=1;
-           for ($i=0;($i<=$#lda)&&($notdone);$i++) {
-               if ($lda[$i]==$uda[$i]) {
-		   $num.=$lda[$i];
-                   $j=$i;
-               } else {
-                   $notdone=0;
-               }
-           }
-           if ($j<$#lda-1) {
-	       $num.='('.$lda[$j+1];
-               for ($i=$j+2;$i<=$#lda;$i++) {
-                   $num.='['.$lda[$i].'-9]';
-               }
-               if ($uda[$j+1]-$lda[$j+1]>1) {
-		   $num.='|['.($lda[$j+1]+1).'-'.($uda[$j+1]-1).']\d{'.
-                   ($#lda-$j-1).'}';
-               }
-	       $num.='|'.$uda[$j+1];
-               for ($i=$j+2;$i<=$#uda;$i++) {
-                   $num.='[0-'.$uda[$i].']';
-               }
-               $num.=')';
-           } else {
-               if ($lda[-1]!=$uda[-1]) {
-                  $num.='['.$lda[-1].'-'.$uda[-1].']';
-	       }
-           }
-       }
+        $num = '(\d+)';
     }
-    my $expression ='^'.$alpha.$num."\$";
+    my $expression = '^'.$alpha.$num.'$';
     $memoizer{$key} = $expression;
     return $expression;
 }
 
+#
+# Debugging routine
+sub dump_memoized_values {
+    while (my ($key,$value) = each(%memoizer)) {
+        &Apache::lonnet::logthis('memoizer: '.$key.' = '.$value);
+    }
+    return;
+}
+
 }
 
 ##
@@ -777,8 +812,8 @@ sub expandnamed {
 	my @vars=split(/\W+/,$formula);
         my %values=();
 	foreach my $varname ( @vars ) {
-            if ($varname=~/\D/) {
-               $formula=~s/$varname/'$c{\''.$varname.'\'}'/ge;
+            if ($varname=~/^(parameter|stores|timestamp)/) {
+                $formula=~s/$varname/'$c{\''.$varname.'\'}'/ge;
                $varname=~s/$var/\([\\w:\\- ]\+\)/g;
 	       foreach (keys(%{$self->{'constants'}})) {
 		  if ($_=~/$varname/) {
@@ -808,30 +843,32 @@ sub expandnamed {
         #    4. If there is a collision, return 'bad parameter name error'
         my $returnvalue = '';
         my @matches = ();
+        my @values = ();
         $#matches = -1;
         study $expression;
-        my $parameter;
-        foreach $parameter (keys(%{$self->{'constants'}})) {
-            push @matches,$parameter if ($parameter =~ /$expression/);
+        while (my($parameter,$value) = each(%{$self->{'constants'}})) {
+            next if ($parameter !~ /$expression/);
+            push(@matches,$parameter);
+            push(@values,$value);
         }
         if (scalar(@matches) == 0) {
-            $returnvalue = 'unmatched parameter: '.$parameter;
+            $returnvalue = '""';#'"unmatched parameter: '.$parameter.'"';
         } elsif (scalar(@matches) == 1) {
             # why do we not do this lookup here, instead of delaying it?
-            $returnvalue = '$c{\''.$matches[0].'\'}';
+            $returnvalue = $values[0];
         } elsif (scalar(@matches) > 0) {
             # more than one match.  Look for a concise one
             $returnvalue =  "'non-unique parameter name : $expression'";
-            foreach (@matches) {
-                if (/^$expression$/) {
+            for (my $i=0; $i<=$#matches;$i++) {
+                if ($matches[$i] =~ /^$expression$/) {
                     # why do we not do this lookup here?
-                    $returnvalue = '$c{\''.$_.'\'}';
+                    $returnvalue = $values[$i];
                 }
             }
         } else {
             # There was a negative number of matches, which indicates 
             # something is wrong with reality.  Better warn the user.
-            $returnvalue = 'bizzare parameter: '.$parameter;
+            $returnvalue = '"bizzare parameter: '.$expression.'"';
         }
         return $returnvalue;
     }
@@ -1125,6 +1162,7 @@ sub display {
     } elsif ($outputmode eq 'csv') {
         $self->outsheet_csv($r);
     }
+    $self->cleanup();
     return;
 }
 
@@ -1133,13 +1171,15 @@ sub display {
 ############################################
 sub html_export_row {
     my $self = shift();
+    my ($color) = @_;
+    $color = '#CCCCFF' if (! defined($color));
     my $allowed = &Apache::lonnet::allowed('mgr',$ENV{'request.course.id'});
     my $row_html;
     my @rowdata = $self->get_row(0);
     foreach my $cell (@rowdata) {
         if ($cell->{'name'} =~ /^[A-Z]/) {
-	    $row_html .= '<td bgcolor="#CCCCFF">'.
-                &html_editable_cell($cell,'#CCCCFF',$allowed).'</td>';
+	    $row_html .= '<td bgcolor="'.$color.'">'.
+                &html_editable_cell($cell,$color,$allowed).'</td>';
         } else {
 	    $row_html .= '<td bgcolor="#DDCCFF">'.
                 &html_editable_cell($cell,'#DDCCFF',$allowed).'</td>';
@@ -1151,15 +1191,15 @@ sub html_export_row {
 sub html_template_row {
     my $self = shift();
     my $allowed = &Apache::lonnet::allowed('mgr',$ENV{'request.course.id'});
-    my ($num_uneditable) = @_;
+    my ($num_uneditable,$importcolor) = @_;
     my $row_html;
     my @rowdata = $self->get_template_row();
     my $count = 0;
     for (my $i = 0; $i<=$#rowdata; $i++) {
         my $cell = $rowdata[$i];
         if ($i < $num_uneditable) {
-	    $row_html .= '<td bgcolor="#DDCCFF">'.
-                &html_editable_cell($cell,'#DDCCFF',$allowed).'</td>';
+	    $row_html .= '<td bgcolor="'.$importcolor.'">'.
+                &html_uneditable_cell($cell,'#FFDDDD',$allowed).'</td>';
         } else {
 	    $row_html .= '<td bgcolor="#EOFFDD">'.
                 &html_editable_cell($cell,'#EOFFDD',$allowed).'</td>';
@@ -1190,13 +1230,17 @@ sub html_editable_cell {
         $value = &HTML::Entities::encode($value) if ($value !~/&nbsp;/);
     }
     return $value if (! $allowed);
-    # Make the formula safe for outputting
-    $formula =~ s/\'/\"/g;
+    #
     # The formula will be parsed by the browser twice before being 
-    # displayed to the user for editing.
-    $formula = &HTML::Entities::encode(&HTML::Entities::encode($formula));
-    # Escape newlines so they make it into the edit window
-    $formula =~ s/\n/\\n/gs;
+    # displayed to the user for editing. 
+    #
+    # The encoding string "^A-blah" is placed in []'s inside a regexp, so 
+    # we specify the characters we want left alone by putting a '^' in front.
+    $formula = &HTML::Entities::encode($formula,'^A-z0-9 !#$%-;=?~');
+    # HTML::Entities::encode does not catch everything - we need '\' encoded
+    $formula =~ s/\\/&\#092/g;
+    # Escape it again - this time the only encodable character is '&'
+    $formula =~ s/\&/\&amp;/g;
     # Glue everything together
     $result .= "<a href=\"javascript:celledit(\'".
         $name."','".$formula."');\">".$value."</a>";
@@ -1212,14 +1256,19 @@ sub html_uneditable_cell {
 
 sub html_row {
     my $self = shift();
-    my ($num_uneditable,$row) = @_;
+    my ($num_uneditable,$row,$exportcolor,$importcolor) = @_;
     my $allowed = &Apache::lonnet::allowed('mgr',$ENV{'request.course.id'});
     my @rowdata = $self->get_row($row);
     my $num_cols_output = 0;
     my $row_html;
+    my $color = $importcolor;
+    if ($row == 0) {
+        $color = $exportcolor;
+    }
+    $color = '#FFDDDD' if (! defined($color));
     foreach my $cell (@rowdata) {
 	if ($num_cols_output++ < $num_uneditable) {
-	    $row_html .= '<td bgcolor="#FFDDDD">';
+	    $row_html .= '<td bgcolor="'.$color.'">';
 	    $row_html .= &html_uneditable_cell($cell,'#FFDDDD');
 	} else {
 	    $row_html .= '<td bgcolor="#EOFFDD">';
@@ -1234,7 +1283,7 @@ sub html_header {
     my $self = shift;
     return '' if (! $ENV{'request.role.adv'});
     return "<table>\n".
-        '<tr><th align="center">Output Format</th><tr>'."\n".
+        '<tr><th align="center">'.&mt('Output Format').'</th></tr>'."\n".
         '<tr><td>'.&output_selector()."</td></tr>\n".
         "</table>\n";
 }
@@ -1249,13 +1298,13 @@ sub output_selector {
     }
     foreach (['html','HTML'],
              ['excel','Excel'],
-             ['csv','Comma Seperated Values']) {
+             ['csv','Comma Separated Values']) {
         my ($name,$description) = @{$_};
         $output_selector.=qq{<option value="$name"};
         if ($name eq $default) {
             $output_selector .= ' selected';
         }
-        $output_selector .= ">$description</option>\n";
+        $output_selector .= ">".&mt($description)."</option>\n";
     }
     $output_selector .= "</select>\n";
     return $output_selector;
@@ -1288,9 +1337,9 @@ sub create_excel_spreadsheet {
     my $workbook  = Spreadsheet::WriteExcel->new('/home/httpd'.$filename);
     if (! defined($workbook)) {
         $r->log_error("Error creating excel spreadsheet $filename: $!");
-        $r->print("Problems creating new Excel file.  ".
+        $r->print(&mt("Problems creating new Excel file.  ".
                   "This error has been logged.  ".
-                  "Please alert your LON-CAPA administrator");
+                  "Please alert your LON-CAPA administrator"));
         return undef;
     }
     #
@@ -1307,7 +1356,8 @@ sub create_excel_spreadsheet {
 sub outsheet_excel {
     my $self = shift;
     my ($r) = @_;
-    $r->print("<h2>Preparing Excel Spreadsheet</h2>");
+    my $connection = $r->connection();
+    $r->print("<h2>".&mt('Preparing Excel Spreadsheet')."</h2>");
     #
     # Create excel worksheet
     my ($workbook,$filename) = $self->create_excel_spreadsheet($r);
@@ -1330,7 +1380,7 @@ sub outsheet_excel {
     $self->excel_output_row($worksheet,0,$rows_output++,'Summary');
     $rows_output++;    # skip a line
     #
-    $self->excel_rows($worksheet,$cols_output,$rows_output);
+    $self->excel_rows($connection,$worksheet,$cols_output,$rows_output);
     #
     #
     # Close the excel file
@@ -1342,6 +1392,66 @@ sub outsheet_excel {
     return;
 }
 
+#################################
+## CSV output routines         ##
+#################################
+sub outsheet_csv   {
+    my $self = shift;
+    my ($r) = @_;
+    my $connection = $r->connection();
+    my $csvdata = '';
+    my @Values;
+    #
+    # Open the csv file
+    my $filename = '/prtspool/'.
+        $ENV{'user.name'}.'_'.$ENV{'user.domain'}.'_'.
+        time.'_'.rand(1000000000).'.csv';
+    my $file;
+    unless ($file = Apache::File->new('>'.'/home/httpd'.$filename)) {
+        $r->log_error("Couldn't open $filename for output $!");
+        $r->print(&mt("Problems occured in writing the csv file.  ".
+                  "This error has been logged.  ".
+                  "Please alert your LON-CAPA administrator."));
+        $r->print("<pre>\n".$csvdata."</pre>\n");
+        return 0;
+    }
+    #
+    # Output the title information
+    foreach my $value ($self->get_title()) {
+        print $file "'".&Apache::loncommon::csv_translate($value)."'\n";
+    }
+    #
+    # Output the body of the spreadsheet
+    $self->csv_rows($connection,$file);
+    #
+    # Close the csv file
+    close($file);
+    $r->print('<br /><br />'.
+              '<a href="'.$filename.'">'.&mt('Your CSV spreadsheet.').'</a>'."\n");
+    #
+    return 1;
+}
+
+sub csv_output_row {
+    my $self = shift;
+    my ($filehandle,$rownum,@prepend) = @_;
+    #
+    my @rowdata = ();
+    if (defined($rownum)) {
+        @rowdata = $self->get_row($rownum);
+    }
+    my @output = ();
+    foreach my $cell (@prepend,@rowdata) {
+        my $value = $cell;
+        $value = $cell->{'value'} if (ref($value));
+        $value =~ s/\&nbsp;/ /gi;
+        $value = "'".$value."'";
+        push (@output,$value);
+    }
+    print $filehandle join(',',@output )."\n";
+    return;
+}
+
 ############################################
 ##          XML output routines           ##
 ############################################
@@ -1408,16 +1518,33 @@ sub clear_spreadsheet_definition_cache {
     undef(%spreadsheets);
 }
 
-sub load {
+sub load_system_default_sheet {
     my $self = shift;
     my $includedir = $Apache::lonnet::perlvar{'lonIncludes'};
+    # load in the default defined spreadsheet
+    my $sheetxml='';
+    my $fh;
+    if ($fh=Apache::File->new($includedir.'/default_'.$self->{'type'})) {
+        $sheetxml=join('',<$fh>);
+        $fh->close();
+    } else {
+        # $sheetxml='<field row="0" col="A">"Error"</field>';
+        $sheetxml='<field row="0" col="A"></field>';
+    }
+    $self->filename('default_');
+    my ($formulas,undef) = &parse_sheet(\$sheetxml);
+    return $formulas;
+}
+
+sub load {
+    my $self = shift;
     #
     my $stype = $self->{'type'};
     my $cnum  = $self->{'cnum'};
     my $cdom  = $self->{'cdom'};
     my $chome = $self->{'chome'};
-    my $filename = $self->{'filename'};
     #
+    my $filename = $self->filename();
     my $cachekey = join('_',($cnum,$cdom,$stype,$filename));
     #
     # see if sheet is cached
@@ -1426,19 +1553,9 @@ sub load {
         $formulas = $spreadsheets{$cachekey}->{'formulas'};
     } else {
         # Not cached, need to read
-        if (! defined($self->filename())) {
-            # load in the default defined spreadsheet
-            my $sheetxml='';
-            my $fh;
-            if ($fh=Apache::File->new($includedir.'/default.'.$filename)) {
-                $sheetxml=join('',<$fh>);
-                $fh->close();
-            } else {
-                # $sheetxml='<field row="0" col="A">"Error"</field>';
-                $sheetxml='<field row="0" col="A"></field>';
-            }
-            ($formulas,undef) = &parse_sheet(\$sheetxml);
-        } elsif($self->filename() =~ /^\/*\.spreadsheet$/) {
+        if (! defined($filename)) {
+            $formulas = $self->load_system_default_sheet();
+        } elsif($self->filename() =~ /^\/res\/.*\.spreadsheet$/) {
             # Load a spreadsheet definition file
             my $sheetxml=&Apache::lonnet::getfile
                 (&Apache::lonnet::filelocation('',$filename));
@@ -1447,31 +1564,27 @@ sub load {
                     .$self->filename().'"</field>';
             }
             ($formulas,undef) = &parse_sheet(\$sheetxml);
+            # Get just the filename and set the sheets filename
+            my ($newfilename) = ($filename =~ /\/([^\/]*)\.spreadsheet$/);
+            if ($self->is_default()) {
+                $self->filename($newfilename);
+                $self->make_default();
+            } else {
+                $self->filename($newfilename);
+            }
         } else {
             # Load the spreadsheet definition file from the save file
-            my %tmphash = &Apache::lonnet::dump($self->filename(),$cdom,$cnum);
+            my %tmphash = &Apache::lonnet::dump($filename,$cdom,$cnum);
             my ($tmp) = keys(%tmphash);
             if ($tmp !~ /^(con_lost|error|no_such_host)/i) {
                 while (my ($cell,$formula) = each(%tmphash)) {
                     $formulas->{$cell}=$formula;
                 }
             } else {
-                # Unable to grab the specified spreadsheet,
-                # so we get the default ones instead.
-                $filename = 'default.'.$stype;
-                $self->filename($filename);
-                my $sheetxml;
-                if (my $fh=Apache::File->new($includedir.'/'.$filename)) {
-                    $sheetxml = join('',<$fh>);
-                    $fh->close();
-                } else {
-                    $sheetxml='<field row="0" col="A">'.
-                        '"Unable to load spreadsheet"</field>';
-                }
-                ($formulas,undef) = &parse_sheet(\$sheetxml);
-                $self->formulas($formulas);
+                $formulas = $self->load_system_default_sheet();
             }
         }
+        $filename=$self->filename(); # filename may have changed
         $cachekey = join('_',($cnum,$cdom,$stype,$filename));
         %{$spreadsheets{$cachekey}->{'formulas'}} = %{$formulas};
     }
@@ -1483,13 +1596,23 @@ sub load {
 sub set_row_sources {
     my $self = shift;
     while (my ($cell,$value) = each(%{$self->{'formulas'}})) {
-        next if ($cell !~ /^A(\d+)/ && $1 > 0);
+        next if ($cell !~ /^A(\d+)/ || $1 < 1);
         my $row = $1;
         $self->{'row_source'}->{$row} = $value;
     }
     return;
 }
 
+sub set_row_numbers {
+    my $self = shift;
+    while (my ($cell,$value) = each(%{$self->{'formulas'}})) {
+	next if ($cell !~ /^A(\d+)$/);
+        next if (! defined($value));
+	$self->{'row_numbers'}->{$value} = $1;
+        $self->{'maxrow'} = $1 if ($1 > $self->{'maxrow'});
+    }
+}
+
 ##
 ## exportrow is *not* used to get the export row from a computed sub-sheet.
 ##
@@ -1514,27 +1637,33 @@ sub save {
         my $cnum  = $self->{'cnum'};
         my $cdom  = $self->{'cdom'};
         my $chome = $self->{'chome'};
-        my $fn    = $self->{'filename'};
+        my $filename    = $self->{'filename'};
+        my $cachekey = join('_',($cnum,$cdom,$stype,$filename));
         # Cache new sheet
-        $spreadsheets{$cnum.'_'.$cdom.'_'.$stype.'_'.$fn}=join('___;___',%f);
+        %{$spreadsheets{$cachekey}->{'formulas'}}=%f;
         # Write sheet
         foreach (keys(%f)) {
             delete($f{$_}) if ($f{$_} eq 'import');
         }
-        my $reply = &Apache::lonnet::put($fn,\%f,$cdom,$cnum);
+        my $reply = &Apache::lonnet::put($filename,\%f,$cdom,$cnum);
         return $reply if ($reply ne 'ok');
         $reply = &Apache::lonnet::put($stype.'_spreadsheets',
-                       {$fn => $ENV{'user.name'}.'@'.$ENV{'user.domain'}},
+                     {$filename => $ENV{'user.name'}.'@'.$ENV{'user.domain'}},
                                       $cdom,$cnum);
         return $reply if ($reply ne 'ok');
         if ($makedef) { 
             $reply = &Apache::lonnet::put('environment',
-                                {'spreadsheet_default_'.$stype => $fn },
+                                {'spreadsheet_default_'.$stype => $filename },
                                           $cdom,$cnum);
             return $reply if ($reply ne 'ok');
         } 
         if ($self->is_default()) {
-            &Apache::lonnet::expirespread('','',$self->{'type'},'');
+            if ($self->{'type'} eq 'studentcalc') {
+                &Apache::lonnet::expirespread('','','studentcalc','');
+            } elsif ($self->{'type'} eq 'assesscalc') {
+                &Apache::lonnet::expirespread('','','assesscalc','');
+                &Apache::lonnet::expirespread('','','studentcalc','');
+            }
         }
         return $reply;
     }
@@ -1545,14 +1674,14 @@ sub save {
 
 sub save_tmp {
     my $self = shift;
-    my $fn=$ENV{'user.name'}.'_'.
-        $ENV{'user.domain'}.'_spreadsheet_'.$self->{'usymb'}.'_'.
+    my $filename=$ENV{'user.name'}.'_'.
+        $ENV{'user.domain'}.'_spreadsheet_'.$self->{'symb'}.'_'.
            $self->{'filename'};
-    $fn=~s/\W/\_/g;
-    $fn=$Apache::lonnet::tmpdir.$fn.'.tmp';
+    $filename=~s/\W/\_/g;
+    $filename=$Apache::lonnet::tmpdir.$filename.'.tmp';
     $self->temporary(1);
     my $fh;
-    if ($fh=Apache::File->new('>'.$fn)) {
+    if ($fh=Apache::File->new('>'.$filename)) {
         my %f = $self->formulas();
         while( my ($cell,$formula) = each(%f)) {
             next if ($formula eq 'import');
@@ -1566,7 +1695,7 @@ sub save_tmp {
 sub load_tmp {
     my $self = shift;
     my $filename=$ENV{'user.name'}.'_'.
-        $ENV{'user.domain'}.'_spreadsheet_'.$self->{'usymb'}.'_'.
+        $ENV{'user.domain'}.'_spreadsheet_'.$self->{'symb'}.'_'.
             $self->{'filename'};
     $filename=~s/\W/\_/g;
     $filename=$Apache::lonnet::tmpdir.$filename.'.tmp';
@@ -1594,7 +1723,6 @@ sub temporary {
     if (@_) {
         ($self->{'temporary'})= @_;
     }
-    $self->logthis('temporary = '.$self->{'temporary'});
     return $self->{'temporary'};
 }
 
@@ -1625,9 +1753,9 @@ sub othersheets {
                                       $self->{'cdom'}, $self->{'cnum'});
     my ($tmp) = keys(%results);
     if ($tmp =~ /^(con_lost|error|no_such_host)/i ) {
-        @alternatives = ('Default');
+        @alternatives = (&mt('Default'));
     } else {
-        @alternatives = sort (keys(%results));
+        @alternatives = (&mt('Default'), sort (keys(%results)));
     }
     return @alternatives; 
 }
@@ -1663,14 +1791,12 @@ sub get_template_row {
     return @cols;
 }
 
-sub set_row_numbers {
+sub need_to_save {
     my $self = shift;
-    my %f=$self->formulas();
-    while (my ($cell,$value) = each(%{$self->{'formulas'}})) {
-	next if ($cell !~ /^A(\d+)$/);
-        next if (! defined($value));
-	$self->{'row_numbers'}->{$value} = $1;
+    if ($self->{'new_rows'} && ! $self->temporary()) {
+        return 1;
     }
+    return 0;
 }
 
 sub get_row_number_from_key {
@@ -1682,6 +1808,9 @@ sub get_row_number_from_key {
         # may not be the key we need to save
 	$self->{'maxrow'}++;
 	$self->{'row_numbers'}->{$key} = $self->{'maxrow'};
+#        $self->logthis('added row '.$self->{'row_numbers'}->{$key}.
+#                       ' for '.$key);
+        $self->{'new_rows'} = 1;
     }
     return $self->{'row_numbers'}->{$key};
 }