--- loncom/interface/statistics/lonstudentsubmissions.pm 2004/02/23 16:13:31 1.4 +++ loncom/interface/statistics/lonstudentsubmissions.pm 2011/11/18 22:20:20 1.54.10.3 @@ -1,6 +1,6 @@ # The LearningOnline Network with CAPA # -# $Id: lonstudentsubmissions.pm,v 1.4 2004/02/23 16:13:31 matthew Exp $ +# $Id: lonstudentsubmissions.pm,v 1.54.10.3 2011/11/18 22:20:20 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -27,7 +27,7 @@ package Apache::lonstudentsubmissions; use strict; -use Apache::lonnet(); +use Apache::lonnet; use Apache::loncommon(); use Apache::lonhtmlcommon(); use Apache::loncoursedata(); @@ -37,20 +37,15 @@ use Apache::lonstathelpers; use HTML::Entities(); use Time::Local(); use Spreadsheet::WriteExcel(); +use Crypt::PasswdMD5; +use lib '/home/httpd/lib/perl/'; +use LONCAPA; + -my @SubmitButtons = ({ name => 'PrevProblem', - text => 'Previous Problem' }, - { name => 'NextProblem', - text => 'Next Problem' }, - { name => 'break'}, - { name => 'ClearCache', - text => 'Clear Caches' }, - { name => 'updatecaches', - text => 'Update Student Data' }, - { name => 'SelectAnother', +my @SubmitButtons = ({ name => 'SelectAnother', text => 'Choose a different Problem' }, { name => 'Generate', - text => 'Generate Spreadsheet'}, + text => 'Generate Report'}, ); sub BuildStudentSubmissionsPage { @@ -72,25 +67,18 @@ sub BuildStudentSubmissionsPage { my @Students = @Apache::lonstatistics::Students; # if (@Students < 1) { - $r->print('

There are no students in the sections selected

'); + $r->print('
' + .&mt('There are no students in the sections selected.') + .'
'); } # - &Apache::loncoursedata::clear_internal_caches(); - if (exists($ENV{'form.ClearCache'}) || - exists($ENV{'form.updatecaches'}) || - (exists($ENV{'form.firstanalysis'}) && - $ENV{'form.firstanalysis'} ne 'no')) { - &Apache::lonstatistics::Gather_Full_Student_Data($r); - } - if (! exists($ENV{'form.firstanalysis'})) { - $r->print(''); - } else { - $r->print(''); - } + my @CacheButtonHTML = + &Apache::lonstathelpers::manage_caches($r,'Statistics','stats_status', + '
'.&mt('Loading student data...').'
'); $r->rflush(); # - if (exists($ENV{'form.problemchoice'}) && - ! exists($ENV{'form.SelectAnother'})) { + if (exists($env{'form.problemchoice'}) && + ! exists($env{'form.SelectAnother'})) { foreach my $button (@SubmitButtons) { if ($button->{'name'} eq 'break') { $r->print("
\n"); @@ -100,182 +88,802 @@ sub BuildStudentSubmissionsPage { $r->print(' 'x5); } } + foreach my $html (@CacheButtonHTML) { + $r->print($html.(' 'x5)); + } # - $r->print('
'); + $r->print('
'.$/); $r->rflush(); # - # Determine which problem we are to analyze - my $current_problem = &Apache::lonstathelpers::get_target_from_id - ($ENV{'form.problemchoice'}); - # - my ($prev,$curr,$next) = - &Apache::lonstathelpers::get_prev_curr_next($current_problem, - '.', - 'response', - ); - if (exists($ENV{'form.PrevProblem'}) && defined($prev)) { - $current_problem = $prev; - } elsif (exists($ENV{'form.NextProblem'}) && defined($next)) { - $current_problem = $next; - } else { - $current_problem = $curr; + # Determine which problems we are to analyze + my @Symbs = + &Apache::lonstathelpers::get_selected_symbs('problemchoice'); + foreach my $selected (@Symbs) { + $r->print(''.$/); } # - # Store the current problem choice and send it out in the form - $ENV{'form.problemchoice'} = - &Apache::lonstathelpers::make_target_id($current_problem); - $r->print(''); - # - if (! defined($current_problem->{'resource'})) { + # Get resource objects + my $navmap = Apache::lonnavmaps::navmap->new(); + if (!defined($navmap)) { + $r->print('
'.&mt("Internal error").'
'); + return; + } + my %already_seen; + my (@Problems,$show_named); + unless (&Apache::loncommon::needs_gci_custom()) { + $show_named = 1; + } + foreach my $symb (@Symbs) { + my $resource = $navmap->getBySymb($symb); + push(@Problems,$resource); + } + # If these are to be anonymized, do a random shuffle of @Students. + unless ($show_named) { + &array_shuffle(\@Students); + } + my $threshold = $env{'course.'.$env{'request.course.id'}.'.internal.anonsurvey_threshold'}; + if ($threshold eq '') { + my %domconfig = + &Apache::lonnet::get_dom('configuration',['coursedefaults'], + $env{'course.'.$env{'request.course.id'}.'.domain'}); + if (ref($domconfig{'coursedefaults'}) eq 'HASH') { + $threshold = $domconfig{'coursedefaults'}{'anonsurvey_threshold'}; + if ($threshold eq '') { + $threshold = 10; + } + } else { + $threshold = 10; + } + } + # + $r->print('

'. + &Apache::lonstatistics::section_and_enrollment_description(). + '

'); + if (! scalar(@Problems) || ! defined($Problems[0])) { $r->print('resource is undefined'); + } elsif (!$show_named && @Students < $threshold) { + $r->print(&mt('The number of students matching the selection criteria is too few for display of submission data for anonymous surveys.').'
'.&mt('There must be at least [quant,_1,student].',$threshold).' '.&mt('Contact the LON-CAPA [_1]Helpdesk[_2] if you need the threshold to be changed for this course.',''); + &mt('Prepare Report').'" />'); $r->print(' 'x5); - $r->print('

'.&mt('Please select a problem to analyze').'

'); - $r->print(&Apache::lonstathelpers::ProblemSelector('.')); + $r->print('

'. + &mt('Computing correct answers greatly increasese the amount of time required to prepare a report.'). + '

'); + $r->print('

'. + &mt('Please select problems and use the [_1]Prepare Report[_2] button to continue.','',''). + '

'); + $r->print(&Apache::lonstathelpers::MultipleProblemSelector + (undef,'problemchoice','Statistics')); } } +sub array_shuffle { + my $array = shift; + return unless (ref($array) eq 'ARRAY'); + my $i = scalar(@$array); + my $j; + foreach my $item (@$array) { + --$i; + $j = int(rand($i+1)); + next if($i == $j); + @$array [$i,$j] = @$array[$j,$i]; + } + return @$array; +} + +## +## get_extra_response_headers +## + +sub get_extra_response_headers { + my ($show_named) = @_; + my @extra_resp_headers; + if ($env{'form.correctans'} eq 'true') { + push(@extra_resp_headers,'Correct'); + } + if ($show_named) { + if ($env{'form.prob_status'} eq 'true') { + push(@extra_resp_headers,'Award Detail'); + push(@extra_resp_headers,'Time'); + push(@extra_resp_headers,'Attempt'); + push(@extra_resp_headers,'Awarded'); + } + } + return @extra_resp_headers; +} + +## +## get_headers: +## return the proper headers for the given response +sub get_headers { + my ($prob,$partid,$respid,$resptype,$analysis,$output,$purpose, + @basic_headers) = @_; + my @headers; + if ($resptype eq 'essay' && $purpose eq 'display' && + ($output eq 'html')) {# || scalar(@{$prob->parts})!=1)) { + @headers = (); + } elsif ($resptype =~ /^(option|match|rank)$/) { + my $prefix = '_'; + if ($purpose eq 'display') { + $prefix = ''; + } + my @foils = + map { + $prefix.$_; + } sort(keys(%{$analysis->{$partid.'.'.$respid}->{'_Foils'}})); + if (scalar(@basic_headers) && $basic_headers[0] eq 'Correct') { + @foils = map { ($_ , $_.' Correct') } @foils; + shift(@basic_headers); # Get rid of 'Correct' + } + @headers = (@foils,@basic_headers); + } elsif (lc($resptype) eq 'task') { + @headers = ('Grader','Status',@basic_headers,'Submission'); + } else { + @headers = ('Submission',@basic_headers); + } + return @headers; +} ######################################################### ######################################################### ## -## Excel output of student answers and correct answers +## HTML Output Routines ## ######################################################### ######################################################### -sub prepare_excel_output { - my ($r,$problem,$ProblemData,$Students) = @_; - my ($resource,$respid,$partid) = ($problem->{'resource'}, - $problem->{'respid'}, - $problem->{'part'}); - $r->print('

'. - &mt('Preparing Excel spreadsheet of student responses'). - '

'); +sub prepare_html_output { + my ($r,$problems,$students,$show_named) = @_; + my $c = $r->connection(); + my $salt = '$1$'.$Apache::lonnet::perlvar{'AnonymousSalt'}; + # + # Set a flag for the case when there is just one problem + my $single_response = 0; + if (scalar(@$problems) == 1 && + $problems->[0]->countResponses == 1) { + $single_response = 1; + } # - &GetStudentAnswers($r,$problem,$Students); + # Compute the number of columns per response + my @extra_resp_headers = &get_extra_response_headers($show_named); # - my @Columns = ( 'username','domain','attempt','time', - 'submission','correct', 'grading','awarded','weight', - 'score'); - my $awarded_col = 7; - my $weight_col = 8; + # Create the table header + my @student_columns; + if ($show_named) { + @student_columns = @Apache::lonstatistics::SelectedStudentData; + if (grep(/^all$/,@student_columns)) { + @student_columns = qw(fullname username domain id section status groups comments); + } + } else { + @student_columns = ('username'); + } # - # Create excel worksheet - my $filename = '/prtspool/'. - $ENV{'user.name'}.'_'.$ENV{'user.domain'}.'_'. - time.'_'.rand(1000000000).'.xls'; - my $workbook = Spreadsheet::WriteExcel->new('/home/httpd'.$filename); - if (! defined($workbook)) { - $r->log_error("Error creating excel spreadsheet $filename: $!"); - $r->print('

'.&mt("Unable to create new Excel file. ". - "This error has been logged. ". - "Please alert your LON-CAPA administrator"). - '

'); - return undef; + my %headers; + my $student_column_count = scalar(@student_columns); + $headers{'problem'} = qq{\ }; + foreach (@student_columns) { + $headers{'student'}.= ''.ucfirst($_).''; + } + # + # we put the headers into the %headers hash + my $total_col = scalar(@student_columns); + my $nonempty_part_headers = 0; + # + my %problem_analysis; + foreach my $prob (@$problems) { + my %analysis = &Apache::lonstathelpers::get_problem_data($prob->src); + $problem_analysis{$prob->src}=\%analysis; + # + my $prob_span = 0; + my $single_part = 0; + if (scalar(@{$prob->parts}) == 1) { + $single_part = 1; + } + foreach my $partid (@{$prob->parts}) { + my $part_span = 0; + my $responses = [$prob->responseIds($partid)]; + my $resptypes = [$prob->responseType($partid)]; + for (my $i=0;$i[$i]; + my @headers = &get_headers($prob,$partid,$respid, + $resptypes->[$i], + $problem_analysis{$prob->src}, + 'html','display', + @extra_resp_headers); + if (scalar(@headers)>0) { + $total_col += scalar(@headers); + $part_span += scalar(@headers); + $headers{'response'} .= + ''. + &mt('Response [_1]',$responses->[$i]).''; + $headers{'student'}.= ''. + join('', + @headers). + ''; + } + } + if ($part_span == 0) { + next; + } + if (! $single_part) { + my $tmpname = $partid; + if ($partid =~/^\d+$/) { + $tmpname = $prob->part_display($partid); + } + if ($tmpname !~ /^part/) { + $tmpname = 'Part '.$tmpname; + } + $headers{'part'} .= qq{$tmpname}; + $nonempty_part_headers = 1; + } else { + $headers{'part'} .= qq{ }; + } + $prob_span += $part_span; + } + my $title = $prob->compTitle; + if ($prob_span > 0) { + $headers{'problem'}.= qq{$title}; + } elsif ($single_response) { + $prob_span = scalar(@student_columns); + $headers{'problem'} = qq{$title}; + } + } + if (exists($headers{'part'})) { + $headers{'part'} = qq{\ }. + $headers{'part'}; + } + if (exists($headers{'response'})) { + $headers{'response'}= + qq{\ }. + $headers{'response'}; + } + my $full_header = $/.''.$/; + $full_header .= ''.$headers{'problem'}.''.$/; + if ($nonempty_part_headers) { + $full_header .= ''.$headers{'part'}.''.$/; } + $full_header .= ''.$headers{'response'}.''.$/; + $full_header .= ''.$headers{'student'}.''.$/; # - $workbook->set_tempdir('/home/httpd/perl/tmp'); + # Main loop + my $count; + $r->print($/.$full_header.$/); + my $row_class = 'odd'; # css + foreach my $student (@$students) { + my $student_row_data; + if ($count++ >= 30) { + $r->print('
'.$/.$full_header.$/); + $count = 0; + } + last if ($c->aborted()); + if ($show_named) { + foreach my $field (@student_columns) { + $student_row_data .= ''; + # handle comments like in lonstudentassessment.pm + if($field eq 'comments') { + $student_row_data .= + '
'.&mt('Comments').''; + } else { + $student_row_data .= $student->{$field}; + } + $student_row_data .= ''; + } + } else { + my $anonid = &Crypt::PasswdMD5::unix_md5_crypt($student->{'username'}, + $salt); + $anonid = substr($anonid,length($salt)+1); + $student_row_data = ''. + $anonid.''; + } + # + # Figure out what it is we need to output for this student + my @essays; + my %prob_data; + my $maxrow; + foreach my $prob (@$problems) { + $prob_data{$prob->symb}={}; + foreach my $partid (@{$prob->parts}) { + my @responses = $prob->responseIds($partid); + my @response_type = $prob->responseType($partid); + for (my $i=0;$i<=$#responses;$i++) { + my $respid = $responses[$i]; + my $results = + &Apache::loncoursedata::get_response_data_by_student + ($student,$prob->symb(),$respid); + my $resptype = $response_type[$i]; + my @headers = &get_headers($prob,$partid,$respid, + $resptype, + $problem_analysis{$prob->src}, + 'html','normal', + @extra_resp_headers); + my $width = scalar(@headers); + next if ($width < 1); + my $resp_data; + $resp_data->{'fake'} = qq{ }; + if (! defined($results)) { + $results = []; + } + # + if (scalar(@$results) > $maxrow && $resptype ne 'essay') { + $maxrow = scalar(@$results); + } + for (my $j=scalar(@$results)-1;$j>=0;$j--) { + if ($env{'form.all_sub'} ne 'true') { + next if ($j ne scalar(@$results)-1); + } + my $response = &hashify_response($results->[$j], + $prob, + $student, + $partid, + $respid); + if ($resptype eq 'essay') { + push(@essays, + &html_essay_results(\@headers, + $prob,$partid,$respid, + $response, + $single_response). + ''); + } elsif (lc($resptype) eq 'task') { + my $results = + &html_task_results(\@headers, + $prob,$partid,$respid, + $response,$resptype); + if ($results) { + push(@{$resp_data->{'real'}},$results); + } + } else { + push(@{$resp_data->{'real'}}, + &html_non_essay_results(\@headers, + $prob,$partid,$respid, + $response,$resptype)); + } + } + $prob_data{$prob->symb}->{$partid}->{$respid}=$resp_data; + } # end of $i loop + } # end of partid loop + } # end of prob loop + # + # if there is no data, skip this student. + next if (! $maxrow && ! scalar(@essays)); + # + # Go through the problem data and output a row. + if ($row_class eq 'even') { + $row_class = 'odd'; + } else { + $row_class = 'even'; + } + my $printed_something; + for (my $rows_output = 0;$rows_output<$maxrow;$rows_output++) { + my $html; + my $no_data = 1; + foreach my $prob (@$problems) { + foreach my $partid (@{$prob->parts}) { + my @responses = $prob->responseIds($partid); + my @response_type = $prob->responseType($partid); + for (my $i=0;$i<=$#responses;$i++) { + my $respid = $responses[$i]; + my $resp_data = + $prob_data{$prob->symb}->{$partid}->{$respid}; + next if ($response_type[$i] eq 'essay'); + if (defined($resp_data->{'real'}->[$rows_output])) { + $html .= $resp_data->{'real'}->[$rows_output]; + $no_data = 0; + } else { + $html .= $resp_data->{'fake'}; + } + } + } + } + if (! $no_data) { + $r->print(qq{$student_row_data$html}.$/); + $printed_something=1; + } + } + if (@essays) { + my $tr = qq{}; + my $td = qq{}; + if (! $printed_something) { + $r->print($tr.$student_row_data.''.$/); + } + $r->print($tr.$td. + join(''.$/.$tr.$td,@essays).''.$/); + undef(@essays); + } + } # end of student loop + $r->print(''.$/); + return; +} + +sub hashify_response { + my ($response,$prob,$student,$partid,$respid) =@_; + my $resp_hash = {}; + if ($env{'form.correctans'} eq 'true') { + $resp_hash->{'Correct'} = + &Apache::lonstathelpers::get_student_answer + ($prob,$student->{'username'},$student->{'domain'}, + $partid,$respid); + } + $resp_hash->{'Submission'} = + $response->[&Apache::loncoursedata::RDs_submission()]; + $resp_hash->{'Time'} = + $response->[&Apache::loncoursedata::RDs_timestamp()]; + $resp_hash->{'Attempt'} = + $response->[&Apache::loncoursedata::RDs_tries()]; + $resp_hash->{'Awarded'} = + $response->[&Apache::loncoursedata::RDs_awarded()]; + if ($prob->is_task()) { + $resp_hash->{'Grader'} = + $response->[&Apache::loncoursedata::RDs_response_eval_2()]; + if ($resp_hash->{'Attempt'} eq '0') { + $resp_hash->{'Attempt'} = ''; + } + $resp_hash->{'Award Detail'} = + $response->[&Apache::loncoursedata::RDs_part_award()]; + $resp_hash->{'Status'} = + $response->[&Apache::loncoursedata::RDs_response_eval()]; + } else { + $resp_hash->{'Award Detail'} = + $response->[&Apache::loncoursedata::RDs_awarddetail()]; + } + + return $resp_hash; +} + +##################################################### +## +## HTML helper routines +## +##################################################### +sub html_essay_results { + my ($headers,$prob,$partid,$respid,$response,$single_response)=@_; + if (! ref($headers) || ref($headers) ne 'ARRAY') { + return ''; + } + # Start of telling them what problem, part, and response + my $Str; + if (! $single_response) { + my $id = $prob->compTitle; + if (defined($partid) && $partid ne '0') { + $id .= ' '.$prob->part_display($partid); + } + if (defined($respid)) { + $id .= ' '.$respid; + } + $Str .= ''.$id.''.(' 'x4); + } # - my $format = &Apache::loncommon::define_excel_formats($workbook); - my $worksheet = $workbook->addworksheet('Student Submission Data'); + shift(@$headers); # Get rid of the Submission header + my $correct = ''; + if ($headers->[0] eq 'Correct') { + $correct = &html_format_essay_sub($response->{'Correct'}); + shift(@$headers); + } + $Str .= ''. + join('', + map { + (' 'x4).&mt($_.': [_1]',$response->{$_}); + } @$headers).''; + if (@$headers || ! $single_response) { + $Str .= '
'; + } + $Str .= &html_format_essay_sub($response->{'Submission'}); + # + if (defined($correct) && $correct !~ /^\s*$/) { + $Str .= '
'.&mt('Correct').''.$correct + } + return $Str; +} + +sub html_format_essay_sub { + my ($submission) = @_; + return '' if (! defined($submission) || $submission eq ''); + $submission = &HTML::Entities::decode($submission); + $submission =~ s/\\\"/\"/g; + $submission =~ s/\\\'/\'/g; + $submission =~ s|\\r\\n|$/|g; + $submission = &HTML::Entities::encode($submission,'<>&"'); + $submission =~ s|$/\s*$/|$/

$/|g; + $submission =~ s|\\||g; + $submission = '

'.$submission.'

'; + return $submission; +} + +sub html_task_results { + my ($headers,$prob,$partid,$respid,$response,$resptype) = @_; + if (! ref($headers) || ref($headers) ne 'ARRAY' || ! scalar(@$headers)) { + return ''; + } + + my @values; + @values = map { $response->{$_}; } @$headers; + + my $td = ''; + my $str = $td.join(''.$td,@values).''; + return $str; +} + +sub html_non_essay_results { + my ($headers,$prob,$partid,$respid,$response,$resptype) = @_; + if (! ref($headers) || ref($headers) ne 'ARRAY' || ! scalar(@$headers)) { + return ''; + } + # + my $submission = &HTML::Entities::decode(&unescape($response->{'Submission'})); + return '' if (! defined($submission) || $submission eq ''); + $submission =~ s/\\\"/\"/g; + $submission =~ s/\\\'/\'/g; + if ($resptype eq 'radiobutton') { + $submission = &HTML::Entities::encode($submission,'<>&"'); + $submission =~ s/=([^=])$//; + $submission = ''.$submission.''; + } + $response->{'Submission'} = $submission; + # + my @values; + if ($resptype =~ /^(option|match|rank)$/) { + my %submission = + map { + my ($foil,$value) = split('=',&unescape($_)); + ($foil,$value); + } split('&',$response->{'Submission'}); + my %correct; + if (exists($response->{'Correct'})) { + %correct = + map { + my ($foil,$value)=split('=',&unescape($_)); + ($foil,$value); + } split('&',$response->{'Correct'}); + } + # + foreach my $original_header (@$headers) { + if ($original_header =~ /^_/) { + # '_' denotes a foil column + my ($header) = ($original_header =~ m/^_(.*)$/); + my $option = ''; + if ( my ($foil) = ($header =~ /(.*) Correct$/)) { + if (exists($correct{$foil})) { + $option = $correct{$foil}; + } + } elsif (exists($submission{$header})) { + $option = $submission{$header}; + } + push(@values,&HTML::Entities::encode($option)); + } elsif ($original_header eq 'Time') { + push(@values,&Apache::lonlocal::locallocaltime($response->{$original_header})); + } else { + # A normal column + push(@values,$response->{$original_header}); + } + } + } else { + @values = map { $response->{$_}; } @$headers; + } + my $td = ''; + my $str = $td.join(''.$td,@values).''; + return $str; +} + + +######################################################### +######################################################### +## +## Excel Output Routines +## +######################################################### +######################################################### +sub prepare_excel_output { + my ($r,$Problems,$Students,$show_named) = @_; + my $c = $r->connection(); + my $salt = '$1$'.$Apache::lonnet::perlvar{'AnonymousSalt'}; + # + # + # Determine the number of columns in the spreadsheet + my $columncount = 3; # username, domain, id + my @extra_resp_headers = &get_extra_response_headers(); + my $lastprob; + my %problem_analysis; + foreach my $prob (@$Problems) { + my %analysis = &Apache::lonstathelpers::get_problem_data($prob->src); + $problem_analysis{$prob->src}=\%analysis; + foreach my $partid (@{$prob->parts}) { + my $responses = [$prob->responseIds($partid)]; + my $resptypes = [$prob->responseType($partid)]; + for (my $i=0;$i[$i], + $resptypes->[$i], + $problem_analysis{$prob->src}, + 'excel','display', + @extra_resp_headers); + $columncount += scalar(@headers); + } + } + last if ($columncount > 255); + $lastprob = $prob; + } + if ($columncount > 255) { + $r->print('

'.&mt('Unable to complete request').'

'.$/. + '

'.&mt('LON-CAPA is unable to produce your Excel spreadsheet because your selections will result in more than 255 columns. Excel allows only 255 columns in a spreadsheet.').'

'.$/. + '

'.&mt('Consider selecting fewer problems to generate reports on, or reducing the number of items per problem. Or use HTML or CSV output.').'

'.$/. + '

'.&mt('The last problem that will fit in the current spreadsheet is [_1].',$lastprob->compTitle).'

'); + $r->rflush(); + return; + } + # + # Print out a message telling them what we are doing + if (scalar(@$Problems) > 1) { + $r->print('

'. + &mt('Preparing Excel spreadsheet of student responses to [_1] problems', + scalar(@$Problems)). + '

'); + } else { + $r->print('

'. + &mt('Preparing Excel spreadsheet of student responses'). + '

'); + } + $r->rflush(); # - # Make sure we get new weight data instead of data on a 10 minute delay - &Apache::lonnet::clear_EXT_cache_status(); + # Create the excel spreadsheet + my ($workbook,$filename,$format) = + &Apache::loncommon::create_workbook($r); + return if (! defined($workbook)); + my $worksheet = $workbook->addworksheet('Student Submission Data'); # - # Put on the standard headers and whatnot - my $rows_output=0; - $worksheet->write($rows_output++,0,$resource->{'title'},$format->{'h1'}); - $worksheet->write($rows_output++,0,$resource->{'src'},$format->{'h3'}); + # Add headers to the worksheet + my $rows_output = 0; + $worksheet->write($rows_output++,0, + $env{'course.'.$env{'request.course.id'}.'.description'}, + $format->{'h1'}); $rows_output++; - $worksheet->write_row($rows_output++,0,\@Columns,$format->{'bold'}); + my $cols_output = 0; + my $title_row = $rows_output++; + my $partid_row = $rows_output++; + my $respid_row = $rows_output++; + my $header_row = $rows_output++; + $worksheet->write($title_row ,0,'Problem Title',$format->{'bold'}); + $worksheet->write($partid_row,0,'Part ID',$format->{'bold'}); + $worksheet->write($respid_row,0,'Response ID',$format->{'bold'}); + # Student headers + my @StudentColumns; + if ($show_named) { + @StudentColumns = qw(username domain id section); + } else { + @StudentColumns = qw(username); + } + foreach (@StudentColumns) { + $worksheet->write($header_row,$cols_output++,ucfirst($_), + $format->{'bold'}); + } + # Problem headers + my %start_col; + foreach my $prob (@$Problems) { + my $title = $prob->compTitle; + $worksheet->write($title_row,$cols_output, + $title,$format->{'h3'}); + foreach my $partid (@{$prob->parts}) { + $worksheet->write($partid_row,$cols_output, + $prob->part_display($partid)); + my $responses = [$prob->responseIds($partid)]; + my $resptypes = [$prob->responseType($partid)]; + for (my $i=0;$isymb}->{$partid}->{$responses->[$i]}= + $cols_output; + $worksheet->write($respid_row,$cols_output, + $resptypes->[$i].', '.$responses->[$i]); + my @headers = &get_headers($prob,$partid,$responses->[$i], + $resptypes->[$i], + $problem_analysis{$prob->src}, + 'excel','display', + @extra_resp_headers); + foreach my $text (@headers) { + if ($text eq 'Time') { + $worksheet->set_column($cols_output,$cols_output,undef, + $format->{'date'}); + } + $worksheet->write($header_row,$cols_output++,$text); + } + } + } + } # # Populate the worksheet with the student data + my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin + ($r,'Excel File Compilation Status', + 'Excel File Compilation Progress', + scalar(@$Students),'inline',undef,'Statistics','stats_status'); + my $max_row = $rows_output; foreach my $student (@$Students) { - my $results = &Apache::loncoursedata::get_response_data_by_student - ($student,$resource->{'symb'},$respid); - my %row; - $row{'username'} = $student->{'username'}; - $row{'domain'} = $student->{'domain'}; - $row{'correct'} = $student->{'answer'}; - $row{'weight'} = &Apache::lonnet::EXT - ('resource.'.$partid.'.weight',$resource->{'symb'}, - undef,undef,undef); - if (! defined($results) || ref($results) ne 'ARRAY') { - $row{'score'} = '='. - &Spreadsheet::WriteExcel::Utility::xl_rowcol_to_cell - ($rows_output,$awarded_col) - .'*'. - &Spreadsheet::WriteExcel::Utility::xl_rowcol_to_cell - ($rows_output,$weight_col); - my $cols_output = 0; - foreach my $col (@Columns) { - if (! exists($row{$col})) { - $cols_output++; - next; + last if ($c->aborted()); + $cols_output = 0; + my $student_row = $max_row; + my $anonid = &Crypt::PasswdMD5::unix_md5_crypt($student->{'username'}, + $salt); + $anonid = substr($anonid,length($salt)+1); + foreach my $prob (@$Problems) { + foreach my $partid (@{$prob->parts}) { + my @Response = $prob->responseIds($partid); + my @ResponseType = $prob->responseType($partid); + for (my $i=0;$i<=$#Response;$i++) { + my $respid = $Response[$i]; + my $resptype = $ResponseType[$i]; + my $results = + &Apache::loncoursedata::get_response_data_by_student + ($student,$prob->symb(),$respid); + my @headers = &get_headers($prob,$partid,$respid, + $resptype, + $problem_analysis{$prob->src}, + 'excel','normal', + @extra_resp_headers); + + if (! defined($results)) { + $results = []; + } + # + $rows_output = $student_row; + # + my $response_start_col = $start_col{$prob->symb}->{$partid}->{$respid}; + for (my $j=scalar(@$results)-1;$j>=0;$j--) { + $cols_output = $response_start_col; + if ($env{'form.all_sub'} ne 'true') { + next if ($j ne scalar(@$results)-1); + } + my $response = &hashify_response($results->[$j], + $prob, + $student, + $partid, + $respid); + my @response_data = + &compile_response_data(\@headers,$response, + $prob,$partid,$respid, + $resptype, + \&excel_format_item); + $worksheet->write_row($rows_output++,$cols_output, + \@response_data); + $cols_output+=scalar(@response_data); + if ($rows_output > $max_row) { + $max_row = $rows_output; + } + } } - $worksheet->write($rows_output,$cols_output++,$row{$col}); } - $rows_output++; - } else { - foreach my $response (@$results) { - delete($row{'time'}); - delete($row{'attempt'}); - delete($row{'submission'}); - delete($row{'awarded'}); - delete($row{'grading'}); - delete($row{'score'}); - my %row_format; - # - # Time is handled differently - $row{'time'} = &Apache::lonstathelpers::calc_serial - ($response->[&Apache::loncoursedata::RDs_timestamp()]); - $row_format{'time'}=$format->{'date'}; - # - $row{'attempt'} = $response->[ - &Apache::loncoursedata::RDs_tries()]; - $row{'submission'} = $response->[ - &Apache::loncoursedata::RDs_submission()]; - if ($row{'submission'} =~ m/^=/) { - # This will be interpreted as a formula. That is bad! - $row{'submission'} = " ".$row{'submission'}; - } - $row{'grading'} = $response->[ - &Apache::loncoursedata::RDs_awarddetail()]; - $row{'awarded'} = $response->[ - &Apache::loncoursedata::RDs_awarded()]; - $row{'score'} = '='. - &Spreadsheet::WriteExcel::Utility::xl_rowcol_to_cell - ($rows_output,$awarded_col) - .'*'. - &Spreadsheet::WriteExcel::Utility::xl_rowcol_to_cell - ($rows_output,$weight_col); - my $cols_output = 0; - foreach my $col (@Columns) { - $worksheet->write($rows_output,$cols_output++,$row{$col}, - $row_format{$col}); + } + # Prepend current student's user information to all rows + for (my $row = $student_row;$row<$max_row;$row++) { + my $cols = 0; + foreach my $field (@StudentColumns) { + if ($show_named) { + $worksheet->write($row,$cols++, + $student->{$field}); + } else { + $worksheet->write($row,$cols++, + $anonid); } - $rows_output++; } - } # End of else clause on if (! defined($results) .... + } + &Apache::lonhtmlcommon::Increment_PrgWin($r,\%prog_state, + 'last student'); } + &Apache::lonhtmlcommon::Close_PrgWin($r,\%prog_state); # # Close the excel file $workbook->close(); @@ -284,36 +892,272 @@ sub prepare_excel_output { $r->print('

'. &mt('Your Excel spreadsheet.'). '

'."\n"); + $r->print(''); + $r->rflush(); + return; +} + +sub compile_response_data { + my ($headers,$response,$prob,$partid,$respid,$resptype,$format) = @_; + if (! ref($headers) || ref($headers) ne 'ARRAY' || ! scalar(@$headers)) { + return (); + } + if (ref($format) ne 'CODE') { + $format = sub { return $_[0]; }; + } + # + my $submission = + &HTML::Entities::decode + (&unescape($response->{'Submission'})); + if (!$prob->is_task()) { + return () if (! defined($submission) || $submission eq ''); + } + $submission =~ s/\\\"/\"/g; + $submission =~ s/\\\'/\'/g; + if ($resptype eq 'radiobutton') { + $submission =~ s/=([^=])$//; + } + $response->{'Submission'} = $submission; + # + my @values; + if ($resptype =~ /^(option|match|rank)$/) { + my %submission = + map { + my ($foil,$value) = split('=',&unescape($_)); + ($foil,$value); + } split('&',$response->{'Submission'}); + my %correct; + if (exists($response->{'Correct'})) { + %correct = + map { + my ($foil,$value)=split('=',&unescape($_)); + ($foil,$value); + } split('&',$response->{'Correct'}); + } + # + foreach my $original_header (@$headers) { + if ($original_header =~ /^_/) { + # '_' denotes a foil column + my ($header) = ($original_header =~ m/^_(.*)$/); + my $option = ''; + if ( my ($foil) = ($header =~ /(.*) Correct$/)) { + if (exists($correct{$foil})) { + $option = $correct{$foil}; + } + } elsif (exists($submission{$header})) { + $option = $submission{$header}; + } + push(@values,&{$format}($option,$header)); + } else { + # A normal column + push(@values,&{$format}($response->{$original_header}, + $original_header)); + } + } + } else { + @values = map { &{$format}($response->{$_},$_); } @$headers; + } + return @values; +} + +sub excel_format_item { + my ($item,$type) = @_; + if ($type eq 'Time') { + $item = &Apache::lonstathelpers::calc_serial($item); + } else { + if ($item =~ m/^=/) { + $item = ' '.$item; + } + $item =~ s/\\r//g; + $item =~ s/\\n/\n/g; + $item =~ s/(\s*$|^\s*)//g; + $item =~ s/\\\'/\'/g; + } + return $item; } -sub GetStudentAnswers { - my ($r,$problem,$Students) = @_; - my %Answers; - my ($resource,$partid,$respid) = ($problem->{'resource'}, - $problem->{'part'}, - $problem->{'respid'}); - # Open progress window +######################################################### +######################################################### +## +## CSV output of student answers +## +######################################################### +######################################################### +sub prepare_csv_output { + my ($r,$problems,$students,$show_named) = @_; + my $c = $r->connection(); + my $salt = '$1$'.$Apache::lonnet::perlvar{'AnonymousSalt'}; + # + $r->print('

'. + &mt('Generating CSV report of student responses').'

'); + # + # Progress window my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin - ($r,'Student Answer Compilation Status', - 'Student Answer Compilation Progress', scalar(@$Students)); - $r->print("\n"); + ($r,'CSV File Compilation Status', + 'CSV File Compilation Progress', + scalar(@$students),'inline',undef,'Statistics','stats_status'); + $r->rflush(); - foreach my $student (@$Students) { - my $sname = $student->{'username'}; - my $sdom = $student->{'domain'}; - my $answer = &Apache::lonstathelpers::analyze_problem_as_student - ($resource,$sname,$sdom,$partid,$respid); + # + # Open a file + my $outputfile; + my $filename = '/prtspool/'. + $env{'user.name'}.'_'.$env{'user.domain'}.'_'. + time.'_'.rand(1000000000).'.csv'; + unless ($outputfile = Apache::File->new('>/home/httpd'.$filename)) { + $r->log_error("Couldn't open $filename for output $!"); + $r->print('
' + .&mt('Problems occurred in writing the CSV file. ' + .'This error has been logged. ' + .'Please alert your LON-CAPA administrator.') + .'
'); + $outputfile = undef; + } + # + # Compute the number of columns per response + my @extra_resp_headers = &get_extra_response_headers($show_named); + # + # Create the table header + my @student_columns; + if ($show_named) { + @student_columns = qw(username domain id section); + } else { + @student_columns = qw(username); + } + # + my %headers; + push(@{$headers{'student'}},@student_columns); + # Pad for the student data + foreach my $row ('problem','part','response') { + $headers{$row}=[map {''} @student_columns]; + } + # + # we put the headers into the %headers hash + my %problem_analysis; + my %start_col; + my $max_column = scalar(@student_columns); + foreach my $prob (@$problems) { + my %analysis = &Apache::lonstathelpers::get_problem_data($prob->src); + $problem_analysis{$prob->src}=\%analysis; + $headers{'problem'}->[$max_column] = $prob->compTitle; + foreach my $partid (@{$prob->parts}) { + $headers{'part'}->[$max_column] = $prob->part_display($partid); + my $responses = [$prob->responseIds($partid)]; + my $resptypes = [$prob->responseType($partid)]; + for (my $i=0;$i[$i], + $resptypes->[$i], + $problem_analysis{$prob->src}, + 'csv','display', + @extra_resp_headers); + $start_col{$prob->symb}->{$partid}->{$responses->[$i]}= + $max_column; + $headers{'response'}->[$max_column]= + &mt('Response [_1]',$responses->[$i]); + for (my $j=0;$j<=$#headers;$j++) { + $headers{'student'}->[$max_column+$j]=$headers[$j]; + } + $max_column += scalar(@headers); + } + } + } + foreach my $row ('problem','part','response','student') { + print $outputfile '"'. + join('","', + map { + &Apache::loncommon::csv_translate($_); + } @{$headers{$row}}).'"'.$/; + } + # + # Main loop + foreach my $student (@$students) { + last if ($c->aborted()); + my $anonid = &Crypt::PasswdMD5::unix_md5_crypt($student->{'username'}, + $salt); + $anonid = substr($anonid,length($salt)+1); + my @rows; + foreach my $prob (@$problems) { + foreach my $partid (@{$prob->parts}) { + my @responses = $prob->responseIds($partid); + my @response_type = $prob->responseType($partid); + for (my $i=0;$i<=$#responses;$i++) { + my $respid = $responses[$i]; + my $resptype = $response_type[$i]; + my @headers = &get_headers($prob,$partid,$respid,$resptype, + $problem_analysis{$prob->src}, + 'csv','normal', + @extra_resp_headers); + my $results = + &Apache::loncoursedata::get_response_data_by_student + ($student,$prob->symb(),$respid); + if (! defined($results)) { + $results = []; + } + for (my $j=0; $j[$idx], + $prob,$student, + $partid,$respid); + my @data = &compile_response_data(\@headers,$response, + $prob,$partid, + $respid,$resptype, + \&csv_format_item); + my $resp_start_idx = + $start_col{$prob->symb}->{$partid}->{$respid}; + for (my $k=0;$k<=$#data;$k++) { + $rows[$j]->[$resp_start_idx + $k] = $data[$k]; + } + } + } + } + } + foreach my $row (@rows) { + my $student_row_data = ''; + if ($show_named) { + $student_row_data = '"'.join('","', + map { $student->{$_}; } + @student_columns).'"'; + } else { + $student_row_data = '"'.$anonid.'"'; + } + print $outputfile $student_row_data; + for (my $i=scalar(@student_columns);$i<$max_column;$i++) { + my $value = &Apache::loncommon::csv_translate($row->[$i]); + $value ||=''; + print $outputfile ',"'.$value.'"'; + } + print $outputfile $/; + } + undef(@rows); &Apache::lonhtmlcommon::Increment_PrgWin($r,\%prog_state, - &mt('last student')); - $student->{'answer'} = $answer; + 'last student'); } - $r->print("
\n"); - $r->rflush(); - # close progress window + close($outputfile); + # + # Close the progress window &Apache::lonhtmlcommon::Close_PrgWin($r,\%prog_state); + # + # Tell the user where to get their CSV file + $r->print('
'. + ''.&mt('Your CSV file.').''."\n"); + $r->rflush(); return; } +sub csv_format_item { + my ($item,$type) = @_; + if ($type eq 'Time') { + $item = localtime($item); + } + $item =&Apache::loncommon::csv_translate($item); + return $item; +} ######################################################### ######################################################### @@ -324,49 +1168,100 @@ sub GetStudentAnswers { ######################################################### sub CreateInterface { ## + ## Output Selection + my $output_selector = $/.''.$/; + ## ## Environment variable initialization my $Str = ''; - $Str .= &Apache::lonhtmlcommon::breadcrumbs - (undef,&mt('Student Submission Reports')); - $Str .= ''."\n"; - $Str .= ''; - $Str .= ''; - $Str .= ''; - $Str .= ''; - $Str .= ''."\n"; - ## - ## - $Str .= ''; + $Str .= ''; + $Str .= ''; + $Str .= ''; + $Str .= ''; + $Str .= &Apache::loncommon::end_data_table_header_row(); + # + $Str .= &Apache::loncommon::start_data_table_row(); + $Str .= ''; # + $Str .= ''; + # $Str .= ''; # - my $only_seq_with_assessments = sub { - my $s=shift; - if ($s->{'num_assess'} < 1) { - return 0; - } else { - return 1; - } - }; - ## - ## - $Str .= ''."\n"; - $Str .= '
'.&mt('Sections').''.&mt('Enrollment Status').' 
'."\n"; + $Str .= &Apache::lonhtmlcommon::breadcrumbs('Student Submission Reports'); + $Str .= '
'; + $Str .= &Apache::loncommon::start_data_table(); + $Str .= &Apache::loncommon::start_data_table_header_row(); + $Str .= '
'.&mt('Sections').''.&mt('Groups').''.&mt('Access Status').''.&mt('Options').''.&mt('Output Format').''."\n"; $Str .= &Apache::lonstatistics::SectionSelect('Section','multiple',5); $Str .= ''."\n"; + $Str .= &Apache::lonstatistics::GroupSelect('Group','multiple',5); + $Str .= ''; $Str .= &Apache::lonhtmlcommon::StatusOptions(undef,undef,5); $Str .= '
'."\n"; + # Render problem checkbox + my $prob_checkbox = ''. + &mt('Show problem').' '.$prob_checkbox.'
'. + '
'. + '
'. + '
'. + ''; + # + $Str .= ''.$output_selector.''; + # + $Str .= &Apache::loncommon::end_data_table_row(); + $Str .= &Apache::loncommon::end_data_table(); + # + $Str .= '

' + .&mt('Status: [_1]', + '') + .'

'; + ## return $Str; } - - 1; __END__ 500 Internal Server Error

Internal Server Error

The server encountered an internal error or misconfiguration and was unable to complete your request.

Please contact the server administrator at root@localhost to inform them of the time this error occurred, and the actions you performed just before this error.

More information about this error may be available in the server error log.