File:  [LON-CAPA] / loncom / homework / grades.pm
Revision 1.577: download - view: text, annotated - select for diffs
Thu May 28 14:30:04 2009 UTC (15 years ago) by bisitz
Branches: MAIN
CVS tags: HEAD
Localization:
- Resolved translation issues with ID: Now use "Part ID" instead of just "ID"
- Optimized &mt usage for "Part", "Part ID" and other
  (Note: Values are also displayed in bold now. Redesign expected anyway.)
- Added missing &mt calls
- Added missing translations (more to do)

- Optimized nowrap
- de.pm: some other little updates/corrections

    1: # The LearningOnline Network with CAPA
    2: # The LON-CAPA Grading handler
    3: #
    4: # $Id: grades.pm,v 1.577 2009/05/28 14:30:04 bisitz Exp $
    5: #
    6: # Copyright Michigan State University Board of Trustees
    7: #
    8: # This file is part of the LearningOnline Network with CAPA (LON-CAPA).
    9: #
   10: # LON-CAPA is free software; you can redistribute it and/or modify
   11: # it under the terms of the GNU General Public License as published by
   12: # the Free Software Foundation; either version 2 of the License, or
   13: # (at your option) any later version.
   14: #
   15: # LON-CAPA is distributed in the hope that it will be useful,
   16: # but WITHOUT ANY WARRANTY; without even the implied warranty of
   17: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   18: # GNU General Public License for more details.
   19: #
   20: # You should have received a copy of the GNU General Public License
   21: # along with LON-CAPA; if not, write to the Free Software
   22: # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
   23: #
   24: # /home/httpd/html/adm/gpl.txt
   25: #
   26: # http://www.lon-capa.org/
   27: #
   28: 
   29: 
   30: 
   31: package Apache::grades;
   32: use strict;
   33: use Apache::style;
   34: use Apache::lonxml;
   35: use Apache::lonnet;
   36: use Apache::loncommon;
   37: use Apache::lonhtmlcommon;
   38: use Apache::lonnavmaps;
   39: use Apache::lonhomework;
   40: use Apache::lonpickcode;
   41: use Apache::loncoursedata;
   42: use Apache::lonmsg();
   43: use Apache::Constants qw(:common);
   44: use Apache::lonlocal;
   45: use Apache::lonenc;
   46: use String::Similarity;
   47: use LONCAPA;
   48: 
   49: use POSIX qw(floor);
   50: 
   51: 
   52: 
   53: my %perm=();
   54: 
   55: #  These variables are used to recover from ssi errors
   56: 
   57: my $ssi_retries = 5;
   58: my $ssi_error;
   59: my $ssi_error_resource;
   60: my $ssi_error_message;
   61: 
   62: 
   63: sub ssi_with_retries {
   64:     my ($resource, $retries, %form) = @_;
   65:     my ($content, $response) = &Apache::loncommon::ssi_with_retries($resource, $retries, %form);
   66:     if ($response->is_error) {
   67: 	$ssi_error          = 1;
   68: 	$ssi_error_resource = $resource;
   69: 	$ssi_error_message  = $response->code . " " . $response->message;
   70:     }
   71: 
   72:     return $content;
   73: 
   74: }
   75: #
   76: #  Prodcuces an ssi retry failure error message to the user:
   77: #
   78: 
   79: sub ssi_print_error {
   80:     my ($r) = @_;
   81:     my $helpurl = &Apache::loncommon::top_nav_help('Helpdesk');
   82:     $r->print('
   83: <br />
   84: <h2>'.&mt('An unrecoverable network error occurred:').'</h2>
   85: <p>
   86: '.&mt('Unable to retrieve a resource from a server:').'<br />
   87: '.&mt('Resource:').' '.$ssi_error_resource.'<br />
   88: '.&mt('Error:').' '.$ssi_error_message.'
   89: </p>
   90: <p>'.
   91: &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 />'.
   92: &mt('If the error persists, please contact the [_1] for assistance.',$helpurl).
   93: '</p>');
   94:     return;
   95: }
   96: 
   97: #
   98: # --- Retrieve the parts from the metadata file.---
   99: sub getpartlist {
  100:     my ($symb) = @_;
  101: 
  102:     my $navmap   = Apache::lonnavmaps::navmap->new();
  103:     my $res      = $navmap->getBySymb($symb);
  104:     my $partlist = $res->parts();
  105:     my $url      = $res->src();
  106:     my @metakeys = split(/,/,&Apache::lonnet::metadata($url,'keys'));
  107: 
  108:     my @stores;
  109:     foreach my $part (@{ $partlist }) {
  110: 	foreach my $key (@metakeys) {
  111: 	    if ($key =~ m/^stores_\Q$part\E_/) { push(@stores,$key); }
  112: 	}
  113:     }
  114:     return @stores;
  115: }
  116: 
  117: # --- Get the symbolic name of a problem and the url
  118: sub get_symb {
  119:     my ($request,$silent) = @_;
  120:     (my $url=$env{'form.url'}) =~ s-^http://($ENV{'SERVER_NAME'}|$ENV{'HTTP_HOST'})--;
  121:     my $symb=($env{'form.symb'} ne '' ? $env{'form.symb'} : (&Apache::lonnet::symbread($url)));
  122:     if ($symb eq '') { 
  123: 	if (!$silent) {
  124: 	    $request->print("Unable to handle ambiguous references:$url:.");
  125: 	    return ();
  126: 	}
  127:     }
  128:     &Apache::lonenc::check_decrypt(\$symb);
  129:     return ($symb);
  130: }
  131: 
  132: #--- Format fullname, username:domain if different for display
  133: #--- Use anywhere where the student names are listed
  134: sub nameUserString {
  135:     my ($type,$fullname,$uname,$udom) = @_;
  136:     if ($type eq 'header') {
  137: 	return '<b>&nbsp;'.&mt('Fullname').'&nbsp;</b><span class="LC_internal_info">('.&mt('Username').')</span>';
  138:     } else {
  139: 	return '&nbsp;'.$fullname.'<span class="LC_internal_info">&nbsp;('.$uname.
  140: 	    ($env{'user.domain'} eq $udom ? '' : ' ('.$udom.')').')</span>';
  141:     }
  142: }
  143: 
  144: #--- Get the partlist and the response type for a given problem. ---
  145: #--- Indicate if a response type is coded handgraded or not. ---
  146: sub response_type {
  147:     my ($symb) = shift;
  148: 
  149:     my $navmap = Apache::lonnavmaps::navmap->new();
  150:     my $res = $navmap->getBySymb($symb);
  151:     my $partlist = $res->parts();
  152:     my %vPart = 
  153: 	map { $_ => 1 } (&Apache::loncommon::get_env_multiple('form.vPart'));
  154:     my (%response_types,%handgrade);
  155:     foreach my $part (@{ $partlist }) {
  156: 	next if (%vPart && !exists($vPart{$part}));
  157: 
  158: 	my @types = $res->responseType($part);
  159: 	my @ids = $res->responseIds($part);
  160: 	for (my $i=0; $i < scalar(@ids); $i++) {
  161: 	    $response_types{$part}{$ids[$i]} = $types[$i];
  162: 	    $handgrade{$part.'_'.$ids[$i]} = 
  163: 		&Apache::lonnet::EXT('resource.'.$part.'_'.$ids[$i].
  164: 				     '.handgrade',$symb);
  165: 	}
  166:     }
  167:     return ($partlist,\%handgrade,\%response_types);
  168: }
  169: 
  170: sub flatten_responseType {
  171:     my ($responseType) = @_;
  172:     my @part_response_id =
  173: 	map { 
  174: 	    my $part = $_;
  175: 	    map {
  176: 		[$part,$_]
  177: 		} sort(keys(%{ $responseType->{$part} }));
  178: 	} sort(keys(%$responseType));
  179:     return @part_response_id;
  180: }
  181: 
  182: sub get_display_part {
  183:     my ($partID,$symb)=@_;
  184:     my $display=&Apache::lonnet::EXT('resource.'.$partID.'.display',$symb);
  185:     if (defined($display) and $display ne '') {
  186:         $display.= ' (<span class="LC_internal_info">'
  187:                   .&mt('Part ID: [_1]',$partID).'</span>)';
  188:     } else {
  189: 	$display=$partID;
  190:     }
  191:     return $display;
  192: }
  193: 
  194: #--- Show resource title
  195: #--- and parts and response type
  196: sub showResourceInfo {
  197:     my ($symb,$probTitle,$checkboxes) = @_;
  198:     my $col=3;
  199:     if ($checkboxes) { $col=4; }
  200:     my $result = '<h3>'.&mt('Current Resource').': '.$probTitle.'</h3>'."\n";
  201:     $result .='<table border="0">';
  202:     my ($partlist,$handgrade,$responseType) = &response_type($symb);
  203:     my %resptype = ();
  204:     my $hdgrade='no';
  205:     my %partsseen;
  206:     foreach my $partID (sort(keys(%$responseType))) {
  207: 	foreach my $resID (sort(keys(%{ $responseType->{$partID} }))) {
  208: 	    my $handgrade=$$handgrade{$partID.'_'.$resID};
  209: 	    my $responsetype = $responseType->{$partID}->{$resID};
  210: 	    $hdgrade = $handgrade if ($handgrade eq 'yes');
  211: 	    $result.='<tr>';
  212: 	    if ($checkboxes) {
  213: 		if (exists($partsseen{$partID})) {
  214: 		    $result.="<td>&nbsp;</td>";
  215: 		} else {
  216: 		    $result.="<td><input type='checkbox' name='vPart' value='$partID' checked='checked' /></td>";
  217: 		}
  218: 		$partsseen{$partID}=1;
  219: 	    }
  220: 	    my $display_part=&get_display_part($partID,$symb);
  221:             $result.='<td><b>'.&mt('Part: [_1]',$display_part).'</b>'.
  222:                 ' <span class="LC_internal_info">'.$resID.'</span></td>'.
  223:                 '<td><b>'.&mt('Type: [_1]',$responsetype).'</b></td></tr>';
  224: #	    '<td>'.&mt('<b>Handgrade: </b>[_1]',$handgrade).'</td></tr>';
  225: 	}
  226:     }
  227:     $result.='</table>'."\n";
  228:     return $result,$responseType,$hdgrade,$partlist,$handgrade;
  229: }
  230: 
  231: sub reset_caches {
  232:     &reset_analyze_cache();
  233:     &reset_perm();
  234: }
  235: 
  236: {
  237:     my %analyze_cache;
  238:     my %analyze_cache_formkeys;
  239: 
  240:     sub reset_analyze_cache {
  241: 	undef(%analyze_cache);
  242:         undef(%analyze_cache_formkeys);
  243:     }
  244: 
  245:     sub get_analyze {
  246: 	my ($symb,$uname,$udom,$no_increment,$add_to_hash)=@_;
  247: 	my $key = "$symb\0$uname\0$udom";
  248: 	if (exists($analyze_cache{$key})) {
  249:             my $getupdate = 0;
  250:             if (ref($add_to_hash) eq 'HASH') {
  251:                 foreach my $item (keys(%{$add_to_hash})) {
  252:                     if (ref($analyze_cache_formkeys{$key}) eq 'HASH') {
  253:                         if (!exists($analyze_cache_formkeys{$key}{$item})) {
  254:                             $getupdate = 1;
  255:                             last;
  256:                         }
  257:                     } else {
  258:                         $getupdate = 1;
  259:                     }
  260:                 }
  261:             }
  262:             if (!$getupdate) {
  263:                 return $analyze_cache{$key};
  264:             }
  265:         }
  266: 
  267: 	my (undef,undef,$url)=&Apache::lonnet::decode_symb($symb);
  268: 	$url=&Apache::lonnet::clutter($url);
  269:         my %form = ('grade_target'      => 'analyze',
  270:                     'grade_domain'      => $udom,
  271:                     'grade_symb'        => $symb,
  272:                     'grade_courseid'    =>  $env{'request.course.id'},
  273:                     'grade_username'    => $uname,
  274:                     'grade_noincrement' => $no_increment);
  275:         if (ref($add_to_hash)) {
  276:             %form = (%form,%{$add_to_hash});
  277:         } 
  278: 	my $subresult=&ssi_with_retries($url, $ssi_retries,%form);
  279: 	(undef,$subresult)=split(/_HASH_REF__/,$subresult,2);
  280: 	my %analyze=&Apache::lonnet::str2hash($subresult);
  281:         if (ref($add_to_hash) eq 'HASH') {
  282:             $analyze_cache_formkeys{$key} = $add_to_hash;
  283:         } else {
  284:             $analyze_cache_formkeys{$key} = {};
  285:         }
  286: 	return $analyze_cache{$key} = \%analyze;
  287:     }
  288: 
  289:     sub get_order {
  290: 	my ($partid,$respid,$symb,$uname,$udom,$no_increment)=@_;
  291: 	my $analyze = &get_analyze($symb,$uname,$udom,$no_increment);
  292: 	return $analyze->{"$partid.$respid.shown"};
  293:     }
  294: 
  295:     sub get_radiobutton_correct_foil {
  296: 	my ($partid,$respid,$symb,$uname,$udom)=@_;
  297: 	my $analyze = &get_analyze($symb,$uname,$udom);
  298:         my $foils = &get_order($partid,$respid,$symb,$uname,$udom);
  299:         if (ref($foils) eq 'ARRAY') {
  300: 	    foreach my $foil (@{$foils}) {
  301: 	        if ($analyze->{"$partid.$respid.foil.value.$foil"} eq 'true') {
  302: 		    return $foil;
  303: 	        }
  304: 	    }
  305: 	}
  306:     }
  307: 
  308:     sub scantron_partids_tograde {
  309:         my ($resource,$cid,$uname,$udom,$check_for_randomlist) = @_;
  310:         my (%analysis,@parts);
  311:         if (ref($resource)) {
  312:             my $symb = $resource->symb();
  313:             my $add_to_form;
  314:             if ($check_for_randomlist) {
  315:                 $add_to_form = { 'check_parts_withrandomlist' => 1,};
  316:             }
  317:             my $analyze = &get_analyze($symb,$uname,$udom,undef,$add_to_form);
  318:             if (ref($analyze) eq 'HASH') {
  319:                 %analysis = %{$analyze};
  320:             }
  321:             if (ref($analysis{'parts'}) eq 'ARRAY') {
  322:                 foreach my $part (@{$analysis{'parts'}}) {
  323:                     my ($id,$respid) = split(/\./,$part);
  324:                     if (!&Apache::loncommon::check_if_partid_hidden($id,$symb,$udom,$uname)) {
  325:                         push(@parts,$part);
  326:                     }
  327:                 }
  328:             }
  329:         }
  330:         return (\%analysis,\@parts);
  331:     }
  332: 
  333: }
  334: 
  335: #--- Clean response type for display
  336: #--- Currently filters option/rank/radiobutton/match/essay/Task
  337: #        response types only.
  338: sub cleanRecord {
  339:     my ($answer,$response,$symb,$partid,$respid,$record,$order,$version,
  340: 	$uname,$udom) = @_;
  341:     my $grayFont = '<span class="LC_internal_info">';
  342:     if ($response =~ /^(option|rank)$/) {
  343: 	my %answer=&Apache::lonnet::str2hash($answer);
  344: 	my %grading=&Apache::lonnet::str2hash($record->{$version."resource.$partid.$respid.submissiongrading"});
  345: 	my ($toprow,$bottomrow);
  346: 	foreach my $foil (@$order) {
  347: 	    if ($grading{$foil} == 1) {
  348: 		$toprow.='<td><b>'.$answer{$foil}.'&nbsp;</b></td>';
  349: 	    } else {
  350: 		$toprow.='<td><i>'.$answer{$foil}.'&nbsp;</i></td>';
  351: 	    }
  352: 	    $bottomrow.='<td>'.$grayFont.$foil.'</span>&nbsp;</td>';
  353: 	}
  354: 	return '<blockquote><table border="1">'.
  355: 	    '<tr valign="top"><td>'.&mt('Answer').'</td>'.$toprow.'</tr>'.
  356: 	    '<tr valign="top"><td>'.$grayFont.&mt('Option ID').'</span></td>'.
  357: 	    $grayFont.$bottomrow.'</tr>'.'</table></blockquote>';
  358:     } elsif ($response eq 'match') {
  359: 	my %answer=&Apache::lonnet::str2hash($answer);
  360: 	my %grading=&Apache::lonnet::str2hash($record->{$version."resource.$partid.$respid.submissiongrading"});
  361: 	my @items=&Apache::lonnet::str2array($record->{$version."resource.$partid.$respid.submissionitems"});
  362: 	my ($toprow,$middlerow,$bottomrow);
  363: 	foreach my $foil (@$order) {
  364: 	    my $item=shift(@items);
  365: 	    if ($grading{$foil} == 1) {
  366: 		$toprow.='<td><b>'.$item.'&nbsp;</b></td>';
  367: 		$middlerow.='<td><b>'.$grayFont.$answer{$foil}.'&nbsp;</span></b></td>';
  368: 	    } else {
  369: 		$toprow.='<td><i>'.$item.'&nbsp;</i></td>';
  370: 		$middlerow.='<td><i>'.$grayFont.$answer{$foil}.'&nbsp;</span></i></td>';
  371: 	    }
  372: 	    $bottomrow.='<td>'.$grayFont.$foil.'</span>&nbsp;</td>';
  373: 	}
  374: 	return '<blockquote><table border="1">'.
  375: 	    '<tr valign="top"><td>'.&mt('Answer').'</td>'.$toprow.'</tr>'.
  376: 	    '<tr valign="top"><td>'.$grayFont.&mt('Item ID').'</span></td>'.
  377: 	    $middlerow.'</tr>'.
  378: 	    '<tr valign="top"><td>'.$grayFont.&mt('Option ID').'</span></td>'.
  379: 	    $bottomrow.'</tr>'.'</table></blockquote>';
  380:     } elsif ($response eq 'radiobutton') {
  381: 	my %answer=&Apache::lonnet::str2hash($answer);
  382: 	my ($toprow,$bottomrow);
  383: 	my $correct = 
  384: 	    &get_radiobutton_correct_foil($partid,$respid,$symb,$uname,$udom);
  385: 	foreach my $foil (@$order) {
  386: 	    if (exists($answer{$foil})) {
  387: 		if ($foil eq $correct) {
  388: 		    $toprow.='<td><b>'.&mt('true').'</b></td>';
  389: 		} else {
  390: 		    $toprow.='<td><i>'.&mt('true').'</i></td>';
  391: 		}
  392: 	    } else {
  393: 		$toprow.='<td>'.&mt('false').'</td>';
  394: 	    }
  395: 	    $bottomrow.='<td>'.$grayFont.$foil.'</span>&nbsp;</td>';
  396: 	}
  397: 	return '<blockquote><table border="1">'.
  398: 	    '<tr valign="top"><td>'.&mt('Answer').'</td>'.$toprow.'</tr>'.
  399: 	    '<tr valign="top"><td>'.$grayFont.&mt('Option ID').'</span></td>'.
  400: 	    $grayFont.$bottomrow.'</tr>'.'</table></blockquote>';
  401:     } elsif ($response eq 'essay') {
  402: 	if (! exists ($env{'form.'.$symb})) {
  403: 	    my (%keyhash) = &Apache::lonnet::dump('nohist_handgrade',
  404: 						  $env{'course.'.$env{'request.course.id'}.'.domain'},
  405: 						  $env{'course.'.$env{'request.course.id'}.'.num'});
  406: 
  407: 	    my $loginuser = $env{'user.name'}.':'.$env{'user.domain'};
  408: 	    $env{'form.keywords'} = $keyhash{$symb.'_keywords'} ne '' ? $keyhash{$symb.'_keywords'} : '';
  409: 	    $env{'form.kwclr'}    = $keyhash{$loginuser.'_kwclr'} ne '' ? $keyhash{$loginuser.'_kwclr'} : 'red';
  410: 	    $env{'form.kwsize'}   = $keyhash{$loginuser.'_kwsize'} ne '' ? $keyhash{$loginuser.'_kwsize'} : '0';
  411: 	    $env{'form.kwstyle'}  = $keyhash{$loginuser.'_kwstyle'} ne '' ? $keyhash{$loginuser.'_kwstyle'} : '';
  412: 	    $env{'form.'.$symb} = 1; # so that we don't have to read it from disk for multiple sub of the same prob.
  413: 	}
  414: 	$answer =~ s-\n-<br />-g;
  415: 	return '<br /><br /><blockquote><tt>'.&keywords_highlight($answer).'</tt></blockquote>';
  416:     } elsif ( $response eq 'organic') {
  417: 	my $result='Smile representation: "<tt>'.$answer.'</tt>"';
  418: 	my $jme=$record->{$version."resource.$partid.$respid.molecule"};
  419: 	$result.=&Apache::chemresponse::jme_img($jme,$answer,400);
  420: 	return $result;
  421:     } elsif ( $response eq 'Task') {
  422: 	if ( $answer eq 'SUBMITTED') {
  423: 	    my $files = $record->{$version."resource.$respid.$partid.bridgetask.portfiles"};
  424: 	    my $result = &Apache::bridgetask::file_list($files,$uname,$udom);
  425: 	    return $result;
  426: 	} elsif ( grep(/^\Q$version\E.*?\.instance$/, keys(%{$record})) ) {
  427: 	    my @matches = grep(/^\Q$version\E.*?\.instance$/,
  428: 			       keys(%{$record}));
  429: 	    return join('<br />',($version,@matches));
  430: 			       
  431: 			       
  432: 	} else {
  433: 	    my $result =
  434: 		'<p>'
  435: 		.&mt('Overall result: [_1]',
  436: 		     $record->{$version."resource.$respid.$partid.status"})
  437: 		.'</p>';
  438: 	    
  439: 	    $result .= '<ul>';
  440: 	    my @grade = grep(/^\Q${version}resource.$respid.$partid.\E[^.]*[.]status$/,
  441: 			     keys(%{$record}));
  442: 	    foreach my $grade (sort(@grade)) {
  443: 		my ($dim) = ($grade =~/[.]([^.]+)[.]status$/);
  444: 		$result.= '<li>'.&mt("Dimension: [_1], status [_2] ",
  445: 				     $dim, $record->{$grade}).
  446: 			  '</li>';
  447: 	    }
  448: 	    $result.='</ul>';
  449: 	    return $result;
  450: 	}
  451:     } elsif ( $response =~ m/(?:numerical|formula)/) {
  452: 	$answer = 
  453: 	    &Apache::loncommon::format_previous_attempt_value('submission',
  454: 							      $answer);
  455:     }
  456:     return $answer;
  457: }
  458: 
  459: #-- A couple of common js functions
  460: sub commonJSfunctions {
  461:     my $request = shift;
  462:     $request->print(<<COMMONJSFUNCTIONS);
  463: <script type="text/javascript" language="javascript">
  464:     function radioSelection(radioButton) {
  465: 	var selection=null;
  466: 	if (radioButton.length > 1) {
  467: 	    for (var i=0; i<radioButton.length; i++) {
  468: 		if (radioButton[i].checked) {
  469: 		    return radioButton[i].value;
  470: 		}
  471: 	    }
  472: 	} else {
  473: 	    if (radioButton.checked) return radioButton.value;
  474: 	}
  475: 	return selection;
  476:     }
  477: 
  478:     function pullDownSelection(selectOne) {
  479: 	var selection="";
  480: 	if (selectOne.length > 1) {
  481: 	    for (var i=0; i<selectOne.length; i++) {
  482: 		if (selectOne[i].selected) {
  483: 		    return selectOne[i].value;
  484: 		}
  485: 	    }
  486: 	} else {
  487:             // only one value it must be the selected one
  488: 	    return selectOne.value;
  489: 	}
  490:     }
  491: </script>
  492: COMMONJSFUNCTIONS
  493: }
  494: 
  495: #--- Dumps the class list with usernames,list of sections,
  496: #--- section, ids and fullnames for each user.
  497: sub getclasslist {
  498:     my ($getsec,$filterlist,$getgroup) = @_;
  499:     my @getsec;
  500:     my @getgroup;
  501:     my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status'));
  502:     if (!ref($getsec)) {
  503: 	if ($getsec ne '' && $getsec ne 'all') {
  504: 	    @getsec=($getsec);
  505: 	}
  506:     } else {
  507: 	@getsec=@{$getsec};
  508:     }
  509:     if (grep(/^all$/,@getsec)) { undef(@getsec); }
  510:     if (!ref($getgroup)) {
  511: 	if ($getgroup ne '' && $getgroup ne 'all') {
  512: 	    @getgroup=($getgroup);
  513: 	}
  514:     } else {
  515: 	@getgroup=@{$getgroup};
  516:     }
  517:     if (grep(/^all$/,@getgroup)) { undef(@getgroup); }
  518: 
  519:     my ($classlist,$keylist)=&Apache::loncoursedata::get_classlist();
  520:     # Bail out if we were unable to get the classlist
  521:     return if (! defined($classlist));
  522:     &Apache::loncoursedata::get_group_memberships($classlist,$keylist);
  523:     #
  524:     my %sections;
  525:     my %fullnames;
  526:     foreach my $student (keys(%$classlist)) {
  527:         my $end      = 
  528:             $classlist->{$student}->[&Apache::loncoursedata::CL_END()];
  529:         my $start    = 
  530:             $classlist->{$student}->[&Apache::loncoursedata::CL_START()];
  531:         my $id       = 
  532:             $classlist->{$student}->[&Apache::loncoursedata::CL_ID()];
  533:         my $section  = 
  534:             $classlist->{$student}->[&Apache::loncoursedata::CL_SECTION()];
  535:         my $fullname = 
  536:             $classlist->{$student}->[&Apache::loncoursedata::CL_FULLNAME()];
  537:         my $status   = 
  538:             $classlist->{$student}->[&Apache::loncoursedata::CL_STATUS()];
  539:         my $group   = 
  540:             $classlist->{$student}->[&Apache::loncoursedata::CL_GROUP()];
  541: 	# filter students according to status selected
  542: 	if ($filterlist && (!($stu_status =~ /Any/))) {
  543: 	    if (!($stu_status =~ $status)) {
  544: 		delete($classlist->{$student});
  545: 		next;
  546: 	    }
  547: 	}
  548: 	# filter students according to groups selected
  549: 	my @stu_groups = split(/,/,$group);
  550: 	if (@getgroup) {
  551: 	    my $exclude = 1;
  552: 	    foreach my $grp (@getgroup) {
  553: 	        foreach my $stu_group (@stu_groups) {
  554: 	            if ($stu_group eq $grp) {
  555: 	                $exclude = 0;
  556:     	            } 
  557: 	        }
  558:     	        if (($grp eq 'none') && !$group) {
  559:         	        $exclude = 0;
  560:         	}
  561: 	    }
  562: 	    if ($exclude) {
  563: 	        delete($classlist->{$student});
  564: 	    }
  565: 	}
  566: 	$section = ($section ne '' ? $section : 'none');
  567: 	if (&canview($section)) {
  568: 	    if (!@getsec || grep(/^\Q$section\E$/,@getsec)) {
  569: 		$sections{$section}++;
  570: 		if ($classlist->{$student}) {
  571: 		    $fullnames{$student}=$fullname;
  572: 		}
  573: 	    } else {
  574: 		delete($classlist->{$student});
  575: 	    }
  576: 	} else {
  577: 	    delete($classlist->{$student});
  578: 	}
  579:     }
  580:     my %seen = ();
  581:     my @sections = sort(keys(%sections));
  582:     return ($classlist,\@sections,\%fullnames);
  583: }
  584: 
  585: sub canmodify {
  586:     my ($sec)=@_;
  587:     if ($perm{'mgr'}) {
  588: 	if (!defined($perm{'mgr_section'})) {
  589: 	    # can modify whole class
  590: 	    return 1;
  591: 	} else {
  592: 	    if ($sec eq $perm{'mgr_section'}) {
  593: 		#can modify the requested section
  594: 		return 1;
  595: 	    } else {
  596: 		# can't modify the request section
  597: 		return 0;
  598: 	    }
  599: 	}
  600:     }
  601:     #can't modify
  602:     return 0;
  603: }
  604: 
  605: sub canview {
  606:     my ($sec)=@_;
  607:     if ($perm{'vgr'}) {
  608: 	if (!defined($perm{'vgr_section'})) {
  609: 	    # can modify whole class
  610: 	    return 1;
  611: 	} else {
  612: 	    if ($sec eq $perm{'vgr_section'}) {
  613: 		#can modify the requested section
  614: 		return 1;
  615: 	    } else {
  616: 		# can't modify the request section
  617: 		return 0;
  618: 	    }
  619: 	}
  620:     }
  621:     #can't modify
  622:     return 0;
  623: }
  624: 
  625: #--- Retrieve the grade status of a student for all the parts
  626: sub student_gradeStatus {
  627:     my ($symb,$udom,$uname,$partlist) = @_;
  628:     my %record     = &Apache::lonnet::restore($symb,$env{'request.course.id'},$udom,$uname);
  629:     my %partstatus = ();
  630:     foreach (@$partlist) {
  631: 	my ($status,undef)   = split(/_/,$record{"resource.$_.solved"},2);
  632: 	$status              = 'nothing' if ($status eq '');
  633: 	$partstatus{$_}      = $status;
  634: 	my $subkey           = "resource.$_.submitted_by";
  635: 	$partstatus{$subkey} = $record{$subkey} if ($record{$subkey} ne '');
  636:     }
  637:     return %partstatus;
  638: }
  639: 
  640: # hidden form and javascript that calls the form
  641: # Use by verifyscript and viewgrades
  642: # Shows a student's view of problem and submission
  643: sub jscriptNform {
  644:     my ($symb) = @_;
  645:     my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status'));
  646:     my $jscript='<script type="text/javascript" language="javascript">'."\n".
  647: 	'    function viewOneStudent(user,domain) {'."\n".
  648: 	'	document.onestudent.student.value = user;'."\n".
  649: 	'	document.onestudent.userdom.value = domain;'."\n".
  650: 	'	document.onestudent.submit();'."\n".
  651: 	'    }'."\n".
  652: 	'</script>'."\n";
  653:     $jscript.= '<form action="/adm/grades" method="post" name="onestudent">'."\n".
  654: 	'<input type="hidden" name="symb"    value="'.&Apache::lonenc::check_encrypt($symb).'" />'."\n".
  655: 	'<input type="hidden" name="saveState" value="'.$env{'form.saveState'}.'" />'."\n".
  656: 	'<input type="hidden" name="probTitle" value="'.$env{'form.probTitle'}.'" />'."\n".
  657: 	'<input type="hidden" name="Status"  value="'.$stu_status.'" />'."\n".
  658: 	'<input type="hidden" name="command" value="submission" />'."\n".
  659: 	'<input type="hidden" name="student" value="" />'."\n".
  660: 	'<input type="hidden" name="userdom" value="" />'."\n".
  661: 	'</form>'."\n";
  662:     return $jscript;
  663: }
  664: 
  665: 
  666: 
  667: # Given the score (as a number [0-1] and the weight) what is the final
  668: # point value? This function will round to the nearest tenth, third,
  669: # or quarter if one of those is within the tolerance of .00001.
  670: sub compute_points {
  671:     my ($score, $weight) = @_;
  672:     
  673:     my $tolerance = .00001;
  674:     my $points = $score * $weight;
  675: 
  676:     # Check for nearness to 1/x.
  677:     my $check_for_nearness = sub {
  678:         my ($factor) = @_;
  679:         my $num = ($points * $factor) + $tolerance;
  680:         my $floored_num = floor($num);
  681:         if ($num - $floored_num < 2 * $tolerance * $factor) {
  682:             return $floored_num / $factor;
  683:         }
  684:         return $points;
  685:     };
  686: 
  687:     $points = $check_for_nearness->(10);
  688:     $points = $check_for_nearness->(3);
  689:     $points = $check_for_nearness->(4);
  690:     
  691:     return $points;
  692: }
  693: 
  694: #------------------ End of general use routines --------------------
  695: 
  696: #
  697: # Find most similar essay
  698: #
  699: 
  700: sub most_similar {
  701:     my ($uname,$udom,$uessay,$old_essays)=@_;
  702: 
  703: # ignore spaces and punctuation
  704: 
  705:     $uessay=~s/\W+/ /gs;
  706: 
  707: # ignore empty submissions (occuring when only files are sent)
  708: 
  709:     unless ($uessay=~/\w+/) { return ''; }
  710: 
  711: # these will be returned. Do not care if not at least 50 percent similar
  712:     my $limit=0.6;
  713:     my $sname='';
  714:     my $sdom='';
  715:     my $scrsid='';
  716:     my $sessay='';
  717: # go through all essays ...
  718:     foreach my $tkey (keys(%$old_essays)) {
  719: 	my ($tname,$tdom,$tcrsid)=map {&unescape($_)} (split(/\./,$tkey));
  720: # ... except the same student
  721:         next if (($tname eq $uname) && ($tdom eq $udom));
  722: 	my $tessay=$old_essays->{$tkey};
  723: 	$tessay=~s/\W+/ /gs;
  724: # String similarity gives up if not even limit
  725: 	my $tsimilar=&String::Similarity::similarity($uessay,$tessay,$limit);
  726: # Found one
  727: 	if ($tsimilar>$limit) {
  728: 	    $limit=$tsimilar;
  729: 	    $sname=$tname;
  730: 	    $sdom=$tdom;
  731: 	    $scrsid=$tcrsid;
  732: 	    $sessay=$old_essays->{$tkey};
  733: 	}
  734:     }
  735:     if ($limit>0.6) {
  736:        return ($sname,$sdom,$scrsid,$sessay,$limit);
  737:     } else {
  738:        return ('','','','',0);
  739:     }
  740: }
  741: 
  742: #-------------------------------------------------------------------
  743: 
  744: #------------------------------------ Receipt Verification Routines
  745: #
  746: #--- Check whether a receipt number is valid.---
  747: sub verifyreceipt {
  748:     my $request  = shift;
  749: 
  750:     my $courseid = $env{'request.course.id'};
  751:     my $receipt  = &Apache::lonnet::recprefix($courseid).'-'.
  752: 	$env{'form.receipt'};
  753:     $receipt     =~ s/[^\-\d]//g;
  754:     my ($symb)   = &get_symb($request);
  755: 
  756:     my $title.=
  757: 	'<h3><span class="LC_info">'.
  758: 	&mt('Verifying  Receipt No. [_1]',$receipt).
  759: 	'</span></h3>'."\n".
  760: 	'<h4>'.&mt('<b>Resource: </b>[_1]',$env{'form.probTitle'}).
  761: 	'</h4>'."\n";
  762: 
  763:     my ($string,$contents,$matches) = ('','',0);
  764:     my (undef,undef,$fullname) = &getclasslist('all','0');
  765:     
  766:     my $receiptparts=0;
  767:     if ($env{"course.$courseid.receiptalg"} eq 'receipt2' ||
  768: 	$env{"course.$courseid.receiptalg"} eq 'receipt3') { $receiptparts=1; }
  769:     my $parts=['0'];
  770:     if ($receiptparts) { ($parts)=&response_type($symb); }
  771:     
  772:     my $header = 
  773: 	&Apache::loncommon::start_data_table().
  774: 	&Apache::loncommon::start_data_table_header_row().
  775: 	'<th>&nbsp;'.&mt('Fullname').'&nbsp;</th>'."\n".
  776: 	'<th>&nbsp;'.&mt('Username').'&nbsp;</th>'."\n".
  777: 	'<th>&nbsp;'.&mt('Domain').'&nbsp;</th>';
  778:     if ($receiptparts) {
  779: 	$header.='<th>&nbsp;'.&mt('Problem Part').'&nbsp;</th>';
  780:     }
  781:     $header.=
  782: 	&Apache::loncommon::end_data_table_header_row();
  783: 
  784:     foreach (sort 
  785: 	     {
  786: 		 if (lc($$fullname{$a}) ne lc($$fullname{$b})) {
  787: 		     return (lc($$fullname{$a}) cmp lc($$fullname{$b}));
  788: 		 }
  789: 		 return $a cmp $b;
  790: 	     } (keys(%$fullname))) {
  791: 	my ($uname,$udom)=split(/\:/);
  792: 	foreach my $part (@$parts) {
  793: 	    if ($receipt eq &Apache::lonnet::ireceipt($uname,$udom,$courseid,$symb,$part)) {
  794: 		$contents.=
  795: 		    &Apache::loncommon::start_data_table_row().
  796: 		    '<td>&nbsp;'."\n".
  797: 		    '<a href="javascript:viewOneStudent(\''.$uname.'\',\''.$udom.
  798: 		    '\');" target="_self">'.$$fullname{$_}.'</a>&nbsp;</td>'."\n".
  799: 		    '<td>&nbsp;'.$uname.'&nbsp;</td>'.
  800: 		    '<td>&nbsp;'.$udom.'&nbsp;</td>';
  801: 		if ($receiptparts) {
  802: 		    $contents.='<td>&nbsp;'.$part.'&nbsp;</td>';
  803: 		}
  804: 		$contents.= 
  805: 		    &Apache::loncommon::end_data_table_row()."\n";
  806: 		
  807: 		$matches++;
  808: 	    }
  809: 	}
  810:     }
  811:     if ($matches == 0) {
  812: 	$string = $title.&mt('No match found for the above receipt.');
  813:     } else {
  814: 	$string = &jscriptNform($symb).$title.
  815: 	    '<p>'.
  816: 	    &mt('The above receipt matches the following [numerate,_1,student].',$matches).
  817: 	    '</p>'.
  818: 	    $header.
  819: 	    $contents.
  820: 	    &Apache::loncommon::end_data_table()."\n";
  821:     }
  822:     return $string.&show_grading_menu_form($symb);
  823: }
  824: 
  825: #--- This is called by a number of programs.
  826: #--- Called from the Grading Menu - View/Grade an individual student
  827: #--- Also called directly when one clicks on the subm button 
  828: #    on the problem page.
  829: sub listStudents {
  830:     my ($request) = shift;
  831: 
  832:     my ($symb) = &get_symb($request);
  833:     my $cdom      = $env{"course.$env{'request.course.id'}.domain"};
  834:     my $cnum      = $env{"course.$env{'request.course.id'}.num"};
  835:     my $getsec    = $env{'form.section'} eq '' ? 'all' : $env{'form.section'};
  836:     my $getgroup  = $env{'form.group'} eq '' ? 'all' : $env{'form.group'};
  837:     my $submitonly= $env{'form.submitonly'} eq '' ? 'all' : $env{'form.submitonly'};
  838:     my $viewgrade = $env{'form.showgrading'} eq 'yes' ? 'View/Grade/Regrade' : 'View';
  839:     $env{'form.probTitle'} = $env{'form.probTitle'} eq '' ? 
  840: 	&Apache::lonnet::gettitle($symb) : $env{'form.probTitle'};
  841: 
  842:     my $result='<h3><span class="LC_info">&nbsp;'
  843: 	.&mt("$viewgrade Submissions for a Student or a Group of Students")
  844: 	.'</span></h3>';
  845: 
  846:     my ($table,undef,$hdgrade,$partlist,$handgrade) = &showResourceInfo($symb,$env{'form.probTitle'},($env{'form.showgrading'} eq 'yes'));
  847: 
  848:     my %lt = &Apache::lonlocal::texthash (
  849: 		'multiple' => 'Please select a student or group of students before clicking on the Next button.',
  850: 		'single'   => 'Please select the student before clicking on the Next button.',
  851: 	     );
  852:     $request->print(<<LISTJAVASCRIPT);
  853: <script type="text/javascript" language="javascript">
  854:     function checkSelect(checkBox) {
  855: 	var ctr=0;
  856: 	var sense="";
  857: 	if (checkBox.length > 1) {
  858: 	    for (var i=0; i<checkBox.length; i++) {
  859: 		if (checkBox[i].checked) {
  860: 		    ctr++;
  861: 		}
  862: 	    }
  863: 	    sense = '$lt{'multiple'}';
  864: 	} else {
  865: 	    if (checkBox.checked) {
  866: 		ctr = 1;
  867: 	    }
  868: 	    sense = '$lt{'single'}';
  869: 	}
  870: 	if (ctr == 0) {
  871: 	    alert(sense);
  872: 	    return false;
  873: 	}
  874: 	document.gradesub.submit();
  875:     }
  876: 
  877:     function reLoadList(formname) {
  878: 	if (formname.saveStatusOld.value == pullDownSelection(formname.Status)) {return;}
  879: 	formname.command.value = 'submission';
  880: 	formname.submit();
  881:     }
  882: </script>
  883: LISTJAVASCRIPT
  884: 
  885:     &commonJSfunctions($request);
  886:     $request->print($result);
  887: 
  888:     my $checkhdgrade = ($env{'form.handgrade'} eq 'yes' && scalar(@$partlist) > 1 ) ? 'checked="checked"' : '';
  889:     my $checklastsub = $checkhdgrade eq '' ? 'checked="checked"' : '';
  890:     my $gradeTable='<form action="/adm/grades" method="post" name="gradesub">'.
  891: 	"\n".$table;
  892: 	
  893:     $gradeTable .= &Apache::lonhtmlcommon::start_pick_box();
  894:     $gradeTable .= &Apache::lonhtmlcommon::row_title(&mt('View Problem Text'))
  895:                   .'<label><input type="radio" name="vProb" value="no" checked="checked" /> '.&mt('no').' </label>'."\n"
  896:                   .'<label><input type="radio" name="vProb" value="yes" /> '.&mt('one student').' </label>'."\n"
  897:                   .'<label><input type="radio" name="vProb" value="all" /> '.&mt('all students').' </label><br />'."\n"
  898:                   .&Apache::lonhtmlcommon::row_closure();
  899:     $gradeTable .= &Apache::lonhtmlcommon::row_title(&mt('View Answer'))
  900:                   .'<label><input type="radio" name="vAns" value="no"  /> '.&mt('no').' </label>'."\n"
  901:                   .'<label><input type="radio" name="vAns" value="yes" /> '.&mt('one student').' </label>'."\n"
  902:                   .'<label><input type="radio" name="vAns" value="all" checked="checked" /> '.&mt('all students').' </label><br />'."\n"
  903:                   .&Apache::lonhtmlcommon::row_closure();
  904: 
  905:     my $submission_options;
  906:     if ($env{'form.handgrade'} eq 'yes' && scalar(@$partlist) > 1) {
  907: 	$submission_options.=
  908: 	    '<label><input type="radio" name="lastSub" value="hdgrade" '.$checkhdgrade.' /> '.&mt('essay part only').' </label>'."\n";
  909:     }
  910:     my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status'));
  911:     my $saveStatus = $stu_status eq '' ? 'Active' : $stu_status;
  912:     $env{'form.Status'} = $saveStatus;
  913:     $submission_options.=
  914: 	'<label><input type="radio" name="lastSub" value="lastonly" '.$checklastsub.' /> '.&mt('last submission only').' </label>'."\n".
  915: 	'<label><input type="radio" name="lastSub" value="last" /> '.&mt('last submission &amp; parts info').' </label>'."\n".
  916: 	'<label><input type="radio" name="lastSub" value="datesub" /> '.&mt('by dates and submissions').' </label>'."\n".
  917: 	'<label><input type="radio" name="lastSub" value="all" /> '.&mt('all details').'</label>';
  918:     $gradeTable .= &Apache::lonhtmlcommon::row_title(&mt('Submissions'))
  919:                   .$submission_options
  920:                   .&Apache::lonhtmlcommon::row_closure();
  921: 
  922:     $gradeTable .= &Apache::lonhtmlcommon::row_title(&mt('Grading Increments'))
  923:                   .'<select name="increment">'
  924:                   .'<option value="1">'.&mt('Whole Points').'</option>'
  925:                   .'<option value=".5">'.&mt('Half Points').'</option>'
  926:                   .'<option value=".25">'.&mt('Quarter Points').'</option>'
  927:                   .'<option value=".1">'.&mt('Tenths of a Point').'</option>'
  928:                   .'</select>'
  929:                   .&Apache::lonhtmlcommon::row_closure();
  930: 
  931:     $gradeTable .= 
  932:         &build_section_inputs().
  933: 	'<input type="hidden" name="submitonly"  value="'.$submitonly.'" />'."\n".
  934: 	'<input type="hidden" name="handgrade"   value="'.$env{'form.handgrade'}.'" /><br />'."\n".
  935: 	'<input type="hidden" name="showgrading" value="'.$env{'form.showgrading'}.'" /><br />'."\n".
  936: 	'<input type="hidden" name="saveState"   value="'.$env{'form.saveState'}.'" />'."\n".
  937: 	'<input type="hidden" name="probTitle"   value="'.$env{'form.probTitle'}.'" />'."\n".
  938: 	'<input type="hidden" name="symb" value="'.&Apache::lonenc::check_encrypt($symb).'" />'."\n".
  939: 	'<input type="hidden" name="saveStatusOld" value="'.$saveStatus.'" />'."\n";
  940: 
  941:     if (exists($env{'form.gradingMenu'}) && exists($env{'form.Status'})) {
  942: 	$gradeTable .= '<input type="hidden" name="Status" value="'.$stu_status.'" />'."\n";
  943:     } else {
  944:         $gradeTable .= &Apache::lonhtmlcommon::row_title(&mt('Student Status'))
  945:                       .&Apache::lonhtmlcommon::StatusOptions(
  946:                            $saveStatus,undef,1,'javascript:reLoadList(this.form);')
  947:                       .&Apache::lonhtmlcommon::row_closure();
  948:     }
  949: 
  950:     $gradeTable .= &Apache::lonhtmlcommon::row_title(&mt('Check For Plagiarism'))
  951:                   .'<input type="checkbox" name="checkPlag" checked="checked" />'
  952:                   .&Apache::lonhtmlcommon::row_closure(1)
  953:                   .&Apache::lonhtmlcommon::end_pick_box();
  954: 
  955:     $gradeTable .= '<p>'
  956:                   .&mt('To '.lc($viewgrade)." a submission or a group of submissions, click on the check box(es) next to the student's name(s). Then click on the Next button.")."\n"
  957:                   .'<input type="hidden" name="command" value="processGroup" />'
  958:                   .'</p>';
  959: 
  960: # checkall buttons
  961:     $gradeTable.=&check_script('gradesub', 'stuinfo');
  962:     $gradeTable.='<input type="button" '."\n".
  963: 	'onClick="javascript:checkSelect(this.form.stuinfo);" '."\n".
  964: 	'value="'.&mt('Next').' &rarr;" /> <br />'."\n";
  965:     $gradeTable.=&check_buttons();
  966:     my ($classlist, undef, $fullname) = &getclasslist($getsec,'1',$getgroup);
  967:     $gradeTable.= &Apache::loncommon::start_data_table().
  968: 	&Apache::loncommon::start_data_table_header_row();
  969:     my $loop = 0;
  970:     while ($loop < 2) {
  971: 	$gradeTable.='<th>'.&mt('No.').'</th><th>'.&mt('Select').'</th>'.
  972: 	    '<th>'.&nameUserString('header').'&nbsp;'.&mt('Section/Group').'</th>';
  973: 	if ($env{'form.showgrading'} eq 'yes' 
  974: 	    && $submitonly ne 'queued'
  975: 	    && $submitonly ne 'all') {
  976: 	    foreach my $part (sort(@$partlist)) {
  977: 		my $display_part=
  978: 		    &get_display_part((split(/_/,$part))[0],$symb);
  979: 		$gradeTable.=
  980: 		    '<th>'.&mt('Part: [_1] Status',$display_part).'</th>';
  981: 	    }
  982: 	} elsif ($submitonly eq 'queued') {
  983: 	    $gradeTable.='<th>'.&mt('Queue Status').'&nbsp;</th>';
  984: 	}
  985: 	$loop++;
  986: #	$gradeTable.='<td></td>' if ($loop%2 ==1);
  987:     }
  988:     $gradeTable.=&Apache::loncommon::end_data_table_header_row()."\n";
  989: 
  990:     my $ctr = 0;
  991:     foreach my $student (sort 
  992: 			 {
  993: 			     if (lc($$fullname{$a}) ne lc($$fullname{$b})) {
  994: 				 return (lc($$fullname{$a}) cmp lc($$fullname{$b}));
  995: 			     }
  996: 			     return $a cmp $b;
  997: 			 }
  998: 			 (keys(%$fullname))) {
  999: 	my ($uname,$udom) = split(/:/,$student);
 1000: 
 1001: 	my %status = ();
 1002: 
 1003: 	if ($submitonly eq 'queued') {
 1004: 	    my %queue_status = 
 1005: 		&Apache::bridgetask::get_student_status($symb,$cdom,$cnum,
 1006: 							$udom,$uname);
 1007: 	    next if (!defined($queue_status{'gradingqueue'}));
 1008: 	    $status{'gradingqueue'} = $queue_status{'gradingqueue'};
 1009: 	}
 1010: 
 1011: 	if ($env{'form.showgrading'} eq 'yes' 
 1012: 	    && $submitonly ne 'queued'
 1013: 	    && $submitonly ne 'all') {
 1014: 	    (%status) =&student_gradeStatus($symb,$udom,$uname,$partlist);
 1015: 	    my $submitted = 0;
 1016: 	    my $graded = 0;
 1017: 	    my $incorrect = 0;
 1018: 	    foreach (keys(%status)) {
 1019: 		$submitted = 1 if ($status{$_} ne 'nothing');
 1020: 		$graded = 1 if ($status{$_} =~ /^ungraded/);
 1021: 		$incorrect = 1 if ($status{$_} =~ /^incorrect/);
 1022: 		
 1023: 		my ($foo,$partid,$foo1) = split(/\./,$_);
 1024: 		if ($status{'resource.'.$partid.'.submitted_by'} ne '') {
 1025: 		    $submitted = 0;
 1026: 		    my ($part)=split(/\./,$partid);
 1027: 		    $gradeTable.='<input type="hidden" name="'.
 1028: 			$student.':'.$part.':submitted_by" value="'.
 1029: 			$status{'resource.'.$partid.'.submitted_by'}.'" />';
 1030: 		}
 1031: 	    }
 1032: 	    
 1033: 	    next if (!$submitted && ($submitonly eq 'yes' ||
 1034: 				     $submitonly eq 'incorrect' ||
 1035: 				     $submitonly eq 'graded'));
 1036: 	    next if (!$graded && ($submitonly eq 'graded'));
 1037: 	    next if (!$incorrect && $submitonly eq 'incorrect');
 1038: 	}
 1039: 
 1040: 	$ctr++;
 1041: 	my $section = $classlist->{$student}->[&Apache::loncoursedata::CL_SECTION()];
 1042:         my $group = $classlist->{$student}->[&Apache::loncoursedata::CL_GROUP()];
 1043: 	if ( $perm{'vgr'} eq 'F' ) {
 1044: 	    if ($ctr%2 ==1) {
 1045: 		$gradeTable.= &Apache::loncommon::start_data_table_row();
 1046: 	    }
 1047: 	    $gradeTable.='<td align="right">'.$ctr.'&nbsp;</td>'.
 1048:                '<td align="center"><label><input type="checkbox" name="stuinfo" value="'.
 1049:                $student.':'.$$fullname{$student}.':::SECTION'.$section.
 1050: 	       ')&nbsp;" />&nbsp;&nbsp;</label></td>'."\n".'<td>'.
 1051: 	       &nameUserString(undef,$$fullname{$student},$uname,$udom).
 1052: 	       '&nbsp;'.$section.($group ne '' ?'/'.$group:'').'</td>'."\n";
 1053: 
 1054: 	    if ($env{'form.showgrading'} eq 'yes' && $submitonly ne 'all') {
 1055: 		foreach (sort(keys(%status))) {
 1056: 		    next if ($_ =~ /^resource.*?submitted_by$/);
 1057: 		    $gradeTable.='<td align="center">&nbsp;'.&mt($status{$_}).'&nbsp;</td>'."\n";
 1058: 		}
 1059: 	    }
 1060: #	    $gradeTable.='<td></td>' if ($ctr%2 ==1);
 1061: 	    if ($ctr%2 ==0) {
 1062: 		$gradeTable.=&Apache::loncommon::end_data_table_row()."\n";
 1063: 	    }
 1064: 	}
 1065:     }
 1066:     if ($ctr%2 ==1) {
 1067: 	$gradeTable.='<td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td>';
 1068: 	    if ($env{'form.showgrading'} eq 'yes' 
 1069: 		&& $submitonly ne 'queued'
 1070: 		&& $submitonly ne 'all') {
 1071: 		foreach (@$partlist) {
 1072: 		    $gradeTable.='<td>&nbsp;</td>';
 1073: 		}
 1074: 	    } elsif ($submitonly eq 'queued') {
 1075: 		$gradeTable.='<td>&nbsp;</td>';
 1076: 	    }
 1077: 	$gradeTable.=&Apache::loncommon::end_data_table_row();
 1078:     }
 1079: 
 1080:     $gradeTable.=&Apache::loncommon::end_data_table()."\n".
 1081: 	'<input type="button" '.
 1082: 	'onClick="javascript:checkSelect(this.form.stuinfo);" '.
 1083: 	'value="'.&mt('Next').' &rarr;" /></form>'."\n";
 1084:     if ($ctr == 0) {
 1085: 	my $num_students=(scalar(keys(%$fullname)));
 1086: 	if ($num_students eq 0) {
 1087: 	    $gradeTable='<br />&nbsp;<span class="LC_warning">'.&mt('There are no students currently enrolled.').'</span>';
 1088: 	} else {
 1089: 	    my $submissions='submissions';
 1090: 	    if ($submitonly eq 'incorrect') { $submissions = 'incorrect submissions'; }
 1091: 	    if ($submitonly eq 'graded'   ) { $submissions = 'ungraded submissions'; }
 1092: 	    if ($submitonly eq 'queued'   ) { $submissions = 'queued submissions'; }
 1093: 	    $gradeTable='<br />&nbsp;<span class="LC_warning">'.
 1094: 		&mt('No '.$submissions.' found for this resource for any students. ([_1] students checked for '.$submissions.')',
 1095: 		    $num_students).
 1096: 		'</span><br />';
 1097: 	}
 1098:     } elsif ($ctr == 1) {
 1099: 	$gradeTable =~ s/type="checkbox"/type="checkbox" checked="checked"/;
 1100:     }
 1101:     $gradeTable.=&show_grading_menu_form($symb);
 1102:     $request->print($gradeTable);
 1103:     return '';
 1104: }
 1105: 
 1106: #---- Called from the listStudents routine
 1107: 
 1108: sub check_script {
 1109:     my ($form, $type)=@_;
 1110:     my $chkallscript='<script type="text/javascript">
 1111:     function checkall() {
 1112:         for (i=0; i<document.forms.'.$form.'.elements.length; i++) {
 1113:             ele = document.forms.'.$form.'.elements[i];
 1114:             if (ele.name == "'.$type.'") {
 1115:             document.forms.'.$form.'.elements[i].checked=true;
 1116:                                        }
 1117:         }
 1118:     }
 1119: 
 1120:     function checksec() {
 1121:         for (i=0; i<document.forms.'.$form.'.elements.length; i++) {
 1122:             ele = document.forms.'.$form.'.elements[i];
 1123:            string = document.forms.'.$form.'.chksec.value;
 1124:            if
 1125:           (ele.value.indexOf(":::SECTION"+string)>0) {
 1126:               document.forms.'.$form.'.elements[i].checked=true;
 1127:             }
 1128:         }
 1129:     }
 1130: 
 1131: 
 1132:     function uncheckall() {
 1133:         for (i=0; i<document.forms.'.$form.'.elements.length; i++) {
 1134:             ele = document.forms.'.$form.'.elements[i];
 1135:             if (ele.name == "'.$type.'") {
 1136:             document.forms.'.$form.'.elements[i].checked=false;
 1137:                                        }
 1138:         }
 1139:     }
 1140: 
 1141: </script>'."\n";
 1142:     return $chkallscript;
 1143: }
 1144: 
 1145: sub check_buttons {
 1146:     my $buttons.='<input type="button" onclick="checkall()" value="'.&mt('Check All').'" />';
 1147:     $buttons.='<input type="button" onclick="uncheckall()" value="'.&mt('Uncheck All').'" />&nbsp;';
 1148:     $buttons.='<input type="button" onclick="checksec()" value="'.&mt('Check Section/Group').'" />';
 1149:     $buttons.='<input type="text" size="5" name="chksec" />&nbsp;';
 1150:     return $buttons;
 1151: }
 1152: 
 1153: #     Displays the submissions for one student or a group of students
 1154: sub processGroup {
 1155:     my ($request)  = shift;
 1156:     my $ctr        = 0;
 1157:     my @stuchecked = &Apache::loncommon::get_env_multiple('form.stuinfo');
 1158:     my $total      = scalar(@stuchecked)-1;
 1159: 
 1160:     foreach my $student (@stuchecked) {
 1161: 	my ($uname,$udom,$fullname) = split(/:/,$student);
 1162: 	$env{'form.student'}        = $uname;
 1163: 	$env{'form.userdom'}        = $udom;
 1164: 	$env{'form.fullname'}       = $fullname;
 1165: 	&submission($request,$ctr,$total);
 1166: 	$ctr++;
 1167:     }
 1168:     return '';
 1169: }
 1170: 
 1171: #------------------------------------------------------------------------------------
 1172: #
 1173: #-------------------------- Next few routines handles grading by student, essentially
 1174: #                           handles essay response type problem/part
 1175: #
 1176: #--- Javascript to handle the submission page functionality ---
 1177: sub sub_page_js {
 1178:     my $request = shift;
 1179: 	    my $alertmsg = &mt('A number equal or greater than 0 is expected. Entered value = ');
 1180:     $request->print(<<SUBJAVASCRIPT);
 1181: <script type="text/javascript" language="javascript">
 1182:     function updateRadio(formname,id,weight) {
 1183: 	var gradeBox = formname["GD_BOX"+id];
 1184: 	var radioButton = formname["RADVAL"+id];
 1185: 	var oldpts = formname["oldpts"+id].value;
 1186: 	var pts = checkSolved(formname,id) == 'update' ? gradeBox.value : oldpts;
 1187: 	gradeBox.value = pts;
 1188: 	var resetbox = false;
 1189: 	if (isNaN(pts) || pts < 0) {
 1190: 	    alert("$alertmsg"+pts);
 1191: 	    for (var i=0; i<radioButton.length; i++) {
 1192: 		if (radioButton[i].checked) {
 1193: 		    gradeBox.value = i;
 1194: 		    resetbox = true;
 1195: 		}
 1196: 	    }
 1197: 	    if (!resetbox) {
 1198: 		formtextbox.value = "";
 1199: 	    }
 1200: 	    return;
 1201: 	}
 1202: 
 1203: 	if (pts > weight) {
 1204: 	    var resp = confirm("You entered a value ("+pts+
 1205: 			       ") greater than the weight for the part. Accept?");
 1206: 	    if (resp == false) {
 1207: 		gradeBox.value = oldpts;
 1208: 		return;
 1209: 	    }
 1210: 	}
 1211: 
 1212: 	for (var i=0; i<radioButton.length; i++) {
 1213: 	    radioButton[i].checked=false;
 1214: 	    if (pts == i && pts != "") {
 1215: 		radioButton[i].checked=true;
 1216: 	    }
 1217: 	}
 1218: 	updateSelect(formname,id);
 1219: 	formname["stores"+id].value = "0";
 1220:     }
 1221: 
 1222:     function writeBox(formname,id,pts) {
 1223: 	var gradeBox = formname["GD_BOX"+id];
 1224: 	if (checkSolved(formname,id) == 'update') {
 1225: 	    gradeBox.value = pts;
 1226: 	} else {
 1227: 	    var oldpts = formname["oldpts"+id].value;
 1228: 	    gradeBox.value = oldpts;
 1229: 	    var radioButton = formname["RADVAL"+id];
 1230: 	    for (var i=0; i<radioButton.length; i++) {
 1231: 		radioButton[i].checked=false;
 1232: 		if (i == oldpts) {
 1233: 		    radioButton[i].checked=true;
 1234: 		}
 1235: 	    }
 1236: 	}
 1237: 	formname["stores"+id].value = "0";
 1238: 	updateSelect(formname,id);
 1239: 	return;
 1240:     }
 1241: 
 1242:     function clearRadBox(formname,id) {
 1243: 	if (checkSolved(formname,id) == 'noupdate') {
 1244: 	    updateSelect(formname,id);
 1245: 	    return;
 1246: 	}
 1247: 	gradeSelect = formname["GD_SEL"+id];
 1248: 	for (var i=0; i<gradeSelect.length; i++) {
 1249: 	    if (gradeSelect[i].selected) {
 1250: 		var selectx=i;
 1251: 	    }
 1252: 	}
 1253: 	var stores = formname["stores"+id];
 1254: 	if (selectx == stores.value) { return };
 1255: 	var gradeBox = formname["GD_BOX"+id];
 1256: 	gradeBox.value = "";
 1257: 	var radioButton = formname["RADVAL"+id];
 1258: 	for (var i=0; i<radioButton.length; i++) {
 1259: 	    radioButton[i].checked=false;
 1260: 	}
 1261: 	stores.value = selectx;
 1262:     }
 1263: 
 1264:     function checkSolved(formname,id) {
 1265: 	if (formname["solved"+id].value == "correct_by_student" && formname.overRideScore.value == 'no') {
 1266: 	    var reply = confirm("This problem has been graded correct by the computer. Do you want to change the score?");
 1267: 	    if (!reply) {return "noupdate";}
 1268: 	    formname.overRideScore.value = 'yes';
 1269: 	}
 1270: 	return "update";
 1271:     }
 1272: 
 1273:     function updateSelect(formname,id) {
 1274: 	formname["GD_SEL"+id][0].selected = true;
 1275: 	return;
 1276:     }
 1277: 
 1278: //=========== Check that a point is assigned for all the parts  ============
 1279:     function checksubmit(formname,val,total,parttot) {
 1280: 	formname.gradeOpt.value = val;
 1281: 	if (val == "Save & Next") {
 1282: 	    for (i=0;i<=total;i++) {
 1283: 		for (j=0;j<parttot;j++) {
 1284: 		    var partid = formname["partid"+i+"_"+j].value;
 1285: 		    if (formname["GD_SEL"+i+"_"+partid][0].selected) {
 1286: 			var points = formname["GD_BOX"+i+"_"+partid].value;
 1287: 			if (points == "") {
 1288: 			    var name = formname["name"+i].value;
 1289: 			    var studentID = (name != '' ? name : formname["unamedom"+i].value);
 1290: 			    var resp = confirm("You did not assign a score for "+studentID+
 1291: 					       ", part "+partid+". Continue?");
 1292: 			    if (resp == false) {
 1293: 				formname["GD_BOX"+i+"_"+partid].focus();
 1294: 				return false;
 1295: 			    }
 1296: 			}
 1297: 		    }
 1298: 		    
 1299: 		}
 1300: 	    }
 1301: 	    
 1302: 	}
 1303: 	if (val == "Grade Student") {
 1304: 	    formname.showgrading.value = "yes";
 1305: 	    if (formname.Status.value == "") {
 1306: 		formname.Status.value = "Active";
 1307: 	    }
 1308: 	    formname.studentNo.value = total;
 1309: 	}
 1310: 	formname.submit();
 1311:     }
 1312: 
 1313: //======= Check that a score is assigned for all the problems (page/sequence grading only) =========
 1314:     function checkSubmitPage(formname,total) {
 1315: 	noscore = new Array(100);
 1316: 	var ptr = 0;
 1317: 	for (i=1;i<total;i++) {
 1318: 	    var partid = formname["q_"+i].value;
 1319: 	    if (formname["GD_SEL"+i+"_"+partid][0].selected) {
 1320: 		var points = formname["GD_BOX"+i+"_"+partid].value;
 1321: 		var status = formname["solved"+i+"_"+partid].value;
 1322: 		if (points == "" && status != "correct_by_student") {
 1323: 		    noscore[ptr] = i;
 1324: 		    ptr++;
 1325: 		}
 1326: 	    }
 1327: 	}
 1328: 	if (ptr != 0) {
 1329: 	    var sense = ptr == 1 ? ": " : "s: ";
 1330: 	    var prolist = "";
 1331: 	    if (ptr == 1) {
 1332: 		prolist = noscore[0];
 1333: 	    } else {
 1334: 		var i = 0;
 1335: 		while (i < ptr-1) {
 1336: 		    prolist += noscore[i]+", ";
 1337: 		    i++;
 1338: 		}
 1339: 		prolist += "and "+noscore[i];
 1340: 	    }
 1341: 	    var resp = confirm("You did not assign any score for the following problem"+sense+prolist+". Continue?");
 1342: 	    if (resp == false) {
 1343: 		return false;
 1344: 	    }
 1345: 	}
 1346: 
 1347: 	formname.submit();
 1348:     }
 1349: </script>
 1350: SUBJAVASCRIPT
 1351: }
 1352: 
 1353: #--- javascript for essay type problem --
 1354: sub sub_page_kw_js {
 1355:     my $request = shift;
 1356:     my $iconpath = $request->dir_config('lonIconsURL');
 1357:     &commonJSfunctions($request);
 1358: 
 1359:     my $inner_js_msg_central=<<INNERJS;
 1360:     <script text="text/javascript">
 1361:     function checkInput() {
 1362:       opener.document.SCORE.msgsub.value = opener.checkEntities(document.msgcenter.msgsub.value);
 1363:       var nmsg   = opener.document.SCORE.savemsgN.value;
 1364:       var usrctr = document.msgcenter.usrctr.value;
 1365:       var newval = opener.document.SCORE["newmsg"+usrctr];
 1366:       newval.value = opener.checkEntities(document.msgcenter.newmsg.value);
 1367: 
 1368:       var msgchk = "";
 1369:       if (document.msgcenter.subchk.checked) {
 1370:          msgchk = "msgsub,";
 1371:       }
 1372:       var includemsg = 0;
 1373:       for (var i=1; i<=nmsg; i++) {
 1374:           var opnmsg = opener.document.SCORE["savemsg"+i];
 1375:           var frmmsg = document.msgcenter["msg"+i];
 1376:           opnmsg.value = opener.checkEntities(frmmsg.value);
 1377:           var showflg = opener.document.SCORE["shownOnce"+i];
 1378:           showflg.value = "1";
 1379:           var chkbox = document.msgcenter["msgn"+i];
 1380:           if (chkbox.checked) {
 1381:              msgchk += "savemsg"+i+",";
 1382:              includemsg = 1;
 1383:           }
 1384:       }
 1385:       if (document.msgcenter.newmsgchk.checked) {
 1386:          msgchk += "newmsg"+usrctr;
 1387:          includemsg = 1;
 1388:       }
 1389:       imgformname = opener.document.SCORE["mailicon"+usrctr];
 1390:       imgformname.src = "$iconpath/"+((includemsg) ? "mailto.gif" : "mailbkgrd.gif");
 1391:       var includemsg = opener.document.SCORE["includemsg"+usrctr];
 1392:       includemsg.value = msgchk;
 1393: 
 1394:       self.close()
 1395: 
 1396:     }
 1397:     </script>
 1398: INNERJS
 1399: 
 1400:     my $inner_js_highlight_central=<<INNERJS;
 1401:  <script type="text/javascript">
 1402:     function updateChoice(flag) {
 1403:       opener.document.SCORE.kwclr.value = opener.radioSelection(document.hlCenter.kwdclr);
 1404:       opener.document.SCORE.kwsize.value = opener.radioSelection(document.hlCenter.kwdsize);
 1405:       opener.document.SCORE.kwstyle.value = opener.radioSelection(document.hlCenter.kwdstyle);
 1406:       opener.document.SCORE.refresh.value = "on";
 1407:       if (opener.document.SCORE.keywords.value!=""){
 1408:          opener.document.SCORE.submit();
 1409:       }
 1410:       self.close()
 1411:     }
 1412: </script>
 1413: INNERJS
 1414: 
 1415:     my $start_page_msg_central = 
 1416:         &Apache::loncommon::start_page('Message Central',$inner_js_msg_central,
 1417: 				       {'js_ready'  => 1,
 1418: 					'only_body' => 1,
 1419: 					'bgcolor'   =>'#FFFFFF',});
 1420:     my $end_page_msg_central = 
 1421: 	&Apache::loncommon::end_page({'js_ready' => 1});
 1422: 
 1423: 
 1424:     my $start_page_highlight_central = 
 1425:         &Apache::loncommon::start_page('Highlight Central',
 1426: 				       $inner_js_highlight_central,
 1427: 				       {'js_ready'  => 1,
 1428: 					'only_body' => 1,
 1429: 					'bgcolor'   =>'#FFFFFF',});
 1430:     my $end_page_highlight_central = 
 1431: 	&Apache::loncommon::end_page({'js_ready' => 1});
 1432: 
 1433:     my $docopen=&Apache::lonhtmlcommon::javascript_docopen();
 1434:     $docopen=~s/^document\.//;
 1435:     my $alertmsg = &mt('Please select a word or group of words from document and then click this link.');
 1436:     $request->print(<<SUBJAVASCRIPT);
 1437: <script type="text/javascript" language="javascript">
 1438: 
 1439: //===================== Show list of keywords ====================
 1440:   function keywords(formname) {
 1441:     var nret = prompt("Keywords list, separated by a space. Add/delete to list if desired.",formname.keywords.value);
 1442:     if (nret==null) return;
 1443:     formname.keywords.value = nret;
 1444: 
 1445:     if (formname.keywords.value != "") {
 1446: 	formname.refresh.value = "on";
 1447: 	formname.submit();
 1448:     }
 1449:     return;
 1450:   }
 1451: 
 1452: //===================== Script to view submitted by ==================
 1453:   function viewSubmitter(submitter) {
 1454:     document.SCORE.refresh.value = "on";
 1455:     document.SCORE.NCT.value = "1";
 1456:     document.SCORE.unamedom0.value = submitter;
 1457:     document.SCORE.submit();
 1458:     return;
 1459:   }
 1460: 
 1461: //===================== Script to add keyword(s) ==================
 1462:   function getSel() {
 1463:     if (document.getSelection) txt = document.getSelection();
 1464:     else if (document.selection) txt = document.selection.createRange().text;
 1465:     else return;
 1466:     var cleantxt = txt.replace(new RegExp('([\\f\\n\\r\\t\\v ])+', 'g')," ");
 1467:     if (cleantxt=="") {
 1468: 	alert("$alertmsg");
 1469: 	return;
 1470:     }
 1471:     var nret = prompt("Add selection to keyword list? Edit if desired.",cleantxt);
 1472:     if (nret==null) return;
 1473:     document.SCORE.keywords.value = document.SCORE.keywords.value+" "+nret;
 1474:     if (document.SCORE.keywords.value != "") {
 1475: 	document.SCORE.refresh.value = "on";
 1476: 	document.SCORE.submit();
 1477:     }
 1478:     return;
 1479:   }
 1480: 
 1481: //====================== Script for composing message ==============
 1482:    // preload images
 1483:    img1 = new Image();
 1484:    img1.src = "$iconpath/mailbkgrd.gif";
 1485:    img2 = new Image();
 1486:    img2.src = "$iconpath/mailto.gif";
 1487: 
 1488:   function msgCenter(msgform,usrctr,fullname) {
 1489:     var Nmsg  = msgform.savemsgN.value;
 1490:     savedMsgHeader(Nmsg,usrctr,fullname);
 1491:     var subject = msgform.msgsub.value;
 1492:     var msgchk = document.SCORE["includemsg"+usrctr].value;
 1493:     re = /msgsub/;
 1494:     var shwsel = "";
 1495:     if (re.test(msgchk)) { shwsel = "checked" }
 1496:     subject = (document.SCORE.shownSub.value == 0 ? checkEntities(subject) : subject);
 1497:     displaySubject(checkEntities(subject),shwsel);
 1498:     for (var i=1; i<=Nmsg; i++) {
 1499: 	var testmsg = "savemsg"+i+",";
 1500: 	re = new RegExp(testmsg,"g");
 1501: 	shwsel = "";
 1502: 	if (re.test(msgchk)) { shwsel = "checked" }
 1503: 	var message = document.SCORE["savemsg"+i].value;
 1504: 	message = (document.SCORE["shownOnce"+i].value == 0 ? checkEntities(message) : message);
 1505: 	displaySavedMsg(i,message,shwsel); //I do not get it. w/o checkEntities on saved messages,
 1506: 	                                   //any &lt; is already converted to <, etc. However, only once!!
 1507:     }
 1508:     newmsg = document.SCORE["newmsg"+usrctr].value;
 1509:     shwsel = "";
 1510:     re = /newmsg/;
 1511:     if (re.test(msgchk)) { shwsel = "checked" }
 1512:     newMsg(newmsg,shwsel);
 1513:     msgTail(); 
 1514:     return;
 1515:   }
 1516: 
 1517:   function checkEntities(strx) {
 1518:     if (strx.length == 0) return strx;
 1519:     var orgStr = ["&", "<", ">", '"']; 
 1520:     var newStr = ["&amp;", "&lt;", "&gt;", "&quot;"];
 1521:     var counter = 0;
 1522:     while (counter < 4) {
 1523: 	strx = strReplace(strx,orgStr[counter],newStr[counter]);
 1524: 	counter++;
 1525:     }
 1526:     return strx;
 1527:   }
 1528: 
 1529:   function strReplace(strx, orgStr, newStr) {
 1530:     return strx.split(orgStr).join(newStr);
 1531:   }
 1532: 
 1533:   function savedMsgHeader(Nmsg,usrctr,fullname) {
 1534:     var height = 70*Nmsg+250;
 1535:     var scrollbar = "no";
 1536:     if (height > 600) {
 1537: 	height = 600;
 1538: 	scrollbar = "yes";
 1539:     }
 1540:     var xpos = (screen.width-600)/2;
 1541:     xpos = (xpos < 0) ? '0' : xpos;
 1542:     var ypos = (screen.height-height)/2-30;
 1543:     ypos = (ypos < 0) ? '0' : ypos;
 1544: 
 1545:     pWin = window.open('', 'MessageCenter', 'resizable=yes,toolbar=no,location=no,scrollbars='+scrollbar+',screenx='+xpos+',screeny='+ypos+',width=600,height='+height);
 1546:     pWin.focus();
 1547:     pDoc = pWin.document;
 1548:     pDoc.$docopen;
 1549:     pDoc.write('$start_page_msg_central');
 1550: 
 1551:     pDoc.write("<form action=\\"inactive\\" name=\\"msgcenter\\">");
 1552:     pDoc.write("<input value=\\""+usrctr+"\\" name=\\"usrctr\\" type=\\"hidden\\">");
 1553:     pDoc.write("<h3><span class=\\"LC_info\\">&nbsp;Compose Message for \"+fullname+\"<\\/span><\\/h3><br /><br />");
 1554: 
 1555:     pDoc.write('<table border="0" width="100%"><tr><td bgcolor="#777777">');
 1556:     pDoc.write('<table border="0" width="100%"><tr bgcolor="#DDFFFF">');
 1557:     pDoc.write("<td><b>Type<\\/b><\\/td><td><b>Include<\\/b><\\/td><td><b>Message<\\/td><\\/tr>");
 1558: }
 1559:     function displaySubject(msg,shwsel) {
 1560:     pDoc = pWin.document;
 1561:     pDoc.write("<tr bgcolor=\\"#ffffdd\\">");
 1562:     pDoc.write("<td>Subject<\\/td>");
 1563:     pDoc.write("<td align=\\"center\\"><input name=\\"subchk\\" type=\\"checkbox\\"" +shwsel+"><\\/td>");
 1564:     pDoc.write("<td><input name=\\"msgsub\\" type=\\"text\\" value=\\""+msg+"\\"size=\\"60\\" maxlength=\\"80\\"><\\/td><\\/tr>");
 1565: }
 1566: 
 1567:   function displaySavedMsg(ctr,msg,shwsel) {
 1568:     pDoc = pWin.document;
 1569:     pDoc.write("<tr bgcolor=\\"#ffffdd\\">");
 1570:     pDoc.write("<td align=\\"center\\">"+ctr+"<\\/td>");
 1571:     pDoc.write("<td align=\\"center\\"><input name=\\"msgn"+ctr+"\\" type=\\"checkbox\\"" +shwsel+"><\\/td>");
 1572:     pDoc.write("<td><textarea name=\\"msg"+ctr+"\\" cols=\\"60\\" rows=\\"3\\">"+msg+"<\\/textarea><\\/td><\\/tr>");
 1573: }
 1574: 
 1575:   function newMsg(newmsg,shwsel) {
 1576:     pDoc = pWin.document;
 1577:     pDoc.write("<tr bgcolor=\\"#ffffdd\\">");
 1578:     pDoc.write("<td align=\\"center\\">New<\\/td>");
 1579:     pDoc.write("<td align=\\"center\\"><input name=\\"newmsgchk\\" type=\\"checkbox\\"" +shwsel+"><\\/td>");
 1580:     pDoc.write("<td><textarea name=\\"newmsg\\" cols=\\"60\\" rows=\\"3\\" onchange=\\"javascript:this.form.newmsgchk.checked=true\\" >"+newmsg+"<\\/textarea><\\/td><\\/tr>");
 1581: }
 1582: 
 1583:   function msgTail() {
 1584:     pDoc = pWin.document;
 1585:     pDoc.write("<\\/table>");
 1586:     pDoc.write("<\\/td><\\/tr><\\/table>&nbsp;");
 1587:     pDoc.write("<input type=\\"button\\" value=\\"Save\\" onClick=\\"javascript:checkInput()\\">&nbsp;&nbsp;");
 1588:     pDoc.write("<input type=\\"button\\" value=\\"Cancel\\" onClick=\\"self.close()\\"><br /><br />");
 1589:     pDoc.write("<\\/form>");
 1590:     pDoc.write('$end_page_msg_central');
 1591:     pDoc.close();
 1592: }
 1593: 
 1594: //====================== Script for keyword highlight options ==============
 1595:   function kwhighlight() {
 1596:     var kwclr    = document.SCORE.kwclr.value;
 1597:     var kwsize   = document.SCORE.kwsize.value;
 1598:     var kwstyle  = document.SCORE.kwstyle.value;
 1599:     var redsel = "";
 1600:     var grnsel = "";
 1601:     var blusel = "";
 1602:     if (kwclr=="red")   {var redsel="checked"};
 1603:     if (kwclr=="green") {var grnsel="checked"};
 1604:     if (kwclr=="blue")  {var blusel="checked"};
 1605:     var sznsel = "";
 1606:     var sz1sel = "";
 1607:     var sz2sel = "";
 1608:     if (kwsize=="0")  {var sznsel="checked"};
 1609:     if (kwsize=="+1") {var sz1sel="checked"};
 1610:     if (kwsize=="+2") {var sz2sel="checked"};
 1611:     var synsel = "";
 1612:     var syisel = "";
 1613:     var sybsel = "";
 1614:     if (kwstyle=="")    {var synsel="checked"};
 1615:     if (kwstyle=="<i>") {var syisel="checked"};
 1616:     if (kwstyle=="<b>") {var sybsel="checked"};
 1617:     highlightCentral();
 1618:     highlightbody('red','red',redsel,'0','normal',sznsel,'','normal',synsel);
 1619:     highlightbody('green','green',grnsel,'+1','+1',sz1sel,'<i>','italic',syisel);
 1620:     highlightbody('blue','blue',blusel,'+2','+2',sz2sel,'<b>','bold',sybsel);
 1621:     highlightend();
 1622:     return;
 1623:   }
 1624: 
 1625:   function highlightCentral() {
 1626: //    if (window.hwdWin) window.hwdWin.close();
 1627:     var xpos = (screen.width-400)/2;
 1628:     xpos = (xpos < 0) ? '0' : xpos;
 1629:     var ypos = (screen.height-330)/2-30;
 1630:     ypos = (ypos < 0) ? '0' : ypos;
 1631: 
 1632:     hwdWin = window.open('', 'KeywordHighlightCentral', 'resizeable=yes,toolbar=no,location=no,scrollbars=no,width=400,height=300,screenx='+xpos+',screeny='+ypos);
 1633:     hwdWin.focus();
 1634:     var hDoc = hwdWin.document;
 1635:     hDoc.$docopen;
 1636:     hDoc.write('$start_page_highlight_central');
 1637:     hDoc.write("<form action=\\"inactive\\" name=\\"hlCenter\\">");
 1638:     hDoc.write("<h3><span class=\\"LC_info\\">&nbsp;Keyword Highlight Options<\\/span><\\/h3><br /><br />");
 1639: 
 1640:     hDoc.write('<table border="0" width="100%"><tr><td bgcolor="#777777">');
 1641:     hDoc.write('<table border="0" width="100%"><tr bgcolor="#DDFFFF">');
 1642:     hDoc.write("<td><b>Text Color<\\/b><\\/td><td><b>Font Size<\\/b><\\/td><td><b>Font Style<\\/td><\\/tr>");
 1643:   }
 1644: 
 1645:   function highlightbody(clrval,clrtxt,clrsel,szval,sztxt,szsel,syval,sytxt,sysel) { 
 1646:     var hDoc = hwdWin.document;
 1647:     hDoc.write("<tr bgcolor=\\"#ffffdd\\">");
 1648:     hDoc.write("<td align=\\"left\\">");
 1649:     hDoc.write("<input name=\\"kwdclr\\" type=\\"radio\\" value=\\""+clrval+"\\" "+clrsel+">&nbsp;"+clrtxt+"<\\/td>");
 1650:     hDoc.write("<td align=\\"left\\">");
 1651:     hDoc.write("<input name=\\"kwdsize\\" type=\\"radio\\" value=\\""+szval+"\\" "+szsel+">&nbsp;"+sztxt+"<\\/td>");
 1652:     hDoc.write("<td align=\\"left\\">");
 1653:     hDoc.write("<input name=\\"kwdstyle\\" type=\\"radio\\" value=\\""+syval+"\\" "+sysel+">&nbsp;"+sytxt+"<\\/td>");
 1654:     hDoc.write("<\\/tr>");
 1655:   }
 1656: 
 1657:   function highlightend() { 
 1658:     var hDoc = hwdWin.document;
 1659:     hDoc.write("<\\/table>");
 1660:     hDoc.write("<\\/td><\\/tr><\\/table>&nbsp;");
 1661:     hDoc.write("<input type=\\"button\\" value=\\"Save\\" onClick=\\"javascript:updateChoice(1)\\">&nbsp;&nbsp;");
 1662:     hDoc.write("<input type=\\"button\\" value=\\"Cancel\\" onClick=\\"self.close()\\"><br /><br />");
 1663:     hDoc.write("<\\/form>");
 1664:     hDoc.write('$end_page_highlight_central');
 1665:     hDoc.close();
 1666:   }
 1667: 
 1668: </script>
 1669: SUBJAVASCRIPT
 1670: }
 1671: 
 1672: sub get_increment {
 1673:     my $increment = $env{'form.increment'};
 1674:     if ($increment != 1 && $increment != .5 && $increment != .25 &&
 1675:         $increment != .1) {
 1676:         $increment = 1;
 1677:     }
 1678:     return $increment;
 1679: }
 1680: 
 1681: #--- displays the grading box, used in essay type problem and grading by page/sequence
 1682: sub gradeBox {
 1683:     my ($request,$symb,$uname,$udom,$counter,$partid,$record) = @_;
 1684:     my $checkIcon = '<img alt="'.&mt('Check Mark').
 1685: 	'" src="'.&Apache::loncommon::lonhttpdurl($request->dir_config('lonIconsURL').'/check.gif').'" height="16" border="0" />';
 1686:     my $wgt    = &Apache::lonnet::EXT('resource.'.$partid.'.weight',$symb,$udom,$uname);
 1687:     my $wgtmsg = ($wgt > 0) ? &mt('(problem weight)') 
 1688:                            : '<span class="LC_info">'.&mt('problem weight assigned by computer').'</span>';
 1689:     $wgt       = ($wgt > 0 ? $wgt : '1');
 1690:     my $score  = ($$record{'resource.'.$partid.'.awarded'} eq '' ?
 1691: 		  '' : &compute_points($$record{'resource.'.$partid.'.awarded'},$wgt));
 1692:     my $result='<input type="hidden" name="WGT'.$counter.'_'.$partid.'" value="'.$wgt.'" />'."\n";
 1693:     my $display_part= &get_display_part($partid,$symb);
 1694:     my %last_resets = &get_last_resets($symb,$env{'request.course.id'},
 1695: 				       [$partid]);
 1696:     my $aggtries = $$record{'resource.'.$partid.'.tries'};
 1697:     if ($last_resets{$partid}) {
 1698:         $aggtries = &get_num_tries($record,$last_resets{$partid},$partid);
 1699:     }
 1700:     $result.='<table border="0"><tr>';
 1701:     my $ctr = 0;
 1702:     my $thisweight = 0;
 1703:     my $increment = &get_increment();
 1704: 
 1705:     my $radio.='<table border="0"><tr>'."\n";  # display radio buttons in a nice table 10 across
 1706:     while ($thisweight<=$wgt) {
 1707: 	$radio.= '<td><span class="LC_nobreak"><label><input type="radio" name="RADVAL'.$counter.'_'.$partid.'" '.
 1708: 	    'onclick="javascript:writeBox(this.form,\''.$counter.'_'.$partid.'\','.
 1709: 	    $thisweight.')" value="'.$thisweight.'" '.
 1710: 	    ($score eq $thisweight ? 'checked="checked"':'').' /> '.$thisweight."</label></span></td>\n";
 1711: 	$radio.=(($ctr+1)%10 == 0 ? '</tr><tr>' : '');
 1712:         $thisweight += $increment;
 1713: 	$ctr++;
 1714:     }
 1715:     $radio.='</tr></table>';
 1716: 
 1717:     my $line.='<input type="text" name="GD_BOX'.$counter.'_'.$partid.'"'.
 1718: 	($score ne ''? ' value = "'.$score.'"':'').' size="4" '.
 1719: 	'onChange="javascript:updateRadio(this.form,\''.$counter.'_'.$partid.'\','.
 1720: 	$wgt.')" /></td>'."\n";
 1721:     $line.='<td>/'.$wgt.' '.$wgtmsg.
 1722: 	($$record{'resource.'.$partid.'.solved'} eq 'correct_by_student' ? '&nbsp;'.$checkIcon : '').
 1723: 	' </td><td><b>'.&mt('Grade Status').':</b>'."\n";
 1724:     $line.='<select name="GD_SEL'.$counter.'_'.$partid.'" '.
 1725: 	'onChange="javascript:clearRadBox(this.form,\''.$counter.'_'.$partid.'\')" >'."\n";
 1726:     if ($$record{'resource.'.$partid.'.solved'} eq 'excused') {
 1727: 	$line.='<option></option>'.
 1728: 	    '<option value="excused" selected="selected">'.&mt('excused').'</option>';
 1729:     } else {
 1730: 	$line.='<option selected="selected"></option>'.
 1731: 	    '<option value="excused" >'.&mt('excused').'</option>';
 1732:     }
 1733:     $line.='<option value="reset status">'.&mt('reset status').'</option></select>'."\n";
 1734: 
 1735: 
 1736: 	#&mt('<td><b>Part:</b></td><td>[_1]</td><td><b>Points:</b></td><td>[_2]</td><td>or</td><td>[_3]</td>',$display_part,$radio,$line);
 1737:     $result .= 
 1738: 	    '<td><b>'.&mt('Part:').'</b></td><td>'.$display_part.'</td><td><b>'.&mt('Points:').'</b></td><td>'.$radio.'</td><td>'.&mt('or').'</td><td>'.$line.'</td>'.
 1739:     
 1740:     $result.='</tr></table>'."\n";
 1741:     $result.='<input type="hidden" name="stores'.$counter.'_'.$partid.'" value="" />'."\n".
 1742: 	'<input type="hidden" name="oldpts'.$counter.'_'.$partid.'" value="'.$score.'" />'."\n".
 1743: 	'<input type="hidden" name="solved'.$counter.'_'.$partid.'" value="'.
 1744: 	$$record{'resource.'.$partid.'.solved'}.'" />'."\n".
 1745:         '<input type="hidden" name="totaltries'.$counter.'_'.$partid.'" value="'.
 1746:         $$record{'resource.'.$partid.'.tries'}.'" />'."\n".
 1747:         '<input type="hidden" name="aggtries'.$counter.'_'.$partid.'" value="'.
 1748:         $aggtries.'" />'."\n";
 1749:     $result.=&handback_box($symb,$uname,$udom,$counter,$partid,$record);
 1750:     return $result;
 1751: }
 1752: 
 1753: sub handback_box {
 1754:     my ($symb,$uname,$udom,$counter,$partid,$record) = @_;
 1755:     my ($partlist,$handgrade,$responseType) = &response_type($symb);
 1756:     my (@respids);
 1757:      my @part_response_id = &flatten_responseType($responseType);
 1758:     foreach my $part_response_id (@part_response_id) {
 1759:     	my ($part,$resp) = @{ $part_response_id };
 1760:         if ($part eq $partid) {
 1761:             push(@respids,$resp);
 1762:         }
 1763:     }
 1764:     my $result;
 1765:     foreach my $respid (@respids) {
 1766: 	my $prefix = $counter.'_'.$partid.'_'.$respid.'_';
 1767: 	my $files=&get_submitted_files($udom,$uname,$partid,$respid,$record);
 1768: 	next if (!@$files);
 1769: 	my $file_counter = 1;
 1770: 	foreach my $file (@$files) {
 1771: 	    if ($file =~ /\/portfolio\//) {
 1772:     	        my ($file_path, $file_disp) = ($file =~ m|(.+/)(.+)$|);
 1773:     	        my ($name,$version,$ext) = &file_name_version_ext($file_disp);
 1774:     	        $file_disp = "$name.$ext";
 1775:     	        $file = $file_path.$file_disp;
 1776:     	        $result.=&mt('Return commented version of [_1] to student.',
 1777:     			 '<span class="LC_filename">'.$file_disp.'</span>');
 1778:     	        $result.='<input type="file"   name="'.$prefix.'returndoc'.$file_counter.'" />'."\n";
 1779:     	        $result.='<input type="hidden" name="'.$prefix.'origdoc'.$file_counter.'" value="'.$file.'" /><br />';
 1780:     	        $result.='('.&mt('File will be uploaded when you click on Save &amp; Next below.').')<br />';
 1781:     	        $file_counter++;
 1782: 	    }
 1783: 	}
 1784:     }
 1785:     return $result;    
 1786: }
 1787: 
 1788: sub show_problem {
 1789:     my ($request,$symb,$uname,$udom,$removeform,$viewon,$mode,$form) = @_;
 1790:     my $rendered;
 1791:     my %form = ((ref($form) eq 'HASH')? %{$form} : ());
 1792:     &Apache::lonxml::remember_problem_counter();
 1793:     if ($mode eq 'both' or $mode eq 'text') {
 1794: 	$rendered=&Apache::loncommon::get_student_view($symb,$uname,$udom,
 1795: 						       $env{'request.course.id'},
 1796: 						       undef,\%form);
 1797:     }
 1798:     if ($removeform) {
 1799: 	$rendered=~s|<form(.*?)>||g;
 1800: 	$rendered=~s|</form>||g;
 1801: 	$rendered=~s|(<input[^>]*name\s*=\s*"?)(\w+)("?)|$1would_have_been_$2$3|g;
 1802:     }
 1803:     my $companswer;
 1804:     if ($mode eq 'both' or $mode eq 'answer') {
 1805: 	&Apache::lonxml::restore_problem_counter();
 1806: 	$companswer=
 1807: 	    &Apache::loncommon::get_student_answers($symb,$uname,$udom,
 1808: 						    $env{'request.course.id'},
 1809: 						    %form);
 1810:     }
 1811:     if ($removeform) {
 1812: 	$companswer=~s|<form(.*?)>||g;
 1813: 	$companswer=~s|</form>||g;
 1814: 	$companswer=~s|name="submit"|name="would_have_been_submit"|g;
 1815:     }
 1816:     $rendered=
 1817: 	'<div class="LC_grade_show_problem_header">'.
 1818: 	&mt('View of the problem').
 1819: 	'</div><div class="LC_grade_show_problem_problem">'.
 1820: 	$rendered.
 1821: 	'</div>';
 1822:     $companswer=
 1823: 	'<div class="LC_grade_show_problem_header">'.
 1824: 	&mt('Correct answer').
 1825: 	'</div><div class="LC_grade_show_problem_problem">'.
 1826: 	$companswer.
 1827: 	'</div>';
 1828:     my $result;
 1829:     if ($mode eq 'both') {
 1830: 	$result=$rendered.$companswer;
 1831:     } elsif ($mode eq 'text') {
 1832: 	$result=$rendered;
 1833:     } elsif ($mode eq 'answer') {
 1834: 	$result=$companswer;
 1835:     }
 1836:     $result='<div class="LC_grade_show_problem">'.$result.'</div>';
 1837:     return $result;
 1838: }
 1839: 
 1840: sub files_exist {
 1841:     my ($r, $symb) = @_;
 1842:     my @students = &Apache::loncommon::get_env_multiple('form.stuinfo');
 1843: 
 1844:     foreach my $student (@students) {
 1845:         my ($uname,$udom,$fullname) = split(/:/,$student);
 1846:         my %record = &Apache::lonnet::restore($symb,$env{'request.course.id'},
 1847: 					      $udom,$uname);
 1848:         my ($string,$timestamp)= &get_last_submission(\%record);
 1849:         foreach my $submission (@$string) {
 1850:             my ($partid,$respid) =
 1851: 		($submission =~ /^resource\.([^\.]*)\.([^\.]*)\.submission/);
 1852:             my $files=&get_submitted_files($udom,$uname,$partid,$respid,
 1853: 					   \%record);
 1854:             return 1 if (@$files);
 1855:         }
 1856:     }
 1857:     return 0;
 1858: }
 1859: 
 1860: sub download_all_link {
 1861:     my ($r,$symb) = @_;
 1862:     my $all_students = 
 1863: 	join("\n", &Apache::loncommon::get_env_multiple('form.stuinfo'));
 1864: 
 1865:     my $parts =
 1866: 	join("\n",&Apache::loncommon::get_env_multiple('form.vPart'));
 1867: 
 1868:     my $identifier = &Apache::loncommon::get_cgi_id();
 1869:     &Apache::lonnet::appenv({'cgi.'.$identifier.'.students' => $all_students,
 1870:                              'cgi.'.$identifier.'.symb' => $symb,
 1871:                              'cgi.'.$identifier.'.parts' => $parts,});
 1872:     $r->print('<a href="/cgi-bin/multidownload.pl?'.$identifier.'">'.
 1873: 	      &mt('Download All Submitted Documents').'</a>');
 1874:     return
 1875: }
 1876: 
 1877: sub build_section_inputs {
 1878:     my $section_inputs;
 1879:     if ($env{'form.section'} eq '') {
 1880:         $section_inputs .= '<input type="hidden" name="section" value="all" />'."\n";
 1881:     } else {
 1882:         my @sections = &Apache::loncommon::get_env_multiple('form.section');
 1883:         foreach my $section (@sections) {
 1884:             $section_inputs .= '<input type="hidden" name="section" value="'.$section.'" />'."\n";
 1885:         }
 1886:     }
 1887:     return $section_inputs;
 1888: }
 1889: 
 1890: # --------------------------- show submissions of a student, option to grade 
 1891: sub submission {
 1892:     my ($request,$counter,$total) = @_;
 1893:     my ($uname,$udom)     = ($env{'form.student'},$env{'form.userdom'});
 1894:     $udom = ($udom eq '' ? $env{'user.domain'} : $udom); #has form.userdom changed for a student?
 1895:     my $usec = &Apache::lonnet::getsection($udom,$uname,$env{'request.course.id'});
 1896:     $env{'form.fullname'} = &Apache::loncommon::plainname($uname,$udom,'lastname') if $env{'form.fullname'} eq '';
 1897:     my $symb = &get_symb($request); 
 1898:     if ($symb eq '') { $request->print("Unable to handle ambiguous references:."); return ''; }
 1899: 
 1900:     if (!&canview($usec)) {
 1901: 	$request->print('<span class="LC_warning">Unable to view requested student.('.
 1902: 			$uname.':'.$udom.' in section '.$usec.' in course id '.
 1903: 			$env{'request.course.id'}.')</span>');
 1904: 	$request->print(&show_grading_menu_form($symb));
 1905: 	return;
 1906:     }
 1907: 
 1908:     if (!$env{'form.lastSub'}) { $env{'form.lastSub'} = 'datesub'; }
 1909:     if (!$env{'form.vProb'}) { $env{'form.vProb'} = 'yes'; }
 1910:     if (!$env{'form.vAns'}) { $env{'form.vAns'} = 'yes'; }
 1911:     my $last = ($env{'form.lastSub'} eq 'last' ? 'last' : '');
 1912:     my $checkIcon = '<img alt="'.&mt('Check Mark').
 1913: 	'" src="'.$request->dir_config('lonIconsURL').
 1914: 	'/check.gif" height="16" border="0" />';
 1915: 
 1916:     my %old_essays;
 1917:     # header info
 1918:     if ($counter == 0) {
 1919: 	&sub_page_js($request);
 1920: 	&sub_page_kw_js($request) if ($env{'form.handgrade'} eq 'yes');
 1921: 	$env{'form.probTitle'} = $env{'form.probTitle'} eq '' ? 
 1922: 	    &Apache::lonnet::gettitle($symb) : $env{'form.probTitle'};
 1923: 	if ($env{'form.handgrade'} eq 'yes' && &files_exist($request, $symb)) {
 1924: 	    &download_all_link($request, $symb);
 1925: 	}
 1926: 	$request->print('<h3>&nbsp;<span class="LC_info">'.&mt('Submission Record').'</span></h3>'."\n".
 1927: 			'<h4>&nbsp;'.&mt('<b>Resource: </b> [_1]',$env{'form.probTitle'}).'</h4>'."\n");
 1928: 
 1929: 	# option to display problem, only once else it cause problems 
 1930:         # with the form later since the problem has a form.
 1931: 	if ($env{'form.vProb'} eq 'yes' or $env{'form.vAns'} eq 'yes') {
 1932: 	    my $mode;
 1933: 	    if ($env{'form.vProb'} eq 'yes' && $env{'form.vAns'} eq 'yes') {
 1934: 		$mode='both';
 1935: 	    } elsif ($env{'form.vProb'} eq 'yes') {
 1936: 		$mode='text';
 1937: 	    } elsif ($env{'form.vAns'} eq 'yes') {
 1938: 		$mode='answer';
 1939: 	    }
 1940: 	    &Apache::lonxml::clear_problem_counter();
 1941: 	    $request->print(&show_problem($request,$symb,$uname,$udom,0,1,$mode));
 1942: 	}
 1943: 
 1944: 	# kwclr is the only variable that is guaranteed to be non blank 
 1945:         # if this subroutine has been called once.
 1946: 	my %keyhash = ();
 1947: 	if ($env{'form.kwclr'} eq '' && $env{'form.handgrade'} eq 'yes') {
 1948: 	    %keyhash = &Apache::lonnet::dump('nohist_handgrade',
 1949: 					     $env{'course.'.$env{'request.course.id'}.'.domain'},
 1950: 					     $env{'course.'.$env{'request.course.id'}.'.num'});
 1951: 
 1952: 	    my $loginuser = $env{'user.name'}.':'.$env{'user.domain'};
 1953: 	    $env{'form.keywords'} = $keyhash{$symb.'_keywords'} ne '' ? $keyhash{$symb.'_keywords'} : '';
 1954: 	    $env{'form.kwclr'}    = $keyhash{$loginuser.'_kwclr'} ne '' ? $keyhash{$loginuser.'_kwclr'} : 'red';
 1955: 	    $env{'form.kwsize'}   = $keyhash{$loginuser.'_kwsize'} ne '' ? $keyhash{$loginuser.'_kwsize'} : '0';
 1956: 	    $env{'form.kwstyle'}  = $keyhash{$loginuser.'_kwstyle'} ne '' ? $keyhash{$loginuser.'_kwstyle'} : '';
 1957: 	    $env{'form.msgsub'}   = $keyhash{$symb.'_subject'} ne '' ? 
 1958: 		$keyhash{$symb.'_subject'} : $env{'form.probTitle'};
 1959: 	    $env{'form.savemsgN'} = $keyhash{$symb.'_savemsgN'} ne '' ? $keyhash{$symb.'_savemsgN'} : '0';
 1960: 	}
 1961: 	my $overRideScore = $env{'form.overRideScore'} eq '' ? 'no' : $env{'form.overRideScore'};
 1962: 	my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status'));
 1963: 	$request->print('<form action="/adm/grades" method="post" name="SCORE" enctype="multipart/form-data">'."\n".
 1964: 			'<input type="hidden" name="command"    value="handgrade" />'."\n".
 1965: 			'<input type="hidden" name="saveState"  value="'.$env{'form.saveState'}.'" />'."\n".
 1966: 			'<input type="hidden" name="Status"     value="'.$stu_status.'" />'."\n".
 1967: 			'<input type="hidden" name="overRideScore" value="'.$overRideScore.'" />'."\n".
 1968: 			'<input type="hidden" name="probTitle"  value="'.$env{'form.probTitle'}.'" />'."\n".
 1969: 			'<input type="hidden" name="refresh"    value="off" />'."\n".
 1970: 			'<input type="hidden" name="studentNo"  value="" />'."\n".
 1971: 			'<input type="hidden" name="gradeOpt"   value="" />'."\n".
 1972: 			'<input type="hidden" name="symb"       value="'.&Apache::lonenc::check_encrypt($symb).'" />'."\n".
 1973: 			'<input type="hidden" name="showgrading" value="'.$env{'form.showgrading'}.'" />'."\n".
 1974: 			'<input type="hidden" name="vProb"      value="'.$env{'form.vProb'}.'" />'."\n".
 1975: 			'<input type="hidden" name="vAns"       value="'.$env{'form.vAns'}.'" />'."\n".
 1976: 			'<input type="hidden" name="lastSub"    value="'.$env{'form.lastSub'}.'" />'."\n".
 1977: 			&build_section_inputs().
 1978: 			'<input type="hidden" name="submitonly" value="'.$env{'form.submitonly'}.'" />'."\n".
 1979: 			'<input type="hidden" name="handgrade"  value="'.$env{'form.handgrade'}.'" />'."\n".
 1980: 			'<input type="hidden" name="NCT"'.
 1981: 			' value="'.($env{'form.NTSTU'} ne '' ? $env{'form.NTSTU'} : $total+1).'" />'."\n");
 1982: 	if ($env{'form.handgrade'} eq 'yes') {
 1983: 	    $request->print('<input type="hidden" name="keywords" value="'.$env{'form.keywords'}.'" />'."\n".
 1984: 			    '<input type="hidden" name="kwclr"    value="'.$env{'form.kwclr'}.'" />'."\n".
 1985: 			    '<input type="hidden" name="kwsize"   value="'.$env{'form.kwsize'}.'" />'."\n".
 1986: 			    '<input type="hidden" name="kwstyle"  value="'.$env{'form.kwstyle'}.'" />'."\n".
 1987: 			    '<input type="hidden" name="msgsub"   value="'.$env{'form.msgsub'}.'" />'."\n".
 1988: 			    '<input type="hidden" name="shownSub" value="0" />'."\n".
 1989: 			    '<input type="hidden" name="savemsgN" value="'.$env{'form.savemsgN'}.'" />'."\n");
 1990: 	    foreach my $partid (&Apache::loncommon::get_env_multiple('form.vPart')) {
 1991: 		$request->print('<input type="hidden" name="vPart" value="'.$partid.'" />'."\n");
 1992: 	    }
 1993: 	}
 1994: 	
 1995: 	my ($cts,$prnmsg) = (1,'');
 1996: 	while ($cts <= $env{'form.savemsgN'}) {
 1997: 	    $prnmsg.='<input type="hidden" name="savemsg'.$cts.'" value="'.
 1998: 		(!exists($keyhash{$symb.'_savemsg'.$cts}) ? 
 1999: 		 &Apache::lonfeedback::clear_out_html($env{'form.savemsg'.$cts}) :
 2000: 		 &Apache::lonfeedback::clear_out_html($keyhash{$symb.'_savemsg'.$cts})).
 2001: 		'" />'."\n".
 2002: 		'<input type="hidden" name="shownOnce'.$cts.'" value="0" />'."\n";
 2003: 	    $cts++;
 2004: 	}
 2005: 	$request->print($prnmsg);
 2006: 
 2007: 	if ($env{'form.handgrade'} eq 'yes' && $env{'form.showgrading'} eq 'yes') {
 2008: #
 2009: # Print out the keyword options line
 2010: #
 2011: 	    $request->print(<<KEYWORDS);
 2012: &nbsp;<b>Keyword Options:</b>&nbsp;
 2013: <a href="javascript:keywords(document.SCORE);" target="_self">List</a>&nbsp; &nbsp;
 2014: <a href="#" onMouseDown="javascript:getSel(); return false"
 2015:  CLASS="page">Paste Selection to List</a>&nbsp; &nbsp;
 2016: <a href="javascript:kwhighlight();" target="_self">Highlight Attribute</a><br /><br />
 2017: KEYWORDS
 2018: #
 2019: # Load the other essays for similarity check
 2020: #
 2021:             my (undef,undef,$essayurl) = &Apache::lonnet::decode_symb($symb);
 2022: 	    my ($adom,$aname,$apath)=($essayurl=~/^($LONCAPA::domain_re)\/($LONCAPA::username_re)\/(.*)$/);
 2023: 	    $apath=&escape($apath);
 2024: 	    $apath=~s/\W/\_/gs;
 2025: 	    %old_essays=&Apache::lonnet::dump('nohist_essay_'.$apath,$adom,$aname);
 2026:         }
 2027:     }
 2028: 
 2029: # This is where output for one specific student would start
 2030:     my $add_class = ($counter%2) ? 'LC_grade_show_user_odd_row' : '';
 2031:     $request->print("\n\n".
 2032:                     '<div class="LC_grade_show_user '.$add_class.'">'.
 2033: 		    '<div class="LC_grade_user_name">'.&nameUserString(undef,$env{'form.fullname'},$uname,$udom).'</div>'.
 2034: 		    '<div class="LC_grade_show_user_body">'."\n");
 2035: 
 2036:     if ($env{'form.vProb'} eq 'all' or $env{'form.vAns'} eq 'all') {
 2037: 	my $mode;
 2038: 	if ($env{'form.vProb'} eq 'all' && $env{'form.vAns'} eq 'all') {
 2039: 	    $mode='both';
 2040: 	} elsif ($env{'form.vProb'} eq 'all' ) {
 2041: 	    $mode='text';
 2042: 	} elsif ($env{'form.vAns'} eq 'all') {
 2043: 	    $mode='answer';
 2044: 	}
 2045: 	&Apache::lonxml::clear_problem_counter();
 2046: 	$request->print(&show_problem($request,$symb,$uname,$udom,1,1,$mode,{'request.prefix' => 'ctr'.$counter}));
 2047:     }
 2048: 
 2049:     my %record = &Apache::lonnet::restore($symb,$env{'request.course.id'},$udom,$uname);
 2050:     my ($partlist,$handgrade,$responseType) = &response_type($symb);
 2051: 
 2052:     # Display student info
 2053:     $request->print(($counter == 0 ? '' : '<br />'));
 2054:     my $result='<div class="LC_grade_submissions">';
 2055:     
 2056:     $result.='<div class="LC_grade_submissions_header">';
 2057:     $result.= &mt('Submissions');
 2058:     $result.='<input type="hidden" name="name'.$counter.
 2059: 	'" value="'.$env{'form.fullname'}.'" />'."\n";
 2060:     if ($env{'form.handgrade'} eq 'no') {
 2061: 	$result.='<span class="LC_grade_check_note">'.
 2062: 	    &mt('Part(s) graded correct by the computer is marked with a [_1] symbol.',$checkIcon)."</span>\n";
 2063: 
 2064:     }
 2065: 
 2066: 
 2067: 
 2068:     # If any part of the problem is an essay-response (handgraded), then check for collaborators
 2069:     my $fullname;
 2070:     my $col_fullnames = [];
 2071:     if ($env{'form.handgrade'} eq 'yes') {
 2072: 	(my $sub_result,$fullname,$col_fullnames)=
 2073: 	    &check_collaborators($symb,$uname,$udom,\%record,$handgrade,
 2074: 				 $counter);
 2075: 	$result.=$sub_result;
 2076:     }
 2077:     $request->print($result."\n");
 2078:     $request->print('</div>'."\n");
 2079:     # print student answer/submission
 2080:     # Options are (1) Handgaded submission only
 2081:     #             (2) Last submission, includes submission that is not handgraded 
 2082:     #                  (for multi-response type part)
 2083:     #             (3) Last submission plus the parts info
 2084:     #             (4) The whole record for this student
 2085:     if ($env{'form.lastSub'} =~ /^(lastonly|hdgrade)$/) {
 2086: 	my ($string,$timestamp)= &get_last_submission(\%record);
 2087: 	
 2088: 	my $lastsubonly;
 2089: 
 2090: 	if ($$timestamp eq '') {
 2091: 	    $lastsubonly.='<div class="LC_grade_submissions_body">'.$$string[0].'</div>'; 
 2092: 	} else {
 2093: 	    $lastsubonly = '<div class="LC_grade_submissions_body"> <b>Date Submitted:</b> '.$$timestamp."\n";
 2094: 
 2095: 	    my %seenparts;
 2096: 	    my @part_response_id = &flatten_responseType($responseType);
 2097: 	    foreach my $part (@part_response_id) {
 2098: 		next if ($env{'form.lastSub'} eq 'hdgrade' 
 2099: 			 && $$handgrade{$$part[0].'_'.$$part[1]} ne 'yes');
 2100: 
 2101: 		my ($partid,$respid) = @{ $part };
 2102: 		my $display_part=&get_display_part($partid,$symb);
 2103: 		if ($env{"form.$uname:$udom:$partid:submitted_by"}) {
 2104: 		    if (exists($seenparts{$partid})) { next; }
 2105: 		    $seenparts{$partid}=1;
 2106: 		    my $submitby='<b>Part:</b> '.$display_part.
 2107: 			' <b>Collaborative submission by:</b> '.
 2108: 			'<a href="javascript:viewSubmitter(\''.
 2109: 			$env{"form.$uname:$udom:$partid:submitted_by"}.
 2110: 			'\');" target="_self">'.
 2111: 			$$fullname{$env{"form.$uname:$udom:$partid:submitted_by"}}.'</a><br />';
 2112: 		    $request->print($submitby);
 2113: 		    next;
 2114: 		}
 2115: 		my $responsetype = $responseType->{$partid}->{$respid};
 2116: 		if (!exists($record{"resource.$partid.$respid.submission"})) {
 2117:                     $lastsubonly.="\n".'<div class="LC_grade_submission_part">'.
 2118:                         '<b>'.&mt('Part: [_1]',$display_part).'</b>'.
 2119:                         ' <span class="LC_internal_info">'.
 2120:                         '('.&mt('Part ID: [_1]',$respid).')</b>'.
 2121:                         '</span>&nbsp; &nbsp;'.
 2122: 			'<span class="LC_warning">'.&mt('Nothing submitted - no attempts.').'</span><br /><br /></div>';
 2123: 		    next;
 2124: 		}
 2125: 		foreach my $submission (@$string) {
 2126: 		    my ($partid,$respid) = ($submission =~ /^resource\.([^\.]*)\.([^\.]*)\.submission/);
 2127: 		    if (join('_',@{$part}) ne ($partid.'_'.$respid)) { next; }
 2128: 		    my ($ressub,$subval) = split(/:/,$submission,2);
 2129: 		    # Similarity check
 2130: 		    my $similar='';
 2131: 		    if($env{'form.checkPlag'}){
 2132: 			my ($oname,$odom,$ocrsid,$oessay,$osim)=
 2133: 			    &most_similar($uname,$udom,$subval,\%old_essays);
 2134: 			if ($osim) {
 2135: 			    $osim=int($osim*100.0);
 2136: 			    my %old_course_desc = 
 2137: 				&Apache::lonnet::coursedescription($ocrsid,
 2138: 								   {'one_time' => 1});
 2139: 
 2140: 			    $similar="<hr /><h3><span class=\"LC_warning\">".
 2141: 				&mt('Essay is [_1]% similar to an essay by [_2] in course [_3] (course id [_4]:[_5])',
 2142: 				    $osim,
 2143: 				    &Apache::loncommon::plainname($oname,$odom).' ('.$oname.':'.$odom.')',
 2144: 				    $old_course_desc{'description'},
 2145: 				    $old_course_desc{'num'},
 2146: 				    $old_course_desc{'domain'}).
 2147: 				'</span></h3><blockquote><i>'.
 2148: 				&keywords_highlight($oessay).
 2149: 				'</i></blockquote><hr />';
 2150: 			}
 2151: 		    }
 2152: 		    my $order=&get_order($partid,$respid,$symb,$uname,$udom);
 2153: 		    if ($env{'form.lastSub'} eq 'lastonly' || 
 2154: 			($env{'form.lastSub'} eq 'hdgrade' && 
 2155: 			 $$handgrade{$$part[0].'_'.$$part[1]} eq 'yes')) {
 2156: 			my $display_part=&get_display_part($partid,$symb);
 2157:                         $lastsubonly.='<div class="LC_grade_submission_part">'.
 2158:                             '<b>'.&mt('Part: [_1]',$display_part).'</b>'.
 2159:                             ' <span class="LC_internal_info">'.
 2160:                             '('.&mt('Part ID: [_1]',$respid).')'.
 2161:                             '</b></span>&nbsp; &nbsp;';
 2162: 			my $files=&get_submitted_files($udom,$uname,$partid,$respid,\%record);
 2163: 			if (@$files) {
 2164: 			    $lastsubonly.='<br /><span class="LC_warning">'.&mt('Like all files provided by users, this file may contain viruses').'</span><br />';
 2165: 			    my $file_counter = 0;
 2166: 			    foreach my $file (@$files) {
 2167: 			        $file_counter++;
 2168: 				&Apache::lonnet::allowuploaded('/adm/grades',$file);
 2169: 				$lastsubonly.='<br /><a href="'.$file.'?rawmode=1" target="lonGRDs"><img src="'.&Apache::loncommon::icon($file).'" border="0" /> '.$file.'</a>';
 2170: 			    }
 2171: 			    $lastsubonly.='<br />';
 2172: 			}
 2173: 			$lastsubonly.='<b>'.&mt('Submitted Answer:').' </b>'.
 2174: 			    &cleanRecord($subval,$responsetype,$symb,$partid,
 2175: 					 $respid,\%record,$order,undef,$uname,$udom);
 2176: 			if ($similar) {$lastsubonly.="<br /><br />$similar\n";}
 2177: 			$lastsubonly.='</div>';
 2178: 		    }
 2179: 		}
 2180: 	    }
 2181: 	    $lastsubonly.='</div>'."\n";
 2182: 	}
 2183: 	$request->print($lastsubonly);
 2184:    } elsif ($env{'form.lastSub'} eq 'datesub') {
 2185: 	my (undef,$responseType,undef,$parts) = &showResourceInfo($symb);
 2186: 	$request->print(&displaySubByDates($symb,\%record,$parts,$responseType,$checkIcon,$uname,$udom));
 2187:     } elsif ($env{'form.lastSub'} =~ /^(last|all)$/) {
 2188: 	$request->print(&Apache::loncommon::get_previous_attempt($symb,$uname,$udom,
 2189: 								 $env{'request.course.id'},
 2190: 								 $last,'.submission',
 2191: 								 'Apache::grades::keywords_highlight'));
 2192:     }
 2193: 
 2194:     $request->print('<input type="hidden" name="unamedom'.$counter.'" value="'.$uname.':'
 2195: 	.$udom.'" />'."\n");
 2196:     # return if view submission with no grading option
 2197:     if ($env{'form.showgrading'} eq '' || (!&canmodify($usec))) {
 2198: 	my $toGrade.='<input type="button" value="Grade Student" '.
 2199: 	    'onClick="javascript:checksubmit(this.form,\'Grade Student\',\''
 2200: 	    .$counter.'\');" target="_self" /> &nbsp;'."\n" if (&canmodify($usec));
 2201: 	$toGrade.='</div>'."\n";
 2202: 	if (($env{'form.command'} eq 'submission') || 
 2203: 	    ($env{'form.command'} eq 'processGroup' && $counter == $total)) {
 2204: 	    $toGrade.='</form>'.&show_grading_menu_form($symb); 
 2205: 	}
 2206: 	$request->print($toGrade);
 2207: 	return;
 2208:     } else {
 2209: 	$request->print('</div>'."\n");
 2210:     }
 2211: 
 2212:     # essay grading message center
 2213:     if ($env{'form.handgrade'} eq 'yes') {
 2214: 	my $result='<div class="LC_grade_message_center">';
 2215:     
 2216: 	$result.='<div class="LC_grade_message_center_header">'.
 2217: 	    &mt('Send Message').'</div><div class="LC_grade_message_center_body">';
 2218: 	my ($lastname,$givenn) = split(/,/,$env{'form.fullname'});
 2219: 	my $msgfor = $givenn.' '.$lastname;
 2220: 	if (scalar(@$col_fullnames) > 0) {
 2221: 	    my $lastone = pop(@$col_fullnames);
 2222: 	    $msgfor .= ', '.(join ', ',@$col_fullnames).' and '.$lastone.'.';
 2223: 	}
 2224: 	$msgfor =~ s/\'/\\'/g; #' stupid emacs - no! javascript
 2225: 	$result.='<input type="hidden" name="includemsg'.$counter.'" value="" />'."\n".
 2226: 	    '<input type="hidden" name="newmsg'.$counter.'" value="" />'."\n";
 2227: 	$result.='&nbsp;<a href="javascript:msgCenter(document.SCORE,'.$counter.
 2228: 	    ',\''.$msgfor.'\');" target="_self">'.
 2229: 	    &mt('Compose message to student').(scalar(@$col_fullnames) >= 1 ? 's' : '').'</a><label> ('.
 2230: 	    &mt('incl. grades').' <input type="checkbox" name="withgrades'.$counter.'" /></label>)'.
 2231: 	    '<img src="'.$request->dir_config('lonIconsURL').
 2232: 	    '/mailbkgrd.gif" width="14" height="10" name="mailicon'.$counter.'" />'."\n".
 2233: 	    '<br />&nbsp;('.
 2234: 	    &mt('Message will be sent when you click on Save &amp; Next below.').")\n";
 2235: 	$result.='</div></div>';
 2236: 	$request->print($result);
 2237:     }
 2238: 
 2239:     my %seen = ();
 2240:     my @partlist;
 2241:     my @gradePartRespid;
 2242:     my @part_response_id = &flatten_responseType($responseType);
 2243:     $request->print('<div class="LC_grade_assign">'.
 2244: 		    
 2245: 		    '<div class="LC_grade_assign_header">'.
 2246: 		    &mt('Assign Grades').'</div>'.
 2247: 		    '<div class="LC_grade_assign_body">');
 2248:     foreach my $part_response_id (@part_response_id) {
 2249:     	my ($partid,$respid) = @{ $part_response_id };
 2250: 	my $part_resp = join('_',@{ $part_response_id });
 2251: 	next if ($seen{$partid} > 0);
 2252: 	$seen{$partid}++;
 2253: 	next if ($$handgrade{$part_resp} ne 'yes' 
 2254: 		 && $env{'form.lastSub'} eq 'hdgrade');
 2255: 	push(@partlist,$partid);
 2256: 	push(@gradePartRespid,$partid.'.'.$respid);
 2257: 	$request->print(&gradeBox($request,$symb,$uname,$udom,$counter,$partid,\%record));
 2258:     }
 2259:     $request->print('</div></div>');
 2260: 
 2261:     $request->print('<div class="LC_grade_info_links">');
 2262:     if ($perm{'vgr'}) {
 2263: 	$request->print(
 2264: 	    &Apache::loncommon::track_student_link(&mt('View recent activity'),
 2265: 						   $uname,$udom,'check'));
 2266:     }
 2267:     if ($perm{'opa'}) {
 2268: 	$request->print(
 2269: 	    &Apache::loncommon::pprmlink(&mt('Set/Change parameters'),
 2270: 					 $uname,$udom,$symb,'check'));
 2271:     }
 2272:     $request->print('</div>');
 2273: 
 2274:     $result='<input type="hidden" name="partlist'.$counter.
 2275: 	'" value="'.(join ":",@partlist).'" />'."\n";
 2276:     $result.='<input type="hidden" name="gradePartRespid'.
 2277: 	'" value="'.(join ":",@gradePartRespid).'" />'."\n" if ($counter == 0);
 2278:     my $ctr = 0;
 2279:     while ($ctr < scalar(@partlist)) {
 2280: 	$result.='<input type="hidden" name="partid'.$counter.'_'.$ctr.'" value="'.
 2281: 	    $partlist[$ctr].'" />'."\n";
 2282: 	$ctr++;
 2283:     }
 2284:     $request->print($result.''."\n");
 2285: 
 2286: # Done with printing info for one student
 2287: 
 2288:     $request->print('</div>');#LC_grade_show_user_body
 2289:     $request->print('</div>');#LC_grade_show_user
 2290: 
 2291: 
 2292:     # print end of form
 2293:     if ($counter == $total) {
 2294: 	my $endform='<table border="0"><tr><td>'."\n";
 2295: 	$endform.='<input type="button" value="'.&mt('Save &amp; Next').'" '.
 2296: 	    'onClick="javascript:checksubmit(this.form,\'Save & Next\','.
 2297: 	    $total.','.scalar(@partlist).');" target="_self" /> &nbsp;'."\n";
 2298: 	my $ntstu ='<select name="NTSTU">'.
 2299: 	    '<option>1</option><option>2</option>'.
 2300: 	    '<option>3</option><option>5</option>'.
 2301: 	    '<option>7</option><option>10</option></select>'."\n";
 2302: 	my $nsel = ($env{'form.NTSTU'} ne '' ? $env{'form.NTSTU'} : '1');
 2303: 	$ntstu =~ s/<option>$nsel</<option selected="selected">$nsel</;
 2304: 	$endform.=&mt('[quant,_1,student]',$ntstu);
 2305: 	$endform.='&nbsp;&nbsp;<input type="button" value="'.&mt('Previous').'" '.
 2306: 	    'onClick="javascript:checksubmit(this.form,\'Previous\');" target="_self" /> &nbsp;'."\n".
 2307: 	    '<input type="button" value="'.&mt('Next').'" '.
 2308: 	    'onClick="javascript:checksubmit(this.form,\'Next\');" target="_self" /> &nbsp;';
 2309: 	$endform.=&mt('(Next and Previous (student) do not save the scores.)')."\n" ;
 2310:         $endform.="<input type='hidden' value='".&get_increment().
 2311:             "' name='increment' />";
 2312: 	$endform.='</td></tr></table></form>';
 2313: 	$endform.=&show_grading_menu_form($symb);
 2314: 	$request->print($endform);
 2315:     }
 2316:     return '';
 2317: }
 2318: 
 2319: sub check_collaborators {
 2320:     my ($symb,$uname,$udom,$record,$handgrade,$counter) = @_;
 2321:     my ($result,@col_fullnames);
 2322:     my ($classlist,undef,$fullname) = &getclasslist('all','0');
 2323:     foreach my $part (keys(%$handgrade)) {
 2324: 	my $ncol = &Apache::lonnet::EXT('resource.'.$part.
 2325: 					'.maxcollaborators',
 2326: 					$symb,$udom,$uname);
 2327: 	next if ($ncol <= 0);
 2328: 	$part =~ s/\_/\./g;
 2329: 	next if ($record->{'resource.'.$part.'.collaborators'} eq '');
 2330: 	my (@good_collaborators, @bad_collaborators);
 2331: 	foreach my $possible_collaborator
 2332: 	    (split(/,?\s+/,$record->{'resource.'.$part.'.collaborators'})) { 
 2333: 	    $possible_collaborator =~ s/[\$\^\(\)]//g;
 2334: 	    next if ($possible_collaborator eq '');
 2335: 	    my ($co_name,$co_dom) = split(/\@|:/,$possible_collaborator);
 2336: 	    $co_dom = $udom if (! defined($co_dom) || $co_dom =~ /^domain$/i);
 2337: 	    next if ($co_name eq $uname && $co_dom eq $udom);
 2338: 	    # Doing this grep allows 'fuzzy' specification
 2339: 	    my @matches = grep(/^\Q$co_name\E:\Q$co_dom\E$/i, 
 2340: 			       keys(%$classlist));
 2341: 	    if (! scalar(@matches)) {
 2342: 		push(@bad_collaborators, $possible_collaborator);
 2343: 	    } else {
 2344: 		push(@good_collaborators, @matches);
 2345: 	    }
 2346: 	}
 2347: 	if (scalar(@good_collaborators) != 0) {
 2348: 	    $result.='<br />'.&mt('Collaborators: ');
 2349: 	    foreach my $name (@good_collaborators) {
 2350: 		my ($lastname,$givenn) = split(/,/,$$fullname{$name});
 2351: 		push(@col_fullnames, $givenn.' '.$lastname);
 2352: 		$result.=$fullname->{$name}.'&nbsp; &nbsp; &nbsp;';
 2353: 	    }
 2354: 	    $result.='<br />'."\n";
 2355: 	    my ($part)=split(/\./,$part);
 2356: 	    $result.='<input type="hidden" name="collaborator'.$counter.
 2357: 		'" value="'.$part.':'.(join ':',@good_collaborators).'" />'.
 2358: 		"\n";
 2359: 	}
 2360: 	if (scalar(@bad_collaborators) > 0) {
 2361: 	    $result.='<div class="LC_warning">';
 2362: 	    $result.=&mt('This student has submitted [quant,_1,invalid collaborator]: [_2]',scalar(@bad_collaborators),join(', ',@bad_collaborators));
 2363: 	    $result .= '</div>';
 2364: 	}         
 2365: 	if (scalar(@bad_collaborators > $ncol)) {
 2366: 	    $result .= '<div class="LC_warning">';
 2367: 	    $result .= &mt('This student has submitted too many '.
 2368: 		'collaborators.  Maximum is [_1].',$ncol);
 2369: 	    $result .= '</div>';
 2370: 	}
 2371:     }
 2372:     return ($result,$fullname,\@col_fullnames);
 2373: }
 2374: 
 2375: #--- Retrieve the last submission for all the parts
 2376: sub get_last_submission {
 2377:     my ($returnhash)=@_;
 2378:     my (@string,$timestamp);
 2379:     if ($$returnhash{'version'}) {
 2380: 	my %lasthash=();
 2381: 	my ($version);
 2382: 	for ($version=1;$version<=$$returnhash{'version'};$version++) {
 2383: 	    foreach my $key (sort(split(/\:/,
 2384: 					$$returnhash{$version.':keys'}))) {
 2385: 		$lasthash{$key}=$$returnhash{$version.':'.$key};
 2386: 		$timestamp = 
 2387: 		    &Apache::lonlocal::locallocaltime($$returnhash{$version.':timestamp'});
 2388: 	    }
 2389: 	}
 2390: 	foreach my $key (keys(%lasthash)) {
 2391: 	    next if ($key !~ /\.submission$/);
 2392: 
 2393: 	    my ($partid,$foo) = split(/submission$/,$key);
 2394: 	    my $draft  = $lasthash{$partid.'awarddetail'} eq 'DRAFT' ?
 2395: 		'<span class="LC_warning">Draft Copy</span> ' : '';
 2396: 	    push(@string, join(':', $key, $draft.$lasthash{$key}));
 2397: 	}
 2398:     }
 2399:     if (!@string) {
 2400: 	$string[0] =
 2401: 	    '<span class="LC_warning">'.&mt('Nothing submitted - no attempts.').'</span>';
 2402:     }
 2403:     return (\@string,\$timestamp);
 2404: }
 2405: 
 2406: #--- High light keywords, with style choosen by user.
 2407: sub keywords_highlight {
 2408:     my $string    = shift;
 2409:     my $size      = $env{'form.kwsize'} eq '0' ? '' : 'size='.$env{'form.kwsize'};
 2410:     my $styleon   = $env{'form.kwstyle'} eq ''  ? '' : $env{'form.kwstyle'};
 2411:     (my $styleoff = $styleon) =~ s/\</\<\//;
 2412:     my @keylist   = split(/[,\s+]/,$env{'form.keywords'});
 2413:     foreach my $keyword (@keylist) {
 2414: 	$string =~ s/\b\Q$keyword\E(\b|\.)/<font color\=$env{'form.kwclr'} $size\>$styleon$keyword$styleoff<\/font>/gi;
 2415:     }
 2416:     return $string;
 2417: }
 2418: 
 2419: #--- Called from submission routine
 2420: sub processHandGrade {
 2421:     my ($request) = shift;
 2422:     my $symb   = &get_symb($request);
 2423:     my (undef,undef,$url) = &Apache::lonnet::decode_symb($symb);
 2424:     my $button = $env{'form.gradeOpt'};
 2425:     my $ngrade = $env{'form.NCT'};
 2426:     my $ntstu  = $env{'form.NTSTU'};
 2427:     my $cdom   = $env{'course.'.$env{'request.course.id'}.'.domain'};
 2428:     my $cnum   = $env{'course.'.$env{'request.course.id'}.'.num'};
 2429: 
 2430:     if ($button eq 'Save & Next') {
 2431: 	my $ctr = 0;
 2432: 	while ($ctr < $ngrade) {
 2433: 	    my ($uname,$udom) = split(/:/,$env{'form.unamedom'.$ctr});
 2434: 	    my ($errorflag,$pts,$wgt) = &saveHandGrade($request,$symb,$uname,$udom,$ctr);
 2435: 	    if ($errorflag eq 'no_score') {
 2436: 		$ctr++;
 2437: 		next;
 2438: 	    }
 2439: 	    if ($errorflag eq 'not_allowed') {
 2440: 		$request->print("<span class=\"LC_warning\">Not allowed to modify grades for $uname:$udom</span>");
 2441: 		$ctr++;
 2442: 		next;
 2443: 	    }
 2444: 	    my $includemsg = $env{'form.includemsg'.$ctr};
 2445: 	    my ($subject,$message,$msgstatus) = ('','','');
 2446: 	    my $restitle = &Apache::lonnet::gettitle($symb);
 2447:             my ($feedurl,$showsymb) =
 2448: 		&get_feedurl_and_symb($symb,$uname,$udom);
 2449: 	    my $messagetail;
 2450: 	    if ($includemsg =~ /savemsg|newmsg\Q$ctr\E/) {
 2451: 		$subject = $env{'form.msgsub'} if ($includemsg =~ /msgsub/);
 2452: 		unless ($subject=~/\w/) { $subject=&mt('Grading Feedback'); }
 2453: 		$subject.=' ['.$restitle.']';
 2454: 		my (@msgnum) = split(/,/,$includemsg);
 2455: 		foreach (@msgnum) {
 2456: 		    $message.=$env{'form.'.$_} if ($_ =~ /savemsg|newmsg/ && $_ ne '');
 2457: 		}
 2458: 		$message =&Apache::lonfeedback::clear_out_html($message);
 2459: 		if ($env{'form.withgrades'.$ctr}) {
 2460: 		    $message.="\n\nPoint".($pts > 1 ? 's':'').' awarded = '.$pts.' out of '.$wgt;
 2461: 		    $messagetail = " for <a href=\"".
 2462: 		                   $feedurl."?symb=$showsymb\">$env{'form.probTitle'}</a>";
 2463: 		}
 2464: 		$msgstatus = 
 2465:                     &Apache::lonmsg::user_normal_msg($uname,$udom,$subject,
 2466: 						     $message.$messagetail,
 2467:                                                      undef,$feedurl,undef,
 2468:                                                      undef,undef,$showsymb,
 2469:                                                      $restitle);
 2470: 		$request->print('<br />'.&mt('Sending message to [_1]',$uname.':'.$udom).': '.
 2471: 				$msgstatus);
 2472: 	    }
 2473: 	    if ($env{'form.collaborator'.$ctr}) {
 2474: 		my @collabstrs=&Apache::loncommon::get_env_multiple("form.collaborator$ctr");
 2475: 		foreach my $collabstr (@collabstrs) {
 2476: 		    my ($part,@collaborators) = split(/:/,$collabstr);
 2477: 		    foreach my $collaborator (@collaborators) {
 2478: 			my ($errorflag,$pts,$wgt) = 
 2479: 			    &saveHandGrade($request,$symb,$collaborator,$udom,$ctr,
 2480: 					   $env{'form.unamedom'.$ctr},$part);
 2481: 			if ($errorflag eq 'not_allowed') {
 2482: 			    $request->print("<span class=\"LC_error\">".&mt('Not allowed to modify grades for [_1]',"$collaborator:$udom")."</span>");
 2483: 			    next;
 2484: 			} elsif ($message ne '') {
 2485: 			    my ($baseurl,$showsymb) = 
 2486: 				&get_feedurl_and_symb($symb,$collaborator,
 2487: 						      $udom);
 2488: 			    if ($env{'form.withgrades'.$ctr}) {
 2489: 				$messagetail = " for <a href=\"".
 2490:                                     $baseurl."?symb=$showsymb\">$env{'form.probTitle'}</a>";
 2491: 			    }
 2492: 			    $msgstatus = 
 2493: 				&Apache::lonmsg::user_normal_msg($collaborator,$udom,$subject,$message.$messagetail,undef,$baseurl,undef,undef,undef,$showsymb,$restitle);
 2494: 			}
 2495: 		    }
 2496: 		}
 2497: 	    }
 2498: 	    $ctr++;
 2499: 	}
 2500:     }
 2501: 
 2502:     if ($env{'form.handgrade'} eq 'yes') {
 2503: 	# Keywords sorted in alphabatical order
 2504: 	my $loginuser = $env{'user.name'}.':'.$env{'user.domain'};
 2505: 	my %keyhash = ();
 2506: 	$env{'form.keywords'}           =~ s/,\s{0,}|\s+/ /g;
 2507: 	$env{'form.keywords'}           =~ s/^\s+|\s+$//;
 2508: 	my (@keywords) = sort(split(/\s+/,$env{'form.keywords'}));
 2509: 	$env{'form.keywords'} = join(' ',@keywords);
 2510: 	$keyhash{$symb.'_keywords'}     = $env{'form.keywords'};
 2511: 	$keyhash{$symb.'_subject'}      = $env{'form.msgsub'};
 2512: 	$keyhash{$loginuser.'_kwclr'}   = $env{'form.kwclr'};
 2513: 	$keyhash{$loginuser.'_kwsize'}  = $env{'form.kwsize'};
 2514: 	$keyhash{$loginuser.'_kwstyle'} = $env{'form.kwstyle'};
 2515: 
 2516: 	# message center - Order of message gets changed. Blank line is eliminated.
 2517: 	# New messages are saved in env for the next student.
 2518: 	# All messages are saved in nohist_handgrade.db
 2519: 	my ($ctr,$idx) = (1,1);
 2520: 	while ($ctr <= $env{'form.savemsgN'}) {
 2521: 	    if ($env{'form.savemsg'.$ctr} ne '') {
 2522: 		$keyhash{$symb.'_savemsg'.$idx} = $env{'form.savemsg'.$ctr};
 2523: 		$idx++;
 2524: 	    }
 2525: 	    $ctr++;
 2526: 	}
 2527: 	$ctr = 0;
 2528: 	while ($ctr < $ngrade) {
 2529: 	    if ($env{'form.newmsg'.$ctr} ne '') {
 2530: 		$keyhash{$symb.'_savemsg'.$idx} = $env{'form.newmsg'.$ctr};
 2531: 		$env{'form.savemsg'.$idx} = $env{'form.newmsg'.$ctr};
 2532: 		$idx++;
 2533: 	    }
 2534: 	    $ctr++;
 2535: 	}
 2536: 	$env{'form.savemsgN'} = --$idx;
 2537: 	$keyhash{$symb.'_savemsgN'} = $env{'form.savemsgN'};
 2538: 	my $putresult = &Apache::lonnet::put
 2539: 	    ('nohist_handgrade',\%keyhash,$cdom,$cnum);
 2540:     }
 2541:     # Called by Save & Refresh from Highlight Attribute Window
 2542:     my (undef,undef,$fullname) = &getclasslist($env{'form.section'},'1');
 2543:     if ($env{'form.refresh'} eq 'on') {
 2544: 	my ($ctr,$total) = (0,0);
 2545: 	while ($ctr < $ngrade) {
 2546: 	    $total++ if  $env{'form.unamedom'.$ctr} ne '';
 2547: 	    $ctr++;
 2548: 	}
 2549: 	$env{'form.NTSTU'}=$ngrade;
 2550: 	$ctr = 0;
 2551: 	while ($ctr < $total) {
 2552: 	    my $processUser = $env{'form.unamedom'.$ctr};
 2553: 	    ($env{'form.student'},$env{'form.userdom'}) = split(/:/,$processUser);
 2554: 	    $env{'form.fullname'} = $$fullname{$processUser};
 2555: 	    &submission($request,$ctr,$total-1);
 2556: 	    $ctr++;
 2557: 	}
 2558: 	return '';
 2559:     }
 2560: 
 2561: # Go directly to grade student - from submission or link from chart page
 2562:     if ($button eq 'Grade Student') {
 2563: 	(undef,undef,$env{'form.handgrade'},undef,undef) = &showResourceInfo($symb);
 2564: 	my $processUser = $env{'form.unamedom'.$env{'form.studentNo'}};
 2565: 	($env{'form.student'},$env{'form.userdom'}) = split(/:/,$processUser);
 2566: 	$env{'form.fullname'} = $$fullname{$processUser};
 2567: 	&submission($request,0,0);
 2568: 	return '';
 2569:     }
 2570: 
 2571:     # Get the next/previous one or group of students
 2572:     my $firststu = $env{'form.unamedom0'};
 2573:     my $laststu = $env{'form.unamedom'.($ngrade-1)};
 2574:     my $ctr = 2;
 2575:     while ($laststu eq '') {
 2576: 	$laststu  = $env{'form.unamedom'.($ngrade-$ctr)};
 2577: 	$ctr++;
 2578: 	$laststu = $firststu if ($ctr > $ngrade);
 2579:     }
 2580: 
 2581:     my (@parsedlist,@nextlist);
 2582:     my ($nextflg) = 0;
 2583:     foreach my $item (sort 
 2584: 	     {
 2585: 		 if (lc($$fullname{$a}) ne lc($$fullname{$b})) {
 2586: 		     return (lc($$fullname{$a}) cmp lc($$fullname{$b}));
 2587: 		 }
 2588: 		 return $a cmp $b;
 2589: 	     } (keys(%$fullname))) {
 2590: 	if ($nextflg == 1 && $button =~ /Next$/) {
 2591: 	    push(@parsedlist,$item);
 2592: 	}
 2593: 	$nextflg = 1 if ($item eq $laststu);
 2594: 	if ($button eq 'Previous') {
 2595: 	    last if ($item eq $firststu);
 2596: 	    push(@parsedlist,$item);
 2597: 	}
 2598:     }
 2599:     $ctr = 0;
 2600:     @parsedlist = reverse @parsedlist if ($button eq 'Previous');
 2601:     my ($partlist) = &response_type($symb);
 2602:     foreach my $student (@parsedlist) {
 2603: 	my $submitonly=$env{'form.submitonly'};
 2604: 	my ($uname,$udom) = split(/:/,$student);
 2605: 	
 2606: 	if ($submitonly eq 'queued') {
 2607: 	    my %queue_status = 
 2608: 		&Apache::bridgetask::get_student_status($symb,$cdom,$cnum,
 2609: 							$udom,$uname);
 2610: 	    next if (!defined($queue_status{'gradingqueue'}));
 2611: 	}
 2612: 
 2613: 	if ($submitonly =~ /^(yes|graded|incorrect)$/) {
 2614: #	    my %record = &Apache::lonnet::restore($symb,$env{'request.course.id'},$udom,$uname);
 2615: 	    my %status=&student_gradeStatus($symb,$udom,$uname,$partlist);
 2616: 	    my $submitted = 0;
 2617: 	    my $ungraded = 0;
 2618: 	    my $incorrect = 0;
 2619: 	    foreach my $item (keys(%status)) {
 2620: 		$submitted = 1 if ($status{$item} ne 'nothing');
 2621: 		$ungraded = 1 if ($status{$item} =~ /^ungraded/);
 2622: 		$incorrect = 1 if ($status{$item} =~ /^incorrect/);
 2623: 		my ($foo,$partid,$foo1) = split(/\./,$item);
 2624: 		if ($status{'resource.'.$partid.'.submitted_by'} ne '') {
 2625: 		    $submitted = 0;
 2626: 		}
 2627: 	    }
 2628: 	    next if (!$submitted && ($submitonly eq 'yes' ||
 2629: 				     $submitonly eq 'incorrect' ||
 2630: 				     $submitonly eq 'graded'));
 2631: 	    next if (!$ungraded && ($submitonly eq 'graded'));
 2632: 	    next if (!$incorrect && $submitonly eq 'incorrect');
 2633: 	}
 2634: 	push(@nextlist,$student) if ($ctr < $ntstu);
 2635: 	last if ($ctr == $ntstu);
 2636: 	$ctr++;
 2637:     }
 2638: 
 2639:     $ctr = 0;
 2640:     my $total = scalar(@nextlist)-1;
 2641: 
 2642:     foreach (sort(@nextlist)) {
 2643: 	my ($uname,$udom,$submitter) = split(/:/);
 2644: 	$env{'form.student'}  = $uname;
 2645: 	$env{'form.userdom'}  = $udom;
 2646: 	$env{'form.fullname'} = $$fullname{$_};
 2647: 	&submission($request,$ctr,$total);
 2648: 	$ctr++;
 2649:     }
 2650:     if ($total < 0) {
 2651: 	my $the_end = '<h3><span class="LC_info">'.&mt('LON-CAPA User Message').'</span></h3><br />'."\n";
 2652: 	$the_end.=&mt('<b>Message: </b> No more students for this section or class.').'<br /><br />'."\n";
 2653: 	$the_end.=&mt('Click on the button below to return to the grading menu.').'<br /><br />'."\n";
 2654: 	$the_end.=&show_grading_menu_form($symb);
 2655: 	$request->print($the_end);
 2656:     }
 2657:     return '';
 2658: }
 2659: 
 2660: #---- Save the score and award for each student, if changed
 2661: sub saveHandGrade {
 2662:     my ($request,$symb,$stuname,$domain,$newflg,$submitter,$part) = @_;
 2663:     my @version_parts;
 2664:     my $usec = &Apache::lonnet::getsection($domain,$stuname,
 2665: 					   $env{'request.course.id'});
 2666:     if (!&canmodify($usec)) { return('not_allowed'); }
 2667:     my %record = &Apache::lonnet::restore($symb,$env{'request.course.id'},$domain,$stuname);
 2668:     my @parts_graded;
 2669:     my %newrecord  = ();
 2670:     my ($pts,$wgt) = ('','');
 2671:     my %aggregate = ();
 2672:     my $aggregateflag = 0;
 2673:     my @parts = split(/:/,$env{'form.partlist'.$newflg});
 2674:     foreach my $new_part (@parts) {
 2675: 	#collaborator ($submi may vary for different parts
 2676: 	if ($submitter && $new_part ne $part) { next; }
 2677: 	my $dropMenu = $env{'form.GD_SEL'.$newflg.'_'.$new_part};
 2678: 	if ($dropMenu eq 'excused') {
 2679: 	    if ($record{'resource.'.$new_part.'.solved'} ne 'excused') {
 2680: 		$newrecord{'resource.'.$new_part.'.solved'} = 'excused';
 2681: 		if (exists($record{'resource.'.$new_part.'.awarded'})) {
 2682: 		    $newrecord{'resource.'.$new_part.'.awarded'} = '';
 2683: 		}
 2684: 	        $newrecord{'resource.'.$new_part.'.regrader'}="$env{'user.name'}:$env{'user.domain'}";
 2685: 	    }
 2686: 	} elsif ($dropMenu eq 'reset status'
 2687: 		 && exists($record{'resource.'.$new_part.'.solved'})) { #don't bother if no old records -> no attempts
 2688: 	    foreach my $key (keys(%record)) {
 2689: 		if ($key=~/^resource\.\Q$new_part\E\./) { $newrecord{$key} = ''; }
 2690: 	    }
 2691: 	    $newrecord{'resource.'.$new_part.'.regrader'}=
 2692: 		"$env{'user.name'}:$env{'user.domain'}";
 2693:             my $totaltries = $record{'resource.'.$part.'.tries'};
 2694: 
 2695:             my %last_resets = &get_last_resets($symb,$env{'request.course.id'},
 2696: 					       [$new_part]);
 2697:             my $aggtries =$totaltries;
 2698:             if ($last_resets{$new_part}) {
 2699:                 $aggtries = &get_num_tries(\%record,$last_resets{$new_part},
 2700: 					   $new_part);
 2701:             }
 2702: 
 2703:             my $solvedstatus = $record{'resource.'.$new_part.'.solved'};
 2704:             if ($aggtries > 0) {
 2705:                 &decrement_aggs($symb,$new_part,\%aggregate,$aggtries,$totaltries,$solvedstatus);
 2706:                 $aggregateflag = 1;
 2707:             }
 2708: 	} elsif ($dropMenu eq '') {
 2709: 	    $pts = ($env{'form.GD_BOX'.$newflg.'_'.$new_part} ne '' ? 
 2710: 		    $env{'form.GD_BOX'.$newflg.'_'.$new_part} : 
 2711: 		    $env{'form.RADVAL'.$newflg.'_'.$new_part});
 2712: 	    if ($pts eq '' && $env{'form.GD_SEL'.$newflg.'_'.$new_part} eq '') {
 2713: 		next;
 2714: 	    }
 2715: 	    $wgt = $env{'form.WGT'.$newflg.'_'.$new_part} eq '' ? 1 : 
 2716: 		$env{'form.WGT'.$newflg.'_'.$new_part};
 2717: 	    my $partial= $pts/$wgt;
 2718: 	    if ($partial eq $record{'resource.'.$new_part.'.awarded'}) {
 2719: 		#do not update score for part if not changed.
 2720:                 &handback_files($request,$symb,$stuname,$domain,$newflg,$new_part,\%newrecord);
 2721: 		next;
 2722: 	    } else {
 2723: 	        push(@parts_graded,$new_part);
 2724: 	    }
 2725: 	    if ($record{'resource.'.$new_part.'.awarded'} ne $partial) {
 2726: 		$newrecord{'resource.'.$new_part.'.awarded'}  = $partial;
 2727: 	    }
 2728: 	    my $reckey = 'resource.'.$new_part.'.solved';
 2729: 	    if ($partial == 0) {
 2730: 		if ($record{$reckey} ne 'incorrect_by_override') {
 2731: 		    $newrecord{$reckey} = 'incorrect_by_override';
 2732: 		}
 2733: 	    } else {
 2734: 		if ($record{$reckey} ne 'correct_by_override') {
 2735: 		    $newrecord{$reckey} = 'correct_by_override';
 2736: 		}
 2737: 	    }	    
 2738: 	    if ($submitter && 
 2739: 		($record{'resource.'.$new_part.'.submitted_by'} ne $submitter)) {
 2740: 		$newrecord{'resource.'.$new_part.'.submitted_by'} = $submitter;
 2741: 	    }
 2742: 	    $newrecord{'resource.'.$new_part.'.regrader'}=
 2743: 		"$env{'user.name'}:$env{'user.domain'}";
 2744: 	}
 2745: 	# unless problem has been graded, set flag to version the submitted files
 2746: 	unless ($record{'resource.'.$new_part.'.solved'} =~ /^correct_/  || 
 2747: 	        $record{'resource.'.$new_part.'.solved'} eq 'incorrect_by_override' ||
 2748: 	        $dropMenu eq 'reset status')
 2749: 	   {
 2750: 	    push(@version_parts,$new_part);
 2751: 	}
 2752:     }
 2753:     my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'};
 2754:     my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'};
 2755: 
 2756:     if (%newrecord) {
 2757:         if (@version_parts) {
 2758:             my @changed_keys = &version_portfiles(\%record, \@parts_graded, 
 2759:                                 $env{'request.course.id'}, $symb, $domain, $stuname, \@version_parts);
 2760: 	    @newrecord{@changed_keys} = @record{@changed_keys};
 2761: 	    foreach my $new_part (@version_parts) {
 2762: 		&handback_files($request,$symb,$stuname,$domain,$newflg,
 2763: 				$new_part,\%newrecord);
 2764: 	    }
 2765:         }
 2766: 	&Apache::lonnet::cstore(\%newrecord,$symb,
 2767: 				$env{'request.course.id'},$domain,$stuname);
 2768: 	&check_and_remove_from_queue(\@parts,\%record,\%newrecord,$symb,
 2769: 				     $cdom,$cnum,$domain,$stuname);
 2770:     }
 2771:     if ($aggregateflag) {
 2772:         &Apache::lonnet::cinc('nohist_resourcetracker',\%aggregate,
 2773: 			      $cdom,$cnum);
 2774:     }
 2775:     return ('',$pts,$wgt);
 2776: }
 2777: 
 2778: sub check_and_remove_from_queue {
 2779:     my ($parts,$record,$newrecord,$symb,$cdom,$cnum,$domain,$stuname) = @_;
 2780:     my @ungraded_parts;
 2781:     foreach my $part (@{$parts}) {
 2782: 	if (    $record->{   'resource.'.$part.'.awarded'} eq ''
 2783: 	     && $record->{   'resource.'.$part.'.solved' } ne 'excused'
 2784: 	     && $newrecord->{'resource.'.$part.'.awarded'} eq ''
 2785: 	     && $newrecord->{'resource.'.$part.'.solved' } ne 'excused'
 2786: 		) {
 2787: 	    push(@ungraded_parts, $part);
 2788: 	}
 2789:     }
 2790:     if ( !@ungraded_parts ) {
 2791: 	&Apache::bridgetask::remove_from_queue('gradingqueue',$symb,$cdom,
 2792: 					       $cnum,$domain,$stuname);
 2793:     }
 2794: }
 2795: 
 2796: sub handback_files {
 2797:     my ($request,$symb,$stuname,$domain,$newflg,$new_part,$newrecord) = @_;
 2798:     my $portfolio_root = '/userfiles/portfolio';
 2799:     my ($partlist,$handgrade,$responseType) = &response_type($symb);
 2800: 
 2801:     my @part_response_id = &flatten_responseType($responseType);
 2802:     foreach my $part_response_id (@part_response_id) {
 2803:     	my ($part_id,$resp_id) = @{ $part_response_id };
 2804: 	my $part_resp = join('_',@{ $part_response_id });
 2805:             if (($env{'form.'.$newflg.'_'.$part_resp.'_returndoc1'}) && ($new_part == $part_id)) {
 2806:                 # if multiple files are uploaded names will be 'returndoc2','returndoc3'
 2807:                 my $file_counter = 1;
 2808: 		my $file_msg;
 2809:                 while ($env{'form.'.$newflg.'_'.$part_resp.'_returndoc'.$file_counter}) {
 2810:                     my $fname=$env{'form.'.$newflg.'_'.$part_resp.'_returndoc'.$file_counter.'.filename'};
 2811:                     my ($directory,$answer_file) = 
 2812:                         ($env{'form.'.$newflg.'_'.$part_resp.'_origdoc'.$file_counter} =~ /^(.*?)([^\/]*)$/);
 2813:                     my ($answer_name,$answer_ver,$answer_ext) =
 2814: 		        &file_name_version_ext($answer_file);
 2815: 		    my ($portfolio_path) = ($directory =~ /^.+$stuname\/portfolio(.*)/);
 2816:                     my $getpropath = 1;
 2817: 		    my @dir_list = &Apache::lonnet::dirlist($portfolio_root.$portfolio_path,$domain,$stuname,$getpropath);
 2818: 		    my $version = &get_next_version($answer_name, $answer_ext, \@dir_list);
 2819:                     # fix file name
 2820:                     my ($save_file_name) = (($directory.$answer_name.".$version.".$answer_ext) =~ /^.+\/${stuname}\/(.*)/);
 2821:                     my $result=&Apache::lonnet::finishuserfileupload($stuname,$domain,
 2822:             	                                $newflg.'_'.$part_resp.'_returndoc'.$file_counter,
 2823:             	                                $save_file_name);
 2824:                     if ($result !~ m|^/uploaded/|) {
 2825:                         $request->print('<br /><span class="LC_error">'.
 2826:                             &mt('An error occurred ([_1]) while trying to upload [_2].',
 2827:                                 $result,$newflg.'_'.$part_resp.'_returndoc'.$file_counter).
 2828:                                         '</span>');
 2829:                     } else {
 2830:                         # mark the file as read only
 2831:                         my @files = ($save_file_name);
 2832:                         my @what = ($symb,$env{'request.course.id'},'handback');
 2833:                         &Apache::lonnet::mark_as_readonly($domain,$stuname,\@files,\@what);
 2834: 			if (exists($$newrecord{"resource.$new_part.$resp_id.handback"})) {
 2835: 			    $$newrecord{"resource.$new_part.$resp_id.handback"}.=',';
 2836: 			}
 2837:                         $$newrecord{"resource.$new_part.$resp_id.handback"} .= $save_file_name;
 2838: 			$file_msg.= "\n".'<br /><span class="LC_filename"><a href="/uploaded/'."$domain/$stuname/".$save_file_name.'">'.$save_file_name."</a></span><br />";
 2839: 
 2840:                     }
 2841:                     $request->print("<br />".$fname." will be the uploaded file name");
 2842:                     $request->print(" ".$env{'form.'.$newflg.'_'.$part_resp.'_origdoc'.$file_counter});
 2843:                     $file_counter++;
 2844:                 }
 2845: 		my $subject = "File Handed Back by Instructor ";
 2846: 		my $message = "A file has been returned that was originally submitted in reponse to: <br />";
 2847: 		$message .= "<strong>".&Apache::lonnet::gettitle($symb)."</strong><br />";
 2848: 		$message .= ' The returned file(s) are named: '. $file_msg;
 2849: 		$message .= " and can be found in your portfolio space.";
 2850: 		my ($feedurl,$showsymb) = 
 2851: 		    &get_feedurl_and_symb($symb,$domain,$stuname);
 2852:                 my $restitle = &Apache::lonnet::gettitle($symb);
 2853: 		my $msgstatus = 
 2854:                    &Apache::lonmsg::user_normal_msg($stuname,$domain,$subject.
 2855: 			 ' (File Returned) ['.$restitle.']',$message,undef,
 2856:                          $feedurl,undef,undef,undef,$showsymb,$restitle);
 2857:             }
 2858:         }
 2859:     return;
 2860: }
 2861: 
 2862: sub get_feedurl_and_symb {
 2863:     my ($symb,$uname,$udom) = @_;
 2864:     my (undef,undef,$url) = &Apache::lonnet::decode_symb($symb);
 2865:     $url = &Apache::lonnet::clutter($url);
 2866:     my $encrypturl=&Apache::lonnet::EXT('resource.0.encrypturl',
 2867: 					$symb,$udom,$uname);
 2868:     if ($encrypturl =~ /^yes$/i) {
 2869: 	&Apache::lonenc::encrypted(\$url,1);
 2870: 	&Apache::lonenc::encrypted(\$symb,1);
 2871:     }
 2872:     return ($url,$symb);
 2873: }
 2874: 
 2875: sub get_submitted_files {
 2876:     my ($udom,$uname,$partid,$respid,$record) = @_;
 2877:     my @files;
 2878:     if ($$record{"resource.$partid.$respid.portfiles"}) {
 2879:         my $file_url = '/uploaded/'.$udom.'/'.$uname.'/portfolio';
 2880:         foreach my $file (split(',',$$record{"resource.$partid.$respid.portfiles"})) {
 2881:     	    push(@files,$file_url.$file);
 2882:         }
 2883:     }
 2884:     if ($$record{"resource.$partid.$respid.uploadedurl"}) {
 2885:         push(@files,$$record{"resource.$partid.$respid.uploadedurl"});
 2886:     }
 2887:     return (\@files);
 2888: }
 2889: 
 2890: # ----------- Provides number of tries since last reset.
 2891: sub get_num_tries {
 2892:     my ($record,$last_reset,$part) = @_;
 2893:     my $timestamp = '';
 2894:     my $num_tries = 0;
 2895:     if ($$record{'version'}) {
 2896:         for (my $version=$$record{'version'};$version>=1;$version--) {
 2897:             if (exists($$record{$version.':resource.'.$part.'.solved'})) {
 2898:                 $timestamp = $$record{$version.':timestamp'};
 2899:                 if ($timestamp > $last_reset) {
 2900:                     $num_tries ++;
 2901:                 } else {
 2902:                     last;
 2903:                 }
 2904:             }
 2905:         }
 2906:     }
 2907:     return $num_tries;
 2908: }
 2909: 
 2910: # ----------- Determine decrements required in aggregate totals 
 2911: sub decrement_aggs {
 2912:     my ($symb,$part,$aggregate,$aggtries,$totaltries,$solvedstatus) = @_;
 2913:     my %decrement = (
 2914:                         attempts => 0,
 2915:                         users => 0,
 2916:                         correct => 0
 2917:                     );
 2918:     $decrement{'attempts'} = $aggtries;
 2919:     if ($solvedstatus =~ /^correct/) {
 2920:         $decrement{'correct'} = 1;
 2921:     }
 2922:     if ($aggtries == $totaltries) {
 2923:         $decrement{'users'} = 1;
 2924:     }
 2925:     foreach my $type (keys(%decrement)) {
 2926:         $$aggregate{$symb."\0".$part."\0".$type} = -$decrement{$type};
 2927:     }
 2928:     return;
 2929: }
 2930: 
 2931: # ----------- Determine timestamps for last reset of aggregate totals for parts  
 2932: sub get_last_resets {
 2933:     my ($symb,$courseid,$partids) =@_;
 2934:     my %last_resets;
 2935:     my $cdom = $env{'course.'.$courseid.'.domain'};
 2936:     my $cname = $env{'course.'.$courseid.'.num'};
 2937:     my @keys;
 2938:     foreach my $part (@{$partids}) {
 2939: 	push(@keys,"$symb\0$part\0resettime");
 2940:     }
 2941:     my %results=&Apache::lonnet::get('nohist_resourcetracker',\@keys,
 2942: 				     $cdom,$cname);
 2943:     foreach my $part (@{$partids}) {
 2944: 	$last_resets{$part}=$results{"$symb\0$part\0resettime"};
 2945:     }
 2946:     return %last_resets;
 2947: }
 2948: 
 2949: # ----------- Handles creating versions for portfolio files as answers
 2950: sub version_portfiles {
 2951:     my ($record, $parts_graded, $courseid, $symb, $domain, $stu_name, $v_flag) = @_;
 2952:     my $version_parts = join('|',@$v_flag);
 2953:     my @returned_keys;
 2954:     my $parts = join('|', @$parts_graded);
 2955:     my $portfolio_root = '/userfiles/portfolio';
 2956:     foreach my $key (keys(%$record)) {
 2957:         my $new_portfiles;
 2958:         if ($key =~ /^resource\.($version_parts)\./ && $key =~ /\.portfiles$/ ) {
 2959:             my @versioned_portfiles;
 2960:             my @portfiles = split(/\s*,\s*/,$$record{$key});
 2961:             foreach my $file (@portfiles) {
 2962:                 &Apache::lonnet::unmark_as_readonly($domain,$stu_name,[$symb,$env{'request.course.id'}],$file);
 2963:                 my ($directory,$answer_file) =($file =~ /^(.*?)([^\/]*)$/);
 2964: 		my ($answer_name,$answer_ver,$answer_ext) =
 2965: 		    &file_name_version_ext($answer_file);
 2966:                 my $getpropath = 1;    
 2967:                 my @dir_list = &Apache::lonnet::dirlist($portfolio_root.$directory,$domain,$stu_name,$getpropath);
 2968:                 my $version = &get_next_version($answer_name, $answer_ext, \@dir_list);
 2969:                 my $new_answer = &version_selected_portfile($domain, $stu_name, $directory, $answer_file, $version);
 2970:                 if ($new_answer ne 'problem getting file') {
 2971:                     push(@versioned_portfiles, $directory.$new_answer);
 2972:                     &Apache::lonnet::mark_as_readonly($domain,$stu_name,
 2973:                         [$directory.$new_answer],
 2974:                         [$symb,$env{'request.course.id'},'graded']);
 2975:                 }
 2976:             }
 2977:             $$record{$key} = join(',',@versioned_portfiles);
 2978:             push(@returned_keys,$key);
 2979:         }
 2980:     } 
 2981:     return (@returned_keys);   
 2982: }
 2983: 
 2984: sub get_next_version {
 2985:     my ($answer_name, $answer_ext, $dir_list) = @_;
 2986:     my $version;
 2987:     foreach my $row (@$dir_list) {
 2988:         my ($file) = split(/\&/,$row,2);
 2989:         my ($file_name,$file_version,$file_ext) =
 2990: 	    &file_name_version_ext($file);
 2991:         if (($file_name eq $answer_name) && 
 2992: 	    ($file_ext eq $answer_ext)) {
 2993:                 # gets here if filename and extension match, regardless of version
 2994:                 if ($file_version ne '') {
 2995:                 # a versioned file is found  so save it for later
 2996:                 if ($file_version > $version) {
 2997: 		    $version = $file_version;
 2998: 	        }
 2999:             }
 3000:         }
 3001:     } 
 3002:     $version ++;
 3003:     return($version);
 3004: }
 3005: 
 3006: sub version_selected_portfile {
 3007:     my ($domain,$stu_name,$directory,$file_name,$version) = @_;
 3008:     my ($answer_name,$answer_ver,$answer_ext) =
 3009:         &file_name_version_ext($file_name);
 3010:     my $new_answer;
 3011:     $env{'form.copy'} = &Apache::lonnet::getfile("/uploaded/$domain/$stu_name/portfolio$directory$file_name");
 3012:     if($env{'form.copy'} eq '-1') {
 3013:         $new_answer = 'problem getting file';
 3014:     } else {
 3015:         $new_answer = $answer_name.'.'.$version.'.'.$answer_ext;
 3016:         my $copy_result = &Apache::lonnet::finishuserfileupload(
 3017:                             $stu_name,$domain,'copy',
 3018: 		        '/portfolio'.$directory.$new_answer);
 3019:     }    
 3020:     return ($new_answer);
 3021: }
 3022: 
 3023: sub file_name_version_ext {
 3024:     my ($file)=@_;
 3025:     my @file_parts = split(/\./, $file);
 3026:     my ($name,$version,$ext);
 3027:     if (@file_parts > 1) {
 3028: 	$ext=pop(@file_parts);
 3029: 	if (@file_parts > 1 && $file_parts[-1] =~ /^\d+$/) {
 3030: 	    $version=pop(@file_parts);
 3031: 	}
 3032: 	$name=join('.',@file_parts);
 3033:     } else {
 3034: 	$name=join('.',@file_parts);
 3035:     }
 3036:     return($name,$version,$ext);
 3037: }
 3038: 
 3039: #--------------------------------------------------------------------------------------
 3040: #
 3041: #-------------------------- Next few routines handles grading by section or whole class
 3042: #
 3043: #--- Javascript to handle grading by section or whole class
 3044: sub viewgrades_js {
 3045:     my ($request) = shift;
 3046: 
 3047:     my $alertmsg = &mt('A number equal or greater than 0 is expected. Entered value = ');
 3048:     $request->print(<<VIEWJAVASCRIPT);
 3049: <script type="text/javascript" language="javascript">
 3050:    function writePoint(partid,weight,point) {
 3051: 	var radioButton = document.classgrade["RADVAL_"+partid];
 3052: 	var textbox = document.classgrade["TEXTVAL_"+partid];
 3053: 	if (point == "textval") {
 3054: 	    point = document.classgrade["TEXTVAL_"+partid].value;
 3055: 	    if (isNaN(point) || parseFloat(point) < 0) {
 3056: 		alert("$alertmsg"+parseFloat(point));
 3057: 		var resetbox = false;
 3058: 		for (var i=0; i<radioButton.length; i++) {
 3059: 		    if (radioButton[i].checked) {
 3060: 			textbox.value = i;
 3061: 			resetbox = true;
 3062: 		    }
 3063: 		}
 3064: 		if (!resetbox) {
 3065: 		    textbox.value = "";
 3066: 		}
 3067: 		return;
 3068: 	    }
 3069: 	    if (parseFloat(point) > parseFloat(weight)) {
 3070: 		var resp = confirm("You entered a value ("+parseFloat(point)+
 3071: 				   ") greater than the weight for the part. Accept?");
 3072: 		if (resp == false) {
 3073: 		    textbox.value = "";
 3074: 		    return;
 3075: 		}
 3076: 	    }
 3077: 	    for (var i=0; i<radioButton.length; i++) {
 3078: 		radioButton[i].checked=false;
 3079: 		if (parseFloat(point) == i) {
 3080: 		    radioButton[i].checked=true;
 3081: 		}
 3082: 	    }
 3083: 
 3084: 	} else {
 3085: 	    textbox.value = parseFloat(point);
 3086: 	}
 3087: 	for (i=0;i<document.classgrade.total.value;i++) {
 3088: 	    var user = document.classgrade["ctr"+i].value;
 3089: 	    user = user.replace(new RegExp(':', 'g'),"_");
 3090: 	    var scorename = document.classgrade["GD_"+user+"_"+partid+"_awarded"];
 3091: 	    var saveval   = document.classgrade["GD_"+user+"_"+partid+"_solved_s"].value;
 3092: 	    var selname   = document.classgrade["GD_"+user+"_"+partid+"_solved"];
 3093: 	    if (saveval != "correct") {
 3094: 		scorename.value = point;
 3095: 		if (selname[0].selected != true) {
 3096: 		    selname[0].selected = true;
 3097: 		}
 3098: 	    }
 3099: 	}
 3100: 	document.classgrade["SELVAL_"+partid][0].selected = true;
 3101:     }
 3102: 
 3103:     function writeRadText(partid,weight) {
 3104: 	var selval   = document.classgrade["SELVAL_"+partid];
 3105: 	var radioButton = document.classgrade["RADVAL_"+partid];
 3106:         var override = document.classgrade["FORCE_"+partid].checked;
 3107: 	var textbox = document.classgrade["TEXTVAL_"+partid];
 3108: 	if (selval[1].selected || selval[2].selected) {
 3109: 	    for (var i=0; i<radioButton.length; i++) {
 3110: 		radioButton[i].checked=false;
 3111: 
 3112: 	    }
 3113: 	    textbox.value = "";
 3114: 
 3115: 	    for (i=0;i<document.classgrade.total.value;i++) {
 3116: 		var user = document.classgrade["ctr"+i].value;
 3117: 		user = user.replace(new RegExp(':', 'g'),"_");
 3118: 		var scorename = document.classgrade["GD_"+user+"_"+partid+"_awarded"];
 3119: 		var saveval   = document.classgrade["GD_"+user+"_"+partid+"_solved_s"].value;
 3120: 		var selname   = document.classgrade["GD_"+user+"_"+partid+"_solved"];
 3121: 		if ((saveval != "correct") || override) {
 3122: 		    scorename.value = "";
 3123: 		    if (selval[1].selected) {
 3124: 			selname[1].selected = true;
 3125: 		    } else {
 3126: 			selname[2].selected = true;
 3127: 			if (Number(document.classgrade["GD_"+user+"_"+partid+"_tries"].value)) 
 3128: 			{document.classgrade["GD_"+user+"_"+partid+"_tries"].value = '0';}
 3129: 		    }
 3130: 		}
 3131: 	    }
 3132: 	} else {
 3133: 	    for (i=0;i<document.classgrade.total.value;i++) {
 3134: 		var user = document.classgrade["ctr"+i].value;
 3135: 		user = user.replace(new RegExp(':', 'g'),"_");
 3136: 		var scorename = document.classgrade["GD_"+user+"_"+partid+"_awarded"];
 3137: 		var saveval   = document.classgrade["GD_"+user+"_"+partid+"_solved_s"].value;
 3138: 		var selname   = document.classgrade["GD_"+user+"_"+partid+"_solved"];
 3139: 		if ((saveval != "correct") || override) {
 3140: 		    scorename.value = document.classgrade["GD_"+user+"_"+partid+"_awarded_s"].value;
 3141: 		    selname[0].selected = true;
 3142: 		}
 3143: 	    }
 3144: 	}	    
 3145:     }
 3146: 
 3147:     function changeSelect(partid,user) {
 3148: 	var selval = document.classgrade["GD_"+user+'_'+partid+"_solved"];
 3149: 	var textbox = document.classgrade["GD_"+user+'_'+partid+"_awarded"];
 3150: 	var point  = textbox.value;
 3151: 	var weight = document.classgrade["weight_"+partid].value;
 3152: 
 3153: 	if (isNaN(point) || parseFloat(point) < 0) {
 3154: 	    alert("$alertmsg"+parseFloat(point));
 3155: 	    textbox.value = "";
 3156: 	    return;
 3157: 	}
 3158: 	if (parseFloat(point) > parseFloat(weight)) {
 3159: 	    var resp = confirm("You entered a value ("+parseFloat(point)+
 3160: 			       ") greater than the weight of the part. Accept?");
 3161: 	    if (resp == false) {
 3162: 		textbox.value = "";
 3163: 		return;
 3164: 	    }
 3165: 	}
 3166: 	selval[0].selected = true;
 3167:     }
 3168: 
 3169:     function changeOneScore(partid,user) {
 3170: 	var selval = document.classgrade["GD_"+user+'_'+partid+"_solved"];
 3171: 	if (selval[1].selected || selval[2].selected) {
 3172: 	    document.classgrade["GD_"+user+'_'+partid+"_awarded"].value = "";
 3173: 	    if (selval[2].selected) {
 3174: 		document.classgrade["GD_"+user+'_'+partid+"_tries"].value = "0";
 3175: 	    }
 3176:         }
 3177:     }
 3178: 
 3179:     function resetEntry(numpart) {
 3180: 	for (ctpart=0;ctpart<numpart;ctpart++) {
 3181: 	    var partid = document.classgrade["partid_"+ctpart].value;
 3182: 	    var radioButton = document.classgrade["RADVAL_"+partid];
 3183: 	    var textbox = document.classgrade["TEXTVAL_"+partid];
 3184: 	    var selval  = document.classgrade["SELVAL_"+partid];
 3185: 	    for (var i=0; i<radioButton.length; i++) {
 3186: 		radioButton[i].checked=false;
 3187: 
 3188: 	    }
 3189: 	    textbox.value = "";
 3190: 	    selval[0].selected = true;
 3191: 
 3192: 	    for (i=0;i<document.classgrade.total.value;i++) {
 3193: 		var user = document.classgrade["ctr"+i].value;
 3194: 		user = user.replace(new RegExp(':', 'g'),"_");
 3195: 		var resetscore = document.classgrade["GD_"+user+"_"+partid+"_awarded"];
 3196: 		resetscore.value = document.classgrade["GD_"+user+"_"+partid+"_awarded_s"].value;
 3197: 		var resettries = document.classgrade["GD_"+user+"_"+partid+"_tries"];
 3198: 		resettries.value = document.classgrade["GD_"+user+"_"+partid+"_tries_s"].value;
 3199: 		var saveselval   = document.classgrade["GD_"+user+"_"+partid+"_solved_s"].value;
 3200: 		var selname   = document.classgrade["GD_"+user+"_"+partid+"_solved"];
 3201: 		if (saveselval == "excused") {
 3202: 		    if (selname[1].selected == false) { selname[1].selected = true;}
 3203: 		} else {
 3204: 		    if (selname[0].selected == false) {selname[0].selected = true};
 3205: 		}
 3206: 	    }
 3207: 	}
 3208:     }
 3209: 
 3210: </script>
 3211: VIEWJAVASCRIPT
 3212: }
 3213: 
 3214: #--- show scores for a section or whole class w/ option to change/update a score
 3215: sub viewgrades {
 3216:     my ($request) = shift;
 3217:     &viewgrades_js($request);
 3218: 
 3219:     my ($symb) = &get_symb($request);
 3220:     #need to make sure we have the correct data for later EXT calls, 
 3221:     #thus invalidate the cache
 3222:     &Apache::lonnet::devalidatecourseresdata(
 3223:                  $env{'course.'.$env{'request.course.id'}.'.num'},
 3224:                  $env{'course.'.$env{'request.course.id'}.'.domain'});
 3225:     &Apache::lonnet::clear_EXT_cache_status();
 3226: 
 3227:     my $result='<h3><span class="LC_info">'.&mt('Manual Grading').'</span></h3>';
 3228:     $result.='<h4>'.&mt('<b>Current Resource: </b>[_1]',$env{'form.probTitle'}).'</h4>'."\n";
 3229: 
 3230:     #view individual student submission form - called using Javascript viewOneStudent
 3231:     $result.=&jscriptNform($symb);
 3232: 
 3233:     #beginning of class grading form
 3234:     my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status'));
 3235:     $result.= '<form action="/adm/grades" method="post" name="classgrade">'."\n".
 3236: 	'<input type="hidden" name="symb"    value="'.&Apache::lonenc::check_encrypt($symb).'" />'."\n".
 3237: 	'<input type="hidden" name="command" value="editgrades" />'."\n".
 3238: 	&build_section_inputs().
 3239: 	'<input type="hidden" name="saveState" value="'.$env{'form.saveState'}.'" />'."\n".
 3240: 	'<input type="hidden" name="Status" value="'.$env{'stu_status'}.'" />'."\n".
 3241: 	'<input type="hidden" name="probTitle" value="'.$env{'form.probTitle'}.'" />'."\n";
 3242: 
 3243:     my ($common_header,$specific_header);
 3244:     if ($env{'form.section'} eq 'all') {
 3245: 	$common_header = &mt('Assign Common Grade to Class');
 3246:         $specific_header = &mt('Assign Grade to Specific Students in Class');
 3247:     } elsif ($env{'form.section'} eq 'none') {
 3248:         $common_header = &mt('Assign Common Grade to Students in no Section');
 3249: 	$specific_header = &mt('Assign Grade to Specific Students in no Section');
 3250:     } else {
 3251:         my $section_display = join (", ",&Apache::loncommon::get_env_multiple('form.section'));
 3252:         $common_header = &mt('Assign Common Grade to Students in Section(s) [_1]',$section_display);
 3253: 	$specific_header = &mt('Assign Grade to Specific Students in Section(s) [_1]',$section_display);
 3254:     }
 3255:     $result.= '<h3>'.$common_header.'</h3>'.&Apache::loncommon::start_data_table();
 3256:     #radio buttons/text box for assigning points for a section or class.
 3257:     #handles different parts of a problem
 3258:     my ($partlist,$handgrade,$responseType) = &response_type($symb);
 3259:     my %weight = ();
 3260:     my $ctsparts = 0;
 3261:     my %seen = ();
 3262:     my @part_response_id = &flatten_responseType($responseType);
 3263:     foreach my $part_response_id (@part_response_id) {
 3264:     	my ($partid,$respid) = @{ $part_response_id };
 3265: 	my $part_resp = join('_',@{ $part_response_id });
 3266: 	next if $seen{$partid};
 3267: 	$seen{$partid}++;
 3268: 	my $handgrade=$$handgrade{$part_resp};
 3269: 	my $wgt = &Apache::lonnet::EXT('resource.'.$partid.'.weight',$symb);
 3270: 	$weight{$partid} = $wgt eq '' ? '1' : $wgt;
 3271: 
 3272: 	my $display_part=&get_display_part($partid,$symb);
 3273: 	my $radio.='<table border="0"><tr>';  
 3274: 	my $ctr = 0;
 3275: 	while ($ctr<=$weight{$partid}) { # display radio buttons in a nice table 10 across
 3276: 	    $radio.= '<td><label><input type="radio" name="RADVAL_'.$partid.'" '.
 3277: 		'onclick="javascript:writePoint(\''.$partid.'\','.$weight{$partid}.
 3278: 		','.$ctr.')" />'.$ctr."</label></td>\n";
 3279: 	    $result.=(($ctr+1)%10 == 0 ? '</tr><tr>' : '');
 3280: 	    $ctr++;
 3281: 	}
 3282: 	$radio.='</tr></table>';
 3283: 	my $line = '<input type="text" name="TEXTVAL_'.
 3284: 	    $partid.'" size="4" '.'onChange="javascript:writePoint(\''.
 3285: 		$partid.'\','.$weight{$partid}.',\'textval\')" /> /'.
 3286: 	    $weight{$partid}.' '.&mt('(problem weight)').'</td>'."\n";
 3287: 	$line.= '<td><b>'.&mt('Grade Status').':</b><select name="SELVAL_'.$partid.'"'.
 3288: 	    'onChange="javascript:writeRadText(\''.$partid.'\','.
 3289: 		$weight{$partid}.')"> '.
 3290: 	    '<option selected="selected"> </option>'.
 3291: 	    '<option value="excused">'.&mt('excused').'</option>'.
 3292: 	    '<option value="reset status">'.&mt('reset status').'</option>'.
 3293: 	    '</select></td>'.
 3294:             '<td><label><input type="checkbox" name="FORCE_'.$partid.'" />'.&mt('Override "Correct"').'</label>';
 3295: 	$line.='<input type="hidden" name="partid_'.
 3296: 	    $ctsparts.'" value="'.$partid.'" />'."\n";
 3297: 	$line.='<input type="hidden" name="weight_'.
 3298: 	    $partid.'" value="'.$weight{$partid}.'" />'."\n";
 3299: 
 3300: 	$result.=
 3301: 	    &Apache::loncommon::start_data_table_row()."\n".
 3302: 	    '<td><b>'.&mt('Part:').'</b></td><td>'.$display_part.'</td><td><b>'.&mt('Points:').'</b></td><td>'.$radio.'</td><td>'.&mt('or').'</td><td>'.$line.'</td>'.
 3303: 	    &Apache::loncommon::end_data_table_row()."\n";
 3304: 	$ctsparts++;
 3305:     }
 3306:     $result.=&Apache::loncommon::end_data_table()."\n".
 3307: 	'<input type="hidden" name="totalparts" value="'.$ctsparts.'" />';
 3308:     $result.='<input type="button" value="'.&mt('Revert to Default').'" '.
 3309: 	'onClick="javascript:resetEntry('.$ctsparts.');" />';
 3310: 
 3311:     #table listing all the students in a section/class
 3312:     #header of table
 3313:     $result.= '<h3>'.$specific_header.'</h3>'.
 3314:               &Apache::loncommon::start_data_table().
 3315: 	      &Apache::loncommon::start_data_table_header_row().
 3316: 	      '<th>'.&mt('No.').'</th>'.
 3317: 	      '<th>'.&nameUserString('header')."</th>\n";
 3318:     my (@parts) = sort(&getpartlist($symb));
 3319:     my (undef,undef,$url)=&Apache::lonnet::decode_symb($symb);
 3320:     my @partids = ();
 3321:     foreach my $part (@parts) {
 3322: 	my $display=&Apache::lonnet::metadata($url,$part.'.display');
 3323:         my $narrowtext = &mt('Tries');
 3324: 	$display =~ s|^Number of Attempts|$narrowtext <br />|; # makes the column narrower
 3325: 	if  (!$display) { $display = &Apache::lonnet::metadata($url,$part.'.name'); }
 3326: 	my ($partid) = &split_part_type($part);
 3327:         push(@partids,$partid);
 3328: 	my $display_part=&get_display_part($partid,$symb);
 3329: 	if ($display =~ /^Partial Credit Factor/) {
 3330: 	    $result.='<th>'.
 3331: 		&mt('Score Part: [_1]<br /> (weight = [_2])',
 3332: 		    $display_part,$weight{$partid}).'</th>'."\n";
 3333: 	    next;
 3334: 	    
 3335: 	} else {
 3336: 	    if ($display =~ /Problem Status/) {
 3337: 		my $grade_status_mt = &mt('Grade Status');
 3338: 		$display =~ s{Problem Status}{$grade_status_mt<br />};
 3339: 	    }
 3340: 	    my $part_mt = &mt('Part:');
 3341: 	    $display =~s{\[Part: \Q$partid\E\]}{$part_mt $display_part};
 3342: 	}
 3343: 
 3344: 	$result.='<th>'.$display.'</th>'."\n";
 3345:     }
 3346:     $result.=&Apache::loncommon::end_data_table_header_row();
 3347: 
 3348:     my %last_resets = 
 3349: 	&get_last_resets($symb,$env{'request.course.id'},\@partids);
 3350: 
 3351:     #get info for each student
 3352:     #list all the students - with points and grade status
 3353:     my (undef,undef,$fullname) = &getclasslist($env{'form.section'},'1');
 3354:     my $ctr = 0;
 3355:     foreach (sort 
 3356: 	     {
 3357: 		 if (lc($$fullname{$a}) ne lc($$fullname{$b})) {
 3358: 		     return (lc($$fullname{$a}) cmp lc($$fullname{$b}));
 3359: 		 }
 3360: 		 return $a cmp $b;
 3361: 	     } (keys(%$fullname))) {
 3362: 	$ctr++;
 3363: 	$result.=&viewstudentgrade($symb,$env{'request.course.id'},
 3364: 				   $_,$$fullname{$_},\@parts,\%weight,$ctr,\%last_resets);
 3365:     }
 3366:     $result.=&Apache::loncommon::end_data_table();
 3367:     $result.='<input type="hidden" name="total" value="'.$ctr.'" />'."\n";
 3368:     $result.='<input type="button" value="'.&mt('Save').'" '.
 3369: 	'onClick="javascript:submit();" target="_self" /></form>'."\n";
 3370:     if (scalar(%$fullname) eq 0) {
 3371: 	my $colspan=3+scalar(@parts);
 3372: 	my $section_display = join (", ",&Apache::loncommon::get_env_multiple('form.section'));
 3373:         my $stu_status = join(' or ',&Apache::loncommon::get_env_multiple('form.Status'));
 3374: 	$result='<span class="LC_warning">'.
 3375: 	    &mt('There are no students in section(s) [_1] with enrollment status [_2] to modify or grade.',
 3376: 	        $section_display, $stu_status).
 3377: 	    '</span>';
 3378:     }
 3379:     $result.=&show_grading_menu_form($symb);
 3380:     return $result;
 3381: }
 3382: 
 3383: #--- call by previous routine to display each student
 3384: sub viewstudentgrade {
 3385:     my ($symb,$courseid,$student,$fullname,$parts,$weight,$ctr,$last_resets) = @_;
 3386:     my ($uname,$udom) = split(/:/,$student);
 3387:     my %record=&Apache::lonnet::restore($symb,$courseid,$udom,$uname);
 3388:     my %aggregates = (); 
 3389:     my $result=&Apache::loncommon::start_data_table_row().'<td align="right">'.
 3390: 	'<input type="hidden" name="ctr'.($ctr-1).'" value="'.$student.'" />'.
 3391: 	"\n".$ctr.'&nbsp;</td><td>&nbsp;'.
 3392: 	'<a href="javascript:viewOneStudent(\''.$uname.'\',\''.$udom.
 3393: 	'\');" target="_self">'.$fullname.'</a> '.
 3394: 	'<span class="LC_internal_info">('.$uname.($env{'user.domain'} eq $udom ? '' : ':'.$udom).')</span></td>'."\n";
 3395:     $student=~s/:/_/; # colon doen't work in javascript for names
 3396:     foreach my $apart (@$parts) {
 3397: 	my ($part,$type) = &split_part_type($apart);
 3398: 	my $score=$record{"resource.$part.$type"};
 3399:         $result.='<td align="center">';
 3400:         my ($aggtries,$totaltries);
 3401:         unless (exists($aggregates{$part})) {
 3402: 	    $totaltries = $record{'resource.'.$part.'.tries'};
 3403: 
 3404: 	    $aggtries = $totaltries;
 3405:             if ($$last_resets{$part}) {  
 3406:                 $aggtries = &get_num_tries(\%record,$$last_resets{$part},
 3407: 					   $part);
 3408:             }
 3409:             $result.='<input type="hidden" name="'.
 3410:                 'GD_'.$student.'_'.$part.'_aggtries" value="'.$aggtries.'" />'."\n";
 3411:             $result.='<input type="hidden" name="'.
 3412:                 'GD_'.$student.'_'.$part.'_totaltries" value="'.$totaltries.'" />'."\n";
 3413:             $aggregates{$part} = 1;
 3414:         }
 3415: 	if ($type eq 'awarded') {
 3416: 	    my $pts = $score eq '' ? '' : &compute_points($score,$$weight{$part});
 3417: 	    $result.='<input type="hidden" name="'.
 3418: 		'GD_'.$student.'_'.$part.'_awarded_s" value="'.$pts.'" />'."\n";
 3419: 	    $result.='<input type="text" name="'.
 3420: 		'GD_'.$student.'_'.$part.'_awarded" '.
 3421: 		'onChange="javascript:changeSelect(\''.$part.'\',\''.$student.
 3422: 		'\')" value="'.$pts.'" size="4" /></td>'."\n";
 3423: 	} elsif ($type eq 'solved') {
 3424: 	    my ($status,$foo)=split(/_/,$score,2);
 3425: 	    $status = 'nothing' if ($status eq '');
 3426: 	    $result.='<input type="hidden" name="'.'GD_'.$student.'_'.
 3427: 		$part.'_solved_s" value="'.$status.'" />'."\n";
 3428: 	    $result.='&nbsp;<select name="'.
 3429: 		'GD_'.$student.'_'.$part.'_solved" '.
 3430: 		'onChange="javascript:changeOneScore(\''.$part.'\',\''.$student.'\')" >'."\n";
 3431: 	    $result.= (($status eq 'excused') ? '<option> </option><option selected="selected" value="excused">'.&mt('excused').'</option>' 
 3432: 		: '<option selected="selected"> </option><option value="excused">'.&mt('excused').'</option>')."\n";
 3433: 	    $result.='<option value="reset status">'.&mt('reset status').'</option>';
 3434: 	    $result.="</select>&nbsp;</td>\n";
 3435: 	} else {
 3436: 	    $result.='<input type="hidden" name="'.
 3437: 		'GD_'.$student.'_'.$part.'_'.$type.'_s" value="'.$score.'" />'.
 3438: 		    "\n";
 3439: 	    $result.='<input type="text" name="'.
 3440: 		'GD_'.$student.'_'.$part.'_'.$type.'" '.
 3441: 		'value="'.$score.'" size="4" /></td>'."\n";
 3442: 	}
 3443:     }
 3444:     $result.=&Apache::loncommon::end_data_table_row();
 3445:     return $result;
 3446: }
 3447: 
 3448: #--- change scores for all the students in a section/class
 3449: #    record does not get update if unchanged
 3450: sub editgrades {
 3451:     my ($request) = @_;
 3452: 
 3453:     my $symb=&get_symb($request);
 3454:     my $section_display = join (", ",&Apache::loncommon::get_env_multiple('form.section'));
 3455:     my $title='<h2>'.&mt('Current Grade Status').'</h2>';
 3456:     $title.='<h4>'.&mt('<b>Current Resource: </b>[_1]',$env{'form.probTitle'}).'</h4>'."\n";
 3457:     $title.='<h4>'.&mt('<b>Section: </b>[_1]',$section_display).'</h4>'."\n";
 3458: 
 3459:     my $result= &Apache::loncommon::start_data_table().
 3460: 	&Apache::loncommon::start_data_table_header_row().
 3461: 	'<th rowspan="2" valign="middle">'.&mt('No.').'</th>'.
 3462: 	'<th rowspan="2" valign="middle">'.&nameUserString('header')."</th>\n";
 3463:     my %scoreptr = (
 3464: 		    'correct'  =>'correct_by_override',
 3465: 		    'incorrect'=>'incorrect_by_override',
 3466: 		    'excused'  =>'excused',
 3467: 		    'ungraded' =>'ungraded_attempted',
 3468: 		    'nothing'  => '',
 3469: 		    );
 3470:     my ($classlist,undef,$fullname) = &getclasslist($env{'form.section'},'0');
 3471: 
 3472:     my (@partid);
 3473:     my %weight = ();
 3474:     my %columns = ();
 3475:     my ($i,$ctr,$count,$rec_update) = (0,0,0,0);
 3476: 
 3477:     my (@parts) = sort(&getpartlist($symb));
 3478:     my $header;
 3479:     while ($ctr < $env{'form.totalparts'}) {
 3480: 	my $partid = $env{'form.partid_'.$ctr};
 3481: 	push(@partid,$partid);
 3482: 	$weight{$partid} = $env{'form.weight_'.$partid};
 3483: 	$ctr++;
 3484:     }
 3485:     my (undef,undef,$url) = &Apache::lonnet::decode_symb($symb);
 3486:     foreach my $partid (@partid) {
 3487: 	$header .= '<th align="center">'.&mt('Old Score').'</th>'.
 3488: 	    '<th align="center">'.&mt('New Score').'</th>';
 3489: 	$columns{$partid}=2;
 3490: 	foreach my $stores (@parts) {
 3491: 	    my ($part,$type) = &split_part_type($stores);
 3492: 	    if ($part !~ m/^\Q$partid\E/) { next;}
 3493: 	    if ($type eq 'awarded' || $type eq 'solved') { next; }
 3494: 	    my $display=&Apache::lonnet::metadata($url,$stores.'.display');
 3495: 	    $display =~ s/\[Part: \Q$part\E\]//;
 3496:             my $narrowtext = &mt('Tries');
 3497: 	    $display =~ s/Number of Attempts/$narrowtext/;
 3498: 	    $header .= '<th align="center">'.&mt('Old').' '.$display.'</th>'.
 3499: 		'<th align="center">'.&mt('New').' '.$display.'</th>';
 3500: 	    $columns{$partid}+=2;
 3501: 	}
 3502:     }
 3503:     foreach my $partid (@partid) {
 3504: 	my $display_part=&get_display_part($partid,$symb);
 3505: 	$result .= '<th colspan="'.$columns{$partid}.'" align="center">'.
 3506: 	    &mt('Part: [_1] (Weight = [_2])',$display_part,$weight{$partid}).
 3507: 	    '</th>';
 3508: 
 3509:     }
 3510:     $result .= &Apache::loncommon::end_data_table_header_row().
 3511: 	&Apache::loncommon::start_data_table_header_row().
 3512: 	$header.
 3513: 	&Apache::loncommon::end_data_table_header_row();
 3514:     my @noupdate;
 3515:     my ($updateCtr,$noupdateCtr) = (1,1);
 3516:     for ($i=0; $i<$env{'form.total'}; $i++) {
 3517: 	my $line;
 3518: 	my $user = $env{'form.ctr'.$i};
 3519: 	my ($uname,$udom)=split(/:/,$user);
 3520: 	my %newrecord;
 3521: 	my $updateflag = 0;
 3522: 	$line .= '<td>'.&nameUserString(undef,$$fullname{$user},$uname,$udom).'</td>';
 3523: 	my $usec=$classlist->{"$uname:$udom"}[5];
 3524: 	if (!&canmodify($usec)) {
 3525: 	    my $numcols=scalar(@partid)*4+2;
 3526: 	    push(@noupdate,
 3527: 		 $line."<td colspan=\"$numcols\"><span class=\"LC_warning\">".
 3528: 		 &mt('Not allowed to modify student')."</span></td></tr>");
 3529: 	    next;
 3530: 	}
 3531:         my %aggregate = ();
 3532:         my $aggregateflag = 0;
 3533: 	$user=~s/:/_/; # colon doen't work in javascript for names
 3534: 	foreach (@partid) {
 3535: 	    my $old_aw    = $env{'form.GD_'.$user.'_'.$_.'_awarded_s'};
 3536: 	    my $old_part_pcr = $old_aw/($weight{$_} ne '0' ? $weight{$_}:1);
 3537: 	    my $old_part  = $old_aw eq '' ? '' : $old_part_pcr;
 3538: 	    my $old_score = $scoreptr{$env{'form.GD_'.$user.'_'.$_.'_solved_s'}};
 3539: 	    my $awarded   = $env{'form.GD_'.$user.'_'.$_.'_awarded'};
 3540: 	    my $pcr       = $awarded/($weight{$_} ne '0' ? $weight{$_} : 1);
 3541: 	    my $partial   = $awarded eq '' ? '' : $pcr;
 3542: 	    my $score;
 3543: 	    if ($partial eq '') {
 3544: 		$score = $scoreptr{$env{'form.GD_'.$user.'_'.$_.'_solved_s'}};
 3545: 	    } elsif ($partial > 0) {
 3546: 		$score = 'correct_by_override';
 3547: 	    } elsif ($partial == 0) {
 3548: 		$score = 'incorrect_by_override';
 3549: 	    }
 3550: 	    my $dropMenu = $env{'form.GD_'.$user.'_'.$_.'_solved'};
 3551: 	    $score = 'excused' if (($dropMenu eq 'excused') && ($score ne 'excused'));
 3552: 
 3553: 	    $newrecord{'resource.'.$_.'.regrader'}=
 3554: 		"$env{'user.name'}:$env{'user.domain'}";
 3555: 	    if ($dropMenu eq 'reset status' &&
 3556: 		$old_score ne '') { # ignore if no previous attempts => nothing to reset
 3557: 		$newrecord{'resource.'.$_.'.tries'} = '';
 3558: 		$newrecord{'resource.'.$_.'.solved'} = '';
 3559: 		$newrecord{'resource.'.$_.'.award'} = '';
 3560: 		$newrecord{'resource.'.$_.'.awarded'} = '';
 3561: 		$updateflag = 1;
 3562:                 if ($env{'form.GD_'.$user.'_'.$_.'_aggtries'} > 0) {
 3563:                     my $aggtries = $env{'form.GD_'.$user.'_'.$_.'_aggtries'};
 3564:                     my $totaltries = $env{'form.GD_'.$user.'_'.$_.'_totaltries'};
 3565:                     my $solvedstatus = $env{'form.GD_'.$user.'_'.$_.'_solved_s'};
 3566:                     &decrement_aggs($symb,$_,\%aggregate,$aggtries,$totaltries,$solvedstatus);
 3567:                     $aggregateflag = 1;
 3568:                 }
 3569: 	    } elsif (!($old_part eq $partial && $old_score eq $score)) {
 3570: 		$updateflag = 1;
 3571: 		$newrecord{'resource.'.$_.'.awarded'}  = $partial if $partial ne '';
 3572: 		$newrecord{'resource.'.$_.'.solved'}   = $score;
 3573: 		$rec_update++;
 3574: 	    }
 3575: 
 3576: 	    $line .= '<td align="center">'.$old_aw.'&nbsp;</td>'.
 3577: 		'<td align="center">'.$awarded.
 3578: 		($score eq 'excused' ? $score : '').'&nbsp;</td>';
 3579: 
 3580: 
 3581: 	    my $partid=$_;
 3582: 	    foreach my $stores (@parts) {
 3583: 		my ($part,$type) = &split_part_type($stores);
 3584: 		if ($part !~ m/^\Q$partid\E/) { next;}
 3585: 		if ($type eq 'awarded' || $type eq 'solved') { next; }
 3586: 		my $old_aw    = $env{'form.GD_'.$user.'_'.$part.'_'.$type.'_s'};
 3587: 		my $awarded   = $env{'form.GD_'.$user.'_'.$part.'_'.$type};
 3588: 		if ($awarded ne '' && $awarded ne $old_aw) {
 3589: 		    $newrecord{'resource.'.$part.'.'.$type}= $awarded;
 3590: 		    $newrecord{'resource.'.$part.'.regrader'}="$env{'user.name'}:$env{'user.domain'}";
 3591: 		    $updateflag=1;
 3592: 		}
 3593: 		$line .= '<td align="center">'.$old_aw.'&nbsp;</td>'.
 3594: 		    '<td align="center">'.$awarded.'&nbsp;</td>';
 3595: 	    }
 3596: 	}
 3597: 	$line.="\n";
 3598: 
 3599: 	my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'};
 3600: 	my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'};
 3601: 
 3602: 	if ($updateflag) {
 3603: 	    $count++;
 3604: 	    &Apache::lonnet::cstore(\%newrecord,$symb,$env{'request.course.id'},
 3605: 				    $udom,$uname);
 3606: 
 3607: 	    if (&Apache::bridgetask::in_queue('gradingqueue',$symb,$cdom,
 3608: 					      $cnum,$udom,$uname)) {
 3609: 		# need to figure out if should be in queue.
 3610: 		my %record =  
 3611: 		    &Apache::lonnet::restore($symb,$env{'request.course.id'},
 3612: 					     $udom,$uname);
 3613: 		my $all_graded = 1;
 3614: 		my $none_graded = 1;
 3615: 		foreach my $part (@parts) {
 3616: 		    if ( $record{'resource.'.$part.'.awarded'} eq '' ) {
 3617: 			$all_graded = 0;
 3618: 		    } else {
 3619: 			$none_graded = 0;
 3620: 		    }
 3621: 		}
 3622: 
 3623: 		if ($all_graded || $none_graded) {
 3624: 		    &Apache::bridgetask::remove_from_queue('gradingqueue',
 3625: 							   $symb,$cdom,$cnum,
 3626: 							   $udom,$uname);
 3627: 		}
 3628: 	    }
 3629: 
 3630: 	    $result.=&Apache::loncommon::start_data_table_row().
 3631: 		'<td align="right">&nbsp;'.$updateCtr.'&nbsp;</td>'.$line.
 3632: 		&Apache::loncommon::end_data_table_row();
 3633: 	    $updateCtr++;
 3634: 	} else {
 3635: 	    push(@noupdate,
 3636: 		 '<td align="right">&nbsp;'.$noupdateCtr.'&nbsp;</td>'.$line);
 3637: 	    $noupdateCtr++;
 3638: 	}
 3639:         if ($aggregateflag) {
 3640:             &Apache::lonnet::cinc('nohist_resourcetracker',\%aggregate,
 3641: 				  $cdom,$cnum);
 3642:         }
 3643:     }
 3644:     if (@noupdate) {
 3645: #	my $numcols=(scalar(@partid)*(scalar(@parts)-1)*2)+3;
 3646: 	my $numcols=scalar(@partid)*4+2;
 3647: 	$result .= &Apache::loncommon::start_data_table_row('LC_empty_row').
 3648: 	    '<td align="center" colspan="'.$numcols.'">'.
 3649: 	    &mt('No Changes Occurred For the Students Below').
 3650: 	    '</td>'.
 3651: 	    &Apache::loncommon::end_data_table_row();
 3652: 	foreach my $line (@noupdate) {
 3653: 	    $result.=
 3654: 		&Apache::loncommon::start_data_table_row().
 3655: 		$line.
 3656: 		&Apache::loncommon::end_data_table_row();
 3657: 	}
 3658:     }
 3659:     $result .= &Apache::loncommon::end_data_table().
 3660: 	&show_grading_menu_form($symb);
 3661:     my $msg = '<p><b>'.
 3662: 	&mt('Number of records updated = [_1] for [quant,_2,student].',
 3663: 	    $rec_update,$count).'</b><br />'.
 3664: 	'<b>'.&mt('Total number of students = [_1]',$env{'form.total'}).
 3665: 	'</b></p>';
 3666:     return $title.$msg.$result;
 3667: }
 3668: 
 3669: sub split_part_type {
 3670:     my ($partstr) = @_;
 3671:     my ($temp,@allparts)=split(/_/,$partstr);
 3672:     my $type=pop(@allparts);
 3673:     my $part=join('_',@allparts);
 3674:     return ($part,$type);
 3675: }
 3676: 
 3677: #------------- end of section for handling grading by section/class ---------
 3678: #
 3679: #----------------------------------------------------------------------------
 3680: 
 3681: 
 3682: #----------------------------------------------------------------------------
 3683: #
 3684: #-------------------------- Next few routines handles grading by csv upload
 3685: #
 3686: #--- Javascript to handle csv upload
 3687: sub csvupload_javascript_reverse_associate {
 3688:     my $error1=&mt('You need to specify the username or the student/employee ID');
 3689:     my $error2=&mt('You need to specify at least one grading field');
 3690:   return(<<ENDPICK);
 3691:   function verify(vf) {
 3692:     var foundsomething=0;
 3693:     var founduname=0;
 3694:     var foundID=0;
 3695:     for (i=0;i<=vf.nfields.value;i++) {
 3696:       tw=eval('vf.f'+i+'.selectedIndex');
 3697:       if (i==0 && tw!=0) { foundID=1; }
 3698:       if (i==1 && tw!=0) { founduname=1; }
 3699:       if (i!=0 && i!=1 && i!=2 && tw!=0) { foundsomething=1; }
 3700:     }
 3701:     if (founduname==0 && foundID==0) {
 3702: 	alert('$error1');
 3703: 	return;
 3704:     }
 3705:     if (foundsomething==0) {
 3706: 	alert('$error2');
 3707: 	return;
 3708:     }
 3709:     vf.submit();
 3710:   }
 3711:   function flip(vf,tf) {
 3712:     var nw=eval('vf.f'+tf+'.selectedIndex');
 3713:     var i;
 3714:     for (i=0;i<=vf.nfields.value;i++) {
 3715:       //can not pick the same destination field for both name and domain
 3716:       if (((i ==0)||(i ==1)) && 
 3717:           ((tf==0)||(tf==1)) && 
 3718:           (i!=tf) &&
 3719:           (eval('vf.f'+i+'.selectedIndex')==nw)) {
 3720:         eval('vf.f'+i+'.selectedIndex=0;')
 3721:       }
 3722:     }
 3723:   }
 3724: ENDPICK
 3725: }
 3726: 
 3727: sub csvupload_javascript_forward_associate {
 3728:     my $error1=&mt('You need to specify the username or the student/employee ID');
 3729:     my $error2=&mt('You need to specify at least one grading field');
 3730:   return(<<ENDPICK);
 3731:   function verify(vf) {
 3732:     var foundsomething=0;
 3733:     var founduname=0;
 3734:     var foundID=0;
 3735:     for (i=0;i<=vf.nfields.value;i++) {
 3736:       tw=eval('vf.f'+i+'.selectedIndex');
 3737:       if (tw==1) { foundID=1; }
 3738:       if (tw==2) { founduname=1; }
 3739:       if (tw>3) { foundsomething=1; }
 3740:     }
 3741:     if (founduname==0 && foundID==0) {
 3742: 	alert('$error1');
 3743: 	return;
 3744:     }
 3745:     if (foundsomething==0) {
 3746: 	alert('$error2');
 3747: 	return;
 3748:     }
 3749:     vf.submit();
 3750:   }
 3751:   function flip(vf,tf) {
 3752:     var nw=eval('vf.f'+tf+'.selectedIndex');
 3753:     var i;
 3754:     //can not pick the same destination field twice
 3755:     for (i=0;i<=vf.nfields.value;i++) {
 3756:       if ((i!=tf) && (eval('vf.f'+i+'.selectedIndex')==nw)) {
 3757:         eval('vf.f'+i+'.selectedIndex=0;')
 3758:       }
 3759:     }
 3760:   }
 3761: ENDPICK
 3762: }
 3763: 
 3764: sub csvuploadmap_header {
 3765:     my ($request,$symb,$datatoken,$distotal)= @_;
 3766:     my $javascript;
 3767:     if ($env{'form.upfile_associate'} eq 'reverse') {
 3768: 	$javascript=&csvupload_javascript_reverse_associate();
 3769:     } else {
 3770: 	$javascript=&csvupload_javascript_forward_associate();
 3771:     }
 3772: 
 3773:     my ($result) = &showResourceInfo($symb,$env{'form.probTitle'});
 3774:     my $checked=(($env{'form.noFirstLine'})?' checked="checked"':'');
 3775:     my $ignore=&mt('Ignore First Line');
 3776:     $symb = &Apache::lonenc::check_encrypt($symb);
 3777:     $request->print(<<ENDPICK);
 3778: <form method="post" enctype="multipart/form-data" action="/adm/grades" name="gradesupload">
 3779: <h3><span class="LC_info">Uploading Class Grades</span></h3>
 3780: $result
 3781: <hr />
 3782: <h3>Identify fields</h3>
 3783: Total number of records found in file: $distotal <hr />
 3784: Enter as many fields as you can. The system will inform you and bring you back
 3785: to this page if the data selected is insufficient to run your class.<hr />
 3786: <input type="button" value="Reverse Association" onClick="javascript:this.form.associate.value='Reverse Association';submit(this.form);" />
 3787: <label><input type="checkbox" name="noFirstLine" $checked />$ignore</label>
 3788: <input type="hidden" name="associate"  value="" />
 3789: <input type="hidden" name="phase"      value="three" />
 3790: <input type="hidden" name="datatoken"  value="$datatoken" />
 3791: <input type="hidden" name="fileupload" value="$env{'form.fileupload'}" />
 3792: <input type="hidden" name="upfiletype" value="$env{'form.upfiletype'}" />
 3793: <input type="hidden" name="upfile_associate" 
 3794:                                        value="$env{'form.upfile_associate'}" />
 3795: <input type="hidden" name="symb"       value="$symb" />
 3796: <input type="hidden" name="saveState"  value="$env{'form.saveState'}" />
 3797: <input type="hidden" name="probTitle"  value="$env{'form.probTitle'}" />
 3798: <input type="hidden" name="command"    value="csvuploadoptions" />
 3799: <hr />
 3800: <script type="text/javascript" language="Javascript">
 3801: $javascript
 3802: </script>
 3803: ENDPICK
 3804:     return '';
 3805: 
 3806: }
 3807: 
 3808: sub csvupload_fields {
 3809:     my ($symb) = @_;
 3810:     my (@parts) = &getpartlist($symb);
 3811:     my @fields=(['ID','Student/Employee ID'],
 3812: 		['username','Student Username'],
 3813: 		['domain','Student Domain']);
 3814:     my (undef,undef,$url) = &Apache::lonnet::decode_symb($symb);
 3815:     foreach my $part (sort(@parts)) {
 3816: 	my @datum;
 3817: 	my $display=&Apache::lonnet::metadata($url,$part.'.display');
 3818: 	my $name=$part;
 3819: 	if  (!$display) { $display = $name; }
 3820: 	@datum=($name,$display);
 3821: 	if ($name=~/^stores_(.*)_awarded/) {
 3822: 	    push(@fields,['stores_'.$1.'_points',"Points [Part: $1]"]);
 3823: 	}
 3824: 	push(@fields,\@datum);
 3825:     }
 3826:     return (@fields);
 3827: }
 3828: 
 3829: sub csvuploadmap_footer {
 3830:     my ($request,$i,$keyfields) =@_;
 3831:     $request->print(<<ENDPICK);
 3832: </table>
 3833: <input type="hidden" name="nfields" value="$i" />
 3834: <input type="hidden" name="keyfields" value="$keyfields" />
 3835: <input type="button" onClick="javascript:verify(this.form)" value="Assign Grades" /><br />
 3836: </form>
 3837: ENDPICK
 3838: }
 3839: 
 3840: sub checkforfile_js {
 3841:     my $alertmsg = &mt('Please use the browse button to select a file from your local directory.');
 3842:     my $result =<<CSVFORMJS;
 3843: <script type="text/javascript" language="javascript">
 3844:     function checkUpload(formname) {
 3845: 	if (formname.upfile.value == "") {
 3846: 	    alert("$alertmsg");
 3847: 	    return false;
 3848: 	}
 3849: 	formname.submit();
 3850:     }
 3851:     </script>
 3852: CSVFORMJS
 3853:     return $result;
 3854: }
 3855: 
 3856: sub upcsvScores_form {
 3857:     my ($request) = shift;
 3858:     my ($symb)=&get_symb($request);
 3859:     if (!$symb) {return '';}
 3860:     my $result=&checkforfile_js();
 3861:     $env{'form.probTitle'} = &Apache::lonnet::gettitle($symb);
 3862:     my ($table) = &showResourceInfo($symb,$env{'form.probTitle'});
 3863:     $result.=$table;
 3864:     $result.='<br /><table width="100%" border="0"><tr><td bgcolor="#777777">'."\n";
 3865:     $result.='<table width="100%" border="0"><tr bgcolor="#e6ffff"><td>'."\n";
 3866:     $result.='&nbsp;<b>'.&mt('Specify a file containing the class scores for current resource.').
 3867: 	'</b></td></tr>'."\n";
 3868:     $result.='<tr bgcolor=#ffffe6><td>'."\n";
 3869:     my $upload=&mt("Upload Scores");
 3870:     my $upfile_select=&Apache::loncommon::upfile_select_html();
 3871:     my $ignore=&mt('Ignore First Line');
 3872:     $symb = &Apache::lonenc::check_encrypt($symb);
 3873:     $result.=<<ENDUPFORM;
 3874: <form method="post" enctype="multipart/form-data" action="/adm/grades" name="gradesupload">
 3875: <input type="hidden" name="symb" value="$symb" />
 3876: <input type="hidden" name="command" value="csvuploadmap" />
 3877: <input type="hidden" name="probTitle" value="$env{'form.probTitle'}" />
 3878: <input type="hidden" name="saveState"  value="$env{'form.saveState'}" />
 3879: $upfile_select
 3880: <br /><input type="button" onClick="javascript:checkUpload(this.form);" value="$upload" />
 3881: <label><input type="checkbox" name="noFirstLine" />$ignore</label>
 3882: </form>
 3883: ENDUPFORM
 3884:     $result.=&Apache::loncommon::help_open_topic("Course_Convert_To_CSV",
 3885:                            &mt("How do I create a CSV file from a spreadsheet"))
 3886:     .'</td></tr></table>'."\n";
 3887:     $result.='</td></tr></table><br /><br />'."\n";
 3888:     $result.=&show_grading_menu_form($symb);
 3889:     return $result;
 3890: }
 3891: 
 3892: 
 3893: sub csvuploadmap {
 3894:     my ($request)= @_;
 3895:     my ($symb)=&get_symb($request);
 3896:     if (!$symb) {return '';}
 3897: 
 3898:     my $datatoken;
 3899:     if (!$env{'form.datatoken'}) {
 3900: 	$datatoken=&Apache::loncommon::upfile_store($request);
 3901:     } else {
 3902: 	$datatoken=$env{'form.datatoken'};
 3903: 	&Apache::loncommon::load_tmp_file($request);
 3904:     }
 3905:     my @records=&Apache::loncommon::upfile_record_sep();
 3906:     if ($env{'form.noFirstLine'}) { shift(@records); }
 3907:     &csvuploadmap_header($request,$symb,$datatoken,$#records+1);
 3908:     my ($i,$keyfields);
 3909:     if (@records) {
 3910: 	my @fields=&csvupload_fields($symb);
 3911: 
 3912: 	if ($env{'form.upfile_associate'} eq 'reverse') {	
 3913: 	    &Apache::loncommon::csv_print_samples($request,\@records);
 3914: 	    $i=&Apache::loncommon::csv_print_select_table($request,\@records,
 3915: 							  \@fields);
 3916: 	    foreach (@fields) { $keyfields.=$_->[0].','; }
 3917: 	    chop($keyfields);
 3918: 	} else {
 3919: 	    unshift(@fields,['none','']);
 3920: 	    $i=&Apache::loncommon::csv_samples_select_table($request,\@records,
 3921: 							    \@fields);
 3922:             foreach my $rec (@records) {
 3923:                 my %temp = &Apache::loncommon::record_sep($rec);
 3924:                 if (%temp) {
 3925:                     $keyfields=join(',',sort(keys(%temp)));
 3926:                     last;
 3927:                 }
 3928:             }
 3929: 	}
 3930:     }
 3931:     &csvuploadmap_footer($request,$i,$keyfields);
 3932:     $request->print(&show_grading_menu_form($symb));
 3933: 
 3934:     return '';
 3935: }
 3936: 
 3937: sub csvuploadoptions {
 3938:     my ($request)= @_;
 3939:     my ($symb)=&get_symb($request);
 3940:     my $checked=(($env{'form.noFirstLine'})?'1':'0');
 3941:     my $ignore=&mt('Ignore First Line');
 3942:     $request->print(<<ENDPICK);
 3943: <form method="post" enctype="multipart/form-data" action="/adm/grades" name="gradesupload">
 3944: <h3><span class="LC_info">Uploading Class Grade Options</span></h3>
 3945: <input type="hidden" name="command"    value="csvuploadassign" />
 3946: <!--
 3947: <p>
 3948: <label>
 3949:    <input type="checkbox" name="show_full_results" />
 3950:    Show a table of all changes
 3951: </label>
 3952: </p>
 3953: -->
 3954: <p>
 3955: <label>
 3956:    <input type="checkbox" name="overwite_scores" checked="checked" />
 3957:    Overwrite any existing score
 3958: </label>
 3959: </p>
 3960: ENDPICK
 3961:     my %fields=&get_fields();
 3962:     if (!defined($fields{'domain'})) {
 3963: 	my $domform = &Apache::loncommon::select_dom_form($env{'request.role.domain'},'default_domain');
 3964: 	$request->print("\n<p> Users are in domain: ".$domform."</p>\n");
 3965:     }
 3966:     foreach my $key (sort(keys(%env))) {
 3967: 	if ($key !~ /^form\.(.*)$/) { next; }
 3968: 	my $cleankey=$1;
 3969: 	if ($cleankey eq 'command') { next; }
 3970: 	$request->print('<input type="hidden" name="'.$cleankey.
 3971: 			'"  value="'.$env{$key}.'" />'."\n");
 3972:     }
 3973:     # FIXME do a check for any duplicated user ids...
 3974:     # FIXME do a check for any invalid user ids?...
 3975:     $request->print('<input type="submit" value="Assign Grades" /><br />
 3976: <hr /></form>'."\n");
 3977:     $request->print(&show_grading_menu_form($symb));
 3978:     return '';
 3979: }
 3980: 
 3981: sub get_fields {
 3982:     my %fields;
 3983:     my @keyfields = split(/\,/,$env{'form.keyfields'});
 3984:     for (my $i=0; $i<=$env{'form.nfields'}; $i++) {
 3985: 	if ($env{'form.upfile_associate'} eq 'reverse') {
 3986: 	    if ($env{'form.f'.$i} ne 'none') {
 3987: 		$fields{$keyfields[$i]}=$env{'form.f'.$i};
 3988: 	    }
 3989: 	} else {
 3990: 	    if ($env{'form.f'.$i} ne 'none') {
 3991: 		$fields{$env{'form.f'.$i}}=$keyfields[$i];
 3992: 	    }
 3993: 	}
 3994:     }
 3995:     return %fields;
 3996: }
 3997: 
 3998: sub csvuploadassign {
 3999:     my ($request)= @_;
 4000:     my ($symb)=&get_symb($request);
 4001:     if (!$symb) {return '';}
 4002:     my $error_msg = '';
 4003:     &Apache::loncommon::load_tmp_file($request);
 4004:     my @gradedata = &Apache::loncommon::upfile_record_sep();
 4005:     if ($env{'form.noFirstLine'}) { shift(@gradedata); }
 4006:     my %fields=&get_fields();
 4007:     $request->print('<h3>Assigning Grades</h3>');
 4008:     my $courseid=$env{'request.course.id'};
 4009:     my ($classlist) = &getclasslist('all',0);
 4010:     my @notallowed;
 4011:     my @skipped;
 4012:     my $countdone=0;
 4013:     foreach my $grade (@gradedata) {
 4014: 	my %entries=&Apache::loncommon::record_sep($grade);
 4015: 	my $domain;
 4016: 	if ($entries{$fields{'domain'}}) {
 4017: 	    $domain=$entries{$fields{'domain'}};
 4018: 	} else {
 4019: 	    $domain=$env{'form.default_domain'};
 4020: 	}
 4021: 	$domain=~s/\s//g;
 4022: 	my $username=$entries{$fields{'username'}};
 4023: 	$username=~s/\s//g;
 4024: 	if (!$username) {
 4025: 	    my $id=$entries{$fields{'ID'}};
 4026: 	    $id=~s/\s//g;
 4027: 	    my %ids=&Apache::lonnet::idget($domain,$id);
 4028: 	    $username=$ids{$id};
 4029: 	}
 4030: 	if (!exists($$classlist{"$username:$domain"})) {
 4031: 	    my $id=$entries{$fields{'ID'}};
 4032: 	    $id=~s/\s//g;
 4033: 	    if ($id) {
 4034: 		push(@skipped,"$id:$domain");
 4035: 	    } else {
 4036: 		push(@skipped,"$username:$domain");
 4037: 	    }
 4038: 	    next;
 4039: 	}
 4040: 	my $usec=$classlist->{"$username:$domain"}[5];
 4041: 	if (!&canmodify($usec)) {
 4042: 	    push(@notallowed,"$username:$domain");
 4043: 	    next;
 4044: 	}
 4045: 	my %points;
 4046: 	my %grades;
 4047: 	foreach my $dest (keys(%fields)) {
 4048: 	    if ($dest eq 'ID' || $dest eq 'username' ||
 4049: 		$dest eq 'domain') { next; }
 4050: 	    if ($entries{$fields{$dest}} =~ /^\s*$/) { next; }
 4051: 	    if ($dest=~/stores_(.*)_points/) {
 4052: 		my $part=$1;
 4053: 		my $wgt =&Apache::lonnet::EXT('resource.'.$part.'.weight',
 4054: 					      $symb,$domain,$username);
 4055:                 if ($wgt) {
 4056:                     $entries{$fields{$dest}}=~s/\s//g;
 4057:                     my $pcr=$entries{$fields{$dest}} / $wgt;
 4058:                     my $award=($pcr == 0) ? 'incorrect_by_override'
 4059:                                           : 'correct_by_override';
 4060:                     $grades{"resource.$part.awarded"}=$pcr;
 4061:                     $grades{"resource.$part.solved"}=$award;
 4062:                     $points{$part}=1;
 4063:                 } else {
 4064:                     $error_msg = "<br />" .
 4065:                         &mt("Some point values were assigned"
 4066:                             ." for problems with a weight "
 4067:                             ."of zero. These values were "
 4068:                             ."ignored.");
 4069:                 }
 4070: 	    } else {
 4071: 		if ($dest=~/stores_(.*)_awarded/) { if ($points{$1}) {next;} }
 4072: 		if ($dest=~/stores_(.*)_solved/)  { if ($points{$1}) {next;} }
 4073: 		my $store_key=$dest;
 4074: 		$store_key=~s/^stores/resource/;
 4075: 		$store_key=~s/_/\./g;
 4076: 		$grades{$store_key}=$entries{$fields{$dest}};
 4077: 	    }
 4078: 	}
 4079: 	if (! %grades) { 
 4080:            push(@skipped,&mt("[_1]: no data to save","$username:$domain")); 
 4081:         } else {
 4082: 	   $grades{"resource.regrader"}="$env{'user.name'}:$env{'user.domain'}";
 4083: 	   my $result=&Apache::lonnet::cstore(\%grades,$symb,
 4084: 					   $env{'request.course.id'},
 4085: 					   $domain,$username);
 4086: 	   if ($result eq 'ok') {
 4087: 	      $request->print('.');
 4088: 	   } else {
 4089: 	      $request->print("<p><span class=\"LC_error\">".
 4090:                               &mt("Failed to save data for student [_1]. Message when trying to save was: [_2]",
 4091:                                   "$username:$domain",$result)."</span></p>");
 4092: 	   }
 4093: 	   $request->rflush();
 4094: 	   $countdone++;
 4095:         }
 4096:     }
 4097:     $request->print('<br />'.&Apache::lonhtmlcommon::confirm_success(&mt("Saved scores for [quant,_1,student]",$countdone),$countdone==0));
 4098:     if (@skipped) {
 4099: 	$request->print('<br />'.&Apache::lonhtmlcommon::confirm_success(&mt('No scores stored for the following username(s):'),1).'<br />');
 4100:         $request->print(join(', ',@skipped));
 4101:     }
 4102:     if (@notallowed) {
 4103: 	$request->print('<br />'.&Apache::lonhtmlcommon::confirm_success(&mt('Modification of scores not allowed for the following username(s):'),1).'<br />');
 4104: 	$request->print(join(', ',@notallowed));
 4105:     }
 4106:     $request->print("<br />\n");
 4107:     $request->print(&show_grading_menu_form($symb));
 4108:     return $error_msg;
 4109: }
 4110: #------------- end of section for handling csv file upload ---------
 4111: #
 4112: #-------------------------------------------------------------------
 4113: #
 4114: #-------------- Next few routines handle grading by page/sequence
 4115: #
 4116: #--- Select a page/sequence and a student to grade
 4117: sub pickStudentPage {
 4118:     my ($request) = shift;
 4119: 
 4120:     my $alertmsg = &mt('Please select the student you wish to grade.');
 4121:     $request->print(<<LISTJAVASCRIPT);
 4122: <script type="text/javascript" language="javascript">
 4123: 
 4124: function checkPickOne(formname) {
 4125:     if (radioSelection(formname.student) == null) {
 4126: 	alert("$alertmsg");
 4127: 	return;
 4128:     }
 4129:     ptr = pullDownSelection(formname.selectpage);
 4130:     formname.page.value = formname["page"+ptr].value;
 4131:     formname.title.value = formname["title"+ptr].value;
 4132:     formname.submit();
 4133: }
 4134: 
 4135: </script>
 4136: LISTJAVASCRIPT
 4137:     &commonJSfunctions($request);
 4138:     my ($symb) = &get_symb($request);
 4139:     my $cdom      = $env{"course.$env{'request.course.id'}.domain"};
 4140:     my $cnum      = $env{"course.$env{'request.course.id'}.num"};
 4141:     my $getsec    = $env{'form.section'} eq '' ? 'all' : $env{'form.section'};
 4142: 
 4143:     my $result='<h3><span class="LC_info">&nbsp;'.
 4144: 	&mt('Manual Grading by Page or Sequence').'</span></h3>';
 4145: 
 4146:     $result.='<form action="/adm/grades" method="post" name="displayPage">'."\n";
 4147:     my ($titles,$symbx) = &getSymbMap();
 4148:     my ($curpage) =&Apache::lonnet::decode_symb($symb); 
 4149: #    my ($curpage,$mapId) =&Apache::lonnet::decode_symb($symb); 
 4150: #    my $type=($curpage =~ /\.(page|sequence)/);
 4151:     my $select = '<select name="selectpage">'."\n";
 4152:     my $ctr=0;
 4153:     foreach (@$titles) {
 4154: 	my ($minder,$showtitle) = ($_ =~ /(\d+)\.(.*)/);
 4155: 	$select.='<option value="'.$ctr.'" '.
 4156: 	    ($$symbx{$_} =~ /$curpage$/ ? 'selected="selected"' : '').
 4157: 	    '>'.$showtitle.'</option>'."\n";
 4158: 	$ctr++;
 4159:     }
 4160:     $select.= '</select>';
 4161:     $result.='&nbsp;<b>'.&mt('Problems from').':</b> '.$select."<br />\n";
 4162: 
 4163:     $ctr=0;
 4164:     foreach (@$titles) {
 4165: 	my ($minder,$showtitle) = ($_ =~ /(\d+)\.(.*)/);
 4166: 	$result.='<input type="hidden" name="page'.$ctr.'" value="'.$$symbx{$_}.'" />'."\n";
 4167: 	$result.='<input type="hidden" name="title'.$ctr.'" value="'.$showtitle.'" />'."\n";
 4168: 	$ctr++;
 4169:     }
 4170:     $result.='<input type="hidden" name="page" />'."\n".
 4171: 	'<input type="hidden" name="title" />'."\n";
 4172: 
 4173:     my $options =
 4174: 	'<label><input type="radio" name="vProb" value="no" checked="checked" /> '.&mt('no').' </label>'."\n".
 4175: 	'<label><input type="radio" name="vProb" value="yes" /> '.&mt('yes').' </label>'."<br />\n";
 4176:     $result.='&nbsp;<b>'.&mt('View Problem Text').': </b>'.$options;
 4177: 
 4178:     $options =
 4179: 	'<label><input type="radio" name="lastSub" value="none" /> '.&mt('none').' </label>'."\n".
 4180: 	'<label><input type="radio" name="lastSub" value="datesub" checked="checked" /> '.&mt('by dates and submissions').'</label>'."\n".
 4181: 	'<label><input type="radio" name="lastSub" value="all" /> '.&mt('all details').' </label>'."\n";
 4182:     $result.='&nbsp;<b>'.&mt('Submissions').': </b>'.$options;
 4183:     
 4184:     $result.=&build_section_inputs();
 4185:     my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status'));
 4186:     $result.='<input type="hidden" name="Status"  value="'.$stu_status.'" />'."\n".
 4187: 	'<input type="hidden" name="command" value="displayPage" />'."\n".
 4188: 	'<input type="hidden" name="symb"    value="'.&Apache::lonenc::check_encrypt($symb).'" />'."\n".
 4189: 	'<input type="hidden" name="saveState" value="'.$env{'form.saveState'}.'" />'."<br />\n";
 4190: 
 4191:     $result.='&nbsp;<b>'.&mt('Use CODE').': </b> <input type="text" name="CODE" value="" /> <br />'."\n";
 4192: 
 4193:     $result.='&nbsp;<input type="button" '.
 4194: 	'onClick="javascript:checkPickOne(this.form);" value="'.&mt('Next').' &rarr;" /><br />'."\n";
 4195: 
 4196:     $request->print($result);
 4197: 
 4198:     my $studentTable.='&nbsp;<b>'.&mt('Select a student you wish to grade and then click on the Next button.').'</b><br />'.
 4199: 	&Apache::loncommon::start_data_table().
 4200: 	&Apache::loncommon::start_data_table_header_row().
 4201: 	'<th align="right">&nbsp;'.&mt('No.').'</th>'.
 4202: 	'<th>'.&nameUserString('header').'</th>'.
 4203: 	'<th align="right">&nbsp;'.&mt('No.').'</th>'.
 4204: 	'<th>'.&nameUserString('header').'</th>'.
 4205: 	&Apache::loncommon::end_data_table_header_row();
 4206:  
 4207:     my (undef,undef,$fullname) = &getclasslist($getsec,'1');
 4208:     my $ptr = 1;
 4209:     foreach my $student (sort 
 4210: 			 {
 4211: 			     if (lc($$fullname{$a}) ne lc($$fullname{$b})) {
 4212: 				 return (lc($$fullname{$a}) cmp lc($$fullname{$b}));
 4213: 			     }
 4214: 			     return $a cmp $b;
 4215: 			 } (keys(%$fullname))) {
 4216: 	my ($uname,$udom) = split(/:/,$student);
 4217: 	$studentTable.=($ptr%2==1 ? &Apache::loncommon::start_data_table_row()
 4218:                                   : '</td>');
 4219: 	$studentTable.='<td align="right">'.$ptr.'&nbsp;</td>';
 4220: 	$studentTable.='<td>&nbsp;<label><input type="radio" name="student" value="'.$student.'" /> '
 4221: 	    .&nameUserString(undef,$$fullname{$student},$uname,$udom)."</label>\n";
 4222: 	$studentTable.=
 4223: 	    ($ptr%2 == 0 ? '</td>'.&Apache::loncommon::end_data_table_row() 
 4224:                          : '');
 4225: 	$ptr++;
 4226:     }
 4227:     if ($ptr%2 == 0) {
 4228: 	$studentTable.='</td><td>&nbsp;</td><td>&nbsp;</td>'.
 4229: 	    &Apache::loncommon::end_data_table_row();
 4230:     }
 4231:     $studentTable.=&Apache::loncommon::end_data_table()."\n";
 4232:     $studentTable.='<input type="button" '.
 4233: 	'onClick="javascript:checkPickOne(this.form);" value="'.&mt('Next').' &rarr;" /></form>'."\n";
 4234: 
 4235:     $studentTable.=&show_grading_menu_form($symb);
 4236:     $request->print($studentTable);
 4237: 
 4238:     return '';
 4239: }
 4240: 
 4241: sub getSymbMap {
 4242:     my $navmap = Apache::lonnavmaps::navmap->new();
 4243: 
 4244:     my %symbx = ();
 4245:     my @titles = ();
 4246:     my $minder = 0;
 4247: 
 4248:     # Gather every sequence that has problems.
 4249:     my @sequences = $navmap->retrieveResources(undef, sub { shift->is_map(); },
 4250: 					       1,0,1);
 4251:     for my $sequence ($navmap->getById('0.0'), @sequences) {
 4252: 	if ($navmap->hasResource($sequence, sub { shift->is_problem(); }, 0) ) {
 4253: 	    my $title = $minder.'.'.
 4254: 		&HTML::Entities::encode($sequence->compTitle(),'"\'&');
 4255: 	    push(@titles, $title); # minder in case two titles are identical
 4256: 	    $symbx{$title} = &HTML::Entities::encode($sequence->symb(),'"\'&');
 4257: 	    $minder++;
 4258: 	}
 4259:     }
 4260:     return \@titles,\%symbx;
 4261: }
 4262: 
 4263: #
 4264: #--- Displays a page/sequence w/wo problems, w/wo submissions
 4265: sub displayPage {
 4266:     my ($request) = shift;
 4267: 
 4268:     my ($symb) = &get_symb($request);
 4269:     my $cdom      = $env{"course.$env{'request.course.id'}.domain"};
 4270:     my $cnum      = $env{"course.$env{'request.course.id'}.num"};
 4271:     my $getsec    = $env{'form.section'} eq '' ? 'all' : $env{'form.section'};
 4272:     my $pageTitle = $env{'form.page'};
 4273:     my ($classlist,undef,$fullname) = &getclasslist($getsec,'1');
 4274:     my ($uname,$udom) = split(/:/,$env{'form.student'});
 4275:     my $usec=$classlist->{$env{'form.student'}}[5];
 4276: 
 4277:     #need to make sure we have the correct data for later EXT calls, 
 4278:     #thus invalidate the cache
 4279:     &Apache::lonnet::devalidatecourseresdata(
 4280:                  $env{'course.'.$env{'request.course.id'}.'.num'},
 4281:                  $env{'course.'.$env{'request.course.id'}.'.domain'});
 4282:     &Apache::lonnet::clear_EXT_cache_status();
 4283: 
 4284:     if (!&canview($usec)) {
 4285: 	$request->print('<span class="LC_warning">'.&mt('Unable to view requested student. ([_1])',$env{'form.student'}).'</span>');
 4286: 	$request->print(&show_grading_menu_form($symb));
 4287: 	return;
 4288:     }
 4289:     my $result='<h3><span class="LC_info">&nbsp;'.$env{'form.title'}.'</span></h3>';
 4290:     $result.='<h3>&nbsp;'.&mt('Student: [_1]',&nameUserString(undef,$$fullname{$env{'form.student'}},$uname,$udom)).
 4291: 	'</h3>'."\n";
 4292:     $env{'form.CODE'} = uc($env{'form.CODE'});
 4293:     if (&Apache::lonnet::validCODE(uc($env{'form.CODE'}))) {
 4294: 	$result.='<h3>&nbsp;'.&mt('CODE: [_1]',$env{'form.CODE'}).'</h3>'."\n";
 4295:     } else {
 4296: 	delete($env{'form.CODE'});
 4297:     }
 4298:     &sub_page_js($request);
 4299:     $request->print($result);
 4300: 
 4301:     my $navmap = Apache::lonnavmaps::navmap->new();
 4302:     my ($mapUrl, $id, $resUrl)=&Apache::lonnet::decode_symb($env{'form.page'});
 4303:     my $map = $navmap->getResourceByUrl($resUrl); # add to navmaps
 4304:     if (!$map) {
 4305: 	$request->print('<span class="LC_warning">'.&mt('Unable to view requested sequence. ([_1])',$resUrl).'</span>');
 4306: 	$request->print(&show_grading_menu_form($symb));
 4307: 	return; 
 4308:     }
 4309:     my $iterator = $navmap->getIterator($map->map_start(),
 4310: 					$map->map_finish());
 4311: 
 4312:     my $studentTable='<form action="/adm/grades" method="post" name="gradePage">'."\n".
 4313: 	'<input type="hidden" name="command" value="gradeByPage" />'."\n".
 4314: 	'<input type="hidden" name="fullname" value="'.$$fullname{$env{'form.student'}}.'" />'."\n".
 4315: 	'<input type="hidden" name="student" value="'.$env{'form.student'}.'" />'."\n".
 4316: 	'<input type="hidden" name="page"    value="'.$pageTitle.'" />'."\n".
 4317: 	'<input type="hidden" name="title"   value="'.$env{'form.title'}.'" />'."\n".
 4318: 	'<input type="hidden" name="symb"    value="'.&Apache::lonenc::check_encrypt($symb).'" />'."\n".
 4319: 	'<input type="hidden" name="overRideScore" value="no" />'."\n".
 4320: 	'<input type="hidden" name="saveState" value="'.$env{'form.saveState'}.'" />'."\n";
 4321: 
 4322:     if (defined($env{'form.CODE'})) {
 4323: 	$studentTable.=
 4324: 	    '<input type="hidden" name="CODE" value="'.$env{'form.CODE'}.'" />'."\n";
 4325:     }
 4326:     my $checkIcon = '<img alt="'.&mt('Check Mark').
 4327: 	'" src="'.&Apache::loncommon::lonhttpdurl($request->dir_config('lonIconsURL').'/check.gif').'" height="16" border="0" />';
 4328: 
 4329:     $studentTable.='&nbsp;'.&mt('<b>Note:</b> Problems graded correct by the computer are marked with a [_1] symbol.',$checkIcon)."\n".
 4330: 	&Apache::loncommon::start_data_table().
 4331: 	&Apache::loncommon::start_data_table_header_row().
 4332: 	'<th align="center">&nbsp;Prob.&nbsp;</th>'.
 4333: 	'<th>&nbsp;'.($env{'form.vProb'} eq 'no' ? &mt('Title') : &mt('Problem Text')).'/'.&mt('Grade').'</th>'.
 4334: 	&Apache::loncommon::end_data_table_header_row();
 4335: 
 4336:     &Apache::lonxml::clear_problem_counter();
 4337:     my ($depth,$question,$prob) = (1,1,1);
 4338:     $iterator->next(); # skip the first BEGIN_MAP
 4339:     my $curRes = $iterator->next(); # for "current resource"
 4340:     while ($depth > 0) {
 4341:         if($curRes == $iterator->BEGIN_MAP) { $depth++; }
 4342:         if($curRes == $iterator->END_MAP) { $depth--; }
 4343: 
 4344:         if (ref($curRes) && $curRes->is_problem()) {
 4345: 	    my $parts = $curRes->parts();
 4346:             my $title = $curRes->compTitle();
 4347: 	    my $symbx = $curRes->symb();
 4348: 	    $studentTable.=
 4349: 		&Apache::loncommon::start_data_table_row().
 4350: 		'<td align="center" valign="top" >'.$prob.
 4351: 		(scalar(@{$parts}) == 1 ? '' 
 4352: 		                        : '<br />('.&mt('[_1]&nbsp;parts)',
 4353: 							scalar(@{$parts}))
 4354: 		 ).
 4355: 		 '</td>';
 4356: 	    $studentTable.='<td valign="top">';
 4357: 	    my %form = ('CODE' => $env{'form.CODE'},);
 4358: 	    if ($env{'form.vProb'} eq 'yes' ) {
 4359: 		$studentTable.=&show_problem($request,$symbx,$uname,$udom,1,
 4360: 					     undef,'both',\%form);
 4361: 	    } else {
 4362: 		my $companswer = &Apache::loncommon::get_student_answers($symbx,$uname,$udom,$env{'request.course.id'},%form);
 4363: 		$companswer =~ s|<form(.*?)>||g;
 4364: 		$companswer =~ s|</form>||g;
 4365: #		while ($companswer =~ /(<a href\=\"javascript:newWindow.*?Script Vars<\/a>)/s) { #<a href="javascript:newWindow</a>
 4366: #		    $companswer =~ s/$1/ /ms;
 4367: #		    $request->print('match='.$1."<br />\n");
 4368: #		}
 4369: #		$companswer =~ s|<table border=\"1\">|<table border=\"0\">|g;
 4370: 		$studentTable.='&nbsp;<b>'.$title.'</b>&nbsp;<br />&nbsp;<b>'.&mt('Correct answer').':</b><br />'.$companswer;
 4371: 	    }
 4372: 
 4373: 	    my %record = &Apache::lonnet::restore($symbx,$env{'request.course.id'},$udom,$uname);
 4374: 
 4375: 	    if ($env{'form.lastSub'} eq 'datesub') {
 4376: 		if ($record{'version'} eq '') {
 4377: 		    $studentTable.='<br />&nbsp;<span class="LC_warning">'.&mt('No recorded submission for this problem.').'</span><br />';
 4378: 		} else {
 4379: 		    my %responseType = ();
 4380: 		    foreach my $partid (@{$parts}) {
 4381: 			my @responseIds =$curRes->responseIds($partid);
 4382: 			my @responseType =$curRes->responseType($partid);
 4383: 			my %responseIds;
 4384: 			for (my $i=0;$i<=$#responseIds;$i++) {
 4385: 			    $responseIds{$responseIds[$i]}=$responseType[$i];
 4386: 			}
 4387: 			$responseType{$partid} = \%responseIds;
 4388: 		    }
 4389: 		    $studentTable.= &displaySubByDates($symbx,\%record,$parts,\%responseType,$checkIcon,$uname,$udom);
 4390: 
 4391: 		}
 4392: 	    } elsif ($env{'form.lastSub'} eq 'all') {
 4393: 		my $last = ($env{'form.lastSub'} eq 'last' ? 'last' : '');
 4394: 		$studentTable.=&Apache::loncommon::get_previous_attempt($symbx,$uname,$udom,
 4395: 									$env{'request.course.id'},
 4396: 									'','.submission');
 4397:  
 4398: 	    }
 4399: 	    if (&canmodify($usec)) {
 4400: 		foreach my $partid (@{$parts}) {
 4401: 		    $studentTable.=&gradeBox($request,$symbx,$uname,$udom,$question,$partid,\%record);
 4402: 		    $studentTable.='<input type="hidden" name="q_'.$question.'" value="'.$partid.'" />'."\n";
 4403: 		    $question++;
 4404: 		}
 4405: 		$prob++;
 4406: 	    }
 4407: 	    $studentTable.='</td></tr>';
 4408: 
 4409: 	}
 4410:         $curRes = $iterator->next();
 4411:     }
 4412: 
 4413:     $studentTable.='</table>'."\n".
 4414: 	'<input type="button" value="'.&mt('Save').'" '.
 4415: 	'onClick="javascript:checkSubmitPage(this.form,'.$question.');" />'.
 4416: 	'</form>'."\n";
 4417:     $studentTable.=&show_grading_menu_form($symb);
 4418:     $request->print($studentTable);
 4419: 
 4420:     return '';
 4421: }
 4422: 
 4423: sub displaySubByDates {
 4424:     my ($symb,$record,$parts,$responseType,$checkIcon,$uname,$udom) = @_;
 4425:     my $isCODE=0;
 4426:     my $isTask = ($symb =~/\.task$/);
 4427:     if (exists($record->{'resource.CODE'})) { $isCODE=1; }
 4428:     my $studentTable=&Apache::loncommon::start_data_table().
 4429: 	&Apache::loncommon::start_data_table_header_row().
 4430: 	'<th>'.&mt('Date/Time').'</th>'.
 4431: 	($isCODE?'<th>'.&mt('CODE').'</th>':'').
 4432: 	'<th>'.&mt('Submission').'</th>'.
 4433: 	'<th>'.&mt('Status').'</th>'.
 4434: 	&Apache::loncommon::end_data_table_header_row();
 4435:     my ($version);
 4436:     my %mark;
 4437:     my %orders;
 4438:     $mark{'correct_by_student'} = $checkIcon;
 4439:     if (!exists($$record{'1:timestamp'})) {
 4440: 	return '<br />&nbsp;<span class="LC_warning">'.&mt('Nothing submitted - no attempts.').'</span><br />';
 4441:     }
 4442: 
 4443:     my $interaction;
 4444:     my $no_increment = 1;
 4445:     for ($version=1;$version<=$$record{'version'};$version++) {
 4446: 	my $timestamp = 
 4447: 	    &Apache::lonlocal::locallocaltime($$record{$version.':timestamp'});
 4448: 	if (exists($$record{$version.':resource.0.version'})) {
 4449: 	    $interaction = $$record{$version.':resource.0.version'};
 4450: 	}
 4451: 
 4452: 	my $where = ($isTask ? "$version:resource.$interaction"
 4453: 		             : "$version:resource");
 4454: 	$studentTable.=&Apache::loncommon::start_data_table_row().
 4455: 	    '<td>'.$timestamp.'</td>';
 4456: 	if ($isCODE) {
 4457: 	    $studentTable.='<td>'.$record->{$version.':resource.CODE'}.'</td>';
 4458: 	}
 4459: 	my @versionKeys = split(/\:/,$$record{$version.':keys'});
 4460: 	my @displaySub = ();
 4461: 	foreach my $partid (@{$parts}) {
 4462: 	    my @matchKey = ($isTask ? sort(grep /^resource\.\d+\.\Q$partid\E\.award$/,@versionKeys)
 4463: 			            : sort(grep /^resource\.\Q$partid\E\..*?\.submission$/,@versionKeys));
 4464: 	    
 4465: 
 4466: #	    next if ($$record{"$version:resource.$partid.solved"} eq '');
 4467: 	    my $display_part=&get_display_part($partid,$symb);
 4468: 	    foreach my $matchKey (@matchKey) {
 4469: 		if (exists($$record{$version.':'.$matchKey}) &&
 4470: 		    $$record{$version.':'.$matchKey} ne '') {
 4471: 
 4472: 		    my ($responseId)= ($isTask ? ($matchKey=~ /^resource\.(.*?)\.\Q$partid\E\.award$/)
 4473: 				               : ($matchKey=~ /^resource\.\Q$partid\E\.(.*?)\.submission$/));
 4474:                     $displaySub[0].='<span class="LC_nobreak"';
 4475:                     $displaySub[0].='<b>'.&mt('Part: [_1]',$display_part).'</b>'
 4476:                                    .' <span class="LC_internal_info">'
 4477:                                    .'('.&mt('Part ID: [_1]',$responseId).')'
 4478:                                    .'</span>'
 4479:                                    .' <b>';
 4480: 		    if ($$record{"$where.$partid.tries"} eq '') {
 4481: 			$displaySub[0].=&mt('Trial not counted');
 4482: 		    } else {
 4483: 			$displaySub[0].=&mt('Trial: [_1]',
 4484: 					    $$record{"$where.$partid.tries"});
 4485: 		    }
 4486: 		    my $responseType=($isTask ? 'Task'
 4487:                                               : $responseType->{$partid}->{$responseId});
 4488: 		    if (!exists($orders{$partid})) { $orders{$partid}={}; }
 4489: 		    if (!exists($orders{$partid}->{$responseId})) {
 4490: 			$orders{$partid}->{$responseId}=
 4491: 			    &get_order($partid,$responseId,$symb,$uname,$udom,
 4492:                                        $no_increment);
 4493: 		    }
 4494: 		    $displaySub[0].='</b></span>'; # /nobreak
 4495: 		    $displaySub[0].='&nbsp; '.
 4496: 			&cleanRecord($$record{$version.':'.$matchKey},$responseType,$symb,$partid,$responseId,$record,$orders{$partid}->{$responseId},"$version:",$uname,$udom).'<br />';
 4497: 		}
 4498: 	    }
 4499: 	    if (exists($$record{"$where.$partid.checkedin"})) {
 4500: 		$displaySub[1].=&mt('Checked in by [_1] into slot [_2]',
 4501: 				    $$record{"$where.$partid.checkedin"},
 4502: 				    $$record{"$where.$partid.checkedin.slot"}).
 4503: 					'<br />';
 4504: 	    }
 4505: 	    if (exists $$record{"$where.$partid.award"}) {
 4506: 		$displaySub[1].='<b>'.&mt('Part:').'</b>&nbsp;'.$display_part.' &nbsp;'.
 4507: 		    lc($$record{"$where.$partid.award"}).' '.
 4508: 		    $mark{$$record{"$where.$partid.solved"}}.
 4509: 		    '<br />';
 4510: 	    }
 4511: 	    if (exists $$record{"$where.$partid.regrader"}) {
 4512: 		$displaySub[2].=$$record{"$where.$partid.regrader"}.
 4513: 		    ' (<b>'.&mt('Part').':</b> '.$display_part.')';
 4514: 	    } elsif ($$record{"$version:resource.$partid.regrader"} =~ /\S/) {
 4515: 		$displaySub[2].=
 4516: 		    $$record{"$version:resource.$partid.regrader"}.
 4517: 		    ' (<b>'.&mt('Part').':</b> '.$display_part.')';
 4518: 	    }
 4519: 	}
 4520: 	# needed because old essay regrader has not parts info
 4521: 	if (exists $$record{"$version:resource.regrader"}) {
 4522: 	    $displaySub[2].=$$record{"$version:resource.regrader"};
 4523: 	}
 4524: 	$studentTable.='<td>'.$displaySub[0].'&nbsp;</td><td>'.$displaySub[1];
 4525: 	if ($displaySub[2]) {
 4526: 	    $studentTable.=&mt('Manually graded by [_1]',$displaySub[2]);
 4527: 	}
 4528: 	$studentTable.='&nbsp;</td>'.
 4529: 	    &Apache::loncommon::end_data_table_row();
 4530:     }
 4531:     $studentTable.=&Apache::loncommon::end_data_table();
 4532:     return $studentTable;
 4533: }
 4534: 
 4535: sub updateGradeByPage {
 4536:     my ($request) = shift;
 4537: 
 4538:     my $cdom      = $env{"course.$env{'request.course.id'}.domain"};
 4539:     my $cnum      = $env{"course.$env{'request.course.id'}.num"};
 4540:     my $getsec    = $env{'form.section'} eq '' ? 'all' : $env{'form.section'};
 4541:     my $pageTitle = $env{'form.page'};
 4542:     my ($classlist,undef,$fullname) = &getclasslist($getsec,'1');
 4543:     my ($uname,$udom) = split(/:/,$env{'form.student'});
 4544:     my $usec=$classlist->{$env{'form.student'}}[5];
 4545:     if (!&canmodify($usec)) {
 4546: 	$request->print('<span class="LC_warning">'.&mt('Unable to modify requested student ([_1])',$env{'form.student'}).'</span>');
 4547: 	$request->print(&show_grading_menu_form($env{'form.symb'}));
 4548: 	return;
 4549:     }
 4550:     my $result='<h3><span class="LC_info">&nbsp;'.$env{'form.title'}.'</span></h3>';
 4551:     $result.='<h3>&nbsp;'.&mt('Student: ').&nameUserString(undef,$env{'form.fullname'},$uname,$udom).
 4552: 	'</h3>'."\n";
 4553: 
 4554:     $request->print($result);
 4555: 
 4556:     my $navmap = Apache::lonnavmaps::navmap->new();
 4557:     my ($mapUrl, $id, $resUrl) = &Apache::lonnet::decode_symb( $env{'form.page'});
 4558:     my $map = $navmap->getResourceByUrl($resUrl); # add to navmaps
 4559:     if (!$map) {
 4560: 	$request->print('<span class="LC_warning">'.&mt('Unable to grade requested sequence ([_1]).',$resUrl).'</span>');
 4561: 	my ($symb)=&get_symb($request);
 4562: 	$request->print(&show_grading_menu_form($symb));
 4563: 	return; 
 4564:     }
 4565:     my $iterator = $navmap->getIterator($map->map_start(),
 4566: 					$map->map_finish());
 4567: 
 4568:     my $studentTable=
 4569: 	&Apache::loncommon::start_data_table().
 4570: 	&Apache::loncommon::start_data_table_header_row().
 4571: 	'<th align="center">&nbsp;'.&mt('Prob.').'&nbsp;</th>'.
 4572: 	'<th>&nbsp;'.&mt('Title').'&nbsp;</th>'.
 4573: 	'<th>&nbsp;'.&mt('Previous Score').'&nbsp;</th>'.
 4574: 	'<th>&nbsp;'.&mt('New Score').'&nbsp;</th>'.
 4575: 	&Apache::loncommon::end_data_table_header_row();
 4576: 
 4577:     $iterator->next(); # skip the first BEGIN_MAP
 4578:     my $curRes = $iterator->next(); # for "current resource"
 4579:     my ($depth,$question,$prob,$changeflag)= (1,1,1,0);
 4580:     while ($depth > 0) {
 4581:         if($curRes == $iterator->BEGIN_MAP) { $depth++; }
 4582:         if($curRes == $iterator->END_MAP) { $depth--; }
 4583: 
 4584:         if (ref($curRes) && $curRes->is_problem()) {
 4585: 	    my $parts = $curRes->parts();
 4586:             my $title = $curRes->compTitle();
 4587: 	    my $symbx = $curRes->symb();
 4588: 	    $studentTable.=
 4589: 		&Apache::loncommon::start_data_table_row().
 4590: 		'<td align="center" valign="top" >'.$prob.
 4591: 		(scalar(@{$parts}) == 1 ? '' 
 4592:                                         : '<br />('.&mt('[quant,_1,&nbsp;part]',scalar(@{$parts}))
 4593: 		.')').'</td>';
 4594: 	    $studentTable.='<td valign="top">&nbsp;<b>'.$title.'</b>&nbsp;</td>';
 4595: 
 4596: 	    my %newrecord=();
 4597: 	    my @displayPts=();
 4598:             my %aggregate = ();
 4599:             my $aggregateflag = 0;
 4600: 	    foreach my $partid (@{$parts}) {
 4601: 		my $newpts = $env{'form.GD_BOX'.$question.'_'.$partid};
 4602: 		my $oldpts = $env{'form.oldpts'.$question.'_'.$partid};
 4603: 
 4604: 		my $wgt = $env{'form.WGT'.$question.'_'.$partid} != 0 ? 
 4605: 		    $env{'form.WGT'.$question.'_'.$partid} : 1;
 4606: 		my $partial = $newpts/$wgt;
 4607: 		my $score;
 4608: 		if ($partial > 0) {
 4609: 		    $score = 'correct_by_override';
 4610: 		} elsif ($newpts ne '') { #empty is taken as 0
 4611: 		    $score = 'incorrect_by_override';
 4612: 		}
 4613: 		my $dropMenu = $env{'form.GD_SEL'.$question.'_'.$partid};
 4614: 		if ($dropMenu eq 'excused') {
 4615: 		    $partial = '';
 4616: 		    $score = 'excused';
 4617: 		} elsif ($dropMenu eq 'reset status'
 4618: 			 && $env{'form.solved'.$question.'_'.$partid} ne '') { #update only if previous record exists
 4619: 		    $newrecord{'resource.'.$partid.'.tries'} = 0;
 4620: 		    $newrecord{'resource.'.$partid.'.solved'} = '';
 4621: 		    $newrecord{'resource.'.$partid.'.award'} = '';
 4622: 		    $newrecord{'resource.'.$partid.'.awarded'} = 0;
 4623: 		    $newrecord{'resource.'.$partid.'.regrader'} = "$env{'user.name'}:$env{'user.domain'}";
 4624: 		    $changeflag++;
 4625: 		    $newpts = '';
 4626:                     
 4627:                     my $aggtries =  $env{'form.aggtries'.$question.'_'.$partid};
 4628:                     my $totaltries = $env{'form.totaltries'.$question.'_'.$partid};
 4629:                     my $solvedstatus = $env{'form.solved'.$question.'_'.$partid};
 4630:                     if ($aggtries > 0) {
 4631:                         &decrement_aggs($symbx,$partid,\%aggregate,$aggtries,$totaltries,$solvedstatus);
 4632:                         $aggregateflag = 1;
 4633:                     }
 4634: 		}
 4635: 		my $display_part=&get_display_part($partid,$curRes->symb());
 4636: 		my $oldstatus = $env{'form.solved'.$question.'_'.$partid};
 4637: 		$displayPts[0].='&nbsp;<b>'.&mt('Part').':</b> '.$display_part.' = '.
 4638: 		    (($oldstatus eq 'excused') ? 'excused' : $oldpts).
 4639: 		    '&nbsp;<br />';
 4640: 		$displayPts[1].='&nbsp;<b>'.&mt('Part').':</b> '.$display_part.' = '.
 4641: 		     (($score eq 'excused') ? 'excused' : $newpts).
 4642: 		    '&nbsp;<br />';
 4643: 		$question++;
 4644: 		next if ($dropMenu eq 'reset status' || ($newpts eq $oldpts && $score ne 'excused'));
 4645: 
 4646: 		$newrecord{'resource.'.$partid.'.awarded'}  = $partial if $partial ne '';
 4647: 		$newrecord{'resource.'.$partid.'.solved'}   = $score if $score ne '';
 4648: 		$newrecord{'resource.'.$partid.'.regrader'} = "$env{'user.name'}:$env{'user.domain'}"
 4649: 		    if (scalar(keys(%newrecord)) > 0);
 4650: 
 4651: 		$changeflag++;
 4652: 	    }
 4653: 	    if (scalar(keys(%newrecord)) > 0) {
 4654: 		my %record = 
 4655: 		    &Apache::lonnet::restore($symbx,$env{'request.course.id'},
 4656: 					     $udom,$uname);
 4657: 
 4658: 		if (&Apache::lonnet::validCODE($env{'form.CODE'})) {
 4659: 		    $newrecord{'resource.CODE'} = $env{'form.CODE'};
 4660: 		} elsif (&Apache::lonnet::validCODE($record{'resource.CODE'})) {
 4661: 		    $newrecord{'resource.CODE'} = '';
 4662: 		}
 4663: 		&Apache::lonnet::cstore(\%newrecord,$symbx,$env{'request.course.id'},
 4664: 					$udom,$uname);
 4665: 		%record = &Apache::lonnet::restore($symbx,
 4666: 						   $env{'request.course.id'},
 4667: 						   $udom,$uname);
 4668: 		&check_and_remove_from_queue($parts,\%record,undef,$symbx,
 4669: 					     $cdom,$cnum,$udom,$uname);
 4670: 	    }
 4671: 	    
 4672:             if ($aggregateflag) {
 4673:                 &Apache::lonnet::cinc('nohist_resourcetracker',\%aggregate,
 4674:                       $env{'course.'.$env{'request.course.id'}.'.domain'},
 4675:                       $env{'course.'.$env{'request.course.id'}.'.num'});
 4676:             }
 4677: 
 4678: 	    $studentTable.='<td valign="top">'.$displayPts[0].'</td>'.
 4679: 		'<td valign="top">'.$displayPts[1].'</td>'.
 4680: 		&Apache::loncommon::end_data_table_row();
 4681: 
 4682: 	    $prob++;
 4683: 	}
 4684:         $curRes = $iterator->next();
 4685:     }
 4686: 
 4687:     $studentTable.=&Apache::loncommon::end_data_table();
 4688:     $studentTable.=&show_grading_menu_form($env{'form.symb'});
 4689:     my $grademsg=($changeflag == 0 ? &mt('No score was changed or updated.') :
 4690: 		  &mt('The scores were changed for [quant,_1,problem].',
 4691: 		  $changeflag));
 4692:     $request->print($grademsg.$studentTable);
 4693: 
 4694:     return '';
 4695: }
 4696: 
 4697: #-------- end of section for handling grading by page/sequence ---------
 4698: #
 4699: #-------------------------------------------------------------------
 4700: 
 4701: #--------------------Scantron Grading-----------------------------------
 4702: #
 4703: #------ start of section for handling grading by page/sequence ---------
 4704: 
 4705: =pod
 4706: 
 4707: =head1 Bubble sheet grading routines
 4708: 
 4709:   For this documentation:
 4710: 
 4711:    'scanline' refers to the full line of characters
 4712:    from the file that we are parsing that represents one entire sheet
 4713: 
 4714:    'bubble line' refers to the data
 4715:    representing the line of bubbles that are on the physical bubble sheet
 4716: 
 4717: 
 4718: The overall process is that a scanned in bubble sheet data is uploaded
 4719: into a course. When a user wants to grade, they select a
 4720: sequence/folder of resources, a file of bubble sheet info, and pick
 4721: one of the predefined configurations for what each scanline looks
 4722: like.
 4723: 
 4724: Next each scanline is checked for any errors of either 'missing
 4725: bubbles' (it's an error because it may have been mis-scanned
 4726: because too light bubbling), 'double bubble' (each bubble line should
 4727: have no more that one letter picked), invalid or duplicated CODE,
 4728: invalid student/employee ID
 4729: 
 4730: If the CODE option is used that determines the randomization of the
 4731: homework problems, either way the student/employee ID is looked up into a
 4732: username:domain.
 4733: 
 4734: During the validation phase the instructor can choose to skip scanlines. 
 4735: 
 4736: After the validation phase, there are now 3 bubble sheet files
 4737: 
 4738:   scantron_original_filename (unmodified original file)
 4739:   scantron_corrected_filename (file where the corrected information has replaced the original information)
 4740:   scantron_skipped_filename (contains the exact text of scanlines that where skipped)
 4741: 
 4742: Also there is a separate hash nohist_scantrondata that contains extra
 4743: correction information that isn't representable in the bubble sheet
 4744: file (see &scantron_getfile() for more information)
 4745: 
 4746: After all scanlines are either valid, marked as valid or skipped, then
 4747: foreach line foreach problem in the picked sequence, an ssi request is
 4748: made that simulates a user submitting their selected letter(s) against
 4749: the homework problem.
 4750: 
 4751: =over 4
 4752: 
 4753: 
 4754: 
 4755: =item defaultFormData
 4756: 
 4757:   Returns html hidden inputs used to hold context/default values.
 4758: 
 4759:  Arguments:
 4760:   $symb - $symb of the current resource 
 4761: 
 4762: =cut
 4763: 
 4764: sub defaultFormData {
 4765:     my ($symb)=@_;
 4766:     return '<input type="hidden" name="symb"    value="'.&Apache::lonenc::check_encrypt($symb).'" />'."\n".
 4767:      '<input type="hidden" name="saveState" value="'.$env{'form.saveState'}.'" />'."\n".
 4768:      '<input type="hidden" name="probTitle" value="'.$env{'form.probTitle'}.'" />'."\n";
 4769: }
 4770: 
 4771: 
 4772: =pod 
 4773: 
 4774: =item getSequenceDropDown
 4775: 
 4776:    Return html dropdown of possible sequences to grade
 4777:  
 4778:  Arguments:
 4779:    $symb - $symb of the current resource 
 4780: 
 4781: =cut
 4782: 
 4783: sub getSequenceDropDown {
 4784:     my ($symb)=@_;
 4785:     my $result='<select name="selectpage">'."\n";
 4786:     my ($titles,$symbx) = &getSymbMap();
 4787:     my ($curpage)=&Apache::lonnet::decode_symb($symb); 
 4788:     my $ctr=0;
 4789:     foreach (@$titles) {
 4790: 	my ($minder,$showtitle) = ($_ =~ /(\d+)\.(.*)/);
 4791: 	$result.='<option value="'.$$symbx{$_}.'" '.
 4792: 	    ($$symbx{$_} =~ /$curpage$/ ? 'selected="selected"' : '').
 4793: 	    '>'.$showtitle.'</option>'."\n";
 4794: 	$ctr++;
 4795:     }
 4796:     $result.= '</select>';
 4797:     return $result;
 4798: }
 4799: 
 4800: my %bubble_lines_per_response;     # no. bubble lines for each response.
 4801:                                    # key is zero-based index - 0, 1, 2 ...
 4802: 
 4803: my %first_bubble_line;             # First bubble line no. for each bubble.
 4804: 
 4805: my %subdivided_bubble_lines;       # no. bubble lines for optionresponse, 
 4806:                                    # matchresponse or rankresponse, where 
 4807:                                    # an individual response can have multiple 
 4808:                                    # lines
 4809: 
 4810: my %responsetype_per_response;     # responsetype for each response
 4811: 
 4812: # Save and restore the bubble lines array to the form env.
 4813: 
 4814: 
 4815: sub save_bubble_lines {
 4816:     foreach my $line (keys(%bubble_lines_per_response)) {
 4817: 	$env{"form.scantron.bubblelines.$line"}  = $bubble_lines_per_response{$line};
 4818: 	$env{"form.scantron.first_bubble_line.$line"} =
 4819: 	    $first_bubble_line{$line};
 4820:         $env{"form.scantron.sub_bubblelines.$line"} = 
 4821:             $subdivided_bubble_lines{$line};
 4822:         $env{"form.scantron.responsetype.$line"} =
 4823:             $responsetype_per_response{$line};
 4824:     }
 4825: }
 4826: 
 4827: 
 4828: sub restore_bubble_lines {
 4829:     my $line = 0;
 4830:     %bubble_lines_per_response = ();
 4831:     while ($env{"form.scantron.bubblelines.$line"}) {
 4832: 	my $value = $env{"form.scantron.bubblelines.$line"};
 4833: 	$bubble_lines_per_response{$line} = $value;
 4834: 	$first_bubble_line{$line}  =
 4835: 	    $env{"form.scantron.first_bubble_line.$line"};
 4836:         $subdivided_bubble_lines{$line} =
 4837:             $env{"form.scantron.sub_bubblelines.$line"};
 4838:         $responsetype_per_response{$line} =
 4839:             $env{"form.scantron.responsetype.$line"};
 4840: 	$line++;
 4841:     }
 4842: }
 4843: 
 4844: #  Given the parsed scanline, get the response for 
 4845: #  'answer' number n:
 4846: 
 4847: sub get_response_bubbles {
 4848:     my ($parsed_line, $response)  = @_;
 4849: 
 4850:     my $bubble_line = $first_bubble_line{$response-1} +1;
 4851:     my $bubble_lines= $bubble_lines_per_response{$response-1};
 4852:     
 4853:     my $selected = "";
 4854: 
 4855:     for (my $bline = 0; $bline < $bubble_lines; $bline++) {
 4856: 	$selected .= $$parsed_line{"scantron.$bubble_line.answer"}.":";
 4857: 	$bubble_line++;
 4858:     }
 4859:     return $selected;
 4860: }
 4861: 
 4862: =pod 
 4863: 
 4864: =item scantron_filenames
 4865: 
 4866:    Returns a list of the scantron files in the current course 
 4867: 
 4868: =cut
 4869: 
 4870: sub scantron_filenames {
 4871:     my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'};
 4872:     my $cname=$env{'course.'.$env{'request.course.id'}.'.num'};
 4873:     my $getpropath = 1;
 4874:     my @files=&Apache::lonnet::dirlist('userfiles',$cdom,$cname,
 4875:                                        $getpropath);
 4876:     my @possiblenames;
 4877:     foreach my $filename (sort(@files)) {
 4878: 	($filename)=split(/&/,$filename);
 4879: 	if ($filename!~/^scantron_orig_/) { next ; }
 4880: 	$filename=~s/^scantron_orig_//;
 4881: 	push(@possiblenames,$filename);
 4882:     }
 4883:     return @possiblenames;
 4884: }
 4885: 
 4886: =pod 
 4887: 
 4888: =item scantron_uploads
 4889: 
 4890:    Returns  html drop-down list of scantron files in current course.
 4891: 
 4892:  Arguments:
 4893:    $file2grade - filename to set as selected in the dropdown
 4894: 
 4895: =cut
 4896: 
 4897: sub scantron_uploads {
 4898:     my ($file2grade) = @_;
 4899:     my $result=	'<select name="scantron_selectfile">';
 4900:     $result.="<option></option>";
 4901:     foreach my $filename (sort(&scantron_filenames())) {
 4902: 	$result.="<option".($filename eq $file2grade ? ' selected="selected"':'').">$filename</option>\n";
 4903:     }
 4904:     $result.="</select>";
 4905:     return $result;
 4906: }
 4907: 
 4908: =pod 
 4909: 
 4910: =item scantron_scantab
 4911: 
 4912:   Returns html drop down of the scantron formats in the scantronformat.tab
 4913:   file.
 4914: 
 4915: =cut
 4916: 
 4917: sub scantron_scantab {
 4918:     my $result='<select name="scantron_format">'."\n";
 4919:     $result.='<option></option>'."\n";
 4920:     my @lines = &get_scantronformat_file();
 4921:     if (@lines > 0) {
 4922:         foreach my $line (@lines) {
 4923:             next if (($line =~ /^\#/) || ($line eq ''));
 4924: 	    my ($name,$descrip)=split(/:/,$line);
 4925: 	    $result.='<option value="'.$name.'">'.$descrip.'</option>'."\n";
 4926:         }
 4927:     }
 4928:     $result.='</select>'."\n";
 4929:     return $result;
 4930: }
 4931: 
 4932: =pod
 4933: 
 4934: =item get_scantronformat_file
 4935: 
 4936:   Returns an array containing lines from the scantron format file for
 4937:   the domain of the course.
 4938: 
 4939:   If a url for a custom.tab file is listed in domain's configuration.db, 
 4940:   lines are from this file.
 4941: 
 4942:   Otherwise, if a default.tab has been published in RES space by the 
 4943:   domainconfig user, lines are from this file.
 4944: 
 4945:   Otherwise, fall back to getting lines from the legacy file on the
 4946:   local server:  /home/httpd/lonTabs/default_scantronformat.tab    
 4947: 
 4948: =cut
 4949: 
 4950: sub get_scantronformat_file {
 4951:     my $cdom= $env{'course.'.$env{'request.course.id'}.'.domain'};
 4952:     my %domconfig = &Apache::lonnet::get_dom('configuration',['scantron'],$cdom);
 4953:     my $gottab = 0;
 4954:     my @lines;
 4955:     if (ref($domconfig{'scantron'}) eq 'HASH') {
 4956:         if ($domconfig{'scantron'}{'scantronformat'} ne '') {
 4957:             my $formatfile = &Apache::lonnet::getfile($Apache::lonnet::perlvar{'lonDocRoot'}.$domconfig{'scantron'}{'scantronformat'});
 4958:             if ($formatfile ne '-1') {
 4959:                 @lines = split("\n",$formatfile,-1);
 4960:                 $gottab = 1;
 4961:             }
 4962:         }
 4963:     }
 4964:     if (!$gottab) {
 4965:         my $confname = $cdom.'-domainconfig';
 4966:         my $default = $Apache::lonnet::perlvar{'lonDocRoot'}.'/res/'.$cdom.'/'.$confname.'/default.tab';
 4967:         my $formatfile =  &Apache::lonnet::getfile($default);
 4968:         if ($formatfile ne '-1') {
 4969:             @lines = split("\n",$formatfile,-1);
 4970:             $gottab = 1;
 4971:         }
 4972:     }
 4973:     if (!$gottab) {
 4974:         my @domains = &Apache::lonnet::current_machine_domains();
 4975:         if (grep(/^\Q$cdom\E$/,@domains)) {
 4976:             my $fh=Apache::File->new($Apache::lonnet::perlvar{'lonTabDir'}.'/scantronformat.tab');
 4977:             @lines = <$fh>;
 4978:             close($fh);
 4979:         } else {
 4980:             my $fh=Apache::File->new($Apache::lonnet::perlvar{'lonTabDir'}.'/default_scantronformat.tab');
 4981:             @lines = <$fh>;
 4982:             close($fh);
 4983:         }
 4984:     }
 4985:     return @lines;
 4986: }
 4987: 
 4988: =pod 
 4989: 
 4990: =item scantron_CODElist
 4991: 
 4992:   Returns html drop down of the saved CODE lists from current course,
 4993:   generated from earlier printings.
 4994: 
 4995: =cut
 4996: 
 4997: sub scantron_CODElist {
 4998:     my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'};
 4999:     my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'};
 5000:     my @names=&Apache::lonnet::getkeys('CODEs',$cdom,$cnum);
 5001:     my $namechoice='<option></option>';
 5002:     foreach my $name (sort {uc($a) cmp uc($b)} @names) {
 5003: 	if ($name =~ /^error: 2 /) { next; }
 5004: 	if ($name =~ /^type\0/) { next; }
 5005: 	$namechoice.='<option value="'.$name.'">'.$name.'</option>';
 5006:     }
 5007:     $namechoice='<select name="scantron_CODElist">'.$namechoice.'</select>';
 5008:     return $namechoice;
 5009: }
 5010: 
 5011: =pod 
 5012: 
 5013: =item scantron_CODEunique
 5014: 
 5015:   Returns the html for "Each CODE to be used once" radio.
 5016: 
 5017: =cut
 5018: 
 5019: sub scantron_CODEunique {
 5020:     my $result='<span class="LC_nobreak">
 5021:                  <label><input type="radio" name="scantron_CODEunique"
 5022:                         value="yes" checked="checked" />'.&mt('Yes').' </label>
 5023:                 </span>
 5024:                 <span class="LC_nobreak">
 5025:                  <label><input type="radio" name="scantron_CODEunique"
 5026:                         value="no" />'.&mt('No').' </label>
 5027:                 </span>';
 5028:     return $result;
 5029: }
 5030: 
 5031: =pod 
 5032: 
 5033: =item scantron_selectphase
 5034: 
 5035:   Generates the initial screen to start the bubble sheet process.
 5036:   Allows for - starting a grading run.
 5037:              - downloading existing scan data (original, corrected
 5038:                                                 or skipped info)
 5039: 
 5040:              - uploading new scan data
 5041: 
 5042:  Arguments:
 5043:   $r          - The Apache request object
 5044:   $file2grade - name of the file that contain the scanned data to score
 5045: 
 5046: =cut
 5047: 
 5048: sub scantron_selectphase {
 5049:     my ($r,$file2grade) = @_;
 5050:     my ($symb)=&get_symb($r);
 5051:     if (!$symb) {return '';}
 5052:     my $sequence_selector=&getSequenceDropDown($symb);
 5053:     my $default_form_data=&defaultFormData($symb);
 5054:     my $grading_menu_button=&show_grading_menu_form($symb);
 5055:     my $file_selector=&scantron_uploads($file2grade);
 5056:     my $format_selector=&scantron_scantab();
 5057:     my $CODE_selector=&scantron_CODElist();
 5058:     my $CODE_unique=&scantron_CODEunique();
 5059:     my $result;
 5060: 
 5061:     $ssi_error = 0;
 5062: 
 5063:     # Chunk of form to prompt for a file to grade and how:
 5064: 
 5065:     $result.= '
 5066:     <br />
 5067:     <form method="post" enctype="multipart/form-data" action="/adm/grades" name="scantron_process">
 5068:     <input type="hidden" name="command" value="scantron_warning" />
 5069:     '.$default_form_data.'
 5070:     '.&Apache::loncommon::start_data_table('LC_scantron_action').'
 5071:        '.&Apache::loncommon::start_data_table_header_row().'
 5072:             <th colspan="2">
 5073:               &nbsp;'.&mt('Specify file and which Folder/Sequence to grade').'
 5074:             </th>
 5075:        '.&Apache::loncommon::end_data_table_header_row().'
 5076:        '.&Apache::loncommon::start_data_table_row().'
 5077:             <td> '.&mt('Sequence to grade:').' </td><td> '.$sequence_selector.' </td>
 5078:        '.&Apache::loncommon::end_data_table_row().'
 5079:        '.&Apache::loncommon::start_data_table_row().'
 5080:             <td> '.&mt('Filename of bubblesheet data file:').' </td><td> '.$file_selector.' </td>
 5081:        '.&Apache::loncommon::end_data_table_row().'
 5082:        '.&Apache::loncommon::start_data_table_row().'
 5083:             <td> '.&mt('Format of bubblesheet data file:').' </td><td> '.$format_selector.' </td>
 5084:        '.&Apache::loncommon::end_data_table_row().'
 5085:        '.&Apache::loncommon::start_data_table_row().'
 5086:             <td> '.&mt('Saved CODEs to validate against:').' </td><td> '.$CODE_selector.' </td>
 5087:        '.&Apache::loncommon::end_data_table_row().'
 5088:        '.&Apache::loncommon::start_data_table_row().'
 5089:             <td> '.&mt('Each CODE is only to be used once:').'</td><td> '.$CODE_unique.' </td>
 5090:        '.&Apache::loncommon::end_data_table_row().'
 5091:        '.&Apache::loncommon::start_data_table_row().'
 5092: 	    <td> '.&mt('Options:').' </td>
 5093:             <td>
 5094: 	       <label><input type="checkbox" name="scantron_options_redo" value="redo_skipped"/> '.&mt('Do only previously skipped records').'</label> <br />
 5095:                <label><input type="checkbox" name="scantron_options_ignore" value="ignore_corrections"/> '.&mt('Remove all existing corrections').'</label> <br />
 5096:                <label><input type="checkbox" name="scantron_options_hidden" value="ignore_hidden"/> '.&mt('Skip hidden resources when grading').'</label>
 5097: 	    </td>
 5098:        '.&Apache::loncommon::end_data_table_row().'
 5099:        '.&Apache::loncommon::start_data_table_row().'
 5100:             <td colspan="2">
 5101:               <input type="submit" value="'.&mt('Grading: Validate Bubblesheet Records').'" />
 5102:             </td>
 5103:        '.&Apache::loncommon::end_data_table_row().'
 5104:     '.&Apache::loncommon::end_data_table().'
 5105:     </form>
 5106: ';
 5107:    
 5108:     $r->print($result);
 5109: 
 5110:     if (&Apache::lonnet::allowed('usc',$env{'request.role.domain'}) ||
 5111:         &Apache::lonnet::allowed('usc',$env{'request.course.id'})) {
 5112: 
 5113: 	# Chunk of form to prompt for a scantron file upload.
 5114: 
 5115:         $r->print('
 5116:     <br />
 5117:     '.&Apache::loncommon::start_data_table('LC_scantron_action').'
 5118:        '.&Apache::loncommon::start_data_table_header_row().'
 5119:             <th>
 5120:               &nbsp;'.&mt('Specify a bubblesheet data file to upload.').'
 5121:             </th>
 5122:        '.&Apache::loncommon::end_data_table_header_row().'
 5123:        '.&Apache::loncommon::start_data_table_row().'
 5124:             <td>
 5125: ');
 5126:     my $default_form_data=&defaultFormData(&get_symb($r,1));
 5127:     my $cdom= $env{'course.'.$env{'request.course.id'}.'.domain'};
 5128:     my $cnum= $env{'course.'.$env{'request.course.id'}.'.num'};
 5129:     $r->print('
 5130:               <script type="text/javascript" language="javascript">
 5131:     function checkUpload(formname) {
 5132: 	if (formname.upfile.value == "") {
 5133: 	    alert("'.&mt('Please use the browse button to select a file from your local directory.').'");
 5134: 	    return false;
 5135: 	}
 5136: 	formname.submit();
 5137:     }
 5138:               </script>
 5139: 
 5140:               <form enctype="multipart/form-data" action="/adm/grades" name="rules" method="post">
 5141:                 '.$default_form_data.'
 5142:                 <input name="courseid" type="hidden" value="'.$cnum.'" />
 5143:                 <input name="domainid" type="hidden" value="'.$cdom.'" />
 5144:                 <input name="command" value="scantronupload_save" type="hidden" />
 5145:                 '.&mt('File to upload: [_1]','<input type="file" name="upfile" size="50" />').'
 5146:                 <br />
 5147:                 <input type="button" onClick="javascript:checkUpload(this.form);" value="'.&mt('Upload Bubblesheet Data').'" />
 5148:               </form>
 5149: ');
 5150: 
 5151:         $r->print('
 5152:             </td>
 5153:        '.&Apache::loncommon::end_data_table_row().'
 5154:        '.&Apache::loncommon::end_data_table().'
 5155: ');
 5156:     }
 5157: 
 5158:     # Chunk of the form that prompts to view a scoring office file,
 5159:     # corrected file, skipped records in a file.
 5160: 
 5161:     $r->print('
 5162:    <br />
 5163:    <form action="/adm/grades" name="scantron_download">
 5164:      '.$default_form_data.'
 5165:      <input type="hidden" name="command" value="scantron_download" />
 5166:      '.&Apache::loncommon::start_data_table('LC_scantron_action').'
 5167:        '.&Apache::loncommon::start_data_table_header_row().'
 5168:               <th>
 5169:                 &nbsp;'.&mt('Download a scoring office file').'
 5170:               </th>
 5171:        '.&Apache::loncommon::end_data_table_header_row().'
 5172:        '.&Apache::loncommon::start_data_table_row().'
 5173:               <td> '.&mt('Filename of scoring office file: [_1]',$file_selector).' 
 5174:                 <br />
 5175:                 <input type="submit" value="'.&mt('Download: Show List of Associated Files').'" />
 5176:        '.&Apache::loncommon::end_data_table_row().'
 5177:      '.&Apache::loncommon::end_data_table().'
 5178:    </form>
 5179:    <br />
 5180: ');
 5181: 
 5182:     &Apache::lonpickcode::code_list($r,2);
 5183: 
 5184:     $r->print('<br /><form method="post" name="checkscantron">'.
 5185:              $default_form_data."\n".
 5186:              &Apache::loncommon::start_data_table('LC_scantron_action')."\n".
 5187:              &Apache::loncommon::start_data_table_header_row()."\n".
 5188:              '<th colspan="2">
 5189:               &nbsp;'.&mt('Review bubblesheet data and submissions for a previously graded folder/sequence')."\n".
 5190:              '</th>'."\n".
 5191:               &Apache::loncommon::end_data_table_header_row()."\n".
 5192:               &Apache::loncommon::start_data_table_row()."\n".
 5193:               '<td> '.&mt('Graded folder/sequence:').' </td>'."\n".
 5194:               '<td> '.$sequence_selector.' </td>'.
 5195:               &Apache::loncommon::end_data_table_row()."\n".
 5196:               &Apache::loncommon::start_data_table_row()."\n".
 5197:               '<td> '.&mt('Filename of scoring office file:').' </td>'."\n".
 5198:               '<td> '.$file_selector.' </td>'."\n".
 5199:               &Apache::loncommon::end_data_table_row()."\n".
 5200:               &Apache::loncommon::start_data_table_row()."\n".
 5201:               '<td> '.&mt('Format of data file:').' </td>'."\n".
 5202:               '<td> '.$format_selector.' </td>'."\n".
 5203:               &Apache::loncommon::end_data_table_row()."\n".
 5204:               &Apache::loncommon::start_data_table_row()."\n".
 5205:               '<td> '.&mt('Options').' </td>'."\n".
 5206:               '<td> <label><input type="checkbox" name="scantron_options_hidden" value="ignore_hidden"/> '.&mt('Skip hidden resources').'</label></td>'.
 5207:               &Apache::loncommon::end_data_table_row()."\n".
 5208:               &Apache::loncommon::start_data_table_row()."\n".
 5209:               '<td colspan="2">'."\n".
 5210:               '<input type="hidden" name="command" value="checksubmissions" />'."\n".
 5211:               '<input type="submit" value="'.&mt('Review Bubblesheet Data and Submission Records').'" />'."\n".
 5212:               '</td>'."\n".
 5213:               &Apache::loncommon::end_data_table_row()."\n".
 5214:               &Apache::loncommon::end_data_table()."\n".
 5215:               '</form><br />');
 5216:     $r->print($grading_menu_button);
 5217:     return;
 5218: }
 5219: 
 5220: =pod
 5221: 
 5222: =item get_scantron_config
 5223: 
 5224:    Parse and return the scantron configuration line selected as a
 5225:    hash of configuration file fields.
 5226: 
 5227:  Arguments:
 5228:     which - the name of the configuration to parse from the file.
 5229: 
 5230: 
 5231:  Returns:
 5232:             If the named configuration is not in the file, an empty
 5233:             hash is returned.
 5234:     a hash with the fields
 5235:       name         - internal name for the this configuration setup
 5236:       description  - text to display to operator that describes this config
 5237:       CODElocation - if 0 or the string 'none'
 5238:                           - no CODE exists for this config
 5239:                      if -1 || the string 'letter'
 5240:                           - a CODE exists for this config and is
 5241:                             a string of letters
 5242:                      Unsupported value (but planned for future support)
 5243:                           if a positive integer
 5244:                                - The CODE exists as the first n items from
 5245:                                  the question section of the form
 5246:                           if the string 'number'
 5247:                                - The CODE exists for this config and is
 5248:                                  a string of numbers
 5249:       CODEstart   - (only matter if a CODE exists) column in the line where
 5250:                      the CODE starts
 5251:       CODElength  - length of the CODE
 5252:       IDstart     - column where the student/employee ID starts
 5253:       IDlength    - length of the student/employee ID info
 5254:       Qstart      - column where the information from the bubbled
 5255:                     'questions' start
 5256:       Qlength     - number of columns comprising a single bubble line from
 5257:                     the sheet. (usually either 1 or 10)
 5258:       Qon         - either a single character representing the character used
 5259:                     to signal a bubble was chosen in the positional setup, or
 5260:                     the string 'letter' if the letter of the chosen bubble is
 5261:                     in the final, or 'number' if a number representing the
 5262:                     chosen bubble is in the file (1->A 0->J)
 5263:       Qoff        - the character used to represent that a bubble was
 5264:                     left blank
 5265:       PaperID     - if the scanning process generates a unique number for each
 5266:                     sheet scanned the column that this ID number starts in
 5267:       PaperIDlength - number of columns that comprise the unique ID number
 5268:                       for the sheet of paper
 5269:       FirstName   - column that the first name starts in
 5270:       FirstNameLength - number of columns that the first name spans
 5271:  
 5272:       LastName    - column that the last name starts in
 5273:       LastNameLength - number of columns that the last name spans
 5274: 
 5275: =cut
 5276: 
 5277: sub get_scantron_config {
 5278:     my ($which) = @_;
 5279:     my @lines = &get_scantronformat_file();
 5280:     my %config;
 5281:     #FIXME probably should move to XML it has already gotten a bit much now
 5282:     foreach my $line (@lines) {
 5283: 	my ($name,$descrip)=split(/:/,$line);
 5284: 	if ($name ne $which ) { next; }
 5285: 	chomp($line);
 5286: 	my @config=split(/:/,$line);
 5287: 	$config{'name'}=$config[0];
 5288: 	$config{'description'}=$config[1];
 5289: 	$config{'CODElocation'}=$config[2];
 5290: 	$config{'CODEstart'}=$config[3];
 5291: 	$config{'CODElength'}=$config[4];
 5292: 	$config{'IDstart'}=$config[5];
 5293: 	$config{'IDlength'}=$config[6];
 5294: 	$config{'Qstart'}=$config[7];
 5295:  	$config{'Qlength'}=$config[8];
 5296: 	$config{'Qoff'}=$config[9];
 5297: 	$config{'Qon'}=$config[10];
 5298: 	$config{'PaperID'}=$config[11];
 5299: 	$config{'PaperIDlength'}=$config[12];
 5300: 	$config{'FirstName'}=$config[13];
 5301: 	$config{'FirstNamelength'}=$config[14];
 5302: 	$config{'LastName'}=$config[15];
 5303: 	$config{'LastNamelength'}=$config[16];
 5304: 	last;
 5305:     }
 5306:     return %config;
 5307: }
 5308: 
 5309: =pod 
 5310: 
 5311: =item username_to_idmap
 5312: 
 5313:     creates a hash keyed by student/employee ID with values of the corresponding
 5314:     student username:domain.
 5315: 
 5316:   Arguments:
 5317: 
 5318:     $classlist - reference to the class list hash. This is a hash
 5319:                  keyed by student name:domain  whose elements are references
 5320:                  to arrays containing various chunks of information
 5321:                  about the student. (See loncoursedata for more info).
 5322: 
 5323:   Returns
 5324:     %idmap - the constructed hash
 5325: 
 5326: =cut
 5327: 
 5328: sub username_to_idmap {
 5329:     my ($classlist)= @_;
 5330:     my %idmap;
 5331:     foreach my $student (keys(%$classlist)) {
 5332: 	$idmap{$classlist->{$student}->[&Apache::loncoursedata::CL_ID]}=
 5333: 	    $student;
 5334:     }
 5335:     return %idmap;
 5336: }
 5337: 
 5338: =pod
 5339: 
 5340: =item scantron_fixup_scanline
 5341: 
 5342:    Process a requested correction to a scanline.
 5343: 
 5344:   Arguments:
 5345:     $scantron_config   - hash from &get_scantron_config()
 5346:     $scan_data         - hash of correction information 
 5347:                           (see &scantron_getfile())
 5348:     $line              - existing scanline
 5349:     $whichline         - line number of the passed in scanline
 5350:     $field             - type of change to process 
 5351:                          (either 
 5352:                           'ID'     -> correct the student/employee ID
 5353:                           'CODE'   -> correct the CODE
 5354:                           'answer' -> fixup the submitted answers)
 5355:     
 5356:    $args               - hash of additional info,
 5357:                           - 'ID' 
 5358:                                'newid' -> studentID to use in replacement
 5359:                                           of existing one
 5360:                           - 'CODE' 
 5361:                                'CODE_ignore_dup' - set to true if duplicates
 5362:                                                    should be ignored.
 5363: 	                       'CODE' - is new code or 'use_unfound'
 5364:                                         if the existing unfound code should
 5365:                                         be used as is
 5366:                           - 'answer'
 5367:                                'response' - new answer or 'none' if blank
 5368:                                'question' - the bubble line to change
 5369:                                'questionnum' - the question identifier,
 5370:                                                may include subquestion. 
 5371: 
 5372:   Returns:
 5373:     $line - the modified scanline
 5374: 
 5375:   Side effects: 
 5376:     $scan_data - may be updated
 5377: 
 5378: =cut
 5379: 
 5380: 
 5381: sub scantron_fixup_scanline {
 5382:     my ($scantron_config,$scan_data,$line,$whichline,$field,$args)=@_;
 5383:     if ($field eq 'ID') {
 5384: 	if (length($args->{'newid'}) > $$scantron_config{'IDlength'}) {
 5385: 	    return ($line,1,'New value too large');
 5386: 	}
 5387: 	if (length($args->{'newid'}) < $$scantron_config{'IDlength'}) {
 5388: 	    $args->{'newid'}=sprintf('%-'.$$scantron_config{'IDlength'}.'s',
 5389: 				     $args->{'newid'});
 5390: 	}
 5391: 	substr($line,$$scantron_config{'IDstart'}-1,
 5392: 	       $$scantron_config{'IDlength'})=$args->{'newid'};
 5393: 	if ($args->{'newid'}=~/^\s*$/) {
 5394: 	    &scan_data($scan_data,"$whichline.user",
 5395: 		       $args->{'username'}.':'.$args->{'domain'});
 5396: 	}
 5397:     } elsif ($field eq 'CODE') {
 5398: 	if ($args->{'CODE_ignore_dup'}) {
 5399: 	    &scan_data($scan_data,"$whichline.CODE_ignore_dup",'1');
 5400: 	}
 5401: 	&scan_data($scan_data,"$whichline.useCODE",'1');
 5402: 	if ($args->{'CODE'} ne 'use_unfound') {
 5403: 	    if (length($args->{'CODE'}) > $$scantron_config{'CODElength'}) {
 5404: 		return ($line,1,'New CODE value too large');
 5405: 	    }
 5406: 	    if (length($args->{'CODE'}) < $$scantron_config{'CODElength'}) {
 5407: 		$args->{'CODE'}=sprintf('%-'.$$scantron_config{'CODElength'}.'s',$args->{'CODE'});
 5408: 	    }
 5409: 	    substr($line,$$scantron_config{'CODEstart'}-1,
 5410: 		   $$scantron_config{'CODElength'})=$args->{'CODE'};
 5411: 	}
 5412:     } elsif ($field eq 'answer') {
 5413: 	my $length=$scantron_config->{'Qlength'};
 5414: 	my $off=$scantron_config->{'Qoff'};
 5415: 	my $on=$scantron_config->{'Qon'};
 5416: 	my $answer=${off}x$length;
 5417: 	if ($args->{'response'} eq 'none') {
 5418: 	    &scan_data($scan_data,
 5419: 		       "$whichline.no_bubble.".$args->{'questionnum'},'1');
 5420: 	} else {
 5421: 	    if ($on eq 'letter') {
 5422: 		my @alphabet=('A'..'Z');
 5423: 		$answer=$alphabet[$args->{'response'}];
 5424: 	    } elsif ($on eq 'number') {
 5425: 		$answer=$args->{'response'}+1;
 5426: 		if ($answer == 10) { $answer = '0'; }
 5427: 	    } else {
 5428: 		substr($answer,$args->{'response'},1)=$on;
 5429: 	    }
 5430: 	    &scan_data($scan_data,
 5431: 		       "$whichline.no_bubble.".$args->{'questionnum'},undef,'1');
 5432: 	}
 5433: 	my $where=$length*($args->{'question'}-1)+$scantron_config->{'Qstart'};
 5434: 	substr($line,$where-1,$length)=$answer;
 5435:     }
 5436:     return $line;
 5437: }
 5438: 
 5439: =pod
 5440: 
 5441: =item scan_data
 5442: 
 5443:     Edit or look up  an item in the scan_data hash.
 5444: 
 5445:   Arguments:
 5446:     $scan_data  - The hash (see scantron_getfile)
 5447:     $key        - shorthand of the key to edit (actual key is
 5448:                   scantronfilename_key).
 5449:     $data        - New value of the hash entry.
 5450:     $delete      - If true, the entry is removed from the hash.
 5451: 
 5452:   Returns:
 5453:     The new value of the hash table field (undefined if deleted).
 5454: 
 5455: =cut
 5456: 
 5457: 
 5458: sub scan_data {
 5459:     my ($scan_data,$key,$value,$delete)=@_;
 5460:     my $filename=$env{'form.scantron_selectfile'};
 5461:     if (defined($value)) {
 5462: 	$scan_data->{$filename.'_'.$key} = $value;
 5463:     }
 5464:     if ($delete) { delete($scan_data->{$filename.'_'.$key}); }
 5465:     return $scan_data->{$filename.'_'.$key};
 5466: }
 5467: 
 5468: # ----- These first few routines are general use routines.----
 5469: 
 5470: # Return the number of occurences of a pattern in a string.
 5471: 
 5472: sub occurence_count {
 5473:     my ($string, $pattern) = @_;
 5474: 
 5475:     my @matches = ($string =~ /$pattern/g);
 5476: 
 5477:     return scalar(@matches);
 5478: }
 5479: 
 5480: 
 5481: # Take a string known to have digits and convert all the
 5482: # digits into letters in the range J,A..I.
 5483: 
 5484: sub digits_to_letters {
 5485:     my ($input) = @_;
 5486: 
 5487:     my @alphabet = ('J', 'A'..'I');
 5488: 
 5489:     my @input    = split(//, $input);
 5490:     my $output ='';
 5491:     for (my $i = 0; $i < scalar(@input); $i++) {
 5492: 	if ($input[$i] =~ /\d/) {
 5493: 	    $output .= $alphabet[$input[$i]];
 5494: 	} else {
 5495: 	    $output .= $input[$i];
 5496: 	}
 5497:     }
 5498:     return $output;
 5499: }
 5500: 
 5501: =pod 
 5502: 
 5503: =item scantron_parse_scanline
 5504: 
 5505:   Decodes a scanline from the selected scantron file
 5506: 
 5507:  Arguments:
 5508:     line             - The text of the scantron file line to process
 5509:     whichline        - Line number
 5510:     scantron_config  - Hash describing the format of the scantron lines.
 5511:     scan_data        - Hash of extra information about the scanline
 5512:                        (see scantron_getfile for more information)
 5513:     just_header      - True if should not process question answers but only
 5514:                        the stuff to the left of the answers.
 5515:  Returns:
 5516:    Hash containing the result of parsing the scanline
 5517: 
 5518:    Keys are all proceeded by the string 'scantron.'
 5519: 
 5520:        CODE    - the CODE in use for this scanline
 5521:        useCODE - 1 if the CODE is invalid but it usage has been forced
 5522:                  by the operator
 5523:        CODE_ignore_dup - 1 if the CODE is a duplicated use when unique
 5524:                             CODEs were selected, but the usage has been
 5525:                             forced by the operator
 5526:        ID  - student/employee ID
 5527:        PaperID - if used, the ID number printed on the sheet when the 
 5528:                  paper was scanned
 5529:        FirstName - first name from the sheet
 5530:        LastName  - last name from the sheet
 5531: 
 5532:      if just_header was not true these key may also exist
 5533: 
 5534:        missingerror - a list of bubble ranges that are considered to be answers
 5535:                       to a single question that don't have any bubbles filled in.
 5536:                       Of the form questionnumber:firstbubblenumber:count.
 5537:        doubleerror  - a list of bubble ranges that are considered to be answers
 5538:                       to a single question that have more than one bubble filled in.
 5539:                       Of the form questionnumber::firstbubblenumber:count
 5540:    
 5541:                 In the above, count is the number of bubble responses in the
 5542:                 input line needed to represent the possible answers to the question.
 5543:                 e.g. a radioresponse with 15 choices in an answer sheet with 10 choices
 5544:                 per line would have count = 2.
 5545: 
 5546:        maxquest     - the number of the last bubble line that was parsed
 5547: 
 5548:        (<number> starts at 1)
 5549:        <number>.answer - zero or more letters representing the selected
 5550:                          letters from the scanline for the bubble line 
 5551:                          <number>.
 5552:                          if blank there was either no bubble or there where
 5553:                          multiple bubbles, (consult the keys missingerror and
 5554:                          doubleerror if this is an error condition)
 5555: 
 5556: =cut
 5557: 
 5558: sub scantron_parse_scanline {
 5559:     my ($line,$whichline,$scantron_config,$scan_data,$just_header)=@_;
 5560: 
 5561:     my %record;
 5562:     my $lastpos = $env{'form.scantron_maxbubble'}*$$scantron_config{'Qlength'};
 5563:     my $questions=substr($line,$$scantron_config{'Qstart'}-1,$lastpos);  # Answers
 5564:     my $data=substr($line,0,$$scantron_config{'Qstart'}-1);     # earlier stuff
 5565:     if (!($$scantron_config{'CODElocation'} eq 0 ||
 5566: 	  $$scantron_config{'CODElocation'} eq 'none')) {
 5567: 	if ($$scantron_config{'CODElocation'} < 0 ||
 5568: 	    $$scantron_config{'CODElocation'} eq 'letter' ||
 5569: 	    $$scantron_config{'CODElocation'} eq 'number') {
 5570: 	    $record{'scantron.CODE'}=substr($data,
 5571: 					    $$scantron_config{'CODEstart'}-1,
 5572: 					    $$scantron_config{'CODElength'});
 5573: 	    if (&scan_data($scan_data,"$whichline.useCODE")) {
 5574: 		$record{'scantron.useCODE'}=1;
 5575: 	    }
 5576: 	    if (&scan_data($scan_data,"$whichline.CODE_ignore_dup")) {
 5577: 		$record{'scantron.CODE_ignore_dup'}=1;
 5578: 	    }
 5579: 	} else {
 5580: 	    #FIXME interpret first N questions
 5581: 	}
 5582:     }
 5583:     $record{'scantron.ID'}=substr($data,$$scantron_config{'IDstart'}-1,
 5584: 				  $$scantron_config{'IDlength'});
 5585:     $record{'scantron.PaperID'}=
 5586: 	substr($data,$$scantron_config{'PaperID'}-1,
 5587: 	       $$scantron_config{'PaperIDlength'});
 5588:     $record{'scantron.FirstName'}=
 5589: 	substr($data,$$scantron_config{'FirstName'}-1,
 5590: 	       $$scantron_config{'FirstNamelength'});
 5591:     $record{'scantron.LastName'}=
 5592: 	substr($data,$$scantron_config{'LastName'}-1,
 5593: 	       $$scantron_config{'LastNamelength'});
 5594:     if ($just_header) { return \%record; }
 5595: 
 5596:     my @alphabet=('A'..'Z');
 5597:     my $questnum=0;
 5598:     my $ansnum  =1;		# Multiple 'answer lines'/question.
 5599: 
 5600:     chomp($questions);		# Get rid of any trailing \n.
 5601:     $questions =~ s/\r$//;      # Get rid of trailing \r too (MAC or Win uploads).
 5602:     while (length($questions)) {
 5603: 	my $answers_needed = $bubble_lines_per_response{$questnum};
 5604:         my $answer_length  = ($$scantron_config{'Qlength'} * $answers_needed)
 5605:                              || 1;
 5606:         $questnum++;
 5607:         my $quest_id = $questnum;
 5608:         my $currentquest = substr($questions,0,$answer_length);
 5609:         $questions       = substr($questions,$answer_length);
 5610:         if (length($currentquest) < $answer_length) { next; }
 5611: 
 5612:         if ($subdivided_bubble_lines{$questnum-1} =~ /,/) {
 5613:             my $subquestnum = 1;
 5614:             my $subquestions = $currentquest;
 5615:             my @subanswers_needed = 
 5616:                 split(/,/,$subdivided_bubble_lines{$questnum-1});  
 5617:             foreach my $subans (@subanswers_needed) {
 5618:                 my $subans_length =
 5619:                     ($$scantron_config{'Qlength'} * $subans)  || 1;
 5620:                 my $currsubquest = substr($subquestions,0,$subans_length);
 5621:                 $subquestions   = substr($subquestions,$subans_length);
 5622:                 $quest_id = "$questnum.$subquestnum";
 5623:                 if (($$scantron_config{'Qon'} eq 'letter') ||
 5624:                     ($$scantron_config{'Qon'} eq 'number')) {
 5625:                     $ansnum = &scantron_validator_lettnum($ansnum, 
 5626:                         $questnum,$quest_id,$subans,$currsubquest,$whichline,
 5627:                         \@alphabet,\%record,$scantron_config,$scan_data);
 5628:                 } else {
 5629:                     $ansnum = &scantron_validator_positional($ansnum,
 5630:                         $questnum,$quest_id,$subans,$currsubquest,$whichline,                        \@alphabet,\%record,$scantron_config,$scan_data);
 5631:                 }
 5632:                 $subquestnum ++;
 5633:             }
 5634:         } else {
 5635:             if (($$scantron_config{'Qon'} eq 'letter') ||
 5636:                 ($$scantron_config{'Qon'} eq 'number')) {
 5637:                 $ansnum = &scantron_validator_lettnum($ansnum,$questnum,
 5638:                     $quest_id,$answers_needed,$currentquest,$whichline,
 5639:                     \@alphabet,\%record,$scantron_config,$scan_data);
 5640:             } else {
 5641:                 $ansnum = &scantron_validator_positional($ansnum,$questnum,
 5642:                     $quest_id,$answers_needed,$currentquest,$whichline,
 5643:                     \@alphabet,\%record,$scantron_config,$scan_data);
 5644:             }
 5645:         }
 5646:     }
 5647:     $record{'scantron.maxquest'}=$questnum;
 5648:     return \%record;
 5649: }
 5650: 
 5651: sub scantron_validator_lettnum {
 5652:     my ($ansnum,$questnum,$quest_id,$answers_needed,$currquest,$whichline,
 5653:         $alphabet,$record,$scantron_config,$scan_data) = @_;
 5654: 
 5655:     # Qon 'letter' implies for each slot in currquest we have:
 5656:     #    ? or * for doubles, a letter in A-Z for a bubble, and
 5657:     #    about anything else (esp. a value of Qoff) for missing
 5658:     #    bubbles.
 5659:     #
 5660:     # Qon 'number' implies each slot gives a digit that indexes the
 5661:     #    bubbles filled, or Qoff, or a non-number for unbubbled lines,
 5662:     #    and * or ? for double bubbles on a single line.
 5663:     #
 5664: 
 5665:     my $matchon;
 5666:     if ($$scantron_config{'Qon'} eq 'letter') {
 5667:         $matchon = '[A-Z]';
 5668:     } elsif ($$scantron_config{'Qon'} eq 'number') {
 5669:         $matchon = '\d';
 5670:     }
 5671:     my $occurrences = 0;
 5672:     if (($responsetype_per_response{$questnum-1} eq 'essayresponse') ||
 5673:         ($responsetype_per_response{$questnum-1} eq 'formularesponse') ||
 5674:         ($responsetype_per_response{$questnum-1} eq 'stringresponse') ||
 5675:         ($responsetype_per_response{$questnum-1} eq 'imageresponse') ||
 5676:         ($responsetype_per_response{$questnum-1} eq 'reactionresponse') ||
 5677:         ($responsetype_per_response{$questnum-1} eq 'organicresponse')) {
 5678:         my @singlelines = split('',$currquest);
 5679:         foreach my $entry (@singlelines) {
 5680:             $occurrences = &occurence_count($entry,$matchon);
 5681:             if ($occurrences > 1) {
 5682:                 last;
 5683:             }
 5684:         } 
 5685:     } else {
 5686:         $occurrences = &occurence_count($currquest,$matchon); 
 5687:     }
 5688:     if (($currquest =~ /\?/ || $currquest =~ /\*/) || ($occurrences > 1)) {
 5689:         push(@{$record->{'scantron.doubleerror'}},$quest_id);
 5690:         for (my $ans=0; $ans<$answers_needed; $ans++) {
 5691:             my $bubble = substr($currquest,$ans,1);
 5692:             if ($bubble =~ /$matchon/ ) {
 5693:                 if ($$scantron_config{'Qon'} eq 'number') {
 5694:                     if ($bubble == 0) {
 5695:                         $bubble = 10; 
 5696:                     }
 5697:                     $record->{"scantron.$ansnum.answer"} = 
 5698:                         $alphabet->[$bubble-1];
 5699:                 } else {
 5700:                     $record->{"scantron.$ansnum.answer"} = $bubble;
 5701:                 }
 5702:             } else {
 5703:                 $record->{"scantron.$ansnum.answer"}='';
 5704:             }
 5705:             $ansnum++;
 5706:         }
 5707:     } elsif (!defined($currquest)
 5708:             || (&occurence_count($currquest, $$scantron_config{'Qoff'}) == length($currquest))
 5709:             || (&occurence_count($currquest,$matchon) == 0)) {
 5710:         for (my $ans=0; $ans<$answers_needed; $ans++ ) {
 5711:             $record->{"scantron.$ansnum.answer"}='';
 5712:             $ansnum++;
 5713:         }
 5714:         if (!&scan_data($scan_data,"$whichline.no_bubble.$quest_id")) {
 5715:             push(@{$record->{'scantron.missingerror'}},$quest_id);
 5716:         }
 5717:     } else {
 5718:         if ($$scantron_config{'Qon'} eq 'number') {
 5719:             $currquest = &digits_to_letters($currquest);            
 5720:         }
 5721:         for (my $ans=0; $ans<$answers_needed; $ans++) {
 5722:             my $bubble = substr($currquest,$ans,1);
 5723:             $record->{"scantron.$ansnum.answer"} = $bubble;
 5724:             $ansnum++;
 5725:         }
 5726:     }
 5727:     return $ansnum;
 5728: }
 5729: 
 5730: sub scantron_validator_positional {
 5731:     my ($ansnum,$questnum,$quest_id,$answers_needed,$currquest,
 5732:         $whichline,$alphabet,$record,$scantron_config,$scan_data) = @_;
 5733: 
 5734:     # Otherwise there's a positional notation;
 5735:     # each bubble line requires Qlength items, and there are filled in
 5736:     # bubbles for each case where there 'Qon' characters.
 5737:     #
 5738: 
 5739:     my @array=split($$scantron_config{'Qon'},$currquest,-1);
 5740: 
 5741:     # If the split only gives us one element.. the full length of the
 5742:     # answer string, no bubbles are filled in:
 5743: 
 5744:     if ($answers_needed eq '') {
 5745:         return;
 5746:     }
 5747: 
 5748:     if (length($array[0]) eq $$scantron_config{'Qlength'}*$answers_needed) {
 5749:         for (my $ans=0; $ans<$answers_needed; $ans++ ) {
 5750:             $record->{"scantron.$ansnum.answer"}='';
 5751:             $ansnum++;
 5752:         }
 5753:         if (!&scan_data($scan_data,"$whichline.no_bubble.$quest_id")) {
 5754:             push(@{$record->{"scantron.missingerror"}},$quest_id);
 5755:         }
 5756:     } elsif (scalar(@array) == 2) {
 5757:         my $location = length($array[0]);
 5758:         my $line_num = int($location / $$scantron_config{'Qlength'});
 5759:         my $bubble   = $alphabet->[$location % $$scantron_config{'Qlength'}];
 5760:         for (my $ans=0; $ans<$answers_needed; $ans++) {
 5761:             if ($ans eq $line_num) {
 5762:                 $record->{"scantron.$ansnum.answer"} = $bubble;
 5763:             } else {
 5764:                 $record->{"scantron.$ansnum.answer"} = ' ';
 5765:             }
 5766:             $ansnum++;
 5767:          }
 5768:     } else {
 5769:         #  If there's more than one instance of a bubble character
 5770:         #  That's a double bubble; with positional notation we can
 5771:         #  record all the bubbles filled in as well as the
 5772:         #  fact this response consists of multiple bubbles.
 5773:         #
 5774:         if (($responsetype_per_response{$questnum-1} eq 'essayresponse') ||
 5775:             ($responsetype_per_response{$questnum-1} eq 'formularesponse') ||
 5776:             ($responsetype_per_response{$questnum-1} eq 'stringresponse') ||
 5777:             ($responsetype_per_response{$questnum-1} eq 'imageresponse') ||
 5778:             ($responsetype_per_response{$questnum-1} eq 'reactionresponse') ||
 5779:             ($responsetype_per_response{$questnum-1} eq 'organicresponse')) {
 5780:             my $doubleerror = 0;
 5781:             while (($currquest >= $$scantron_config{'Qlength'}) && 
 5782:                    (!$doubleerror)) {
 5783:                my $currline = substr($currquest,0,$$scantron_config{'Qlength'});
 5784:                $currquest = substr($currquest,$$scantron_config{'Qlength'});
 5785:                my @currarray = split($$scantron_config{'Qon'},$currline,-1);
 5786:                if (length(@currarray) > 2) {
 5787:                    $doubleerror = 1;
 5788:                } 
 5789:             }
 5790:             if ($doubleerror) {
 5791:                 push(@{$record->{'scantron.doubleerror'}},$quest_id);
 5792:             }
 5793:         } else {
 5794:             push(@{$record->{'scantron.doubleerror'}},$quest_id);
 5795:         }
 5796:         my $item = $ansnum;
 5797:         for (my $ans=0; $ans<$answers_needed; $ans++) {
 5798:             $record->{"scantron.$item.answer"} = '';
 5799:             $item ++;
 5800:         }
 5801: 
 5802:         my @ans=@array;
 5803:         my $i=0;
 5804:         my $increment = 0;
 5805:         while ($#ans) {
 5806:             $i+=length($ans[0]) + $increment;
 5807:             my $line   = int($i/$$scantron_config{'Qlength'} + $ansnum);
 5808:             my $bubble = $i%$$scantron_config{'Qlength'};
 5809:             $record->{"scantron.$line.answer"}.=$alphabet->[$bubble];
 5810:             shift(@ans);
 5811:             $increment = 1;
 5812:         }
 5813:         $ansnum += $answers_needed;
 5814:     }
 5815:     return $ansnum;
 5816: }
 5817: 
 5818: =pod
 5819: 
 5820: =item scantron_add_delay
 5821: 
 5822:    Adds an error message that occurred during the grading phase to a
 5823:    queue of messages to be shown after grading pass is complete
 5824: 
 5825:  Arguments:
 5826:    $delayqueue  - arrary ref of hash ref of error messages
 5827:    $scanline    - the scanline that caused the error
 5828:    $errormesage - the error message
 5829:    $errorcode   - a numeric code for the error
 5830: 
 5831:  Side Effects:
 5832:    updates the $delayqueue to have a new hash ref of the error
 5833: 
 5834: =cut
 5835: 
 5836: sub scantron_add_delay {
 5837:     my ($delayqueue,$scanline,$errormessage,$errorcode)=@_;
 5838:     push(@$delayqueue,
 5839: 	 {'line' => $scanline, 'emsg' => $errormessage,
 5840: 	  'ecode' => $errorcode }
 5841: 	 );
 5842: }
 5843: 
 5844: =pod
 5845: 
 5846: =item scantron_find_student
 5847: 
 5848:    Finds the username for the current scanline
 5849: 
 5850:   Arguments:
 5851:    $scantron_record - hash result from scantron_parse_scanline
 5852:    $scan_data       - hash of correction information 
 5853:                       (see &scantron_getfile() form more information)
 5854:    $idmap           - hash from &username_to_idmap()
 5855:    $line            - number of current scanline
 5856:  
 5857:   Returns:
 5858:    Either 'username:domain' or undef if unknown
 5859: 
 5860: =cut
 5861: 
 5862: sub scantron_find_student {
 5863:     my ($scantron_record,$scan_data,$idmap,$line)=@_;
 5864:     my $scanID=$$scantron_record{'scantron.ID'};
 5865:     if ($scanID =~ /^\s*$/) {
 5866:  	return &scan_data($scan_data,"$line.user");
 5867:     }
 5868:     foreach my $id (keys(%$idmap)) {
 5869:  	if (lc($id) eq lc($scanID)) {
 5870:  	    return $$idmap{$id};
 5871:  	}
 5872:     }
 5873:     return undef;
 5874: }
 5875: 
 5876: =pod
 5877: 
 5878: =item scantron_filter
 5879: 
 5880:    Filter sub for lonnavmaps, filters out hidden resources if ignore
 5881:    hidden resources was selected
 5882: 
 5883: =cut
 5884: 
 5885: sub scantron_filter {
 5886:     my ($curres)=@_;
 5887: 
 5888:     if (ref($curres) && $curres->is_problem()) {
 5889: 	# if the user has asked to not have either hidden
 5890: 	# or 'randomout' controlled resources to be graded
 5891: 	# don't include them
 5892: 	if ($env{'form.scantron_options_hidden'} eq 'ignore_hidden'
 5893: 	    && $curres->randomout) {
 5894: 	    return 0;
 5895: 	}
 5896: 	return 1;
 5897:     }
 5898:     return 0;
 5899: }
 5900: 
 5901: =pod
 5902: 
 5903: =item scantron_process_corrections
 5904: 
 5905:    Gets correction information out of submitted form data and corrects
 5906:    the scanline
 5907: 
 5908: =cut
 5909: 
 5910: sub scantron_process_corrections {
 5911:     my ($r) = @_;
 5912:     my %scantron_config=&get_scantron_config($env{'form.scantron_format'});
 5913:     my ($scanlines,$scan_data)=&scantron_getfile();
 5914:     my $classlist=&Apache::loncoursedata::get_classlist();
 5915:     my $which=$env{'form.scantron_line'};
 5916:     my $line=&scantron_get_line($scanlines,$scan_data,$which);
 5917:     my ($skip,$err,$errmsg);
 5918:     if ($env{'form.scantron_skip_record'}) {
 5919: 	$skip=1;
 5920:     } elsif ($env{'form.scantron_corrections'} =~ /^(duplicate|incorrect)ID$/) {
 5921: 	my $newstudent=$env{'form.scantron_username'}.':'.
 5922: 	    $env{'form.scantron_domain'};
 5923: 	my $newid=$classlist->{$newstudent}->[&Apache::loncoursedata::CL_ID];
 5924: 	($line,$err,$errmsg)=
 5925: 	    &scantron_fixup_scanline(\%scantron_config,$scan_data,$line,$which,
 5926: 				     'ID',{'newid'=>$newid,
 5927: 				    'username'=>$env{'form.scantron_username'},
 5928: 				    'domain'=>$env{'form.scantron_domain'}});
 5929:     } elsif ($env{'form.scantron_corrections'} =~ /^(duplicate|incorrect)CODE$/) {
 5930: 	my $resolution=$env{'form.scantron_CODE_resolution'};
 5931: 	my $newCODE;
 5932: 	my %args;
 5933: 	if      ($resolution eq 'use_unfound') {
 5934: 	    $newCODE='use_unfound';
 5935: 	} elsif ($resolution eq 'use_found') {
 5936: 	    $newCODE=$env{'form.scantron_CODE_selectedvalue'};
 5937: 	} elsif ($resolution eq 'use_typed') {
 5938: 	    $newCODE=$env{'form.scantron_CODE_newvalue'};
 5939: 	} elsif ($resolution =~ /^use_closest_(\d+)/) {
 5940: 	    $newCODE=$env{"form.scantron_CODE_closest_$1"};
 5941: 	}
 5942: 	if ($env{'form.scantron_corrections'} eq 'duplicateCODE') {
 5943: 	    $args{'CODE_ignore_dup'}=1;
 5944: 	}
 5945: 	$args{'CODE'}=$newCODE;
 5946: 	($line,$err,$errmsg)=
 5947: 	    &scantron_fixup_scanline(\%scantron_config,$scan_data,$line,$which,
 5948: 				     'CODE',\%args);
 5949:     } elsif ($env{'form.scantron_corrections'} =~ /^(missing|double)bubble$/) {
 5950: 	foreach my $question (split(',',$env{'form.scantron_questions'})) {
 5951: 	    ($line,$err,$errmsg)=
 5952: 		&scantron_fixup_scanline(\%scantron_config,$scan_data,$line,
 5953: 					 $which,'answer',
 5954: 					 { 'question'=>$question,
 5955: 		      		   'response'=>$env{"form.scantron_correct_Q_$question"},
 5956:                                    'questionnum'=>$env{"form.scantron_questionnum_Q_$question"}});
 5957: 	    if ($err) { last; }
 5958: 	}
 5959:     }
 5960:     if ($err) {
 5961: 	$r->print("<span class=\"LC_warning\">Unable to accept last correction, an error occurred :$errmsg:</span>");
 5962:     } else {
 5963: 	&scantron_put_line($scanlines,$scan_data,$which,$line,$skip);
 5964: 	&scantron_putfile($scanlines,$scan_data);
 5965:     }
 5966: }
 5967: 
 5968: =pod
 5969: 
 5970: =item reset_skipping_status
 5971: 
 5972:    Forgets the current set of remember skipped scanlines (and thus
 5973:    reverts back to considering all lines in the
 5974:    scantron_skipped_<filename> file)
 5975: 
 5976: =cut
 5977: 
 5978: sub reset_skipping_status {
 5979:     my ($scanlines,$scan_data)=&scantron_getfile();
 5980:     &scan_data($scan_data,'remember_skipping',undef,1);
 5981:     &scantron_putfile(undef,$scan_data);
 5982: }
 5983: 
 5984: =pod
 5985: 
 5986: =item start_skipping
 5987: 
 5988:    Marks a scanline to be skipped. 
 5989: 
 5990: =cut
 5991: 
 5992: sub start_skipping {
 5993:     my ($scan_data,$i)=@_;
 5994:     my %remembered=split(':',&scan_data($scan_data,'remember_skipping'));
 5995:     if ($env{'form.scantron_options_redo'} =~ /^redo_/) {
 5996: 	$remembered{$i}=2;
 5997:     } else {
 5998: 	$remembered{$i}=1;
 5999:     }
 6000:     &scan_data($scan_data,'remember_skipping',join(':',%remembered));
 6001: }
 6002: 
 6003: =pod
 6004: 
 6005: =item should_be_skipped
 6006: 
 6007:    Checks whether a scanline should be skipped.
 6008: 
 6009: =cut
 6010: 
 6011: sub should_be_skipped {
 6012:     my ($scanlines,$scan_data,$i)=@_;
 6013:     if ($env{'form.scantron_options_redo'} !~ /^redo_/) {
 6014: 	# not redoing old skips
 6015: 	if ($scanlines->{'skipped'}[$i]) { return 1; }
 6016: 	return 0;
 6017:     }
 6018:     my %remembered=split(':',&scan_data($scan_data,'remember_skipping'));
 6019: 
 6020:     if (exists($remembered{$i}) && $remembered{$i} != 2 ) {
 6021: 	return 0;
 6022:     }
 6023:     return 1;
 6024: }
 6025: 
 6026: =pod
 6027: 
 6028: =item remember_current_skipped
 6029: 
 6030:    Discovers what scanlines are in the scantron_skipped_<filename>
 6031:    file and remembers them into scan_data for later use.
 6032: 
 6033: =cut
 6034: 
 6035: sub remember_current_skipped {
 6036:     my ($scanlines,$scan_data)=&scantron_getfile();
 6037:     my %to_remember;
 6038:     for (my $i=0;$i<=$scanlines->{'count'};$i++) {
 6039: 	if ($scanlines->{'skipped'}[$i]) {
 6040: 	    $to_remember{$i}=1;
 6041: 	}
 6042:     }
 6043: 
 6044:     &scan_data($scan_data,'remember_skipping',join(':',%to_remember));
 6045:     &scantron_putfile(undef,$scan_data);
 6046: }
 6047: 
 6048: =pod
 6049: 
 6050: =item check_for_error
 6051: 
 6052:     Checks if there was an error when attempting to remove a specific
 6053:     scantron_.. bubble sheet data file. Prints out an error if
 6054:     something went wrong.
 6055: 
 6056: =cut
 6057: 
 6058: sub check_for_error {
 6059:     my ($r,$result)=@_;
 6060:     if ($result ne 'ok' && $result ne 'not_found' ) {
 6061: 	$r->print(&mt("An error occurred ([_1]) when trying to remove the existing corrections.",$result));
 6062:     }
 6063: }
 6064: 
 6065: =pod
 6066: 
 6067: =item scantron_warning_screen
 6068: 
 6069:    Interstitial screen to make sure the operator has selected the
 6070:    correct options before we start the validation phase.
 6071: 
 6072: =cut
 6073: 
 6074: sub scantron_warning_screen {
 6075:     my ($button_text)=@_;
 6076:     my $title=&Apache::lonnet::gettitle($env{'form.selectpage'});
 6077:     my %scantron_config=&get_scantron_config($env{'form.scantron_format'});
 6078:     my $CODElist;
 6079:     if ($scantron_config{'CODElocation'} &&
 6080: 	$scantron_config{'CODEstart'} &&
 6081: 	$scantron_config{'CODElength'}) {
 6082: 	$CODElist=$env{'form.scantron_CODElist'};
 6083: 	if ($env{'form.scantron_CODElist'} eq '') { $CODElist='<span class="LC_warning">None</span>'; }
 6084: 	$CODElist=
 6085: 	    '<tr><td><b>'.&mt('List of CODES to validate against:').'</b></td><td><tt>'.
 6086: 	    $env{'form.scantron_CODElist'}.'</tt></td></tr>';
 6087:     }
 6088:     return ('
 6089: <p>
 6090: <span class="LC_warning">
 6091: '.&mt('Please double check the information below before clicking on \'[_1]\'',&mt($button_text)).'</span>
 6092: </p>
 6093: <table>
 6094: <tr><td><b>'.&mt('Sequence to be Graded:').'</b></td><td>'.$title.'</td></tr>
 6095: <tr><td><b>'.&mt('Data File that will be used:').'</b></td><td><tt>'.$env{'form.scantron_selectfile'}.'</tt></td></tr>
 6096: '.$CODElist.'
 6097: </table>
 6098: <br />
 6099: <p> '.&mt('If this information is correct, please click on \'[_1]\'.',&mt($button_text)).'</p>
 6100: <p> '.&mt('If something is incorrect, please click the \'Grading Menu\' button to start over.').'</p>
 6101: 
 6102: <br />
 6103: ');
 6104: }
 6105: 
 6106: =pod
 6107: 
 6108: =item scantron_do_warning
 6109: 
 6110:    Check if the operator has picked something for all required
 6111:    fields. Error out if something is missing.
 6112: 
 6113: =cut
 6114: 
 6115: sub scantron_do_warning {
 6116:     my ($r)=@_;
 6117:     my ($symb)=&get_symb($r);
 6118:     if (!$symb) {return '';}
 6119:     my $default_form_data=&defaultFormData($symb);
 6120:     $r->print(&scantron_form_start().$default_form_data);
 6121:     if ( $env{'form.selectpage'} eq '' ||
 6122: 	 $env{'form.scantron_selectfile'} eq '' ||
 6123: 	 $env{'form.scantron_format'} eq '' ) {
 6124: 	$r->print("<p>".&mt('You have forgetten to specify some information. Please go Back and try again.')."</p>");
 6125: 	if ( $env{'form.selectpage'} eq '') {
 6126: 	    $r->print('<p><span class="LC_error">'.&mt('You have not selected a Sequence to grade').'</span></p>');
 6127: 	} 
 6128: 	if ( $env{'form.scantron_selectfile'} eq '') {
 6129: 	    $r->print('<p><span class="LC_error">'.&mt('You have not selected a file that contains the student\'s response data.').'</span></p>');
 6130: 	} 
 6131: 	if ( $env{'form.scantron_format'} eq '') {
 6132: 	    $r->print('<p><span class="LC_error">'.&mt('You have not selected a the format of the student\'s response data.').'</span></p>');
 6133: 	} 
 6134:     } else {
 6135: 	my $warning=&scantron_warning_screen('Grading: Validate Records');
 6136: 	$r->print('
 6137: '.$warning.'
 6138: <input type="submit" name="submit" value="'.&mt('Grading: Validate Records').'" />
 6139: <input type="hidden" name="command" value="scantron_validate" />
 6140: ');
 6141:     }
 6142:     $r->print("</form><br />".&show_grading_menu_form($symb));
 6143:     return '';
 6144: }
 6145: 
 6146: =pod
 6147: 
 6148: =item scantron_form_start
 6149: 
 6150:     html hidden input for remembering all selected grading options
 6151: 
 6152: =cut
 6153: 
 6154: sub scantron_form_start {
 6155:     my ($max_bubble)=@_;
 6156:     my $result= <<SCANTRONFORM;
 6157: <form method="post" enctype="multipart/form-data" action="/adm/grades" name="scantronupload">
 6158:   <input type="hidden" name="selectpage" value="$env{'form.selectpage'}" />
 6159:   <input type="hidden" name="scantron_format" value="$env{'form.scantron_format'}" />
 6160:   <input type="hidden" name="scantron_selectfile" value="$env{'form.scantron_selectfile'}" />
 6161:   <input type="hidden" name="scantron_maxbubble" value="$max_bubble" />
 6162:   <input type="hidden" name="scantron_CODElist" value="$env{'form.scantron_CODElist'}" />
 6163:   <input type="hidden" name="scantron_CODEunique" value="$env{'form.scantron_CODEunique'}" />
 6164:   <input type="hidden" name="scantron_options_redo" value="$env{'form.scantron_options_redo'}" />
 6165:   <input type="hidden" name="scantron_options_ignore" value="$env{'form.scantron_options_ignore'}" />
 6166:   <input type="hidden" name="scantron_options_hidden" value="$env{'form.scantron_options_hidden'}" />
 6167: SCANTRONFORM
 6168: 
 6169:   my $line = 0;
 6170:     while (defined($env{"form.scantron.bubblelines.$line"})) {
 6171:        my $chunk =
 6172: 	   '<input type="hidden" name="scantron.bubblelines.'.$line.'" value="'.$env{"form.scantron.bubblelines.$line"}.'" />'."\n";
 6173:        $chunk .=
 6174: 	   '<input type="hidden" name="scantron.first_bubble_line.'.$line.'" value="'.$env{"form.scantron.first_bubble_line.$line"}.'" />'."\n";
 6175:        $chunk .= 
 6176:            '<input type="hidden" name="scantron.sub_bubblelines.'.$line.'" value="'.$env{"form.scantron.sub_bubblelines.$line"}.'" />'."\n";
 6177:        $chunk .=
 6178:            '<input type="hidden" name="scantron.responsetype.'.$line.'" value="'.$env{"form.scantron.responsetype.$line"}.'" />'."\n";
 6179:        $result .= $chunk;
 6180:        $line++;
 6181:    }
 6182:     return $result;
 6183: }
 6184: 
 6185: =pod
 6186: 
 6187: =item scantron_validate_file
 6188: 
 6189:     Dispatch routine for doing validation of a bubble sheet data file.
 6190: 
 6191:     Also processes any necessary information resets that need to
 6192:     occur before validation begins (ignore previous corrections,
 6193:     restarting the skipped records processing)
 6194: 
 6195: =cut
 6196: 
 6197: sub scantron_validate_file {
 6198:     my ($r) = @_;
 6199:     my ($symb)=&get_symb($r);
 6200:     if (!$symb) {return '';}
 6201:     my $default_form_data=&defaultFormData($symb);
 6202:     
 6203:     # do the detection of only doing skipped records first befroe we delete
 6204:     # them when doing the corrections reset
 6205:     if ($env{'form.scantron_options_redo'} ne 'redo_skipped_ready') {
 6206: 	&reset_skipping_status();
 6207:     }
 6208:     if ($env{'form.scantron_options_redo'} eq 'redo_skipped') {
 6209: 	&remember_current_skipped();
 6210: 	$env{'form.scantron_options_redo'}='redo_skipped_ready';
 6211:     }
 6212: 
 6213:     if ($env{'form.scantron_options_ignore'} eq 'ignore_corrections') {
 6214: 	&check_for_error($r,&scantron_remove_file('corrected'));
 6215: 	&check_for_error($r,&scantron_remove_file('skipped'));
 6216: 	&check_for_error($r,&scantron_remove_scan_data());
 6217: 	$env{'form.scantron_options_ignore'}='done';
 6218:     }
 6219: 
 6220:     if ($env{'form.scantron_corrections'}) {
 6221: 	&scantron_process_corrections($r);
 6222:     }
 6223:     $r->print('<p>'.&mt('Gathering necessary information.').'</p>');$r->rflush();
 6224:     #get the student pick code ready
 6225:     $r->print(&Apache::loncommon::studentbrowser_javascript());
 6226:     my $max_bubble=&scantron_get_maxbubble();
 6227:     my $result=&scantron_form_start($max_bubble).$default_form_data;
 6228:     $r->print($result);
 6229:     
 6230:     my @validate_phases=( 'sequence',
 6231: 			  'ID',
 6232: 			  'CODE',
 6233: 			  'doublebubble',
 6234: 			  'missingbubbles');
 6235:     if (!$env{'form.validatepass'}) {
 6236: 	$env{'form.validatepass'} = 0;
 6237:     }
 6238:     my $currentphase=$env{'form.validatepass'};
 6239: 
 6240: 
 6241:     my $stop=0;
 6242:     while (!$stop && $currentphase < scalar(@validate_phases)) {
 6243: 	$r->print(&mt('Validating '.$validate_phases[$currentphase]).'<br />');
 6244: 	$r->rflush();
 6245: 	my $which="scantron_validate_".$validate_phases[$currentphase];
 6246: 	{
 6247: 	    no strict 'refs';
 6248: 	    ($stop,$currentphase)=&$which($r,$currentphase);
 6249: 	}
 6250:     }
 6251:     if (!$stop) {
 6252: 	my $warning=&scantron_warning_screen('Start Grading');
 6253: 	$r->print(&mt('Validation process complete.').'<br />'.
 6254:                   $warning.
 6255:                   &mt('Perform verification for each student after storage of submissions?').
 6256:                   '&nbsp;<span class="LC_nobreak"><label>'.
 6257:                   '<input type="radio" name="verifyrecord" value="1" />'.&mt('Yes').'</label>'.
 6258:                   ('&nbsp;'x3).'<label>'.
 6259:                   '<input type="radio" name="verifyrecord" value="0" checked="checked" />'.&mt('No').
 6260:                   '</label></span><br />'.
 6261:                   &mt('Grading will take longer if you use verification.').'<br />'.
 6262:                   &mt("Alternatively, the 'Review bubblesheet data' utility (see grading menu) can be used for all students after grading is complete.").'<br /><br />'.
 6263:                   '<input type="submit" name="submit" value="'.&mt('Start Grading').'" />'.
 6264:                   '<input type="hidden" name="command" value="scantron_process" />'."\n");
 6265:     } else {
 6266: 	$r->print('<input type="hidden" name="command" value="scantron_validate" />');
 6267: 	$r->print("<input type='hidden' name='validatepass' value='".$currentphase."' />");
 6268:     }
 6269:     if ($stop) {
 6270: 	if ($validate_phases[$currentphase] eq 'sequence') {
 6271: 	    $r->print('<input type="submit" name="submit" value="'.&mt('Ignore').' &rarr; " />');
 6272: 	    $r->print(' '.&mt('this error').' <br />');
 6273: 
 6274: 	    $r->print(" <p>".&mt("Or click the 'Grading Menu' button to start over.")."</p>");
 6275: 	} else {
 6276:             if ($validate_phases[$currentphase] eq 'doublebubble' || $validate_phases[$currentphase] eq 'missingbubbles') {
 6277: 	        $r->print('<input type="button" name="submitbutton" value="'.&mt('Continue').' &rarr;" onclick="javascript:verify_bubble_radio(this.form)" />');
 6278:             } else {
 6279:                 $r->print('<input type="submit" name="submit" value="'.&mt('Continue').' &rarr;" />');
 6280:             }
 6281: 	    $r->print(' '.&mt('using corrected info').' <br />');
 6282: 	    $r->print("<input type='submit' value='".&mt("Skip")."' name='scantron_skip_record' />");
 6283: 	    $r->print(" ".&mt("this scanline saving it for later."));
 6284: 	}
 6285:     }
 6286:     $r->print(" </form><br />".&show_grading_menu_form($symb));
 6287:     return '';
 6288: }
 6289: 
 6290: 
 6291: =pod
 6292: 
 6293: =item scantron_remove_file
 6294: 
 6295:    Removes the requested bubble sheet data file, makes sure that
 6296:    scantron_original_<filename> is never removed
 6297: 
 6298: 
 6299: =cut
 6300: 
 6301: sub scantron_remove_file {
 6302:     my ($which)=@_;
 6303:     my $cname=$env{'course.'.$env{'request.course.id'}.'.num'};
 6304:     my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'};
 6305:     my $file='scantron_';
 6306:     if ($which eq 'corrected' || $which eq 'skipped') {
 6307: 	$file.=$which.'_';
 6308:     } else {
 6309: 	return 'refused';
 6310:     }
 6311:     $file.=$env{'form.scantron_selectfile'};
 6312:     return &Apache::lonnet::removeuserfile($cname,$cdom,$file);
 6313: }
 6314: 
 6315: 
 6316: =pod
 6317: 
 6318: =item scantron_remove_scan_data
 6319: 
 6320:    Removes all scan_data correction for the requested bubble sheet
 6321:    data file.  (In the case that both the are doing skipped records we need
 6322:    to remember the old skipped lines for the time being so that element
 6323:    persists for a while.)
 6324: 
 6325: =cut
 6326: 
 6327: sub scantron_remove_scan_data {
 6328:     my $cname=$env{'course.'.$env{'request.course.id'}.'.num'};
 6329:     my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'};
 6330:     my @keys=&Apache::lonnet::getkeys('nohist_scantrondata',$cdom,$cname);
 6331:     my @todelete;
 6332:     my $filename=$env{'form.scantron_selectfile'};
 6333:     foreach my $key (@keys) {
 6334: 	if ($key=~/^\Q$filename\E_/) {
 6335: 	    if ($env{'form.scantron_options_redo'} eq 'redo_skipped_ready' &&
 6336: 		$key=~/remember_skipping/) {
 6337: 		next;
 6338: 	    }
 6339: 	    push(@todelete,$key);
 6340: 	}
 6341:     }
 6342:     my $result;
 6343:     if (@todelete) {
 6344: 	$result = &Apache::lonnet::del('nohist_scantrondata',
 6345: 				       \@todelete,$cdom,$cname);
 6346:     } else {
 6347: 	$result = 'ok';
 6348:     }
 6349:     return $result;
 6350: }
 6351: 
 6352: 
 6353: =pod
 6354: 
 6355: =item scantron_getfile
 6356: 
 6357:     Fetches the requested bubble sheet data file (all 3 versions), and
 6358:     the scan_data hash
 6359:   
 6360:   Arguments:
 6361:     None
 6362: 
 6363:   Returns:
 6364:     2 hash references
 6365: 
 6366:      - first one has 
 6367:          orig      -
 6368:          corrected -
 6369:          skipped   -  each of which points to an array ref of the specified
 6370:                       file broken up into individual lines
 6371:          count     - number of scanlines
 6372:  
 6373:      - second is the scan_data hash possible keys are
 6374:        ($number refers to scanline numbered $number and thus the key affects
 6375:         only that scanline
 6376:         $bubline refers to the specific bubble line element and the aspects
 6377:         refers to that specific bubble line element)
 6378: 
 6379:        $number.user - username:domain to use
 6380:        $number.CODE_ignore_dup 
 6381:                     - ignore the duplicate CODE error 
 6382:        $number.useCODE
 6383:                     - use the CODE in the scanline as is
 6384:        $number.no_bubble.$bubline
 6385:                     - it is valid that there is no bubbled in bubble
 6386:                       at $number $bubline
 6387:        remember_skipping
 6388:                     - a frozen hash containing keys of $number and values
 6389:                       of either 
 6390:                         1 - we are on a 'do skipped records pass' and plan
 6391:                             on processing this line
 6392:                         2 - we are on a 'do skipped records pass' and this
 6393:                             scanline has been marked to skip yet again
 6394: 
 6395: =cut
 6396: 
 6397: sub scantron_getfile {
 6398:     #FIXME really would prefer a scantron directory
 6399:     my $cname=$env{'course.'.$env{'request.course.id'}.'.num'};
 6400:     my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'};
 6401:     my $lines;
 6402:     $lines=&Apache::lonnet::getfile('/uploaded/'.$cdom.'/'.$cname.'/'.
 6403: 		       'scantron_orig_'.$env{'form.scantron_selectfile'});
 6404:     my %scanlines;
 6405:     $scanlines{'orig'}=[(split("\n",$lines,-1))];
 6406:     my $temp=$scanlines{'orig'};
 6407:     $scanlines{'count'}=$#$temp;
 6408: 
 6409:     $lines=&Apache::lonnet::getfile('/uploaded/'.$cdom.'/'.$cname.'/'.
 6410: 		       'scantron_corrected_'.$env{'form.scantron_selectfile'});
 6411:     if ($lines eq '-1') {
 6412: 	$scanlines{'corrected'}=[];
 6413:     } else {
 6414: 	$scanlines{'corrected'}=[(split("\n",$lines,-1))];
 6415:     }
 6416:     $lines=&Apache::lonnet::getfile('/uploaded/'.$cdom.'/'.$cname.'/'.
 6417: 		       'scantron_skipped_'.$env{'form.scantron_selectfile'});
 6418:     if ($lines eq '-1') {
 6419: 	$scanlines{'skipped'}=[];
 6420:     } else {
 6421: 	$scanlines{'skipped'}=[(split("\n",$lines,-1))];
 6422:     }
 6423:     my @tmp=&Apache::lonnet::dump('nohist_scantrondata',$cdom,$cname);
 6424:     if ($tmp[0] =~ /^(error:|no_such_host)/) { @tmp=(); }
 6425:     my %scan_data = @tmp;
 6426:     return (\%scanlines,\%scan_data);
 6427: }
 6428: 
 6429: =pod
 6430: 
 6431: =item lonnet_putfile
 6432: 
 6433:    Wrapper routine to call &Apache::lonnet::finishuserfileupload
 6434: 
 6435:  Arguments:
 6436:    $contents - data to store
 6437:    $filename - filename to store $contents into
 6438: 
 6439:  Returns:
 6440:    result value from &Apache::lonnet::finishuserfileupload
 6441: 
 6442: =cut
 6443: 
 6444: sub lonnet_putfile {
 6445:     my ($contents,$filename)=@_;
 6446:     my $docuname=$env{'course.'.$env{'request.course.id'}.'.num'};
 6447:     my $docudom=$env{'course.'.$env{'request.course.id'}.'.domain'};
 6448:     $env{'form.sillywaytopassafilearound'}=$contents;
 6449:     &Apache::lonnet::finishuserfileupload($docuname,$docudom,'sillywaytopassafilearound',$filename);
 6450: 
 6451: }
 6452: 
 6453: =pod
 6454: 
 6455: =item scantron_putfile
 6456: 
 6457:     Stores the current version of the bubble sheet data files, and the
 6458:     scan_data hash. (Does not modify the original version only the
 6459:     corrected and skipped versions.
 6460: 
 6461:  Arguments:
 6462:     $scanlines - hash ref that looks like the first return value from
 6463:                  &scantron_getfile()
 6464:     $scan_data - hash ref that looks like the second return value from
 6465:                  &scantron_getfile()
 6466: 
 6467: =cut
 6468: 
 6469: sub scantron_putfile {
 6470:     my ($scanlines,$scan_data) = @_;
 6471:     #FIXME really would prefer a scantron directory
 6472:     my $cname=$env{'course.'.$env{'request.course.id'}.'.num'};
 6473:     my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'};
 6474:     if ($scanlines) {
 6475: 	my $prefix='scantron_';
 6476: # no need to update orig, shouldn't change
 6477: #   &lonnet_putfile(join("\n",@{$scanlines->{'orig'}}),$prefix.'orig_'.
 6478: #		    $env{'form.scantron_selectfile'});
 6479: 	&lonnet_putfile(join("\n",@{$scanlines->{'corrected'}}),
 6480: 			$prefix.'corrected_'.
 6481: 			$env{'form.scantron_selectfile'});
 6482: 	&lonnet_putfile(join("\n",@{$scanlines->{'skipped'}}),
 6483: 			$prefix.'skipped_'.
 6484: 			$env{'form.scantron_selectfile'});
 6485:     }
 6486:     &Apache::lonnet::put('nohist_scantrondata',$scan_data,$cdom,$cname);
 6487: }
 6488: 
 6489: =pod
 6490: 
 6491: =item scantron_get_line
 6492: 
 6493:    Returns the correct version of the scanline
 6494: 
 6495:  Arguments:
 6496:     $scanlines - hash ref that looks like the first return value from
 6497:                  &scantron_getfile()
 6498:     $scan_data - hash ref that looks like the second return value from
 6499:                  &scantron_getfile()
 6500:     $i         - number of the requested line (starts at 0)
 6501: 
 6502:  Returns:
 6503:    A scanline, (either the original or the corrected one if it
 6504:    exists), or undef if the requested scanline should be
 6505:    skipped. (Either because it's an skipped scanline, or it's an
 6506:    unskipped scanline and we are not doing a 'do skipped scanlines'
 6507:    pass.
 6508: 
 6509: =cut
 6510: 
 6511: sub scantron_get_line {
 6512:     my ($scanlines,$scan_data,$i)=@_;
 6513:     if (&should_be_skipped($scanlines,$scan_data,$i)) { return undef; }
 6514:     #if ($scanlines->{'skipped'}[$i]) { return undef; }
 6515:     if ($scanlines->{'corrected'}[$i]) {return $scanlines->{'corrected'}[$i];}
 6516:     return $scanlines->{'orig'}[$i]; 
 6517: }
 6518: 
 6519: =pod
 6520: 
 6521: =item scantron_todo_count
 6522: 
 6523:     Counts the number of scanlines that need processing.
 6524: 
 6525:  Arguments:
 6526:     $scanlines - hash ref that looks like the first return value from
 6527:                  &scantron_getfile()
 6528:     $scan_data - hash ref that looks like the second return value from
 6529:                  &scantron_getfile()
 6530: 
 6531:  Returns:
 6532:     $count - number of scanlines to process
 6533: 
 6534: =cut
 6535: 
 6536: sub get_todo_count {
 6537:     my ($scanlines,$scan_data)=@_;
 6538:     my $count=0;
 6539:     for (my $i=0;$i<=$scanlines->{'count'};$i++) {
 6540: 	my $line=&scantron_get_line($scanlines,$scan_data,$i);
 6541: 	if ($line=~/^[\s\cz]*$/) { next; }
 6542: 	$count++;
 6543:     }
 6544:     return $count;
 6545: }
 6546: 
 6547: =pod
 6548: 
 6549: =item scantron_put_line
 6550: 
 6551:     Updates the 'corrected' or 'skipped' versions of the bubble sheet
 6552:     data file.
 6553: 
 6554:  Arguments:
 6555:     $scanlines - hash ref that looks like the first return value from
 6556:                  &scantron_getfile()
 6557:     $scan_data - hash ref that looks like the second return value from
 6558:                  &scantron_getfile()
 6559:     $i         - line number to update
 6560:     $newline   - contents of the updated scanline
 6561:     $skip      - if true make the line for skipping and update the
 6562:                  'skipped' file
 6563: 
 6564: =cut
 6565: 
 6566: sub scantron_put_line {
 6567:     my ($scanlines,$scan_data,$i,$newline,$skip)=@_;
 6568:     if ($skip) {
 6569: 	$scanlines->{'skipped'}[$i]=$newline;
 6570: 	&start_skipping($scan_data,$i);
 6571: 	return;
 6572:     }
 6573:     $scanlines->{'corrected'}[$i]=$newline;
 6574: }
 6575: 
 6576: =pod
 6577: 
 6578: =item scantron_clear_skip
 6579: 
 6580:    Remove a line from the 'skipped' file
 6581: 
 6582:  Arguments:
 6583:     $scanlines - hash ref that looks like the first return value from
 6584:                  &scantron_getfile()
 6585:     $scan_data - hash ref that looks like the second return value from
 6586:                  &scantron_getfile()
 6587:     $i         - line number to update
 6588: 
 6589: =cut
 6590: 
 6591: sub scantron_clear_skip {
 6592:     my ($scanlines,$scan_data,$i)=@_;
 6593:     if (exists($scanlines->{'skipped'}[$i])) {
 6594: 	undef($scanlines->{'skipped'}[$i]);
 6595: 	return 1;
 6596:     }
 6597:     return 0;
 6598: }
 6599: 
 6600: =pod
 6601: 
 6602: =item scantron_filter_not_exam
 6603: 
 6604:    Filter routine used by &Apache::lonnavmaps::retrieveResources(), to
 6605:    filter out resources that are not marked as 'exam' mode
 6606: 
 6607: =cut
 6608: 
 6609: sub scantron_filter_not_exam {
 6610:     my ($curres)=@_;
 6611:     
 6612:     if (ref($curres) && $curres->is_problem() && !$curres->is_exam()) {
 6613: 	# if the user has asked to not have either hidden
 6614: 	# or 'randomout' controlled resources to be graded
 6615: 	# don't include them
 6616: 	if ($env{'form.scantron_options_hidden'} eq 'ignore_hidden'
 6617: 	    && $curres->randomout) {
 6618: 	    return 0;
 6619: 	}
 6620: 	return 1;
 6621:     }
 6622:     return 0;
 6623: }
 6624: 
 6625: =pod
 6626: 
 6627: =item scantron_validate_sequence
 6628: 
 6629:     Validates the selected sequence, checking for resource that are
 6630:     not set to exam mode.
 6631: 
 6632: =cut
 6633: 
 6634: sub scantron_validate_sequence {
 6635:     my ($r,$currentphase) = @_;
 6636: 
 6637:     my $navmap=Apache::lonnavmaps::navmap->new();
 6638:     my (undef,undef,$sequence)=
 6639: 	&Apache::lonnet::decode_symb($env{'form.selectpage'});
 6640: 
 6641:     my $map=$navmap->getResourceByUrl($sequence);
 6642: 
 6643:     $r->print('<input type="hidden" name="validate_sequence_exam"
 6644:                                     value="ignore" />');
 6645:     if ($env{'form.validate_sequence_exam'} ne 'ignore') {
 6646: 	my @resources=
 6647: 	    $navmap->retrieveResources($map,\&scantron_filter_not_exam,1,0);
 6648: 	if (@resources) {
 6649: 	    $r->print("<p>".&mt('Some resources in the sequence currently are not set to exam mode. Grading these resources currently may not work correctly.')."</p>");
 6650: 	    return (1,$currentphase);
 6651: 	}
 6652:     }
 6653: 
 6654:     return (0,$currentphase+1);
 6655: }
 6656: 
 6657: 
 6658: 
 6659: sub scantron_validate_ID {
 6660:     my ($r,$currentphase) = @_;
 6661:     
 6662:     #get student info
 6663:     my $classlist=&Apache::loncoursedata::get_classlist();
 6664:     my %idmap=&username_to_idmap($classlist);
 6665: 
 6666:     #get scantron line setup
 6667:     my %scantron_config=&get_scantron_config($env{'form.scantron_format'});
 6668:     my ($scanlines,$scan_data)=&scantron_getfile();
 6669:     
 6670:     &scantron_get_maxbubble();	# parse needs the bubble_lines.. array.
 6671: 
 6672:     my %found=('ids'=>{},'usernames'=>{});
 6673:     for (my $i=0;$i<=$scanlines->{'count'};$i++) {
 6674: 	my $line=&scantron_get_line($scanlines,$scan_data,$i);
 6675: 	if ($line=~/^[\s\cz]*$/) { next; }
 6676: 	my $scan_record=&scantron_parse_scanline($line,$i,\%scantron_config,
 6677: 						 $scan_data);
 6678: 	my $id=$$scan_record{'scantron.ID'};
 6679: 	my $found;
 6680: 	foreach my $checkid (keys(%idmap)) {
 6681: 	    if (lc($checkid) eq lc($id)) { $found=$checkid;last; }
 6682: 	}
 6683: 	if ($found) {
 6684: 	    my $username=$idmap{$found};
 6685: 	    if ($found{'ids'}{$found}) {
 6686: 		&scantron_get_correction($r,$i,$scan_record,\%scantron_config,
 6687: 					 $line,'duplicateID',$found);
 6688: 		return(1,$currentphase);
 6689: 	    } elsif ($found{'usernames'}{$username}) {
 6690: 		&scantron_get_correction($r,$i,$scan_record,\%scantron_config,
 6691: 					 $line,'duplicateID',$username);
 6692: 		return(1,$currentphase);
 6693: 	    }
 6694: 	    #FIXME store away line we previously saw the ID on to use above
 6695: 	    $found{'ids'}{$found}++;
 6696: 	    $found{'usernames'}{$username}++;
 6697: 	} else {
 6698: 	    if ($id =~ /^\s*$/) {
 6699: 		my $username=&scan_data($scan_data,"$i.user");
 6700: 		if (defined($username) && $found{'usernames'}{$username}) {
 6701: 		    &scantron_get_correction($r,$i,$scan_record,
 6702: 					     \%scantron_config,
 6703: 					     $line,'duplicateID',$username);
 6704: 		    return(1,$currentphase);
 6705: 		} elsif (!defined($username)) {
 6706: 		    &scantron_get_correction($r,$i,$scan_record,
 6707: 					     \%scantron_config,
 6708: 					     $line,'incorrectID');
 6709: 		    return(1,$currentphase);
 6710: 		}
 6711: 		$found{'usernames'}{$username}++;
 6712: 	    } else {
 6713: 		&scantron_get_correction($r,$i,$scan_record,\%scantron_config,
 6714: 					 $line,'incorrectID');
 6715: 		return(1,$currentphase);
 6716: 	    }
 6717: 	}
 6718:     }
 6719: 
 6720:     return (0,$currentphase+1);
 6721: }
 6722: 
 6723: 
 6724: sub scantron_get_correction {
 6725:     my ($r,$i,$scan_record,$scan_config,$line,$error,$arg)=@_;
 6726: #FIXME in the case of a duplicated ID the previous line, probably need
 6727: #to show both the current line and the previous one and allow skipping
 6728: #the previous one or the current one
 6729: 
 6730:     if ( $$scan_record{'scantron.PaperID'} =~ /\S/) {
 6731: 	$r->print("<p>".&mt("<b>An error was detected ($error)</b>".
 6732: 			    " for PaperID <tt>[_1]</tt>",
 6733: 			    $$scan_record{'scantron.PaperID'})."</p> \n");
 6734:     } else {
 6735: 	$r->print("<p>".&mt("<b>An error was detected ($error)</b>".
 6736: 			    " in scanline [_1] <pre>[_2]</pre>",
 6737: 			    $i,$line)."</p> \n");
 6738:     }
 6739:     my $message="<p>".&mt("The ID on the form is  <tt>[_1]</tt><br />".
 6740: 			  "The name on the paper is [_2],[_3]",
 6741: 			  $$scan_record{'scantron.ID'},
 6742: 			  $$scan_record{'scantron.LastName'},
 6743: 			  $$scan_record{'scantron.FirstName'})."</p>";
 6744: 
 6745:     $r->print('<input type="hidden" name="scantron_corrections" value="'.$error.'" />'."\n");
 6746:     $r->print('<input type="hidden" name="scantron_line" value="'.$i.'" />'."\n");
 6747:                            # Array populated for doublebubble or
 6748:     my @lines_to_correct;  # missingbubble errors to build javascript
 6749:                            # to validate radio button checking   
 6750: 
 6751:     if ($error =~ /ID$/) {
 6752: 	if ($error eq 'incorrectID') {
 6753: 	    $r->print("<p>".&mt("The encoded ID is not in the classlist").
 6754: 		      "</p>\n");
 6755: 	} elsif ($error eq 'duplicateID') {
 6756: 	    $r->print("<p>".&mt("The encoded ID has also been used by a previous paper [_1]",$arg)."</p>\n");
 6757: 	}
 6758: 	$r->print($message);
 6759: 	$r->print("<p>".&mt("How should I handle this?")." <br /> \n");
 6760: 	$r->print("\n<ul><li> ");
 6761: 	#FIXME it would be nice if this sent back the user ID and
 6762: 	#could do partial userID matches
 6763: 	$r->print(&Apache::loncommon::selectstudent_link('scantronupload',
 6764: 				       'scantron_username','scantron_domain'));
 6765: 	$r->print(": <input type='text' name='scantron_username' value='' />");
 6766: 	$r->print("\n@".
 6767: 		 &Apache::loncommon::select_dom_form($env{'request.role.domain'},'scantron_domain'));
 6768: 
 6769: 	$r->print('</li>');
 6770:     } elsif ($error =~ /CODE$/) {
 6771: 	if ($error eq 'incorrectCODE') {
 6772: 	    $r->print("<p>".&mt("The encoded CODE is not in the list of possible CODEs.")."</p>\n");
 6773: 	} elsif ($error eq 'duplicateCODE') {
 6774: 	    $r->print("<p>".&mt("The encoded CODE has also been used by a previous paper [_1], and CODEs are supposed to be unique.",join(', ',@{$arg}))."</p>\n");
 6775: 	}
 6776: 	$r->print("<p>".&mt("The CODE on the form is  <tt>'[_1]'</tt>",
 6777: 			    $$scan_record{'scantron.CODE'})."<br />\n");
 6778: 	$r->print($message);
 6779: 	$r->print("<p>".&mt("How should I handle this?")." <br /> \n");
 6780: 	$r->print("\n<br /> ");
 6781: 	my $i=0;
 6782: 	if ($error eq 'incorrectCODE' 
 6783: 	    && $$scan_record{'scantron.CODE'}=~/\S/ ) {
 6784: 	    my ($max,$closest)=&scantron_get_closely_matching_CODEs($arg,$$scan_record{'scantron.CODE'});
 6785: 	    if ($closest > 0) {
 6786: 		foreach my $testcode (@{$closest}) {
 6787: 		    my $checked='';
 6788: 		    if (!$i) { $checked=' checked="checked"'; }
 6789: 		    $r->print("
 6790:    <label>
 6791:        <input type='radio' name='scantron_CODE_resolution' value='use_closest_$i'$checked />
 6792:        ".&mt("Use the similar CODE [_1] instead.",
 6793: 	    "<b><tt>".$testcode."</tt></b>")."
 6794:     </label>
 6795:     <input type='hidden' name='scantron_CODE_closest_$i' value='$testcode' />");
 6796: 		    $r->print("\n<br />");
 6797: 		    $i++;
 6798: 		}
 6799: 	    }
 6800: 	}
 6801: 	if ($$scan_record{'scantron.CODE'}=~/\S/ ) {
 6802: 	    my $checked; if (!$i) { $checked=' checked="checked"'; }
 6803: 	    $r->print("
 6804:     <label>
 6805:         <input type='radio' name='scantron_CODE_resolution' value='use_unfound'$checked />
 6806:        ".&mt("Use the CODE [_1] that is was on the paper, ignoring the error.",
 6807: 	     "<b><tt>".$$scan_record{'scantron.CODE'}."</tt></b>")."
 6808:     </label>");
 6809: 	    $r->print("\n<br />");
 6810: 	}
 6811: 
 6812: 	$r->print(<<ENDSCRIPT);
 6813: <script type="text/javascript">
 6814: function change_radio(field) {
 6815:     var slct=document.scantronupload.scantron_CODE_resolution;
 6816:     var i;
 6817:     for (i=0;i<slct.length;i++) {
 6818:         if (slct[i].value==field) { slct[i].checked=true; }
 6819:     }
 6820: }
 6821: </script>
 6822: ENDSCRIPT
 6823: 	my $href="/adm/pickcode?".
 6824: 	   "form=".&escape("scantronupload").
 6825: 	   "&scantron_format=".&escape($env{'form.scantron_format'}).
 6826: 	   "&scantron_CODElist=".&escape($env{'form.scantron_CODElist'}).
 6827: 	   "&curCODE=".&escape($$scan_record{'scantron.CODE'}).
 6828: 	   "&scantron_selectfile=".&escape($env{'form.scantron_selectfile'});
 6829: 	if ($env{'form.scantron_CODElist'} =~ /\S/) { 
 6830: 	    $r->print("
 6831:     <label>
 6832:        <input type='radio' name='scantron_CODE_resolution' value='use_found' />
 6833:        ".&mt("[_1]Select[_2] a CODE from the list of all CODEs and use it.",
 6834: 	     "<a target='_blank' href='$href'>","</a>")."
 6835:     </label> 
 6836:     ".&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\')" />'));
 6837: 	    $r->print("\n<br />");
 6838: 	}
 6839: 	$r->print("
 6840:     <label>
 6841:        <input type='radio' name='scantron_CODE_resolution' value='use_typed' />
 6842:        ".&mt("Use [_1] as the CODE.",
 6843: 	     "</label><input type='text' size='8' name='scantron_CODE_newvalue' onfocus=\"javascript:change_radio('use_typed')\" onkeypress=\"javascript:change_radio('use_typed')\" />"));
 6844: 	$r->print("\n<br /><br />");
 6845:     } elsif ($error eq 'doublebubble') {
 6846: 	$r->print("<p>".&mt("There have been multiple bubbles scanned for some question(s)")."</p>\n");
 6847: 
 6848: 	# The form field scantron_questions is acutally a list of line numbers.
 6849: 	# represented by this form so:
 6850: 
 6851: 	my $line_list = &questions_to_line_list($arg);
 6852: 
 6853: 	$r->print('<input type="hidden" name="scantron_questions" value="'.
 6854: 		  $line_list.'" />');
 6855: 	$r->print($message);
 6856: 	$r->print("<p>".&mt("Please indicate which bubble should be used for grading")."</p>");
 6857: 	foreach my $question (@{$arg}) {
 6858: 	    my @linenums = &prompt_for_corrections($r,$question,$scan_config,
 6859:                                                    $scan_record, $error);
 6860:             push(@lines_to_correct,@linenums);
 6861: 	}
 6862:         $r->print(&verify_bubbles_checked(@lines_to_correct));
 6863:     } elsif ($error eq 'missingbubble') {
 6864: 	$r->print("<p>".&mt("There have been <b>no</b> bubbles scanned for some question(s)")."</p>\n");
 6865: 	$r->print($message);
 6866: 	$r->print("<p>".&mt("Please indicate which bubble should be used for grading.")."</p>");
 6867: 	$r->print(&mt("Some questions have no scanned bubbles.")."\n");
 6868: 
 6869: 	# The form field scantron_questions is actually a list of line numbers not
 6870: 	# a list of question numbers. Therefore:
 6871: 	#
 6872: 	
 6873: 	my $line_list = &questions_to_line_list($arg);
 6874: 
 6875: 	$r->print('<input type="hidden" name="scantron_questions" value="'.
 6876: 		  $line_list.'" />');
 6877: 	foreach my $question (@{$arg}) {
 6878: 	    my @linenums = &prompt_for_corrections($r,$question,$scan_config,
 6879:                                                    $scan_record, $error);
 6880:             push(@lines_to_correct,@linenums);
 6881: 	}
 6882:         $r->print(&verify_bubbles_checked(@lines_to_correct));
 6883:     } else {
 6884: 	$r->print("\n<ul>");
 6885:     }
 6886:     $r->print("\n</li></ul>");
 6887: }
 6888: 
 6889: sub verify_bubbles_checked {
 6890:     my (@ansnums) = @_;
 6891:     my $ansnumstr = join('","',@ansnums);
 6892:     my $warning = &mt("A bubble or 'No bubble' selection has not been made for one or more lines.");
 6893:     my $output = (<<ENDSCRIPT);
 6894: <script type="text/javascript">
 6895: function verify_bubble_radio(form) {
 6896:     var ansnumArray = new Array ("$ansnumstr");
 6897:     var need_bubble_count = 0;
 6898:     for (var i=0; i<ansnumArray.length; i++) {
 6899:         if (form.elements["scantron_correct_Q_"+ansnumArray[i]].length > 1) {
 6900:             var bubble_picked = 0; 
 6901:             for (var j=0; j<form.elements["scantron_correct_Q_"+ansnumArray[i]].length; j++) {
 6902:                 if (form.elements["scantron_correct_Q_"+ansnumArray[i]][j].checked == true) {
 6903:                     bubble_picked = 1;
 6904:                 }
 6905:             }
 6906:             if (bubble_picked == 0) {
 6907:                 need_bubble_count ++;
 6908:             }
 6909:         }
 6910:     }
 6911:     if (need_bubble_count) {
 6912:         alert("$warning");
 6913:         return;
 6914:     }
 6915:     form.submit(); 
 6916: }
 6917: </script>
 6918: ENDSCRIPT
 6919:     return $output;
 6920: }
 6921: 
 6922: =pod
 6923: 
 6924: =item  questions_to_line_list
 6925: 
 6926: Converts a list of questions into a string of comma separated
 6927: line numbers in the answer sheet used by the questions.  This is
 6928: used to fill in the scantron_questions form field.
 6929: 
 6930:   Arguments:
 6931:      questions    - Reference to an array of questions.
 6932: 
 6933: =cut
 6934: 
 6935: 
 6936: sub questions_to_line_list {
 6937:     my ($questions) = @_;
 6938:     my @lines;
 6939: 
 6940:     foreach my $item (@{$questions}) {
 6941:         my $question = $item;
 6942:         my ($first,$count,$last);
 6943:         if ($item =~ /^(\d+)\.(\d+)$/) {
 6944:             $question = $1;
 6945:             my $subquestion = $2;
 6946:             $first = $first_bubble_line{$question-1} + 1;
 6947:             my @subans = split(/,/,$subdivided_bubble_lines{$question-1});
 6948:             my $subcount = 1;
 6949:             while ($subcount<$subquestion) {
 6950:                 $first += $subans[$subcount-1];
 6951:                 $subcount ++;
 6952:             }
 6953:             $count = $subans[$subquestion-1];
 6954:         } else {
 6955: 	    $first   = $first_bubble_line{$question-1} + 1;
 6956: 	    $count   = $bubble_lines_per_response{$question-1};
 6957:         }
 6958:         $last = $first+$count-1;
 6959:         push(@lines, ($first..$last));
 6960:     }
 6961:     return join(',', @lines);
 6962: }
 6963: 
 6964: =pod 
 6965: 
 6966: =item prompt_for_corrections
 6967: 
 6968: Prompts for a potentially multiline correction to the
 6969: user's bubbling (factors out common code from scantron_get_correction
 6970: for multi and missing bubble cases).
 6971: 
 6972:  Arguments:
 6973:    $r           - Apache request object.
 6974:    $question    - The question number to prompt for.
 6975:    $scan_config - The scantron file configuration hash.
 6976:    $scan_record - Reference to the hash that has the the parsed scanlines.
 6977:    $error       - Type of error
 6978: 
 6979:  Implicit inputs:
 6980:    %bubble_lines_per_response   - Starting line numbers for each question.
 6981:                                   Numbered from 0 (but question numbers are from
 6982:                                   1.
 6983:    %first_bubble_line           - Starting bubble line for each question.
 6984:    %subdivided_bubble_lines     - optionresponse, matchresponse and rankresponse 
 6985:                                   type problems render as separate sub-questions, 
 6986:                                   in exam mode. This hash contains a 
 6987:                                   comma-separated list of the lines per 
 6988:                                   sub-question.
 6989:    %responsetype_per_response   - essayresponse, formularesponse,
 6990:                                   stringresponse, imageresponse, reactionresponse,
 6991:                                   and organicresponse type problem parts can have
 6992:                                   multiple lines per response if the weight
 6993:                                   assigned exceeds 10.  In this case, only
 6994:                                   one bubble per line is permitted, but more 
 6995:                                   than one line might contain bubbles, e.g.
 6996:                                   bubbling of: line 1 - J, line 2 - J, 
 6997:                                   line 3 - B would assign 22 points.  
 6998: 
 6999: =cut
 7000: 
 7001: sub prompt_for_corrections {
 7002:     my ($r, $question, $scan_config, $scan_record, $error) = @_;
 7003:     my ($current_line,$lines);
 7004:     my @linenums;
 7005:     my $questionnum = $question;
 7006:     if ($question =~ /^(\d+)\.(\d+)$/) {
 7007:         $question = $1;
 7008:         $current_line = $first_bubble_line{$question-1} + 1 ;
 7009:         my $subquestion = $2;
 7010:         my @subans = split(/,/,$subdivided_bubble_lines{$question-1});
 7011:         my $subcount = 1;
 7012:         while ($subcount<$subquestion) {
 7013:             $current_line += $subans[$subcount-1];
 7014:             $subcount ++;
 7015:         }
 7016:         $lines = $subans[$subquestion-1];
 7017:     } else {
 7018:         $current_line = $first_bubble_line{$question-1} + 1 ;
 7019:         $lines        = $bubble_lines_per_response{$question-1};
 7020:     }
 7021:     if ($lines > 1) {
 7022:         $r->print(&mt('The group of bubble lines below responds to a single question.').'<br />');
 7023:         if (($responsetype_per_response{$question-1} eq 'essayresponse') ||
 7024:             ($responsetype_per_response{$question-1} eq 'formularesponse') ||
 7025:             ($responsetype_per_response{$question-1} eq 'stringresponse') ||
 7026:             ($responsetype_per_response{$question-1} eq 'imageresponse') ||
 7027:             ($responsetype_per_response{$question-1} eq 'reactionresponse') ||
 7028:             ($responsetype_per_response{$question-1} eq 'organicresponse')) {
 7029:             $r->print(&mt("Although this particular question type requires handgrading, the instructions for this question in the exam directed students to leave [quant,_1,line] blank on their bubblesheets.",$lines).'<br /><br />'.&mt('A non-zero score can be assigned to the student during bubblesheet grading by selecting a bubble in at least one line.').'<br />'.&mt('The score for this question will be a sum of the numeric values for the selected bubbles from each line, where A=1 point, B=2 points etc.').'<br />'.&mt("To assign a score of zero for this question, mark all lines as 'No bubble'.").'<br /><br />');
 7030:         } else {
 7031:             $r->print(&mt("Select at most one bubble in a single line and select 'No Bubble' in all the other lines. ")."<br />");
 7032:         }
 7033:     }
 7034:     for (my $i =0; $i < $lines; $i++) {
 7035:         my $selected = $$scan_record{"scantron.$current_line.answer"};
 7036: 	&scantron_bubble_selector($r,$scan_config,$current_line, 
 7037: 	        		  $questionnum,$error,split('', $selected));
 7038:         push(@linenums,$current_line);
 7039: 	$current_line++;
 7040:     }
 7041:     if ($lines > 1) {
 7042: 	$r->print("<hr /><br />");
 7043:     }
 7044:     return @linenums;
 7045: }
 7046: 
 7047: =pod
 7048: 
 7049: =item scantron_bubble_selector
 7050:   
 7051:    Generates the html radiobuttons to correct a single bubble line
 7052:    possibly showing the existing the selected bubbles if known
 7053: 
 7054:  Arguments:
 7055:     $r           - Apache request object
 7056:     $scan_config - hash from &get_scantron_config()
 7057:     $line        - Number of the line being displayed.
 7058:     $questionnum - Question number (may include subquestion)
 7059:     $error       - Type of error.
 7060:     @selected    - Array of bubbles picked on this line.
 7061: 
 7062: =cut
 7063: 
 7064: sub scantron_bubble_selector {
 7065:     my ($r,$scan_config,$line,$questionnum,$error,@selected)=@_;
 7066:     my $max=$$scan_config{'Qlength'};
 7067: 
 7068:     my $scmode=$$scan_config{'Qon'};
 7069:     if ($scmode eq 'number' || $scmode eq 'letter') { $max=10; }	     
 7070: 
 7071:     my @alphabet=('A'..'Z');
 7072:     $r->print(&Apache::loncommon::start_data_table().
 7073:               &Apache::loncommon::start_data_table_row());
 7074:     $r->print('<td rowspan="2" class="LC_leftcol_header">'.$line.'</td>');
 7075:     for (my $i=0;$i<$max+1;$i++) {
 7076: 	$r->print("\n".'<td align="center">');
 7077: 	if ($selected[0] eq $alphabet[$i]) { $r->print('X'); shift(@selected) }
 7078: 	else { $r->print('&nbsp;'); }
 7079: 	$r->print('</td>');
 7080:     }
 7081:     $r->print(&Apache::loncommon::end_data_table_row().
 7082:               &Apache::loncommon::start_data_table_row());
 7083:     for (my $i=0;$i<$max;$i++) {
 7084: 	$r->print("\n".
 7085: 		  '<td><label><input type="radio" name="scantron_correct_Q_'.
 7086: 		  $line.'" value="'.$i.'" />'.$alphabet[$i]."</label></td>");
 7087:     }
 7088:     my $nobub_checked = ' ';
 7089:     if ($error eq 'missingbubble') {
 7090:         $nobub_checked = ' checked = "checked" ';
 7091:     }
 7092:     $r->print("\n".'<td><label><input type="radio" name="scantron_correct_Q_'.
 7093: 	      $line.'" value="none"'.$nobub_checked.'/>'.&mt('No bubble').
 7094:               '</label>'."\n".'<input type="hidden" name="scantron_questionnum_Q_'.
 7095:               $line.'" value="'.$questionnum.'" /></td>');
 7096:     $r->print(&Apache::loncommon::end_data_table_row().
 7097:               &Apache::loncommon::end_data_table());
 7098: }
 7099: 
 7100: =pod
 7101: 
 7102: =item num_matches
 7103: 
 7104:    Counts the number of characters that are the same between the two arguments.
 7105: 
 7106:  Arguments:
 7107:    $orig - CODE from the scanline
 7108:    $code - CODE to match against
 7109: 
 7110:  Returns:
 7111:    $count - integer count of the number of same characters between the
 7112:             two arguments
 7113: 
 7114: =cut
 7115: 
 7116: sub num_matches {
 7117:     my ($orig,$code) = @_;
 7118:     my @code=split(//,$code);
 7119:     my @orig=split(//,$orig);
 7120:     my $same=0;
 7121:     for (my $i=0;$i<scalar(@code);$i++) {
 7122: 	if ($code[$i] eq $orig[$i]) { $same++; }
 7123:     }
 7124:     return $same;
 7125: }
 7126: 
 7127: =pod
 7128: 
 7129: =item scantron_get_closely_matching_CODEs
 7130: 
 7131:    Cycles through all CODEs and finds the set that has the greatest
 7132:    number of same characters as the provided CODE
 7133: 
 7134:  Arguments:
 7135:    $allcodes - hash ref returned by &get_codes()
 7136:    $CODE     - CODE from the current scanline
 7137: 
 7138:  Returns:
 7139:    2 element list
 7140:     - first elements is number of how closely matching the best fit is 
 7141:       (5 means best set has 5 matching characters)
 7142:     - second element is an arrary ref containing the set of valid CODEs
 7143:       that best fit the passed in CODE
 7144: 
 7145: =cut
 7146: 
 7147: sub scantron_get_closely_matching_CODEs {
 7148:     my ($allcodes,$CODE)=@_;
 7149:     my @CODEs;
 7150:     foreach my $testcode (sort(keys(%{$allcodes}))) {
 7151: 	push(@{$CODEs[&num_matches($CODE,$testcode)]},$testcode);
 7152:     }
 7153: 
 7154:     return ($#CODEs,$CODEs[-1]);
 7155: }
 7156: 
 7157: =pod
 7158: 
 7159: =item get_codes
 7160: 
 7161:    Builds a hash which has keys of all of the valid CODEs from the selected
 7162:    set of remembered CODEs.
 7163: 
 7164:  Arguments:
 7165:   $old_name - name of the set of remembered CODEs
 7166:   $cdom     - domain of the course
 7167:   $cnum     - internal course name
 7168: 
 7169:  Returns:
 7170:   %allcodes - keys are the valid CODEs, values are all 1
 7171: 
 7172: =cut
 7173: 
 7174: sub get_codes {
 7175:     my ($old_name, $cdom, $cnum) = @_;
 7176:     if (!$old_name) {
 7177: 	$old_name=$env{'form.scantron_CODElist'};
 7178:     }
 7179:     if (!$cdom) {
 7180: 	$cdom =$env{'course.'.$env{'request.course.id'}.'.domain'};
 7181:     }
 7182:     if (!$cnum) {
 7183: 	$cnum =$env{'course.'.$env{'request.course.id'}.'.num'};
 7184:     }
 7185:     my %result=&Apache::lonnet::get('CODEs',[$old_name,"type\0$old_name"],
 7186: 				    $cdom,$cnum);
 7187:     my %allcodes;
 7188:     if ($result{"type\0$old_name"} eq 'number') {
 7189: 	%allcodes=map {($_,1)} split(',',$result{$old_name});
 7190:     } else {
 7191: 	%allcodes=map {(&Apache::lonprintout::num_to_letters($_),1)} split(',',$result{$old_name});
 7192:     }
 7193:     return %allcodes;
 7194: }
 7195: 
 7196: =pod
 7197: 
 7198: =item scantron_validate_CODE
 7199: 
 7200:    Validates all scanlines in the selected file to not have any
 7201:    invalid or underspecified CODEs and that none of the codes are
 7202:    duplicated if this was requested.
 7203: 
 7204: =cut
 7205: 
 7206: sub scantron_validate_CODE {
 7207:     my ($r,$currentphase) = @_;
 7208:     my %scantron_config=&get_scantron_config($env{'form.scantron_format'});
 7209:     if ($scantron_config{'CODElocation'} &&
 7210: 	$scantron_config{'CODEstart'} &&
 7211: 	$scantron_config{'CODElength'}) {
 7212: 	if (!defined($env{'form.scantron_CODElist'})) {
 7213: 	    &FIXME_blow_up()
 7214: 	}
 7215:     } else {
 7216: 	return (0,$currentphase+1);
 7217:     }
 7218:     
 7219:     my %usedCODEs;
 7220: 
 7221:     my %allcodes=&get_codes();
 7222: 
 7223:     &scantron_get_maxbubble();	# parse needs the lines per response array.
 7224: 
 7225:     my ($scanlines,$scan_data)=&scantron_getfile();
 7226:     for (my $i=0;$i<=$scanlines->{'count'};$i++) {
 7227: 	my $line=&scantron_get_line($scanlines,$scan_data,$i);
 7228: 	if ($line=~/^[\s\cz]*$/) { next; }
 7229: 	my $scan_record=&scantron_parse_scanline($line,$i,\%scantron_config,
 7230: 						 $scan_data);
 7231: 	my $CODE=$$scan_record{'scantron.CODE'};
 7232: 	my $error=0;
 7233: 	if (!&Apache::lonnet::validCODE($CODE)) {
 7234: 	    &scantron_get_correction($r,$i,$scan_record,
 7235: 				     \%scantron_config,
 7236: 				     $line,'incorrectCODE',\%allcodes);
 7237: 	    return(1,$currentphase);
 7238: 	}
 7239: 	if (%allcodes && !exists($allcodes{$CODE}) 
 7240: 	    && !$$scan_record{'scantron.useCODE'}) {
 7241: 	    &scantron_get_correction($r,$i,$scan_record,
 7242: 				     \%scantron_config,
 7243: 				     $line,'incorrectCODE',\%allcodes);
 7244: 	    return(1,$currentphase);
 7245: 	}
 7246: 	if (exists($usedCODEs{$CODE}) 
 7247: 	    && $env{'form.scantron_CODEunique'} eq 'yes'
 7248: 	    && !$$scan_record{'scantron.CODE_ignore_dup'}) {
 7249: 	    &scantron_get_correction($r,$i,$scan_record,
 7250: 				     \%scantron_config,
 7251: 				     $line,'duplicateCODE',$usedCODEs{$CODE});
 7252: 	    return(1,$currentphase);
 7253: 	}
 7254: 	push(@{$usedCODEs{$CODE}},$$scan_record{'scantron.PaperID'});
 7255:     }
 7256:     return (0,$currentphase+1);
 7257: }
 7258: 
 7259: =pod
 7260: 
 7261: =item scantron_validate_doublebubble
 7262: 
 7263:    Validates all scanlines in the selected file to not have any
 7264:    bubble lines with multiple bubbles marked.
 7265: 
 7266: =cut
 7267: 
 7268: sub scantron_validate_doublebubble {
 7269:     my ($r,$currentphase) = @_;
 7270:     #get student info
 7271:     my $classlist=&Apache::loncoursedata::get_classlist();
 7272:     my %idmap=&username_to_idmap($classlist);
 7273: 
 7274:     #get scantron line setup
 7275:     my %scantron_config=&get_scantron_config($env{'form.scantron_format'});
 7276:     my ($scanlines,$scan_data)=&scantron_getfile();
 7277:     &scantron_get_maxbubble();	# parse needs the bubble line array.
 7278: 
 7279:     for (my $i=0;$i<=$scanlines->{'count'};$i++) {
 7280: 	my $line=&scantron_get_line($scanlines,$scan_data,$i);
 7281: 	if ($line=~/^[\s\cz]*$/) { next; }
 7282: 	my $scan_record=&scantron_parse_scanline($line,$i,\%scantron_config,
 7283: 						 $scan_data);
 7284: 	if (!defined($$scan_record{'scantron.doubleerror'})) { next; }
 7285: 	&scantron_get_correction($r,$i,$scan_record,\%scantron_config,$line,
 7286: 				 'doublebubble',
 7287: 				 $$scan_record{'scantron.doubleerror'});
 7288:     	return (1,$currentphase);
 7289:     }
 7290:     return (0,$currentphase+1);
 7291: }
 7292: 
 7293: 
 7294: sub scantron_get_maxbubble {
 7295:     if (defined($env{'form.scantron_maxbubble'}) &&
 7296: 	$env{'form.scantron_maxbubble'}) {
 7297: 	&restore_bubble_lines();
 7298: 	return $env{'form.scantron_maxbubble'};
 7299:     }
 7300: 
 7301:     my (undef, undef, $sequence) =
 7302: 	&Apache::lonnet::decode_symb($env{'form.selectpage'});
 7303: 
 7304:     my $navmap=Apache::lonnavmaps::navmap->new();
 7305:     my $map=$navmap->getResourceByUrl($sequence);
 7306:     my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0);
 7307: 
 7308:     &Apache::lonxml::clear_problem_counter();
 7309: 
 7310:     my $uname       = $env{'user.name'};
 7311:     my $udom        = $env{'user.domain'};
 7312:     my $cid         = $env{'request.course.id'};
 7313:     my $total_lines = 0;
 7314:     %bubble_lines_per_response = ();
 7315:     %first_bubble_line         = ();
 7316:     %subdivided_bubble_lines   = ();
 7317:     %responsetype_per_response = ();
 7318: 
 7319:     my $response_number = 0;
 7320:     my $bubble_line     = 0;
 7321:     foreach my $resource (@resources) {
 7322:         my ($analysis,$parts) = &scantron_partids_tograde($resource,$cid,$uname,$udom);
 7323:         if ((ref($analysis) eq 'HASH') && (ref($parts) eq 'ARRAY')) {
 7324: 	    foreach my $part_id (@{$parts}) {
 7325:                 my $lines;
 7326: 
 7327: 	        # TODO - make this a persistent hash not an array.
 7328: 
 7329:                 # optionresponse, matchresponse and rankresponse type items 
 7330:                 # render as separate sub-questions in exam mode.
 7331:                 if (($analysis->{$part_id.'.type'} eq 'optionresponse') ||
 7332:                     ($analysis->{$part_id.'.type'} eq 'matchresponse') ||
 7333:                     ($analysis->{$part_id.'.type'} eq 'rankresponse')) {
 7334:                     my ($numbub,$numshown);
 7335:                     if ($analysis->{$part_id.'.type'} eq 'optionresponse') {
 7336:                         if (ref($analysis->{$part_id.'.options'}) eq 'ARRAY') {
 7337:                             $numbub = scalar(@{$analysis->{$part_id.'.options'}});
 7338:                         }
 7339:                     } elsif ($analysis->{$part_id.'.type'} eq 'matchresponse') {
 7340:                         if (ref($analysis->{$part_id.'.items'}) eq 'ARRAY') {
 7341:                             $numbub = scalar(@{$analysis->{$part_id.'.items'}});
 7342:                         }
 7343:                     } elsif ($analysis->{$part_id.'.type'} eq 'rankresponse') {
 7344:                         if (ref($analysis->{$part_id.'.foils'}) eq 'ARRAY') {
 7345:                             $numbub = scalar(@{$analysis->{$part_id.'.foils'}});
 7346:                         }
 7347:                     }
 7348:                     if (ref($analysis->{$part_id.'.shown'}) eq 'ARRAY') {
 7349:                         $numshown = scalar(@{$analysis->{$part_id.'.shown'}});
 7350:                     }
 7351:                     my $bubbles_per_line = 10;
 7352:                     my $inner_bubble_lines = int($numbub/$bubbles_per_line);
 7353:                     if (($numbub % $bubbles_per_line) != 0) {
 7354:                         $inner_bubble_lines++;
 7355:                     }
 7356:                     for (my $i=0; $i<$numshown; $i++) {
 7357:                         $subdivided_bubble_lines{$response_number} .= 
 7358:                             $inner_bubble_lines.',';
 7359:                     }
 7360:                     $subdivided_bubble_lines{$response_number} =~ s/,$//;
 7361:                     $lines = $numshown * $inner_bubble_lines;
 7362:                 } else {
 7363:                     $lines = $analysis->{"$part_id.bubble_lines"};
 7364:                 } 
 7365: 
 7366:                 $first_bubble_line{$response_number} = $bubble_line;
 7367: 	        $bubble_lines_per_response{$response_number} = $lines;
 7368:                 $responsetype_per_response{$response_number} = 
 7369:                     $analysis->{$part_id.'.type'};
 7370: 	        $response_number++;
 7371: 
 7372: 	        $bubble_line +=  $lines;
 7373: 	        $total_lines +=  $lines;
 7374: 	    }
 7375:         }
 7376:     }
 7377:     &Apache::lonnet::delenv('scantron.');
 7378: 
 7379:     &save_bubble_lines();
 7380:     $env{'form.scantron_maxbubble'} =
 7381: 	$total_lines;
 7382:     return $env{'form.scantron_maxbubble'};
 7383: }
 7384: 
 7385: sub scantron_validate_missingbubbles {
 7386:     my ($r,$currentphase) = @_;
 7387:     #get student info
 7388:     my $classlist=&Apache::loncoursedata::get_classlist();
 7389:     my %idmap=&username_to_idmap($classlist);
 7390: 
 7391:     #get scantron line setup
 7392:     my %scantron_config=&get_scantron_config($env{'form.scantron_format'});
 7393:     my ($scanlines,$scan_data)=&scantron_getfile();
 7394:     my $max_bubble=&scantron_get_maxbubble();
 7395:     if (!$max_bubble) { $max_bubble=2**31; }
 7396:     for (my $i=0;$i<=$scanlines->{'count'};$i++) {
 7397: 	my $line=&scantron_get_line($scanlines,$scan_data,$i);
 7398: 	if ($line=~/^[\s\cz]*$/) { next; }
 7399: 	my $scan_record=&scantron_parse_scanline($line,$i,\%scantron_config,
 7400: 						 $scan_data);
 7401: 	if (!defined($$scan_record{'scantron.missingerror'})) { next; }
 7402: 	my @to_correct;
 7403: 	
 7404: 	# Probably here's where the error is...
 7405: 
 7406: 	foreach my $missing (@{$$scan_record{'scantron.missingerror'}}) {
 7407:             my $lastbubble;
 7408:             if ($missing =~ /^(\d+)\.(\d+)$/) {
 7409:                my $question = $1;
 7410:                my $subquestion = $2;
 7411:                if (!defined($first_bubble_line{$question -1})) { next; }
 7412:                my $first = $first_bubble_line{$question-1};
 7413:                my @subans = split(/,/,$subdivided_bubble_lines{$question-1});
 7414:                my $subcount = 1;
 7415:                while ($subcount<$subquestion) {
 7416:                    $first += $subans[$subcount-1];
 7417:                    $subcount ++;
 7418:                }
 7419:                my $count = $subans[$subquestion-1];
 7420:                $lastbubble = $first + $count;
 7421:             } else {
 7422:                 if (!defined($first_bubble_line{$missing - 1})) { next; }
 7423:                 $lastbubble = $first_bubble_line{$missing - 1} + $bubble_lines_per_response{$missing - 1};
 7424:             }
 7425:             if ($lastbubble > $max_bubble) { next; }
 7426: 	    push(@to_correct,$missing);
 7427: 	}
 7428: 	if (@to_correct) {
 7429: 	    &scantron_get_correction($r,$i,$scan_record,\%scantron_config,
 7430: 				     $line,'missingbubble',\@to_correct);
 7431: 	    return (1,$currentphase);
 7432: 	}
 7433: 
 7434:     }
 7435:     return (0,$currentphase+1);
 7436: }
 7437: 
 7438: 
 7439: sub scantron_process_students {
 7440:     my ($r) = @_;
 7441: 
 7442:     my (undef,undef,$sequence)=&Apache::lonnet::decode_symb($env{'form.selectpage'});
 7443:     my ($symb)=&get_symb($r);
 7444:     if (!$symb) {
 7445: 	return '';
 7446:     }
 7447:     my $default_form_data=&defaultFormData($symb);
 7448: 
 7449:     my %scantron_config=&get_scantron_config($env{'form.scantron_format'});
 7450:     my ($scanlines,$scan_data)=&scantron_getfile();
 7451:     my $classlist=&Apache::loncoursedata::get_classlist();
 7452:     my %idmap=&username_to_idmap($classlist);
 7453:     my $navmap=Apache::lonnavmaps::navmap->new();
 7454:     my $map=$navmap->getResourceByUrl($sequence);
 7455:     my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0);
 7456:     my (%grader_partids_by_symb,%grader_randomlists_by_symb);
 7457:     &graders_resources_pass(\@resources,\%grader_partids_by_symb,
 7458:                             \%grader_randomlists_by_symb);
 7459:     foreach my $resource (@resources) {
 7460:         my $ressymb = $resource->symb();
 7461:         my ($analysis,$parts) =
 7462:             &scantron_partids_tograde($resource,$env{'request.course.id'},
 7463:                                       $env{'user.name'},$env{'user.domain'},1);
 7464:         $grader_partids_by_symb{$ressymb} = $parts;
 7465:         if (ref($analysis) eq 'HASH') {
 7466:             if (ref($analysis->{'parts_withrandomlist'}) eq 'ARRAY') {
 7467:                 $grader_randomlists_by_symb{$ressymb} = 
 7468:                     $analysis->{'parts_withrandomlist'};
 7469:             }
 7470:         }
 7471:     }
 7472: 
 7473:     my ($uname,$udom);
 7474:     my $result= <<SCANTRONFORM;
 7475: <form method="post" enctype="multipart/form-data" action="/adm/grades" name="scantronupload">
 7476:   <input type="hidden" name="command" value="scantron_configphase" />
 7477:   $default_form_data
 7478: SCANTRONFORM
 7479:     $r->print($result);
 7480: 
 7481:     my @delayqueue;
 7482:     my (%completedstudents,%scandata);
 7483:     
 7484:     my $lock=&Apache::lonnet::set_lock(&mt('Grading bubblesheet exam'));
 7485:     my $count=&get_todo_count($scanlines,$scan_data);
 7486:     my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin($r,'Bubblesheet Status',
 7487:  				    'Bubblesheet Progress',$count,
 7488: 				    'inline',undef,'scantronupload');
 7489:     &Apache::lonhtmlcommon::Update_PrgWin($r,\%prog_state,
 7490: 					  'Processing first student');
 7491:     $r->print('<br />');
 7492:     my $start=&Time::HiRes::time();
 7493:     my $i=-1;
 7494:     my $started;
 7495: 
 7496:     &scantron_get_maxbubble();	# Need the bubble lines array to parse.
 7497: 
 7498:     # If an ssi failed in scantron_get_maxbubble, put an error message out to
 7499:     # the user and return.
 7500: 
 7501:     if ($ssi_error) {
 7502: 	$r->print("</form>");
 7503: 	&ssi_print_error($r);
 7504: 	$r->print(&show_grading_menu_form($symb));
 7505:         &Apache::lonnet::remove_lock($lock);
 7506: 	return '';		# Dunno why the other returns return '' rather than just returning.
 7507:     }
 7508: 
 7509:     my %lettdig = &letter_to_digits();
 7510:     my $numletts = scalar(keys(%lettdig));
 7511: 
 7512:     while ($i<$scanlines->{'count'}) {
 7513:  	($uname,$udom)=('','');
 7514:  	$i++;
 7515:  	my $line=&scantron_get_line($scanlines,$scan_data,$i);
 7516:  	if ($line=~/^[\s\cz]*$/) { next; }
 7517: 	if ($started) {
 7518: 	    &Apache::lonhtmlcommon::Increment_PrgWin($r,\%prog_state,
 7519: 						     'last student');
 7520: 	}
 7521: 	$started=1;
 7522:  	my $scan_record=&scantron_parse_scanline($line,$i,\%scantron_config,
 7523:  						 $scan_data);
 7524:  	unless ($uname=&scantron_find_student($scan_record,$scan_data,
 7525:  					      \%idmap,$i)) {
 7526:   	    &scantron_add_delay(\@delayqueue,$line,
 7527:  				'Unable to find a student that matches',1);
 7528:  	    next;
 7529:   	}
 7530:  	if (exists $completedstudents{$uname}) {
 7531:  	    &scantron_add_delay(\@delayqueue,$line,
 7532:  				'Student '.$uname.' has multiple sheets',2);
 7533:  	    next;
 7534:  	}
 7535:   	($uname,$udom)=split(/:/,$uname);
 7536: 
 7537:         my %partids_by_symb;
 7538:         foreach my $resource (@resources) {
 7539:             my $ressymb = $resource->symb();
 7540:             if ((exists($grader_randomlists_by_symb{$ressymb})) ||
 7541:                 (ref($grader_partids_by_symb{$ressymb}) ne 'ARRAY')) {
 7542:                 my ($analysis,$parts) =
 7543:                     &scantron_partids_tograde($resource,$env{'request.course.id'},$uname,$udom);
 7544:                 $partids_by_symb{$ressymb} = $parts;
 7545:             } else {
 7546:                 $partids_by_symb{$ressymb} = $grader_partids_by_symb{$ressymb};
 7547:             }
 7548:         }
 7549: 
 7550: 	&Apache::lonxml::clear_problem_counter();
 7551:   	&Apache::lonnet::appenv($scan_record);
 7552: 
 7553: 	if (&scantron_clear_skip($scanlines,$scan_data,$i)) {
 7554: 	    &scantron_putfile($scanlines,$scan_data);
 7555: 	}
 7556: 	
 7557:         my $scancode;
 7558:         if ((exists($scan_record->{'scantron.CODE'})) &&
 7559:             (&Apache::lonnet::validCODE($scan_record->{'scantron.CODE'}))) {
 7560:             $scancode = $scan_record->{'scantron.CODE'};
 7561:         } else {
 7562:             $scancode = '';
 7563:         }
 7564: 
 7565:         if (&grade_student_bubbles($r,$uname,$udom,$scan_record,$scancode,
 7566:                                    \@resources,\%partids_by_symb) eq 'ssi_error') {
 7567:             $ssi_error = 0; # So end of handler error message does not trigger.
 7568:             $r->print("</form>");
 7569:             &ssi_print_error($r);
 7570:             $r->print(&show_grading_menu_form($symb));
 7571:             &Apache::lonnet::remove_lock($lock);
 7572:             return '';      # Why return ''?  Beats me.
 7573:         }
 7574: 
 7575: 	$completedstudents{$uname}={'line'=>$line};
 7576:         if ($env{'form.verifyrecord'}) {
 7577:             my $lastpos = $env{'form.scantron_maxbubble'}*$scantron_config{'Qlength'};
 7578:             my $studentdata = substr($line,$scantron_config{'Qstart'}-1,$lastpos);
 7579:             chomp($studentdata);
 7580:             $studentdata =~ s/\r$//;
 7581:             my $studentrecord = '';
 7582:             my $counter = -1;
 7583:             foreach my $resource (@resources) {
 7584:                 my $ressymb = $resource->symb();
 7585:                 ($counter,my $recording) =
 7586:                     &verify_scantron_grading($resource,$udom,$uname,$env{'request.course.id'},
 7587:                                              $counter,$studentdata,$partids_by_symb{$ressymb},
 7588:                                              \%scantron_config,\%lettdig,$numletts);
 7589:                 $studentrecord .= $recording;
 7590:             }
 7591:             if ($studentrecord ne $studentdata) {
 7592:                 &Apache::lonxml::clear_problem_counter();
 7593:                 if (&grade_student_bubbles($r,$uname,$udom,$scan_record,$scancode,
 7594:                                            \@resources,\%partids_by_symb) eq 'ssi_error') {
 7595:                     $ssi_error = 0; # So end of handler error message does not trigger.
 7596:                     $r->print("</form>");
 7597:                     &ssi_print_error($r);
 7598:                     $r->print(&show_grading_menu_form($symb));
 7599:                     &Apache::lonnet::remove_lock($lock);
 7600:                     delete($completedstudents{$uname});
 7601:                     return '';
 7602:                 }
 7603:                 $counter = -1;
 7604:                 $studentrecord = '';
 7605:                 foreach my $resource (@resources) {
 7606:                     my $ressymb = $resource->symb();
 7607:                     ($counter,my $recording) =
 7608:                         &verify_scantron_grading($resource,$udom,$uname,$env{'request.course.id'},
 7609:                                                  $counter,$studentdata,$partids_by_symb{$ressymb},
 7610:                                                  \%scantron_config,\%lettdig,$numletts);
 7611:                     $studentrecord .= $recording;
 7612:                 }
 7613:                 if ($studentrecord ne $studentdata) {
 7614:                     $r->print('<p><span class="LC_error">');
 7615:                     if ($scancode eq '') {
 7616:                         $r->print(&mt('Mismatch grading bubble sheet for user: [_1] with ID: [_2].',
 7617:                                   $uname.':'.$udom,$scan_record->{'scantron.ID'}));
 7618:                     } else {
 7619:                         $r->print(&mt('Mismatch grading bubble sheet for user: [_1] with ID: [_2] and CODE: [_3].',
 7620:                                   $uname.':'.$udom,$scan_record->{'scantron.ID'},$scancode));
 7621:                     }
 7622:                     $r->print('</span><br />'.&Apache::loncommon::start_data_table()."\n".
 7623:                               &Apache::loncommon::start_data_table_header_row()."\n".
 7624:                               '<th>'.&mt('Source').'</th><th>'.&mt('Bubbled responses').'</th>'.
 7625:                               &Apache::loncommon::end_data_table_header_row()."\n".
 7626:                               &Apache::loncommon::start_data_table_row().
 7627:                               '<td>'.&mt('Bubble Sheet').'</td>'.
 7628:                               '<td><span class="LC_nobreak">'.$studentdata.'</span></td>'.
 7629:                               &Apache::loncommon::end_data_table_row().
 7630:                               &Apache::loncommon::start_data_table_row().
 7631:                               '<td>Stored submissions</td>'.
 7632:                               '<td><span class="LC_nobreak">'.$studentrecord.'</span></td>'."\n".
 7633:                               &Apache::loncommon::end_data_table_row().
 7634:                               &Apache::loncommon::end_data_table().'</p>');
 7635:                 } else {
 7636:                     $r->print('<br /><span class="LC_warning">'.
 7637:                              &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 />'.
 7638:                              &mt("As a consequence, this user's submission history records two tries.").
 7639:                                  '</span><br />');
 7640:                 }
 7641:             }
 7642:         }
 7643:         if (&Apache::loncommon::connection_aborted($r)) { last; }
 7644:     } continue {
 7645: 	&Apache::lonxml::clear_problem_counter();
 7646: 	&Apache::lonnet::delenv('scantron.');
 7647:     }
 7648:     &Apache::lonhtmlcommon::Close_PrgWin($r,\%prog_state);
 7649:     &Apache::lonnet::remove_lock($lock);
 7650: #    my $lasttime = &Time::HiRes::time()-$start;
 7651: #    $r->print("<p>took $lasttime</p>");
 7652: 
 7653:     $r->print("</form>");
 7654:     $r->print(&show_grading_menu_form($symb));
 7655:     return '';
 7656: }
 7657: 
 7658: sub graders_resources_pass {
 7659:     my ($resources,$grader_partids_by_symb,$grader_randomlists_by_symb) = @_;
 7660:     if ((ref($resources) eq 'ARRAY') && (ref($grader_partids_by_symb)) && 
 7661:         (ref($grader_randomlists_by_symb) eq 'HASH')) {
 7662:         foreach my $resource (@{$resources}) {
 7663:             my $ressymb = $resource->symb();
 7664:             my ($analysis,$parts) =
 7665:                 &scantron_partids_tograde($resource,$env{'request.course.id'},
 7666:                                           $env{'user.name'},$env{'user.domain'},1);
 7667:             $grader_partids_by_symb->{$ressymb} = $parts;
 7668:             if (ref($analysis) eq 'HASH') {
 7669:                 if (ref($analysis->{'parts_withrandomlist'}) eq 'ARRAY') {
 7670:                     $grader_randomlists_by_symb->{$ressymb} =
 7671:                         $analysis->{'parts_withrandomlist'};
 7672:                 }
 7673:             }
 7674:         }
 7675:     }
 7676:     return;
 7677: }
 7678: 
 7679: sub grade_student_bubbles {
 7680:     my ($r,$uname,$udom,$scan_record,$scancode,$resources,$parts) = @_;
 7681:     if (ref($resources) eq 'ARRAY') {
 7682:         my $count = 0;
 7683:         foreach my $resource (@{$resources}) {
 7684:             my $ressymb = $resource->symb();
 7685:             my %form = ('submitted'      => 'scantron',
 7686:                         'grade_target'   => 'grade',
 7687:                         'grade_username' => $uname,
 7688:                         'grade_domain'   => $udom,
 7689:                         'grade_courseid' => $env{'request.course.id'},
 7690:                         'grade_symb'     => $ressymb,
 7691:                         'CODE'           => $scancode
 7692:                        );
 7693:             if (ref($parts) eq 'HASH') {
 7694:                 if (ref($parts->{$ressymb}) eq 'ARRAY') {
 7695:                     foreach my $part (@{$parts->{$ressymb}}) {
 7696:                         $form{'scantron_questnum_start.'.$part} =
 7697:                             1+$env{'form.scantron.first_bubble_line.'.$count};
 7698:                         $count++;
 7699:                     }
 7700:                 }
 7701:             }
 7702:             my $result=&ssi_with_retries($resource->src(),$ssi_retries,%form);
 7703:             return 'ssi_error' if ($ssi_error);
 7704:             last if (&Apache::loncommon::connection_aborted($r));
 7705:         }
 7706:     }
 7707:     return;
 7708: }
 7709: 
 7710: sub scantron_upload_scantron_data {
 7711:     my ($r)=@_;
 7712:     my $dom = $env{'request.role.domain'};
 7713:     my $domdesc = &Apache::lonnet::domain($dom,'description');
 7714:     $r->print(&Apache::loncommon::coursebrowser_javascript($dom));
 7715:     my $select_link=&Apache::loncommon::selectcourse_link('rules','courseid',
 7716: 							  'domainid',
 7717: 							  'coursename',$dom);
 7718:     my $syllabuslink = '<a href="javascript:ToSyllabus();">'.&mt('Syllabus').'</a>'.
 7719:                        ('&nbsp'x2).&mt('(shows course personnel)'); 
 7720:     my $default_form_data=&defaultFormData(&get_symb($r,1));
 7721:     $r->print('
 7722: <script type="text/javascript" language="javascript">
 7723:     function checkUpload(formname) {
 7724: 	if (formname.upfile.value == "") {
 7725: 	    alert("'.&mt('Please use the browse button to select a file from your local directory.').'");
 7726: 	    return false;
 7727: 	}
 7728:         if (formname.courseid.value == "") {
 7729:             alert("'.&mt('Please use the \"Select Course\" link to open a separate window where you can search for a course to which a file can be uploaded.').'");
 7730:             return false;
 7731:         }
 7732: 	formname.submit();
 7733:     }
 7734: 
 7735:     function ToSyllabus() {
 7736:         var cdom = '."'$dom'".';
 7737:         var cnum = document.rules.courseid.value;
 7738:         if (cdom == "" || cdom == null) {
 7739:             return;
 7740:         }
 7741:         if (cnum == "" || cnum == null) {
 7742:            return;
 7743:         }
 7744:         syllwin=window.open("/public/"+cdom+"/"+cnum+"/syllabus","LONCAPASyllabus",
 7745:                             "height=350,width=350,scrollbars=yes,menubar=no");
 7746:         return;
 7747:     }
 7748: 
 7749: </script>
 7750: 
 7751: <h3>'.&mt('Send scanned bubblesheet data to a course').'</h3>
 7752: 
 7753: <form enctype="multipart/form-data" action="/adm/grades" name="rules" method="post">
 7754: '.$default_form_data.
 7755:   &Apache::lonhtmlcommon::start_pick_box().
 7756:   &Apache::lonhtmlcommon::row_title(&mt('Course ID')).
 7757:   '<input name="courseid" type="text" size="30" />'.$select_link.
 7758:   &Apache::lonhtmlcommon::row_closure().
 7759:   &Apache::lonhtmlcommon::row_title(&mt('Course Name')).
 7760:   '<input name="coursename" type="text" size="30" />'.$syllabuslink.
 7761:   &Apache::lonhtmlcommon::row_closure().
 7762:   &Apache::lonhtmlcommon::row_title(&mt('Domain')).
 7763:   '<input name="domainid" type="hidden" />'.$domdesc.
 7764:   &Apache::lonhtmlcommon::row_closure().
 7765:   &Apache::lonhtmlcommon::row_title(&mt('File to upload')).
 7766:   '<input type="file" name="upfile" size="50" />'.
 7767:   &Apache::lonhtmlcommon::row_closure(1).
 7768:   &Apache::lonhtmlcommon::end_pick_box().'<br />
 7769: 
 7770: <input name="command" value="scantronupload_save" type="hidden" />
 7771: <input type="button" onClick="javascript:checkUpload(this.form);" value="'.&mt('Upload Bubblesheet Data').'" />
 7772: </form>
 7773: ');
 7774:     return '';
 7775: }
 7776: 
 7777: 
 7778: sub scantron_upload_scantron_data_save {
 7779:     my($r)=@_;
 7780:     my ($symb)=&get_symb($r,1);
 7781:     my $doanotherupload=
 7782: 	'<br /><form action="/adm/grades" method="post">'."\n".
 7783: 	'<input type="hidden" name="command" value="scantronupload" />'."\n".
 7784: 	'<input type="submit" name="submit" value="'.&mt('Do Another Upload').'" />'."\n".
 7785: 	'</form>'."\n";
 7786:     if (!&Apache::lonnet::allowed('usc',$env{'form.domainid'}) &&
 7787: 	!&Apache::lonnet::allowed('usc',
 7788: 			    $env{'form.domainid'}.'_'.$env{'form.courseid'})) {
 7789: 	$r->print(&mt("You are not allowed to upload bubblesheet data to the requested course.")."<br />");
 7790: 	if ($symb) {
 7791: 	    $r->print(&show_grading_menu_form($symb));
 7792: 	} else {
 7793: 	    $r->print($doanotherupload);
 7794: 	}
 7795: 	return '';
 7796:     }
 7797:     my %coursedata=&Apache::lonnet::coursedescription($env{'form.domainid'}.'_'.$env{'form.courseid'});
 7798:     my $uploadedfile;
 7799:     $r->print('<h3>'.&mt("Uploading file to [_1]",$coursedata{'description'}).'</h3>');
 7800:     if (length($env{'form.upfile'}) < 2) {
 7801:         $r->print(&mt('[_1]Error:[_2] The file you attempted to upload, [_3] contained no information. Please check that you entered the correct filename.','<span class="LC_error">','</span>','<span class="LC_filename">'.&HTML::Entities::encode($env{'form.upfile.filename'},'<>&"').'</span>'));
 7802:     } else {
 7803:         my $result = 
 7804:             &Apache::lonnet::userfileupload('upfile','','scantron','','','',
 7805:                                             $env{'form.courseid'},$env{'form.domainid'});
 7806: 	if ($result =~ m{^/uploaded/}) {
 7807: 	    $r->print(&mt('[_1]Success:[_2] Successfully uploaded [_3] bytes of data into location: [_4]',
 7808:                           '<span class="LC_success">','</span>',(length($env{'form.upfile'})-1),
 7809: 			  '<span class="LC_filename">'.$result.'</span>'));
 7810:             ($uploadedfile) = ($result =~ m{/([^/]+)$});
 7811:             $r->print(&validate_uploaded_scantron_file($env{'form.domainid'},
 7812:                                                        $env{'form.courseid'},$uploadedfile));
 7813: 	} else {
 7814: 	    $r->print(&mt('[_1]Error:[_2] An error ([_3]) occurred when attempting to upload the file, [_4]',
 7815:                           '<span class="LC_error">','</span>',$result,
 7816: 			  '<span class="LC_filename">'.&HTML::Entities::encode($env{'form.upfile.filename'},'<>&"').'</span>'));
 7817: 	}
 7818:     }
 7819:     if ($symb) {
 7820: 	$r->print(&scantron_selectphase($r,$uploadedfile));
 7821:     } else {
 7822: 	$r->print($doanotherupload);
 7823:     }
 7824:     return '';
 7825: }
 7826: 
 7827: sub validate_uploaded_scantron_file {
 7828:     my ($cdom,$cname,$fname) = @_;
 7829:     my $scanlines=&Apache::lonnet::getfile('/uploaded/'.$cdom.'/'.$cname.'/'.$fname);
 7830:     my @lines;
 7831:     if ($scanlines ne '-1') {
 7832:         @lines=split("\n",$scanlines,-1);
 7833:     }
 7834:     my $output;
 7835:     if (@lines) {
 7836:         my (%counts,$max_match_format);
 7837:         my ($max_match_count,$max_match_pct) = (0,0);
 7838:         my $classlist = &Apache::loncoursedata::get_classlist($cdom,$cname);
 7839:         my %idmap = &username_to_idmap($classlist);
 7840:         foreach my $key (keys(%idmap)) {
 7841:             my $lckey = lc($key);
 7842:             $idmap{$lckey} = $idmap{$key};
 7843:         }
 7844:         my %unique_formats;
 7845:         my @formatlines = &get_scantronformat_file();
 7846:         foreach my $line (@formatlines) {
 7847:             chomp($line);
 7848:             my @config = split(/:/,$line);
 7849:             my $idstart = $config[5];
 7850:             my $idlength = $config[6];
 7851:             if (($idstart ne '') && ($idlength > 0)) {
 7852:                 if (ref($unique_formats{$idstart.':'.$idlength}) eq 'ARRAY') {
 7853:                     push(@{$unique_formats{$idstart.':'.$idlength}},$config[0].':'.$config[1]); 
 7854:                 } else {
 7855:                     $unique_formats{$idstart.':'.$idlength} = [$config[0].':'.$config[1]];
 7856:                 }
 7857:             }
 7858:         }
 7859:         foreach my $key (keys(%unique_formats)) {
 7860:             my ($idstart,$idlength) = split(':',$key);
 7861:             %{$counts{$key}} = (
 7862:                                'found'   => 0,
 7863:                                'total'   => 0,
 7864:                               );
 7865:             foreach my $line (@lines) {
 7866:                 next if ($line =~ /^#/);
 7867:                 next if ($line =~ /^[\s\cz]*$/);
 7868:                 my $id = substr($line,$idstart-1,$idlength);
 7869:                 $id = lc($id);
 7870:                 if (exists($idmap{$id})) {
 7871:                     $counts{$key}{'found'} ++;
 7872:                 }
 7873:                 $counts{$key}{'total'} ++;
 7874:             }
 7875:             if ($counts{$key}{'total'}) {
 7876:                 my $percent_match = (100*$counts{$key}{'found'})/($counts{$key}{'total'});
 7877:                 if (($max_match_format eq '') || ($percent_match > $max_match_pct)) {
 7878:                     $max_match_pct = $percent_match;
 7879:                     $max_match_format = $key;
 7880:                     $max_match_count = $counts{$key}{'total'};
 7881:                 }
 7882:             }
 7883:         }
 7884:         if (ref($unique_formats{$max_match_format}) eq 'ARRAY') {
 7885:             my $format_descs;
 7886:             my $numwithformat = @{$unique_formats{$max_match_format}};
 7887:             for (my $i=0; $i<$numwithformat; $i++) {
 7888:                 my ($name,$desc) = split(':',$unique_formats{$max_match_format}[$i]);
 7889:                 if ($i<$numwithformat-2) {
 7890:                     $format_descs .= '"<i>'.$desc.'</i>", ';
 7891:                 } elsif ($i==$numwithformat-2) {
 7892:                     $format_descs .= '"<i>'.$desc.'</i>" '.&mt('and').' ';
 7893:                 } elsif ($i==$numwithformat-1) {
 7894:                     $format_descs .= '"<i>'.$desc.'</i>"';
 7895:                 }
 7896:             }
 7897:             my $showpct = sprintf("%.0f",$max_match_pct).'%';
 7898:             $output .= '<br />'.&mt('Comparison of student IDs in the uploaded file with the course roster found matches for [_1] of the [_2] entries in the file (for the format defined for [_3]).','<b>'.$showpct.'</b>','<b>'.$max_match_count.'</b>',$format_descs).
 7899:                        '<br />'.&mt('A low percentage of matches results from one of the following:').'<ul>'.
 7900:                        '<li>'.&mt('The file was uploaded to the wrong course').'</li>'.
 7901:                        '<li>'.&mt('The data are not in the format expected for the domain: [_1]',
 7902:                                   '<i>'.$cdom.'</i>').'</li>'.
 7903:                        '<li>'.&mt('Students did not bubble their IDs, or mis-bubbled them').'</li>'.
 7904:                        '<li>'.&mt('The course roster is not up to date').'</li>'.
 7905:                        '</ul>';
 7906:         }
 7907:     } else {
 7908:         $output = '<span class="LC_warning">'.&mt('Uploaded file contained no data').'</span>';
 7909:     }
 7910:     return $output;
 7911: }
 7912: 
 7913: sub valid_file {
 7914:     my ($requested_file)=@_;
 7915:     foreach my $filename (sort(&scantron_filenames())) {
 7916: 	if ($requested_file eq $filename) { return 1; }
 7917:     }
 7918:     return 0;
 7919: }
 7920: 
 7921: sub scantron_download_scantron_data {
 7922:     my ($r)=@_;
 7923:     my $default_form_data=&defaultFormData(&get_symb($r,1));
 7924:     my $cname=$env{'course.'.$env{'request.course.id'}.'.num'};
 7925:     my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'};
 7926:     my $file=$env{'form.scantron_selectfile'};
 7927:     if (! &valid_file($file)) {
 7928: 	$r->print('
 7929: 	<p>
 7930: 	    '.&mt('The requested file name was invalid.').'
 7931:         </p>
 7932: ');
 7933: 	$r->print(&show_grading_menu_form(&get_symb($r,1)));
 7934: 	return;
 7935:     }
 7936:     my $orig='/uploaded/'.$cdom.'/'.$cname.'/scantron_orig_'.$file;
 7937:     my $corrected='/uploaded/'.$cdom.'/'.$cname.'/scantron_corrected_'.$file;
 7938:     my $skipped='/uploaded/'.$cdom.'/'.$cname.'/scantron_skipped_'.$file;
 7939:     &Apache::lonnet::allowuploaded('/adm/grades',$orig);
 7940:     &Apache::lonnet::allowuploaded('/adm/grades',$corrected);
 7941:     &Apache::lonnet::allowuploaded('/adm/grades',$skipped);
 7942:     $r->print('
 7943:     <p>
 7944: 	'.&mt('[_1]Original[_2] file as uploaded by the scantron office.',
 7945: 	      '<a href="'.$orig.'">','</a>').'
 7946:     </p>
 7947:     <p>
 7948: 	'.&mt('[_1]Corrections[_2], a file of corrected records that were used in grading.',
 7949: 	      '<a href="'.$corrected.'">','</a>').'
 7950:     </p>
 7951:     <p>
 7952: 	'.&mt('[_1]Skipped[_2], a file of records that were skipped.',
 7953: 	      '<a href="'.$skipped.'">','</a>').'
 7954:     </p>
 7955: ');
 7956:     $r->print(&show_grading_menu_form(&get_symb($r,1)));
 7957:     return '';
 7958: }
 7959: 
 7960: sub checkscantron_results {
 7961:     my ($r) = @_;
 7962:     my ($symb)=&get_symb($r);
 7963:     if (!$symb) {return '';}
 7964:     my $grading_menu_button=&show_grading_menu_form($symb);
 7965:     my $cid = $env{'request.course.id'};
 7966:     my %lettdig = &letter_to_digits();
 7967:     my $numletts = scalar(keys(%lettdig));
 7968:     my $cnum = $env{'course.'.$cid.'.num'};
 7969:     my $cdom = $env{'course.'.$cid.'.domain'};
 7970:     my (undef, undef, $sequence) = &Apache::lonnet::decode_symb($env{'form.selectpage'});
 7971:     my %record;
 7972:     my %scantron_config =
 7973:         &Apache::grades::get_scantron_config($env{'form.scantron_format'});
 7974:     my ($scanlines,$scan_data)=&Apache::grades::scantron_getfile();
 7975:     my $classlist=&Apache::loncoursedata::get_classlist();
 7976:     my %idmap=&Apache::grades::username_to_idmap($classlist);
 7977:     my $navmap=Apache::lonnavmaps::navmap->new();
 7978:     my $map=$navmap->getResourceByUrl($sequence);
 7979:     my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0);
 7980:     my (%grader_partids_by_symb,%grader_randomlists_by_symb);
 7981:     &graders_resources_pass(\@resources,\%grader_partids_by_symb,                             \%grader_randomlists_by_symb);
 7982: 
 7983:     my ($uname,$udom);
 7984:     my (%scandata,%lastname,%bylast);
 7985:     $r->print('
 7986: <form method="post" enctype="multipart/form-data" action="/adm/grades" name="checkscantron">'."\n");
 7987: 
 7988:     my @delayqueue;
 7989:     my %completedstudents;
 7990: 
 7991:     my $count=&Apache::grades::get_todo_count($scanlines,$scan_data);
 7992:     my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin($r,'Scantron/Submissions Comparison Status',
 7993:                                     'Progress of Scantron Data/Submission Records Comparison',$count,
 7994:                                     'inline',undef,'checkscantron');
 7995:     my ($username,$domain,$started);
 7996: 
 7997:     &scantron_get_maxbubble();  # Need the bubble lines array to parse.
 7998: 
 7999:     &Apache::lonhtmlcommon::Update_PrgWin($r,\%prog_state,
 8000:                                           'Processing first student');
 8001:     my $start=&Time::HiRes::time();
 8002:     my $i=-1;
 8003: 
 8004:     while ($i<$scanlines->{'count'}) {
 8005:         ($username,$domain,$uname)=('','','');
 8006:         $i++;
 8007:         my $line=&Apache::grades::scantron_get_line($scanlines,$scan_data,$i);
 8008:         if ($line=~/^[\s\cz]*$/) { next; }
 8009:         if ($started) {
 8010:             &Apache::lonhtmlcommon::Increment_PrgWin($r,\%prog_state,
 8011:                                                      'last student');
 8012:         }
 8013:         $started=1;
 8014:         my $scan_record=
 8015:             &Apache::grades::scantron_parse_scanline($line,$i,\%scantron_config,
 8016:                                                      $scan_data);
 8017:         unless ($uname=&Apache::grades::scantron_find_student($scan_record,$scan_data,
 8018:                                                               \%idmap,$i)) {
 8019:             &Apache::grades::scantron_add_delay(\@delayqueue,$line,
 8020:                                 'Unable to find a student that matches',1);
 8021:             next;
 8022:         }
 8023:         if (exists $completedstudents{$uname}) {
 8024:             &Apache::grades::scantron_add_delay(\@delayqueue,$line,
 8025:                                 'Student '.$uname.' has multiple sheets',2);
 8026:             next;
 8027:         }
 8028:         my $pid = $scan_record->{'scantron.ID'};
 8029:         $lastname{$pid} = $scan_record->{'scantron.LastName'};
 8030:         push(@{$bylast{$lastname{$pid}}},$pid);
 8031:         my $lastpos = $env{'form.scantron_maxbubble'}*$scantron_config{'Qlength'};
 8032:         $scandata{$pid} = substr($line,$scantron_config{'Qstart'}-1,$lastpos);
 8033:         chomp($scandata{$pid});
 8034:         $scandata{$pid} =~ s/\r$//;
 8035:         ($username,$domain)=split(/:/,$uname);
 8036:         my $counter = -1;
 8037:         foreach my $resource (@resources) {
 8038:             my $parts;
 8039:             my $ressymb = $resource->symb();
 8040:             if ((exists($grader_randomlists_by_symb{$ressymb})) ||
 8041:                 (ref($grader_partids_by_symb{$ressymb}) ne 'ARRAY')) {
 8042:                 (my $analysis,$parts) =
 8043:                     &scantron_partids_tograde($resource,$env{'request.course.id'},$username,$domain);
 8044:             } else {
 8045:                 $parts = $grader_partids_by_symb{$ressymb};
 8046:             }
 8047:             ($counter,my $recording) =
 8048:                 &verify_scantron_grading($resource,$domain,$username,$cid,$counter,
 8049:                                          $scandata{$pid},$parts,
 8050:                                          \%scantron_config,\%lettdig,$numletts);
 8051:             $record{$pid} .= $recording;
 8052:         }
 8053:     }
 8054:     &Apache::lonhtmlcommon::Close_PrgWin($r,\%prog_state);
 8055:     $r->print('<br />');
 8056:     my ($okstudents,$badstudents,$numstudents,$passed,$failed);
 8057:     $passed = 0;
 8058:     $failed = 0;
 8059:     $numstudents = 0;
 8060:     foreach my $last (sort(keys(%bylast))) {
 8061:         if (ref($bylast{$last}) eq 'ARRAY') {
 8062:             foreach my $pid (sort(@{$bylast{$last}})) {
 8063:                 my $showscandata = $scandata{$pid};
 8064:                 my $showrecord = $record{$pid};
 8065:                 $showscandata =~ s/\s/&nbsp;/g;
 8066:                 $showrecord =~ s/\s/&nbsp;/g;
 8067:                 if ($scandata{$pid} eq $record{$pid}) {
 8068:                     my $css_class = ($passed % 2)?'LC_odd_row':'LC_even_row';
 8069:                     $okstudents .= '<tr class="'.$css_class.'">'.
 8070: '<td>'.&mt('Scantron').'</td><td>'.$showscandata.'</td><td rowspan="2">'.$last.'</td><td rowspan="2">'.$pid.'</td>'."\n".
 8071: '</tr>'."\n".
 8072: '<tr class="'.$css_class.'">'."\n".
 8073: '<td>Submissions</td><td>'.$showrecord.'</td></tr>'."\n";
 8074:                     $passed ++;
 8075:                 } else {
 8076:                     my $css_class = ($failed % 2)?'LC_odd_row':'LC_even_row';
 8077:                     $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".
 8078: '</tr>'."\n".
 8079: '<tr class="'.$css_class.'">'."\n".
 8080: '<td>Submissions</td><td><span class="LC_nobreak">'.$record{$pid}.'</span></td>'."\n".
 8081: '</tr>'."\n";
 8082:                     $failed ++;
 8083:                 }
 8084:                 $numstudents ++;
 8085:             }
 8086:         }
 8087:     }
 8088:     $r->print('<p>'.&mt('Comparison of bubblesheet 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>');
 8089:     $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>');
 8090:     if ($passed) {
 8091:         $r->print(&mt('Students with exact correspondence between bubblesheet data and submissions are as follows:').'<br /><br />');
 8092:         $r->print(&Apache::loncommon::start_data_table()."\n".
 8093:                  &Apache::loncommon::start_data_table_header_row()."\n".
 8094:                  '<th>'.&mt('Source').'</th><th>'.&mt('Bubble records').'</th><th>'.&mt('Name').'</th><th>'.&mt('ID').'</th>'.
 8095:                  &Apache::loncommon::end_data_table_header_row()."\n".
 8096:                  $okstudents."\n".
 8097:                  &Apache::loncommon::end_data_table().'<br />');
 8098:     }
 8099:     if ($failed) {
 8100:         $r->print(&mt('Students with differences between bubblesheet data and submissions are as follows:').'<br /><br />');
 8101:         $r->print(&Apache::loncommon::start_data_table()."\n".
 8102:                  &Apache::loncommon::start_data_table_header_row()."\n".
 8103:                  '<th>'.&mt('Source').'</th><th>'.&mt('Bubble records').'</th><th>'.&mt('Name').'</th><th>'.&mt('ID').'</th>'.
 8104:                  &Apache::loncommon::end_data_table_header_row()."\n".
 8105:                  $badstudents."\n".
 8106:                  &Apache::loncommon::end_data_table()).'<br />'.
 8107:                  &mt('Differences can occur if submissions were modified using manual grading after a bubblesheet grading pass.').'<br />'.&mt('If unexpected discrepancies were detected, it is recommended that you inspect the original bubblesheets.');  
 8108:     }
 8109:     $r->print('</form><br />'.$grading_menu_button);
 8110:     return;
 8111: }
 8112: 
 8113: sub verify_scantron_grading {
 8114:     my ($resource,$domain,$username,$cid,$counter,$scandata,$partids,
 8115:         $scantron_config,$lettdig,$numletts) = @_;
 8116:     my ($record,%expected,%startpos);
 8117:     return ($counter,$record) if (!ref($resource));
 8118:     return ($counter,$record) if (!$resource->is_problem());
 8119:     my $symb = $resource->symb();
 8120:     return ($counter,$record) if (ref($partids) ne 'ARRAY');
 8121:     foreach my $part_id (@{$partids}) {
 8122:         $counter ++;
 8123:         $expected{$part_id} = 0;
 8124:         if ($env{"form.scantron.sub_bubblelines.$counter"}) {
 8125:             my @sub_lines = split(/,/,$env{"form.scantron.sub_bubblelines.$counter"});
 8126:             foreach my $item (@sub_lines) {
 8127:                 $expected{$part_id} += $item;
 8128:             }
 8129:         } else {
 8130:             $expected{$part_id} = $env{"form.scantron.bubblelines.$counter"};
 8131:         }
 8132:         $startpos{$part_id} = $env{"form.scantron.first_bubble_line.$counter"};
 8133:     }
 8134:     if ($symb) {
 8135:         my %recorded;
 8136:         my (%returnhash) = &Apache::lonnet::restore($symb,$cid,$domain,$username);
 8137:         if ($returnhash{'version'}) {
 8138:             my %lasthash=();
 8139:             my $version;
 8140:             for ($version=1;$version<=$returnhash{'version'};$version++) {
 8141:                 foreach my $key (sort(split(/\:/,$returnhash{$version.':keys'}))) {
 8142:                     $lasthash{$key}=$returnhash{$version.':'.$key};
 8143:                 }
 8144:             }
 8145:             foreach my $key (keys(%lasthash)) {
 8146:                 if ($key =~ /\.scantron$/) {
 8147:                     my $value = &unescape($lasthash{$key});
 8148:                     my ($part_id) = ($key =~ /^resource\.(.+)\.scantron$/);
 8149:                     if ($value eq '') {
 8150:                         for (my $i=0; $i<$expected{$part_id}; $i++) {
 8151:                             for (my $j=0; $j<$scantron_config->{'length'}; $j++) {
 8152:                                 $recorded{$part_id} .= $scantron_config->{'Qoff'};
 8153:                             }
 8154:                         }
 8155:                     } else {
 8156:                         my @tocheck;
 8157:                         my @items = split(//,$value);
 8158:                         if (($scantron_config->{'Qon'} eq 'letter') ||
 8159:                             ($scantron_config->{'Qon'} eq 'number')) {
 8160:                             if (@items < $expected{$part_id}) {
 8161:                                 my $fragment = substr($scandata,$startpos{$part_id},$expected{$part_id});
 8162:                                 my @singles = split(//,$fragment);
 8163:                                 foreach my $pos (@singles) {
 8164:                                     if ($pos eq ' ') {
 8165:                                         push(@tocheck,$pos);
 8166:                                     } else {
 8167:                                         my $next = shift(@items);
 8168:                                         push(@tocheck,$next);
 8169:                                     }
 8170:                                 }
 8171:                             } else {
 8172:                                 @tocheck = @items;
 8173:                             }
 8174:                             foreach my $letter (@tocheck) {
 8175:                                 if ($scantron_config->{'Qon'} eq 'letter') {
 8176:                                     if ($letter !~ /^[A-J]$/) {
 8177:                                         $letter = $scantron_config->{'Qoff'};
 8178:                                     }
 8179:                                     $recorded{$part_id} .= $letter;
 8180:                                 } elsif ($scantron_config->{'Qon'} eq 'number') {
 8181:                                     my $digit;
 8182:                                     if ($letter !~ /^[A-J]$/) {
 8183:                                         $digit = $scantron_config->{'Qoff'};
 8184:                                     } else {
 8185:                                         $digit = $lettdig->{$letter};
 8186:                                     }
 8187:                                     $recorded{$part_id} .= $digit;
 8188:                                 }
 8189:                             }
 8190:                         } else {
 8191:                             @tocheck = @items;
 8192:                             for (my $i=0; $i<$expected{$part_id}; $i++) {
 8193:                                 my $curr_sub = shift(@tocheck);
 8194:                                 my $digit;
 8195:                                 if ($curr_sub =~ /^[A-J]$/) {
 8196:                                     $digit = $lettdig->{$curr_sub}-1;
 8197:                                 }
 8198:                                 if ($curr_sub eq 'J') {
 8199:                                     $digit += scalar($numletts);
 8200:                                 }
 8201:                                 for (my $j=0; $j<$scantron_config->{'Qlength'}; $j++) {
 8202:                                     if ($j == $digit) {
 8203:                                         $recorded{$part_id} .= $scantron_config->{'Qon'};
 8204:                                     } else {
 8205:                                         $recorded{$part_id} .= $scantron_config->{'Qoff'};
 8206:                                     }
 8207:                                 }
 8208:                             }
 8209:                         }
 8210:                     }
 8211:                 }
 8212:             }
 8213:         }
 8214:         foreach my $part_id (@{$partids}) {
 8215:             if ($recorded{$part_id} eq '') {
 8216:                 for (my $i=0; $i<$expected{$part_id}; $i++) {
 8217:                     for (my $j=0; $j<$scantron_config->{'Qlength'}; $j++) {
 8218:                         $recorded{$part_id} .= $scantron_config->{'Qoff'};
 8219:                     }
 8220:                 }
 8221:             }
 8222:             $record .= $recorded{$part_id};
 8223:         }
 8224:     }
 8225:     return ($counter,$record);
 8226: }
 8227: 
 8228: sub letter_to_digits { 
 8229:     my %lettdig = (
 8230:                     A => 1,
 8231:                     B => 2,
 8232:                     C => 3,
 8233:                     D => 4,
 8234:                     E => 5,
 8235:                     F => 6,
 8236:                     G => 7,
 8237:                     H => 8,
 8238:                     I => 9,
 8239:                     J => 0,
 8240:                   );
 8241:     return %lettdig;
 8242: }
 8243: 
 8244: 
 8245: #-------- end of section for handling grading scantron forms -------
 8246: #
 8247: #-------------------------------------------------------------------
 8248: 
 8249: #-------------------------- Menu interface -------------------------
 8250: #
 8251: #--- Show a Grading Menu button - Calls the next routine ---
 8252: sub show_grading_menu_form {
 8253:     my ($symb)=@_;
 8254:     my $result.='<br /><form action="/adm/grades" method="post">'."\n".
 8255: 	'<input type="hidden" name="symb" value="'.&Apache::lonenc::check_encrypt($symb).'" />'."\n".
 8256: 	'<input type="hidden" name="saveState"  value="'.$env{'form.saveState'}.'" />'."\n".
 8257: 	'<input type="hidden" name="command" value="gradingmenu" />'."\n".
 8258: 	'<input type="submit" name="submit" value="'.&mt('Grading Menu').'" />'."\n".
 8259: 	'</form>'."\n";
 8260:     return $result;
 8261: }
 8262: 
 8263: # -- Retrieve choices for grading form
 8264: sub savedState {
 8265:     my %savedState = ();
 8266:     if ($env{'form.saveState'}) {
 8267: 	foreach (split(/:/,$env{'form.saveState'})) {
 8268: 	    my ($key,$value) = split(/=/,$_,2);
 8269: 	    $savedState{$key} = $value;
 8270: 	}
 8271:     }
 8272:     return \%savedState;
 8273: }
 8274: 
 8275: sub grading_menu {
 8276:     my ($request) = @_;
 8277:     my ($symb)=&get_symb($request);
 8278:     if (!$symb) {return '';}
 8279:     my $probTitle = &Apache::lonnet::gettitle($symb);
 8280:     my ($table,undef,$hdgrade) = &showResourceInfo($symb,$probTitle);
 8281: 
 8282:     $request->print($table);
 8283:     my %fields = ('symb'=>&Apache::lonenc::check_encrypt($symb),
 8284:                   'handgrade'=>$hdgrade,
 8285:                   'probTitle'=>$probTitle,
 8286:                   'command'=>'submit_options',
 8287:                   'saveState'=>"",
 8288:                   'gradingMenu'=>1,
 8289:                   'showgrading'=>"yes");
 8290:     
 8291:     my $url1 = &Apache::lonhtmlcommon::build_url('grades/',\%fields);
 8292:     
 8293:     $fields{'command'} = 'csvform';
 8294:     my $url2 = &Apache::lonhtmlcommon::build_url('grades/',\%fields);
 8295:     
 8296:     $fields{'command'} = 'processclicker';
 8297:     my $url3 = &Apache::lonhtmlcommon::build_url('grades/',\%fields);
 8298:     
 8299:     $fields{'command'} = 'scantron_selectphase';
 8300:     my $url4 = &Apache::lonhtmlcommon::build_url('grades/',\%fields);
 8301:     
 8302:     my @menu = ({	categorytitle=>'Course Grading',
 8303:             items =>[
 8304:                         {	linktext => 'Manual Grading/View Submissions',
 8305:                     		url => $url1,
 8306:                     		permission => 'F',
 8307:                     		icon => 'edit-find-replace.png',
 8308:                     		linktitle => 'Start the process of hand grading submissions.'
 8309:                         },
 8310:                 	    {	linktext => 'Upload Scores',
 8311:                     		url => $url2,
 8312:                     		permission => 'F',
 8313:                     		icon => 'uploadscores.png',
 8314:                     		linktitle => 'Specify a file containing the class scores for current resource.'
 8315:                 	    },
 8316:                 	    {	linktext => 'Process Clicker',
 8317:                     		url => $url3,
 8318:                     		permission => 'F',
 8319:                     		icon => 'addClickerInfoFile.png',
 8320:                     		linktitle => 'Specify a file containing the clicker information for this resource.'
 8321:                 	    },
 8322:                 	    {	linktext => 'Grade/Manage/Review Scantron Forms',
 8323:                     		url => $url4,
 8324:                     		permission => 'F',
 8325:                     		icon => 'stat.png',
 8326:                     		linktitle => 'Grade scantron exams, upload/download scantron data files, and review previously graded scantron exams.'
 8327:                 	    }
 8328:                     ]
 8329:             });
 8330: 
 8331:     #$fields{'command'} = 'verify';
 8332:     #$url = &Apache::lonhtmlcommon::build_url('grades/',\%fields);
 8333:     #
 8334:     # Create the menu
 8335:     my $Str;
 8336:     # $Str .= '<h2>'.&mt('Please select a grading task').'</h2>';
 8337:     $Str .= '<form method="post" action="" name="gradingMenu">';
 8338:     $Str .= '<input type="hidden" name="command" value="" />'.
 8339:     	'<input type="hidden" name="symb"        value="'.&Apache::lonenc::check_encrypt($symb).'" />'."\n".
 8340: 	'<input type="hidden" name="handgrade"   value="'.$hdgrade.'" />'."\n".
 8341: 	'<input type="hidden" name="probTitle"   value="'.$probTitle.'" />'."\n".
 8342: 	'<input type="hidden" name="saveState"   value="" />'."\n".
 8343: 	'<input type="hidden" name="gradingMenu" value="1" />'."\n".
 8344: 	'<input type="hidden" name="showgrading" value="yes" />'."\n";
 8345: 
 8346:     $Str .= Apache::lonhtmlcommon::generate_menu(@menu);
 8347:     #$menudata->{'jscript'}
 8348:     $Str .='<hr /><input type="button" value="'.&mt('Verify Receipt').'" '.
 8349:         ' onClick="javascript:checkChoice(document.forms.gradingMenu,\'5\',\'verify\')" '.
 8350:         ' /> '.
 8351:         &Apache::lonnet::recprefix($env{'request.course.id'}).
 8352:         '-<input type="text" name="receipt" size="4" onChange="javascript:checkReceiptNo(this.form,\'OK\')" />';
 8353: 
 8354:     $Str .="</form>\n";
 8355:     my $receiptalert = &mt("Please enter a receipt number given by a student in the receipt box.");
 8356:     $request->print(<<GRADINGMENUJS);
 8357: <script type="text/javascript" language="javascript">
 8358:     function checkChoice(formname,val,cmdx) {
 8359: 	if (val <= 2) {
 8360: 	    var cmd = radioSelection(formname.radioChoice);
 8361: 	    var cmdsave = cmd;
 8362: 	} else {
 8363: 	    cmd = cmdx;
 8364: 	    cmdsave = 'submission';
 8365: 	}
 8366: 	formname.command.value = cmd;
 8367: 	if (val < 5) formname.submit();
 8368: 	if (val == 5) {
 8369: 	    if (!checkReceiptNo(formname,'notOK')) { 
 8370: 	        return false;
 8371: 	    } else {
 8372: 	        formname.submit();
 8373: 	    }
 8374: 	}
 8375:     }
 8376: 
 8377:     function checkReceiptNo(formname,nospace) {
 8378: 	var receiptNo = formname.receipt.value;
 8379: 	var checkOpt = false;
 8380: 	if (nospace == "OK" && isNaN(receiptNo)) {checkOpt = true;}
 8381: 	if (nospace == "notOK" && (isNaN(receiptNo) || receiptNo == "")) {checkOpt = true;}
 8382: 	if (checkOpt) {
 8383: 	    alert("$receiptalert");
 8384: 	    formname.receipt.value = "";
 8385: 	    formname.receipt.focus();
 8386: 	    return false;
 8387: 	}
 8388: 	return true;
 8389:     }
 8390: </script>
 8391: GRADINGMENUJS
 8392:     &commonJSfunctions($request);
 8393:     return $Str;    
 8394: }
 8395: 
 8396: 
 8397: #--- Displays the submissions first page -------
 8398: sub submit_options {
 8399:     my ($request) = @_;
 8400:     my ($symb)=&get_symb($request);
 8401:     if (!$symb) {return '';}
 8402:     my $probTitle = &Apache::lonnet::gettitle($symb);
 8403: 
 8404:     my $receiptalert = &mt("Please enter a receipt number given by a student in the receipt box."); 
 8405:     $request->print(<<GRADINGMENUJS);
 8406: <script type="text/javascript" language="javascript">
 8407:     function checkChoice(formname,val,cmdx) {
 8408: 	if (val <= 2) {
 8409: 	    var cmd = radioSelection(formname.radioChoice);
 8410: 	    var cmdsave = cmd;
 8411: 	} else {
 8412: 	    cmd = cmdx;
 8413: 	    cmdsave = 'submission';
 8414: 	}
 8415: 	formname.command.value = cmd;
 8416: 	formname.saveState.value = "saveCmd="+cmdsave+":saveSec="+pullDownSelection(formname.section)+
 8417: 	    ":saveSub="+pullDownSelection(formname.submitonly)+":saveStatus="+pullDownSelection(formname.Status);
 8418: 	if (val < 5) formname.submit();
 8419: 	if (val == 5) {
 8420: 	    if (!checkReceiptNo(formname,'notOK')) { return false;}
 8421: 	    formname.submit();
 8422: 	}
 8423: 	if (val < 7) formname.submit();
 8424:     }
 8425: 
 8426:     function checkReceiptNo(formname,nospace) {
 8427: 	var receiptNo = formname.receipt.value;
 8428: 	var checkOpt = false;
 8429: 	if (nospace == "OK" && isNaN(receiptNo)) {checkOpt = true;}
 8430: 	if (nospace == "notOK" && (isNaN(receiptNo) || receiptNo == "")) {checkOpt = true;}
 8431: 	if (checkOpt) {
 8432: 	    alert("$receiptalert");
 8433: 	    formname.receipt.value = "";
 8434: 	    formname.receipt.focus();
 8435: 	    return false;
 8436: 	}
 8437: 	return true;
 8438:     }
 8439: </script>
 8440: GRADINGMENUJS
 8441:     &commonJSfunctions($request);
 8442:     my ($table,undef,$hdgrade) = &showResourceInfo($symb,$probTitle);
 8443:     my $result;
 8444:     my (undef,$sections) = &getclasslist('all','0');
 8445:     my $savedState = &savedState();
 8446:     my $saveCmd = ($$savedState{'saveCmd'} eq '' ? 'submission' : $$savedState{'saveCmd'});
 8447:     my $saveSec = ($$savedState{'saveSec'} eq '' ? 'all' : $$savedState{'saveSec'});
 8448:     my $saveSub = ($$savedState{'saveSub'} eq '' ? 'all' : $$savedState{'saveSub'});
 8449:     my $saveStatus = ($$savedState{'saveStatus'} eq '' ? 'Active' : $$savedState{'saveStatus'});
 8450: 
 8451:     # Preselect sections
 8452:     my $selsec="";
 8453:     if (ref($sections)) {
 8454:         foreach my $section (sort(@$sections)) {
 8455:             $selsec.='<option value="'.$section.'" '.
 8456:                 ($saveSec eq $section ? 'selected="selected"':'').'>'.$section.'</option>'."\n";
 8457:         }
 8458:     }
 8459: 
 8460:     $result.='<form action="/adm/grades" method="post" name="gradingMenu">'."\n".
 8461: 	'<input type="hidden" name="symb"        value="'.&Apache::lonenc::check_encrypt($symb).'" />'."\n".
 8462: 	'<input type="hidden" name="handgrade"   value="'.$hdgrade.'" />'."\n".
 8463: 	'<input type="hidden" name="probTitle"   value="'.$probTitle.'" />'."\n".
 8464: 	'<input type="hidden" name="command"     value="" />'."\n".
 8465: 	'<input type="hidden" name="saveState"   value="" />'."\n".
 8466: 	'<input type="hidden" name="gradingMenu" value="1" />'."\n".
 8467: 	'<input type="hidden" name="showgrading" value="yes" />'."\n";
 8468: 
 8469:     $result.='
 8470: <h2>
 8471:   '.&mt('Grade Current Resource').'
 8472: </h2>
 8473: <div>
 8474:   '.$table.'
 8475: </div>
 8476: 
 8477: <div class="LC_columnSection">
 8478:   
 8479:     <fieldset>
 8480:       <legend>
 8481:        '.&mt('Sections').'
 8482:       </legend>
 8483:       <select name="section" multiple="multiple" size="5">'."\n";
 8484:     $result.= $selsec;
 8485:     $result.= '<option value="all" '.($saveSec eq 'all' ? 'selected="selected"' : ''). '>all</option></select> &nbsp; ';
 8486:     $result.='
 8487:     </fieldset>
 8488:   
 8489:     <fieldset>
 8490:       <legend>
 8491:         '.&mt('Groups').'
 8492:       </legend>
 8493:       '.&Apache::lonstatistics::GroupSelect('group','multiple',5).'
 8494:     </fieldset>
 8495:   
 8496:     <fieldset>
 8497:       <legend>
 8498:         '.&mt('Access Status').'
 8499:       </legend>
 8500:       '.&Apache::lonhtmlcommon::StatusOptions($saveStatus,undef,5,undef,'mult').'
 8501:     </fieldset>
 8502:   
 8503:     <fieldset>
 8504:       <legend>
 8505:         '.&mt('Submission Status').'
 8506:       </legend>
 8507:       <select name="submitonly" size="5">
 8508: 	         <option value="yes" '.      ($saveSub eq 'yes'       ? 'selected="selected"' : '').'>'.&mt('with submissions').'</option>
 8509: 	         <option value="queued" '.   ($saveSub eq 'queued'    ? 'selected="selected"' : '').'>'.&mt('in grading queue').'</option>
 8510: 	         <option value="graded" '.   ($saveSub eq 'graded'    ? 'selected="selected"' : '').'>'.&mt('with ungraded submissions').'</option>
 8511: 	         <option value="incorrect" '.($saveSub eq 'incorrect' ? 'selected="selected"' : '').'>'.&mt('with incorrect submissions').'</option>
 8512:                  <option value="all" '.      ($saveSub eq 'all'       ? 'selected="selected"' : '').'>'.&mt('with any status').'</option>
 8513:       </select>
 8514:     </fieldset>
 8515:   
 8516: </div>
 8517: 
 8518: <br />
 8519:           <div>
 8520:             <div>
 8521:               <label>
 8522:                 <input type="radio" name="radioChoice" value="submission" '.
 8523:                   ($saveCmd eq 'submission' ? 'checked="checked"' : '').' /> '.
 8524:              &mt('Select individual students to grade and view submissions.').'
 8525: 	      </label> 
 8526:             </div>
 8527:             <div>
 8528: 	      <label>
 8529:                 <input type="radio" name="radioChoice" value="viewgrades" '.
 8530:                   ($saveCmd eq 'viewgrades' ? 'checked="checked"' : '').' /> '.
 8531:                     &mt('Grade all selected students in a grading table.').'
 8532:               </label>
 8533:             </div>
 8534:             <div>
 8535: 	      <input type="button" onClick="javascript:checkChoice(this.form,\'2\');" value="'.&mt('Next').' &rarr;" />
 8536:             </div>
 8537:           </div>
 8538: 
 8539: 
 8540:         <h2>
 8541:          '.&mt('Grade Complete Folder for One Student').'
 8542:         </h2>
 8543:         <div>
 8544:             <div>
 8545:               <label>
 8546:                 <input type="radio" name="radioChoice" value="pickStudentPage" '.
 8547: 	  ($saveCmd eq 'pickStudentPage' ? 'checked="checked"' : '').' /> '.
 8548:   &mt('The <b>complete</b> page/sequence/folder: For one student').'
 8549:               </label>
 8550:             </div>
 8551:             <div>
 8552: 	      <input type="button" onClick="javascript:checkChoice(this.form,\'2\');" value="'.&mt('Next').' &rarr;" />
 8553:             </div>
 8554:         </div>
 8555:   </form>';
 8556:     $result .= &show_grading_menu_form($symb);
 8557:     return $result;
 8558: }
 8559: 
 8560: sub reset_perm {
 8561:     undef(%perm);
 8562: }
 8563: 
 8564: sub init_perm {
 8565:     &reset_perm();
 8566:     foreach my $test_perm ('vgr','mgr','opa') {
 8567: 
 8568: 	my $scope = $env{'request.course.id'};
 8569: 	if (!($perm{$test_perm}=&Apache::lonnet::allowed($test_perm,$scope))) {
 8570: 
 8571: 	    $scope .= '/'.$env{'request.course.sec'};
 8572: 	    if ( $perm{$test_perm}=
 8573: 		 &Apache::lonnet::allowed($test_perm,$scope)) {
 8574: 		$perm{$test_perm.'_section'}=$env{'request.course.sec'};
 8575: 	    } else {
 8576: 		delete($perm{$test_perm});
 8577: 	    }
 8578: 	}
 8579:     }
 8580: }
 8581: 
 8582: sub gather_clicker_ids {
 8583:     my %clicker_ids;
 8584: 
 8585:     my $classlist = &Apache::loncoursedata::get_classlist();
 8586: 
 8587:     # Set up a couple variables.
 8588:     my $username_idx = &Apache::loncoursedata::CL_SNAME();
 8589:     my $domain_idx   = &Apache::loncoursedata::CL_SDOM();
 8590:     my $status_idx   = &Apache::loncoursedata::CL_STATUS();
 8591: 
 8592:     foreach my $student (keys(%$classlist)) {
 8593:         if ($classlist->{$student}->[$status_idx] ne 'Active') { next; }
 8594:         my $username = $classlist->{$student}->[$username_idx];
 8595:         my $domain   = $classlist->{$student}->[$domain_idx];
 8596:         my $clickers =
 8597: 	    (&Apache::lonnet::userenvironment($domain,$username,'clickers'))[1];
 8598:         foreach my $id (split(/\,/,$clickers)) {
 8599:             $id=~s/^[\#0]+//;
 8600:             $id=~s/[\-\:]//g;
 8601:             if (exists($clicker_ids{$id})) {
 8602: 		$clicker_ids{$id}.=','.$username.':'.$domain;
 8603:             } else {
 8604: 		$clicker_ids{$id}=$username.':'.$domain;
 8605:             }
 8606:         }
 8607:     }
 8608:     return %clicker_ids;
 8609: }
 8610: 
 8611: sub gather_adv_clicker_ids {
 8612:     my %clicker_ids;
 8613:     my $cnum=$env{'course.'.$env{'request.course.id'}.'.num'};
 8614:     my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'};
 8615:     my %coursepersonnel=&Apache::lonnet::get_course_adv_roles($cdom.'/'.$cnum);
 8616:     foreach my $element (sort(keys(%coursepersonnel))) {
 8617:         foreach my $person (split(/\,/,$coursepersonnel{$element})) {
 8618:             my ($puname,$pudom)=split(/\:/,$person);
 8619:             my $clickers =
 8620: 		(&Apache::lonnet::userenvironment($pudom,$puname,'clickers'))[1];
 8621:             foreach my $id (split(/\,/,$clickers)) {
 8622: 		$id=~s/^[\#0]+//;
 8623:                 $id=~s/[\-\:]//g;
 8624: 		if (exists($clicker_ids{$id})) {
 8625: 		    $clicker_ids{$id}.=','.$puname.':'.$pudom;
 8626: 		} else {
 8627: 		    $clicker_ids{$id}=$puname.':'.$pudom;
 8628: 		}
 8629:             }
 8630:         }
 8631:     }
 8632:     return %clicker_ids;
 8633: }
 8634: 
 8635: sub clicker_grading_parameters {
 8636:     return ('gradingmechanism' => 'scalar',
 8637:             'upfiletype' => 'scalar',
 8638:             'specificid' => 'scalar',
 8639:             'pcorrect' => 'scalar',
 8640:             'pincorrect' => 'scalar');
 8641: }
 8642: 
 8643: sub process_clicker {
 8644:     my ($r)=@_;
 8645:     my ($symb)=&get_symb($r);
 8646:     if (!$symb) {return '';}
 8647:     my $result=&checkforfile_js();
 8648:     $env{'form.probTitle'} = &Apache::lonnet::gettitle($symb);
 8649:     my ($table) = &showResourceInfo($symb,$env{'form.probTitle'});
 8650:     $result.=$table;
 8651:     $result.='<br /><table width="100%" border="0"><tr><td bgcolor="#777777">'."\n";
 8652:     $result.='<table width="100%" border="0"><tr bgcolor="#e6ffff"><td>'."\n";
 8653:     $result.='&nbsp;<b>'.&mt('Specify a file containing the clicker information for this resource.').
 8654:         '</b></td></tr>'."\n";
 8655:     $result.='<tr bgcolor=#ffffe6><td>'."\n";
 8656: # Attempt to restore parameters from last session, set defaults if not present
 8657:     my %Saveable_Parameters=&clicker_grading_parameters();
 8658:     &Apache::loncommon::restore_course_settings('grades_clicker',
 8659:                                                  \%Saveable_Parameters);
 8660:     if (!$env{'form.pcorrect'}) { $env{'form.pcorrect'}=100; }
 8661:     if (!$env{'form.pincorrect'}) { $env{'form.pincorrect'}=100; }
 8662:     if (!$env{'form.gradingmechanism'}) { $env{'form.gradingmechanism'}='attendance'; }
 8663:     if (!$env{'form.upfiletype'}) { $env{'form.upfiletype'}='iclicker'; }
 8664: 
 8665:     my %checked;
 8666:     foreach my $gradingmechanism ('attendance','personnel','specific','given') {
 8667:        if ($env{'form.gradingmechanism'} eq $gradingmechanism) {
 8668:           $checked{$gradingmechanism}=' checked="checked"';
 8669:        }
 8670:     }
 8671: 
 8672:     my $upload=&mt("Upload File");
 8673:     my $type=&mt("Type");
 8674:     my $attendance=&mt("Award points just for participation");
 8675:     my $personnel=&mt("Correctness determined from response by course personnel");
 8676:     my $specific=&mt("Correctness determined from response with clicker ID(s)"); 
 8677:     my $given=&mt("Correctness determined from given list of answers").' '.
 8678:               '<font size="-2"><tt>('.&mt("Provide comma-separated list. Use '*' for any answer correct, '-' for skip").')</tt></font>';
 8679:     my $pcorrect=&mt("Percentage points for correct solution");
 8680:     my $pincorrect=&mt("Percentage points for incorrect solution");
 8681:     my $selectform=&Apache::loncommon::select_form($env{'form.upfiletype'},'upfiletype',
 8682: 						   ('iclicker' => 'i>clicker',
 8683:                                                     'interwrite' => 'interwrite PRS'));
 8684:     $symb = &Apache::lonenc::check_encrypt($symb);
 8685:     $result.=<<ENDUPFORM;
 8686: <script type="text/javascript">
 8687: function sanitycheck() {
 8688: // Accept only integer percentages
 8689:    document.forms.gradesupload.pcorrect.value=Math.round(document.forms.gradesupload.pcorrect.value);
 8690:    document.forms.gradesupload.pincorrect.value=Math.round(document.forms.gradesupload.pincorrect.value);
 8691: // Find out grading choice
 8692:    for (i=0; i<document.forms.gradesupload.gradingmechanism.length; i++) {
 8693:       if (document.forms.gradesupload.gradingmechanism[i].checked) {
 8694:          gradingchoice=document.forms.gradesupload.gradingmechanism[i].value;
 8695:       }
 8696:    }
 8697: // By default, new choice equals user selection
 8698:    newgradingchoice=gradingchoice;
 8699: // Not good to give more points for false answers than correct ones
 8700:    if (Math.round(document.forms.gradesupload.pcorrect.value)<Math.round(document.forms.gradesupload.pincorrect.value)) {
 8701:       document.forms.gradesupload.pcorrect.value=document.forms.gradesupload.pincorrect.value;
 8702:    }
 8703: // If new choice is attendance only, and old choice was correctness-based, restore defaults
 8704:    if ((gradingchoice=='attendance') && (document.forms.gradesupload.waschecked.value!='attendance')) {
 8705:       document.forms.gradesupload.pcorrect.value=100;
 8706:       document.forms.gradesupload.pincorrect.value=100;
 8707:    }
 8708: // If the values are different, cannot be attendance only
 8709:    if ((Math.round(document.forms.gradesupload.pcorrect.value)!=Math.round(document.forms.gradesupload.pincorrect.value)) &&
 8710:        (gradingchoice=='attendance')) {
 8711:        newgradingchoice='personnel';
 8712:    }
 8713: // Change grading choice to new one
 8714:    for (i=0; i<document.forms.gradesupload.gradingmechanism.length; i++) {
 8715:       if (document.forms.gradesupload.gradingmechanism[i].value==newgradingchoice) {
 8716:          document.forms.gradesupload.gradingmechanism[i].checked=true;
 8717:       } else {
 8718:          document.forms.gradesupload.gradingmechanism[i].checked=false;
 8719:       }
 8720:    }
 8721: // Remember the old state
 8722:    document.forms.gradesupload.waschecked.value=newgradingchoice;
 8723: }
 8724: </script>
 8725: <form method="post" enctype="multipart/form-data" action="/adm/grades" name="gradesupload">
 8726: <input type="hidden" name="symb" value="$symb" />
 8727: <input type="hidden" name="command" value="processclickerfile" />
 8728: <input type="hidden" name="probTitle" value="$env{'form.probTitle'}" />
 8729: <input type="hidden" name="saveState"  value="$env{'form.saveState'}" />
 8730: <input type="file" name="upfile" size="50" />
 8731: <br /><label>$type: $selectform</label>
 8732: <br /><label><input type="radio" name="gradingmechanism" value="attendance"$checked{'attendance'} onClick="sanitycheck()" />$attendance </label>
 8733: <br /><label><input type="radio" name="gradingmechanism" value="personnel"$checked{'personnel'} onClick="sanitycheck()" />$personnel</label>
 8734: <br /><label><input type="radio" name="gradingmechanism" value="specific"$checked{'specific'} onClick="sanitycheck()" />$specific </label>
 8735: <input type="text" name="specificid" value="$env{'form.specificid'}" size="20" />
 8736: <br /><label><input type="radio" name="gradingmechanism" value="given"$checked{'given'} onClick="sanitycheck()" />$given </label>
 8737: <br />&nbsp;&nbsp;&nbsp;
 8738: <input type="text" name="givenanswer" size="50" />
 8739: <input type="hidden" name="waschecked" value="$env{'form.gradingmechanism'}" />
 8740: <br /><label>$pcorrect: <input type="text" name="pcorrect" size="4" value="$env{'form.pcorrect'}" onChange="sanitycheck()" /></label>
 8741: <br /><label>$pincorrect: <input type="text" name="pincorrect" size="4" value="$env{'form.pincorrect'}" onChange="sanitycheck()" /></label>
 8742: <br /><input type="button" onClick="javascript:checkUpload(this.form);" value="$upload" />
 8743: </form>
 8744: ENDUPFORM
 8745:     $result.='</td></tr></table>'."\n".
 8746:              '</td></tr></table><br /><br />'."\n";
 8747:     $result.=&show_grading_menu_form($symb);
 8748:     return $result;
 8749: }
 8750: 
 8751: sub process_clicker_file {
 8752:     my ($r)=@_;
 8753:     my ($symb)=&get_symb($r);
 8754:     if (!$symb) {return '';}
 8755: 
 8756:     my %Saveable_Parameters=&clicker_grading_parameters();
 8757:     &Apache::loncommon::store_course_settings('grades_clicker',
 8758:                                               \%Saveable_Parameters);
 8759: 
 8760:     my ($result) = &showResourceInfo($symb,$env{'form.probTitle'});
 8761:     if (($env{'form.gradingmechanism'} eq 'specific') && ($env{'form.specificid'}!~/\w/)) {
 8762: 	$result.='<span class="LC_error">'.&mt('You need to specify a clicker ID for the correct answer').'</span>';
 8763: 	return $result.&show_grading_menu_form($symb);
 8764:     }
 8765:     if (($env{'form.gradingmechanism'} eq 'given') && ($env{'form.givenanswer'}!~/\S/)) {
 8766:         $result.='<span class="LC_error">'.&mt('You need to specify the correct answer').'</span>';
 8767:         return $result.&show_grading_menu_form($symb);
 8768:     }
 8769:     my $foundgiven=0;
 8770:     if ($env{'form.gradingmechanism'} eq 'given') {
 8771:         $env{'form.givenanswer'}=~s/^\s*//gs;
 8772:         $env{'form.givenanswer'}=~s/\s*$//gs;
 8773:         $env{'form.givenanswer'}=~s/[^a-zA-Z0-9\.\*\-]+/\,/g;
 8774:         $env{'form.givenanswer'}=uc($env{'form.givenanswer'});
 8775:         my @answers=split(/\,/,$env{'form.givenanswer'});
 8776:         $foundgiven=$#answers+1;
 8777:     }
 8778:     my %clicker_ids=&gather_clicker_ids();
 8779:     my %correct_ids;
 8780:     if ($env{'form.gradingmechanism'} eq 'personnel') {
 8781: 	%correct_ids=&gather_adv_clicker_ids();
 8782:     }
 8783:     if ($env{'form.gradingmechanism'} eq 'specific') {
 8784: 	foreach my $correct_id (split(/[\s\,]/,$env{'form.specificid'})) {;
 8785: 	   $correct_id=~tr/a-z/A-Z/;
 8786: 	   $correct_id=~s/\s//gs;
 8787: 	   $correct_id=~s/^[\#0]+//;
 8788:            $correct_id=~s/[\-\:]//g;
 8789:            if ($correct_id) {
 8790: 	      $correct_ids{$correct_id}='specified';
 8791:            }
 8792:         }
 8793:     }
 8794:     if ($env{'form.gradingmechanism'} eq 'attendance') {
 8795: 	$result.=&mt('Score based on attendance only');
 8796:     } elsif ($env{'form.gradingmechanism'} eq 'given') {
 8797:         $result.=&mt('Score based on [_1] ([_2] answers)','<tt>'.$env{'form.givenanswer'}.'</tt>',$foundgiven);
 8798:     } else {
 8799: 	my $number=0;
 8800: 	$result.='<p><b>'.&mt('Correctness determined by the following IDs').'</b>';
 8801: 	foreach my $id (sort(keys(%correct_ids))) {
 8802: 	    $result.='<br /><tt>'.$id.'</tt> - ';
 8803: 	    if ($correct_ids{$id} eq 'specified') {
 8804: 		$result.=&mt('specified');
 8805: 	    } else {
 8806: 		my ($uname,$udom)=split(/\:/,$correct_ids{$id});
 8807: 		$result.=&Apache::loncommon::plainname($uname,$udom);
 8808: 	    }
 8809: 	    $number++;
 8810: 	}
 8811:         $result.="</p>\n";
 8812: 	if ($number==0) {
 8813: 	    $result.='<span class="LC_error">'.&mt('No IDs found to determine correct answer').'</span>';
 8814: 	    return $result.&show_grading_menu_form($symb);
 8815: 	}
 8816:     }
 8817:     if (length($env{'form.upfile'}) < 2) {
 8818:         $result.=&mt('[_1] Error: [_2] The file you attempted to upload, [_3] contained no information. Please check that you entered the correct filename.',
 8819: 		     '<span class="LC_error">',
 8820: 		     '</span>',
 8821: 		     '<span class="LC_filename">'.&HTML::Entities::encode($env{'form.upfile.filename'},'<>&"').'</span>');
 8822:         return $result.&show_grading_menu_form($symb);
 8823:     }
 8824: 
 8825: # Were able to get all the info needed, now analyze the file
 8826: 
 8827:     $result.=&Apache::loncommon::studentbrowser_javascript();
 8828:     $symb = &Apache::lonenc::check_encrypt($symb);
 8829:     my $heading=&mt('Scanning clicker file');
 8830:     $result.=(<<ENDHEADER);
 8831: <br /><table width="100%" border="0"><tr><td bgcolor="#777777">
 8832: <table width="100%" border="0"><tr bgcolor="#e6ffff"><td>
 8833: <b>$heading</b></td></tr><tr bgcolor=#ffffe6><td>
 8834: <form method="post" action="/adm/grades" name="clickeranalysis">
 8835: <input type="hidden" name="symb" value="$symb" />
 8836: <input type="hidden" name="command" value="assignclickergrades" />
 8837: <input type="hidden" name="probTitle" value="$env{'form.probTitle'}" />
 8838: <input type="hidden" name="saveState"  value="$env{'form.saveState'}" />
 8839: <input type="hidden" name="gradingmechanism" value="$env{'form.gradingmechanism'}" />
 8840: <input type="hidden" name="pcorrect" value="$env{'form.pcorrect'}" />
 8841: <input type="hidden" name="pincorrect" value="$env{'form.pincorrect'}" />
 8842: ENDHEADER
 8843:     if ($env{'form.gradingmechanism'} eq 'given') {
 8844:        $result.='<input type="hidden" name="correct:given" value="'.$env{'form.givenanswer'}.'" />';
 8845:     } 
 8846:     my %responses;
 8847:     my @questiontitles;
 8848:     my $errormsg='';
 8849:     my $number=0;
 8850:     if ($env{'form.upfiletype'} eq 'iclicker') {
 8851: 	($errormsg,$number)=&iclicker_eval(\@questiontitles,\%responses);
 8852:     }
 8853:     if ($env{'form.upfiletype'} eq 'interwrite') {
 8854:         ($errormsg,$number)=&interwrite_eval(\@questiontitles,\%responses);
 8855:     }
 8856:     $result.='<br />'.&mt('Found [_1] question(s)',$number).'<br />'.
 8857:              '<input type="hidden" name="number" value="'.$number.'" />'.
 8858:              &mt('Awarding [_1] percent for correct and [_2] percent for incorrect responses',
 8859:                  $env{'form.pcorrect'},$env{'form.pincorrect'}).
 8860:              '<br />';
 8861:     if (($env{'form.gradingmechanism'} eq 'given') && ($number!=$foundgiven)) {
 8862:        $result.='<span class="LC_error">'.&mt('Number of given answers does not agree with number of questions in file.').'</span>';
 8863:        return $result.&show_grading_menu_form($symb);
 8864:     } 
 8865: # Remember Question Titles
 8866: # FIXME: Possibly need delimiter other than ":"
 8867:     for (my $i=0;$i<$number;$i++) {
 8868:         $result.='<input type="hidden" name="question:'.$i.'" value="'.
 8869:                  &HTML::Entities::encode($questiontitles[$i],'"&<>').'" />';
 8870:     }
 8871:     my $correct_count=0;
 8872:     my $student_count=0;
 8873:     my $unknown_count=0;
 8874: # Match answers with usernames
 8875: # FIXME: Possibly need delimiter other than ":"
 8876:     foreach my $id (keys(%responses)) {
 8877:        if ($correct_ids{$id}) {
 8878:           $result.="\n".'<input type="hidden" name="correct:'.$correct_count.':'.$correct_ids{$id}.'" value="'.$responses{$id}.'" />';
 8879:           $correct_count++;
 8880:        } elsif ($clicker_ids{$id}) {
 8881:           if ($clicker_ids{$id}=~/\,/) {
 8882: # More than one user with the same clicker!
 8883:              $result.="\n<hr />".&mt('Clicker registered more than once').": <tt>".$id."</tt><br />";
 8884:              $result.="\n".'<input type="hidden" name="unknown:'.$id.'" value="'.$responses{$id}.'" />'.
 8885:                            "<select name='multi".$id."'>";
 8886:              foreach my $reguser (sort(split(/\,/,$clicker_ids{$id}))) {
 8887:                  $result.="<option value='".$reguser."'>".&Apache::loncommon::plainname(split(/\:/,$reguser)).' ('.$reguser.')</option>';
 8888:              }
 8889:              $result.='</select>';
 8890:              $unknown_count++;
 8891:           } else {
 8892: # Good: found one and only one user with the right clicker
 8893:              $result.="\n".'<input type="hidden" name="student:'.$clicker_ids{$id}.'" value="'.$responses{$id}.'" />';
 8894:              $student_count++;
 8895:           }
 8896:        } else {
 8897:           $result.="\n<hr />".&mt('Unregistered Clicker')." <tt>".$id."</tt><br />";
 8898:           $result.="\n".'<input type="hidden" name="unknown:'.$id.'" value="'.$responses{$id}.'" />'.
 8899:                    "\n".&mt("Username").": <input type='text' name='uname".$id."' />&nbsp;".
 8900:                    "\n".&mt("Domain").": ".
 8901:                    &Apache::loncommon::select_dom_form($env{'course.'.$env{'request.course.id'}.'.domain'},'udom'.$id).'&nbsp;'.
 8902:                    &Apache::loncommon::selectstudent_link('clickeranalysis','uname'.$id,'udom'.$id);
 8903:           $unknown_count++;
 8904:        }
 8905:     }
 8906:     $result.='<hr />'.
 8907:              &mt('Found [_1] registered and [_2] unregistered clickers.',$student_count,$unknown_count);
 8908:     if (($env{'form.gradingmechanism'} ne 'attendance') && ($env{'form.gradingmechanism'} ne 'given')) {
 8909:        if ($correct_count==0) {
 8910:           $errormsg.="Found no correct answers answers for grading!";
 8911:        } elsif ($correct_count>1) {
 8912:           $result.='<br /><span class="LC_warning">'.&mt("Found [_1] entries for grading!",$correct_count).'</span>';
 8913:        }
 8914:     }
 8915:     if ($number<1) {
 8916:        $errormsg.="Found no questions.";
 8917:     }
 8918:     if ($errormsg) {
 8919:        $result.='<br /><span class="LC_error">'.&mt($errormsg).'</span>';
 8920:     } else {
 8921:        $result.='<br /><input type="submit" name="finalize" value="'.&mt('Finalize Grading').'" />';
 8922:     }
 8923:     $result.='</form></td></tr></table>'."\n".
 8924:              '</td></tr></table><br /><br />'."\n";
 8925:     return $result.&show_grading_menu_form($symb);
 8926: }
 8927: 
 8928: sub iclicker_eval {
 8929:     my ($questiontitles,$responses)=@_;
 8930:     my $number=0;
 8931:     my $errormsg='';
 8932:     foreach my $line (split(/[\n\r]/,$env{'form.upfile'})) {
 8933:         my %components=&Apache::loncommon::record_sep($line);
 8934:         my @entries=map {$components{$_}} (sort(keys(%components)));
 8935: 	if ($entries[0] eq 'Question') {
 8936: 	    for (my $i=3;$i<$#entries;$i+=6) {
 8937: 		$$questiontitles[$number]=$entries[$i];
 8938: 		$number++;
 8939: 	    }
 8940: 	}
 8941: 	if ($entries[0]=~/^\#/) {
 8942: 	    my $id=$entries[0];
 8943: 	    my @idresponses;
 8944: 	    $id=~s/^[\#0]+//;
 8945: 	    for (my $i=0;$i<$number;$i++) {
 8946: 		my $idx=3+$i*6;
 8947: 		push(@idresponses,$entries[$idx]);
 8948: 	    }
 8949: 	    $$responses{$id}=join(',',@idresponses);
 8950: 	}
 8951:     }
 8952:     return ($errormsg,$number);
 8953: }
 8954: 
 8955: sub interwrite_eval {
 8956:     my ($questiontitles,$responses)=@_;
 8957:     my $number=0;
 8958:     my $errormsg='';
 8959:     my $skipline=1;
 8960:     my $questionnumber=0;
 8961:     my %idresponses=();
 8962:     foreach my $line (split(/[\n\r]/,$env{'form.upfile'})) {
 8963:         my %components=&Apache::loncommon::record_sep($line);
 8964:         my @entries=map {$components{$_}} (sort(keys(%components)));
 8965:         if ($entries[1] eq 'Time') { $skipline=0; next; }
 8966:         if ($entries[1] eq 'Response') { $skipline=1; }
 8967:         next if $skipline;
 8968:         if ($entries[0]!=$questionnumber) {
 8969:            $questionnumber=$entries[0];
 8970:            $$questiontitles[$number]=&mt('Question [_1]',$questionnumber);
 8971:            $number++;
 8972:         }
 8973:         my $id=$entries[4];
 8974:         $id=~s/^[\#0]+//;
 8975:         $id=~s/^v\d*\://i;
 8976:         $id=~s/[\-\:]//g;
 8977:         $idresponses{$id}[$number]=$entries[6];
 8978:     }
 8979:     foreach my $id (keys(%idresponses)) {
 8980:        $$responses{$id}=join(',',@{$idresponses{$id}});
 8981:        $$responses{$id}=~s/^\s*\,//;
 8982:     }
 8983:     return ($errormsg,$number);
 8984: }
 8985: 
 8986: sub assign_clicker_grades {
 8987:     my ($r)=@_;
 8988:     my ($symb)=&get_symb($r);
 8989:     if (!$symb) {return '';}
 8990: # See which part we are saving to
 8991:     my ($partlist,$handgrade,$responseType) = &response_type($symb);
 8992: # FIXME: This should probably look for the first handgradeable part
 8993:     my $part=$$partlist[0];
 8994: # Start screen output
 8995:     my ($result) = &showResourceInfo($symb,$env{'form.probTitle'});
 8996: 
 8997:     my $heading=&mt('Assigning grades based on clicker file');
 8998:     $result.=(<<ENDHEADER);
 8999: <br /><table width="100%" border="0"><tr><td bgcolor="#777777">
 9000: <table width="100%" border="0"><tr bgcolor="#e6ffff"><td>
 9001: <b>$heading</b></td></tr><tr bgcolor=#ffffe6><td>
 9002: ENDHEADER
 9003: # Get correct result
 9004: # FIXME: Possibly need delimiter other than ":"
 9005:     my @correct=();
 9006:     my $gradingmechanism=$env{'form.gradingmechanism'};
 9007:     my $number=$env{'form.number'};
 9008:     if ($gradingmechanism ne 'attendance') {
 9009:        foreach my $key (keys(%env)) {
 9010:           if ($key=~/^form\.correct\:/) {
 9011:              my @input=split(/\,/,$env{$key});
 9012:              for (my $i=0;$i<=$#input;$i++) {
 9013:                  if (($correct[$i]) && ($input[$i]) &&
 9014:                      ($correct[$i] ne $input[$i])) {
 9015:                     $result.='<br /><span class="LC_warning">'.
 9016:                              &mt('More than one correct result given for question "[_1]": [_2] versus [_3].',
 9017:                                  $env{'form.question:'.$i},$correct[$i],$input[$i]).'</span>';
 9018:                  } elsif ($input[$i]) {
 9019:                     $correct[$i]=$input[$i];
 9020:                  }
 9021:              }
 9022:           }
 9023:        }
 9024:        for (my $i=0;$i<$number;$i++) {
 9025:           if (!$correct[$i]) {
 9026:              $result.='<br /><span class="LC_error">'.
 9027:                       &mt('No correct result given for question "[_1]"!',
 9028:                           $env{'form.question:'.$i}).'</span>';
 9029:           }
 9030:        }
 9031:        $result.='<br />'.&mt("Correct answer: [_1]",join(', ',map { ($_?$_:'-') } @correct));
 9032:     }
 9033: # Start grading
 9034:     my $pcorrect=$env{'form.pcorrect'};
 9035:     my $pincorrect=$env{'form.pincorrect'};
 9036:     my $storecount=0;
 9037:     foreach my $key (keys(%env)) {
 9038:        my $user='';
 9039:        if ($key=~/^form\.student\:(.*)$/) {
 9040:           $user=$1;
 9041:        }
 9042:        if ($key=~/^form\.unknown\:(.*)$/) {
 9043:           my $id=$1;
 9044:           if (($env{'form.uname'.$id}) && ($env{'form.udom'.$id})) {
 9045:              $user=$env{'form.uname'.$id}.':'.$env{'form.udom'.$id};
 9046:           } elsif ($env{'form.multi'.$id}) {
 9047:              $user=$env{'form.multi'.$id};
 9048:           }
 9049:        }
 9050:        if ($user) { 
 9051:           my @answer=split(/\,/,$env{$key});
 9052:           my $sum=0;
 9053:           my $realnumber=$number;
 9054:           for (my $i=0;$i<$number;$i++) {
 9055:              if  ($correct[$i] eq '-') {
 9056:                 $realnumber--;
 9057:              } elsif ($answer[$i]) {
 9058:                 if ($gradingmechanism eq 'attendance') {
 9059:                    $sum+=$pcorrect;
 9060:                 } elsif ($correct[$i] eq '*') {
 9061:                    $sum+=$pcorrect;
 9062:                 } else {
 9063:                    if ($answer[$i] eq $correct[$i]) {
 9064:                       $sum+=$pcorrect;
 9065:                    } else {
 9066:                       $sum+=$pincorrect;
 9067:                    }
 9068:                 }
 9069:              }
 9070:           }
 9071:           my $ave=$sum/(100*$realnumber);
 9072: # Store
 9073:           my ($username,$domain)=split(/\:/,$user);
 9074:           my %grades=();
 9075:           $grades{"resource.$part.solved"}='correct_by_override';
 9076:           $grades{"resource.$part.awarded"}=$ave;
 9077:           $grades{"resource.regrader"}="$env{'user.name'}:$env{'user.domain'}";
 9078:           my $returncode=&Apache::lonnet::cstore(\%grades,$symb,
 9079:                                                  $env{'request.course.id'},
 9080:                                                  $domain,$username);
 9081:           if ($returncode ne 'ok') {
 9082:              $result.="<br /><span class=\"LC_error\">Failed to save student $username:$domain. Message when trying to save was ($returncode)</span>";
 9083:           } else {
 9084:              $storecount++;
 9085:           }
 9086:        }
 9087:     }
 9088: # We are done
 9089:     $result.='<br />'.&mt('Successfully stored grades for [quant,_1,student].',$storecount).
 9090:              '</td></tr></table>'."\n".
 9091:              '</td></tr></table><br /><br />'."\n";
 9092:     return $result.&show_grading_menu_form($symb);
 9093: }
 9094: 
 9095: sub handler {
 9096:     my $request=$_[0];
 9097:     &reset_caches();
 9098:     if ($env{'browser.mathml'}) {
 9099: 	&Apache::loncommon::content_type($request,'text/xml');
 9100:     } else {
 9101: 	&Apache::loncommon::content_type($request,'text/html');
 9102:     }
 9103:     $request->send_http_header;
 9104:     return '' if $request->header_only;
 9105:     &Apache::loncommon::get_unprocessed_cgi($ENV{'QUERY_STRING'});
 9106:     my $symb=&get_symb($request,1);
 9107:     my @commands=&Apache::loncommon::get_env_multiple('form.command');
 9108:     my $command=$commands[0];
 9109: 
 9110:     if ($#commands > 0) {
 9111: 	&Apache::lonnet::logthis("grades got multiple commands ".join(':',@commands));
 9112:     }
 9113: 
 9114:     $ssi_error = 0;
 9115:     my $brcrum = [{href=>"/adm/grades",text=>"Grading"}];
 9116:     $request->print(&Apache::loncommon::start_page('Grading',undef,
 9117:                                           {'bread_crumbs' => $brcrum}));
 9118:     if ($symb eq '' && $command eq '') {
 9119: 	if ($env{'user.adv'}) {
 9120: 	    if (($env{'form.codeone'}) && ($env{'form.codetwo'}) &&
 9121: 		($env{'form.codethree'})) {
 9122: 		my $token=$env{'form.codeone'}.'*'.$env{'form.codetwo'}.'*'.
 9123: 		    $env{'form.codethree'};
 9124: 		my ($tsymb,$tuname,$tudom,$tcrsid)=
 9125: 		    &Apache::lonnet::checkin($token);
 9126: 		if ($tsymb) {
 9127: 		    my ($map,$id,$url)=&Apache::lonnet::decode_symb($tsymb);
 9128: 		    if (&Apache::lonnet::allowed('mgr',$tcrsid)) {
 9129: 			$request->print(&ssi_with_retries('/res/'.$url, $ssi_retries,
 9130: 					  ('grade_username' => $tuname,
 9131: 					   'grade_domain' => $tudom,
 9132: 					   'grade_courseid' => $tcrsid,
 9133: 					   'grade_symb' => $tsymb)));
 9134: 		    } else {
 9135: 			$request->print('<h3>Not authorized: '.$token.'</h3>');
 9136: 		    }
 9137: 		} else {
 9138: 		    $request->print('<h3>Not a valid DocID: '.$token.'</h3>');
 9139: 		}
 9140: 	    } else {
 9141: 		$request->print(&Apache::lonxml::tokeninputfield());
 9142: 	    }
 9143: 	}
 9144:     } else {
 9145: 	&init_perm();
 9146: 	if ($command eq 'submission' && $perm{'vgr'}) {
 9147: 	    ($env{'form.student'} eq '' ? &listStudents($request) : &submission($request,0,0));
 9148: 	} elsif ($command eq 'pickStudentPage' && $perm{'vgr'}) {
 9149: 	    &pickStudentPage($request);
 9150: 	} elsif ($command eq 'displayPage' && $perm{'vgr'}) {
 9151: 	    &displayPage($request);
 9152: 	} elsif ($command eq 'gradeByPage' && $perm{'mgr'}) {
 9153: 	    &updateGradeByPage($request);
 9154: 	} elsif ($command eq 'processGroup' && $perm{'vgr'}) {
 9155: 	    &processGroup($request);
 9156: 	} elsif ($command eq 'gradingmenu' && $perm{'vgr'}) {
 9157: 	    $request->print(&grading_menu($request));
 9158: 	} elsif ($command eq 'submit_options' && $perm{'vgr'}) {
 9159: 	    $request->print(&submit_options($request));
 9160: 	} elsif ($command eq 'viewgrades' && $perm{'vgr'}) {
 9161: 	    $request->print(&viewgrades($request));
 9162: 	} elsif ($command eq 'handgrade' && $perm{'mgr'}) {
 9163: 	    $request->print(&processHandGrade($request));
 9164: 	} elsif ($command eq 'editgrades' && $perm{'mgr'}) {
 9165: 	    $request->print(&editgrades($request));
 9166: 	} elsif ($command eq 'verify' && $perm{'vgr'}) {
 9167: 	    $request->print(&verifyreceipt($request));
 9168:         } elsif ($command eq 'processclicker' && $perm{'mgr'}) {
 9169:             $request->print(&process_clicker($request));
 9170:         } elsif ($command eq 'processclickerfile' && $perm{'mgr'}) {
 9171:             $request->print(&process_clicker_file($request));
 9172:         } elsif ($command eq 'assignclickergrades' && $perm{'mgr'}) {
 9173:             $request->print(&assign_clicker_grades($request));
 9174: 	} elsif ($command eq 'csvform' && $perm{'mgr'}) {
 9175: 	    $request->print(&upcsvScores_form($request));
 9176: 	} elsif ($command eq 'csvupload' && $perm{'mgr'}) {
 9177: 	    $request->print(&csvupload($request));
 9178: 	} elsif ($command eq 'csvuploadmap' && $perm{'mgr'} ) {
 9179: 	    $request->print(&csvuploadmap($request));
 9180: 	} elsif ($command eq 'csvuploadoptions' && $perm{'mgr'}) {
 9181: 	    if ($env{'form.associate'} ne 'Reverse Association') {
 9182: 		$request->print(&csvuploadoptions($request));
 9183: 	    } else {
 9184: 		if ( $env{'form.upfile_associate'} ne 'reverse' ) {
 9185: 		    $env{'form.upfile_associate'} = 'reverse';
 9186: 		} else {
 9187: 		    $env{'form.upfile_associate'} = 'forward';
 9188: 		}
 9189: 		$request->print(&csvuploadmap($request));
 9190: 	    }
 9191: 	} elsif ($command eq 'csvuploadassign' && $perm{'mgr'} ) {
 9192: 	    $request->print(&csvuploadassign($request));
 9193: 	} elsif ($command eq 'scantron_selectphase' && $perm{'mgr'}) {
 9194: 	    $request->print(&scantron_selectphase($request));
 9195:  	} elsif ($command eq 'scantron_warning' && $perm{'mgr'}) {
 9196:  	    $request->print(&scantron_do_warning($request));
 9197: 	} elsif ($command eq 'scantron_validate' && $perm{'mgr'}) {
 9198: 	    $request->print(&scantron_validate_file($request));
 9199: 	} elsif ($command eq 'scantron_process' && $perm{'mgr'}) {
 9200: 	    $request->print(&scantron_process_students($request));
 9201:  	} elsif ($command eq 'scantronupload' && 
 9202:  		 (&Apache::lonnet::allowed('usc',$env{'request.role.domain'})||
 9203: 		  &Apache::lonnet::allowed('usc',$env{'request.course.id'}))) {
 9204:  	    $request->print(&scantron_upload_scantron_data($request)); 
 9205:  	} elsif ($command eq 'scantronupload_save' &&
 9206:  		 (&Apache::lonnet::allowed('usc',$env{'request.role.domain'})||
 9207: 		  &Apache::lonnet::allowed('usc',$env{'request.course.id'}))) {
 9208:  	    $request->print(&scantron_upload_scantron_data_save($request));
 9209:  	} elsif ($command eq 'scantron_download' &&
 9210: 		 &Apache::lonnet::allowed('usc',$env{'request.course.id'})) {
 9211:  	    $request->print(&scantron_download_scantron_data($request));
 9212:         } elsif ($command eq 'checksubmissions' && $perm{'vgr'}) {
 9213:             $request->print(&checkscantron_results($request));     
 9214: 	} elsif ($command) {
 9215: 	    $request->print('<p class="LC_error">'.&mt('Access Denied ([_1])',$command).'</p>');
 9216: 	}
 9217:     }
 9218:     if ($ssi_error) {
 9219: 	&ssi_print_error($request);
 9220:     }
 9221:     $request->print(&Apache::loncommon::end_page());
 9222:     &reset_caches();
 9223:     return '';
 9224: }
 9225: 
 9226: 1;
 9227: 
 9228: __END__;
 9229: 
 9230: 
 9231: =head1 NAME
 9232: 
 9233: Apache::grades
 9234: 
 9235: =head1 SYNOPSIS
 9236: 
 9237: Handles the viewing of grades.
 9238: 
 9239: This is part of the LearningOnline Network with CAPA project
 9240: described at http://www.lon-capa.org.
 9241: 
 9242: =head1 OVERVIEW
 9243: 
 9244: Do an ssi with retries:
 9245: While I'd love to factor out this with the vesrion in lonprintout,
 9246: that would either require a data coupling between modules, which I refuse to perpetuate (there's quite enough of that already), or would require the invention of another infrastructure
 9247: I'm not quite ready to invent (e.g. an ssi_with_retry object).
 9248: 
 9249: At least the logic that drives this has been pulled out into loncommon.
 9250: 
 9251: 
 9252: 
 9253: ssi_with_retries - Does the server side include of a resource.
 9254:                      if the ssi call returns an error we'll retry it up to
 9255:                      the number of times requested by the caller.
 9256:                      If we still have a proble, no text is appended to the
 9257:                      output and we set some global variables.
 9258:                      to indicate to the caller an SSI error occurred.  
 9259:                      All of this is supposed to deal with the issues described
 9260:                      in LonCAPA BZ 5631 see:
 9261:                      http://bugs.lon-capa.org/show_bug.cgi?id=5631
 9262:                      by informing the user that this happened.
 9263: 
 9264: Parameters:
 9265:   resource   - The resource to include.  This is passed directly, without
 9266:                interpretation to lonnet::ssi.
 9267:   form       - The form hash parameters that guide the interpretation of the resource
 9268:                
 9269:   retries    - Number of retries allowed before giving up completely.
 9270: Returns:
 9271:   On success, returns the rendered resource identified by the resource parameter.
 9272: Side Effects:
 9273:   The following global variables can be set:
 9274:    ssi_error                - If an unrecoverable error occurred this becomes true.
 9275:                               It is up to the caller to initialize this to false
 9276:                               if desired.
 9277:    ssi_error_resource  - If an unrecoverable error occurred, this is the value
 9278:                               of the resource that could not be rendered by the ssi
 9279:                               call.
 9280:    ssi_error_message   - The error string fetched from the ssi response
 9281:                               in the event of an error.
 9282: 
 9283: 
 9284: =head1 HANDLER SUBROUTINE
 9285: 
 9286: ssi_with_retries()
 9287: 
 9288: =head1 SUBROUTINES
 9289: 
 9290: =over
 9291: 
 9292: =item scantron_get_correction() : 
 9293: 
 9294:    Builds the interface screen to interact with the operator to fix a
 9295:    specific error condition in a specific scanline
 9296: 
 9297:  Arguments:
 9298:     $r           - Apache request object
 9299:     $i           - number of the current scanline
 9300:     $scan_record - hash ref as returned from &scantron_parse_scanline()
 9301:     $scan_config - hash ref as returned from &get_scantron_config()
 9302:     $line        - full contents of the current scanline
 9303:     $error       - error condition, valid values are
 9304:                    'incorrectCODE', 'duplicateCODE',
 9305:                    'doublebubble', 'missingbubble',
 9306:                    'duplicateID', 'incorrectID'
 9307:     $arg         - extra information needed
 9308:        For errors:
 9309:          - duplicateID   - paper number that this studentID was seen before on
 9310:          - duplicateCODE - array ref of the paper numbers this CODE was
 9311:                            seen on before
 9312:          - incorrectCODE - current incorrect CODE 
 9313:          - doublebubble  - array ref of the bubble lines that have double
 9314:                            bubble errors
 9315:          - missingbubble - array ref of the bubble lines that have missing
 9316:                            bubble errors
 9317: 
 9318: =item  scantron_get_maxbubble() : 
 9319: 
 9320:    Returns the maximum number of bubble lines that are expected to
 9321:    occur. Does this by walking the selected sequence rendering the
 9322:    resource and then checking &Apache::lonxml::get_problem_counter()
 9323:    for what the current value of the problem counter is.
 9324: 
 9325:    Caches the results to $env{'form.scantron_maxbubble'},
 9326:    $env{'form.scantron.bubble_lines.n'}, 
 9327:    $env{'form.scantron.first_bubble_line.n'} and
 9328:    $env{"form.scantron.sub_bubblelines.n"}
 9329:    which are the total number of bubble, lines, the number of bubble
 9330:    lines for response n and number of the first bubble line for response n,
 9331:    and a comma separated list of numbers of bubble lines for sub-questions
 9332:    (for optionresponse, matchresponse, and rankresponse items), for response n.  
 9333: 
 9334: 
 9335: =item  scantron_validate_missingbubbles() : 
 9336: 
 9337:    Validates all scanlines in the selected file to not have any
 9338:     answers that don't have bubbles that have not been verified
 9339:     to be bubble free.
 9340: 
 9341: =item  scantron_process_students() : 
 9342: 
 9343:    Routine that does the actual grading of the bubble sheet information.
 9344: 
 9345:    The parsed scanline hash is added to %env 
 9346: 
 9347:    Then foreach unskipped scanline it does an &Apache::lonnet::ssi()
 9348:    foreach resource , with the form data of
 9349: 
 9350: 	'submitted'     =>'scantron' 
 9351: 	'grade_target'  =>'grade',
 9352: 	'grade_username'=> username of student
 9353: 	'grade_domain'  => domain of student
 9354: 	'grade_courseid'=> of course
 9355: 	'grade_symb'    => symb of resource to grade
 9356: 
 9357:     This triggers a grading pass. The problem grading code takes care
 9358:     of converting the bubbled letter information (now in %env) into a
 9359:     valid submission.
 9360: 
 9361: =item  scantron_upload_scantron_data() :
 9362: 
 9363:     Creates the screen for adding a new bubble sheet data file to a course.
 9364: 
 9365: =item  scantron_upload_scantron_data_save() : 
 9366: 
 9367:    Adds a provided bubble information data file to the course if user
 9368:    has the correct privileges to do so. 
 9369: 
 9370: =item  valid_file() :
 9371: 
 9372:    Validates that the requested bubble data file exists in the course.
 9373: 
 9374: =item  scantron_download_scantron_data() : 
 9375: 
 9376:    Shows a list of the three internal files (original, corrected,
 9377:    skipped) for a specific bubble sheet data file that exists in the
 9378:    course.
 9379: 
 9380: =item  scantron_validate_ID() : 
 9381: 
 9382:    Validates all scanlines in the selected file to not have any
 9383:    invalid or underspecified student/employee IDs
 9384: 
 9385: =back
 9386: 
 9387: =cut

FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>
500 Internal Server Error

Internal Server Error

The server encountered an internal error or misconfiguration and was unable to complete your request.

Please contact the server administrator at root@localhost to inform them of the time this error occurred, and the actions you performed just before this error.

More information about this error may be available in the server error log.