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