--- loncom/interface/Attic/lonspreadsheet.pm	2000/12/11 16:51:12	1.12
+++ loncom/interface/Attic/lonspreadsheet.pm	2001/01/02 22:35:30	1.30
@@ -1,7 +1,9 @@
 # The LearningOnline Network with CAPA
 # Spreadsheet/Grades Display Handler
 #
-# 11/11,11/15,11/27,12/04,12/05,12/06,12/07,12/08,12/09,12/11 Gerd Kortemeyer
+# 11/11,11/15,11/27,12/04,12/05,12/06,12/07,
+# 12/08,12/09,12/11,12/12,12/15,12/16,12/18,12/19,12/30,
+# 01/01/01,02/01 Gerd Kortemeyer
 
 package Apache::lonspreadsheet;
 
@@ -11,14 +13,15 @@ use Safe::Hole;
 use Opcode;
 use Apache::lonnet;
 use Apache::Constants qw(:common :http);
-use HTML::TokeParser;
 use GDBM_File;
+use HTML::TokeParser;
 
 #
 # These cache hashes need to be independent of user, resource and course
-# (user and course are in the keys)
+# (user and course can/should be in the keys)
 #
-use vars qw(%spreadsheets %courserdatas %userrdatas);
+use vars qw(%spreadsheets %courserdatas %userrdatas %defaultsheets);
+
 #
 # These global hashes are dependent on user, course and resource, 
 # and need to be initialized every time when a sheet is calculated
@@ -26,9 +29,11 @@ use vars qw(%spreadsheets %courserdatas
 my %courseopt;
 my %useropt;
 my %parmhash;
-my $csec;
-my $uname;
-my $udom;
+
+# Stuff that only the screen handler can know
+
+my $includedir;
+my $tmpdir;
 
 # =============================================================================
 # ===================================== Implements an instance of a spreadsheet
@@ -59,8 +64,29 @@ sub initsheet {
 
 $maxrow=0;
 $sheettype='';
+
+# filename/reference of the sheet
+
 $filename='';
 
+# user data
+$uname='';
+$uhome='';
+$udom='';
+
+# course data
+
+$csec='';
+$chome='';
+$cnum='';
+$cdom='';
+$cid='';
+$cfn='';
+
+# symb
+
+$usymb='';
+
 sub mask {
     my ($lower,$upper)=@_;
 
@@ -259,9 +285,34 @@ sub SUMMIN {
 
 sub sett {
     %t=();
+    my $pattern='';
+    if ($sheettype eq 'assesscalc') {
+	$pattern='A';
+    } else {
+        $pattern='[A-Z]';
+    }
+    map {
+	if ($_=~/template\_(\w)/) {
+	  my $col=$1;
+          unless ($col=~/^$pattern/) {
+            map {
+	      if ($_=~/A(\d+)/) {
+		my $trow=$1;
+                if ($trow) {
+		    my $lb=$col.$trow;
+                    $t{$lb}=$f{'template_'.$col};
+                    $t{$lb}=~s/\#/$trow/g;
+                    $t{$lb}=~s/\.\.+/\,/g;
+                    $t{$lb}=~s/(^|[^\"\'])([A-Za-z]\d+)/$1\$v\{\'$2\'\}/g;
+                }
+	      }
+            } keys %f;
+	  }
+      }
+    } keys %f;
     map {
-	if ($f{$_}) {
-            if ($_=~/^A/) {
+	if (($f{$_}) && ($_!~/template\_/)) {
+            if ($_=~/^$pattern/) {
 	        unless ($f{$_}=~/^\!/) {
 		    $t{$_}=$c{$_};
                 }
@@ -272,6 +323,9 @@ sub sett {
             }
         }
     } keys %f;
+    $t{'A0'}=$f{'A0'};
+    $t{'A0'}=~s/\.\.+/\,/g;
+    $t{'A0'}=~s/(^|[^\"\'])([A-Za-z]\d+)/$1\$v\{\'$2\'\}/g;
 }
 
 sub calc {
@@ -299,6 +353,39 @@ sub calc {
     return '';
 }
 
+sub templaterow {
+    my @cols=();
+    $cols[0]='<b><font size=+1>Template</font></b>';
+    map {
+        my $fm=$f{'template_'.$_};
+        $fm=~s/[\'\"]/\&\#34;/g;
+        $cols[$#cols+1]="'template_$_','$fm'".'___eq___'.$fm;
+    } ('A','B','C','D','E','F','G','H','I','J','K','L','M',
+       'N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
+       'a','b','c','d','e','f','g','h','i','j','k','l','m',
+       'n','o','p','q','r','s','t','u','v','w','x','y','z');
+    return @cols;
+}
+
+sub outrowassess {
+    my $n=shift;
+    my @cols=();
+    if ($n) {
+       $cols[0]=$rl{$f{'A'.$n}};
+    } else {
+       $cols[0]='<b><font size=+1>Export</font></b>';
+    }
+    map {
+        my $fm=$f{$_.$n};
+        $fm=~s/[\'\"]/\&\#34;/g;
+        $cols[$#cols+1]="'$_$n','$fm'".'___eq___'.$v{$_.$n};
+    } ('A','B','C','D','E','F','G','H','I','J','K','L','M',
+       'N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
+       'a','b','c','d','e','f','g','h','i','j','k','l','m',
+       'n','o','p','q','r','s','t','u','v','w','x','y','z');
+    return @cols;
+}
+
 sub outrow {
     my $n=shift;
     my @cols=();
@@ -318,6 +405,15 @@ sub outrow {
     return @cols;
 }
 
+sub exportrowa {
+    my @exportarray=();
+    map {
+	$exportarray[$#exportarray+1]=$v{$_.'0'};
+    } ('A','B','C','D','E','F','G','H','I','J','K','L','M',
+       'N','O','P','Q','R','S','T','U','V','W','X','Y','Z');
+    return @exportarray;
+}
+
 # ------------------------------------------- End of "Inside of the safe space"
 ENDDEFS
     $safeeval->reval($code);
@@ -366,19 +462,13 @@ sub getformulas {
     return $safeeval->reval('%f');
 }
 
-# -------------------------------------------------------------------- Set type
-
-sub settype {
-    my ($safeeval,$type)=@_;
-    $safeeval->reval('$sheettype="'.$type.'";');
-}
-
 # -------------------------------------------------------------------- Get type
 
 sub gettype {
     my $safeeval=shift;
     return $safeeval->reval('$sheettype');
 }
+
 # ------------------------------------------------------------------ Set maxrow
 
 sub setmaxrow {
@@ -406,25 +496,137 @@ sub getfilename {
     my $safeeval=shift;
     return $safeeval->reval('$filename');
 }
-    
+
+# --------------------------------------------------------------- Get course ID
+
+sub getcid {
+    my $safeeval=shift;
+    return $safeeval->reval('$cid');
+}
+
+# --------------------------------------------------------- Get course filename
+
+sub getcfn {
+    my $safeeval=shift;
+    return $safeeval->reval('$cfn');
+}
+
+# ----------------------------------------------------------- Get course number
+
+sub getcnum {
+    my $safeeval=shift;
+    return $safeeval->reval('$cnum');
+}
+
+# ------------------------------------------------------------- Get course home
+
+sub getchome {
+    my $safeeval=shift;
+    return $safeeval->reval('$chome');
+}
+
+# ----------------------------------------------------------- Get course domain
+
+sub getcdom {
+    my $safeeval=shift;
+    return $safeeval->reval('$cdom');
+}
+
+# ---------------------------------------------------------- Get course section
+
+sub getcsec {
+    my $safeeval=shift;
+    return $safeeval->reval('$csec');
+}
+
+# --------------------------------------------------------------- Get user name
+
+sub getuname {
+    my $safeeval=shift;
+    return $safeeval->reval('$uname');
+}
+
+# ------------------------------------------------------------- Get user domain
+
+sub getudom {
+    my $safeeval=shift;
+    return $safeeval->reval('$udom');
+}
+
+# --------------------------------------------------------------- Get user home
+
+sub getuhome {
+    my $safeeval=shift;
+    return $safeeval->reval('$uhome');
+}
+
+# -------------------------------------------------------------------- Get symb
+
+sub getusymb {
+    my $safeeval=shift;
+    return $safeeval->reval('$usymb');
+}
+
+# ------------------------------------------------------------- Export of A-row
+
+sub exportdata {
+    my $safeeval=shift;
+    return $safeeval->reval('&exportrowa()');
+}
+
 # ========================================================== End of Spreadsheet
 # =============================================================================
 
-
+#
+# Procedures for screen output
+#
 # --------------------------------------------- Produce output row n from sheet
 
 sub rown {
     my ($safeeval,$n)=@_;
-    my $defaultbg=((($n-1)/5)==int(($n-1)/5))?'#E0E0':'#FFFF';
-    my $rowdata="\n<tr><td><b><font size=+1>$n</font></b></td>";
+    my $defaultbg;
+    my $rowdata='';
+    unless ($n eq '-') {
+       $defaultbg=((($n-1)/5)==int(($n-1)/5))?'#E0E0':'#FFFF';
+    } else {
+       $defaultbg='#E0FF';
+    }
+    if ((($n-1)/25)==int(($n-1)/25)) {
+        my $what='Student';
+        if (&gettype($safeeval) eq 'assesscalc') {
+	    $what='Item';
+	} elsif (&gettype($safeeval) eq 'studentcalc') {
+            $what='Assessment';
+        }
+	$rowdata.="</table>\n<br><table border=2>".
+        '<tr><td>&nbsp;<td>'.$what.'</td>';
+        map {
+           $rowdata.='<td>'.$_.'</td>';
+        } ('A','B','C','D','E','F','G','H','I','J','K','L','M',
+           'N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
+           'a','b','c','d','e','f','g','h','i','j','k','l','m',
+           'n','o','p','q','r','s','t','u','v','w','x','y','z');
+        $rowdata.='</tr>';
+    }
+    $rowdata.="\n<tr><td><b><font size=+1>$n</font></b></td>";
     my $showf=0;
+    my $proc;
+    my $maxred;
+    if (&gettype($safeeval) eq 'assesscalc') {
+        $proc='&outrowassess';
+        $maxred=1;
+    } else {
+        $proc='&outrow';
+        $maxred=26;
+    }
+    if ($n eq '-') { $proc='&templaterow'; $n=-1; }
     map {
        my $bgcolor=$defaultbg.((($showf-1)/5==int(($showf-1)/5))?'99':'DD');
        my ($fm,$vl)=split(/\_\_\_eq\_\_\_/,$_);
        if ($showf==0) { $vl=$_; }
-       if ($showf<=1) { $bgcolor='#FFDDDD'; }
+       if ($showf<=$maxred) { $bgcolor='#FFDDDD'; }
        if (($n==0) && ($showf<=26)) { $bgcolor='#CCCCFF'; } 
-       if ($showf>1) {
+       if (($showf>$maxred) || ((!$n) && ($showf>0))) {
 	   if ($vl eq '') {
 	       $vl='<font size=+2 color='.$bgcolor.'>&#35;</font>';
            }
@@ -435,70 +637,217 @@ sub rown {
            $rowdata.='<td bgcolor='.$bgcolor.'>&nbsp;'.$vl.'&nbsp;</td>';
        }
        $showf++;
-    } $safeeval->reval('&outrow('.$n.')');
+    } $safeeval->reval($proc.'('.$n.')');
     return $rowdata.'</tr>';
 }
 
 # ------------------------------------------------------------- Print out sheet
 
 sub outsheet {
-    my $safeeval=shift;
-    my $tabledata='<table border=2><tr><td colspan=2>&nbsp;</td>'.
-                  '<td bgcolor=#FFDDDD><b>A Import</b></td>';
+    my ($r,$safeeval)=@_;
+    my $maxred;
+    my $realm;
+    if (&gettype($safeeval) eq 'assesscalc') {
+        $maxred=1;
+        $realm='Assessment';
+    } elsif (&gettype($safeeval) eq 'studentcalc') {
+        $maxred=26;
+        $realm='User';
+    } else {
+        $maxred=26;
+        $realm='Course';
+    }
+    my $maxyellow=52-$maxred;
+    my $tabledata=
+        '<table border=2><tr><th colspan=2 rowspan=2><font size=+2>'.
+                  $realm.'</font></th>'.
+                  '<td bgcolor=#FFDDDD colspan='.$maxred.
+                  '><b><font size=+1>Import</font></b></td>'.
+                  '<td colspan='.$maxyellow.
+		  '><b><font size=+1>Calculations</font></b></td></tr><tr>';
+    my $showf=0;
     map {
-        $tabledata.="<td><b><font size=+1>$_</font></b></td>";
-    } ('B','C','D','E','F','G','H','I','J','K','L','M',
+        $showf++;
+        if ($showf<=$maxred) { 
+           $tabledata.='<td bgcolor="#FFDDDD">'; 
+        } else {
+           $tabledata.='<td>';
+        }
+        $tabledata.="<b><font size=+1>$_</font></b></td>";
+    } ('A','B','C','D','E','F','G','H','I','J','K','L','M',
        'N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
        'a','b','c','d','e','f','g','h','i','j','k','l','m',
        'n','o','p','q','r','s','t','u','v','w','x','y','z');
     $tabledata.='</tr>';
     my $row;
     my $maxrow=&getmaxrow($safeeval);
+    $tabledata.=&rown($safeeval,'-');
+    $r->print($tabledata);
     for ($row=0;$row<=$maxrow;$row++) {
-        $tabledata.=&rown($safeeval,$row);
+        $r->print(&rown($safeeval,$row));
     }
-    $tabledata.='</table>';
+    $r->print('</table>');
 }
 
+#
+# -------------------------------------- Read spreadsheet formulas for a course
+#
+
+sub readsheet {
+  my ($safeeval,$fn)=@_;
+  my $stype=&gettype($safeeval);
+  my $cnum=&getcnum($safeeval);
+  my $cdom=&getcdom($safeeval);
+  my $chome=&getchome($safeeval);
+
+# --------- There is no filename. Look for defaults in course and global, cache
+
+  unless($fn) {
+      unless ($fn=$defaultsheets{$cnum.'_'.$cdom.'_'.$stype}) {
+         $fn=&Apache::lonnet::reply('get:'.$cdom.':'.$cnum.
+                                    ':environment:spreadsheet_default_'.$stype,
+                                    $chome);
+         unless (($fn) && ($fn!~/^error\:/)) {
+	     $fn='default_'.$stype;
+         }
+         $defaultsheets{$cnum.'_'.$cdom.'_'.$stype}=$fn; 
+      }
+  }
 
+# ---------------------------------------------------------- fn now has a value
 
-# --------------------------------------- Read spreadsheet formulas from a file
+  &setfilename($safeeval,$fn);
 
-sub readsheet {
-    my ($safeeval,$fn)=@_;
-    &setfilename($safeeval,$fn);
-    $fn=~/\.(\w+)/;
-    &settype($safeeval,$1);
-    my %f=();
-    unless ($spreadsheets{$fn}) {
-       $spreadsheets{$fn}='';
+# ------------------------------------------------------ see if sheet is cached
+  my $fstring='';
+  if ($fstring=$spreadsheets{$cnum.'_'.$cdom.'_'.$stype.'_'.$fn}) {
+      &setformulas($safeeval,split(/\_\_\_\;\_\_\_/,$fstring));
+  } else {
+
+# ---------------------------------------------------- Not cached, need to read
+
+     my %f=();
+
+     if ($fn=~/^default\_/) {
+	my $sheetxml='';
        {
          my $fh;
-         if ($fh=Apache::File->new($fn)) {
-            $spreadsheets{$fn}=join('',<$fh>);
-         }
+         if ($fh=Apache::File->new($includedir.
+                         '/default.'.&gettype($safeeval))) {
+               $sheetxml=join('',<$fh>);
+          }
        }
+        my $parser=HTML::TokeParser->new(\$sheetxml);
+        my $token;
+        while ($token=$parser->get_token) {
+          if ($token->[0] eq 'S') {
+ 	     if ($token->[1] eq 'field') {
+ 		 $f{$token->[2]->{'col'}.$token->[2]->{'row'}}=
+ 		     $parser->get_text('/field');
+ 	     }
+             if ($token->[1] eq 'template') {
+                 $f{'template_'.$token->[2]->{'col'}}=
+                     $parser->get_text('/template');
+             }
+          }
+        }
+      } else {
+          my $sheet='';
+          my $reply=&Apache::lonnet::reply('dump:'.$cdom.':'.$cnum.':'.$fn,
+                                         $chome);
+          unless ($reply=~/^error\:/) {
+             $sheet=$reply;
+	  }
+          map {
+             my ($name,$value)=split(/\=/,$_);
+             $f{&Apache::lonnet::unescape($name)}=
+	        &Apache::lonnet::unescape($value);
+          } split(/\&/,$sheet);
+       }
+# --------------------------------------------------------------- Cache and set
+       $spreadsheets{$cnum.'_'.$cdom.'_'.$stype.'_'.$fn}=join('___;___',%f);  
+       &setformulas($safeeval,%f);
     }
-    {
-      my $parser=HTML::TokeParser->new(\$spreadsheets{$fn});
-      my $token;
-      while ($token=$parser->get_token) {
-         if ($token->[0] eq 'S') {
-	     if ($token->[1] eq 'field') {
-		 $f{$token->[2]->{'col'}.$token->[2]->{'row'}}=
-		     $parser->get_text('/field');
-	     }
-         }
+}
+
+# -------------------------------------------------------- Make new spreadsheet
+
+sub makenewsheet {
+    my ($uname,$udom,$stype,$usymb)=@_;
+    my $safeeval=initsheet();
+    $safeeval->reval(
+       '$uname="'.$uname.
+      '";$udom="'.$udom.
+      '";$uhome="'.&Apache::lonnet::homeserver($uname,$udom).
+      '";$sheettype="'.$stype.
+      '";$usymb="'.$usymb.
+      '";$csec="'.&Apache::lonnet::usection($udom,$uname,
+                                            $ENV{'request.course.id'}).
+      '";$cid="'.$ENV{'request.course.id'}.
+      '";$cfn="'.$ENV{'request.course.fn'}.
+      '";$cnum="'.$ENV{'course.'.$ENV{'request.course.id'}.'.num'}.
+      '";$cdom="'.$ENV{'course.'.$ENV{'request.course.id'}.'.domain'}.
+      '";$chome="'.$ENV{'course.'.$ENV{'request.course.id'}.'.home'}.'";');
+    return $safeeval;
+}
+
+# ------------------------------------------------------------ Save spreadsheet
+
+sub writesheet {
+  my ($safeeval,$makedef)=@_;
+  my $cid=&getcid($safeeval);
+  if (&Apache::lonnet::allowed('opa',$cid)) {
+    my %f=&getformulas($safeeval);
+    my $stype=&gettype($safeeval);
+    my $cnum=&getcnum($safeeval);
+    my $cdom=&getcdom($safeeval);
+    my $chome=&getchome($safeeval);
+    my $fn=&getfilename($safeeval);
+
+# ------------------------------------------------------------- Cache new sheet
+    $spreadsheets{$cnum.'_'.$cdom.'_'.$stype.'_'.$fn}=join('___;___',%f);    
+# ----------------------------------------------------------------- Write sheet
+    my $sheetdata='';
+    map {
+       $sheetdata.=&Apache::lonnet::escape($_).'='.
+	   &Apache::lonnet::escape($f{$_}).'&';
+    } keys %f;
+    $sheetdata=~s/\&$//;
+    my $reply=&Apache::lonnet::reply('put:'.$cdom.':'.$cnum.':'.$fn.':'.
+              $sheetdata,$chome);
+    if ($reply eq 'ok') {
+          $reply=&Apache::lonnet::reply('put:'.$cdom.':'.$cnum.':'.
+              $stype.'_spreadsheets:'.
+              &Apache::lonnet::escape($fn).'='.$ENV{'user.name'},
+              $chome);
+          if ($reply eq 'ok') {
+              if ($makedef) { 
+                return &Apache::lonnet::reply('put:'.$cdom.':'.$cnum.
+                                ':environment:spreadsheet_default_'.$stype.'='.
+                                &Apache::lonnet::escape($fn),
+                                $chome);
+	      } else {
+		  return $reply;
+    	      }
+	   } else {
+	       return $reply;
+           }
+      } else {
+	  return $reply;
       }
-    }
-    &setformulas($safeeval,%f);
+  }
+  return 'unauthorized';
 }
 
 # ----------------------------------------------- Make a temp copy of the sheet
+# "Modified workcopy" - interactive only
+#
 
 sub tmpwrite {
-    my ($safeeval,$tmpdir,$symb)=@_;
-    my $fn=$uname.'_'.$udom.'_spreadsheet_'.&getfilename($safeeval);
+    my $safeeval=shift;
+    my $fn=$ENV{'user.name'}.'_'.
+           $ENV{'user.domain'}.'_spreadsheet_'.&getusymb($safeeval).'_'.
+           &getfilename($safeeval);
     $fn=~s/\W/\_/g;
     $fn=$tmpdir.$fn.'.tmp';
     my $fh;
@@ -510,8 +859,10 @@ sub tmpwrite {
 # ---------------------------------------------------------- Read the temp copy
 
 sub tmpread {
-    my ($safeeval,$tmpdir,$symb,$nfield,$nform)=@_;
-    my $fn=$uname.'_'.$udom.'_spreadsheet_'.&getfilename($safeeval);
+    my ($safeeval,$nfield,$nform)=@_;
+    my $fn=$ENV{'user.name'}.'_'.
+           $ENV{'user.domain'}.'_spreadsheet_'.&getusymb($safeeval).'_'.
+           &getfilename($safeeval);
     $fn=~s/\W/\_/g;
     $fn=$tmpdir.$fn.'.tmp';
     my $fh;
@@ -525,42 +876,26 @@ sub tmpread {
             $fo{$name}=$value;
         }
     }
-    $fo{$nfield}=$nform;
+    if ($nfield) { $fo{$nfield}=$nform; }
     &setformulas($safeeval,%fo);
 }
 
-# --------------------------------------------------------------- Read metadata
-
-sub readmeta {
-    my $fn=shift;
-    unless ($fn=~/\.meta$/) { $fn.='meta'; }
-    my $content;
-    my %returnhash=();
-    {
-      my $fh=Apache::File->new($fn);
-      $content=join('',<$fh>);
-    }
-   my $parser=HTML::TokeParser->new(\$content);
-   my $token;
-   while ($token=$parser->get_token) {
-      if ($token->[0] eq 'S') {
-         my $entry=$token->[1];
-         if (($entry eq 'stores') || ($entry eq 'parameter')) {
-             my $unikey=$entry;
-             $unikey.='_'.$token->[2]->{'part'}; 
-             $unikey.='_'.$token->[2]->{'name'}; 
-             $returnhash{$unikey}=$token->[2]->{'display'};
-         }
-     }
-  }
-    return %returnhash;
-}
-
 # ================================================================== Parameters
 # -------------------------------------------- Figure out a cascading parameter
+#
+# For this function to work
+#
+# * parmhash needs to be tied
+# * courseopt and useropt need to be initialized for this user and course
+#
 
 sub parmval {
-    my ($what,$symb)=@_;
+    my ($what,$safeeval)=@_;
+    my $cid=&getcid($safeeval);
+    my $csec=&getcsec($safeeval);
+    my $uname=&getuname($safeeval);
+    my $udom=&getudom($safeeval);
+    my $symb=&getusymb($safeeval);
 
     unless ($symb) { return ''; }
     my $result='';
@@ -573,23 +908,24 @@ sub parmval {
 
        my $symbparm=$symb.'.'.$what;
        my $mapparm=$mapname.'___(all).'.$what;
+       my $usercourseprefix=$uname.'_'.$udom.'_'.$cid;
 
        my $seclevel=
-            $ENV{'request.course.id'}.'.['.
+            $usercourseprefix.'.['.
 		$csec.'].'.$what;
        my $seclevelr=
-            $ENV{'request.course.id'}.'.['.
+            $usercourseprefix.'.['.
 		$csec.'].'.$symbparm;
        my $seclevelm=
-            $ENV{'request.course.id'}.'.['.
+            $usercourseprefix.'.['.
 		$csec.'].'.$mapparm;
 
        my $courselevel=
-            $ENV{'request.course.id'}.'.'.$what;
+            $usercourseprefix.'.'.$what;
        my $courselevelr=
-            $ENV{'request.course.id'}.'.'.$symbparm;
+            $usercourseprefix.'.'.$symbparm;
        my $courselevelm=
-            $ENV{'request.course.id'}.'.'.$mapparm;
+            $usercourseprefix.'.'.$mapparm;
 
 # ---------------------------------------------------------- fourth, check user
       
@@ -632,10 +968,92 @@ sub parmval {
         
 }
 
+# ---------------------------------------------- Update rows for course listing
+
+sub updateclasssheet {
+    my $safeeval=shift;
+    my $cnum=&getcnum($safeeval);
+    my $cdom=&getcdom($safeeval);
+    my $cid=&getcid($safeeval);
+    my $chome=&getchome($safeeval);
+
+# ---------------------------------------------- Read class list and row labels
+
+    my $classlst=&Apache::lonnet::reply
+                                 ('dump:'.$cdom.':'.$cnum.':classlist',$chome);
+    my %currentlist=();
+    my $now=time;
+    unless ($classlst=~/^error\:/) {
+        map {
+            my ($name,$value)=split(/\=/,$_);
+            my ($end,$start)=split(/\:/,&Apache::lonnet::unescape($value));
+            my $active=1;
+            if (($end) && ($now>$end)) { $active=0; }
+            if ($active) {
+                my $rowlabel='';
+                $name=&Apache::lonnet::unescape($name);
+                my ($sname,$sdom)=split(/\:/,$name);
+                my $ssec=&Apache::lonnet::usection($sdom,$sname,$cid);
+                if ($ssec==-1) {
+                    $rowlabel='<font color=red>Data not available: '.$name.
+			      '</font>';
+                } else {
+                    my %reply=&Apache::lonnet::idrget($sdom,$sname);
+                    my $reply=&Apache::lonnet::reply('get:'.$sdom.':'.$sname.
+		      ':environment:firstname&middlename&lastname&generation',
+                      &Apache::lonnet::homeserver($sname,$sdom));
+                    $rowlabel=$ssec.'&nbsp;'.$reply{$sname}.'<br>';
+                    map {
+                        $rowlabel.=&Apache::lonnet::unescape($_).' ';
+                    } split(/\&/,$reply);
+                }
+		$currentlist{&Apache::lonnet::unescape($name)}=$rowlabel;
+            }
+        } split(/\&/,$classlst);
+#
+# -------------------- Find discrepancies between the course row table and this
+#
+        my %f=&getformulas($safeeval);
+        my $changed=0;
 
-# ----------------------------------------------------------------- Update rows
+        my $maxrow=0;
+        my %existing=();
 
-sub updaterows {
+# ----------------------------------------------------------- Now obsolete rows
+	map {
+	    if ($_=~/^A(\d+)/) {
+                $maxrow=($1>$maxrow)?$1:$maxrow;
+                $existing{$f{$_}}=1;
+		unless ((defined($currentlist{$f{$_}})) || (!$1)) {
+		   $f{$_}='!!! Obsolete';
+                   $changed=1;
+                }
+            }
+        } keys %f;
+
+# -------------------------------------------------------- New and unknown keys
+     
+        map {
+            unless ($existing{$_}) {
+		$changed=1;
+                $maxrow++;
+                $f{'A'.$maxrow}=$_;
+            }
+        } sort keys %currentlist;        
+     
+        if ($changed) { &setformulas($safeeval,%f); }
+
+        &setmaxrow($safeeval,$maxrow);
+        &setrowlabels($safeeval,%currentlist);
+
+    } else {
+        return 'Could not access course data';
+    }
+}
+
+# ----------------------------------- Update rows for student and assess sheets
+
+sub updatestudentassesssheet {
     my $safeeval=shift;
     my %bighash;
 # -------------------------------------------------------------------- Tie hash
@@ -704,7 +1122,7 @@ sub updaterows {
 	    if ($_=~/^A(\d+)/) {
                 $maxrow=($1>$maxrow)?$1:$maxrow;
                 $existing{$f{$_}}=1;
-		unless (defined($current{$f{$_}})) {
+		unless ((defined($current{$f{$_}})) || (!$1)) {
 		   $f{$_}='!!! Obsolete';
                    $changed=1;
                 }
@@ -733,15 +1151,92 @@ sub updaterows {
 
 # ------------------------------------------------ Load data for one assessment
 
-sub rowaassess {
-    my ($safeeval,$symb)=@_;
-    my $uhome=&Apache::lonnet::homeserver($uname,$udom);
+sub loadstudent {
+    my $safeeval=shift;
+    my %c=();
+    my %f=&getformulas($safeeval);
+    map {
+	if ($_=~/^A(\d+)/) {
+	   my $row=$1;
+           unless ($f{$_}=~/^\!/) {
+	      my @assessdata=&exportsheet(&getuname($safeeval),
+                                          &getudom($safeeval),
+                                          'assesscalc',$f{$_});
+              my $index=0;
+              map {
+                  if ($assessdata[$index]) {
+		     $c{$_.$row}=$assessdata[$index];
+                     unless ($_ eq 'A') { 
+			 $f{$_.$row}='import';
+                     }
+		  }
+                  $index++;
+              } ('A','B','C','D','E','F','G','H','I','J','K','L','M',
+                 'N','O','P','Q','R','S','T','U','V','W','X','Y','Z');
+	   }
+        }
+    } keys %f;
+    &setformulas($safeeval,%f);
+    &setconstants($safeeval,%c);
+}
+
+# --------------------------------------------------- Load data for one student
+
+sub loadcourse {
+    my $safeeval=shift;
+    my %c=();
+    my %f=&getformulas($safeeval);
+    map {
+	if ($_=~/^A(\d+)/) {
+	   my $row=$1;
+           unless (($f{$_}=~/^\!/)
+
+|| ($row>5))
+
+ {
+	      my @studentdata=&exportsheet(&getuname($safeeval),
+                                           &getudom($safeeval),
+                                           'studentcalc');
+              my $index=0;
+              map {
+                  if ($studentdata[$index]) {
+		     $c{$_.$row}=$studentdata[$index];
+                     unless ($_ eq 'A') { 
+			 $f{$_.$row}='import';
+                     }
+		  }
+                  $index++;
+              } ('A','B','C','D','E','F','G','H','I','J','K','L','M',
+                 'N','O','P','Q','R','S','T','U','V','W','X','Y','Z');
+	   }
+        }
+    } keys %f;
+    &setformulas($safeeval,%f);
+    &setconstants($safeeval,%c);
+}
+
+# ------------------------------------------------ Load data for one assessment
+
+sub loadassessment {
+    my $safeeval=shift;
+
+    my $uhome=&getuhome($safeeval);
+    my $uname=&getuname($safeeval);
+    my $udom=&getudom($safeeval);
+    my $symb=&getusymb($safeeval);
+    my $cid=&getcid($safeeval);
+    my $cnum=&getcnum($safeeval);
+    my $cdom=&getcdom($safeeval);
+    my $chome=&getchome($safeeval);
+
     my $namespace;
-    unless ($namespace=$ENV{'request.course.id'}) { return ''; }
+    unless ($namespace=$cid) { return ''; }
 
 # ----------------------------------------------------------- Get stored values
     my $answer=&Apache::lonnet::reply(
-       "restore:$udom:$uname:$namespace:$symb",$uhome);
+       "restore:$udom:$uname:".
+       &Apache::lonnet::escape($namespace).":".
+       &Apache::lonnet::escape($symb),$uhome);
     my %returnhash=();
     map {
 	my ($name,$value)=split(/\=/,$_);
@@ -759,54 +1254,67 @@ sub rowaassess {
 # ---------------------------- initialize coursedata and userdata for this user
     %courseopt=();
     %useropt=();
-    my $uhome=&Apache::lonnet::homeserver($uname,$udom);
+
+    my $userprefix=$uname.'_'.$udom.'_';
+
     unless ($uhome eq 'no_host') { 
 # -------------------------------------------------------------- Get coursedata
-      unless ($courserdatas{$ENV{'request.course.id'}}) {
-         my $reply=&Apache::lonnet::reply('dump:'.
-              $ENV{'course.'.$ENV{'request.course.id'}.'.domain'}.':'.
-              $ENV{'course.'.$ENV{'request.course.id'}.'.num'}.':resourcedata',
-              $ENV{'course.'.$ENV{'request.course.id'}.'.home'});
+      unless
+        ((time-$courserdatas{$cid.'.last_cache'})<120) {
+         my $reply=&Apache::lonnet::reply('dump:'.$cdom.':'.$cnum.
+              ':resourcedata',$chome);
          if ($reply!~/^error\:/) {
-            $courserdatas{$ENV{'request.course.id'}}=$reply;
+            $courserdatas{$cid}=$reply;
+            $courserdatas{$cid.'.last_cache'}=time;
          }
       }
       map {
          my ($name,$value)=split(/\=/,$_);
-         $courseopt{&Apache::lonnet::unescape($name)}=
+         $courseopt{$userprefix.&Apache::lonnet::unescape($name)}=
                     &Apache::lonnet::unescape($value);  
       } split(/\&/,$courserdatas{$ENV{'request.course.id'}});
 # --------------------------------------------------- Get userdata (if present)
-      unless ($userrdatas{$uname.'___'.$udom}) {
+      unless
+        ((time-$userrdatas{$uname.'___'.$udom.'.last_cache'})<120) {
          my $reply=
        &Apache::lonnet::reply('dump:'.$udom.':'.$uname.':resourcedata',$uhome);
          if ($reply!~/^error\:/) {
 	     $userrdatas{$uname.'___'.$udom}=$reply;
+	     $userrdatas{$uname.'___'.$udom.'.last_cache'}=time;
          }
       }
       map {
          my ($name,$value)=split(/\=/,$_);
-         $useropt{&Apache::lonnet::unescape($name)}=
-                  &Apache::lonnet::unescape($value);
+         $useropt{$userprefix.&Apache::lonnet::unescape($name)}=
+	          &Apache::lonnet::unescape($value);
       } split(/\&/,$userrdatas{$uname.'___'.$udom});
-   }
-# -- now courseopt, useropt initialized for this user and course (used parmval)
+    }
 
-    my %c=();
+# ----------------- now courseopt, useropt initialized for this user and course
+# (used by parmval)
+
+   my %c=();
+
+   if (tie(%parmhash,'GDBM_File',
+           &getcfn($safeeval).'_parms.db',&GDBM_READER,0640)) {
     my %f=&getformulas($safeeval);
     map {
 	if ($_=~/^A/) {
             unless ($f{$_}=~/^\!/) {
   	       if ($f{$_}=~/^parameter/) {
-	          $c{$_}=&parmval($f{$_},$symb);
+	          $c{$_}=&parmval($f{$_},$safeeval);
 	       } else {
- 	          $c{$_}=$returnhash{$f{$_}};
+		  my $key=$f{$_};
+                  $key=~s/^stores\_/resource\./;
+                  $key=~s/\_/\./;
+ 	          $c{$_}=$returnhash{$key};
 	       }
 	   }
         }
     } keys %f;
-
-    &setconstants($safeeval,%c);
+    untie(%parmhash);
+   }
+   &setconstants($safeeval,%c);
 }
 
 # --------------------------------------------------------- Various form fields
@@ -833,34 +1341,86 @@ sub selectbox {
     return $selout.'</select>';
 }
 
-# ==================================== Sub handler to get export of assessments
+# =============================================== Update information in a sheet
+#
+# Add new users or assessments, etc.
+#
+
+sub updatesheet {
+    my $safeeval=shift;
+    my $stype=&gettype($safeeval);
+    if ($stype eq 'classcalc') {
+	return &updateclasssheet($safeeval);
+    } else {
+        return &updatestudentassesssheet($safeeval);
+    }
+}
+
+# =================================================== Load the rows for a sheet
+#
+# Import the data for rows
+#
+
+sub loadrows() {
+    my $safeeval=shift;
+    my $stype=&gettype($safeeval);
+    if ($stype eq 'classcalc') {
+	&loadcourse($safeeval);
+    } elsif ($stype eq 'studentcalc') {
+        &loadstudent($safeeval);
+    } else {
+        &loadassessment($safeeval);
+    }
+}
+
+# ============================================================== Export handler
+#
+# Non-interactive call from with program
+#
+
+sub exportsheet {
+    my ($uname,$udom,$stype,$usymb,$fn)=@_;
+    my $thissheet=&makenewsheet($uname,$udom,$stype,$usymb);
+    &readsheet($thissheet,$fn);
+    &updatesheet($thissheet);
+    &loadrows($thissheet);
+    &calcsheet($thissheet);
+    return &exportdata($thissheet);
+}
 
 # ================================================================ Main handler
+#
+# Interactive call to screen
+#
+#
+
 
 sub handler {
     my $r=shift;
 
-    $uname='';
-    $udom='';
-    $csec='';
-
-   if ($r->header_only) {
+    if ($r->header_only) {
       $r->content_type('text/html');
       $r->send_http_header;
       return OK;
-   }
+    }
+
+# ---------------------------------------------------- Global directory configs
+
+$includedir=$r->dir_config('lonIncludes');
+$tmpdir=$r->dir_config('lonDaemons').'/tmp/';
 
 # ----------------------------------------------------- Needs to be in a course
 
-  if (($ENV{'request.course.fn'}) || 
-      ($ENV{'request.state'} eq 'construct')) { 
+  if ($ENV{'request.course.fn'}) { 
 
 # --------------------------- Get query string for limited number of parameters
+
     map {
        my ($name, $value) = split(/=/,$_);
        $value =~ tr/+/ /;
        $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C",hex($1))/eg;
-       if (($name eq 'uname') || ($name eq 'udom') || ($name eq 'usymb')) {
+       if (($name eq 'uname') || ($name eq 'udom') || 
+           ($name eq 'usymb') || ($name eq 'ufn')) {
            unless ($ENV{'form.'.$name}) {
               $ENV{'form.'.$name}=$value;
 	   }
@@ -869,13 +1429,17 @@ sub handler {
 
 # ------------------------------------------- Nothing there? Must be login user
 
+    my $aname;
+    my $adom;
+
     unless ($ENV{'form.uname'}) {
-	$uname=$ENV{'user.name'};
-        $udom=$ENV{'user.domain'};
+	$aname=$ENV{'user.name'};
+        $adom=$ENV{'user.domain'};
     } else {
-        $uname=$ENV{'form.uname'};
-        $udom=$ENV{'form.udom'};
+        $aname=$ENV{'form.uname'};
+        $adom=$ENV{'form.udom'};
     }
+
 # ------------------------------------------------------------------- Open page
 
     $r->content_type('text/html');
@@ -883,6 +1447,8 @@ sub handler {
     $r->header_out('Pragma','no-cache');
     $r->send_http_header;
 
+# --------------------------------------------------------------- Screen output
+
     $r->print('<html><head><title>LON-CAPA Spreadsheet</title>');
     $r->print(<<ENDSCRIPT);
 <script language="JavaScript">
@@ -899,56 +1465,112 @@ sub handler {
 </script>
 ENDSCRIPT
     $r->print('</head><body bgcolor="#FFFFFF">'.
+       '<img align=right src=/adm/lonIcons/lonlogos.gif>'.
+       '<h1>LON-CAPA Spreadsheet</h1>'.
        '<form action="'.$r->uri.'" name=sheet method=post>'.
        &hiddenfield('uname',$ENV{'form.uname'}).
        &hiddenfield('udom',$ENV{'form.udom'}).
        &hiddenfield('usymb',$ENV{'form.usymb'}).
        &hiddenfield('unewfield','').
        &hiddenfield('unewformula',''));
-    my $sheetone=initsheet();
+
+# ---------------------- Make sure that this gets out, even if user hits "stop"
+
+    $r->rflush();
+
+# ---------------------------------------- Read new sheet or modified worksheet
+
+    $r->uri=~/\/(\w+)$/;
+
+    my $asheet=&makenewsheet($aname,$adom,$1,$ENV{'form.usymb'});
+
+# ------------------------ If a new formula had been entered, go from work copy
+
     if ($ENV{'form.unewfield'}) {
         $r->print('<h2>Modified Workcopy</h2>');
         $ENV{'form.unewformula'}=~s/\'/\"/g;
-        $r->print('New formula: '.$ENV{'form.unewfield'}.'='.
-                  $ENV{'form.unewformula'}.'<br>');
-	&tmpread($sheetone,$r->dir_config('lonDaemons').'/tmp/',
-                 $ENV{'form.usymb'},
+        $r->print('<p>New formula: '.$ENV{'form.unewfield'}.'='.
+                  $ENV{'form.unewformula'}.'<p>');
+        &setfilename($asheet,$ENV{'form.ufn'});
+	&tmpread($asheet,
                  $ENV{'form.unewfield'},$ENV{'form.unewformula'});
-        &setfilename($sheetone,$r->filename);
-        $r->filename=~/\.(\w+)/;
-        &settype($sheetone,$1);
-    } else {
-        &readsheet($sheetone,$r->filename);
-    }
-    if (tie(%parmhash,'GDBM_File',
-       $ENV{'request.course.fn'}.'_parms.db',&GDBM_READER,0640)) {
-       $csec=&Apache::lonnet::usection($udom,$uname,$ENV{'request.course.id'});
-       if ($csec eq '-1') {
-          $r->print('<h3><font color=red>'.
-   "User '$uname' at domain '$udom' not a student in this course</font></h3>");
-       }
-       &updaterows($sheetone);
-       untie(%parmhash);
-   } else {
-       $r->print('<h3><font color=red>'.
-	   'Could not initialize import fields (not in a course)</font></h3>');
-   }
-   &tmpwrite($sheetone,$r->dir_config('lonDaemons').'/tmp/',
-              $ENV{'form.usymb'});
-    $r->print("<b>User '$uname' at domain '$udom' for '".
-              $ENV{'course.'.$ENV{'request.course.id'}.'.description'}."'");
-    if ($csec) {
-       $r->print(", group/section '$csec'");
-    }
-    $r->print("</b>\n");
-    if (&gettype($sheetone) eq 'assesscalc') {
-	&rowaassess($sheetone,$ENV{'form.usymb'});
+
+     } elsif ($ENV{'form.saveas'}) {
+        &setfilename($asheet,$ENV{'form.ufn'});
+	&tmpread($asheet);
+    } else {
+        &readsheet($asheet,$ENV{'form.ufn'});
+    }
+
+# -------------------------------------------------- Print out user information
+
+    unless (&gettype($asheet) eq 'classcalc') {
+        $r->print('<p><b>User:</b> '.&getuname($asheet).
+                  '<br><b>Domain:</b> '.&getudom($asheet));
+        if (&getcsec($asheet) eq '-1') {
+           $r->print('<h3><font color=red>'.
+                     'Not a student in this course</font></h3>');
+        } else {
+           $r->print('<br><b>Section/Group:</b> '.&getcsec($asheet));
+        }
+    }
+
+# ---------------------------------------------------------------- Course title
+
+    $r->print('<h1>'.
+            $ENV{'course.'.$ENV{'request.course.id'}.'.description'}.'</h1>');
+
+
+# ---------------------------------------------------- See if something to save
+
+    if (&Apache::lonnet::allowed('opa',$ENV{'request.course.id'})) {
+        my $fname='';
+	if ($ENV{'form.saveas'} && ($fname=$ENV{'form.newfn'})) {
+            $fname=~s/\W/\_/g;
+            if ($fname eq 'default') { $fname='course_default'; }
+            $fname.='_'.&gettype($asheet);
+            &setfilename($asheet,$fname);
+            $ENV{'form.ufn'}=$fname;
+	    $r->print('<p>Saving spreadsheet: '.
+                         &writesheet($asheet,$ENV{'form.makedefufn'}).'<p>');
+	}
     }
-    &calcsheet($sheetone);
-    $r->print(&outsheet($sheetone));
 
+# ------------------------------------------------ Write the modified worksheet
+
+   $r->print('<b>Current sheet:</b> '.&getfilename($asheet).'<p>');
+
+   &tmpwrite($asheet);
+
+# ----------------------------------------------------------------- Save dialog
+
+
+    if (&Apache::lonnet::allowed('opa',$ENV{'request.course.id'})) {
+        my $fname=$ENV{'form.ufn'};
+        $fname=~s/\_[^\_]+$//;
+        if ($fname eq 'default') { $fname='course_default'; }
+        $r->print('<input type=submit name=saveas value="Save as ...">'.
+              '<input type=text size=20 name=newfn value="'.$fname.
+              '"> (make default: <input type=checkbox name="makedefufn">)<p>');
+    }
+
+    $r->print(&hiddenfield('ufn',&getfilename($asheet)));
+
+
+# ----------------------------------------------------- Update sheet, load rows
+
+    &updatesheet($asheet);
+    &loadrows($asheet);
+
+
+    my $calcoutput=&calcsheet($asheet);
+    $r->print('<h3><font color=red>'.$calcoutput.'</h3></font>');
+
+    &outsheet($r,$asheet);
     $r->print('</form></body></html>');
 
+
+# ------------------------------------------------------------------------ Done
   } else {
 # ----------------------------- Not in a course, or not allowed to modify parms
       $ENV{'user.error.msg'}=
@@ -956,6 +1578,7 @@ ENDSCRIPT
       return HTTP_NOT_ACCEPTABLE; 
   }
     return OK;
+
 }
 
 1;