--- loncom/interface/spreadsheet/Spreadsheet.pm	2005/04/07 06:56:23	1.41
+++ loncom/interface/spreadsheet/Spreadsheet.pm	2024/11/16 02:41:05	1.86
@@ -1,5 +1,5 @@
 #
-# $Id: Spreadsheet.pm,v 1.41 2005/04/07 06:56:23 albertel Exp $
+# $Id: Spreadsheet.pm,v 1.86 2024/11/16 02:41:05 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -60,6 +60,9 @@ use HTML::TokeParser;
 use Spreadsheet::WriteExcel;
 use Time::HiRes;
 use Apache::lonlocal;
+use lib '/home/httpd/lib/perl/';
+use LONCAPA;
+ 
 
 ##
 ## Package Variables
@@ -85,7 +88,7 @@ sub new {
     my $class = ref($this) || $this;
     my ($stype) = ($class =~ /Apache::(.*)$/);
     #
-    my ($name,$domain,$filename,$usymb)=@_;
+    my ($name,$domain,$filename,$usymb,$section,$groups)=@_;
     if (defined($usymb) && ref($usymb)) {
         $usymb = $usymb->symb;
     }
@@ -95,10 +98,22 @@ sub new {
     if (! defined($domain) || $domain eq '') {
         $domain = $env{'user.domain'};
     }
+    if (! defined($section) || $section eq '') {
+        $section = &Apache::lonnet::getsection($domain,$name,
+					       $env{'request.course.id'});
+    }
+    if (! defined($groups)) {
+
+        my @usersgroups = &Apache::lonnet::get_users_groups($domain,$name,
+                                                    $env{'request.course.id'});
+        $groups = \@usersgroups;
+    }
     #
     my $self = {
         name     => $name,
         domain   => $domain,
+        section  => $section,
+        groups   => $groups, 
         type     => $stype,
         symb     => $usymb,
         errorlog => '',
@@ -106,13 +121,13 @@ sub new {
         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'},
         #
         # Flags
         temporary => 0,  # true if this sheet has been modified but not saved
-        new_rows  => 0, # true if this sheet has new rows
+        new_rows  => 0,  # true if this sheet has new rows
+	loaded    => 0,  # true if the formulas have been loaded
         #
         # blackout is used to determine if any data needs to be hidden from the
         # student.
@@ -126,18 +141,9 @@ sub new {
         othersheets => [],
     };
     #
-    $self->{'uhome'} = &Apache::lonnet::homeserver($name,$domain);
-    #
     bless($self,$class);
-    #
-    # Load in the spreadsheet definition
     $self->filename($filename);
-    if (exists($env{'form.workcopy'}) && 
-        $self->{'type'} eq $env{'form.workcopy'}) {
-        $self->load_tmp();
-    } else {
-        $self->load();
-    }
+    #
     return $self;
 }
 
@@ -166,16 +172,20 @@ sub filename {
                 $newfilename = 'default_'.$self->{'type'};
             }
         }
-        if ($newfilename !~ /\w/ || $newfilename =~ /^\W*$/) {
-            $newfilename = 'default_'.$self->{'type'};
-        }
-        if ($newfilename !~ /^default\.$self->{'type'}$/ &&
-            $newfilename !~ /^\/res\/(.*)spreadsheet$/) {
-            if ($newfilename !~ /_$self->{'type'}$/) {
-                $newfilename =~ s/[\s_]*$//;
-                $newfilename .= '_'.$self->{'type'};
-            }
-        }
+	if ($newfilename eq &mt('LON-CAPA Standard')) {
+	    undef($newfilename);
+	} else {
+	    if ($newfilename !~ /\w/ || $newfilename =~ /^\W*$/) {
+		$newfilename = 'default_'.$self->{'type'};
+	    }
+	    if ($newfilename !~ /^default\.$self->{'type'}$/ &&
+		$newfilename !~ /^\/res\/(.*)spreadsheet$/) {
+		if ($newfilename !~ /_$self->{'type'}$/) {
+		    $newfilename =~ s/[\s_]*$//;
+		    $newfilename .= '_'.$self->{'type'};
+		}
+	    }
+	}
         $self->{'filename'} = $newfilename;
         return;
     }
@@ -200,6 +210,8 @@ sub make_default {
             {'spreadsheet_default_'.$self->{'type'} => $self->filename()},
                                      $self->{'cdom'},$self->{'cnum'});
     return $result if ($result ne 'ok');
+    &Apache::lonnet::appenv({'course.'.$self->{'cid'}.'.spreadsheet_default_'.
+			    $self->{'type'} => $self->filename()});
     my $symb = $self->{'symb'};
     $symb = '' if (! defined($symb));
     &Apache::lonnet::expirespread('','',$self->{'type'},$symb);    
@@ -220,16 +232,8 @@ course environment.  Returns 0 otherwise
 sub is_default {
     my $self = shift;
     # Check to find out if we are the default spreadsheet (filenames match)
-    my $default_filename = '';
-    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) {
-        $default_filename = $tmphash{'spreadsheet_default_'.$self->{'type'}};
-    }
+    my $default_filename = $env{'course.'.$self->{'cid'}.
+				    '.spreadsheet_default_'.$self->{'type'}};
     if ($default_filename =~ /^\s*$/) {
         $default_filename = 'default_'.$self->{'type'};
     }
@@ -323,12 +327,18 @@ sub initialize_safe_space {
       $safeeval = new Safe(shift);
       my $safehole = new Safe::Hole;
       $safeeval->permit("entereval");
+      $safeeval->permit("hintseval"); 
       $safeeval->permit(":base_math");
       $safeeval->permit("sort");
       $safeeval->deny(":base_io");
       $safehole->wrap(\&Apache::lonnet::EXT,$safeeval,'&Apache::lonnet::EXT');
       $safehole->wrap(\&mask,$safeeval,'&mask');
+      $safehole->wrap(\&Apache::lonnet::logthis,$safeeval,'&logthis');
       $safeeval->share('$@');
+    # Holds the (computed, final) values for the sheet
+    # This is only written to by &calc, the spreadsheet computation routine.
+    # It is read by many functions
+      $safeeval->share('%sheet_values');
       my $code=<<'ENDDEFS';
 # ---------------------------------------------------- Inside of the safe space
 #
@@ -338,12 +348,9 @@ sub initialize_safe_space {
 # c: preloaded constants (A-column)
 # rl: row label
 # os: other spreadsheets (for student spreadsheet only)
-undef %sheet_values;   # Holds the (computed, final) values for the sheet
-    # This is only written to by &calc, the spreadsheet computation routine.
-    # It is read by many functions
-undef %t; # Holds the values of the spreadsheet temporarily. Set in &sett, 
-    # which does the translation of strings like C5 into the value in C5.
-    # Used in &calc - %t holds the values that are actually eval'd.
+undef %t; # Holds the forumlas of the spreadsheet to be computed. Set in
+    # &sett, which does the translation of strings like C5 into the value
+    # in C5. Used in &calc - %t holds the values that are actually eval'd.
 undef %f;    # Holds the formulas for each cell.  This is the users
     # (spreadsheet authors) data for each cell.
 undef %c; # Holds the constants for a sheet.  In the assessment
@@ -364,12 +371,10 @@ $filename = '';
 #
 # user data
 $name = '';
-$uhome = '';
 $domain  = '';
 #
 # course data
 $csec = '';
-$chome= '';
 $cnum = '';
 $cdom = '';
 $cid  = '';
@@ -393,10 +398,12 @@ Calls the system EXT function to determi
 
 #-------------------------------------------------------
 sub EXT {
-    my ($parameter) = @_;
+    my ($parameter,$specific_symb) = @_;
     return '' if (! defined($parameter) || $parameter eq '');
     $parameter =~ s/^parameter\./resource\./;
-    my $value = &Apache::lonnet::EXT($parameter,$symb,$domain,$name,$usection);
+    if ($specific_symb eq '') { $specific_symb = $symb; }
+    my $value = &Apache::lonnet::EXT($parameter,$specific_symb,$domain,$name,
+				     $usection);
     return $value;
 }
 
@@ -412,8 +419,8 @@ returns the number of items in the range
 
 #-------------------------------------------------------
 sub NUM {
-    my $mask=&mask(@_);
-    my $num= $#{@{grep(eval("/$mask/"),keys(%sheet_values))}}+1;
+    my $values=&get_values(@_);
+    my $num= scalar(@$values);
     return $num;   
 }
 
@@ -428,10 +435,10 @@ sub NUM {
 #-------------------------------------------------------
 sub BIN {
     my ($low,$high,$lower,$upper)=@_;
-    my $mask=&mask($lower,$upper);
+    my $values=&get_values($lower,$upper);
     my $num=0;
-    foreach (grep eval("/$mask/"),keys(%sheet_values)) {
-        if (($sheet_values{$_}>=$low) && ($sheet_values{$_}<=$high)) {
+    foreach (@$values) {
+        if (($_>=$low) && ($_<=$high)) {
             $num++;
         }
     }
@@ -450,10 +457,10 @@ returns the sum of items in the range.
 
 #-------------------------------------------------------
 sub SUM {
-    my $mask=&mask(@_);
+    my $values=&get_values(@_);
     my $sum=0;
-    foreach (grep eval("/$mask/"),keys(%sheet_values)) {
-        $sum+=$sheet_values{$_};
+    foreach (@$values) {
+        $sum+=$_;
     }
     return $sum;   
 }
@@ -470,11 +477,11 @@ compute the average of the items in the
 
 #-------------------------------------------------------
 sub MEAN {
-    my $mask=&mask(@_);
+    my $values=&get_values(@_);
     my $sum=0; 
     my $num=0;
-    foreach (grep eval("/$mask/"),keys(%sheet_values)) {
-        $sum+=$sheet_values{$_};
+    foreach (@$values) {
+        $sum+=$_;
         $num++;
     }
     if ($num) {
@@ -496,17 +503,17 @@ compute the standard deviation of the it
 
 #-------------------------------------------------------
 sub STDDEV {
-    my $mask=&mask(@_);
+    my $values=&get_values(@_);
     my $sum=0; my $num=0;
-    foreach (grep eval("/$mask/"),keys(%sheet_values)) {
-        $sum+=$sheet_values{$_};
+    foreach (@$values) {
+        $sum+=$_;
         $num++;
     }
     unless ($num>1) { return undef; }
     my $mean=$sum/$num;
     $sum=0;
-    foreach (grep eval("/$mask/"),keys(%sheet_values)) {
-        $sum+=($sheet_values{$_}-$mean)**2;
+    foreach (@$values) {
+        $sum+=($_-$mean)**2;
     }
     return sqrt($sum/($num-1));    
 }
@@ -523,10 +530,10 @@ compute the product of the items in the
 
 #-------------------------------------------------------
 sub PROD {
-    my $mask=&mask(@_);
+    my $values=&get_values(@_);
     my $prod=1;
-    foreach (grep eval("/$mask/"),keys(%sheet_values)) {
-        $prod*=$sheet_values{$_};
+    foreach (@$values) {
+        $prod*=$_;
     }
     return $prod;   
 }
@@ -543,12 +550,11 @@ compute the maximum of the items in the
 
 #-------------------------------------------------------
 sub MAX {
-    my $mask=&mask(@_);
+    my $values=&get_values(@_);
     my $max='-';
-    foreach (grep eval("/$mask/"),keys(%sheet_values)) {
-        unless ($max) { $max=$sheet_values{$_}; }
-        if (($sheet_values{$_}>$max) || ($max eq '-')) { 
-            $max=$sheet_values{$_}; 
+    foreach (@$values) {
+        if (($_>$max) || ($max eq '-')) { 
+            $max=$_; 
         }
     } 
     return $max;   
@@ -566,12 +572,11 @@ compute the minimum of the items in the
 
 #-------------------------------------------------------
 sub MIN {
-    my $mask=&mask(@_);
+    my $values=&get_values(@_);
     my $min='-';
-    foreach (grep eval("/$mask/"),keys(%sheet_values)) {
-        unless ($max) { $max=$sheet_values{$_}; }
-        if (($sheet_values{$_}<$min) || ($min eq '-')) { 
-            $min=$sheet_values{$_}; 
+    foreach (@$values) {
+        if (($_<$min) || ($min eq '-')) { 
+            $min=$_; 
         }
     }
     return $min;   
@@ -591,12 +596,8 @@ compute the sum of the largest 'num' ite
 #-------------------------------------------------------
 sub SUMMAX {
     my ($num,$lower,$upper)=@_;
-    my $mask=&mask($lower,$upper);
-    my @inside=();
-    foreach (grep eval("/$mask/"),keys(%sheet_values)) {
-	push (@inside,$sheet_values{$_});
-    }
-    @inside=sort(@inside);
+    my $values=&get_values($lower,$upper);
+    my @inside=sort {$a <=> $b} (@$values);
     my $sum=0; my $i;
     for ($i=$#inside;(($i>$#inside-$num) && ($i>=0));$i--) { 
         $sum+=$inside[$i];
@@ -618,12 +619,8 @@ compute the sum of the smallest 'num' it
 #-------------------------------------------------------
 sub SUMMIN {
     my ($num,$lower,$upper)=@_;
-    my $mask=&mask($lower,$upper);
-    my @inside=();
-    foreach (grep eval("/$mask/"),keys(%sheet_values)) {
-	$inside[$#inside+1]=$sheet_values{$_};
-    }
-    @inside=sort(@inside);
+    my $values=&get_values($lower,$upper);
+    my @inside=sort {$a <=> $b} (@$values);
     my $sum=0; my $i;
     for ($i=0;(($i<$num) && ($i<=$#inside));$i++) { 
         $sum+=$inside[$i];
@@ -680,8 +677,81 @@ sub MAXPARM {
 }
 
 
+=pod
+
+=item PARM(parametername)
+
+Returns the value of the parameter matching the input parameter name.
+parametername should be a string such as 'parameter_1_opendate'.
+
+=cut
+
+#-------------------------------------------------------
+sub PARM {
+    return $c{$_[0]};
+}
+
+#-------------------------------------------------------
+
+=pod
+
+=item  &get_values($lower,$upper)
+
+Inputs: $lower and $upper, cell names ("X12" or "a150") or globs ("X*").
+
+Returns: an array ref of the values of the cells that exist in the 
+         speced range
+
+=cut
+
+#-------------------------------------------------------
+sub get_values {
+    my ($lower,$upper)=@_;
+    $upper = $lower if (! defined($upper));
+    my @values;
+    my ($la,$ld) = ($lower=~/([A-z]|\*)(\d+|\*)/);
+    my ($ua,$ud) = ($upper=~/([A-z]|\*)(\d+|\*)/);
+    my ($alpha,$num);
+    if ($ld ne '*' && $ud ne '*') {
+	my @alpha;
+	if (($la eq '*') || ($ua eq '*')) {
+	    @alpha=('A'..'z');
+	} else {
+	    if ($la gt $ua) { ($la,$ua)=($ua,$la); }
+	    if ((lc($la) ne $la) && (lc($ua) eq $ua)) {
+		@alpha=($la..'Z','a'..$ua);
+	    } else {
+		@alpha=($la..$ua);
+            }
+	}
+	my @num=($ld..$ud);
+	foreach my $a (@alpha) {
+	    foreach my $n (@num) {
+		if ((exists($sheet_values{$a.$n})) && ($sheet_values{$a.$n} ne '')) {
+		    push(@values,$sheet_values{$a.$n});
+		}
+	    }
+	}
+	return \@values;
+    } else {
+	$num = '([1-9]\d*)';
+    }
+    if (($la eq '*') || ($ua eq '*')) {
+        $alpha='[A-z]';
+    } else {
+	if ($la gt $ua) { ($la,$ua)=($ua,$la); }
+        $alpha=qq/[$la-$ua]/;
+    }
+    my $expression = '^'.$alpha.$num.'$';
+    foreach my $item (grep(/$expression/,keys(%sheet_values))) {
+        unless ($sheet_values{$item} eq '') {
+	    push(@values,$sheet_values{$item});
+        }
+    }
+    return \@values;
+}
+
 sub calc {
-    %sheet_values = %t;
     my $notfinished = 1;
     my $lastcalc = '';
     my $depth = 0;
@@ -705,7 +775,7 @@ sub calc {
         $depth++;
         if ($depth>100) {
 	    undef %sheet_values;
-            return $lastcalc.': Maximum calculation depth exceeded';
+            return $lastcalc.': '.&mt('Maximum calculation depth exceeded');
         }
     }
     return 'okay';
@@ -722,7 +792,7 @@ ENDDEFS
     # itself
     my $initstring = '';
     foreach (qw/name domain type symb cid csec coursefilename
-             cnum cdom chome uhome/) {
+             cnum cdom/) {
         $initstring.= qq{\$$_="$self->{$_}";};
     }
     $initstring.=qq{\$usection="$usection";};
@@ -742,92 +812,6 @@ 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
-
-######################################################
-{
-
-my %memoizer;
-
-sub mask {
-    my ($lower,$upper)=@_;
-    my $key = $lower.'_'.$upper;
-    if (exists($memoizer{$key})) {
-        return $memoizer{$key};
-    }
-    $upper = $lower if (! defined($upper));
-    #
-    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-z]';
-    } else {
-        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 {
-        $num = '(\d+)';
-    }
-    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;
-}
-
-}
-
 ##
 ## sub add_hash_to_safe {} # spreadsheet, would like to destroy
 ##
@@ -839,18 +823,20 @@ sub expandnamed {
     my $self = shift;
     my $expression=shift;
     if ($expression=~/^\&/) {
-	my ($func,$var,$formula)=($expression=~/^\&(\w+)\(([^\;]+)\;(.*)\)/);
+	my ($func,$var,$formula)=($expression=~/^\&(\w+)\(([^\;]+)\;(.*)\)/s);
 	my @vars=split(/\W+/,$formula);
+	# make the list uniq
+	@vars = keys(%{{ map { $_ => 1 } @vars }});
         my %values=();
 	foreach my $varname ( @vars ) {
             if ($varname=~/^(parameter|stores|timestamp)/) {
                 $formula=~s/$varname/'$c{\''.$varname.'\'}'/ge;
-               $varname=~s/$var/\([\\w:\\- ]\+\)/g;
-	       foreach (keys(%{$self->{'constants'}})) {
-		  if ($_=~/$varname/) {
-		      $values{$1}=1;
-                  }
-               }
+		$varname=~s/$var/\([\\w:\\- ]\+\)/g;
+		foreach (keys(%{$self->{'constants'}})) {
+		    if ($_=~/$varname/) {
+			$values{$1}=1;
+		    }
+		}
 	    }
         }
         if ($func eq 'EXPANDSUM') {
@@ -861,7 +847,7 @@ sub expandnamed {
                 $result.=$thissum.'+';
             } 
             $result=~s/\+$//;
-            return $result;
+            return '('.$result.')';
         } else {
 	    return 0;
         }
@@ -888,7 +874,7 @@ sub expandnamed {
             $returnvalue = $values[0];
         } elsif (scalar(@matches) > 0) {
             # more than one match.  Look for a concise one
-            $returnvalue =  "'non-unique parameter name : $expression'";
+            $returnvalue =  "'".&mt('non-unique parameter name: [_1]',$expression).'"';
             for (my $i=0; $i<=$#matches;$i++) {
                 if ($matches[$i] =~ /^$expression$/) {
                     # why do we not do this lookup here?
@@ -898,7 +884,7 @@ sub expandnamed {
         } else {
             # There was a negative number of matches, which indicates 
             # something is wrong with reality.  Better warn the user.
-            $returnvalue = '"bizzare parameter: '.$expression.'"';
+            $returnvalue = "'".&mt('bizarre parameter: [_1]',$expression)."'";
         }
         return $returnvalue;
     }
@@ -907,6 +893,7 @@ sub expandnamed {
 sub sett {
     my $self = shift;
     my %t=();
+    undef(%Apache::Spreadsheet::sheet_values);
     #
     # Deal with the template row
     foreach my $col ($self->template_cells()) {
@@ -923,7 +910,7 @@ sub sett {
             # Replace 'A0' with the value from 'A0'
             $t{$cell}=~s/(^|[^\"\'])([A-Za-z]\d+)/$1\$sheet_values\{\'$2\'\}/g;
             # Replace parameters
-            $t{$cell}=~s/(^|[^\"\'])\[([^\]]+)\]/$1.$self->expandnamed($2)/ge;
+            $t{$cell}=~s/(^|[^\"\'])\[([^\]]+)\]/$1.$self->expandnamed($2)/sge;
         }
     }
     #
@@ -935,17 +922,18 @@ sub sett {
             $t{$cell}=$formula;
             $t{$cell}=~s/\.\.+/\,/g;
             $t{$cell}=~s/(^|[^\"\'])([A-Za-z]\d+)/$1\$sheet_values\{\'$2\'\}/g;
-            $t{$cell}=~s/(^|[^\"\'])\[([^\]]+)\]/$1.$self->expandnamed($2)/ge;
+            $t{$cell}=~s/(^|[^\"\'])\[([^\]]+)\]/$1.$self->expandnamed($2)/sge;
         } elsif  ( $col  =~ /^[A-Z]$/  ) {
-            if ($formula !~ /^\!/ && exists($self->{'constants'}->{$cell})) {
-                my $data = $self->{'constants'}->{$cell};
-                $t{$cell} = $data;
+            if ($formula !~ /^\!/ && exists($self->{'constants'}->{$cell})
+		&& $self->{'constants'}->{$cell} ne '') {
+		$Apache::Spreadsheet::sheet_values{$cell}=
+		    eval($self->{'constants'}->{$cell});
             }
         } else { # $row > 1 and $col =~ /[a-z]
             $t{$cell}=$formula;
             $t{$cell}=~s/\.\.+/\,/g;
             $t{$cell}=~s/(^|[^\"\'])([A-Za-z]\d+)/$1\$sheet_values\{\'$2\'\}/g;
-            $t{$cell}=~s/(^|[^\"\'])\[([^\]]+)\]/$1.$self->expandnamed($2)/ge;
+            $t{$cell}=~s/(^|[^\"\'])\[([^\]]+)\]/$1.$self->expandnamed($2)/sge;
         }
     }
     %{$self->{'safe'}->varglob('t')}=%t;
@@ -958,11 +946,11 @@ sub sett {
 sub sync_safe_space {
     my $self = shift;
     # Inside the safe space 'formulas' has a diabolical alter-ego named 'f'.
-    %{$self->{'safe'}->varglob('f')}=%{$self->{'formulas'}};
+    #%{$self->{'safe'}->varglob('f')}=%{$self->{'formulas'}};
     # 'constants' leads a peaceful hidden life of 'c'.
     %{$self->{'safe'}->varglob('c')}=%{$self->{'constants'}};
     # 'othersheets' hides as 'os', a disguise few can penetrate.
-    @{$self->{'safe'}->varglob('os')}=@{$self->{'othersheets'}};
+    #@{$self->{'safe'}->varglob('os')}=@{$self->{'othersheets'}};
 }
 
 ##
@@ -1015,15 +1003,32 @@ sub formulas {
         $self->{'formulas'} = $formulas;
         $self->{'rows'} = [];
         $self->{'template_cells'} = [];
+	$self->{'loaded'} = 1;
         return;
     } else {
+	$self->check_formulas_loaded();
         return %{$self->{'formulas'}};
     }
 }
 
+sub check_formulas_loaded {
+    my $self=shift;
+    if (!$self->{'loaded'}) {
+	$self->{'loaded'}=1;
+	# Load in the spreadsheet definition
+	if (exists($env{'form.workcopy'}) && 
+	    $self->{'type'} eq $env{'form.workcopy'}) {
+	    $self->load_tmp();
+	} else {
+	    $self->load();
+	}
+    }
+}
+
 sub set_formula {
     my $self = shift;
     my ($cell,$formula) = @_;
+    $self->check_formulas_loaded();
     $self->{'formulas'}->{$cell}=$formula;
     return;
 }
@@ -1033,7 +1038,7 @@ sub set_formula {
 ##
 sub formulas_keys {
     my $self = shift;
-    my @keys = keys(%{$self->{'formulas'}});
+    $self->check_formulas_loaded();
     return keys(%{$self->{'formulas'}});
 }
 
@@ -1044,6 +1049,7 @@ sub formulas_keys {
 sub formula {
     my $self = shift;
     my $cell = shift;
+    $self->check_formulas_loaded();
     if (defined($cell) && exists($self->{'formulas'}->{$cell})) {
         return $self->{'formulas'}->{$cell};
     }
@@ -1122,6 +1128,7 @@ sub rebuild_stats {
     my $self = shift;
     $self->{'rows'}=[];
     $self->{'template_cells'}=[];
+    $self->check_formulas_loaded();
     while (my ($cell,$formula) = each(%{$self->{'formulas'}})) {
         push(@{$self->{'rows'}},$1) if ($cell =~ /^A(\d+)/ && $1 != 0);
         push(@{$self->{'template_cells'}},$1) if ($cell =~ /^template_(\w+)/);
@@ -1227,12 +1234,19 @@ sub display {
             last;
         }
     }
+    $self->{outputmode} = $outputmode;
     if ($outputmode eq 'html') {
         $self->compute($r);
         $self->outsheet_html($r);
     } elsif ($outputmode eq 'htmlclasslist') {
         # No computation neccessary...  This is kludgy
         $self->outsheet_htmlclasslist($r);
+    } elsif ($outputmode eq 'source') {
+        # No computation necessary. Rumor has it that this is some
+        # sort of kludge w.r.t. not "computing". It's also
+        # a bit of of a kludge that we call "outsheet_html" and 
+        # let the 'outputmode' cause the outputting of source.
+        $self->outsheet_html($r);
     } elsif ($outputmode eq 'excel') {
         $self->compute($r);
         $self->outsheet_excel($r);
@@ -1254,9 +1268,9 @@ sub html_report_error {
     my $self = shift();
     my $Str = '';
     if ($self->badcalc()) {
-        $Str = '<h3 style="color:red">'.
+        $Str = '<p class="LC_error">'.
             &mt('An error occurred while calculating this spreadsheet').
-            "</h3>\n".
+            "</p>\n".
             '<pre>'.$self->calcerror()."</pre>\n";
     }
     return $Str;
@@ -1272,10 +1286,12 @@ sub html_export_row {
     foreach my $cell (@rowdata) {
         if ($cell->{'name'} =~ /^[A-Z]/) {
 	    $row_html .= '<td bgcolor="'.$color.'">'.
-                &html_editable_cell($cell,$color,$allowed).'</td>';
+                &html_editable_cell($cell,$color,$allowed,
+                                    $self->{outputmode} eq 'source').'</td>';
         } else {
 	    $row_html .= '<td bgcolor="#DDCCFF">'.
-                &html_editable_cell($cell,'#DDCCFF',$allowed).'</td>';
+                &html_editable_cell($cell,'#DDCCFF',$allowed,
+                                    $self->{outputmode} eq 'source').'</td>';
         }
     }
     return $row_html;
@@ -1294,15 +1310,16 @@ sub html_template_row {
 	    $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>';
+	    $row_html .= '<td bgcolor="#E0FFDD">'.
+                &html_editable_cell($cell,'#E0FFDD',$allowed,
+                                    $self->{outputmode} eq 'source').'</td>';
         }
     }
     return $row_html;
 }
 
 sub html_editable_cell {
-    my ($cell,$bgcolor,$allowed) = @_;
+    my ($cell,$bgcolor,$allowed,$showsource) = @_;
     my $result;
     my ($name,$formula,$value);
     if (defined($cell)) {
@@ -1312,7 +1329,13 @@ sub html_editable_cell {
     }
     $name    = '' if (! defined($name));
     $formula = '' if (! defined($formula));
-    if (! defined($value)) {
+    if ($showsource) {
+        if (!defined($formula) || $formula =~ /^\s*$/) {
+            $value = '<font color="'.$bgcolor.'">#</font>';
+        } else {
+            $value = &HTML::Entities::encode($formula, '<>&"');
+        }
+    } elsif (! defined($value)) {
         $value = '<font color="'.$bgcolor.'">#</font>';
         if ($formula ne '') {
             $value = '<i>undefined value</i>';
@@ -1329,7 +1352,7 @@ sub html_editable_cell {
     #
     # 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 !#$%-;=?~');
+    $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 '&'
@@ -1364,8 +1387,9 @@ sub html_row {
 	    $row_html .= '<td bgcolor="'.$color.'">';
 	    $row_html .= &html_uneditable_cell($cell,'#FFDDDD');
 	} else {
-	    $row_html .= '<td bgcolor="#EOFFDD">';
-	    $row_html .= &html_editable_cell($cell,'#E0FFDD',$allowed);
+	    $row_html .= '<td bgcolor="#E0FFDD">';
+	    $row_html .= &html_editable_cell($cell,'#E0FFDD',$allowed,
+                                             $self->{outputmode} eq 'source');
 	}
 	$row_html .= '</td>';
     }
@@ -1389,6 +1413,8 @@ sub output_options {
               description => 'HTML'},
              {value       => 'excel',
               description => 'Excel'},
+             {value       => 'source',
+              description => 'Show Source'},
 #             {value       => 'xml',
 #              description => 'XML'},
              {value       => 'csv',
@@ -1407,7 +1433,7 @@ sub output_selector {
     foreach  ($self->output_options()) {
         $output_selector.='<option value="'.$_->{'value'}.'"';
         if ($_->{'value'} eq $default) {
-            $output_selector .= ' selected';
+            $output_selector .= ' selected="selected"';
         }
         $output_selector .= ">".&mt($_->{'description'})."</option>\n";
     }
@@ -1484,7 +1510,7 @@ sub outsheet_excel {
     #
     # Write a link to allow them to download it
     $r->print('<br />'.
-              '<a href="'.$filename.'">Your Excel spreadsheet.</a>'."\n");
+              '<a href="'.$filename.'">'.&mt('Your Excel spreadsheet').'</a>'."\n");
     return;
 }
 
@@ -1502,16 +1528,20 @@ sub outsheet_csv   {
     my $csvdata = '';
     my @Values;
     #
-    # Open the csv file
+    # 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(
+            '<p class="LC_error">'
+           .&mt('Problems occurred in writing the CSV file.')
+           .' '.&mt('This error has been logged.')
+           .' '.&mt('Please alert your LON-CAPA administrator.')
+           .'</p>'
+        );
         $r->print("<pre>\n".$csvdata."</pre>\n");
         return 0;
     }
@@ -1524,7 +1554,7 @@ sub outsheet_csv   {
     # Output the body of the spreadsheet
     $self->csv_rows($connection,$file);
     #
-    # Close the csv file
+    # Close the CSV file
     close($file);
     $r->print('<br /><br />'.
               '<a href="'.$filename.'">'.&mt('Your CSV spreadsheet.').'</a>'."\n");
@@ -1562,6 +1592,7 @@ sub outsheet_xml   {
     ## Will be rendered for the user
     ## But not on this day
     my $Str = '<spreadsheet type="'.$self->{'type'}.'">'."\n";
+    $self->check_formulas_loaded();
     while (my ($cell,$formula) = each(%{$self->{'formulas'}})) {
         if ($cell =~ /^template_(\w+)/) {
             my $col = $1;
@@ -1644,7 +1675,6 @@ sub load {
     my $stype = $self->{'type'};
     my $cnum  = $self->{'cnum'};
     my $cdom  = $self->{'cdom'};
-    my $chome = $self->{'chome'};
     #
     my $filename = $self->filename();
     my $cachekey = join('_',($cnum,$cdom,$stype,$filename));
@@ -1653,6 +1683,10 @@ sub load {
     my ($formulas);
     if (exists($spreadsheets{$cachekey})) {
         $formulas = $spreadsheets{$cachekey}->{'formulas'};
+	$self->formulas($formulas);
+        $self->{'row_source'}=$spreadsheets{$cachekey}->{'row_source'};
+        $self->{'row_numbers'}=$spreadsheets{$cachekey}->{'row_numbers'};
+        $self->{'maxrow'}=$spreadsheets{$cachekey}->{'maxrow'};
     } else {
         # Not cached, need to read
         if (! defined($filename)) {
@@ -1662,8 +1696,10 @@ sub load {
             my $sheetxml=&Apache::lonnet::getfile
                 (&Apache::lonnet::filelocation('',$filename));
             if ($sheetxml == -1) {
-                $sheetxml='<field row="0" col="A">"Error loading spreadsheet '
-                    .$self->filename().'"</field>';
+                $sheetxml='<field row="0" col="A">'.
+                          &mt('Error loading spreadsheet [_1]',
+                                  '"'.$self->filename().'"').
+                          '</field>';
             }
             ($formulas,undef) = &parse_sheet(\$sheetxml);
             # Get just the filename and set the sheets filename
@@ -1678,7 +1714,8 @@ sub load {
             # Load the spreadsheet definition file from the save file
             my %tmphash = &Apache::lonnet::dump($filename,$cdom,$cnum);
             my ($tmp) = keys(%tmphash);
-            if ($tmp !~ /^(con_lost|error|no_such_host)/i) {
+            if (%tmphash
+		&& $tmp !~ /^(con_lost|error|no_such_host)/i) {
                 while (my ($cell,$formula) = each(%tmphash)) {
                     $formulas->{$cell}=$formula;
                 }
@@ -1686,17 +1723,38 @@ sub load {
                 $formulas = $self->load_system_default_sheet();
             }
         }
-        $filename=$self->filename(); # filename may have changed
-        $cachekey = join('_',($cnum,$cdom,$stype,$filename));
-        %{$spreadsheets{$cachekey}->{'formulas'}} = %{$formulas};
+	$self->formulas($formulas);
+	$self->set_row_sources();
+	$self->set_row_numbers();
+	$self->cache_sheet($formulas);
     }
-    $self->formulas($formulas);
-    $self->set_row_sources();
-    $self->set_row_numbers();
+}
+
+sub cache_sheet {
+    my $self = shift;
+    my ($formulas) = @_;
+    my $stype = $self->{'type'};
+    my $cnum  = $self->{'cnum'};
+    my $cdom  = $self->{'cdom'};
+    #
+    my $filename = $self->filename();
+    my $cachekey = join('_',($cnum,$cdom,$stype,$filename));
+
+    if (ref($formulas) eq 'HASH') {
+	%{$spreadsheets{$cachekey}->{'formulas'}} = %{$formulas};
+    }
+    if (ref($self->{'row_source'})) {
+	%{$spreadsheets{$cachekey}->{'row_source'}} =%{$self->{'row_source'}};
+    }
+    if (ref($self->{'row_numbers'})) {
+	%{$spreadsheets{$cachekey}->{'row_numbers'}}=%{$self->{'row_numbers'}};
+    }
+    $spreadsheets{$cachekey}->{'maxrow'} = $self->{'maxrow'};
 }
 
 sub set_row_sources {
     my $self = shift;
+    $self->check_formulas_loaded();
     while (my ($cell,$value) = each(%{$self->{'formulas'}})) {
         next if ($cell !~ /^A(\d+)/ || $1 < 1);
         my $row = $1;
@@ -1707,6 +1765,7 @@ sub set_row_sources {
 
 sub set_row_numbers {
     my $self = shift;
+    $self->check_formulas_loaded();
     while (my ($cell,$value) = each(%{$self->{'formulas'}})) {
 	next if ($cell !~ /^A(\d+)$/);
         next if (! defined($value));
@@ -1741,11 +1800,9 @@ sub save {
         my $stype = $self->{'type'};
         my $cnum  = $self->{'cnum'};
         my $cdom  = $self->{'cdom'};
-        my $chome = $self->{'chome'};
         my $filename    = $self->{'filename'};
-        my $cachekey = join('_',($cnum,$cdom,$stype,$filename));
         # Cache new sheet
-        %{$spreadsheets{$cachekey}->{'formulas'}}=%f;
+	$self->cache_sheet(\%f);
         # Write sheet
         foreach (keys(%f)) {
             delete($f{$_}) if ($f{$_} eq 'import');
@@ -1761,14 +1818,14 @@ sub save {
                                 {'spreadsheet_default_'.$stype => $filename },
                                           $cdom,$cnum);
             return $reply if ($reply ne 'ok');
+	    &Apache::lonnet::appenv({'course.'.$self->{'cid'}.'.spreadsheet_default_'.
+				    $self->{'type'} => $self->filename()});
         } 
-        if ($self->is_default()) {
-            if ($self->{'type'} eq 'studentcalc') {
-                &Apache::lonnet::expirespread('','','studentcalc','');
-            } elsif ($self->{'type'} eq 'assesscalc') {
-                &Apache::lonnet::expirespread('','','assesscalc','');
-                &Apache::lonnet::expirespread('','','studentcalc','');
-            }
+	if ($self->{'type'} eq 'studentcalc') {
+	    &Apache::lonnet::expirespread('','','studentcalc','');
+	} elsif ($self->{'type'} eq 'assesscalc') {
+	    &Apache::lonnet::expirespread('','','assesscalc','');
+	    &Apache::lonnet::expirespread('','','studentcalc','');
         }
         return $reply;
     }
@@ -1790,8 +1847,8 @@ sub save_tmp {
         my %f = $self->formulas();
         while( my ($cell,$formula) = each(%f)) {
             next if ($formula eq 'import');
-            print $fh &Apache::lonnet::escape($cell)."=".
-                &Apache::lonnet::escape($formula)."\n";
+            print $fh &escape($cell)."=".
+                &escape($formula)."\n";
         }
         $fh->close();
     }
@@ -1809,8 +1866,8 @@ sub load_tmp {
         while (<$spreadsheet_file>) {
 	    chomp;
             my ($cell,$formula) = split(/=/);
-            $cell    = &Apache::lonnet::unescape($cell);
-            $formula = &Apache::lonnet::unescape($formula);
+            $cell    = &unescape($cell);
+            $formula = &unescape($formula);
             $formulas{$cell} = $formula;
         }
         $spreadsheet_file->close();
@@ -1853,14 +1910,13 @@ sub othersheets {
     my ($stype) = @_;
     $stype = $self->{'type'} if (! defined($stype) || $stype !~ /calc$/);
     #
-    my @alternatives=();
+    my @alternatives=(&mt('Default'), &mt('LON-CAPA Standard'));
     my %results=&Apache::lonnet::dump($stype.'_spreadsheets',
                                       $self->{'cdom'}, $self->{'cnum'});
     my ($tmp) = keys(%results);
-    if ($tmp =~ /^(con_lost|error|no_such_host)/i ) {
-        @alternatives = (&mt('Default'));
-    } else {
-        @alternatives = (&mt('Default'), sort (keys(%results)));
+    if (%results
+	&& $tmp !~ /^(con_lost|error|no_such_host)/i ) {
+        push(@alternatives, sort(keys(%results)));
     }
     return @alternatives; 
 }