1: #
2: # $Id: studentcalc.pm,v 1.16.2.1 2003/12/05 22:24:20 matthew Exp $
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: studentcalc
34:
35: =head1 SYNOPSIS
36:
37: =head1 DESCRIPTION
38:
39: =over 4
40:
41: =cut
42:
43: ###################################################
44: ### StudentSheet ###
45: ###################################################
46: package Apache::studentcalc;
47:
48: use strict;
49: use Apache::Constants qw(:common :http);
50: use Apache::lonnet;
51: use Apache::loncommon();
52: use Apache::loncoursedata();
53: use Apache::lonnavmaps;
54: use Apache::Spreadsheet();
55: use Apache::assesscalc();
56: use HTML::Entities();
57: use Spreadsheet::WriteExcel;
58: use Time::HiRes;
59:
60: @Apache::studentcalc::ISA = ('Apache::Spreadsheet');
61:
62: my @Sequences = ();
63: my %Exportrows = ();
64:
65: my $current_course;
66:
67: sub initialize {
68: &Apache::assesscalc::initialize();
69: &initialize_sequence_cache();
70: }
71:
72: sub initialize_package {
73: $current_course = $ENV{'request.course.id'};
74: &initialize_sequence_cache();
75: &load_cached_export_rows();
76: }
77:
78: sub ensure_correct_sequence_data {
79: if ($current_course ne $ENV{'request.course.id'}) {
80: &initialize_sequence_cache();
81: $current_course = $ENV{'request.course.id'};
82: }
83: return;
84: }
85:
86: sub initialize_sequence_cache {
87: #
88: # Set up the sequences and assessments
89: @Sequences = ();
90: my ($top,$sequences,$assessments) =
91: &Apache::loncoursedata::get_sequence_assessment_data();
92: if (! defined($top) || ! ref($top)) {
93: # There has been an error, better report it
94: &Apache::lonnet::logthis('top is undefined (studentcalc.pm)');
95: return;
96: }
97: @Sequences = @{$sequences} if (ref($sequences) eq 'ARRAY');
98: }
99:
100: sub clear_package {
101: @Sequences = undef;
102: %Exportrows = undef;
103: }
104:
105: sub get_title {
106: my $self = shift;
107: my @title = ();
108: #
109: # Determine the students name
110: my %userenv = &Apache::loncoursedata::GetUserName($self->{'name'},
111: $self->{'domain'});
112: my $name = join(' ',
113: @userenv{'firstname','middlename','lastname','generation'});
114: $name =~ s/\s+$//;
115:
116: push (@title,$name);
117: push (@title,$self->{'coursedesc'});
118: push (@title,scalar(localtime(time)));
119: return @title;
120: }
121:
122: sub get_html_title {
123: my $self = shift;
124: my ($name,$desc,$time) = $self->get_title();
125: my $title = '<h1>'.$name;
126: if ($ENV{'user.name'} ne $self->{'name'} &&
127: $ENV{'user.domain'} ne $self->{'domain'}) {
128: $title .= &Apache::loncommon::aboutmewrapper
129: ($self->{'name'}.'@'.$self->{'domain'},
130: $self->{'name'},$self->{'domain'});
131: }
132: $title .= "</h1>\n";
133: $title .= '<h2>'.$desc."</h2>\n";
134: $title .= '<h3>'.$time.'</h3>';
135: return $title;
136: }
137:
138: sub parent_link {
139: my $self = shift;
140: my $link .= '<p><a href="/adm/classcalc?'.
141: 'sname='.$self->{'name'}.
142: '&sdomain='.$self->{'domain'}.'">'.
143: 'Course level sheet</a></p>'."\n";
144: return $link;
145: }
146:
147: sub convenience_links {
148: my $self = shift;
149: my ($resource) = @_;
150: my $symb = &Apache::lonnet::escape($resource->{'symb'});
151: my $result = <<"END";
152: <a href="/adm/grades?symb=$symb&command=submission" target="LONcatInfo">
153: <img src="/adm/lonMisc/subm_button.gif" border=0 />
154: </a>
155: <a href="/adm/grades?symb=$symb&command=gradingmenu" target="LONcatInfo">
156: <img src="/adm/lonMisc/pgrd_button.gif" border=0 />
157: </a>
158: <a href="/adm/parmset?symb=$symb" target="LONcatInfo">
159: <img src="/adm/lonMisc/pprm_button.gif" border=0 />
160: </a>
161: END
162: return $result;
163: }
164:
165: sub outsheet_html {
166: my $self = shift;
167: my ($r) = @_;
168: my $importcolor = '#FFFFAA';
169: my $exportcolor = '#88FF88';
170: ####################################
171: # Get the list of assessment files #
172: ####################################
173: my @AssessFileNames = $self->othersheets('assesscalc');
174: my $editing_is_allowed = &Apache::lonnet::allowed('mgr',
175: $ENV{'request.course.id'});
176: ####################################
177: # Report any calculation errors #
178: ####################################
179: $r->print($self->html_report_error());
180: ####################################
181: # Determine table structure #
182: ####################################
183: my $num_uneditable = 26;
184: my $num_left = 52-$num_uneditable;
185: my $tableheader =<<"END";
186: <p>
187: <table border="2">
188: <tr>
189: <th colspan="2" rowspan="2"><font size="+2">Student</font></th>
190: <td bgcolor="$importcolor" colspan="$num_uneditable">
191: <b><font size="+1">Import</font></b></td>
192: <td colspan="$num_left">
193: <b><font size="+1">Calculations</font></b></td>
194: </tr><tr>
195: END
196: my $label_num = 0;
197: foreach (split(//,'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz')){
198: if ($label_num<$num_uneditable) {
199: $tableheader .='<td bgcolor="'.$importcolor.'">';
200: } else {
201: $tableheader .='<td>';
202: }
203: $tableheader .="<b><font size=+1>$_</font></b></td>";
204: $label_num++;
205: }
206: $tableheader .="</tr>\n";
207: if ($self->blackout()) {
208: $r->print('<font color="red" size="+2"><p>'.
209: 'Some computations are not available at this time.<br />'.
210: 'There are problems whose status you are allowed to view.'.
211: '</font></p>'."\n");
212: } else {
213: $r->print($tableheader);
214: #
215: # Print out template row
216: if (exists($ENV{'request.role.adv'}) && $ENV{'request.role.adv'}) {
217: $r->print('<tr><td>Template</td><td> </td>'.
218: $self->html_template_row($num_uneditable,
219: $importcolor)."</tr>\n");
220: }
221: #
222: # Print out summary/export row
223: $r->print('<tr><td>Summary</td><td>0</td>'.
224: $self->html_export_row($exportcolor)."</tr>\n");
225: }
226: $r->print("</table>\n");
227: #
228: # Prepare to output rows
229: if (exists($ENV{'request.role.adv'}) && $ENV{'request.role.adv'}) {
230: $tableheader =<<"END";
231: </p><p>
232: <table border="2">
233: <tr><th>Row</th><th> </th><th>Assessment</th>
234: END
235: } else {
236: $tableheader =<<"END";
237: </p><p>
238: <table border="2">
239: <tr><th> </th><th>Assessment</th>
240: END
241: }
242: foreach (split(//,'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz')){
243: if ($label_num<$num_uneditable) {
244: $tableheader.='<td bgcolor="#FFDDDD">';
245: } else {
246: $tableheader.='<td>';
247: }
248: $tableheader.="<b><font size=+1>$_</font></b></td>";
249: }
250: $tableheader.="\n";
251: #
252: my $num_output = 1;
253: if (scalar(@Sequences)< 1) {
254: &initialize_sequence_cache();
255: }
256: foreach my $Sequence (@Sequences) {
257: next if ($Sequence->{'num_assess'} < 1);
258: $r->print("<h3>".$Sequence->{'title'}."</h3>\n");
259: $r->print($tableheader);
260: foreach my $resource (@{$Sequence->{'contents'}}) {
261: next if ($resource->{'type'} ne 'assessment');
262: my $rownum = $self->get_row_number_from_key($resource->{'symb'});
263: my $assess_filename = $self->{'row_source'}->{$rownum};
264: my $row_output = '<tr>';
265: if ($editing_is_allowed) {
266: $row_output .= '<td>'.$rownum.'</td>';
267: $row_output .= '<td>'.$self->convenience_links($resource).'</td>';
268: $row_output .= '<td>'.
269: '<a href="/adm/assesscalc?sname='.$self->{'name'}.
270: '&sdomain='.$self->{'domain'}.
271: '&filename='.$assess_filename.
272: '&usymb='.&Apache::lonnet::escape($resource->{'symb'}).
273: '">'.$resource->{'title'}.'</a><br />';
274: $row_output .= &assess_file_selector($rownum,
275: $assess_filename,
276: \@AssessFileNames).
277: '</td>';
278: } else {
279: $row_output .= '<td><a href="'.$resource->{'src'}.'?symb='.
280: &Apache::lonnet::escape($resource->{'symb'}).
281: '">Go To</a>';
282: $row_output .= '</td><td>'.$resource->{'title'}.'</td>';
283: }
284: if ($self->blackout() && $self->{'blackout_rows'}->{$rownum}>0) {
285: $row_output .=
286: '<td colspan="52">Unavailable at this time</td></tr>'."\n";
287: } else {
288: $row_output .= $self->html_row($num_uneditable,$rownum,
289: $exportcolor,$importcolor).
290: "</tr>\n";
291: }
292: $r->print($row_output);
293: }
294: $r->print("</table>\n");
295: }
296: $r->print("</p>\n");
297: return;
298: }
299:
300: ########################################################
301: ########################################################
302:
303: =pod
304:
305: =item &assess_file_selector()
306:
307: =cut
308:
309: ########################################################
310: ########################################################
311: sub assess_file_selector {
312: my ($row,$default,$AssessFiles)=@_;
313: if (!defined($AssessFiles) || ! @$AssessFiles) {
314: return '';
315: }
316: return '' if (! &Apache::lonnet::allowed('mgr',$ENV{'request.course.id'}));
317: my $element_name = 'FileSelect_'.$row;
318: my $load_dialog = '<select size="1" name="'.$element_name.'" '.
319: 'onchange="'.
320: "document.sheet.cell.value='source_$row';".
321: "document.sheet.newformula.value=document.sheet.$element_name\.value;".
322: 'document.sheet.submit()" '.'>'."\n";
323: foreach my $file (@{$AssessFiles}) {
324: $load_dialog .= ' <option name="'.$file.'"';
325: $load_dialog .= ' selected' if ($default eq $file);
326: $load_dialog .= '>'.$file."</option>\n";
327: }
328: $load_dialog .= "</select>\n";
329: return $load_dialog;
330: }
331:
332: sub modify_cell {
333: my $self = shift;
334: my ($cell,$formula) = @_;
335: if ($cell =~ /^source_(\d+)$/) {
336: # Need to make sure $formula is a valid filename....
337: my $row = $1;
338: $cell = 'A'.$row;
339: $self->{'row_source'}->{$row} = $formula;
340: my $original_source = $self->formula($cell);
341: if ($original_source =~ /__&&&__/) {
342: ($original_source,undef) = split('__&&&__',$original_source);
343: }
344: $formula = $original_source.'__&&&__'.$formula;
345: } elsif ($cell =~ /([A-z])\-/) {
346: $cell = 'template_'.$1;
347: } elsif ($cell !~ /^([A-z](\d+)|template_[A-z])$/) {
348: return;
349: }
350: $self->set_formula($cell,$formula);
351: $self->rebuild_stats();
352: return;
353: }
354:
355: sub csv_rows {
356: # writes the meat of the spreadsheet to an excel worksheet. Called
357: # by Spreadsheet::outsheet_excel;
358: my $self = shift;
359: my ($filehandle) = @_;
360: #
361: # Write a header row
362: $self->csv_output_row($filehandle,undef,
363: ('Sequence or Folder','Assessment title'));
364: #
365: # Write each assessments row
366: if (scalar(@Sequences)< 1) {
367: &initialize_sequence_cache();
368: }
369: foreach my $Sequence (@Sequences) {
370: next if ($Sequence->{'num_assess'} < 1);
371: foreach my $resource (@{$Sequence->{'contents'}}) {
372: my $rownum = $self->get_row_number_from_key($resource->{'symb'});
373: my @assessdata = ($Sequence->{'title'},
374: $resource->{'title'});
375: $self->csv_output_row($filehandle,$rownum,@assessdata);
376: }
377: }
378: return;
379: }
380:
381: sub excel_rows {
382: # writes the meat of the spreadsheet to an excel worksheet. Called
383: # by Spreadsheet::outsheet_excel;
384: my $self = shift;
385: my ($worksheet,$cols_output,$rows_output) = @_;
386: #
387: # Write a header row
388: $cols_output = 0;
389: foreach my $value ('Container','Assessment title') {
390: $worksheet->write($rows_output,$cols_output++,$value);
391: }
392: $rows_output++;
393: #
394: # Write each assessments row
395: if (scalar(@Sequences)< 1) {
396: &initialize_sequence_cache();
397: }
398: foreach my $Sequence (@Sequences) {
399: next if ($Sequence->{'num_assess'} < 1);
400: foreach my $resource (@{$Sequence->{'contents'}}) {
401: my $rownum = $self->get_row_number_from_key($resource->{'symb'});
402: my @assessdata = ($Sequence->{'title'},
403: $resource->{'title'});
404: $self->excel_output_row($worksheet,$rownum,$rows_output++,
405: @assessdata);
406: }
407: }
408: return;
409: }
410:
411: sub outsheet_recursive_excel {
412: my $self = shift;
413: my ($r) = @_;
414: }
415:
416: sub compute {
417: my $self = shift;
418: if (! defined($current_course) ||
419: $current_course ne $ENV{'request.course.id'}) {
420: $current_course = $ENV{'request.course.id'};
421: &clear_package();
422: &initialize_sequence_cache();
423: }
424: $self->initialize_safe_space();
425: my @sequences = @Sequences;
426: if (@sequences < 1) {
427: my ($top,$sequences,$assessments) =
428: &Apache::loncoursedata::get_sequence_assessment_data();
429: if (! defined($top) || ! ref($top)) {
430: &Apache::lonnet::logthis('top is undefined');
431: return;
432: }
433: @sequences = @{$sequences} if (ref($sequences) eq 'ARRAY');
434: }
435: &Apache::assesscalc::initialize_package($self->{'name'},$self->{'domain'});
436: my %f = $self->formulas();
437: #
438: # Process the formulas list -
439: # the formula for the A column of a row is symb__&&__filename
440: my %c = $self->constants();
441: foreach my $seq (@sequences) {
442: next if ($seq->{'num_assess'}<1);
443: foreach my $resource (@{$seq->{'contents'}}) {
444: next if ($resource->{'type'} ne 'assessment');
445: my $rownum = $self->get_row_number_from_key($resource->{'symb'});
446: my $cell = 'A'.$rownum;
447: my $assess_filename = 'Default';
448: if (exists($self->{'row_source'}->{$rownum})) {
449: $assess_filename = $self->{'row_source'}->{$rownum};
450: } else {
451: $self->{'row_source'}->{$rownum} = $assess_filename;
452: }
453: $f{$cell} = $resource->{'symb'}.'__&&&__'.$assess_filename;
454: my $assessSheet = Apache::assesscalc->new($self->{'name'},
455: $self->{'domain'},
456: $assess_filename,
457: $resource->{'symb'});
458: my @exportdata = $assessSheet->export_data();
459: if ($assessSheet->badcalc()) {
460: $self->set_calcerror(
461: 'Error computing row for assessment '.
462: $assessSheet->get_title().'(row '.$rownum.'):'.
463: $assessSheet->calcerror());
464: }
465: if ($assessSheet->blackout()) {
466: $self->blackout(1);
467: $self->{'blackout_rows'}->{$rownum} = 1;
468: }
469: #
470: # Be sure not to disturb the formulas in the 'A' column
471: my $data = shift(@exportdata);
472: $c{$cell} = $data if (defined($data));
473: #
474: # Deal with the remaining columns
475: my $i=0;
476: foreach (split(//,'BCDEFGHIJKLMNOPQRSTUVWXYZ')) {
477: my $cell = $_.$rownum;
478: my $data = shift(@exportdata);
479: if (defined($data)) {
480: $f{$cell} = 'import';
481: $c{$cell} = $data;
482: }
483: $i++;
484: }
485: }
486: }
487: $self->constants(\%c);
488: $self->formulas(\%f);
489: $self->calcsheet();
490: #
491: # Store export row in cache
492: my @exportarray=$self->exportrow();
493: my $student = $self->{'name'}.':'.$self->{'domain'};
494: $Exportrows{$student}->{'time'} = time;
495: $Exportrows{$student}->{'data'} = \@exportarray;
496: # save export row
497: $self->save_export_data();
498: #
499: $self->save() if ($self->need_to_save());
500: return;
501: }
502:
503: sub set_row_sources {
504: my $self = shift;
505: while (my ($cell,$value) = each(%{$self->{'formulas'}})) {
506: next if ($cell !~ /^A(\d+)/ && $1 > 0);
507: my $row = $1;
508: (undef,$value) = split('__&&&__',$value);
509: $value = 'Default' if (! defined($value));
510: $self->{'row_source'}->{$row} = $value;
511: }
512: return;
513: }
514:
515: sub set_row_numbers {
516: my $self = shift;
517: while (my ($cell,$formula) = each(%{$self->{'formulas'}})) {
518: next if ($cell !~ /^A(\d+)/);
519: my $row = $1;
520: next if ($row == 0);
521: my ($symb,undef) = split('__&&&__',$formula);
522: $self->{'row_numbers'}->{$symb} = $row;
523: $self->{'maxrow'} = $1 if ($1 > $self->{'maxrow'});
524: }
525: }
526:
527: sub get_row_number_from_symb {
528: my $self = shift;
529: my ($key) = @_;
530: ($key,undef) = split('__&&&__',$key) if ($key =~ /__&&&__/);
531: return $self->get_row_number_from_key($key);
532: }
533:
534: #############################################
535: #############################################
536:
537: =pod
538:
539: =item &load_cached_export_rows
540:
541: Retrieves and parsers the export rows of the student spreadsheets.
542: These rows are saved in the courses directory in the format:
543:
544: sname:sdom:studentcalc:.time => time
545:
546: sname:sdom:studentcalc => ___=___Adata___;___Bdata___;___Cdata___;___ .....
547:
548: =cut
549:
550: #############################################
551: #############################################
552: sub load_cached_export_rows {
553: %Exportrows = undef;
554: my @tmp = &Apache::lonnet::dump('nohist_calculatedsheets',
555: $ENV{'course.'.$ENV{'request.course.id'}.'.domain'},
556: $ENV{'course.'.$ENV{'request.course.id'}.'.num'},undef);
557: my %Selected_Assess_Sheet;
558: if ($tmp[0] =~ /^error/) {
559: &Apache::lonnet::logthis('unable to read cached student export rows '.
560: 'for course '.$ENV{'request.course.id'});
561: return;
562: }
563: my %tmp = @tmp;
564: while (my ($key,$sheetdata) = each(%tmp)) {
565: my ($sname,$sdom,$sheettype,$remainder) = split(':',$key);
566: my $student = $sname.':'.$sdom;
567: if ($remainder =~ /\.time/) {
568: $Exportrows{$student}->{'time'} = $sheetdata;
569: } else {
570: $sheetdata =~ s/^___=___//;
571: my @Data = split('___;___',$sheetdata);
572: $Exportrows{$student}->{'data'} = \@Data;
573: }
574: }
575: }
576:
577: #############################################
578: #############################################
579:
580: =pod
581:
582: =item &save_export_data()
583:
584: Writes the export data for this student to the course cache.
585:
586: =cut
587:
588: #############################################
589: #############################################
590: sub save_export_data {
591: my $self = shift;
592: return if ($self->temporary());
593: my $student = $self->{'name'}.':'.$self->{'domain'};
594: if ($self->badcalc()){
595: # do not save data away when calculations have not been done properly.
596: delete($Exportrows{$student});
597: return;
598: }
599: return if (! exists($Exportrows{$student}));
600: return if (! $self->is_default());
601: my $key = join(':',($self->{'name'},$self->{'domain'},'studentcalc')).':';
602: my $timekey = $key.'.time';
603: my $newstore = join('___;___',
604: @{$Exportrows{$student}->{'data'}});
605: $newstore = '___=___'.$newstore;
606: my $result= &Apache::lonnet::put('nohist_calculatedsheets',
607: { $key => $newstore,
608: $timekey => $Exportrows{$student}->{'time'} },
609: $self->{'cdom'},
610: $self->{'cnum'});
611: return;
612: }
613:
614: #############################################
615: #############################################
616:
617: =pod
618:
619: =item &export_data()
620:
621: Returns the export data associated with the spreadsheet. Computes the
622: spreadsheet only if necessary.
623:
624: =cut
625:
626: #############################################
627: #############################################
628: sub export_data {
629: my $self = shift;
630: my $student = $self->{'name'}.':'.$self->{'domain'};
631: if (! exists($Exportrows{$student}) ||
632: ! defined($Exportrows{$student}) ||
633: ! exists($Exportrows{$student}->{'data'}) ||
634: ! defined($Exportrows{$student}->{'data'}) ||
635: ! exists($Exportrows{$student}->{'time'}) ||
636: ! defined($Exportrows{$student}->{'time'}) ||
637: ! $self->check_expiration_time($Exportrows{$student}->{'time'})) {
638: $self->compute();
639: }
640: my @Data;
641: if ($self->badcalc()) {
642: @Data = ();
643: } else {
644: @Data = @{$Exportrows{$student}->{'data'}};
645: for (my $i=0; $i<=$#Data;$i++) {
646: if ($Data[$i]=~/\D/ && defined($Data[$i])) {
647: $Data[$i]="'".$Data[$i]."'";
648: }
649: }
650: }
651: return @Data;
652: }
653:
654: 1;
655:
656: __END__
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>