# # $Id: lonspreadsheet.pm,v 1.142 2002/11/19 19:20:50 matthew Exp $ # # Copyright Michigan State University Board of Trustees # # This file is part of the LearningOnline Network with CAPA (LON-CAPA). # # LON-CAPA is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # LON-CAPA is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with LON-CAPA; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # /home/httpd/html/adm/gpl.txt # # http://www.lon-capa.org/ # # The LearningOnline Network with CAPA # Spreadsheet/Grades Display Handler # # POD required stuff: =head1 NAME lonspreadsheet =head1 SYNOPSIS Spreadsheet interface to internal LON-CAPA data =head1 DESCRIPTION Lonspreadsheet provides course coordinators the ability to manage their students grades online. The students are able to view their own grades, but not the grades of their peers. The spreadsheet is highly customizable, offering the ability to use Perl code to manipulate data, as well as many built-in functions. =head2 Functions available to user of lonspreadsheet =over 4 =cut package Apache::lonspreadsheet; use strict; use Apache::Constants qw(:common :http); use Apache::lonnet; use Apache::lonhtmlcommon; use Apache::loncoursedata; use Apache::File(); use Safe; use Safe::Hole; use Opcode; use GDBM_File; use HTML::TokeParser; use Spreadsheet::WriteExcel; # # Caches for coursewide information # my %Section; # # Caches for previously calculated spreadsheets # my %oldsheets; my %loadedcaches; my %expiredates; # # Cache for stores of an individual user # my $cachedassess; my %cachedstores; # # These cache hashes need to be independent of user, resource and course # (user and course can/should be in the keys) # my %spreadsheets; my %courserdatas; my %userrdatas; my %defaultsheets; my %rowlabel_cache; # # These global hashes are dependent on user, course and resource, # and need to be initialized every time when a sheet is calculated # my %courseopt; my %useropt; my %parmhash; # # Some hashes for stats on timing and performance # my %starttimes; my %usedtimes; my %numbertimes; # Stuff that only the screen handler can know my $includedir; my $tmpdir; # ============================================================================= # ===================================== Implements an instance of a spreadsheet ## ## mask - used to reside in the safe space. ## sub mask { my ($lower,$upper)=@_; $upper = $lower if (! defined($upper)); # my ($la,$ld) = ($lower=~/([A-Za-z]|\*)(\d+|\*)/); my ($ua,$ud) = ($upper=~/([A-Za-z]|\*)(\d+|\*)/); # my $alpha=''; my $num=''; # if (($la eq '*') || ($ua eq '*')) { $alpha='[A-Za-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+'; } 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].']'; } } } } return '^'.$alpha.$num."\$"; } sub initsheet { 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(\&Apache::lonspreadsheet::mask,$safeeval,'&mask'); $safeeval->share('$@'); my $code=<<'ENDDEFS'; # ---------------------------------------------------- Inside of the safe space # # f: formulas # t: intermediate format (variable references expanded) # v: output values # 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 %f; # Holds the formulas for each cell. This is the users # (spreadsheet authors) data for each cell. # set by &setformulas and returned by &getformulas # &setformulas is called by &readsheet, &tmpread, &updateclasssheet, # &updatestudentassesssheet, &loadstudent, &loadcourse # &getformulas is called by &writesheet, &tmpwrite, &updateclasssheet, # &updatestudentassesssheet, &loadstudent, &loadcourse, &loadassessment, undef %c; # Holds the constants for a sheet. In the assessment # sheets, this is the A column. Used in &MINPARM, &MAXPARM, &expandnamed, # &sett, and &setconstants. There is no &getconstants. # &setconstants is called by &loadstudent, &loadcourse, &load assessment, undef @os; # Holds the names of other spreadsheets - this is used to specify # the spreadsheets that are available for the assessment sheet. # Set by &setothersheets. &setothersheets is called by &handler. A # related subroutine is &othersheets. #$errorlog = ''; $maxrow = 0; $sheettype = ''; # filename/reference of the sheet $filename = ''; # user data $uname = ''; $uhome = ''; $udom = ''; # course data $csec = ''; $chome= ''; $cnum = ''; $cdom = ''; $cid = ''; $coursefilename = ''; # symb $usymb = ''; # error messages $errormsg = ''; #------------------------------------------------------- =item UWCALC(hashname,modules,units,date) returns the proportion of the module weights not previously completed by the student. =over 4 =item hashname name of the hash the module dates have been inserted into =item modules reference to a cell which contains a comma deliminated list of modules covered by the assignment. =item units reference to a cell which contains a comma deliminated list of module weights with respect to the assignment =item date reference to a cell which contains the date the assignment was completed. =back =cut #------------------------------------------------------- sub UWCALC { my ($hashname,$modules,$units,$date) = @_; my @Modules = split(/,/,$modules); my @Units = split(/,/,$units); my $total_weight; foreach (@Units) { $total_weight += $_; } my $usum=0; for (my $i=0; $i<=$#Modules; $i++) { if (&HASH($hashname,$Modules[$i]) eq $date) { $usum += $Units[$i]; } } return $usum/$total_weight; } #------------------------------------------------------- =item CDLSUM(list) returns the sum of the elements in a cell which contains a Comma Deliminate List of numerical values. 'list' is a reference to a cell which contains a comma deliminated list. =cut #------------------------------------------------------- sub CDLSUM { my ($list)=@_; my $sum; foreach (split/,/,$list) { $sum += $_; } return $sum; } #------------------------------------------------------- =item CDLITEM(list,index) returns the item at 'index' in a Comma Deliminated List. =over 4 =item list reference to a cell which contains a comma deliminated list. =item index the Perl index of the item requested (first element in list has an index of 0) =back =cut #------------------------------------------------------- sub CDLITEM { my ($list,$index)=@_; my @Temp = split/,/,$list; return $Temp[$index]; } #------------------------------------------------------- =item CDLHASH(name,key,value) loads a comma deliminated list of keys into the hash 'name', all with a value of 'value'. =over 4 =item name name of the hash. =item key (a pointer to) a comma deliminated list of keys. =item value a single value to be entered for each key. =back =cut #------------------------------------------------------- sub CDLHASH { my ($name,$key,$value)=@_; my @Keys; my @Values; # Check to see if we have multiple $key values if ($key =~ /[A-z](\-[A-z])?\d+(\-\d+)?/) { my $keymask = &mask($key); # Assume the keys are addresses my @Temp = grep /$keymask/,keys(%sheet_values); @Keys = $sheet_values{@Temp}; } else { $Keys[0]= $key; } my @Temp; foreach $key (@Keys) { @Temp = (@Temp, split/,/,$key); } @Keys = @Temp; if ($value =~ /[A-z](\-[A-z])?\d+(\-\d+)?/) { my $valmask = &mask($value); my @Temp = grep /$valmask/,keys(%sheet_values); @Values =$sheet_values{@Temp}; } else { $Values[0]= $value; } $value = $Values[0]; # Add values to hash for (my $i = 0; $i<=$#Keys; $i++) { my $key = $Keys[$i]; if (! exists ($hashes{$name}->{$key})) { $hashes{$name}->{$key}->[0]=$value; } else { my @Temp = sort(@{$hashes{$name}->{$key}},$value); $hashes{$name}->{$key} = \@Temp; } } return "hash '$name' updated"; } #------------------------------------------------------- =item GETHASH(name,key,index) returns the element in hash 'name' reference by the key 'key', at index 'index' in the values list. =cut #------------------------------------------------------- sub GETHASH { my ($name,$key,$index)=@_; if (! defined($index)) { $index = 0; } if ($key =~ /^[A-z]\d+$/) { $key = $sheet_values{$key}; } return $hashes{$name}->{$key}->[$index]; } #------------------------------------------------------- =item CLEARHASH(name) clears all the values from the hash 'name' =item CLEARHASH(name,key) clears all the values from the hash 'name' associated with the given key. =cut #------------------------------------------------------- sub CLEARHASH { my ($name,$key)=@_; if (defined($key)) { if (exists($hashes{$name}->{$key})) { $hashes{$name}->{$key}=undef; return "hash '$name' key '$key' cleared"; } } else { if (exists($hashes{$name})) { $hashes{$name}=undef; return "hash '$name' cleared"; } } return "Error in clearing hash"; } #------------------------------------------------------- =item HASH(name,key,value) loads values into an internal hash. If a key already has a value associated with it, the values are sorted numerically. =item HASH(name,key) returns the 0th value in the hash 'name' associated with 'key'. =cut #------------------------------------------------------- sub HASH { my ($name,$key,$value)=@_; my @Keys; undef @Keys; my @Values; # Check to see if we have multiple $key values if ($key =~ /[A-z](\-[A-z])?\d+(\-\d+)?/) { my $keymask = &mask($key); # Assume the keys are addresses my @Temp = grep /$keymask/,keys(%sheet_values); @Keys = $sheet_values{@Temp}; } else { $Keys[0]= $key; } # If $value is empty, return the first value associated # with the first key. if (! $value) { return $hashes{$name}->{$Keys[0]}->[0]; } # Check to see if we have multiple $value(s) if ($value =~ /[A-z](\-[A-z])?\d+(\-\d+)?/) { my $valmask = &mask($value); my @Temp = grep /$valmask/,keys(%sheet_values); @Values =$sheet_values{@Temp}; } else { $Values[0]= $value; } # Add values to hash for (my $i = 0; $i<=$#Keys; $i++) { my $key = $Keys[$i]; my $value = ($i<=$#Values ? $Values[$i] : $Values[0]); if (! exists ($hashes{$name}->{$key})) { $hashes{$name}->{$key}->[0]=$value; } else { my @Temp = sort(@{$hashes{$name}->{$key}},$value); $hashes{$name}->{$key} = \@Temp; } } return $Values[-1]; } #------------------------------------------------------- =item NUM(range) returns the number of items in the range. =cut #------------------------------------------------------- sub NUM { my $mask=mask(@_); my $num= $#{@{grep(/$mask/,keys(%sheet_values))}}+1; return $num; } sub BIN { my ($low,$high,$lower,$upper)=@_; my $mask=mask($lower,$upper); my $num=0; foreach (grep /$mask/,keys(%sheet_values)) { if (($sheet_values{$_}>=$low) && ($sheet_values{$_}<=$high)) { $num++; } } return $num; } #------------------------------------------------------- =item SUM(range) returns the sum of items in the range. =cut #------------------------------------------------------- sub SUM { my $mask=mask(@_); my $sum=0; foreach (grep /$mask/,keys(%sheet_values)) { $sum+=$sheet_values{$_}; } return $sum; } #------------------------------------------------------- =item MEAN(range) compute the average of the items in the range. =cut #------------------------------------------------------- sub MEAN { my $mask=mask(@_); my $sum=0; my $num=0; foreach (grep /$mask/,keys(%sheet_values)) { $sum+=$sheet_values{$_}; $num++; } if ($num) { return $sum/$num; } else { return undef; } } #------------------------------------------------------- =item STDDEV(range) compute the standard deviation of the items in the range. =cut #------------------------------------------------------- sub STDDEV { my $mask=mask(@_); my $sum=0; my $num=0; foreach (grep /$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)) { $sum+=($sheet_values{$_}-$mean)**2; } return sqrt($sum/($num-1)); } #------------------------------------------------------- =item PROD(range) compute the product of the items in the range. =cut #------------------------------------------------------- sub PROD { my $mask=mask(@_); my $prod=1; foreach (grep /$mask/,keys(%sheet_values)) { $prod*=$sheet_values{$_}; } return $prod; } #------------------------------------------------------- =item MAX(range) compute the maximum of the items in the range. =cut #------------------------------------------------------- sub MAX { my $mask=mask(@_); my $max='-'; foreach (grep /$mask/,keys(%sheet_values)) { unless ($max) { $max=$sheet_values{$_}; } if (($sheet_values{$_}>$max) || ($max eq '-')) { $max=$sheet_values{$_}; } } return $max; } #------------------------------------------------------- =item MIN(range) compute the minimum of the items in the range. =cut #------------------------------------------------------- sub MIN { my $mask=mask(@_); my $min='-'; foreach (grep /$mask/,keys(%sheet_values)) { unless ($max) { $max=$sheet_values{$_}; } if (($sheet_values{$_}<$min) || ($min eq '-')) { $min=$sheet_values{$_}; } } return $min; } #------------------------------------------------------- =item SUMMAX(num,lower,upper) compute the sum of the largest 'num' items in the range from 'lower' to 'upper' =cut #------------------------------------------------------- sub SUMMAX { my ($num,$lower,$upper)=@_; my $mask=mask($lower,$upper); my @inside=(); foreach (grep /$mask/,keys(%sheet_values)) { push (@inside,$sheet_values{$_}); } @inside=sort(@inside); my $sum=0; my $i; for ($i=$#inside;(($i>$#inside-$num) && ($i>=0));$i--) { $sum+=$inside[$i]; } return $sum; } #------------------------------------------------------- =item SUMMIN(num,lower,upper) compute the sum of the smallest 'num' items in the range from 'lower' to 'upper' =cut #------------------------------------------------------- sub SUMMIN { my ($num,$lower,$upper)=@_; my $mask=mask($lower,$upper); my @inside=(); foreach (grep /$mask/,keys(%sheet_values)) { $inside[$#inside+1]=$sheet_values{$_}; } @inside=sort(@inside); my $sum=0; my $i; for ($i=0;(($i<$num) && ($i<=$#inside));$i++) { $sum+=$inside[$i]; } return $sum; } #------------------------------------------------------- =item MINPARM(parametername) Returns the minimum value of the parameters matching the parametername. parametername should be a string such as 'duedate'. =cut #------------------------------------------------------- sub MINPARM { my ($expression) = @_; my $min = undef; study($expression); foreach $parameter (keys(%c)) { next if ($parameter !~ /$expression/); if ((! defined($min)) || ($min > $c{$parameter})) { $min = $c{$parameter} } } return $min; } #------------------------------------------------------- =item MAXPARM(parametername) Returns the maximum value of the parameters matching the input parameter name. parametername should be a string such as 'duedate'. =cut #------------------------------------------------------- sub MAXPARM { my ($expression) = @_; my $max = undef; study($expression); foreach $parameter (keys(%c)) { next if ($parameter !~ /$expression/); if ((! defined($min)) || ($max < $c{$parameter})) { $max = $c{$parameter} } } return $max; } #-------------------------------------------------------- sub expandnamed { my $expression=shift; if ($expression=~/^\&/) { my ($func,$var,$formula)=($expression=~/^\&(\w+)\(([^\;]+)\;(.*)\)/); my @vars=split(/\W+/,$formula); my %values=(); undef %values; foreach ( @vars ) { my $varname=$_; if ($varname=~/\D/) { $formula=~s/$varname/'$c{\''.$varname.'\'}'/ge; $varname=~s/$var/\(\\w\+\)/g; foreach (keys(%c)) { if ($_=~/$varname/) { $values{$1}=1; } } } } if ($func eq 'EXPANDSUM') { my $result=''; foreach (keys(%values)) { my $thissum=$formula; $thissum=~s/$var/$_/g; $result.=$thissum.'+'; } $result=~s/\+$//; return $result; } else { return 0; } } else { # it is not a function, so it is a parameter name # We should do the following: # 1. Take the list of parameter names # 2. look through the list for ones that match the parameter we want # 3. If there are no collisions, return the one that matches # 4. If there is a collision, return 'bad parameter name error' my $returnvalue = ''; my @matches = (); $#matches = -1; study $expression; foreach $parameter (keys(%c)) { push @matches,$parameter if ($parameter =~ /$expression/); } if (scalar(@matches) == 0) { $returnvalue = 'unmatched parameter: '.$parameter; } elsif (scalar(@matches) == 1) { $returnvalue = '$c{\''.$matches[0].'\'}'; } elsif (scalar(@matches) > 0) { # more than one match. Look for a concise one $returnvalue = "'non-unique parameter name : $expression'"; foreach (@matches) { if (/^$expression$/) { $returnvalue = '$c{\''.$_.'\'}'; } } } else { # There was a negative number of matches, which indicates # something is wrong with reality. Better warn the user. $returnvalue = 'bizzare parameter: '.$parameter; } return $returnvalue; } } sub sett { %t=(); my $pattern=''; if ($sheettype eq 'assesscalc') { $pattern='A'; } else { $pattern='[A-Z]'; } # Deal with the template row foreach (keys(%f)) { next if ($_!~/template\_(\w)/); my $col=$1; next if ($col=~/^$pattern/); foreach (keys(%f)) { next if ($_!~/A(\d+)/); my $trow=$1; next if (! $trow); # Get the name of this cell my $lb=$col.$trow; # Grab the template declaration $t{$lb}=$f{'template_'.$col}; # Replace '#' with the row number $t{$lb}=~s/\#/$trow/g; # Replace '....' with ',' $t{$lb}=~s/\.\.+/\,/g; # Replace 'A0' with the value from 'A0' $t{$lb}=~s/(^|[^\"\'])([A-Za-z]\d+)/$1\$sheet_values\{\'$2\'\}/g; # Replace parameters $t{$lb}=~s/(^|[^\"\'])\[([^\]]+)\]/$1.&expandnamed($2)/ge; } } # Deal with the normal cells foreach (keys(%f)) { if (exists($f{$_}) && ($_!~/template\_/)) { my $matches=($_=~/^$pattern(\d+)/); if (($matches) && ($1)) { unless ($f{$_}=~/^\!/) { $t{$_}=$c{$_}; } } else { $t{$_}=$f{$_}; $t{$_}=~s/\.\.+/\,/g; $t{$_}=~s/(^|[^\"\'])([A-Za-z]\d+)/$1\$sheet_values\{\'$2\'\}/g; $t{$_}=~s/(^|[^\"\'])\[([^\]]+)\]/$1.&expandnamed($2)/ge; } } } # For inserted lines, [B-Z] is also valid unless ($sheettype eq 'assesscalc') { foreach (keys(%f)) { if ($_=~/[B-Z](\d+)/) { if ($f{'A'.$1}=~/^[\~\-]/) { $t{$_}=$f{$_}; $t{$_}=~s/\.\.+/\,/g; $t{$_}=~s/(^|[^\"\'])([A-Za-z]\d+)/$1\$sheet_values\{\'$2\'\}/g; $t{$_}=~s/(^|[^\"\'])\[([^\]]+)\]/$1.&expandnamed($2)/ge; } } } } # For some reason 'A0' gets special treatment... This seems superfluous # but I imagine it is here for a reason. $t{'A0'}=$f{'A0'}; $t{'A0'}=~s/\.\.+/\,/g; $t{'A0'}=~s/(^|[^\"\'])([A-Za-z]\d+)/$1\$sheet_values\{\'$2\'\}/g; $t{'A0'}=~s/(^|[^\"\'])\[([^\]]+)\]/$1.&expandnamed($2)/ge; } sub calc { undef %sheet_values; &sett(); my $notfinished=1; my $lastcalc=''; my $depth=0; while ($notfinished) { $notfinished=0; foreach (keys(%t)) { #$errorlog .= "$_:".$t{$_}; my $old=$sheet_values{$_}; $sheet_values{$_}=eval $t{$_}; if ($@) { undef %sheet_values; return $_.': '.$@; } if ($sheet_values{$_} ne $old) { $notfinished=1; $lastcalc=$_; } #$errorlog .= ":".$sheet_values{$_}."\n"; } $depth++; if ($depth>100) { undef %sheet_values; return $lastcalc.': Maximum calculation depth exceeded'; } } return ''; } # ------------------------------------------- End of "Inside of the safe space" ENDDEFS $safeeval->reval($code); return $safeeval; } # # # sub templaterow { my $sheet = shift; my @cols=(); my $rowlabel = 'Template'; foreach ('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') { my $fm=$sheet->{'f'}->{'template_'.$_}; $fm=~s/[\'\"]/\&\#34;/g; push(@cols,{ name => 'template_'.$_, formula => $fm, value => $fm }); } return ($rowlabel,@cols); } sub outrowassess { # $n is the current row number my ($sheet,$n) = @_; my @cols=(); my $rowlabel=''; if ($n) { my ($usy,$ufn)=split(/__&&&\__/,$sheet->{'f'}->{'A'.$n}); if (exists($sheet->{'rowlabel'}->{$usy})) { $rowlabel = $sheet->{'rowlabel'}->{$usy}; } else { $rowlabel = ''; } } else { $rowlabel = 'Export'; } foreach ('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') { my $fm=$sheet->{'f'}->{$_.$n}; $fm=~s/[\'\"]/\&\#34;/g; push(@cols,{ name => $_.$n, formula => $fm, value => $sheet->{'values'}->{$_.$n}}); } return ($rowlabel,@cols); } sub outrow { my ($sheet,$n)=@_; my @cols=(); my $rowlabel; if ($n) { $rowlabel = $sheet->{'rowlabel'}->{$sheet->{'f'}->{'A'.$n}}; } else { if ($sheet->{'sheettype'} eq 'classcalc') { $rowlabel = 'Summary'; } else { $rowlabel = 'Export'; } } foreach ('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') { my $fm=$sheet->{'f'}->{$_.$n}; $fm=~s/[\'\"]/\&\#34;/g; push(@cols,{ name => $_.$n, formula => $fm, value => $sheet->{'values'}->{$_.$n}}); } return ($rowlabel,@cols); } # ------------------------------------------------ Add or change formula values sub setformulas { my ($sheet)=shift; %{$sheet->{'safe'}->varglob('f')}=%{$sheet->{'f'}}; } # ------------------------------------------------ Add or change formula values sub setconstants { my ($sheet)=shift; my ($constants) = @_; if (! ref($constants)) { my %tmp = @_; $constants = \%tmp; } $sheet->{'constants'} = $constants; return %{$sheet->{'safe'}->varglob('c')}=%{$sheet->{'constants'}}; } # --------------------------------------------- Set names of other spreadsheets sub setothersheets { my $sheet = shift; my @othersheets = @_; $sheet->{'othersheets'} = \@othersheets; @{$sheet->{'safe'}->varglob('os')}=@othersheets; return; } # ------------------------------------------------ Add or change formula values sub setrowlabels { my $sheet=shift; my ($rowlabel) = @_; if (! ref($rowlabel)) { my %tmp = @_; $rowlabel = \%tmp; } $sheet->{'rowlabel'}=$rowlabel; } # ------------------------------------------------------- Calculate spreadsheet sub calcsheet { my $sheet=shift; my $result = $sheet->{'safe'}->reval('&calc();'); %{$sheet->{'values'}} = %{$sheet->{'safe'}->varglob('sheet_values')}; return $result; } # ---------------------------------------------------------------- Get formulas # Return a copy of the formulas sub getformulas { my $sheet = shift; return %{$sheet->{'safe'}->varglob('f')}; } sub geterrorlog { my $sheet = shift; return ${$sheet->{'safe'}->varglob('errorlog')}; } sub gettitle { my $sheet = shift; if ($sheet->{'sheettype'} eq 'classcalc') { return $sheet->{'coursedesc'}; } elsif ($sheet->{'sheettype'} eq 'studentcalc') { return 'Grades for '.$sheet->{'uname'}.'@'.$sheet->{'udom'}; } elsif ($sheet->{'sheettype'} eq 'assesscalc') { if (($sheet->{'usymb'} eq '_feedback') || ($sheet->{'usymb'} eq '_evaluation') || ($sheet->{'usymb'} eq '_discussion') || ($sheet->{'usymb'} eq '_tutoring')) { my $title = $sheet->{'usymb'}; $title =~ s/^_//; $title = ucfirst($title); return $title; } return if (! defined($sheet->{'mapid'}) || $sheet->{'mapid'} !~ /^\d+$/); my $mapid = $sheet->{'mapid'}; return if (! defined($sheet->{'resid'}) || $sheet->{'resid'} !~ /^\d+$/); my $resid = $sheet->{'resid'}; my %course_db; tie(%course_db,'GDBM_File',$sheet->{'coursefilename'}.'.db', &GDBM_READER(),0640); return if (! tied(%course_db)); my $key = 'title_'.$mapid.'.'.$resid; my $title = ''; if (exists($course_db{$key})) { $title = $course_db{$key}; } else { $title = $sheet->{'usymb'}; } untie (%course_db); return $title; } } # ----------------------------------------------------- Get value of $f{'A'.$n} sub getfa { my $sheet = shift; my ($n)=@_; return $sheet->{'safe'}->reval('$f{"A'.$n.'"}'); } # ------------------------------------------------------------- Export of A-row sub exportdata { my $sheet=shift; my @exportarray=(); foreach ('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') { if (exists($sheet->{'values'}->{$_.'0'})) { push(@exportarray,$sheet->{'values'}->{$_.'0'}); } else { push(@exportarray,''); } } return @exportarray; } sub update_student_sheet{ my ($sheet,$c) = @_; # Load in the studentcalc sheet &readsheet($sheet,'default_studentcalc'); # Determine the structure (contained assessments, etc) of the sheet &updatesheet($sheet); # Load in the cached sheets for this student &cachedssheets($sheet); # Load in the (possibly cached) data from the assessment sheets &loadstudent($sheet,$c); # Compute the sheet &calcsheet($sheet); } # ========================================================== End of Spreadsheet # ============================================================================= # # Procedures for spreadsheet output # # --------------------------------------------- Produce output row n from sheet sub get_row { my ($sheet,$n) = @_; my ($rowlabel,@rowdata); if ($n eq '-') { ($rowlabel,@rowdata) = &templaterow($sheet); } elsif ($sheet->{'sheettype'} eq 'studentcalc') { ($rowlabel,@rowdata) = &outrowassess($sheet,$n); } else { ($rowlabel,@rowdata) = &outrow($sheet,$n); } return ($rowlabel,@rowdata); } ######################################################################## ######################################################################## sub sort_indicies { my $sheet = shift; my @sortidx=(); # if ($sheet->{'sheettype'} eq 'classcalc') { my @sortby=(); # Skip row 0 for (my $row=1;$row<=$sheet->{'maxrow'};$row++) { my (undef,$sname,$sdom,$fullname,$section,$id) = split(':',$sheet->{'rowlabel'}->{$sheet->{'f'}->{'A'.$row}}); push (@sortby, lc($fullname)); push (@sortidx, $row); } @sortidx = sort { $sortby[$a] cmp $sortby[$b]; } @sortidx; } elsif ($sheet->{'sheettype'} eq 'studentcalc') { my @sortby=(); # Skip row 0 &Apache::lonnet::logthis('starting sort for studentcalc'); for (my $row=1;$row<=$sheet->{'maxrow'};$row++) { my (undef,$symb,$uname,$udom,$mapid,$resid,$title) = split(':',$sheet->{'rowlabel'}->{$sheet->{'f'}->{'A'.$row}}); $symb = &Apache::lonnet::unescape($symb); my ($sequence) = ($symb =~ /\/([^\/]*\.sequence)/); if ($sequence eq '') { $sequence = $symb; } push (@sortby, $sequence); push (@sortidx, $row); } @sortidx = sort { $sortby[$a] cmp $sortby[$b]; } @sortidx; } else { my @sortby=(); # Skip row 0 for (my $row=1;$row<=$sheet->{'maxrow'};$row++) { push (@sortby, $sheet->{'safe'}->reval('$f{"A'.$row.'"}')); push (@sortidx, $row); } @sortidx = sort { $sortby[$a] cmp $sortby[$b]; } @sortidx; } return @sortidx; } ############################################################# ### ### ### Spreadsheet Output Routines ### ### ### ############################################################# ############################################ ## HTML output routines ## ############################################ sub html_editable_cell { my ($cell,$bgcolor) = @_; my $result; my ($name,$formula,$value); if (defined($cell)) { $name = $cell->{'name'}; $formula = $cell->{'formula'}; $value = $cell->{'value'}; } $name = '' if (! defined($name)); $formula = '' if (! defined($formula)); if (! defined($value)) { $value = '#'; if ($formula ne '') { $value = 'undefined value'; } } if ($value =~ /^\s*$/ ) { $value = '#'; } $formula =~ s/\n/\\n/gs; $result .= ''.$value.''; return $result; } sub html_uneditable_cell { my ($cell,$bgcolor) = @_; my $value = (defined($cell) ? $cell->{'value'} : ''); return ' '.$value.' '; } sub outsheet_html { my ($sheet,$r) = @_; my ($num_uneditable,$realm,$row_type); if ($sheet->{'sheettype'} eq 'assesscalc') { $num_uneditable = 1; $realm = 'Assessment'; $row_type = 'Item'; } elsif ($sheet->{'sheettype'} eq 'studentcalc') { $num_uneditable = 26; $realm = 'User'; $row_type = 'Assessment'; } elsif ($sheet->{'sheettype'} eq 'classcalc') { $num_uneditable = 26; $realm = 'Course'; $row_type = 'Student'; } else { return; # error } #################################### # Print out header table #################################### my $num_left = 52-$num_uneditable; my $tabledata =<<"END"; END my $label_num = 0; foreach (split(//,'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz')){ if ($label_num<$num_uneditable) { $tabledata.='"; $label_num++; } $tabledata.="\n"; $r->print($tabledata); #################################### # Print out template row #################################### my ($rowlabel,@rowdata) = &get_row($sheet,'-'); my $row_html = ''; my $num_cols_output = 0; foreach my $cell (@rowdata) { if ($num_cols_output++ < $num_uneditable) { $row_html .= ''; } $row_html.= "\n"; $r->print($row_html); #################################### # Print out summary/export row #################################### my ($rowlabel,@rowdata) = &get_row($sheet,'0'); $row_html = ''; $num_cols_output = 0; foreach my $cell (@rowdata) { if ($num_cols_output++ < 26) { $row_html .= ''; } $row_html.= "\n"; $r->print($row_html); $r->print('
$realm Import Calculations
'; } else { $tabledata.=''; } $tabledata.="$_
'.&format_html_rowlabel($rowlabel).''; $row_html .= &html_uneditable_cell($cell,'#FFDDDD'); } else { $row_html .= ''; $row_html .= &html_editable_cell($cell,'#E0FFDD'); } $row_html .= '
'.&format_html_rowlabel($rowlabel).''; $row_html .= &html_editable_cell($cell,'#CCCCFF'); } else { $row_html .= ''; $row_html .= &html_uneditable_cell(undef,'#CCCCFF'); } $row_html .= '
'); #################################### # Prepare to output rows #################################### my @Rows = &sort_indicies($sheet); # # Loop through the rows and output them one at a time my $rows_output=0; foreach my $rownum (@Rows) { my ($rowlabel,@rowdata) = &get_row($sheet,$rownum); next if ($rowlabel =~ /^\s*$/); next if (($sheet->{'sheettype'} eq 'assesscalc') && (! $ENV{'form.showall'}) && ($rowdata[0]->{'value'} =~ /^\s*$/)); if ($sheet->{'sheettype'} =~ /^(studentcalc|classcalc)$/) { my $row_is_empty = 1; foreach my $cell (@rowdata) { if ($cell->{'value'} !~ /^\s*$/) { $row_is_empty = 0; last; } } next if $row_is_empty; } # my $defaultbg='#E0FF'; # my $row_html ="\n".''.$rownum. ''; # if ($sheet->{'sheettype'} eq 'classcalc') { $row_html.=''.&format_html_rowlabel($rowlabel).''; # Output links for each student? # Nope, that is already done for us in format_html_rowlabel (for now) } elsif ($sheet->{'sheettype'} eq 'studentcalc') { $row_html.=''.&format_html_rowlabel($rowlabel); $row_html.= '
'. ''; } elsif ($sheet->{'sheettype'} eq 'assesscalc') { $row_html.=''.&format_html_rowlabel($rowlabel).''; } # my $shown_cells = 0; foreach my $cell (@rowdata) { my $value = $cell->{'value'}; my $formula = $cell->{'formula'}; my $cellname = $cell->{'name'}; # my $bgcolor; if ($shown_cells && ($shown_cells/5 == int($shown_cells/5))) { $bgcolor = $defaultbg.'99'; } else { $bgcolor = $defaultbg.'DD'; } $bgcolor='#FFDDDD' if ($shown_cells < $num_uneditable); # $row_html.=''; if ($shown_cells < $num_uneditable) { $row_html .= &html_uneditable_cell($cell,$bgcolor); } else { $row_html .= &html_editable_cell($cell,$bgcolor); } $row_html.=''; $shown_cells++; } if ($row_html) { if ($rows_output % 25 == 0) { $r->print("\n
\n"); $r->rflush(); $r->print(''. ''. '\n"); } $rows_output++; $r->print($row_html); } } # $r->print('
 '.$row_type.''. join('', (split(//,'ABCDEFGHIJKLMNOPQRSTUVWXYZ'. 'abcdefghijklmnopqrstuvwxyz'))). "
'); # # Debugging code (be sure to uncomment errorlog code in safe space): # # $r->print("\n
");
    # $r->print(&geterrorlog($sheet));
    # $r->print("\n
"); return 1; } ############################################ ## csv output routines ## ############################################ sub outsheet_csv { my ($sheet,$r) = @_; my $csvdata = ''; my @Values; #################################### # Prepare to output rows #################################### my @Rows = &sort_indicies($sheet); # # Loop through the rows and output them one at a time my $rows_output=0; foreach my $rownum (@Rows) { my ($rowlabel,@rowdata) = &get_row($sheet,$rownum); next if ($rowlabel =~ /^\s*$/); push (@Values,&format_csv_rowlabel($rowlabel)); foreach my $cell (@rowdata) { push (@Values,'"'.$cell->{'value'}.'"'); } $csvdata.= join(',',@Values)."\n"; @Values = (); } # # Write the CSV data to a file and serve up a link # 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("Problems occured in writing the csv file. ". "This error has been logged. ". "Please alert your LON-CAPA administrator."); $r->print("
\n".$csvdata."
\n"); return 0; } print $file $csvdata; close($file); $r->print('

'. 'Your CSV spreadsheet.'."\n"); # return 1; } ############################################ ## Excel output routines ## ############################################ sub outsheet_recursive_excel { my ($sheet,$r) = @_; my $c = $r->connection; return undef if ($sheet->{'sheettype'} ne 'classcalc'); my ($workbook,$filename) = &create_excel_spreadsheet($sheet,$r); return undef if (! defined($workbook)); # # Create main worksheet my $main_worksheet = $workbook->addworksheet('main'); # # Figure out who the students are my %f=&getformulas($sheet); my $count = 0; $r->print(< Compiling Excel Workbook with a worksheet for each student.

This operation may take longer than a complete recalculation of the spreadsheet.

To abort this operation, hit the stop button on your browser.

A link to the spreadsheet will be available at the end of this process.

END $r->rflush(); my $starttime = time; foreach (keys(%f)) { next if ($_!~/^A(\d+)/ || $1 == 0 || ($f{$_}=~/^[!~-]/)); $count++; my ($sname,$sdom) = split(':',$f{$_}); my $student_excel_worksheet=$workbook->addworksheet($sname.'@'.$sdom); # Create a new spreadsheet my $studentsheet = &makenewsheet($sname,$sdom,'studentcalc',undef); # Read in the spreadsheet definition &update_student_sheet($studentsheet,$c); # Stuff the sheet into excel &export_sheet_as_excel($studentsheet,$student_excel_worksheet); my $totaltime = int((time - $starttime) / $count * $sheet->{'maxrow'}); my $timeleft = int((time - $starttime) / $count * ($sheet->{'maxrow'} - $count)); if ($count % 5 == 0) { $r->print($count.' students completed.'. ' Time remaining: '.$timeleft.' sec. '. ' Estimated total time: '.$totaltime." sec
\n"); $r->rflush(); } if(defined($c) && ($c->aborted())) { last; } } # if(! $c->aborted() ) { $r->print('All students spreadsheets completed!
'); $r->rflush(); # # &export_sheet_as_excel fills $worksheet with the data from $sheet &export_sheet_as_excel($sheet,$main_worksheet); # $workbook->close(); # Okay, the spreadsheet is taken care of, so give the user a link. $r->print('

'. 'Your Excel spreadsheet.'."\n"); } else { $workbook->close(); # Not sure how necessary this is. #unlink('/home/httpd'.$filename); # No need to keep this around? } return 1; } sub outsheet_excel { my ($sheet,$r) = @_; my ($workbook,$filename) = &create_excel_spreadsheet($sheet,$r); return undef if (! defined($workbook)); my $sheetname; if ($sheet->{'sheettype'} eq 'classcalc') { $sheetname = 'Main'; } elsif ($sheet->{'sheettype'} eq 'studentcalc') { $sheetname = $sheet->{'uname'}.'@'.$sheet->{'udom'}; } elsif ($sheet->{'sheettype'} eq 'assesscalc') { $sheetname = $sheet->{'uname'}.'@'.$sheet->{'udom'}.' assessment'; } my $worksheet = $workbook->addworksheet($sheetname); # # &export_sheet_as_excel fills $worksheet with the data from $sheet &export_sheet_as_excel($sheet,$worksheet); # $workbook->close(); # Okay, the spreadsheet is taken care of, so give the user a link. $r->print('

'. 'Your Excel spreadsheet.'."\n"); return 1; } sub create_excel_spreadsheet { my ($sheet,$r) = @_; my $filename = '/prtspool/'. $ENV{'user.name'}.'_'.$ENV{'user.domain'}.'_'. time.'_'.rand(1000000000).'.xls'; 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. ". "This error has been logged. ". "Please alert your LON-CAPA administrator"); return undef; } # # The spreadsheet stores temporary data in files, then put them # together. If needed we should be able to disable this (memory only). # The temporary directory must be specified before calling 'addworksheet'. # File::Temp is used to determine the temporary directory. $workbook->set_tempdir('/home/httpd/perl/tmp'); # # Determine the name to give the worksheet return ($workbook,$filename); } sub export_sheet_as_excel { my $sheet = shift; my $worksheet = shift; # my $rows_output = 0; my $cols_output = 0; #################################### # Write an identifying row # #################################### my @Headerinfo = ($sheet->{'coursedesc'}); my $title = &gettitle($sheet); $cols_output = 0; if (defined($title)) { $worksheet->write($rows_output++,$cols_output++,$title); } #################################### # Write the summary/export row # #################################### my ($rowlabel,@rowdata) = &get_row($sheet,'0'); my $label = &format_excel_rowlabel($rowlabel); $cols_output = 0; $worksheet->write($rows_output,$cols_output++,$label); foreach my $cell (@rowdata) { $worksheet->write($rows_output,$cols_output++,$cell->{'value'}); } $rows_output+= 2; # Skip a row, just for fun #################################### # Prepare to output rows #################################### my @Rows = &sort_indicies($sheet); # # Loop through the rows and output them one at a time foreach my $rownum (@Rows) { my ($rowlabel,@rowdata) = &get_row($sheet,$rownum); next if ($rowlabel =~ /^\s*$/); $cols_output = 0; my $label = &format_excel_rowlabel($rowlabel); $worksheet->write($rows_output,$cols_output++,$label); if (ref($label)) { $cols_output = (scalar(@$label)); } foreach my $cell (@rowdata) { $worksheet->write($rows_output,$cols_output++,$cell->{'value'}); } $rows_output++; } return; } ############################################ ## XML output routines ## ############################################ sub outsheet_xml { my ($sheet,$r) = @_; ## Someday XML ## Will be rendered for the user ## But not on this day } ## ## Outsheet - calls other outsheet_* functions ## sub outsheet { my ($r,$sheet)=@_; if (! exists($ENV{'form.output'})) { $ENV{'form.output'} = 'HTML'; } if (lc($ENV{'form.output'}) eq 'csv') { &outsheet_csv($sheet,$r); } elsif (lc($ENV{'form.output'}) eq 'excel') { &outsheet_excel($sheet,$r); } elsif (lc($ENV{'form.output'}) eq 'recursive excel') { &outsheet_recursive_excel($sheet,$r); # } elsif (lc($ENV{'form.output'}) eq 'xml' ) { # &outsheet_xml($sheet,$r); } else { &outsheet_html($sheet,$r); } } ######################################################################## ######################################################################## sub othersheets { my ($sheet,$stype)=@_; $stype = $sheet->{'sheettype'} if (! defined($stype)); # my $cnum = $sheet->{'cnum'}; my $cdom = $sheet->{'cdom'}; my $chome = $sheet->{'chome'}; # my @alternatives=(); my %results=&Apache::lonnet::dump($stype.'_spreadsheets',$cdom,$cnum); my ($tmp) = keys(%results); unless ($tmp =~ /^(con_lost|error|no_such_host)/i) { @alternatives = sort (keys(%results)); } return @alternatives; } # # -------------------------------------- Parse a spreadsheet # sub parse_sheet { # $sheetxml is a scalar reference or a scalar my ($sheetxml) = @_; if (! ref($sheetxml)) { my $tmp = $sheetxml; $sheetxml = \$tmp; } my %f; 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'); } } } return \%f; } # # -------------------------------------- Read spreadsheet formulas for a course # sub readsheet { my ($sheet,$fn)=@_; # my $stype = $sheet->{'sheettype'}; my $cnum = $sheet->{'cnum'}; my $cdom = $sheet->{'cdom'}; my $chome = $sheet->{'chome'}; # if (! defined($fn)) { # There is no filename. Look for defaults in course and global, cache unless ($fn=$defaultsheets{$cnum.'_'.$cdom.'_'.$stype}) { my %tmphash = &Apache::lonnet::get('environment', ['spreadsheet_default_'.$stype], $cdom,$cnum); my ($tmp) = keys(%tmphash); if ($tmp =~ /^(con_lost|error|no_such_host)/i) { $fn = 'default_'.$stype; } else { $fn = $tmphash{'spreadsheet_default_'.$stype}; } unless (($fn) && ($fn!~/^error\:/)) { $fn='default_'.$stype; } $defaultsheets{$cnum.'_'.$cdom.'_'.$stype}=$fn; } } # $fn now has a value $sheet->{'filename'} = $fn; # see if sheet is cached my $fstring=''; if ($fstring=$spreadsheets{$cnum.'_'.$cdom.'_'.$stype.'_'.$fn}) { my %tmp = split(/___;___/,$fstring); $sheet->{'f'} = \%tmp; &setformulas($sheet); } else { # Not cached, need to read my %f=(); if ($fn=~/^default\_/) { my $sheetxml=''; my $fh; my $dfn=$fn; $dfn=~s/\_/\./g; if ($fh=Apache::File->new($includedir.'/'.$dfn)) { $sheetxml=join('',<$fh>); } else { # $sheetxml='"Error"'; $sheetxml=''; } %f=%{&parse_sheet(\$sheetxml)}; } elsif($fn=~/\/*\.spreadsheet$/) { my $sheetxml=&Apache::lonnet::getfile (&Apache::lonnet::filelocation('',$fn)); if ($sheetxml == -1) { $sheetxml='"Error loading spreadsheet ' .$fn.'"'; } %f=%{&parse_sheet(\$sheetxml)}; } else { my $sheet=''; my %tmphash = &Apache::lonnet::dump($fn,$cdom,$cnum); my ($tmp) = keys(%tmphash); unless ($tmp =~ /^(con_lost|error|no_such_host)/i) { foreach (keys(%tmphash)) { $f{$_}=$tmphash{$_}; } } } # Cache and set $spreadsheets{$cnum.'_'.$cdom.'_'.$stype.'_'.$fn}=join('___;___',%f); $sheet->{'f'}=\%f; &setformulas($sheet); } } # -------------------------------------------------------- Make new spreadsheet sub makenewsheet { my ($uname,$udom,$stype,$usymb)=@_; my $sheet={}; $sheet->{'uname'} = $uname; $sheet->{'udom'} = $udom; $sheet->{'sheettype'} = $stype; $sheet->{'usymb'} = $usymb; $sheet->{'mapid'} = $ENV{'form.mapid'}; $sheet->{'resid'} = $ENV{'form.resid'}; $sheet->{'cid'} = $ENV{'request.course.id'}; $sheet->{'csec'} = $Section{$uname.':'.$udom}; $sheet->{'coursefilename'} = $ENV{'request.course.fn'}; $sheet->{'cnum'} = $ENV{'course.'.$ENV{'request.course.id'}.'.num'}; $sheet->{'cdom'} = $ENV{'course.'.$ENV{'request.course.id'}.'.domain'}; $sheet->{'chome'} = $ENV{'course.'.$ENV{'request.course.id'}.'.home'}; $sheet->{'coursedesc'} = $ENV{'course.'.$ENV{'request.course.id'}. '.description'}; $sheet->{'uhome'} = &Apache::lonnet::homeserver($uname,$udom); # # $sheet->{'f'} = {}; $sheet->{'constants'} = {}; $sheet->{'othersheets'} = []; $sheet->{'rowlabel'} = {}; # # $sheet->{'safe'}=&initsheet($sheet->{'sheettype'}); # # Place all the %$sheet items into the safe space except the safe space # itself my $initstring = ''; foreach (qw/uname udom sheettype usymb cid csec coursefilename cnum cdom chome uhome/) { $initstring.= qq{\$$_="$sheet->{$_}";}; } $sheet->{'safe'}->reval($initstring); return $sheet; } # ------------------------------------------------------------ Save spreadsheet sub writesheet { my ($sheet,$makedef)=@_; my $cid=$sheet->{'cid'}; if (&Apache::lonnet::allowed('opa',$cid)) { my %f=&getformulas($sheet); my $stype= $sheet->{'sheettype'}; my $cnum = $sheet->{'cnum'}; my $cdom = $sheet->{'cdom'}; my $chome= $sheet->{'chome'}; my $fn = $sheet->{'filename'}; # Cache new sheet $spreadsheets{$cnum.'_'.$cdom.'_'.$stype.'_'.$fn}=join('___;___',%f); # Write sheet foreach (keys(%f)) { delete($f{$_}) if ($f{$_} eq 'import'); } my $reply = &Apache::lonnet::put($fn,\%f,$cdom,$cnum); if ($reply eq 'ok') { $reply = &Apache::lonnet::put($stype.'_spreadsheets', {$fn => $ENV{'user.name'}.'@'.$ENV{'user.domain'}}, $cdom,$cnum); if ($reply eq 'ok') { if ($makedef) { return &Apache::lonnet::put('environment', {'spreadsheet_default_'.$stype => $fn }, $cdom,$cnum); } return $reply; } return $reply; } return $reply; } return 'unauthorized'; } # ----------------------------------------------- Make a temp copy of the sheet # "Modified workcopy" - interactive only # sub tmpwrite { my ($sheet) = @_; my $fn=$ENV{'user.name'}.'_'. $ENV{'user.domain'}.'_spreadsheet_'.$sheet->{'usymb'}.'_'. $sheet->{'filename'}; $fn=~s/\W/\_/g; $fn=$tmpdir.$fn.'.tmp'; my $fh; if ($fh=Apache::File->new('>'.$fn)) { print $fh join("\n",&getformulas($sheet)); } } # ---------------------------------------------------------- Read the temp copy sub tmpread { my ($sheet,$nfield,$nform)=@_; my $fn=$ENV{'user.name'}.'_'. $ENV{'user.domain'}.'_spreadsheet_'.$sheet->{'usymb'}.'_'. $sheet->{'filename'}; $fn=~s/\W/\_/g; $fn=$tmpdir.$fn.'.tmp'; my $fh; my %fo=(); my $countrows=0; if ($fh=Apache::File->new($fn)) { my $name; while ($name=<$fh>) { chomp($name); my $value=<$fh>; chomp($value); $fo{$name}=$value; if ($name=~/^A(\d+)$/) { if ($1>$countrows) { $countrows=$1; } } } } if ($nform eq 'changesheet') { $fo{'A'.$nfield}=(split(/__&&&\__/,$fo{'A'.$nfield}))[0]; unless ($ENV{'form.sel_'.$nfield} eq 'Default') { $fo{'A'.$nfield}.='__&&&__'.$ENV{'form.sel_'.$nfield}; } } elsif ($nfield eq 'insertrow') { $countrows++; my $newrow=substr('000000'.$countrows,-7); if ($nform eq 'top') { $fo{'A'.$countrows}='--- '.$newrow; } else { $fo{'A'.$countrows}='~~~ '.$newrow; } } else { if ($nfield) { $fo{$nfield}=$nform; } } $sheet->{'f'}=\%fo; &setformulas($sheet); } ################################################## ################################################## =pod =item &parmval() Determine the value of a parameter. Inputs: $what, the parameter needed, $sheet, the safe space Returns: The value of a parameter, or '' if none. This function cascades through the possible levels searching for a value for a parameter. The levels are checked in the following order: user, course (at section level and course level), map, and lonnet::metadata. This function uses %parmhash, which must be tied prior to calling it. This function also requires %courseopt and %useropt to be initialized for this user and course. =cut ################################################## ################################################## sub parmval { my ($what,$sheet)=@_; my $symb = $sheet->{'usymb'}; unless ($symb) { return ''; } # my $cid = $sheet->{'cid'}; my $csec = $sheet->{'csec'}; my $uname = $sheet->{'uname'}; my $udom = $sheet->{'udom'}; my $result=''; # my ($mapname,$id,$fn)=split(/\_\_\_/,$symb); # Cascading lookup scheme my $rwhat=$what; $what =~ s/^parameter\_//; $what =~ s/\_([^\_]+)$/\.$1/; # my $symbparm = $symb.'.'.$what; my $mapparm = $mapname.'___(all).'.$what; my $usercourseprefix = $uname.'_'.$udom.'_'.$cid; # my $seclevel = $usercourseprefix.'.['.$csec.'].'.$what; my $seclevelr = $usercourseprefix.'.['.$csec.'].'.$symbparm; my $seclevelm = $usercourseprefix.'.['.$csec.'].'.$mapparm; # my $courselevel = $usercourseprefix.'.'.$what; my $courselevelr = $usercourseprefix.'.'.$symbparm; my $courselevelm = $usercourseprefix.'.'.$mapparm; # fourth, check user if (defined($uname)) { return $useropt{$courselevelr} if (defined($useropt{$courselevelr})); return $useropt{$courselevelm} if (defined($useropt{$courselevelm})); return $useropt{$courselevel} if (defined($useropt{$courselevel})); } # third, check course if (defined($csec)) { return $courseopt{$seclevelr} if (defined($courseopt{$seclevelr})); return $courseopt{$seclevelm} if (defined($courseopt{$seclevelm})); return $courseopt{$seclevel} if (defined($courseopt{$seclevel})); } # return $courseopt{$courselevelr} if (defined($courseopt{$courselevelr})); return $courseopt{$courselevelm} if (defined($courseopt{$courselevelm})); return $courseopt{$courselevel} if (defined($courseopt{$courselevel})); # second, check map parms my $thisparm = $parmhash{$symbparm}; return $thisparm if (defined($thisparm)); # first, check default return &Apache::lonnet::metadata($fn,$rwhat.'.default'); } ################################################################## ## Row label formatting routines ## ################################################################## sub format_html_rowlabel { my $rowlabel = shift; return '' if ($rowlabel eq ''); my ($type,$labeldata) = split(':',$rowlabel,2); my $result = ''; if ($type eq 'symb') { my ($symb,$uname,$udom,$mapid,$resid,$title) = split(':',$labeldata); $symb = &Apache::lonnet::unescape($symb); $result = ''.$title.''; } elsif ($type eq 'student') { my ($sname,$sdom,$fullname,$section,$id) = split(':',$labeldata); $result =''; $result.=$section.' '.$id." ".$fullname.''; } elsif ($type eq 'parameter') { $result = $labeldata; } else { $result = ''.$rowlabel.''; } return $result; } sub format_csv_rowlabel { my $rowlabel = shift; return '' if ($rowlabel eq ''); my ($type,$labeldata) = split(':',$rowlabel,2); my $result = ''; if ($type eq 'symb') { my ($symb,$uname,$udom,$mapid,$resid,$title) = split(':',$labeldata); $symb = &Apache::lonnet::unescape($symb); $result = $title; } elsif ($type eq 'student') { my ($sname,$sdom,$fullname,$section,$id) = split(':',$labeldata); $result = join('","',($sname,$sdom,$fullname,$section,$id)); } elsif ($type eq 'parameter') { $labeldata =~ s/
/ /g; $result = $labeldata; } else { $result = $rowlabel; } return '"'.$result.'"'; } sub format_excel_rowlabel { my $rowlabel = shift; return '' if ($rowlabel eq ''); my ($type,$labeldata) = split(':',$rowlabel,2); my $result = ''; if ($type eq 'symb') { my ($symb,$uname,$udom,$mapid,$resid,$title) = split(':',$labeldata); $symb = &Apache::lonnet::unescape($symb); $result = $title; } elsif ($type eq 'student') { my ($sname,$sdom,$fullname,$section,$id) = split(':',$labeldata); $section = '' if (! defined($section)); $id = '' if (! defined($id)); my @Data = ($sname,$sdom,$fullname,$section,$id); $result = \@Data; } elsif ($type eq 'parameter') { $labeldata =~ s/
/ /g; $result = $labeldata; } else { $result = $rowlabel; } return $result; } # ---------------------------------------------- Update rows for course listing sub updateclasssheet { my ($sheet) = @_; my $cnum =$sheet->{'cnum'}; my $cdom =$sheet->{'cdom'}; my $cid =$sheet->{'cid'}; my $chome =$sheet->{'chome'}; # %Section = (); # # Read class list and row labels my $classlist = &Apache::loncoursedata::get_classlist(); if (! defined($classlist)) { return 'Could not access course classlist'; } # my %currentlist=(); foreach my $student (keys(%$classlist)) { my ($studentDomain,$studentName,$end,$start,$id,$studentSection, $fullname,$status) = @{$classlist->{$student}}; $Section{$studentName.':'.$studentDomain} = $studentSection; if ($ENV{'form.Status'} eq $status || $ENV{'form.Status'} eq 'Any') { $currentlist{$student}=join(':',('student',$studentName, $studentDomain,$fullname, $studentSection,$id)); } } # # Find discrepancies between the course row table and this # my %f=&getformulas($sheet); my $changed=0; # $sheet->{'maxrow'}=0; my %existing=(); # # Now obsolete rows foreach (keys(%f)) { if ($_=~/^A(\d+)/) { if ($1 > $sheet->{'maxrow'}) { $sheet->{'maxrow'}= $1; } $existing{$f{$_}}=1; unless ((defined($currentlist{$f{$_}})) || (!$1) || ($f{$_}=~/^(~~~|---)/)) { $f{$_}='!!! Obsolete'; $changed=1; } } } # # New and unknown keys foreach my $student (sort keys(%currentlist)) { unless ($existing{$student}) { $changed=1; $sheet->{'maxrow'}++; $f{'A'.$sheet->{'maxrow'}}=$student; } } if ($changed) { $sheet->{'f'} = \%f; &setformulas($sheet,%f); } # &setrowlabels($sheet,\%currentlist); } # ----------------------------------- Update rows for student and assess sheets sub get_student_rowlabels { my ($sheet) = @_; # my %course_db; # my $stype = $sheet->{'sheettype'}; my $uname = $sheet->{'uname'}; my $udom = $sheet->{'udom'}; # $sheet->{'rowlabel'} = {}; # my $identifier =$sheet->{'coursefilename'}.'_'.$stype; if ($rowlabel_cache{$identifier}) { %{$sheet->{'rowlabel'}}=split(/___;___/,$rowlabel_cache{$identifier}); } else { # Get the data and store it in the cache # Tie hash tie(%course_db,'GDBM_File',$sheet->{'coursefilename'}.'.db', &GDBM_READER(),0640); if (! tied(%course_db)) { return 'Could not access course data'; } # my %assesslist; foreach ('Feedback','Evaluation','Tutoring','Discussion') { my $symb = '_'.lc($_); $assesslist{$symb} = join(':',('symb',$symb,$uname,$udom,0,0,$_)); } # while (my ($key,$srcf) = each(%course_db)) { next if ($key !~ /^src_(\d+)\.(\d+)$/); my $mapid = $1; my $resid = $2; my $id = $mapid.'.'.$resid; if ($srcf=~/\.(problem|exam|quiz|assess|survey|form)$/) { my $symb= &Apache::lonnet::declutter($course_db{'map_id_'.$mapid}). '___'.$resid.'___'.&Apache::lonnet::declutter($srcf); $assesslist{$symb}='symb:'.&Apache::lonnet::escape($symb).':' .$uname.':'.$udom.':'.$mapid.':'.$resid.':'. $course_db{'title_'.$id}; } } untie(%course_db); # Store away the data $sheet->{'rowlabel'} = \%assesslist; $rowlabel_cache{$identifier}=join('___;___',%{$sheet->{'rowlabel'}}); } } sub get_assess_rowlabels { my ($sheet) = @_; # my %course_db; # my $stype = $sheet->{'sheettype'}; my $uname = $sheet->{'uname'}; my $udom = $sheet->{'udom'}; my $usymb = $sheet->{'usymb'}; # $sheet->{'rowlabel'} = {}; my $identifier =$sheet->{'coursefilename'}.'_'.$stype.'_'.$usymb; # if ($rowlabel_cache{$identifier}) { %{$sheet->{'rowlabel'}}=split(/___;___/,$rowlabel_cache{$identifier}); } else { # Get the data and store it in the cache # Tie hash tie(%course_db,'GDBM_File',$sheet->{'coursefilename'}.'.db', &GDBM_READER(),0640); if (! tied(%course_db)) { return 'Could not access course data'; } # my %parameter_labels= ('timestamp' => 'parameter:Timestamp of Last Transaction
timestamp', 'subnumber' => 'parameter:Number of Submissions
subnumber', 'tutornumber' => 'parameter:Number of Tutor Responses
tutornumber', 'totalpoints' => 'parameter:Total Points Granted
totalpoints'); while (my ($key,$srcf) = each(%course_db)) { next if ($key !~ /^src_(\d+)\.(\d+)$/); my $mapid = $1; my $resid = $2; my $id = $mapid.'.'.$resid; if ($srcf=~/\.(problem|exam|quiz|assess|survey|form)$/) { # Loop through the metadata for this key my @Metadata = split(/,/, &Apache::lonnet::metadata($srcf,'keys')); foreach my $key (@Metadata) { next if ($key !~ /^(stores|parameter)_/); my $display= &Apache::lonnet::metadata($srcf,$key.'.display'); unless ($display) { $display.= &Apache::lonnet::metadata($srcf,$key.'.name'); } $display.='
'.$key; $parameter_labels{$key}='parameter:'.$display; } # end of foreach } } untie(%course_db); # Store away the results $sheet->{'rowlabel'} = \%parameter_labels; $rowlabel_cache{$identifier}=join('___;___',%{$sheet->{'rowlabel'}}); } } sub updatestudentassesssheet { my $sheet = shift; if ($sheet->{'sheettype'} eq 'studentcalc') { &get_student_rowlabels($sheet); } else { &get_assess_rowlabels($sheet); } # Determine if any of the information has changed my %f=&getformulas($sheet); my $changed=0; $sheet->{'maxrow'} = 0; my %existing=(); # Now obsolete rows while (my ($cell, $formula) = each (%f)) { next if ($cell !~ /^A(\d+)/); $sheet->{'maxrow'} = $1 if ($1 > $sheet->{'maxrow'}); my ($usy,$ufn)=split(/__&&&\__/,$formula); $existing{$usy}=1; unless ((exists($sheet->{'rowlabel'}->{$usy}) && (defined($sheet->{'rowlabel'}->{$usy})) || (!$1) || ($formula =~ /^(~~~|---)/) )) { $f{$_}='!!! Obsolete'; $changed=1; } elsif ($ufn) { # I do not think this works any more $sheet->{'rowlabel'}->{$usy} =~s/assesscalc\?usymb\=/assesscalc\?ufn\=$ufn&\usymb\=/; } } # New and unknown keys foreach (keys(%{$sheet->{'rowlabel'}})) { unless ($existing{$_}) { $changed=1; $sheet->{'maxrow'}++; $f{'A'.$sheet->{'maxrow'}}=$_; } } if ($changed) { $sheet->{'f'} = \%f; &setformulas($sheet); } } # ------------------------------------------------ Load data for one assessment sub loadstudent{ my ($sheet,$r,$c)=@_; my %constants=(); my %formulas=&getformulas($sheet); $cachedassess=$sheet->{'uname'}.':'.$sheet->{'udom'}; # Get ALL the student preformance data my @tmp = &Apache::lonnet::dump($sheet->{'cid'}, $sheet->{'udom'}, $sheet->{'uname'}, undef); if ($tmp[0] !~ /^error:/) { %cachedstores = @tmp; } undef @tmp; # my @assessdata=(); while (my ($cell,$value) = each (%formulas)) { if(defined($c) && ($c->aborted())) { last; } next if ($cell !~ /^A(\d+)/); my $row=$1; next if (($value =~ /^[!~-]/) || ($row==0)); my ($usy,$ufn)=split(/__&&&\__/,$value); @assessdata=&exportsheet($sheet,$sheet->{'uname'}, $sheet->{'udom'}, 'assesscalc',$usy,$ufn,$r); my $index=0; foreach ('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') { if (defined($assessdata[$index])) { my $col=$_; if ($assessdata[$index]=~/\D/) { $constants{$col.$row}="'".$assessdata[$index]."'"; } else { $constants{$col.$row}=$assessdata[$index]; } unless ($col eq 'A') { $formulas{$col.$row}='import'; } } $index++; } } $cachedassess=''; undef %cachedstores; $sheet->{'f'} = \%formulas; &setformulas($sheet); &setconstants($sheet,\%constants); } # --------------------------------------------------- Load data for one student # sub loadcourse { my ($sheet,$r,$c)=@_; # my %constants=(); my %formulas=&getformulas($sheet); # my $total=0; foreach (keys(%formulas)) { if ($_=~/^A(\d+)/) { unless ($formulas{$_}=~/^[\!\~\-]/) { $total++; } } } my $now=0; my $since=time; $r->print(< popwin=open('','popwin','width=400,height=100'); popwin.document.writeln(''+ '

Spreadsheet Calculation Progress

'+ '
'+ '
'+ ''); popwin.document.close(); ENDPOP $r->rflush(); foreach (keys(%formulas)) { if(defined($c) && ($c->aborted())) { last; } next if ($_!~/^A(\d+)/); my $row=$1; next if (($formulas{$_}=~/^[\!\~\-]/) || ($row==0)); my ($sname,$sdom) = split(':',$formulas{$_}); my @studentdata=&exportsheet($sheet,$sname,$sdom,'studentcalc', undef,undef,$r); undef %userrdatas; $now++; $r->print(''); $r->rflush(); # my $index=0; foreach ('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') { if (defined($studentdata[$index])) { my $col=$_; if ($studentdata[$index]=~/\D/) { $constants{$col.$row}="'".$studentdata[$index]."'"; } else { $constants{$col.$row}=$studentdata[$index]; } unless ($col eq 'A') { $formulas{$col.$row}='import'; } } $index++; } } $sheet->{'f'}=\%formulas; &setformulas($sheet); &setconstants($sheet,\%constants); $r->print(''); $r->rflush(); } # ------------------------------------------------ Load data for one assessment # sub loadassessment { my ($sheet,$r,$c)=@_; my $uhome = $sheet->{'uhome'}; my $uname = $sheet->{'uname'}; my $udom = $sheet->{'udom'}; my $symb = $sheet->{'usymb'}; my $cid = $sheet->{'cid'}; my $cnum = $sheet->{'cnum'}; my $cdom = $sheet->{'cdom'}; my $chome = $sheet->{'chome'}; my $namespace; unless ($namespace=$cid) { return ''; } # Get stored values my %returnhash=(); if ($cachedassess eq $uname.':'.$udom) { # # get data out of the dumped stores # my $version=$cachedstores{'version:'.$symb}; my $scope; for ($scope=1;$scope<=$version;$scope++) { foreach (split(/\:/,$cachedstores{$scope.':keys:'.$symb})) { $returnhash{$_}=$cachedstores{$scope.':'.$symb.':'.$_}; } } } else { # # restore individual # %returnhash = &Apache::lonnet::restore($symb,$namespace,$udom,$uname); for (my $version=1;$version<=$returnhash{'version'};$version++) { foreach (split(/\:/,$returnhash{$version.':keys'})) { $returnhash{$_}=$returnhash{$version.':'.$_}; } } } # # returnhash now has all stores for this resource # convert all "_" to "." to be able to use libraries, multiparts, etc # # This is dumb. It is also necessary :( my @oldkeys=keys %returnhash; # foreach my $name (@oldkeys) { my $value=$returnhash{$name}; delete $returnhash{$name}; $name=~s/\_/\./g; $returnhash{$name}=$value; } # initialize coursedata and userdata for this user undef %courseopt; undef %useropt; my $userprefix=$uname.'_'.$udom.'_'; unless ($uhome eq 'no_host') { # Get coursedata unless ((time-$courserdatas{$cid.'.last_cache'})<240) { my %Tmp = &Apache::lonnet::dump('resourcedata',$cdom,$cnum); $courserdatas{$cid}=\%Tmp; $courserdatas{$cid.'.last_cache'}=time; } while (my ($name,$value) = each(%{$courserdatas{$cid}})) { $courseopt{$userprefix.$name}=$value; } # Get userdata (if present) unless ((time-$userrdatas{$uname.'@'.$udom.'.last_cache'})<240) { my %Tmp = &Apache::lonnet::dump('resourcedata',$udom,$uname); $userrdatas{$cid} = \%Tmp; # Most of the time the user does not have a 'resourcedata.db' # file. We need to cache that we got nothing instead of bothering # with requesting it every time. $userrdatas{$uname.'@'.$udom.'.last_cache'}=time; } while (my ($name,$value) = each(%{$userrdatas{$cid}})) { $useropt{$userprefix.$name}=$value; } } # now courseopt, useropt initialized for this user and course # (used by parmval) # # Load keys for this assessment only # my %thisassess=(); my ($symap,$syid,$srcf)=split(/\_\_\_/,$symb); foreach (split(/\,/,&Apache::lonnet::metadata($srcf,'keys'))) { $thisassess{$_}=1; } # # Load parameters # my %c=(); if (tie(%parmhash,'GDBM_File', $sheet->{'coursefilename'}.'_parms.db',&GDBM_READER(),0640)) { my %f=&getformulas($sheet); foreach my $cell (keys(%f)) { next if ($cell !~ /^A/); next if ($f{$cell} =~/^[\!\~\-]/); if ($f{$cell}=~/^parameter/) { if (defined($thisassess{$f{$cell}})) { my $val = &parmval($f{$cell},$sheet); $c{$cell} = $val; $c{$f{$cell}} = $val; } } else { my $key=$f{$cell}; my $ckey=$key; $key=~s/^stores\_/resource\./; $key=~s/\_/\./g; $c{$cell}=$returnhash{$key}; $c{$ckey}=$returnhash{$key}; } } untie(%parmhash); } &setconstants($sheet,\%c); } # --------------------------------------------------------- Various form fields sub textfield { my ($title,$name,$value)=@_; return "\n

$title:
". ''; } sub hiddenfield { my ($name,$value)=@_; return "\n".''; } sub selectbox { my ($title,$name,$value,%options)=@_; my $selout="\n

$title:
".''; } # =============================================== Update information in a sheet # # Add new users or assessments, etc. # sub updatesheet { my ($sheet)=@_; if ($sheet->{'sheettype'} eq 'classcalc') { return &updateclasssheet($sheet); } else { return &updatestudentassesssheet($sheet); } } # =================================================== Load the rows for a sheet # # Import the data for rows # sub loadrows { my ($sheet,$r)=@_; my $c = $r->connection; my $stype=$sheet->{'sheettype'}; if ($stype eq 'classcalc') { &loadcourse($sheet,$r,$c); } elsif ($stype eq 'studentcalc') { &loadstudent($sheet,$r,$c); } else { &loadassessment($sheet,$r,$c); } } # ======================================================= Forced recalculation? sub checkthis { my ($keyname,$time)=@_; return ($time<$expiredates{$keyname}); } sub forcedrecalc { my ($uname,$udom,$stype,$usymb)=@_; my $key=$uname.':'.$udom.':'.$stype.':'.$usymb; my $time=$oldsheets{$key.'.time'}; if ($ENV{'form.forcerecalc'}) { return 1; } unless ($time) { return 1; } if ($stype eq 'assesscalc') { my $map=(split(/___/,$usymb))[0]; if (&checkthis('::assesscalc:',$time) || &checkthis('::assesscalc:'.$map,$time) || &checkthis('::assesscalc:'.$usymb,$time) || &checkthis($uname.':'.$udom.':assesscalc:',$time) || &checkthis($uname.':'.$udom.':assesscalc:'.$map,$time) || &checkthis($uname.':'.$udom.':assesscalc:'.$usymb,$time)) { return 1; } } else { if (&checkthis('::studentcalc:',$time) || &checkthis($uname.':'.$udom.':studentcalc:',$time)) { return 1; } } return 0; } # ============================================================== Export handler # exportsheet # returns the export row for a spreadsheet. # sub exportsheet { my ($sheet,$uname,$udom,$stype,$usymb,$fn,$r)=@_; $uname = $uname || $sheet->{'uname'}; $udom = $udom || $sheet->{'udom'}; $stype = $stype || $sheet->{'sheettype'}; my @exportarr=(); if (defined($usymb) && ($usymb=~/^\_(\w+)/) && (!defined($fn) || $fn eq '')) { $fn='default_'.$1; } # # Check if cached # my $key=$uname.':'.$udom.':'.$stype.':'.$usymb; my $found=''; if ($oldsheets{$key}) { foreach (split(/___&\___/,$oldsheets{$key})) { my ($name,$value)=split(/___=___/,$_); if ($name eq $fn) { $found=$value; } } } unless ($found) { &cachedssheets($sheet,$uname,$udom); if ($oldsheets{$key}) { foreach (split(/___&\___/,$oldsheets{$key})) { my ($name,$value)=split(/___=___/,$_); if ($name eq $fn) { $found=$value; } } } } # # Check if still valid # if ($found) { if (&forcedrecalc($uname,$udom,$stype,$usymb)) { $found=''; } } if ($found) { # # Return what was cached # @exportarr=split(/___;___/,$found); return @exportarr; } # # Not cached # my ($newsheet)=&makenewsheet($uname,$udom,$stype,$usymb); &readsheet($newsheet,$fn); &updatesheet($newsheet); &loadrows($newsheet,$r); &calcsheet($newsheet); @exportarr=&exportdata($newsheet); ## ## Store now ## # # load in the old value # my %currentlystored=(); if ($stype eq 'studentcalc') { my @tmp = &Apache::lonnet::get('nohist_calculatedsheets', [$key], $sheet->{'cdom'},$sheet->{'cnum'}); if ($tmp[0]!~/^error/) { %currentlystored = @tmp; } } else { my @tmp = &Apache::lonnet::get('nohist_calculatedsheets_'. $sheet->{'cid'},[$key], $sheet->{'udom'},$sheet->{'uname'}); if ($tmp[0]!~/^error/) { %currentlystored = @tmp; } } # # Add the new line # $currentlystored{$fn}=join('___;___',@exportarr); # # Stick everything back together # my $newstore=''; foreach (keys(%currentlystored)) { if ($newstore) { $newstore.='___&___'; } $newstore.=$_.'___=___'.$currentlystored{$_}; } my $now=time; # # Store away the new value # if ($stype eq 'studentcalc') { &Apache::lonnet::put('nohist_calculatedsheets', { $key => $newstore, $key.time => $now }, $sheet->{'cdom'},$sheet->{'cnum'}); } else { &Apache::lonnet::put('nohist_calculatedsheets_'.$sheet->{'cid'}, { $key => $newstore, $key.time => $now }, $sheet->{'udom'}, $sheet->{'uname'}) } return @exportarr; } # ============================================================ Expiration Dates # # Load previously cached student spreadsheets for this course # sub load_spreadsheet_expirationdates { undef %expiredates; my $cid=$ENV{'request.course.id'}; my @tmp = &Apache::lonnet::dump('nohist_expirationdates', $ENV{'course.'.$cid.'.domain'}, $ENV{'course.'.$cid.'.num'}); if (lc($tmp[0]) !~ /^error/){ %expiredates = @tmp; } } # ===================================================== Calculated sheets cache # # Load previously cached student spreadsheets for this course # sub cachedcsheets { my $cid=$ENV{'request.course.id'}; my @tmp = &Apache::lonnet::dump('nohist_calculatedsheets', $ENV{'course.'.$cid.'.domain'}, $ENV{'course.'.$cid.'.num'}); if ($tmp[0] !~ /^error/) { my %StupidTempHash = @tmp; while (my ($key,$value) = each %StupidTempHash) { $oldsheets{$key} = $value; } } } # ===================================================== Calculated sheets cache # # Load previously cached assessment spreadsheets for this student # sub cachedssheets { my ($sheet,$uname,$udom) = @_; $uname = $uname || $sheet->{'uname'}; $udom = $udom || $sheet->{'udom'}; if (! $loadedcaches{$uname.'_'.$udom}) { my @tmp = &Apache::lonnet::dump('nohist_calculatedsheets', $sheet->{'udom'}, $sheet->{'uname'}); if ($tmp[0] !~ /^error/) { my %TempHash = @tmp; my $count = 0; while (my ($key,$value) = each %TempHash) { $oldsheets{$key} = $value; $count++; } $loadedcaches{$sheet->{'uname'}.'_'.$sheet->{'udom'}}=1; } } } # ===================================================== Calculated sheets cache # # Load previously cached assessment spreadsheets for this student # # ================================================================ Main handler # # Interactive call to screen # # sub handler { my $r=shift; my ($sheettype) = ($r->uri=~/\/(\w+)$/); if (! exists($ENV{'form.Status'})) { $ENV{'form.Status'} = 'Active'; } if ( ! exists($ENV{'form.output'}) || ($sheettype ne 'classcalc' && lc($ENV{'form.output'}) eq 'recursive excel')) { $ENV{'form.output'} = 'HTML'; } # # Overload checking # # Check this server my $loaderror=&Apache::lonnet::overloaderror($r); if ($loaderror) { return $loaderror; } # Check the course homeserver $loaderror= &Apache::lonnet::overloaderror($r, $ENV{'course.'.$ENV{'request.course.id'}.'.home'}); if ($loaderror) { return $loaderror; } # # HTML Header # 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/'; # # Roles Checking # # Needs to be in a course if (! $ENV{'request.course.fn'}) { # Not in a course, or not allowed to modify parms $ENV{'user.error.msg'}= $r->uri.":opa:0:0:Cannot modify spreadsheet"; return HTTP_NOT_ACCEPTABLE; } # # Get query string for limited number of parameters # &Apache::loncommon::get_unprocessed_cgi ($ENV{'QUERY_STRING'},['uname','udom','usymb','ufn','mapid','resid']); # # Deal with restricted student permissions # if ($ENV{'request.role'} =~ /^st\./) { delete $ENV{'form.unewfield'} if (exists($ENV{'form.unewfield'})); delete $ENV{'form.unewformula'} if (exists($ENV{'form.unewformula'})); } # # Clean up symb and spreadsheet filename # if (($ENV{'form.usymb'}=~/^\_(\w+)/) && (!$ENV{'form.ufn'})) { $ENV{'form.ufn'}='default_'.$1; } # # Interactive loading of specific sheet? # if (($ENV{'form.load'}) && ($ENV{'form.loadthissheet'} ne 'Default')) { $ENV{'form.ufn'}=$ENV{'form.loadthissheet'}; } # # Determine the user name and domain for the sheet. my $aname; my $adom; unless ($ENV{'form.uname'}) { $aname=$ENV{'user.name'}; $adom=$ENV{'user.domain'}; } else { $aname=$ENV{'form.uname'}; $adom=$ENV{'form.udom'}; } # # Open page, try to prevent browser cache. # $r->content_type('text/html'); $r->header_out('Cache-control','no-cache'); $r->header_out('Pragma','no-cache'); $r->send_http_header; # # Header.... # $r->print('LON-CAPA Spreadsheet'); my $nothing = "''"; if ($ENV{'browser.type'} eq 'explorer') { $nothing = "'javascript:void(0);'"; } if ($ENV{'request.role'} !~ /^st\./) { $r->print(< var editwin; function celledit(cellname,cellformula) { var edit_text = ''; edit_text +='Cell Edit Window'; edit_text += '

'; edit_text += '

Cell '+cellname+'

'; edit_text += '