# # $Id: assesscalc.pm,v 1.28 2003/11/11 15:36:28 matthew 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 assesscalc =head1 SYNOPSIS =head1 DESCRIPTION =cut ################################################### ### AssessSheet ### ################################################### package Apache::assesscalc; use strict; use warnings FATAL=>'all'; no warnings 'uninitialized'; use Apache::Constants qw(:common :http); use Apache::lonnet; use Apache::loncommon; use Apache::Spreadsheet; use Apache::loncoursedata(); use HTML::Entities(); use Spreadsheet::WriteExcel; use GDBM_File; use Time::HiRes; use Apache::lonlocal; @Apache::assesscalc::ISA = ('Apache::Spreadsheet'); ######################################################## ######################################################## =pod =head2 Package Variables =over 4 =item %Exportrows =item $current_name =item $current_domain =item $current_course =item %parmhash =item %nice_parameter_name =item %useropt =item %courseopt =back =cut ######################################################## ######################################################## my %Exportrows; my %newExportrows; my $current_name; my $current_domain; my $current_course; my %parmhash; my %nice_parameter_name; my %useropt; my %userdata; my %courseopt; ######################################################## ######################################################## =pod =head2 Package Subroutines =item &clear_package() Reset all package variables and clean up caches. =cut ######################################################## ######################################################## sub clear_package { if (defined($current_name) && defined($current_domain) && defined($current_course) && $current_course eq $ENV{'request.course.id'} && %newExportrows) { &save_cached_export_rows($current_name,$current_domain); } undef(%Exportrows); undef(%newExportrows); undef($current_name); undef($current_domain); undef($current_course); undef(%useropt); undef(%userdata); undef(%courseopt); } sub save_cached_export_rows { my ($sname,$sdomain) = @_; my $start = Time::HiRes::time; my $result = &Apache::lonnet::put ('nohist_calculatedsheets_'.$ENV{'request.course.id'}, $newExportrows{$sname.':'.$sdomain}, $sdomain,$sname); delete($newExportrows{$sname.':'.$sdomain}); } sub initialize { &clear_package(); &Apache::loncoursedata::clear_internal_caches(); } ######################################################## ######################################################## =pod =item &initialize_package() =cut ######################################################## ######################################################## sub initialize_package { my ($sname,$sdomain) = @_; $current_name = $sname; $current_domain = $sdomain; undef(%useropt); undef(%userdata); if ($current_course ne $ENV{'request.course.id'}) { $current_course = $ENV{'request.course.id'}; undef(%courseopt); } &load_cached_export_rows(); &load_parameter_caches(); &Apache::loncoursedata::clear_internal_caches(); } ######################################################## ######################################################## =pod =item &load_parameter_caches() =cut ######################################################## ######################################################## sub load_parameter_caches { my $userprefix = $current_name.':'.$current_domain.'_'; $userprefix =~ s/:/_/g; # # Course Parameters Cache if (! %courseopt) { $current_course = $ENV{'request.course.id'}; undef(%courseopt); if (! defined($current_name) || ! defined($current_domain)) { return; } my $dom = $ENV{'course.'.$ENV{'request.course.id'}.'.domain'}; my $id = $ENV{'course.'.$ENV{'request.course.id'}.'.num'}; my %Tmp = &Apache::lonnet::dump('resourcedata',$dom,$id); while (my ($name,$value) = each(%Tmp)) { $courseopt{$name}=$value; } } if (! %useropt) { my %Tmp = &Apache::lonnet::dump('resourcedata', $current_domain,$current_name); while (my ($name,$value) = each(%Tmp)) { if ($name =~ /^error: 2/ || $name =~ /no such file/) { undef(%useropt); last; } $useropt{$userprefix.$name}=$value; } $useropt{'loadtime'} = time; } if (! %userdata) { %userdata = &Apache::loncoursedata::get_current_state($current_name, $current_domain); $userdata{'loadtime'} = time; } return; } ######################################################## ######################################################## =pod =head2 assesscalc object methods =cut ######################################################## ######################################################## sub ensure_current_caches { my $self = shift; ## ## Check for a modified parameters ## if (! defined($current_course) || $current_course ne $ENV{'request.course.id'} ) { $current_course = $ENV{'request.course.id'}; undef(%courseopt); undef(%useropt); undef(%userdata); } ## ## Check for new user ## if (! defined($current_name) || $current_name ne $self->{'name'} || ! defined($current_domain) || $current_domain ne $self->{'domain'}) { $current_domain = $self->{'domain'}; $current_name = $self->{'name'}; undef(%useropt); undef(%userdata); } &load_parameter_caches(); } ################################################## ################################################## =pod =item &parmval() Determine the value of a parameter. Inputs: $what, the parameter needed, $symb, $uname, $udom, $csec Returns: The value of a parameter, or '' if none. This function cascades through the possible levels searching for a value for a parameter. The levels are checked in the following order: user, course (at section level and course level), map, and lonnet::metadata. This function uses %parmhash, which must be tied prior to calling it. This function also requires %courseopt and %useropt to be initialized for this user and course. =cut ################################################## ################################################## sub parmval { my $self = shift; my ($what,$symb,$uname,$udom,$csec,$recurse)=@_; $uname = $self->{'name'} if (! defined($uname)); $udom = $self->{'domain'} if (! defined($udom)); $csec = $self->{'section'} if (! defined($csec)); $symb = $self->{'symb'} if (! defined($symb)); # my $result=''; # # This should be a my ($mapname,$id,$fn)=&Apache::lonnet::decode_symb($symb); # Cascading lookup scheme my $rwhat=$what; $what =~ s/^parameter\_//; $what =~ s/\_([^\_]+)$/\.$1/; # my $symbparm = $symb.'.'.$what; my $mapparm = $mapname.'___(all).'.$what; my $courseprefix = $self->{'cid'}; my $usercourseprefix = $uname.'_'.$udom.'_'.$self->{'cid'}; # my $seclevel = $courseprefix.'.['.$csec.'].'.$what; my $seclevelr = $courseprefix.'.['.$csec.'].'.$symbparm; my $seclevelm = $courseprefix.'.['.$csec.'].'.$mapparm; # my $courselevel = $courseprefix.'.'.$what; my $courselevelr = $courseprefix.'.'.$symbparm; my $courselevelm = $courseprefix.'.'.$mapparm; # my $ucourselevel = $usercourseprefix.'.'.$what; my $ucourselevelr = $usercourseprefix.'.'.$symbparm; my $ucourselevelm = $usercourseprefix.'.'.$mapparm; # check user if (defined($uname)) { return $useropt{$ucourselevelr} if (defined($useropt{$ucourselevelr})); return $useropt{$ucourselevelm} if (defined($useropt{$ucourselevelm})); return $useropt{$ucourselevel} if (defined($useropt{$ucourselevel})); } # check section if (defined($csec)) { return $courseopt{$seclevelr} if (defined($courseopt{$seclevelr})); return $courseopt{$seclevelm} if (defined($courseopt{$seclevelm})); return $courseopt{$seclevel} if (defined($courseopt{$seclevel})); } # # check course return $courseopt{$courselevelr} if (defined($courseopt{$courselevelr})); return $courseopt{$courselevelm} if (defined($courseopt{$courselevelm})); return $courseopt{$courselevel} if (defined($courseopt{$courselevel})); # check map parms my $thisparm = $parmhash{$symbparm}; return $thisparm if (defined($thisparm)); # check default $thisparm = &Apache::lonnet::metadata($fn,$rwhat.'.default'); return $thisparm if (defined($thisparm)); # # Cascade Up my $space=$what; $space=~s/\.\w+$//; if ($space ne '0') { my @parts=split(/_/,$space); my $id=pop(@parts); my $part=join('_',@parts); if ($part eq '') { $part='0'; } my $newwhat=$rwhat; $newwhat=~s/\Q$space\E/$part/; my $partgeneral=$self->parmval($newwhat,$symb,$uname,$udom,$csec,1); if (defined($partgeneral)) { return $partgeneral; } } if ($recurse) { return undef; } my $pack_def=&Apache::lonnet::packages_tab_default($fn,'resource.'.$what); if (defined($pack_def)) { return $pack_def; } #nothing defined return ''; } sub get_html_title { my $self = shift; my ($assess_title,$name,$time) = $self->get_title(); my $title = '

'.$assess_title.'

'. '

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

'.$time.'

'; return $title; } sub get_title { my $self = shift; my @title = (); if (($self->{'symb'} eq '_feedback') || ($self->{'symb'} eq '_evaluation') || ($self->{'symb'} eq '_discussion') || ($self->{'symb'} eq '_tutoring')) { my $assess_title = ucfirst($self->{'symb'}); $assess_title =~ s/^_//; push(@title,$assess_title); } else { push(@title,&Apache::lonnet::gettitle($self->{'symb'})); } # Look up the users identifying information # Get the users information my %userenv = &Apache::loncoursedata::GetUserName($self->{'name'}, $self->{'domain'}); my $name = join(' ',@userenv{'firstname','middlename','lastname','generation'}); $name =~ s/\s+$//; push (@title,$name); push (@title,&Apache::lonlocal::locallocaltime(time)); return @title; } sub parent_link { my $self = shift; my $link .= '

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

'."\n"; return $link; } sub outsheet_html { my $self = shift; my ($r) = @_; ################################### # Determine table structure ################################### my $importcolor = '#FFFFFF'; my $exportcolor = '#FFFFAA'; my $num_uneditable = 1; my $num_left = 52-$num_uneditable; my %lt=&Apache::lonlocal::texthash( 'as' => 'Assessment', 'ca' => 'Calculations', ); my $tableheader =<<"END"; END my $label_num = 0; foreach (split(//,'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz')){ if ($label_num<$num_uneditable) { $tableheader .= '"; $label_num++; } $tableheader.="\n"; # $r->print($tableheader); # # Print out template row $r->print(''. $self->html_template_row($num_uneditable,$importcolor). "\n"); # # Print out summary/export row $r->print(''. $self->html_export_row($exportcolor)."\n"); # # Prepare to output rows $tableheader =<<"END";
$lt{'as'}   $lt{'ca'}
'; } else { $tableheader .= ''; } $tableheader .= "$_
Template 
Export0
END foreach (split(//,'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz')){ if ($label_num<$num_uneditable) { $tableheader.='"; } # my $num_output = 0; foreach my $rownum (sort {$a <=> $b} ($self->rows())) { if (! $self->parameter_part_is_valid( $self->{'formulas'}->{'A'.$rownum} )) { next; } if ($num_output++ % 50 == 0) { $r->print("
rowItem'; } else { $tableheader.=''; } $tableheader.="$_
\n".$tableheader); } $r->print(''.$rownum.''. $self->assess_html_row($rownum,$importcolor)."\n"); } $r->print("\n"); return; } sub assess_html_row { my $self = shift(); my ($row,$importcolor) = @_; my $parameter_name = $self->{'formulas'}->{'A'.$row}; my @rowdata = $self->get_row($row); my $num_cols_output = 0; my $row_html; if (exists($nice_parameter_name{$parameter_name})) { my $name = $nice_parameter_name{$parameter_name}; $name =~ s/ /\ /g; $row_html .= ''.$name.'
'.$parameter_name.''; } else { $row_html .= ''.$parameter_name.''; } foreach my $cell (@rowdata) { if ($num_cols_output < 1) { $row_html .= ''; $row_html .= &Apache::Spreadsheet::html_uneditable_cell($cell, '#FFDDDD'); } else { $row_html .= ''; $row_html .= &Apache::Spreadsheet::html_editable_cell($cell, '#E0FFDD',1); } $row_html .= ''; $num_cols_output++; } return $row_html; } sub csv_rows { # writes the meat of the spreadsheet to an excel worksheet. Called # by Spreadsheet::outsheet_excel; my $self = shift; my ($filehandle) = @_; # # Write a header row $self->csv_output_row($filehandle,undef, (&mt('Parameter'),&mt('Description'),&mt('Value'))); # # Write each row foreach my $rownum (sort {$a <=> $b} ($self->rows())) { my $parameter_name = $self->{'formulas'}->{'A'.$rownum}; my $description = ''; if (exists($nice_parameter_name{$parameter_name})) { $description = $nice_parameter_name{$parameter_name}; } $self->csv_output_row($filehandle,$rownum, $parameter_name,$description); } return; } sub excel_rows { # writes the meat of the spreadsheet to an excel worksheet. Called # by Spreadsheet::outsheet_excel; my $self = shift; my ($worksheet,$cols_output,$rows_output) = @_; # # Write a header row $cols_output = 0; foreach my $value ('Parameter','Description','Value') { $worksheet->write($rows_output,$cols_output++,&mt($value)); } $rows_output++; # # Write each row foreach my $rownum (sort {$a <=> $b} ($self->rows())) { my $parameter_name = $self->{'formulas'}->{'A'.$rownum}; my $description = ''; if (exists($nice_parameter_name{$parameter_name})) { $description = $nice_parameter_name{$parameter_name}; } $self->excel_output_row($worksheet,$rownum,$rows_output++, $parameter_name,$description); } return; } ## ## Routines to support assesscalc::compute ## sub get_parm_names { my $self = shift; my @Mandatory_parameters = @_; my %parameters_and_names; # my ($symap,$syid,$srcf) = &Apache::lonnet::decode_symb($self->{'symb'}); my @Metadata = split(/\,/,&Apache::lonnet::metadata($srcf,'keys')); foreach my $parm (@Mandatory_parameters,@Metadata) { next if ($parm !~ /^(resource\.|stores|parameter)_/); my $cleaned_name = $parm; $cleaned_name =~ s/^resource\./stores_/; $cleaned_name =~ s/\./_/g; my $display = &Apache::lonnet::metadata($srcf, $cleaned_name.'.display'); if (! $display) { $display .= &Apache::lonnet::metadata($srcf,$cleaned_name.'.name'); } $parameters_and_names{$cleaned_name}=$display; } return (%parameters_and_names); } sub get_parameter_values { my $self = shift(); my @Parameters; my ($parameters) = @_; if (!ref($parameters)) { @Parameters = @_; } elsif (ref($parameters) eq 'ARRAY') { @Parameters = @$parameters; } elsif (ref($parameters) eq 'HASH') { @Parameters = keys(%$parameters); } # my %parameters; # my $filename = $self->{'coursefilename'}.'_parms.db'; if (tie(%parmhash,'GDBM_File', $self->{'coursefilename'}.'_parms.db',&GDBM_READER(),0640)) { foreach my $parmname (@Parameters) { my $value = $self->parmval($parmname); $parameters{$parmname} =$value; } untie(%parmhash); } else { $self->logthis('unable to tie '.$filename); } return %parameters; } sub deal_with_export_row { my $self = shift(); my @exportarray = @_; $Exportrows{$self->{'symb'}}->{'time'} = time; $Exportrows{$self->{'symb'}}->{$self->{'filename'}} = \@exportarray; # # Save the export data $self->save_export_data(); return; } sub get_problem_state { my $self = shift; my %student_parameters; if (exists($userdata{$self->{'symb'}}) && ref($userdata{$self->{'symb'}}) eq 'HASH') { %student_parameters = %{$userdata{$self->{'symb'}}}; } return %student_parameters; } sub determine_parts { my $self = shift; if (exists($self->{'Parts'}) && ref($self->{'Parts'}) eq 'HASH') { return; } my (undef,undef,$url) = &Apache::lonnet::decode_symb($self->{'symb'}); my $src = &Apache::lonnet::clutter($url); return if (! defined($src)); my %Parts; my $metadata = &Apache::lonnet::metadata($src,'packages'); foreach (split(',',$metadata)) { my ($part) = (/^part_(.*)$/); if (defined($part) && ! &Apache::loncommon::check_if_partid_hidden ($part,$self->{'symb'},$self->{'name'},$self->{'domain'}) ) { $Parts{$part}++; } } # Make sure part 0 is defined. $Parts{'0'}++; $self->{'Parts'} = \%Parts; return; } sub parameter_part_is_valid { my $self = shift; my ($parameter) = @_; return 1 if ($parameter eq 'timestamp'); if (! defined($self->{'Parts'}) || ! ref ($self->{'Parts'}) || ref($self->{'Parts'}) ne 'HASH') { return 1; } # my (undef,$part) = ($parameter =~ m/^(resource|stores|parameter)_([^_]+)_.*/); if (exists($self->{'Parts'}) && exists($self->{'Parts'}->{$part}) && $self->{'Parts'}->{$part} ) { return 1; } else { return 0; } } sub compute { my $self = shift; my ($r) = @_; my $connection = $r->connection(); if ($connection->aborted()) { $self->cleanup(); return; } $self->initialize_safe_space(); ######################################### ######################################### ### ### ### Retrieve the problem parameters ### ### ### ######################################### ######################################### my @Mandatory_parameters = ("stores_0_solved", "stores_0_awarddetail", "stores_0_awarded", "timestamp", "stores_0_tries", "stores_0_award"); # # Definitions undef(%nice_parameter_name); my %parameters; # holds underscored parameters by name # # Get the metadata fields and determine their proper names my %nice_parm_names = $self->get_parm_names(@Mandatory_parameters); while (my($cleaned_name,$display) = each(%nice_parm_names)) { $parameters{$cleaned_name}++; $nice_parameter_name{$cleaned_name} = $display; } # # Get the values of the metadata fields if ($connection->aborted()) { $self->cleanup(); return; } $self->ensure_current_caches(); if ($connection->aborted()) { $self->cleanup(); return; } %parameters = $self->get_parameter_values(keys(%parameters)); if ($connection->aborted()) { $self->cleanup(); return; } # # Clean out unnecessary parameters foreach (keys(%parameters)) { delete($parameters{$_}) if (! /(resource\.|stores_|parameter_)/); } # # Get the students performance data my %student_parameters = $self->get_problem_state(); while (my ($parm,$value) = each(%student_parameters)) { $parm =~ s/^resource\./stores_/; $parm =~ s/\./_/g; $parameters{$parm} = $value; } # # Clean out any bad parameters $self->determine_parts(); foreach my $param (keys(%parameters)) { if (! $self->parameter_part_is_valid($param)) { delete ($parameters{$param}); } } # # Set up the formulas and parameter values my %f=$self->formulas(); my %c; # # Check for blackout requirements if ((!exists($ENV{'request.role.adv'}) || !$ENV{'request.role.adv'})) { while (my ($parm,$value) = each(%parameters)) { last if ($self->blackout()); next if ($parm !~ /^(parameter_.*)_problemstatus$/); if ($parameters{$1.'_answerdate'} ne '' && $parameters{$1.'_answerdate'} < time) { next; } if (lc($value) eq 'no') { # We must blackout this sheet $self->blackout(1); } } } if ($connection->aborted()) { $self->cleanup(); return; } # # Move the parameters into the spreadsheet while (my ($parm,$value) = each(%parameters)) { my $cell = 'A'.$self->get_row_number_from_key($parm); $f{$cell} = $parm; $value = '"'.$value.'"' if ($value =~/[^0-9.]/); $c{$parm} = $value; } $self->formulas(\%f); $self->constants(\%c); if ($connection->aborted()) { $self->cleanup(); return; } $self->calcsheet(); # # Store export row in cache my @exportarray = $self->exportrow(); $self->deal_with_export_row(@exportarray); $self->save() if ($self->need_to_save()); if ($connection->aborted()) { $self->cleanup(); return; } return; } ## ## sett overrides Spreadsheet::sett ## sub sett { my $self = shift; my %t=(); # # Deal with the template row by copying the template formulas into each # row. foreach my $col ($self->template_cells()) { next if ($col=~/^A/); foreach my $row ($self->rows()) { # Get the name of this cell my $cell=$col.$row; # Grab the template declaration $t{$cell}=$self->formula('template_'.$col); # Replace '#' with the row number $t{$cell}=~s/\#/$row/g; # Replace '....' with ',' $t{$cell}=~s/\.\.+/\,/g; # Replace 'A0' with the value from 'A0' $t{$cell}=~s/(^|[^\"\'])([A-Za-z]\d+)/$1\$sheet_values\{\'$2\'\}/g; # Replace parameters $t{$cell}=~s/(^|[^\"\'])\[([^\]]+)\]/$1.$self->expandnamed($2)/ge; } } # # Deal with the cells which have formulas while (my ($cell,$formula) = each(%{$self->{'formulas'}})) { next if ($cell =~ /template_/); if ($cell =~ /^A/ && $cell ne 'A0') { if ($formula !~ /^\!/) { $t{$cell}=$self->{'constants'}->{$formula}; } } else { $t{$cell}=$formula; $t{$cell}=~s/\.\.+/\,/g; $t{$cell}=~s/(^|[^\"\'])([A-Za-z]\d+)/$1\$sheet_values\{\'$2\'\}/g; $t{$cell}=~s/(^|[^\"\'])\[([^\]]+)\]/$1.$self->expandnamed($2)/ge; } } # Put %t into the safe space %{$self->{'safe'}->varglob('t')}=%t; } ######################################################## ######################################################## =pod =item &load_cached_export_rows() Retrieves and parsers the export rows of the assessment spreadsheets. These rows are saved in the students directory in the format: sname:sdom:assesscalc:symb.time => time sname:sdom:assesscalc:symb => filename___=___Adata___;___Bdata___;___ ... =cut ######################################################## ######################################################## sub load_cached_export_rows { undef(%Exportrows); my @tmp = &Apache::lonnet::dump('nohist_calculatedsheets_'. $ENV{'request.course.id'}, $current_domain,$current_name,undef); if ($tmp[0]!~/^error/) { my %tmp = @tmp; my $default_filename = $ENV{'course.'.$ENV{'request.course.id'}. '.spreadsheet_default_assesscalc'}; # We only got one key, so we will access it directly. while (my ($key,$sheetdata) = each(%tmp)) { my ($sname,$sdom,$sheettype,$symb) = split(':',$key); if ($symb =~ /\.time$/) { $symb =~ s/\.time$//; $Exportrows{$symb}->{'time'} = $sheetdata; } else { $sheetdata =~ s/^(.*)___=___//; my $filename = $1; $filename = $default_filename if (! defined($filename)); my @Data = split('___;___',$sheetdata); $Exportrows{$symb}->{$filename} = \@Data; } } } } ############################################# ############################################# =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 $symb = $self->{'symb'}; if (! exists($ENV{'request.role.adv'}) || ! $ENV{'request.role.adv'} || ! exists($Exportrows{$symb}) || ! defined($Exportrows{$symb}) || ! $self->check_expiration_time($Exportrows{$symb}->{'time'}) || ! exists($Exportrows{$symb}->{$self->{'filename'}}) || ! defined($Exportrows{$symb}->{$self->{'filename'}}) || ! ref($Exportrows{$symb}->{$self->{'filename'}}) ) { $self->compute($r); } if ($connection->aborted()) { $self->cleanup(); return; } my @Data = @{$Exportrows{$symb}->{$self->{'filename'}}}; if ($Data[0] =~ /^(.*)___=___/) { $self->{'sheetname'} = $1; $Data[0] =~ s/^(.*)___=___//; } for (my $i=0;$i<$#Data;$i++) { $Data[$i]="'".$Data[$i]."'" if ($Data[$i]=~/\D/ && defined($Data[$i])); } return @Data; } ############################################# ############################################# =pod =item &save_export_data() Writes the export data for this spreadsheet to the students cache. =cut ############################################# ############################################# sub save_export_data { my $self = shift; return if ($self->temporary()); my $student = $self->{'name'}.':'.$self->{'domain'}; my $symb = $self->{'symb'}; if (! exists($Exportrows{$symb}) || ! exists($Exportrows{$symb}->{$self->{'filename'}})) { return; } my $key = join(':',($self->{'name'},$self->{'domain'},'assesscalc',$symb)); my $timekey = $key.'.time'; my $newstore= join('___;___', map {s/[^[:print:]]//g;$_;} # strip out unprintable @{$Exportrows{$symb}->{$self->{'filename'}}}); $newstore = $self->{'filename'}.'___=___'.$newstore; $newExportrows{$student}->{$key} = $newstore; $newExportrows{$student}->{$timekey} = $Exportrows{$symb}->{'time'}; return; } 1; __END__