Annotation of loncom/interface/spreadsheet/Spreadsheet.pm, revision 1.31
1.1 matthew 1: #
1.31 ! matthew 2: # $Id: Spreadsheet.pm,v 1.30 2003/11/17 19:55:41 matthew Exp $
1.1 matthew 3: #
4: # Copyright Michigan State University Board of Trustees
5: #
6: # This file is part of the LearningOnline Network with CAPA (LON-CAPA).
7: #
8: # LON-CAPA is free software; you can redistribute it and/or modify
9: # it under the terms of the GNU General Public License as published by
10: # the Free Software Foundation; either version 2 of the License, or
11: # (at your option) any later version.
12: #
13: # LON-CAPA is distributed in the hope that it will be useful,
14: # but WITHOUT ANY WARRANTY; without even the implied warranty of
15: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16: # GNU General Public License for more details.
17: #
18: # You should have received a copy of the GNU General Public License
19: # along with LON-CAPA; if not, write to the Free Software
20: # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21: #
22: # /home/httpd/html/adm/gpl.txt
23: #
24: # http://www.lon-capa.org/
25: #
26: # The LearningOnline Network with CAPA
27: # Spreadsheet/Grades Display Handler
28: #
29: # POD required stuff:
30:
31: =head1 NAME
32:
33: Spreadsheet
34:
35: =head1 SYNOPSIS
36:
37: =head1 DESCRIPTION
38:
39: =over 4
40:
41: =cut
42:
43: ###################################################
44: ###################################################
45: ### Spreadsheet ###
46: ###################################################
47: ###################################################
48: package Apache::Spreadsheet;
49:
50: use strict;
1.24 matthew 51: #use warnings FATAL=>'all';
52: #no warnings 'uninitialized';
1.1 matthew 53: use Apache::Constants qw(:common :http);
54: use Apache::lonnet;
55: use Safe;
56: use Safe::Hole;
57: use Opcode;
58: use HTML::Entities();
59: use HTML::TokeParser;
60: use Spreadsheet::WriteExcel;
61: use Time::HiRes;
1.28 www 62: use Apache::lonlocal;
1.1 matthew 63:
64: ##
65: ## Package Variables
66: ##
67: my %expiredates;
68:
69: my @UC_Columns = split(//,'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
70: my @LC_Columns = split(//,'abcdefghijklmnopqrstuvwxyz');
71:
72: ######################################################
73:
74: =pod
75:
76: =item &new
77:
78: Returns a new spreadsheet object.
79:
80: =cut
81:
82: ######################################################
83: sub new {
84: my $this = shift;
85: my $class = ref($this) || $this;
86: my ($stype) = ($class =~ /Apache::(.*)$/);
87: #
88: my ($name,$domain,$filename,$usymb)=@_;
89: #
90: my $self = {
91: name => $name,
92: domain => $domain,
93: type => $stype,
94: symb => $usymb,
95: errorlog => '',
1.22 matthew 96: maxrow => 0,
1.1 matthew 97: cid => $ENV{'request.course.id'},
98: cnum => $ENV{'course.'.$ENV{'request.course.id'}.'.num'},
99: cdom => $ENV{'course.'.$ENV{'request.course.id'}.'.domain'},
100: chome => $ENV{'course.'.$ENV{'request.course.id'}.'.home'},
101: coursedesc => $ENV{'course.'.$ENV{'request.course.id'}.'.description'},
102: coursefilename => $ENV{'request.course.fn'},
1.12 matthew 103: #
104: # Flags
105: temporary => 0, # true if this sheet has been modified but not saved
106: new_rows => 0, # true if this sheet has new rows
1.1 matthew 107: #
1.3 matthew 108: # blackout is used to determine if any data needs to be hidden from the
109: # student.
110: blackout => 0,
111: #
1.1 matthew 112: # Data storage
113: formulas => {},
114: constants => {},
115: rows => [],
116: row_source => {},
117: othersheets => [],
118: };
119: #
120: $self->{'uhome'} = &Apache::lonnet::homeserver($name,$domain);
121: #
122: bless($self,$class);
123: #
124: # Load in the spreadsheet definition
125: $self->filename($filename);
126: if (exists($ENV{'form.workcopy'}) &&
127: $self->{'type'} eq $ENV{'form.workcopy'}) {
128: $self->load_tmp();
129: } else {
130: $self->load();
131: }
132: return $self;
133: }
134:
135: ######################################################
136:
137: =pod
138:
139: =item &filename
140:
141: get or set the filename for a spreadsheet.
142:
143: =cut
144:
145: ######################################################
146: sub filename {
147: my $self = shift();
148: if (@_) {
149: my ($newfilename) = @_;
150: if (! defined($newfilename) || $newfilename eq 'Default' ||
1.13 matthew 151: $newfilename !~ /\w/ || $newfilename eq '') {
152: my $key = 'course.'.$self->{'cid'}.'.spreadsheet_default_'.
153: $self->{'type'};
154: if (exists($ENV{$key}) && $ENV{$key} ne '') {
155: $newfilename = $ENV{$key};
156: } else {
157: $newfilename = 'default_'.$self->{'type'};
1.1 matthew 158: }
1.13 matthew 159: }
160: if ($newfilename !~ /\w/ || $newfilename =~ /^\W*$/) {
161: $newfilename = 'default_'.$self->{'type'};
162: }
163: if ($newfilename !~ /^default\.$self->{'type'}$/ ) {
164: if ($newfilename !~ /_$self->{'type'}$/) {
165: $newfilename =~ s/[\s_]*$//;
1.1 matthew 166: $newfilename .= '_'.$self->{'type'};
167: }
168: }
169: $self->{'filename'} = $newfilename;
170: return;
171: }
172: return $self->{'filename'};
173: }
174:
175: ######################################################
176:
177: =pod
178:
179: =item &make_default()
180:
181: Make the current spreadsheet file the default for the course. Expires all the
182: default spreadsheets.......!
183:
184: =cut
185:
186: ######################################################
187: sub make_default {
188: my $self = shift();
189: my $result = &Apache::lonnet::put('environment',
1.13 matthew 190: {'spreadsheet_default_'.$self->{'type'} => $self->filename()},
1.1 matthew 191: $self->{'cdom'},$self->{'cnum'});
192: return $result if ($result ne 'ok');
193: my $symb = $self->{'symb'};
194: $symb = '' if (! defined($symb));
195: &Apache::lonnet::expirespread('','',$self->{'type'},$symb);
196: }
197:
198: ######################################################
199:
200: =pod
201:
202: =item &is_default()
203:
204: Returns 1 if the current spreadsheet is the default as specified in the
205: course environment. Returns 0 otherwise.
206:
207: =cut
208:
209: ######################################################
210: sub is_default {
211: my $self = shift;
212: # Check to find out if we are the default spreadsheet (filenames match)
213: my $default_filename = '';
214: my %tmphash = &Apache::lonnet::get('environment',
215: ['spreadsheet_default_'.
216: $self->{'type'}],
217: $self->{'cdom'},
218: $self->{'cnum'});
219: my ($tmp) = keys(%tmphash);
220: if ($tmp !~ /^(con_lost|error|no_such_host)/i) {
221: $default_filename = $tmphash{'spreadsheet_default_'.$self->{'type'}};
222: }
1.13 matthew 223: if ($default_filename =~ /^\s*$/) {
224: $default_filename = 'default_'.$self->{'type'};
225: }
1.1 matthew 226: return 1 if ($self->filename() eq $default_filename);
227: return 0;
1.11 matthew 228: }
229:
230: sub initialize {
231: # This method is here to remind you that it will be overridden by
232: # the descendents of the spreadsheet class.
1.1 matthew 233: }
234:
1.23 matthew 235: sub clear_package {
236: # This method is here to remind you that it will be overridden by
237: # the descendents of the spreadsheet class.
238: }
239:
240: sub cleanup {
241: my $self = shift();
242: $self->clear_package();
243: }
244:
1.1 matthew 245: sub initialize_spreadsheet_package {
246: &load_spreadsheet_expirationdates();
247: &clear_spreadsheet_definition_cache();
248: }
249:
250: sub load_spreadsheet_expirationdates {
251: undef %expiredates;
252: my $cid=$ENV{'request.course.id'};
253: my @tmp = &Apache::lonnet::dump('nohist_expirationdates',
254: $ENV{'course.'.$cid.'.domain'},
255: $ENV{'course.'.$cid.'.num'});
256: if (lc($tmp[0]) !~ /^error/){
257: %expiredates = @tmp;
258: }
259: }
260:
261: sub check_expiration_time {
262: my $self = shift;
263: my ($time)=@_;
1.22 matthew 264: return 0 if (! defined($time));
1.19 matthew 265: my ($key1,$key2,$key3,$key4,$key5);
266: # Description of keys
267: #
268: # key1: all sheets of this type have expired
269: # key2: all sheets of this type for this student
270: # key3: all sheets of this type in this map for this student
271: # key4: this assessment sheet for this student
272: # key5: this assessment sheet for all students
1.1 matthew 273: $key1 = '::'.$self->{'type'}.':';
274: $key2 = $self->{'name'}.':'.$self->{'domain'}.':'.$self->{'type'}.':';
275: $key3 = $key2.$self->{'container'} if (defined($self->{'container'}));
1.19 matthew 276: $key4 = $key2.$self->{'symb'} if (defined($self->{'symb'}));
277: $key5 = $key1.$self->{'symb'} if (defined($self->{'symb'}));
278: my $returnvalue = 1; # default to okay
279: foreach my $key ($key1,$key2,$key3,$key4,$key5) {
1.1 matthew 280: next if (! defined($key));
1.19 matthew 281: if (exists($expiredates{$key}) && $expiredates{$key} > $time) {
282: $returnvalue = 0; # need to recompute
1.1 matthew 283: }
284: }
1.19 matthew 285: return $returnvalue;
1.1 matthew 286: }
287:
288: ######################################################
289:
290: =pod
291:
292: =item &initialize_safe_space
293:
294: Returns the safe space required by a Spreadsheet object.
295:
296: =head 2 Safe Space Functions
297:
298: =over 4
299:
300: =cut
301:
302: ######################################################
1.25 matthew 303: {
304:
305: my $safeeval;
306:
1.1 matthew 307: sub initialize_safe_space {
1.25 matthew 308: my $self = shift;
309: if (! defined($safeeval)) {
310: $safeeval = new Safe(shift);
311: my $safehole = new Safe::Hole;
312: $safeeval->permit("entereval");
313: $safeeval->permit(":base_math");
314: $safeeval->permit("sort");
315: $safeeval->deny(":base_io");
316: $safehole->wrap(\&Apache::lonnet::EXT,$safeeval,'&EXT');
317: $safehole->wrap(\&mask,$safeeval,'&mask');
318: $safeeval->share('$@');
319: my $code=<<'ENDDEFS';
1.1 matthew 320: # ---------------------------------------------------- Inside of the safe space
321: #
322: # f: formulas
323: # t: intermediate format (variable references expanded)
324: # v: output values
325: # c: preloaded constants (A-column)
326: # rl: row label
327: # os: other spreadsheets (for student spreadsheet only)
328: undef %sheet_values; # Holds the (computed, final) values for the sheet
329: # This is only written to by &calc, the spreadsheet computation routine.
330: # It is read by many functions
331: undef %t; # Holds the values of the spreadsheet temporarily. Set in &sett,
332: # which does the translation of strings like C5 into the value in C5.
333: # Used in &calc - %t holds the values that are actually eval'd.
334: undef %f; # Holds the formulas for each cell. This is the users
335: # (spreadsheet authors) data for each cell.
336: undef %c; # Holds the constants for a sheet. In the assessment
337: # sheets, this is the A column. Used in &MINPARM, &MAXPARM, &expandnamed,
338: # &sett, and &constants. There is no &getconstants.
339: # &constants is called by &loadstudent, &loadcourse, &load assessment,
340: undef @os; # Holds the names of other spreadsheets - this is used to specify
341: # the spreadsheets that are available for the assessment sheet.
342: # Set by &setothersheets. &setothersheets is called by &handler. A
343: # related subroutine is &othersheets.
344: $errorlog = '';
345: #
346: $maxrow = 0;
347: $type = '';
348: #
349: # filename/reference of the sheet
350: $filename = '';
351: #
352: # user data
353: $name = '';
354: $uhome = '';
355: $domain = '';
356: #
357: # course data
358: $csec = '';
359: $chome= '';
360: $cnum = '';
361: $cdom = '';
362: $cid = '';
363: $coursefilename = '';
364: #
365: # symb
366: $usymb = '';
367: #
368: # error messages
369: $errormsg = '';
370: #
371: #-------------------------------------------------------
372:
373: =pod
374:
375: =item NUM(range)
376:
377: returns the number of items in the range.
378:
379: =cut
380:
381: #-------------------------------------------------------
382: sub NUM {
383: my $mask=&mask(@_);
1.26 matthew 384: my $num= $#{@{grep(eval("/$mask/"),keys(%sheet_values))}}+1;
1.1 matthew 385: return $num;
386: }
387:
388: #-------------------------------------------------------
389:
390: =pod
391:
392: =item BIN(low,high,lower,upper)
393:
394: =cut
395:
396: #-------------------------------------------------------
397: sub BIN {
398: my ($low,$high,$lower,$upper)=@_;
399: my $mask=&mask($lower,$upper);
400: my $num=0;
1.26 matthew 401: foreach (grep eval("/$mask/"),keys(%sheet_values)) {
1.1 matthew 402: if (($sheet_values{$_}>=$low) && ($sheet_values{$_}<=$high)) {
403: $num++;
404: }
405: }
406: return $num;
407: }
408:
409: #-------------------------------------------------------
410:
411: =pod
412:
413: =item SUM(range)
414:
415: returns the sum of items in the range.
416:
417: =cut
418:
419: #-------------------------------------------------------
420: sub SUM {
421: my $mask=&mask(@_);
422: my $sum=0;
1.26 matthew 423: foreach (grep eval("/$mask/"),keys(%sheet_values)) {
1.1 matthew 424: $sum+=$sheet_values{$_};
425: }
426: return $sum;
427: }
428:
429: #-------------------------------------------------------
430:
431: =pod
432:
433: =item MEAN(range)
434:
435: compute the average of the items in the range.
436:
437: =cut
438:
439: #-------------------------------------------------------
440: sub MEAN {
441: my $mask=&mask(@_);
442: my $sum=0;
443: my $num=0;
1.26 matthew 444: foreach (grep eval("/$mask/"),keys(%sheet_values)) {
1.1 matthew 445: $sum+=$sheet_values{$_};
446: $num++;
447: }
448: if ($num) {
449: return $sum/$num;
450: } else {
451: return undef;
452: }
453: }
454:
455: #-------------------------------------------------------
456:
457: =pod
458:
459: =item STDDEV(range)
460:
461: compute the standard deviation of the items in the range.
462:
463: =cut
464:
465: #-------------------------------------------------------
466: sub STDDEV {
467: my $mask=&mask(@_);
468: my $sum=0; my $num=0;
1.26 matthew 469: foreach (grep eval("/$mask/"),keys(%sheet_values)) {
1.1 matthew 470: $sum+=$sheet_values{$_};
471: $num++;
472: }
473: unless ($num>1) { return undef; }
474: my $mean=$sum/$num;
475: $sum=0;
1.26 matthew 476: foreach (grep eval("/$mask/"),keys(%sheet_values)) {
1.1 matthew 477: $sum+=($sheet_values{$_}-$mean)**2;
478: }
479: return sqrt($sum/($num-1));
480: }
481:
482: #-------------------------------------------------------
483:
484: =pod
485:
486: =item PROD(range)
487:
488: compute the product of the items in the range.
489:
490: =cut
491:
492: #-------------------------------------------------------
493: sub PROD {
494: my $mask=&mask(@_);
495: my $prod=1;
1.26 matthew 496: foreach (grep eval("/$mask/"),keys(%sheet_values)) {
1.1 matthew 497: $prod*=$sheet_values{$_};
498: }
499: return $prod;
500: }
501:
502: #-------------------------------------------------------
503:
504: =pod
505:
506: =item MAX(range)
507:
508: compute the maximum of the items in the range.
509:
510: =cut
511:
512: #-------------------------------------------------------
513: sub MAX {
514: my $mask=&mask(@_);
515: my $max='-';
1.26 matthew 516: foreach (grep eval("/$mask/"),keys(%sheet_values)) {
1.1 matthew 517: unless ($max) { $max=$sheet_values{$_}; }
518: if (($sheet_values{$_}>$max) || ($max eq '-')) {
519: $max=$sheet_values{$_};
520: }
521: }
522: return $max;
523: }
524:
525: #-------------------------------------------------------
526:
527: =pod
528:
529: =item MIN(range)
530:
531: compute the minimum of the items in the range.
532:
533: =cut
534:
535: #-------------------------------------------------------
536: sub MIN {
537: my $mask=&mask(@_);
538: my $min='-';
1.26 matthew 539: foreach (grep eval("/$mask/"),keys(%sheet_values)) {
1.1 matthew 540: unless ($max) { $max=$sheet_values{$_}; }
541: if (($sheet_values{$_}<$min) || ($min eq '-')) {
542: $min=$sheet_values{$_};
543: }
544: }
545: return $min;
546: }
547:
548: #-------------------------------------------------------
549:
550: =pod
551:
552: =item SUMMAX(num,lower,upper)
553:
554: compute the sum of the largest 'num' items in the range from
555: 'lower' to 'upper'
556:
557: =cut
558:
559: #-------------------------------------------------------
560: sub SUMMAX {
561: my ($num,$lower,$upper)=@_;
562: my $mask=&mask($lower,$upper);
563: my @inside=();
1.26 matthew 564: foreach (grep eval("/$mask/"),keys(%sheet_values)) {
1.1 matthew 565: push (@inside,$sheet_values{$_});
566: }
567: @inside=sort(@inside);
568: my $sum=0; my $i;
569: for ($i=$#inside;(($i>$#inside-$num) && ($i>=0));$i--) {
570: $sum+=$inside[$i];
571: }
572: return $sum;
573: }
574:
575: #-------------------------------------------------------
576:
577: =pod
578:
579: =item SUMMIN(num,lower,upper)
580:
581: compute the sum of the smallest 'num' items in the range from
582: 'lower' to 'upper'
583:
584: =cut
585:
586: #-------------------------------------------------------
587: sub SUMMIN {
588: my ($num,$lower,$upper)=@_;
589: my $mask=&mask($lower,$upper);
590: my @inside=();
1.26 matthew 591: foreach (grep eval("/$mask/"),keys(%sheet_values)) {
1.1 matthew 592: $inside[$#inside+1]=$sheet_values{$_};
593: }
594: @inside=sort(@inside);
595: my $sum=0; my $i;
596: for ($i=0;(($i<$num) && ($i<=$#inside));$i++) {
597: $sum+=$inside[$i];
598: }
599: return $sum;
600: }
601:
602: #-------------------------------------------------------
603:
604: =pod
605:
606: =item MINPARM(parametername)
607:
608: Returns the minimum value of the parameters matching the parametername.
609: parametername should be a string such as 'duedate'.
610:
611: =cut
612:
613: #-------------------------------------------------------
614: sub MINPARM {
615: my ($expression) = @_;
616: my $min = undef;
617: study($expression);
618: foreach $parameter (keys(%c)) {
619: next if ($parameter !~ /$expression/);
620: if ((! defined($min)) || ($min > $c{$parameter})) {
621: $min = $c{$parameter}
622: }
623: }
624: return $min;
625: }
626:
627: #-------------------------------------------------------
628:
629: =pod
630:
631: =item MAXPARM(parametername)
632:
633: Returns the maximum value of the parameters matching the input parameter name.
634: parametername should be a string such as 'duedate'.
635:
636: =cut
637:
638: #-------------------------------------------------------
639: sub MAXPARM {
640: my ($expression) = @_;
641: my $max = undef;
642: study($expression);
643: foreach $parameter (keys(%c)) {
644: next if ($parameter !~ /$expression/);
645: if ((! defined($min)) || ($max < $c{$parameter})) {
646: $max = $c{$parameter}
647: }
648: }
649: return $max;
650: }
651:
652:
653: sub calc {
654: %sheet_values = %t;
655: my $notfinished = 1;
656: my $lastcalc = '';
657: my $depth = 0;
658: while ($notfinished) {
659: $notfinished=0;
660: while (my ($cell,$value) = each(%t)) {
661: my $old=$sheet_values{$cell};
662: $sheet_values{$cell}=eval $value;
663: # $errorlog .= $cell.' = '.$old.'->'.$sheet_values{$cell}."\n";
664: if ($@) {
665: undef %sheet_values;
666: return $cell.': '.$@;
667: }
668: if ($sheet_values{$cell} ne $old) {
669: $notfinished=1;
670: $lastcalc=$cell;
671: }
672: }
673: # $errorlog.="------------------------------------------------";
674:
675: $depth++;
676: if ($depth>100) {
677: undef %sheet_values;
678: return $lastcalc.': Maximum calculation depth exceeded';
679: }
680: }
1.30 matthew 681: return 'okay';
1.1 matthew 682: }
683:
684: # ------------------------------------------- End of "Inside of the safe space"
685: ENDDEFS
1.25 matthew 686: $safeeval->reval($code);
687: }
1.1 matthew 688: $self->{'safe'} = $safeeval;
689: $self->{'root'} = $self->{'safe'}->root();
690: #
691: # Place some of the %$self items into the safe space except the safe space
692: # itself
693: my $initstring = '';
694: foreach (qw/name domain type usymb cid csec coursefilename
695: cnum cdom chome uhome/) {
696: $initstring.= qq{\$$_="$self->{$_}";};
697: }
698: $self->{'safe'}->reval($initstring);
699: return $self;
700: }
1.25 matthew 701:
702: }
703:
1.1 matthew 704: ######################################################
705:
706: =pod
707:
708: =back
709:
710: =cut
711:
712: ######################################################
713:
714:
715: ######################################################
716:
1.26 matthew 717: =pod
718:
719: =item &mask($lower,$upper)
720:
721: Inputs: $lower and $upper, cell names ("X12" or "a150") or globs ("X*").
722:
723: Returns: Regular expression matching spreadsheet cells that are within
724: the rectangle defined by $lower and $upper. Due to the nature of the
725: regular expression this result must be used inside an eval().
726:
727: =cut
1.1 matthew 728:
729: ######################################################
730: {
731:
732: my %memoizer;
733:
734: sub mask {
735: my ($lower,$upper)=@_;
736: my $key = $lower.'_'.$upper;
737: if (exists($memoizer{$key})) {
738: return $memoizer{$key};
739: }
740: $upper = $lower if (! defined($upper));
741: #
1.26 matthew 742: my ($la,$ld) = ($lower=~/([A-z]|\*)(\d+|\*)/);
743: my ($ua,$ud) = ($upper=~/([A-z]|\*)(\d+|\*)/);
1.1 matthew 744: #
745: my $alpha='';
746: my $num='';
747: #
1.26 matthew 748: # Do not put parenthases around $alpha.
749: # $num depends on the value in $1.
1.1 matthew 750: if (($la eq '*') || ($ua eq '*')) {
1.26 matthew 751: $alpha='[A-z]';
1.1 matthew 752: } else {
1.27 matthew 753: if ($la gt $ua) {
754: my $tmp = $ua;
755: $ua = $la;
756: $la = $ua;
757: }
1.26 matthew 758: $alpha=qq/[$la-$ua]/;
759: }
760: if ($ld ne '*' && $ud ne '*') {
761: # Make sure $ld <= $ud
762: if ($ld > $ud) {
763: my $tmp = $ud;
764: $ud = $ld;
765: $ld = $tmp;
766: }
767: # Here we make a regular expression using some advanced regexp
768: # abilities.
769: # (\d+) will match the digits of the cell name and dump them in
770: # to $1
771: # (?(?{ ... code ...} pattern_if_true | pattern_if_false)) will
772: # choose pattern_if_true if { ... code ... } is true and
773: # pattern_if_false if { ... code ... } is false.
774: # In this case, pattern_if_true is empty. pattern_if_false is
775: # 'donotmatch' and will not match our cells because none of
776: # them end with donotmatch.
777: # Unfortunately, the use of this type of regular expression
778: # requires that each match be wrapped in an eval(). Search for
779: # $mask in this module for examples
780: $num = '(\d+)(?(?{$1>= '.$ld.' && $1<='.$ud.'})|donotmatch)';
1.1 matthew 781: } else {
1.26 matthew 782: $num = '(\d+)';
1.1 matthew 783: }
1.26 matthew 784: my $expression = '^'.$alpha.$num.'$';
1.1 matthew 785: $memoizer{$key} = $expression;
786: return $expression;
1.26 matthew 787: }
788:
789: #
790: # Debugging routine
791: sub dump_memoized_values {
792: while (my ($key,$value) = each(%memoizer)) {
793: &Apache::lonnet::logthis('memoizer: '.$key.' = '.$value);
794: }
795: return;
1.1 matthew 796: }
797:
798: }
799:
800: ##
801: ## sub add_hash_to_safe {} # spreadsheet, would like to destroy
802: ##
803:
804: #
805: # expandnamed used to reside in the safe space
806: #
807: sub expandnamed {
808: my $self = shift;
809: my $expression=shift;
810: if ($expression=~/^\&/) {
811: my ($func,$var,$formula)=($expression=~/^\&(\w+)\(([^\;]+)\;(.*)\)/);
812: my @vars=split(/\W+/,$formula);
813: my %values=();
814: foreach my $varname ( @vars ) {
1.20 matthew 815: if ($varname=~/^(parameter|stores|timestamp)/) {
816: $formula=~s/$varname/'$c{\''.$varname.'\'}'/ge;
1.1 matthew 817: $varname=~s/$var/\([\\w:\\- ]\+\)/g;
818: foreach (keys(%{$self->{'constants'}})) {
819: if ($_=~/$varname/) {
820: $values{$1}=1;
821: }
822: }
823: }
824: }
825: if ($func eq 'EXPANDSUM') {
826: my $result='';
827: foreach (keys(%values)) {
828: my $thissum=$formula;
829: $thissum=~s/$var/$_/g;
830: $result.=$thissum.'+';
831: }
832: $result=~s/\+$//;
833: return $result;
834: } else {
835: return 0;
836: }
837: } else {
838: # it is not a function, so it is a parameter name
839: # We should do the following:
840: # 1. Take the list of parameter names
841: # 2. look through the list for ones that match the parameter we want
842: # 3. If there are no collisions, return the one that matches
843: # 4. If there is a collision, return 'bad parameter name error'
844: my $returnvalue = '';
845: my @matches = ();
1.14 matthew 846: my @values = ();
1.1 matthew 847: $#matches = -1;
848: study $expression;
1.14 matthew 849: while (my($parameter,$value) = each(%{$self->{'constants'}})) {
850: next if ($parameter !~ /$expression/);
851: push(@matches,$parameter);
852: push(@values,$value);
1.1 matthew 853: }
854: if (scalar(@matches) == 0) {
1.10 matthew 855: $returnvalue = '""';#'"unmatched parameter: '.$parameter.'"';
1.1 matthew 856: } elsif (scalar(@matches) == 1) {
857: # why do we not do this lookup here, instead of delaying it?
1.14 matthew 858: $returnvalue = $values[0];
1.1 matthew 859: } elsif (scalar(@matches) > 0) {
860: # more than one match. Look for a concise one
861: $returnvalue = "'non-unique parameter name : $expression'";
1.14 matthew 862: for (my $i=0; $i<=$#matches;$i++) {
863: if ($matches[$i] =~ /^$expression$/) {
1.1 matthew 864: # why do we not do this lookup here?
1.14 matthew 865: $returnvalue = $values[$i];
1.1 matthew 866: }
867: }
868: } else {
869: # There was a negative number of matches, which indicates
870: # something is wrong with reality. Better warn the user.
1.14 matthew 871: $returnvalue = '"bizzare parameter: '.$expression.'"';
1.1 matthew 872: }
873: return $returnvalue;
874: }
875: }
876:
877: sub sett {
878: my $self = shift;
879: my %t=();
880: #
881: # Deal with the template row
882: foreach my $col ($self->template_cells()) {
883: next if ($col=~/^[A-Z]/);
884: foreach my $row ($self->rows()) {
885: # Get the name of this cell
886: my $cell=$col.$row;
887: # Grab the template declaration
888: $t{$cell}=$self->formula('template_'.$col);
889: # Replace '#' with the row number
890: $t{$cell}=~s/\#/$row/g;
891: # Replace '....' with ','
892: $t{$cell}=~s/\.\.+/\,/g;
893: # Replace 'A0' with the value from 'A0'
894: $t{$cell}=~s/(^|[^\"\'])([A-Za-z]\d+)/$1\$sheet_values\{\'$2\'\}/g;
895: # Replace parameters
896: $t{$cell}=~s/(^|[^\"\'])\[([^\]]+)\]/$1.$self->expandnamed($2)/ge;
897: }
898: }
899: #
900: # Deal with the normal cells
901: while (my($cell,$formula) = each(%{$self->{'formulas'}})) {
902: next if ($_=~/^template\_/);
903: my ($col,$row) = ($cell =~ /^([A-z])(\d+)$/);
904: if ($row eq '0') {
905: $t{$cell}=$formula;
906: $t{$cell}=~s/\.\.+/\,/g;
907: $t{$cell}=~s/(^|[^\"\'])([A-Za-z]\d+)/$1\$sheet_values\{\'$2\'\}/g;
908: $t{$cell}=~s/(^|[^\"\'])\[([^\]]+)\]/$1.$self->expandnamed($2)/ge;
909: } elsif ( $col =~ /^[A-Z]$/ ) {
910: if ($formula !~ /^\!/ && exists($self->{'constants'}->{$cell})) {
911: my $data = $self->{'constants'}->{$cell};
912: $t{$cell} = $data;
913: }
914: } else { # $row > 1 and $col =~ /[a-z]
915: $t{$cell}=$formula;
916: $t{$cell}=~s/\.\.+/\,/g;
917: $t{$cell}=~s/(^|[^\"\'])([A-Za-z]\d+)/$1\$sheet_values\{\'$2\'\}/g;
918: $t{$cell}=~s/(^|[^\"\'])\[([^\]]+)\]/$1.$self->expandnamed($2)/ge;
919: }
920: }
921: %{$self->{'safe'}->varglob('t')}=%t;
922: }
923:
924: ##
925: ## sync_safe_space: Called by calcsheet to make sure all the data we
926: # need to calculate is placed into the safe space
927: ##
928: sub sync_safe_space {
929: my $self = shift;
930: # Inside the safe space 'formulas' has a diabolical alter-ego named 'f'.
931: %{$self->{'safe'}->varglob('f')}=%{$self->{'formulas'}};
932: # 'constants' leads a peaceful hidden life of 'c'.
933: %{$self->{'safe'}->varglob('c')}=%{$self->{'constants'}};
934: # 'othersheets' hides as 'os', a disguise few can penetrate.
935: @{$self->{'safe'}->varglob('os')}=@{$self->{'othersheets'}};
936: }
937:
938: ##
939: ## Retrieve the error log from the safe space (used for debugging)
940: ##
941: sub get_errorlog {
942: my $self = shift;
943: $self->{'errorlog'} = $ { $self->{'safe'}->varglob('errorlog') };
944: return $self->{'errorlog'};
945: }
946:
947: ##
948: ## Clear the error log inside the safe space
949: ##
950: sub clear_errorlog {
951: my $self = shift;
952: $ {$self->{'safe'}->varglob('errorlog')} = '';
953: $self->{'errorlog'} = '';
954: }
955:
956: ##
957: ## constants: either set or get the constants
958: ##
959: sub constants {
960: my $self=shift;
961: my ($constants) = @_;
962: if (defined($constants)) {
963: if (! ref($constants)) {
964: my %tmp = @_;
965: $constants = \%tmp;
966: }
967: $self->{'constants'} = $constants;
968: return;
969: } else {
970: return %{$self->{'constants'}};
971: }
972: }
973:
974: ##
975: ## formulas: either set or get the formulas
976: ##
977: sub formulas {
978: my $self=shift;
979: my ($formulas) = @_;
980: if (defined($formulas)) {
981: if (! ref($formulas)) {
982: my %tmp = @_;
983: $formulas = \%tmp;
984: }
985: $self->{'formulas'} = $formulas;
986: $self->{'rows'} = [];
987: $self->{'template_cells'} = [];
988: return;
989: } else {
990: return %{$self->{'formulas'}};
991: }
992: }
993:
994: sub set_formula {
995: my $self = shift;
996: my ($cell,$formula) = @_;
997: $self->{'formulas'}->{$cell}=$formula;
998: return;
999: }
1000:
1001: ##
1002: ## formulas_keys: Return the keys to the formulas hash.
1003: ##
1004: sub formulas_keys {
1005: my $self = shift;
1006: my @keys = keys(%{$self->{'formulas'}});
1007: return keys(%{$self->{'formulas'}});
1008: }
1009:
1010: ##
1011: ## formula: Return the formula for a given cell in the spreadsheet
1012: ## returns '' if the cell does not have a formula or does not exist
1013: ##
1014: sub formula {
1015: my $self = shift;
1016: my $cell = shift;
1017: if (defined($cell) && exists($self->{'formulas'}->{$cell})) {
1018: return $self->{'formulas'}->{$cell};
1019: }
1020: return '';
1021: }
1022:
1023: ##
1024: ## logthis: write the input to lonnet.log
1025: ##
1026: sub logthis {
1027: my $self = shift;
1028: my $message = shift;
1029: &Apache::lonnet::logthis($self->{'type'}.':'.
1030: $self->{'name'}.':'.$self->{'domain'}.':'.
1031: $message);
1032: return;
1033: }
1034:
1035: ##
1036: ## dump_formulas_to_log: makes lonnet.log huge...
1037: ##
1038: sub dump_formulas_to_log {
1039: my $self =shift;
1040: $self->logthis("Spreadsheet formulas");
1041: $self->logthis("--------------------------------------------------------");
1042: while (my ($cell, $formula) = each(%{$self->{'formulas'}})) {
1043: $self->logthis(' '.$cell.' = '.$formula);
1044: }
1045: $self->logthis("--------------------------------------------------------");}
1046:
1047: ##
1048: ## value: returns the computed value of a particular cell
1049: ##
1050: sub value {
1051: my $self = shift;
1052: my $cell = shift;
1053: if (defined($cell) && exists($self->{'values'}->{$cell})) {
1054: return $self->{'values'}->{$cell};
1055: }
1056: return '';
1057: }
1058:
1059: ##
1060: ## dump_values_to_log: makes lonnet.log huge...
1061: ##
1062: sub dump_values_to_log {
1063: my $self =shift;
1064: $self->logthis("Spreadsheet Values");
1065: $self->logthis("------------------------------------------------------");
1066: while (my ($cell, $value) = each(%{$self->{'values'}})) {
1067: $self->logthis(' '.$cell.' = '.$value);
1068: }
1069: $self->logthis("------------------------------------------------------");
1070: }
1071:
1072: ##
1073: ## Yet another debugging function
1074: ##
1075: sub dump_hash_to_log {
1076: my $self= shift();
1077: my %tmp = @_;
1078: if (@_<2) {
1079: %tmp = %{$_[0]};
1080: }
1081: $self->logthis('---------------------------- (begin hash dump)');
1082: while (my ($key,$val) = each (%tmp)) {
1083: $self->logthis(' '.$key.' = '.$val.':');
1084: }
1085: $self->logthis('---------------------------- (finished hash dump)');
1086: }
1087:
1088: ##
1089: ## rebuild_stats: rebuilds the rows and template_cells arrays
1090: ##
1091: sub rebuild_stats {
1092: my $self = shift;
1093: $self->{'rows'}=[];
1094: $self->{'template_cells'}=[];
1095: while (my ($cell,$formula) = each(%{$self->{'formulas'}})) {
1096: push(@{$self->{'rows'}},$1) if ($cell =~ /^A(\d+)/ && $1 != 0);
1097: push(@{$self->{'template_cells'}},$1) if ($cell =~ /^template_(\w+)/);
1098: }
1099: return;
1100: }
1101:
1102: ##
1103: ## template_cells returns a list of the cells defined in the template row
1104: ##
1105: sub template_cells {
1106: my $self = shift;
1107: $self->rebuild_stats() if (! defined($self->{'template_cells'}) ||
1108: ! @{$self->{'template_cells'}});
1109: return @{$self->{'template_cells'}};
1110: }
1111:
1112: ##
1113: ## Sigh....
1114: ##
1115: sub setothersheets {
1116: my $self = shift;
1117: my @othersheets = @_;
1118: $self->{'othersheets'} = \@othersheets;
1119: }
1120:
1121: ##
1122: ## rows returns a list of the names of cells defined in the A column
1123: ##
1124: sub rows {
1125: my $self = shift;
1126: $self->rebuild_stats() if (!@{$self->{'rows'}});
1127: return @{$self->{'rows'}};
1128: }
1129:
1130: #
1131: # calcsheet: makes all the calls to compute the spreadsheet.
1132: #
1133: sub calcsheet {
1134: my $self = shift;
1135: $self->sync_safe_space();
1136: $self->clear_errorlog();
1137: $self->sett();
1138: my $result = $self->{'safe'}->reval('&calc();');
1139: # $self->logthis($self->get_errorlog());
1140: %{$self->{'values'}} = %{$self->{'safe'}->varglob('sheet_values')};
1141: # $self->logthis($self->get_errorlog());
1.30 matthew 1142: if ($result ne 'okay') {
1143: $self->set_calcerror($result);
1144: }
1.1 matthew 1145: return $result;
1146: }
1147:
1.30 matthew 1148: sub set_badcalc {
1149: my $self = shift();
1150: $self->{'badcalc'} =1;
1151: return;
1152: }
1153:
1154: sub badcalc {
1155: my $self = shift;
1156: if (exists($self->{'badcalc'}) && $self->{'badcalc'}) {
1157: return 1;
1158: } else {
1159: return 0;
1160: }
1161: }
1162:
1163: sub set_calcerror {
1164: my $self = shift;
1165: if (@_) {
1166: $self->set_badcalc();
1167: if (exists($self->{'calcerror'})) {
1168: $self->{'calcerror'}.="\n".$_[0];
1169: } else {
1170: $self->{'calcerror'}.=$_[0];
1171: }
1172: }
1173: }
1174:
1175: sub calcerror {
1176: my $self = shift;
1177: if ($self->badcalc()) {
1178: if (exists($self->{'calcerror'})) {
1179: return $self->{'calcerror'};
1180: }
1181: }
1182: return;
1183: }
1184:
1.1 matthew 1185: ###########################################################
1186: ##
1187: ## Output Helpers
1188: ##
1189: ###########################################################
1.5 matthew 1190: sub display {
1191: my $self = shift;
1192: my ($r) = @_;
1193: my $outputmode = 'html';
1.31 ! matthew 1194: foreach ($self->output_options()) {
! 1195: if ($ENV{'form.output_format'} eq $_->{'value'}) {
! 1196: $outputmode = $_->{'value'};
! 1197: last;
! 1198: }
1.5 matthew 1199: }
1200: if ($outputmode eq 'html') {
1.31 ! matthew 1201: $self->compute($r);
1.5 matthew 1202: $self->outsheet_html($r);
1.31 ! matthew 1203: } elsif ($outputmode eq 'htmlclasslist') {
! 1204: # No computation neccessary... This is kludgy
! 1205: $self->outsheet_htmlclasslist($r);
1.5 matthew 1206: } elsif ($outputmode eq 'excel') {
1.31 ! matthew 1207: $self->compute($r);
1.5 matthew 1208: $self->outsheet_excel($r);
1209: } elsif ($outputmode eq 'csv') {
1.31 ! matthew 1210: $self->compute($r);
1.5 matthew 1211: $self->outsheet_csv($r);
1212: }
1.22 matthew 1213: $self->cleanup();
1.5 matthew 1214: return;
1215: }
1216:
1.1 matthew 1217: ############################################
1218: ## HTML output routines ##
1219: ############################################
1.30 matthew 1220: sub html_report_error {
1221: my $self = shift();
1222: my $Str = '';
1223: if ($self->badcalc()) {
1224: $Str = '<h3 style="color:red">'.
1225: &mt('An error occurred while calculating this spreadsheet').
1226: "</h3>\n".
1227: '<pre>'.$self->calcerror()."</pre>\n";
1228: }
1229: return $Str;
1230: }
1231:
1.1 matthew 1232: sub html_export_row {
1233: my $self = shift();
1.17 matthew 1234: my ($color) = @_;
1235: $color = '#CCCCFF' if (! defined($color));
1.1 matthew 1236: my $allowed = &Apache::lonnet::allowed('mgr',$ENV{'request.course.id'});
1237: my $row_html;
1238: my @rowdata = $self->get_row(0);
1239: foreach my $cell (@rowdata) {
1240: if ($cell->{'name'} =~ /^[A-Z]/) {
1.17 matthew 1241: $row_html .= '<td bgcolor="'.$color.'">'.
1242: &html_editable_cell($cell,$color,$allowed).'</td>';
1.1 matthew 1243: } else {
1244: $row_html .= '<td bgcolor="#DDCCFF">'.
1245: &html_editable_cell($cell,'#DDCCFF',$allowed).'</td>';
1246: }
1247: }
1248: return $row_html;
1249: }
1250:
1251: sub html_template_row {
1252: my $self = shift();
1253: my $allowed = &Apache::lonnet::allowed('mgr',$ENV{'request.course.id'});
1.17 matthew 1254: my ($num_uneditable,$importcolor) = @_;
1.1 matthew 1255: my $row_html;
1256: my @rowdata = $self->get_template_row();
1257: my $count = 0;
1258: for (my $i = 0; $i<=$#rowdata; $i++) {
1259: my $cell = $rowdata[$i];
1260: if ($i < $num_uneditable) {
1.17 matthew 1261: $row_html .= '<td bgcolor="'.$importcolor.'">'.
1.7 matthew 1262: &html_uneditable_cell($cell,'#FFDDDD',$allowed).'</td>';
1.1 matthew 1263: } else {
1264: $row_html .= '<td bgcolor="#EOFFDD">'.
1265: &html_editable_cell($cell,'#EOFFDD',$allowed).'</td>';
1266: }
1267: }
1268: return $row_html;
1269: }
1270:
1271: sub html_editable_cell {
1272: my ($cell,$bgcolor,$allowed) = @_;
1273: my $result;
1274: my ($name,$formula,$value);
1275: if (defined($cell)) {
1276: $name = $cell->{'name'};
1277: $formula = $cell->{'formula'};
1278: $value = $cell->{'value'};
1279: }
1280: $name = '' if (! defined($name));
1281: $formula = '' if (! defined($formula));
1282: if (! defined($value)) {
1283: $value = '<font color="'.$bgcolor.'">#</font>';
1284: if ($formula ne '') {
1285: $value = '<i>undefined value</i>';
1286: }
1287: } elsif ($value =~ /^\s*$/ ) {
1288: $value = '<font color="'.$bgcolor.'">#</font>';
1289: } else {
1290: $value = &HTML::Entities::encode($value) if ($value !~/ /);
1291: }
1292: return $value if (! $allowed);
1.18 matthew 1293: #
1.1 matthew 1294: # The formula will be parsed by the browser twice before being
1.18 matthew 1295: # displayed to the user for editing.
1296: #
1297: # The encoding string "^A-blah" is placed in []'s inside a regexp, so
1298: # we specify the characters we want left alone by putting a '^' in front.
1.21 matthew 1299: $formula = &HTML::Entities::encode($formula,'^A-z0-9 !#$%-;=?~');
1300: # HTML::Entities::encode does not catch everything - we need '\' encoded
1301: $formula =~ s/\\/&\#092/g;
1.18 matthew 1302: # Escape it again - this time the only encodable character is '&'
1303: $formula =~ s/\&/\&/g;
1.1 matthew 1304: # Glue everything together
1305: $result .= "<a href=\"javascript:celledit(\'".
1306: $name."','".$formula."');\">".$value."</a>";
1307: return $result;
1308: }
1309:
1310: sub html_uneditable_cell {
1311: my ($cell,$bgcolor) = @_;
1312: my $value = (defined($cell) ? $cell->{'value'} : '');
1313: $value = &HTML::Entities::encode($value) if ($value !~/ /);
1314: return ' '.$value.' ';
1315: }
1316:
1317: sub html_row {
1318: my $self = shift();
1.17 matthew 1319: my ($num_uneditable,$row,$exportcolor,$importcolor) = @_;
1.1 matthew 1320: my $allowed = &Apache::lonnet::allowed('mgr',$ENV{'request.course.id'});
1321: my @rowdata = $self->get_row($row);
1322: my $num_cols_output = 0;
1323: my $row_html;
1.17 matthew 1324: my $color = $importcolor;
1325: if ($row == 0) {
1326: $color = $exportcolor;
1327: }
1328: $color = '#FFDDDD' if (! defined($color));
1.1 matthew 1329: foreach my $cell (@rowdata) {
1330: if ($num_cols_output++ < $num_uneditable) {
1.17 matthew 1331: $row_html .= '<td bgcolor="'.$color.'">';
1.1 matthew 1332: $row_html .= &html_uneditable_cell($cell,'#FFDDDD');
1333: } else {
1334: $row_html .= '<td bgcolor="#EOFFDD">';
1335: $row_html .= &html_editable_cell($cell,'#E0FFDD',$allowed);
1336: }
1337: $row_html .= '</td>';
1338: }
1339: return $row_html;
1340: }
1341:
1.5 matthew 1342: sub html_header {
1343: my $self = shift;
1344: return '' if (! $ENV{'request.role.adv'});
1345: return "<table>\n".
1.29 matthew 1346: '<tr><th align="center">'.&mt('Output Format').'</th></tr>'."\n".
1.31 ! matthew 1347: '<tr><td>'.$self->output_selector()."</td></tr>\n".
1.5 matthew 1348: "</table>\n";
1349: }
1350:
1.31 ! matthew 1351: ##
! 1352: ## Default output types are HTML, Excel, and CSV
! 1353: sub output_options {
! 1354: my $self = shift();
! 1355: return ({value => 'html',
! 1356: description => 'HTML'},
! 1357: {value => 'excel',
! 1358: description => 'Excel'},
! 1359: {value => 'csv',
! 1360: description => 'Comma Separated Values'},);
! 1361: }
! 1362:
1.5 matthew 1363: sub output_selector {
1.31 ! matthew 1364: my $self = shift();
1.5 matthew 1365: my $output_selector = '<select name="output_format" size="3">'."\n";
1366: my $default = 'html';
1367: if (exists($ENV{'form.output_format'})) {
1368: $default = $ENV{'form.output_format'}
1369: } else {
1370: $ENV{'form.output_format'} = $default;
1371: }
1.31 ! matthew 1372: foreach ($self->output_options()) {
! 1373: $output_selector.='<option value="'.$_->{'value'}.'"';
! 1374: if ($_->{'value'} eq $default) {
1.5 matthew 1375: $output_selector .= ' selected';
1376: }
1.31 ! matthew 1377: $output_selector .= ">".&mt($_->{'description'})."</option>\n";
1.5 matthew 1378: }
1379: $output_selector .= "</select>\n";
1380: return $output_selector;
1381: }
1382:
1383: ################################################
1384: ## Excel output routines ##
1385: ################################################
1386: sub excel_output_row {
1387: my $self = shift;
1388: my ($worksheet,$rownum,$rows_output,@prepend) = @_;
1389: my $cols_output = 0;
1390: #
1391: my @rowdata = $self->get_row($rownum);
1392: foreach my $cell (@prepend,@rowdata) {
1393: my $value = $cell;
1394: $value = $cell->{'value'} if (ref($value));
1395: $value =~ s/\ / /gi;
1396: $worksheet->write($rows_output,$cols_output++,$value);
1397: }
1398: return;
1399: }
1400:
1.1 matthew 1401: sub create_excel_spreadsheet {
1402: my $self = shift;
1403: my ($r) = @_;
1404: my $filename = '/prtspool/'.
1405: $ENV{'user.name'}.'_'.$ENV{'user.domain'}.'_'.
1406: time.'_'.rand(1000000000).'.xls';
1407: my $workbook = Spreadsheet::WriteExcel->new('/home/httpd'.$filename);
1408: if (! defined($workbook)) {
1409: $r->log_error("Error creating excel spreadsheet $filename: $!");
1.28 www 1410: $r->print(&mt("Problems creating new Excel file. ".
1.1 matthew 1411: "This error has been logged. ".
1.28 www 1412: "Please alert your LON-CAPA administrator"));
1.1 matthew 1413: return undef;
1414: }
1415: #
1416: # The excel spreadsheet stores temporary data in files, then put them
1417: # together. If needed we should be able to disable this (memory only).
1418: # The temporary directory must be specified before calling 'addworksheet'.
1419: # File::Temp is used to determine the temporary directory.
1420: $workbook->set_tempdir('/home/httpd/perl/tmp');
1421: #
1422: # Determine the name to give the worksheet
1423: return ($workbook,$filename);
1.31 ! matthew 1424: }
! 1425:
! 1426: #
! 1427: # This routine is just a stub
! 1428: sub outsheet_htmlclasslist {
! 1429: my $self = shift;
! 1430: my ($r) = @_;
! 1431: $r->print('<h2>'.&mt("This output is not supported").'</h2>');
! 1432: $r->rflush();
! 1433: return;
1.5 matthew 1434: }
1435:
1436: sub outsheet_excel {
1437: my $self = shift;
1438: my ($r) = @_;
1.23 matthew 1439: my $connection = $r->connection();
1.28 www 1440: $r->print("<h2>".&mt('Preparing Excel Spreadsheet')."</h2>");
1.5 matthew 1441: #
1442: # Create excel worksheet
1443: my ($workbook,$filename) = $self->create_excel_spreadsheet($r);
1444: return if (! defined($workbook));
1445: #
1446: # Create main worksheet
1447: my $worksheet = $workbook->addworksheet('main');
1448: my $rows_output = 0;
1449: my $cols_output = 0;
1450: #
1451: # Write excel header
1452: foreach my $value ($self->get_title()) {
1453: $cols_output = 0;
1454: $worksheet->write($rows_output++,$cols_output,$value);
1455: }
1456: $rows_output++; # skip a line
1457: #
1458: # Write summary/export row
1459: $cols_output = 0;
1460: $self->excel_output_row($worksheet,0,$rows_output++,'Summary');
1461: $rows_output++; # skip a line
1462: #
1.23 matthew 1463: $self->excel_rows($connection,$worksheet,$cols_output,$rows_output);
1.5 matthew 1464: #
1465: #
1466: # Close the excel file
1467: $workbook->close();
1468: #
1469: # Write a link to allow them to download it
1470: $r->print('<br />'.
1471: '<a href="'.$filename.'">Your Excel spreadsheet.</a>'."\n");
1.6 matthew 1472: return;
1473: }
1474:
1475: #################################
1476: ## CSV output routines ##
1477: #################################
1478: sub outsheet_csv {
1479: my $self = shift;
1480: my ($r) = @_;
1.23 matthew 1481: my $connection = $r->connection();
1.6 matthew 1482: my $csvdata = '';
1483: my @Values;
1484: #
1485: # Open the csv file
1486: my $filename = '/prtspool/'.
1487: $ENV{'user.name'}.'_'.$ENV{'user.domain'}.'_'.
1488: time.'_'.rand(1000000000).'.csv';
1489: my $file;
1490: unless ($file = Apache::File->new('>'.'/home/httpd'.$filename)) {
1491: $r->log_error("Couldn't open $filename for output $!");
1.28 www 1492: $r->print(&mt("Problems occured in writing the csv file. ".
1.6 matthew 1493: "This error has been logged. ".
1.28 www 1494: "Please alert your LON-CAPA administrator."));
1.6 matthew 1495: $r->print("<pre>\n".$csvdata."</pre>\n");
1496: return 0;
1497: }
1498: #
1499: # Output the title information
1500: foreach my $value ($self->get_title()) {
1501: print $file "'".&Apache::loncommon::csv_translate($value)."'\n";
1502: }
1503: #
1504: # Output the body of the spreadsheet
1.23 matthew 1505: $self->csv_rows($connection,$file);
1.6 matthew 1506: #
1507: # Close the csv file
1508: close($file);
1509: $r->print('<br /><br />'.
1.28 www 1510: '<a href="'.$filename.'">'.&mt('Your CSV spreadsheet.').'</a>'."\n");
1.6 matthew 1511: #
1512: return 1;
1513: }
1514:
1515: sub csv_output_row {
1516: my $self = shift;
1517: my ($filehandle,$rownum,@prepend) = @_;
1518: #
1519: my @rowdata = ();
1520: if (defined($rownum)) {
1521: @rowdata = $self->get_row($rownum);
1522: }
1523: my @output = ();
1524: foreach my $cell (@prepend,@rowdata) {
1525: my $value = $cell;
1526: $value = $cell->{'value'} if (ref($value));
1527: $value =~ s/\ / /gi;
1528: $value = "'".$value."'";
1529: push (@output,$value);
1530: }
1531: print $filehandle join(',',@output )."\n";
1.5 matthew 1532: return;
1.1 matthew 1533: }
1534:
1535: ############################################
1536: ## XML output routines ##
1537: ############################################
1538: sub outsheet_xml {
1539: my $self = shift;
1540: my ($r) = @_;
1541: ## Someday XML
1542: ## Will be rendered for the user
1543: ## But not on this day
1544: my $Str = '<spreadsheet type="'.$self->{'type'}.'">'."\n";
1545: while (my ($cell,$formula) = each(%{$self->{'formulas'}})) {
1546: if ($cell =~ /^template_(\d+)/) {
1547: my $col = $1;
1548: $Str .= '<template col="'.$col.'">'.$formula.'</template>'."\n";
1549: } else {
1550: my ($row,$col) = ($cell =~ /^([A-z])(\d+)/);
1551: next if (! defined($row) || ! defined($col));
1552: $Str .= '<field row="'.$row.'" col="'.$col.'" >'.$formula.'</cell>'
1553: ."\n";
1554: }
1555: }
1556: $Str.="</spreadsheet>";
1557: return $Str;
1558: }
1559:
1560: ############################################
1561: ### Filesystem routines ###
1562: ############################################
1563: sub parse_sheet {
1564: # $sheetxml is a scalar reference or a scalar
1565: my ($sheetxml) = @_;
1566: if (! ref($sheetxml)) {
1567: my $tmp = $sheetxml;
1568: $sheetxml = \$tmp;
1569: }
1570: my %formulas;
1571: my %sources;
1572: my $parser=HTML::TokeParser->new($sheetxml);
1573: my $token;
1574: while ($token=$parser->get_token) {
1575: if ($token->[0] eq 'S') {
1576: if ($token->[1] eq 'field') {
1577: my $cell = $token->[2]->{'col'}.$token->[2]->{'row'};
1578: my $source = $token->[2]->{'source'};
1579: my $formula = $parser->get_text('/field');
1580: $formulas{$cell} = $formula;
1581: $sources{$cell} = $source if (defined($source));
1582: $parser->get_text('/field');
1583: }
1584: if ($token->[1] eq 'template') {
1585: $formulas{'template_'.$token->[2]->{'col'}}=
1586: $parser->get_text('/template');
1587: }
1588: }
1589: }
1590: return (\%formulas,\%sources);
1591: }
1592:
1593: {
1594:
1595: my %spreadsheets;
1596:
1597: sub clear_spreadsheet_definition_cache {
1598: undef(%spreadsheets);
1599: }
1600:
1.13 matthew 1601: sub load_system_default_sheet {
1602: my $self = shift;
1603: my $includedir = $Apache::lonnet::perlvar{'lonIncludes'};
1604: # load in the default defined spreadsheet
1605: my $sheetxml='';
1606: my $fh;
1607: if ($fh=Apache::File->new($includedir.'/default_'.$self->{'type'})) {
1608: $sheetxml=join('',<$fh>);
1609: $fh->close();
1610: } else {
1611: # $sheetxml='<field row="0" col="A">"Error"</field>';
1612: $sheetxml='<field row="0" col="A"></field>';
1613: }
1614: $self->filename('default_');
1615: my ($formulas,undef) = &parse_sheet(\$sheetxml);
1616: return $formulas;
1617: }
1618:
1.1 matthew 1619: sub load {
1620: my $self = shift;
1621: #
1622: my $stype = $self->{'type'};
1623: my $cnum = $self->{'cnum'};
1624: my $cdom = $self->{'cdom'};
1625: my $chome = $self->{'chome'};
1626: #
1.13 matthew 1627: my $filename = $self->filename();
1.1 matthew 1628: my $cachekey = join('_',($cnum,$cdom,$stype,$filename));
1629: #
1630: # see if sheet is cached
1631: my ($formulas);
1632: if (exists($spreadsheets{$cachekey})) {
1633: $formulas = $spreadsheets{$cachekey}->{'formulas'};
1634: } else {
1635: # Not cached, need to read
1.13 matthew 1636: if (! defined($filename)) {
1637: $formulas = $self->load_system_default_sheet();
1.8 matthew 1638: } elsif($self->filename() =~ /^\/res\/.*\.spreadsheet$/) {
1.1 matthew 1639: # Load a spreadsheet definition file
1640: my $sheetxml=&Apache::lonnet::getfile
1641: (&Apache::lonnet::filelocation('',$filename));
1642: if ($sheetxml == -1) {
1643: $sheetxml='<field row="0" col="A">"Error loading spreadsheet '
1644: .$self->filename().'"</field>';
1645: }
1646: ($formulas,undef) = &parse_sheet(\$sheetxml);
1.13 matthew 1647: # Get just the filename and set the sheets filename
1648: my ($newfilename) = ($filename =~ /\/([^\/]*)\.spreadsheet$/);
1649: if ($self->is_default()) {
1650: $self->filename($newfilename);
1651: $self->make_default();
1652: } else {
1653: $self->filename($newfilename);
1654: }
1.1 matthew 1655: } else {
1656: # Load the spreadsheet definition file from the save file
1.13 matthew 1657: my %tmphash = &Apache::lonnet::dump($filename,$cdom,$cnum);
1.1 matthew 1658: my ($tmp) = keys(%tmphash);
1659: if ($tmp !~ /^(con_lost|error|no_such_host)/i) {
1660: while (my ($cell,$formula) = each(%tmphash)) {
1661: $formulas->{$cell}=$formula;
1662: }
1663: } else {
1.13 matthew 1664: $formulas = $self->load_system_default_sheet();
1.1 matthew 1665: }
1666: }
1.13 matthew 1667: $filename=$self->filename(); # filename may have changed
1.1 matthew 1668: $cachekey = join('_',($cnum,$cdom,$stype,$filename));
1669: %{$spreadsheets{$cachekey}->{'formulas'}} = %{$formulas};
1670: }
1671: $self->formulas($formulas);
1672: $self->set_row_sources();
1673: $self->set_row_numbers();
1674: }
1675:
1676: sub set_row_sources {
1677: my $self = shift;
1678: while (my ($cell,$value) = each(%{$self->{'formulas'}})) {
1.22 matthew 1679: next if ($cell !~ /^A(\d+)/ || $1 < 1);
1.1 matthew 1680: my $row = $1;
1681: $self->{'row_source'}->{$row} = $value;
1682: }
1683: return;
1684: }
1685:
1.12 matthew 1686: sub set_row_numbers {
1687: my $self = shift;
1688: while (my ($cell,$value) = each(%{$self->{'formulas'}})) {
1689: next if ($cell !~ /^A(\d+)$/);
1690: next if (! defined($value));
1691: $self->{'row_numbers'}->{$value} = $1;
1692: $self->{'maxrow'} = $1 if ($1 > $self->{'maxrow'});
1693: }
1694: }
1695:
1.1 matthew 1696: ##
1697: ## exportrow is *not* used to get the export row from a computed sub-sheet.
1698: ##
1699: sub exportrow {
1700: my $self = shift;
1.30 matthew 1701: if (exists($self->{'badcalc'}) && $self->{'badcalc'}) {
1702: return ();
1703: }
1.1 matthew 1704: my @exportarray;
1705: foreach my $column (@UC_Columns) {
1706: push(@exportarray,$self->value($column.'0'));
1707: }
1708: return @exportarray;
1709: }
1710:
1711: sub save {
1712: my $self = shift;
1713: my ($makedef)=@_;
1714: my $cid=$self->{'cid'};
1.4 matthew 1715: # If we are saving it, it must not be temporary
1716: $self->temporary(0);
1.1 matthew 1717: if (&Apache::lonnet::allowed('opa',$cid)) {
1718: my %f=$self->formulas();
1719: my $stype = $self->{'type'};
1720: my $cnum = $self->{'cnum'};
1721: my $cdom = $self->{'cdom'};
1722: my $chome = $self->{'chome'};
1.12 matthew 1723: my $filename = $self->{'filename'};
1724: my $cachekey = join('_',($cnum,$cdom,$stype,$filename));
1.1 matthew 1725: # Cache new sheet
1.13 matthew 1726: %{$spreadsheets{$cachekey}->{'formulas'}}=%f;
1.1 matthew 1727: # Write sheet
1728: foreach (keys(%f)) {
1729: delete($f{$_}) if ($f{$_} eq 'import');
1730: }
1.12 matthew 1731: my $reply = &Apache::lonnet::put($filename,\%f,$cdom,$cnum);
1.1 matthew 1732: return $reply if ($reply ne 'ok');
1733: $reply = &Apache::lonnet::put($stype.'_spreadsheets',
1.12 matthew 1734: {$filename => $ENV{'user.name'}.'@'.$ENV{'user.domain'}},
1.1 matthew 1735: $cdom,$cnum);
1736: return $reply if ($reply ne 'ok');
1737: if ($makedef) {
1738: $reply = &Apache::lonnet::put('environment',
1.12 matthew 1739: {'spreadsheet_default_'.$stype => $filename },
1.1 matthew 1740: $cdom,$cnum);
1741: return $reply if ($reply ne 'ok');
1742: }
1743: if ($self->is_default()) {
1.22 matthew 1744: if ($self->{'type'} eq 'studentcalc') {
1745: &Apache::lonnet::expirespread('','','studentcalc','');
1746: } elsif ($self->{'type'} eq 'assesscalc') {
1747: &Apache::lonnet::expirespread('','','assesscalc','');
1.16 matthew 1748: &Apache::lonnet::expirespread('','','studentcalc','');
1749: }
1.1 matthew 1750: }
1751: return $reply;
1752: }
1753: return 'unauthorized';
1754: }
1755:
1756: } # end of scope for %spreadsheets
1757:
1758: sub save_tmp {
1759: my $self = shift;
1.9 matthew 1760: my $filename=$ENV{'user.name'}.'_'.
1.19 matthew 1761: $ENV{'user.domain'}.'_spreadsheet_'.$self->{'symb'}.'_'.
1.1 matthew 1762: $self->{'filename'};
1.9 matthew 1763: $filename=~s/\W/\_/g;
1764: $filename=$Apache::lonnet::tmpdir.$filename.'.tmp';
1.4 matthew 1765: $self->temporary(1);
1.1 matthew 1766: my $fh;
1.9 matthew 1767: if ($fh=Apache::File->new('>'.$filename)) {
1.1 matthew 1768: my %f = $self->formulas();
1769: while( my ($cell,$formula) = each(%f)) {
1770: next if ($formula eq 'import');
1771: print $fh &Apache::lonnet::escape($cell)."=".
1772: &Apache::lonnet::escape($formula)."\n";
1773: }
1774: $fh->close();
1775: }
1776: }
1777:
1778: sub load_tmp {
1779: my $self = shift;
1780: my $filename=$ENV{'user.name'}.'_'.
1.19 matthew 1781: $ENV{'user.domain'}.'_spreadsheet_'.$self->{'symb'}.'_'.
1.1 matthew 1782: $self->{'filename'};
1783: $filename=~s/\W/\_/g;
1784: $filename=$Apache::lonnet::tmpdir.$filename.'.tmp';
1785: my %formulas = ();
1786: if (my $spreadsheet_file = Apache::File->new($filename)) {
1787: while (<$spreadsheet_file>) {
1788: chomp;
1789: my ($cell,$formula) = split(/=/);
1790: $cell = &Apache::lonnet::unescape($cell);
1791: $formula = &Apache::lonnet::unescape($formula);
1792: $formulas{$cell} = $formula;
1793: }
1794: $spreadsheet_file->close();
1795: }
1.4 matthew 1796: # flag the sheet as temporary
1797: $self->temporary(1);
1.1 matthew 1798: $self->formulas(\%formulas);
1799: $self->set_row_sources();
1800: $self->set_row_numbers();
1801: return;
1.4 matthew 1802: }
1803:
1804: sub temporary {
1805: my $self=shift;
1806: if (@_) {
1807: ($self->{'temporary'})= @_;
1808: }
1809: return $self->{'temporary'};
1.1 matthew 1810: }
1811:
1812: sub modify_cell {
1813: # studentcalc overrides this
1814: my $self = shift;
1815: my ($cell,$formula) = @_;
1816: if ($cell =~ /([A-z])\-/) {
1817: $cell = 'template_'.$1;
1818: } elsif ($cell !~ /^([A-z](\d+)|template_[A-z])$/) {
1819: return;
1820: }
1821: $self->set_formula($cell,$formula);
1822: $self->rebuild_stats();
1823: return;
1824: }
1825:
1826: ###########################################
1827: # othersheets: Returns the list of other spreadsheets available
1828: ###########################################
1829: sub othersheets {
1830: my $self = shift();
1831: my ($stype) = @_;
1832: $stype = $self->{'type'} if (! defined($stype) || $stype !~ /calc$/);
1833: #
1834: my @alternatives=();
1835: my %results=&Apache::lonnet::dump($stype.'_spreadsheets',
1836: $self->{'cdom'}, $self->{'cnum'});
1837: my ($tmp) = keys(%results);
1.2 matthew 1838: if ($tmp =~ /^(con_lost|error|no_such_host)/i ) {
1.28 www 1839: @alternatives = (&mt('Default'));
1.2 matthew 1840: } else {
1.28 www 1841: @alternatives = (&mt('Default'), sort (keys(%results)));
1.1 matthew 1842: }
1843: return @alternatives;
1.3 matthew 1844: }
1845:
1846: sub blackout {
1847: my $self = shift;
1848: $self->{'blackout'} = $_[0] if (@_);
1849: return $self->{'blackout'};
1.1 matthew 1850: }
1851:
1852: sub get_row {
1853: my $self = shift;
1854: my ($n)=@_;
1855: my @cols=();
1856: foreach my $col (@UC_Columns,@LC_Columns) {
1857: my $cell = $col.$n;
1858: push(@cols,{ name => $cell,
1859: formula => $self->formula($cell),
1860: value => $self->value($cell)});
1861: }
1862: return @cols;
1863: }
1864:
1865: sub get_template_row {
1866: my $self = shift;
1867: my @cols=();
1868: foreach my $col (@UC_Columns,@LC_Columns) {
1869: my $cell = 'template_'.$col;
1870: push(@cols,{ name => $cell,
1871: formula => $self->formula($cell),
1872: value => $self->formula($cell) });
1873: }
1874: return @cols;
1875: }
1876:
1.12 matthew 1877: sub need_to_save {
1.1 matthew 1878: my $self = shift;
1.12 matthew 1879: if ($self->{'new_rows'} && ! $self->temporary()) {
1880: return 1;
1.1 matthew 1881: }
1.12 matthew 1882: return 0;
1.1 matthew 1883: }
1884:
1885: sub get_row_number_from_key {
1886: my $self = shift;
1887: my ($key) = @_;
1888: if (! exists($self->{'row_numbers'}->{$key}) ||
1889: ! defined($self->{'row_numbers'}->{$key})) {
1890: # I used to set $f here to the new value, but the key passed for lookup
1891: # may not be the key we need to save
1892: $self->{'maxrow'}++;
1893: $self->{'row_numbers'}->{$key} = $self->{'maxrow'};
1.13 matthew 1894: # $self->logthis('added row '.$self->{'row_numbers'}->{$key}.
1895: # ' for '.$key);
1.12 matthew 1896: $self->{'new_rows'} = 1;
1.1 matthew 1897: }
1898: return $self->{'row_numbers'}->{$key};
1899: }
1900:
1901: 1;
1902:
1903: __END__
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>