--- loncom/homework/grades.pm 2012/12/18 18:03:40 1.682 +++ loncom/homework/grades.pm 2015/02/10 04:02:17 1.731 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # The LON-CAPA Grading handler # -# $Id: grades.pm,v 1.682 2012/12/18 18:03:40 raeburn Exp $ +# $Id: grades.pm,v 1.731 2015/02/10 04:02:17 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -330,6 +330,8 @@ sub cleanRecord { my $grayFont = ''; if ($response =~ /^(option|rank)$/) { my %answer=&Apache::lonnet::str2hash($answer); + my @answer = %answer; + %answer = map {&HTML::Entities::encode($_, '"<>&')} @answer; my %grading=&Apache::lonnet::str2hash($record->{$version."resource.$partid.$respid.submissiongrading"}); my ($toprow,$bottomrow); foreach my $foil (@$order) { @@ -346,6 +348,8 @@ sub cleanRecord { $bottomrow.''; } elsif ($response eq 'match') { my %answer=&Apache::lonnet::str2hash($answer); + my @answer = %answer; + %answer = map {&HTML::Entities::encode($_, '"<>&')} @answer; my %grading=&Apache::lonnet::str2hash($record->{$version."resource.$partid.$respid.submissiongrading"}); my @items=&Apache::lonnet::str2array($record->{$version."resource.$partid.$respid.submissionitems"}); my ($toprow,$middlerow,$bottomrow); @@ -368,6 +372,8 @@ sub cleanRecord { $bottomrow.''; } elsif ($response eq 'radiobutton') { my %answer=&Apache::lonnet::str2hash($answer); + my @answer = %answer; + %answer = map {&HTML::Entities::encode($_, '"<>&')} @answer; my ($toprow,$bottomrow); my $correct = &get_radiobutton_correct_foil($partid,$respid,$symb,$uname,$udom,$type,$trial,$rndseed); @@ -400,10 +406,11 @@ sub cleanRecord { $env{'form.kwstyle'} = $keyhash{$loginuser.'_kwstyle'} ne '' ? $keyhash{$loginuser.'_kwstyle'} : ''; $env{'form.'.$symb} = 1; # so that we don't have to read it from disk for multiple sub of the same prob. } - $answer =~ s-\n-
-g; return '

'.&keywords_highlight($answer).'
'; + } elsif ( $response eq 'organic') { - my $result='Smile representation: "'.$answer.'"'; + my $result=&mt('Smile representation: [_1]', + '"'.&HTML::Entities::encode($answer, '"<>&').'"'); my $jme=$record->{$version."resource.$partid.$respid.molecule"}; $result.=&Apache::chemresponse::jme_img($jme,$answer,400); return $result; @@ -437,12 +444,14 @@ sub cleanRecord { $result.=''; return $result; } - } elsif ( $response =~ m/(?:numerical|formula)/) { + } elsif ( $response =~ m/(?:numerical|formula|custom)/) { + # Respect multiple input fields, see Bug #5409 $answer = &Apache::loncommon::format_previous_attempt_value('submission', $answer); + return $answer; } - return $answer; + return &HTML::Entities::encode($answer, '"<>&'); } #-- A couple of common js functions @@ -736,7 +745,7 @@ sub most_similar { sub initialverifyreceipt { my ($request,$symb) = @_; &commonJSfunctions($request); - return '
'. + return ''. &Apache::lonnet::recprefix($env{'request.course.id'}). '-'. ''."\n". @@ -908,17 +917,17 @@ LISTJAVASCRIPT $submission_options.= ''. ''."\n". + &mt('last submission').' '."\n". ''. ''."\n". + &mt('last submission with details').' '."\n". ''. ''."\n". + &mt('all submissions').''."\n". ''. ''; - $gradeTable .= &Apache::lonhtmlcommon::row_title(&mt('Submissions')) + &mt('all submissions with details').''; + $gradeTable .= &Apache::lonhtmlcommon::row_title(&mt('View Submissions')) .$submission_options .&Apache::lonhtmlcommon::row_closure(); @@ -1084,7 +1093,7 @@ LISTJAVASCRIPT if ($submitonly eq 'graded' ) { $submissions = 'ungraded submissions'; } if ($submitonly eq 'queued' ) { $submissions = 'queued submissions'; } $gradeTable='
 '. - &mt('No '.$submissions.' found for this resource for any students. ([_1] students checked for '.$submissions.')', + &mt('No '.$submissions.' found for this resource for any students. ([quant,_1,student] checked for '.$submissions.')', $num_students). '
'; } @@ -1286,10 +1295,8 @@ sub sub_page_js { } } } - } } - } formname.submit(); } @@ -1431,6 +1438,15 @@ INNERJS txtc => 'Text Color', font => 'Font Size', fnst => 'Font Style', + col1 => 'red', + col2 => 'green', + col3 => 'blue', + siz1 => 'normal', + siz2 => '+1', + siz3 => '+2', + sty1 => 'normal', + sty2 => 'italic', + sty3 => 'bold', ); $request->print(&Apache::lonhtmlcommon::scripttag(<") {var syisel="checked"}; - if (kwstyle=="") {var sybsel="checked"}; + if (kwstyle=="") {var synsel="checked='checked'"}; + if (kwstyle=="") {var syisel="checked='checked'"}; + if (kwstyle=="") {var sybsel="checked='checked'"}; highlightCentral(); - highlightbody('red','red',redsel,'0','normal',sznsel,'','normal',synsel); - highlightbody('green','green',grnsel,'+1','+1',sz1sel,'','italic',syisel); - highlightbody('blue','blue',blusel,'+2','+2',sz2sel,'','bold',sybsel); + highlightbody('red',txtcol1,redsel,'0',txtsiz1,sznsel,'',txtsty1,synsel); + highlightbody('green',txtcol2,grnsel,'+1',txtsiz2,sz1sel,'',txtsty2,syisel); + highlightbody('blue',txtcol3,blusel,'+2',txtsiz3,sz2sel,'',txtsty3,sybsel); highlightend(); return; } @@ -1630,31 +1655,29 @@ INNERJS hDoc.$docopen; hDoc.write('$start_page_highlight_central'); hDoc.write(""); - hDoc.write("

 $lt{'kehi'}<\\/span><\\/h3>

"); + hDoc.write("

$lt{'kehi'}<\\/h1>"); - hDoc.write('
'); - hDoc.write(''); - hDoc.write("
$lt{'txtc'}<\\/b><\\/td>$lt{'font'}<\\/b><\\/td>$lt{'fnst'}<\\/td><\\/tr>"); + hDoc.write(''); + hDoc.write(""); + hDoc.write(""); hDoc.write("'; + ''; $result.=&Apache::loncommon::end_data_table_row(); + $result.=&Apache::loncommon::start_data_table_row().''.&Apache::loncommon::end_data_table_row(); if ($res_error) { return &navmap_errormsg(); } @@ -1786,7 +1811,7 @@ sub handback_box { if ($file =~ /\/portfolio\//) { $file_counter++; my ($file_path, $file_disp) = ($file =~ m|(.+/)(.+)$|); - my ($name,$version,$ext) = &file_name_version_ext($file_disp); + my ($name,$version,$ext) = &Apache::lonnet::file_name_version_ext($file_disp); $file_disp = "$name.$ext"; $file = $file_path.$file_disp; $result.=&mt('Return commented version of [_1] to student.', @@ -1940,9 +1965,12 @@ sub submission { if ($symb eq '') { $request->print("Unable to handle ambiguous references:."); return ''; } if (!&canview($usec)) { - $request->print('Unable to view requested student.('. - $uname.':'.$udom.' in section '.$usec.' in course id '. - $env{'request.course.id'}.')'); + $request->print( + ''. + &mt('Unable to view requested student.'). + ' '.&mt('([_1] in section [_2] in course id [_3])', + $uname.':'.$udom,$usec,$env{'request.course.id'}). + ''); return; } @@ -1974,7 +2002,7 @@ sub submission { $request->print(&show_problem($request,$symb,$uname,$udom,0,1,$mode)); } - # kwclr is the only variable that is guaranteed to be non blank + # kwclr is the only variable that is guaranteed not to be blank # if this subroutine has been called once. my %keyhash = (); # if ($env{'form.kwclr'} eq '' && $env{'form.handgrade'} eq 'yes') { @@ -2039,6 +2067,7 @@ sub submission { if (1) { my %lt = &Apache::lonlocal::texthash( + keyh => 'Keyword Highlighting for Essays', keyw => 'Keyword Options', list => 'List', past => 'Paste Selection to List', @@ -2047,13 +2076,18 @@ sub submission { # # Print out the keyword options line # - $request->print(<$lt{'keyw'}:  -$lt{'list'}    -$lt{'past'}    -$lt{'high'}

-KEYWORDS + $request->print( + '
' + .'
'.$lt{'keyh'}.'' + .&Apache::lonhtmlcommon::funclist_from_array( + [''.$lt{'list'}.'', + ''.$lt{'past'}.'', + ''.$lt{'high'}.''], + {legend => $lt{'keyw'}}) + .'
' + ); + # # Load the other essays for similarity check # @@ -2078,7 +2112,7 @@ KEYWORDS if ($perm{'vgr'}) { $request->print( &Apache::loncommon::track_student_link( - &mt('View recent activity'), + 'View recent activity', $uname,$udom,'check') .' ' ); @@ -2137,140 +2171,160 @@ KEYWORDS $result.=$sub_result; } $request->print($result."\n"); - + # print student answer/submission # Options are (1) Handgraded submission only # (2) Last submission, includes submission that is not handgraded # (for multi-response type part) # (3) Last submission plus the parts info # (4) The whole record for this student - if ($env{'form.lastSub'} =~ /^(lastonly|hdgrade)$/) { - my ($string,$timestamp)= &get_last_submission(\%record); + + my ($string,$timestamp)= &get_last_submission(\%record); - my $lastsubonly; + my $lastsubonly; - if ($$timestamp eq '') { - $lastsubonly.='
'.$$string[0].'
'; - } else { - $lastsubonly = - '
' - .''.&mt('Date Submitted:').' '.$$timestamp."\n"; - - my %seenparts; - my @part_response_id = &flatten_responseType($responseType); - foreach my $part (@part_response_id) { - next if ($env{'form.lastSub'} eq 'hdgrade' + if ($$timestamp eq '') { + $lastsubonly.='
'.$$string[0].'
'; + } else { + $lastsubonly = + '
' + .''.&mt('Date Submitted:').' '.$$timestamp."\n"; + + my %seenparts; + my @part_response_id = &flatten_responseType($responseType); + foreach my $part (@part_response_id) { + next if ($env{'form.lastSub'} eq 'hdgrade' && $$handgrade{$$part[0].'_'.$$part[1]} ne 'yes'); - my ($partid,$respid) = @{ $part }; - my $display_part=&get_display_part($partid,$symb); - if ($env{"form.$uname:$udom:$partid:submitted_by"}) { - if (exists($seenparts{$partid})) { next; } - $seenparts{$partid}=1; - my $submitby='Part: '.$display_part. - ' Collaborative submission by: '. - ''. - $$fullname{$env{"form.$uname:$udom:$partid:submitted_by"}}.'
'; - $request->print($submitby); - next; - } - my $responsetype = $responseType->{$partid}->{$respid}; - if (!exists($record{"resource.$partid.$respid.submission"})) { - $lastsubonly.="\n".'
'. - ''.&mt('Part: [_1]',$display_part).''. - ' '. - '('.&mt('Response ID: [_1]',$respid).')'. - '   '. - ''.&mt('Nothing submitted - no attempts.').'

'; - next; - } - foreach my $submission (@$string) { - my ($partid,$respid) = ($submission =~ /^resource\.([^\.]*)\.([^\.]*)\.submission/); - if (join('_',@{$part}) ne ($partid.'_'.$respid)) { next; } - my ($ressub,$hide,$subval) = split(/:/,$submission,3); - # Similarity check - my $similar=''; - my ($type,$trial,$rndseed); - if ($hide eq 'rand') { - $type = 'randomizetry'; - $trial = $record{"resource.$partid.tries"}; - $rndseed = $record{"resource.$partid.rndseed"}; - } - if($env{'form.checkPlag'}){ - my ($oname,$odom,$ocrsid,$oessay,$osim)= - &most_similar($uname,$udom,$symb,$subval); - if ($osim) { - $osim=int($osim*100.0); - my %old_course_desc = - &Apache::lonnet::coursedescription($ocrsid, - {'one_time' => 1}); - - if ($hide eq 'anon') { - $similar='
'.&mt("Essay was found to be similar to another essay submitted for this assignment.").'
'. - &mt('As the current submission is for an anonymous survey, no other details are available.').'

'; - } else { - $similar="

". - &mt('Essay is [_1]% similar to an essay by [_2] in course [_3] (course id [_4]:[_5])', - $osim, - &Apache::loncommon::plainname($oname,$odom).' ('.$oname.':'.$odom.')', + my ($partid,$respid) = @{ $part }; + my $display_part=&get_display_part($partid,$symb); + if ($env{"form.$uname:$udom:$partid:submitted_by"}) { + if (exists($seenparts{$partid})) { next; } + $seenparts{$partid}=1; + $request->print( + ''.&mt('Part: [_1]',$display_part).''. + ' '.&mt('Collaborative submission by: [_1]', + ''. + $$fullname{$env{"form.$uname:$udom:$partid:submitted_by"}}.''). + '
'); + next; + } + my $responsetype = $responseType->{$partid}->{$respid}; + if (!exists($record{"resource.$partid.$respid.submission"})) { + $lastsubonly.="\n".'
'. + ''.&mt('Part: [_1]',$display_part).''. + ' '. + '('.&mt('Response ID: [_1]',$respid).')'. + '   '. + ''.&mt('Nothing submitted - no attempts.').'

'; + next; + } + foreach my $submission (@$string) { + my ($partid,$respid) = ($submission =~ /^resource\.([^\.]*)\.([^\.]*)\.submission/); + if (join('_',@{$part}) ne ($partid.'_'.$respid)) { next; } + my ($ressub,$hide,$draft,$subval) = split(/:/,$submission,4); + # Similarity check + my $similar=''; + my ($type,$trial,$rndseed); + if ($hide eq 'rand') { + $type = 'randomizetry'; + $trial = $record{"resource.$partid.tries"}; + $rndseed = $record{"resource.$partid.rndseed"}; + } + if ($env{'form.checkPlag'}) { + my ($oname,$odom,$ocrsid,$oessay,$osim)= + &most_similar($uname,$udom,$symb,$subval); + if ($osim) { + $osim=int($osim*100.0); + my %old_course_desc = + &Apache::lonnet::coursedescription($ocrsid, + {'one_time' => 1}); + + if ($hide eq 'anon') { + $similar='
'.&mt("Essay was found to be similar to another essay submitted for this assignment.").'
'. + &mt('As the current submission is for an anonymous survey, no other details are available.').'

'; + } else { + $similar="

". + &mt('Essay is [_1]% similar to an essay by [_2] in course [_3] (course id [_4]:[_5])', + $osim, + &Apache::loncommon::plainname($oname,$odom).' ('.$oname.':'.$odom.')', $old_course_desc{'description'}, $old_course_desc{'num'}, $old_course_desc{'domain'}). '

'. &keywords_highlight($oessay). '

'; - } - } - } - my $order=&get_order($partid,$respid,$symb,$uname,$udom, - undef,$type,$trial,$rndseed); - if ($env{'form.lastSub'} eq 'lastonly' || - ($env{'form.lastSub'} eq 'hdgrade' && + } + } + } + my $order=&get_order($partid,$respid,$symb,$uname,$udom, + undef,$type,$trial,$rndseed); + if ($env{'form.lastSub'} eq 'lastonly' || $env{'form.lastSub'} eq 'datesub' || $env{'form.lastSub'} =~ /^(last|all)$/ || ($env{'form.lastSub'} eq 'hdgrade' && $$handgrade{$$part[0].'_'.$$part[1]} eq 'yes')) { - my $display_part=&get_display_part($partid,$symb); - $lastsubonly.='
'. - ''.&mt('Part: [_1]',$display_part).''. - ' '. - '('.&mt('Response ID: [_1]',$respid).')'. - '   '; - my $files=&get_submitted_files($udom,$uname,$partid,$respid,\%record); - if (@$files) { - if ($hide eq 'anon') { - $lastsubonly.='
'.&mt('[quant,_1,file] uploaded to this anonymous survey',scalar(@{$files})); - } else { - $lastsubonly.='
'.&mt('Like all files provided by users, this file may contain viruses').'
'; - foreach my $file (@$files) { - &Apache::lonnet::allowuploaded('/adm/grades',$file); - $lastsubonly.='
'.$file.''; - } - } - $lastsubonly.='
'; - } + my $display_part=&get_display_part($partid,$symb); + $lastsubonly.='
'. + ''.&mt('Part: [_1]',$display_part).''. + ' '. + '('.&mt('Response ID: [_1]',$respid).')'. + '   '; + my $files=&get_submitted_files($udom,$uname,$partid,$respid,\%record); + + if (@$files) { if ($hide eq 'anon') { - $lastsubonly.=''.&mt('Anonymous Survey').''; + $lastsubonly.='
'.&mt('[quant,_1,file] uploaded to this anonymous survey',scalar(@{$files})); } else { - $lastsubonly.=''.&mt('Submitted Answer:').' '. - &cleanRecord($subval,$responsetype,$symb,$partid, - $respid,\%record,$order,undef,$uname,$udom,$type,$trial,$rndseed); + $lastsubonly.='

'.''.&mt('Submitted Files:').'' + .'
'; + if(@$files == 1) { + $lastsubonly .= &mt('Like all files provided by users, this file may contain viruses!'); + } else { + $lastsubonly .= &mt('Like all files provided by users, these files may contain viruses!'); + } + $lastsubonly .= ''; + foreach my $file (@$files) { + &Apache::lonnet::allowuploaded('/adm/grades',$file); + $lastsubonly.='
'.$file.''; + } } - if ($similar) {$lastsubonly.="

$similar\n";} - $lastsubonly.='
'; - } + $lastsubonly.='
'; + } + if ($hide eq 'anon') { + $lastsubonly.='
'.&mt('Anonymous Survey').''; + } else { + $lastsubonly.='
'.&mt('Submitted Answer:').' '; + if ($draft) { + $lastsubonly.= ' '.&mt('Draft Copy').''; + } + $subval = + &cleanRecord($subval,$responsetype,$symb,$partid, + $respid,\%record,$order,undef,$uname,$udom,$type,$trial,$rndseed); + if ($responsetype eq 'essay') { + $subval =~ s{\n}{
}g; + } + $lastsubonly.=$subval."\n"; + } + if ($similar) {$lastsubonly.="

$similar\n";} + $lastsubonly.='
'; } - } - $lastsubonly.='

'."\n"; # End: LC_grade_submissions_body + } } - $request->print($lastsubonly); - } elsif ($env{'form.lastSub'} eq 'datesub') { + $lastsubonly.='
'."\n"; # End: LC_grade_submissions_body + } + $request->print($lastsubonly); + if ($env{'form.lastSub'} eq 'datesub') { my ($parts,$handgrade,$responseType) = &response_type($symb,\$res_error); $request->print(&displaySubByDates($symb,\%record,$parts,$responseType,$checkIcon,$uname,$udom)); - } elsif ($env{'form.lastSub'} =~ /^(last|all)$/) { - $request->print(&Apache::loncommon::get_previous_attempt($symb,$uname,$udom, + + } + if ($env{'form.lastSub'} =~ /^(last|all)$/) { + my $identifier = (&canmodify($usec)? $counter : ''); + $request->print(&Apache::loncommon::get_previous_attempt($symb,$uname,$udom, $env{'request.course.id'}, $last,'.submission', - 'Apache::grades::keywords_highlight')); + 'Apache::grades::keywords_highlight', + $usec,$identifier)); } $request->print(''."\n"); @@ -2300,10 +2354,10 @@ KEYWORDS ''."\n"; $result.=' '. - &mt('Compose message to student').(scalar(@$col_fullnames) >= 1 ? 's' : '').'
'."\n"; + &mt('Score Part: [_1][_2](weight = [_3])', + $display_part,'
',$weight{$partid}).''."\n"; next; } else { @@ -4131,11 +4164,12 @@ sub csvupload_fields { sub csvuploadmap_footer { my ($request,$i,$keyfields) =@_; + my $buttontext = &mt('Assign Grades'); $request->print(< -
+
ENDPICK } @@ -4256,7 +4290,7 @@ ENDPICK } # FIXME do a check for any duplicated user ids... # FIXME do a check for any invalid user ids?... - $request->print('
+ $request->print('

'."\n"); return ''; } @@ -4444,48 +4478,68 @@ LISTJAVASCRIPT my ($curpage) =&Apache::lonnet::decode_symb($symb); # my ($curpage,$mapId) =&Apache::lonnet::decode_symb($symb); # my $type=($curpage =~ /\.(page|sequence)/); - my $select = ''."\n"; + $result.=''."\n"; + $ctr++; } - $select.= ''; - $result.=' '.&mt('Problems from').': '.$select."
\n"; + $result.=''."\n". + ''."\n"; + $result.=&build_section_inputs(); + my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status')); + $result.=''."\n". + ''."\n". + ''."\n"; + + # Show grading options + $result.=&Apache::lonhtmlcommon::start_pick_box(); + my $select = ''."\n"; - $result.=''."\n"; + $select.=''."\n"; $ctr++; } - $result.=''."\n". - ''."\n"; + $select.= ''; - my $options = - ''."\n". - ''."
\n"; - $result.=' '.&mt('View Problem Text').': '.$options; - - $options = - ''."\n". - ''."\n". - ''."\n"; - $result.=' '.&mt('Submissions').': '.$options; + $result.= + &Apache::lonhtmlcommon::row_title(&mt('Problems from')) + .$select + .&Apache::lonhtmlcommon::row_closure(); + + $result.= + &Apache::lonhtmlcommon::row_title(&mt('View Problem Text')) + .''."\n" + .''."\n" + .&Apache::lonhtmlcommon::row_closure(); + + $result.= + &Apache::lonhtmlcommon::row_title(&mt('View Submissions')) + .''."\n" + .''."\n" + .'' + .&Apache::lonhtmlcommon::row_closure(); - $result.=&build_section_inputs(); - my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status')); - $result.=''."\n". - ''."\n". - ''."
\n"; - - $result.=' '.&mt('Use CODE').':
'."\n"; + $result.= + &Apache::lonhtmlcommon::row_title(&mt('Use CODE')) + .'' + .&Apache::lonhtmlcommon::row_closure(1) + .&Apache::lonhtmlcommon::end_pick_box(); - $result.=' 
'."\n"; $request->print($result); @@ -4580,8 +4634,12 @@ sub displayPage { &Apache::lonnet::clear_EXT_cache_status(); if (!&canview($usec)) { - $request->print(''.&mt('Unable to view requested student. ([_1])',$env{'form.student'}).''); - return; + $request->print( + ''. + &mt('Unable to view requested student. ([_1])', + $env{'form.student'}). + ''); + return; } my $result='

 '.$env{'form.title'}.'

'; $result.='

 '.&mt('Student: [_1]',&nameUserString(undef,$$fullname{$env{'form.student'}},$uname,$udom)). @@ -4630,7 +4688,7 @@ sub displayPage { ''."\n". &Apache::loncommon::start_data_table(). &Apache::loncommon::start_data_table_header_row(). - '

'. + ''. ''. &Apache::loncommon::end_data_table_header_row(); @@ -4692,9 +4750,11 @@ sub displayPage { } } elsif ($env{'form.lastSub'} eq 'all') { my $last = ($env{'form.lastSub'} eq 'last' ? 'last' : ''); + my $identifier = (&canmodify($usec)? $prob : ''); $studentTable.=&Apache::loncommon::get_previous_attempt($symbx,$uname,$udom, $env{'request.course.id'}, - '','.submission'); + '','.submission',undef, + $usec,$identifier); } if (&canmodify($usec)) { @@ -4907,7 +4967,7 @@ sub updateGradeByPage { $iterator->next(); # skip the first BEGIN_MAP my $curRes = $iterator->next(); # for "current resource" - my ($depth,$question,$prob,$changeflag)= (1,1,1,0); + my ($depth,$question,$prob,$changeflag,$hideflag)= (1,1,1,0,0); while ($depth > 0) { if($curRes == $iterator->BEGIN_MAP) { $depth++; } if($curRes == $iterator->END_MAP) { $depth--; } @@ -4928,6 +4988,12 @@ sub updateGradeByPage { my @displayPts=(); my %aggregate = (); my $aggregateflag = 0; + if ($env{'form.HIDE'.$prob}) { + my %record = &Apache::lonnet::restore($symbx,$env{'request.course.id'},$udom,$uname); + my ($version,$parts) = split(/:/,$env{'form.HIDE'.$prob},2); + my $numchgs = &makehidden($version,$parts,\%record,$symbx,$udom,$uname,1); + $hideflag += $numchgs; + } foreach my $partid (@{$parts}) { my $newpts = $env{'form.GD_BOX'.$question.'_'.$partid}; my $oldpts = $env{'form.oldpts'.$question.'_'.$partid}; @@ -5018,8 +5084,11 @@ sub updateGradeByPage { $studentTable.=&Apache::loncommon::end_data_table(); my $grademsg=($changeflag == 0 ? &mt('No score was changed or updated.') : &mt('The scores were changed for [quant,_1,problem].', - $changeflag)); - $request->print($grademsg.$studentTable); + $changeflag).'
'); + my $hidemsg=($hideflag == 0 ? '' : + &mt('Submissions were marked "hidden" for [quant,_1,transaction].', + $hideflag).'
'); + $request->print($hidemsg.$grademsg.$studentTable); return ''; } @@ -5054,7 +5123,7 @@ like. Next each scanline is checked for any errors of either 'missing bubbles' (it's an error because it may have been mis-scanned because too light bubbling), 'double bubble' (each bubble line should -have no more that one letter picked), invalid or duplicated CODE, +have no more than one letter picked), invalid or duplicated CODE, invalid student/employee ID If the CODE option is used that determines the randomization of the @@ -5142,6 +5211,11 @@ my %subdivided_bubble_lines; # no. my %responsetype_per_response; # responsetype for each response +my %masterseq_id_responsenum; # src_id (e.g., 12.3_0.11 etc.) for each + # numbered response. Needed when randomorder + # or randompick are in use. Key is ID, value + # is response number. + # Save and restore the bubble lines array to the form env. @@ -5155,12 +5229,17 @@ sub save_bubble_lines { $env{"form.scantron.responsetype.$line"} = $responsetype_per_response{$line}; } + foreach my $resid (keys(%masterseq_id_responsenum)) { + my $line = $masterseq_id_responsenum{$resid}; + $env{"form.scantron.residpart.$line"} = $resid; + } } sub restore_bubble_lines { my $line = 0; %bubble_lines_per_response = (); + %masterseq_id_responsenum = (); while ($env{"form.scantron.bubblelines.$line"}) { my $value = $env{"form.scantron.bubblelines.$line"}; $bubble_lines_per_response{$line} = $value; @@ -5170,28 +5249,12 @@ sub restore_bubble_lines { $env{"form.scantron.sub_bubblelines.$line"}; $responsetype_per_response{$line} = $env{"form.scantron.responsetype.$line"}; + my $id = $env{"form.scantron.residpart.$line"}; + $masterseq_id_responsenum{$id} = $line; $line++; } } -# Given the parsed scanline, get the response for -# 'answer' number n: - -sub get_response_bubbles { - my ($parsed_line, $response) = @_; - - my $bubble_line = $first_bubble_line{$response-1} +1; - my $bubble_lines= $bubble_lines_per_response{$response-1}; - - my $selected = ""; - - for (my $bline = 0; $bline < $bubble_lines; $bline++) { - $selected .= $$parsed_line{"scantron.$bubble_line.answer"}.":"; - $bubble_line++; - } - return $selected; -} - =pod =item scantron_filenames @@ -5519,7 +5582,7 @@ sub scantron_selectphase { &Apache::lonpickcode::code_list($r,2); - $r->print('
'. + $r->print('
'. $default_form_data."\n". &Apache::loncommon::start_data_table('LC_scantron_action')."\n". &Apache::loncommon::start_data_table_header_row()."\n". @@ -5558,7 +5621,7 @@ sub scantron_selectphase { =item get_scantron_config - Parse and return the scantron configuration line selected as a + Parse and return the bubblesheet configuration line selected as a hash of configuration file fields. Arguments: @@ -5651,7 +5714,9 @@ sub get_scantron_config { =item username_to_idmap creates a hash keyed by student/employee ID with values of the corresponding - student username:domain. + student username:domain. If a single ID occurs for more than one student, + the status of the student is checked, and if Active, the value in the hash + will be set to the Active student. Arguments: @@ -5669,8 +5734,17 @@ sub username_to_idmap { my ($classlist)= @_; my %idmap; foreach my $student (keys(%$classlist)) { - $idmap{$classlist->{$student}->[&Apache::loncoursedata::CL_ID]}= - $student; + my $id = $classlist->{$student}->[&Apache::loncoursedata::CL_ID]; + unless ($id eq '') { + if (!exists($idmap{$id})) { + $idmap{$id} = $student; + } else { + my $status = $classlist->{$student}->[&Apache::loncoursedata::CL_STATUS]; + if ($status eq 'Active') { + $idmap{$id} = $student; + } + } + } } return %idmap; } @@ -5842,16 +5916,37 @@ sub digits_to_letters { =item scantron_parse_scanline - Decodes a scanline from the selected scantron file + Decodes a scanline from the selected bubblesheet file Arguments: - line - The text of the scantron file line to process + line - The text of the bubblesheet file line to process whichline - Line number - scantron_config - Hash describing the format of the scantron lines. + scantron_config - Hash describing the format of the bubblesheet lines. scan_data - Hash of extra information about the scanline (see scantron_getfile for more information) just_header - True if should not process question answers but only the stuff to the left of the answers. + randomorder - True if randomorder in use + randompick - True if randompick in use + sequence - Exam folder URL + master_seq - Ref to array containing symbs in exam folder + symb_to_resource - Ref to hash of symbs for resources in exam folder + (corresponding values are resource objects) + partids_by_symb - Ref to hash of symb -> array ref of partIDs + orderedforcode - Ref to hash of arrays. keys are CODEs and values + are refs to an array of resource objects, ordered + according to order used for CODE, when randomorder + and or randompick are in use. + respnumlookup - Ref to hash mapping question numbers in bubble lines + for current line to question number used for same question + in "Master Sequence" (as seen by Course Coordinator). + startline - Ref to hash where key is question number (0 is first) + and value is number of first bubble line for current + student or code-based randompick and/or randomorder. + totalref - Ref of scalar used to score total number of bubble + lines needed for responses in a scan line (used when + randompick in use. + Returns: Hash containing the result of parsing the scanline @@ -5896,12 +5991,12 @@ sub digits_to_letters { =cut sub scantron_parse_scanline { - my ($line,$whichline,$scantron_config,$scan_data,$just_header)=@_; + my ($line,$whichline,$scantron_config,$scan_data,$just_header,$idmap, + $randomorder,$randompick,$sequence,$master_seq,$symb_to_resource, + $partids_by_symb,$orderedforcode,$respnumlookup,$startline,$totalref)=@_; my %record; - my $lastpos = $env{'form.scantron_maxbubble'}*$$scantron_config{'Qlength'}; - my $questions=substr($line,$$scantron_config{'Qstart'}-1,$lastpos); # Answers - my $data=substr($line,0,$$scantron_config{'Qstart'}-1); # earlier stuff + my $data=substr($line,0,$$scantron_config{'Qstart'}-1); # stuff before answers if (!($$scantron_config{'CODElocation'} eq 0 || $$scantron_config{'CODElocation'} eq 'none')) { if ($$scantron_config{'CODElocation'} < 0 || @@ -5937,10 +6032,29 @@ sub scantron_parse_scanline { my $questnum=0; my $ansnum =1; # Multiple 'answer lines'/question. + my $lastpos = $env{'form.scantron_maxbubble'}*$$scantron_config{'Qlength'}; + if ($randompick || $randomorder) { + my $total = &get_respnum_lookups($sequence,$scan_data,$idmap,$line,\%record, + $master_seq,$symb_to_resource, + $partids_by_symb,$orderedforcode, + $respnumlookup,$startline); + if ($total) { + $lastpos = $total*$$scantron_config{'Qlength'}; + } + if (ref($totalref)) { + $$totalref = $total; + } + } + my $questions=substr($line,$$scantron_config{'Qstart'}-1,$lastpos); # Answers chomp($questions); # Get rid of any trailing \n. $questions =~ s/\r$//; # Get rid of trailing \r too (MAC or Win uploads). while (length($questions)) { - my $answers_needed = $bubble_lines_per_response{$questnum}; + my $answers_needed; + if (($randompick || $randomorder) && (ref($respnumlookup) eq 'HASH')) { + $answers_needed = $bubble_lines_per_response{$respnumlookup->{$questnum}}; + } else { + $answers_needed = $bubble_lines_per_response{$questnum}; + } my $answer_length = ($$scantron_config{'Qlength'} * $answers_needed) || 1; $questnum++; @@ -5949,11 +6063,16 @@ sub scantron_parse_scanline { $questions = substr($questions,$answer_length); if (length($currentquest) < $answer_length) { next; } - if ($subdivided_bubble_lines{$questnum-1} =~ /,/) { + my $subdivided; + if (($randompick || $randomorder) && (ref($respnumlookup) eq 'HASH')) { + $subdivided = $subdivided_bubble_lines{$respnumlookup->{$questnum-1}}; + } else { + $subdivided = $subdivided_bubble_lines{$questnum-1}; + } + if ($subdivided =~ /,/) { my $subquestnum = 1; my $subquestions = $currentquest; - my @subanswers_needed = - split(/,/,$subdivided_bubble_lines{$questnum-1}); + my @subanswers_needed = split(/,/,$subdivided); foreach my $subans (@subanswers_needed) { my $subans_length = ($$scantron_config{'Qlength'} * $subans) || 1; @@ -5964,10 +6083,13 @@ sub scantron_parse_scanline { ($$scantron_config{'Qon'} eq 'number')) { $ansnum = &scantron_validator_lettnum($ansnum, $questnum,$quest_id,$subans,$currsubquest,$whichline, - \@alphabet,\%record,$scantron_config,$scan_data); + \@alphabet,\%record,$scantron_config,$scan_data, + $randomorder,$randompick,$respnumlookup); } else { $ansnum = &scantron_validator_positional($ansnum, - $questnum,$quest_id,$subans,$currsubquest,$whichline, \@alphabet,\%record,$scantron_config,$scan_data); + $questnum,$quest_id,$subans,$currsubquest,$whichline, + \@alphabet,\%record,$scantron_config,$scan_data, + $randomorder,$randompick,$respnumlookup); } $subquestnum ++; } @@ -5976,11 +6098,13 @@ sub scantron_parse_scanline { ($$scantron_config{'Qon'} eq 'number')) { $ansnum = &scantron_validator_lettnum($ansnum,$questnum, $quest_id,$answers_needed,$currentquest,$whichline, - \@alphabet,\%record,$scantron_config,$scan_data); + \@alphabet,\%record,$scantron_config,$scan_data, + $randomorder,$randompick,$respnumlookup); } else { $ansnum = &scantron_validator_positional($ansnum,$questnum, $quest_id,$answers_needed,$currentquest,$whichline, - \@alphabet,\%record,$scantron_config,$scan_data); + \@alphabet,\%record,$scantron_config,$scan_data, + $randomorder,$randompick,$respnumlookup); } } } @@ -5988,9 +6112,66 @@ sub scantron_parse_scanline { return \%record; } +sub get_master_seq { + my ($resources,$master_seq,$symb_to_resource) = @_; + return unless ((ref($resources) eq 'ARRAY') && (ref($master_seq) eq 'ARRAY') && + (ref($symb_to_resource) eq 'HASH')); + my $resource_error; + foreach my $resource (@{$resources}) { + my $ressymb; + if (ref($resource)) { + $ressymb = $resource->symb(); + push(@{$master_seq},$ressymb); + $symb_to_resource->{$ressymb} = $resource; + } else { + $resource_error = 1; + last; + } + } + return $resource_error; +} + +sub get_respnum_lookups { + my ($sequence,$scan_data,$idmap,$line,$record,$master_seq,$symb_to_resource, + $partids_by_symb,$orderedforcode,$respnumlookup,$startline) = @_; + return unless ((ref($record) eq 'HASH') && (ref($master_seq) eq 'ARRAY') && + (ref($symb_to_resource) eq 'HASH') && (ref($partids_by_symb) eq 'HASH') && + (ref($orderedforcode) eq 'HASH') && (ref($respnumlookup) eq 'HASH') && + (ref($startline) eq 'HASH')); + my ($user,$scancode); + if ((exists($record->{'scantron.CODE'})) && + (&Apache::lonnet::validCODE($record->{'scantron.CODE'}))) { + $scancode = $record->{'scantron.CODE'}; + } else { + $user = &scantron_find_student($record,$scan_data,$idmap,$line); + } + my @mapresources = + &users_order($user,$scancode,$sequence,$master_seq,$symb_to_resource, + $orderedforcode); + my $total = 0; + my $count = 0; + foreach my $resource (@mapresources) { + my $id = $resource->id(); + my $symb = $resource->symb(); + if (ref($partids_by_symb->{$symb}) eq 'ARRAY') { + foreach my $partid (@{$partids_by_symb->{$symb}}) { + my $respnum = $masterseq_id_responsenum{$id.'_'.$partid}; + if ($respnum ne '') { + $respnumlookup->{$count} = $respnum; + $startline->{$count} = $total; + $total += $bubble_lines_per_response{$respnum}; + $count ++; + } + } + } + } + return $total; +} + sub scantron_validator_lettnum { my ($ansnum,$questnum,$quest_id,$answers_needed,$currquest,$whichline, - $alphabet,$record,$scantron_config,$scan_data) = @_; + $alphabet,$record,$scantron_config,$scan_data,$randomorder, + $randompick,$respnumlookup) = @_; # Qon 'letter' implies for each slot in currquest we have: # ? or * for doubles, a letter in A-Z for a bubble, and @@ -6009,19 +6190,23 @@ sub scantron_validator_lettnum { $matchon = '\d'; } my $occurrences = 0; - if (($responsetype_per_response{$questnum-1} eq 'essayresponse') || - ($responsetype_per_response{$questnum-1} eq 'formularesponse') || - ($responsetype_per_response{$questnum-1} eq 'stringresponse') || - ($responsetype_per_response{$questnum-1} eq 'imageresponse') || - ($responsetype_per_response{$questnum-1} eq 'reactionresponse') || - ($responsetype_per_response{$questnum-1} eq 'organicresponse')) { + my $responsenum = $questnum-1; + if (($randompick || $randomorder) && (ref($respnumlookup) eq 'HASH')) { + $responsenum = $respnumlookup->{$questnum-1} + } + if (($responsetype_per_response{$responsenum} eq 'essayresponse') || + ($responsetype_per_response{$responsenum} eq 'formularesponse') || + ($responsetype_per_response{$responsenum} eq 'stringresponse') || + ($responsetype_per_response{$responsenum} eq 'imageresponse') || + ($responsetype_per_response{$responsenum} eq 'reactionresponse') || + ($responsetype_per_response{$responsenum} eq 'organicresponse')) { my @singlelines = split('',$currquest); foreach my $entry (@singlelines) { $occurrences = &occurence_count($entry,$matchon); if ($occurrences > 1) { last; } - } + } } else { $occurrences = &occurence_count($currquest,$matchon); } @@ -6069,7 +6254,8 @@ sub scantron_validator_lettnum { sub scantron_validator_positional { my ($ansnum,$questnum,$quest_id,$answers_needed,$currquest, - $whichline,$alphabet,$record,$scantron_config,$scan_data) = @_; + $whichline,$alphabet,$record,$scantron_config,$scan_data, + $randomorder,$randompick,$respnumlookup) = @_; # Otherwise there's a positional notation; # each bubble line requires Qlength items, and there are filled in @@ -6111,12 +6297,16 @@ sub scantron_validator_positional { # record all the bubbles filled in as well as the # fact this response consists of multiple bubbles. # - if (($responsetype_per_response{$questnum-1} eq 'essayresponse') || - ($responsetype_per_response{$questnum-1} eq 'formularesponse') || - ($responsetype_per_response{$questnum-1} eq 'stringresponse') || - ($responsetype_per_response{$questnum-1} eq 'imageresponse') || - ($responsetype_per_response{$questnum-1} eq 'reactionresponse') || - ($responsetype_per_response{$questnum-1} eq 'organicresponse')) { + my $responsenum = $questnum-1; + if (($randompick || $randomorder) && (ref($respnumlookup) eq 'HASH')) { + $responsenum = $respnumlookup->{$questnum-1} + } + if (($responsetype_per_response{$responsenum} eq 'essayresponse') || + ($responsetype_per_response{$responsenum} eq 'formularesponse') || + ($responsetype_per_response{$responsenum} eq 'stringresponse') || + ($responsetype_per_response{$responsenum} eq 'imageresponse') || + ($responsetype_per_response{$responsenum} eq 'reactionresponse') || + ($responsetype_per_response{$responsenum} eq 'organicresponse')) { my $doubleerror = 0; while (($currquest >= $$scantron_config{'Qlength'}) && (!$doubleerror)) { @@ -6298,7 +6488,11 @@ sub scantron_process_corrections { } } if ($err) { - $r->print("Unable to accept last correction, an error occurred :$errmsg:"); + $r->print( + '

' + .&mt('Unable to accept last correction, an error occurred: [_1]', + $errmsg) + .'

'); } else { &scantron_put_line($scanlines,$scan_data,$which,$line,$skip); &scantron_putfile($scanlines,$scan_data); @@ -6420,7 +6614,7 @@ sub scantron_warning_screen { $scantron_config{'CODEstart'} && $scantron_config{'CODElength'}) { $CODElist=$env{'form.scantron_CODElist'}; - if ($env{'form.scantron_CODElist'} eq '') { $CODElist='None'; } + if ($env{'form.scantron_CODElist'} eq '') { $CODElist=''.&mt('None').''; } $CODElist= '
'; @@ -6434,7 +6628,7 @@ sub scantron_warning_screen { return ('

-'.&mt('Please double check the information below before clicking on \'[_1]\'',&mt($button_text)).' +'.&mt("Please double check the information below before clicking on '[_1]'",&mt($button_text)).'

$lt{'txtc'}<\\/th>$lt{'font'}<\\/th>$lt{'fnst'}<\\/th><\\/tr>"); } function highlightbody(clrval,clrtxt,clrsel,szval,sztxt,szsel,syval,sytxt,sysel) { var hDoc = hwdWin.document; - hDoc.write("
"); - hDoc.write(" "+clrtxt+"<\\/td>"); + hDoc.write(" "+clrtxt+"<\\/td>"); hDoc.write(""); - hDoc.write(" "+sztxt+"<\\/td>"); + hDoc.write(" "+sztxt+"<\\/td>"); hDoc.write(""); - hDoc.write(" "+sytxt+"<\\/td>"); + hDoc.write(" "+sytxt+"<\\/td>"); hDoc.write("<\\/tr>"); } function highlightend() { var hDoc = hwdWin.document; - hDoc.write("<\\/table>"); - hDoc.write("<\\/td><\\/tr><\\/table> "); - hDoc.write("  "); - hDoc.write("

"); + hDoc.write("<\\/table>
"); + hDoc.write("  "); + hDoc.write("

"); hDoc.write("<\\/form>"); hDoc.write('$end_page_highlight_central'); hDoc.close(); @@ -1702,7 +1725,7 @@ sub gradeBox { $wgt = ($wgt > 0 ? $wgt : '1'); my $score = ($$record{'resource.'.$partid.'.awarded'} eq '' ? '' : &compute_points($$record{'resource.'.$partid.'.awarded'},$wgt)); - my $result=''."\n"; + my $data_WGT=''."\n"; my $display_part= &get_display_part($partid,$symb); my %last_resets = &get_last_resets($symb,$env{'request.course.id'}, [$partid]); @@ -1710,7 +1733,7 @@ sub gradeBox { if ($last_resets{$partid}) { $aggtries = &get_num_tries($record,$last_resets{$partid},$partid); } - $result.=&Apache::loncommon::start_data_table_row(); + my $result=&Apache::loncommon::start_data_table_row(); my $ctr = 0; my $thisweight = 0; my $increment = &get_increment(); @@ -1747,8 +1770,9 @@ sub gradeBox { $result .= - '
'.$display_part.''.$radio.''.&mt('or').''.$line.''.$data_WGT.$display_part.''.$radio.''.&mt('or').''.$line.''; $result.=''."\n". ''."\n". ''."\n"; my $res_error; $result.=&handback_box($symb,$uname,$udom,$counter,$partid,$record,\$res_error); + $result.=''.&mt('Grade Status').':'.&mt('Grade Status').':'. + ''. - &mt('Score Part: [_1]
(weight = [_2])', - $display_part,$weight{$partid}).'
 Prob. '.&mt('Prob.').' '.($env{'form.vProb'} eq 'no' ? &mt('Title') : &mt('Problem Text')).'/'.&mt('Grade').'
'.&mt('List of CODES to validate against:').''. $env{'form.scantron_CODElist'}.'
@@ -6521,9 +6715,11 @@ SCANTRONFORM ''."\n"; $chunk .= ''."\n"; + $chunk .= + ''."\n"; $result .= $chunk; $line++; - } + } return $result; } @@ -6544,7 +6740,7 @@ sub scantron_validate_file { if (!$symb) {return '';} my $default_form_data=&defaultFormData($symb); - # do the detection of only doing skipped records first befroe we delete + # do the detection of only doing skipped records first before we delete # them when doing the corrections reset if ($env{'form.scantron_options_redo'} ne 'redo_skipped_ready') { &reset_skipping_status(); @@ -6595,6 +6791,7 @@ sub scantron_validate_file { while (!$stop && $currentphase < scalar(@validate_phases)) { $r->print(&mt('Validating '.$validate_phases[$currentphase]).'
'); $r->rflush(); + my $which="scantron_validate_".$validate_phases[$currentphase]; { no strict 'refs'; @@ -7006,7 +7203,7 @@ sub scantron_validate_sequence { $r->print( '

' .&mt('Some resources in the sequence currently are not set to' - .' exam mode. Grading these resources currently may not' + .' bubblesheet exam mode. Grading these resources currently may not' .' work correctly.') .'

' ); @@ -7090,7 +7287,8 @@ sub scantron_validate_ID { sub scantron_get_correction { - my ($r,$i,$scan_record,$scan_config,$line,$error,$arg)=@_; + my ($r,$i,$scan_record,$scan_config,$line,$error,$arg, + $randomorder,$randompick,$respnumlookup,$startline)=@_; #FIXME in the case of a duplicated ID the previous line, probably need #to show both the current line and the previous one and allow skipping #the previous one or the current one @@ -7140,7 +7338,7 @@ sub scantron_get_correction { $r->print(&Apache::loncommon::selectstudent_link('scantronupload', 'scantron_username','scantron_domain')); $r->print(": "); - $r->print("\n@". + $r->print("\n:\n". &Apache::loncommon::select_dom_form($env{'request.role.domain'},'scantron_domain')); $r->print(''); @@ -7224,7 +7422,8 @@ ENDSCRIPT # The form field scantron_questions is acutally a list of line numbers. # represented by this form so: - my $line_list = &questions_to_line_list($arg); + my $line_list = &questions_to_line_list($arg,$randomorder,$randompick, + $respnumlookup,$startline); $r->print(''); @@ -7232,7 +7431,9 @@ ENDSCRIPT $r->print("

".&mt("Please indicate which bubble should be used for grading")."

"); foreach my $question (@{$arg}) { my @linenums = &prompt_for_corrections($r,$question,$scan_config, - $scan_record, $error); + $scan_record, $error, + $randomorder,$randompick, + $respnumlookup,$startline); push(@lines_to_correct,@linenums); } $r->print(&verify_bubbles_checked(@lines_to_correct)); @@ -7245,14 +7446,17 @@ ENDSCRIPT # The form field scantron_questions is actually a list of line numbers not # a list of question numbers. Therefore: # - - my $line_list = &questions_to_line_list($arg); + + my $line_list = &questions_to_line_list($arg,$randomorder,$randompick, + $respnumlookup,$startline); $r->print(''); foreach my $question (@{$arg}) { my @linenums = &prompt_for_corrections($r,$question,$scan_config, - $scan_record, $error); + $scan_record, $error, + $randomorder,$randompick, + $respnumlookup,$startline); push(@lines_to_correct,@linenums); } $r->print(&verify_bubbles_checked(@lines_to_correct)); @@ -7303,12 +7507,20 @@ used to fill in the scantron_questions f Arguments: questions - Reference to an array of questions. + randomorder - True if randomorder in use. + randompick - True if randompick in use. + respnumlookup - Reference to HASH mapping question numbers in bubble lines + for current line to question number used for same question + in "Master Seqence" (as seen by Course Coordinator). + startline - Reference to hash where key is question number (0 is first) + and key is number of first bubble line for current student + or code-based randompick and/or randomorder. =cut sub questions_to_line_list { - my ($questions) = @_; + my ($questions,$randomorder,$randompick,$respnumlookup,$startline) = @_; my @lines; foreach my $item (@{$questions}) { @@ -7317,8 +7529,16 @@ sub questions_to_line_list { if ($item =~ /^(\d+)\.(\d+)$/) { $question = $1; my $subquestion = $2; - $first = $first_bubble_line{$question-1} + 1; - my @subans = split(/,/,$subdivided_bubble_lines{$question-1}); + my $responsenum = $question-1; + if (($randomorder || $randompick) && (ref($respnumlookup) eq 'HASH')) { + $responsenum = $respnumlookup->{$question-1}; + if (ref($startline) eq 'HASH') { + $first = $startline->{$question-1} + 1; + } + } else { + $first = $first_bubble_line{$responsenum} + 1; + } + my @subans = split(/,/,$subdivided_bubble_lines{$responsenum}); my $subcount = 1; while ($subcount<$subquestion) { $first += $subans[$subcount-1]; @@ -7326,8 +7546,16 @@ sub questions_to_line_list { } $count = $subans[$subquestion-1]; } else { - $first = $first_bubble_line{$question-1} + 1; - $count = $bubble_lines_per_response{$question-1}; + my $responsenum = $question-1; + if (($randomorder || $randompick) && (ref($respnumlookup) eq 'HASH')) { + $responsenum = $respnumlookup->{$question-1}; + if (ref($startline) eq 'HASH') { + $first = $startline->{$question-1} + 1; + } + } else { + $first = $first_bubble_line{$responsenum} + 1; + } + $count = $bubble_lines_per_response{$responsenum}; } $last = $first+$count-1; push(@lines, ($first..$last)); @@ -7349,6 +7577,15 @@ for multi and missing bubble cases). $scan_config - The scantron file configuration hash. $scan_record - Reference to the hash that has the the parsed scanlines. $error - Type of error + $randomorder - True if randomorder in use. + $randompick - True if randompick in use. + $respnumlookup - Reference to HASH mapping question numbers in bubble lines + for current line to question number used for same question + in "Master Seqence" (as seen by Course Coordinator). + $startline - Reference to hash where key is question number (0 is first) + and value is number of first bubble line for current student + or code-based randompick and/or randomorder. + Implicit inputs: %bubble_lines_per_response - Starting line numbers for each question. @@ -7373,15 +7610,26 @@ for multi and missing bubble cases). =cut sub prompt_for_corrections { - my ($r, $question, $scan_config, $scan_record, $error) = @_; + my ($r, $question, $scan_config, $scan_record, $error, $randomorder, + $randompick, $respnumlookup, $startline) = @_; my ($current_line,$lines); my @linenums; my $questionnum = $question; + my ($first,$responsenum); if ($question =~ /^(\d+)\.(\d+)$/) { $question = $1; - $current_line = $first_bubble_line{$question-1} + 1 ; my $subquestion = $2; - my @subans = split(/,/,$subdivided_bubble_lines{$question-1}); + if (($randomorder || $randompick) && (ref($respnumlookup) eq 'HASH')) { + $responsenum = $respnumlookup->{$question-1}; + if (ref($startline) eq 'HASH') { + $first = $startline->{$question-1}; + } + } else { + $responsenum = $question-1; + $first = $first_bubble_line{$responsenum}; + } + $current_line = $first + 1 ; + my @subans = split(/,/,$subdivided_bubble_lines{$responsenum}); my $subcount = 1; while ($subcount<$subquestion) { $current_line += $subans[$subcount-1]; @@ -7389,25 +7637,43 @@ sub prompt_for_corrections { } $lines = $subans[$subquestion-1]; } else { - $current_line = $first_bubble_line{$question-1} + 1 ; - $lines = $bubble_lines_per_response{$question-1}; + if (($randomorder || $randompick) && (ref($respnumlookup) eq 'HASH')) { + $responsenum = $respnumlookup->{$question-1}; + if (ref($startline) eq 'HASH') { + $first = $startline->{$question-1}; + } + } else { + $responsenum = $question-1; + $first = $first_bubble_line{$responsenum}; + } + $current_line = $first + 1; + $lines = $bubble_lines_per_response{$responsenum}; } if ($lines > 1) { $r->print(&mt('The group of bubble lines below responds to a single question.').'
'); - if (($responsetype_per_response{$question-1} eq 'essayresponse') || - ($responsetype_per_response{$question-1} eq 'formularesponse') || - ($responsetype_per_response{$question-1} eq 'stringresponse') || - ($responsetype_per_response{$question-1} eq 'imageresponse') || - ($responsetype_per_response{$question-1} eq 'reactionresponse') || - ($responsetype_per_response{$question-1} eq 'organicresponse')) { - $r->print(&mt("Although this particular question type requires handgrading, the instructions for this question in the exam directed students to leave [quant,_1,line] blank on their bubblesheets.",$lines).'

'.&mt('A non-zero score can be assigned to the student during bubblesheet grading by selecting a bubble in at least one line.').'
'.&mt('The score for this question will be a sum of the numeric values for the selected bubbles from each line, where A=1 point, B=2 points etc.').'
'.&mt("To assign a score of zero for this question, mark all lines as 'No bubble'.").'

'); + if (($responsetype_per_response{$responsenum} eq 'essayresponse') || + ($responsetype_per_response{$responsenum} eq 'formularesponse') || + ($responsetype_per_response{$responsenum} eq 'stringresponse') || + ($responsetype_per_response{$responsenum} eq 'imageresponse') || + ($responsetype_per_response{$responsenum} eq 'reactionresponse') || + ($responsetype_per_response{$responsenum} eq 'organicresponse')) { + $r->print( + &mt("Although this particular question type requires handgrading, the instructions for this question in the bubblesheet exam directed students to leave [quant,_1,line] blank on their bubblesheets.",$lines) + .'

' + .&mt('A non-zero score can be assigned to the student during bubblesheet grading by selecting a bubble in at least one line.') + .'
' + .&mt('The score for this question will be a sum of the numeric values for the selected bubbles from each line, where A=1 point, B=2 points etc.') + .'
' + .&mt("To assign a score of zero for this question, mark all lines as 'No bubble'.") + .'

' + ); } else { $r->print(&mt("Select at most one bubble in a single line and select 'No Bubble' in all the other lines. ")."
"); } } for (my $i =0; $i < $lines; $i++) { my $selected = $$scan_record{"scantron.$current_line.answer"}; - &scantron_bubble_selector($r,$scan_config,$current_line, + &scantron_bubble_selector($r,$scan_config,$current_line, $questionnum,$error,split('', $selected)); push(@linenums,$current_line); $current_line++; @@ -7661,11 +7927,42 @@ sub scantron_validate_doublebubble { #get student info my $classlist=&Apache::loncoursedata::get_classlist(); my %idmap=&username_to_idmap($classlist); + my (undef,undef,$sequence)= + &Apache::lonnet::decode_symb($env{'form.selectpage'}); #get scantron line setup my %scantron_config=&get_scantron_config($env{'form.scantron_format'}); my ($scanlines,$scan_data)=&scantron_getfile(); + + my $navmap = Apache::lonnavmaps::navmap->new(); + unless (ref($navmap)) { + $r->print(&navmap_errormsg()); + return(1,$currentphase); + } + my $map=$navmap->getResourceByUrl($sequence); + my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0); + my ($randomorder,$randompick,@master_seq,%symb_to_resource,%grader_partids_by_symb, + %grader_randomlists_by_symb,%orderedforcode,%respnumlookup,%startline); + my $bubbles_per_row = &bubblesheet_bubbles_per_row(\%scantron_config); + my $nav_error; + if (ref($map)) { + $randomorder = $map->randomorder(); + $randompick = $map->randompick(); + if ($randomorder || $randompick) { + $nav_error = &get_master_seq(\@resources,\@master_seq,\%symb_to_resource); + if ($nav_error) { + $r->print(&navmap_errormsg()); + return(1,$currentphase); + } + &graders_resources_pass(\@resources,\%grader_partids_by_symb, + \%grader_randomlists_by_symb,$bubbles_per_row); + } + } else { + $r->print(&navmap_errormsg()); + return(1,$currentphase); + } + &scantron_get_maxbubble(\$nav_error,\%scantron_config); # parse needs the bubble line array. if ($nav_error) { $r->print(&navmap_errormsg()); @@ -7676,11 +7973,15 @@ sub scantron_validate_doublebubble { my $line=&scantron_get_line($scanlines,$scan_data,$i); if ($line=~/^[\s\cz]*$/) { next; } my $scan_record=&scantron_parse_scanline($line,$i,\%scantron_config, - $scan_data); + $scan_data,undef,\%idmap,$randomorder, + $randompick,$sequence,\@master_seq, + \%symb_to_resource,\%grader_partids_by_symb, + \%orderedforcode,\%respnumlookup,\%startline); if (!defined($$scan_record{'scantron.doubleerror'})) { next; } &scantron_get_correction($r,$i,$scan_record,\%scantron_config,$line, 'doublebubble', - $$scan_record{'scantron.doubleerror'}); + $$scan_record{'scantron.doubleerror'}, + $randomorder,$randompick,\%respnumlookup,\%startline); return (1,$currentphase); } return (0,$currentphase+1); @@ -7719,10 +8020,12 @@ sub scantron_get_maxbubble { %first_bubble_line = (); %subdivided_bubble_lines = (); %responsetype_per_response = (); + %masterseq_id_responsenum = (); my $response_number = 0; my $bubble_line = 0; foreach my $resource (@resources) { + my $resid = $resource->id(); my ($analysis,$parts) = &scantron_partids_tograde($resource,$cid,$uname, $udom,undef,$bubbles_per_row); if ((ref($analysis) eq 'HASH') && (ref($parts) eq 'ARRAY')) { @@ -7773,6 +8076,7 @@ sub scantron_get_maxbubble { $bubble_lines_per_response{$response_number} = $lines; $responsetype_per_response{$response_number} = $analysis->{$part_id.'.type'}; + $masterseq_id_responsenum{$resid.'_'.$part_id} = $response_number; $response_number++; $bubble_line += $lines; @@ -7805,21 +8109,59 @@ sub scantron_validate_missingbubbles { #get student info my $classlist=&Apache::loncoursedata::get_classlist(); my %idmap=&username_to_idmap($classlist); + my (undef,undef,$sequence)= + &Apache::lonnet::decode_symb($env{'form.selectpage'}); #get scantron line setup my %scantron_config=&get_scantron_config($env{'form.scantron_format'}); my ($scanlines,$scan_data)=&scantron_getfile(); + + my $navmap = Apache::lonnavmaps::navmap->new(); + unless (ref($navmap)) { + $r->print(&navmap_errormsg()); + return(1,$currentphase); + } + + my $map=$navmap->getResourceByUrl($sequence); + my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0); + my ($randomorder,$randompick,@master_seq,%symb_to_resource,%grader_partids_by_symb, + %grader_randomlists_by_symb,%orderedforcode,%respnumlookup,%startline); + my $bubbles_per_row = &bubblesheet_bubbles_per_row(\%scantron_config); + my $nav_error; + if (ref($map)) { + $randomorder = $map->randomorder(); + $randompick = $map->randompick(); + if ($randomorder || $randompick) { + $nav_error = &get_master_seq(\@resources,\@master_seq,\%symb_to_resource); + if ($nav_error) { + $r->print(&navmap_errormsg()); + return(1,$currentphase); + } + &graders_resources_pass(\@resources,\%grader_partids_by_symb, + \%grader_randomlists_by_symb,$bubbles_per_row); + } + } else { + $r->print(&navmap_errormsg()); + return(1,$currentphase); + } + + my $max_bubble=&scantron_get_maxbubble(\$nav_error,\%scantron_config); if ($nav_error) { + $r->print(&navmap_errormsg()); return(1,$currentphase); } + if (!$max_bubble) { $max_bubble=2**31; } for (my $i=0;$i<=$scanlines->{'count'};$i++) { my $line=&scantron_get_line($scanlines,$scan_data,$i); if ($line=~/^[\s\cz]*$/) { next; } - my $scan_record=&scantron_parse_scanline($line,$i,\%scantron_config, - $scan_data); + my $scan_record = + &scantron_parse_scanline($line,$i,\%scantron_config,$scan_data,undef,\%idmap, + $randomorder,$randompick,$sequence,\@master_seq, + \%symb_to_resource,\%grader_partids_by_symb, + \%orderedforcode,\%respnumlookup,\%startline); if (!defined($$scan_record{'scantron.missingerror'})) { next; } my @to_correct; @@ -7830,9 +8172,16 @@ sub scantron_validate_missingbubbles { if ($missing =~ /^(\d+)\.(\d+)$/) { my $question = $1; my $subquestion = $2; - if (!defined($first_bubble_line{$question -1})) { next; } - my $first = $first_bubble_line{$question-1}; - my @subans = split(/,/,$subdivided_bubble_lines{$question-1}); + my ($first,$responsenum); + if ($randomorder || $randompick) { + $responsenum = $respnumlookup{$question-1}; + $first = $startline{$question-1}; + } else { + $responsenum = $question-1; + $first = $first_bubble_line{$responsenum}; + } + if (!defined($first)) { next; } + my @subans = split(/,/,$subdivided_bubble_lines{$responsenum}); my $subcount = 1; while ($subcount<$subquestion) { $first += $subans[$subcount-1]; @@ -7841,15 +8190,25 @@ sub scantron_validate_missingbubbles { my $count = $subans[$subquestion-1]; $lastbubble = $first + $count; } else { - if (!defined($first_bubble_line{$missing - 1})) { next; } - $lastbubble = $first_bubble_line{$missing - 1} + $bubble_lines_per_response{$missing - 1}; + my ($first,$responsenum); + if ($randomorder || $randompick) { + $responsenum = $respnumlookup{$missing-1}; + $first = $startline{$missing-1}; + } else { + $responsenum = $missing-1; + $first = $first_bubble_line{$responsenum}; + } + if (!defined($first)) { next; } + $lastbubble = $first + $bubble_lines_per_response{$responsenum}; } if ($lastbubble > $max_bubble) { next; } push(@to_correct,$missing); } if (@to_correct) { &scantron_get_correction($r,$i,$scan_record,\%scantron_config, - $line,'missingbubble',\@to_correct); + $line,'missingbubble',\@to_correct, + $randomorder,$randompick,\%respnumlookup, + \%startline); return (1,$currentphase); } @@ -7888,7 +8247,7 @@ sub hand_bubble_option { return &mt('The sequence to be graded contains response types which are handgraded.').'

'. &mt('If you have already graded these by bubbling sheets to indicate points awarded, [_1]what point value is assigned to a filled last bubble in each row?','
'). ' '.&mt('or').' '. - '

'; + '

'; } return; } @@ -7903,8 +8262,7 @@ sub scantron_process_students { my $default_form_data=&defaultFormData($symb); my %scantron_config=&get_scantron_config($env{'form.scantron_format'}); - my $bubbles_per_row = - &bubblesheet_bubbles_per_row(\%scantron_config); + my $bubbles_per_row = &bubblesheet_bubbles_per_row(\%scantron_config); my ($scanlines,$scan_data)=&scantron_getfile(); my $classlist=&Apache::loncoursedata::get_classlist(); my %idmap=&username_to_idmap($classlist); @@ -7912,43 +8270,28 @@ sub scantron_process_students { unless (ref($navmap)) { $r->print(&navmap_errormsg()); return ''; - } + } my $map=$navmap->getResourceByUrl($sequence); - my $randomorder; + my ($randomorder,$randompick,@master_seq,%symb_to_resource,%grader_partids_by_symb, + %grader_randomlists_by_symb); if (ref($map)) { $randomorder = $map->randomorder(); + $randompick = $map->randompick(); + } else { + $r->print(&navmap_errormsg()); + return ''; } + my $nav_error; my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0); - my (%grader_partids_by_symb,%grader_randomlists_by_symb,%ordered); - &graders_resources_pass(\@resources,\%grader_partids_by_symb, - \%grader_randomlists_by_symb,$bubbles_per_row); - my ($resource_error,%symb_to_resource,@master_seq); - foreach my $resource (@resources) { - my $ressymb; - if (ref($resource)) { - $ressymb = $resource->symb(); - push(@master_seq,$ressymb); - $symb_to_resource{$ressymb} = $resource; - } else { - $resource_error = 1; - last; - } - my ($analysis,$parts) = - &scantron_partids_tograde($resource,$env{'request.course.id'}, - $env{'user.name'},$env{'user.domain'}, - 1,$bubbles_per_row); - $grader_partids_by_symb{$ressymb} = $parts; - if (ref($analysis) eq 'HASH') { - if (ref($analysis->{'parts_withrandomlist'}) eq 'ARRAY') { - $grader_randomlists_by_symb{$ressymb} = - $analysis->{'parts_withrandomlist'}; - } + if ($randomorder || $randompick) { + $nav_error = &get_master_seq(\@resources,\@master_seq,\%symb_to_resource); + if ($nav_error) { + $r->print(&navmap_errormsg()); + return ''; } } - if ($resource_error) { - $r->print(&navmap_errormsg()); - return ''; - } + &graders_resources_pass(\@resources,\%grader_partids_by_symb, + \%grader_randomlists_by_symb,$bubbles_per_row); my ($uname,$udom); my $result= <print(&navmap_errormsg()); @@ -7989,6 +8331,7 @@ SCANTRONFORM my %lettdig = &letter_to_digits(); my $numletts = scalar(keys(%lettdig)); + my %orderedforcode; while ($i<$scanlines->{'count'}) { ($uname,$udom)=('',''); @@ -7999,8 +8342,15 @@ SCANTRONFORM &Apache::lonhtmlcommon::Increment_PrgWin($r,\%prog_state,'last student'); } $started=1; + my %respnumlookup = (); + my %startline = (); + my $total; my $scan_record=&scantron_parse_scanline($line,$i,\%scantron_config, - $scan_data); + $scan_data,undef,\%idmap,$randomorder, + $randompick,$sequence,\@master_seq, + \%symb_to_resource,\%grader_partids_by_symb, + \%orderedforcode,\%respnumlookup,\%startline, + \$total); unless ($uname=&scantron_find_student($scan_record,$scan_data, \%idmap,$i)) { &scantron_add_delay(\@delayqueue,$line, @@ -8025,10 +8375,10 @@ SCANTRONFORM } my @mapresources = @resources; - if ($randomorder) { + if ($randomorder || $randompick) { @mapresources = - &users_order($user,$scancode,$sequence,\@master_seq,\%ordered, - \%symb_to_resource); + &users_order($user,$scancode,$sequence,\@master_seq,\%symb_to_resource, + \%orderedforcode); } my (%partids_by_symb,$res_error); foreach my $resource (@mapresources) { @@ -8065,7 +8415,9 @@ SCANTRONFORM if (&grade_student_bubbles($r,$uname,$udom,$scan_record,$scancode, \@mapresources,\%partids_by_symb, - $bubbles_per_row) eq 'ssi_error') { + $bubbles_per_row,$randomorder,$randompick, + \%respnumlookup,\%startline) + eq 'ssi_error') { $ssi_error = 0; # So end of handler error message does not trigger. $r->print(""); &ssi_print_error($r); @@ -8073,9 +8425,22 @@ SCANTRONFORM return ''; # Why return ''? Beats me. } + if (($scancode) && ($randomorder || $randompick)) { + my $parmresult = + &Apache::lonparmset::storeparm_by_symb($symb, + '0_examcode',2,$scancode, + 'string_examcode',$uname, + $udom); + } $completedstudents{$uname}={'line'=>$line}; if ($env{'form.verifyrecord'}) { my $lastpos = $env{'form.scantron_maxbubble'}*$scantron_config{'Qlength'}; + if ($randompick) { + if ($total) { + $lastpos = $total*$scantron_config{'Qlength'}; + } + } + my $studentdata = substr($line,$scantron_config{'Qstart'}-1,$lastpos); chomp($studentdata); $studentdata =~ s/\r$//; @@ -8086,14 +8451,17 @@ SCANTRONFORM ($counter,my $recording) = &verify_scantron_grading($resource,$udom,$uname,$env{'request.course.id'}, $counter,$studentdata,$partids_by_symb{$ressymb}, - \%scantron_config,\%lettdig,$numletts); + \%scantron_config,\%lettdig,$numletts,$randomorder, + $randompick,\%respnumlookup,\%startline); $studentrecord .= $recording; } if ($studentrecord ne $studentdata) { &Apache::lonxml::clear_problem_counter(); if (&grade_student_bubbles($r,$uname,$udom,$scan_record,$scancode, \@mapresources,\%partids_by_symb, - $bubbles_per_row) eq 'ssi_error') { + $bubbles_per_row,$randomorder,$randompick, + \%respnumlookup,\%startline) + eq 'ssi_error') { $ssi_error = 0; # So end of handler error message does not trigger. $r->print(""); &ssi_print_error($r); @@ -8108,7 +8476,9 @@ SCANTRONFORM ($counter,my $recording) = &verify_scantron_grading($resource,$udom,$uname,$env{'request.course.id'}, $counter,$studentdata,$partids_by_symb{$ressymb}, - \%scantron_config,\%lettdig,$numletts); + \%scantron_config,\%lettdig,$numletts, + $randomorder,$randompick,\%respnumlookup, + \%startline); $studentrecord .= $recording; } if ($studentrecord ne $studentdata) { @@ -8126,11 +8496,11 @@ SCANTRONFORM &Apache::loncommon::end_data_table_header_row()."\n". &Apache::loncommon::start_data_table_row(). '
'. - ''. + ''. &Apache::loncommon::end_data_table_row(). &Apache::loncommon::start_data_table_row(). ''. - ''."\n". + ''."\n". &Apache::loncommon::end_data_table_row(). &Apache::loncommon::end_data_table().'

'); } else { @@ -8186,47 +8556,59 @@ sub graders_resources_pass { if this is a CODEd exam, or based on student's identity if this is a "NAMEd" exam. - Should be used when randomorder applied when the corresponding exam was - printed, prior to students completing bubblesheets for the version of the - exam the student received. + Should be used when randomorder and/or randompick applied when the + corresponding exam was printed, prior to students completing bubblesheets + for the version of the exam the student received. =cut sub users_order { - my ($user,$scancode,$mapurl,$master_seq,$ordered,$symb_to_resource) = @_; + my ($user,$scancode,$mapurl,$master_seq,$symb_to_resource,$orderedforcode) = @_; my @mapresources; - unless ((ref($ordered) eq 'HASH') && (ref($symb_to_resource) eq 'HASH')) { + unless ((ref($master_seq) eq 'ARRAY') && (ref($symb_to_resource) eq 'HASH')) { return @mapresources; - } - if (($scancode) && (ref($ordered->{$scancode}) eq 'ARRAY')) { - @mapresources = @{$ordered->{$scancode}}; - } elsif ($scancode) { - $env{'form.CODE'} = $scancode; - my $actual_seq = - &Apache::lonprintout::master_seq_to_person_seq($mapurl, - $master_seq, - $user,$scancode); - if (ref($actual_seq) eq 'ARRAY') { - @{$ordered->{$scancode}} = - map { $symb_to_resource->{$_}; } @{$actual_seq}; - @mapresources = @{$ordered->{$scancode}}; + } + if ($scancode) { + if ((ref($orderedforcode) eq 'HASH') && (ref($orderedforcode->{$scancode}) eq 'ARRAY')) { + @mapresources = @{$orderedforcode->{$scancode}}; + } else { + $env{'form.CODE'} = $scancode; + my $actual_seq = + &Apache::lonprintout::master_seq_to_person_seq($mapurl, + $master_seq, + $user,$scancode,1); + if (ref($actual_seq) eq 'ARRAY') { + @mapresources = map { $symb_to_resource->{$_}; } @{$actual_seq}; + if (ref($orderedforcode) eq 'HASH') { + if (@mapresources > 0) { + $orderedforcode->{$scancode} = \@mapresources; + } + } + } + delete($env{'form.CODE'}); } - delete($env{'form.CODE'}); } else { my $actual_seq = &Apache::lonprintout::master_seq_to_person_seq($mapurl, $master_seq, - $user); + $user,undef,1); if (ref($actual_seq) eq 'ARRAY') { @mapresources = map { $symb_to_resource->{$_}; } @{$actual_seq}; } - } - return @mapresources; + } + return @mapresources; } sub grade_student_bubbles { - my ($r,$uname,$udom,$scan_record,$scancode,$resources,$parts,$bubbles_per_row) = @_; + my ($r,$uname,$udom,$scan_record,$scancode,$resources,$parts,$bubbles_per_row, + $randomorder,$randompick,$respnumlookup,$startline) = @_; + my $uselookup = 0; + if (($randomorder || $randompick) && (ref($respnumlookup) eq 'HASH') && + (ref($startline) eq 'HASH')) { + $uselookup = 1; + } + if (ref($resources) eq 'ARRAY') { my $count = 0; foreach my $resource (@{$resources}) { @@ -8248,8 +8630,12 @@ sub grade_student_bubbles { if (ref($parts) eq 'HASH') { if (ref($parts->{$ressymb}) eq 'ARRAY') { foreach my $part (@{$parts->{$ressymb}}) { - $form{'scantron_questnum_start.'.$part} = - 1+$env{'form.scantron.first_bubble_line.'.$count}; + if ($uselookup) { + $form{'scantron_questnum_start.'.$part} = $startline->{$count} + 1; + } else { + $form{'scantron_questnum_start.'.$part} = + 1+$env{'form.scantron.first_bubble_line.'.$count}; + } $count++; } } @@ -8349,23 +8735,30 @@ sub scantron_upload_scantron_data_save { } my %coursedata=&Apache::lonnet::coursedescription($env{'form.domainid'}.'_'.$env{'form.courseid'}); my $uploadedfile; - $r->print('

'.&mt("Uploading file to [_1]",$coursedata{'description'}).'

'); + $r->print('

'.&mt('Uploading file to [_1]','"'.$coursedata{'description'}.'"').'

'); if (length($env{'form.upfile'}) < 2) { - $r->print(&mt('[_1]Error:[_2] The file you attempted to upload, [_3] contained no information. Please check that you entered the correct filename.','','',''.&HTML::Entities::encode($env{'form.upfile.filename'},'<>&"').'')); + $r->print( + &Apache::lonhtmlcommon::confirm_success( + &mt('The file: [_1] you attempted to upload contained no information. Please check that you entered the correct filename.', + ''.&HTML::Entities::encode($env{'form.upfile.filename'},'<>&"').''),1)); } else { my $result = &Apache::lonnet::userfileupload('upfile','','scantron','','','', $env{'form.courseid'},$env{'form.domainid'}); - if ($result =~ m{^/uploaded/}) { - $r->print(&mt('[_1]Success:[_2] Successfully uploaded [_3] bytes of data into location: [_4]', - '','',(length($env{'form.upfile'})-1), - ''.$result.'')); + if ($result =~ m{^/uploaded/}) { + $r->print( + &Apache::lonhtmlcommon::confirm_success(&mt('Upload successful')).'
'. + &mt('Uploaded [_1] bytes of data into location: [_2]', + (length($env{'form.upfile'})-1), + ''.$result.'')); ($uploadedfile) = ($result =~ m{/([^/]+)$}); $r->print(&validate_uploaded_scantron_file($env{'form.domainid'}, $env{'form.courseid'},$uploadedfile)); - } else { - $r->print(&mt('[_1]Error:[_2] An error ([_3]) occurred when attempting to upload the file, [_4]', - '','',$result, + } else { + $r->print( + &Apache::lonhtmlcommon::confirm_success(&mt('Upload failed'),1).'
'. + &mt('An error ([_1]) occurred when attempting to upload the file: [_2]', + $result, ''.&HTML::Entities::encode($env{'form.upfile.filename'},'<>&"').'')); } } @@ -8387,7 +8780,7 @@ sub validate_uploaded_scantron_file { my $output; if (@lines) { my (%counts,$max_match_format); - my ($max_match_count,$max_match_pct) = (0,0); + my ($found_match_count,$max_match_count,$max_match_pct) = (0,0,0); my $classlist = &Apache::loncoursedata::get_classlist($cdom,$cname); my %idmap = &username_to_idmap($classlist); foreach my $key (keys(%idmap)) { @@ -8430,6 +8823,7 @@ sub validate_uploaded_scantron_file { if (($max_match_format eq '') || ($percent_match > $max_match_pct)) { $max_match_pct = $percent_match; $max_match_format = $key; + $found_match_count = $counts{$key}{'found'}; $max_match_count = $counts{$key}{'total'}; } } @@ -8448,17 +8842,40 @@ sub validate_uploaded_scantron_file { } } my $showpct = sprintf("%.0f",$max_match_pct).'%'; - $output .= '
'.&mt('Comparison of student IDs in the uploaded file with the course roster found matches for [_1] of the [_2] entries in the file (for the format defined for [_3]).',''.$showpct.'',''.$max_match_count.'',$format_descs). - '
'.&mt('A low percentage of matches results from one of the following:').'
    '. - '
  • '.&mt('The file was uploaded to the wrong course').'
  • '. - '
  • '.&mt('The data are not in the format expected for the domain: [_1]', - ''.$cdom.'').'
  • '. - '
  • '.&mt('Students did not bubble their IDs, or mis-bubbled them').'
  • '. - '
  • '.&mt('The course roster is not up to date').'
  • '. - '
'; + $output .= '
'; + if ($found_match_count == $max_match_count) { + # 100% matching entries + $output .= &Apache::lonhtmlcommon::confirm_success( + &mt('Comparison of student IDs: [_1] matching ([quant,_2,entry,entries])', + ''.$showpct.'',$found_match_count)).'
'. + &mt('Comparison of student IDs in the uploaded file with'. + ' the course roster found matches for [_1] of the [_2] entries'. + ' in the file (for the format defined for [_3]).', + ''.$showpct.'',''.$max_match_count.'',$format_descs); + } else { + # Not all entries matching? -> Show warning and additional info + $output .= + &Apache::lonhtmlcommon::confirm_success( + &mt('Comparison of student IDs: [_1] matching ([_2]/[quant,_3,entry,entries])', + ''.$showpct.'',$found_match_count,$max_match_count).'
'. + &mt('Not all entries could be matched!'),1).'
'. + &mt('Comparison of student IDs in the uploaded file with'. + ' the course roster found matches for [_1] of the [_2] entries'. + ' in the file (for the format defined for [_3]).', + ''.$showpct.'',''.$max_match_count.'',$format_descs). + '

'. + &mt('A low percentage of matches results from one of the following:'). + '

    '. + '
  • '.&mt('The file was uploaded to the wrong course.').'
  • '. + '
  • '.&mt('The data is not in the format expected for the domain: [_1]', + ''.$cdom.'').'
  • '. + '
  • '.&mt('Students did not bubble their IDs, or mis-bubbled them').'
  • '. + '
  • '.&mt('The course roster is not up to date.').'
  • '. + '
'; + } } } else { - $output = ''.&mt('Uploaded file contained no data').''; + $output = '

'.&mt('Uploaded file contained no data').'

'; } return $output; } @@ -8480,7 +8897,7 @@ sub scantron_download_scantron_data { if (! &valid_file($file)) { $r->print('

- '.&mt('The requested file name was invalid.').' + '.&mt('The requested filename was invalid.').'

'); return; @@ -8493,7 +8910,7 @@ sub scantron_download_scantron_data { &Apache::lonnet::allowuploaded('/adm/grades',$skipped); $r->print('

- '.&mt('[_1]Original[_2] file as uploaded by the scantron office.', + '.&mt('[_1]Original[_2] file as uploaded by the bubblesheet scanning office.', '','').'

@@ -8530,19 +8947,18 @@ sub checkscantron_results { return ''; } my $map=$navmap->getResourceByUrl($sequence); - my ($randomorder,@master_seq,%symb_to_resource); + my ($randomorder,$randompick,@master_seq,%symb_to_resource,%grader_partids_by_symb, + %grader_randomlists_by_symb,%orderedforcode); if (ref($map)) { $randomorder=$map->randomorder(); + $randompick=$map->randompick(); } my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0); - foreach my $resource (@resources) { - if (ref($resource)) { - my $ressymb = $resource->symb(); - push(@master_seq,$ressymb); - $symb_to_resource{$ressymb} = $resource; - } + my $nav_error = &get_master_seq(\@resources,\@master_seq,\%symb_to_resource); + if ($nav_error) { + $r->print(&navmap_errormsg()); + return ''; } - my (%grader_partids_by_symb,%grader_randomlists_by_symb); &graders_resources_pass(\@resources,\%grader_partids_by_symb, \%grader_randomlists_by_symb,$bubbles_per_row); my ($uname,$udom); @@ -8553,10 +8969,9 @@ sub checkscantron_results { my @delayqueue; my %completedstudents; - my $count=&Apache::grades::get_todo_count($scanlines,$scan_data); + my $count=&get_todo_count($scanlines,$scan_data); my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin($r,$count); - my ($username,$domain,$started,%ordered); - my $nav_error; + my ($username,$domain,$started); &scantron_get_maxbubble(\$nav_error,\%scantron_config); # Need the bubble lines array to parse. if ($nav_error) { $r->print(&navmap_errormsg()); @@ -8579,8 +8994,8 @@ sub checkscantron_results { my $scan_record= &Apache::grades::scantron_parse_scanline($line,$i,\%scantron_config, $scan_data); - unless ($uname=&Apache::grades::scantron_find_student($scan_record,$scan_data, - \%idmap,$i)) { + unless ($uname=&scantron_find_student($scan_record,$scan_data, + \%idmap,$i)) { &Apache::grades::scantron_add_delay(\@delayqueue,$line, 'Unable to find a student that matches',1); next; @@ -8593,10 +9008,6 @@ sub checkscantron_results { my $pid = $scan_record->{'scantron.ID'}; $lastname{$pid} = $scan_record->{'scantron.LastName'}; push(@{$bylast{$lastname{$pid}}},$pid); - my $lastpos = $env{'form.scantron_maxbubble'}*$scantron_config{'Qlength'}; - $scandata{$pid} = substr($line,$scantron_config{'Qstart'}-1,$lastpos); - chomp($scandata{$pid}); - $scandata{$pid} =~ s/\r$//; my $usec = $classlist->{$uname}->[&Apache::loncoursedata::CL_SECTION]; my $user = $uname.':'.$usec; ($username,$domain)=split(/:/,$uname); @@ -8610,11 +9021,25 @@ sub checkscantron_results { } my @mapresources = @resources; - if ($randomorder) { + my $lastpos = $env{'form.scantron_maxbubble'}*$scantron_config{'Qlength'}; + my %respnumlookup=(); + my %startline=(); + if ($randomorder || $randompick) { @mapresources = - &users_order($user,$scancode,$sequence,\@master_seq,\%ordered, - \%symb_to_resource); + &users_order($user,$scancode,$sequence,\@master_seq,\%symb_to_resource, + \%orderedforcode); + my $total = &get_respnum_lookups($sequence,$scan_data,\%idmap,$line, + $scan_record,\@master_seq,\%symb_to_resource, + \%grader_partids_by_symb,\%orderedforcode, + \%respnumlookup,\%startline); + if ($randompick && $total) { + $lastpos = $total*$scantron_config{'Qlength'}; + } } + $scandata{$pid} = substr($line,$scantron_config{'Qstart'}-1,$lastpos); + chomp($scandata{$pid}); + $scandata{$pid} =~ s/\r$//; + my $counter = -1; foreach my $resource (@mapresources) { my $parts; @@ -8631,7 +9056,9 @@ sub checkscantron_results { ($counter,my $recording) = &verify_scantron_grading($resource,$domain,$username,$cid,$counter, $scandata{$pid},$parts, - \%scantron_config,\%lettdig,$numletts); + \%scantron_config,\%lettdig,$numletts, + $randomorder,$randompick, + \%respnumlookup,\%startline); $record{$pid} .= $recording; } } @@ -8654,14 +9081,14 @@ sub checkscantron_results { '

'."\n". ''."\n". ''."\n". -''."\n"; +''."\n"; $passed ++; } else { my $css_class = ($failed % 2)?'LC_odd_row':'LC_even_row'; $badstudents .= ''."\n". ''."\n". ''."\n". -''."\n". +''."\n". ''."\n"; $failed ++; } @@ -8679,7 +9106,7 @@ sub checkscantron_results { .'

' ); $r->print('

' - .&mt('Exact matches for [_1][quant,_2,student][_3].','',$passed,''). + .&mt('Exact matches for [_1][quant,_2,student][_3].','',$passed,'') .'
' .&mt('Discrepancies detected for [_1][quant,_2,student][_3].','',$failed,'') .'

' @@ -8709,7 +9136,8 @@ sub checkscantron_results { sub verify_scantron_grading { my ($resource,$domain,$username,$cid,$counter,$scandata,$partids, - $scantron_config,$lettdig,$numletts) = @_; + $scantron_config,$lettdig,$numletts,$randomorder,$randompick, + $respnumlookup,$startline) = @_; my ($record,%expected,%startpos); return ($counter,$record) if (!ref($resource)); return ($counter,$record) if (!$resource->is_problem()); @@ -8718,15 +9146,21 @@ sub verify_scantron_grading { foreach my $part_id (@{$partids}) { $counter ++; $expected{$part_id} = 0; - if ($env{"form.scantron.sub_bubblelines.$counter"}) { - my @sub_lines = split(/,/,$env{"form.scantron.sub_bubblelines.$counter"}); + my $respnum = $counter; + if ($randomorder || $randompick) { + $respnum = $respnumlookup->{$counter}; + $startpos{$part_id} = $startline->{$counter} + 1; + } else { + $startpos{$part_id} = $env{"form.scantron.first_bubble_line.$counter"}; + } + if ($env{"form.scantron.sub_bubblelines.$respnum"}) { + my @sub_lines = split(/,/,$env{"form.scantron.sub_bubblelines.$respnum"}); foreach my $item (@sub_lines) { $expected{$part_id} += $item; } } else { - $expected{$part_id} = $env{"form.scantron.bubblelines.$counter"}; + $expected{$part_id} = $env{"form.scantron.bubblelines.$respnum"}; } - $startpos{$part_id} = $env{"form.scantron.first_bubble_line.$counter"}; } if ($symb) { my %recorded; @@ -8822,7 +9256,7 @@ sub verify_scantron_grading { return ($counter,$record); } -sub letter_to_digits { +sub letter_to_digits { my %lettdig = ( A => 1, B => 2, @@ -9356,16 +9790,18 @@ sub process_clicker_file { $number++; } $result.="

\n"; - if ($number==0) { - $result.=''.&mt('No IDs found to determine correct answer').''; - return $result; - } + if ($number==0) { + $result .= + &Apache::lonhtmlcommon::confirm_success( + &mt('No IDs found to determine correct answer'),1); + return $result; + } } if (length($env{'form.upfile'}) < 2) { - $result.=&mt('[_1] Error: [_2] The file you attempted to upload, [_3] contained no information. Please check that you entered the correct filename.', - '', - '', - ''.&HTML::Entities::encode($env{'form.upfile.filename'},'<>&"').''); + $result .= + &Apache::lonhtmlcommon::confirm_success( + &mt('The file: [_1] you attempted to upload contained no information. Please check that you entered the correct filename.', + ''.&HTML::Entities::encode($env{'form.upfile.filename'},'<>&"').''),1); return $result; } @@ -9460,7 +9896,7 @@ ENDHEADER &mt('Found [_1] registered and [_2] unregistered clickers.',$student_count,$unknown_count); if (($env{'form.gradingmechanism'} ne 'attendance') && ($env{'form.gradingmechanism'} ne 'given')) { if ($correct_count==0) { - $errormsg.="Found no correct answers answers for grading!"; + $errormsg.="Found no correct answers for grading!"; } elsif ($correct_count>1) { $result.='
'.&mt("Found [_1] entries for grading!",$correct_count).''; } @@ -9632,7 +10068,7 @@ sub assign_clicker_grades { if ($user) { if ($users{$user}) { $result.='
'. - &mt("More than one entry found for [_1]!",$user). + &mt('More than one entry found for [_1]!',''.$user.''). '
'; } $users{$user}=1; @@ -9748,6 +10184,7 @@ sub handler { } } elsif (!%perm) { $request->internal_redirect('/adm/quickgrades'); + return OK; } &Apache::loncommon::content_type($request,'text/html'); $request->send_http_header; @@ -9962,7 +10399,7 @@ described at http://www.lon-capa.org. =head1 OVERVIEW Do an ssi with retries: -While I'd love to factor out this with the vesrion in lonprintout, +While I'd love to factor out this with the version in lonprintout, that would either require a data coupling between modules, which I refuse to perpetuate (there's quite enough of that already), or would require the invention of another infrastructure I'm not quite ready to invent (e.g. an ssi_with_retry object). @@ -9973,11 +10410,11 @@ At least the logic that drives this has ssi_with_retries - Does the server side include of a resource. if the ssi call returns an error we'll retry it up to the number of times requested by the caller. - If we still have a proble, no text is appended to the + If we still have a problem, no text is appended to the output and we set some global variables. to indicate to the caller an SSI error occurred. All of this is supposed to deal with the issues described - in LonCAPA BZ 5631 see: + in LON-CAPA BZ 5631 see: http://bugs.lon-capa.org/show_bug.cgi?id=5631 by informing the user that this happened. @@ -10104,6 +10541,17 @@ Side Effects: None. - missingbubble - array ref of the bubble lines that have missing bubble errors + $randomorder - True if exam folder has randomorder set + $randompick - True if exam folder has randompick set + $respnumlookup - Reference to HASH mapping question numbers in bubble lines + for current line to question number used for same question + in "Master Seqence" (as seen by Course Coordinator). + $startline - Reference to hash where key is question number (0 is first) + and value is number of first bubble line for current student + or code-based randompick and/or randomorder. + + + =item scantron_get_maxbubble() : Arguments: @@ -10124,7 +10572,7 @@ Side Effects: None. $env{'form.scantron.bubble_lines.n'}, $env{'form.scantron.first_bubble_line.n'} and $env{"form.scantron.sub_bubblelines.n"} - which are the total number of bubble, lines, the number of bubble + which are the total number of bubble lines, the number of bubble lines for response n and number of the first bubble line for response n, and a comma separated list of numbers of bubble lines for sub-questions (for optionresponse, matchresponse, and rankresponse items), for response n.
'.&mt('Sequence to be Graded:').''.$title.'
'.&mt('Bubblesheet').''.$studentdata.''.$studentdata.''.&mt('Stored submissions').''.$studentrecord.''.$studentrecord.''.&mt('Bubblesheet').''.$showscandata.''.$last.''.$pid.'
Submissions'.$showrecord.'
'.&mt('Submissions').''.$showrecord.'
'.&mt('Bubblesheet').''.$scandata{$pid}.''.$last.''.$pid.'
Submissions'.$record{$pid}.''.&mt('Submissions').''.$record{$pid}.'