--- loncom/homework/grades.pm	2008/05/23 22:14:25	1.521
+++ loncom/homework/grades.pm	2008/06/25 11:59:59	1.525
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # The LON-CAPA Grading handler
 #
-# $Id: grades.pm,v 1.521 2008/05/23 22:14:25 www Exp $
+# $Id: grades.pm,v 1.525 2008/06/25 11:59:59 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -240,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');
@@ -278,7 +278,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 +290,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 +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";
 		}
@@ -2225,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>');
@@ -2553,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}));
@@ -2561,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;
@@ -2589,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;
 		}
@@ -2604,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++;
     }
@@ -2612,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;
@@ -2658,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'}=
@@ -2693,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;
@@ -2720,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'};
@@ -2893,7 +2894,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 +3295,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 +3449,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 +4411,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 +4455,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 />';
@@ -5145,8 +5148,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
@@ -6818,7 +6850,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 +6870,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 +7028,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 +7244,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);
 }
@@ -7300,30 +7332,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 +7369,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 +7411,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 +7420,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;
@@ -7802,6 +7842,271 @@ 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 = (
+                    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");
+
+    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;
+}
+
 =pod
 
 =back
@@ -7861,25 +8166,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 +8340,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";
 	}
@@ -8325,15 +8630,18 @@ 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'}!~/\w/)) {
+    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;
@@ -8354,7 +8662,7 @@ 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]','<tt>'.$env{'form.givenanswer'}.'</tt>');
+        $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>';
@@ -8397,10 +8705,12 @@ sub process_clicker_file {
 <input type="hidden" name="probTitle" value="$env{'form.probTitle'}" />
 <input type="hidden" name="saveState"  value="$env{'form.saveState'}" />
 <input type="hidden" name="gradingmechanism" value="$env{'form.gradingmechanism'}" />
-<input type="hidden" name="givenanswer" value="$env{'form.givenanswer'}" />
 <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='';
@@ -8416,6 +8726,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++) {
@@ -8530,7 +8844,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*\,//;
     }
@@ -8604,10 +8918,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;
@@ -8617,7 +8936,7 @@ ENDHEADER
                 }
              }
           }
-          my $ave=$sum/(100*$number);
+          my $ave=$sum/(100*$realnumber);
 # Store
           my ($username,$domain)=split(/\:/,$user);
           my %grades=();
@@ -8756,6 +9075,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)");
 	}