--- loncom/homework/grades.pm	2008/03/17 20:39:50	1.515
+++ loncom/homework/grades.pm	2008/12/31 21:10:29	1.528.2.7
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # The LON-CAPA Grading handler
 #
-# $Id: grades.pm,v 1.515 2008/03/17 20:39:50 raeburn Exp $
+# $Id: grades.pm,v 1.528.2.7 2008/12/31 21:10:29 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -73,7 +73,7 @@ my $ssi_error_message;
 #                      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 occured.  
+#                      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
@@ -89,13 +89,13 @@ my $ssi_error_message;
 #   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 occured this becomes true.
+#    ssi_error                - If an unrecoverable error occurred this becomes true.
 #                               It is up to the caller to initialize this to false
 #                               if desired.
-#    ssi_last_error_resource  - If an unrecoverable error occured, this is the value
+#    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_last_error           - The error string fetched from the ssi response
+#    ssi_error_message   - The error string fetched from the ssi response
 #                               in the event of an error.
 #
 sub ssi_with_retries {
@@ -116,11 +116,20 @@ sub ssi_with_retries {
 
 sub ssi_print_error {
     my ($r) = @_;
-    $r->print('<h2>Unrecoverable network error</h2>');
-    $r->print('<p>Unable to perform a resource fetch from a server: <br />');
-    $r->print("Resource: $ssi_error_resource <br />");
-    $r->print("Error: $ssi_error_message <br /> Try again later.");
-    $r->print('If errors persist, contact LonCAPA support for assistance</p>');
+    my $helpurl = &Apache::loncommon::top_nav_help('Helpdesk');
+    $r->print('
+<br />
+<h2>'.&mt('An unrecoverable network error occurred:').'</h2>
+<p>
+'.&mt('Unable to retrieve a resource from a server:').'<br />
+'.&mt('Resource:').' '.$ssi_error_resource.'<br />
+'.&mt('Error:').' '.$ssi_error_message.'
+</p>
+<p>'.
+&mt('It is recommended that you try again later, as this error may mean the server was just temporarily unavailable, or is down for maintenance.').'<br />'.
+&mt('If the error persists, please contact the [_1] for assistance.',$helpurl).
+'</p>');
+    return;
 }
 
 #
@@ -231,8 +240,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');
@@ -269,27 +278,28 @@ 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}));
 
 	my (undef,undef,$url)=&Apache::lonnet::decode_symb($symb);
 	$url=&Apache::lonnet::clutter($url);
 	my $subresult=&ssi_with_retries($url, $ssi_retries,
-					   ('grade_target' => 'analyze'),
-					   ('grade_domain' => $udom),
-					   ('grade_symb' => $symb),
-					   ('grade_courseid' => 
-					    $env{'request.course.id'}),
-					   ('grade_username' => $uname));
+					   ('grade_target' => 'analyze',
+					    'grade_domain' => $udom,
+					    'grade_symb' => $symb,
+					    'grade_courseid' => 
+					    $env{'request.course.id'},
+					    '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"};
     }
 
@@ -1021,7 +1031,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";
 		}
@@ -1671,7 +1681,7 @@ sub gradeBox {
 
     my $radio.='<table border="0"><tr>'."\n";  # display radio buttons in a nice table 10 across
     while ($thisweight<=$wgt) {
-	$radio.= '<td><span style="white-space: nowrap;"><label><input type="radio" name="RADVAL'.$counter.'_'.$partid.'" '.
+	$radio.= '<td><span class="LC_nobreak"><label><input type="radio" name="RADVAL'.$counter.'_'.$partid.'" '.
 	    'onclick="javascript:writeBox(this.form,\''.$counter.'_'.$partid.'\','.
 	    $thisweight.')" value="'.$thisweight.'" '.
 	    ($score eq $thisweight ? 'checked="checked"':'').' /> '.$thisweight."</label></span></td>\n";
@@ -2125,7 +2135,7 @@ KEYWORDS
 			    ' )</span>&nbsp; &nbsp;';
 			my $files=&get_submitted_files($udom,$uname,$partid,$respid,\%record);
 			if (@$files) {
-			    $lastsubonly.='<br /><span class="LC_warning">'.&mt('Like all files provided by users, this file may contain virusses').'</span><br />';
+			    $lastsubonly.='<br /><span class="LC_warning">'.&mt('Like all files provided by users, this file may contain viruses').'</span><br />';
 			    my $file_counter = 0;
 			    foreach my $file (@$files) {
 			        $file_counter++;
@@ -2216,8 +2226,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>');
@@ -2348,7 +2358,7 @@ sub get_last_submission {
 					$$returnhash{$version.':keys'}))) {
 		$lasthash{$key}=$$returnhash{$version.':'.$key};
 		$timestamp = 
-		    scalar(localtime($$returnhash{$version.':timestamp'}));
+		    &Apache::lonlocal::locallocaltime($$returnhash{$version.':timestamp'});
 	    }
 	}
 	foreach my $key (keys(%lasthash)) {
@@ -2544,7 +2554,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}));
@@ -2552,12 +2562,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;
@@ -2580,11 +2590,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;
 		}
@@ -2595,7 +2605,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++;
     }
@@ -2603,7 +2613,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;
@@ -2649,7 +2659,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'}=
@@ -2684,7 +2694,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;
@@ -2711,7 +2721,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'};
@@ -2759,7 +2769,7 @@ sub check_and_remove_from_queue {
 
 sub handback_files {
     my ($request,$symb,$stuname,$domain,$newflg,$new_part,$newrecord) = @_;
-    my $portfolio_root = &propath($domain,$stuname).'/userfiles/portfolio';
+    my $portfolio_root = '/userfiles/portfolio';
     my ($partlist,$handgrade,$responseType) = &response_type($symb);
 
     my @part_response_id = &flatten_responseType($responseType);
@@ -2777,7 +2787,8 @@ sub handback_files {
                     my ($answer_name,$answer_ver,$answer_ext) =
 		        &file_name_version_ext($answer_file);
 		    my ($portfolio_path) = ($directory =~ /^.+$stuname\/portfolio(.*)/);
-		    my @dir_list = &Apache::lonnet::dirlist($portfolio_path,$domain,$stuname,$portfolio_root);
+                    my $getpropath = 1;
+		    my @dir_list = &Apache::lonnet::dirlist($portfolio_root.$portfolio_path,$domain,$stuname,$getpropath);
 		    my $version = &get_next_version($answer_name, $answer_ext, \@dir_list);
                     # fix file name
                     my ($save_file_name) = (($directory.$answer_name.".$version.".$answer_ext) =~ /^.+\/${stuname}\/(.*)/);
@@ -2785,8 +2796,10 @@ sub handback_files {
             	                                $newflg.'_'.$part_resp.'_returndoc'.$file_counter,
             	                                $save_file_name);
                     if ($result !~ m|^/uploaded/|) {
-                        $request->print('<span class="LC_error">An error occurred ('.$result.
-                        ') while trying to upload '.$newflg.'_'.$part_resp.'_returndoc'.$file_counter.'</span><br />');
+                        $request->print('<br /><span class="LC_error">'.
+                            &mt('An error occurred ([_1]) while trying to upload [_2].',
+                                $result,$newflg.'_'.$part_resp.'_returndoc'.$file_counter).
+                                        '</span>');
                     } else {
                         # mark the file as read only
                         my @files = ($save_file_name);
@@ -2883,7 +2896,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;
@@ -2913,8 +2926,7 @@ sub version_portfiles {
     my $version_parts = join('|',@$v_flag);
     my @returned_keys;
     my $parts = join('|', @$parts_graded);
-    my $portfolio_root = &propath($domain,$stu_name).
-	'/userfiles/portfolio';
+    my $portfolio_root = '/userfiles/portfolio';
     foreach my $key (keys(%$record)) {
         my $new_portfiles;
         if ($key =~ /^resource\.($version_parts)\./ && $key =~ /\.portfiles$/ ) {
@@ -2925,7 +2937,8 @@ sub version_portfiles {
                 my ($directory,$answer_file) =($file =~ /^(.*?)([^\/]*)$/);
 		my ($answer_name,$answer_ver,$answer_ext) =
 		    &file_name_version_ext($answer_file);
-                my @dir_list = &Apache::lonnet::dirlist($directory,$domain,$stu_name,$portfolio_root);
+                my $getpropath = 1;    
+                my @dir_list = &Apache::lonnet::dirlist($portfolio_root.$directory,$domain,$stu_name,$getpropath);
                 my $version = &get_next_version($answer_name, $answer_ext, \@dir_list);
                 my $new_answer = &version_selected_portfile($domain, $stu_name, $directory, $answer_file, $version);
                 if ($new_answer ne 'problem getting file') {
@@ -3284,7 +3297,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>'.
@@ -3438,7 +3451,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++;
     }
@@ -4400,6 +4413,7 @@ sub displaySubByDates {
     }
 
     my $interaction;
+    my $no_increment = 1;
     for ($version=1;$version<=$$record{'version'};$version++) {
 	my $timestamp = 
 	    &Apache::lonlocal::locallocaltime($$record{$version.':timestamp'});
@@ -4443,7 +4457,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 />';
@@ -4496,12 +4511,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);
@@ -4510,7 +4525,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; 
@@ -4542,8 +4557,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=();
@@ -4587,10 +4602,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++;
@@ -4639,9 +4654,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 '';
@@ -4825,8 +4840,9 @@ sub get_response_bubbles {
 sub scantron_filenames {
     my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'};
     my $cname=$env{'course.'.$env{'request.course.id'}.'.num'};
+    my $getpropath = 1;
     my @files=&Apache::lonnet::dirlist('userfiles',$cdom,$cname,
-				    &propath($cdom,$cname));
+                                       $getpropath);
     my @possiblenames;
     foreach my $filename (sort(@files)) {
 	($filename)=split(/&/,$filename);
@@ -4869,19 +4885,76 @@ sub scantron_uploads {
 =cut
 
 sub scantron_scantab {
-    my $fh=Apache::File->new($Apache::lonnet::perlvar{'lonTabDir'}.'/scantronformat.tab');
     my $result='<select name="scantron_format">'."\n";
     $result.='<option></option>'."\n";
-    foreach my $line (<$fh>) {
-	my ($name,$descrip)=split(/:/,$line);
-	if ($name =~ /^\#/) { next; }
-	$result.='<option value="'.$name.'">'.$descrip.'</option>'."\n";
+    my @lines = &get_scantronformat_file();
+    if (@lines > 0) {
+        foreach my $line (@lines) {
+            next if (($line =~ /^\#/) || ($line eq ''));
+	    my ($name,$descrip)=split(/:/,$line);
+	    $result.='<option value="'.$name.'">'.$descrip.'</option>'."\n";
+        }
     }
     $result.='</select>'."\n";
-
     return $result;
 }
 
+=pod
+
+=item get_scantronformat_file
+
+  Returns an array containing lines from the scantron format file for
+  the domain of the course.
+
+  If a url for a custom.tab file is listed in domain's configuration.db, 
+  lines are from this file.
+
+  Otherwise, if a default.tab has been published in RES space by the 
+  domainconfig user, lines are from this file.
+
+  Otherwise, fall back to getting lines from the legacy file on the
+  local server:  /home/httpd/lonTabs/default_scantronformat.tab    
+
+=cut
+
+sub get_scantronformat_file {
+    my $cdom= $env{'course.'.$env{'request.course.id'}.'.domain'};
+    my %domconfig = &Apache::lonnet::get_dom('configuration',['scantron'],$cdom);
+    my $gottab = 0;
+    my @lines;
+    if (ref($domconfig{'scantron'}) eq 'HASH') {
+        if ($domconfig{'scantron'}{'scantronformat'} ne '') {
+            my $formatfile = &Apache::lonnet::getfile($Apache::lonnet::perlvar{'lonDocRoot'}.$domconfig{'scantron'}{'scantronformat'});
+            if ($formatfile ne '-1') {
+                @lines = split("\n",$formatfile,-1);
+                $gottab = 1;
+            }
+        }
+    }
+    if (!$gottab) {
+        my $confname = $cdom.'-domainconfig';
+        my $default = $Apache::lonnet::perlvar{'lonDocRoot'}.'/res/'.$cdom.'/'.$confname.'/default.tab';
+        my $formatfile =  &Apache::lonnet::getfile($default);
+        if ($formatfile ne '-1') {
+            @lines = split("\n",$formatfile,-1);
+            $gottab = 1;
+        }
+    }
+    if (!$gottab) {
+        my @domains = &Apache::lonnet::current_machine_domains();
+        if (grep(/^\Q$cdom\E$/,@domains)) {
+            my $fh=Apache::File->new($Apache::lonnet::perlvar{'lonTabDir'}.'/scantronformat.tab');
+            @lines = <$fh>;
+            close($fh);
+        } else {
+            my $fh=Apache::File->new($Apache::lonnet::perlvar{'lonTabDir'}.'/default_scantronformat.tab');
+            @lines = <$fh>;
+            close($fh);
+        }
+    }
+    return @lines;
+}
+
 =pod 
 
 =item scantron_CODElist
@@ -4914,11 +4987,11 @@ sub scantron_CODElist {
 =cut
 
 sub scantron_CODEunique {
-    my $result='<span style="white-space: nowrap;">
+    my $result='<span class="LC_nobreak">
                  <label><input type="radio" name="scantron_CODEunique"
                         value="yes" checked="checked" />'.&mt('Yes').' </label>
                 </span>
-                <span style="white-space: nowrap;">
+                <span class="LC_nobreak">
                  <label><input type="radio" name="scantron_CODEunique"
                         value="no" />'.&mt('No').' </label>
                 </span>';
@@ -5077,8 +5150,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
@@ -5140,10 +5242,10 @@ sub scantron_selectphase {
 
 sub get_scantron_config {
     my ($which) = @_;
-    my $fh=Apache::File->new($Apache::lonnet::perlvar{'lonTabDir'}.'/scantronformat.tab');
+    my @lines = &get_scantronformat_file();
     my %config;
     #FIXME probably should move to XML it has already gotten a bit much now
-    foreach my $line (<$fh>) {
+    foreach my $line (@lines) {
 	my ($name,$descrip)=split(/:/,$line);
 	if ($name ne $which ) { next; }
 	chomp($line);
@@ -6750,7 +6852,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') {
@@ -6770,7 +6872,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 {
@@ -6928,7 +7030,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) {
@@ -7144,7 +7246,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);
 }
@@ -7231,33 +7333,84 @@ sub scantron_get_maxbubble {
     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,
-        # reactionresponse and organicresponse 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()}) {
-                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 ($analysis,$parts) = &scantron_partids_tograde($resource,$cid,$uname,$udom);
+        if ((ref($analysis) eq 'HASH') && (ref($parts) eq 'ARRAY')) {
+            foreach my $part_id (@{$parts}) {
+
+                my $lines;
+
+	        # TODO - make this a persistent hash not an array.
+
+                # optionresponse, matchresponse and rankresponse type items 
+                # render as separate sub-questions in exam mode.
+                if (($analysis->{$part_id.'.type'} eq 'optionresponse') ||
+                    ($analysis->{$part_id.'.type'} eq 'matchresponse') ||
+                    ($analysis->{$part_id.'.type'} eq 'rankresponse')) {
+                    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'}});
+                        }
+                    } elsif ($analysis->{$part_id.'.type'} eq 'rankresponse') {
+                        if (ref($analysis->{$part_id.'.foils'}) eq 'ARRAY') {
+                            $numbub = scalar(@{$analysis->{$part_id.'.foils'}});
+                        }
                     }
-                }
-            }
+                    if (ref($analysis->{$part_id.'.shown'}) eq 'ARRAY') {
+                        $numshown = scalar(@{$analysis->{$part_id.'.shown'}});
+                    }
+                    my $bubbles_per_line = 10;
+                    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++) {
+                        $subdivided_bubble_lines{$response_number} .= 
+                            $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;
+	        $bubble_lines_per_response{$response_number} = $lines;
+                $responsetype_per_response{$response_number} = 
+                    $analysis->{$part_id.'.type'};
+	        $response_number++;
+
+	        $bubble_line +=  $lines;
+	        $total_lines +=  $lines;
+	    }
         }
-	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);
+    }
+    &Apache::lonnet::delenv('scantron\.');
+
+    &save_bubble_lines();
+    $env{'form.scantron_maxbubble'} =
+	$total_lines;
+    return $env{'form.scantron_maxbubble'};
+}
 
-        my @parts;
+sub scantron_partids_tograde {
+    my ($resource,$cid,$uname,$udom) = @_;
+    my (%analysis,@parts);
 
-	my %analysis = &Apache::lonnet::str2hash($an);
+    if (ref($resource)) {
+        my $symb = $resource->symb();
+        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);
+        %analysis = &Apache::lonnet::str2hash($an);
 
         if (ref($analysis{'parts'}) eq 'ARRAY') {
             foreach my $part (@{$analysis{'parts'}}) {
@@ -7267,73 +7420,8 @@ 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);
-                }
-            }
-        }
-
-	foreach my $part_id (@parts) {
-            my $lines = $analysis{"$part_id.bubble_lines"};
-
-	    # TODO - make this a persistent hash not an array.
-
-            # optionresponse, matchresponse and rankresponse type items 
-            # render as separate sub-questions in exam mode.
-            if (($analysis{$part_id.'.type'} eq 'optionresponse') ||
-                ($analysis{$part_id.'.type'} eq 'matchresponse') ||
-                ($analysis{$part_id.'.type'} eq 'rankresponse')) {
-                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'}});
-                    }
-                } elsif ($analysis{$part_id.'.type'} eq 'rankresponse') {
-                    if (ref($analysis{$part_id.'.foils'}) eq 'ARRAY') {
-                        $numbub = scalar(@{$analysis{$part_id.'.foils'}});
-                    }
-                }
-                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;
-            $responsetype_per_response{$response_number} = 
-                $analysis{$part_id.'.type'};
-	    $response_number++;
-
-	    $bubble_line +=  $lines;
-	    $total_lines +=  $lines;
-	}
-
     }
-    &Apache::lonnet::delenv('scantron\.');
-
-    &save_bubble_lines();
-    $env{'form.scantron_maxbubble'} =
-	$total_lines;
-    return $env{'form.scantron_maxbubble'};
+    return (\%analysis,\@parts);
 }
 
 =pod
@@ -7440,6 +7528,14 @@ sub scantron_process_students {
     my $navmap=Apache::lonnavmaps::navmap->new();
     my $map=$navmap->getResourceByUrl($sequence);
     my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0);
+
+    my ($uname,$udom,%partids_by_symb);
+    foreach my $resource (@resources) {
+        my $ressymb = $resource->symb();
+        my ($analysis,$parts) =
+            &scantron_partids_tograde($resource,$env{'request.course.id'},$uname,$udom);
+        $partids_by_symb{$ressymb} = $parts;
+    }
 #    $r->print("geto ".scalar(@resources)."<br />");
     my $result= <<SCANTRONFORM;
 <form method="post" enctype="multipart/form-data" action="/adm/grades" name="scantronupload">
@@ -7449,17 +7545,19 @@ SCANTRONFORM
     $r->print($result);
 
     my @delayqueue;
-    my %completedstudents;
+    my (%completedstudents,,%scandata);
     
+    my $lock=&Apache::lonnet::set_lock(&mt('Grading bubblesheet exam'));
     my $count=&get_todo_count($scanlines,$scan_data);
     my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin($r,'Scantron Status',
  				    'Scantron Progress',$count,
 				    'inline',undef,'scantronupload');
     &Apache::lonhtmlcommon::Update_PrgWin($r,\%prog_state,
 					  'Processing first student');
+    $r->print('<br />');
     my $start=&Time::HiRes::time();
     my $i=-1;
-    my ($uname,$udom,$started);
+    my $started;
 
     &scantron_get_maxbubble();	# Need the bubble lines array to parse.
     
@@ -7471,9 +7569,13 @@ SCANTRONFORM
 	$r->print("</form>");
 	&ssi_print_error($r);
 	$r->print(&show_grading_menu_form($symb));
+        &Apache::lonnet::remove_lock($lock);
 	return '';		# Dunno why the other returns return '' rather than just returning.
     }
 
+    my %lettdig = &letter_to_digits();
+    my $numletts = scalar(keys(%lettdig));
+
     while ($i<$scanlines->{'count'}) {
  	($uname,$udom)=('','');
  	$i++;
@@ -7505,41 +7607,87 @@ SCANTRONFORM
 	if (&scantron_clear_skip($scanlines,$scan_data,$i)) {
 	    &scantron_putfile($scanlines,$scan_data);
 	}
-	
-	my $i=0;
-	foreach my $resource (@resources) {
-	    $i++;
-	    my %form=('submitted'     =>'scantron',
-		      'grade_target'  =>'grade',
-		      'grade_username'=>$uname,
-		      'grade_domain'  =>$udom,
-		      'grade_courseid'=>$env{'request.course.id'},
-		      'grade_symb'    =>$resource->symb());
-	    if (exists($scan_record->{'scantron.CODE'})
-		&& 
-		&Apache::lonnet::validCODE($scan_record->{'scantron.CODE'})) {
-		$form{'CODE'}=$scan_record->{'scantron.CODE'};
-	    } else {
-		$form{'CODE'}='';
-	    } 
-	    my $result=&ssi_with_retries($resource->src(), $ssi_retries, %form);
-	    if ($ssi_error) {
-		$ssi_error = 0;	# So end of handler error message does not trigger.
-		$r->print("</form>");
-		&ssi_print_error($r);
-		$r->print(&show_grading_menu_form($symb));
-		return '';	# Why return ''?  Beats me.
-	    }
 
-	    if (&Apache::loncommon::connection_aborted($r)) { last; }
-	}
+        my $scancode;
+        if ((exists($scan_record->{'scantron.CODE'})) &&
+            (&Apache::lonnet::validCODE($scan_record->{'scantron.CODE'}))) {
+            $scancode = $scan_record->{'scantron.CODE'};
+        } else {
+            $scancode = '';
+        }
+
+        if (&grade_student_bubbles($r,$uname,$udom,$scan_record,$scancode,
+                                   @resources) eq 'ssi_error') {
+            $ssi_error = 0; # So end of handler error message does not trigger.
+            $r->print("</form>");
+            &ssi_print_error($r);
+            $r->print(&show_grading_menu_form($symb));
+            &Apache::lonnet::remove_lock($lock);
+            return '';      # Why return ''?  Beats me.
+        }
+
 	$completedstudents{$uname}={'line'=>$line};
+        if ($env{'form.verifyrecord'}) {
+            my $lastpos = $env{'form.scantron_maxbubble'}*$scantron_config{'Qlength'};
+            my $studentdata = substr($line,$scantron_config{'Qstart'}-1,$lastpos);
+            chomp($studentdata);
+            $studentdata =~ s/\r$//;
+            my $studentrecord = '';
+            my $counter = -1;
+            foreach my $resource (@resources) {
+                ($counter,my $recording) =
+                    &verify_scantron_grading($resource,$udom,$uname,$env{'request.course.id'},
+                                             $counter,$studentdata,\%partids_by_symb,
+                                             \%scantron_config,\%lettdig,$numletts);
+                $studentrecord .= $recording;
+            }
+            if ($studentrecord ne $studentdata) {
+                $counter = -1;
+                $studentrecord = '';
+                foreach my $resource (@resources) {
+                    ($counter,my $recording) =
+                        &verify_scantron_grading($resource,$udom,$uname,$env{'request.course.id'},
+                                                 $counter,$studentdata,\%partids_by_symb,
+                                                 \%scantron_config,\%lettdig,$numletts);
+                    $studentrecord .= $recording;
+                }
+                if ($studentrecord ne $studentdata) {
+                    $r->print('<p><span class="LC_error">');
+                    if ($scancode eq '') {
+                        $r->print(&mt('Mismatch grading bubble sheet for user: [_1] with ID: [_2].',
+                                  $uname.':'.$udom,$scan_record->{'scantron.ID'}));
+                    } else {
+                        $r->print(&mt('Mismatch grading bubble sheet for user: [_1] with ID: [_2] and CODE: [_3].',
+                                  $uname.':'.$udom,$scan_record->{'scantron.ID'},$scancode));
+                    }
+                    $r->print('</span><br />'.&Apache::loncommon::start_data_table()."\n".
+                              &Apache::loncommon::start_data_table_header_row()."\n".
+                              '<th>'.&mt('Source').'</th><th>'.&mt('Bubbled responses').'</th>'.
+                              &Apache::loncommon::end_data_table_header_row()."\n".
+                              &Apache::loncommon::start_data_table_row().
+                              '<td>'.&mt('Bubble Sheet').'</td>'.
+                              '<td><span class="LC_nobreak">'.$studentdata.'</span></td>'.
+                              &Apache::loncommon::end_data_table_row().
+                              &Apache::loncommon::start_data_table_row().
+                              '<td>Stored submissions</td>'.
+                              '<td><span class="LC_nobreak">'.$studentrecord.'</span></td>'."\n".
+                              &Apache::loncommon::end_data_table_row().
+                              &Apache::loncommon::end_data_table().'</p>');
+                } else {
+                    $r->print('<br /><span class="LC_warning">'.
+                             &mt('A second grading pass was needed for user: [_1] with ID: [_2], because a mismatch was seen on the first pass.',$uname.':'.$udom,$scan_record->{'scantron.ID'}).'<br />'.
+                             &mt("As a consequence, this user's submission history records two tries.").
+                                 '</span><br />');
+                }
+            }
+        }
 	if (&Apache::loncommon::connection_aborted($r)) { last; }
     } continue {
 	&Apache::lonxml::clear_problem_counter();
 	&Apache::lonnet::delenv('scantron\.');
     }
     &Apache::lonhtmlcommon::Close_PrgWin($r,\%prog_state);
+    &Apache::lonnet::remove_lock($lock);
 #    my $lasttime = &Time::HiRes::time()-$start;
 #    $r->print("<p>took $lasttime</p>");
 
@@ -7548,6 +7696,23 @@ SCANTRONFORM
     return '';
 }
 
+sub grade_student_bubbles {
+    my ($r,$uname,$udom,$scan_record,$scancode,@resources) = @_;
+    foreach my $resource (@resources) {
+        my %form = ('submitted'     => 'scantron',
+                    'grade_target'  => 'grade',
+                    'grade_username'=> $uname,
+                    'grade_domain'  => $udom,
+                    'grade_courseid'=> $env{'request.course.id'},
+                    'grade_symb'    => $resource->symb(),
+                    'code'          => $scancode);
+        my $result=&ssi_with_retries($resource->src(),$ssi_retries,%form);
+        return 'ssi_error' if ($ssi_error);
+        last if (&Apache::loncommon::connection_aborted($r));
+    }
+    return;
+}
+
 =pod
 
 =item scantron_upload_scantron_data
@@ -7730,6 +7895,285 @@ sub scantron_download_scantron_data {
     return '';
 }
 
+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 = &letter_to_digits();
+    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 ($uname,$udom,%partids_by_symb);
+    foreach my $resource (@resources) {
+        my $ressymb = $resource->symb();
+        my ($analysis,$parts) =
+            &scantron_partids_tograde($resource,$env{'request.course.id'},$uname,$udom);
+        $partids_by_symb{$ressymb} = $parts;
+    }
+    my (%scandata,%lastname,%bylast);
+    $r->print('
+<form method="post" enctype="multipart/form-data" action="/adm/grades" name="checkscantron">'."\n");
+
+    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;
+        foreach my $resource (@resources) {
+            ($counter,my $recording) =
+                &verify_scantron_grading($resource,$domain,$username,$cid,$counter,
+                                         $scandata{$pid},\%partids_by_symb,
+                                         \%scantron_config,\%lettdig,$numletts);
+            $record{$pid} .= $recording;
+        }
+    }
+    &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;
+}
+
+sub verify_scantron_grading {
+    my ($resource,$domain,$username,$cid,$counter,$scandata,$partids_by_symb,
+        $scantron_config,$lettdig,$numletts) = @_;
+    my ($record,%expected,%startpos);
+    return ($counter,$record) if (!ref($resource));
+    return ($counter,$record) if (!$resource->is_problem());
+    my $symb = $resource->symb();
+    return ($counter,$record) if (ref($partids_by_symb) ne 'HASH');
+    return ($counter,$record) if (ref($partids_by_symb->{$symb}) ne 'ARRAY');
+    foreach my $part_id (@{$partids_by_symb->{$symb}}) {
+        $counter ++;
+        $expected{$part_id} = 0;
+        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} .= $scantron_config->{'Qoff'};
+                            }
+                        }
+                    } 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,$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 (@{$partids_by_symb->{$symb}}) {
+            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 .= $recorded{$part_id};
+        }
+    }
+    return ($counter,$record);
+}
+
+sub letter_to_digits {
+    my %lettdig = (
+                    A => 1,
+                    B => 2,
+                    C => 3,
+                    D => 4,
+                    E => 5,
+                    F => 6,
+                    G => 7,
+                    H => 8,
+                    I => 9,
+                    J => 0,
+                  );
+    return %lettdig;
+}
+
 =pod
 
 =back
@@ -7789,25 +8233,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('')});
@@ -7963,7 +8407,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";
 	}
@@ -8154,7 +8598,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'";
        }
@@ -8165,6 +8609,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',
@@ -8222,6 +8668,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>
@@ -8248,6 +8697,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') {
@@ -8266,6 +8728,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>';
@@ -8311,6 +8775,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='';
@@ -8326,6 +8793,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++) {
@@ -8369,7 +8840,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) {
@@ -8440,7 +8911,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*\,//;
     }
@@ -8514,10 +8985,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;
@@ -8527,7 +9003,7 @@ ENDHEADER
                 }
              }
           }
-          my $ave=$sum/(100*$number);
+          my $ave=$sum/(100*$realnumber);
 # Store
           my ($username,$domain)=split(/\:/,$user);
           my %grades=();
@@ -8666,6 +9142,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)");
 	}