--- loncom/homework/grades.pm 2012/12/18 18:05:48 1.596.2.12.2.12 +++ loncom/homework/grades.pm 2015/03/17 12:37:40 1.596.2.12.2.34 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # The LON-CAPA Grading handler # -# $Id: grades.pm,v 1.596.2.12.2.12 2012/12/18 18:05:48 raeburn Exp $ +# $Id: grades.pm,v 1.596.2.12.2.34 2015/03/17 12:37:40 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -391,6 +391,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) { @@ -407,6 +409,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); @@ -461,10 +465,10 @@ 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; @@ -498,12 +502,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 @@ -811,7 +817,7 @@ sub verifyreceipt { '

'. &mt('Verifying Receipt No. [_1]',$receipt). '

'."\n". - '

'.&mt('Resource: [_1]',$env{'form.probTitle'}). + '

'.&mt('[_1]Resource: [_2]','',''.$env{'form.probTitle'}). '

'."\n"; my ($string,$contents,$matches) = ('','',0); @@ -1162,7 +1168,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). '
'; } @@ -1366,10 +1372,8 @@ sub sub_page_js { } } } - } } - } if (val == "Grade Student") { formname.showgrading.value = "yes"; @@ -1519,6 +1523,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(< @@ -1684,25 +1697,34 @@ INNERJS var redsel = ""; var grnsel = ""; var blusel = ""; - if (kwclr=="red") {var redsel="checked"}; - if (kwclr=="green") {var grnsel="checked"}; - if (kwclr=="blue") {var blusel="checked"}; + var txtcol1 = "$lt{'col1'}"; + var txtcol2 = "$lt{'col2'}"; + var txtcol3 = "$lt{'col3'}"; + var txtsiz1 = "$lt{'siz1'}"; + var txtsiz2 = "$lt{'siz2'}"; + var txtsiz3 = "$lt{'siz3'}"; + var txtsty1 = "$lt{'sty1'}"; + var txtsty2 = "$lt{'sty2'}"; + var txtsty3 = "$lt{'sty3'}"; + if (kwclr=="red") {var redsel="checked='checked'"}; + if (kwclr=="green") {var grnsel="checked='checked'"}; + if (kwclr=="blue") {var blusel="checked='checked'"}; var sznsel = ""; var sz1sel = ""; var sz2sel = ""; - if (kwsize=="0") {var sznsel="checked"}; - if (kwsize=="+1") {var sz1sel="checked"}; - if (kwsize=="+2") {var sz2sel="checked"}; + if (kwsize=="0") {var sznsel="checked='checked'"}; + if (kwsize=="+1") {var sz1sel="checked='checked'"}; + if (kwsize=="+2") {var sz2sel="checked='checked'"}; var synsel = ""; var syisel = ""; var sybsel = ""; - if (kwstyle=="") {var synsel="checked"}; - if (kwstyle=="") {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; } @@ -1720,31 +1742,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::end_data_table_row().''.&Apache::loncommon::end_data_table_row(); if ($res_error) { return &navmap_errormsg(); } @@ -2018,9 +2039,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'}). + ''); $request->print(&show_grading_menu_form($symb)); return; } @@ -2043,7 +2067,7 @@ sub submission { &download_all_link($request, $symb); } $request->print('

 '.&mt('Submission Record').'

'."\n". - '

 '.&mt('Resource: [_1]',$env{'form.probTitle'}).'

'."\n"); + '

 '.&mt('[_1]Resource: [_2]','',''.$env{'form.probTitle'}).'

'."\n"); # option to display problem, only once else it cause problems # with the form later since the problem has a form. @@ -2060,7 +2084,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') { @@ -2126,6 +2150,7 @@ sub submission { if ($env{'form.handgrade'} eq 'yes' && $env{'form.showgrading'} eq 'yes') { my %lt = &Apache::lonlocal::texthash( + keyh => 'Keyword Highlighting for Essays', keyw => 'Keyword Options', list => 'List', past => 'Paste Selection to List', @@ -2134,13 +2159,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 # @@ -2165,7 +2195,7 @@ KEYWORDS if ($perm{'vgr'}) { $request->print( &Apache::loncommon::track_student_link( - &mt('View recent activity'), + 'View recent activity', $uname,$udom,'check') .' ' ); @@ -2229,7 +2259,7 @@ KEYWORDS # (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 $lastsubonly; @@ -2252,13 +2282,14 @@ KEYWORDS 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); + $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}; @@ -2274,7 +2305,7 @@ KEYWORDS foreach my $submission (@$string) { my ($partid,$respid) = ($submission =~ /^resource\.([^\.]*)\.([^\.]*)\.submission/); if (join('_',@{$part}) ne ($partid.'_'.$respid)) { next; } - my ($ressub,$hide,$subval) = split(/:/,$submission,3); + my ($ressub,$hide,$draft,$subval) = split(/:/,$submission,4); # Similarity check my $similar=''; my ($type,$trial,$rndseed); @@ -2283,7 +2314,7 @@ KEYWORDS $trial = $record{"resource.$partid.tries"}; $rndseed = $record{"resource.$partid.rndseed"}; } - if($env{'form.checkPlag'}){ + if ($env{'form.checkPlag'}) { my ($oname,$odom,$ocrsid,$oessay,$osim)= &most_similar($uname,$udom,$symb,$subval); if ($osim) { @@ -2311,9 +2342,8 @@ KEYWORDS } my $order=&get_order($partid,$respid,$symb,$uname,$udom, undef,$type,$trial,$rndseed); - if ($env{'form.lastSub'} eq 'lastonly' || - ($env{'form.lastSub'} eq 'hdgrade' && - $$handgrade{$$part[0].'_'.$$part[1]} eq 'yes')) { + 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).''. @@ -2325,20 +2355,36 @@ KEYWORDS 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').'
'; + $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.''; + $lastsubonly.='
'.$file.''; } } $lastsubonly.='
'; } if ($hide eq 'anon') { - $lastsubonly.=''.&mt('Anonymous Survey').''; + $lastsubonly.='
'.&mt('Anonymous Survey').''; } else { - $lastsubonly.=''.&mt('Submitted Answer:').' '. + $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.='
'; @@ -2348,14 +2394,17 @@ KEYWORDS $lastsubonly.=''."\n"; # End: LC_grade_submissions_body } $request->print($lastsubonly); - } elsif ($env{'form.lastSub'} eq 'datesub') { + if ($env{'form.lastSub'} eq 'datesub') { my (undef,$responseType,undef,$parts) = &showResourceInfo($symb); $request->print(&displaySubByDates($symb,\%record,$parts,$responseType,$checkIcon,$uname,$udom)); - } elsif ($env{'form.lastSub'} =~ /^(last|all)$/) { + } + 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"; $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 { @@ -3868,8 +3968,8 @@ sub editgrades { my ($symb)=&get_symb($request); my $section_display = join (", ",&Apache::loncommon::get_env_multiple('form.section')); my $title='

'.&mt('Current Grade Status').'

'; - $title.='

'.&mt('Current Resource: [_1]',$env{'form.probTitle'}).'

'."\n"; - $title.='

'.&mt('Section: [_1]',$section_display).'

'."\n"; + $title.='

'.&mt('Current Resource').': '.$env{'form.probTitle'}.'

'."\n"; + $title.='

'.&mt('Section:').' '.$section_display.'

'."\n"; my $result= &Apache::loncommon::start_data_table(). &Apache::loncommon::start_data_table_header_row(). @@ -4254,11 +4354,12 @@ sub csvupload_fields { sub csvuploadmap_footer { my ($request,$i,$keyfields) =@_; + my $buttontext = &mt('Assign Grades'); $request->print(< -
+
ENDPICK } @@ -4402,7 +4503,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"); $request->print(&show_grading_menu_form($symb)); return ''; @@ -4736,9 +4837,12 @@ sub displayPage { &Apache::lonnet::clear_EXT_cache_status(); if (!&canview($usec)) { - $request->print(''.&mt('Unable to view requested student. ([_1])',$env{'form.student'}).''); - $request->print(&show_grading_menu_form($symb)); - return; + $request->print(''. + &mt('Unable to view requested student. ([_1])', + $env{'form.student'}). + ''); + $request->print(&show_grading_menu_form($symb)); + return; } my $result='

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

'; $result.='

 '.&mt('Student: [_1]',&nameUserString(undef,$$fullname{$env{'form.student'}},$uname,$udom)). @@ -4852,9 +4956,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)) { @@ -4965,11 +5071,12 @@ sub displaySubByDates { } else { $displaySub[0].=&mt('Trial: [_1]', $$record{"$where.$partid.tries"}); - if ($rndseed || $lastrndseed{$partid}) { + if (($rndseed ne '') && ($lastrndseed{$partid} ne '')) { if ($rndseed ne $lastrndseed{$partid}) { $newvariation = ' ('.&mt('New variation this try').')'; } } + $lastrndseed{$partid} = $rndseed; } my $responseType=($isTask ? 'Task' : $responseType->{$partid}->{$responseId}); @@ -5070,7 +5177,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--; } @@ -5091,6 +5198,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}; @@ -5182,8 +5295,11 @@ sub updateGradeByPage { $studentTable.=&show_grading_menu_form($env{'form.symb'}); 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 ''; } @@ -5218,7 +5334,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 @@ -5308,6 +5424,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. @@ -5321,12 +5442,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; @@ -5336,28 +5462,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 @@ -5687,7 +5797,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". @@ -5838,8 +5948,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; } @@ -6021,6 +6140,27 @@ sub digits_to_letters { (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 @@ -6065,12 +6205,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 || @@ -6106,10 +6246,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++; @@ -6118,11 +6277,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; @@ -6133,10 +6297,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 ++; } @@ -6145,11 +6312,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); } } } @@ -6157,9 +6326,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 @@ -6178,19 +6404,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); } @@ -6238,7 +6468,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 @@ -6280,12 +6511,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)) { @@ -6467,7 +6702,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); @@ -6589,7 +6828,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= '

'; @@ -6603,7 +6842,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(); @@ -1793,7 +1813,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]); @@ -1801,7 +1821,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(); @@ -1838,8 +1858,8 @@ 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('Score Part: [_1]
(weight = [_2])', - $display_part,$weight{$partid}).'
'.&mt('List of CODES to validate against:').''. $env{'form.scantron_CODElist'}.'
@@ -6692,9 +6931,11 @@ SCANTRONFORM ''."\n"; $chunk .= ''."\n"; + $chunk .= + ''."\n"; $result .= $chunk; $line++; - } + } return $result; } @@ -6716,7 +6957,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(); @@ -6767,6 +7008,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'; @@ -7261,7 +7503,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 @@ -7311,7 +7554,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(''); @@ -7397,7 +7640,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(''); @@ -7405,7 +7649,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)); @@ -7419,13 +7665,16 @@ ENDSCRIPT # 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)); @@ -7478,12 +7727,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}) { @@ -7492,8 +7749,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]; @@ -7501,8 +7766,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)); @@ -7524,6 +7797,14 @@ 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. @@ -7548,15 +7829,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]; @@ -7564,25 +7856,34 @@ 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++; @@ -7836,11 +8137,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()); @@ -7851,11 +8183,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); @@ -7894,12 +8230,14 @@ 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,$bubbles_per_row); + $udom,undef,$bubbles_per_row); if ((ref($analysis) eq 'HASH') && (ref($parts) eq 'ARRAY')) { foreach my $part_id (@{$parts}) { my $lines; @@ -7948,6 +8286,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; @@ -7980,21 +8319,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; @@ -8003,28 +8380,45 @@ sub scantron_validate_missingbubbles { foreach my $missing (@{$$scan_record{'scantron.missingerror'}}) { my $lastbubble; 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 $subcount = 1; - while ($subcount<$subquestion) { - $first += $subans[$subcount-1]; - $subcount ++; - } - my $count = $subans[$subquestion-1]; - $lastbubble = $first + $count; + my $question = $1; + my $subquestion = $2; + 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]; + $subcount ++; + } + 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); } @@ -8063,7 +8457,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; } @@ -8079,8 +8473,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); @@ -8088,43 +8481,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()); @@ -8167,6 +8544,7 @@ SCANTRONFORM my %lettdig = &letter_to_digits(); my $numletts = scalar(keys(%lettdig)); + my %orderedforcode; while ($i<$scanlines->{'count'}) { ($uname,$udom)=('',''); @@ -8178,8 +8556,15 @@ SCANTRONFORM '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, @@ -8204,10 +8589,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) { @@ -8244,7 +8629,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); @@ -8253,9 +8640,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$//; @@ -8266,14 +8666,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); @@ -8289,7 +8692,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) { @@ -8307,11 +8712,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 { @@ -8368,47 +8773,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}) { @@ -8430,8 +8847,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++; } } @@ -8536,23 +8957,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.'')); + $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, + $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'},'<>&"').'')); } } @@ -8574,7 +9002,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)) { @@ -8617,6 +9045,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'}; } } @@ -8635,17 +9064,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; } @@ -8668,7 +9120,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.').'

'); $r->print(&show_grading_menu_form($symb)); @@ -8682,7 +9134,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 bubblesheet scanning office.', '','').'

@@ -8722,19 +9174,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); @@ -8745,10 +9196,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()); @@ -8773,8 +9223,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; @@ -8787,10 +9237,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); @@ -8804,11 +9250,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; @@ -8825,7 +9285,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; } } @@ -8848,14 +9310,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 ++; } @@ -8901,7 +9363,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()); @@ -8910,15 +9373,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; @@ -9014,7 +9483,7 @@ sub verify_scantron_grading { return ($counter,$record); } -sub letter_to_digits { +sub letter_to_digits { my %lettdig = ( A => 1, B => 2, @@ -9621,16 +10090,18 @@ sub process_clicker_file { $number++; } $result.="

\n"; - if ($number==0) { - $result.=''.&mt('No IDs found to determine correct answer').''; - return $result.&show_grading_menu_form($symb); - } + if ($number==0) { + $result .= + &Apache::lonhtmlcommon::confirm_success( + &mt('No IDs found to determine correct answer'),1); + return $result,.&show_grading_menu_form($symb); + } } 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.&show_grading_menu_form($symb); } @@ -9722,7 +10193,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).''; } @@ -9897,7 +10368,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; @@ -10032,6 +10503,7 @@ sub handler { &init_perm(); if (!%perm) { $request->internal_redirect('/adm/quickgrades'); + return OK; } else { &Apache::loncommon::content_type($request,'text/html'); $request->send_http_header; @@ -10187,11 +10659,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. @@ -10249,6 +10721,16 @@ ssi_with_retries() - 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: @@ -10269,7 +10751,7 @@ ssi_with_retries() $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}.'