[LON-CAPA-cvs] cvs: loncom /homework grades.pm

raeburn lon-capa-cvs-allow@mail.lon-capa.org
Fri, 01 Feb 2008 22:50:46 -0000


This is a MIME encoded message

--raeburn1201906246
Content-Type: text/plain

raeburn		Fri Feb  1 17:50:46 2008 EDT

  Modified files:              
    /loncom/homework	grades.pm 
  Log:
  Bug 5589
  In exam mode, optionresponse and matchresponse problems in a single part are divided into sub-questions, each one with its own scantron bubble line(s).
  
  In exam mode, essayresponse, formularesponse and stringresponse can have multiple bubble lines associated with a single part, and bubbles in more than one of these lines, if the weight assigned to the part exceeds 10.
  
  These two cases are addressed.  The first, by passing scantron data lines to the validator for each of the sub-questions in turn, and the second by performing the occurrence_count check on each line in turn.  Some bookkeeping code has been added to keep track of data for each subquestion.
  
  -  %subdivided_bubble_lines hash provides a place to store bubble lines per subquestion for optionresponse and matchresponse.
  - %responsetype_per_response provides a place to store the responsetype for each response. 
  
  - In the case of essayresponse problems, items are not included in $analysis{'parts'} from lonnet::ssi, so possible part IDs need to be retrieved from the  $resource object.
  
  - When preserving information about missingbubbles, use questionnum (which includes subquestion) instead of question.
  
  - Validation for the different scantron formats - letter/number or positional moved to two subroutines - &scantron_validator_lettnum(), and &scantron_validator_positional().
  
  - &prompt_for_corrections() can return an array containing numbers of lines with missingbubble or doublebubble.
  
  - This array is passed to &verify_bubbles_checked() which provides a javascript function to alert the user if any lines are without a choice of bubble or 'No bubble' for the current validation screen, for missingbubble or doublebubble cases only.
  
  - Data tabling the validation tables for missing or double bubbles.
  
  - Fix some typos and expand the documentation for bubbling multiple lines for essayresponse, formularesponse or string response (where more than 1 line may contain a bubble).
  
  
--raeburn1201906246
Content-Type: text/plain
Content-Disposition: attachment; filename="raeburn-20080201175046.txt"

Index: loncom/homework/grades.pm
diff -u loncom/homework/grades.pm:1.502 loncom/homework/grades.pm:1.503
--- loncom/homework/grades.pm:1.502	Wed Jan  9 09:16:52 2008
+++ loncom/homework/grades.pm	Fri Feb  1 17:50:43 2008
@@ -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 %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 @@
 	$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 @@
 	$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 @@
                           - '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 @@
 	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 @@
 		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 @@
     $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 @@
 		&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 @@
 	   '<input type="hidden" name="scantron.bubblelines.'.$line.'" value="'.$env{"form.scantron.bubblelines.$line"}.'" />'."\n";
        $chunk .=
 	   '<input type="hidden" name="scantron.first_bubble_line.'.$line.'" value="'.$env{"form.scantron.first_bubble_line.$line"}.'" />'."\n";
+       $chunk .= 
+           '<input type="hidden" name="scantron.sub_bubblelines.'.$line.'" value="'.$env{"form.scantron.sub_bubblelines.$line"}.'" />'."\n";
        $result .= $chunk;
        $line++;
    }
@@ -5933,7 +5994,7 @@
     if ($env{'form.scantron_corrections'}) {
 	&scantron_process_corrections($r);
     }
-    $r->print('<p>'.&mt('Gathering necessary info.').'</p>');$r->rflush();
+    $r->print('<p>'.&mt('Gathering necessary information.').'</p>');$r->rflush();
     #get the student pick code ready
     $r->print(&Apache::loncommon::studentbrowser_javascript());
     my $max_bubble=&scantron_get_maxbubble();
@@ -5953,7 +6014,7 @@
 
     my $stop=0;
     while (!$stop && $currentphase < scalar(@validate_phases)) {
-	$r->print('<p> '.&mt('Validating '.$validate_phases[$currentphase]).'</p>');
+	$r->print(&mt('Validating '.$validate_phases[$currentphase]).'<br />');
 	$r->rflush();
 	my $which="scantron_validate_".$validate_phases[$currentphase];
 	{
@@ -5964,7 +6025,7 @@
     if (!$stop) {
 	my $warning=&scantron_warning_screen('Start Grading');
 	$r->print('
-'.&mt('Validation process complete.').'<br />
+<b>'.&mt('Validation process complete.').'<b><br />
 '.$warning.'
 <input type="submit" name="submit" value="'.&mt('Start Grading').'" />
 <input type="hidden" name="command" value="scantron_process" />
@@ -5981,7 +6042,11 @@
 
 	    $r->print(" <p>".&mt("Or click the 'Grading Menu' button to start over.")."</p>");
 	} else {
-	    $r->print('<input type="submit" name="submit" value="'.&mt('Continue -&gt;').'" />');
+            if ($validate_phases[$currentphase] eq 'doublebubble' || $validate_phases[$currentphase] eq 'missingbubbles') {
+	        $r->print('<input type="button" name="submitbutton" value="'.&mt('Continue -&gt;').'" onclick="javascript:verify_bubble_radio(this.form)" />');
+            } else {
+                $r->print('<input type="submit" name="submit" value="'.&mt('Continue -&gt;').'" />');
+            }
 	    $r->print(' '.&mt('using corrected info').' <br />');
 	    $r->print("<input type='submit' value='".&mt("Skip")."' name='scantron_skip_record' />");
 	    $r->print(" ".&mt("this scanline saving it for later."));
@@ -6463,7 +6528,6 @@
 
 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 @@
 
     $r->print('<input type="hidden" name="scantron_corrections" value="'.$error.'" />'."\n");
     $r->print('<input type="hidden" name="scantron_line" value="'.$i.'" />'."\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("<p>".&mt("The encoded ID is not in the classlist").
@@ -6580,7 +6648,7 @@
 	     "</label><input type='text' size='8' name='scantron_CODE_newvalue' onfocus=\"javascript:change_radio('use_typed')\" onkeypress=\"javascript:change_radio('use_typed')\" />"));
 	$r->print("\n<br /><br />");
     } elsif ($error eq 'doublebubble') {
-	$r->print("<p>".&mt("There have been multiple bubbles scanned for a some question(s)")."</p>\n");
+	$r->print("<p>".&mt("There have been multiple bubbles scanned for some question(s)")."</p>\n");
 
 	# The form field scantron_questions is acutally a list of line numbers.
 	# represented by this form so:
@@ -6592,15 +6660,18 @@
 	$r->print($message);
 	$r->print("<p>".&mt("Please indicate which bubble should be used for grading")."</p>");
 	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("<p>".&mt("There have been <b>no</b> bubbles scanned for some question(s)")."</p>\n");
 	$r->print($message);
 	$r->print("<p>".&mt("Please indicate which bubble should be used for grading.")."</p>");
-	$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 @@
 	$r->print('<input type="hidden" name="scantron_questions" value="'.
 		  $line_list.'" />');
 	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<ul>");
     }
     $r->print("\n</li></ul>");
 }
 
+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 = (<<ENDSCRIPT);
+<script type="text/javascript">
+function verify_bubble_radio(form) {
+    var ansnumArray = new Array ("$ansnumstr");
+    var need_bubble_count = 0;
+    for (var i=0; i<ansnumArray.length; i++) {
+        if (form.elements["scantron_correct_Q_"+ansnumArray[i]].length > 1) {
+            var bubble_picked = 0; 
+            for (var j=0; j<form.elements["scantron_correct_Q_"+ansnumArray[i]].length; j++) {
+                if (form.elements["scantron_correct_Q_"+ansnumArray[i]][j].checked == true) {
+                    bubble_picked = 1;
+                }
+            }
+            if (bubble_picked == 0) {
+                need_bubble_count ++;
+            }
+        }
+    }
+    if (need_bubble_count) {
+        alert("$warning");
+        return;
+    }
+    form.submit(); 
+}
+</script>
+ENDSCRIPT
+    return $output;
+}
+
 =pod
 
 =item  questions_to_line_list
@@ -6635,11 +6742,26 @@
     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 @@
    $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. ")."<br />");
+        $r->print(&mt('The group of bubble lines below responds to a single question.').'<br />');
+        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).'<br /><br />'.&mt('A non-zero score can be assigned to the student during scantron grading by selecting a bubble in at least one line.').'<br />'.&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.').'<br />'.&mt("To assign a score of zero for this question, mark all lines as 'No bubble'.").'<br /><br />');
+        } else {
+            $r->print(&mt("Select at most one bubble in a single line and select 'No Bubble' in all the other lines. ")."<br />");
+        }
     }
     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("<hr /><br />");
     }
+    return @linenums;
 }
 
 =pod
@@ -6697,34 +6856,46 @@
     $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("<table border='1'><tr><td rowspan='2'>$line</td>");
+    $r->print(&Apache::loncommon::start_data_table().
+              &Apache::loncommon::start_data_table_row());
+    $r->print('<td rowspan="2" class="LC_leftcol_header">'.$line.'</td>');
     for (my $i=0;$i<$max+1;$i++) {
 	$r->print("\n".'<td align="center">');
 	if ($selected[0] eq $alphabet[$i]) { $r->print('X'); shift(@selected) }
 	else { $r->print('&nbsp;'); }
 	$r->print('</td>');
     }
-    $r->print('</tr><tr>');
+    $r->print(&Apache::loncommon::end_data_table_row().
+              &Apache::loncommon::start_data_table_row());
     for (my $i=0;$i<$max;$i++) {
 	$r->print("\n".
 		  '<td><label><input type="radio" name="scantron_correct_Q_'.
 		  $line.'" value="'.$i.'" />'.$alphabet[$i]."</label></td>");
     }
-    $r->print('<td><label><input type="radio" name="scantron_correct_Q_'.
-	      $line.'" value="none" /> No bubble </label></td>');
-    $r->print('</tr></table>');
+    my $nobub_checked = ' ';
+    if ($error eq 'missingbubble') {
+        $nobub_checked = ' checked = "checked" ';
+    }
+    $r->print("\n".'<td><label><input type="radio" name="scantron_correct_Q_'.
+	      $line.'" value="none"'.$nobub_checked.'/>'.&mt('No bubble').
+              '</label>'."\n".'<input type="hidden" name="scantron_questionnum_Q_'.
+              $line.'" value="'.$questionnum.'" /></td>');
+    $r->print(&Apache::loncommon::end_data_table_row().
+              &Apache::loncommon::end_data_table());
 }
 
 =pod
@@ -6904,7 +7075,6 @@
     #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 @@
    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 @@
     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 @@
 	my (undef, $an) =
 	    split(/_HASH_REF__/,$result, 2);
 
-	my %analysis = &Apache::lonnet::str2hash($an);
-
-
+        my @parts;
 
-	foreach my $part_id (@{$analysis{'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;

--raeburn1201906246--