--- loncom/homework/grades.pm	2009/02/18 07:06:12	1.552
+++ loncom/homework/grades.pm	2009/03/18 12:26:21	1.558
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # The LON-CAPA Grading handler
 #
-# $Id: grades.pm,v 1.552 2009/02/18 07:06:12 raeburn Exp $
+# $Id: grades.pm,v 1.558 2009/03/18 12:26:21 bisitz Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -234,28 +234,54 @@ sub reset_caches {
 
 {
     my %analyze_cache;
+    my %analyze_cache_formkeys;
 
     sub reset_analyze_cache {
 	undef(%analyze_cache);
+        undef(%analyze_cache_formkeys);
     }
 
     sub get_analyze {
-	my ($symb,$uname,$udom,$no_increment)=@_;
+	my ($symb,$uname,$udom,$no_increment,$add_to_hash)=@_;
 	my $key = "$symb\0$uname\0$udom";
-	return $analyze_cache{$key} if (exists($analyze_cache{$key}));
+	if (exists($analyze_cache{$key})) {
+            my $getupdate = 0;
+            if (ref($add_to_hash) eq 'HASH') {
+                foreach my $item (keys(%{$add_to_hash})) {
+                    if (ref($analyze_cache_formkeys{$key}) eq 'HASH') {
+                        if (!exists($analyze_cache_formkeys{$key}{$item})) {
+                            $getupdate = 1;
+                            last;
+                        }
+                    } else {
+                        $getupdate = 1;
+                    }
+                }
+            }
+            if (!$getupdate) {
+                return $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_noincrement' => $no_increment));
+        my %form = ('grade_target'      => 'analyze',
+                    'grade_domain'      => $udom,
+                    'grade_symb'        => $symb,
+                    'grade_courseid'    =>  $env{'request.course.id'},
+                    'grade_username'    => $uname,
+                    'grade_noincrement' => $no_increment);
+        if (ref($add_to_hash)) {
+            %form = (%form,%{$add_to_hash});
+        } 
+	my $subresult=&ssi_with_retries($url, $ssi_retries,%form);
 	(undef,$subresult)=split(/_HASH_REF__/,$subresult,2);
 	my %analyze=&Apache::lonnet::str2hash($subresult);
+        if (ref($add_to_hash) eq 'HASH') {
+            $analyze_cache_formkeys{$key} = $add_to_hash;
+        } else {
+            $analyze_cache_formkeys{$key} = {};
+        }
 	return $analyze_cache{$key} = \%analyze;
     }
 
@@ -268,12 +294,41 @@ sub reset_caches {
     sub get_radiobutton_correct_foil {
 	my ($partid,$respid,$symb,$uname,$udom)=@_;
 	my $analyze = &get_analyze($symb,$uname,$udom);
-	foreach my $foil (@{&get_order($partid,$respid,$symb,$uname,$udom)}) {
-	    if ($analyze->{"$partid.$respid.foil.value.$foil"} eq 'true') {
-		return $foil;
+        my $foils = &get_order($partid,$respid,$symb,$uname,$udom);
+        if (ref($foils) eq 'ARRAY') {
+	    foreach my $foil (@{$foils}) {
+	        if ($analyze->{"$partid.$respid.foil.value.$foil"} eq 'true') {
+		    return $foil;
+	        }
 	    }
 	}
     }
+
+    sub scantron_partids_tograde {
+        my ($resource,$cid,$uname,$udom,$check_for_randomlist) = @_;
+        my (%analysis,@parts);
+        if (ref($resource)) {
+            my $symb = $resource->symb();
+            my $add_to_form;
+            if ($check_for_randomlist) {
+                $add_to_form = { 'check_parts_withrandomlist' => 1,};
+            }
+            my $analyze = &get_analyze($symb,$uname,$udom,undef,$add_to_form);
+            if (ref($analyze) eq 'HASH') {
+                %analysis = %{$analyze};
+            }
+            if (ref($analysis{'parts'}) eq 'ARRAY') {
+                foreach my $part (@{$analysis{'parts'}}) {
+                    my ($id,$respid) = split(/\./,$part);
+                    if (!&Apache::loncommon::check_if_partid_hidden($id,$symb,$udom,$uname)) {
+                        push(@parts,$part);
+                    }
+                }
+            }
+        }
+        return (\%analysis,\@parts);
+    }
+
 }
 
 #--- Clean response type for display
@@ -699,7 +754,7 @@ sub verifyreceipt {
 
     my $title.=
 	'<h3><span class="LC_info">'.
-	&mt('Verifying Submission Receipt [_1]',$receipt).
+	&mt('Verifying  Receipt No. [_1]',$receipt).
 	'</span></h3>'."\n".
 	'<h4>'.&mt('<b>Resource: </b>[_1]',$env{'form.probTitle'}).
 	'</h4>'."\n";
@@ -2105,7 +2160,7 @@ KEYWORDS
 			}
 			$lastsubonly.='<b>'.&mt('Submitted Answer:').' </b>'.
 			    &cleanRecord($subval,$responsetype,$symb,$partid,
-					 $respid,\%record,$order);
+					 $respid,\%record,$order,undef,$uname,$udom);
 			if ($similar) {$lastsubonly.="<br /><br />$similar\n";}
 			$lastsubonly.='</div>';
 		    }
@@ -3742,7 +3797,7 @@ ENDPICK
 sub csvupload_fields {
     my ($symb) = @_;
     my (@parts) = &getpartlist($symb);
-    my @fields=(['ID','Student ID'],
+    my @fields=(['ID','Student/Employee ID'],
 		['username','Student Username'],
 		['domain','Student Domain']);
     my (undef,undef,$url) = &Apache::lonnet::decode_symb($symb);
@@ -4655,10 +4710,10 @@ Next each scanline is checked for any er
 bubbles' (it's an error because it may have been mis-scanned
 because too light bubbling), 'double bubble' (each bubble line should
 have no more that one letter picked), invalid or duplicated CODE,
-invalid student ID
+invalid student/employee ID
 
 If the CODE option is used that determines the randomization of the
-homework problems, either way the student ID is looked up into a
+homework problems, either way the student/employee ID is looked up into a
 username:domain.
 
 During the validation phase the instructor can choose to skip scanlines. 
@@ -4728,7 +4783,7 @@ sub getSequenceDropDown {
 }
 
 my %bubble_lines_per_response;     # no. bubble lines for each response.
-                                   # index is "symb.part_id"
+                                   # key is zero-based index - 0, 1, 2 ...
 
 my %first_bubble_line;             # First bubble line no. for each bubble.
 
@@ -4769,7 +4824,6 @@ sub restore_bubble_lines {
             $env{"form.scantron.responsetype.$line"};
 	$line++;
     }
-
 }
 
 #  Given the parsed scanline, get the response for 
@@ -4778,7 +4832,6 @@ sub restore_bubble_lines {
 sub get_response_bubbles {
     my ($parsed_line, $response)  = @_;
 
-
     my $bubble_line = $first_bubble_line{$response-1} +1;
     my $bubble_lines= $bubble_lines_per_response{$response-1};
     
@@ -5134,6 +5187,10 @@ sub scantron_selectphase {
               '<td> '.$format_selector.' </td>'."\n".
               &Apache::loncommon::end_data_table_row()."\n".
               &Apache::loncommon::start_data_table_row()."\n".
+              '<td> '.&mt('Options').' </td>'."\n".
+              '<td> <label><input type="checkbox" name="scantron_options_hidden" value="ignore_hidden"/> '.&mt('Skip hidden resources').'</label></td>'.
+              &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".
@@ -5177,8 +5234,8 @@ sub scantron_selectphase {
       CODEstart   - (only matter if a CODE exists) column in the line where
                      the CODE starts
       CODElength  - length of the CODE
-      IDstart     - column where the student ID number starts
-      IDlength    - length of the student ID info
+      IDstart     - column where the student/employee ID number starts
+      IDlength    - length of the student/employee ID info
       Qstart      - column where the information from the bubbled
                     'questions' start
       Qlength     - number of columns comprising a single bubble line from
@@ -5238,7 +5295,7 @@ sub get_scantron_config {
 
 =item username_to_idmap
 
-    creates a hash keyed by student id with values of the corresponding
+    creates a hash keyed by student/employee ID with values of the corresponding
     student username:domain.
 
   Arguments:
@@ -5277,7 +5334,7 @@ sub username_to_idmap {
     $whichline         - line number of the passed in scanline
     $field             - type of change to process 
                          (either 
-                          'ID'     -> correct the student ID number
+                          'ID'     -> correct the student/employee ID number
                           'CODE'   -> correct the CODE
                           'answer' -> fixup the submitted answers)
     
@@ -5451,7 +5508,7 @@ sub digits_to_letters {
        CODE_ignore_dup - 1 if the CODE is a duplicated use when unique
                             CODEs were selected, but the usage has been
                             forced by the operator
-       ID  - student ID
+       ID  - student/employee ID
        PaperID - if used, the ID number printed on the sheet when the 
                  paper was scanned
        FirstName - first name from the sheet
@@ -6761,7 +6818,7 @@ ENDSCRIPT
        ".&mt("[_1]Select[_2] a CODE from the list of all CODEs and use it.",
 	     "<a target='_blank' href='$href'>","</a>")."
     </label> 
-    ".&mt("Selected CODE is [_1]","<input readonly='true' type='text' size='8' name='scantron_CODE_selectedvalue' onfocus=\"javascript:change_radio('use_found')\" onchange=\"javascript:change_radio('use_found')\" />"));
+    ".&mt("Selected CODE is [_1]",'<input readonly="readonly" type="text" size="8" name="scantron_CODE_selectedvalue" onfocus="javascript:change_radio(\'use_found\')" onchange="javascript:change_radio(\'use_found\')" />'));
 	    $r->print("\n<br />");
 	}
 	$r->print("
@@ -7235,15 +7292,15 @@ sub scantron_get_maxbubble {
 
     &Apache::lonxml::clear_problem_counter();
 
-    my $uname       = $env{'form.student'};
-    my $udom        = $env{'form.userdom'};
+    my $uname       = $env{'user.name'};
+    my $udom        = $env{'user.domain'};
     my $cid         = $env{'request.course.id'};
     my $total_lines = 0;
     %bubble_lines_per_response = ();
     %first_bubble_line         = ();
     %subdivided_bubble_lines   = ();
     %responsetype_per_response = ();
-  
+
     my $response_number = 0;
     my $bubble_line     = 0;
     foreach my $resource (@resources) {
@@ -7310,33 +7367,6 @@ sub scantron_get_maxbubble {
     return $env{'form.scantron_maxbubble'};
 }
 
-sub scantron_partids_tograde {
-    my ($resource,$cid,$uname,$udom) = @_;
-    my (%analysis,@parts); 
-
-    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'}}) {
-                my ($id,$respid) = split(/\./,$part);
-                if (!&Apache::loncommon::check_if_partid_hidden($id,$symb,$udom,$uname)) {
-                    push(@parts,$part);
-                }
-            }
-        }
-    }
-    return (\%analysis,\@parts);
-}
-
 sub scantron_validate_missingbubbles {
     my ($r,$currentphase) = @_;
     #get student info
@@ -7408,15 +7438,24 @@ 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);
+    my (%grader_partids_by_symb,%grader_randomlists_by_symb);
+    &graders_resources_pass(\@resources,\%grader_partids_by_symb,
+                            \%grader_randomlists_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 $ressymb = $resource->symb();
+        my ($analysis,$parts) =
+            &scantron_partids_tograde($resource,$env{'request.course.id'},
+                                      $env{'user.name'},$env{'user.domain'},1);
+        $grader_partids_by_symb{$ressymb} = $parts;
+        if (ref($analysis) eq 'HASH') {
+            if (ref($analysis->{'parts_withrandomlist'}) eq 'ARRAY') {
+                $grader_randomlists_by_symb{$ressymb} = 
+                    $analysis->{'parts_withrandomlist'};
+            }
+        }
     }
-#    $r->print("geto ".scalar(@resources)."<br />");
+
+    my ($uname,$udom);
     my $result= <<SCANTRONFORM;
 <form method="post" enctype="multipart/form-data" action="/adm/grades" name="scantronupload">
   <input type="hidden" name="command" value="scantron_configphase" />
@@ -7440,7 +7479,6 @@ SCANTRONFORM
     my $started;
 
     &scantron_get_maxbubble();	# Need the bubble lines array to parse.
-    
 
     # If an ssi failed in scantron_get_maxbubble, put an error message out to
     # the user and return.
@@ -7481,6 +7519,19 @@ SCANTRONFORM
  	}
   	($uname,$udom)=split(/:/,$uname);
 
+        my %partids_by_symb;
+        foreach my $resource (@resources) {
+            my $ressymb = $resource->symb();
+            if ((exists($grader_randomlists_by_symb{$ressymb})) ||
+                (ref($grader_partids_by_symb{$ressymb}) ne 'ARRAY')) {
+                my ($analysis,$parts) =
+                    &scantron_partids_tograde($resource,$env{'request.course.id'},$uname,$udom);
+                $partids_by_symb{$ressymb} = $parts;
+            } else {
+                $partids_by_symb{$ressymb} = $grader_partids_by_symb{$ressymb};
+            }
+        }
+
 	&Apache::lonxml::clear_problem_counter();
   	&Apache::lonnet::appenv($scan_record);
 
@@ -7497,7 +7548,7 @@ SCANTRONFORM
         }
 
         if (&grade_student_bubbles($r,$uname,$udom,$scan_record,$scancode,
-                                   @resources) eq 'ssi_error') {
+                                   \@resources,\%partids_by_symb) eq 'ssi_error') {
             $ssi_error = 0; # So end of handler error message does not trigger.
             $r->print("</form>");
             &ssi_print_error($r);
@@ -7515,19 +7566,32 @@ SCANTRONFORM
             my $studentrecord = '';
             my $counter = -1;
             foreach my $resource (@resources) {
+                my $ressymb = $resource->symb();
                 ($counter,my $recording) =
                     &verify_scantron_grading($resource,$udom,$uname,$env{'request.course.id'},
-                                             $counter,$studentdata,\%partids_by_symb,
+                                             $counter,$studentdata,$partids_by_symb{$ressymb},
                                              \%scantron_config,\%lettdig,$numletts);
                 $studentrecord .= $recording;
             }
             if ($studentrecord ne $studentdata) {
+                &Apache::lonxml::clear_problem_counter();
+                if (&grade_student_bubbles($r,$uname,$udom,$scan_record,$scancode,
+                                           \@resources,\%partids_by_symb) 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);
+                    delete($completedstudents{$uname});
+                    return '';
+                }
                 $counter = -1;
                 $studentrecord = '';
                 foreach my $resource (@resources) {
+                    my $ressymb = $resource->symb();
                     ($counter,my $recording) =
                         &verify_scantron_grading($resource,$udom,$uname,$env{'request.course.id'},
-                                                 $counter,$studentdata,\%partids_by_symb,
+                                                 $counter,$studentdata,$partids_by_symb{$ressymb},
                                                  \%scantron_config,\%lettdig,$numletts);
                     $studentrecord .= $recording;
                 }
@@ -7576,19 +7640,54 @@ SCANTRONFORM
     return '';
 }
 
+sub graders_resources_pass {
+    my ($resources,$grader_partids_by_symb,$grader_randomlists_by_symb) = @_;
+    if ((ref($resources) eq 'ARRAY') && (ref($grader_partids_by_symb)) && 
+        (ref($grader_randomlists_by_symb) eq 'HASH')) {
+        foreach my $resource (@{$resources}) {
+            my $ressymb = $resource->symb();
+            my ($analysis,$parts) =
+                &scantron_partids_tograde($resource,$env{'request.course.id'},
+                                          $env{'user.name'},$env{'user.domain'},1);
+            $grader_partids_by_symb->{$ressymb} = $parts;
+            if (ref($analysis) eq 'HASH') {
+                if (ref($analysis->{'parts_withrandomlist'}) eq 'ARRAY') {
+                    $grader_randomlists_by_symb->{$ressymb} =
+                        $analysis->{'parts_withrandomlist'};
+                }
+            }
+        }
+    }
+    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));
+    my ($r,$uname,$udom,$scan_record,$scancode,$resources,$parts) = @_;
+    if (ref($resources) eq 'ARRAY') {
+        my $count = 0;
+        foreach my $resource (@{$resources}) {
+            my $ressymb = $resource->symb();
+            my %form = ('submitted'      => 'scantron',
+                        'grade_target'   => 'grade',
+                        'grade_username' => $uname,
+                        'grade_domain'   => $udom,
+                        'grade_courseid' => $env{'request.course.id'},
+                        'grade_symb'     => $ressymb,
+                        'CODE'           => $scancode
+                       );
+            if (ref($parts) eq 'HASH') {
+                if (ref($parts->{$ressymb}) eq 'ARRAY') {
+                    foreach my $part (@{$parts->{$ressymb}}) {
+                        $form{'scantron_questnum_start.'.$part} =
+                            1+$env{'form.scantron.first_bubble_line.'.$count};
+                        $count++;
+                    }
+                }
+            }
+            my $result=&ssi_with_retries($resource->src(),$ssi_retries,%form);
+            return 'ssi_error' if ($ssi_error);
+            last if (&Apache::loncommon::connection_aborted($r));
+        }
     }
     return;
 }
@@ -7760,14 +7859,11 @@ sub checkscantron_results {
     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 @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0);
+    my (%grader_partids_by_symb,%grader_randomlists_by_symb);
+    &graders_resources_pass(\@resources,\%grader_partids_by_symb,                             \%grader_randomlists_by_symb);
+
+    my ($uname,$udom);
     my (%scandata,%lastname,%bylast);
     $r->print('
 <form method="post" enctype="multipart/form-data" action="/adm/grades" name="checkscantron">'."\n");
@@ -7781,7 +7877,7 @@ sub checkscantron_results {
                                     'inline',undef,'checkscantron');
     my ($username,$domain,$started);
 
-    &Apache::grades::scantron_get_maxbubble();  # Need the bubble lines array to parse.
+    &scantron_get_maxbubble();  # Need the bubble lines array to parse.
 
     &Apache::lonhtmlcommon::Update_PrgWin($r,\%prog_state,
                                           'Processing first student');
@@ -7822,9 +7918,18 @@ sub checkscantron_results {
         ($username,$domain)=split(/:/,$uname);
         my $counter = -1;
         foreach my $resource (@resources) {
+            my $parts;
+            my $ressymb = $resource->symb();
+            if ((exists($grader_randomlists_by_symb{$ressymb})) ||
+                (ref($grader_partids_by_symb{$ressymb}) ne 'ARRAY')) {
+                (my $analysis,$parts) =
+                    &scantron_partids_tograde($resource,$env{'request.course.id'},$username,$domain);
+            } else {
+                $parts = $grader_partids_by_symb{$ressymb};
+            }
             ($counter,my $recording) =
                 &verify_scantron_grading($resource,$domain,$username,$cid,$counter,
-                                         $scandata{$pid},\%partids_by_symb,
+                                         $scandata{$pid},$parts,
                                          \%scantron_config,\%lettdig,$numletts);
             $record{$pid} .= $recording;
         }
@@ -7889,15 +7994,14 @@ sub checkscantron_results {
 }
 
 sub verify_scantron_grading {
-    my ($resource,$domain,$username,$cid,$counter,$scandata,$partids_by_symb,
+    my ($resource,$domain,$username,$cid,$counter,$scandata,$partids,
         $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}}) {
+    return ($counter,$record) if (ref($partids) ne 'ARRAY');
+    foreach my $part_id (@{$partids}) {
         $counter ++;
         $expected{$part_id} = 0;
         if ($env{"form.scantron.sub_bubblelines.$counter"}) {
@@ -7990,7 +8094,7 @@ sub verify_scantron_grading {
                 }
             }
         }
-        foreach my $part_id (@{$partids_by_symb->{$symb}}) {
+        foreach my $part_id (@{$partids}) {
             if ($recorded{$part_id} eq '') {
                 for (my $i=0; $i<$expected{$part_id}; $i++) {
                     for (my $j=0; $j<$scantron_config->{'Qlength'}; $j++) {
@@ -9159,7 +9263,7 @@ ssi_with_retries()
 =item  scantron_validate_ID() : 
 
    Validates all scanlines in the selected file to not have any
-   invalid or underspecified student IDs
+   invalid or underspecified student/employee IDs
 
 =back