--- loncom/homework/grades.pm	2008/05/14 16:36:31	1.520
+++ loncom/homework/grades.pm	2008/11/11 16:40:47	1.529
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # The LON-CAPA Grading handler
 #
-# $Id: grades.pm,v 1.520 2008/05/14 16:36:31 www Exp $
+# $Id: grades.pm,v 1.529 2008/11/11 16:40:47 jms Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -26,6 +26,183 @@
 # http://www.lon-capa.org/
 #
 
+=head1 NAME
+
+Apache::grades
+
+=head1 SYNOPSIS
+
+Handles the viewing of grades.
+
+This is part of the LearningOnline Network with CAPA project
+described at http://www.lon-capa.org.
+
+=head1 OVERVIEW
+
+Do an ssi with retries:
+While I'd love to factor out this with the vesrion in lonprintout,
+that would either require a data coupling between modules, which I refuse to perpetuate (there's quite enough of that already), or would require the invention of another infrastructure
+I'm not quite ready to invent (e.g. an ssi_with_retry object).
+
+At least the logic that drives this has been pulled out into loncommon.
+
+
+
+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
+                     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:
+                     http://bugs.lon-capa.org/show_bug.cgi?id=5631
+                     by informing the user that this happened.
+
+Parameters:
+  resource   - The resource to include.  This is passed directly, without
+               interpretation to lonnet::ssi.
+  form       - The form hash parameters that guide the interpretation of the resource
+               
+  retries    - Number of retries allowed before giving up completely.
+Returns:
+  On success, returns the rendered resource identified by the resource parameter.
+Side Effects:
+  The following global variables can be set:
+   ssi_error                - If an unrecoverable error occurred this becomes true.
+                              It is up to the caller to initialize this to false
+                              if desired.
+   ssi_error_resource  - If an unrecoverable error occurred, this is the value
+                              of the resource that could not be rendered by the ssi
+                              call.
+   ssi_error_message   - The error string fetched from the ssi response
+                              in the event of an error.
+
+
+=head1 HANDLER SUBROUTINE
+
+ssi_with_retries()
+
+=head1 OTHER SUBROUTINES
+
+=over
+
+=item *
+
+
+scantron_get_correction() : 
+
+   Builds the interface screen to interact with the operator to fix a
+   specific error condition in a specific scanline
+
+ Arguments:
+    $r           - Apache request object
+    $i           - number of the current scanline
+    $scan_record - hash ref as returned from &scantron_parse_scanline()
+    $scan_config - hash ref as returned from &get_scantron_config()
+    $line        - full contents of the current scanline
+    $error       - error condition, valid values are
+                   'incorrectCODE', 'duplicateCODE',
+                   'doublebubble', 'missingbubble',
+                   'duplicateID', 'incorrectID'
+    $arg         - extra information needed
+       For errors:
+         - duplicateID   - paper number that this studentID was seen before on
+         - duplicateCODE - array ref of the paper numbers this CODE was
+                           seen on before
+         - incorrectCODE - current incorrect CODE 
+         - doublebubble  - array ref of the bubble lines that have double
+                           bubble errors
+         - missingbubble - array ref of the bubble lines that have missing
+                           bubble errors
+
+=item *
+
+scantron_get_maxbubble() : 
+
+   Returns the maximum number of bubble lines that are expected to
+   occur. Does this by walking the selected sequence rendering the
+   resource and then checking &Apache::lonxml::get_problem_counter()
+   for what the current value of the problem counter is.
+
+   Caches the results to $env{'form.scantron_maxbubble'},
+   $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 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.  
+
+
+=item *
+
+scantron_validate_missingbubbles() : 
+
+   Validates all scanlines in the selected file to not have any
+    answers that don't have bubbles that have not been verified
+    to be bubble free.
+
+=item *
+
+scantron_process_students() : 
+
+   Routine that does the actual grading of the bubble sheet information.
+
+   The parsed scanline hash is added to %env 
+
+   Then foreach unskipped scanline it does an &Apache::lonnet::ssi()
+   foreach resource , with the form data of
+
+	'submitted'     =>'scantron' 
+	'grade_target'  =>'grade',
+	'grade_username'=> username of student
+	'grade_domain'  => domain of student
+	'grade_courseid'=> of course
+	'grade_symb'    => symb of resource to grade
+
+    This triggers a grading pass. The problem grading code takes care
+    of converting the bubbled letter information (now in %env) into a
+    valid submission.
+
+=item *
+
+scantron_upload_scantron_data() :
+
+    Creates the screen for adding a new bubble sheet data file to a course.
+
+=item *
+
+scantron_upload_scantron_data_save() : 
+
+   Adds a provided bubble information data file to the course if user
+   has the correct privileges to do so. 
+
+=item *
+
+valid_file() :
+
+   Validates that the requested bubble data file exists in the course.
+
+=item *
+
+scantron_download_scantron_data() : 
+
+   Shows a list of the three internal files (original, corrected,
+   skipped) for a specific bubble sheet data file that exists in the
+   course.
+
+=item *
+
+scantron_validate_ID() : 
+
+   Validates all scanlines in the selected file to not have any
+   invalid or underspecified student IDs
+
+=back
+
+=cut
+
 package Apache::grades;
 use strict;
 use Apache::style;
@@ -58,46 +235,6 @@ my $ssi_error_resource;
 my $ssi_error_message;
 
 
-#  Do an ssi with retries:
-#  While I'd love to factor out this with the vesrion in lonprintout,
-#  that would either require a data coupling between modules, which I refuse to perpetuate
-#  (there's quite enough of that already), or would require the invention of another infrastructure
-#  I'm not quite ready to invent (e.g. an ssi_with_retry object).
-#
-# At least the logic that drives this has been pulled out into loncommon.
-
-
-#
-#   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
-#                      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:
-#                      http://bugs.lon-capa.org/show_bug.cgi?id=5631
-#                      by informing the user that this happened.
-#
-# Parameters:
-#   resource   - The resource to include.  This is passed directly, without
-#                interpretation to lonnet::ssi.
-#   form       - The form hash parameters that guide the interpretation of the resource
-#                
-#   retries    - Number of retries allowed before giving up completely.
-# Returns:
-#   On success, returns the rendered resource identified by the resource parameter.
-# Side Effects:
-#   The following global variables can be set:
-#    ssi_error                - If an unrecoverable error occurred this becomes true.
-#                               It is up to the caller to initialize this to false
-#                               if desired.
-#    ssi_error_resource  - If an unrecoverable error occurred, this is the value
-#                               of the resource that could not be rendered by the ssi
-#                               call.
-#    ssi_error_message   - The error string fetched from the ssi response
-#                               in the event of an error.
-#
 sub ssi_with_retries {
     my ($resource, $retries, %form) = @_;
     my ($content, $response) = &Apache::loncommon::ssi_with_retries($resource, $retries, %form);
@@ -240,8 +377,8 @@ sub showResourceInfo {
     my %resptype = ();
     my $hdgrade='no';
     my %partsseen;
-    foreach my $partID (sort keys(%$responseType)) {
-	foreach my $resID (sort keys(%{ $responseType->{$partID} })) {
+    foreach my $partID (sort(keys(%$responseType))) {
+	foreach my $resID (sort(keys(%{ $responseType->{$partID} }))) {
 	    my $handgrade=$$handgrade{$partID.'_'.$resID};
 	    my $responsetype = $responseType->{$partID}->{$resID};
 	    $hdgrade = $handgrade if ($handgrade eq 'yes');
@@ -278,7 +415,7 @@ sub reset_caches {
     }
 
     sub get_analyze {
-	my ($symb,$uname,$udom)=@_;
+	my ($symb,$uname,$udom,$no_increment)=@_;
 	my $key = "$symb\0$uname\0$udom";
 	return $analyze_cache{$key} if (exists($analyze_cache{$key}));
 
@@ -290,15 +427,16 @@ sub reset_caches {
 					    'grade_symb' => $symb,
 					    'grade_courseid' => 
 					    $env{'request.course.id'},
-					    'grade_username' => $uname));
+					    'grade_username' => $uname,
+                                            'grade_noincrement' => $no_increment));
 	(undef,$subresult)=split(/_HASH_REF__/,$subresult,2);
 	my %analyze=&Apache::lonnet::str2hash($subresult);
 	return $analyze_cache{$key} = \%analyze;
     }
 
     sub get_order {
-	my ($partid,$respid,$symb,$uname,$udom)=@_;
-	my $analyze = &get_analyze($symb,$uname,$udom);
+	my ($partid,$respid,$symb,$uname,$udom,$no_increment)=@_;
+	my $analyze = &get_analyze($symb,$uname,$udom,$no_increment);
 	return $analyze->{"$partid.$respid.shown"};
     }
 
@@ -1030,7 +1168,7 @@ LISTJAVASCRIPT
 	       '&nbsp;'.$section.($group ne '' ?'/'.$group:'').'</td>'."\n";
 
 	    if ($env{'form.showgrading'} eq 'yes' && $submitonly ne 'all') {
-		foreach (sort keys(%status)) {
+		foreach (sort(keys(%status))) {
 		    next if ($_ =~ /^resource.*?submitted_by$/);
 		    $gradeTable.='<td align="center">&nbsp;'.&mt($status{$_}).'&nbsp;</td>'."\n";
 		}
@@ -2225,8 +2363,8 @@ KEYWORDS
 	$seen{$partid}++;
 	next if ($$handgrade{$part_resp} ne 'yes' 
 		 && $env{'form.lastSub'} eq 'hdgrade');
-	push @partlist,$partid;
-	push @gradePartRespid,$partid.'.'.$respid;
+	push(@partlist,$partid);
+	push(@gradePartRespid,$partid.'.'.$respid);
 	$request->print(&gradeBox($request,$symb,$uname,$udom,$counter,$partid,\%record));
     }
     $request->print('</div></div>');
@@ -2553,7 +2691,7 @@ sub processHandGrade {
 
     my (@parsedlist,@nextlist);
     my ($nextflg) = 0;
-    foreach (sort 
+    foreach my $item (sort 
 	     {
 		 if (lc($$fullname{$a}) ne lc($$fullname{$b})) {
 		     return (lc($$fullname{$a}) cmp lc($$fullname{$b}));
@@ -2561,12 +2699,12 @@ sub processHandGrade {
 		 return $a cmp $b;
 	     } (keys(%$fullname))) {
 	if ($nextflg == 1 && $button =~ /Next$/) {
-	    push @parsedlist,$_;
+	    push(@parsedlist,$item);
 	}
-	$nextflg = 1 if ($_ eq $laststu);
+	$nextflg = 1 if ($item eq $laststu);
 	if ($button eq 'Previous') {
-	    last if ($_ eq $firststu);
-	    push @parsedlist,$_;
+	    last if ($item eq $firststu);
+	    push(@parsedlist,$item);
 	}
     }
     $ctr = 0;
@@ -2589,11 +2727,11 @@ sub processHandGrade {
 	    my $submitted = 0;
 	    my $ungraded = 0;
 	    my $incorrect = 0;
-	    foreach (keys(%status)) {
-		$submitted = 1 if ($status{$_} ne 'nothing');
-		$ungraded = 1 if ($status{$_} =~ /^ungraded/);
-		$incorrect = 1 if ($status{$_} =~ /^incorrect/);
-		my ($foo,$partid,$foo1) = split(/\./,$_);
+	    foreach my $item (keys(%status)) {
+		$submitted = 1 if ($status{$item} ne 'nothing');
+		$ungraded = 1 if ($status{$item} =~ /^ungraded/);
+		$incorrect = 1 if ($status{$item} =~ /^incorrect/);
+		my ($foo,$partid,$foo1) = split(/\./,$item);
 		if ($status{'resource.'.$partid.'.submitted_by'} ne '') {
 		    $submitted = 0;
 		}
@@ -2604,7 +2742,7 @@ sub processHandGrade {
 	    next if (!$ungraded && ($submitonly eq 'graded'));
 	    next if (!$incorrect && $submitonly eq 'incorrect');
 	}
-	push @nextlist,$student if ($ctr < $ntstu);
+	push(@nextlist,$student) if ($ctr < $ntstu);
 	last if ($ctr == $ntstu);
 	$ctr++;
     }
@@ -2612,7 +2750,7 @@ sub processHandGrade {
     $ctr = 0;
     my $total = scalar(@nextlist)-1;
 
-    foreach (sort @nextlist) {
+    foreach (sort(@nextlist)) {
 	my ($uname,$udom,$submitter) = split(/:/);
 	$env{'form.student'}  = $uname;
 	$env{'form.userdom'}  = $udom;
@@ -2658,7 +2796,7 @@ sub saveHandGrade {
 	    }
 	} elsif ($dropMenu eq 'reset status'
 		 && exists($record{'resource.'.$new_part.'.solved'})) { #don't bother if no old records -> no attempts
-	    foreach my $key (keys (%record)) {
+	    foreach my $key (keys(%record)) {
 		if ($key=~/^resource\.\Q$new_part\E\./) { $newrecord{$key} = ''; }
 	    }
 	    $newrecord{'resource.'.$new_part.'.regrader'}=
@@ -2693,7 +2831,7 @@ sub saveHandGrade {
                 &handback_files($request,$symb,$stuname,$domain,$newflg,$new_part,\%newrecord);
 		next;
 	    } else {
-	        push @parts_graded, $new_part;
+	        push(@parts_graded,$new_part);
 	    }
 	    if ($record{'resource.'.$new_part.'.awarded'} ne $partial) {
 		$newrecord{'resource.'.$new_part.'.awarded'}  = $partial;
@@ -2720,7 +2858,7 @@ sub saveHandGrade {
 	        $record{'resource.'.$new_part.'.solved'} eq 'incorrect_by_override' ||
 	        $dropMenu eq 'reset status')
 	   {
-	    push (@version_parts,$new_part);
+	    push(@version_parts,$new_part);
 	}
     }
     my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'};
@@ -2893,7 +3031,7 @@ sub decrement_aggs {
     if ($aggtries == $totaltries) {
         $decrement{'users'} = 1;
     }
-    foreach my $type (keys (%decrement)) {
+    foreach my $type (keys(%decrement)) {
         $$aggregate{$symb."\0".$part."\0".$type} = -$decrement{$type};
     }
     return;
@@ -3294,7 +3432,7 @@ sub viewgrades {
 	$display =~ s|^Number of Attempts|Tries<br />|; # makes the column narrower
 	if  (!$display) { $display = &Apache::lonnet::metadata($url,$part.'.name'); }
 	my ($partid) = &split_part_type($part);
-        push(@partids, $partid);
+        push(@partids,$partid);
 	my $display_part=&get_display_part($partid,$symb);
 	if ($display =~ /^Partial Credit Factor/) {
 	    $result.='<th>'.
@@ -3448,7 +3586,7 @@ sub editgrades {
     my $header;
     while ($ctr < $env{'form.totalparts'}) {
 	my $partid = $env{'form.partid_'.$ctr};
-	push @partid,$partid;
+	push(@partid,$partid);
 	$weight{$partid} = $env{'form.weight_'.$partid};
 	$ctr++;
     }
@@ -4410,6 +4548,7 @@ sub displaySubByDates {
     }
 
     my $interaction;
+    my $no_increment = 1;
     for ($version=1;$version<=$$record{'version'};$version++) {
 	my $timestamp = 
 	    &Apache::lonlocal::locallocaltime($$record{$version.':timestamp'});
@@ -4453,7 +4592,8 @@ sub displaySubByDates {
 		    if (!exists($orders{$partid})) { $orders{$partid}={}; }
 		    if (!exists($orders{$partid}->{$responseId})) {
 			$orders{$partid}->{$responseId}=
-			    &get_order($partid,$responseId,$symb,$uname,$udom);
+			    &get_order($partid,$responseId,$symb,$uname,$udom,
+                                       $no_increment);
 		    }
 		    $displaySub[0].='</b>&nbsp; '.
 			&cleanRecord($$record{$version.':'.$matchKey},$responseType,$symb,$partid,$responseId,$record,$orders{$partid}->{$responseId},"$version:",$uname,$udom).'<br />';
@@ -4506,12 +4646,12 @@ sub updateGradeByPage {
     my ($uname,$udom) = split(/:/,$env{'form.student'});
     my $usec=$classlist->{$env{'form.student'}}[5];
     if (!&canmodify($usec)) {
-	$request->print('<span class="LC_warning">Unable to modify requested student.('.$env{'form.student'}.'</span>');
+	$request->print('<span class="LC_warning">'.&mt('Unable to modify requested student ([_1])',$env{'form.student'}).'</span>');
 	$request->print(&show_grading_menu_form($env{'form.symb'}));
 	return;
     }
     my $result='<h3><span class="LC_info">&nbsp;'.$env{'form.title'}.'</span></h3>';
-    $result.='<h3>&nbsp;Student: '.&nameUserString(undef,$env{'form.fullname'},$uname,$udom).
+    $result.='<h3>&nbsp;'.&mt('Student: ').&nameUserString(undef,$env{'form.fullname'},$uname,$udom).
 	'</h3>'."\n";
 
     $request->print($result);
@@ -4520,7 +4660,7 @@ sub updateGradeByPage {
     my ($mapUrl, $id, $resUrl) = &Apache::lonnet::decode_symb( $env{'form.page'});
     my $map = $navmap->getResourceByUrl($resUrl); # add to navmaps
     if (!$map) {
-	$request->print('<span class="LC_warning">Unable to grade requested sequence. ('.$resUrl.')</span>');
+	$request->print('<span class="LC_warning">'.&mt('Unable to grade requested sequence ([_1]).',$resUrl).'</span>');
 	my ($symb)=&get_symb($request);
 	$request->print(&show_grading_menu_form($symb));
 	return; 
@@ -4552,8 +4692,8 @@ sub updateGradeByPage {
 		&Apache::loncommon::start_data_table_row().
 		'<td align="center" valign="top" >'.$prob.
 		(scalar(@{$parts}) == 1 ? '' 
-                                        : '<br />('.&mt('[quant,_1,&nbsp;parts]',scalar(@{$parts}))
-		 ).')</td>';
+                                        : '<br />('.&mt('[quant,_1,&nbsp;part]',scalar(@{$parts}))
+		.')').'</td>';
 	    $studentTable.='<td valign="top">&nbsp;<b>'.$title.'</b>&nbsp;</td>';
 
 	    my %newrecord=();
@@ -4597,10 +4737,10 @@ sub updateGradeByPage {
 		}
 		my $display_part=&get_display_part($partid,$curRes->symb());
 		my $oldstatus = $env{'form.solved'.$question.'_'.$partid};
-		$displayPts[0].='&nbsp;<b>Part:</b> '.$display_part.' = '.
+		$displayPts[0].='&nbsp;<b>'.&mt('Part').':</b> '.$display_part.' = '.
 		    (($oldstatus eq 'excused') ? 'excused' : $oldpts).
 		    '&nbsp;<br />';
-		$displayPts[1].='&nbsp;<b>Part:</b> '.$display_part.' = '.
+		$displayPts[1].='&nbsp;<b>'.&mt('Part').':</b> '.$display_part.' = '.
 		     (($score eq 'excused') ? 'excused' : $newpts).
 		    '&nbsp;<br />';
 		$question++;
@@ -4649,9 +4789,9 @@ sub updateGradeByPage {
 
     $studentTable.=&Apache::loncommon::end_data_table();
     $studentTable.=&show_grading_menu_form($env{'form.symb'});
-    my $grademsg=($changeflag == 0 ? 'No score was changed or updated.' :
-		  'The scores were changed for '.
-		  $changeflag.' problem'.($changeflag == 1 ? '.' : 's.'));
+    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);
 
     return '';
@@ -5145,8 +5285,37 @@ sub scantron_selectphase {
 ');
 
     &Apache::lonpickcode::code_list($r,2);
+
+    $r->print('<br /><form method="post" name="checkscantron">'.
+             $default_form_data."\n".
+             &Apache::loncommon::start_data_table('LC_scantron_action')."\n".
+             &Apache::loncommon::start_data_table_header_row()."\n".
+             '<th colspan="2">
+              &nbsp;'.&mt('Review scantron data and submissions for a previously graded folder/sequence')."\n".
+             '</th>'."\n".
+              &Apache::loncommon::end_data_table_header_row()."\n".
+              &Apache::loncommon::start_data_table_row()."\n".
+              '<td> '.&mt('Graded folder/sequence:').' </td>'."\n".
+              '<td> '.$sequence_selector.' </td>'.
+              &Apache::loncommon::end_data_table_row()."\n".
+              &Apache::loncommon::start_data_table_row()."\n".
+              '<td> '.&mt('Filename of scoring office file:').' </td>'."\n".
+              '<td> '.$file_selector.' </td>'."\n".
+              &Apache::loncommon::end_data_table_row()."\n".
+              &Apache::loncommon::start_data_table_row()."\n".
+              '<td> '.&mt('Format of data file:').' </td>'."\n".
+              '<td> '.$format_selector.' </td>'."\n".
+              &Apache::loncommon::end_data_table_row()."\n".
+              &Apache::loncommon::start_data_table_row()."\n".
+              '<td colspan="2">'."\n".
+              '<input type="hidden" name="command" value="checksubmissions" />'."\n".
+              '<input type="submit" value="'.&mt('Review Scantron Data and Submission Records').'" />'."\n".
+              '</td>'."\n".
+              &Apache::loncommon::end_data_table_row()."\n".
+              &Apache::loncommon::end_data_table()."\n".
+              '</form><br />');
     $r->print($grading_menu_button);
-    return
+    return;
 }
 
 =pod
@@ -6579,14 +6748,7 @@ sub scantron_validate_sequence {
     return (0,$currentphase+1);
 }
 
-=pod
-
-=item scantron_validate_ID
 
-   Validates all scanlines in the selected file to not have any
-   invalid or underspecified student IDs
-
-=cut
 
 sub scantron_validate_ID {
     my ($r,$currentphase) = @_;
@@ -6652,35 +6814,6 @@ sub scantron_validate_ID {
     return (0,$currentphase+1);
 }
 
-=pod
-
-=item scantron_get_correction
-
-   Builds the interface screen to interact with the operator to fix a
-   specific error condition in a specific scanline
-
- Arguments:
-    $r           - Apache request object
-    $i           - number of the current scanline
-    $scan_record - hash ref as returned from &scantron_parse_scanline()
-    $scan_config - hash ref as returned from &get_scantron_config()
-    $line        - full contents of the current scanline
-    $error       - error condition, valid values are
-                   'incorrectCODE', 'duplicateCODE',
-                   'doublebubble', 'missingbubble',
-                   'duplicateID', 'incorrectID'
-    $arg         - extra information needed
-       For errors:
-         - duplicateID   - paper number that this studentID was seen before on
-         - duplicateCODE - array ref of the paper numbers this CODE was
-                           seen on before
-         - incorrectCODE - current incorrect CODE 
-         - doublebubble  - array ref of the bubble lines that have double
-                           bubble errors
-         - missingbubble - array ref of the bubble lines that have missing
-                           bubble errors
-
-=cut
 
 sub scantron_get_correction {
     my ($r,$i,$scan_record,$scan_config,$line,$error,$arg)=@_;
@@ -6818,7 +6951,7 @@ ENDSCRIPT
 	foreach my $question (@{$arg}) {
 	    my @linenums = &prompt_for_corrections($r,$question,$scan_config,
                                                    $scan_record, $error);
-            push (@lines_to_correct,@linenums);
+            push(@lines_to_correct,@linenums);
 	}
         $r->print(&verify_bubbles_checked(@lines_to_correct));
     } elsif ($error eq 'missingbubble') {
@@ -6838,7 +6971,7 @@ ENDSCRIPT
 	foreach my $question (@{$arg}) {
 	    my @linenums = &prompt_for_corrections($r,$question,$scan_config,
                                                    $scan_record, $error);
-            push (@lines_to_correct,@linenums);
+            push(@lines_to_correct,@linenums);
 	}
         $r->print(&verify_bubbles_checked(@lines_to_correct));
     } else {
@@ -6996,7 +7129,7 @@ sub prompt_for_corrections {
         my $selected = $$scan_record{"scantron.$current_line.answer"};
 	&scantron_bubble_selector($r,$scan_config,$current_line, 
 	        		  $questionnum,$error,split('', $selected));
-        push (@linenums,$current_line);
+        push(@linenums,$current_line);
 	$current_line++;
     }
     if ($lines > 1) {
@@ -7212,7 +7345,7 @@ sub scantron_validate_CODE {
 				     $line,'duplicateCODE',$usedCODEs{$CODE});
 	    return(1,$currentphase);
 	}
-	push (@{$usedCODEs{$CODE}},$$scan_record{'scantron.PaperID'});
+	push(@{$usedCODEs{$CODE}},$$scan_record{'scantron.PaperID'});
     }
     return (0,$currentphase+1);
 }
@@ -7251,25 +7384,6 @@ sub scantron_validate_doublebubble {
     return (0,$currentphase+1);
 }
 
-=pod
-
-=item scantron_get_maxbubble
-
-   Returns the maximum number of bubble lines that are expected to
-   occur. Does this by walking the selected sequence rendering the
-   resource and then checking &Apache::lonxml::get_problem_counter()
-   for what the current value of the problem counter is.
-
-   Caches the results to $env{'form.scantron_maxbubble'},
-   $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 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.  
-
-=cut
 
 sub scantron_get_maxbubble {
     if (defined($env{'form.scantron_maxbubble'}) &&
@@ -7300,30 +7414,32 @@ sub scantron_get_maxbubble {
     my $bubble_line     = 0;
     foreach my $resource (@resources) {
         my $symb = $resource->symb();
+
+        my (@parts,@allparts,@possible_parts);
+
         # Need to retrieve part IDs and response IDs because essayresponse,
         # reactionresponse and organicresponse items are not included in 
         # $analysis{'parts'} from lonnet::ssi.  
-        my %possible_part_ids; 
-        if (ref($resource->parts()) eq 'ARRAY') { 
+        if (ref($resource->parts()) eq 'ARRAY') {
             foreach my $part (@{$resource->parts()}) {
                 if (!&Apache::loncommon::check_if_partid_hidden($part,$symb,$udom,$uname)) {
                     my @resp_ids = $resource->responseIds($part);
                     foreach my $id (@resp_ids) {
-                        $possible_part_ids{$part.'.'.$id} = 1;
+                        my $part_id = $part.'.'.$id;
+                        push(@possible_parts,$part_id);
                     }
                 }
             }
         }
-	my $result=&ssi_with_retries($resource->src(), $ssi_retries,
-					('symb' => $symb,
-					 'grade_target' => 'analyze',
-					 'grade_courseid' => $cid,
-					 'grade_domain' => $udom,
-					 'grade_username' => $uname));
-	my (undef, $an) =
-	    split(/_HASH_REF__/,$result, 2);
 
-        my @parts;
+        my $result=&ssi_with_retries($resource->src(), $ssi_retries,
+                                        ('symb' => $symb,
+                                         'grade_target' => 'analyze',
+                                         'grade_courseid' => $cid,
+                                         'grade_domain' => $udom,
+                                         'grade_username' => $uname));
+        my (undef, $an) =
+            split(/_HASH_REF__/,$result, 2);
 
 	my %analysis = &Apache::lonnet::str2hash($an);
 
@@ -7335,19 +7451,22 @@ sub scantron_get_maxbubble {
                 }
             }
         }
-        # Add part_ids for any essayresponse items. 
-        foreach my $part_id (keys(%possible_part_ids)) {
-            if (($analysis{$part_id.'.type'} eq 'essayresponse') ||
-                ($analysis{$part_id.'.type'} eq 'reactionresponse') ||
-                ($analysis{$part_id.'.type'} eq 'organicresponse')) {
-                if (!grep(/^\Q$part_id\E$/,@parts)) {
-                    push (@parts,$part_id);
+        # Add part_ids for any essayresponse, reactionresponse or 
+        # organicresponse items. 
+        foreach my $part_id (@possible_parts) {
+            if (grep(/^\Q$part_id\E$/,@parts)) {
+                push(@allparts,$part_id);
+            } else {
+                if (($analysis{$part_id.'.type'} eq 'essayresponse') ||
+                    ($analysis{$part_id.'.type'} eq 'reactionresponse') ||
+                    ($analysis{$part_id.'.type'} eq 'organicresponse')) {
+                    push(@allparts,$part_id);
                 }
             }
         }
 
-	foreach my $part_id (@parts) {
-            my $lines = $analysis{"$part_id.bubble_lines"};
+	foreach my $part_id (@allparts) {
+            my $lines;
 
 	    # TODO - make this a persistent hash not an array.
 
@@ -7374,8 +7493,8 @@ sub scantron_get_maxbubble {
                     $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) {
+                my $inner_bubble_lines = int($numbub/$bubbles_per_line);
+                if (($numbub % $bubbles_per_line) != 0) {
                     $inner_bubble_lines++;
                 }
                 for (my $i=0; $i<$numshown; $i++) {
@@ -7383,6 +7502,9 @@ sub scantron_get_maxbubble {
                         $inner_bubble_lines.',';
                 }
                 $subdivided_bubble_lines{$response_number} =~ s/,$//;
+                $lines = $numshown * $inner_bubble_lines;
+            } else {
+                $lines = $analysis{"$part_id.bubble_lines"};
             } 
 
             $first_bubble_line{$response_number} = $bubble_line;
@@ -7404,15 +7526,6 @@ sub scantron_get_maxbubble {
     return $env{'form.scantron_maxbubble'};
 }
 
-=pod
-
-=item scantron_validate_missingbubbles
-
-   Validates all scanlines in the selected file to not have any
-    answers that don't have bubbles that have not been verified
-    to be bubble free.
-
-=cut
 
 sub scantron_validate_missingbubbles {
     my ($r,$currentphase) = @_;
@@ -7467,29 +7580,6 @@ sub scantron_validate_missingbubbles {
     return (0,$currentphase+1);
 }
 
-=pod
-
-=item scantron_process_students
-
-   Routine that does the actual grading of the bubble sheet information.
-
-   The parsed scanline hash is added to %env 
-
-   Then foreach unskipped scanline it does an &Apache::lonnet::ssi()
-   foreach resource , with the form data of
-
-	'submitted'     =>'scantron' 
-	'grade_target'  =>'grade',
-	'grade_username'=> username of student
-	'grade_domain'  => domain of student
-	'grade_courseid'=> of course
-	'grade_symb'    => symb of resource to grade
-
-    This triggers a grading pass. The problem grading code takes care
-    of converting the bubbled letter information (now in %env) into a
-    valid submission.
-
-=cut
 
 sub scantron_process_students {
     my ($r) = @_;
@@ -7620,14 +7710,6 @@ SCANTRONFORM
     return '';
 }
 
-=pod
-
-=item scantron_upload_scantron_data
-
-    Creates the screen for adding a new bubble sheet data file to a course.
-
-=cut
-
 sub scantron_upload_scantron_data {
     my ($r)=@_;
     $r->print(&Apache::loncommon::coursebrowser_javascript($env{'request.role.domain'}));
@@ -7668,14 +7750,6 @@ sub scantron_upload_scantron_data {
     return '';
 }
 
-=pod
-
-=item scantron_upload_scantron_data_save
-
-   Adds a provided bubble information data file to the course if user
-   has the correct privileges to do so.  
-
-=cut
 
 sub scantron_upload_scantron_data_save {
     my($r)=@_;
@@ -7737,14 +7811,6 @@ sub scantron_upload_scantron_data_save {
     return '';
 }
 
-=pod
-
-=item valid_file
-
-   Validates that the requested bubble data file exists in the course.
-
-=cut
-
 sub valid_file {
     my ($requested_file)=@_;
     foreach my $filename (sort(&scantron_filenames())) {
@@ -7753,16 +7819,6 @@ sub valid_file {
     return 0;
 }
 
-=pod
-
-=item scantron_download_scantron_data
-
-   Shows a list of the three internal files (original, corrected,
-   skipped) for a specific bubble sheet data file that exists in the
-   course.
-
-=cut
-
 sub scantron_download_scantron_data {
     my ($r)=@_;
     my $default_form_data=&defaultFormData(&get_symb($r,1));
@@ -7802,11 +7858,271 @@ sub scantron_download_scantron_data {
     return '';
 }
 
-=pod
+sub checkscantron_results {
+    my ($r) = @_;
+    my ($symb)=&get_symb($r);
+    if (!$symb) {return '';}
+    my $grading_menu_button=&show_grading_menu_form($symb);
+    my $cid = $env{'request.course.id'};
+    my %lettdig = (
+                    A => 1,
+                    B => 2,
+                    C => 3,
+                    D => 4,
+                    E => 5,
+                    F => 6,
+                    G => 7,
+                    H => 8,
+                    I => 9,
+                    J => 0,
+                  );
+    my $numletts = scalar(keys(%lettdig));
+    my $cnum = $env{'course.'.$cid.'.num'};
+    my $cdom = $env{'course.'.$cid.'.domain'};
+    my (undef, undef, $sequence) = &Apache::lonnet::decode_symb($env{'form.selectpage'});
+    my %record;
+    my %scantron_config =
+        &Apache::grades::get_scantron_config($env{'form.scantron_format'});
+    my ($scanlines,$scan_data)=&Apache::grades::scantron_getfile();
+    my $classlist=&Apache::loncoursedata::get_classlist();
+    my %idmap=&Apache::grades::username_to_idmap($classlist);
+    my $navmap=Apache::lonnavmaps::navmap->new();
+    my $map=$navmap->getResourceByUrl($sequence);
+    my @resources=$navmap->retrieveResources($map,undef,1,0);
+    my (%scandata,%lastname,%bylast);
+    $r->print('
+<form method="post" enctype="multipart/form-data" action="/adm/grades" name="checkscantron">'."\n");
 
-=back
+    my @delayqueue;
+    my %completedstudents;
+
+    my $count=&Apache::grades::get_todo_count($scanlines,$scan_data);
+    my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin($r,'Scantron/Submissions Comparison Status',
+                                    'Progress of Scantron Data/Submission Records Comparison',$count,
+                                    'inline',undef,'checkscantron');
+    my ($username,$domain,$uname,$started);
+
+    &Apache::grades::scantron_get_maxbubble();  # Need the bubble lines array to parse.
+
+    &Apache::lonhtmlcommon::Update_PrgWin($r,\%prog_state,
+                                          'Processing first student');
+    my $start=&Time::HiRes::time();
+    my $i=-1;
+
+    while ($i<$scanlines->{'count'}) {
+        ($username,$domain,$uname)=('','','');
+        $i++;
+        my $line=&Apache::grades::scantron_get_line($scanlines,$scan_data,$i);
+        if ($line=~/^[\s\cz]*$/) { next; }
+        if ($started) {
+            &Apache::lonhtmlcommon::Increment_PrgWin($r,\%prog_state,
+                                                     'last student');
+        }
+        $started=1;
+        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)) {
+            &Apache::grades::scantron_add_delay(\@delayqueue,$line,
+                                'Unable to find a student that matches',1);
+            next;
+        }
+        if (exists $completedstudents{$uname}) {
+            &Apache::grades::scantron_add_delay(\@delayqueue,$line,
+                                'Student '.$uname.' has multiple sheets',2);
+            next;
+        }
+        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$//;
+        ($username,$domain)=split(/:/,$uname);
+        my $counter = -1;
+        my (%expected,%startpos);
+        foreach my $resource (@resources) {
+            next if (!$resource->is_problem());
+            my $symb = $resource->symb();
+            my $partsref = $resource->parts();
+            my @parts;
+            my @part_ids = ();
+            if (ref($partsref) eq 'ARRAY') {
+               @parts = @{$partsref};
+               foreach my $part (@parts) {
+                   my @resp_ids = $resource->responseIds($part);
+                   foreach my $resp (@resp_ids) {
+                       $counter ++;
+                       my $part_id = $part.'.'.$resp;
+                       $expected{$part_id} = 0;
+                       push(@part_ids,$part_id);
+                       if ($env{"form.scantron.sub_bubblelines.$counter"}) {
+                           my @sub_lines = split(/,/,$env{"form.scantron.sub_bubblelines.$counter"});
+                           foreach my $item (@sub_lines) {
+                               $expected{$part_id} += $item;
+                           }
+                       } else {
+                           $expected{$part_id} = $env{"form.scantron.bubblelines.$counter"};
+                       }
+                       $startpos{$part_id} = $env{"form.scantron.first_bubble_line.$counter"};
+                   }
+                }
+            }
+            if ($symb) {
+                my %recorded;
+                my (%returnhash) =
+                    &Apache::lonnet::restore($symb,$cid,$domain,$username);
+                if ($returnhash{'version'}) {
+                    my %lasthash=();
+                    my $version;
+                    for ($version=1;$version<=$returnhash{'version'};$version++) {
+                        foreach my $key (sort(split(/\:/,$returnhash{$version.':keys'}))) {
+                            $lasthash{$key}=$returnhash{$version.':'.$key};
+                        }
+                    }
+                    foreach my $key (keys(%lasthash)) {
+                        if ($key =~ /\.scantron$/) {
+                            my $value = &unescape($lasthash{$key});
+                            my ($part_id) = ($key =~ /^resource\.(.+)\.scantron$/);
+                            if ($value eq '') {
+                                for (my $i=0; $i<$expected{$part_id}; $i++) {
+                                    for (my $j=0; $j<$scantron_config{'length'}; $j++) {
+                                        $recorded{$part_id} .= $;
+                                    }
+                                }
+                            } else {
+                                my @tocheck;
+                                my @items = split(//,$value);
+                                if (($scantron_config{'Qon'} eq 'letter') ||
+                                    ($scantron_config{'Qon'} eq 'number')) {
+                                    if (@items < $expected{$part_id}) {
+                                        my $fragment = substr($scandata{$pid},$startpos{$part_id},$expected{$part_id});
+                                        my @singles = split(//,$fragment);
+                                        foreach my $pos (@singles) {
+                                            if ($pos eq ' ') {
+                                                push(@tocheck,$pos);
+                                            } else {
+                                                my $next = shift(@items);
+                                                push(@tocheck,$next);
+                                            }
+                                        }
+                                    } else {
+                                        @tocheck = @items;
+                                    }
+                                    foreach my $letter (@tocheck) {
+                                        if ($scantron_config{'Qon'} eq 'letter') {
+                                            if ($letter !~ /^[A-J]$/) {
+                                                $letter = $scantron_config{'Qoff'};
+                                            }
+                                            $recorded{$part_id} .= $letter;
+                                        } elsif ($scantron_config{'Qon'} eq 'number') {
+                                            my $digit;
+                                            if ($letter !~ /^[A-J]$/) {
+                                                $digit = $scantron_config{'Qoff'};
+                                            } else {
+                                                $digit = $lettdig{$letter};
+                                            }
+                                            $recorded{$part_id} .= $digit;
+                                        }
+                                    }
+                                } else {
+                                    @tocheck = @items;
+                                    for (my $i=0; $i<$expected{$part_id}; $i++) {
+                                        my $curr_sub = shift(@tocheck);
+                                        my $digit;
+                                        if ($curr_sub =~ /^[A-J]$/) {
+                                            $digit = $lettdig{$curr_sub}-1;
+                                        }
+                                        if ($curr_sub eq 'J') {
+                                            $digit += scalar($numletts);
+                                        }
+                                        for (my $j=0; $j<$scantron_config{'Qlength'}; $j++) {
+                                            if ($j == $digit) {
+                                                $recorded{$part_id} .= $scantron_config{'Qon'};
+                                            } else {
+                                                $recorded{$part_id} .= $scantron_config{'Qoff'};
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+                foreach my $part_id (@part_ids) {
+                    if ($recorded{$part_id} eq '') {
+                        for (my $i=0; $i<$expected{$part_id}; $i++) {
+                            for (my $j=0; $j<$scantron_config{'Qlength'}; $j++) {
+                                $recorded{$part_id} .= $scantron_config{'Qoff'};
+                            }
+                        }
+                    }
+                    $record{$pid} .= $recorded{$part_id};
+                }
+            }
+        }
+    }
+    &Apache::lonhtmlcommon::Close_PrgWin($r,\%prog_state);
+    $r->print('<br />');
+    my ($okstudents,$badstudents,$numstudents,$passed,$failed);
+    $passed = 0;
+    $failed = 0;
+    $numstudents = 0;
+    foreach my $last (sort(keys(%bylast))) {
+        if (ref($bylast{$last}) eq 'ARRAY') {
+            foreach my $pid (sort(@{$bylast{$last}})) {
+                my $showscandata = $scandata{$pid};
+                my $showrecord = $record{$pid};
+                $showscandata =~ s/\s/&nbsp;/g;
+                $showrecord =~ s/\s/&nbsp;/g;
+                if ($scandata{$pid} eq $record{$pid}) {
+                    my $css_class = ($passed % 2)?'LC_odd_row':'LC_even_row';
+                    $okstudents .= '<tr class="'.$css_class.'">'.
+'<td>'.&mt('Scantron').'</td><td>'.$showscandata.'</td><td rowspan="2">'.$last.'</td><td rowspan="2">'.$pid.'</td>'."\n".
+'</tr>'."\n".
+'<tr class="'.$css_class.'">'."\n".
+'<td>Submissions</td><td>'.$showrecord.'</td></tr>'."\n";
+                    $passed ++;
+                } else {
+                    my $css_class = ($failed % 2)?'LC_odd_row':'LC_even_row';
+                    $badstudents .= '<tr class="'.$css_class.'"><td>'.&mt('Scantron').'</td><td><span class="LC_nobreak">'.$scandata{$pid}.'</span></td><td rowspan="2">'.$last.'</td><td rowspan="2">'.$pid.'</td>'."\n".
+'</tr>'."\n".
+'<tr class="'.$css_class.'">'."\n".
+'<td>Submissions</td><td><span class="LC_nobreak">'.$record{$pid}.'</span></td>'."\n".
+'</tr>'."\n";
+                    $failed ++;
+                }
+                $numstudents ++;
+            }
+        }
+    }
+    $r->print('<p>'.&mt('Comparison of scantron data (including corrections) with corresponding submission records (most recent submission) for <b>[quant,_1,student]</b>  ([_2] scantron lines/student).',$numstudents,$env{'form.scantron_maxbubble'}).'</p>');
+    $r->print('<p>'.&mt('Exact matches for <b>[quant,_1,student]</b>.',$passed).'<br />'.&mt('Discrepancies detected for <b>[quant,_1,student]</b>.',$failed).'</p>');
+    if ($passed) {
+        $r->print(&mt('Students with exact correspondence between scantron data and submissions are as follows:').'<br /><br />');
+        $r->print(&Apache::loncommon::start_data_table()."\n".
+                 &Apache::loncommon::start_data_table_header_row()."\n".
+                 '<th>'.&mt('Source').'</th><th>'.&mt('Bubble records').'</th><th>'.&mt('Name').'</th><th>'.&mt('ID').'</th>'.
+                 &Apache::loncommon::end_data_table_header_row()."\n".
+                 $okstudents."\n".
+                 &Apache::loncommon::end_data_table().'<br />');
+    }
+    if ($failed) {
+        $r->print(&mt('Students with differences between scantron data and submissions are as follows:').'<br /><br />');
+        $r->print(&Apache::loncommon::start_data_table()."\n".
+                 &Apache::loncommon::start_data_table_header_row()."\n".
+                 '<th>'.&mt('Source').'</th><th>'.&mt('Bubble records').'</th><th>'.&mt('Name').'</th><th>'.&mt('ID').'</th>'.
+                 &Apache::loncommon::end_data_table_header_row()."\n".
+                 $badstudents."\n".
+                 &Apache::loncommon::end_data_table()).'<br />'.
+                 &mt('Differences can occur if submissions were modified using manual grading after a scantron grading pass.').'<br />'.&mt('If unexpected discrepancies were detected, it is recommended that you inspect the original scantron sheets.');  
+    }
+    $r->print('</form><br />'.$grading_menu_button);
+    return;
+}
 
-=cut
 
 #-------- end of section for handling grading scantron forms -------
 #
@@ -7861,25 +8177,25 @@ sub grading_menu {
                  });
     $fields{'command'} = 'csvform';
     $url = &Apache::lonhtmlcommon::build_url('grades/',\%fields);
-    push (@menu, { url => $url,
+    push(@menu, { url => $url,
                    name => &mt('Upload Scores'),
                    short_description => 
             &mt('Specify a file containing the class scores for current resource.')});
     $fields{'command'} = 'processclicker';
     $url = &Apache::lonhtmlcommon::build_url('grades/',\%fields);
-    push (@menu, { url => $url,
+    push(@menu, { url => $url,
                    name => &mt('Process Clicker'),
                    short_description => 
             &mt('Specify a file containing the clicker information for this resource.')});
     $fields{'command'} = 'scantron_selectphase';
     $url = &Apache::lonhtmlcommon::build_url('grades/',\%fields);
-    push (@menu, { url => $url,
-                   name => &mt('Grade/Manage Scantron Forms'),
+    push(@menu, { url => $url,
+                   name => &mt('Grade/Manage/Review Scantron Forms'),
                    short_description => 
-            &mt('')});
+            &mt('Grade scantron exams, upload/download scantron data files, and review previously graded scantron exams.')});
     $fields{'command'} = 'verify';
     $url = &Apache::lonhtmlcommon::build_url('grades/',\%fields);
-    push (@menu, { url => "",
+    push(@menu, { url => "",
                    name => &mt('Verify Receipt'),
                    short_description => 
             &mt('')});
@@ -8035,7 +8351,7 @@ GRADINGMENUJS
              <div class="LC_grade_select_mode_selector_body">
 	       <select name="section" multiple="multiple" size="5">'."\n";
     if (ref($sections)) {
-	foreach my $section (sort (@$sections)) {
+	foreach my $section (sort(@$sections)) {
 	    $result.='<option value="'.$section.'" '.
 		($saveSec eq $section ? 'selected="selected"':'').'>'.$section.'</option>'."\n";
 	}
@@ -8226,7 +8542,7 @@ sub process_clicker {
     if (!$env{'form.upfiletype'}) { $env{'form.upfiletype'}='iclicker'; }
 
     my %checked;
-    foreach my $gradingmechanism ('attendance','personnel','specific') {
+    foreach my $gradingmechanism ('attendance','personnel','specific','given') {
        if ($env{'form.gradingmechanism'} eq $gradingmechanism) {
           $checked{$gradingmechanism}="checked='checked'";
        }
@@ -8237,6 +8553,8 @@ sub process_clicker {
     my $attendance=&mt("Award points just for participation");
     my $personnel=&mt("Correctness determined from response by course personnel");
     my $specific=&mt("Correctness determined from response with clicker ID(s)"); 
+    my $given=&mt("Correctness determined from given list of answers").' '.
+              '<font size="-2"><tt>('.&mt("Provide comma-separated list. Use '*' for any answer correct, '-' for skip").')</tt></font>';
     my $pcorrect=&mt("Percentage points for correct solution");
     my $pincorrect=&mt("Percentage points for incorrect solution");
     my $selectform=&Apache::loncommon::select_form($env{'form.upfiletype'},'upfiletype',
@@ -8294,6 +8612,9 @@ function sanitycheck() {
 <br /><label><input type="radio" name="gradingmechanism" value="personnel" $checked{'personnel'} onClick="sanitycheck()" />$personnel</label>
 <br /><label><input type="radio" name="gradingmechanism" value="specific" $checked{'specific'} onClick="sanitycheck()" />$specific </label>
 <input type="text" name="specificid" value="$env{'form.specificid'}" size="20" />
+<br /><label><input type="radio" name="gradingmechanism" value="given" $checked{'given'} onClick="sanitycheck()" />$given </label>
+<br />&nbsp;&nbsp;&nbsp;
+<input type="text" name="givenanswer" size="50" />
 <input type="hidden" name="waschecked" value="$env{'form.gradingmechanism'}" />
 <br /><label>$pcorrect: <input type="text" name="pcorrect" size="4" value="$env{'form.pcorrect'}" onChange="sanitycheck()" /></label>
 <br /><label>$pincorrect: <input type="text" name="pincorrect" size="4" value="$env{'form.pincorrect'}" onChange="sanitycheck()" /></label>
@@ -8320,6 +8641,19 @@ sub process_clicker_file {
 	$result.='<span class="LC_error">'.&mt('You need to specify a clicker ID for the correct answer').'</span>';
 	return $result.&show_grading_menu_form($symb);
     }
+    if (($env{'form.gradingmechanism'} eq 'given') && ($env{'form.givenanswer'}!~/\S/)) {
+        $result.='<span class="LC_error">'.&mt('You need to specify the correct answer').'</span>';
+        return $result.&show_grading_menu_form($symb);
+    }
+    my $foundgiven=0;
+    if ($env{'form.gradingmechanism'} eq 'given') {
+        $env{'form.givenanswer'}=~s/^\s*//gs;
+        $env{'form.givenanswer'}=~s/\s*$//gs;
+        $env{'form.givenanswer'}=~s/[^a-zA-Z0-9\.\*\-]+/\,/g;
+        $env{'form.givenanswer'}=uc($env{'form.givenanswer'});
+        my @answers=split(/\,/,$env{'form.givenanswer'});
+        $foundgiven=$#answers+1;
+    }
     my %clicker_ids=&gather_clicker_ids();
     my %correct_ids;
     if ($env{'form.gradingmechanism'} eq 'personnel') {
@@ -8338,6 +8672,8 @@ sub process_clicker_file {
     }
     if ($env{'form.gradingmechanism'} eq 'attendance') {
 	$result.=&mt('Score based on attendance only');
+    } elsif ($env{'form.gradingmechanism'} eq 'given') {
+        $result.=&mt('Score based on [_1] ([_2] answers)','<tt>'.$env{'form.givenanswer'}.'</tt>',$foundgiven);
     } else {
 	my $number=0;
 	$result.='<p><b>'.&mt('Correctness determined by the following IDs').'</b>';
@@ -8383,6 +8719,9 @@ sub process_clicker_file {
 <input type="hidden" name="pcorrect" value="$env{'form.pcorrect'}" />
 <input type="hidden" name="pincorrect" value="$env{'form.pincorrect'}" />
 ENDHEADER
+    if ($env{'form.gradingmechanism'} eq 'given') {
+       $result.='<input type="hidden" name="correct:given" value="'.$env{'form.givenanswer'}.'" />';
+    } 
     my %responses;
     my @questiontitles;
     my $errormsg='';
@@ -8398,6 +8737,10 @@ ENDHEADER
              &mt('Awarding [_1] percent for correct and [_2] percent for incorrect responses',
                  $env{'form.pcorrect'},$env{'form.pincorrect'}).
              '<br />';
+    if (($env{'form.gradingmechanism'} eq 'given') && ($number!=$foundgiven)) {
+       $result.='<span class="LC_error">'.&mt('Number of given answers does not agree with number of questions in file.').'</span>';
+       return $result.&show_grading_menu_form($symb);
+    } 
 # Remember Question Titles
 # FIXME: Possibly need delimiter other than ":"
     for (my $i=0;$i<$number;$i++) {
@@ -8441,7 +8784,7 @@ ENDHEADER
     }
     $result.='<hr />'.
              &mt('Found [_1] registered and [_2] unregistered clickers.',$student_count,$unknown_count);
-    if ($env{'form.gradingmechanism'} ne 'attendance') {
+    if (($env{'form.gradingmechanism'} ne 'attendance') && ($env{'form.gradingmechanism'} ne 'given')) {
        if ($correct_count==0) {
           $errormsg.="Found no correct answers answers for grading!";
        } elsif ($correct_count>1) {
@@ -8512,7 +8855,7 @@ sub interwrite_eval {
         $id=~s/[\-\:]//g;
         $idresponses{$id}[$number]=$entries[6];
     }
-    foreach my $id (keys %idresponses) {
+    foreach my $id (keys(%idresponses)) {
        $$responses{$id}=join(',',@{$idresponses{$id}});
        $$responses{$id}=~s/^\s*\,//;
     }
@@ -8586,10 +8929,15 @@ ENDHEADER
        if ($user) { 
           my @answer=split(/\,/,$env{$key});
           my $sum=0;
+          my $realnumber=$number;
           for (my $i=0;$i<$number;$i++) {
              if ($answer[$i]) {
                 if ($gradingmechanism eq 'attendance') {
                    $sum+=$pcorrect;
+                } elsif ($answer[$i] eq '*') {
+                   $sum+=$pcorrect;
+                } elsif ($answer[$i] eq '-') {
+                   $realnumber--;
                 } else {
                    if ($answer[$i] eq $correct[$i]) {
                       $sum+=$pcorrect;
@@ -8599,7 +8947,7 @@ ENDHEADER
                 }
              }
           }
-          my $ave=$sum/(100*$number);
+          my $ave=$sum/(100*$realnumber);
 # Store
           my ($username,$domain)=split(/\:/,$user);
           my %grades=();
@@ -8738,6 +9086,8 @@ sub handler {
  	} elsif ($command eq 'scantron_download' &&
 		 &Apache::lonnet::allowed('usc',$env{'request.course.id'})) {
  	    $request->print(&scantron_download_scantron_data($request));
+        } elsif ($command eq 'checksubmissions' && $perm{'vgr'}) {
+            $request->print(&checkscantron_results($request));     
 	} elsif ($command) {
 	    $request->print("Access Denied ($command)");
 	}