--- loncom/interface/statistics/lonstudentsubmissions.pm 2004/09/16 21:54:22 1.19 +++ loncom/interface/statistics/lonstudentsubmissions.pm 2012/10/12 14:08:42 1.69 @@ -1,6 +1,6 @@ # The LearningOnline Network with CAPA # -# $Id: lonstudentsubmissions.pm,v 1.19 2004/09/16 21:54:22 matthew Exp $ +# $Id: lonstudentsubmissions.pm,v 1.69 2012/10/12 14:08:42 bisitz Exp $ # # Copyright Michigan State University Board of Trustees # @@ -27,16 +27,21 @@ package Apache::lonstudentsubmissions; use strict; -use Apache::lonnet(); +use Apache::lonnet; use Apache::loncommon(); use Apache::lonhtmlcommon(); +use Apache::lonquickgrades(); use Apache::loncoursedata(); use Apache::lonstatistics; use Apache::lonlocal; use Apache::lonstathelpers; +use Apache::lonmsgdisplay(); use HTML::Entities(); use Time::Local(); use Spreadsheet::WriteExcel(); +use lib '/home/httpd/lib/perl/'; +use LONCAPA; + my @SubmitButtons = ({ name => 'SelectAnother', text => 'Choose a different Problem' }, @@ -58,21 +63,29 @@ sub BuildStudentSubmissionsPage { # &Apache::lonstatistics::PrepareClasslist(); # + $r->print( &Apache::lonhtmlcommon::breadcrumbs('Student Submission Reports')); + &Apache::lonquickgrades::startGradeScreen($r,'statistics'); $r->print(&CreateInterface()); # my @Students = @Apache::lonstatistics::Students; # if (@Students < 1) { - $r->print('<h2>There are no students in the sections selected</h2>'); + $r->print('<div class="LC_warning">' + .&mt('There are no students in the sections selected.') + .'</div>'); } # my @CacheButtonHTML = &Apache::lonstathelpers::manage_caches($r,'Statistics','stats_status', - '<h3>'.&mt('Loading student data').'</h3>'); + '<div class="LC_info">'.&mt('Loading student data...').'</div>'); $r->rflush(); # - if (exists($ENV{'form.problemchoice'}) && - ! exists($ENV{'form.SelectAnother'})) { + my %anoncounter = + &Apache::lonnet::dump('nohist_anonsurveys', + $env{'course.'.$env{'request.course.id'}.'.domain'}, + $env{'course.'.$env{'request.course.id'}.'.num'}); + if (exists($env{'form.problemchoice'}) && + ! exists($env{'form.SelectAnother'})) { foreach my $button (@SubmitButtons) { if ($button->{'name'} eq 'break') { $r->print("<br />\n"); @@ -92,39 +105,121 @@ sub BuildStudentSubmissionsPage { # Determine which problems we are to analyze my @Symbs = &Apache::lonstathelpers::get_selected_symbs('problemchoice'); - foreach my $selected (@Symbs) { - $r->print('<input type="hidden" name="problemchoice" value="'. - $selected.'" />'.$/); + + # If there are multi-part problems with anonymous survey and named + # parts check if named was picked for display. + # + my %mixed_named; + foreach my $envkey (%env) { + if ($envkey =~ /^form\.mixed_(\d+:\d+)$/) { + my $item = $1; + if ($env{$envkey} =~ /^symb_(.+)$/) { + my $symb = &unescape($1); + if (ref($mixed_named{$symb}) eq 'ARRAY') { + push(@{$mixed_named{$symb}},$item); + } else { + @{$mixed_named{$symb}} = ($item); + } + } + } } # # Get resource objects my $navmap = Apache::lonnavmaps::navmap->new(); if (!defined($navmap)) { - $r->print('<h1>'.&mt("Internal error").'</h1>'); + foreach my $selected (@Symbs) { + $r->print('<input type="hidden" name="problemchoice" value="'. + &escape($selected).'" />'.$/); + if (ref($mixed_named{$selected}) eq 'ARRAY') { + foreach my $item (@{$mixed_named{$selected}}) { + $r->print('<input type="hidden" name="mixed_'.$item.'" value="'.&escape($selected).'" />'.$/); + } + } + } + $r->print('<div class="LC_error">'.&mt("Internal error").'</div>'); return; } my %already_seen; - my @Problems; + my (@Problems,@anonProbs,@namedProbs,$show_named); foreach my $symb (@Symbs) { my $resource = $navmap->getBySymb($symb); - push(@Problems,$resource); + my ($hasanon,$hasnamed); + if (ref($resource)) { + foreach my $partid (@{$resource->parts}) { + if (($anoncounter{$symb."\0".$partid}) || ($resource->is_anonsurvey($partid))) { + unless (exists($mixed_named{$symb})) { + $hasanon = 1; + } + } else { + $hasnamed = 1; + } + } + if ($hasanon) { + push(@anonProbs,$resource); + } elsif ($hasnamed) { + push(@namedProbs,$resource); + } + } + } + if (@namedProbs > 0) { + @Problems = @namedProbs; + $show_named = 1; + } elsif (@anonProbs > 0) { + @Problems = @anonProbs; + } + foreach my $selected (@Symbs) { + $r->print('<input type="hidden" name="problemchoice" value="'. + &escape($selected).'" />'.$/); + if (ref($mixed_named{$selected}) eq 'ARRAY') { + foreach my $item (@{$mixed_named{$selected}}) { + $r->print('<input type="hidden" name="mixed_'.$item.'" value="'.&escape($selected).'" />'.$/); + } + } + } + # 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('<p>'. + &Apache::lonstatistics::section_and_enrollment_description(). + '</p>'); if (! scalar(@Problems) || ! defined($Problems[0])) { - $r->print('resource is undefined'); + $r->print(&mt('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.').'<br />'.&mt('There must be at least [quant,_1,student].',$threshold).' '.&mt('Contact a Domain Coordinator if you need the threshold to be changed for this course.')); } else { if (scalar(@Problems) == 1) { my $resource = $Problems[0]; $r->print('<h1>'.$resource->title.'</h1>'); $r->print('<h3>'.$resource->src.'</h3>'); - if ($ENV{'form.renderprob'} eq 'true') { - $r->print( - &Apache::lonstathelpers::render_resource({src => $resource->src}) - ); + if ($env{'form.renderprob'} eq 'true') { + $r->print(&Apache::lonstathelpers::render_resource($resource)); $r->rflush(); } } - &new_excel_output($r,\@Problems,\@Students); + if ($env{'form.output'} eq 'excel') { + &prepare_excel_output($r,\@Problems,\@Students,\%anoncounter,$show_named); + } elsif ($env{'form.output'} eq 'csv') { + &prepare_csv_output($r,\@Problems,\@Students,\%anoncounter,$show_named); + } else { + &prepare_html_output($r,\@Problems,\@Students,\%anoncounter,$show_named); + } } $r->print('<hr />'); } else { @@ -132,76 +227,612 @@ sub BuildStudentSubmissionsPage { &mt('Prepare Report').'" />'); $r->print(' 'x5); $r->print('<p>'. - &mt('Computing correct answers greatly increasese the amount of time required to prepare a report.'). + &mt('Computing correct answers greatly increases the amount of time required to prepare a report.'). '</p>'); $r->print('<p>'. - &mt('please select problems and use the <b>Prepare Report</b> button to continue.'). + &mt('Please select problems and use the [_1]Prepare Report[_2] button to continue.','<b>','</b>'). '</p>'); $r->print(&Apache::lonstathelpers::MultipleProblemSelector - (undef,'problemchoice','Statistics')); + (undef,'problemchoice','Statistics',\%anoncounter)); + } +} + +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; } ######################################################### ######################################################### +## +## HTML Output Routines +## +######################################################### +######################################################### +sub prepare_html_output { + my ($r,$problems,$students,$anoncounter,$show_named) = @_; + my $c = $r->connection(); + # + # 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; + } + # + # Compute the number of columns per response + my @extra_resp_headers = &get_extra_response_headers($show_named); + # + # Create the table header + my @student_columns = &get_student_columns($show_named); + my %headers; + my $student_column_count = scalar(@student_columns); + $headers{'problem'} = qq{<th colspan="$student_column_count">\ </th>}; + foreach my $field (@student_columns) { + $headers{'student'}.= '<th>'.&mt(ucfirst($field)).'</th>'; + } + # + # we put the headers into the %headers hash + my $total_col = $student_column_count; + 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 $symb = $prob->symb(); + # + my $prob_span = 0; + my $single_part = 0; + if (scalar(@{$prob->parts}) == 1) { + $single_part = 1; + } + my $shown_parts = 0; + foreach my $partid (@{$prob->parts}) { + if (($prob->is_anonsurvey($partid)) || ($anoncounter->{$symb."\0".$partid})) { + next if ($show_named); + } else { + next unless ($show_named); + } + $shown_parts ++; + my $part_span = 0; + my $responses = [$prob->responseIds($partid)]; + my $resptypes = [$prob->responseType($partid)]; + for (my $i=0;$i<scalar(@$responses);$i++) { + my $respid = $responses->[$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'} .= + '<th colspan="'.scalar(@headers).'">'. + &mt('Response [_1]',$responses->[$i]).'</th>'; + $headers{'student'}.= '<th><span class="LC_nobreak">'. + join('</span></th><th><span class="LC_nobreak">', + @headers). + '</span></th>'; + } + } + 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{<th colspan="$part_span">$tmpname</th>}; + $nonempty_part_headers = 1; + } else { + $headers{'part'} .= qq{<th colspan="$part_span"> </th>}; + } + $prob_span += $part_span; + } + next if (!$shown_parts); + my $title = $prob->compTitle; + if ($prob_span > 0) { + $headers{'problem'}.= qq{<th colspan="$prob_span">$title</th>}; + } elsif ($single_response) { + $prob_span = scalar(@student_columns); + $headers{'problem'} = qq{<th colspan="$prob_span">$title</th>}; + } + } + if (exists($headers{'part'})) { + $headers{'part'} = qq{<th colspan="$student_column_count">\ </th>}. + $headers{'part'}; + } + if (exists($headers{'response'})) { + $headers{'response'}= + qq{<th colspan="$student_column_count">\ </th>}. + $headers{'response'}; + } + #my $full_header = $/.'<table>'.$/; + my $full_header = $/.&Apache::loncommon::start_data_table().$/; + $full_header .= '<tr align="left">'.$headers{'problem'}.'</tr>'.$/; + if ($nonempty_part_headers) { + $full_header .= '<tr align="left">'.$headers{'part'}.'</tr>'.$/; + } + $full_header .= '<tr align="left">'.$headers{'response'}.'</tr>'.$/; + $full_header .= '<tr align="left">'.$headers{'student'}.'</tr>'.$/; + # + # Main loop + my $count; + $r->print($/.$full_header.$/); + foreach my $student (@$students) { + my $student_row_data; + if ($count++ >= 30) { + $r->print(&Apache::loncommon::end_data_table().$/.$full_header.$/); + $count = 0; + } + last if ($c->aborted()); + if ($show_named) { + foreach my $field (@student_columns) { + $student_row_data .= '<td valign="top">'; + # handle comments like in lonstudentassessment.pm + if($field eq 'comments') { + $student_row_data .= + '<a href="/adm/'.$student->{'domain'}.'/'. + $student->{'username'}.'/'.'aboutme#coursecomment">'.&mt('Comments').'</a>'; + } else { + $student_row_data .= $student->{$field}; + } + $student_row_data .= '</td>'; + } + } else { + $student_row_data = '<td valign="top" colspan="'.$student_column_count.'">'.&mt('Anonymized').'</td>'; + } + # + # Figure out what it is we need to output for this student + my @essays; + my %prob_data; + my $maxrow; + foreach my $prob (@$problems) { + my $symb = $prob->symb; + $prob_data{$symb}={}; + foreach my $partid (@{$prob->parts}) { + if (($prob->is_anonsurvey($partid)) || ($anoncounter->{$symb."\0".$partid})) { + next if ($show_named); + } else { + next unless ($show_named); + } + 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{<td colspan="$width"> </td>}; + 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). + '</td>'); + } 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. + 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(&Apache::loncommon::start_data_table_row().$student_row_data.$html. + &Apache::loncommon::end_data_table_row().$/); + $printed_something=1; + } + } + if (@essays) { + my $tr = &Apache::loncommon::start_data_table_row(); + my $td = qq{<td valign="top" class="essay" colspan="$total_col">}; + if (! $printed_something) { + $r->print($tr.$student_row_data.&Apache::loncommon::end_data_table_row().$/); + } + $r->print($tr.$td. + join('</td></tr>'.$/.$tr.$td,@essays).'</td></tr>'.$/); + undef(@essays); + } + } # end of student loop + $r->print(&Apache::loncommon::end_data_table().$/); + 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()]; + } -sub new_excel_output { - my ($r,$Problems,$Students) = @_; + 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 .= '<span class="LC_nobreak">'.$id.'</span>'.(' 'x4); + } + # + 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 .= '<span class="LC_nobreak">'. + join('', + map { + (' 'x4).&mt($_.': [_1]',$response->{$_}); + } @$headers).'</span>'; + if (@$headers || ! $single_response) { + $Str .= '<br />'; + } + $Str .= &html_format_essay_sub($response->{'Submission'}); + # + if (defined($correct) && $correct !~ /^\s*$/) { + $Str .= '<hr /><b>'.&mt('Correct').'</b>'.$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*$/|$/</p><p>$/|g; + $submission =~ s|\\||g; + $submission = '<p>'.$submission.'</p>'; + 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 = '<td valign="top">'; + my $str = $td.join('</td>'.$td,@values).'</td>'; + 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 = '<span class="LC_nobreak">'.$submission.'</span>'; + } + $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 { + foreach my $original_header (@$headers) { + if ($original_header eq 'Time') { + push(@values,&Apache::lonlocal::locallocaltime($response->{$original_header})); + } else { + # A normal column + push(@values,$response->{$original_header}); + } + } + } + my $td = '<td valign="top">'; + my $str = $td.join('</td>'.$td,@values).'</td>'; + return $str; +} + + +######################################################### +######################################################### +## +## Excel Output Routines +## +######################################################### +######################################################### +sub prepare_excel_output { + my ($r,$Problems,$Students,$anoncounter,$show_named) = @_; my $c = $r->connection(); # # # Determine the number of columns in the spreadsheet my $columncount = 3; # username, domain, id - my $multiplier = 1; - $multiplier ++ if ($ENV{'form.correctans'} eq 'true'); + my @extra_resp_headers = &get_extra_response_headers($show_named); + my $lastprob; + my %problem_analysis; foreach my $prob (@$Problems) { - $columncount += ($multiplier * $prob->countResponses); + my $symb = $prob->symb(); + my %analysis = &Apache::lonstathelpers::get_problem_data($prob->src); + $problem_analysis{$prob->src}=\%analysis; + foreach my $partid (@{$prob->parts}) { + if (($prob->is_anonsurvey($partid)) || ($anoncounter->{$symb."\0".$partid})) { + next if ($show_named); + } else { + next unless ($show_named); + } + + my $responses = [$prob->responseIds($partid)]; + my $resptypes = [$prob->responseType($partid)]; + for (my $i=0;$i<scalar(@$responses);$i++) { + my @headers = &get_headers($prob,$partid,$responses->[$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('<h1>'.&mt('Unable to complete request').'</h1>'.$/. - '<p>'.&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.').'</p>'.$/. - '<p>'.&mt('Consider selecting fewer problems to generate reports on, or reducing the number of items per problem. Or use HTML or CSV output.').'</p>'.$/); + $r->print('<p class="LC_error">'.&mt('Unable to complete request').'</p>'.$/. + '<p class="LC_warning">'.&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.').'</p>'.$/. + '<p class="LC_warning">'.&mt('Consider selecting fewer problems to generate reports on, or reducing the number of items per problem. Or use HTML or CSV output.').'</p>'.$/); + if (ref($lastprob)) { + $r->print('<p>'.&mt('The last problem that will fit in the current spreadsheet is [_1].',$lastprob->compTitle).'</p>'); + } $r->rflush(); return; } # # Print out a message telling them what we are doing if (scalar(@$Problems) > 1) { - $r->print('<h2>'. + $r->print('<p class="LC_info">'. &mt('Preparing Excel spreadsheet of student responses to [_1] problems', scalar(@$Problems)). - '</h2>'); + '</p>'); } else { - $r->print('<h2>'. + $r->print('<p class="LC_info">'. &mt('Preparing Excel spreadsheet of student responses'). - '</h2>'); + '</p>'); } $r->rflush(); # # Create the excel spreadsheet - 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('<p>'.&mt("Unable to create new Excel file. ". - "This error has been logged. ". - "Please alert your LON-CAPA administrator"). - '</p>'); - return undef; + my ($workbook,$filename,$format) = + &Apache::loncommon::create_workbook($r); + return if (! defined($workbook)); + + # Worksheet name + # The length of a worksheet name in Excel 95 is limited to 31 characters. + # Let's make sure to not exceed the maximum length in the translation files. + # Otherwise, no Excel file would be created. + my $worksheetname = &mt('Student Submission Data'); + if (length($worksheetname) > 31) { + # nice way to cut off too long name + $worksheetname = substr($worksheetname,0,28,).'...'; } - # - $workbook->set_tempdir('/home/httpd/perl/tmp'); - # - my $format = &Apache::loncommon::define_excel_formats($workbook); - my $worksheet = $workbook->addworksheet('Student Submission Data'); + my $worksheet = $workbook->addworksheet($worksheetname); # # Add headers to the worksheet my $rows_output = 0; $worksheet->write($rows_output++,0, - $ENV{'course.'.$ENV{'request.course.id'}.'.description'}, + $env{'course.'.$env{'request.course.id'}.'.description'}, $format->{'h1'}); $rows_output++; my $cols_output = 0; @@ -209,51 +840,68 @@ sub new_excel_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'}); + $worksheet->write($title_row ,0,&mt('Problem Title'),$format->{'bold'}); + $worksheet->write($partid_row,0,&mt('Part ID'),$format->{'bold'}); + $worksheet->write($respid_row,0,&mt('Response ID'),$format->{'bold'}); # Student headers - my @StudentColumns = ('username','domain','id'); - foreach (@StudentColumns) { - $worksheet->write($header_row,$cols_output++,ucfirst($_), + my @StudentColumns = &get_student_columns($show_named); + foreach my $field (@StudentColumns) { + $worksheet->write($header_row,$cols_output++,&mt(ucfirst($field)), $format->{'bold'}); } # Problem headers + my %start_col; foreach my $prob (@$Problems) { - my $title = &get_title($prob->title,$prob->src); + my $title = $prob->compTitle; + my $symb = $prob->symb(); $worksheet->write($title_row,$cols_output, $title,$format->{'h3'}); foreach my $partid (@{$prob->parts}) { - $worksheet->write($partid_row,$cols_output,$partid); + if (($prob->is_anonsurvey($partid)) || ($anoncounter->{$symb."\0".$partid})) { + next if ($show_named); + } else { + next unless ($show_named); + } + $worksheet->write($partid_row,$cols_output, + $prob->part_display($partid)); my $responses = [$prob->responseIds($partid)]; my $resptypes = [$prob->responseType($partid)]; for (my $i=0;$i<scalar(@$responses);$i++) { + $start_col{$prob->symb}->{$partid}->{$responses->[$i]}= + $cols_output; $worksheet->write($respid_row,$cols_output, $resptypes->[$i].', '.$responses->[$i]); - $worksheet->write($header_row,$cols_output,'Submission'); - $cols_output++; - if ($ENV{'form.correctans'} eq 'true') { - $worksheet->write($header_row,$cols_output,'Correct'); - $cols_output++; + 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 %prog_state=&Apache::lonhtmlcommon::Create_PrgWin($r,scalar(@$Students)); + my $max_row = $rows_output; foreach my $student (@$Students) { last if ($c->aborted()); $cols_output = 0; - foreach my $field (@StudentColumns) { - $worksheet->write($rows_output,$cols_output++, - $student->{$field}); - } + my $student_row = $max_row; foreach my $prob (@$Problems) { + my $symb = $prob->symb(); foreach my $partid (@{$prob->parts}) { + if (($prob->is_anonsurvey($partid)) || ($anoncounter->{$symb."\0".$partid})) { + next if ($show_named); + } else { + next unless ($show_named); + } my @Response = $prob->responseIds($partid); my @ResponseType = $prob->responseType($partid); for (my $i=0;$i<=$#Response;$i++) { @@ -262,29 +910,62 @@ sub new_excel_output { my $results = &Apache::loncoursedata::get_response_data_by_student ($student,$prob->symb(),$respid); - my $final_response = $results->[-1]; - my $submission = - $final_response->[ - &Apache::loncoursedata::RDs_submission() - ]; - $submission=&excel_format_response($submission,$resptype); - $worksheet->write($rows_output,$cols_output++, - $submission); - if ($ENV{'form.correctans'} eq 'true') { - my $correct = - &Apache::lonstathelpers::analyze_problem_as_student - ($prob,$student->{'username'},$student->{'domain'}, - $partid,$respid); - $correct =&excel_format_response($correct,$resptype); - $worksheet->write($rows_output,$cols_output++, - $correct); + 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; + } + } + } + } + } + # 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) { + my $value = $student->{$field}; + if ($field eq 'comments') { + $value = &Apache::lonmsgdisplay::retrieve_instructor_comments + ($student->{'username'},$student->{'domain'}); + } + $worksheet->write($row,$cols++,$value); + } else { + $worksheet->write($row,$cols++, + &mt('Anonymized')); } } } - $rows_output++; - &Apache::lonhtmlcommon::Increment_PrgWin($r,\%prog_state, - 'last student'); + &Apache::lonhtmlcommon::Increment_PrgWin($r,\%prog_state,'last student'); } &Apache::lonhtmlcommon::Close_PrgWin($r,\%prog_state); # @@ -303,331 +984,83 @@ sub new_excel_output { return; } -sub get_title { - my ($title,$src) = @_; - if ($title eq '') { - ($title) = ($src =~ m|/([^/]+)$|); - } else { - $title =~ s/\:/:/g; - } - return $title; -} - -sub excel_format_response { - my ($answer,$responsetype) = @_; - if ($responsetype eq 'radiobutton') { - $answer =~ s/=([^=])$//; - } elsif ($responsetype =~ /^(option|match)$/) { - $answer = join("\n", - map { - &Apache::lonnet::unescape($_) ; - } sort split('&',$answer) - ); - } - if ($answer =~ m/^=/) { - $answer = ' '.$answer; - } - return $answer; -} - -## -## Currently not used -sub get_problem_data { - my ($r,$Problems) = @_; - # - # Analyze - my %Data; - if (scalar(@$Problems) > 5) { - # progress window - my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin - ($r,'Problem Analysis Status', - 'Problem Analysis Progress', - scalar(@$Problems), - 'inline',undef,'Statistics','stats_status'); - foreach my $problem (@$Problems) { - $Data{$problem->symb} = - {&Apache::lonstathelpers::get_problem_data - ($problem->src)}; - &Apache::lonhtmlcommon::Increment_PrgWin($r,\%prog_state, - 'last problem'); - } - &Apache::lonhtmlcommon::Close_PrgWin($r,\%prog_state); - } else { - foreach my $problem (@$Problems) { - $Data{$problem->symb} = - {&Apache::lonstathelpers::get_problem_data - ($problem->src)}; - } - } - return \%Data; -} - - -=pod - -######################################################### -######################################################### - -my @DefaultColumns = - ( - {name=>'username', - display=>'Student'}, - {name=>'domain', - display=>'Domain'}, - {name => 'id', - display => 'Id'}, - # FIXME: Probably need to add score - ); - -my @PartColumns = - ( - {name => 'time', - display =>'Time'}, - {name => 'attempt', - display =>'Attempt'}, - {name => 'awarded', - display =>'Award'}, - {name => 'weight', - display =>'Part Weight'}, - {name => 'score', - display =>'Score'}, - ); - -my @ResponseColumns = - ( - {name => 'submission', - display =>'Submission'}, - {name => 'answer', - display =>'Correct Answer'}, - {name => 'awarddetail', - display =>'Awarddetail'}, - ); - -sub get_response_type { - my ($resource,$partid,$respid) = @_; - my $response_type = ''; - for (my $i=0; - $i<scalar(@{$resource->{'partdata'}->{$partid}->{'ResponseIds'}}); - $i++) { - if($resource->{'partdata'}->{$partid}->{'ResponseIds'}->[$i] eq $respid){ - $response_type = - $resource->{'partdata'}->{$partid}->{'ResponseTypes'}->[$i]; - last; - } - } - return $response_type; -} - - -######################################################### -######################################################### -## -## prepare_html_output -## -######################################################### -######################################################### -sub prepare_html_output { - my ($r,$Problems,$Students) = @_; - my $problem; - my $c = $r->connection(); - my ($resource,$respid,$partid) = ($problem->{'resource'}, - $problem->{'respid'}, - $problem->{'part'}); - # - if ($ENV{'form.correctans'} eq 'true') { - $r->print('<h2>'.&mt('Generating Correct Answers').'</h2>'); - &Apache::lonstathelpers::GetStudentAnswers($r,$problem,$Students, - 'Statistics', - 'stats_status'); - } - # - $r->print('<h2>'.&mt('Student Responses').'</h2>'); - # - $r->rflush(); - my $response_type = &get_response_type($resource,$partid,$respid); - if (! defined($response_type)) { - $r->print('<h2>'.&mt('Unable to determine response type').'</h2>'); - return; - } - my $count = 0; - my @Columns; - if (exists($ENV{'form.concise'}) && $ENV{'form.concise'} eq 'true') { - foreach (@DefaultColumns) { - if ($_->{'name'} =~ /^(username|domain|id)$/){ - push(@Columns,$_); - } - } - } else { - @Columns = @DefaultColumns; - } - my $header = '<table>'.$/.&html_headers(\@Columns); - if ($response_type eq 'essay') { - $header .= &html_essay_headers(); - } elsif ($response_type eq 'option') { - $header .= &html_option_headers(); - } else { - $header .= &html_generic_headers(); - } - $header = '<tr>'.$header.'</tr>'; - # - $r->print($/.$header.$/); - foreach my $student (@$Students) { - if ($count >= 50) { - $r->print('</table>'.$/.$header.$/); - $count = 0; +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'}); } - last if ($c->aborted()); - my $results = &Apache::loncoursedata::get_response_data_by_student - ($student,$resource->{'symb'},$respid); - next if (! defined($results) || ref($results) ne 'ARRAY'); - for (my $i=0;$i<scalar(@$results);$i++) { - my $response = $results->[$i]; - if ($ENV{'form.last_sub_only'} eq 'true' && - $i < (scalar(@$results)-1)) { - next; - } - my $data; - $data->{'username'} = $student->{'username'}; - $data->{'domain'} = $student->{'domain'}; - $data->{'id'} = $student->{'id'}; - $data->{'fullname'} = $student->{'fullanem'}; - $data->{'status'} = $student->{'status'}; - $data->{'time'} = &Apache::lonlocal::locallocaltime - ($response->[&Apache::loncoursedata::RDs_timestamp()]); - $data->{'attempt'} = - $response->[&Apache::loncoursedata::RDs_tries()]; - $data->{'awarded'} = - $response->[&Apache::loncoursedata::RDs_awarded()]; - $data->{'awarddetail'} = - $response->[&Apache::loncoursedata::RDs_awarddetail()]; - $data->{'weight'} = &Apache::lonnet::EXT - ('resource.'.$partid.'.weight',$resource->{'symb'}, - undef,undef,undef); - $data->{'score'} = $data->{'weight'} * $data->{'awarded'}; - my $rowextra = 'bgcolor="#CCCCCC"'; - if ($count % 2 == 1) { - $rowextra = 'bgcolor="#EEEEEE"'; - } - my $row = '<tr '.$rowextra.'>'; - foreach my $col (@Columns) { - $row .= '<td valign="top">'. - $data->{$col->{'name'}}.'</td>'; - } - if ($response_type eq 'essay') { - $row .= &html_essay_results - ($response->[&Apache::loncoursedata::RDs_submission()], - $student->{'answer'}, - scalar(@Columns),$rowextra); - } elsif ($response_type eq 'option') { - $row .= &html_option_results - ($response->[&Apache::loncoursedata::RDs_submission()], - $student->{'answer'}, - scalar(@Columns),$rowextra); + # + 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 { - $row .= &html_generic_results - ($response->[&Apache::loncoursedata::RDs_submission()], - $student->{'answer'}, - scalar(@Columns),$rowextra); + # A normal column + push(@values,&{$format}($response->{$original_header}, + $original_header)); } - $row .= '</tr>'; - $r->print($row.$/); - $count++; } + } else { + @values = map { &{$format}($response->{$_},$_); } @$headers; } - $r->print('</table>'.$/); - return; -} - -##################################################### -## -## HTML helper routines -## -##################################################### -sub html_headers { - my ($Columns) = @_; - my $Str; - foreach my $column (@$Columns) { - $Str .= '<th align="left">'.$column->{'display'}.'</th>'; - } - return $Str; + return @values; } -sub html_essay { - my ($submission,$correct,$tablewidth,$rowextra)=@_; - # - $submission =~ s|\\r\\n|$/|g; - $submission = &HTML::Entities::encode($submission,'<>&"'); - $submission =~ s|$/\s*$/|$/</p><p>$/|g; - $submission =~ s|\\||g; - $submission = '<p>'.$submission.'</p>'; - # - my $Str = '</tr><tr '.$rowextra.'>'. - '<td colspan="'.$tablewidth.'">'.$submission.'</td>'; - if ($ENV{'form.correctans'} eq 'true') { - $Str .= '</tr>'; - if (defined($correct) && $correct !~ /^\s*$/) { - $Str .= '<tr '.$rowextra.'><td colspan="'.$tablewidth.'">'. - '<b>'.&mt('Correct Answer:').'</b>'.$correct.'</td>'; +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; } - $Str .= '</tr>'; - # - return $Str; -} - -sub html_essay_headers { - return ''; -} - -sub html_generic_headers { - my $header ='<th>'.&mt('Submission').'</th>'; - if ($ENV{'form.correctans'} eq 'true') { - $header .= '<th>'.&mt('Correct').'</th>'; - } - return $header; -} - -sub html_option_headers { - return &html_generic_headers(); -} - -sub html_radiobutton_results { - my ($submission,$correct,$tablewidth,$rowclass)=@_; - $submission =~ s/=([^=])$//; - return &html_generic_results($submission,$correct,$tablewidth,$rowclass); -} - -sub html_generic_results { - my ($submission,$correct,$tablewidth,$rowclass)=@_; - my $Str .= '<td>'.$submission.'</td>'; - if ($ENV{'form.correctans'} eq 'true') { - $Str .= '<td>'.$correct.'</td>'; - } - $Str .= '</tr>'; - return $Str; -} - -sub html_option_results { - my ($submission,$correct,$tablewidth,$rowclass)=@_; - $submission = - '<ul class="sub_studentans">'. - '<li>'.join('</li><li>', - map { - &Apache::lonnet::unescape($_) ; - } sort split('&',$submission) - ). - '</li><ul>'; - if (defined($correct) && $correct !~ /^\s*$/) { - $correct = '<ul class="sub_correctans">'. - '<li>'.join('</li><li>', - map { - &Apache::lonnet::unescape($_) ; - } sort split('&',$correct) - ). - '</li></ul>'; - } - # - return &html_generic_results($submission,$correct,$tablewidth,$rowclass); + return $item; } ######################################################### @@ -638,407 +1071,186 @@ sub html_option_results { ######################################################### ######################################################### sub prepare_csv_output { - my ($r,$Problems,$Students) = @_; - my $problem; - # + my ($r,$problems,$students,$anoncounter,$show_named) = @_; my $c = $r->connection(); - my ($resource,$respid,$partid) = ($problem->{'resource'}, - $problem->{'respid'}, - $problem->{'part'}); - # - if ($ENV{'form.correctans'} eq 'true') { - $r->print('<h2>'.&mt('Generating Correct Answers').'</h2>'); - &Apache::lonstathelpers::GetStudentAnswers($r,$problem,$Students, - 'Statistics', - 'stats_status'); - } # - $r->print('<h2>'. - &mt('Generating CSV report of student responses').'</h2>'); + $r->print('<p class="LC_info">'. + &mt('Generating CSV report of student responses').'</p>'); # # Progress window - my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin - ($r,'CSV File Compilation Status', - 'CSV File Compilation Progress', - scalar(@$Students),'inline',undef,'Statistics','stats_status'); - + my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin($r,scalar(@$students)); + $r->rflush(); # # Open a file my $outputfile; my $filename = '/prtspool/'. - $ENV{'user.name'}.'_'.$ENV{'user.domain'}.'_'. + $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("Problems occured in writing the csv file. ". - "This error has been logged. ". - "Please alert your LON-CAPA administrator."); + $r->print( + '<p class="LC_error">' + .&mt('Problems occurred in writing the CSV file.') + .' '.&mt('This error has been logged.') + .' '.&mt('Please alert your LON-CAPA administrator.') + .'</p>' + ); $outputfile = undef; } # + # Compute the number of columns per response + my @extra_resp_headers = &get_extra_response_headers($show_named); # - my @Columns; - if (exists($ENV{'form.concise'}) && $ENV{'form.concise'} eq 'true') { - foreach (@DefaultColumns) { - if ($_->{'name'} =~ /^(username|domain|id)$/){ - push(@Columns,$_); + # Create the table header + my @student_columns = &get_student_columns($show_named); + my $student_column_count = scalar(@student_columns); + # + 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 = $student_column_count; + foreach my $prob (@$problems) { + my $symb = $prob->symb(); + 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}) { + if (($prob->is_anonsurvey($partid)) || ($anoncounter->{$symb."\0".$partid})) { + next if ($show_named); + } else { + next unless ($show_named); + } + $headers{'part'}->[$max_column] = $prob->part_display($partid); + my $responses = [$prob->responseIds($partid)]; + my $resptypes = [$prob->responseType($partid)]; + for (my $i=0;$i<scalar(@$responses);$i++) { + my @headers = &get_headers($prob,$partid,$responses->[$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); } } - } else { - @Columns = @DefaultColumns; } - # - my $response_type = &get_response_type($resource,$partid,$respid); - if (! defined($response_type) || $response_type eq '') { - $r->print('<h2>'.&mt('Unable to determine response type').'</h2>'); - return; + foreach my $row ('problem','part','response','student') { + print $outputfile '"'. + join('","', + map { + &Apache::loncommon::csv_translate($_); + } @{$headers{$row}}).'"'.$/; } # - my $header = &csv_headers(\@Columns).','.&csv_generic_headers(); - print $outputfile $header.$/; - # - foreach my $student (@$Students) { + # Main loop + foreach my $student (@$students) { last if ($c->aborted()); - my $results = &Apache::loncoursedata::get_response_data_by_student - ($student,$resource->{'symb'},$respid); - next if (! defined($results) || ref($results) ne 'ARRAY'); - for (my $i=0;$i<scalar(@$results);$i++) { - my $response = $results->[$i]; - if ($ENV{'form.last_sub_only'} eq 'true' && - $i < (scalar(@$results)-1)) { - next; + my @rows; + foreach my $prob (@$problems) { + my $symb = $prob->symb; + foreach my $partid (@{$prob->parts}) { + if (($prob->is_anonsurvey($partid)) || ($anoncounter->{$symb."\0".$partid})) { + next if ($show_named); + } else { + next unless ($show_named); + } + 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<scalar(@$results);$j++) { + if ($env{'form.all_sub'} ne 'true') { + next if ($j != 0); + } + my $idx = scalar(@$results) - $j - 1; + my $response = &hashify_response($results->[$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]; + } + } + } } - my $data; - $data->{'username'} = $student->{'username'}; - $data->{'domain'} = $student->{'domain'}; - $data->{'id'} = $student->{'id'}; - $data->{'fullname'} = $student->{'fullanem'}; - $data->{'status'} = $student->{'status'}; - $data->{'time'} = &Apache::lonlocal::locallocaltime - ($response->[&Apache::loncoursedata::RDs_timestamp()]); - $data->{'attempt'} = - $response->[&Apache::loncoursedata::RDs_tries()]; - $data->{'awarded'} = - $response->[&Apache::loncoursedata::RDs_awarded()]; - $data->{'awarddetail'} = - $response->[&Apache::loncoursedata::RDs_awarddetail()]; - $data->{'weight'} = &Apache::lonnet::EXT - ('resource.'.$partid.'.weight',$resource->{'symb'}, - undef,undef,undef); - $data->{'score'} = $data->{'weight'} * $data->{'awarded'}; - my $rowextra = ''; - my $row; - foreach my $col (@Columns) { - $row .= '"'. - &Apache::loncommon::csv_translate($data->{$col->{'name'}}).'",'; - } - if ($response_type eq 'option') { - $row .= &csv_option_results - ($response->[&Apache::loncoursedata::RDs_submission()], - $student->{'answer'}, - scalar(@Columns),$rowextra); - } elsif ($response_type eq 'radiobutton') { - $row .= &csv_radiobutton_results - ($response->[&Apache::loncoursedata::RDs_submission()], - $student->{'answer'}, - scalar(@Columns),$rowextra); + } + foreach my $row (@rows) { + my $student_row_data = ''; + if ($show_named) { + foreach my $field (@student_columns) { + my $value = $student->{$field}; + if ($field eq 'comments') { + $value = &Apache::lonmsgdisplay::retrieve_instructor_comments + ($student->{'username'},$student->{'domain'}); + } + $student_row_data .= '"'.&Apache::loncommon::csv_translate($value).'",'; + } + $student_row_data =~ s/,$//; } else { - $row .= &csv_generic_results - ($response->[&Apache::loncoursedata::RDs_submission()], - $student->{'answer'}, - scalar(@Columns),$rowextra); + $student_row_data = '"'.&mt('Anonymized').'"'; } - print $outputfile $row.$/; + print $outputfile $student_row_data; + for (my $i=$student_column_count;$i<$max_column;$i++) { + my $value = &Apache::loncommon::csv_translate($row->[$i]); + $value ||=''; + print $outputfile ',"'.$value.'"'; + } + print $outputfile $/; } - &Apache::lonhtmlcommon::Increment_PrgWin($r,\%prog_state, - 'last student'); + undef(@rows); + &Apache::lonhtmlcommon::Increment_PrgWin($r,\%prog_state,'last student'); } close($outputfile); # # Close the progress window &Apache::lonhtmlcommon::Close_PrgWin($r,\%prog_state); # - # Tell the user where to get their csv file + # Tell the user where to get their CSV file $r->print('<br />'. - '<a href="'.$filename.'">'.&mt('Your csv file.').'</a>'."\n"); + '<a href="'.$filename.'">'.&mt('Your CSV file.').'</a>'."\n"); $r->rflush(); return; } -sub csv_headers { - my ($Columns) = @_; - my $Str; - foreach my $column (@$Columns) { - $Str .= - '"'.&Apache::loncommon::csv_translate($column->{'display'}).'",'; - } - chop($Str); - return $Str; -} - -sub csv_generic_headers { - my ($title) = @_; - if (! defined($title)) { - $title = &mt('Submission'); - } - my $header = '"'.&Apache::loncommon::csv_translate($title).'"'; - if ($ENV{'form.correctans'} eq 'true') { - $header .= ',"'.&Apache::loncommon::csv_translate(&mt('Correct')).'"'; - } - return $header; -} - -#------------------------------------------ -sub csv_essay_results { - my ($submission,$correct,$tablewidth,$rowextra)=@_; - # - $submission =~ s|\\r|\\\\r|g; - $submission =~ s|\\n|\\\\n|g; - # - return &csv_generic_results($submission,$correct,$tablewidth); -} - -#------------------------------------------ -sub csv_radiobutton_results { - my ($submission,$correct,$tablewidth,$rowclass)=@_; - $submission =~ s/=[^=]*$//; - return &csv_generic_results($submission,$correct,$tablewidth,$rowclass); -} - -#------------------------------------------ -sub csv_option_results { - my ($submission,$correct,$tablewidth,$rowclass)=@_; - $submission = join(',', - map { - &Apache::lonnet::unescape($_) ; - } sort split('&',$submission) - ); - if (defined($correct) && $correct !~ /^\s*$/) { - $correct =join(',', - map { - &Apache::lonnet::unescape($_) ; - } sort split('&',$submission)); - } - return &csv_generic_results($submission,$correct,$tablewidth,$rowclass); -} - -#------------------------------------------ -sub csv_generic_results { - my ($submission,$correct,$tablewidth,$rowclass)=@_; - my $Str .= - '"'.&Apache::loncommon::csv_translate($submission).'"'; - if ($ENV{'form.correctans'} eq 'true') { - $Str .= ',"'.&Apache::loncommon::csv_translate($correct).'"'; - } - return $Str; -} - -######################################################### -######################################################### -## -## Excel output of student answers and correct answers -## -######################################################### -######################################################### -sub prepare_excel_output { - my ($r,$Problems,$Students) = @_; - my $c = $r->connection(); - # - $r->print('<h2>'. - &mt('Preparing Excel spreadsheet of student responses'). - '</h2>'); - if ($ENV{'form.correctans'} eq 'true') { - $r->print('<p>'. - &mt('See the status bar above for student answer computation progress'). - '</p>'); - foreach my $problem (@$Problems) { - my ($resource,$respid,$partid) = ($problem->{'resource'}, - $problem->{'respid'}, - $problem->{'part'}); - # - &Apache::lonstathelpers::GetStudentAnswers($r,$problem,$Students, - 'Statistics', - 'stats_status'); - } - $r->print('<script>'. - 'window.document.Statistics.stats_status.value="'. - 'Done computing student answers. Compiling spreadsheet.'. - '";</script>'); - } - # - $r->rflush(); - - - - - - - - - - - my @ColumnLabels; - my @Columns = @DefaultColumns; - my %seen; - foreach my $problem (@$Problems) { - my ($resource,$respid,$partid) = ($problem->{'resource'}, - $problem->{'respid'}, - $problem->{'part'}); - my $partkey = $resource->{'symb'}.':'.$partid; - if (! $seen{$partkey}) { - $ColumnLabels[$#Columns]=$resource->{'title'}; - push(@ColumnLabels,$partid); - push(@Columns,@PartColumns); - } - push(@Column - if ($ENV{'form.correctans'} eq 'true') { - push(@Columns,'Computers answer'); - } - $seen{$resource->{'symb'}.':'.$partid} .= ':'.$respid; - +sub csv_format_item { + my ($item,$type) = @_; + if ($type eq 'Time') { + $item = &Apache::lonlocal::locallocaltime($item); } - -# my ($awarded_col,$weight_col); -# for (my $i=0;$i<=$#Columns;$i++) { -# if ($Columns[$i]->{'name'} eq 'weight' ) { $weight_col = $i; } -# if ($Columns[$i]->{'name'} eq 'awarded') { $awarded_col = $i; } -# } - - # - # 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('<p>'.&mt("Unable to create new Excel file. ". - "This error has been logged. ". - "Please alert your LON-CAPA administrator"). - '</p>'); - return undef; - } - # - $workbook->set_tempdir('/home/httpd/perl/tmp'); - # - my $format = &Apache::loncommon::define_excel_formats($workbook); - my $worksheet = $workbook->addworksheet('Student Submission Data'); - # - # Make sure we get new weight data instead of data on a 10 minute delay - &Apache::lonnet::clear_EXT_cache_status(); - # - # 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'}); - $rows_output++; - $worksheet->write_row($rows_output++,0, - [map {$_->{'display'}} @Columns], - $format->{'bold'}); - # - # 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'); - foreach my $student (@$Students) { - last if ($c->aborted()); - my $results = &Apache::loncoursedata::get_response_data_by_student - ($student,$resource->{'symb'},$respid); - my %row; - $row{'username'} = $student->{'username'}; - $row{'domain'} = $student->{'domain'}; - $row{'id'} = $student->{'id'}; - $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->{'name'}})) { - $cols_output++; - next; - } - $worksheet->write($rows_output,$cols_output++, - $row{$col->{'name'}}); - } - $rows_output++; - } else { - for (my $i=0;$i<scalar(@$results);$i++) { - my $response = $results->[$i]; - if ($ENV{'form.last_sub_only'} eq 'true' && - $i < (scalar(@$results)-1)) { - next; - } - 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{'awarddetail'} = $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->{'name'}}, - $row_format{$col->{'name'}}); - } - $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(); - # - # Write a link to allow them to download it - $r->print('<p><a href="'.$filename.'">'. - &mt('Your Excel spreadsheet.'). - '</a></p>'."\n"); - $r->print('<script>'. - 'window.document.Statistics.stats_status.value="'. - 'Done compiling spreadsheet. See link below to download.'. - '";</script>'); - $r->rflush(); - return; + $item =&Apache::loncommon::csv_translate($item); + return $item; } -=cut - ######################################################### ######################################################### ## @@ -1052,8 +1264,8 @@ sub CreateInterface { my $output_selector = $/.'<select name="output">'.$/; foreach ('HTML','Excel','CSV') { $output_selector .= ' <option value="'.lc($_).'"'; - if ($ENV{'form.output'} eq lc($_)) { - $output_selector .= ' selected '; + if ($env{'form.output'} eq lc($_)) { + $output_selector .= ' selected="selected"'; } $output_selector .='>'.&mt($_).'</option>'.$/; } @@ -1061,76 +1273,102 @@ sub CreateInterface { ## ## Environment variable initialization my $Str = ''; - $Str .= &Apache::lonhtmlcommon::breadcrumbs - (undef,'Student Submission Reports'); - $Str .= '<p>'; - $Str .= '<table cellspacing="5">'."\n"; - $Str .= '<tr>'; + $Str .= '<br />'; + $Str .= &Apache::loncommon::start_data_table(); + $Str .= &Apache::loncommon::start_data_table_header_row(); $Str .= '<th>'.&mt('Sections').'</th>'; - $Str .= '<th>'.&mt('Enrollment Status').'</th>'; - $Str .= '<th>'.&mt('Output Options').'</th>'; - $Str .= '</tr>'."\n"; + $Str .= '<th>'.&mt('Groups').'</th>'; + $Str .= '<th>'.&mt('Student Data').&Apache::loncommon::help_open_topic("Chart_Student_Data").'</th>'; + $Str .= '<th>'.&mt('Access Status').'</th>'; + $Str .= '<th>'.&mt('Options').'</th>'; + $Str .= '<th>'.&mt('Output Format').'</th>'; + $Str .= &Apache::loncommon::end_data_table_header_row(); # - $Str .= '<tr><td align="center">'."\n"; + $Str .= &Apache::loncommon::start_data_table_row(); + $Str .= '<td align="center">'."\n"; $Str .= &Apache::lonstatistics::SectionSelect('Section','multiple',5); $Str .= '</td>'; # + $Str .= '<td align="center">'."\n"; + $Str .= &Apache::lonstatistics::GroupSelect('Group','multiple',5); + $Str .= '</td>'; + # + $Str .= '<td align="center">'."\n"; + $Str .= &Apache::lonstatistics::StudentDataSelect('StudentData','multiple', 5,undef); + $Str .= '</td>'; + # $Str .= '<td align="center">'; $Str .= &Apache::lonhtmlcommon::StatusOptions(undef,undef,5); $Str .= '</td>'; # # Render problem checkbox my $prob_checkbox = '<input type="checkbox" name="renderprob" '; - if (exists($ENV{'form.renderprob'}) && $ENV{'form.renderprob'} eq 'true') { - $prob_checkbox .= 'checked '; + if (exists($env{'form.renderprob'}) && $env{'form.renderprob'} eq 'true') { + $prob_checkbox .= 'checked="checked" '; } $prob_checkbox .= 'value="true" />'; # # Compute correct answers checkbox my $ans_checkbox = '<input type="checkbox" name="correctans" '; - if (exists($ENV{'form.correctans'}) && $ENV{'form.correctans'} eq 'true') { - $ans_checkbox .= 'checked '; + if (exists($env{'form.correctans'}) && $env{'form.correctans'} eq 'true') { + $ans_checkbox .= 'checked="checked" '; } $ans_checkbox .= 'value="true" />'; # # Show all submissions checkbox my $all_sub_checkbox = '<input type="checkbox" name="all_sub" '; - if (exists($ENV{'form.all_sub'}) && - $ENV{'form.all_sub'} eq 'true') { - $all_sub_checkbox .= 'checked '; + if (exists($env{'form.all_sub'}) && + $env{'form.all_sub'} eq 'true') { + $all_sub_checkbox .= 'checked="checked" '; } $all_sub_checkbox.= 'value="true" />'; # - # Concise view checkbox - my $concise_view_checkbox = '<input type="checkbox" name="concise" '; - if (exists($ENV{'form.concise'}) && $ENV{'form.concise'} eq 'true') { - $concise_view_checkbox .= 'checked '; - } - $concise_view_checkbox .= 'value="true" />'; - # - $Str .= '<td align="right" halign="top">'. -# '<b>'.&mt('Output Format: [_1]',$output_selector).'</b><br />'.$/. - '<label><b>'. - &mt('show problem [_1]',$prob_checkbox).'</b></label><br />'. - '<label><b>'. - &mt('compute correct answers [_1]',$ans_checkbox).'</b></label><br />'. -# '<label><b>'. -# &mt('All submission [_1]',$all_sub_checkbox).'</b></label><br />'. -# '<label><b>'. -# &mt('concise view [_1]',$concise_view_checkbox).'</b></label><br />'. - '</td>'; - # - $Str .= '</tr>'."\n"; - $Str .= '</table>'."\n"; - # - $Str .= '<nobr>'.&mt('Status: [_1]', - '<input type="text" '. - 'name="stats_status" size="60" value="" />'). - '</nobr>'.'</p>'; - ## + # problem status checkbox + my $prob_status_checkbox = '<input type="checkbox" name="prob_status" '; + if (exists($env{'form.prob_status'}) && + $env{'form.prob_status'} eq 'true') { + $prob_status_checkbox .= 'checked="checked" '; + } + $prob_status_checkbox .= 'value="true" />'; + # + $Str .= + '<td valign="top">' + .'<label>' + .$prob_checkbox.&mt('Show problem') + .'</label><br />' + .'<label>' + .' '.$ans_checkbox.&mt('Show correct answers') + .'</label><br />' + .'<label>' + .$all_sub_checkbox.&mt('Show all submissions') + .'</label><br />' + .'<label>' + .$prob_status_checkbox.&mt('Show problem grading') + .'</label>' + .'</td>'; + # + $Str .= '<td align="center" valign="top">'.$output_selector.'</td>'; + # + $Str .= &Apache::loncommon::end_data_table_row(); + $Str .= &Apache::loncommon::end_data_table(); + # return $Str; } +sub get_student_columns { + my ($show_named) = @_; + 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'); + } + return @student_columns; +} + 1; __END__