# # $Id: studentcalc.pm,v 1.39 2006/05/01 06:17:16 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # # This file is part of the LearningOnline Network with CAPA (LON-CAPA). # # LON-CAPA is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # LON-CAPA is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with LON-CAPA; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # /home/httpd/html/adm/gpl.txt # # http://www.lon-capa.org/ # # The LearningOnline Network with CAPA # Spreadsheet/Grades Display Handler # # POD required stuff: =head1 NAME studentcalc =head1 SYNOPSIS =head1 DESCRIPTION =over 4 =cut ################################################### ### StudentSheet ### ################################################### package Apache::studentcalc; use warnings FATAL=>'all'; no warnings 'uninitialized'; use strict; use Apache::Constants qw(:common :http); use Apache::lonnet; use Apache::loncommon(); use Apache::loncoursedata(); use Apache::lonnavmaps; use Apache::Spreadsheet(); use Apache::assesscalc(); use HTML::Entities(); use Time::HiRes; use Apache::lonlocal; @Apache::studentcalc::ISA = ('Apache::Spreadsheet'); my @Sequences = (); my $navmap; my %Exportrows = (); my $current_course; sub initialize { &initialize_sequence_cache(); &Apache::assesscalc::initialize($navmap); } sub initialize_package { $current_course = $env{'request.course.id'}; &initialize_sequence_cache(); &load_cached_export_rows(); } sub ensure_correct_sequence_data { if ($current_course ne $env{'request.course.id'}) { &initialize_sequence_cache(); $current_course = $env{'request.course.id'}; } return; } sub initialize_sequence_cache { # # Set up the sequences and assessments undef(@Sequences); undef($navmap); $navmap = Apache::lonnavmaps::navmap->new(); if (!defined($navmap)) { &Apache::lonnet::logthis('student spreadsheet:Can not open Coursemap'); } my @all_sequences = $navmap->retrieveResources(undef, sub { shift->is_map(); },1,0,1); for my $sequence ($navmap->getById('0.0'), @all_sequences) { if ($navmap->hasResource($sequence,sub { shift->is_problem(); }, 0)){ push(@Sequences,$sequence); &get_resources($sequence); } } } my %res_memoize; sub get_resources { my ($seq) = @_; if (exists($res_memoize{$seq->symb()})) { return @{$res_memoize{$seq->symb()}}; } return () if (! defined($navmap) || ! ref($navmap)); my @resources = $navmap->retrieveResources($seq, sub { shift->is_problem(); }, 0,0,0); $res_memoize{$seq->symb()}=\@resources; return @resources; } sub clear_package { undef(@Sequences); undef(%Exportrows); undef(%res_memoize); undef($navmap); &Apache::assesscalc::clear_package(); } sub get_title { my $self = shift; my @title = (); # # Determine the students name my $name = &Apache::loncommon::plainname($self->{'name'}, $self->{'domain'}); push (@title,$name); push (@title,$self->{'coursedesc'}); push (@title,&Apache::lonlocal::locallocaltime(time)); return @title; } sub get_html_title { my $self = shift; my ($name,$desc,$time) = $self->get_title(); my $title = '

'.$name; if ($env{'user.name'} ne $self->{'name'} && $env{'user.domain'} ne $self->{'domain'}) { $title .= ' '.&Apache::loncommon::aboutmewrapper ($self->{'name'}.'@'.$self->{'domain'}, $self->{'name'},$self->{'domain'}); } $title .= "

\n"; $title .= '

'.$desc."

\n"; $title .= '

'.$time.'

'; return $title; } sub parent_link { my $self = shift; return '

'.&mt('Course level sheet').'

'."\n"; } sub convenience_links { my $self = shift; my ($resource) = @_; my $result=&Apache::loncommon::submlink('',$self->{'name'},$self->{'domain'},$resource->symb,'LONcatInfo'); $result .= &Apache::loncommon::pgrdlink('',$self->{'name'},$self->{'domain'},$resource->symb,'LONcatInfo'); $result .= &Apache::loncommon::pprmlink('',$self->{'name'},$self->{'domain'},$resource->symb,'LONcatInfo'); return $result; } sub outsheet_html { my $self = shift; my ($r) = @_; my $importcolor = '#FFFFAA'; my $exportcolor = '#88FF88'; #################################### # Get the list of assessment files # #################################### my @AssessFileNames = $self->othersheets('assesscalc'); my $editing_is_allowed = &Apache::lonnet::allowed('mgr', $env{'request.course.id'}); #################################### # Report any calculation errors # #################################### $r->print($self->html_report_error()); #################################### # Determine table structure # #################################### my $num_uneditable = 26; my $num_left = 52-$num_uneditable; my %lt=&Apache::lonlocal::texthash( 'st' => 'Student', 'im' => 'Import', 'ca' => 'Calculations', 'as' => 'Assessment', 'ro' => 'Row', ); my $tableheader =<<"END";

END my $label_num = 0; foreach (split(//,'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz')){ if ($label_num<$num_uneditable) { $tableheader .='"; $label_num++; } $tableheader .="\n"; if ($self->blackout()) { $r->print('

'. &mt('Some computations are not available at this time.').'
'. &mt('There are problems whose status you are not allowed to view.'). '

'."\n"); } else { $r->print($tableheader); # # Print out template row if (exists($env{'request.role.adv'}) && $env{'request.role.adv'}) { $r->print(''. $self->html_template_row($num_uneditable, $importcolor)."\n"); } # # Print out summary/export row $r->print(''. $self->html_export_row($exportcolor)."\n"); } $r->print("
$lt{'st'} $lt{'im'} $lt{'ca'}
'; } else { $tableheader .=''; } $tableheader .="$_
Template 
'.&mt('Summary').'0
\n"); # # Prepare to output rows if (exists($env{'request.role.adv'}) && $env{'request.role.adv'}) { $tableheader =<<"END";

END } else { $tableheader =<<"END";

$lt{'ro'} $lt{'as'}
END } foreach (split(//,'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz')){ if ($label_num<$num_uneditable) { $tableheader.='"; } $tableheader.="\n"; # my $num_output = 1; if (scalar(@Sequences)< 1) { &initialize_sequence_cache(); } foreach my $Sequence (@Sequences) { $r->print("

".$Sequence->compTitle."

\n"); my @resources = &get_resources($Sequence); my $first_rownum = $self->get_row_number_from_key($resources[0]->symb); my $last_rownum = $self->get_row_number_from_key($resources[-1]->symb); $r->print(&assess_file_selector([$first_rownum, $last_rownum], undef, \@AssessFileNames)); $r->print($tableheader); foreach my $resource (@resources) { my $rownum = $self->get_row_number_from_key($resource->symb); my $assess_filename = $self->{'row_source'}->{$rownum}; my $row_output = ''; if ($editing_is_allowed) { $row_output .= ''; $row_output .= ''; $row_output .= ''; } else { $row_output .= ''; } if ($self->blackout() && $self->{'blackout_rows'}->{$rownum}>0) { $row_output .= ''."\n"; } else { $row_output .= $self->html_row($num_uneditable,$rownum, $exportcolor,$importcolor). "\n"; } $r->print($row_output); } $r->print("
 $lt{'as'}'; } else { $tableheader.=''; } $tableheader.="$_
'.$rownum.''.$self->convenience_links($resource).''. ''.$resource->compTitle.'
'; $row_output .= &assess_file_selector($rownum, $assess_filename, \@AssessFileNames). '
Go To'; $row_output .= ''.$resource->compTitle.''.&mt('Unavailable at this time').'
\n"); } $r->print("

\n"); return; } ######################################################## ######################################################## =pod =item &assess_file_selector() =cut ######################################################## ######################################################## sub assess_file_selector { my ($row,$default,$AssessFiles)=@_; if (!defined($AssessFiles) || ! @$AssessFiles) { return ''; } return '' if (! &Apache::lonnet::allowed('mgr',$env{'request.course.id'})); my $element_name; my $source_row = $row; if (ref($row)) { my ($first_rownum, $last_rownum) = @$row; $element_name = "FileSelect_${first_rownum}_${last_rownum}"; $source_row = "${first_rownum}:${last_rownum}"; } else { $element_name = 'FileSelect_'.$row; } my $load_dialog = '\n"; return $load_dialog; } sub modify_cell { my $self = shift; my ($cell,$formula) = @_; my $set_row = sub { my $row = shift; my $formula = shift; my $cell = 'A' . $row; $self->{'row_source'}->{$row} = $formula; my $original_source = $self->formula($cell); if ($original_source =~ /__&&&__/) { ($original_source,undef) = split('__&&&__',$original_source); } $formula = $original_source.'__&&&__'.$formula; $self->set_formula($cell,$formula); }; if ($cell =~ /^source_(\d+):(\d+)$/) { my $first_row = $1; my $last_row = $2; for my $row ($first_row..$last_row) { $set_row->($row, $formula); } } elsif ($cell =~ /^source_(\d+)$/) { # Need to make sure $formula is a valid filename.... my $row = $1; $set_row->($row, $formula); } elsif ($cell =~ /([A-z])\-/) { $cell = 'template_'.$1; $self->set_formula($cell,$formula); } elsif ($cell !~ /^([A-z](\d+)|template_[A-z])$/) { return; } $self->rebuild_stats(); return; } sub csv_rows { # writes the meat of the spreadsheet to an excel worksheet. Called # by Spreadsheet::outsheet_excel; my $self = shift; my ($connection,$filehandle) = @_; # # Write a header row $self->csv_output_row($filehandle,undef, (&mt('Sequence or Folder'),&mt('Assessment title'))); # # Write each assessments row if (scalar(@Sequences)< 1) { &initialize_sequence_cache(); } foreach my $Sequence (@Sequences) { foreach my $resource (&get_resources($Sequence)) { my $rownum = $self->get_row_number_from_key($resource->symb); my @assessdata = ($Sequence->compTitle, $resource->compTitle); $self->csv_output_row($filehandle,$rownum,@assessdata); } } return; } sub excel_rows { # writes the meat of the spreadsheet to an excel worksheet. Called # by Spreadsheet::outsheet_excel; my $self = shift; my ($connection,$worksheet,$cols_output,$rows_output,$format) = @_; # # Write a header row $cols_output = 0; foreach my $value ('Container','Assessment title') { $worksheet->write($rows_output,$cols_output++,&mt($value),$format->{'h4'}); } $rows_output++; # # Write each assessments row if (scalar(@Sequences)< 1) { &initialize_sequence_cache(); } foreach my $Sequence (@Sequences) { foreach my $resource (&get_resources($Sequence)) { my $rownum = $self->get_row_number_from_key($resource->symb); my @assessdata = ($Sequence->compTitle, $resource->compTitle); $self->excel_output_row($worksheet,$rownum,$rows_output++, @assessdata); } } return; } sub outsheet_recursive_excel { my $self = shift; my ($r) = @_; } ## ## Routines to deal with sequences in the safe space ## sub get_rows_in_sequence { my $self = shift(); my ($sequence) = @_; my @Rows; my @resources = &get_resources($sequence); foreach my $resource (@resources) { my $rownum = $self->get_row_number_from_key($resource->symb); push (@Rows,$rownum); } return @Rows; } sub remove_sequence_data_from_safe_space { my $self = shift(); my $command = 'undef(%Sequence_Rows);'; $self->{'safe'}->reval($command); } sub put_sequence_data_in_safe_space { my $self = shift(); my $data = 'undef(%Sequence_Rows);'; # Build up the %Sequence_Rows hash - each sequence title is associated with # an array pointer, which holds the rows in the sequence. foreach my $seq (@Sequences) { my @Rows = $self->get_rows_in_sequence($seq); # # Potential problems with sequence titles: # 1. duplicate titles - they get the total for the titles # 2. control characters in titles - use q{} around the string to # deal with it. my $title = &HTML::Entities::decode($seq->title()); $title =~ s/&\#058;/:/g; if (@Rows) { $data .= 'push(@{$Sequence_Rows{"'.quotemeta($title).'"}},'. '('.join(',',@Rows).'));'."\n";; } } my $new_code = $data.<<'END'; sub SUMSEQ { my ($col,@titles) = @_; return 'bad column: '.$col if ($col !~ /^[A-z]$/); my $sum = 0; foreach my $title (@titles) { while (my ($seq_title,$rows) = each(%Sequence_Rows)) { my $regexp; if ($title =~ /^regexp:(.*)$/) { $regexp = $1; } elsif (lc($title) eq 'all') { $regexp = '.'; } if (defined($regexp)) { next if ($seq_title !~ /$regexp/); } else { next if ($seq_title ne $title); } foreach my $rownum (@{$rows}) { my $cell = $col.$rownum; if (exists($sheet_values{$cell})) { $sum += $sheet_values{$cell}; } } } } return $sum; } END $self->{'safe'}->reval($new_code); return; } ## ## Main computation method ## sub compute { my $self = shift; my ($r) = @_; if (! defined($current_course) || $current_course ne $env{'request.course.id'} || ! @Sequences ) { $current_course = $env{'request.course.id'}; &clear_package(); &initialize_sequence_cache(); } $self->initialize_safe_space(); &Apache::assesscalc::initialize_package($self->{'name'},$self->{'domain'}, $navmap); my %f = $self->formulas(); # # Process the formulas list - # the formula for the A column of a row is symb__&&__filename my %c = $self->constants(); foreach my $seq (@Sequences) { foreach my $resource (&get_resources($seq)) { my $rownum = $self->get_row_number_from_key($resource->symb); my $cell = 'A'.$rownum; my $assess_filename = 'Default'; if (exists($self->{'row_source'}->{$rownum})) { $assess_filename = $self->{'row_source'}->{$rownum}; } else { $self->{'row_source'}->{$rownum} = $assess_filename; } $f{$cell} = $resource->symb.'__&&&__'.$assess_filename; my $assessSheet; $assessSheet = Apache::assesscalc->new($self->{'name'}, $self->{'domain'}, $assess_filename, $resource->symb, $self->{'section'}, $self->{'groups'}); my @exportdata = $assessSheet->export_data($r); # if ($assessSheet->badcalc()) { $self->set_calcerror( &mt('Error computing row for assessment "[_1]" (row [_2]):[_3]', $assessSheet->get_title(),$rownum,$assessSheet->calcerror())); } # if ($assessSheet->blackout()) { $self->blackout(1); $self->{'blackout_rows'}->{$rownum} = 1; } # # Be sure not to disturb the formulas in the 'A' column my $data = shift(@exportdata); $c{$cell} = $data if (defined($data)); # # Deal with the remaining columns my $i=0; foreach (split(//,'BCDEFGHIJKLMNOPQRSTUVWXYZ')) { my $cell = $_.$rownum; my $data = shift(@exportdata); if (defined($data)) { $f{$cell} = 'import'; $c{$cell} = $data; } $i++; } } } $self->constants(\%c); $self->formulas(\%f); $self->put_sequence_data_in_safe_space(); $self->calcsheet(); $self->remove_sequence_data_from_safe_space(); # # Store export row in cache my @exportarray=$self->exportrow(); my $student = $self->{'name'}.':'.$self->{'domain'}; $Exportrows{$student}->{'time'} = time; $Exportrows{$student}->{'data'} = \@exportarray; # save export row $self->save_export_data(); # $self->save() if ($self->need_to_save()); return; } sub set_row_sources { my $self = shift; $self->check_formulas_loaded(); while (my ($cell,$value) = each(%{$self->{'formulas'}})) { next if ($cell !~ /^A(\d+)$/ || $1 < 1); my $row = $1; (undef,$value) = split('__&&&__',$value); $value = 'Default' if (! defined($value)); $self->{'row_source'}->{$row} = $value; } return; } sub set_row_numbers { my $self = shift; $self->check_formulas_loaded(); while (my ($cell,$formula) = each(%{$self->{'formulas'}})) { next if ($cell !~ /^A(\d+)/); my $row = $1; next if ($row == 0); my ($symb,undef) = split('__&&&__',$formula); $self->{'row_numbers'}->{$symb} = $row; $self->{'maxrow'} = $row if ($row > $self->{'maxrow'}); } } sub get_row_number_from_symb { my $self = shift; my ($key) = @_; ($key,undef) = split('__&&&__',$key) if ($key =~ /__&&&__/); return $self->get_row_number_from_key($key); } ############################################# ############################################# =pod =item &load_cached_export_rows Retrieves and parsers the export rows of the student spreadsheets. These rows are saved in the courses directory in the format: sname:sdom:studentcalc:.time => time sname:sdom:studentcalc => ___=___Adata___;___Bdata___;___Cdata___;___ ..... =cut ############################################# ############################################# sub load_cached_export_rows { undef(%Exportrows); my @tmp = &Apache::lonnet::dump('nohist_calculatedsheets', $env{'course.'.$env{'request.course.id'}.'.domain'}, $env{'course.'.$env{'request.course.id'}.'.num'},undef); my %Selected_Assess_Sheet; if ($tmp[0] =~ /^error/) { &Apache::lonnet::logthis('unable to read cached student export rows '. 'for course '.$env{'request.course.id'}); return; } my %tmp = @tmp; while (my ($key,$sheetdata) = each(%tmp)) { my ($sname,$sdom,$sheettype,$remainder) = split(':',$key); my $student = $sname.':'.$sdom; if ($remainder =~ /\.time/) { $Exportrows{$student}->{'time'} = $sheetdata; } else { $sheetdata =~ s/^___=___//; my @Data = split('___;___',$sheetdata); $Exportrows{$student}->{'data'} = \@Data; } } } ############################################# ############################################# =pod =item &save_export_data() Writes the export data for this student to the course cache. =cut ############################################# ############################################# sub save_export_data { my $self = shift; my $student = $self->{'name'}.':'.$self->{'domain'}; return if ($self->temporary()); if ($self->badcalc()){ # do not save data away when calculations have not been done properly. delete($Exportrows{$student}); return; } return if (! exists($Exportrows{$student})); &Apache::assesscalc::save_cached_export_rows($self->{'name'}, $self->{'domain'}); return if (! $self->is_default()); my $key = join(':',($self->{'name'},$self->{'domain'},'studentcalc')).':'; my $timekey = $key.'.time'; my $newstore = join('___;___', @{$Exportrows{$student}->{'data'}}); $newstore = '___=___'.$newstore; my $result= &Apache::lonnet::put('nohist_calculatedsheets', { $key => $newstore, $timekey => $Exportrows{$student}->{'time'} }, $self->{'cdom'}, $self->{'cnum'}); return; } ############################################# ############################################# =pod =item &export_data() Returns the export data associated with the spreadsheet. Computes the spreadsheet only if necessary. =cut ############################################# ############################################# sub export_data { my $self = shift; my ($r) = @_; my $connection = $r->connection(); my $student = $self->{'name'}.':'.$self->{'domain'}; if (! exists($Exportrows{$student}) || ! defined($Exportrows{$student}) || ! exists($Exportrows{$student}->{'data'}) || ! defined($Exportrows{$student}->{'data'}) || ! exists($Exportrows{$student}->{'time'}) || ! defined($Exportrows{$student}->{'time'}) || ! $self->check_expiration_time($Exportrows{$student}->{'time'})) { $self->compute($r); } if ($connection->aborted()) { $self->cleanup(); return; } my @Data; if ($self->badcalc()) { @Data = (); } else { @Data = @{$Exportrows{$student}->{'data'}}; for (my $i=0; $i<=$#Data;$i++) { if ($Data[$i]=~/\D/ && defined($Data[$i])) { $Data[$i]="'".$Data[$i]."'"; } } } return @Data; } 1; __END__