--- loncom/homework/grades.pm 2008/01/09 14:16:52 1.502 +++ loncom/homework/grades.pm 2008/02/01 22:50:43 1.503 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # The LON-CAPA Grading handler # -# $Id: grades.pm,v 1.502 2008/01/09 14:16:52 www Exp $ +# $Id: grades.pm,v 1.503 2008/02/01 22:50:43 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -4681,6 +4681,12 @@ my %bubble_lines_per_response; # no. my %first_bubble_line; # First bubble line no. for each bubble. +my %subdivided_bubble_lines; # no. bubble lines for optionresponse + # or matchresponse where an individual + # response can have multiple lines + +my %responsetype_per_response; # responsetype for each response + # Save and restore the bubble lines array to the form env. @@ -4689,6 +4695,10 @@ sub save_bubble_lines { $env{"form.scantron.bubblelines.$line"} = $bubble_lines_per_response{$line}; $env{"form.scantron.first_bubble_line.$line"} = $first_bubble_line{$line}; + $env{"form.scantron.sub_bubblelines.$line"} = + $subdivided_bubble_lines{$line}; + $env{"form.scantron.responsetype.$line"} = + $responsetype_per_response{$line}; } } @@ -4701,6 +4711,10 @@ sub restore_bubble_lines { $bubble_lines_per_response{$line} = $value; $first_bubble_line{$line} = $env{"form.scantron.first_bubble_line.$line"}; + $subdivided_bubble_lines{$line} = + $env{"form.scantron.sub_bubblelines.$line"}; + $responsetype_per_response{$line} = + $env{"form.scantron.responsetype.$line"}; $line++; } @@ -5139,6 +5153,8 @@ sub username_to_idmap { - 'answer' 'response' - new answer or 'none' if blank 'question' - the bubble line to change + 'questionnum' - the question identifier, + may include subquestion. Returns: $line - the modified scanline @@ -5187,7 +5203,7 @@ sub scantron_fixup_scanline { my $answer=${off}x$length; if ($args->{'response'} eq 'none') { &scan_data($scan_data, - "$whichline.no_bubble.".$args->{'question'},'1'); + "$whichline.no_bubble.".$args->{'questionnum'},'1'); } else { if ($on eq 'letter') { my @alphabet=('A'..'Z'); @@ -5199,7 +5215,7 @@ sub scantron_fixup_scanline { substr($answer,$args->{'response'},1)=$on; } &scan_data($scan_data, - "$whichline.no_bubble.".$args->{'question'},undef,'1'); + "$whichline.no_bubble.".$args->{'questionnum'},undef,'1'); } my $where=$length*($args->{'question'}-1)+$scantron_config->{'Qstart'}; substr($line,$where-1,$length)=$answer; @@ -5371,166 +5387,208 @@ sub scantron_parse_scanline { $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 $answer_length = ($$scantron_config{'Qlength'} * $answers_needed) - || 1; - - $questnum++; - my $currentquest = substr($questions,0,$answer_length); - $questions = substr($questions,$answer_length); - if (length($currentquest) < $answer_length) { next; } - - # Qon letter implies for each slot in currentquest we have: - # ? or * for doubles a letter in A-Z for a bubble and - # about anything else (esp. a value of Qoff for missing - # bubbles. - - - if ($$scantron_config{'Qon'} eq 'letter') { - if ($currentquest =~ /\?/ - || $currentquest =~ /\*/ - || (&occurence_count($currentquest, "[A-Z]") > 1)) { - push(@{$record{'scantron.doubleerror'}},$questnum); - for (my $ans = 0; $ans < $answers_needed; $ans++) { - my $bubble = substr($currentquest, $ans, 1); - if ($bubble =~ /[A-Z]/ ) { - $record{"scantron.$ansnum.answer"} = $bubble; - } else { - $record{"scantron.$ansnum.answer"}=''; - } - $ansnum++; - } - - } elsif (!defined($currentquest) - || (&occurence_count($currentquest, $$scantron_config{'Qoff'}) == length($currentquest)) - || (&occurence_count($currentquest, "[A-Z]") == 0)) { - for (my $ans = 0; $ans < $answers_needed; $ans++ ) { - $record{"scantron.$ansnum.answer"}=''; - $ansnum++; - - } - if (!&scan_data($scan_data,"$whichline.no_bubble.$questnum")) { - push(@{$record{"scantron.missingerror"}},$questnum); - # $ansnum += $answers_needed; - } - } else { - for (my $ans = 0; $ans < $answers_needed; $ans++) { - my $bubble = substr($currentquest, $ans, 1); - $record{"scantron.$ansnum.answer"} = $bubble; - $ansnum++; - } - } - - # Qon 'number' implies each slot gives a digit that indexes the - # the bubbles filled or Qoff or a non number for unbubbled lines. - # and *? for double bubbles on a line. - # these answers are also stored as letters. - - } elsif ($$scantron_config{'Qon'} eq 'number') { - if ($currentquest =~ /\?/ - || $currentquest =~ /\*/ - || (&occurence_count($currentquest, '\d') > 1)) { - push(@{$record{'scantron.doubleerror'}},$questnum); - for (my $ans = 0; $ans < $answers_needed; $ans++) { - my $bubble = substr($currentquest, $ans, 1); - if ($bubble =~ /\d/) { - $record{"scantron.$ansnum.answer"} = $alphabet[$bubble]; - } else { - $record{"scantron.$ansnum.answer"}=' '; - } - $ansnum++; - } - - } elsif (!defined($currentquest) - || (&occurence_count($currentquest,$$scantron_config{'Qoff'}) == length($currentquest)) - || (&occurence_count($currentquest, '\d') == 0)) { - for (my $ans = 0; $ans < $answers_needed; $ans++ ) { - $record{"scantron.$ansnum.answer"}=''; - $ansnum++; - - } - if (!&scan_data($scan_data,"$whichline.no_bubble.$questnum")) { - push(@{$record{"scantron.missingerror"}},$questnum); - $ansnum += $answers_needed; - } - - } else { - $currentquest = &digits_to_letters($currentquest); - for (my $ans =0; $ans < $answers_needed; $ans++) { - $record{"scantron.$ansnum.answer"} = substr($currentquest, $ans, 1); - $ansnum++; - } - } - } else { - - # Otherwise there's a positional notation; - # each bubble line requires Qlength items, and there are filled in - # bubbles for each case where there 'Qon' characters. - # + my $answer_length = ($$scantron_config{'Qlength'} * $answers_needed) + || 1; + $questnum++; + my $quest_id = $questnum; + my $currentquest = substr($questions,0,$answer_length); + $questions = substr($questions,$answer_length); + if (length($currentquest) < $answer_length) { next; } + + if ($subdivided_bubble_lines{$questnum-1} =~ /,/) { + my $subquestnum = 1; + my $subquestions = $currentquest; + my @subanswers_needed = + split(/,/,$subdivided_bubble_lines{$questnum-1}); + foreach my $subans (@subanswers_needed) { + my $subans_length = + ($$scantron_config{'Qlength'} * $subans) || 1; + my $currsubquest = substr($subquestions,0,$subans_length); + $subquestions = substr($subquestions,$subans_length); + $quest_id = "$questnum.$subquestnum"; + if (($$scantron_config{'Qon'} eq 'letter') || + ($$scantron_config{'Qon'} eq 'number')) { + $ansnum = &scantron_validator_lettnum($ansnum, + $questnum,$quest_id,$subans,$currsubquest,$whichline, + \@alphabet,\%record,$scantron_config,$scan_data); + } else { + $ansnum = &scantron_validator_positional($ansnum, + $questnum,$quest_id,$subans,$currsubquest,$whichline, \@alphabet,\%record,$scantron_config,$scan_data); + } + $subquestnum ++; + } + } else { + if (($$scantron_config{'Qon'} eq 'letter') || + ($$scantron_config{'Qon'} eq 'number')) { + $ansnum = &scantron_validator_lettnum($ansnum,$questnum, + $quest_id,$answers_needed,$currentquest,$whichline, + \@alphabet,\%record,$scantron_config,$scan_data); + } else { + $ansnum = &scantron_validator_positional($ansnum,$questnum, + $quest_id,$answers_needed,$currentquest,$whichline, + \@alphabet,\%record,$scantron_config,$scan_data); + } + } + } + $record{'scantron.maxquest'}=$questnum; + return \%record; +} - my @array=split($$scantron_config{'Qon'},$currentquest,-1); +sub scantron_validator_lettnum { + my ($ansnum,$questnum,$quest_id,$answers_needed,$currquest,$whichline, + $alphabet,$record,$scantron_config,$scan_data) = @_; + + # Qon 'letter' implies for each slot in currquest we have: + # ? or * for doubles, a letter in A-Z for a bubble, and + # about anything else (esp. a value of Qoff) for missing + # bubbles. + # + # Qon 'number' implies each slot gives a digit that indexes the + # bubbles filled, or Qoff, or a non-number for unbubbled lines, + # and * or ? for double bubbles on a single line. + # - # If the split only giveas us one element.. the full length of the - # answser string, no bubbles are filled in: + my $matchon; + if ($$scantron_config{'Qon'} eq 'letter') { + $matchon = '[A-Z]'; + } elsif ($$scantron_config{'Qon'} eq 'number') { + $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')) { + my @singlelines = split('',$currquest); + foreach my $entry (@singlelines) { + $occurrences = &occurence_count($entry,$matchon); + if ($occurrences > 1) { + last; + } + } + } else { + $occurrences = &occurence_count($currquest,$matchon); + } + if (($currquest =~ /\?/ || $currquest =~ /\*/) || ($occurrences > 1)) { + push(@{$record->{'scantron.doubleerror'}},$quest_id); + for (my $ans=0; $ans<$answers_needed; $ans++) { + my $bubble = substr($currquest,$ans,1); + if ($bubble =~ /$matchon/ ) { + if ($$scantron_config{'Qon'} eq 'number') { + if ($bubble == 0) { + $bubble = 10; + } + $record->{"scantron.$ansnum.answer"} = + $alphabet->[$bubble-1]; + } else { + $record->{"scantron.$ansnum.answer"} = $bubble; + } + } else { + $record->{"scantron.$ansnum.answer"}=''; + } + $ansnum++; + } + } elsif (!defined($currquest) + || (&occurence_count($currquest, $$scantron_config{'Qoff'}) == length($currquest)) + || (&occurence_count($currquest,$matchon) == 0)) { + for (my $ans=0; $ans<$answers_needed; $ans++ ) { + $record->{"scantron.$ansnum.answer"}=''; + $ansnum++; + } + if (!&scan_data($scan_data,"$whichline.no_bubble.$quest_id")) { + push(@{$record->{'scantron.missingerror'}},$quest_id); + } + } else { + if ($$scantron_config{'Qon'} eq 'number') { + $currquest = &digits_to_letters($currquest); + } + for (my $ans=0; $ans<$answers_needed; $ans++) { + my $bubble = substr($currquest,$ans,1); + $record->{"scantron.$ansnum.answer"} = $bubble; + $ansnum++; + } + } + return $ansnum; +} - if (length($array[0]) eq $$scantron_config{'Qlength'}*$answers_needed) { - for (my $ans = 0; $ans < $answers_needed; $ans++ ) { - $record{"scantron.$ansnum.answer"}=''; - $ansnum++; +sub scantron_validator_positional { + my ($ansnum,$questnum,$quest_id,$answers_needed,$currquest, + $whichline,$alphabet,$record,$scantron_config,$scan_data) = @_; - } - if (!&scan_data($scan_data,"$whichline.no_bubble.$questnum")) { - push(@{$record{"scantron.missingerror"}},$questnum); - } - + # Otherwise there's a positional notation; + # each bubble line requires Qlength items, and there are filled in + # bubbles for each case where there 'Qon' characters. + # + my @array=split($$scantron_config{'Qon'},$currquest,-1); - } elsif (scalar(@array) eq 2) { + # If the split only gives us one element.. the full length of the + # answer string, no bubbles are filled in: - my $location = length($array[0]); - my $line_num = int($location / $$scantron_config{'Qlength'}); - my $bubble = $alphabet[$location % $$scantron_config{'Qlength'}]; - + if (length($array[0]) eq $$scantron_config{'Qlength'}*$answers_needed) { + for (my $ans=0; $ans<$answers_needed; $ans++ ) { + $record->{"scantron.$ansnum.answer"}=''; + $ansnum++; + } + if (!&scan_data($scan_data,"$whichline.no_bubble.$quest_id")) { + push(@{$record->{"scantron.missingerror"}},$quest_id); + } + } elsif (scalar(@array) == 2) { + my $location = length($array[0]); + my $line_num = int($location / $$scantron_config{'Qlength'}); + my $bubble = $alphabet->[$location % $$scantron_config{'Qlength'}]; + for (my $ans=0; $ans<$answers_needed; $ans++) { + if ($ans eq $line_num) { + $record->{"scantron.$ansnum.answer"} = $bubble; + } else { + $record->{"scantron.$ansnum.answer"} = ' '; + } + $ansnum++; + } + } else { + # If there's more than one instance of a bubble character + # That's a double bubble; with positional notation we can + # 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')) { + my $doubleerror = 0; + while (($currquest >= $$scantron_config{'Qlength'}) && + (!$doubleerror)) { + my $currline = substr($currquest,0,$$scantron_config{'Qlength'}); + $currquest = substr($currquest,$$scantron_config{'Qlength'}); + my @currarray = split($$scantron_config{'Qon'},$currline,-1); + if (length(@currarray) > 2) { + $doubleerror = 1; + } + } + if ($doubleerror) { + push(@{$record->{'scantron.doubleerror'}},$quest_id); + } + } else { + push(@{$record->{'scantron.doubleerror'}},$quest_id); + } + my $item = $ansnum; + for (my $ans=0; $ans<$answers_needed; $ans++) { + $record->{"scantron.$item.answer"} = ''; + $item ++; + } - for (my $ans = 0; $ans < $answers_needed; $ans++) { - if ($ans eq $line_num) { - $record{"scantron.$ansnum.answer"} = $bubble; - } else { - $record{"scantron.$ansnum.answer"} = ' '; - } - $ansnum++; - } - } - # If there's more than one instance of a bubble character - # That's a double bubble; with positional notation we can - # record all the bubbles filled in as well as the - # fact this response consists of multiple bubbles. - # - else { - push(@{$record{'scantron.doubleerror'}},$questnum); - - my $first_answer = $ansnum; - for (my $ans =0; $ans < $answers_needed; $ans++) { - my $item = $first_answer+$ans; - $record{"scantron.$item.answer"} = ''; - } - - my @ans=@array; - my $i=0; - my $increment = 0; - while ($#ans) { - $i+=length($ans[0]) + $increment; - my $line = int($i/$$scantron_config{'Qlength'} + $first_answer); - my $bubble = $i%$$scantron_config{'Qlength'}; - $record{"scantron.$line.answer"}.=$alphabet[$bubble]; - shift(@ans); - $increment = 1; - } - $ansnum += $answers_needed; - } - } + my @ans=@array; + my $i=0; + my $increment = 0; + while ($#ans) { + $i+=length($ans[0]) + $increment; + my $line = int($i/$$scantron_config{'Qlength'} + $ansnum); + my $bubble = $i%$$scantron_config{'Qlength'}; + $record->{"scantron.$line.answer"}.=$alphabet->[$bubble]; + shift(@ans); + $increment = 1; + } + $ansnum += $answers_needed; } - $record{'scantron.maxquest'}=$questnum; - return \%record; + return $ansnum; } =pod @@ -5670,7 +5728,8 @@ sub scantron_process_corrections { &scantron_fixup_scanline(\%scantron_config,$scan_data,$line, $which,'answer', { 'question'=>$question, - 'response'=>$env{"form.scantron_correct_Q_$question"}}); + 'response'=>$env{"form.scantron_correct_Q_$question"}, + 'questionnum'=>$env{"form.scantron_questionnum_Q_$question"}}); if ($err) { last; } } } @@ -5889,6 +5948,8 @@ SCANTRONFORM ''."\n"; $chunk .= ''."\n"; + $chunk .= + ''."\n"; $result .= $chunk; $line++; } @@ -5933,7 +5994,7 @@ sub scantron_validate_file { if ($env{'form.scantron_corrections'}) { &scantron_process_corrections($r); } - $r->print('

'.&mt('Gathering necessary info.').'

');$r->rflush(); + $r->print('

'.&mt('Gathering necessary information.').'

');$r->rflush(); #get the student pick code ready $r->print(&Apache::loncommon::studentbrowser_javascript()); my $max_bubble=&scantron_get_maxbubble(); @@ -5953,7 +6014,7 @@ sub scantron_validate_file { my $stop=0; while (!$stop && $currentphase < scalar(@validate_phases)) { - $r->print('

'.&mt('Validating '.$validate_phases[$currentphase]).'

'); + $r->print(&mt('Validating '.$validate_phases[$currentphase]).'
'); $r->rflush(); my $which="scantron_validate_".$validate_phases[$currentphase]; { @@ -5964,7 +6025,7 @@ sub scantron_validate_file { if (!$stop) { my $warning=&scantron_warning_screen('Start Grading'); $r->print(' -'.&mt('Validation process complete.').'
+'.&mt('Validation process complete.').'
'.$warning.' @@ -5981,7 +6042,11 @@ sub scantron_validate_file { $r->print("

".&mt("Or click the 'Grading Menu' button to start over.")."

"); } else { - $r->print(''); + if ($validate_phases[$currentphase] eq 'doublebubble' || $validate_phases[$currentphase] eq 'missingbubbles') { + $r->print(''); + } else { + $r->print(''); + } $r->print(' '.&mt('using corrected info').'
'); $r->print(""); $r->print(" ".&mt("this scanline saving it for later.")); @@ -6463,7 +6528,6 @@ sub scantron_validate_ID { sub scantron_get_correction { my ($r,$i,$scan_record,$scan_config,$line,$error,$arg)=@_; - #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 @@ -6485,6 +6549,10 @@ sub scantron_get_correction { $r->print(''."\n"); $r->print(''."\n"); + # Array populated for doublebubble or + my @lines_to_correct; # missingbubble errors to build javascript + # to validate radio button checking + if ($error =~ /ID$/) { if ($error eq 'incorrectID') { $r->print("

".&mt("The encoded ID is not in the classlist"). @@ -6580,7 +6648,7 @@ ENDSCRIPT "")); $r->print("\n

"); } elsif ($error eq 'doublebubble') { - $r->print("

".&mt("There have been multiple bubbles scanned for a some question(s)")."

\n"); + $r->print("

".&mt("There have been multiple bubbles scanned for some question(s)")."

\n"); # The form field scantron_questions is acutally a list of line numbers. # represented by this form so: @@ -6592,15 +6660,18 @@ ENDSCRIPT $r->print($message); $r->print("

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

"); foreach my $question (@{$arg}) { - &prompt_for_corrections($r, $question, $scan_config, $scan_record); + my @linenums = &prompt_for_corrections($r,$question,$scan_config, + $scan_record, $error); + push (@lines_to_correct,@linenums); } + $r->print(&verify_bubbles_checked(@lines_to_correct)); } elsif ($error eq 'missingbubble') { $r->print("

".&mt("There have been no bubbles scanned for some question(s)")."

\n"); $r->print($message); $r->print("

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

"); - $r->print(&mt("Some questions have no scanned bubbles")."\n"); + $r->print(&mt("Some questions have no scanned bubbles.")."\n"); - # The form field scantron_questinos is actually a list of line numbers not + # The form field scantron_questions is actually a list of line numbers not # a list of question numbers. Therefore: # @@ -6609,14 +6680,50 @@ ENDSCRIPT $r->print(''); foreach my $question (@{$arg}) { - &prompt_for_corrections($r, $question, $scan_config, $scan_record); + my @linenums = &prompt_for_corrections($r,$question,$scan_config, + $scan_record, $error); + push (@lines_to_correct,@linenums); } + $r->print(&verify_bubbles_checked(@lines_to_correct)); } else { $r->print("\n"); } +sub verify_bubbles_checked { + my (@ansnums) = @_; + my $ansnumstr = join('","',@ansnums); + my $warning = &mt("A bubble or 'No bubble' selection has not been made for one or more lines."); + my $output = (< +function verify_bubble_radio(form) { + var ansnumArray = new Array ("$ansnumstr"); + var need_bubble_count = 0; + for (var i=0; i 1) { + var bubble_picked = 0; + for (var j=0; j +ENDSCRIPT + return $output; +} + =pod =item questions_to_line_list @@ -6635,11 +6742,26 @@ sub questions_to_line_list { my ($questions) = @_; my @lines; - foreach my $question (@{$questions}) { - my $first = $first_bubble_line{$question-1} + 1; - my $count = $bubble_lines_per_response{$question-1}; - my $last = $first+$count-1; - push(@lines, ($first..$last)); + foreach my $item (@{$questions}) { + my $question = $item; + my ($first,$count,$last); + 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 $subcount = 1; + while ($subcount<$subquestion) { + $first += $subans[$subcount-1]; + $subcount ++; + } + $count = $subans[$subquestion-1]; + } else { + $first = $first_bubble_line{$question-1} + 1; + $count = $bubble_lines_per_response{$question-1}; + } + my $last = $first+$count-1; + push(@lines, ($first..$last)); } return join(',', @lines); } @@ -6657,33 +6779,70 @@ for multi and missing bubble cases). $question - The question number to prompt for. $scan_config - The scantron file configuration hash. $scan_record - Reference to the hash that has the the parsed scanlines. + $error - Type of error Implicit inputs: %bubble_lines_per_response - Starting line numbers for each question. Numbered from 0 (but question numbers are from 1. %first_bubble_line - Starting bubble line for each question. + %subdivided_bubble_lines - optionresponse and matchresponse type + problems render as separate sub-questions, + in exam mode. This hash contains a + comma-separated list of the lines per + sub-question. + %responsetype_per_response - essayresponse, forumalaresponse, and + stringresponse type problem parts can have + multiple lines per response if the weight + assigned exceeds 10. In this case, only + one bubble per line is permitted, but more + than one line might contain bubbles, e.g. + bubbling of: line 1 - J, line 2 - J, + line 3 - B would assign 22 points. =cut sub prompt_for_corrections { - my ($r, $question, $scan_config, $scan_record) = @_; - - my $lines = $bubble_lines_per_response{$question-1}; - my $current_line = $first_bubble_line{$question-1} + 1 ; - + my ($r, $question, $scan_config, $scan_record, $error) = @_; + my ($current_line,$lines); + my @linenums; + my $questionnum = $question; + 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}); + my $subcount = 1; + while ($subcount<$subquestion) { + $current_line += $subans[$subcount-1]; + $subcount ++; + } + $lines = $subans[$subquestion-1]; + } else { + $current_line = $first_bubble_line{$question-1} + 1 ; + $lines = $bubble_lines_per_response{$question-1}; + } if ($lines > 1) { - $r->print(&mt("The group of bubble lines below responds to a single question. Select at most one bubble in a single line and select 'No Bubble' in all the other lines. ")."
"); + $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')) { + $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 scantron sheets.",$lines).'

'.&mt('A non-zero score can be assigned to the student during scantron 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, - split('', $selected)); + my $selected = $$scan_record{"scantron.$current_line.answer"}; + &scantron_bubble_selector($r,$scan_config,$current_line, + $questionnum,$error,split('', $selected)); + push (@linenums,$current_line); $current_line++; } if ($lines > 1) { $r->print("

"); } + return @linenums; } =pod @@ -6697,34 +6856,46 @@ sub prompt_for_corrections { $r - Apache request object $scan_config - hash from &get_scantron_config() $line - Number of the line being displayed. + $questionnum - Question number (may include subquestion) + $error - Type of error. @selected - Array of bubbles picked on this line. =cut sub scantron_bubble_selector { - my ($r,$scan_config,$line,@selected)=@_; + my ($r,$scan_config,$line,$questionnum,$error,@selected)=@_; my $max=$$scan_config{'Qlength'}; my $scmode=$$scan_config{'Qon'}; if ($scmode eq 'number' || $scmode eq 'letter') { $max=10; } my @alphabet=('A'..'Z'); - $r->print(""); + $r->print(&Apache::loncommon::start_data_table(). + &Apache::loncommon::start_data_table_row()); + $r->print(''); for (my $i=0;$i<$max+1;$i++) { $r->print("\n".''); } - $r->print(''); + $r->print(&Apache::loncommon::end_data_table_row(). + &Apache::loncommon::start_data_table_row()); for (my $i=0;$i<$max;$i++) { $r->print("\n". '"); } - $r->print(''); - $r->print('
$line'.$line.''); if ($selected[0] eq $alphabet[$i]) { $r->print('X'); shift(@selected) } else { $r->print(' '); } $r->print('
'); + my $nobub_checked = ' '; + if ($error eq 'missingbubble') { + $nobub_checked = ' checked = "checked" '; + } + $r->print("\n".''."\n".''); + $r->print(&Apache::loncommon::end_data_table_row(). + &Apache::loncommon::end_data_table()); } =pod @@ -6904,7 +7075,6 @@ sub scantron_validate_doublebubble { #get scantron line setup my %scantron_config=&get_scantron_config($env{'form.scantron_format'}); my ($scanlines,$scan_data)=&scantron_getfile(); - &scantron_get_maxbubble(); # parse needs the bubble line array. for (my $i=0;$i<=$scanlines->{'count'};$i++) { @@ -6931,14 +7101,17 @@ sub scantron_validate_doublebubble { for what the current value of the problem counter is. Caches the results to $env{'form.scantron_maxbubble'}, - $env{'form.scantron.bubble_lines.n'} and - $env{'form.scantron.first_bubble_line.n'} + $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 - lines for reponse n and number of the first bubble line for response n. + 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 items only), for response n. =cut -sub scantron_get_maxbubble { +sub scantron_get_maxbubble { if (defined($env{'form.scantron_maxbubble'}) && $env{'form.scantron_maxbubble'}) { &restore_bubble_lines(); @@ -6960,12 +7133,23 @@ sub scantron_get_maxbubble { my $total_lines = 0; %bubble_lines_per_response = (); %first_bubble_line = (); - + %subdivided_bubble_lines = (); + %responsetype_per_response = (); my $response_number = 0; my $bubble_line = 0; foreach my $resource (@resources) { - my $symb = $resource->symb(); + # Need to retrieve part IDs and response IDs because essayresponse + # items are not included in $analysis{'parts'} from lonnet::ssi. + my %possible_part_ids; + if (ref($resource->parts()) eq 'ARRAY') { + foreach my $part (@{$resource->parts()}) { + my @resp_ids = $resource->responseIds($part); + foreach my $id (@resp_ids) { + $possible_part_ids{$part.'.'.$id} = 1; + } + } + } my $result=&Apache::lonnet::ssi($resource->src(), ('symb' => $resource->symb()), ('grade_target' => 'analyze'), @@ -6975,21 +7159,60 @@ sub scantron_get_maxbubble { my (undef, $an) = split(/_HASH_REF__/,$result, 2); - my %analysis = &Apache::lonnet::str2hash($an); - - - - foreach my $part_id (@{$analysis{'parts'}}) { + my @parts; - my $lines = $analysis{"$part_id.bubble_lines"};; + my %analysis = &Apache::lonnet::str2hash($an); + if (ref($analysis{'parts'}) eq 'ARRAY') { + @parts = @{$analysis{'parts'}}; + } + # Add part_ids for any essayresponse items. + foreach my $part_id (keys(%possible_part_ids)) { + if ($analysis{$part_id.'.type'} eq 'essayresponse') { + if (!grep(/^\Q$part_id\E$/,@parts)) { + push (@parts,$part_id); + } + } + } + foreach my $part_id (@parts) { + my $lines = $analysis{"$part_id.bubble_lines"}; # TODO - make this a persistent hash not an array. + # optionresponse and matchresponse type items render as + # separate sub-questions in exam mode. + if (($analysis{$part_id.'.type'} eq 'optionresponse') || + ($analysis{$part_id.'.type'} eq 'matchresponse')) { + my ($numbub,$numshown); + if ($analysis{$part_id.'.type'} eq 'optionresponse') { + if (ref($analysis{$part_id.'.options'}) eq 'ARRAY') { + $numbub = scalar(@{$analysis{$part_id.'.options'}}); + } + } elsif ($analysis{$part_id.'.type'} eq 'matchresponse') { + if (ref($analysis{$part_id.'.items'}) eq 'ARRAY') { + $numbub = scalar(@{$analysis{$part_id.'.items'}}); + } + } + if (ref($analysis{$part_id.'.shown'}) eq 'ARRAY') { + $numshown = scalar(@{$analysis{$part_id.'.shown'}}); + } + my $bubbles_per_line = 10; + my $inner_bubble_lines = int($numshown/$bubbles_per_line); + if (($numshown % $bubbles_per_line) != 0) { + $inner_bubble_lines++; + } + for (my $i=0; $i<$numshown; $i++) { + $subdivided_bubble_lines{$response_number} .= + $inner_bubble_lines.','; + } + $subdivided_bubble_lines{$response_number} =~ s/,$//; + } - $first_bubble_line{$response_number} = $bubble_line; - $bubble_lines_per_response{$response_number} = $lines; + $first_bubble_line{$response_number} = $bubble_line; + $bubble_lines_per_response{$response_number} = $lines; + $responsetype_per_response{$response_number} = + $analysis{$part_id.'.type'}; $response_number++; $bubble_line += $lines;