--- loncom/homework/grades.pm	2007/07/25 00:00:23	1.424
+++ loncom/homework/grades.pm	2007/09/02 02:10:31	1.438
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # The LON-CAPA Grading handler
 #
-# $Id: grades.pm,v 1.424 2007/07/25 00:00:23 albertel Exp $
+# $Id: grades.pm,v 1.438 2007/09/02 02:10:31 www Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -45,8 +45,11 @@ use LONCAPA;
 
 use POSIX qw(floor);
 
-my %oldessays=();
+
 my %perm=();
+my %bubble_lines_per_response;     # no. bubble lines for each response.
+                                   # index is "symb.part_id"
+
 
 # ----- These first few routines are general use routines.----
 #
@@ -195,22 +198,54 @@ sub showResourceInfo {
     return $result,$responseType,$hdgrade,$partlist,$handgrade;
 }
 
+sub reset_caches {
+    &reset_analyze_cache();
+    &reset_perm();
+}
+
+{
+    my %analyze_cache;
 
-sub get_order {
-    my ($partid,$respid,$symb,$uname,$udom)=@_;
-    my (undef,undef,$url)=&Apache::lonnet::decode_symb($symb);
-    $url=&Apache::lonnet::clutter($url);
-    my $subresult=&Apache::lonnet::ssi($url,
-				       ('grade_target' => 'analyze'),
-				       ('grade_domain' => $udom),
-				       ('grade_symb' => $symb),
-				       ('grade_courseid' => 
-					        $env{'request.course.id'}),
-				       ('grade_username' => $uname));
-    (undef,$subresult)=split(/_HASH_REF__/,$subresult,2);
-    my %analyze=&Apache::lonnet::str2hash($subresult);
-    return ($analyze{"$partid.$respid.shown"});
+    sub reset_analyze_cache {
+	undef(%analyze_cache);
+    }
+
+    sub get_analyze {
+	my ($symb,$uname,$udom)=@_;
+	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=&Apache::lonnet::ssi($url,
+					   ('grade_target' => 'analyze'),
+					   ('grade_domain' => $udom),
+					   ('grade_symb' => $symb),
+					   ('grade_courseid' => 
+					    $env{'request.course.id'}),
+					   ('grade_username' => $uname));
+	(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);
+	return $analyze->{"$partid.$respid.shown"};
+    }
+
+    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;
+	    }
+	}
+    }
 }
+
 #--- Clean response type for display
 #--- Currently filters option/rank/radiobutton/match/essay/Task
 #        response types only.
@@ -259,11 +294,11 @@ sub cleanRecord {
     } elsif ($response eq 'radiobutton') {
 	my %answer=&Apache::lonnet::str2hash($answer);
 	my ($toprow,$bottomrow);
-	my $correct=($order->[0])+1;
-	for (my $i=1;$i<=$#$order;$i++) {
-	    my $foil=$order->[$i];
+	my $correct = 
+	    &get_radiobutton_correct_foil($partid,$respid,$symb,$uname,$udom);
+	foreach my $foil (@$order) {
 	    if (exists($answer{$foil})) {
-		if ($i == $correct) {
+		if ($foil eq $correct) {
 		    $toprow.='<td><b>true</b></td>';
 		} else {
 		    $toprow.='<td><i>true</i></td>';
@@ -538,7 +573,7 @@ sub compute_points {
 #
 
 sub most_similar {
-    my ($uname,$udom,$uessay)=@_;
+    my ($uname,$udom,$uessay,$old_essays)=@_;
 
 # ignore spaces and punctuation
 
@@ -555,23 +590,22 @@ sub most_similar {
     my $scrsid='';
     my $sessay='';
 # go through all essays ...
-    foreach my $tkey (keys %oldessays) {
-	my ($tname,$tdom,$tcrsid)=split(/\./,$tkey);
+    foreach my $tkey (keys(%$old_essays)) {
+	my ($tname,$tdom,$tcrsid)=map {&unescape($_)} (split(/\./,$tkey));
 # ... except the same student
-        if (($tname ne $uname) || ($tdom ne $udom)) {
-	    my $tessay=$oldessays{$tkey};
-            $tessay=~s/\W+/ /gs;
+        next if (($tname eq $uname) && ($tdom eq $udom));
+	my $tessay=$old_essays->{$tkey};
+	$tessay=~s/\W+/ /gs;
 # String similarity gives up if not even limit
-            my $tsimilar=&String::Similarity::similarity($uessay,$tessay,$limit);
+	my $tsimilar=&String::Similarity::similarity($uessay,$tessay,$limit);
 # Found one
-            if ($tsimilar>$limit) {
-		$limit=$tsimilar;
-                $sname=$tname;
-                $sdom=$tdom;
-                $scrsid=$tcrsid;
-                $sessay=$oldessays{$tkey};
-            }
-        } 
+	if ($tsimilar>$limit) {
+	    $limit=$tsimilar;
+	    $sname=$tname;
+	    $sdom=$tdom;
+	    $scrsid=$tcrsid;
+	    $sessay=$old_essays->{$tkey};
+	}
     }
     if ($limit>0.6) {
        return ($sname,$sdom,$scrsid,$sessay,$limit);
@@ -725,7 +759,6 @@ LISTJAVASCRIPT
 
     my $saveStatus = $env{'form.Status'} eq '' ? 'Active' : $env{'form.Status'};
     $env{'form.Status'} = $saveStatus;
-
     $gradeTable.='<label><input type="radio" name="lastSub" value="lastonly" '.$checklastsub.' /> last submission only </label>'."\n".
 	'<label><input type="radio" name="lastSub" value="last" /> last submission & parts info </label>'."\n".
 	'<label><input type="radio" name="lastSub" value="datesub" /> by dates and submissions </label>'."\n".
@@ -736,8 +769,7 @@ LISTJAVASCRIPT
         '<option value=".25">Quarter Points</option>'.
         '<option value=".1">Tenths of a Point</option>'.
         '</select>'.
-
-	'<input type="hidden" name="section"     value="'.$getsec.'" />'."\n".
+        &build_section_inputs().
 	'<input type="hidden" name="submitonly"  value="'.$submitonly.'" />'."\n".
 	'<input type="hidden" name="handgrade"   value="'.$env{'form.handgrade'}.'" /><br />'."\n".
 	'<input type="hidden" name="showgrading" value="'.$env{'form.showgrading'}.'" /><br />'."\n".
@@ -1663,6 +1695,19 @@ sub download_all_link {
     return
 }
 
+sub build_section_inputs {
+    my $section_inputs;
+    if ($env{'form.section'} eq '') {
+        $section_inputs .= '<input type="hidden" name="section" value="all" />'."\n";
+    } else {
+        my @sections = &Apache::loncommon::get_env_multiple('form.section');
+        foreach my $section (@sections) {
+            $section_inputs .= '<input type="hidden" name="section" value="'.$section.'" />'."\n";
+        }
+    }
+    return $section_inputs;
+}
+
 # --------------------------- show submissions of a student, option to grade 
 sub submission {
     my ($request,$counter,$total) = @_;
@@ -1691,6 +1736,7 @@ sub submission {
 	'" src="'.$request->dir_config('lonIconsURL').
 	'/check.gif" height="16" border="0" />';
 
+    my %old_essays;
     # header info
     if ($counter == 0) {
 	&sub_page_js($request);
@@ -1742,7 +1788,6 @@ sub submission {
 	    $env{'form.savemsgN'} = $keyhash{$symb.'_savemsgN'} ne '' ? $keyhash{$symb.'_savemsgN'} : '0';
 	}
 	my $overRideScore = $env{'form.overRideScore'} eq '' ? 'no' : $env{'form.overRideScore'};
-
 	$request->print('<form action="/adm/grades" method="post" name="SCORE" enctype="multipart/form-data">'."\n".
 			'<input type="hidden" name="command"    value="handgrade" />'."\n".
 			'<input type="hidden" name="saveState"  value="'.$env{'form.saveState'}.'" />'."\n".
@@ -1757,7 +1802,7 @@ sub submission {
 			'<input type="hidden" name="vProb"      value="'.$env{'form.vProb'}.'" />'."\n".
 			'<input type="hidden" name="vAns"       value="'.$env{'form.vAns'}.'" />'."\n".
 			'<input type="hidden" name="lastSub"    value="'.$env{'form.lastSub'}.'" />'."\n".
-			'<input type="hidden" name="section"    value="'.$env{'form.section'}.'" />'."\n".
+			&build_section_inputs().
 			'<input type="hidden" name="submitonly" value="'.$env{'form.submitonly'}.'" />'."\n".
 			'<input type="hidden" name="handgrade"  value="'.$env{'form.handgrade'}.'" />'."\n".
 			'<input type="hidden" name="NCT"'.
@@ -1805,7 +1850,7 @@ KEYWORDS
 	    my ($adom,$aname,$apath)=($essayurl=~/^($LONCAPA::domain_re)\/($LONCAPA::username_re)\/(.*)$/);
 	    $apath=&escape($apath);
 	    $apath=~s/\W/\_/gs;
-	    %oldessays=&Apache::lonnet::dump('nohist_essay_'.$apath,$adom,$aname);
+	    %old_essays=&Apache::lonnet::dump('nohist_essay_'.$apath,$adom,$aname);
         }
     }
 
@@ -1943,12 +1988,21 @@ KEYWORDS
 		    my $similar='';
 		    if($env{'form.checkPlag'}){
 			my ($oname,$odom,$ocrsid,$oessay,$osim)=
-			    &most_similar($uname,$udom,$subval);
+			    &most_similar($uname,$udom,$subval,\%old_essays);
 			if ($osim) {
 			    $osim=int($osim*100.0);
-			    $similar="<hr /><h3><span class=\"LC_warning\">Essay".
-				" is $osim% similar to an essay by ".
-				&Apache::loncommon::plainname($oname,$odom).
+			    my %old_course_desc = 
+				&Apache::lonnet::coursedescription($ocrsid,
+								   {'one_time' => 1});
+
+			    $similar="<hr /><h3><span class=\"LC_warning\">".
+				&mt('Essay is [_1]% similar to an essay by [_2] ([_3]:[_4]) in course [_5] (course id [_6]:[_7])',
+				    $osim,
+				    &Apache::loncommon::plainname($oname,$odom),
+				    $oname,$odom,
+				    $old_course_desc{'description'},
+				    $old_course_desc{'num'},
+				    $old_course_desc{'domain'}).
 				'</span></h3><blockquote><i>'.
 				&keywords_highlight($oessay).
 				'</i></blockquote><hr />';
@@ -2957,20 +3011,21 @@ sub viewgrades {
     $result.= '<form action="/adm/grades" method="post" name="classgrade">'."\n".
 	'<input type="hidden" name="symb"    value="'.&Apache::lonenc::check_encrypt($symb).'" />'."\n".
 	'<input type="hidden" name="command" value="editgrades" />'."\n".
-	'<input type="hidden" name="section" value="'.$env{'form.section'}.'" />'."\n".
+	&build_section_inputs().
 	'<input type="hidden" name="saveState" value="'.$env{'form.saveState'}.'" />'."\n".
 	'<input type="hidden" name="Status" value="'.$env{'form.Status'}.'" />'."\n".
 	'<input type="hidden" name="probTitle" value="'.$env{'form.probTitle'}.'" />'."\n";
 
     my $sectionClass;
+    my $section_display = join (", ",&Apache::loncommon::get_env_multiple('form.section'));
     if ($env{'form.section'} eq 'all') {
 	$sectionClass='Class </h3>';
     } elsif ($env{'form.section'} eq 'none') {
-	$sectionClass='Students in no Section </h3>';
+	$sectionClass=&mt('Students in no Section').'</h3>';
     } else {
-	$sectionClass='Students in Section '.$env{'form.section'}.'</h3>';
+	$sectionClass=&mt('Students in Section(s) [_1]',$section_display).'</h3>';
     }
-    $result.='<h3>Assign Common Grade To '.$sectionClass;
+    $result.='<h3>'.&mt('Assign Common Grade To [_1]',$sectionClass);
     $result.= '<table border=0><tr><td bgcolor="#777777">'."\n".
 	'<table border=0><tr bgcolor="#ffffdd"><td>';
     #radio buttons/text box for assigning points for a section or class.
@@ -3076,8 +3131,11 @@ sub viewgrades {
 	'onClick="javascript:submit();" target="_self" /></form>'."\n";
     if (scalar(%$fullname) eq 0) {
 	my $colspan=3+scalar(@parts);
-	$result='<span class="LC_warning">There are no students in section "'.$env{'form.section'}.
-	    '" with enrollment status "'.$env{'form.Status'}.'" to modify or grade.</span>';
+	my $section_display = join (", ",&Apache::loncommon::get_env_multiple('form.section'));
+	$result='<span class="LC_warning">'.
+	    &mt('There are no students in section(s) [_1] with enrollment status [_2] to modify or grade',
+	        $section_display, $env{'form.Status'}).
+	    '</span>';
     }
     $result.=&show_grading_menu_form($symb);
     return $result;
@@ -3154,9 +3212,10 @@ sub editgrades {
     my ($request) = @_;
 
     my $symb=&get_symb($request);
-    my $title='<h3><span class="LC_info">Current Grade Status</span></h3>';
-    $title.='<h4><b>Current Resource: </b>'.$env{'form.probTitle'}.'</h4><br />'."\n";
-    $title.='<h4><b>Section: </b>'.$env{'form.section'}.'</h4>'."\n";
+    my $section_display = join (", ",&Apache::loncommon::get_env_multiple('form.section'));
+    my $title='<h3><span class="LC_info">'.&mt('Current Grade Status').'</span></h3>';
+    $title.='<h4>'.&mt('<b>Current Resource: </b>[_1]',$env{'form.probTitle'}).'</h4><br />'."\n";
+    $title.='<h4>'.&mt('<b>Section: </b>[_1]',$section_display).'</h4>'."\n";
 
     my $result= '<table border="0"><tr><td bgcolor="#777777">'."\n";
     $result.= '<table border="0"><tr bgcolor="#deffff">'.
@@ -3857,9 +3916,9 @@ LISTJAVASCRIPT
 	'<label><input type="radio" name="lastSub" value="none" /> none</label>'."\n".
 	'<label><input type="radio" name="lastSub" value="datesub" checked="checked" /> by dates and submissions</label>'."\n".
 	'<label><input type="radio" name="lastSub" value="all" /> all details</label>'."\n";
-
-    $result.='<input type="hidden" name="section"     value="'.$getsec.'" />'."\n".
-	'<input type="hidden" name="Status"  value="'.$env{'form.Status'}.'" />'."\n".
+    
+    $result.=&build_section_inputs();
+    $result.='<input type="hidden" name="Status"  value="'.$env{'form.Status'}.'" />'."\n".
 	'<input type="hidden" name="command" value="displayPage" />'."\n".
 	'<input type="hidden" name="symb"    value="'.&Apache::lonenc::check_encrypt($symb).'" />'."\n".
 	'<input type="hidden" name="saveState" value="'.$env{'form.saveState'}.'" />'."<br />\n";
@@ -4373,7 +4432,7 @@ one of the predefined configurations for
 like.
 
 Next each scanline is checked for any errors of either 'missing
-bubbles' (it's an error because it may have been missed scanned
+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
@@ -4384,7 +4443,7 @@ username:domain.
 
 During the validation phase the instructor can choose to skip scanlines. 
 
-After the validation phase, there is now 3 bubble sheet files
+After the validation phase, there are now 3 bubble sheet files
 
   scantron_original_filename (unmodified original file)
   scantron_corrected_filename (file where the corrected information has replaced the original information)
@@ -5666,6 +5725,26 @@ sub scantron_remove_scan_data {
          count     - number of scanlines
  
      - second is the scan_data hash possible keys are
+       ($number refers to scanline numbered $number and thus the key affects
+        only that scanline
+        $bubline refers to the specific bubble line element and the aspects
+        refers to that specific bubble line element)
+
+       $number.user - username:domain to use
+       $number.CODE_ignore_dup 
+                    - ignore the duplicate CODE error 
+       $number.useCODE
+                    - use the CODE in the scanline as is
+       $number.no_bubble.$bubline
+                    - it is valid that there is no bubbled in bubble
+                      at $number $bubline
+       remember_skipping
+                    - a frozen hash containing keys of $number and values
+                      of either 
+                        1 - we are on a 'do skipped records pass' and plan
+                            on processing this line
+                        2 - we are on a 'do skipped records pass' and this
+                            scanline has been marked to skip yet again
 
 =cut
 
@@ -6434,6 +6513,7 @@ sub scantron_validate_doublebubble {
 =cut
 
 sub scantron_get_maxbubble {    
+
     if (defined($env{'form.scantron_maxbubble'}) &&
 	$env{'form.scantron_maxbubble'}) {
 	return $env{'form.scantron_maxbubble'};
@@ -6448,14 +6528,40 @@ sub scantron_get_maxbubble {
 
     &Apache::lonxml::clear_problem_counter();
 
+    my $uname       = $env{'form.student'};
+    my $udom        = $env{'form.userdom'};
+    my $cid         = $env{'request.course.id'};
+    my $total_lines = 0;
+    %bubble_lines_per_response = ();
+
     foreach my $resource (@resources) {
+	my $symb = $resource->symb();
 	my $result=&Apache::lonnet::ssi($resource->src(),
-					('symb' => $resource->symb()));
+					('symb' => $resource->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);
+
+
+
+	foreach my $part_id (@{$analysis{'parts'}}) {
+	    my $bubble_lines = $analysis{"$part_id.bubble_lines"}[0];
+	    if (!$bubble_lines) {
+		$bubble_lines = 1;
+	    }
+	    $bubble_lines_per_response{"$symb.$part_id"} = $bubble_lines;
+	    $total_lines = $total_lines + $bubble_lines;
+	}
+
     }
     &Apache::lonnet::delenv('scantron\.');
     $env{'form.scantron_maxbubble'} =
-	&Apache::lonxml::get_problem_counter()-1;
-
+	$total_lines;
     return $env{'form.scantron_maxbubble'};
 }
 
@@ -6904,7 +7010,7 @@ GRADINGMENUJS
 
     $result.='<table width="100%" border="0">';
     $result.='<tr bgcolor="#ffffe6" valign="top"><td>'."\n".
-	'&nbsp;'.&mt('Select Section').': <select name="section">'."\n";
+	'&nbsp;'.&mt('Select Section').': <select name="section" multiple="multiple" size="3">'."\n";
     if (ref($sections)) {
 	foreach (sort (@$sections)) {
 	    $result.='<option value="'.$_.'" '.
@@ -7012,9 +7118,10 @@ sub gather_clicker_ids {
     # Set up a couple variables.
     my $username_idx = &Apache::loncoursedata::CL_SNAME();
     my $domain_idx   = &Apache::loncoursedata::CL_SDOM();
+    my $status_idx   = &Apache::loncoursedata::CL_STATUS();
 
     foreach my $student (keys(%$classlist)) {
-
+        if ($classlist->{$student}->[$status_idx] ne 'Active') { next; }
         my $username = $classlist->{$student}->[$username_idx];
         my $domain   = $classlist->{$student}->[$domain_idx];
         my $clickers =
@@ -7275,8 +7382,21 @@ ENDHEADER
           $result.="\n".'<input type="hidden" name="correct:'.$correct_count.':'.$correct_ids{$id}.'" value="'.$responses{$id}.'" />';
           $correct_count++;
        } elsif ($clicker_ids{$id}) {
-          $result.="\n".'<input type="hidden" name="student:'.$clicker_ids{$id}.'" value="'.$responses{$id}.'" />';
-          $student_count++;
+          if ($clicker_ids{$id}=~/\,/) {
+# More than one user with the same clicker!
+             $result.="\n<hr />".&mt('Clicker registered more than once').": <tt>".$id."</tt><br />";
+             $result.="\n".'<input type="hidden" name="unknown:'.$id.'" value="'.$responses{$id}.'" />'.
+                           "<select name='multi".$id."'>";
+             foreach my $reguser (sort(split(/\,/,$clicker_ids{$id}))) {
+                 $result.="<option value='".$reguser."'>".&Apache::loncommon::plainname(split(/\:/,$reguser)).' ('.$reguser.')</option>';
+             }
+             $result.='</select>';
+             $unknown_count++;
+          } else {
+# Good: found one and only one user with the right clicker
+             $result.="\n".'<input type="hidden" name="student:'.$clicker_ids{$id}.'" value="'.$responses{$id}.'" />';
+             $student_count++;
+          }
        } else {
           $result.="\n<hr />".&mt('Unregistered Clicker')." <tt>".$id."</tt><br />";
           $result.="\n".'<input type="hidden" name="unknown:'.$id.'" value="'.$responses{$id}.'" />'.
@@ -7296,6 +7416,9 @@ ENDHEADER
           $result.='<br /><span class="LC_warning">'.&mt("Found [_1] entries for grading!",$correct_count).'</span>';
        }
     }
+    if ($number<1) {
+       $errormsg.="Found no questions.";
+    }
     if ($errormsg) {
        $result.='<br /><span class="LC_error">'.&mt($errormsg).'</span>';
     } else {
@@ -7424,6 +7547,8 @@ ENDHEADER
           my $id=$1;
           if (($env{'form.uname'.$id}) && ($env{'form.udom'.$id})) {
              $user=$env{'form.uname'.$id}.':'.$env{'form.udom'.$id};
+          } elsif ($env{'form.multi'.$id}) {
+             $user=$env{'form.multi'.$id};
           }
        }
        if ($user) { 
@@ -7469,7 +7594,7 @@ ENDHEADER
 sub handler {
     my $request=$_[0];
 
-    &reset_perm();
+    &reset_caches();
     if ($env{'browser.mathml'}) {
 	&Apache::loncommon::content_type($request,'text/xml');
     } else {
@@ -7582,6 +7707,7 @@ sub handler {
 	}
     }
     $request->print(&Apache::loncommon::end_page());
+    &reset_caches();
     return '';
 }