--- loncom/interface/Attic/lonspreadsheet.pm 2002/01/22 10:05:24 1.79 +++ loncom/interface/Attic/lonspreadsheet.pm 2002/07/30 21:23:44 1.98 @@ -1,5 +1,5 @@ # -# $Id: lonspreadsheet.pm,v 1.79 2002/01/22 10:05:24 matthew Exp $ +# $Id: lonspreadsheet.pm,v 1.98 2002/07/30 21:23:44 matthew Exp $ # # Copyright Michigan State University Board of Trustees # @@ -34,6 +34,32 @@ # 07/09,07/14,07/21,09/01,09/10,9/11,9/12,9/13,9/14,9/17, # 10/16,10/17,10/20,11/05,11/28,12/27 Gerd Kortemeyer # 01/14/02 Matthew +# 02/04/02 Matthew + +# 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; @@ -45,7 +71,7 @@ use Apache::lonnet; use Apache::Constants qw(:common :http); use GDBM_File; use HTML::TokeParser; - +use Apache::lonhtmlcommon; # # Caches for previously calculated spreadsheets # @@ -80,6 +106,14 @@ 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; @@ -96,6 +130,7 @@ sub initsheet { $safeeval->permit("sort"); $safeeval->deny(":base_io"); $safehole->wrap(\&Apache::lonnet::EXT,$safeeval,'&EXT'); + $safeeval->share('$@'); my $code=<<'ENDDEFS'; # ---------------------------------------------------- Inside of the safe space @@ -139,6 +174,10 @@ $cfn=''; $usymb=''; +# error messages + +$errormsg=''; + sub mask { my ($lower,$upper)=@_; @@ -215,6 +254,282 @@ sub mask { return '^'.$alpha.$num."\$"; } +#------------------------------------------------------- + +=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(%v); + @Keys = $v{@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(%v); + @Values =$v{@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 = $v{$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(%v); + @Keys = $v{@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(%v); + @Values =$v{@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(%v))}}+1; @@ -234,6 +549,15 @@ sub BIN { } +#------------------------------------------------------- + +=item SUM(range) + +returns the sum of items in the range. + +=cut + +#------------------------------------------------------- sub SUM { my $mask=mask(@_); my $sum=0; @@ -243,6 +567,15 @@ sub SUM { 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; @@ -257,6 +590,15 @@ sub MEAN { } } +#------------------------------------------------------- + +=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; @@ -273,6 +615,15 @@ sub STDDEV { 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; @@ -282,6 +633,15 @@ sub PROD { return $prod; } +#------------------------------------------------------- + +=item MAX(range) + +compute the maximum of the items in the range. + +=cut + +#------------------------------------------------------- sub MAX { my $mask=mask(@_); my $max='-'; @@ -292,6 +652,15 @@ sub MAX { return $max; } +#------------------------------------------------------- + +=item MIN(range) + +compute the minimum of the items in the range. + +=cut + +#------------------------------------------------------- sub MIN { my $mask=mask(@_); my $min='-'; @@ -302,12 +671,22 @@ sub MIN { 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(%v)) { - $inside[$#inside+1]=$v{$_}; + push (@inside,$v{$_}); } @inside=sort(@inside); my $sum=0; my $i; @@ -317,6 +696,16 @@ sub SUMMAX { 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); @@ -364,7 +753,25 @@ sub expandnamed { return 0; } } else { - return '$c{\''.$expression.'\'}'; + # 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 ($#matches == 0) { + $returnvalue = '$c{\''.$matches[0].'\'}'; + } else { + $returnvalue = "'bad parameter name : $expression'"; + } + return $returnvalue; } } @@ -376,6 +783,8 @@ sub sett { } else { $pattern='[A-Z]'; } + +# Deal with the template row foreach (keys(%f)) { if ($_=~/template\_(\w)/) { my $col=$1; @@ -384,11 +793,17 @@ sub sett { if ($_=~/A(\d+)/) { my $trow=$1; 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\$v\{\'$2\'\}/g; + # Replace parameters $t{$lb}=~s/(^|[^\"\'])\[([^\]]+)\]/$1.&expandnamed($2)/ge; } } @@ -396,6 +811,8 @@ sub sett { } } } + +# Deal with the normal cells foreach (keys(%f)) { if (($f{$_}) && ($_!~/template\_/)) { my $matches=($_=~/^$pattern(\d+)/); @@ -411,6 +828,23 @@ sub sett { } } } +# 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\$v\{\'$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\$v\{\'$2\'\}/g; @@ -418,25 +852,26 @@ sub sett { } sub calc { - %v=(); + undef %v; &sett(); my $notfinished=1; + my $lastcalc=''; my $depth=0; while ($notfinished) { $notfinished=0; foreach (keys(%t)) { my $old=$v{$_}; - $v{$_}=eval($t{$_}); + $v{$_}=eval $t{$_}; if ($@) { - %v=(); - return $@; + undef %v; + return $_.': '.$@; } - if ($v{$_} ne $old) { $notfinished=1; } + if ($v{$_} ne $old) { $notfinished=1; $lastcalc=$_; } } $depth++; if ($depth>100) { - %v=(); - return 'Maximum calculation depth exceeded'; + undef %v; + return $lastcalc.': Maximum calculation depth exceeded'; } } return ''; @@ -461,9 +896,11 @@ sub outrowassess { my @cols=(); if ($n) { my ($usy,$ufn)=split(/\_\_\&\&\&\_\_/,$f{'A'.$n}); + if ($rl{$usy}) { $cols[0]=$rl{$usy}.'
'. 'print(' checked'); } $r->print('>'); } + +# ------------------------------------------------------------------ Insertrows + $r->print(' Student Status: '. + &Apache::lonhtmlcommon::StatusOptions + ($ENV{'form.Status'},'sheet')); + + $r->print(< + +
+ENDINSERTBUTTONS + # ------------------------------------------------------------- Print out sheet &outsheet($r,$asheet);