![]() ![]() | ![]() |
- Typo in backport of 1.648 (in 1.596.2.4).
1: # The LearningOnline Network with CAPA 2: # The LON-CAPA Grading handler 3: # 4: # $Id: grades.pm,v 1.596.2.7 2011/10/10 22:45:20 raeburn 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 :http); 44: use Apache::lonlocal; 45: use Apache::lonenc; 46: use Apache::bridgetask(); 47: use String::Similarity; 48: use LONCAPA; 49: 50: use POSIX qw(floor); 51: 52: 53: 54: my %perm=(); 55: 56: # These variables are used to recover from ssi errors 57: 58: my $ssi_retries = 5; 59: my $ssi_error; 60: my $ssi_error_resource; 61: my $ssi_error_message; 62: 63: 64: sub ssi_with_retries { 65: my ($resource, $retries, %form) = @_; 66: my ($content, $response) = &Apache::loncommon::ssi_with_retries($resource, $retries, %form); 67: if ($response->is_error) { 68: $ssi_error = 1; 69: $ssi_error_resource = $resource; 70: $ssi_error_message = $response->code . " " . $response->message; 71: } 72: 73: return $content; 74: 75: } 76: # 77: # Prodcuces an ssi retry failure error message to the user: 78: # 79: 80: sub ssi_print_error { 81: my ($r) = @_; 82: my $helpurl = &Apache::loncommon::top_nav_help('Helpdesk'); 83: $r->print(' 84: <br /> 85: <h2>'.&mt('An unrecoverable network error occurred:').'</h2> 86: <p> 87: '.&mt('Unable to retrieve a resource from a server:').'<br /> 88: '.&mt('Resource:').' '.$ssi_error_resource.'<br /> 89: '.&mt('Error:').' '.$ssi_error_message.' 90: </p> 91: <p>'. 92: &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 />'. 93: &mt('If the error persists, please contact the [_1] for assistance.',$helpurl). 94: '</p>'); 95: return; 96: } 97: 98: # 99: # --- Retrieve the parts from the metadata file.--- 100: sub getpartlist { 101: my ($symb,$errorref) = @_; 102: 103: my $navmap = Apache::lonnavmaps::navmap->new(); 104: unless (ref($navmap)) { 105: if (ref($errorref)) { 106: $$errorref = 'navmap'; 107: return; 108: } 109: } 110: my $res = $navmap->getBySymb($symb); 111: my $partlist = $res->parts(); 112: my $url = $res->src(); 113: my @metakeys = split(/,/,&Apache::lonnet::metadata($url,'keys')); 114: 115: my @stores; 116: foreach my $part (@{ $partlist }) { 117: foreach my $key (@metakeys) { 118: if ($key =~ m/^stores_\Q$part\E_/) { push(@stores,$key); } 119: } 120: } 121: return @stores; 122: } 123: 124: # --- Get the symbolic name of a problem and the url 125: sub get_symb { 126: my ($request,$silent) = @_; 127: (my $url=$env{'form.url'}) =~ s-^http://($ENV{'SERVER_NAME'}|$ENV{'HTTP_HOST'})--; 128: my $symb=($env{'form.symb'} ne '' ? $env{'form.symb'} : (&Apache::lonnet::symbread($url))); 129: if ($symb eq '') { 130: if (!$silent) { 131: $request->print(&mt("Unable to handle ambiguous references: [_1].",$url)); 132: return (); 133: } 134: } 135: &Apache::lonenc::check_decrypt(\$symb); 136: return ($symb); 137: } 138: 139: #--- Format fullname, username:domain if different for display 140: #--- Use anywhere where the student names are listed 141: sub nameUserString { 142: my ($type,$fullname,$uname,$udom) = @_; 143: if ($type eq 'header') { 144: return '<b> '.&mt('Fullname').' </b><span class="LC_internal_info">('.&mt('Username').')</span>'; 145: } else { 146: return ' '.$fullname.'<span class="LC_internal_info"> ('.$uname. 147: ($env{'user.domain'} eq $udom ? '' : ' ('.$udom.')').')</span>'; 148: } 149: } 150: 151: #--- Get the partlist and the response type for a given problem. --- 152: #--- Indicate if a response type is coded handgraded or not. --- 153: sub response_type { 154: my ($symb,$response_error) = @_; 155: 156: my $navmap = Apache::lonnavmaps::navmap->new(); 157: unless (ref($navmap)) { 158: if (ref($response_error)) { 159: $$response_error = 1; 160: } 161: return; 162: } 163: my $res = $navmap->getBySymb($symb); 164: unless (ref($res)) { 165: $$response_error = 1; 166: return; 167: } 168: my $partlist = $res->parts(); 169: my %vPart = 170: map { $_ => 1 } (&Apache::loncommon::get_env_multiple('form.vPart')); 171: my (%response_types,%handgrade); 172: foreach my $part (@{ $partlist }) { 173: next if (%vPart && !exists($vPart{$part})); 174: 175: my @types = $res->responseType($part); 176: my @ids = $res->responseIds($part); 177: for (my $i=0; $i < scalar(@ids); $i++) { 178: $response_types{$part}{$ids[$i]} = $types[$i]; 179: $handgrade{$part.'_'.$ids[$i]} = 180: &Apache::lonnet::EXT('resource.'.$part.'_'.$ids[$i]. 181: '.handgrade',$symb); 182: } 183: } 184: return ($partlist,\%handgrade,\%response_types); 185: } 186: 187: sub flatten_responseType { 188: my ($responseType) = @_; 189: my @part_response_id = 190: map { 191: my $part = $_; 192: map { 193: [$part,$_] 194: } sort(keys(%{ $responseType->{$part} })); 195: } sort(keys(%$responseType)); 196: return @part_response_id; 197: } 198: 199: sub get_display_part { 200: my ($partID,$symb)=@_; 201: my $display=&Apache::lonnet::EXT('resource.'.$partID.'.display',$symb); 202: if (defined($display) and $display ne '') { 203: $display.= ' (<span class="LC_internal_info">' 204: .&mt('Part ID: [_1]',$partID).'</span>)'; 205: } else { 206: $display=$partID; 207: } 208: return $display; 209: } 210: 211: #--- Show resource title 212: #--- and parts and response type 213: sub showResourceInfo { 214: my ($symb,$probTitle,$checkboxes,$res_error) = @_; 215: my $result = '<h3>'.&mt('Current Resource').': '.$probTitle.'</h3>'."\n"; 216: my ($partlist,$handgrade,$responseType) = &response_type($symb,$res_error); 217: if (ref($res_error)) { 218: if ($$res_error) { 219: return; 220: } 221: } 222: $result.=&Apache::loncommon::start_data_table() 223: .&Apache::loncommon::start_data_table_header_row(); 224: if ($checkboxes) { 225: $result.='<th> </th>'; 226: } 227: $result.='<th>'.&mt('Problem Part').'</th>' 228: .'<th>'.&mt('Res. ID').'</th>' 229: .'<th>'.&mt('Type').'</th>' 230: .&Apache::loncommon::end_data_table_header_row(); 231: my %resptype = (); 232: my $hdgrade='no'; 233: my %partsseen; 234: foreach my $partID (sort(keys(%$responseType))) { 235: foreach my $resID (sort(keys(%{ $responseType->{$partID} }))) { 236: my $handgrade=$$handgrade{$partID.'_'.$resID}; 237: my $responsetype = $responseType->{$partID}->{$resID}; 238: $hdgrade = $handgrade if ($handgrade eq 'yes'); 239: $result.=&Apache::loncommon::start_data_table_row(); 240: if ($checkboxes) { 241: if (exists($partsseen{$partID})) { 242: $result.="<td> </td>"; 243: } else { 244: $result.="<td><input type='checkbox' name='vPart' value='$partID' checked='checked' /></td>"; 245: } 246: $partsseen{$partID}=1; 247: } 248: my $display_part=&get_display_part($partID,$symb); 249: $result.='<td>'.$display_part.'</td>' 250: .'<td>'.'<span class="LC_internal_info">'.$resID.'</span></td>' 251: .'<td>'.&mt($responsetype).'</td>' 252: # .'<td>'.&mt('<b>Handgrade: </b>[_1]',$handgrade).'</td>' 253: .&Apache::loncommon::end_data_table_row(); 254: } 255: } 256: $result.=&Apache::loncommon::end_data_table(); 257: return $result,$responseType,$hdgrade,$partlist,$handgrade; 258: } 259: 260: sub reset_caches { 261: &reset_analyze_cache(); 262: &reset_perm(); 263: } 264: 265: { 266: my %analyze_cache; 267: my %analyze_cache_formkeys; 268: 269: sub reset_analyze_cache { 270: undef(%analyze_cache); 271: undef(%analyze_cache_formkeys); 272: } 273: 274: sub get_analyze { 275: my ($symb,$uname,$udom,$no_increment,$add_to_hash,$type,$trial,$rndseed)=@_; 276: my $key = "$symb\0$uname\0$udom"; 277: if ($type eq 'randomizetry') { 278: if ($trial ne '') { 279: $key .= "\0".$trial; 280: } 281: } 282: if (exists($analyze_cache{$key})) { 283: my $getupdate = 0; 284: if (ref($add_to_hash) eq 'HASH') { 285: foreach my $item (keys(%{$add_to_hash})) { 286: if (ref($analyze_cache_formkeys{$key}) eq 'HASH') { 287: if (!exists($analyze_cache_formkeys{$key}{$item})) { 288: $getupdate = 1; 289: last; 290: } 291: } else { 292: $getupdate = 1; 293: } 294: } 295: } 296: if (!$getupdate) { 297: return $analyze_cache{$key}; 298: } 299: } 300: 301: my (undef,undef,$url)=&Apache::lonnet::decode_symb($symb); 302: $url=&Apache::lonnet::clutter($url); 303: my %form = ('grade_target' => 'analyze', 304: 'grade_domain' => $udom, 305: 'grade_symb' => $symb, 306: 'grade_courseid' => $env{'request.course.id'}, 307: 'grade_username' => $uname, 308: 'grade_noincrement' => $no_increment); 309: if ($type eq 'randomizetry') { 310: $form{'grade_questiontype'} = $type; 311: if ($rndseed ne '') { 312: $form{'grade_rndseed'} = $rndseed; 313: } 314: } 315: if (ref($add_to_hash)) { 316: %form = (%form,%{$add_to_hash}); 317: } 318: my $subresult=&ssi_with_retries($url, $ssi_retries,%form); 319: (undef,$subresult)=split(/_HASH_REF__/,$subresult,2); 320: my %analyze=&Apache::lonnet::str2hash($subresult); 321: if (ref($add_to_hash) eq 'HASH') { 322: $analyze_cache_formkeys{$key} = $add_to_hash; 323: } else { 324: $analyze_cache_formkeys{$key} = {}; 325: } 326: return $analyze_cache{$key} = \%analyze; 327: } 328: 329: sub get_order { 330: my ($partid,$respid,$symb,$uname,$udom,$no_increment,$type,$trial,$rndseed)=@_; 331: my $analyze = &get_analyze($symb,$uname,$udom,$no_increment,undef,$type,$trial,$rndseed); 332: return $analyze->{"$partid.$respid.shown"}; 333: } 334: 335: sub get_radiobutton_correct_foil { 336: my ($partid,$respid,$symb,$uname,$udom,$type,$trial,$rndseed)=@_; 337: my $analyze = &get_analyze($symb,$uname,$udom,undef,undef,$type,$trial,$rndseed); 338: my $foils = &get_order($partid,$respid,$symb,$uname,$udom,undef,$type,$trial,$rndseed); 339: if (ref($foils) eq 'ARRAY') { 340: foreach my $foil (@{$foils}) { 341: if ($analyze->{"$partid.$respid.foil.value.$foil"} eq 'true') { 342: return $foil; 343: } 344: } 345: } 346: } 347: 348: sub scantron_partids_tograde { 349: my ($resource,$cid,$uname,$udom,$check_for_randomlist) = @_; 350: my (%analysis,@parts); 351: if (ref($resource)) { 352: my $symb = $resource->symb(); 353: my $add_to_form; 354: if ($check_for_randomlist) { 355: $add_to_form = { 'check_parts_withrandomlist' => 1,}; 356: } 357: my $analyze = &get_analyze($symb,$uname,$udom,undef,$add_to_form); 358: if (ref($analyze) eq 'HASH') { 359: %analysis = %{$analyze}; 360: } 361: if (ref($analysis{'parts'}) eq 'ARRAY') { 362: foreach my $part (@{$analysis{'parts'}}) { 363: my ($id,$respid) = split(/\./,$part); 364: if (!&Apache::loncommon::check_if_partid_hidden($id,$symb,$udom,$uname)) { 365: push(@parts,$part); 366: } 367: } 368: } 369: } 370: return (\%analysis,\@parts); 371: } 372: 373: } 374: 375: #--- Clean response type for display 376: #--- Currently filters option/rank/radiobutton/match/essay/Task 377: # response types only. 378: sub cleanRecord { 379: my ($answer,$response,$symb,$partid,$respid,$record,$order,$version, 380: $uname,$udom,$type,$trial,$rndseed) = @_; 381: my $grayFont = '<span class="LC_internal_info">'; 382: if ($response =~ /^(option|rank)$/) { 383: my %answer=&Apache::lonnet::str2hash($answer); 384: my %grading=&Apache::lonnet::str2hash($record->{$version."resource.$partid.$respid.submissiongrading"}); 385: my ($toprow,$bottomrow); 386: foreach my $foil (@$order) { 387: if ($grading{$foil} == 1) { 388: $toprow.='<td><b>'.$answer{$foil}.' </b></td>'; 389: } else { 390: $toprow.='<td><i>'.$answer{$foil}.' </i></td>'; 391: } 392: $bottomrow.='<td>'.$grayFont.$foil.'</span> </td>'; 393: } 394: return '<blockquote><table border="1">'. 395: '<tr valign="top"><td>'.&mt('Answer').'</td>'.$toprow.'</tr>'. 396: '<tr valign="top"><td>'.$grayFont.&mt('Option ID').'</span></td>'. 397: $bottomrow.'</tr></table></blockquote>'; 398: } elsif ($response eq 'match') { 399: my %answer=&Apache::lonnet::str2hash($answer); 400: my %grading=&Apache::lonnet::str2hash($record->{$version."resource.$partid.$respid.submissiongrading"}); 401: my @items=&Apache::lonnet::str2array($record->{$version."resource.$partid.$respid.submissionitems"}); 402: my ($toprow,$middlerow,$bottomrow); 403: foreach my $foil (@$order) { 404: my $item=shift(@items); 405: if ($grading{$foil} == 1) { 406: $toprow.='<td><b>'.$item.' </b></td>'; 407: $middlerow.='<td><b>'.$grayFont.$answer{$foil}.' </span></b></td>'; 408: } else { 409: $toprow.='<td><i>'.$item.' </i></td>'; 410: $middlerow.='<td><i>'.$grayFont.$answer{$foil}.' </span></i></td>'; 411: } 412: $bottomrow.='<td>'.$grayFont.$foil.'</span> </td>'; 413: } 414: return '<blockquote><table border="1">'. 415: '<tr valign="top"><td>'.&mt('Answer').'</td>'.$toprow.'</tr>'. 416: '<tr valign="top"><td>'.$grayFont.&mt('Item ID').'</span></td>'. 417: $middlerow.'</tr>'. 418: '<tr valign="top"><td>'.$grayFont.&mt('Option ID').'</span></td>'. 419: $bottomrow.'</tr>'.'</table></blockquote>'; 420: } elsif ($response eq 'radiobutton') { 421: my %answer=&Apache::lonnet::str2hash($answer); 422: my ($toprow,$bottomrow); 423: my $correct = 424: &get_radiobutton_correct_foil($partid,$respid,$symb,$uname,$udom,$type,$trial,$rndseed); 425: foreach my $foil (@$order) { 426: if (exists($answer{$foil})) { 427: if ($foil eq $correct) { 428: $toprow.='<td><b>'.&mt('true').'</b></td>'; 429: } else { 430: $toprow.='<td><i>'.&mt('true').'</i></td>'; 431: } 432: } else { 433: $toprow.='<td>'.&mt('false').'</td>'; 434: } 435: $bottomrow.='<td>'.$grayFont.$foil.'</span> </td>'; 436: } 437: return '<blockquote><table border="1">'. 438: '<tr valign="top"><td>'.&mt('Answer').'</td>'.$toprow.'</tr>'. 439: '<tr valign="top"><td>'.$grayFont.&mt('Option ID').'</span></td>'. 440: $bottomrow.'</tr></table></blockquote>'; 441: } elsif ($response eq 'essay') { 442: if (! exists ($env{'form.'.$symb})) { 443: my (%keyhash) = &Apache::lonnet::dump('nohist_handgrade', 444: $env{'course.'.$env{'request.course.id'}.'.domain'}, 445: $env{'course.'.$env{'request.course.id'}.'.num'}); 446: 447: my $loginuser = $env{'user.name'}.':'.$env{'user.domain'}; 448: $env{'form.keywords'} = $keyhash{$symb.'_keywords'} ne '' ? $keyhash{$symb.'_keywords'} : ''; 449: $env{'form.kwclr'} = $keyhash{$loginuser.'_kwclr'} ne '' ? $keyhash{$loginuser.'_kwclr'} : 'red'; 450: $env{'form.kwsize'} = $keyhash{$loginuser.'_kwsize'} ne '' ? $keyhash{$loginuser.'_kwsize'} : '0'; 451: $env{'form.kwstyle'} = $keyhash{$loginuser.'_kwstyle'} ne '' ? $keyhash{$loginuser.'_kwstyle'} : ''; 452: $env{'form.'.$symb} = 1; # so that we don't have to read it from disk for multiple sub of the same prob. 453: } 454: $answer =~ s-\n-<br />-g; 455: return '<br /><br /><blockquote><tt>'.&keywords_highlight($answer).'</tt></blockquote>'; 456: } elsif ( $response eq 'organic') { 457: my $result='Smile representation: "<tt>'.$answer.'</tt>"'; 458: my $jme=$record->{$version."resource.$partid.$respid.molecule"}; 459: $result.=&Apache::chemresponse::jme_img($jme,$answer,400); 460: return $result; 461: } elsif ( $response eq 'Task') { 462: if ( $answer eq 'SUBMITTED') { 463: my $files = $record->{$version."resource.$respid.$partid.bridgetask.portfiles"}; 464: my $result = &Apache::bridgetask::file_list($files,$uname,$udom); 465: return $result; 466: } elsif ( grep(/^\Q$version\E.*?\.instance$/, keys(%{$record})) ) { 467: my @matches = grep(/^\Q$version\E.*?\.instance$/, 468: keys(%{$record})); 469: return join('<br />',($version,@matches)); 470: 471: 472: } else { 473: my $result = 474: '<p>' 475: .&mt('Overall result: [_1]', 476: $record->{$version."resource.$respid.$partid.status"}) 477: .'</p>'; 478: 479: $result .= '<ul>'; 480: my @grade = grep(/^\Q${version}resource.$respid.$partid.\E[^.]*[.]status$/, 481: keys(%{$record})); 482: foreach my $grade (sort(@grade)) { 483: my ($dim) = ($grade =~/[.]([^.]+)[.]status$/); 484: $result.= '<li>'.&mt("Dimension: [_1], status [_2] ", 485: $dim, $record->{$grade}). 486: '</li>'; 487: } 488: $result.='</ul>'; 489: return $result; 490: } 491: } elsif ( $response =~ m/(?:numerical|formula)/) { 492: $answer = 493: &Apache::loncommon::format_previous_attempt_value('submission', 494: $answer); 495: } 496: return $answer; 497: } 498: 499: #-- A couple of common js functions 500: sub commonJSfunctions { 501: my $request = shift; 502: $request->print(<<COMMONJSFUNCTIONS); 503: <script type="text/javascript" language="javascript"> 504: function radioSelection(radioButton) { 505: var selection=null; 506: if (radioButton.length > 1) { 507: for (var i=0; i<radioButton.length; i++) { 508: if (radioButton[i].checked) { 509: return radioButton[i].value; 510: } 511: } 512: } else { 513: if (radioButton.checked) return radioButton.value; 514: } 515: return selection; 516: } 517: 518: function pullDownSelection(selectOne) { 519: var selection=""; 520: if (selectOne.length > 1) { 521: for (var i=0; i<selectOne.length; i++) { 522: if (selectOne[i].selected) { 523: return selectOne[i].value; 524: } 525: } 526: } else { 527: // only one value it must be the selected one 528: return selectOne.value; 529: } 530: } 531: </script> 532: COMMONJSFUNCTIONS 533: } 534: 535: #--- Dumps the class list with usernames,list of sections, 536: #--- section, ids and fullnames for each user. 537: sub getclasslist { 538: my ($getsec,$filterlist,$getgroup) = @_; 539: my @getsec; 540: my @getgroup; 541: my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status')); 542: if (!ref($getsec)) { 543: if ($getsec ne '' && $getsec ne 'all') { 544: @getsec=($getsec); 545: } 546: } else { 547: @getsec=@{$getsec}; 548: } 549: if (grep(/^all$/,@getsec)) { undef(@getsec); } 550: if (!ref($getgroup)) { 551: if ($getgroup ne '' && $getgroup ne 'all') { 552: @getgroup=($getgroup); 553: } 554: } else { 555: @getgroup=@{$getgroup}; 556: } 557: if (grep(/^all$/,@getgroup)) { undef(@getgroup); } 558: 559: my ($classlist,$keylist)=&Apache::loncoursedata::get_classlist(); 560: # Bail out if we were unable to get the classlist 561: return if (! defined($classlist)); 562: &Apache::loncoursedata::get_group_memberships($classlist,$keylist); 563: # 564: my %sections; 565: my %fullnames; 566: foreach my $student (keys(%$classlist)) { 567: my $end = 568: $classlist->{$student}->[&Apache::loncoursedata::CL_END()]; 569: my $start = 570: $classlist->{$student}->[&Apache::loncoursedata::CL_START()]; 571: my $id = 572: $classlist->{$student}->[&Apache::loncoursedata::CL_ID()]; 573: my $section = 574: $classlist->{$student}->[&Apache::loncoursedata::CL_SECTION()]; 575: my $fullname = 576: $classlist->{$student}->[&Apache::loncoursedata::CL_FULLNAME()]; 577: my $status = 578: $classlist->{$student}->[&Apache::loncoursedata::CL_STATUS()]; 579: my $group = 580: $classlist->{$student}->[&Apache::loncoursedata::CL_GROUP()]; 581: # filter students according to status selected 582: if ($filterlist && (!($stu_status =~ /Any/))) { 583: if (!($stu_status =~ $status)) { 584: delete($classlist->{$student}); 585: next; 586: } 587: } 588: # filter students according to groups selected 589: my @stu_groups = split(/,/,$group); 590: if (@getgroup) { 591: my $exclude = 1; 592: foreach my $grp (@getgroup) { 593: foreach my $stu_group (@stu_groups) { 594: if ($stu_group eq $grp) { 595: $exclude = 0; 596: } 597: } 598: if (($grp eq 'none') && !$group) { 599: $exclude = 0; 600: } 601: } 602: if ($exclude) { 603: delete($classlist->{$student}); 604: } 605: } 606: $section = ($section ne '' ? $section : 'none'); 607: if (&canview($section)) { 608: if (!@getsec || grep(/^\Q$section\E$/,@getsec)) { 609: $sections{$section}++; 610: if ($classlist->{$student}) { 611: $fullnames{$student}=$fullname; 612: } 613: } else { 614: delete($classlist->{$student}); 615: } 616: } else { 617: delete($classlist->{$student}); 618: } 619: } 620: my %seen = (); 621: my @sections = sort(keys(%sections)); 622: return ($classlist,\@sections,\%fullnames); 623: } 624: 625: sub canmodify { 626: my ($sec)=@_; 627: if ($perm{'mgr'}) { 628: if (!defined($perm{'mgr_section'})) { 629: # can modify whole class 630: return 1; 631: } else { 632: if ($sec eq $perm{'mgr_section'}) { 633: #can modify the requested section 634: return 1; 635: } else { 636: # can't modify the request section 637: return 0; 638: } 639: } 640: } 641: #can't modify 642: return 0; 643: } 644: 645: sub canview { 646: my ($sec)=@_; 647: if ($perm{'vgr'}) { 648: if (!defined($perm{'vgr_section'})) { 649: # can modify whole class 650: return 1; 651: } else { 652: if ($sec eq $perm{'vgr_section'}) { 653: #can modify the requested section 654: return 1; 655: } else { 656: # can't modify the request section 657: return 0; 658: } 659: } 660: } 661: #can't modify 662: return 0; 663: } 664: 665: #--- Retrieve the grade status of a student for all the parts 666: sub student_gradeStatus { 667: my ($symb,$udom,$uname,$partlist) = @_; 668: my %record = &Apache::lonnet::restore($symb,$env{'request.course.id'},$udom,$uname); 669: my %partstatus = (); 670: foreach (@$partlist) { 671: my ($status,undef) = split(/_/,$record{"resource.$_.solved"},2); 672: $status = 'nothing' if ($status eq ''); 673: $partstatus{$_} = $status; 674: my $subkey = "resource.$_.submitted_by"; 675: $partstatus{$subkey} = $record{$subkey} if ($record{$subkey} ne ''); 676: } 677: return %partstatus; 678: } 679: 680: # hidden form and javascript that calls the form 681: # Use by verifyscript and viewgrades 682: # Shows a student's view of problem and submission 683: sub jscriptNform { 684: my ($symb) = @_; 685: my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status')); 686: my $jscript='<script type="text/javascript" language="javascript">'."\n". 687: ' function viewOneStudent(user,domain) {'."\n". 688: ' document.onestudent.student.value = user;'."\n". 689: ' document.onestudent.userdom.value = domain;'."\n". 690: ' document.onestudent.submit();'."\n". 691: ' }'."\n". 692: '</script>'."\n"; 693: $jscript.= '<form action="/adm/grades" method="post" name="onestudent">'."\n". 694: '<input type="hidden" name="symb" value="'.&Apache::lonenc::check_encrypt($symb).'" />'."\n". 695: '<input type="hidden" name="saveState" value="'.$env{'form.saveState'}.'" />'."\n". 696: '<input type="hidden" name="probTitle" value="'.$env{'form.probTitle'}.'" />'."\n". 697: '<input type="hidden" name="Status" value="'.$stu_status.'" />'."\n". 698: '<input type="hidden" name="command" value="submission" />'."\n". 699: '<input type="hidden" name="student" value="" />'."\n". 700: '<input type="hidden" name="userdom" value="" />'."\n". 701: '</form>'."\n"; 702: return $jscript; 703: } 704: 705: 706: 707: # Given the score (as a number [0-1] and the weight) what is the final 708: # point value? This function will round to the nearest tenth, third, 709: # or quarter if one of those is within the tolerance of .00001. 710: sub compute_points { 711: my ($score, $weight) = @_; 712: 713: my $tolerance = .00001; 714: my $points = $score * $weight; 715: 716: # Check for nearness to 1/x. 717: my $check_for_nearness = sub { 718: my ($factor) = @_; 719: my $num = ($points * $factor) + $tolerance; 720: my $floored_num = floor($num); 721: if ($num - $floored_num < 2 * $tolerance * $factor) { 722: return $floored_num / $factor; 723: } 724: return $points; 725: }; 726: 727: $points = $check_for_nearness->(10); 728: $points = $check_for_nearness->(3); 729: $points = $check_for_nearness->(4); 730: 731: return $points; 732: } 733: 734: #------------------ End of general use routines -------------------- 735: 736: # 737: # Find most similar essay 738: # 739: 740: sub most_similar { 741: my ($uname,$udom,$uessay,$old_essays)=@_; 742: 743: # ignore spaces and punctuation 744: 745: $uessay=~s/\W+/ /gs; 746: 747: # ignore empty submissions (occuring when only files are sent) 748: 749: unless ($uessay=~/\w+/s) { return ''; } 750: 751: # these will be returned. Do not care if not at least 50 percent similar 752: my $limit=0.6; 753: my $sname=''; 754: my $sdom=''; 755: my $scrsid=''; 756: my $sessay=''; 757: # go through all essays ... 758: foreach my $tkey (keys(%$old_essays)) { 759: my ($tname,$tdom,$tcrsid)=map {&unescape($_)} (split(/\./,$tkey)); 760: # ... except the same student 761: next if (($tname eq $uname) && ($tdom eq $udom)); 762: my $tessay=$old_essays->{$tkey}; 763: $tessay=~s/\W+/ /gs; 764: # String similarity gives up if not even limit 765: my $tsimilar=&String::Similarity::similarity($uessay,$tessay,$limit); 766: # Found one 767: if ($tsimilar>$limit) { 768: $limit=$tsimilar; 769: $sname=$tname; 770: $sdom=$tdom; 771: $scrsid=$tcrsid; 772: $sessay=$old_essays->{$tkey}; 773: } 774: } 775: if ($limit>0.6) { 776: return ($sname,$sdom,$scrsid,$sessay,$limit); 777: } else { 778: return ('','','','',0); 779: } 780: } 781: 782: #------------------------------------------------------------------- 783: 784: #------------------------------------ Receipt Verification Routines 785: # 786: #--- Check whether a receipt number is valid.--- 787: sub verifyreceipt { 788: my $request = shift; 789: 790: my $courseid = $env{'request.course.id'}; 791: my $receipt = &Apache::lonnet::recprefix($courseid).'-'. 792: $env{'form.receipt'}; 793: $receipt =~ s/[^\-\d]//g; 794: my ($symb) = &get_symb($request); 795: 796: my $title.= 797: '<h3><span class="LC_info">'. 798: &mt('Verifying Receipt No. [_1]',$receipt). 799: '</span></h3>'."\n". 800: '<h4>'.&mt('<b>Resource: </b>[_1]',$env{'form.probTitle'}). 801: '</h4>'."\n"; 802: 803: my ($string,$contents,$matches) = ('','',0); 804: my (undef,undef,$fullname) = &getclasslist('all','0'); 805: 806: my $receiptparts=0; 807: if ($env{"course.$courseid.receiptalg"} eq 'receipt2' || 808: $env{"course.$courseid.receiptalg"} eq 'receipt3') { $receiptparts=1; } 809: my $parts=['0']; 810: if ($receiptparts) { 811: my $res_error; 812: ($parts)=&response_type($symb,\$res_error); 813: if ($res_error) { 814: return &navmap_errormsg(); 815: } 816: } 817: 818: my $header = 819: &Apache::loncommon::start_data_table(). 820: &Apache::loncommon::start_data_table_header_row(). 821: '<th> '.&mt('Fullname').' </th>'."\n". 822: '<th> '.&mt('Username').' </th>'."\n". 823: '<th> '.&mt('Domain').' </th>'; 824: if ($receiptparts) { 825: $header.='<th> '.&mt('Problem Part').' </th>'; 826: } 827: $header.= 828: &Apache::loncommon::end_data_table_header_row(); 829: 830: foreach (sort 831: { 832: if (lc($$fullname{$a}) ne lc($$fullname{$b})) { 833: return (lc($$fullname{$a}) cmp lc($$fullname{$b})); 834: } 835: return $a cmp $b; 836: } (keys(%$fullname))) { 837: my ($uname,$udom)=split(/\:/); 838: foreach my $part (@$parts) { 839: if ($receipt eq &Apache::lonnet::ireceipt($uname,$udom,$courseid,$symb,$part)) { 840: $contents.= 841: &Apache::loncommon::start_data_table_row(). 842: '<td> '."\n". 843: '<a href="javascript:viewOneStudent(\''.$uname.'\',\''.$udom. 844: '\');" target="_self">'.$$fullname{$_}.'</a> </td>'."\n". 845: '<td> '.$uname.' </td>'. 846: '<td> '.$udom.' </td>'; 847: if ($receiptparts) { 848: $contents.='<td> '.$part.' </td>'; 849: } 850: $contents.= 851: &Apache::loncommon::end_data_table_row()."\n"; 852: 853: $matches++; 854: } 855: } 856: } 857: if ($matches == 0) { 858: $string = $title 859: .'<p class="LC_warning">' 860: .&mt('No match found for the above receipt number.') 861: .'</p>'; 862: } else { 863: $string = &jscriptNform($symb).$title. 864: '<p>'. 865: &mt('The above receipt number matches the following [quant,_1,student].',$matches). 866: '</p>'. 867: $header. 868: $contents. 869: &Apache::loncommon::end_data_table()."\n"; 870: } 871: return $string.&show_grading_menu_form($symb); 872: } 873: 874: #--- This is called by a number of programs. 875: #--- Called from the Grading Menu - View/Grade an individual student 876: #--- Also called directly when one clicks on the subm button 877: # on the problem page. 878: sub listStudents { 879: my ($request) = shift; 880: 881: my ($symb) = &get_symb($request); 882: my $cdom = $env{"course.$env{'request.course.id'}.domain"}; 883: my $cnum = $env{"course.$env{'request.course.id'}.num"}; 884: my $getsec = $env{'form.section'} eq '' ? 'all' : $env{'form.section'}; 885: my $getgroup = $env{'form.group'} eq '' ? 'all' : $env{'form.group'}; 886: my $submitonly= $env{'form.submitonly'} eq '' ? 'all' : $env{'form.submitonly'}; 887: my $viewgrade = $env{'form.showgrading'} eq 'yes' ? 'View/Grade/Regrade' : 'View'; 888: $env{'form.probTitle'} = $env{'form.probTitle'} eq '' ? 889: &Apache::lonnet::gettitle($symb) : $env{'form.probTitle'}; 890: 891: my $result='<h3><span class="LC_info"> ' 892: .&mt("$viewgrade Submissions for a Student or a Group of Students") 893: .'</span></h3>'; 894: 895: my ($table,undef,$hdgrade,$partlist,$handgrade) = &showResourceInfo($symb,$env{'form.probTitle'},($env{'form.showgrading'} eq 'yes')); 896: 897: my %lt = &Apache::lonlocal::texthash ( 898: 'multiple' => 'Please select a student or group of students before clicking on the Next button.', 899: 'single' => 'Please select the student before clicking on the Next button.', 900: ); 901: $request->print(<<LISTJAVASCRIPT); 902: <script type="text/javascript" language="javascript"> 903: function checkSelect(checkBox) { 904: var ctr=0; 905: var sense=""; 906: if (checkBox.length > 1) { 907: for (var i=0; i<checkBox.length; i++) { 908: if (checkBox[i].checked) { 909: ctr++; 910: } 911: } 912: sense = '$lt{'multiple'}'; 913: } else { 914: if (checkBox.checked) { 915: ctr = 1; 916: } 917: sense = '$lt{'single'}'; 918: } 919: if (ctr == 0) { 920: alert(sense); 921: return false; 922: } 923: document.gradesub.submit(); 924: } 925: 926: function reLoadList(formname) { 927: if (formname.saveStatusOld.value == pullDownSelection(formname.Status)) {return;} 928: formname.command.value = 'submission'; 929: formname.submit(); 930: } 931: </script> 932: LISTJAVASCRIPT 933: 934: &commonJSfunctions($request); 935: $request->print($result); 936: 937: my $checkhdgrade = ($env{'form.handgrade'} eq 'yes' && scalar(@$partlist) > 1 ) ? 'checked="checked"' : ''; 938: my $checklastsub = $checkhdgrade eq '' ? 'checked="checked"' : ''; 939: my $gradeTable='<form action="/adm/grades" method="post" name="gradesub">'. 940: "\n".$table; 941: 942: $gradeTable .= &Apache::lonhtmlcommon::start_pick_box(); 943: $gradeTable .= &Apache::lonhtmlcommon::row_title(&mt('View Problem Text')) 944: .'<label><input type="radio" name="vProb" value="no" checked="checked" /> '.&mt('no').' </label>'."\n" 945: .'<label><input type="radio" name="vProb" value="yes" /> '.&mt('one student').' </label>'."\n" 946: .'<label><input type="radio" name="vProb" value="all" /> '.&mt('all students').' </label><br />'."\n" 947: .&Apache::lonhtmlcommon::row_closure(); 948: $gradeTable .= &Apache::lonhtmlcommon::row_title(&mt('View Answer')) 949: .'<label><input type="radio" name="vAns" value="no" /> '.&mt('no').' </label>'."\n" 950: .'<label><input type="radio" name="vAns" value="yes" /> '.&mt('one student').' </label>'."\n" 951: .'<label><input type="radio" name="vAns" value="all" checked="checked" /> '.&mt('all students').' </label><br />'."\n" 952: .&Apache::lonhtmlcommon::row_closure(); 953: 954: my $submission_options; 955: if ($env{'form.handgrade'} eq 'yes' && scalar(@$partlist) > 1) { 956: $submission_options.= 957: '<label><input type="radio" name="lastSub" value="hdgrade" '.$checkhdgrade.' /> '.&mt('essay part only').' </label>'."\n"; 958: } 959: my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status')); 960: my $saveStatus = $stu_status eq '' ? 'Active' : $stu_status; 961: $env{'form.Status'} = $saveStatus; 962: $submission_options.= 963: '<span class="LC_nobreak">'. 964: '<label><input type="radio" name="lastSub" value="lastonly" '.$checklastsub.' /> '. 965: &mt('last submission only').' </label></span>'."\n". 966: '<span class="LC_nobreak">'. 967: '<label><input type="radio" name="lastSub" value="last" /> '. 968: &mt('last submission & parts info').' </label></span>'."\n". 969: '<span class="LC_nobreak">'. 970: '<label><input type="radio" name="lastSub" value="datesub" /> '. 971: &mt('by dates and submissions').'</label></span>'."\n". 972: '<span class="LC_nobreak">'. 973: '<label><input type="radio" name="lastSub" value="all" /> '. 974: &mt('all details').'</label></span>'; 975: $gradeTable .= &Apache::lonhtmlcommon::row_title(&mt('Submissions')) 976: .$submission_options 977: .&Apache::lonhtmlcommon::row_closure(); 978: 979: $gradeTable .= &Apache::lonhtmlcommon::row_title(&mt('Grading Increments')) 980: .'<select name="increment">' 981: .'<option value="1">'.&mt('Whole Points').'</option>' 982: .'<option value=".5">'.&mt('Half Points').'</option>' 983: .'<option value=".25">'.&mt('Quarter Points').'</option>' 984: .'<option value=".1">'.&mt('Tenths of a Point').'</option>' 985: .'</select>' 986: .&Apache::lonhtmlcommon::row_closure(); 987: 988: $gradeTable .= 989: &build_section_inputs(). 990: '<input type="hidden" name="submitonly" value="'.$submitonly.'" />'."\n". 991: '<input type="hidden" name="handgrade" value="'.$env{'form.handgrade'}.'" /><br />'."\n". 992: '<input type="hidden" name="showgrading" value="'.$env{'form.showgrading'}.'" /><br />'."\n". 993: '<input type="hidden" name="saveState" value="'.$env{'form.saveState'}.'" />'."\n". 994: '<input type="hidden" name="probTitle" value="'.$env{'form.probTitle'}.'" />'."\n". 995: '<input type="hidden" name="symb" value="'.&Apache::lonenc::check_encrypt($symb).'" />'."\n". 996: '<input type="hidden" name="saveStatusOld" value="'.$saveStatus.'" />'."\n"; 997: 998: if (exists($env{'form.gradingMenu'}) && exists($env{'form.Status'})) { 999: $gradeTable .= '<input type="hidden" name="Status" value="'.$stu_status.'" />'."\n"; 1000: } else { 1001: $gradeTable .= &Apache::lonhtmlcommon::row_title(&mt('Student Status')) 1002: .&Apache::lonhtmlcommon::StatusOptions( 1003: $saveStatus,undef,1,'javascript:reLoadList(this.form);') 1004: .&Apache::lonhtmlcommon::row_closure(); 1005: } 1006: 1007: $gradeTable .= &Apache::lonhtmlcommon::row_title(&mt('Check For Plagiarism')) 1008: .'<input type="checkbox" name="checkPlag" checked="checked" />' 1009: .&Apache::lonhtmlcommon::row_closure(1) 1010: .&Apache::lonhtmlcommon::end_pick_box(); 1011: 1012: $gradeTable .= '<p>' 1013: .&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" 1014: .'<input type="hidden" name="command" value="processGroup" />' 1015: .'</p>'; 1016: 1017: # checkall buttons 1018: $gradeTable.=&check_script('gradesub', 'stuinfo'); 1019: $gradeTable.='<input type="button" '."\n". 1020: 'onclick="javascript:checkSelect(this.form.stuinfo);" '."\n". 1021: 'value="'.&mt('Next').' →" /> <br />'."\n"; 1022: $gradeTable.=&check_buttons(); 1023: my ($classlist, undef, $fullname) = &getclasslist($getsec,'1',$getgroup); 1024: $gradeTable.= &Apache::loncommon::start_data_table(). 1025: &Apache::loncommon::start_data_table_header_row(); 1026: my $loop = 0; 1027: while ($loop < 2) { 1028: $gradeTable.='<th>'.&mt('No.').'</th><th>'.&mt('Select').'</th>'. 1029: '<th>'.&nameUserString('header').' '.&mt('Section/Group').'</th>'; 1030: if ($env{'form.showgrading'} eq 'yes' 1031: && $submitonly ne 'queued' 1032: && $submitonly ne 'all') { 1033: foreach my $part (sort(@$partlist)) { 1034: my $display_part= 1035: &get_display_part((split(/_/,$part))[0],$symb); 1036: $gradeTable.= 1037: '<th>'.&mt('Part: [_1] Status',$display_part).'</th>'; 1038: } 1039: } elsif ($submitonly eq 'queued') { 1040: $gradeTable.='<th>'.&mt('Queue Status').' </th>'; 1041: } 1042: $loop++; 1043: # $gradeTable.='<td></td>' if ($loop%2 ==1); 1044: } 1045: $gradeTable.=&Apache::loncommon::end_data_table_header_row()."\n"; 1046: 1047: my $ctr = 0; 1048: foreach my $student (sort 1049: { 1050: if (lc($$fullname{$a}) ne lc($$fullname{$b})) { 1051: return (lc($$fullname{$a}) cmp lc($$fullname{$b})); 1052: } 1053: return $a cmp $b; 1054: } 1055: (keys(%$fullname))) { 1056: my ($uname,$udom) = split(/:/,$student); 1057: 1058: my %status = (); 1059: 1060: if ($submitonly eq 'queued') { 1061: my %queue_status = 1062: &Apache::bridgetask::get_student_status($symb,$cdom,$cnum, 1063: $udom,$uname); 1064: next if (!defined($queue_status{'gradingqueue'})); 1065: $status{'gradingqueue'} = $queue_status{'gradingqueue'}; 1066: } 1067: 1068: if ($env{'form.showgrading'} eq 'yes' 1069: && $submitonly ne 'queued' 1070: && $submitonly ne 'all') { 1071: (%status) =&student_gradeStatus($symb,$udom,$uname,$partlist); 1072: my $submitted = 0; 1073: my $graded = 0; 1074: my $incorrect = 0; 1075: foreach (keys(%status)) { 1076: $submitted = 1 if ($status{$_} ne 'nothing'); 1077: $graded = 1 if ($status{$_} =~ /^ungraded/); 1078: $incorrect = 1 if ($status{$_} =~ /^incorrect/); 1079: 1080: my ($foo,$partid,$foo1) = split(/\./,$_); 1081: if ($status{'resource.'.$partid.'.submitted_by'} ne '') { 1082: $submitted = 0; 1083: my ($part)=split(/\./,$partid); 1084: $gradeTable.='<input type="hidden" name="'. 1085: $student.':'.$part.':submitted_by" value="'. 1086: $status{'resource.'.$partid.'.submitted_by'}.'" />'; 1087: } 1088: } 1089: 1090: next if (!$submitted && ($submitonly eq 'yes' || 1091: $submitonly eq 'incorrect' || 1092: $submitonly eq 'graded')); 1093: next if (!$graded && ($submitonly eq 'graded')); 1094: next if (!$incorrect && $submitonly eq 'incorrect'); 1095: } 1096: 1097: $ctr++; 1098: my $section = $classlist->{$student}->[&Apache::loncoursedata::CL_SECTION()]; 1099: my $group = $classlist->{$student}->[&Apache::loncoursedata::CL_GROUP()]; 1100: if ( $perm{'vgr'} eq 'F' ) { 1101: if ($ctr%2 ==1) { 1102: $gradeTable.= &Apache::loncommon::start_data_table_row(); 1103: } 1104: $gradeTable.='<td align="right">'.$ctr.' </td>'. 1105: '<td align="center"><label><input type="checkbox" name="stuinfo" value="'. 1106: $student.':'.$$fullname{$student}.':::SECTION'.$section. 1107: ') " /> </label></td>'."\n".'<td>'. 1108: &nameUserString(undef,$$fullname{$student},$uname,$udom). 1109: ' '.$section.($group ne '' ?'/'.$group:'').'</td>'."\n"; 1110: 1111: if ($env{'form.showgrading'} eq 'yes' && $submitonly ne 'all') { 1112: foreach (sort(keys(%status))) { 1113: next if ($_ =~ /^resource.*?submitted_by$/); 1114: $gradeTable.='<td align="center"> '.&mt($status{$_}).' </td>'."\n"; 1115: } 1116: } 1117: # $gradeTable.='<td></td>' if ($ctr%2 ==1); 1118: if ($ctr%2 ==0) { 1119: $gradeTable.=&Apache::loncommon::end_data_table_row()."\n"; 1120: } 1121: } 1122: } 1123: if ($ctr%2 ==1) { 1124: $gradeTable.='<td> </td><td> </td><td> </td>'; 1125: if ($env{'form.showgrading'} eq 'yes' 1126: && $submitonly ne 'queued' 1127: && $submitonly ne 'all') { 1128: foreach (@$partlist) { 1129: $gradeTable.='<td> </td>'; 1130: } 1131: } elsif ($submitonly eq 'queued') { 1132: $gradeTable.='<td> </td>'; 1133: } 1134: $gradeTable.=&Apache::loncommon::end_data_table_row(); 1135: } 1136: 1137: $gradeTable.=&Apache::loncommon::end_data_table()."\n". 1138: '<input type="button" '. 1139: 'onclick="javascript:checkSelect(this.form.stuinfo);" '. 1140: 'value="'.&mt('Next').' →" /></form>'."\n"; 1141: if ($ctr == 0) { 1142: my $num_students=(scalar(keys(%$fullname))); 1143: if ($num_students eq 0) { 1144: $gradeTable='<br /> <span class="LC_warning">'.&mt('There are no students currently enrolled.').'</span>'; 1145: } else { 1146: my $submissions='submissions'; 1147: if ($submitonly eq 'incorrect') { $submissions = 'incorrect submissions'; } 1148: if ($submitonly eq 'graded' ) { $submissions = 'ungraded submissions'; } 1149: if ($submitonly eq 'queued' ) { $submissions = 'queued submissions'; } 1150: $gradeTable='<br /> <span class="LC_warning">'. 1151: &mt('No '.$submissions.' found for this resource for any students. ([_1] students checked for '.$submissions.')', 1152: $num_students). 1153: '</span><br />'; 1154: } 1155: } elsif ($ctr == 1) { 1156: $gradeTable =~ s/type="checkbox"/type="checkbox" checked="checked"/; 1157: } 1158: $gradeTable.=&show_grading_menu_form($symb); 1159: $request->print($gradeTable); 1160: return ''; 1161: } 1162: 1163: #---- Called from the listStudents routine 1164: 1165: sub check_script { 1166: my ($form, $type)=@_; 1167: my $chkallscript='<script type="text/javascript"> 1168: function checkall() { 1169: for (i=0; i<document.forms.'.$form.'.elements.length; i++) { 1170: ele = document.forms.'.$form.'.elements[i]; 1171: if (ele.name == "'.$type.'") { 1172: document.forms.'.$form.'.elements[i].checked=true; 1173: } 1174: } 1175: } 1176: 1177: function checksec() { 1178: for (i=0; i<document.forms.'.$form.'.elements.length; i++) { 1179: ele = document.forms.'.$form.'.elements[i]; 1180: string = document.forms.'.$form.'.chksec.value; 1181: if 1182: (ele.value.indexOf(":::SECTION"+string)>0) { 1183: document.forms.'.$form.'.elements[i].checked=true; 1184: } 1185: } 1186: } 1187: 1188: 1189: function uncheckall() { 1190: for (i=0; i<document.forms.'.$form.'.elements.length; i++) { 1191: ele = document.forms.'.$form.'.elements[i]; 1192: if (ele.name == "'.$type.'") { 1193: document.forms.'.$form.'.elements[i].checked=false; 1194: } 1195: } 1196: } 1197: 1198: </script>'."\n"; 1199: return $chkallscript; 1200: } 1201: 1202: sub check_buttons { 1203: my $buttons.='<input type="button" onclick="checkall()" value="'.&mt('Check All').'" />'; 1204: $buttons.='<input type="button" onclick="uncheckall()" value="'.&mt('Uncheck All').'" /> '; 1205: $buttons.='<input type="button" onclick="checksec()" value="'.&mt('Check Section/Group').'" />'; 1206: $buttons.='<input type="text" size="5" name="chksec" /> '; 1207: return $buttons; 1208: } 1209: 1210: # Displays the submissions for one student or a group of students 1211: sub processGroup { 1212: my ($request) = shift; 1213: my $ctr = 0; 1214: my @stuchecked = &Apache::loncommon::get_env_multiple('form.stuinfo'); 1215: my $total = scalar(@stuchecked)-1; 1216: 1217: foreach my $student (@stuchecked) { 1218: my ($uname,$udom,$fullname) = split(/:/,$student); 1219: $env{'form.student'} = $uname; 1220: $env{'form.userdom'} = $udom; 1221: $env{'form.fullname'} = $fullname; 1222: &submission($request,$ctr,$total); 1223: $ctr++; 1224: } 1225: return ''; 1226: } 1227: 1228: #------------------------------------------------------------------------------------ 1229: # 1230: #-------------------------- Next few routines handles grading by student, essentially 1231: # handles essay response type problem/part 1232: # 1233: #--- Javascript to handle the submission page functionality --- 1234: sub sub_page_js { 1235: my $request = shift; 1236: my $alertmsg = &mt('A number equal or greater than 0 is expected. Entered value = '); 1237: $request->print(<<SUBJAVASCRIPT); 1238: <script type="text/javascript" language="javascript"> 1239: function updateRadio(formname,id,weight) { 1240: var gradeBox = formname["GD_BOX"+id]; 1241: var radioButton = formname["RADVAL"+id]; 1242: var oldpts = formname["oldpts"+id].value; 1243: var pts = checkSolved(formname,id) == 'update' ? gradeBox.value : oldpts; 1244: gradeBox.value = pts; 1245: var resetbox = false; 1246: if (isNaN(pts) || pts < 0) { 1247: alert("$alertmsg"+pts); 1248: for (var i=0; i<radioButton.length; i++) { 1249: if (radioButton[i].checked) { 1250: gradeBox.value = i; 1251: resetbox = true; 1252: } 1253: } 1254: if (!resetbox) { 1255: formtextbox.value = ""; 1256: } 1257: return; 1258: } 1259: 1260: if (pts > weight) { 1261: var resp = confirm("You entered a value ("+pts+ 1262: ") greater than the weight for the part. Accept?"); 1263: if (resp == false) { 1264: gradeBox.value = oldpts; 1265: return; 1266: } 1267: } 1268: 1269: for (var i=0; i<radioButton.length; i++) { 1270: radioButton[i].checked=false; 1271: if (pts == i && pts != "") { 1272: radioButton[i].checked=true; 1273: } 1274: } 1275: updateSelect(formname,id); 1276: formname["stores"+id].value = "0"; 1277: } 1278: 1279: function writeBox(formname,id,pts) { 1280: var gradeBox = formname["GD_BOX"+id]; 1281: if (checkSolved(formname,id) == 'update') { 1282: gradeBox.value = pts; 1283: } else { 1284: var oldpts = formname["oldpts"+id].value; 1285: gradeBox.value = oldpts; 1286: var radioButton = formname["RADVAL"+id]; 1287: for (var i=0; i<radioButton.length; i++) { 1288: radioButton[i].checked=false; 1289: if (i == oldpts) { 1290: radioButton[i].checked=true; 1291: } 1292: } 1293: } 1294: formname["stores"+id].value = "0"; 1295: updateSelect(formname,id); 1296: return; 1297: } 1298: 1299: function clearRadBox(formname,id) { 1300: if (checkSolved(formname,id) == 'noupdate') { 1301: updateSelect(formname,id); 1302: return; 1303: } 1304: gradeSelect = formname["GD_SEL"+id]; 1305: for (var i=0; i<gradeSelect.length; i++) { 1306: if (gradeSelect[i].selected) { 1307: var selectx=i; 1308: } 1309: } 1310: var stores = formname["stores"+id]; 1311: if (selectx == stores.value) { return }; 1312: var gradeBox = formname["GD_BOX"+id]; 1313: gradeBox.value = ""; 1314: var radioButton = formname["RADVAL"+id]; 1315: for (var i=0; i<radioButton.length; i++) { 1316: radioButton[i].checked=false; 1317: } 1318: stores.value = selectx; 1319: } 1320: 1321: function checkSolved(formname,id) { 1322: if (formname["solved"+id].value == "correct_by_student" && formname.overRideScore.value == 'no') { 1323: var reply = confirm("This problem has been graded correct by the computer. Do you want to change the score?"); 1324: if (!reply) {return "noupdate";} 1325: formname.overRideScore.value = 'yes'; 1326: } 1327: return "update"; 1328: } 1329: 1330: function updateSelect(formname,id) { 1331: formname["GD_SEL"+id][0].selected = true; 1332: return; 1333: } 1334: 1335: //=========== Check that a point is assigned for all the parts ============ 1336: function checksubmit(formname,val,total,parttot) { 1337: formname.gradeOpt.value = val; 1338: if (val == "Save & Next") { 1339: for (i=0;i<=total;i++) { 1340: for (j=0;j<parttot;j++) { 1341: var partid = formname["partid"+i+"_"+j].value; 1342: if (formname["GD_SEL"+i+"_"+partid][0].selected) { 1343: var points = formname["GD_BOX"+i+"_"+partid].value; 1344: if (points == "") { 1345: var name = formname["name"+i].value; 1346: var studentID = (name != '' ? name : formname["unamedom"+i].value); 1347: var resp = confirm("You did not assign a score for "+studentID+ 1348: ", part "+partid+". Continue?"); 1349: if (resp == false) { 1350: formname["GD_BOX"+i+"_"+partid].focus(); 1351: return false; 1352: } 1353: } 1354: } 1355: 1356: } 1357: } 1358: 1359: } 1360: if (val == "Grade Student") { 1361: formname.showgrading.value = "yes"; 1362: if (formname.Status.value == "") { 1363: formname.Status.value = "Active"; 1364: } 1365: formname.studentNo.value = total; 1366: } 1367: formname.submit(); 1368: } 1369: 1370: //======= Check that a score is assigned for all the problems (page/sequence grading only) ========= 1371: function checkSubmitPage(formname,total) { 1372: noscore = new Array(100); 1373: var ptr = 0; 1374: for (i=1;i<total;i++) { 1375: var partid = formname["q_"+i].value; 1376: if (formname["GD_SEL"+i+"_"+partid][0].selected) { 1377: var points = formname["GD_BOX"+i+"_"+partid].value; 1378: var status = formname["solved"+i+"_"+partid].value; 1379: if (points == "" && status != "correct_by_student") { 1380: noscore[ptr] = i; 1381: ptr++; 1382: } 1383: } 1384: } 1385: if (ptr != 0) { 1386: var sense = ptr == 1 ? ": " : "s: "; 1387: var prolist = ""; 1388: if (ptr == 1) { 1389: prolist = noscore[0]; 1390: } else { 1391: var i = 0; 1392: while (i < ptr-1) { 1393: prolist += noscore[i]+", "; 1394: i++; 1395: } 1396: prolist += "and "+noscore[i]; 1397: } 1398: var resp = confirm("You did not assign any score for the following problem"+sense+prolist+". Continue?"); 1399: if (resp == false) { 1400: return false; 1401: } 1402: } 1403: 1404: formname.submit(); 1405: } 1406: </script> 1407: SUBJAVASCRIPT 1408: } 1409: 1410: #--- javascript for essay type problem -- 1411: sub sub_page_kw_js { 1412: my $request = shift; 1413: my $iconpath = $request->dir_config('lonIconsURL'); 1414: &commonJSfunctions($request); 1415: 1416: my $inner_js_msg_central=<<INNERJS; 1417: <script text="text/javascript"> 1418: function checkInput() { 1419: opener.document.SCORE.msgsub.value = opener.checkEntities(document.msgcenter.msgsub.value); 1420: var nmsg = opener.document.SCORE.savemsgN.value; 1421: var usrctr = document.msgcenter.usrctr.value; 1422: var newval = opener.document.SCORE["newmsg"+usrctr]; 1423: newval.value = opener.checkEntities(document.msgcenter.newmsg.value); 1424: 1425: var msgchk = ""; 1426: if (document.msgcenter.subchk.checked) { 1427: msgchk = "msgsub,"; 1428: } 1429: var includemsg = 0; 1430: for (var i=1; i<=nmsg; i++) { 1431: var opnmsg = opener.document.SCORE["savemsg"+i]; 1432: var frmmsg = document.msgcenter["msg"+i]; 1433: opnmsg.value = opener.checkEntities(frmmsg.value); 1434: var showflg = opener.document.SCORE["shownOnce"+i]; 1435: showflg.value = "1"; 1436: var chkbox = document.msgcenter["msgn"+i]; 1437: if (chkbox.checked) { 1438: msgchk += "savemsg"+i+","; 1439: includemsg = 1; 1440: } 1441: } 1442: if (document.msgcenter.newmsgchk.checked) { 1443: msgchk += "newmsg"+usrctr; 1444: includemsg = 1; 1445: } 1446: imgformname = opener.document.SCORE["mailicon"+usrctr]; 1447: imgformname.src = "$iconpath/"+((includemsg) ? "mailto.gif" : "mailbkgrd.gif"); 1448: var includemsg = opener.document.SCORE["includemsg"+usrctr]; 1449: includemsg.value = msgchk; 1450: 1451: self.close() 1452: 1453: } 1454: </script> 1455: INNERJS 1456: 1457: my $inner_js_highlight_central=<<INNERJS; 1458: <script type="text/javascript"> 1459: function updateChoice(flag) { 1460: opener.document.SCORE.kwclr.value = opener.radioSelection(document.hlCenter.kwdclr); 1461: opener.document.SCORE.kwsize.value = opener.radioSelection(document.hlCenter.kwdsize); 1462: opener.document.SCORE.kwstyle.value = opener.radioSelection(document.hlCenter.kwdstyle); 1463: opener.document.SCORE.refresh.value = "on"; 1464: if (opener.document.SCORE.keywords.value!=""){ 1465: opener.document.SCORE.submit(); 1466: } 1467: self.close() 1468: } 1469: </script> 1470: INNERJS 1471: 1472: my $start_page_msg_central = 1473: &Apache::loncommon::start_page('Message Central',$inner_js_msg_central, 1474: {'js_ready' => 1, 1475: 'only_body' => 1, 1476: 'bgcolor' =>'#FFFFFF',}); 1477: my $end_page_msg_central = 1478: &Apache::loncommon::end_page({'js_ready' => 1}); 1479: 1480: 1481: my $start_page_highlight_central = 1482: &Apache::loncommon::start_page('Highlight Central', 1483: $inner_js_highlight_central, 1484: {'js_ready' => 1, 1485: 'only_body' => 1, 1486: 'bgcolor' =>'#FFFFFF',}); 1487: my $end_page_highlight_central = 1488: &Apache::loncommon::end_page({'js_ready' => 1}); 1489: 1490: my $docopen=&Apache::lonhtmlcommon::javascript_docopen(); 1491: $docopen=~s/^document\.//; 1492: my %lt = &Apache::lonlocal::texthash( 1493: keyw => 'Keywords list, separated by a space. Add/delete to list if desired.', 1494: plse => 'Please select a word or group of words from document and then click this link.', 1495: adds => 'Add selection to keyword list? Edit if desired.', 1496: comp => 'Compose Message for: ', 1497: incl => 'Include', 1498: type => 'Type', 1499: subj => 'Subject', 1500: mesa => 'Message', 1501: new => 'New', 1502: save => 'Save', 1503: canc => 'Cancel', 1504: kehi => 'Keyword Highlight Options', 1505: txtc => 'Text Color', 1506: font => 'Font Size', 1507: fnst => 'Font Style', 1508: ); 1509: $request->print(<<SUBJAVASCRIPT); 1510: <script type="text/javascript" language="javascript"> 1511: 1512: //===================== Show list of keywords ==================== 1513: function keywords(formname) { 1514: var nret = prompt("$lt{'keyw'}",formname.keywords.value); 1515: if (nret==null) return; 1516: formname.keywords.value = nret; 1517: 1518: if (formname.keywords.value != "") { 1519: formname.refresh.value = "on"; 1520: formname.submit(); 1521: } 1522: return; 1523: } 1524: 1525: //===================== Script to view submitted by ================== 1526: function viewSubmitter(submitter) { 1527: document.SCORE.refresh.value = "on"; 1528: document.SCORE.NCT.value = "1"; 1529: document.SCORE.unamedom0.value = submitter; 1530: document.SCORE.submit(); 1531: return; 1532: } 1533: 1534: //===================== Script to add keyword(s) ================== 1535: function getSel() { 1536: if (document.getSelection) txt = document.getSelection(); 1537: else if (document.selection) txt = document.selection.createRange().text; 1538: else return; 1539: var cleantxt = txt.replace(new RegExp('([\\f\\n\\r\\t\\v ])+', 'g')," "); 1540: if (cleantxt=="") { 1541: alert("$lt{'plse'}"); 1542: return; 1543: } 1544: var nret = prompt("$lt{'adds'}",cleantxt); 1545: if (nret==null) return; 1546: document.SCORE.keywords.value = document.SCORE.keywords.value+" "+nret; 1547: if (document.SCORE.keywords.value != "") { 1548: document.SCORE.refresh.value = "on"; 1549: document.SCORE.submit(); 1550: } 1551: return; 1552: } 1553: 1554: //====================== Script for composing message ============== 1555: // preload images 1556: img1 = new Image(); 1557: img1.src = "$iconpath/mailbkgrd.gif"; 1558: img2 = new Image(); 1559: img2.src = "$iconpath/mailto.gif"; 1560: 1561: function msgCenter(msgform,usrctr,fullname) { 1562: var Nmsg = msgform.savemsgN.value; 1563: savedMsgHeader(Nmsg,usrctr,fullname); 1564: var subject = msgform.msgsub.value; 1565: var msgchk = document.SCORE["includemsg"+usrctr].value; 1566: re = /msgsub/; 1567: var shwsel = ""; 1568: if (re.test(msgchk)) { shwsel = "checked" } 1569: subject = (document.SCORE.shownSub.value == 0 ? checkEntities(subject) : subject); 1570: displaySubject(checkEntities(subject),shwsel); 1571: for (var i=1; i<=Nmsg; i++) { 1572: var testmsg = "savemsg"+i+","; 1573: re = new RegExp(testmsg,"g"); 1574: shwsel = ""; 1575: if (re.test(msgchk)) { shwsel = "checked" } 1576: var message = document.SCORE["savemsg"+i].value; 1577: message = (document.SCORE["shownOnce"+i].value == 0 ? checkEntities(message) : message); 1578: displaySavedMsg(i,message,shwsel); //I do not get it. w/o checkEntities on saved messages, 1579: //any < is already converted to <, etc. However, only once!! 1580: } 1581: newmsg = document.SCORE["newmsg"+usrctr].value; 1582: shwsel = ""; 1583: re = /newmsg/; 1584: if (re.test(msgchk)) { shwsel = "checked" } 1585: newMsg(newmsg,shwsel); 1586: msgTail(); 1587: return; 1588: } 1589: 1590: function checkEntities(strx) { 1591: if (strx.length == 0) return strx; 1592: var orgStr = ["&", "<", ">", '"']; 1593: var newStr = ["&", "<", ">", """]; 1594: var counter = 0; 1595: while (counter < 4) { 1596: strx = strReplace(strx,orgStr[counter],newStr[counter]); 1597: counter++; 1598: } 1599: return strx; 1600: } 1601: 1602: function strReplace(strx, orgStr, newStr) { 1603: return strx.split(orgStr).join(newStr); 1604: } 1605: 1606: function savedMsgHeader(Nmsg,usrctr,fullname) { 1607: var height = 70*Nmsg+250; 1608: var scrollbar = "no"; 1609: if (height > 600) { 1610: height = 600; 1611: scrollbar = "yes"; 1612: } 1613: var xpos = (screen.width-600)/2; 1614: xpos = (xpos < 0) ? '0' : xpos; 1615: var ypos = (screen.height-height)/2-30; 1616: ypos = (ypos < 0) ? '0' : ypos; 1617: 1618: pWin = window.open('', 'MessageCenter', 'resizable=yes,toolbar=no,location=no,scrollbars='+scrollbar+',screenx='+xpos+',screeny='+ypos+',width=700,height='+height); 1619: pWin.focus(); 1620: pDoc = pWin.document; 1621: pDoc.$docopen; 1622: pDoc.write('$start_page_msg_central'); 1623: 1624: pDoc.write("<form action=\\"inactive\\" name=\\"msgcenter\\">"); 1625: pDoc.write("<input value=\\""+usrctr+"\\" name=\\"usrctr\\" type=\\"hidden\\">"); 1626: pDoc.write("<h3><span class=\\"LC_info\\"> $lt{'comp'}\"+fullname+\"<\\/span><\\/h3><br /><br />"); 1627: 1628: pDoc.write('<table border="0" width="100%"><tr><td bgcolor="#777777">'); 1629: pDoc.write('<table border="0" width="100%"><tr bgcolor="#DDFFFF">'); 1630: pDoc.write("<td><b>$lt{'type'}<\\/b><\\/td><td><b>$lt{'incl'}<\\/b><\\/td><td><b>$lt{'mesa'}<\\/td><\\/tr>"); 1631: } 1632: function displaySubject(msg,shwsel) { 1633: pDoc = pWin.document; 1634: pDoc.write("<tr bgcolor=\\"#ffffdd\\">"); 1635: pDoc.write("<td>$lt{'subj'}<\\/td>"); 1636: pDoc.write("<td align=\\"center\\"><input name=\\"subchk\\" type=\\"checkbox\\"" +shwsel+"><\\/td>"); 1637: pDoc.write("<td><input name=\\"msgsub\\" type=\\"text\\" value=\\""+msg+"\\"size=\\"60\\" maxlength=\\"80\\"><\\/td><\\/tr>"); 1638: } 1639: 1640: function displaySavedMsg(ctr,msg,shwsel) { 1641: pDoc = pWin.document; 1642: pDoc.write("<tr bgcolor=\\"#ffffdd\\">"); 1643: pDoc.write("<td align=\\"center\\">"+ctr+"<\\/td>"); 1644: pDoc.write("<td align=\\"center\\"><input name=\\"msgn"+ctr+"\\" type=\\"checkbox\\"" +shwsel+"><\\/td>"); 1645: pDoc.write("<td><textarea name=\\"msg"+ctr+"\\" cols=\\"60\\" rows=\\"3\\">"+msg+"<\\/textarea><\\/td><\\/tr>"); 1646: } 1647: 1648: function newMsg(newmsg,shwsel) { 1649: pDoc = pWin.document; 1650: pDoc.write("<tr bgcolor=\\"#ffffdd\\">"); 1651: pDoc.write("<td align=\\"center\\">$lt{'new'}<\\/td>"); 1652: pDoc.write("<td align=\\"center\\"><input name=\\"newmsgchk\\" type=\\"checkbox\\"" +shwsel+"><\\/td>"); 1653: pDoc.write("<td><textarea name=\\"newmsg\\" cols=\\"60\\" rows=\\"3\\" onchange=\\"javascript:this.form.newmsgchk.checked=true\\" >"+newmsg+"<\\/textarea><\\/td><\\/tr>"); 1654: } 1655: 1656: function msgTail() { 1657: pDoc = pWin.document; 1658: pDoc.write("<\\/table>"); 1659: pDoc.write("<\\/td><\\/tr><\\/table> "); 1660: pDoc.write("<input type=\\"button\\" value=\\"$lt{'save'}\\" onclick=\\"javascript:checkInput()\\"> "); 1661: pDoc.write("<input type=\\"button\\" value=\\"$lt{'canc'}\\" onclick=\\"self.close()\\"><br /><br />"); 1662: pDoc.write("<\\/form>"); 1663: pDoc.write('$end_page_msg_central'); 1664: pDoc.close(); 1665: } 1666: 1667: //====================== Script for keyword highlight options ============== 1668: function kwhighlight() { 1669: var kwclr = document.SCORE.kwclr.value; 1670: var kwsize = document.SCORE.kwsize.value; 1671: var kwstyle = document.SCORE.kwstyle.value; 1672: var redsel = ""; 1673: var grnsel = ""; 1674: var blusel = ""; 1675: if (kwclr=="red") {var redsel="checked"}; 1676: if (kwclr=="green") {var grnsel="checked"}; 1677: if (kwclr=="blue") {var blusel="checked"}; 1678: var sznsel = ""; 1679: var sz1sel = ""; 1680: var sz2sel = ""; 1681: if (kwsize=="0") {var sznsel="checked"}; 1682: if (kwsize=="+1") {var sz1sel="checked"}; 1683: if (kwsize=="+2") {var sz2sel="checked"}; 1684: var synsel = ""; 1685: var syisel = ""; 1686: var sybsel = ""; 1687: if (kwstyle=="") {var synsel="checked"}; 1688: if (kwstyle=="<i>") {var syisel="checked"}; 1689: if (kwstyle=="<b>") {var sybsel="checked"}; 1690: highlightCentral(); 1691: highlightbody('red','red',redsel,'0','normal',sznsel,'','normal',synsel); 1692: highlightbody('green','green',grnsel,'+1','+1',sz1sel,'<i>','italic',syisel); 1693: highlightbody('blue','blue',blusel,'+2','+2',sz2sel,'<b>','bold',sybsel); 1694: highlightend(); 1695: return; 1696: } 1697: 1698: function highlightCentral() { 1699: // if (window.hwdWin) window.hwdWin.close(); 1700: var xpos = (screen.width-400)/2; 1701: xpos = (xpos < 0) ? '0' : xpos; 1702: var ypos = (screen.height-330)/2-30; 1703: ypos = (ypos < 0) ? '0' : ypos; 1704: 1705: hwdWin = window.open('', 'KeywordHighlightCentral', 'resizeable=yes,toolbar=no,location=no,scrollbars=no,width=400,height=300,screenx='+xpos+',screeny='+ypos); 1706: hwdWin.focus(); 1707: var hDoc = hwdWin.document; 1708: hDoc.$docopen; 1709: hDoc.write('$start_page_highlight_central'); 1710: hDoc.write("<form action=\\"inactive\\" name=\\"hlCenter\\">"); 1711: hDoc.write("<h3><span class=\\"LC_info\\"> $lt{'kehi'}<\\/span><\\/h3><br /><br />"); 1712: 1713: hDoc.write('<table border="0" width="100%"><tr><td bgcolor="#777777">'); 1714: hDoc.write('<table border="0" width="100%"><tr bgcolor="#DDFFFF">'); 1715: hDoc.write("<td><b>$lt{'txtc'}<\\/b><\\/td><td><b>$lt{'font'}<\\/b><\\/td><td><b>$lt{'fnst'}<\\/td><\\/tr>"); 1716: } 1717: 1718: function highlightbody(clrval,clrtxt,clrsel,szval,sztxt,szsel,syval,sytxt,sysel) { 1719: var hDoc = hwdWin.document; 1720: hDoc.write("<tr bgcolor=\\"#ffffdd\\">"); 1721: hDoc.write("<td align=\\"left\\">"); 1722: hDoc.write("<input name=\\"kwdclr\\" type=\\"radio\\" value=\\""+clrval+"\\" "+clrsel+"> "+clrtxt+"<\\/td>"); 1723: hDoc.write("<td align=\\"left\\">"); 1724: hDoc.write("<input name=\\"kwdsize\\" type=\\"radio\\" value=\\""+szval+"\\" "+szsel+"> "+sztxt+"<\\/td>"); 1725: hDoc.write("<td align=\\"left\\">"); 1726: hDoc.write("<input name=\\"kwdstyle\\" type=\\"radio\\" value=\\""+syval+"\\" "+sysel+"> "+sytxt+"<\\/td>"); 1727: hDoc.write("<\\/tr>"); 1728: } 1729: 1730: function highlightend() { 1731: var hDoc = hwdWin.document; 1732: hDoc.write("<\\/table>"); 1733: hDoc.write("<\\/td><\\/tr><\\/table> "); 1734: hDoc.write("<input type=\\"button\\" value=\\"$lt{'save'}\\" onclick=\\"javascript:updateChoice(1)\\"> "); 1735: hDoc.write("<input type=\\"button\\" value=\\"$lt{'canc'}\\" onclick=\\"self.close()\\"><br /><br />"); 1736: hDoc.write("<\\/form>"); 1737: hDoc.write('$end_page_highlight_central'); 1738: hDoc.close(); 1739: } 1740: 1741: </script> 1742: SUBJAVASCRIPT 1743: } 1744: 1745: sub get_increment { 1746: my $increment = $env{'form.increment'}; 1747: if ($increment != 1 && $increment != .5 && $increment != .25 && 1748: $increment != .1) { 1749: $increment = 1; 1750: } 1751: return $increment; 1752: } 1753: 1754: sub gradeBox_start { 1755: return ( 1756: &Apache::loncommon::start_data_table() 1757: .&Apache::loncommon::start_data_table_header_row() 1758: .'<th>'.&mt('Part').'</th>' 1759: .'<th>'.&mt('Points').'</th>' 1760: .'<th> </th>' 1761: .'<th>'.&mt('Assign Grade').'</th>' 1762: .'<th>'.&mt('Weight').'</th>' 1763: .'<th>'.&mt('Grade Status').'</th>' 1764: .&Apache::loncommon::end_data_table_header_row() 1765: ); 1766: } 1767: 1768: sub gradeBox_end { 1769: return ( 1770: &Apache::loncommon::end_data_table() 1771: ); 1772: } 1773: #--- displays the grading box, used in essay type problem and grading by page/sequence 1774: sub gradeBox { 1775: my ($request,$symb,$uname,$udom,$counter,$partid,$record) = @_; 1776: my $checkIcon = '<img alt="'.&mt('Check Mark'). 1777: '" src="'.&Apache::loncommon::lonhttpdurl($request->dir_config('lonIconsURL').'/check.gif').'" height="16" border="0" />'; 1778: my $wgt = &Apache::lonnet::EXT('resource.'.$partid.'.weight',$symb,$udom,$uname); 1779: my $wgtmsg = ($wgt > 0) ? &mt('(problem weight)') 1780: : '<span class="LC_info">'.&mt('problem weight assigned by computer').'</span>'; 1781: $wgt = ($wgt > 0 ? $wgt : '1'); 1782: my $score = ($$record{'resource.'.$partid.'.awarded'} eq '' ? 1783: '' : &compute_points($$record{'resource.'.$partid.'.awarded'},$wgt)); 1784: my $result='<input type="hidden" name="WGT'.$counter.'_'.$partid.'" value="'.$wgt.'" />'."\n"; 1785: my $display_part= &get_display_part($partid,$symb); 1786: my %last_resets = &get_last_resets($symb,$env{'request.course.id'}, 1787: [$partid]); 1788: my $aggtries = $$record{'resource.'.$partid.'.tries'}; 1789: if ($last_resets{$partid}) { 1790: $aggtries = &get_num_tries($record,$last_resets{$partid},$partid); 1791: } 1792: $result.=&Apache::loncommon::start_data_table_row(); 1793: my $ctr = 0; 1794: my $thisweight = 0; 1795: my $increment = &get_increment(); 1796: 1797: my $radio.='<table border="0"><tr>'."\n"; # display radio buttons in a nice table 10 across 1798: while ($thisweight<=$wgt) { 1799: $radio.= '<td><span class="LC_nobreak"><label><input type="radio" name="RADVAL'.$counter.'_'.$partid.'" '. 1800: 'onclick="javascript:writeBox(this.form,\''.$counter.'_'.$partid.'\','. 1801: $thisweight.')" value="'.$thisweight.'" '. 1802: ($score eq $thisweight ? 'checked="checked"':'').' /> '.$thisweight."</label></span></td>\n"; 1803: $radio.=(($ctr+1)%10 == 0 ? '</tr><tr>' : ''); 1804: $thisweight += $increment; 1805: $ctr++; 1806: } 1807: $radio.='</tr></table>'; 1808: 1809: my $line.='<input type="text" name="GD_BOX'.$counter.'_'.$partid.'"'. 1810: ($score ne ''? ' value = "'.$score.'"':'').' size="4" '. 1811: 'onchange="javascript:updateRadio(this.form,\''.$counter.'_'.$partid.'\','. 1812: $wgt.')" /></td>'."\n"; 1813: $line.='<td>/'.$wgt.' '.$wgtmsg. 1814: ($$record{'resource.'.$partid.'.solved'} eq 'correct_by_student' ? ' '.$checkIcon : ''). 1815: ' </td>'."\n"; 1816: $line.='<td><select name="GD_SEL'.$counter.'_'.$partid.'" '. 1817: 'onchange="javascript:clearRadBox(this.form,\''.$counter.'_'.$partid.'\')" >'."\n"; 1818: if ($$record{'resource.'.$partid.'.solved'} eq 'excused') { 1819: $line.='<option></option>'. 1820: '<option value="excused" selected="selected">'.&mt('excused').'</option>'; 1821: } else { 1822: $line.='<option selected="selected"></option>'. 1823: '<option value="excused" >'.&mt('excused').'</option>'; 1824: } 1825: $line.='<option value="reset status">'.&mt('reset status').'</option></select>'."\n"; 1826: 1827: 1828: #&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); 1829: $result .= 1830: '<td>'.$display_part.'</td><td>'.$radio.'</td><td>'.&mt('or').'</td><td>'.$line.'</td>'; 1831: $result.=&Apache::loncommon::end_data_table_row(); 1832: $result.='<input type="hidden" name="stores'.$counter.'_'.$partid.'" value="" />'."\n". 1833: '<input type="hidden" name="oldpts'.$counter.'_'.$partid.'" value="'.$score.'" />'."\n". 1834: '<input type="hidden" name="solved'.$counter.'_'.$partid.'" value="'. 1835: $$record{'resource.'.$partid.'.solved'}.'" />'."\n". 1836: '<input type="hidden" name="totaltries'.$counter.'_'.$partid.'" value="'. 1837: $$record{'resource.'.$partid.'.tries'}.'" />'."\n". 1838: '<input type="hidden" name="aggtries'.$counter.'_'.$partid.'" value="'. 1839: $aggtries.'" />'."\n"; 1840: my $res_error; 1841: $result.=&handback_box($symb,$uname,$udom,$counter,$partid,$record,\$res_error); 1842: if ($res_error) { 1843: return &navmap_errormsg(); 1844: } 1845: return $result; 1846: } 1847: 1848: sub handback_box { 1849: my ($symb,$uname,$udom,$counter,$partid,$record,$res_error) = @_; 1850: my ($partlist,$handgrade,$responseType) = &response_type($symb,$res_error); 1851: my (@respids); 1852: my @part_response_id = &flatten_responseType($responseType); 1853: foreach my $part_response_id (@part_response_id) { 1854: my ($part,$resp) = @{ $part_response_id }; 1855: if ($part eq $partid) { 1856: push(@respids,$resp); 1857: } 1858: } 1859: my $result; 1860: foreach my $respid (@respids) { 1861: my $prefix = $counter.'_'.$partid.'_'.$respid.'_'; 1862: my $files=&get_submitted_files($udom,$uname,$partid,$respid,$record); 1863: next if (!@$files); 1864: my $file_counter = 0; 1865: foreach my $file (@$files) { 1866: if ($file =~ /\/portfolio\//) { 1867: $file_counter++; 1868: my ($file_path, $file_disp) = ($file =~ m|(.+/)(.+)$|); 1869: my ($name,$version,$ext) = &file_name_version_ext($file_disp); 1870: $file_disp = "$name.$ext"; 1871: $file = $file_path.$file_disp; 1872: $result.=&mt('Return commented version of [_1] to student.', 1873: '<span class="LC_filename">'.$file_disp.'</span>'); 1874: $result.='<input type="file" name="'.$prefix.'returndoc'.$file_counter.'" />'."\n"; 1875: $result.='<input type="hidden" name="'.$prefix.'origdoc'.$file_counter.'" value="'.$file.'" /><br />'."\n"; 1876: } 1877: } 1878: if ($file_counter) { 1879: $result .= '<input type="hidden" name="'.$prefix.'countreturndoc" value="'.$file_counter.'" />'."\n". 1880: '<span class="LC_info">'. 1881: '('.&mt('File(s) will be uploaded when you click on Save & Next below.',$file_counter).')</span><br /><br />'; 1882: } 1883: } 1884: return $result; 1885: } 1886: 1887: sub show_problem { 1888: my ($request,$symb,$uname,$udom,$removeform,$viewon,$mode,$form) = @_; 1889: my $rendered; 1890: my %form = ((ref($form) eq 'HASH')? %{$form} : ()); 1891: &Apache::lonxml::remember_problem_counter(); 1892: if ($mode eq 'both' or $mode eq 'text') { 1893: $rendered=&Apache::loncommon::get_student_view($symb,$uname,$udom, 1894: $env{'request.course.id'}, 1895: undef,\%form); 1896: } 1897: if ($removeform) { 1898: $rendered=~s|<form(.*?)>||g; 1899: $rendered=~s|</form>||g; 1900: $rendered=~s|(<input[^>]*name\s*=\s*"?)(\w+)("?)|$1would_have_been_$2$3|g; 1901: } 1902: my $companswer; 1903: if ($mode eq 'both' or $mode eq 'answer') { 1904: &Apache::lonxml::restore_problem_counter(); 1905: $companswer= 1906: &Apache::loncommon::get_student_answers($symb,$uname,$udom, 1907: $env{'request.course.id'}, 1908: %form); 1909: } 1910: if ($removeform) { 1911: $companswer=~s|<form(.*?)>||g; 1912: $companswer=~s|</form>||g; 1913: $companswer=~s|name="submit"|name="would_have_been_submit"|g; 1914: } 1915: $rendered= 1916: '<div class="LC_Box">' 1917: .'<h3 class="LC_hcell">'.&mt('View of the problem').'</h3>' 1918: .$rendered 1919: .'</div>'; 1920: $companswer= 1921: '<div class="LC_Box">' 1922: .'<h3 class="LC_hcell">'.&mt('Correct answer').'</h3>' 1923: .$companswer 1924: .'</div>'; 1925: my $result; 1926: if ($mode eq 'both') { 1927: $result=$rendered.$companswer; 1928: } elsif ($mode eq 'text') { 1929: $result=$rendered; 1930: } elsif ($mode eq 'answer') { 1931: $result=$companswer; 1932: } 1933: return $result; 1934: } 1935: 1936: sub files_exist { 1937: my ($r, $symb) = @_; 1938: my @students = &Apache::loncommon::get_env_multiple('form.stuinfo'); 1939: 1940: foreach my $student (@students) { 1941: my ($uname,$udom,$fullname) = split(/:/,$student); 1942: my %record = &Apache::lonnet::restore($symb,$env{'request.course.id'}, 1943: $udom,$uname); 1944: my ($string,$timestamp)= &get_last_submission(\%record); 1945: foreach my $submission (@$string) { 1946: my ($partid,$respid) = 1947: ($submission =~ /^resource\.([^\.]*)\.([^\.]*)\.submission/); 1948: my $files=&get_submitted_files($udom,$uname,$partid,$respid, 1949: \%record); 1950: return 1 if (@$files); 1951: } 1952: } 1953: return 0; 1954: } 1955: 1956: sub download_all_link { 1957: my ($r,$symb) = @_; 1958: my $all_students = 1959: join("\n", &Apache::loncommon::get_env_multiple('form.stuinfo')); 1960: 1961: my $parts = 1962: join("\n",&Apache::loncommon::get_env_multiple('form.vPart')); 1963: 1964: my $identifier = &Apache::loncommon::get_cgi_id(); 1965: &Apache::lonnet::appenv({'cgi.'.$identifier.'.students' => $all_students, 1966: 'cgi.'.$identifier.'.symb' => $symb, 1967: 'cgi.'.$identifier.'.parts' => $parts,}); 1968: $r->print('<a href="/cgi-bin/multidownload.pl?'.$identifier.'">'. 1969: &mt('Download All Submitted Documents').'</a>'); 1970: return 1971: } 1972: 1973: sub build_section_inputs { 1974: my $section_inputs; 1975: if ($env{'form.section'} eq '') { 1976: $section_inputs .= '<input type="hidden" name="section" value="all" />'."\n"; 1977: } else { 1978: my @sections = &Apache::loncommon::get_env_multiple('form.section'); 1979: foreach my $section (@sections) { 1980: $section_inputs .= '<input type="hidden" name="section" value="'.$section.'" />'."\n"; 1981: } 1982: } 1983: return $section_inputs; 1984: } 1985: 1986: # --------------------------- show submissions of a student, option to grade 1987: sub submission { 1988: my ($request,$counter,$total) = @_; 1989: my ($uname,$udom) = ($env{'form.student'},$env{'form.userdom'}); 1990: $udom = ($udom eq '' ? $env{'user.domain'} : $udom); #has form.userdom changed for a student? 1991: my $usec = &Apache::lonnet::getsection($udom,$uname,$env{'request.course.id'}); 1992: $env{'form.fullname'} = &Apache::loncommon::plainname($uname,$udom,'lastname') if $env{'form.fullname'} eq ''; 1993: my $symb = &get_symb($request); 1994: if ($symb eq '') { $request->print("Unable to handle ambiguous references:."); return ''; } 1995: 1996: if (!&canview($usec)) { 1997: $request->print('<span class="LC_warning">Unable to view requested student.('. 1998: $uname.':'.$udom.' in section '.$usec.' in course id '. 1999: $env{'request.course.id'}.')</span>'); 2000: $request->print(&show_grading_menu_form($symb)); 2001: return; 2002: } 2003: 2004: if (!$env{'form.lastSub'}) { $env{'form.lastSub'} = 'datesub'; } 2005: if (!$env{'form.vProb'}) { $env{'form.vProb'} = 'yes'; } 2006: if (!$env{'form.vAns'}) { $env{'form.vAns'} = 'yes'; } 2007: my $last = ($env{'form.lastSub'} eq 'last' ? 'last' : ''); 2008: my $checkIcon = '<img alt="'.&mt('Check Mark'). 2009: '" src="'.$request->dir_config('lonIconsURL'). 2010: '/check.gif" height="16" border="0" />'; 2011: 2012: my %old_essays; 2013: # header info 2014: if ($counter == 0) { 2015: &sub_page_js($request); 2016: &sub_page_kw_js($request) if ($env{'form.handgrade'} eq 'yes'); 2017: $env{'form.probTitle'} = $env{'form.probTitle'} eq '' ? 2018: &Apache::lonnet::gettitle($symb) : $env{'form.probTitle'}; 2019: if ($env{'form.handgrade'} eq 'yes' && &files_exist($request, $symb)) { 2020: &download_all_link($request, $symb); 2021: } 2022: $request->print('<h3> <span class="LC_info">'.&mt('Submission Record').'</span></h3>'."\n". 2023: '<h4> '.&mt('<b>Resource: </b> [_1]',$env{'form.probTitle'}).'</h4>'."\n"); 2024: 2025: # option to display problem, only once else it cause problems 2026: # with the form later since the problem has a form. 2027: if ($env{'form.vProb'} eq 'yes' or $env{'form.vAns'} eq 'yes') { 2028: my $mode; 2029: if ($env{'form.vProb'} eq 'yes' && $env{'form.vAns'} eq 'yes') { 2030: $mode='both'; 2031: } elsif ($env{'form.vProb'} eq 'yes') { 2032: $mode='text'; 2033: } elsif ($env{'form.vAns'} eq 'yes') { 2034: $mode='answer'; 2035: } 2036: &Apache::lonxml::clear_problem_counter(); 2037: $request->print(&show_problem($request,$symb,$uname,$udom,0,1,$mode)); 2038: } 2039: 2040: # kwclr is the only variable that is guaranteed to be non blank 2041: # if this subroutine has been called once. 2042: my %keyhash = (); 2043: if ($env{'form.kwclr'} eq '' && $env{'form.handgrade'} eq 'yes') { 2044: %keyhash = &Apache::lonnet::dump('nohist_handgrade', 2045: $env{'course.'.$env{'request.course.id'}.'.domain'}, 2046: $env{'course.'.$env{'request.course.id'}.'.num'}); 2047: 2048: my $loginuser = $env{'user.name'}.':'.$env{'user.domain'}; 2049: $env{'form.keywords'} = $keyhash{$symb.'_keywords'} ne '' ? $keyhash{$symb.'_keywords'} : ''; 2050: $env{'form.kwclr'} = $keyhash{$loginuser.'_kwclr'} ne '' ? $keyhash{$loginuser.'_kwclr'} : 'red'; 2051: $env{'form.kwsize'} = $keyhash{$loginuser.'_kwsize'} ne '' ? $keyhash{$loginuser.'_kwsize'} : '0'; 2052: $env{'form.kwstyle'} = $keyhash{$loginuser.'_kwstyle'} ne '' ? $keyhash{$loginuser.'_kwstyle'} : ''; 2053: $env{'form.msgsub'} = $keyhash{$symb.'_subject'} ne '' ? 2054: $keyhash{$symb.'_subject'} : $env{'form.probTitle'}; 2055: $env{'form.savemsgN'} = $keyhash{$symb.'_savemsgN'} ne '' ? $keyhash{$symb.'_savemsgN'} : '0'; 2056: } 2057: my $overRideScore = $env{'form.overRideScore'} eq '' ? 'no' : $env{'form.overRideScore'}; 2058: my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status')); 2059: $request->print('<form action="/adm/grades" method="post" name="SCORE" enctype="multipart/form-data">'."\n". 2060: '<input type="hidden" name="command" value="handgrade" />'."\n". 2061: '<input type="hidden" name="saveState" value="'.$env{'form.saveState'}.'" />'."\n". 2062: '<input type="hidden" name="Status" value="'.$stu_status.'" />'."\n". 2063: '<input type="hidden" name="overRideScore" value="'.$overRideScore.'" />'."\n". 2064: '<input type="hidden" name="probTitle" value="'.$env{'form.probTitle'}.'" />'."\n". 2065: '<input type="hidden" name="refresh" value="off" />'."\n". 2066: '<input type="hidden" name="studentNo" value="" />'."\n". 2067: '<input type="hidden" name="gradeOpt" value="" />'."\n". 2068: '<input type="hidden" name="symb" value="'.&Apache::lonenc::check_encrypt($symb).'" />'."\n". 2069: '<input type="hidden" name="showgrading" value="'.$env{'form.showgrading'}.'" />'."\n". 2070: '<input type="hidden" name="vProb" value="'.$env{'form.vProb'}.'" />'."\n". 2071: '<input type="hidden" name="vAns" value="'.$env{'form.vAns'}.'" />'."\n". 2072: '<input type="hidden" name="lastSub" value="'.$env{'form.lastSub'}.'" />'."\n". 2073: &build_section_inputs(). 2074: '<input type="hidden" name="submitonly" value="'.$env{'form.submitonly'}.'" />'."\n". 2075: '<input type="hidden" name="handgrade" value="'.$env{'form.handgrade'}.'" />'."\n". 2076: '<input type="hidden" name="NCT"'. 2077: ' value="'.($env{'form.NTSTU'} ne '' ? $env{'form.NTSTU'} : $total+1).'" />'."\n"); 2078: if ($env{'form.handgrade'} eq 'yes') { 2079: $request->print('<input type="hidden" name="keywords" value="'.$env{'form.keywords'}.'" />'."\n". 2080: '<input type="hidden" name="kwclr" value="'.$env{'form.kwclr'}.'" />'."\n". 2081: '<input type="hidden" name="kwsize" value="'.$env{'form.kwsize'}.'" />'."\n". 2082: '<input type="hidden" name="kwstyle" value="'.$env{'form.kwstyle'}.'" />'."\n". 2083: '<input type="hidden" name="msgsub" value="'.$env{'form.msgsub'}.'" />'."\n". 2084: '<input type="hidden" name="shownSub" value="0" />'."\n". 2085: '<input type="hidden" name="savemsgN" value="'.$env{'form.savemsgN'}.'" />'."\n"); 2086: foreach my $partid (&Apache::loncommon::get_env_multiple('form.vPart')) { 2087: $request->print('<input type="hidden" name="vPart" value="'.$partid.'" />'."\n"); 2088: } 2089: } 2090: 2091: my ($cts,$prnmsg) = (1,''); 2092: while ($cts <= $env{'form.savemsgN'}) { 2093: $prnmsg.='<input type="hidden" name="savemsg'.$cts.'" value="'. 2094: (!exists($keyhash{$symb.'_savemsg'.$cts}) ? 2095: &Apache::lonfeedback::clear_out_html($env{'form.savemsg'.$cts}) : 2096: &Apache::lonfeedback::clear_out_html($keyhash{$symb.'_savemsg'.$cts})). 2097: '" />'."\n". 2098: '<input type="hidden" name="shownOnce'.$cts.'" value="0" />'."\n"; 2099: $cts++; 2100: } 2101: $request->print($prnmsg); 2102: 2103: if ($env{'form.handgrade'} eq 'yes' && $env{'form.showgrading'} eq 'yes') { 2104: 2105: my %lt = &Apache::lonlocal::texthash( 2106: keyw => 'Keyword Options', 2107: list => 'List', 2108: past => 'Paste Selection to List', 2109: high => 'Hightlight Attribute', 2110: ); 2111: # 2112: # Print out the keyword options line 2113: # 2114: $request->print(<<KEYWORDS); 2115: <b>$lt{'keyw'}:</b> 2116: <a href="javascript:keywords(document.SCORE);" target="_self">$lt{'list'}</a> 2117: <a href="#" onmousedown="javascript:getSel(); return false" 2118: CLASS="page">$lt{'past'}</a> 2119: <a href="javascript:kwhighlight();" target="_self">$lt{'high'}</a><br /><br /> 2120: KEYWORDS 2121: # 2122: # Load the other essays for similarity check 2123: # 2124: my (undef,undef,$essayurl) = &Apache::lonnet::decode_symb($symb); 2125: my ($adom,$aname,$apath)=($essayurl=~/^($LONCAPA::domain_re)\/($LONCAPA::username_re)\/(.*)$/); 2126: $apath=&escape($apath); 2127: $apath=~s/\W/\_/gs; 2128: %old_essays=&Apache::lonnet::dump('nohist_essay_'.$apath,$adom,$aname); 2129: } 2130: } 2131: 2132: # This is where output for one specific student would start 2133: my $add_class = ($counter%2) ? ' LC_grade_show_user_odd_row' : ''; 2134: $request->print( 2135: "\n\n" 2136: .'<div class="LC_grade_show_user'.$add_class.'">' 2137: .'<h2>'.&nameUserString(undef,$env{'form.fullname'},$uname,$udom).'</h2>' 2138: ."\n" 2139: ); 2140: 2141: # Show additional functions if allowed 2142: if ($perm{'vgr'}) { 2143: $request->print( 2144: &Apache::loncommon::track_student_link( 2145: &mt('View recent activity'), 2146: $uname,$udom,'check') 2147: .' ' 2148: ); 2149: } 2150: if ($perm{'opa'}) { 2151: $request->print( 2152: &Apache::loncommon::pprmlink( 2153: &mt('Set/Change parameters'), 2154: $uname,$udom,$symb,'check')); 2155: } 2156: 2157: # Show Problem 2158: if ($env{'form.vProb'} eq 'all' or $env{'form.vAns'} eq 'all') { 2159: my $mode; 2160: if ($env{'form.vProb'} eq 'all' && $env{'form.vAns'} eq 'all') { 2161: $mode='both'; 2162: } elsif ($env{'form.vProb'} eq 'all' ) { 2163: $mode='text'; 2164: } elsif ($env{'form.vAns'} eq 'all') { 2165: $mode='answer'; 2166: } 2167: &Apache::lonxml::clear_problem_counter(); 2168: $request->print(&show_problem($request,$symb,$uname,$udom,1,1,$mode,{'request.prefix' => 'ctr'.$counter})); 2169: } 2170: 2171: my %record = &Apache::lonnet::restore($symb,$env{'request.course.id'},$udom,$uname); 2172: my $res_error; 2173: my ($partlist,$handgrade,$responseType) = &response_type($symb,\$res_error); 2174: if ($res_error) { 2175: $request->print(&navmap_errormsg()); 2176: return; 2177: } 2178: 2179: # Display student info 2180: $request->print(($counter == 0 ? '' : '<br />')); 2181: 2182: my $result='<div class="LC_Box">' 2183: .'<h3 class="LC_hcell">'.&mt('Submissions').'</h3>'; 2184: $result.='<input type="hidden" name="name'.$counter. 2185: '" value="'.$env{'form.fullname'}.'" />'."\n"; 2186: if ($env{'form.handgrade'} eq 'no') { 2187: $result.='<p class="LC_info">' 2188: .&mt('Part(s) graded correct by the computer is marked with a [_1] symbol.',$checkIcon) 2189: ."</p>\n"; 2190: } 2191: 2192: # If any part of the problem is an essay-response (handgraded), then check for collaborators 2193: my $fullname; 2194: my $col_fullnames = []; 2195: if ($env{'form.handgrade'} eq 'yes') { 2196: (my $sub_result,$fullname,$col_fullnames)= 2197: &check_collaborators($symb,$uname,$udom,\%record,$handgrade, 2198: $counter); 2199: $result.=$sub_result; 2200: } 2201: $request->print($result."\n"); 2202: 2203: # print student answer/submission 2204: # Options are (1) Handgraded submission only 2205: # (2) Last submission, includes submission that is not handgraded 2206: # (for multi-response type part) 2207: # (3) Last submission plus the parts info 2208: # (4) The whole record for this student 2209: if ($env{'form.lastSub'} =~ /^(lastonly|hdgrade)$/) { 2210: my ($string,$timestamp)= &get_last_submission(\%record); 2211: 2212: my $lastsubonly; 2213: 2214: if ($$timestamp eq '') { 2215: $lastsubonly.='<div class="LC_grade_submissions_body">'.$$string[0].'</div>'; 2216: } else { 2217: $lastsubonly = 2218: '<div class="LC_grade_submissions_body">' 2219: .'<b>'.&mt('Date Submitted:').'</b> '.$$timestamp."\n"; 2220: 2221: my %seenparts; 2222: my @part_response_id = &flatten_responseType($responseType); 2223: foreach my $part (@part_response_id) { 2224: next if ($env{'form.lastSub'} eq 'hdgrade' 2225: && $$handgrade{$$part[0].'_'.$$part[1]} ne 'yes'); 2226: 2227: my ($partid,$respid) = @{ $part }; 2228: my $display_part=&get_display_part($partid,$symb); 2229: if ($env{"form.$uname:$udom:$partid:submitted_by"}) { 2230: if (exists($seenparts{$partid})) { next; } 2231: $seenparts{$partid}=1; 2232: my $submitby='<b>Part:</b> '.$display_part. 2233: ' <b>Collaborative submission by:</b> '. 2234: '<a href="javascript:viewSubmitter(\''. 2235: $env{"form.$uname:$udom:$partid:submitted_by"}. 2236: '\');" target="_self">'. 2237: $$fullname{$env{"form.$uname:$udom:$partid:submitted_by"}}.'</a><br />'; 2238: $request->print($submitby); 2239: next; 2240: } 2241: my $responsetype = $responseType->{$partid}->{$respid}; 2242: if (!exists($record{"resource.$partid.$respid.submission"})) { 2243: $lastsubonly.="\n".'<div class="LC_grade_submission_part">'. 2244: '<b>'.&mt('Part: [_1]',$display_part).'</b>'. 2245: ' <span class="LC_internal_info">'. 2246: '('.&mt('Response ID: [_1]',$respid).')'. 2247: '</span> '. 2248: '<span class="LC_warning">'.&mt('Nothing submitted - no attempts.').'</span><br /><br /></div>'; 2249: next; 2250: } 2251: foreach my $submission (@$string) { 2252: my ($partid,$respid) = ($submission =~ /^resource\.([^\.]*)\.([^\.]*)\.submission/); 2253: if (join('_',@{$part}) ne ($partid.'_'.$respid)) { next; } 2254: my ($ressub,$hide,$subval) = split(/:/,$submission,3); 2255: # Similarity check 2256: my $similar=''; 2257: my ($type,$trial,$rndseed); 2258: if ($hide eq 'rand') { 2259: $type = 'randomizetry'; 2260: $trial = $record{"resource.$partid.tries"}; 2261: $rndseed = $record{"resource.$partid.rndseed"}; 2262: } 2263: if($env{'form.checkPlag'}){ 2264: my ($oname,$odom,$ocrsid,$oessay,$osim)= 2265: &most_similar($uname,$udom,$subval,\%old_essays); 2266: if ($osim) { 2267: $osim=int($osim*100.0); 2268: my %old_course_desc = 2269: &Apache::lonnet::coursedescription($ocrsid, 2270: {'one_time' => 1}); 2271: 2272: if ($hide eq 'anon') { 2273: $similar='<hr /><span class="LC_warning">'.&mt("Essay was found to be similar to another essay submitted for this assignment.").'<br />'. 2274: &mt('As the current submission is for an anonymous survey, no other details are available.').'</span><hr />'; 2275: } else { 2276: $similar="<hr /><h3><span class=\"LC_warning\">". 2277: &mt('Essay is [_1]% similar to an essay by [_2] in course [_3] (course id [_4]:[_5])', 2278: $osim, 2279: &Apache::loncommon::plainname($oname,$odom).' ('.$oname.':'.$odom.')', 2280: $old_course_desc{'description'}, 2281: $old_course_desc{'num'}, 2282: $old_course_desc{'domain'}). 2283: '</span></h3><blockquote><i>'. 2284: &keywords_highlight($oessay). 2285: '</i></blockquote><hr />'; 2286: } 2287: } 2288: } 2289: my $order=&get_order($partid,$respid,$symb,$uname,$udom, 2290: undef,$type,$trial,$rndseed); 2291: if ($env{'form.lastSub'} eq 'lastonly' || 2292: ($env{'form.lastSub'} eq 'hdgrade' && 2293: $$handgrade{$$part[0].'_'.$$part[1]} eq 'yes')) { 2294: my $display_part=&get_display_part($partid,$symb); 2295: $lastsubonly.='<div class="LC_grade_submission_part">'. 2296: '<b>'.&mt('Part: [_1]',$display_part).'</b>'. 2297: ' <span class="LC_internal_info">'. 2298: '('.&mt('Response ID: [_1]',$respid).')'. 2299: '</span> '; 2300: my $files=&get_submitted_files($udom,$uname,$partid,$respid,\%record); 2301: if (@$files) { 2302: if ($hide eq 'anon') { 2303: $lastsubonly.='<br />'.&mt('[quant,_1,file] uploaded to this anonymous survey',scalar(@{$files})); 2304: } else { 2305: $lastsubonly.='<br /><span class="LC_warning">'.&mt('Like all files provided by users, this file may contain viruses').'</span><br />'; 2306: foreach my $file (@$files) { 2307: &Apache::lonnet::allowuploaded('/adm/grades',$file); 2308: $lastsubonly.='<br /><a href="'.$file.'?rawmode=1" target="lonGRDs"><img src="'.&Apache::loncommon::icon($file).'" border="0" /> '.$file.'</a>'; 2309: } 2310: } 2311: $lastsubonly.='<br />'; 2312: } 2313: if ($hide eq 'anon') { 2314: $lastsubonly.='<b>'.&mt('Anonymous Survey').'</b>'; 2315: } else { 2316: $lastsubonly.='<b>'.&mt('Submitted Answer:').' </b>'. 2317: &cleanRecord($subval,$responsetype,$symb,$partid, 2318: $respid,\%record,$order,undef,$uname,$udom,$type,$trial,$rndseed); 2319: } 2320: if ($similar) {$lastsubonly.="<br /><br />$similar\n";} 2321: $lastsubonly.='</div>'; 2322: } 2323: } 2324: } 2325: $lastsubonly.='</div>'."\n"; # End: LC_grade_submissions_body 2326: } 2327: $request->print($lastsubonly); 2328: } elsif ($env{'form.lastSub'} eq 'datesub') { 2329: my (undef,$responseType,undef,$parts) = &showResourceInfo($symb); 2330: $request->print(&displaySubByDates($symb,\%record,$parts,$responseType,$checkIcon,$uname,$udom)); 2331: } elsif ($env{'form.lastSub'} =~ /^(last|all)$/) { 2332: $request->print(&Apache::loncommon::get_previous_attempt($symb,$uname,$udom, 2333: $env{'request.course.id'}, 2334: $last,'.submission', 2335: 'Apache::grades::keywords_highlight')); 2336: } 2337: 2338: $request->print('<input type="hidden" name="unamedom'.$counter.'" value="'.$uname.':' 2339: .$udom.'" />'."\n"); 2340: # return if view submission with no grading option 2341: if ($env{'form.showgrading'} eq '' || (!&canmodify($usec))) { 2342: my $toGrade.='<input type="button" value="Grade Student" '. 2343: 'onclick="javascript:checksubmit(this.form,\'Grade Student\',\'' 2344: .$counter.'\');" target="_self" /> '."\n" if (&canmodify($usec)); 2345: $toGrade.='</div>'."\n"; 2346: if (($env{'form.command'} eq 'submission') || 2347: ($env{'form.command'} eq 'processGroup' && $counter == $total)) { 2348: $toGrade.='</form>'.&show_grading_menu_form($symb); 2349: } 2350: $request->print($toGrade); 2351: return; 2352: } else { 2353: $request->print('</div>'."\n"); 2354: } 2355: 2356: # essay grading message center 2357: if ($env{'form.handgrade'} eq 'yes') { 2358: my $result='<div class="LC_grade_message_center">'; 2359: 2360: $result.='<div class="LC_grade_message_center_header">'. 2361: &mt('Send Message').'</div><div class="LC_grade_message_center_body">'; 2362: my ($lastname,$givenn) = split(/,/,$env{'form.fullname'}); 2363: my $msgfor = $givenn.' '.$lastname; 2364: if (scalar(@$col_fullnames) > 0) { 2365: my $lastone = pop(@$col_fullnames); 2366: $msgfor .= ', '.(join ', ',@$col_fullnames).' and '.$lastone.'.'; 2367: } 2368: $msgfor =~ s/\'/\\'/g; #' stupid emacs - no! javascript 2369: $result.='<input type="hidden" name="includemsg'.$counter.'" value="" />'."\n". 2370: '<input type="hidden" name="newmsg'.$counter.'" value="" />'."\n"; 2371: $result.=' <a href="javascript:msgCenter(document.SCORE,'.$counter. 2372: ',\''.$msgfor.'\');" target="_self">'. 2373: &mt('Compose message to student').(scalar(@$col_fullnames) >= 1 ? 's' : '').'</a><label> ('. 2374: &mt('incl. grades').' <input type="checkbox" name="withgrades'.$counter.'" /></label>)'. 2375: '<img src="'.$request->dir_config('lonIconsURL'). 2376: '/mailbkgrd.gif" width="14" height="10" name="mailicon'.$counter.'" />'."\n". 2377: '<br /> ('. 2378: &mt('Message will be sent when you click on Save & Next below.').")\n"; 2379: $result.='</div></div>'; 2380: $request->print($result); 2381: } 2382: 2383: my %seen = (); 2384: my @partlist; 2385: my @gradePartRespid; 2386: my @part_response_id = &flatten_responseType($responseType); 2387: $request->print( 2388: '<div class="LC_Box">' 2389: .'<h3 class="LC_hcell">'.&mt('Assign Grades').'</h3>' 2390: ); 2391: $request->print(&gradeBox_start()); 2392: foreach my $part_response_id (@part_response_id) { 2393: my ($partid,$respid) = @{ $part_response_id }; 2394: my $part_resp = join('_',@{ $part_response_id }); 2395: next if ($seen{$partid} > 0); 2396: $seen{$partid}++; 2397: next if ($$handgrade{$part_resp} ne 'yes' 2398: && $env{'form.lastSub'} eq 'hdgrade'); 2399: push(@partlist,$partid); 2400: push(@gradePartRespid,$partid.'.'.$respid); 2401: $request->print(&gradeBox($request,$symb,$uname,$udom,$counter,$partid,\%record)); 2402: } 2403: $request->print(&gradeBox_end()); # </div> 2404: $request->print('</div>'); 2405: 2406: $request->print('<div class="LC_grade_info_links">'); 2407: $request->print('</div>'); 2408: 2409: $result='<input type="hidden" name="partlist'.$counter. 2410: '" value="'.(join ":",@partlist).'" />'."\n"; 2411: $result.='<input type="hidden" name="gradePartRespid'. 2412: '" value="'.(join ":",@gradePartRespid).'" />'."\n" if ($counter == 0); 2413: my $ctr = 0; 2414: while ($ctr < scalar(@partlist)) { 2415: $result.='<input type="hidden" name="partid'.$counter.'_'.$ctr.'" value="'. 2416: $partlist[$ctr].'" />'."\n"; 2417: $ctr++; 2418: } 2419: $request->print($result.''."\n"); 2420: 2421: # Done with printing info for one student 2422: 2423: $request->print('</div>');#LC_grade_show_user 2424: 2425: 2426: # print end of form 2427: if ($counter == $total) { 2428: my $endform='<br /><hr /><table border="0"><tr><td>'."\n"; 2429: $endform.='<input type="button" value="'.&mt('Save & Next').'" '. 2430: 'onclick="javascript:checksubmit(this.form,\'Save & Next\','. 2431: $total.','.scalar(@partlist).');" target="_self" /> '."\n"; 2432: my $ntstu ='<select name="NTSTU">'. 2433: '<option>1</option><option>2</option>'. 2434: '<option>3</option><option>5</option>'. 2435: '<option>7</option><option>10</option></select>'."\n"; 2436: my $nsel = ($env{'form.NTSTU'} ne '' ? $env{'form.NTSTU'} : '1'); 2437: $ntstu =~ s/<option>$nsel</<option selected="selected">$nsel</; 2438: $endform.=&mt('[_1]student(s)',$ntstu); 2439: $endform.=' <input type="button" value="'.&mt('Previous').'" '. 2440: 'onclick="javascript:checksubmit(this.form,\'Previous\');" target="_self" /> '."\n". 2441: '<input type="button" value="'.&mt('Next').'" '. 2442: 'onclick="javascript:checksubmit(this.form,\'Next\');" target="_self" /> '; 2443: $endform.='<span class="LC_warning">'. 2444: &mt('(Next and Previous (student) do not save the scores.)'). 2445: '</span>'."\n" ; 2446: $endform.="<input type='hidden' value='".&get_increment(). 2447: "' name='increment' />"; 2448: $endform.='</td></tr></table></form>'; 2449: $endform.=&show_grading_menu_form($symb); 2450: $request->print($endform); 2451: } 2452: return ''; 2453: } 2454: 2455: sub check_collaborators { 2456: my ($symb,$uname,$udom,$record,$handgrade,$counter) = @_; 2457: my ($result,@col_fullnames); 2458: my ($classlist,undef,$fullname) = &getclasslist('all','0'); 2459: foreach my $part (keys(%$handgrade)) { 2460: my $ncol = &Apache::lonnet::EXT('resource.'.$part. 2461: '.maxcollaborators', 2462: $symb,$udom,$uname); 2463: next if ($ncol <= 0); 2464: $part =~ s/\_/\./g; 2465: next if ($record->{'resource.'.$part.'.collaborators'} eq ''); 2466: my (@good_collaborators, @bad_collaborators); 2467: foreach my $possible_collaborator 2468: (split(/[,;\s]+/,$record->{'resource.'.$part.'.collaborators'})) { 2469: $possible_collaborator =~ s/[\$\^\(\)]//g; 2470: next if ($possible_collaborator eq ''); 2471: my ($co_name,$co_dom) = split(/\@|:/,$possible_collaborator); 2472: $co_dom = $udom if (! defined($co_dom) || $co_dom =~ /^domain$/i); 2473: next if ($co_name eq $uname && $co_dom eq $udom); 2474: # Doing this grep allows 'fuzzy' specification 2475: my @matches = grep(/^\Q$co_name\E:\Q$co_dom\E$/i, 2476: keys(%$classlist)); 2477: if (! scalar(@matches)) { 2478: push(@bad_collaborators, $possible_collaborator); 2479: } else { 2480: push(@good_collaborators, @matches); 2481: } 2482: } 2483: if (scalar(@good_collaborators) != 0) { 2484: $result.='<br />'.&mt('Collaborators: ').'<ol>'; 2485: foreach my $name (@good_collaborators) { 2486: my ($lastname,$givenn) = split(/,/,$$fullname{$name}); 2487: push(@col_fullnames, $givenn.' '.$lastname); 2488: $result.='<li>'.$fullname->{$name}.'</li>'; 2489: } 2490: $result.='</ol><br />'."\n"; 2491: my ($part)=split(/\./,$part); 2492: $result.='<input type="hidden" name="collaborator'.$counter. 2493: '" value="'.$part.':'.(join ':',@good_collaborators).'" />'. 2494: "\n"; 2495: } 2496: if (scalar(@bad_collaborators) > 0) { 2497: $result.='<div class="LC_warning">'; 2498: $result.=&mt('This student has submitted [quant,_1,invalid collaborator]: [_2]',scalar(@bad_collaborators),join(', ',@bad_collaborators)); 2499: $result .= '</div>'; 2500: } 2501: if (scalar(@bad_collaborators > $ncol)) { 2502: $result .= '<div class="LC_warning">'; 2503: $result .= &mt('This student has submitted too many '. 2504: 'collaborators. Maximum is [_1].',$ncol); 2505: $result .= '</div>'; 2506: } 2507: } 2508: return ($result,$fullname,\@col_fullnames); 2509: } 2510: 2511: #--- Retrieve the last submission for all the parts 2512: sub get_last_submission { 2513: my ($returnhash)=@_; 2514: my (@string,$timestamp,%lasthidden); 2515: if ($$returnhash{'version'}) { 2516: my %lasthash=(); 2517: my ($version); 2518: for ($version=1;$version<=$$returnhash{'version'};$version++) { 2519: foreach my $key (sort(split(/\:/, 2520: $$returnhash{$version.':keys'}))) { 2521: $lasthash{$key}=$$returnhash{$version.':'.$key}; 2522: $timestamp = 2523: &Apache::lonlocal::locallocaltime($$returnhash{$version.':timestamp'}); 2524: } 2525: } 2526: my (%typeparts,%randombytry); 2527: my $showsurv = 2528: &Apache::lonnet::allowed('vas',$env{'request.course.id'}); 2529: foreach my $key (sort(keys(%lasthash))) { 2530: if ($key =~ /\.type$/) { 2531: if (($lasthash{$key} eq 'anonsurvey') || 2532: ($lasthash{$key} eq 'anonsurveycred') || 2533: ($lasthash{$key} eq 'randomizetry')) { 2534: my ($ign,@parts) = split(/\./,$key); 2535: pop(@parts); 2536: my $id = join('.',@parts); 2537: if ($lasthash{$key} eq 'randomizetry') { 2538: $randombytry{$ign.'.'.$id} = $lasthash{$key}; 2539: } else { 2540: unless ($showsurv) { 2541: $typeparts{$ign.'.'.$id} = $lasthash{$key}; 2542: } 2543: } 2544: delete($lasthash{$key}); 2545: } 2546: } 2547: } 2548: my @hidden = keys(%typeparts); 2549: my @randomize = keys(%randombytry); 2550: foreach my $key (keys(%lasthash)) { 2551: next if ($key !~ /\.submission$/); 2552: my $hide; 2553: if (@hidden) { 2554: foreach my $id (@hidden) { 2555: if ($key =~ /^\Q$id\E/) { 2556: $hide = 'anon'; 2557: last; 2558: } 2559: } 2560: } 2561: unless ($hide) { 2562: if (@randomize) { 2563: foreach my $id (@hidden) { 2564: if ($key =~ /^\Q$id\E/) { 2565: $hide = 'rand'; 2566: last; 2567: } 2568: } 2569: } 2570: } 2571: my ($partid,$foo) = split(/submission$/,$key); 2572: my $draft = $lasthash{$partid.'awarddetail'} eq 'DRAFT' ? 2573: '<span class="LC_warning">Draft Copy</span> ' : ''; 2574: push(@string, join(':', $key, $hide, $draft.$lasthash{$key})); 2575: } 2576: } 2577: if (!@string) { 2578: $string[0] = 2579: '<span class="LC_warning">'.&mt('Nothing submitted - no attempts.').'</span>'; 2580: } 2581: return (\@string,\$timestamp); 2582: } 2583: 2584: #--- High light keywords, with style choosen by user. 2585: sub keywords_highlight { 2586: my $string = shift; 2587: my $size = $env{'form.kwsize'} eq '0' ? '' : 'size='.$env{'form.kwsize'}; 2588: my $styleon = $env{'form.kwstyle'} eq '' ? '' : $env{'form.kwstyle'}; 2589: (my $styleoff = $styleon) =~ s/\</\<\//; 2590: my @keylist = split(/[,\s+]/,$env{'form.keywords'}); 2591: foreach my $keyword (@keylist) { 2592: $string =~ s/\b\Q$keyword\E(\b|\.)/<font color\=$env{'form.kwclr'} $size\>$styleon$keyword$styleoff<\/font>/gi; 2593: } 2594: return $string; 2595: } 2596: 2597: #--- Called from submission routine 2598: sub processHandGrade { 2599: my ($request) = shift; 2600: my $symb = &get_symb($request); 2601: my (undef,undef,$url) = &Apache::lonnet::decode_symb($symb); 2602: my $button = $env{'form.gradeOpt'}; 2603: my $ngrade = $env{'form.NCT'}; 2604: my $ntstu = $env{'form.NTSTU'}; 2605: my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; 2606: my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'}; 2607: 2608: if ($button eq 'Save & Next') { 2609: my $ctr = 0; 2610: while ($ctr < $ngrade) { 2611: my ($uname,$udom) = split(/:/,$env{'form.unamedom'.$ctr}); 2612: my ($errorflag,$pts,$wgt) = &saveHandGrade($request,$symb,$uname,$udom,$ctr); 2613: if ($errorflag eq 'no_score') { 2614: $ctr++; 2615: next; 2616: } 2617: if ($errorflag eq 'not_allowed') { 2618: $request->print("<span class=\"LC_warning\">Not allowed to modify grades for $uname:$udom</span>"); 2619: $ctr++; 2620: next; 2621: } 2622: my $includemsg = $env{'form.includemsg'.$ctr}; 2623: my ($subject,$message,$msgstatus) = ('','',''); 2624: my $restitle = &Apache::lonnet::gettitle($symb); 2625: my ($feedurl,$showsymb) = 2626: &get_feedurl_and_symb($symb,$uname,$udom); 2627: my $messagetail; 2628: if ($includemsg =~ /savemsg|newmsg\Q$ctr\E/) { 2629: $subject = $env{'form.msgsub'} if ($includemsg =~ /msgsub/); 2630: unless ($subject=~/\w/) { $subject=&mt('Grading Feedback'); } 2631: $subject.=' ['.$restitle.']'; 2632: my (@msgnum) = split(/,/,$includemsg); 2633: foreach (@msgnum) { 2634: $message.=$env{'form.'.$_} if ($_ =~ /savemsg|newmsg/ && $_ ne ''); 2635: } 2636: $message =&Apache::lonfeedback::clear_out_html($message); 2637: if ($env{'form.withgrades'.$ctr}) { 2638: $message.="\n\nPoint".($pts > 1 ? 's':'').' awarded = '.$pts.' out of '.$wgt; 2639: $messagetail = " for <a href=\"". 2640: $feedurl."?symb=$showsymb\">$env{'form.probTitle'}</a>"; 2641: } 2642: $msgstatus = 2643: &Apache::lonmsg::user_normal_msg($uname,$udom,$subject, 2644: $message.$messagetail, 2645: undef,$feedurl,undef, 2646: undef,undef,$showsymb, 2647: $restitle); 2648: $request->print('<br />'.&mt('Sending message to [_1]',$uname.':'.$udom).': '. 2649: $msgstatus.'<br />'); 2650: } 2651: if ($env{'form.collaborator'.$ctr}) { 2652: my @collabstrs=&Apache::loncommon::get_env_multiple("form.collaborator$ctr"); 2653: foreach my $collabstr (@collabstrs) { 2654: my ($part,@collaborators) = split(/:/,$collabstr); 2655: foreach my $collaborator (@collaborators) { 2656: my ($errorflag,$pts,$wgt) = 2657: &saveHandGrade($request,$symb,$collaborator,$udom,$ctr, 2658: $env{'form.unamedom'.$ctr},$part); 2659: if ($errorflag eq 'not_allowed') { 2660: $request->print("<span class=\"LC_error\">".&mt('Not allowed to modify grades for [_1]',"$collaborator:$udom")."</span>"); 2661: next; 2662: } elsif ($message ne '') { 2663: my ($baseurl,$showsymb) = 2664: &get_feedurl_and_symb($symb,$collaborator, 2665: $udom); 2666: if ($env{'form.withgrades'.$ctr}) { 2667: $messagetail = " for <a href=\"". 2668: $baseurl."?symb=$showsymb\">$env{'form.probTitle'}</a>"; 2669: } 2670: $msgstatus = 2671: &Apache::lonmsg::user_normal_msg($collaborator,$udom,$subject,$message.$messagetail,undef,$baseurl,undef,undef,undef,$showsymb,$restitle); 2672: } 2673: } 2674: } 2675: } 2676: $ctr++; 2677: } 2678: } 2679: 2680: if ($env{'form.handgrade'} eq 'yes') { 2681: # Keywords sorted in alphabatical order 2682: my $loginuser = $env{'user.name'}.':'.$env{'user.domain'}; 2683: my %keyhash = (); 2684: $env{'form.keywords'} =~ s/,\s{0,}|\s+/ /g; 2685: $env{'form.keywords'} =~ s/^\s+|\s+$//; 2686: my (@keywords) = sort(split(/\s+/,$env{'form.keywords'})); 2687: $env{'form.keywords'} = join(' ',@keywords); 2688: $keyhash{$symb.'_keywords'} = $env{'form.keywords'}; 2689: $keyhash{$symb.'_subject'} = $env{'form.msgsub'}; 2690: $keyhash{$loginuser.'_kwclr'} = $env{'form.kwclr'}; 2691: $keyhash{$loginuser.'_kwsize'} = $env{'form.kwsize'}; 2692: $keyhash{$loginuser.'_kwstyle'} = $env{'form.kwstyle'}; 2693: 2694: # message center - Order of message gets changed. Blank line is eliminated. 2695: # New messages are saved in env for the next student. 2696: # All messages are saved in nohist_handgrade.db 2697: my ($ctr,$idx) = (1,1); 2698: while ($ctr <= $env{'form.savemsgN'}) { 2699: if ($env{'form.savemsg'.$ctr} ne '') { 2700: $keyhash{$symb.'_savemsg'.$idx} = $env{'form.savemsg'.$ctr}; 2701: $idx++; 2702: } 2703: $ctr++; 2704: } 2705: $ctr = 0; 2706: while ($ctr < $ngrade) { 2707: if ($env{'form.newmsg'.$ctr} ne '') { 2708: $keyhash{$symb.'_savemsg'.$idx} = $env{'form.newmsg'.$ctr}; 2709: $env{'form.savemsg'.$idx} = $env{'form.newmsg'.$ctr}; 2710: $idx++; 2711: } 2712: $ctr++; 2713: } 2714: $env{'form.savemsgN'} = --$idx; 2715: $keyhash{$symb.'_savemsgN'} = $env{'form.savemsgN'}; 2716: my $putresult = &Apache::lonnet::put 2717: ('nohist_handgrade',\%keyhash,$cdom,$cnum); 2718: } 2719: # Called by Save & Refresh from Highlight Attribute Window 2720: my (undef,undef,$fullname) = &getclasslist($env{'form.section'},'1'); 2721: if ($env{'form.refresh'} eq 'on') { 2722: my ($ctr,$total) = (0,0); 2723: while ($ctr < $ngrade) { 2724: $total++ if $env{'form.unamedom'.$ctr} ne ''; 2725: $ctr++; 2726: } 2727: $env{'form.NTSTU'}=$ngrade; 2728: $ctr = 0; 2729: while ($ctr < $total) { 2730: my $processUser = $env{'form.unamedom'.$ctr}; 2731: ($env{'form.student'},$env{'form.userdom'}) = split(/:/,$processUser); 2732: $env{'form.fullname'} = $$fullname{$processUser}; 2733: &submission($request,$ctr,$total-1); 2734: $ctr++; 2735: } 2736: return ''; 2737: } 2738: 2739: # Go directly to grade student - from submission or link from chart page 2740: if ($button eq 'Grade Student') { 2741: (undef,undef,$env{'form.handgrade'},undef,undef) = &showResourceInfo($symb); 2742: my $processUser = $env{'form.unamedom'.$env{'form.studentNo'}}; 2743: ($env{'form.student'},$env{'form.userdom'}) = split(/:/,$processUser); 2744: $env{'form.fullname'} = $$fullname{$processUser}; 2745: &submission($request,0,0); 2746: return ''; 2747: } 2748: 2749: # Get the next/previous one or group of students 2750: my $firststu = $env{'form.unamedom0'}; 2751: my $laststu = $env{'form.unamedom'.($ngrade-1)}; 2752: my $ctr = 2; 2753: while ($laststu eq '') { 2754: $laststu = $env{'form.unamedom'.($ngrade-$ctr)}; 2755: $ctr++; 2756: $laststu = $firststu if ($ctr > $ngrade); 2757: } 2758: 2759: my (@parsedlist,@nextlist); 2760: my ($nextflg) = 0; 2761: foreach my $item (sort 2762: { 2763: if (lc($$fullname{$a}) ne lc($$fullname{$b})) { 2764: return (lc($$fullname{$a}) cmp lc($$fullname{$b})); 2765: } 2766: return $a cmp $b; 2767: } (keys(%$fullname))) { 2768: if ($nextflg == 1 && $button =~ /Next$/) { 2769: push(@parsedlist,$item); 2770: } 2771: $nextflg = 1 if ($item eq $laststu); 2772: if ($button eq 'Previous') { 2773: last if ($item eq $firststu); 2774: push(@parsedlist,$item); 2775: } 2776: } 2777: $ctr = 0; 2778: @parsedlist = reverse @parsedlist if ($button eq 'Previous'); 2779: my $res_error; 2780: my ($partlist) = &response_type($symb,\$res_error); 2781: if ($res_error) { 2782: $request->print(&navmap_errormsg()); 2783: return; 2784: } 2785: foreach my $student (@parsedlist) { 2786: my $submitonly=$env{'form.submitonly'}; 2787: my ($uname,$udom) = split(/:/,$student); 2788: 2789: if ($submitonly eq 'queued') { 2790: my %queue_status = 2791: &Apache::bridgetask::get_student_status($symb,$cdom,$cnum, 2792: $udom,$uname); 2793: next if (!defined($queue_status{'gradingqueue'})); 2794: } 2795: 2796: if ($submitonly =~ /^(yes|graded|incorrect)$/) { 2797: # my %record = &Apache::lonnet::restore($symb,$env{'request.course.id'},$udom,$uname); 2798: my %status=&student_gradeStatus($symb,$udom,$uname,$partlist); 2799: my $submitted = 0; 2800: my $ungraded = 0; 2801: my $incorrect = 0; 2802: foreach my $item (keys(%status)) { 2803: $submitted = 1 if ($status{$item} ne 'nothing'); 2804: $ungraded = 1 if ($status{$item} =~ /^ungraded/); 2805: $incorrect = 1 if ($status{$item} =~ /^incorrect/); 2806: my ($foo,$partid,$foo1) = split(/\./,$item); 2807: if ($status{'resource.'.$partid.'.submitted_by'} ne '') { 2808: $submitted = 0; 2809: } 2810: } 2811: next if (!$submitted && ($submitonly eq 'yes' || 2812: $submitonly eq 'incorrect' || 2813: $submitonly eq 'graded')); 2814: next if (!$ungraded && ($submitonly eq 'graded')); 2815: next if (!$incorrect && $submitonly eq 'incorrect'); 2816: } 2817: push(@nextlist,$student) if ($ctr < $ntstu); 2818: last if ($ctr == $ntstu); 2819: $ctr++; 2820: } 2821: 2822: $ctr = 0; 2823: my $total = scalar(@nextlist)-1; 2824: 2825: foreach (sort(@nextlist)) { 2826: my ($uname,$udom,$submitter) = split(/:/); 2827: $env{'form.student'} = $uname; 2828: $env{'form.userdom'} = $udom; 2829: $env{'form.fullname'} = $$fullname{$_}; 2830: &submission($request,$ctr,$total); 2831: $ctr++; 2832: } 2833: if ($total < 0) { 2834: my $the_end = '<h3><span class="LC_info">'.&mt('LON-CAPA User Message').'</span></h3><br />'."\n"; 2835: $the_end.='<p>'.&mt('[_1]Message:[_2] No more students for this section or class.','<b>','</b>').'</p>'."\n"; 2836: $the_end.=&mt('Click on the button below to return to the grading menu.').'<br /><br />'."\n"; 2837: $the_end.=&show_grading_menu_form($symb); 2838: $request->print($the_end); 2839: } 2840: return ''; 2841: } 2842: 2843: #---- Save the score and award for each student, if changed 2844: sub saveHandGrade { 2845: my ($request,$symb,$stuname,$domain,$newflg,$submitter,$part) = @_; 2846: my @version_parts; 2847: my $usec = &Apache::lonnet::getsection($domain,$stuname, 2848: $env{'request.course.id'}); 2849: if (!&canmodify($usec)) { return('not_allowed'); } 2850: my %record = &Apache::lonnet::restore($symb,$env{'request.course.id'},$domain,$stuname); 2851: my @parts_graded; 2852: my %newrecord = (); 2853: my ($pts,$wgt) = ('',''); 2854: my %aggregate = (); 2855: my $aggregateflag = 0; 2856: my @parts = split(/:/,$env{'form.partlist'.$newflg}); 2857: foreach my $new_part (@parts) { 2858: #collaborator ($submi may vary for different parts 2859: if ($submitter && $new_part ne $part) { next; } 2860: my $dropMenu = $env{'form.GD_SEL'.$newflg.'_'.$new_part}; 2861: if ($dropMenu eq 'excused') { 2862: if ($record{'resource.'.$new_part.'.solved'} ne 'excused') { 2863: $newrecord{'resource.'.$new_part.'.solved'} = 'excused'; 2864: if (exists($record{'resource.'.$new_part.'.awarded'})) { 2865: $newrecord{'resource.'.$new_part.'.awarded'} = ''; 2866: } 2867: $newrecord{'resource.'.$new_part.'.regrader'}="$env{'user.name'}:$env{'user.domain'}"; 2868: } 2869: } elsif ($dropMenu eq 'reset status' 2870: && exists($record{'resource.'.$new_part.'.solved'})) { #don't bother if no old records -> no attempts 2871: foreach my $key (keys(%record)) { 2872: if ($key=~/^resource\.\Q$new_part\E\./) { $newrecord{$key} = ''; } 2873: } 2874: $newrecord{'resource.'.$new_part.'.regrader'}= 2875: "$env{'user.name'}:$env{'user.domain'}"; 2876: my $totaltries = $record{'resource.'.$part.'.tries'}; 2877: 2878: my %last_resets = &get_last_resets($symb,$env{'request.course.id'}, 2879: [$new_part]); 2880: my $aggtries =$totaltries; 2881: if ($last_resets{$new_part}) { 2882: $aggtries = &get_num_tries(\%record,$last_resets{$new_part}, 2883: $new_part); 2884: } 2885: 2886: my $solvedstatus = $record{'resource.'.$new_part.'.solved'}; 2887: if ($aggtries > 0) { 2888: &decrement_aggs($symb,$new_part,\%aggregate,$aggtries,$totaltries,$solvedstatus); 2889: $aggregateflag = 1; 2890: } 2891: } elsif ($dropMenu eq '') { 2892: $pts = ($env{'form.GD_BOX'.$newflg.'_'.$new_part} ne '' ? 2893: $env{'form.GD_BOX'.$newflg.'_'.$new_part} : 2894: $env{'form.RADVAL'.$newflg.'_'.$new_part}); 2895: if ($pts eq '' && $env{'form.GD_SEL'.$newflg.'_'.$new_part} eq '') { 2896: next; 2897: } 2898: $wgt = $env{'form.WGT'.$newflg.'_'.$new_part} eq '' ? 1 : 2899: $env{'form.WGT'.$newflg.'_'.$new_part}; 2900: my $partial= $pts/$wgt; 2901: if ($partial eq $record{'resource.'.$new_part.'.awarded'}) { 2902: #do not update score for part if not changed. 2903: &handback_files($request,$symb,$stuname,$domain,$newflg,$new_part,\%newrecord); 2904: next; 2905: } else { 2906: push(@parts_graded,$new_part); 2907: } 2908: if ($record{'resource.'.$new_part.'.awarded'} ne $partial) { 2909: $newrecord{'resource.'.$new_part.'.awarded'} = $partial; 2910: } 2911: my $reckey = 'resource.'.$new_part.'.solved'; 2912: if ($partial == 0) { 2913: if ($record{$reckey} ne 'incorrect_by_override') { 2914: $newrecord{$reckey} = 'incorrect_by_override'; 2915: } 2916: } else { 2917: if ($record{$reckey} ne 'correct_by_override') { 2918: $newrecord{$reckey} = 'correct_by_override'; 2919: } 2920: } 2921: if ($submitter && 2922: ($record{'resource.'.$new_part.'.submitted_by'} ne $submitter)) { 2923: $newrecord{'resource.'.$new_part.'.submitted_by'} = $submitter; 2924: } 2925: $newrecord{'resource.'.$new_part.'.regrader'}= 2926: "$env{'user.name'}:$env{'user.domain'}"; 2927: } 2928: # unless problem has been graded, set flag to version the submitted files 2929: unless ($record{'resource.'.$new_part.'.solved'} =~ /^correct_/ || 2930: $record{'resource.'.$new_part.'.solved'} eq 'incorrect_by_override' || 2931: $dropMenu eq 'reset status') 2932: { 2933: push(@version_parts,$new_part); 2934: } 2935: } 2936: my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; 2937: my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'}; 2938: 2939: if (%newrecord) { 2940: if (@version_parts) { 2941: my @changed_keys = &version_portfiles(\%record, \@parts_graded, 2942: $env{'request.course.id'}, $symb, $domain, $stuname, \@version_parts); 2943: @newrecord{@changed_keys} = @record{@changed_keys}; 2944: foreach my $new_part (@version_parts) { 2945: &handback_files($request,$symb,$stuname,$domain,$newflg, 2946: $new_part,\%newrecord); 2947: } 2948: } 2949: &Apache::lonnet::cstore(\%newrecord,$symb, 2950: $env{'request.course.id'},$domain,$stuname); 2951: &check_and_remove_from_queue(\@parts,\%record,\%newrecord,$symb, 2952: $cdom,$cnum,$domain,$stuname); 2953: } 2954: if ($aggregateflag) { 2955: &Apache::lonnet::cinc('nohist_resourcetracker',\%aggregate, 2956: $cdom,$cnum); 2957: } 2958: return ('',$pts,$wgt); 2959: } 2960: 2961: sub check_and_remove_from_queue { 2962: my ($parts,$record,$newrecord,$symb,$cdom,$cnum,$domain,$stuname) = @_; 2963: my @ungraded_parts; 2964: foreach my $part (@{$parts}) { 2965: if ( $record->{ 'resource.'.$part.'.awarded'} eq '' 2966: && $record->{ 'resource.'.$part.'.solved' } ne 'excused' 2967: && $newrecord->{'resource.'.$part.'.awarded'} eq '' 2968: && $newrecord->{'resource.'.$part.'.solved' } ne 'excused' 2969: ) { 2970: push(@ungraded_parts, $part); 2971: } 2972: } 2973: if ( !@ungraded_parts ) { 2974: &Apache::bridgetask::remove_from_queue('gradingqueue',$symb,$cdom, 2975: $cnum,$domain,$stuname); 2976: } 2977: } 2978: 2979: sub handback_files { 2980: my ($request,$symb,$stuname,$domain,$newflg,$new_part,$newrecord) = @_; 2981: my $portfolio_root = '/userfiles/portfolio'; 2982: my $res_error; 2983: my ($partlist,$handgrade,$responseType) = &response_type($symb,\$res_error); 2984: if ($res_error) { 2985: $request->print('<br />'.&navmap_errormsg().'<br />'); 2986: return; 2987: } 2988: my @handedback; 2989: my $file_msg; 2990: my @part_response_id = &flatten_responseType($responseType); 2991: foreach my $part_response_id (@part_response_id) { 2992: my ($part_id,$resp_id) = @{ $part_response_id }; 2993: my $part_resp = join('_',@{ $part_response_id }); 2994: if (($env{'form.'.$newflg.'_'.$part_resp.'_countreturndoc'} =~ /^\d+$/) & ($new_part eq $part_id)) { 2995: for (my $counter=1; $counter<=$env{'form.'.$newflg.'_'.$part_resp.'_countreturndoc'}; $counter++) { 2996: # if multiple files are uploaded names will be 'returndoc2','returndoc3' 2997: if ($env{'form.'.$newflg.'_'.$part_resp.'_returndoc'.$counter}) { 2998: my $fname=$env{'form.'.$newflg.'_'.$part_resp.'_returndoc'.$counter.'.filename'}; 2999: my ($directory,$answer_file) = 3000: ($env{'form.'.$newflg.'_'.$part_resp.'_origdoc'.$counter} =~ /^(.*?)([^\/]*)$/); 3001: my ($answer_name,$answer_ver,$answer_ext) = 3002: &file_name_version_ext($answer_file); 3003: my ($portfolio_path) = ($directory =~ /^.+$stuname\/portfolio(.*)/); 3004: my $getpropath = 1; 3005: my @dir_list = &Apache::lonnet::dirlist($portfolio_root.$portfolio_path,$domain,$stuname,$getpropath); 3006: my $version = &get_next_version($answer_name, $answer_ext, \@dir_list); 3007: # fix file name 3008: my ($save_file_name) = (($directory.$answer_name.".$version.".$answer_ext) =~ /^.+\/${stuname}\/(.*)/); 3009: my $result=&Apache::lonnet::finishuserfileupload($stuname,$domain, 3010: $newflg.'_'.$part_resp.'_returndoc'.$counter, 3011: $save_file_name); 3012: if ($result !~ m|^/uploaded/|) { 3013: $request->print('<br /><span class="LC_error">'. 3014: &mt('An error occurred ([_1]) while trying to upload [_2].', 3015: $result,$newflg.'_'.$part_resp.'_returndoc'.$counter). 3016: '</span>'); 3017: } else { 3018: # mark the file as read only 3019: push(@handedback,$save_file_name); 3020: if (exists($$newrecord{"resource.$new_part.$resp_id.handback"})) { 3021: $$newrecord{"resource.$new_part.$resp_id.handback"}.=','; 3022: } 3023: $$newrecord{"resource.$new_part.$resp_id.handback"} .= $save_file_name; 3024: $file_msg.='<span class="LC_filename"><a href="/uploaded/'."$domain/$stuname/".$save_file_name.'">'.$save_file_name."</a></span> <br />"; 3025: 3026: } 3027: $request->print('<br />'.&mt('[_1] will be the uploaded file name [_2]','<span class="LC_info">'.$fname.'</span>','<span class="LC_filename">'.$env{'form.'.$newflg.'_'.$part_resp.'_origdoc'.$counter}.'</span>')); 3028: } 3029: } 3030: } 3031: } 3032: if (@handedback > 0) { 3033: $request->print('<br />'); 3034: my @what = ($symb,$env{'request.course.id'},'handback'); 3035: &Apache::lonnet::mark_as_readonly($domain,$stuname,\@handedback,\@what); 3036: my $user_lh = &Apache::loncommon::user_lang($stuname,$domain,$env{'request.course.id'}); 3037: my ($subject,$message); 3038: if (scalar(@handedback) == 1) { 3039: $subject = &mt_user($user_lh,'File Handed Back by Instructor'); 3040: } else { 3041: $subject = &mt_user($user_lh,'Files Handed Back by Instructor'); 3042: $message = &mt_user($user_lh,'Files have been returned that were originally submitted in response to: '); 3043: } 3044: $message .= "<p><strong>".&Apache::lonnet::gettitle($symb)." </strong></p>"; 3045: $message .= &mt_user($user_lh,'The returned file(s) are named: [_1]',"<br />$file_msg <br />"). 3046: &mt_user($user_lh,'The file(s) can be found in your [_1]portfolio[_2].','<a href="/adm/portfolio">','</a>'); 3047: my ($feedurl,$showsymb) = 3048: &get_feedurl_and_symb($symb,$domain,$stuname); 3049: my $restitle = &Apache::lonnet::gettitle($symb); 3050: $subject .= ' '.&mt_user($user_lh,'(File Returned)').' ['.$restitle.']'; 3051: my $msgstatus = 3052: &Apache::lonmsg::user_normal_msg($stuname,$domain,$subject, 3053: $message,undef,$feedurl,undef,undef,undef,$showsymb, 3054: $restitle); 3055: if ($msgstatus) { 3056: $request->print(&mt('Notification message status: [_1]','<span class="LC_info">'.$msgstatus.'</span>').'<br />'); 3057: } 3058: } 3059: return; 3060: } 3061: 3062: sub get_feedurl_and_symb { 3063: my ($symb,$uname,$udom) = @_; 3064: my (undef,undef,$url) = &Apache::lonnet::decode_symb($symb); 3065: $url = &Apache::lonnet::clutter($url); 3066: my $encrypturl=&Apache::lonnet::EXT('resource.0.encrypturl', 3067: $symb,$udom,$uname); 3068: if ($encrypturl =~ /^yes$/i) { 3069: &Apache::lonenc::encrypted(\$url,1); 3070: &Apache::lonenc::encrypted(\$symb,1); 3071: } 3072: return ($url,$symb); 3073: } 3074: 3075: sub get_submitted_files { 3076: my ($udom,$uname,$partid,$respid,$record) = @_; 3077: my @files; 3078: if ($$record{"resource.$partid.$respid.portfiles"}) { 3079: my $file_url = '/uploaded/'.$udom.'/'.$uname.'/portfolio'; 3080: foreach my $file (split(',',$$record{"resource.$partid.$respid.portfiles"})) { 3081: push(@files,$file_url.$file); 3082: } 3083: } 3084: if ($$record{"resource.$partid.$respid.uploadedurl"}) { 3085: push(@files,$$record{"resource.$partid.$respid.uploadedurl"}); 3086: } 3087: return (\@files); 3088: } 3089: 3090: # ----------- Provides number of tries since last reset. 3091: sub get_num_tries { 3092: my ($record,$last_reset,$part) = @_; 3093: my $timestamp = ''; 3094: my $num_tries = 0; 3095: if ($$record{'version'}) { 3096: for (my $version=$$record{'version'};$version>=1;$version--) { 3097: if (exists($$record{$version.':resource.'.$part.'.solved'})) { 3098: $timestamp = $$record{$version.':timestamp'}; 3099: if ($timestamp > $last_reset) { 3100: $num_tries ++; 3101: } else { 3102: last; 3103: } 3104: } 3105: } 3106: } 3107: return $num_tries; 3108: } 3109: 3110: # ----------- Determine decrements required in aggregate totals 3111: sub decrement_aggs { 3112: my ($symb,$part,$aggregate,$aggtries,$totaltries,$solvedstatus) = @_; 3113: my %decrement = ( 3114: attempts => 0, 3115: users => 0, 3116: correct => 0 3117: ); 3118: $decrement{'attempts'} = $aggtries; 3119: if ($solvedstatus =~ /^correct/) { 3120: $decrement{'correct'} = 1; 3121: } 3122: if ($aggtries == $totaltries) { 3123: $decrement{'users'} = 1; 3124: } 3125: foreach my $type (keys(%decrement)) { 3126: $$aggregate{$symb."\0".$part."\0".$type} = -$decrement{$type}; 3127: } 3128: return; 3129: } 3130: 3131: # ----------- Determine timestamps for last reset of aggregate totals for parts 3132: sub get_last_resets { 3133: my ($symb,$courseid,$partids) =@_; 3134: my %last_resets; 3135: my $cdom = $env{'course.'.$courseid.'.domain'}; 3136: my $cname = $env{'course.'.$courseid.'.num'}; 3137: my @keys; 3138: foreach my $part (@{$partids}) { 3139: push(@keys,"$symb\0$part\0resettime"); 3140: } 3141: my %results=&Apache::lonnet::get('nohist_resourcetracker',\@keys, 3142: $cdom,$cname); 3143: foreach my $part (@{$partids}) { 3144: $last_resets{$part}=$results{"$symb\0$part\0resettime"}; 3145: } 3146: return %last_resets; 3147: } 3148: 3149: # ----------- Handles creating versions for portfolio files as answers 3150: sub version_portfiles { 3151: my ($record, $parts_graded, $courseid, $symb, $domain, $stu_name, $v_flag) = @_; 3152: my $version_parts = join('|',@$v_flag); 3153: my @returned_keys; 3154: my $parts = join('|', @$parts_graded); 3155: my $portfolio_root = '/userfiles/portfolio'; 3156: foreach my $key (keys(%$record)) { 3157: my $new_portfiles; 3158: if ($key =~ /^resource\.($version_parts)\./ && $key =~ /\.portfiles$/ ) { 3159: my @versioned_portfiles; 3160: my @portfiles = split(/\s*,\s*/,$$record{$key}); 3161: foreach my $file (@portfiles) { 3162: &Apache::lonnet::unmark_as_readonly($domain,$stu_name,[$symb,$env{'request.course.id'}],$file); 3163: my ($directory,$answer_file) =($file =~ /^(.*?)([^\/]*)$/); 3164: my ($answer_name,$answer_ver,$answer_ext) = 3165: &file_name_version_ext($answer_file); 3166: my $getpropath = 1; 3167: my @dir_list = &Apache::lonnet::dirlist($portfolio_root.$directory,$domain,$stu_name,$getpropath); 3168: my $version = &get_next_version($answer_name, $answer_ext, \@dir_list); 3169: my $new_answer = &version_selected_portfile($domain, $stu_name, $directory, $answer_file, $version); 3170: if ($new_answer ne 'problem getting file') { 3171: push(@versioned_portfiles, $directory.$new_answer); 3172: &Apache::lonnet::mark_as_readonly($domain,$stu_name, 3173: [$directory.$new_answer], 3174: [$symb,$env{'request.course.id'},'graded']); 3175: } 3176: } 3177: $$record{$key} = join(',',@versioned_portfiles); 3178: push(@returned_keys,$key); 3179: } 3180: } 3181: return (@returned_keys); 3182: } 3183: 3184: sub get_next_version { 3185: my ($answer_name, $answer_ext, $dir_list) = @_; 3186: my $version; 3187: foreach my $row (@$dir_list) { 3188: my ($file) = split(/\&/,$row,2); 3189: my ($file_name,$file_version,$file_ext) = 3190: &file_name_version_ext($file); 3191: if (($file_name eq $answer_name) && 3192: ($file_ext eq $answer_ext)) { 3193: # gets here if filename and extension match, regardless of version 3194: if ($file_version ne '') { 3195: # a versioned file is found so save it for later 3196: if ($file_version > $version) { 3197: $version = $file_version; 3198: } 3199: } 3200: } 3201: } 3202: $version ++; 3203: return($version); 3204: } 3205: 3206: sub version_selected_portfile { 3207: my ($domain,$stu_name,$directory,$file_name,$version) = @_; 3208: my ($answer_name,$answer_ver,$answer_ext) = 3209: &file_name_version_ext($file_name); 3210: my $new_answer; 3211: $env{'form.copy'} = &Apache::lonnet::getfile("/uploaded/$domain/$stu_name/portfolio$directory$file_name"); 3212: if($env{'form.copy'} eq '-1') { 3213: $new_answer = 'problem getting file'; 3214: } else { 3215: $new_answer = $answer_name.'.'.$version.'.'.$answer_ext; 3216: my $copy_result = &Apache::lonnet::finishuserfileupload( 3217: $stu_name,$domain,'copy', 3218: '/portfolio'.$directory.$new_answer); 3219: } 3220: return ($new_answer); 3221: } 3222: 3223: sub file_name_version_ext { 3224: my ($file)=@_; 3225: my @file_parts = split(/\./, $file); 3226: my ($name,$version,$ext); 3227: if (@file_parts > 1) { 3228: $ext=pop(@file_parts); 3229: if (@file_parts > 1 && $file_parts[-1] =~ /^\d+$/) { 3230: $version=pop(@file_parts); 3231: } 3232: $name=join('.',@file_parts); 3233: } else { 3234: $name=join('.',@file_parts); 3235: } 3236: return($name,$version,$ext); 3237: } 3238: 3239: #-------------------------------------------------------------------------------------- 3240: # 3241: #-------------------------- Next few routines handles grading by section or whole class 3242: # 3243: #--- Javascript to handle grading by section or whole class 3244: sub viewgrades_js { 3245: my ($request) = shift; 3246: 3247: my $alertmsg = &mt('A number equal or greater than 0 is expected. Entered value = '); 3248: $request->print(<<VIEWJAVASCRIPT); 3249: <script type="text/javascript" language="javascript"> 3250: function writePoint(partid,weight,point) { 3251: var radioButton = document.classgrade["RADVAL_"+partid]; 3252: var textbox = document.classgrade["TEXTVAL_"+partid]; 3253: if (point == "textval") { 3254: point = document.classgrade["TEXTVAL_"+partid].value; 3255: if (isNaN(point) || parseFloat(point) < 0) { 3256: alert("$alertmsg"+parseFloat(point)); 3257: var resetbox = false; 3258: for (var i=0; i<radioButton.length; i++) { 3259: if (radioButton[i].checked) { 3260: textbox.value = i; 3261: resetbox = true; 3262: } 3263: } 3264: if (!resetbox) { 3265: textbox.value = ""; 3266: } 3267: return; 3268: } 3269: if (parseFloat(point) > parseFloat(weight)) { 3270: var resp = confirm("You entered a value ("+parseFloat(point)+ 3271: ") greater than the weight for the part. Accept?"); 3272: if (resp == false) { 3273: textbox.value = ""; 3274: return; 3275: } 3276: } 3277: for (var i=0; i<radioButton.length; i++) { 3278: radioButton[i].checked=false; 3279: if (parseFloat(point) == i) { 3280: radioButton[i].checked=true; 3281: } 3282: } 3283: 3284: } else { 3285: textbox.value = parseFloat(point); 3286: } 3287: for (i=0;i<document.classgrade.total.value;i++) { 3288: var user = document.classgrade["ctr"+i].value; 3289: user = user.replace(new RegExp(':', 'g'),"_"); 3290: var scorename = document.classgrade["GD_"+user+"_"+partid+"_awarded"]; 3291: var saveval = document.classgrade["GD_"+user+"_"+partid+"_solved_s"].value; 3292: var selname = document.classgrade["GD_"+user+"_"+partid+"_solved"]; 3293: if (saveval != "correct") { 3294: scorename.value = point; 3295: if (selname[0].selected != true) { 3296: selname[0].selected = true; 3297: } 3298: } 3299: } 3300: document.classgrade["SELVAL_"+partid][0].selected = true; 3301: } 3302: 3303: function writeRadText(partid,weight) { 3304: var selval = document.classgrade["SELVAL_"+partid]; 3305: var radioButton = document.classgrade["RADVAL_"+partid]; 3306: var override = document.classgrade["FORCE_"+partid].checked; 3307: var textbox = document.classgrade["TEXTVAL_"+partid]; 3308: if (selval[1].selected || selval[2].selected) { 3309: for (var i=0; i<radioButton.length; i++) { 3310: radioButton[i].checked=false; 3311: 3312: } 3313: textbox.value = ""; 3314: 3315: for (i=0;i<document.classgrade.total.value;i++) { 3316: var user = document.classgrade["ctr"+i].value; 3317: user = user.replace(new RegExp(':', 'g'),"_"); 3318: var scorename = document.classgrade["GD_"+user+"_"+partid+"_awarded"]; 3319: var saveval = document.classgrade["GD_"+user+"_"+partid+"_solved_s"].value; 3320: var selname = document.classgrade["GD_"+user+"_"+partid+"_solved"]; 3321: if ((saveval != "correct") || override) { 3322: scorename.value = ""; 3323: if (selval[1].selected) { 3324: selname[1].selected = true; 3325: } else { 3326: selname[2].selected = true; 3327: if (Number(document.classgrade["GD_"+user+"_"+partid+"_tries"].value)) 3328: {document.classgrade["GD_"+user+"_"+partid+"_tries"].value = '0';} 3329: } 3330: } 3331: } 3332: } else { 3333: for (i=0;i<document.classgrade.total.value;i++) { 3334: var user = document.classgrade["ctr"+i].value; 3335: user = user.replace(new RegExp(':', 'g'),"_"); 3336: var scorename = document.classgrade["GD_"+user+"_"+partid+"_awarded"]; 3337: var saveval = document.classgrade["GD_"+user+"_"+partid+"_solved_s"].value; 3338: var selname = document.classgrade["GD_"+user+"_"+partid+"_solved"]; 3339: if ((saveval != "correct") || override) { 3340: scorename.value = document.classgrade["GD_"+user+"_"+partid+"_awarded_s"].value; 3341: selname[0].selected = true; 3342: } 3343: } 3344: } 3345: } 3346: 3347: function changeSelect(partid,user) { 3348: var selval = document.classgrade["GD_"+user+'_'+partid+"_solved"]; 3349: var textbox = document.classgrade["GD_"+user+'_'+partid+"_awarded"]; 3350: var point = textbox.value; 3351: var weight = document.classgrade["weight_"+partid].value; 3352: 3353: if (isNaN(point) || parseFloat(point) < 0) { 3354: alert("$alertmsg"+parseFloat(point)); 3355: textbox.value = ""; 3356: return; 3357: } 3358: if (parseFloat(point) > parseFloat(weight)) { 3359: var resp = confirm("You entered a value ("+parseFloat(point)+ 3360: ") greater than the weight of the part. Accept?"); 3361: if (resp == false) { 3362: textbox.value = ""; 3363: return; 3364: } 3365: } 3366: selval[0].selected = true; 3367: } 3368: 3369: function changeOneScore(partid,user) { 3370: var selval = document.classgrade["GD_"+user+'_'+partid+"_solved"]; 3371: if (selval[1].selected || selval[2].selected) { 3372: document.classgrade["GD_"+user+'_'+partid+"_awarded"].value = ""; 3373: if (selval[2].selected) { 3374: document.classgrade["GD_"+user+'_'+partid+"_tries"].value = "0"; 3375: } 3376: } 3377: } 3378: 3379: function resetEntry(numpart) { 3380: for (ctpart=0;ctpart<numpart;ctpart++) { 3381: var partid = document.classgrade["partid_"+ctpart].value; 3382: var radioButton = document.classgrade["RADVAL_"+partid]; 3383: var textbox = document.classgrade["TEXTVAL_"+partid]; 3384: var selval = document.classgrade["SELVAL_"+partid]; 3385: for (var i=0; i<radioButton.length; i++) { 3386: radioButton[i].checked=false; 3387: 3388: } 3389: textbox.value = ""; 3390: selval[0].selected = true; 3391: 3392: for (i=0;i<document.classgrade.total.value;i++) { 3393: var user = document.classgrade["ctr"+i].value; 3394: user = user.replace(new RegExp(':', 'g'),"_"); 3395: var resetscore = document.classgrade["GD_"+user+"_"+partid+"_awarded"]; 3396: resetscore.value = document.classgrade["GD_"+user+"_"+partid+"_awarded_s"].value; 3397: var resettries = document.classgrade["GD_"+user+"_"+partid+"_tries"]; 3398: resettries.value = document.classgrade["GD_"+user+"_"+partid+"_tries_s"].value; 3399: var saveselval = document.classgrade["GD_"+user+"_"+partid+"_solved_s"].value; 3400: var selname = document.classgrade["GD_"+user+"_"+partid+"_solved"]; 3401: if (saveselval == "excused") { 3402: if (selname[1].selected == false) { selname[1].selected = true;} 3403: } else { 3404: if (selname[0].selected == false) {selname[0].selected = true}; 3405: } 3406: } 3407: } 3408: } 3409: 3410: </script> 3411: VIEWJAVASCRIPT 3412: } 3413: 3414: #--- show scores for a section or whole class w/ option to change/update a score 3415: sub viewgrades { 3416: my ($request) = shift; 3417: &viewgrades_js($request); 3418: 3419: my ($symb) = &get_symb($request); 3420: #need to make sure we have the correct data for later EXT calls, 3421: #thus invalidate the cache 3422: &Apache::lonnet::devalidatecourseresdata( 3423: $env{'course.'.$env{'request.course.id'}.'.num'}, 3424: $env{'course.'.$env{'request.course.id'}.'.domain'}); 3425: &Apache::lonnet::clear_EXT_cache_status(); 3426: 3427: my $result='<h3><span class="LC_info">'.&mt('Manual Grading').'</span></h3>'; 3428: $result.='<h4>'.&mt('<b>Current Resource: </b>[_1]',$env{'form.probTitle'}).'</h4>'."\n"; 3429: 3430: #view individual student submission form - called using Javascript viewOneStudent 3431: $result.=&jscriptNform($symb); 3432: 3433: #beginning of class grading form 3434: my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status')); 3435: $result.= '<form action="/adm/grades" method="post" name="classgrade">'."\n". 3436: '<input type="hidden" name="symb" value="'.&Apache::lonenc::check_encrypt($symb).'" />'."\n". 3437: '<input type="hidden" name="command" value="editgrades" />'."\n". 3438: &build_section_inputs(). 3439: '<input type="hidden" name="saveState" value="'.$env{'form.saveState'}.'" />'."\n". 3440: '<input type="hidden" name="Status" value="'.$env{'stu_status'}.'" />'."\n". 3441: '<input type="hidden" name="probTitle" value="'.$env{'form.probTitle'}.'" />'."\n"; 3442: 3443: my ($common_header,$specific_header); 3444: if ($env{'form.section'} eq 'all') { 3445: $common_header = &mt('Assign Common Grade to Class'); 3446: $specific_header = &mt('Assign Grade to Specific Students in Class'); 3447: } elsif ($env{'form.section'} eq 'none') { 3448: $common_header = &mt('Assign Common Grade to Students in no Section'); 3449: $specific_header = &mt('Assign Grade to Specific Students in no Section'); 3450: } else { 3451: my $section_display = join (", ",&Apache::loncommon::get_env_multiple('form.section')); 3452: $common_header = &mt('Assign Common Grade to Students in Section(s) [_1]',$section_display); 3453: $specific_header = &mt('Assign Grade to Specific Students in Section(s) [_1]',$section_display); 3454: } 3455: $result.= '<h3>'.$common_header.'</h3>'.&Apache::loncommon::start_data_table(); 3456: #radio buttons/text box for assigning points for a section or class. 3457: #handles different parts of a problem 3458: my $res_error; 3459: my ($partlist,$handgrade,$responseType) = &response_type($symb,\$res_error); 3460: if ($res_error) { 3461: return &navmap_errormsg(); 3462: } 3463: my %weight = (); 3464: my $ctsparts = 0; 3465: my %seen = (); 3466: my @part_response_id = &flatten_responseType($responseType); 3467: foreach my $part_response_id (@part_response_id) { 3468: my ($partid,$respid) = @{ $part_response_id }; 3469: my $part_resp = join('_',@{ $part_response_id }); 3470: next if $seen{$partid}; 3471: $seen{$partid}++; 3472: my $handgrade=$$handgrade{$part_resp}; 3473: my $wgt = &Apache::lonnet::EXT('resource.'.$partid.'.weight',$symb); 3474: $weight{$partid} = $wgt eq '' ? '1' : $wgt; 3475: 3476: my $display_part=&get_display_part($partid,$symb); 3477: my $radio.='<table border="0"><tr>'; 3478: my $ctr = 0; 3479: while ($ctr<=$weight{$partid}) { # display radio buttons in a nice table 10 across 3480: $radio.= '<td><label><input type="radio" name="RADVAL_'.$partid.'" '. 3481: 'onclick="javascript:writePoint(\''.$partid.'\','.$weight{$partid}. 3482: ','.$ctr.')" />'.$ctr."</label></td>\n"; 3483: $result.=(($ctr+1)%10 == 0 ? '</tr><tr>' : ''); 3484: $ctr++; 3485: } 3486: $radio.='</tr></table>'; 3487: my $line = '<input type="text" name="TEXTVAL_'. 3488: $partid.'" size="4" '.'onchange="javascript:writePoint(\''. 3489: $partid.'\','.$weight{$partid}.',\'textval\')" /> /'. 3490: $weight{$partid}.' '.&mt('(problem weight)').'</td>'."\n"; 3491: $line.= '<td><b>'.&mt('Grade Status').':</b><select name="SELVAL_'.$partid.'"'. 3492: 'onchange="javascript:writeRadText(\''.$partid.'\','. 3493: $weight{$partid}.')"> '. 3494: '<option selected="selected"> </option>'. 3495: '<option value="excused">'.&mt('excused').'</option>'. 3496: '<option value="reset status">'.&mt('reset status').'</option>'. 3497: '</select></td>'. 3498: '<td><label><input type="checkbox" name="FORCE_'.$partid.'" />'.&mt('Override "Correct"').'</label>'; 3499: $line.='<input type="hidden" name="partid_'. 3500: $ctsparts.'" value="'.$partid.'" />'."\n"; 3501: $line.='<input type="hidden" name="weight_'. 3502: $partid.'" value="'.$weight{$partid}.'" />'."\n"; 3503: 3504: $result.= 3505: &Apache::loncommon::start_data_table_row()."\n". 3506: '<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>'. 3507: &Apache::loncommon::end_data_table_row()."\n"; 3508: $ctsparts++; 3509: } 3510: $result.=&Apache::loncommon::end_data_table()."\n". 3511: '<input type="hidden" name="totalparts" value="'.$ctsparts.'" />'; 3512: $result.='<input type="button" value="'.&mt('Revert to Default').'" '. 3513: 'onclick="javascript:resetEntry('.$ctsparts.');" />'; 3514: 3515: #table listing all the students in a section/class 3516: #header of table 3517: $result.= '<h3>'.$specific_header.'</h3>'. 3518: &Apache::loncommon::start_data_table(). 3519: &Apache::loncommon::start_data_table_header_row(). 3520: '<th>'.&mt('No.').'</th>'. 3521: '<th>'.&nameUserString('header')."</th>\n"; 3522: my $partserror; 3523: my (@parts) = sort(&getpartlist($symb,\$partserror)); 3524: if ($partserror) { 3525: return &navmap_errormsg(); 3526: } 3527: my (undef,undef,$url)=&Apache::lonnet::decode_symb($symb); 3528: my @partids = (); 3529: foreach my $part (@parts) { 3530: my $display=&Apache::lonnet::metadata($url,$part.'.display'); 3531: my $narrowtext = &mt('Tries'); 3532: $display =~ s|^Number of Attempts|$narrowtext <br />|; # makes the column narrower 3533: if (!$display) { $display = &Apache::lonnet::metadata($url,$part.'.name'); } 3534: my ($partid) = &split_part_type($part); 3535: push(@partids,$partid); 3536: my $display_part=&get_display_part($partid,$symb); 3537: if ($display =~ /^Partial Credit Factor/) { 3538: $result.='<th>'. 3539: &mt('Score Part: [_1]<br /> (weight = [_2])', 3540: $display_part,$weight{$partid}).'</th>'."\n"; 3541: next; 3542: 3543: } else { 3544: if ($display =~ /Problem Status/) { 3545: my $grade_status_mt = &mt('Grade Status'); 3546: $display =~ s{Problem Status}{$grade_status_mt<br />}; 3547: } 3548: my $part_mt = &mt('Part:'); 3549: $display =~s{\[Part: \Q$partid\E\]}{$part_mt $display_part}; 3550: } 3551: 3552: $result.='<th>'.$display.'</th>'."\n"; 3553: } 3554: $result.=&Apache::loncommon::end_data_table_header_row(); 3555: 3556: my %last_resets = 3557: &get_last_resets($symb,$env{'request.course.id'},\@partids); 3558: 3559: #get info for each student 3560: #list all the students - with points and grade status 3561: my (undef,undef,$fullname) = &getclasslist($env{'form.section'},'1'); 3562: my $ctr = 0; 3563: foreach (sort 3564: { 3565: if (lc($$fullname{$a}) ne lc($$fullname{$b})) { 3566: return (lc($$fullname{$a}) cmp lc($$fullname{$b})); 3567: } 3568: return $a cmp $b; 3569: } (keys(%$fullname))) { 3570: $ctr++; 3571: $result.=&viewstudentgrade($symb,$env{'request.course.id'}, 3572: $_,$$fullname{$_},\@parts,\%weight,$ctr,\%last_resets); 3573: } 3574: $result.=&Apache::loncommon::end_data_table(); 3575: $result.='<input type="hidden" name="total" value="'.$ctr.'" />'."\n"; 3576: $result.='<input type="button" value="'.&mt('Save').'" '. 3577: 'onclick="javascript:submit();" target="_self" /></form>'."\n"; 3578: if (scalar(%$fullname) eq 0) { 3579: my $colspan=3+scalar(@parts); 3580: my $section_display = join (", ",&Apache::loncommon::get_env_multiple('form.section')); 3581: my $stu_status = join(' or ',&Apache::loncommon::get_env_multiple('form.Status')); 3582: $result='<span class="LC_warning">'. 3583: &mt('There are no students in section(s) [_1] with enrollment status [_2] to modify or grade.', 3584: $section_display, $stu_status). 3585: '</span>'; 3586: } 3587: $result.=&show_grading_menu_form($symb); 3588: return $result; 3589: } 3590: 3591: #--- call by previous routine to display each student 3592: sub viewstudentgrade { 3593: my ($symb,$courseid,$student,$fullname,$parts,$weight,$ctr,$last_resets) = @_; 3594: my ($uname,$udom) = split(/:/,$student); 3595: my %record=&Apache::lonnet::restore($symb,$courseid,$udom,$uname); 3596: my %aggregates = (); 3597: my $result=&Apache::loncommon::start_data_table_row().'<td align="right">'. 3598: '<input type="hidden" name="ctr'.($ctr-1).'" value="'.$student.'" />'. 3599: "\n".$ctr.' </td><td> '. 3600: '<a href="javascript:viewOneStudent(\''.$uname.'\',\''.$udom. 3601: '\');" target="_self">'.$fullname.'</a> '. 3602: '<span class="LC_internal_info">('.$uname.($env{'user.domain'} eq $udom ? '' : ':'.$udom).')</span></td>'."\n"; 3603: $student=~s/:/_/; # colon doen't work in javascript for names 3604: foreach my $apart (@$parts) { 3605: my ($part,$type) = &split_part_type($apart); 3606: my $score=$record{"resource.$part.$type"}; 3607: $result.='<td align="center">'; 3608: my ($aggtries,$totaltries); 3609: unless (exists($aggregates{$part})) { 3610: $totaltries = $record{'resource.'.$part.'.tries'}; 3611: 3612: $aggtries = $totaltries; 3613: if ($$last_resets{$part}) { 3614: $aggtries = &get_num_tries(\%record,$$last_resets{$part}, 3615: $part); 3616: } 3617: $result.='<input type="hidden" name="'. 3618: 'GD_'.$student.'_'.$part.'_aggtries" value="'.$aggtries.'" />'."\n"; 3619: $result.='<input type="hidden" name="'. 3620: 'GD_'.$student.'_'.$part.'_totaltries" value="'.$totaltries.'" />'."\n"; 3621: $aggregates{$part} = 1; 3622: } 3623: if ($type eq 'awarded') { 3624: my $pts = $score eq '' ? '' : &compute_points($score,$$weight{$part}); 3625: $result.='<input type="hidden" name="'. 3626: 'GD_'.$student.'_'.$part.'_awarded_s" value="'.$pts.'" />'."\n"; 3627: $result.='<input type="text" name="'. 3628: 'GD_'.$student.'_'.$part.'_awarded" '. 3629: 'onchange="javascript:changeSelect(\''.$part.'\',\''.$student. 3630: '\')" value="'.$pts.'" size="4" /></td>'."\n"; 3631: } elsif ($type eq 'solved') { 3632: my ($status,$foo)=split(/_/,$score,2); 3633: $status = 'nothing' if ($status eq ''); 3634: $result.='<input type="hidden" name="'.'GD_'.$student.'_'. 3635: $part.'_solved_s" value="'.$status.'" />'."\n"; 3636: $result.=' <select name="'. 3637: 'GD_'.$student.'_'.$part.'_solved" '. 3638: 'onchange="javascript:changeOneScore(\''.$part.'\',\''.$student.'\')" >'."\n"; 3639: $result.= (($status eq 'excused') ? '<option> </option><option selected="selected" value="excused">'.&mt('excused').'</option>' 3640: : '<option selected="selected"> </option><option value="excused">'.&mt('excused').'</option>')."\n"; 3641: $result.='<option value="reset status">'.&mt('reset status').'</option>'; 3642: $result.="</select> </td>\n"; 3643: } else { 3644: $result.='<input type="hidden" name="'. 3645: 'GD_'.$student.'_'.$part.'_'.$type.'_s" value="'.$score.'" />'. 3646: "\n"; 3647: $result.='<input type="text" name="'. 3648: 'GD_'.$student.'_'.$part.'_'.$type.'" '. 3649: 'value="'.$score.'" size="4" /></td>'."\n"; 3650: } 3651: } 3652: $result.=&Apache::loncommon::end_data_table_row(); 3653: return $result; 3654: } 3655: 3656: #--- change scores for all the students in a section/class 3657: # record does not get update if unchanged 3658: sub editgrades { 3659: my ($request) = @_; 3660: 3661: my $symb=&get_symb($request); 3662: my $section_display = join (", ",&Apache::loncommon::get_env_multiple('form.section')); 3663: my $title='<h2>'.&mt('Current Grade Status').'</h2>'; 3664: $title.='<h4>'.&mt('<b>Current Resource: </b>[_1]',$env{'form.probTitle'}).'</h4>'."\n"; 3665: $title.='<h4>'.&mt('<b>Section: </b>[_1]',$section_display).'</h4>'."\n"; 3666: 3667: my $result= &Apache::loncommon::start_data_table(). 3668: &Apache::loncommon::start_data_table_header_row(). 3669: '<th rowspan="2" valign="middle">'.&mt('No.').'</th>'. 3670: '<th rowspan="2" valign="middle">'.&nameUserString('header')."</th>\n"; 3671: my %scoreptr = ( 3672: 'correct' =>'correct_by_override', 3673: 'incorrect'=>'incorrect_by_override', 3674: 'excused' =>'excused', 3675: 'ungraded' =>'ungraded_attempted', 3676: 'credited' =>'credit_attempted', 3677: 'nothing' => '', 3678: ); 3679: my ($classlist,undef,$fullname) = &getclasslist($env{'form.section'},'0'); 3680: 3681: my (@partid); 3682: my %weight = (); 3683: my %columns = (); 3684: my ($i,$ctr,$count,$rec_update) = (0,0,0,0); 3685: 3686: my $partserror; 3687: my (@parts) = sort(&getpartlist($symb,\$partserror)); 3688: if ($partserror) { 3689: return &navmap_errormsg(); 3690: } 3691: my $header; 3692: while ($ctr < $env{'form.totalparts'}) { 3693: my $partid = $env{'form.partid_'.$ctr}; 3694: push(@partid,$partid); 3695: $weight{$partid} = $env{'form.weight_'.$partid}; 3696: $ctr++; 3697: } 3698: my (undef,undef,$url) = &Apache::lonnet::decode_symb($symb); 3699: foreach my $partid (@partid) { 3700: $header .= '<th align="center">'.&mt('Old Score').'</th>'. 3701: '<th align="center">'.&mt('New Score').'</th>'; 3702: $columns{$partid}=2; 3703: foreach my $stores (@parts) { 3704: my ($part,$type) = &split_part_type($stores); 3705: if ($part !~ m/^\Q$partid\E/) { next;} 3706: if ($type eq 'awarded' || $type eq 'solved') { next; } 3707: my $display=&Apache::lonnet::metadata($url,$stores.'.display'); 3708: $display =~ s/\[Part: \Q$part\E\]//; 3709: my $narrowtext = &mt('Tries'); 3710: $display =~ s/Number of Attempts/$narrowtext/; 3711: $header .= '<th align="center">'.&mt('Old').' '.$display.'</th>'. 3712: '<th align="center">'.&mt('New').' '.$display.'</th>'; 3713: $columns{$partid}+=2; 3714: } 3715: } 3716: foreach my $partid (@partid) { 3717: my $display_part=&get_display_part($partid,$symb); 3718: $result .= '<th colspan="'.$columns{$partid}.'" align="center">'. 3719: &mt('Part: [_1] (Weight = [_2])',$display_part,$weight{$partid}). 3720: '</th>'; 3721: 3722: } 3723: $result .= &Apache::loncommon::end_data_table_header_row(). 3724: &Apache::loncommon::start_data_table_header_row(). 3725: $header. 3726: &Apache::loncommon::end_data_table_header_row(); 3727: my @noupdate; 3728: my ($updateCtr,$noupdateCtr) = (1,1); 3729: for ($i=0; $i<$env{'form.total'}; $i++) { 3730: my $line; 3731: my $user = $env{'form.ctr'.$i}; 3732: my ($uname,$udom)=split(/:/,$user); 3733: my %newrecord; 3734: my $updateflag = 0; 3735: $line .= '<td>'.&nameUserString(undef,$$fullname{$user},$uname,$udom).'</td>'; 3736: my $usec=$classlist->{"$uname:$udom"}[5]; 3737: if (!&canmodify($usec)) { 3738: my $numcols=scalar(@partid)*4+2; 3739: push(@noupdate, 3740: $line."<td colspan=\"$numcols\"><span class=\"LC_warning\">". 3741: &mt('Not allowed to modify student')."</span></td></tr>"); 3742: next; 3743: } 3744: my %aggregate = (); 3745: my $aggregateflag = 0; 3746: $user=~s/:/_/; # colon doen't work in javascript for names 3747: foreach (@partid) { 3748: my $old_aw = $env{'form.GD_'.$user.'_'.$_.'_awarded_s'}; 3749: my $old_part_pcr = $old_aw/($weight{$_} ne '0' ? $weight{$_}:1); 3750: my $old_part = $old_aw eq '' ? '' : $old_part_pcr; 3751: my $old_score = $scoreptr{$env{'form.GD_'.$user.'_'.$_.'_solved_s'}}; 3752: my $awarded = $env{'form.GD_'.$user.'_'.$_.'_awarded'}; 3753: my $pcr = $awarded/($weight{$_} ne '0' ? $weight{$_} : 1); 3754: my $partial = $awarded eq '' ? '' : $pcr; 3755: my $score; 3756: if ($partial eq '') { 3757: $score = $scoreptr{$env{'form.GD_'.$user.'_'.$_.'_solved_s'}}; 3758: } elsif ($partial > 0) { 3759: $score = 'correct_by_override'; 3760: } elsif ($partial == 0) { 3761: $score = 'incorrect_by_override'; 3762: } 3763: my $dropMenu = $env{'form.GD_'.$user.'_'.$_.'_solved'}; 3764: $score = 'excused' if (($dropMenu eq 'excused') && ($score ne 'excused')); 3765: 3766: $newrecord{'resource.'.$_.'.regrader'}= 3767: "$env{'user.name'}:$env{'user.domain'}"; 3768: if ($dropMenu eq 'reset status' && 3769: $old_score ne '') { # ignore if no previous attempts => nothing to reset 3770: $newrecord{'resource.'.$_.'.tries'} = ''; 3771: $newrecord{'resource.'.$_.'.solved'} = ''; 3772: $newrecord{'resource.'.$_.'.award'} = ''; 3773: $newrecord{'resource.'.$_.'.awarded'} = ''; 3774: $updateflag = 1; 3775: if ($env{'form.GD_'.$user.'_'.$_.'_aggtries'} > 0) { 3776: my $aggtries = $env{'form.GD_'.$user.'_'.$_.'_aggtries'}; 3777: my $totaltries = $env{'form.GD_'.$user.'_'.$_.'_totaltries'}; 3778: my $solvedstatus = $env{'form.GD_'.$user.'_'.$_.'_solved_s'}; 3779: &decrement_aggs($symb,$_,\%aggregate,$aggtries,$totaltries,$solvedstatus); 3780: $aggregateflag = 1; 3781: } 3782: } elsif (!($old_part eq $partial && $old_score eq $score)) { 3783: $updateflag = 1; 3784: $newrecord{'resource.'.$_.'.awarded'} = $partial if $partial ne ''; 3785: $newrecord{'resource.'.$_.'.solved'} = $score; 3786: $rec_update++; 3787: } 3788: 3789: $line .= '<td align="center">'.$old_aw.' </td>'. 3790: '<td align="center">'.$awarded. 3791: ($score eq 'excused' ? $score : '').' </td>'; 3792: 3793: 3794: my $partid=$_; 3795: foreach my $stores (@parts) { 3796: my ($part,$type) = &split_part_type($stores); 3797: if ($part !~ m/^\Q$partid\E/) { next;} 3798: if ($type eq 'awarded' || $type eq 'solved') { next; } 3799: my $old_aw = $env{'form.GD_'.$user.'_'.$part.'_'.$type.'_s'}; 3800: my $awarded = $env{'form.GD_'.$user.'_'.$part.'_'.$type}; 3801: if ($awarded ne '' && $awarded ne $old_aw) { 3802: $newrecord{'resource.'.$part.'.'.$type}= $awarded; 3803: $newrecord{'resource.'.$part.'.regrader'}="$env{'user.name'}:$env{'user.domain'}"; 3804: $updateflag=1; 3805: } 3806: $line .= '<td align="center">'.$old_aw.' </td>'. 3807: '<td align="center">'.$awarded.' </td>'; 3808: } 3809: } 3810: $line.="\n"; 3811: 3812: my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; 3813: my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'}; 3814: 3815: if ($updateflag) { 3816: $count++; 3817: &Apache::lonnet::cstore(\%newrecord,$symb,$env{'request.course.id'}, 3818: $udom,$uname); 3819: 3820: if (&Apache::bridgetask::in_queue('gradingqueue',$symb,$cdom, 3821: $cnum,$udom,$uname)) { 3822: # need to figure out if should be in queue. 3823: my %record = 3824: &Apache::lonnet::restore($symb,$env{'request.course.id'}, 3825: $udom,$uname); 3826: my $all_graded = 1; 3827: my $none_graded = 1; 3828: foreach my $part (@parts) { 3829: if ( $record{'resource.'.$part.'.awarded'} eq '' ) { 3830: $all_graded = 0; 3831: } else { 3832: $none_graded = 0; 3833: } 3834: } 3835: 3836: if ($all_graded || $none_graded) { 3837: &Apache::bridgetask::remove_from_queue('gradingqueue', 3838: $symb,$cdom,$cnum, 3839: $udom,$uname); 3840: } 3841: } 3842: 3843: $result.=&Apache::loncommon::start_data_table_row(). 3844: '<td align="right"> '.$updateCtr.' </td>'.$line. 3845: &Apache::loncommon::end_data_table_row(); 3846: $updateCtr++; 3847: } else { 3848: push(@noupdate, 3849: '<td align="right"> '.$noupdateCtr.' </td>'.$line); 3850: $noupdateCtr++; 3851: } 3852: if ($aggregateflag) { 3853: &Apache::lonnet::cinc('nohist_resourcetracker',\%aggregate, 3854: $cdom,$cnum); 3855: } 3856: } 3857: if (@noupdate) { 3858: # my $numcols=(scalar(@partid)*(scalar(@parts)-1)*2)+3; 3859: my $numcols=scalar(@partid)*4+2; 3860: $result .= &Apache::loncommon::start_data_table_row('LC_empty_row'). 3861: '<td align="center" colspan="'.$numcols.'">'. 3862: &mt('No Changes Occurred For the Students Below'). 3863: '</td>'. 3864: &Apache::loncommon::end_data_table_row(); 3865: foreach my $line (@noupdate) { 3866: $result.= 3867: &Apache::loncommon::start_data_table_row(). 3868: $line. 3869: &Apache::loncommon::end_data_table_row(); 3870: } 3871: } 3872: $result .= &Apache::loncommon::end_data_table(). 3873: &show_grading_menu_form($symb); 3874: my $msg = '<p><b>'. 3875: &mt('Number of records updated = [_1] for [quant,_2,student].', 3876: $rec_update,$count).'</b><br />'. 3877: '<b>'.&mt('Total number of students = [_1]',$env{'form.total'}). 3878: '</b></p>'; 3879: return $title.$msg.$result; 3880: } 3881: 3882: sub split_part_type { 3883: my ($partstr) = @_; 3884: my ($temp,@allparts)=split(/_/,$partstr); 3885: my $type=pop(@allparts); 3886: my $part=join('_',@allparts); 3887: return ($part,$type); 3888: } 3889: 3890: #------------- end of section for handling grading by section/class --------- 3891: # 3892: #---------------------------------------------------------------------------- 3893: 3894: 3895: #---------------------------------------------------------------------------- 3896: # 3897: #-------------------------- Next few routines handles grading by csv upload 3898: # 3899: #--- Javascript to handle csv upload 3900: sub csvupload_javascript_reverse_associate { 3901: my $error1=&mt('You need to specify the username or the student/employee ID'); 3902: my $error2=&mt('You need to specify at least one grading field'); 3903: return(<<ENDPICK); 3904: function verify(vf) { 3905: var foundsomething=0; 3906: var founduname=0; 3907: var foundID=0; 3908: for (i=0;i<=vf.nfields.value;i++) { 3909: tw=eval('vf.f'+i+'.selectedIndex'); 3910: if (i==0 && tw!=0) { foundID=1; } 3911: if (i==1 && tw!=0) { founduname=1; } 3912: if (i!=0 && i!=1 && i!=2 && tw!=0) { foundsomething=1; } 3913: } 3914: if (founduname==0 && foundID==0) { 3915: alert('$error1'); 3916: return; 3917: } 3918: if (foundsomething==0) { 3919: alert('$error2'); 3920: return; 3921: } 3922: vf.submit(); 3923: } 3924: function flip(vf,tf) { 3925: var nw=eval('vf.f'+tf+'.selectedIndex'); 3926: var i; 3927: for (i=0;i<=vf.nfields.value;i++) { 3928: //can not pick the same destination field for both name and domain 3929: if (((i ==0)||(i ==1)) && 3930: ((tf==0)||(tf==1)) && 3931: (i!=tf) && 3932: (eval('vf.f'+i+'.selectedIndex')==nw)) { 3933: eval('vf.f'+i+'.selectedIndex=0;') 3934: } 3935: } 3936: } 3937: ENDPICK 3938: } 3939: 3940: sub csvupload_javascript_forward_associate { 3941: my $error1=&mt('You need to specify the username or the student/employee ID'); 3942: my $error2=&mt('You need to specify at least one grading field'); 3943: return(<<ENDPICK); 3944: function verify(vf) { 3945: var foundsomething=0; 3946: var founduname=0; 3947: var foundID=0; 3948: for (i=0;i<=vf.nfields.value;i++) { 3949: tw=eval('vf.f'+i+'.selectedIndex'); 3950: if (tw==1) { foundID=1; } 3951: if (tw==2) { founduname=1; } 3952: if (tw>3) { foundsomething=1; } 3953: } 3954: if (founduname==0 && foundID==0) { 3955: alert('$error1'); 3956: return; 3957: } 3958: if (foundsomething==0) { 3959: alert('$error2'); 3960: return; 3961: } 3962: vf.submit(); 3963: } 3964: function flip(vf,tf) { 3965: var nw=eval('vf.f'+tf+'.selectedIndex'); 3966: var i; 3967: //can not pick the same destination field twice 3968: for (i=0;i<=vf.nfields.value;i++) { 3969: if ((i!=tf) && (eval('vf.f'+i+'.selectedIndex')==nw)) { 3970: eval('vf.f'+i+'.selectedIndex=0;') 3971: } 3972: } 3973: } 3974: ENDPICK 3975: } 3976: 3977: sub csvuploadmap_header { 3978: my ($request,$symb,$datatoken,$distotal)= @_; 3979: my $javascript; 3980: if ($env{'form.upfile_associate'} eq 'reverse') { 3981: $javascript=&csvupload_javascript_reverse_associate(); 3982: } else { 3983: $javascript=&csvupload_javascript_forward_associate(); 3984: } 3985: 3986: my ($result) = &showResourceInfo($symb,$env{'form.probTitle'}); 3987: my $checked=(($env{'form.noFirstLine'})?' checked="checked"':''); 3988: my $ignore=&mt('Ignore First Line'); 3989: $symb = &Apache::lonenc::check_encrypt($symb); 3990: $request->print(<<ENDPICK); 3991: <form method="post" enctype="multipart/form-data" action="/adm/grades" name="gradesupload"> 3992: <h3><span class="LC_info">Uploading Class Grades</span></h3> 3993: $result 3994: <hr /> 3995: <h3>Identify fields</h3> 3996: Total number of records found in file: $distotal <hr /> 3997: Enter as many fields as you can. The system will inform you and bring you back 3998: to this page if the data selected is insufficient to run your class.<hr /> 3999: <input type="button" value="Reverse Association" onclick="javascript:this.form.associate.value='Reverse Association';submit(this.form);" /> 4000: <label><input type="checkbox" name="noFirstLine" $checked />$ignore</label> 4001: <input type="hidden" name="associate" value="" /> 4002: <input type="hidden" name="phase" value="three" /> 4003: <input type="hidden" name="datatoken" value="$datatoken" /> 4004: <input type="hidden" name="fileupload" value="$env{'form.fileupload'}" /> 4005: <input type="hidden" name="upfiletype" value="$env{'form.upfiletype'}" /> 4006: <input type="hidden" name="upfile_associate" 4007: value="$env{'form.upfile_associate'}" /> 4008: <input type="hidden" name="symb" value="$symb" /> 4009: <input type="hidden" name="saveState" value="$env{'form.saveState'}" /> 4010: <input type="hidden" name="probTitle" value="$env{'form.probTitle'}" /> 4011: <input type="hidden" name="command" value="csvuploadoptions" /> 4012: <hr /> 4013: <script type="text/javascript" language="Javascript"> 4014: $javascript 4015: </script> 4016: ENDPICK 4017: return ''; 4018: 4019: } 4020: 4021: sub csvupload_fields { 4022: my ($symb,$errorref) = @_; 4023: my (@parts) = &getpartlist($symb,$errorref); 4024: if (ref($errorref)) { 4025: if ($$errorref) { 4026: return; 4027: } 4028: } 4029: 4030: my @fields=(['ID','Student/Employee ID'], 4031: ['username','Student Username'], 4032: ['domain','Student Domain']); 4033: my (undef,undef,$url) = &Apache::lonnet::decode_symb($symb); 4034: foreach my $part (sort(@parts)) { 4035: my @datum; 4036: my $display=&Apache::lonnet::metadata($url,$part.'.display'); 4037: my $name=$part; 4038: if (!$display) { $display = $name; } 4039: @datum=($name,$display); 4040: if ($name=~/^stores_(.*)_awarded/) { 4041: push(@fields,['stores_'.$1.'_points',"Points [Part: $1]"]); 4042: } 4043: push(@fields,\@datum); 4044: } 4045: return (@fields); 4046: } 4047: 4048: sub csvuploadmap_footer { 4049: my ($request,$i,$keyfields) =@_; 4050: $request->print(<<ENDPICK); 4051: </table> 4052: <input type="hidden" name="nfields" value="$i" /> 4053: <input type="hidden" name="keyfields" value="$keyfields" /> 4054: <input type="button" onclick="javascript:verify(this.form)" value="Assign Grades" /><br /> 4055: </form> 4056: ENDPICK 4057: } 4058: 4059: sub checkforfile_js { 4060: my $alertmsg = &mt('Please use the browse button to select a file from your local directory.'); 4061: my $result =<<CSVFORMJS; 4062: <script type="text/javascript" language="javascript"> 4063: function checkUpload(formname) { 4064: if (formname.upfile.value == "") { 4065: alert("$alertmsg"); 4066: return false; 4067: } 4068: formname.submit(); 4069: } 4070: </script> 4071: CSVFORMJS 4072: return $result; 4073: } 4074: 4075: sub upcsvScores_form { 4076: my ($request) = shift; 4077: my ($symb)=&get_symb($request); 4078: if (!$symb) {return '';} 4079: my $result=&checkforfile_js(); 4080: $env{'form.probTitle'} = &Apache::lonnet::gettitle($symb); 4081: my ($table) = &showResourceInfo($symb,$env{'form.probTitle'}); 4082: $result.=$table; 4083: $result.='<br /><table width="100%" border="0"><tr><td bgcolor="#777777">'."\n"; 4084: $result.='<table width="100%" border="0"><tr bgcolor="#e6ffff"><td>'."\n"; 4085: $result.=' <b>'.&mt('Specify a file containing the class scores for current resource.'). 4086: '</b></td></tr>'."\n"; 4087: $result.='<tr bgcolor="#ffffe6"><td>'."\n"; 4088: my $upload=&mt("Upload Scores"); 4089: my $upfile_select=&Apache::loncommon::upfile_select_html(); 4090: my $ignore=&mt('Ignore First Line'); 4091: $symb = &Apache::lonenc::check_encrypt($symb); 4092: $result.=<<ENDUPFORM; 4093: <form method="post" enctype="multipart/form-data" action="/adm/grades" name="gradesupload"> 4094: <input type="hidden" name="symb" value="$symb" /> 4095: <input type="hidden" name="command" value="csvuploadmap" /> 4096: <input type="hidden" name="probTitle" value="$env{'form.probTitle'}" /> 4097: <input type="hidden" name="saveState" value="$env{'form.saveState'}" /> 4098: $upfile_select 4099: <br /><input type="button" onclick="javascript:checkUpload(this.form);" value="$upload" /> 4100: <label><input type="checkbox" name="noFirstLine" />$ignore</label> 4101: </form> 4102: ENDUPFORM 4103: $result.=&Apache::loncommon::help_open_topic("Course_Convert_To_CSV", 4104: &mt("How do I create a CSV file from a spreadsheet")) 4105: .'</td></tr></table>'."\n"; 4106: $result.='</td></tr></table><br /><br />'."\n"; 4107: $result.=&show_grading_menu_form($symb); 4108: return $result; 4109: } 4110: 4111: 4112: sub csvuploadmap { 4113: my ($request)= @_; 4114: my ($symb)=&get_symb($request); 4115: if (!$symb) {return '';} 4116: 4117: my $datatoken; 4118: if (!$env{'form.datatoken'}) { 4119: $datatoken=&Apache::loncommon::upfile_store($request); 4120: } else { 4121: $datatoken=$env{'form.datatoken'}; 4122: &Apache::loncommon::load_tmp_file($request); 4123: } 4124: my @records=&Apache::loncommon::upfile_record_sep(); 4125: if ($env{'form.noFirstLine'}) { shift(@records); } 4126: &csvuploadmap_header($request,$symb,$datatoken,$#records+1); 4127: my ($i,$keyfields); 4128: if (@records) { 4129: my $fieldserror; 4130: my @fields=&csvupload_fields($symb,\$fieldserror); 4131: if ($fieldserror) { 4132: $request->print(&navmap_errormsg()); 4133: return; 4134: } 4135: if ($env{'form.upfile_associate'} eq 'reverse') { 4136: &Apache::loncommon::csv_print_samples($request,\@records); 4137: $i=&Apache::loncommon::csv_print_select_table($request,\@records, 4138: \@fields); 4139: foreach (@fields) { $keyfields.=$_->[0].','; } 4140: chop($keyfields); 4141: } else { 4142: unshift(@fields,['none','']); 4143: $i=&Apache::loncommon::csv_samples_select_table($request,\@records, 4144: \@fields); 4145: foreach my $rec (@records) { 4146: my %temp = &Apache::loncommon::record_sep($rec); 4147: if (%temp) { 4148: $keyfields=join(',',sort(keys(%temp))); 4149: last; 4150: } 4151: } 4152: } 4153: } 4154: &csvuploadmap_footer($request,$i,$keyfields); 4155: $request->print(&show_grading_menu_form($symb)); 4156: 4157: return ''; 4158: } 4159: 4160: sub csvuploadoptions { 4161: my ($request)= @_; 4162: my ($symb)=&get_symb($request); 4163: my $checked=(($env{'form.noFirstLine'})?'1':'0'); 4164: my $ignore=&mt('Ignore First Line'); 4165: $request->print(<<ENDPICK); 4166: <form method="post" enctype="multipart/form-data" action="/adm/grades" name="gradesupload"> 4167: <h3><span class="LC_info">Uploading Class Grade Options</span></h3> 4168: <input type="hidden" name="command" value="csvuploadassign" /> 4169: <!-- 4170: <p> 4171: <label> 4172: <input type="checkbox" name="show_full_results" /> 4173: Show a table of all changes 4174: </label> 4175: </p> 4176: --> 4177: <p> 4178: <label> 4179: <input type="checkbox" name="overwite_scores" checked="checked" /> 4180: Overwrite any existing score 4181: </label> 4182: </p> 4183: ENDPICK 4184: my %fields=&get_fields(); 4185: if (!defined($fields{'domain'})) { 4186: my $domform = &Apache::loncommon::select_dom_form($env{'request.role.domain'},'default_domain'); 4187: $request->print("\n<p> Users are in domain: ".$domform."</p>\n"); 4188: } 4189: foreach my $key (sort(keys(%env))) { 4190: if ($key !~ /^form\.(.*)$/) { next; } 4191: my $cleankey=$1; 4192: if ($cleankey eq 'command') { next; } 4193: $request->print('<input type="hidden" name="'.$cleankey. 4194: '" value="'.$env{$key}.'" />'."\n"); 4195: } 4196: # FIXME do a check for any duplicated user ids... 4197: # FIXME do a check for any invalid user ids?... 4198: $request->print('<input type="submit" value="Assign Grades" /><br /> 4199: <hr /></form>'."\n"); 4200: $request->print(&show_grading_menu_form($symb)); 4201: return ''; 4202: } 4203: 4204: sub get_fields { 4205: my %fields; 4206: my @keyfields = split(/\,/,$env{'form.keyfields'}); 4207: for (my $i=0; $i<=$env{'form.nfields'}; $i++) { 4208: if ($env{'form.upfile_associate'} eq 'reverse') { 4209: if ($env{'form.f'.$i} ne 'none') { 4210: $fields{$keyfields[$i]}=$env{'form.f'.$i}; 4211: } 4212: } else { 4213: if ($env{'form.f'.$i} ne 'none') { 4214: $fields{$env{'form.f'.$i}}=$keyfields[$i]; 4215: } 4216: } 4217: } 4218: return %fields; 4219: } 4220: 4221: sub csvuploadassign { 4222: my ($request)= @_; 4223: my ($symb)=&get_symb($request); 4224: if (!$symb) {return '';} 4225: my $error_msg = ''; 4226: &Apache::loncommon::load_tmp_file($request); 4227: my @gradedata = &Apache::loncommon::upfile_record_sep(); 4228: if ($env{'form.noFirstLine'}) { shift(@gradedata); } 4229: my %fields=&get_fields(); 4230: $request->print('<h3>Assigning Grades</h3>'); 4231: my $courseid=$env{'request.course.id'}; 4232: my ($classlist) = &getclasslist('all',0); 4233: my @notallowed; 4234: my @skipped; 4235: my @warnings; 4236: my $countdone=0; 4237: foreach my $grade (@gradedata) { 4238: my %entries=&Apache::loncommon::record_sep($grade); 4239: my $domain; 4240: if ($entries{$fields{'domain'}}) { 4241: $domain=$entries{$fields{'domain'}}; 4242: } else { 4243: $domain=$env{'form.default_domain'}; 4244: } 4245: $domain=~s/\s//g; 4246: my $username=$entries{$fields{'username'}}; 4247: $username=~s/\s//g; 4248: if (!$username) { 4249: my $id=$entries{$fields{'ID'}}; 4250: $id=~s/\s//g; 4251: my %ids=&Apache::lonnet::idget($domain,$id); 4252: $username=$ids{$id}; 4253: } 4254: if (!exists($$classlist{"$username:$domain"})) { 4255: my $id=$entries{$fields{'ID'}}; 4256: $id=~s/\s//g; 4257: if ($id) { 4258: push(@skipped,"$id:$domain"); 4259: } else { 4260: push(@skipped,"$username:$domain"); 4261: } 4262: next; 4263: } 4264: my $usec=$classlist->{"$username:$domain"}[5]; 4265: if (!&canmodify($usec)) { 4266: push(@notallowed,"$username:$domain"); 4267: next; 4268: } 4269: my %points; 4270: my %grades; 4271: foreach my $dest (keys(%fields)) { 4272: if ($dest eq 'ID' || $dest eq 'username' || 4273: $dest eq 'domain') { next; } 4274: if ($entries{$fields{$dest}} =~ /^\s*$/) { next; } 4275: if ($dest=~/stores_(.*)_points/) { 4276: my $part=$1; 4277: my $wgt =&Apache::lonnet::EXT('resource.'.$part.'.weight', 4278: $symb,$domain,$username); 4279: if ($wgt) { 4280: $entries{$fields{$dest}}=~s/\s//g; 4281: my $pcr=$entries{$fields{$dest}} / $wgt; 4282: my $award=($pcr == 0) ? 'incorrect_by_override' 4283: : 'correct_by_override'; 4284: if ($pcr>1) { 4285: push(@warnings,&mt("[_1]: point value larger than weight","$username:$domain")); 4286: } 4287: $grades{"resource.$part.awarded"}=$pcr; 4288: $grades{"resource.$part.solved"}=$award; 4289: $points{$part}=1; 4290: } else { 4291: $error_msg = "<br />" . 4292: &mt("Some point values were assigned" 4293: ." for problems with a weight " 4294: ."of zero. These values were " 4295: ."ignored."); 4296: } 4297: } else { 4298: if ($dest=~/stores_(.*)_awarded/) { if ($points{$1}) {next;} } 4299: if ($dest=~/stores_(.*)_solved/) { if ($points{$1}) {next;} } 4300: my $store_key=$dest; 4301: $store_key=~s/^stores/resource/; 4302: $store_key=~s/_/\./g; 4303: $grades{$store_key}=$entries{$fields{$dest}}; 4304: } 4305: } 4306: if (! %grades) { 4307: push(@skipped,&mt("[_1]: no data to save","$username:$domain")); 4308: } else { 4309: $grades{"resource.regrader"}="$env{'user.name'}:$env{'user.domain'}"; 4310: my $result=&Apache::lonnet::cstore(\%grades,$symb, 4311: $env{'request.course.id'}, 4312: $domain,$username); 4313: if ($result eq 'ok') { 4314: $request->print('.'); 4315: # Remove from grading queue 4316: &Apache::bridgetask::remove_from_queue('gradingqueue',$symb, 4317: $env{'course.'.$env{'request.course.id'}.'.domain'}, 4318: $env{'course.'.$env{'request.course.id'}.'.num'}, 4319: $domain,$username); 4320: } else { 4321: $request->print("<p><span class=\"LC_error\">". 4322: &mt("Failed to save data for student [_1]. Message when trying to save was: [_2]", 4323: "$username:$domain",$result)."</span></p>"); 4324: } 4325: $request->rflush(); 4326: $countdone++; 4327: } 4328: } 4329: $request->print('<br />'.&Apache::lonhtmlcommon::confirm_success(&mt("Saved scores for [quant,_1,student]",$countdone),$countdone==0)); 4330: if (@warnings) { 4331: $request->print('<br />'.&Apache::lonhtmlcommon::confirm_success(&mt('Warnings generated for the following saved scores:'),1).'<br />'); 4332: $request->print(join(', ',@warnings)); 4333: } 4334: if (@skipped) { 4335: $request->print('<br />'.&Apache::lonhtmlcommon::confirm_success(&mt('No scores stored for the following username(s):'),1).'<br />'); 4336: $request->print(join(', ',@skipped)); 4337: } 4338: if (@notallowed) { 4339: $request->print('<br />'.&Apache::lonhtmlcommon::confirm_success(&mt('Modification of scores not allowed for the following username(s):'),1).'<br />'); 4340: $request->print(join(', ',@notallowed)); 4341: } 4342: $request->print("<br />\n"); 4343: $request->print(&show_grading_menu_form($symb)); 4344: return $error_msg; 4345: } 4346: #------------- end of section for handling csv file upload --------- 4347: # 4348: #------------------------------------------------------------------- 4349: # 4350: #-------------- Next few routines handle grading by page/sequence 4351: # 4352: #--- Select a page/sequence and a student to grade 4353: sub pickStudentPage { 4354: my ($request) = shift; 4355: 4356: my $alertmsg = &mt('Please select the student you wish to grade.'); 4357: $request->print(<<LISTJAVASCRIPT); 4358: <script type="text/javascript" language="javascript"> 4359: 4360: function checkPickOne(formname) { 4361: if (radioSelection(formname.student) == null) { 4362: alert("$alertmsg"); 4363: return; 4364: } 4365: ptr = pullDownSelection(formname.selectpage); 4366: formname.page.value = formname["page"+ptr].value; 4367: formname.title.value = formname["title"+ptr].value; 4368: formname.submit(); 4369: } 4370: 4371: </script> 4372: LISTJAVASCRIPT 4373: &commonJSfunctions($request); 4374: my ($symb) = &get_symb($request); 4375: my $cdom = $env{"course.$env{'request.course.id'}.domain"}; 4376: my $cnum = $env{"course.$env{'request.course.id'}.num"}; 4377: my $getsec = $env{'form.section'} eq '' ? 'all' : $env{'form.section'}; 4378: 4379: my $result='<h3><span class="LC_info"> '. 4380: &mt('Manual Grading by Page or Sequence').'</span></h3>'; 4381: 4382: $result.='<form action="/adm/grades" method="post" name="displayPage">'."\n"; 4383: my $map_error; 4384: my ($titles,$symbx) = &getSymbMap($map_error); 4385: if ($map_error) { 4386: $request->print(&navmap_errormsg()); 4387: return; 4388: } 4389: my ($curpage) =&Apache::lonnet::decode_symb($symb); 4390: # my ($curpage,$mapId) =&Apache::lonnet::decode_symb($symb); 4391: # my $type=($curpage =~ /\.(page|sequence)/); 4392: my $select = '<select name="selectpage">'."\n"; 4393: my $ctr=0; 4394: foreach (@$titles) { 4395: my ($minder,$showtitle) = ($_ =~ /(\d+)\.(.*)/); 4396: $select.='<option value="'.$ctr.'" '. 4397: ($$symbx{$_} =~ /$curpage$/ ? 'selected="selected"' : ''). 4398: '>'.$showtitle.'</option>'."\n"; 4399: $ctr++; 4400: } 4401: $select.= '</select>'; 4402: $result.=' <b>'.&mt('Problems from').':</b> '.$select."<br />\n"; 4403: 4404: $ctr=0; 4405: foreach (@$titles) { 4406: my ($minder,$showtitle) = ($_ =~ /(\d+)\.(.*)/); 4407: $result.='<input type="hidden" name="page'.$ctr.'" value="'.$$symbx{$_}.'" />'."\n"; 4408: $result.='<input type="hidden" name="title'.$ctr.'" value="'.$showtitle.'" />'."\n"; 4409: $ctr++; 4410: } 4411: $result.='<input type="hidden" name="page" />'."\n". 4412: '<input type="hidden" name="title" />'."\n"; 4413: 4414: my $options = 4415: '<label><input type="radio" name="vProb" value="no" checked="checked" /> '.&mt('no').' </label>'."\n". 4416: '<label><input type="radio" name="vProb" value="yes" /> '.&mt('yes').' </label>'."<br />\n"; 4417: $result.=' <b>'.&mt('View Problem Text').': </b>'.$options; 4418: 4419: $options = 4420: '<label><input type="radio" name="lastSub" value="none" /> '.&mt('none').' </label>'."\n". 4421: '<label><input type="radio" name="lastSub" value="datesub" checked="checked" /> '.&mt('by dates and submissions').'</label>'."\n". 4422: '<label><input type="radio" name="lastSub" value="all" /> '.&mt('all details').' </label>'."\n"; 4423: $result.=' <b>'.&mt('Submissions').': </b>'.$options; 4424: 4425: $result.=&build_section_inputs(); 4426: my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status')); 4427: $result.='<input type="hidden" name="Status" value="'.$stu_status.'" />'."\n". 4428: '<input type="hidden" name="command" value="displayPage" />'."\n". 4429: '<input type="hidden" name="symb" value="'.&Apache::lonenc::check_encrypt($symb).'" />'."\n". 4430: '<input type="hidden" name="saveState" value="'.$env{'form.saveState'}.'" />'."<br />\n"; 4431: 4432: $result.=' <b>'.&mt('Use CODE').': </b> <input type="text" name="CODE" value="" /> <br />'."\n"; 4433: 4434: $result.=' <input type="button" '. 4435: 'onclick="javascript:checkPickOne(this.form);" value="'.&mt('Next').' →" /><br />'."\n"; 4436: 4437: $request->print($result); 4438: 4439: my $studentTable.=' <b>'.&mt('Select a student you wish to grade and then click on the Next button.').'</b><br />'. 4440: &Apache::loncommon::start_data_table(). 4441: &Apache::loncommon::start_data_table_header_row(). 4442: '<th align="right"> '.&mt('No.').'</th>'. 4443: '<th>'.&nameUserString('header').'</th>'. 4444: '<th align="right"> '.&mt('No.').'</th>'. 4445: '<th>'.&nameUserString('header').'</th>'. 4446: &Apache::loncommon::end_data_table_header_row(); 4447: 4448: my (undef,undef,$fullname) = &getclasslist($getsec,'1'); 4449: my $ptr = 1; 4450: foreach my $student (sort 4451: { 4452: if (lc($$fullname{$a}) ne lc($$fullname{$b})) { 4453: return (lc($$fullname{$a}) cmp lc($$fullname{$b})); 4454: } 4455: return $a cmp $b; 4456: } (keys(%$fullname))) { 4457: my ($uname,$udom) = split(/:/,$student); 4458: $studentTable.=($ptr%2==1 ? &Apache::loncommon::start_data_table_row() 4459: : '</td>'); 4460: $studentTable.='<td align="right">'.$ptr.' </td>'; 4461: $studentTable.='<td> <label><input type="radio" name="student" value="'.$student.'" /> ' 4462: .&nameUserString(undef,$$fullname{$student},$uname,$udom)."</label>\n"; 4463: $studentTable.= 4464: ($ptr%2 == 0 ? '</td>'.&Apache::loncommon::end_data_table_row() 4465: : ''); 4466: $ptr++; 4467: } 4468: if ($ptr%2 == 0) { 4469: $studentTable.='</td><td> </td><td> </td>'. 4470: &Apache::loncommon::end_data_table_row(); 4471: } 4472: $studentTable.=&Apache::loncommon::end_data_table()."\n"; 4473: $studentTable.='<input type="button" '. 4474: 'onclick="javascript:checkPickOne(this.form);" value="'.&mt('Next').' →" /></form>'."\n"; 4475: 4476: $studentTable.=&show_grading_menu_form($symb); 4477: $request->print($studentTable); 4478: 4479: return ''; 4480: } 4481: 4482: sub getSymbMap { 4483: my ($map_error) = @_; 4484: my $navmap = Apache::lonnavmaps::navmap->new(); 4485: unless (ref($navmap)) { 4486: if (ref($map_error)) { 4487: $$map_error = 'navmap'; 4488: } 4489: return; 4490: } 4491: my %symbx = (); 4492: my @titles = (); 4493: my $minder = 0; 4494: 4495: # Gather every sequence that has problems. 4496: my @sequences = $navmap->retrieveResources(undef, sub { shift->is_map(); }, 4497: 1,0,1); 4498: for my $sequence ($navmap->getById('0.0'), @sequences) { 4499: if ($navmap->hasResource($sequence, sub { shift->is_problem(); }, 0) ) { 4500: my $title = $minder.'.'. 4501: &HTML::Entities::encode($sequence->compTitle(),'"\'&'); 4502: push(@titles, $title); # minder in case two titles are identical 4503: $symbx{$title} = &HTML::Entities::encode($sequence->symb(),'"\'&'); 4504: $minder++; 4505: } 4506: } 4507: return \@titles,\%symbx; 4508: } 4509: 4510: # 4511: #--- Displays a page/sequence w/wo problems, w/wo submissions 4512: sub displayPage { 4513: my ($request) = shift; 4514: 4515: my ($symb) = &get_symb($request); 4516: my $cdom = $env{"course.$env{'request.course.id'}.domain"}; 4517: my $cnum = $env{"course.$env{'request.course.id'}.num"}; 4518: my $getsec = $env{'form.section'} eq '' ? 'all' : $env{'form.section'}; 4519: my $pageTitle = $env{'form.page'}; 4520: my ($classlist,undef,$fullname) = &getclasslist($getsec,'1'); 4521: my ($uname,$udom) = split(/:/,$env{'form.student'}); 4522: my $usec=$classlist->{$env{'form.student'}}[5]; 4523: 4524: #need to make sure we have the correct data for later EXT calls, 4525: #thus invalidate the cache 4526: &Apache::lonnet::devalidatecourseresdata( 4527: $env{'course.'.$env{'request.course.id'}.'.num'}, 4528: $env{'course.'.$env{'request.course.id'}.'.domain'}); 4529: &Apache::lonnet::clear_EXT_cache_status(); 4530: 4531: if (!&canview($usec)) { 4532: $request->print('<span class="LC_warning">'.&mt('Unable to view requested student. ([_1])',$env{'form.student'}).'</span>'); 4533: $request->print(&show_grading_menu_form($symb)); 4534: return; 4535: } 4536: my $result='<h3><span class="LC_info"> '.$env{'form.title'}.'</span></h3>'; 4537: $result.='<h3> '.&mt('Student: [_1]',&nameUserString(undef,$$fullname{$env{'form.student'}},$uname,$udom)). 4538: '</h3>'."\n"; 4539: $env{'form.CODE'} = uc($env{'form.CODE'}); 4540: if (&Apache::lonnet::validCODE(uc($env{'form.CODE'}))) { 4541: $result.='<h3> '.&mt('CODE: [_1]',$env{'form.CODE'}).'</h3>'."\n"; 4542: } else { 4543: delete($env{'form.CODE'}); 4544: } 4545: &sub_page_js($request); 4546: $request->print($result); 4547: 4548: my $navmap = Apache::lonnavmaps::navmap->new(); 4549: unless (ref($navmap)) { 4550: $request->print(&navmap_errormsg()); 4551: $request->print(&show_grading_menu_form($symb)); 4552: return; 4553: } 4554: my ($mapUrl, $id, $resUrl)=&Apache::lonnet::decode_symb($env{'form.page'}); 4555: my $map = $navmap->getResourceByUrl($resUrl); # add to navmaps 4556: if (!$map) { 4557: $request->print('<span class="LC_warning">'.&mt('Unable to view requested sequence. ([_1])',$resUrl).'</span>'); 4558: $request->print(&show_grading_menu_form($symb)); 4559: return; 4560: } 4561: my $iterator = $navmap->getIterator($map->map_start(), 4562: $map->map_finish()); 4563: 4564: my $studentTable='<form action="/adm/grades" method="post" name="gradePage">'."\n". 4565: '<input type="hidden" name="command" value="gradeByPage" />'."\n". 4566: '<input type="hidden" name="fullname" value="'.$$fullname{$env{'form.student'}}.'" />'."\n". 4567: '<input type="hidden" name="student" value="'.$env{'form.student'}.'" />'."\n". 4568: '<input type="hidden" name="page" value="'.$pageTitle.'" />'."\n". 4569: '<input type="hidden" name="title" value="'.$env{'form.title'}.'" />'."\n". 4570: '<input type="hidden" name="symb" value="'.&Apache::lonenc::check_encrypt($symb).'" />'."\n". 4571: '<input type="hidden" name="overRideScore" value="no" />'."\n". 4572: '<input type="hidden" name="saveState" value="'.$env{'form.saveState'}.'" />'."\n"; 4573: 4574: if (defined($env{'form.CODE'})) { 4575: $studentTable.= 4576: '<input type="hidden" name="CODE" value="'.$env{'form.CODE'}.'" />'."\n"; 4577: } 4578: my $checkIcon = '<img alt="'.&mt('Check Mark'). 4579: '" src="'.&Apache::loncommon::lonhttpdurl($request->dir_config('lonIconsURL').'/check.gif').'" height="16" border="0" />'; 4580: 4581: $studentTable.=' <span class="LC_info">'. 4582: &mt('Problems graded correct by the computer are marked with a [_1] symbol.',$checkIcon). 4583: '</span>'."\n". 4584: &Apache::loncommon::start_data_table(). 4585: &Apache::loncommon::start_data_table_header_row(). 4586: '<th align="center"> Prob. </th>'. 4587: '<th> '.($env{'form.vProb'} eq 'no' ? &mt('Title') : &mt('Problem Text')).'/'.&mt('Grade').'</th>'. 4588: &Apache::loncommon::end_data_table_header_row(); 4589: 4590: &Apache::lonxml::clear_problem_counter(); 4591: my ($depth,$question,$prob) = (1,1,1); 4592: $iterator->next(); # skip the first BEGIN_MAP 4593: my $curRes = $iterator->next(); # for "current resource" 4594: while ($depth > 0) { 4595: if($curRes == $iterator->BEGIN_MAP) { $depth++; } 4596: if($curRes == $iterator->END_MAP) { $depth--; } 4597: 4598: if (ref($curRes) && $curRes->is_problem()) { 4599: my $parts = $curRes->parts(); 4600: my $title = $curRes->compTitle(); 4601: my $symbx = $curRes->symb(); 4602: $studentTable.= 4603: &Apache::loncommon::start_data_table_row(). 4604: '<td align="center" valign="top" >'.$prob. 4605: (scalar(@{$parts}) == 1 ? '' 4606: : '<br />('.&mt('[_1]parts)', 4607: scalar(@{$parts}).' ') 4608: ). 4609: '</td>'; 4610: $studentTable.='<td valign="top">'; 4611: my %form = ('CODE' => $env{'form.CODE'},); 4612: if ($env{'form.vProb'} eq 'yes' ) { 4613: $studentTable.=&show_problem($request,$symbx,$uname,$udom,1, 4614: undef,'both',\%form); 4615: } else { 4616: my $companswer = &Apache::loncommon::get_student_answers($symbx,$uname,$udom,$env{'request.course.id'},%form); 4617: $companswer =~ s|<form(.*?)>||g; 4618: $companswer =~ s|</form>||g; 4619: # while ($companswer =~ /(<a href\=\"javascript:newWindow.*?Script Vars<\/a>)/s) { #<a href="javascript:newWindow</a> 4620: # $companswer =~ s/$1/ /ms; 4621: # $request->print('match='.$1."<br />\n"); 4622: # } 4623: # $companswer =~ s|<table border=\"1\">|<table border=\"0\">|g; 4624: $studentTable.=' <b>'.$title.'</b> <br /> <b>'.&mt('Correct answer').':</b><br />'.$companswer; 4625: } 4626: 4627: my %record = &Apache::lonnet::restore($symbx,$env{'request.course.id'},$udom,$uname); 4628: 4629: if ($env{'form.lastSub'} eq 'datesub') { 4630: if ($record{'version'} eq '') { 4631: $studentTable.='<br /> <span class="LC_warning">'.&mt('No recorded submission for this problem.').'</span><br />'; 4632: } else { 4633: my %responseType = (); 4634: foreach my $partid (@{$parts}) { 4635: my @responseIds =$curRes->responseIds($partid); 4636: my @responseType =$curRes->responseType($partid); 4637: my %responseIds; 4638: for (my $i=0;$i<=$#responseIds;$i++) { 4639: $responseIds{$responseIds[$i]}=$responseType[$i]; 4640: } 4641: $responseType{$partid} = \%responseIds; 4642: } 4643: $studentTable.= &displaySubByDates($symbx,\%record,$parts,\%responseType,$checkIcon,$uname,$udom); 4644: 4645: } 4646: } elsif ($env{'form.lastSub'} eq 'all') { 4647: my $last = ($env{'form.lastSub'} eq 'last' ? 'last' : ''); 4648: $studentTable.=&Apache::loncommon::get_previous_attempt($symbx,$uname,$udom, 4649: $env{'request.course.id'}, 4650: '','.submission'); 4651: 4652: } 4653: if (&canmodify($usec)) { 4654: $studentTable.=&gradeBox_start(); 4655: foreach my $partid (@{$parts}) { 4656: $studentTable.=&gradeBox($request,$symbx,$uname,$udom,$question,$partid,\%record); 4657: $studentTable.='<input type="hidden" name="q_'.$question.'" value="'.$partid.'" />'."\n"; 4658: $question++; 4659: } 4660: $studentTable.=&gradeBox_end(); 4661: $prob++; 4662: } 4663: $studentTable.='</td></tr>'; 4664: 4665: } 4666: $curRes = $iterator->next(); 4667: } 4668: 4669: $studentTable.= 4670: '</table>'."\n". 4671: '<input type="button" value="'.&mt('Save').'" '. 4672: 'onclick="javascript:checkSubmitPage(this.form,'.$question.');" />'. 4673: '</form>'."\n"; 4674: $studentTable.=&show_grading_menu_form($symb); 4675: $request->print($studentTable); 4676: 4677: return ''; 4678: } 4679: 4680: sub displaySubByDates { 4681: my ($symb,$record,$parts,$responseType,$checkIcon,$uname,$udom) = @_; 4682: my $isCODE=0; 4683: my $isTask = ($symb =~/\.task$/); 4684: if (exists($record->{'resource.CODE'})) { $isCODE=1; } 4685: my $studentTable=&Apache::loncommon::start_data_table(). 4686: &Apache::loncommon::start_data_table_header_row(). 4687: '<th>'.&mt('Date/Time').'</th>'. 4688: ($isCODE?'<th>'.&mt('CODE').'</th>':''). 4689: '<th>'.&mt('Submission').'</th>'. 4690: '<th>'.&mt('Status').'</th>'. 4691: &Apache::loncommon::end_data_table_header_row(); 4692: my ($version); 4693: my %mark; 4694: my %orders; 4695: $mark{'correct_by_student'} = $checkIcon; 4696: if (!exists($$record{'1:timestamp'})) { 4697: return '<br /> <span class="LC_warning">'.&mt('Nothing submitted - no attempts.').'</span><br />'; 4698: } 4699: 4700: my $interaction; 4701: my $no_increment = 1; 4702: my %lastrndseed; 4703: for ($version=1;$version<=$$record{'version'};$version++) { 4704: my $timestamp = 4705: &Apache::lonlocal::locallocaltime($$record{$version.':timestamp'}); 4706: if (exists($$record{$version.':resource.0.version'})) { 4707: $interaction = $$record{$version.':resource.0.version'}; 4708: } 4709: 4710: my $where = ($isTask ? "$version:resource.$interaction" 4711: : "$version:resource"); 4712: $studentTable.=&Apache::loncommon::start_data_table_row(). 4713: '<td>'.$timestamp.'</td>'; 4714: if ($isCODE) { 4715: $studentTable.='<td>'.$record->{$version.':resource.CODE'}.'</td>'; 4716: } 4717: my @versionKeys = split(/\:/,$$record{$version.':keys'}); 4718: my @displaySub = (); 4719: foreach my $partid (@{$parts}) { 4720: my ($hidden,$type); 4721: $type = $$record{$version.':resource.'.$partid.'.type'}; 4722: if (($type eq 'anonsurvey') || ($type eq 'anonsurveycred')) { 4723: $hidden = 1; 4724: } 4725: my @matchKey = ($isTask ? sort(grep /^resource\.\d+\.\Q$partid\E\.award$/,@versionKeys) 4726: : sort(grep /^resource\.\Q$partid\E\..*?\.submission$/,@versionKeys)); 4727: 4728: # next if ($$record{"$version:resource.$partid.solved"} eq ''); 4729: my $display_part=&get_display_part($partid,$symb); 4730: foreach my $matchKey (@matchKey) { 4731: if (exists($$record{$version.':'.$matchKey}) && 4732: $$record{$version.':'.$matchKey} ne '') { 4733: 4734: my ($responseId)= ($isTask ? ($matchKey=~ /^resource\.(.*?)\.\Q$partid\E\.award$/) 4735: : ($matchKey=~ /^resource\.\Q$partid\E\.(.*?)\.submission$/)); 4736: $displaySub[0].='<span class="LC_nobreak"'; 4737: $displaySub[0].='<b>'.&mt('Part: [_1]',$display_part).'</b>' 4738: .' <span class="LC_internal_info">' 4739: .'('.&mt('Response ID: [_1]',$responseId).')' 4740: .'</span>' 4741: .' <b>'; 4742: if ($hidden) { 4743: $displaySub[0].= &mt('Anonymous Survey').'</b>'; 4744: } else { 4745: my ($trial,$rndseed,$newvariation); 4746: if ($type eq 'randomizetry') { 4747: $trial = $$record{"$where.$partid.tries"}; 4748: $rndseed = $$record{"$where.$partid.rndseed"}; 4749: } 4750: if ($$record{"$where.$partid.tries"} eq '') { 4751: $displaySub[0].=&mt('Trial not counted'); 4752: } else { 4753: $displaySub[0].=&mt('Trial: [_1]', 4754: $$record{"$where.$partid.tries"}); 4755: if ($rndseed || $lastrndseed{$partid}) { 4756: if ($rndseed ne $lastrndseed{$partid}) { 4757: $newvariation = ' ('.&mt('New variation this try').')'; 4758: } 4759: } 4760: } 4761: my $responseType=($isTask ? 'Task' 4762: : $responseType->{$partid}->{$responseId}); 4763: if (!exists($orders{$partid})) { $orders{$partid}={}; } 4764: if ((!exists($orders{$partid}->{$responseId})) || ($trial)) { 4765: $orders{$partid}->{$responseId}= 4766: &get_order($partid,$responseId,$symb,$uname,$udom, 4767: $no_increment,$type,$trial,$rndseed); 4768: } 4769: $displaySub[0].='</b>'.$newvariation.'</span>'; # /nobreak 4770: $displaySub[0].=' '. 4771: &cleanRecord($$record{$version.':'.$matchKey},$responseType,$symb,$partid,$responseId,$record,$orders{$partid}->{$responseId},"$version:",$uname,$udom,$type,$trial,$rndseed).'<br />'; 4772: } 4773: } 4774: } 4775: if (exists($$record{"$where.$partid.checkedin"})) { 4776: $displaySub[1].=&mt('Checked in by [_1] into slot [_2]', 4777: $$record{"$where.$partid.checkedin"}, 4778: $$record{"$where.$partid.checkedin.slot"}). 4779: '<br />'; 4780: } 4781: if (exists $$record{"$where.$partid.award"}) { 4782: $displaySub[1].='<b>'.&mt('Part:').'</b> '.$display_part.' '. 4783: lc($$record{"$where.$partid.award"}).' '. 4784: $mark{$$record{"$where.$partid.solved"}}. 4785: '<br />'; 4786: } 4787: if (exists $$record{"$where.$partid.regrader"}) { 4788: $displaySub[2].=$$record{"$where.$partid.regrader"}. 4789: ' (<b>'.&mt('Part').':</b> '.$display_part.')'; 4790: } elsif ($$record{"$version:resource.$partid.regrader"} =~ /\S/) { 4791: $displaySub[2].= 4792: $$record{"$version:resource.$partid.regrader"}. 4793: ' (<b>'.&mt('Part').':</b> '.$display_part.')'; 4794: } 4795: } 4796: # needed because old essay regrader has not parts info 4797: if (exists $$record{"$version:resource.regrader"}) { 4798: $displaySub[2].=$$record{"$version:resource.regrader"}; 4799: } 4800: $studentTable.='<td>'.$displaySub[0].' </td><td>'.$displaySub[1]; 4801: if ($displaySub[2]) { 4802: $studentTable.=&mt('Manually graded by [_1]',$displaySub[2]); 4803: } 4804: $studentTable.=' </td>'. 4805: &Apache::loncommon::end_data_table_row(); 4806: } 4807: $studentTable.=&Apache::loncommon::end_data_table(); 4808: return $studentTable; 4809: } 4810: 4811: sub updateGradeByPage { 4812: my ($request) = shift; 4813: 4814: my $cdom = $env{"course.$env{'request.course.id'}.domain"}; 4815: my $cnum = $env{"course.$env{'request.course.id'}.num"}; 4816: my $getsec = $env{'form.section'} eq '' ? 'all' : $env{'form.section'}; 4817: my $pageTitle = $env{'form.page'}; 4818: my ($classlist,undef,$fullname) = &getclasslist($getsec,'1'); 4819: my ($uname,$udom) = split(/:/,$env{'form.student'}); 4820: my $usec=$classlist->{$env{'form.student'}}[5]; 4821: if (!&canmodify($usec)) { 4822: $request->print('<span class="LC_warning">'.&mt('Unable to modify requested student ([_1])',$env{'form.student'}).'</span>'); 4823: $request->print(&show_grading_menu_form($env{'form.symb'})); 4824: return; 4825: } 4826: my $result='<h3><span class="LC_info"> '.$env{'form.title'}.'</span></h3>'; 4827: $result.='<h3> '.&mt('Student: ').&nameUserString(undef,$env{'form.fullname'},$uname,$udom). 4828: '</h3>'."\n"; 4829: 4830: $request->print($result); 4831: 4832: 4833: my $navmap = Apache::lonnavmaps::navmap->new(); 4834: unless (ref($navmap)) { 4835: $request->print(&navmap_errormsg()); 4836: return; 4837: } 4838: my ($mapUrl, $id, $resUrl) = &Apache::lonnet::decode_symb( $env{'form.page'}); 4839: my $map = $navmap->getResourceByUrl($resUrl); # add to navmaps 4840: if (!$map) { 4841: $request->print('<span class="LC_warning">'.&mt('Unable to grade requested sequence ([_1]).',$resUrl).'</span>'); 4842: my ($symb)=&get_symb($request); 4843: $request->print(&show_grading_menu_form($symb)); 4844: return; 4845: } 4846: my $iterator = $navmap->getIterator($map->map_start(), 4847: $map->map_finish()); 4848: 4849: my $studentTable= 4850: &Apache::loncommon::start_data_table(). 4851: &Apache::loncommon::start_data_table_header_row(). 4852: '<th align="center"> '.&mt('Prob.').' </th>'. 4853: '<th> '.&mt('Title').' </th>'. 4854: '<th> '.&mt('Previous Score').' </th>'. 4855: '<th> '.&mt('New Score').' </th>'. 4856: &Apache::loncommon::end_data_table_header_row(); 4857: 4858: $iterator->next(); # skip the first BEGIN_MAP 4859: my $curRes = $iterator->next(); # for "current resource" 4860: my ($depth,$question,$prob,$changeflag)= (1,1,1,0); 4861: while ($depth > 0) { 4862: if($curRes == $iterator->BEGIN_MAP) { $depth++; } 4863: if($curRes == $iterator->END_MAP) { $depth--; } 4864: 4865: if (ref($curRes) && $curRes->is_problem()) { 4866: my $parts = $curRes->parts(); 4867: my $title = $curRes->compTitle(); 4868: my $symbx = $curRes->symb(); 4869: $studentTable.= 4870: &Apache::loncommon::start_data_table_row(). 4871: '<td align="center" valign="top" >'.$prob. 4872: (scalar(@{$parts}) == 1 ? '' 4873: : '<br />('.&mt('[quant,_1,part]',scalar(@{$parts})) 4874: .')').'</td>'; 4875: $studentTable.='<td valign="top"> <b>'.$title.'</b> </td>'; 4876: 4877: my %newrecord=(); 4878: my @displayPts=(); 4879: my %aggregate = (); 4880: my $aggregateflag = 0; 4881: foreach my $partid (@{$parts}) { 4882: my $newpts = $env{'form.GD_BOX'.$question.'_'.$partid}; 4883: my $oldpts = $env{'form.oldpts'.$question.'_'.$partid}; 4884: 4885: my $wgt = $env{'form.WGT'.$question.'_'.$partid} != 0 ? 4886: $env{'form.WGT'.$question.'_'.$partid} : 1; 4887: my $partial = $newpts/$wgt; 4888: my $score; 4889: if ($partial > 0) { 4890: $score = 'correct_by_override'; 4891: } elsif ($newpts ne '') { #empty is taken as 0 4892: $score = 'incorrect_by_override'; 4893: } 4894: my $dropMenu = $env{'form.GD_SEL'.$question.'_'.$partid}; 4895: if ($dropMenu eq 'excused') { 4896: $partial = ''; 4897: $score = 'excused'; 4898: } elsif ($dropMenu eq 'reset status' 4899: && $env{'form.solved'.$question.'_'.$partid} ne '') { #update only if previous record exists 4900: $newrecord{'resource.'.$partid.'.tries'} = 0; 4901: $newrecord{'resource.'.$partid.'.solved'} = ''; 4902: $newrecord{'resource.'.$partid.'.award'} = ''; 4903: $newrecord{'resource.'.$partid.'.awarded'} = 0; 4904: $newrecord{'resource.'.$partid.'.regrader'} = "$env{'user.name'}:$env{'user.domain'}"; 4905: $changeflag++; 4906: $newpts = ''; 4907: 4908: my $aggtries = $env{'form.aggtries'.$question.'_'.$partid}; 4909: my $totaltries = $env{'form.totaltries'.$question.'_'.$partid}; 4910: my $solvedstatus = $env{'form.solved'.$question.'_'.$partid}; 4911: if ($aggtries > 0) { 4912: &decrement_aggs($symbx,$partid,\%aggregate,$aggtries,$totaltries,$solvedstatus); 4913: $aggregateflag = 1; 4914: } 4915: } 4916: my $display_part=&get_display_part($partid,$curRes->symb()); 4917: my $oldstatus = $env{'form.solved'.$question.'_'.$partid}; 4918: $displayPts[0].=' <b>'.&mt('Part').':</b> '.$display_part.' = '. 4919: (($oldstatus eq 'excused') ? 'excused' : $oldpts). 4920: ' <br />'; 4921: $displayPts[1].=' <b>'.&mt('Part').':</b> '.$display_part.' = '. 4922: (($score eq 'excused') ? 'excused' : $newpts). 4923: ' <br />'; 4924: $question++; 4925: next if ($dropMenu eq 'reset status' || ($newpts eq $oldpts && $score ne 'excused')); 4926: 4927: $newrecord{'resource.'.$partid.'.awarded'} = $partial if $partial ne ''; 4928: $newrecord{'resource.'.$partid.'.solved'} = $score if $score ne ''; 4929: $newrecord{'resource.'.$partid.'.regrader'} = "$env{'user.name'}:$env{'user.domain'}" 4930: if (scalar(keys(%newrecord)) > 0); 4931: 4932: $changeflag++; 4933: } 4934: if (scalar(keys(%newrecord)) > 0) { 4935: my %record = 4936: &Apache::lonnet::restore($symbx,$env{'request.course.id'}, 4937: $udom,$uname); 4938: 4939: if (&Apache::lonnet::validCODE($env{'form.CODE'})) { 4940: $newrecord{'resource.CODE'} = $env{'form.CODE'}; 4941: } elsif (&Apache::lonnet::validCODE($record{'resource.CODE'})) { 4942: $newrecord{'resource.CODE'} = ''; 4943: } 4944: &Apache::lonnet::cstore(\%newrecord,$symbx,$env{'request.course.id'}, 4945: $udom,$uname); 4946: %record = &Apache::lonnet::restore($symbx, 4947: $env{'request.course.id'}, 4948: $udom,$uname); 4949: &check_and_remove_from_queue($parts,\%record,undef,$symbx, 4950: $cdom,$cnum,$udom,$uname); 4951: } 4952: 4953: if ($aggregateflag) { 4954: &Apache::lonnet::cinc('nohist_resourcetracker',\%aggregate, 4955: $env{'course.'.$env{'request.course.id'}.'.domain'}, 4956: $env{'course.'.$env{'request.course.id'}.'.num'}); 4957: } 4958: 4959: $studentTable.='<td valign="top">'.$displayPts[0].'</td>'. 4960: '<td valign="top">'.$displayPts[1].'</td>'. 4961: &Apache::loncommon::end_data_table_row(); 4962: 4963: $prob++; 4964: } 4965: $curRes = $iterator->next(); 4966: } 4967: 4968: $studentTable.=&Apache::loncommon::end_data_table(); 4969: $studentTable.=&show_grading_menu_form($env{'form.symb'}); 4970: my $grademsg=($changeflag == 0 ? &mt('No score was changed or updated.') : 4971: &mt('The scores were changed for [quant,_1,problem].', 4972: $changeflag)); 4973: $request->print($grademsg.$studentTable); 4974: 4975: return ''; 4976: } 4977: 4978: #-------- end of section for handling grading by page/sequence --------- 4979: # 4980: #------------------------------------------------------------------- 4981: 4982: #-------------------- Bubblesheet (Scantron) Grading ------------------- 4983: # 4984: #------ start of section for handling grading by page/sequence --------- 4985: 4986: =pod 4987: 4988: =head1 Bubble sheet grading routines 4989: 4990: For this documentation: 4991: 4992: 'scanline' refers to the full line of characters 4993: from the file that we are parsing that represents one entire sheet 4994: 4995: 'bubble line' refers to the data 4996: representing the line of bubbles that are on the physical bubblesheet 4997: 4998: 4999: The overall process is that a scanned in bubblesheet data is uploaded 5000: into a course. When a user wants to grade, they select a 5001: sequence/folder of resources, a file of bubblesheet info, and pick 5002: one of the predefined configurations for what each scanline looks 5003: like. 5004: 5005: Next each scanline is checked for any errors of either 'missing 5006: bubbles' (it's an error because it may have been mis-scanned 5007: because too light bubbling), 'double bubble' (each bubble line should 5008: have no more that one letter picked), invalid or duplicated CODE, 5009: invalid student/employee ID 5010: 5011: If the CODE option is used that determines the randomization of the 5012: homework problems, either way the student/employee ID is looked up into a 5013: username:domain. 5014: 5015: During the validation phase the instructor can choose to skip scanlines. 5016: 5017: After the validation phase, there are now 3 bubblesheet files 5018: 5019: scantron_original_filename (unmodified original file) 5020: scantron_corrected_filename (file where the corrected information has replaced the original information) 5021: scantron_skipped_filename (contains the exact text of scanlines that where skipped) 5022: 5023: Also there is a separate hash nohist_scantrondata that contains extra 5024: correction information that isn't representable in the bubblesheet 5025: file (see &scantron_getfile() for more information) 5026: 5027: After all scanlines are either valid, marked as valid or skipped, then 5028: foreach line foreach problem in the picked sequence, an ssi request is 5029: made that simulates a user submitting their selected letter(s) against 5030: the homework problem. 5031: 5032: =over 4 5033: 5034: 5035: 5036: =item defaultFormData 5037: 5038: Returns html hidden inputs used to hold context/default values. 5039: 5040: Arguments: 5041: $symb - $symb of the current resource 5042: 5043: =cut 5044: 5045: sub defaultFormData { 5046: my ($symb)=@_; 5047: return '<input type="hidden" name="symb" value="'.&Apache::lonenc::check_encrypt($symb).'" />'."\n". 5048: '<input type="hidden" name="saveState" value="'.$env{'form.saveState'}.'" />'."\n". 5049: '<input type="hidden" name="probTitle" value="'.$env{'form.probTitle'}.'" />'."\n"; 5050: } 5051: 5052: 5053: =pod 5054: 5055: =item getSequenceDropDown 5056: 5057: Return html dropdown of possible sequences to grade 5058: 5059: Arguments: 5060: $symb - $symb of the current resource 5061: $map_error - ref to scalar which will container error if 5062: $navmap object is unavailable in &getSymbMap(). 5063: 5064: =cut 5065: 5066: sub getSequenceDropDown { 5067: my ($symb,$map_error)=@_; 5068: my $result='<select name="selectpage">'."\n"; 5069: my ($titles,$symbx) = &getSymbMap($map_error); 5070: if (ref($map_error)) { 5071: return if ($$map_error); 5072: } 5073: my ($curpage)=&Apache::lonnet::decode_symb($symb); 5074: my $ctr=0; 5075: foreach (@$titles) { 5076: my ($minder,$showtitle) = ($_ =~ /(\d+)\.(.*)/); 5077: $result.='<option value="'.$$symbx{$_}.'" '. 5078: ($$symbx{$_} =~ /$curpage$/ ? 'selected="selected"' : ''). 5079: '>'.$showtitle.'</option>'."\n"; 5080: $ctr++; 5081: } 5082: $result.= '</select>'; 5083: return $result; 5084: } 5085: 5086: my %bubble_lines_per_response; # no. bubble lines for each response. 5087: # key is zero-based index - 0, 1, 2 ... 5088: 5089: my %first_bubble_line; # First bubble line no. for each bubble. 5090: 5091: my %subdivided_bubble_lines; # no. bubble lines for optionresponse, 5092: # matchresponse or rankresponse, where 5093: # an individual response can have multiple 5094: # lines 5095: 5096: my %responsetype_per_response; # responsetype for each response 5097: 5098: # Save and restore the bubble lines array to the form env. 5099: 5100: 5101: sub save_bubble_lines { 5102: foreach my $line (keys(%bubble_lines_per_response)) { 5103: $env{"form.scantron.bubblelines.$line"} = $bubble_lines_per_response{$line}; 5104: $env{"form.scantron.first_bubble_line.$line"} = 5105: $first_bubble_line{$line}; 5106: $env{"form.scantron.sub_bubblelines.$line"} = 5107: $subdivided_bubble_lines{$line}; 5108: $env{"form.scantron.responsetype.$line"} = 5109: $responsetype_per_response{$line}; 5110: } 5111: } 5112: 5113: 5114: sub restore_bubble_lines { 5115: my $line = 0; 5116: %bubble_lines_per_response = (); 5117: while ($env{"form.scantron.bubblelines.$line"}) { 5118: my $value = $env{"form.scantron.bubblelines.$line"}; 5119: $bubble_lines_per_response{$line} = $value; 5120: $first_bubble_line{$line} = 5121: $env{"form.scantron.first_bubble_line.$line"}; 5122: $subdivided_bubble_lines{$line} = 5123: $env{"form.scantron.sub_bubblelines.$line"}; 5124: $responsetype_per_response{$line} = 5125: $env{"form.scantron.responsetype.$line"}; 5126: $line++; 5127: } 5128: } 5129: 5130: # Given the parsed scanline, get the response for 5131: # 'answer' number n: 5132: 5133: sub get_response_bubbles { 5134: my ($parsed_line, $response) = @_; 5135: 5136: my $bubble_line = $first_bubble_line{$response-1} +1; 5137: my $bubble_lines= $bubble_lines_per_response{$response-1}; 5138: 5139: my $selected = ""; 5140: 5141: for (my $bline = 0; $bline < $bubble_lines; $bline++) { 5142: $selected .= $$parsed_line{"scantron.$bubble_line.answer"}.":"; 5143: $bubble_line++; 5144: } 5145: return $selected; 5146: } 5147: 5148: =pod 5149: 5150: =item scantron_filenames 5151: 5152: Returns a list of the scantron files in the current course 5153: 5154: =cut 5155: 5156: sub scantron_filenames { 5157: my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'}; 5158: my $cname=$env{'course.'.$env{'request.course.id'}.'.num'}; 5159: my $getpropath = 1; 5160: my @files=&Apache::lonnet::dirlist('userfiles',$cdom,$cname, 5161: $getpropath); 5162: my @possiblenames; 5163: foreach my $filename (sort(@files)) { 5164: ($filename)=split(/&/,$filename); 5165: if ($filename!~/^scantron_orig_/) { next ; } 5166: $filename=~s/^scantron_orig_//; 5167: push(@possiblenames,$filename); 5168: } 5169: return @possiblenames; 5170: } 5171: 5172: =pod 5173: 5174: =item scantron_uploads 5175: 5176: Returns html drop-down list of scantron files in current course. 5177: 5178: Arguments: 5179: $file2grade - filename to set as selected in the dropdown 5180: 5181: =cut 5182: 5183: sub scantron_uploads { 5184: my ($file2grade) = @_; 5185: my $result= '<select name="scantron_selectfile">'; 5186: $result.="<option></option>"; 5187: foreach my $filename (sort(&scantron_filenames())) { 5188: $result.="<option".($filename eq $file2grade ? ' selected="selected"':'').">$filename</option>\n"; 5189: } 5190: $result.="</select>"; 5191: return $result; 5192: } 5193: 5194: =pod 5195: 5196: =item scantron_scantab 5197: 5198: Returns html drop down of the scantron formats in the scantronformat.tab 5199: file. 5200: 5201: =cut 5202: 5203: sub scantron_scantab { 5204: my $result='<select name="scantron_format">'."\n"; 5205: $result.='<option></option>'."\n"; 5206: my @lines = &get_scantronformat_file(); 5207: if (@lines > 0) { 5208: foreach my $line (@lines) { 5209: next if (($line =~ /^\#/) || ($line eq '')); 5210: my ($name,$descrip)=split(/:/,$line); 5211: $result.='<option value="'.$name.'">'.$descrip.'</option>'."\n"; 5212: } 5213: } 5214: $result.='</select>'."\n"; 5215: return $result; 5216: } 5217: 5218: =pod 5219: 5220: =item get_scantronformat_file 5221: 5222: Returns an array containing lines from the scantron format file for 5223: the domain of the course. 5224: 5225: If a url for a custom.tab file is listed in domain's configuration.db, 5226: lines are from this file. 5227: 5228: Otherwise, if a default.tab has been published in RES space by the 5229: domainconfig user, lines are from this file. 5230: 5231: Otherwise, fall back to getting lines from the legacy file on the 5232: local server: /home/httpd/lonTabs/default_scantronformat.tab 5233: 5234: =cut 5235: 5236: sub get_scantronformat_file { 5237: my $cdom= $env{'course.'.$env{'request.course.id'}.'.domain'}; 5238: my %domconfig = &Apache::lonnet::get_dom('configuration',['scantron'],$cdom); 5239: my $gottab = 0; 5240: my @lines; 5241: if (ref($domconfig{'scantron'}) eq 'HASH') { 5242: if ($domconfig{'scantron'}{'scantronformat'} ne '') { 5243: my $formatfile = &Apache::lonnet::getfile($Apache::lonnet::perlvar{'lonDocRoot'}.$domconfig{'scantron'}{'scantronformat'}); 5244: if ($formatfile ne '-1') { 5245: @lines = split("\n",$formatfile,-1); 5246: $gottab = 1; 5247: } 5248: } 5249: } 5250: if (!$gottab) { 5251: my $confname = $cdom.'-domainconfig'; 5252: my $default = $Apache::lonnet::perlvar{'lonDocRoot'}.'/res/'.$cdom.'/'.$confname.'/default.tab'; 5253: my $formatfile = &Apache::lonnet::getfile($default); 5254: if ($formatfile ne '-1') { 5255: @lines = split("\n",$formatfile,-1); 5256: $gottab = 1; 5257: } 5258: } 5259: if (!$gottab) { 5260: my @domains = &Apache::lonnet::current_machine_domains(); 5261: if (grep(/^\Q$cdom\E$/,@domains)) { 5262: my $fh=Apache::File->new($Apache::lonnet::perlvar{'lonTabDir'}.'/scantronformat.tab'); 5263: @lines = <$fh>; 5264: close($fh); 5265: } else { 5266: my $fh=Apache::File->new($Apache::lonnet::perlvar{'lonTabDir'}.'/default_scantronformat.tab'); 5267: @lines = <$fh>; 5268: close($fh); 5269: } 5270: } 5271: return @lines; 5272: } 5273: 5274: =pod 5275: 5276: =item scantron_CODElist 5277: 5278: Returns html drop down of the saved CODE lists from current course, 5279: generated from earlier printings. 5280: 5281: =cut 5282: 5283: sub scantron_CODElist { 5284: my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; 5285: my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'}; 5286: my @names=&Apache::lonnet::getkeys('CODEs',$cdom,$cnum); 5287: my $namechoice='<option></option>'; 5288: foreach my $name (sort {uc($a) cmp uc($b)} @names) { 5289: if ($name =~ /^error: 2 /) { next; } 5290: if ($name =~ /^type\0/) { next; } 5291: $namechoice.='<option value="'.$name.'">'.$name.'</option>'; 5292: } 5293: $namechoice='<select name="scantron_CODElist">'.$namechoice.'</select>'; 5294: return $namechoice; 5295: } 5296: 5297: =pod 5298: 5299: =item scantron_CODEunique 5300: 5301: Returns the html for "Each CODE to be used once" radio. 5302: 5303: =cut 5304: 5305: sub scantron_CODEunique { 5306: my $result='<span class="LC_nobreak"> 5307: <label><input type="radio" name="scantron_CODEunique" 5308: value="yes" checked="checked" />'.&mt('Yes').' </label> 5309: </span> 5310: <span class="LC_nobreak"> 5311: <label><input type="radio" name="scantron_CODEunique" 5312: value="no" />'.&mt('No').' </label> 5313: </span>'; 5314: return $result; 5315: } 5316: 5317: =pod 5318: 5319: =item scantron_selectphase 5320: 5321: Generates the initial screen to start the bubblesheet process. 5322: Allows for - starting a grading run. 5323: - downloading existing scan data (original, corrected 5324: or skipped info) 5325: 5326: - uploading new scan data 5327: 5328: Arguments: 5329: $r - The Apache request object 5330: $file2grade - name of the file that contain the scanned data to score 5331: 5332: =cut 5333: 5334: sub scantron_selectphase { 5335: my ($r,$file2grade) = @_; 5336: my ($symb)=&get_symb($r); 5337: if (!$symb) {return '';} 5338: my $map_error; 5339: my $sequence_selector=&getSequenceDropDown($symb,\$map_error); 5340: if ($map_error) { 5341: $r->print('<br />'.&navmap_errormsg().'<br />'); 5342: return; 5343: } 5344: my $default_form_data=&defaultFormData($symb); 5345: my $grading_menu_button=&show_grading_menu_form($symb); 5346: my $file_selector=&scantron_uploads($file2grade); 5347: my $format_selector=&scantron_scantab(); 5348: my $CODE_selector=&scantron_CODElist(); 5349: my $CODE_unique=&scantron_CODEunique(); 5350: my $result; 5351: 5352: $ssi_error = 0; 5353: 5354: if (&Apache::lonnet::allowed('usc',$env{'request.role.domain'}) || 5355: &Apache::lonnet::allowed('usc',$env{'request.course.id'})) { 5356: 5357: # Chunk of form to prompt for a scantron file upload. 5358: 5359: $r->print(' 5360: <br /> 5361: '.&Apache::loncommon::start_data_table('LC_scantron_action').' 5362: '.&Apache::loncommon::start_data_table_header_row().' 5363: <th> 5364: '.&mt('Specify a bubblesheet data file to upload.').' 5365: </th> 5366: '.&Apache::loncommon::end_data_table_header_row().' 5367: '.&Apache::loncommon::start_data_table_row().' 5368: <td> 5369: '); 5370: my $default_form_data=&defaultFormData(&get_symb($r,1)); 5371: my $cdom= $env{'course.'.$env{'request.course.id'}.'.domain'}; 5372: my $cnum= $env{'course.'.$env{'request.course.id'}.'.num'}; 5373: $r->print(' 5374: <script type="text/javascript" language="javascript"> 5375: function checkUpload(formname) { 5376: if (formname.upfile.value == "") { 5377: alert("'.&mt('Please use the browse button to select a file from your local directory.').'"); 5378: return false; 5379: } 5380: formname.submit(); 5381: } 5382: </script> 5383: 5384: <form enctype="multipart/form-data" action="/adm/grades" name="rules" method="post"> 5385: '.$default_form_data.' 5386: <input name="courseid" type="hidden" value="'.$cnum.'" /> 5387: <input name="domainid" type="hidden" value="'.$cdom.'" /> 5388: <input name="command" value="scantronupload_save" type="hidden" /> 5389: '.&mt('File to upload: [_1]','<input type="file" name="upfile" size="50" />').' 5390: <br /> 5391: <input type="button" onclick="javascript:checkUpload(this.form);" value="'.&mt('Upload Bubblesheet Data').'" /> 5392: </form> 5393: '); 5394: 5395: $r->print(' 5396: </td> 5397: '.&Apache::loncommon::end_data_table_row().' 5398: '.&Apache::loncommon::end_data_table().' 5399: '); 5400: } 5401: 5402: # Chunk of form to prompt for a file to grade and how: 5403: 5404: $result.= ' 5405: <br /> 5406: <form method="post" enctype="multipart/form-data" action="/adm/grades" name="scantron_process"> 5407: <input type="hidden" name="command" value="scantron_warning" /> 5408: '.$default_form_data.' 5409: '.&Apache::loncommon::start_data_table('LC_scantron_action').' 5410: '.&Apache::loncommon::start_data_table_header_row().' 5411: <th colspan="2"> 5412: '.&mt('Specify file and which Folder/Sequence to grade').' 5413: </th> 5414: '.&Apache::loncommon::end_data_table_header_row().' 5415: '.&Apache::loncommon::start_data_table_row().' 5416: <td> '.&mt('Sequence to grade:').' </td><td> '.$sequence_selector.' </td> 5417: '.&Apache::loncommon::end_data_table_row().' 5418: '.&Apache::loncommon::start_data_table_row().' 5419: <td> '.&mt('Filename of bubblesheet data file:').' </td><td> '.$file_selector.' </td> 5420: '.&Apache::loncommon::end_data_table_row().' 5421: '.&Apache::loncommon::start_data_table_row().' 5422: <td> '.&mt('Format of bubblesheet data file:').' </td><td> '.$format_selector.' </td> 5423: '.&Apache::loncommon::end_data_table_row().' 5424: '.&Apache::loncommon::start_data_table_row().' 5425: <td> '.&mt('Saved CODEs to validate against:').' </td><td> '.$CODE_selector.' </td> 5426: '.&Apache::loncommon::end_data_table_row().' 5427: '.&Apache::loncommon::start_data_table_row().' 5428: <td> '.&mt('Each CODE is only to be used once:').'</td><td> '.$CODE_unique.' </td> 5429: '.&Apache::loncommon::end_data_table_row().' 5430: '.&Apache::loncommon::start_data_table_row().' 5431: <td> '.&mt('Options:').' </td> 5432: <td> 5433: <label><input type="checkbox" name="scantron_options_redo" value="redo_skipped"/> '.&mt('Do only previously skipped records').'</label> <br /> 5434: <label><input type="checkbox" name="scantron_options_ignore" value="ignore_corrections"/> '.&mt('Remove all existing corrections').'</label> <br /> 5435: <label><input type="checkbox" name="scantron_options_hidden" value="ignore_hidden"/> '.&mt('Skip hidden resources when grading').'</label> 5436: </td> 5437: '.&Apache::loncommon::end_data_table_row().' 5438: '.&Apache::loncommon::start_data_table_row().' 5439: <td colspan="2"> 5440: <input type="submit" value="'.&mt('Grading: Validate Bubblesheet Records').'" /> 5441: </td> 5442: '.&Apache::loncommon::end_data_table_row().' 5443: '.&Apache::loncommon::end_data_table().' 5444: </form> 5445: '; 5446: 5447: $r->print($result); 5448: 5449: # Chunk of the form that prompts to view a scoring office file, 5450: # corrected file, skipped records in a file. 5451: 5452: $r->print(' 5453: <br /> 5454: <form action="/adm/grades" name="scantron_download"> 5455: '.$default_form_data.' 5456: <input type="hidden" name="command" value="scantron_download" /> 5457: '.&Apache::loncommon::start_data_table('LC_scantron_action').' 5458: '.&Apache::loncommon::start_data_table_header_row().' 5459: <th> 5460: '.&mt('Download a scoring office file').' 5461: </th> 5462: '.&Apache::loncommon::end_data_table_header_row().' 5463: '.&Apache::loncommon::start_data_table_row().' 5464: <td> '.&mt('Filename of scoring office file: [_1]',$file_selector).' 5465: <br /> 5466: <input type="submit" value="'.&mt('Download: Show List of Associated Files').'" /> 5467: '.&Apache::loncommon::end_data_table_row().' 5468: '.&Apache::loncommon::end_data_table().' 5469: </form> 5470: <br /> 5471: '); 5472: 5473: &Apache::lonpickcode::code_list($r,2); 5474: 5475: $r->print('<br /><form method="post" name="checkscantron">'. 5476: $default_form_data."\n". 5477: &Apache::loncommon::start_data_table('LC_scantron_action')."\n". 5478: &Apache::loncommon::start_data_table_header_row()."\n". 5479: '<th colspan="2"> 5480: '.&mt('Review bubblesheet data and submissions for a previously graded folder/sequence')."\n". 5481: '</th>'."\n". 5482: &Apache::loncommon::end_data_table_header_row()."\n". 5483: &Apache::loncommon::start_data_table_row()."\n". 5484: '<td> '.&mt('Graded folder/sequence:').' </td>'."\n". 5485: '<td> '.$sequence_selector.' </td>'. 5486: &Apache::loncommon::end_data_table_row()."\n". 5487: &Apache::loncommon::start_data_table_row()."\n". 5488: '<td> '.&mt('Filename of scoring office file:').' </td>'."\n". 5489: '<td> '.$file_selector.' </td>'."\n". 5490: &Apache::loncommon::end_data_table_row()."\n". 5491: &Apache::loncommon::start_data_table_row()."\n". 5492: '<td> '.&mt('Format of data file:').' </td>'."\n". 5493: '<td> '.$format_selector.' </td>'."\n". 5494: &Apache::loncommon::end_data_table_row()."\n". 5495: &Apache::loncommon::start_data_table_row()."\n". 5496: '<td> '.&mt('Options').' </td>'."\n". 5497: '<td> <label><input type="checkbox" name="scantron_options_hidden" value="ignore_hidden"/> '.&mt('Skip hidden resources').'</label></td>'. 5498: &Apache::loncommon::end_data_table_row()."\n". 5499: &Apache::loncommon::start_data_table_row()."\n". 5500: '<td colspan="2">'."\n". 5501: '<input type="hidden" name="command" value="checksubmissions" />'."\n". 5502: '<input type="submit" value="'.&mt('Review Bubblesheet Data and Submission Records').'" />'."\n". 5503: '</td>'."\n". 5504: &Apache::loncommon::end_data_table_row()."\n". 5505: &Apache::loncommon::end_data_table()."\n". 5506: '</form><br />'); 5507: $r->print($grading_menu_button); 5508: return; 5509: } 5510: 5511: =pod 5512: 5513: =item get_scantron_config 5514: 5515: Parse and return the scantron configuration line selected as a 5516: hash of configuration file fields. 5517: 5518: Arguments: 5519: which - the name of the configuration to parse from the file. 5520: 5521: 5522: Returns: 5523: If the named configuration is not in the file, an empty 5524: hash is returned. 5525: a hash with the fields 5526: name - internal name for the this configuration setup 5527: description - text to display to operator that describes this config 5528: CODElocation - if 0 or the string 'none' 5529: - no CODE exists for this config 5530: if -1 || the string 'letter' 5531: - a CODE exists for this config and is 5532: a string of letters 5533: Unsupported value (but planned for future support) 5534: if a positive integer 5535: - The CODE exists as the first n items from 5536: the question section of the form 5537: if the string 'number' 5538: - The CODE exists for this config and is 5539: a string of numbers 5540: CODEstart - (only matter if a CODE exists) column in the line where 5541: the CODE starts 5542: CODElength - length of the CODE 5543: IDstart - column where the student/employee ID starts 5544: IDlength - length of the student/employee ID info 5545: Qstart - column where the information from the bubbled 5546: 'questions' start 5547: Qlength - number of columns comprising a single bubble line from 5548: the sheet. (usually either 1 or 10) 5549: Qon - either a single character representing the character used 5550: to signal a bubble was chosen in the positional setup, or 5551: the string 'letter' if the letter of the chosen bubble is 5552: in the final, or 'number' if a number representing the 5553: chosen bubble is in the file (1->A 0->J) 5554: Qoff - the character used to represent that a bubble was 5555: left blank 5556: PaperID - if the scanning process generates a unique number for each 5557: sheet scanned the column that this ID number starts in 5558: PaperIDlength - number of columns that comprise the unique ID number 5559: for the sheet of paper 5560: FirstName - column that the first name starts in 5561: FirstNameLength - number of columns that the first name spans 5562: 5563: LastName - column that the last name starts in 5564: LastNameLength - number of columns that the last name spans 5565: 5566: =cut 5567: 5568: sub get_scantron_config { 5569: my ($which) = @_; 5570: my @lines = &get_scantronformat_file(); 5571: my %config; 5572: #FIXME probably should move to XML it has already gotten a bit much now 5573: foreach my $line (@lines) { 5574: my ($name,$descrip)=split(/:/,$line); 5575: if ($name ne $which ) { next; } 5576: chomp($line); 5577: my @config=split(/:/,$line); 5578: $config{'name'}=$config[0]; 5579: $config{'description'}=$config[1]; 5580: $config{'CODElocation'}=$config[2]; 5581: $config{'CODEstart'}=$config[3]; 5582: $config{'CODElength'}=$config[4]; 5583: $config{'IDstart'}=$config[5]; 5584: $config{'IDlength'}=$config[6]; 5585: $config{'Qstart'}=$config[7]; 5586: $config{'Qlength'}=$config[8]; 5587: $config{'Qoff'}=$config[9]; 5588: $config{'Qon'}=$config[10]; 5589: $config{'PaperID'}=$config[11]; 5590: $config{'PaperIDlength'}=$config[12]; 5591: $config{'FirstName'}=$config[13]; 5592: $config{'FirstNamelength'}=$config[14]; 5593: $config{'LastName'}=$config[15]; 5594: $config{'LastNamelength'}=$config[16]; 5595: last; 5596: } 5597: return %config; 5598: } 5599: 5600: =pod 5601: 5602: =item username_to_idmap 5603: 5604: creates a hash keyed by student/employee ID with values of the corresponding 5605: student username:domain. 5606: 5607: Arguments: 5608: 5609: $classlist - reference to the class list hash. This is a hash 5610: keyed by student name:domain whose elements are references 5611: to arrays containing various chunks of information 5612: about the student. (See loncoursedata for more info). 5613: 5614: Returns 5615: %idmap - the constructed hash 5616: 5617: =cut 5618: 5619: sub username_to_idmap { 5620: my ($classlist)= @_; 5621: my %idmap; 5622: foreach my $student (keys(%$classlist)) { 5623: $idmap{$classlist->{$student}->[&Apache::loncoursedata::CL_ID]}= 5624: $student; 5625: } 5626: return %idmap; 5627: } 5628: 5629: =pod 5630: 5631: =item scantron_fixup_scanline 5632: 5633: Process a requested correction to a scanline. 5634: 5635: Arguments: 5636: $scantron_config - hash from &get_scantron_config() 5637: $scan_data - hash of correction information 5638: (see &scantron_getfile()) 5639: $line - existing scanline 5640: $whichline - line number of the passed in scanline 5641: $field - type of change to process 5642: (either 5643: 'ID' -> correct the student/employee ID 5644: 'CODE' -> correct the CODE 5645: 'answer' -> fixup the submitted answers) 5646: 5647: $args - hash of additional info, 5648: - 'ID' 5649: 'newid' -> studentID to use in replacement 5650: of existing one 5651: - 'CODE' 5652: 'CODE_ignore_dup' - set to true if duplicates 5653: should be ignored. 5654: 'CODE' - is new code or 'use_unfound' 5655: if the existing unfound code should 5656: be used as is 5657: - 'answer' 5658: 'response' - new answer or 'none' if blank 5659: 'question' - the bubble line to change 5660: 'questionnum' - the question identifier, 5661: may include subquestion. 5662: 5663: Returns: 5664: $line - the modified scanline 5665: 5666: Side effects: 5667: $scan_data - may be updated 5668: 5669: =cut 5670: 5671: 5672: sub scantron_fixup_scanline { 5673: my ($scantron_config,$scan_data,$line,$whichline,$field,$args)=@_; 5674: if ($field eq 'ID') { 5675: if (length($args->{'newid'}) > $$scantron_config{'IDlength'}) { 5676: return ($line,1,'New value too large'); 5677: } 5678: if (length($args->{'newid'}) < $$scantron_config{'IDlength'}) { 5679: $args->{'newid'}=sprintf('%-'.$$scantron_config{'IDlength'}.'s', 5680: $args->{'newid'}); 5681: } 5682: substr($line,$$scantron_config{'IDstart'}-1, 5683: $$scantron_config{'IDlength'})=$args->{'newid'}; 5684: if ($args->{'newid'}=~/^\s*$/) { 5685: &scan_data($scan_data,"$whichline.user", 5686: $args->{'username'}.':'.$args->{'domain'}); 5687: } 5688: } elsif ($field eq 'CODE') { 5689: if ($args->{'CODE_ignore_dup'}) { 5690: &scan_data($scan_data,"$whichline.CODE_ignore_dup",'1'); 5691: } 5692: &scan_data($scan_data,"$whichline.useCODE",'1'); 5693: if ($args->{'CODE'} ne 'use_unfound') { 5694: if (length($args->{'CODE'}) > $$scantron_config{'CODElength'}) { 5695: return ($line,1,'New CODE value too large'); 5696: } 5697: if (length($args->{'CODE'}) < $$scantron_config{'CODElength'}) { 5698: $args->{'CODE'}=sprintf('%-'.$$scantron_config{'CODElength'}.'s',$args->{'CODE'}); 5699: } 5700: substr($line,$$scantron_config{'CODEstart'}-1, 5701: $$scantron_config{'CODElength'})=$args->{'CODE'}; 5702: } 5703: } elsif ($field eq 'answer') { 5704: my $length=$scantron_config->{'Qlength'}; 5705: my $off=$scantron_config->{'Qoff'}; 5706: my $on=$scantron_config->{'Qon'}; 5707: my $answer=${off}x$length; 5708: if ($args->{'response'} eq 'none') { 5709: &scan_data($scan_data, 5710: "$whichline.no_bubble.".$args->{'questionnum'},'1'); 5711: } else { 5712: if ($on eq 'letter') { 5713: my @alphabet=('A'..'Z'); 5714: $answer=$alphabet[$args->{'response'}]; 5715: } elsif ($on eq 'number') { 5716: $answer=$args->{'response'}+1; 5717: if ($answer == 10) { $answer = '0'; } 5718: } else { 5719: substr($answer,$args->{'response'},1)=$on; 5720: } 5721: &scan_data($scan_data, 5722: "$whichline.no_bubble.".$args->{'questionnum'},undef,'1'); 5723: } 5724: my $where=$length*($args->{'question'}-1)+$scantron_config->{'Qstart'}; 5725: substr($line,$where-1,$length)=$answer; 5726: } 5727: return $line; 5728: } 5729: 5730: =pod 5731: 5732: =item scan_data 5733: 5734: Edit or look up an item in the scan_data hash. 5735: 5736: Arguments: 5737: $scan_data - The hash (see scantron_getfile) 5738: $key - shorthand of the key to edit (actual key is 5739: scantronfilename_key). 5740: $data - New value of the hash entry. 5741: $delete - If true, the entry is removed from the hash. 5742: 5743: Returns: 5744: The new value of the hash table field (undefined if deleted). 5745: 5746: =cut 5747: 5748: 5749: sub scan_data { 5750: my ($scan_data,$key,$value,$delete)=@_; 5751: my $filename=$env{'form.scantron_selectfile'}; 5752: if (defined($value)) { 5753: $scan_data->{$filename.'_'.$key} = $value; 5754: } 5755: if ($delete) { delete($scan_data->{$filename.'_'.$key}); } 5756: return $scan_data->{$filename.'_'.$key}; 5757: } 5758: 5759: # ----- These first few routines are general use routines.---- 5760: 5761: # Return the number of occurences of a pattern in a string. 5762: 5763: sub occurence_count { 5764: my ($string, $pattern) = @_; 5765: 5766: my @matches = ($string =~ /$pattern/g); 5767: 5768: return scalar(@matches); 5769: } 5770: 5771: 5772: # Take a string known to have digits and convert all the 5773: # digits into letters in the range J,A..I. 5774: 5775: sub digits_to_letters { 5776: my ($input) = @_; 5777: 5778: my @alphabet = ('J', 'A'..'I'); 5779: 5780: my @input = split(//, $input); 5781: my $output =''; 5782: for (my $i = 0; $i < scalar(@input); $i++) { 5783: if ($input[$i] =~ /\d/) { 5784: $output .= $alphabet[$input[$i]]; 5785: } else { 5786: $output .= $input[$i]; 5787: } 5788: } 5789: return $output; 5790: } 5791: 5792: =pod 5793: 5794: =item scantron_parse_scanline 5795: 5796: Decodes a scanline from the selected scantron file 5797: 5798: Arguments: 5799: line - The text of the scantron file line to process 5800: whichline - Line number 5801: scantron_config - Hash describing the format of the scantron lines. 5802: scan_data - Hash of extra information about the scanline 5803: (see scantron_getfile for more information) 5804: just_header - True if should not process question answers but only 5805: the stuff to the left of the answers. 5806: Returns: 5807: Hash containing the result of parsing the scanline 5808: 5809: Keys are all proceeded by the string 'scantron.' 5810: 5811: CODE - the CODE in use for this scanline 5812: useCODE - 1 if the CODE is invalid but it usage has been forced 5813: by the operator 5814: CODE_ignore_dup - 1 if the CODE is a duplicated use when unique 5815: CODEs were selected, but the usage has been 5816: forced by the operator 5817: ID - student/employee ID 5818: PaperID - if used, the ID number printed on the sheet when the 5819: paper was scanned 5820: FirstName - first name from the sheet 5821: LastName - last name from the sheet 5822: 5823: if just_header was not true these key may also exist 5824: 5825: missingerror - a list of bubble ranges that are considered to be answers 5826: to a single question that don't have any bubbles filled in. 5827: Of the form questionnumber:firstbubblenumber:count. 5828: doubleerror - a list of bubble ranges that are considered to be answers 5829: to a single question that have more than one bubble filled in. 5830: Of the form questionnumber::firstbubblenumber:count 5831: 5832: In the above, count is the number of bubble responses in the 5833: input line needed to represent the possible answers to the question. 5834: e.g. a radioresponse with 15 choices in an answer sheet with 10 choices 5835: per line would have count = 2. 5836: 5837: maxquest - the number of the last bubble line that was parsed 5838: 5839: (<number> starts at 1) 5840: <number>.answer - zero or more letters representing the selected 5841: letters from the scanline for the bubble line 5842: <number>. 5843: if blank there was either no bubble or there where 5844: multiple bubbles, (consult the keys missingerror and 5845: doubleerror if this is an error condition) 5846: 5847: =cut 5848: 5849: sub scantron_parse_scanline { 5850: my ($line,$whichline,$scantron_config,$scan_data,$just_header)=@_; 5851: 5852: my %record; 5853: my $lastpos = $env{'form.scantron_maxbubble'}*$$scantron_config{'Qlength'}; 5854: my $questions=substr($line,$$scantron_config{'Qstart'}-1,$lastpos); # Answers 5855: my $data=substr($line,0,$$scantron_config{'Qstart'}-1); # earlier stuff 5856: if (!($$scantron_config{'CODElocation'} eq 0 || 5857: $$scantron_config{'CODElocation'} eq 'none')) { 5858: if ($$scantron_config{'CODElocation'} < 0 || 5859: $$scantron_config{'CODElocation'} eq 'letter' || 5860: $$scantron_config{'CODElocation'} eq 'number') { 5861: $record{'scantron.CODE'}=substr($data, 5862: $$scantron_config{'CODEstart'}-1, 5863: $$scantron_config{'CODElength'}); 5864: if (&scan_data($scan_data,"$whichline.useCODE")) { 5865: $record{'scantron.useCODE'}=1; 5866: } 5867: if (&scan_data($scan_data,"$whichline.CODE_ignore_dup")) { 5868: $record{'scantron.CODE_ignore_dup'}=1; 5869: } 5870: } else { 5871: #FIXME interpret first N questions 5872: } 5873: } 5874: $record{'scantron.ID'}=substr($data,$$scantron_config{'IDstart'}-1, 5875: $$scantron_config{'IDlength'}); 5876: $record{'scantron.PaperID'}= 5877: substr($data,$$scantron_config{'PaperID'}-1, 5878: $$scantron_config{'PaperIDlength'}); 5879: $record{'scantron.FirstName'}= 5880: substr($data,$$scantron_config{'FirstName'}-1, 5881: $$scantron_config{'FirstNamelength'}); 5882: $record{'scantron.LastName'}= 5883: substr($data,$$scantron_config{'LastName'}-1, 5884: $$scantron_config{'LastNamelength'}); 5885: if ($just_header) { return \%record; } 5886: 5887: my @alphabet=('A'..'Z'); 5888: my $questnum=0; 5889: my $ansnum =1; # Multiple 'answer lines'/question. 5890: 5891: chomp($questions); # Get rid of any trailing \n. 5892: $questions =~ s/\r$//; # Get rid of trailing \r too (MAC or Win uploads). 5893: while (length($questions)) { 5894: my $answers_needed = $bubble_lines_per_response{$questnum}; 5895: my $answer_length = ($$scantron_config{'Qlength'} * $answers_needed) 5896: || 1; 5897: $questnum++; 5898: my $quest_id = $questnum; 5899: my $currentquest = substr($questions,0,$answer_length); 5900: $questions = substr($questions,$answer_length); 5901: if (length($currentquest) < $answer_length) { next; } 5902: 5903: if ($subdivided_bubble_lines{$questnum-1} =~ /,/) { 5904: my $subquestnum = 1; 5905: my $subquestions = $currentquest; 5906: my @subanswers_needed = 5907: split(/,/,$subdivided_bubble_lines{$questnum-1}); 5908: foreach my $subans (@subanswers_needed) { 5909: my $subans_length = 5910: ($$scantron_config{'Qlength'} * $subans) || 1; 5911: my $currsubquest = substr($subquestions,0,$subans_length); 5912: $subquestions = substr($subquestions,$subans_length); 5913: $quest_id = "$questnum.$subquestnum"; 5914: if (($$scantron_config{'Qon'} eq 'letter') || 5915: ($$scantron_config{'Qon'} eq 'number')) { 5916: $ansnum = &scantron_validator_lettnum($ansnum, 5917: $questnum,$quest_id,$subans,$currsubquest,$whichline, 5918: \@alphabet,\%record,$scantron_config,$scan_data); 5919: } else { 5920: $ansnum = &scantron_validator_positional($ansnum, 5921: $questnum,$quest_id,$subans,$currsubquest,$whichline, \@alphabet,\%record,$scantron_config,$scan_data); 5922: } 5923: $subquestnum ++; 5924: } 5925: } else { 5926: if (($$scantron_config{'Qon'} eq 'letter') || 5927: ($$scantron_config{'Qon'} eq 'number')) { 5928: $ansnum = &scantron_validator_lettnum($ansnum,$questnum, 5929: $quest_id,$answers_needed,$currentquest,$whichline, 5930: \@alphabet,\%record,$scantron_config,$scan_data); 5931: } else { 5932: $ansnum = &scantron_validator_positional($ansnum,$questnum, 5933: $quest_id,$answers_needed,$currentquest,$whichline, 5934: \@alphabet,\%record,$scantron_config,$scan_data); 5935: } 5936: } 5937: } 5938: $record{'scantron.maxquest'}=$questnum; 5939: return \%record; 5940: } 5941: 5942: sub scantron_validator_lettnum { 5943: my ($ansnum,$questnum,$quest_id,$answers_needed,$currquest,$whichline, 5944: $alphabet,$record,$scantron_config,$scan_data) = @_; 5945: 5946: # Qon 'letter' implies for each slot in currquest we have: 5947: # ? or * for doubles, a letter in A-Z for a bubble, and 5948: # about anything else (esp. a value of Qoff) for missing 5949: # bubbles. 5950: # 5951: # Qon 'number' implies each slot gives a digit that indexes the 5952: # bubbles filled, or Qoff, or a non-number for unbubbled lines, 5953: # and * or ? for double bubbles on a single line. 5954: # 5955: 5956: my $matchon; 5957: if ($$scantron_config{'Qon'} eq 'letter') { 5958: $matchon = '[A-Z]'; 5959: } elsif ($$scantron_config{'Qon'} eq 'number') { 5960: $matchon = '\d'; 5961: } 5962: my $occurrences = 0; 5963: if (($responsetype_per_response{$questnum-1} eq 'essayresponse') || 5964: ($responsetype_per_response{$questnum-1} eq 'formularesponse') || 5965: ($responsetype_per_response{$questnum-1} eq 'stringresponse') || 5966: ($responsetype_per_response{$questnum-1} eq 'imageresponse') || 5967: ($responsetype_per_response{$questnum-1} eq 'reactionresponse') || 5968: ($responsetype_per_response{$questnum-1} eq 'organicresponse')) { 5969: my @singlelines = split('',$currquest); 5970: foreach my $entry (@singlelines) { 5971: $occurrences = &occurence_count($entry,$matchon); 5972: if ($occurrences > 1) { 5973: last; 5974: } 5975: } 5976: } else { 5977: $occurrences = &occurence_count($currquest,$matchon); 5978: } 5979: if (($currquest =~ /\?/ || $currquest =~ /\*/) || ($occurrences > 1)) { 5980: push(@{$record->{'scantron.doubleerror'}},$quest_id); 5981: for (my $ans=0; $ans<$answers_needed; $ans++) { 5982: my $bubble = substr($currquest,$ans,1); 5983: if ($bubble =~ /$matchon/ ) { 5984: if ($$scantron_config{'Qon'} eq 'number') { 5985: if ($bubble == 0) { 5986: $bubble = 10; 5987: } 5988: $record->{"scantron.$ansnum.answer"} = 5989: $alphabet->[$bubble-1]; 5990: } else { 5991: $record->{"scantron.$ansnum.answer"} = $bubble; 5992: } 5993: } else { 5994: $record->{"scantron.$ansnum.answer"}=''; 5995: } 5996: $ansnum++; 5997: } 5998: } elsif (!defined($currquest) 5999: || (&occurence_count($currquest, $$scantron_config{'Qoff'}) == length($currquest)) 6000: || (&occurence_count($currquest,$matchon) == 0)) { 6001: for (my $ans=0; $ans<$answers_needed; $ans++ ) { 6002: $record->{"scantron.$ansnum.answer"}=''; 6003: $ansnum++; 6004: } 6005: if (!&scan_data($scan_data,"$whichline.no_bubble.$quest_id")) { 6006: push(@{$record->{'scantron.missingerror'}},$quest_id); 6007: } 6008: } else { 6009: if ($$scantron_config{'Qon'} eq 'number') { 6010: $currquest = &digits_to_letters($currquest); 6011: } 6012: for (my $ans=0; $ans<$answers_needed; $ans++) { 6013: my $bubble = substr($currquest,$ans,1); 6014: $record->{"scantron.$ansnum.answer"} = $bubble; 6015: $ansnum++; 6016: } 6017: } 6018: return $ansnum; 6019: } 6020: 6021: sub scantron_validator_positional { 6022: my ($ansnum,$questnum,$quest_id,$answers_needed,$currquest, 6023: $whichline,$alphabet,$record,$scantron_config,$scan_data) = @_; 6024: 6025: # Otherwise there's a positional notation; 6026: # each bubble line requires Qlength items, and there are filled in 6027: # bubbles for each case where there 'Qon' characters. 6028: # 6029: 6030: my @array=split($$scantron_config{'Qon'},$currquest,-1); 6031: 6032: # If the split only gives us one element.. the full length of the 6033: # answer string, no bubbles are filled in: 6034: 6035: if ($answers_needed eq '') { 6036: return; 6037: } 6038: 6039: if (length($array[0]) eq $$scantron_config{'Qlength'}*$answers_needed) { 6040: for (my $ans=0; $ans<$answers_needed; $ans++ ) { 6041: $record->{"scantron.$ansnum.answer"}=''; 6042: $ansnum++; 6043: } 6044: if (!&scan_data($scan_data,"$whichline.no_bubble.$quest_id")) { 6045: push(@{$record->{"scantron.missingerror"}},$quest_id); 6046: } 6047: } elsif (scalar(@array) == 2) { 6048: my $location = length($array[0]); 6049: my $line_num = int($location / $$scantron_config{'Qlength'}); 6050: my $bubble = $alphabet->[$location % $$scantron_config{'Qlength'}]; 6051: for (my $ans=0; $ans<$answers_needed; $ans++) { 6052: if ($ans eq $line_num) { 6053: $record->{"scantron.$ansnum.answer"} = $bubble; 6054: } else { 6055: $record->{"scantron.$ansnum.answer"} = ' '; 6056: } 6057: $ansnum++; 6058: } 6059: } else { 6060: # If there's more than one instance of a bubble character 6061: # That's a double bubble; with positional notation we can 6062: # record all the bubbles filled in as well as the 6063: # fact this response consists of multiple bubbles. 6064: # 6065: if (($responsetype_per_response{$questnum-1} eq 'essayresponse') || 6066: ($responsetype_per_response{$questnum-1} eq 'formularesponse') || 6067: ($responsetype_per_response{$questnum-1} eq 'stringresponse') || 6068: ($responsetype_per_response{$questnum-1} eq 'imageresponse') || 6069: ($responsetype_per_response{$questnum-1} eq 'reactionresponse') || 6070: ($responsetype_per_response{$questnum-1} eq 'organicresponse')) { 6071: my $doubleerror = 0; 6072: while (($currquest >= $$scantron_config{'Qlength'}) && 6073: (!$doubleerror)) { 6074: my $currline = substr($currquest,0,$$scantron_config{'Qlength'}); 6075: $currquest = substr($currquest,$$scantron_config{'Qlength'}); 6076: my @currarray = split($$scantron_config{'Qon'},$currline,-1); 6077: if (length(@currarray) > 2) { 6078: $doubleerror = 1; 6079: } 6080: } 6081: if ($doubleerror) { 6082: push(@{$record->{'scantron.doubleerror'}},$quest_id); 6083: } 6084: } else { 6085: push(@{$record->{'scantron.doubleerror'}},$quest_id); 6086: } 6087: my $item = $ansnum; 6088: for (my $ans=0; $ans<$answers_needed; $ans++) { 6089: $record->{"scantron.$item.answer"} = ''; 6090: $item ++; 6091: } 6092: 6093: my @ans=@array; 6094: my $i=0; 6095: my $increment = 0; 6096: while ($#ans) { 6097: $i+=length($ans[0]) + $increment; 6098: my $line = int($i/$$scantron_config{'Qlength'} + $ansnum); 6099: my $bubble = $i%$$scantron_config{'Qlength'}; 6100: $record->{"scantron.$line.answer"}.=$alphabet->[$bubble]; 6101: shift(@ans); 6102: $increment = 1; 6103: } 6104: $ansnum += $answers_needed; 6105: } 6106: return $ansnum; 6107: } 6108: 6109: =pod 6110: 6111: =item scantron_add_delay 6112: 6113: Adds an error message that occurred during the grading phase to a 6114: queue of messages to be shown after grading pass is complete 6115: 6116: Arguments: 6117: $delayqueue - arrary ref of hash ref of error messages 6118: $scanline - the scanline that caused the error 6119: $errormesage - the error message 6120: $errorcode - a numeric code for the error 6121: 6122: Side Effects: 6123: updates the $delayqueue to have a new hash ref of the error 6124: 6125: =cut 6126: 6127: sub scantron_add_delay { 6128: my ($delayqueue,$scanline,$errormessage,$errorcode)=@_; 6129: push(@$delayqueue, 6130: {'line' => $scanline, 'emsg' => $errormessage, 6131: 'ecode' => $errorcode } 6132: ); 6133: } 6134: 6135: =pod 6136: 6137: =item scantron_find_student 6138: 6139: Finds the username for the current scanline 6140: 6141: Arguments: 6142: $scantron_record - hash result from scantron_parse_scanline 6143: $scan_data - hash of correction information 6144: (see &scantron_getfile() form more information) 6145: $idmap - hash from &username_to_idmap() 6146: $line - number of current scanline 6147: 6148: Returns: 6149: Either 'username:domain' or undef if unknown 6150: 6151: =cut 6152: 6153: sub scantron_find_student { 6154: my ($scantron_record,$scan_data,$idmap,$line)=@_; 6155: my $scanID=$$scantron_record{'scantron.ID'}; 6156: if ($scanID =~ /^\s*$/) { 6157: return &scan_data($scan_data,"$line.user"); 6158: } 6159: foreach my $id (keys(%$idmap)) { 6160: if (lc($id) eq lc($scanID)) { 6161: return $$idmap{$id}; 6162: } 6163: } 6164: return undef; 6165: } 6166: 6167: =pod 6168: 6169: =item scantron_filter 6170: 6171: Filter sub for lonnavmaps, filters out hidden resources if ignore 6172: hidden resources was selected 6173: 6174: =cut 6175: 6176: sub scantron_filter { 6177: my ($curres)=@_; 6178: 6179: if (ref($curres) && $curres->is_problem()) { 6180: # if the user has asked to not have either hidden 6181: # or 'randomout' controlled resources to be graded 6182: # don't include them 6183: if ($env{'form.scantron_options_hidden'} eq 'ignore_hidden' 6184: && $curres->randomout) { 6185: return 0; 6186: } 6187: return 1; 6188: } 6189: return 0; 6190: } 6191: 6192: =pod 6193: 6194: =item scantron_process_corrections 6195: 6196: Gets correction information out of submitted form data and corrects 6197: the scanline 6198: 6199: =cut 6200: 6201: sub scantron_process_corrections { 6202: my ($r) = @_; 6203: my %scantron_config=&get_scantron_config($env{'form.scantron_format'}); 6204: my ($scanlines,$scan_data)=&scantron_getfile(); 6205: my $classlist=&Apache::loncoursedata::get_classlist(); 6206: my $which=$env{'form.scantron_line'}; 6207: my $line=&scantron_get_line($scanlines,$scan_data,$which); 6208: my ($skip,$err,$errmsg); 6209: if ($env{'form.scantron_skip_record'}) { 6210: $skip=1; 6211: } elsif ($env{'form.scantron_corrections'} =~ /^(duplicate|incorrect)ID$/) { 6212: my $newstudent=$env{'form.scantron_username'}.':'. 6213: $env{'form.scantron_domain'}; 6214: my $newid=$classlist->{$newstudent}->[&Apache::loncoursedata::CL_ID]; 6215: ($line,$err,$errmsg)= 6216: &scantron_fixup_scanline(\%scantron_config,$scan_data,$line,$which, 6217: 'ID',{'newid'=>$newid, 6218: 'username'=>$env{'form.scantron_username'}, 6219: 'domain'=>$env{'form.scantron_domain'}}); 6220: } elsif ($env{'form.scantron_corrections'} =~ /^(duplicate|incorrect)CODE$/) { 6221: my $resolution=$env{'form.scantron_CODE_resolution'}; 6222: my $newCODE; 6223: my %args; 6224: if ($resolution eq 'use_unfound') { 6225: $newCODE='use_unfound'; 6226: } elsif ($resolution eq 'use_found') { 6227: $newCODE=$env{'form.scantron_CODE_selectedvalue'}; 6228: } elsif ($resolution eq 'use_typed') { 6229: $newCODE=$env{'form.scantron_CODE_newvalue'}; 6230: } elsif ($resolution =~ /^use_closest_(\d+)/) { 6231: $newCODE=$env{"form.scantron_CODE_closest_$1"}; 6232: } 6233: if ($env{'form.scantron_corrections'} eq 'duplicateCODE') { 6234: $args{'CODE_ignore_dup'}=1; 6235: } 6236: $args{'CODE'}=$newCODE; 6237: ($line,$err,$errmsg)= 6238: &scantron_fixup_scanline(\%scantron_config,$scan_data,$line,$which, 6239: 'CODE',\%args); 6240: } elsif ($env{'form.scantron_corrections'} =~ /^(missing|double)bubble$/) { 6241: foreach my $question (split(',',$env{'form.scantron_questions'})) { 6242: ($line,$err,$errmsg)= 6243: &scantron_fixup_scanline(\%scantron_config,$scan_data,$line, 6244: $which,'answer', 6245: { 'question'=>$question, 6246: 'response'=>$env{"form.scantron_correct_Q_$question"}, 6247: 'questionnum'=>$env{"form.scantron_questionnum_Q_$question"}}); 6248: if ($err) { last; } 6249: } 6250: } 6251: if ($err) { 6252: $r->print("<span class=\"LC_warning\">Unable to accept last correction, an error occurred :$errmsg:</span>"); 6253: } else { 6254: &scantron_put_line($scanlines,$scan_data,$which,$line,$skip); 6255: &scantron_putfile($scanlines,$scan_data); 6256: } 6257: } 6258: 6259: =pod 6260: 6261: =item reset_skipping_status 6262: 6263: Forgets the current set of remember skipped scanlines (and thus 6264: reverts back to considering all lines in the 6265: scantron_skipped_<filename> file) 6266: 6267: =cut 6268: 6269: sub reset_skipping_status { 6270: my ($scanlines,$scan_data)=&scantron_getfile(); 6271: &scan_data($scan_data,'remember_skipping',undef,1); 6272: &scantron_putfile(undef,$scan_data); 6273: } 6274: 6275: =pod 6276: 6277: =item start_skipping 6278: 6279: Marks a scanline to be skipped. 6280: 6281: =cut 6282: 6283: sub start_skipping { 6284: my ($scan_data,$i)=@_; 6285: my %remembered=split(':',&scan_data($scan_data,'remember_skipping')); 6286: if ($env{'form.scantron_options_redo'} =~ /^redo_/) { 6287: $remembered{$i}=2; 6288: } else { 6289: $remembered{$i}=1; 6290: } 6291: &scan_data($scan_data,'remember_skipping',join(':',%remembered)); 6292: } 6293: 6294: =pod 6295: 6296: =item should_be_skipped 6297: 6298: Checks whether a scanline should be skipped. 6299: 6300: =cut 6301: 6302: sub should_be_skipped { 6303: my ($scanlines,$scan_data,$i)=@_; 6304: if ($env{'form.scantron_options_redo'} !~ /^redo_/) { 6305: # not redoing old skips 6306: if ($scanlines->{'skipped'}[$i]) { return 1; } 6307: return 0; 6308: } 6309: my %remembered=split(':',&scan_data($scan_data,'remember_skipping')); 6310: 6311: if (exists($remembered{$i}) && $remembered{$i} != 2 ) { 6312: return 0; 6313: } 6314: return 1; 6315: } 6316: 6317: =pod 6318: 6319: =item remember_current_skipped 6320: 6321: Discovers what scanlines are in the scantron_skipped_<filename> 6322: file and remembers them into scan_data for later use. 6323: 6324: =cut 6325: 6326: sub remember_current_skipped { 6327: my ($scanlines,$scan_data)=&scantron_getfile(); 6328: my %to_remember; 6329: for (my $i=0;$i<=$scanlines->{'count'};$i++) { 6330: if ($scanlines->{'skipped'}[$i]) { 6331: $to_remember{$i}=1; 6332: } 6333: } 6334: 6335: &scan_data($scan_data,'remember_skipping',join(':',%to_remember)); 6336: &scantron_putfile(undef,$scan_data); 6337: } 6338: 6339: =pod 6340: 6341: =item check_for_error 6342: 6343: Checks if there was an error when attempting to remove a specific 6344: scantron_.. bubblesheet data file. Prints out an error if 6345: something went wrong. 6346: 6347: =cut 6348: 6349: sub check_for_error { 6350: my ($r,$result)=@_; 6351: if ($result ne 'ok' && $result ne 'not_found' ) { 6352: $r->print(&mt("An error occurred ([_1]) when trying to remove the existing corrections.",$result)); 6353: } 6354: } 6355: 6356: =pod 6357: 6358: =item scantron_warning_screen 6359: 6360: Interstitial screen to make sure the operator has selected the 6361: correct options before we start the validation phase. 6362: 6363: =cut 6364: 6365: sub scantron_warning_screen { 6366: my ($button_text)=@_; 6367: my $title=&Apache::lonnet::gettitle($env{'form.selectpage'}); 6368: my %scantron_config=&get_scantron_config($env{'form.scantron_format'}); 6369: my $CODElist; 6370: if ($scantron_config{'CODElocation'} && 6371: $scantron_config{'CODEstart'} && 6372: $scantron_config{'CODElength'}) { 6373: $CODElist=$env{'form.scantron_CODElist'}; 6374: if ($env{'form.scantron_CODElist'} eq '') { $CODElist='<span class="LC_warning">None</span>'; } 6375: $CODElist= 6376: '<tr><td><b>'.&mt('List of CODES to validate against:').'</b></td><td><tt>'. 6377: $env{'form.scantron_CODElist'}.'</tt></td></tr>'; 6378: } 6379: return (' 6380: <p> 6381: <span class="LC_warning"> 6382: '.&mt('Please double check the information below before clicking on \'[_1]\'',&mt($button_text)).'</span> 6383: </p> 6384: <table> 6385: <tr><td><b>'.&mt('Sequence to be Graded:').'</b></td><td>'.$title.'</td></tr> 6386: <tr><td><b>'.&mt('Data File that will be used:').'</b></td><td><tt>'.$env{'form.scantron_selectfile'}.'</tt></td></tr> 6387: '.$CODElist.' 6388: </table> 6389: <br /> 6390: <p> '.&mt('If this information is correct, please click on \'[_1]\'.',&mt($button_text)).'</p> 6391: <p> '.&mt('If something is incorrect, please click the \'Grading Menu\' button to start over.').'</p> 6392: 6393: <br /> 6394: '); 6395: } 6396: 6397: =pod 6398: 6399: =item scantron_do_warning 6400: 6401: Check if the operator has picked something for all required 6402: fields. Error out if something is missing. 6403: 6404: =cut 6405: 6406: sub scantron_do_warning { 6407: my ($r)=@_; 6408: my ($symb)=&get_symb($r); 6409: if (!$symb) {return '';} 6410: my $default_form_data=&defaultFormData($symb); 6411: $r->print(&scantron_form_start().$default_form_data); 6412: if ( $env{'form.selectpage'} eq '' || 6413: $env{'form.scantron_selectfile'} eq '' || 6414: $env{'form.scantron_format'} eq '' ) { 6415: $r->print("<p>".&mt('You have forgotten to specify some information. Please go Back and try again.')."</p>"); 6416: if ( $env{'form.selectpage'} eq '') { 6417: $r->print('<p><span class="LC_error">'.&mt('You have not selected a Sequence to grade').'</span></p>'); 6418: } 6419: if ( $env{'form.scantron_selectfile'} eq '') { 6420: $r->print('<p><span class="LC_error">'.&mt("You have not selected a file that contains the student's response data.").'</span></p>'); 6421: } 6422: if ( $env{'form.scantron_format'} eq '') { 6423: $r->print('<p><span class="LC_error">'.&mt("You have not selected the format of the student's response data.").'</span></p>'); 6424: } 6425: } else { 6426: my $warning=&scantron_warning_screen('Grading: Validate Records'); 6427: $r->print(' 6428: '.$warning.' 6429: <input type="submit" name="submit" value="'.&mt('Grading: Validate Records').'" /> 6430: <input type="hidden" name="command" value="scantron_validate" /> 6431: '); 6432: } 6433: $r->print("</form><br />".&show_grading_menu_form($symb)); 6434: return ''; 6435: } 6436: 6437: =pod 6438: 6439: =item scantron_form_start 6440: 6441: html hidden input for remembering all selected grading options 6442: 6443: =cut 6444: 6445: sub scantron_form_start { 6446: my ($max_bubble)=@_; 6447: my $result= <<SCANTRONFORM; 6448: <form method="post" enctype="multipart/form-data" action="/adm/grades" name="scantronupload"> 6449: <input type="hidden" name="selectpage" value="$env{'form.selectpage'}" /> 6450: <input type="hidden" name="scantron_format" value="$env{'form.scantron_format'}" /> 6451: <input type="hidden" name="scantron_selectfile" value="$env{'form.scantron_selectfile'}" /> 6452: <input type="hidden" name="scantron_maxbubble" value="$max_bubble" /> 6453: <input type="hidden" name="scantron_CODElist" value="$env{'form.scantron_CODElist'}" /> 6454: <input type="hidden" name="scantron_CODEunique" value="$env{'form.scantron_CODEunique'}" /> 6455: <input type="hidden" name="scantron_options_redo" value="$env{'form.scantron_options_redo'}" /> 6456: <input type="hidden" name="scantron_options_ignore" value="$env{'form.scantron_options_ignore'}" /> 6457: <input type="hidden" name="scantron_options_hidden" value="$env{'form.scantron_options_hidden'}" /> 6458: SCANTRONFORM 6459: 6460: my $line = 0; 6461: while (defined($env{"form.scantron.bubblelines.$line"})) { 6462: my $chunk = 6463: '<input type="hidden" name="scantron.bubblelines.'.$line.'" value="'.$env{"form.scantron.bubblelines.$line"}.'" />'."\n"; 6464: $chunk .= 6465: '<input type="hidden" name="scantron.first_bubble_line.'.$line.'" value="'.$env{"form.scantron.first_bubble_line.$line"}.'" />'."\n"; 6466: $chunk .= 6467: '<input type="hidden" name="scantron.sub_bubblelines.'.$line.'" value="'.$env{"form.scantron.sub_bubblelines.$line"}.'" />'."\n"; 6468: $chunk .= 6469: '<input type="hidden" name="scantron.responsetype.'.$line.'" value="'.$env{"form.scantron.responsetype.$line"}.'" />'."\n"; 6470: $result .= $chunk; 6471: $line++; 6472: } 6473: return $result; 6474: } 6475: 6476: =pod 6477: 6478: =item scantron_validate_file 6479: 6480: Dispatch routine for doing validation of a bubblesheet data file. 6481: 6482: Also processes any necessary information resets that need to 6483: occur before validation begins (ignore previous corrections, 6484: restarting the skipped records processing) 6485: 6486: =cut 6487: 6488: sub scantron_validate_file { 6489: my ($r) = @_; 6490: my ($symb)=&get_symb($r); 6491: if (!$symb) {return '';} 6492: my $default_form_data=&defaultFormData($symb); 6493: 6494: # do the detection of only doing skipped records first befroe we delete 6495: # them when doing the corrections reset 6496: if ($env{'form.scantron_options_redo'} ne 'redo_skipped_ready') { 6497: &reset_skipping_status(); 6498: } 6499: if ($env{'form.scantron_options_redo'} eq 'redo_skipped') { 6500: &remember_current_skipped(); 6501: $env{'form.scantron_options_redo'}='redo_skipped_ready'; 6502: } 6503: 6504: if ($env{'form.scantron_options_ignore'} eq 'ignore_corrections') { 6505: &check_for_error($r,&scantron_remove_file('corrected')); 6506: &check_for_error($r,&scantron_remove_file('skipped')); 6507: &check_for_error($r,&scantron_remove_scan_data()); 6508: $env{'form.scantron_options_ignore'}='done'; 6509: } 6510: 6511: if ($env{'form.scantron_corrections'}) { 6512: &scantron_process_corrections($r); 6513: } 6514: $r->print('<p>'.&mt('Gathering necessary information.').'</p>');$r->rflush(); 6515: #get the student pick code ready 6516: $r->print(&Apache::loncommon::studentbrowser_javascript()); 6517: my $nav_error; 6518: my $max_bubble=&scantron_get_maxbubble(\$nav_error); 6519: if ($nav_error) { 6520: $r->print(&navmap_errormsg()); 6521: return ''; 6522: } 6523: my $result=&scantron_form_start($max_bubble).$default_form_data; 6524: $r->print($result); 6525: 6526: my @validate_phases=( 'sequence', 6527: 'ID', 6528: 'CODE', 6529: 'doublebubble', 6530: 'missingbubbles'); 6531: if (!$env{'form.validatepass'}) { 6532: $env{'form.validatepass'} = 0; 6533: } 6534: my $currentphase=$env{'form.validatepass'}; 6535: 6536: 6537: my $stop=0; 6538: while (!$stop && $currentphase < scalar(@validate_phases)) { 6539: $r->print(&mt('Validating '.$validate_phases[$currentphase]).'<br />'); 6540: $r->rflush(); 6541: my $which="scantron_validate_".$validate_phases[$currentphase]; 6542: { 6543: no strict 'refs'; 6544: ($stop,$currentphase)=&$which($r,$currentphase); 6545: } 6546: } 6547: if (!$stop) { 6548: my $warning=&scantron_warning_screen('Start Grading'); 6549: $r->print(&mt('Validation process complete.').'<br />'. 6550: $warning. 6551: &mt('Perform verification for each student after storage of submissions?'). 6552: ' <span class="LC_nobreak"><label>'. 6553: '<input type="radio" name="verifyrecord" value="1" />'.&mt('Yes').'</label>'. 6554: (' 'x3).'<label>'. 6555: '<input type="radio" name="verifyrecord" value="0" checked="checked" />'.&mt('No'). 6556: '</label></span><br />'. 6557: &mt('Grading will take longer if you use verification.').'<br />'. 6558: &mt("Alternatively, the 'Review bubblesheet data' utility (see grading menu) can be used for all students after grading is complete.").'<br /><br />'. 6559: '<input type="submit" name="submit" value="'.&mt('Start Grading').'" />'. 6560: '<input type="hidden" name="command" value="scantron_process" />'."\n"); 6561: } else { 6562: $r->print('<input type="hidden" name="command" value="scantron_validate" />'); 6563: $r->print("<input type='hidden' name='validatepass' value='".$currentphase."' />"); 6564: } 6565: if ($stop) { 6566: if ($validate_phases[$currentphase] eq 'sequence') { 6567: $r->print('<input type="submit" name="submit" value="'.&mt('Ignore').' → " />'); 6568: $r->print(' '.&mt('this error').' <br />'); 6569: 6570: $r->print(" <p>".&mt("Or click the 'Grading Menu' button to start over.")."</p>"); 6571: } else { 6572: if ($validate_phases[$currentphase] eq 'doublebubble' || $validate_phases[$currentphase] eq 'missingbubbles') { 6573: $r->print('<input type="button" name="submitbutton" value="'.&mt('Continue').' →" onclick="javascript:verify_bubble_radio(this.form)" />'); 6574: } else { 6575: $r->print('<input type="submit" name="submit" value="'.&mt('Continue').' →" />'); 6576: } 6577: $r->print(' '.&mt('using corrected info').' <br />'); 6578: $r->print("<input type='submit' value='".&mt("Skip")."' name='scantron_skip_record' />"); 6579: $r->print(" ".&mt("this scanline saving it for later.")); 6580: } 6581: } 6582: $r->print(" </form><br />".&show_grading_menu_form($symb)); 6583: return ''; 6584: } 6585: 6586: 6587: =pod 6588: 6589: =item scantron_remove_file 6590: 6591: Removes the requested bubblesheet data file, makes sure that 6592: scantron_original_<filename> is never removed 6593: 6594: 6595: =cut 6596: 6597: sub scantron_remove_file { 6598: my ($which)=@_; 6599: my $cname=$env{'course.'.$env{'request.course.id'}.'.num'}; 6600: my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'}; 6601: my $file='scantron_'; 6602: if ($which eq 'corrected' || $which eq 'skipped') { 6603: $file.=$which.'_'; 6604: } else { 6605: return 'refused'; 6606: } 6607: $file.=$env{'form.scantron_selectfile'}; 6608: return &Apache::lonnet::removeuserfile($cname,$cdom,$file); 6609: } 6610: 6611: 6612: =pod 6613: 6614: =item scantron_remove_scan_data 6615: 6616: Removes all scan_data correction for the requested bubblesheet 6617: data file. (In the case that both the are doing skipped records we need 6618: to remember the old skipped lines for the time being so that element 6619: persists for a while.) 6620: 6621: =cut 6622: 6623: sub scantron_remove_scan_data { 6624: my $cname=$env{'course.'.$env{'request.course.id'}.'.num'}; 6625: my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'}; 6626: my @keys=&Apache::lonnet::getkeys('nohist_scantrondata',$cdom,$cname); 6627: my @todelete; 6628: my $filename=$env{'form.scantron_selectfile'}; 6629: foreach my $key (@keys) { 6630: if ($key=~/^\Q$filename\E_/) { 6631: if ($env{'form.scantron_options_redo'} eq 'redo_skipped_ready' && 6632: $key=~/remember_skipping/) { 6633: next; 6634: } 6635: push(@todelete,$key); 6636: } 6637: } 6638: my $result; 6639: if (@todelete) { 6640: $result = &Apache::lonnet::del('nohist_scantrondata', 6641: \@todelete,$cdom,$cname); 6642: } else { 6643: $result = 'ok'; 6644: } 6645: return $result; 6646: } 6647: 6648: 6649: =pod 6650: 6651: =item scantron_getfile 6652: 6653: Fetches the requested bubblesheet data file (all 3 versions), and 6654: the scan_data hash 6655: 6656: Arguments: 6657: None 6658: 6659: Returns: 6660: 2 hash references 6661: 6662: - first one has 6663: orig - 6664: corrected - 6665: skipped - each of which points to an array ref of the specified 6666: file broken up into individual lines 6667: count - number of scanlines 6668: 6669: - second is the scan_data hash possible keys are 6670: ($number refers to scanline numbered $number and thus the key affects 6671: only that scanline 6672: $bubline refers to the specific bubble line element and the aspects 6673: refers to that specific bubble line element) 6674: 6675: $number.user - username:domain to use 6676: $number.CODE_ignore_dup 6677: - ignore the duplicate CODE error 6678: $number.useCODE 6679: - use the CODE in the scanline as is 6680: $number.no_bubble.$bubline 6681: - it is valid that there is no bubbled in bubble 6682: at $number $bubline 6683: remember_skipping 6684: - a frozen hash containing keys of $number and values 6685: of either 6686: 1 - we are on a 'do skipped records pass' and plan 6687: on processing this line 6688: 2 - we are on a 'do skipped records pass' and this 6689: scanline has been marked to skip yet again 6690: 6691: =cut 6692: 6693: sub scantron_getfile { 6694: #FIXME really would prefer a scantron directory 6695: my $cname=$env{'course.'.$env{'request.course.id'}.'.num'}; 6696: my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'}; 6697: my $lines; 6698: $lines=&Apache::lonnet::getfile('/uploaded/'.$cdom.'/'.$cname.'/'. 6699: 'scantron_orig_'.$env{'form.scantron_selectfile'}); 6700: my %scanlines; 6701: $scanlines{'orig'}=[(split("\n",$lines,-1))]; 6702: my $temp=$scanlines{'orig'}; 6703: $scanlines{'count'}=$#$temp; 6704: 6705: $lines=&Apache::lonnet::getfile('/uploaded/'.$cdom.'/'.$cname.'/'. 6706: 'scantron_corrected_'.$env{'form.scantron_selectfile'}); 6707: if ($lines eq '-1') { 6708: $scanlines{'corrected'}=[]; 6709: } else { 6710: $scanlines{'corrected'}=[(split("\n",$lines,-1))]; 6711: } 6712: $lines=&Apache::lonnet::getfile('/uploaded/'.$cdom.'/'.$cname.'/'. 6713: 'scantron_skipped_'.$env{'form.scantron_selectfile'}); 6714: if ($lines eq '-1') { 6715: $scanlines{'skipped'}=[]; 6716: } else { 6717: $scanlines{'skipped'}=[(split("\n",$lines,-1))]; 6718: } 6719: my @tmp=&Apache::lonnet::dump('nohist_scantrondata',$cdom,$cname); 6720: if ($tmp[0] =~ /^(error:|no_such_host)/) { @tmp=(); } 6721: my %scan_data = @tmp; 6722: return (\%scanlines,\%scan_data); 6723: } 6724: 6725: =pod 6726: 6727: =item lonnet_putfile 6728: 6729: Wrapper routine to call &Apache::lonnet::finishuserfileupload 6730: 6731: Arguments: 6732: $contents - data to store 6733: $filename - filename to store $contents into 6734: 6735: Returns: 6736: result value from &Apache::lonnet::finishuserfileupload 6737: 6738: =cut 6739: 6740: sub lonnet_putfile { 6741: my ($contents,$filename)=@_; 6742: my $docuname=$env{'course.'.$env{'request.course.id'}.'.num'}; 6743: my $docudom=$env{'course.'.$env{'request.course.id'}.'.domain'}; 6744: $env{'form.sillywaytopassafilearound'}=$contents; 6745: &Apache::lonnet::finishuserfileupload($docuname,$docudom,'sillywaytopassafilearound',$filename); 6746: 6747: } 6748: 6749: =pod 6750: 6751: =item scantron_putfile 6752: 6753: Stores the current version of the bubblesheet data files, and the 6754: scan_data hash. (Does not modify the original version only the 6755: corrected and skipped versions. 6756: 6757: Arguments: 6758: $scanlines - hash ref that looks like the first return value from 6759: &scantron_getfile() 6760: $scan_data - hash ref that looks like the second return value from 6761: &scantron_getfile() 6762: 6763: =cut 6764: 6765: sub scantron_putfile { 6766: my ($scanlines,$scan_data) = @_; 6767: #FIXME really would prefer a scantron directory 6768: my $cname=$env{'course.'.$env{'request.course.id'}.'.num'}; 6769: my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'}; 6770: if ($scanlines) { 6771: my $prefix='scantron_'; 6772: # no need to update orig, shouldn't change 6773: # &lonnet_putfile(join("\n",@{$scanlines->{'orig'}}),$prefix.'orig_'. 6774: # $env{'form.scantron_selectfile'}); 6775: &lonnet_putfile(join("\n",@{$scanlines->{'corrected'}}), 6776: $prefix.'corrected_'. 6777: $env{'form.scantron_selectfile'}); 6778: &lonnet_putfile(join("\n",@{$scanlines->{'skipped'}}), 6779: $prefix.'skipped_'. 6780: $env{'form.scantron_selectfile'}); 6781: } 6782: &Apache::lonnet::put('nohist_scantrondata',$scan_data,$cdom,$cname); 6783: } 6784: 6785: =pod 6786: 6787: =item scantron_get_line 6788: 6789: Returns the correct version of the scanline 6790: 6791: Arguments: 6792: $scanlines - hash ref that looks like the first return value from 6793: &scantron_getfile() 6794: $scan_data - hash ref that looks like the second return value from 6795: &scantron_getfile() 6796: $i - number of the requested line (starts at 0) 6797: 6798: Returns: 6799: A scanline, (either the original or the corrected one if it 6800: exists), or undef if the requested scanline should be 6801: skipped. (Either because it's an skipped scanline, or it's an 6802: unskipped scanline and we are not doing a 'do skipped scanlines' 6803: pass. 6804: 6805: =cut 6806: 6807: sub scantron_get_line { 6808: my ($scanlines,$scan_data,$i)=@_; 6809: if (&should_be_skipped($scanlines,$scan_data,$i)) { return undef; } 6810: #if ($scanlines->{'skipped'}[$i]) { return undef; } 6811: if ($scanlines->{'corrected'}[$i]) {return $scanlines->{'corrected'}[$i];} 6812: return $scanlines->{'orig'}[$i]; 6813: } 6814: 6815: =pod 6816: 6817: =item scantron_todo_count 6818: 6819: Counts the number of scanlines that need processing. 6820: 6821: Arguments: 6822: $scanlines - hash ref that looks like the first return value from 6823: &scantron_getfile() 6824: $scan_data - hash ref that looks like the second return value from 6825: &scantron_getfile() 6826: 6827: Returns: 6828: $count - number of scanlines to process 6829: 6830: =cut 6831: 6832: sub get_todo_count { 6833: my ($scanlines,$scan_data)=@_; 6834: my $count=0; 6835: for (my $i=0;$i<=$scanlines->{'count'};$i++) { 6836: my $line=&scantron_get_line($scanlines,$scan_data,$i); 6837: if ($line=~/^[\s\cz]*$/) { next; } 6838: $count++; 6839: } 6840: return $count; 6841: } 6842: 6843: =pod 6844: 6845: =item scantron_put_line 6846: 6847: Updates the 'corrected' or 'skipped' versions of the bubblesheet 6848: data file. 6849: 6850: Arguments: 6851: $scanlines - hash ref that looks like the first return value from 6852: &scantron_getfile() 6853: $scan_data - hash ref that looks like the second return value from 6854: &scantron_getfile() 6855: $i - line number to update 6856: $newline - contents of the updated scanline 6857: $skip - if true make the line for skipping and update the 6858: 'skipped' file 6859: 6860: =cut 6861: 6862: sub scantron_put_line { 6863: my ($scanlines,$scan_data,$i,$newline,$skip)=@_; 6864: if ($skip) { 6865: $scanlines->{'skipped'}[$i]=$newline; 6866: &start_skipping($scan_data,$i); 6867: return; 6868: } 6869: $scanlines->{'corrected'}[$i]=$newline; 6870: } 6871: 6872: =pod 6873: 6874: =item scantron_clear_skip 6875: 6876: Remove a line from the 'skipped' file 6877: 6878: Arguments: 6879: $scanlines - hash ref that looks like the first return value from 6880: &scantron_getfile() 6881: $scan_data - hash ref that looks like the second return value from 6882: &scantron_getfile() 6883: $i - line number to update 6884: 6885: =cut 6886: 6887: sub scantron_clear_skip { 6888: my ($scanlines,$scan_data,$i)=@_; 6889: if (exists($scanlines->{'skipped'}[$i])) { 6890: undef($scanlines->{'skipped'}[$i]); 6891: return 1; 6892: } 6893: return 0; 6894: } 6895: 6896: =pod 6897: 6898: =item scantron_filter_not_exam 6899: 6900: Filter routine used by &Apache::lonnavmaps::retrieveResources(), to 6901: filter out resources that are not marked as 'exam' mode 6902: 6903: =cut 6904: 6905: sub scantron_filter_not_exam { 6906: my ($curres)=@_; 6907: 6908: if (ref($curres) && $curres->is_problem() && !$curres->is_exam()) { 6909: # if the user has asked to not have either hidden 6910: # or 'randomout' controlled resources to be graded 6911: # don't include them 6912: if ($env{'form.scantron_options_hidden'} eq 'ignore_hidden' 6913: && $curres->randomout) { 6914: return 0; 6915: } 6916: return 1; 6917: } 6918: return 0; 6919: } 6920: 6921: =pod 6922: 6923: =item scantron_validate_sequence 6924: 6925: Validates the selected sequence, checking for resource that are 6926: not set to exam mode. 6927: 6928: =cut 6929: 6930: sub scantron_validate_sequence { 6931: my ($r,$currentphase) = @_; 6932: 6933: my $navmap=Apache::lonnavmaps::navmap->new(); 6934: unless (ref($navmap)) { 6935: $r->print(&navmap_errormsg()); 6936: return (1,$currentphase); 6937: } 6938: my (undef,undef,$sequence)= 6939: &Apache::lonnet::decode_symb($env{'form.selectpage'}); 6940: 6941: my $map=$navmap->getResourceByUrl($sequence); 6942: 6943: $r->print('<input type="hidden" name="validate_sequence_exam" 6944: value="ignore" />'); 6945: if ($env{'form.validate_sequence_exam'} ne 'ignore') { 6946: my @resources= 6947: $navmap->retrieveResources($map,\&scantron_filter_not_exam,1,0); 6948: if (@resources) { 6949: $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>"); 6950: return (1,$currentphase); 6951: } 6952: } 6953: 6954: return (0,$currentphase+1); 6955: } 6956: 6957: 6958: 6959: sub scantron_validate_ID { 6960: my ($r,$currentphase) = @_; 6961: 6962: #get student info 6963: my $classlist=&Apache::loncoursedata::get_classlist(); 6964: my %idmap=&username_to_idmap($classlist); 6965: 6966: #get scantron line setup 6967: my %scantron_config=&get_scantron_config($env{'form.scantron_format'}); 6968: my ($scanlines,$scan_data)=&scantron_getfile(); 6969: 6970: my $nav_error; 6971: &scantron_get_maxbubble(\$nav_error); # parse needs the bubble_lines.. array. 6972: if ($nav_error) { 6973: $r->print(&navmap_errormsg()); 6974: return(1,$currentphase); 6975: } 6976: 6977: my %found=('ids'=>{},'usernames'=>{}); 6978: for (my $i=0;$i<=$scanlines->{'count'};$i++) { 6979: my $line=&scantron_get_line($scanlines,$scan_data,$i); 6980: if ($line=~/^[\s\cz]*$/) { next; } 6981: my $scan_record=&scantron_parse_scanline($line,$i,\%scantron_config, 6982: $scan_data); 6983: my $id=$$scan_record{'scantron.ID'}; 6984: my $found; 6985: foreach my $checkid (keys(%idmap)) { 6986: if (lc($checkid) eq lc($id)) { $found=$checkid;last; } 6987: } 6988: if ($found) { 6989: my $username=$idmap{$found}; 6990: if ($found{'ids'}{$found}) { 6991: &scantron_get_correction($r,$i,$scan_record,\%scantron_config, 6992: $line,'duplicateID',$found); 6993: return(1,$currentphase); 6994: } elsif ($found{'usernames'}{$username}) { 6995: &scantron_get_correction($r,$i,$scan_record,\%scantron_config, 6996: $line,'duplicateID',$username); 6997: return(1,$currentphase); 6998: } 6999: #FIXME store away line we previously saw the ID on to use above 7000: $found{'ids'}{$found}++; 7001: $found{'usernames'}{$username}++; 7002: } else { 7003: if ($id =~ /^\s*$/) { 7004: my $username=&scan_data($scan_data,"$i.user"); 7005: if (defined($username) && $found{'usernames'}{$username}) { 7006: &scantron_get_correction($r,$i,$scan_record, 7007: \%scantron_config, 7008: $line,'duplicateID',$username); 7009: return(1,$currentphase); 7010: } elsif (!defined($username)) { 7011: &scantron_get_correction($r,$i,$scan_record, 7012: \%scantron_config, 7013: $line,'incorrectID'); 7014: return(1,$currentphase); 7015: } 7016: $found{'usernames'}{$username}++; 7017: } else { 7018: &scantron_get_correction($r,$i,$scan_record,\%scantron_config, 7019: $line,'incorrectID'); 7020: return(1,$currentphase); 7021: } 7022: } 7023: } 7024: 7025: return (0,$currentphase+1); 7026: } 7027: 7028: 7029: sub scantron_get_correction { 7030: my ($r,$i,$scan_record,$scan_config,$line,$error,$arg)=@_; 7031: #FIXME in the case of a duplicated ID the previous line, probably need 7032: #to show both the current line and the previous one and allow skipping 7033: #the previous one or the current one 7034: 7035: if ( $$scan_record{'scantron.PaperID'} =~ /\S/) { 7036: $r->print( 7037: '<p class="LC_warning">' 7038: .&mt('An error was detected ([_1]) for PaperID [_2]', 7039: "<b>$error</b>", 7040: '<tt>'.$$scan_record{'scantron.PaperID'}.'</tt>') 7041: ."</p> \n"); 7042: } else { 7043: $r->print( 7044: '<p class="LC_warning">' 7045: .&mt('An error was detected ([_1]) in scanline [_2] [_3]', 7046: "<b>$error</b>", $i, "<pre>$line</pre>") 7047: ."</p> \n"); 7048: } 7049: my $message = 7050: '<p>' 7051: .&mt('The ID on the form is [_1]', 7052: "<tt>$$scan_record{'scantron.ID'}</tt>") 7053: .'<br />' 7054: .&mt('The name on the paper is [_2], [_3]', 7055: $$scan_record{'scantron.LastName'}, 7056: $$scan_record{'scantron.FirstName'}) 7057: .'</p>'; 7058: 7059: $r->print('<input type="hidden" name="scantron_corrections" value="'.$error.'" />'."\n"); 7060: $r->print('<input type="hidden" name="scantron_line" value="'.$i.'" />'."\n"); 7061: # Array populated for doublebubble or 7062: my @lines_to_correct; # missingbubble errors to build javascript 7063: # to validate radio button checking 7064: 7065: if ($error =~ /ID$/) { 7066: if ($error eq 'incorrectID') { 7067: $r->print('<p class="LC_warning">'.&mt("The encoded ID is not in the classlist"). 7068: "</p>\n"); 7069: } elsif ($error eq 'duplicateID') { 7070: $r->print('<p class="LC_warning">'.&mt("The encoded ID has also been used by a previous paper [_1]",$arg)."</p>\n"); 7071: } 7072: $r->print($message); 7073: $r->print("<p>".&mt("How should I handle this?")." <br /> \n"); 7074: $r->print("\n<ul><li> "); 7075: #FIXME it would be nice if this sent back the user ID and 7076: #could do partial userID matches 7077: $r->print(&Apache::loncommon::selectstudent_link('scantronupload', 7078: 'scantron_username','scantron_domain')); 7079: $r->print(": <input type='text' name='scantron_username' value='' />"); 7080: $r->print("\n@". 7081: &Apache::loncommon::select_dom_form($env{'request.role.domain'},'scantron_domain')); 7082: 7083: $r->print('</li>'); 7084: } elsif ($error =~ /CODE$/) { 7085: if ($error eq 'incorrectCODE') { 7086: $r->print('<p class="LC_warning">'.&mt("The encoded CODE is not in the list of possible CODEs.")."</p>\n"); 7087: } elsif ($error eq 'duplicateCODE') { 7088: $r->print('<p class="LC_warning">'.&mt("The encoded CODE has also been used by a previous paper [_1], and CODEs are supposed to be unique.",join(', ',@{$arg}))."</p>\n"); 7089: } 7090: $r->print("<p>".&mt('The CODE on the form is [_1]', 7091: "<tt>'$$scan_record{'scantron.CODE'}'</tt>") 7092: ."</p>\n"); 7093: $r->print($message); 7094: $r->print("<p>".&mt("How should I handle this?")."</p>\n"); 7095: $r->print("\n<br /> "); 7096: my $i=0; 7097: if ($error eq 'incorrectCODE' 7098: && $$scan_record{'scantron.CODE'}=~/\S/ ) { 7099: my ($max,$closest)=&scantron_get_closely_matching_CODEs($arg,$$scan_record{'scantron.CODE'}); 7100: if ($closest > 0) { 7101: foreach my $testcode (@{$closest}) { 7102: my $checked=''; 7103: if (!$i) { $checked=' checked="checked"'; } 7104: $r->print(" 7105: <label> 7106: <input type='radio' name='scantron_CODE_resolution' value='use_closest_$i'$checked /> 7107: ".&mt("Use the similar CODE [_1] instead.", 7108: "<b><tt>".$testcode."</tt></b>")." 7109: </label> 7110: <input type='hidden' name='scantron_CODE_closest_$i' value='$testcode' />"); 7111: $r->print("\n<br />"); 7112: $i++; 7113: } 7114: } 7115: } 7116: if ($$scan_record{'scantron.CODE'}=~/\S/ ) { 7117: my $checked; if (!$i) { $checked=' checked="checked"'; } 7118: $r->print(" 7119: <label> 7120: <input type='radio' name='scantron_CODE_resolution' value='use_unfound'$checked /> 7121: ".&mt("Use the CODE [_1] that was on the paper, ignoring the error.", 7122: "<b><tt>".$$scan_record{'scantron.CODE'}."</tt></b>")." 7123: </label>"); 7124: $r->print("\n<br />"); 7125: } 7126: 7127: $r->print(<<ENDSCRIPT); 7128: <script type="text/javascript"> 7129: function change_radio(field) { 7130: var slct=document.scantronupload.scantron_CODE_resolution; 7131: var i; 7132: for (i=0;i<slct.length;i++) { 7133: if (slct[i].value==field) { slct[i].checked=true; } 7134: } 7135: } 7136: </script> 7137: ENDSCRIPT 7138: my $href="/adm/pickcode?". 7139: "form=".&escape("scantronupload"). 7140: "&scantron_format=".&escape($env{'form.scantron_format'}). 7141: "&scantron_CODElist=".&escape($env{'form.scantron_CODElist'}). 7142: "&curCODE=".&escape($$scan_record{'scantron.CODE'}). 7143: "&scantron_selectfile=".&escape($env{'form.scantron_selectfile'}); 7144: if ($env{'form.scantron_CODElist'} =~ /\S/) { 7145: $r->print(" 7146: <label> 7147: <input type='radio' name='scantron_CODE_resolution' value='use_found' /> 7148: ".&mt("[_1]Select[_2] a CODE from the list of all CODEs and use it.", 7149: "<a target='_blank' href='$href'>","</a>")." 7150: </label> 7151: ".&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\')" />')); 7152: $r->print("\n<br />"); 7153: } 7154: $r->print(" 7155: <label> 7156: <input type='radio' name='scantron_CODE_resolution' value='use_typed' /> 7157: ".&mt("Use [_1] as the CODE.", 7158: "</label><input type='text' size='8' name='scantron_CODE_newvalue' onfocus=\"javascript:change_radio('use_typed')\" onkeypress=\"javascript:change_radio('use_typed')\" />")); 7159: $r->print("\n<br /><br />"); 7160: } elsif ($error eq 'doublebubble') { 7161: $r->print('<p class="LC_warning">'.&mt("There have been multiple bubbles scanned for some question(s)")."</p>\n"); 7162: 7163: # The form field scantron_questions is acutally a list of line numbers. 7164: # represented by this form so: 7165: 7166: my $line_list = &questions_to_line_list($arg); 7167: 7168: $r->print('<input type="hidden" name="scantron_questions" value="'. 7169: $line_list.'" />'); 7170: $r->print($message); 7171: $r->print("<p>".&mt("Please indicate which bubble should be used for grading")."</p>"); 7172: foreach my $question (@{$arg}) { 7173: my @linenums = &prompt_for_corrections($r,$question,$scan_config, 7174: $scan_record, $error); 7175: push(@lines_to_correct,@linenums); 7176: } 7177: $r->print(&verify_bubbles_checked(@lines_to_correct)); 7178: } elsif ($error eq 'missingbubble') { 7179: $r->print('<p class="LC_warning">.&mt("There have been [_1]no[_2] bubbles scanned for some question(s)",'<b>','</b>')."</p>\n"); 7180: $r->print($message); 7181: $r->print("<p>".&mt("Please indicate which bubble should be used for grading.")."</p>"); 7182: $r->print(&mt("Some questions have no scanned bubbles.")."\n"); 7183: 7184: # The form field scantron_questions is actually a list of line numbers not 7185: # a list of question numbers. Therefore: 7186: # 7187: 7188: my $line_list = &questions_to_line_list($arg); 7189: 7190: $r->print('<input type="hidden" name="scantron_questions" value="'. 7191: $line_list.'" />'); 7192: foreach my $question (@{$arg}) { 7193: my @linenums = &prompt_for_corrections($r,$question,$scan_config, 7194: $scan_record, $error); 7195: push(@lines_to_correct,@linenums); 7196: } 7197: $r->print(&verify_bubbles_checked(@lines_to_correct)); 7198: } else { 7199: $r->print("\n<ul>"); 7200: } 7201: $r->print("\n</li></ul>"); 7202: } 7203: 7204: sub verify_bubbles_checked { 7205: my (@ansnums) = @_; 7206: my $ansnumstr = join('","',@ansnums); 7207: my $warning = &mt("A bubble or 'No bubble' selection has not been made for one or more lines."); 7208: my $output = (<<ENDSCRIPT); 7209: <script type="text/javascript"> 7210: function verify_bubble_radio(form) { 7211: var ansnumArray = new Array ("$ansnumstr"); 7212: var need_bubble_count = 0; 7213: for (var i=0; i<ansnumArray.length; i++) { 7214: if (form.elements["scantron_correct_Q_"+ansnumArray[i]].length > 1) { 7215: var bubble_picked = 0; 7216: for (var j=0; j<form.elements["scantron_correct_Q_"+ansnumArray[i]].length; j++) { 7217: if (form.elements["scantron_correct_Q_"+ansnumArray[i]][j].checked == true) { 7218: bubble_picked = 1; 7219: } 7220: } 7221: if (bubble_picked == 0) { 7222: need_bubble_count ++; 7223: } 7224: } 7225: } 7226: if (need_bubble_count) { 7227: alert("$warning"); 7228: return; 7229: } 7230: form.submit(); 7231: } 7232: </script> 7233: ENDSCRIPT 7234: return $output; 7235: } 7236: 7237: =pod 7238: 7239: =item questions_to_line_list 7240: 7241: Converts a list of questions into a string of comma separated 7242: line numbers in the answer sheet used by the questions. This is 7243: used to fill in the scantron_questions form field. 7244: 7245: Arguments: 7246: questions - Reference to an array of questions. 7247: 7248: =cut 7249: 7250: 7251: sub questions_to_line_list { 7252: my ($questions) = @_; 7253: my @lines; 7254: 7255: foreach my $item (@{$questions}) { 7256: my $question = $item; 7257: my ($first,$count,$last); 7258: if ($item =~ /^(\d+)\.(\d+)$/) { 7259: $question = $1; 7260: my $subquestion = $2; 7261: $first = $first_bubble_line{$question-1} + 1; 7262: my @subans = split(/,/,$subdivided_bubble_lines{$question-1}); 7263: my $subcount = 1; 7264: while ($subcount<$subquestion) { 7265: $first += $subans[$subcount-1]; 7266: $subcount ++; 7267: } 7268: $count = $subans[$subquestion-1]; 7269: } else { 7270: $first = $first_bubble_line{$question-1} + 1; 7271: $count = $bubble_lines_per_response{$question-1}; 7272: } 7273: $last = $first+$count-1; 7274: push(@lines, ($first..$last)); 7275: } 7276: return join(',', @lines); 7277: } 7278: 7279: =pod 7280: 7281: =item prompt_for_corrections 7282: 7283: Prompts for a potentially multiline correction to the 7284: user's bubbling (factors out common code from scantron_get_correction 7285: for multi and missing bubble cases). 7286: 7287: Arguments: 7288: $r - Apache request object. 7289: $question - The question number to prompt for. 7290: $scan_config - The scantron file configuration hash. 7291: $scan_record - Reference to the hash that has the the parsed scanlines. 7292: $error - Type of error 7293: 7294: Implicit inputs: 7295: %bubble_lines_per_response - Starting line numbers for each question. 7296: Numbered from 0 (but question numbers are from 7297: 1. 7298: %first_bubble_line - Starting bubble line for each question. 7299: %subdivided_bubble_lines - optionresponse, matchresponse and rankresponse 7300: type problems render as separate sub-questions, 7301: in exam mode. This hash contains a 7302: comma-separated list of the lines per 7303: sub-question. 7304: %responsetype_per_response - essayresponse, formularesponse, 7305: stringresponse, imageresponse, reactionresponse, 7306: and organicresponse type problem parts can have 7307: multiple lines per response if the weight 7308: assigned exceeds 10. In this case, only 7309: one bubble per line is permitted, but more 7310: than one line might contain bubbles, e.g. 7311: bubbling of: line 1 - J, line 2 - J, 7312: line 3 - B would assign 22 points. 7313: 7314: =cut 7315: 7316: sub prompt_for_corrections { 7317: my ($r, $question, $scan_config, $scan_record, $error) = @_; 7318: my ($current_line,$lines); 7319: my @linenums; 7320: my $questionnum = $question; 7321: if ($question =~ /^(\d+)\.(\d+)$/) { 7322: $question = $1; 7323: $current_line = $first_bubble_line{$question-1} + 1 ; 7324: my $subquestion = $2; 7325: my @subans = split(/,/,$subdivided_bubble_lines{$question-1}); 7326: my $subcount = 1; 7327: while ($subcount<$subquestion) { 7328: $current_line += $subans[$subcount-1]; 7329: $subcount ++; 7330: } 7331: $lines = $subans[$subquestion-1]; 7332: } else { 7333: $current_line = $first_bubble_line{$question-1} + 1 ; 7334: $lines = $bubble_lines_per_response{$question-1}; 7335: } 7336: if ($lines > 1) { 7337: $r->print(&mt('The group of bubble lines below responds to a single question.').'<br />'); 7338: if (($responsetype_per_response{$question-1} eq 'essayresponse') || 7339: ($responsetype_per_response{$question-1} eq 'formularesponse') || 7340: ($responsetype_per_response{$question-1} eq 'stringresponse') || 7341: ($responsetype_per_response{$question-1} eq 'imageresponse') || 7342: ($responsetype_per_response{$question-1} eq 'reactionresponse') || 7343: ($responsetype_per_response{$question-1} eq 'organicresponse')) { 7344: $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 />'); 7345: } else { 7346: $r->print(&mt("Select at most one bubble in a single line and select 'No Bubble' in all the other lines. ")."<br />"); 7347: } 7348: } 7349: for (my $i =0; $i < $lines; $i++) { 7350: my $selected = $$scan_record{"scantron.$current_line.answer"}; 7351: &scantron_bubble_selector($r,$scan_config,$current_line, 7352: $questionnum,$error,split('', $selected)); 7353: push(@linenums,$current_line); 7354: $current_line++; 7355: } 7356: if ($lines > 1) { 7357: $r->print("<hr /><br />"); 7358: } 7359: return @linenums; 7360: } 7361: 7362: =pod 7363: 7364: =item scantron_bubble_selector 7365: 7366: Generates the html radiobuttons to correct a single bubble line 7367: possibly showing the existing the selected bubbles if known 7368: 7369: Arguments: 7370: $r - Apache request object 7371: $scan_config - hash from &get_scantron_config() 7372: $line - Number of the line being displayed. 7373: $questionnum - Question number (may include subquestion) 7374: $error - Type of error. 7375: @selected - Array of bubbles picked on this line. 7376: 7377: =cut 7378: 7379: sub scantron_bubble_selector { 7380: my ($r,$scan_config,$line,$questionnum,$error,@selected)=@_; 7381: my $max=$$scan_config{'Qlength'}; 7382: 7383: my $scmode=$$scan_config{'Qon'}; 7384: if ($scmode eq 'number' || $scmode eq 'letter') { $max=10; } 7385: 7386: my @alphabet=('A'..'Z'); 7387: $r->print(&Apache::loncommon::start_data_table(). 7388: &Apache::loncommon::start_data_table_row()); 7389: $r->print('<td rowspan="2" class="LC_leftcol_header">'.$line.'</td>'); 7390: for (my $i=0;$i<$max+1;$i++) { 7391: $r->print("\n".'<td align="center">'); 7392: if ($selected[0] eq $alphabet[$i]) { $r->print('X'); shift(@selected) } 7393: else { $r->print(' '); } 7394: $r->print('</td>'); 7395: } 7396: $r->print(&Apache::loncommon::end_data_table_row(). 7397: &Apache::loncommon::start_data_table_row()); 7398: for (my $i=0;$i<$max;$i++) { 7399: $r->print("\n". 7400: '<td><label><input type="radio" name="scantron_correct_Q_'. 7401: $line.'" value="'.$i.'" />'.$alphabet[$i]."</label></td>"); 7402: } 7403: my $nobub_checked = ' '; 7404: if ($error eq 'missingbubble') { 7405: $nobub_checked = ' checked = "checked" '; 7406: } 7407: $r->print("\n".'<td><label><input type="radio" name="scantron_correct_Q_'. 7408: $line.'" value="none"'.$nobub_checked.'/>'.&mt('No bubble'). 7409: '</label>'."\n".'<input type="hidden" name="scantron_questionnum_Q_'. 7410: $line.'" value="'.$questionnum.'" /></td>'); 7411: $r->print(&Apache::loncommon::end_data_table_row(). 7412: &Apache::loncommon::end_data_table()); 7413: } 7414: 7415: =pod 7416: 7417: =item num_matches 7418: 7419: Counts the number of characters that are the same between the two arguments. 7420: 7421: Arguments: 7422: $orig - CODE from the scanline 7423: $code - CODE to match against 7424: 7425: Returns: 7426: $count - integer count of the number of same characters between the 7427: two arguments 7428: 7429: =cut 7430: 7431: sub num_matches { 7432: my ($orig,$code) = @_; 7433: my @code=split(//,$code); 7434: my @orig=split(//,$orig); 7435: my $same=0; 7436: for (my $i=0;$i<scalar(@code);$i++) { 7437: if ($code[$i] eq $orig[$i]) { $same++; } 7438: } 7439: return $same; 7440: } 7441: 7442: =pod 7443: 7444: =item scantron_get_closely_matching_CODEs 7445: 7446: Cycles through all CODEs and finds the set that has the greatest 7447: number of same characters as the provided CODE 7448: 7449: Arguments: 7450: $allcodes - hash ref returned by &get_codes() 7451: $CODE - CODE from the current scanline 7452: 7453: Returns: 7454: 2 element list 7455: - first elements is number of how closely matching the best fit is 7456: (5 means best set has 5 matching characters) 7457: - second element is an arrary ref containing the set of valid CODEs 7458: that best fit the passed in CODE 7459: 7460: =cut 7461: 7462: sub scantron_get_closely_matching_CODEs { 7463: my ($allcodes,$CODE)=@_; 7464: my @CODEs; 7465: foreach my $testcode (sort(keys(%{$allcodes}))) { 7466: push(@{$CODEs[&num_matches($CODE,$testcode)]},$testcode); 7467: } 7468: 7469: return ($#CODEs,$CODEs[-1]); 7470: } 7471: 7472: =pod 7473: 7474: =item get_codes 7475: 7476: Builds a hash which has keys of all of the valid CODEs from the selected 7477: set of remembered CODEs. 7478: 7479: Arguments: 7480: $old_name - name of the set of remembered CODEs 7481: $cdom - domain of the course 7482: $cnum - internal course name 7483: 7484: Returns: 7485: %allcodes - keys are the valid CODEs, values are all 1 7486: 7487: =cut 7488: 7489: sub get_codes { 7490: my ($old_name, $cdom, $cnum) = @_; 7491: if (!$old_name) { 7492: $old_name=$env{'form.scantron_CODElist'}; 7493: } 7494: if (!$cdom) { 7495: $cdom =$env{'course.'.$env{'request.course.id'}.'.domain'}; 7496: } 7497: if (!$cnum) { 7498: $cnum =$env{'course.'.$env{'request.course.id'}.'.num'}; 7499: } 7500: my %result=&Apache::lonnet::get('CODEs',[$old_name,"type\0$old_name"], 7501: $cdom,$cnum); 7502: my %allcodes; 7503: if ($result{"type\0$old_name"} eq 'number') { 7504: %allcodes=map {($_,1)} split(',',$result{$old_name}); 7505: } else { 7506: %allcodes=map {(&Apache::lonprintout::num_to_letters($_),1)} split(',',$result{$old_name}); 7507: } 7508: return %allcodes; 7509: } 7510: 7511: =pod 7512: 7513: =item scantron_validate_CODE 7514: 7515: Validates all scanlines in the selected file to not have any 7516: invalid or underspecified CODEs and that none of the codes are 7517: duplicated if this was requested. 7518: 7519: =cut 7520: 7521: sub scantron_validate_CODE { 7522: my ($r,$currentphase) = @_; 7523: my %scantron_config=&get_scantron_config($env{'form.scantron_format'}); 7524: if ($scantron_config{'CODElocation'} && 7525: $scantron_config{'CODEstart'} && 7526: $scantron_config{'CODElength'}) { 7527: if (!defined($env{'form.scantron_CODElist'})) { 7528: &FIXME_blow_up() 7529: } 7530: } else { 7531: return (0,$currentphase+1); 7532: } 7533: 7534: my %usedCODEs; 7535: 7536: my %allcodes=&get_codes(); 7537: 7538: my $nav_error; 7539: &scantron_get_maxbubble(\$nav_error); # parse needs the lines per response array. 7540: if ($nav_error) { 7541: $r->print(&navmap_errormsg()); 7542: return(1,$currentphase); 7543: } 7544: 7545: my ($scanlines,$scan_data)=&scantron_getfile(); 7546: for (my $i=0;$i<=$scanlines->{'count'};$i++) { 7547: my $line=&scantron_get_line($scanlines,$scan_data,$i); 7548: if ($line=~/^[\s\cz]*$/) { next; } 7549: my $scan_record=&scantron_parse_scanline($line,$i,\%scantron_config, 7550: $scan_data); 7551: my $CODE=$$scan_record{'scantron.CODE'}; 7552: my $error=0; 7553: if (!&Apache::lonnet::validCODE($CODE)) { 7554: &scantron_get_correction($r,$i,$scan_record, 7555: \%scantron_config, 7556: $line,'incorrectCODE',\%allcodes); 7557: return(1,$currentphase); 7558: } 7559: if (%allcodes && !exists($allcodes{$CODE}) 7560: && !$$scan_record{'scantron.useCODE'}) { 7561: &scantron_get_correction($r,$i,$scan_record, 7562: \%scantron_config, 7563: $line,'incorrectCODE',\%allcodes); 7564: return(1,$currentphase); 7565: } 7566: if (exists($usedCODEs{$CODE}) 7567: && $env{'form.scantron_CODEunique'} eq 'yes' 7568: && !$$scan_record{'scantron.CODE_ignore_dup'}) { 7569: &scantron_get_correction($r,$i,$scan_record, 7570: \%scantron_config, 7571: $line,'duplicateCODE',$usedCODEs{$CODE}); 7572: return(1,$currentphase); 7573: } 7574: push(@{$usedCODEs{$CODE}},$$scan_record{'scantron.PaperID'}); 7575: } 7576: return (0,$currentphase+1); 7577: } 7578: 7579: =pod 7580: 7581: =item scantron_validate_doublebubble 7582: 7583: Validates all scanlines in the selected file to not have any 7584: bubble lines with multiple bubbles marked. 7585: 7586: =cut 7587: 7588: sub scantron_validate_doublebubble { 7589: my ($r,$currentphase) = @_; 7590: #get student info 7591: my $classlist=&Apache::loncoursedata::get_classlist(); 7592: my %idmap=&username_to_idmap($classlist); 7593: 7594: #get scantron line setup 7595: my %scantron_config=&get_scantron_config($env{'form.scantron_format'}); 7596: my ($scanlines,$scan_data)=&scantron_getfile(); 7597: my $nav_error; 7598: &scantron_get_maxbubble(\$nav_error); # parse needs the bubble line array. 7599: if ($nav_error) { 7600: $r->print(&navmap_errormsg()); 7601: return(1,$currentphase); 7602: } 7603: 7604: for (my $i=0;$i<=$scanlines->{'count'};$i++) { 7605: my $line=&scantron_get_line($scanlines,$scan_data,$i); 7606: if ($line=~/^[\s\cz]*$/) { next; } 7607: my $scan_record=&scantron_parse_scanline($line,$i,\%scantron_config, 7608: $scan_data); 7609: if (!defined($$scan_record{'scantron.doubleerror'})) { next; } 7610: &scantron_get_correction($r,$i,$scan_record,\%scantron_config,$line, 7611: 'doublebubble', 7612: $$scan_record{'scantron.doubleerror'}); 7613: return (1,$currentphase); 7614: } 7615: return (0,$currentphase+1); 7616: } 7617: 7618: 7619: sub scantron_get_maxbubble { 7620: my ($nav_error) = @_; 7621: if (defined($env{'form.scantron_maxbubble'}) && 7622: $env{'form.scantron_maxbubble'}) { 7623: &restore_bubble_lines(); 7624: return $env{'form.scantron_maxbubble'}; 7625: } 7626: 7627: my (undef, undef, $sequence) = 7628: &Apache::lonnet::decode_symb($env{'form.selectpage'}); 7629: 7630: my $navmap=Apache::lonnavmaps::navmap->new(); 7631: unless (ref($navmap)) { 7632: if (ref($nav_error)) { 7633: $$nav_error = 1; 7634: } 7635: return; 7636: } 7637: my $map=$navmap->getResourceByUrl($sequence); 7638: my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0); 7639: 7640: &Apache::lonxml::clear_problem_counter(); 7641: 7642: my $uname = $env{'user.name'}; 7643: my $udom = $env{'user.domain'}; 7644: my $cid = $env{'request.course.id'}; 7645: my $total_lines = 0; 7646: %bubble_lines_per_response = (); 7647: %first_bubble_line = (); 7648: %subdivided_bubble_lines = (); 7649: %responsetype_per_response = (); 7650: 7651: my $response_number = 0; 7652: my $bubble_line = 0; 7653: foreach my $resource (@resources) { 7654: my ($analysis,$parts) = &scantron_partids_tograde($resource,$cid,$uname,$udom); 7655: if ((ref($analysis) eq 'HASH') && (ref($parts) eq 'ARRAY')) { 7656: foreach my $part_id (@{$parts}) { 7657: my $lines; 7658: 7659: # TODO - make this a persistent hash not an array. 7660: 7661: # optionresponse, matchresponse and rankresponse type items 7662: # render as separate sub-questions in exam mode. 7663: if (($analysis->{$part_id.'.type'} eq 'optionresponse') || 7664: ($analysis->{$part_id.'.type'} eq 'matchresponse') || 7665: ($analysis->{$part_id.'.type'} eq 'rankresponse')) { 7666: my ($numbub,$numshown); 7667: if ($analysis->{$part_id.'.type'} eq 'optionresponse') { 7668: if (ref($analysis->{$part_id.'.options'}) eq 'ARRAY') { 7669: $numbub = scalar(@{$analysis->{$part_id.'.options'}}); 7670: } 7671: } elsif ($analysis->{$part_id.'.type'} eq 'matchresponse') { 7672: if (ref($analysis->{$part_id.'.items'}) eq 'ARRAY') { 7673: $numbub = scalar(@{$analysis->{$part_id.'.items'}}); 7674: } 7675: } elsif ($analysis->{$part_id.'.type'} eq 'rankresponse') { 7676: if (ref($analysis->{$part_id.'.foils'}) eq 'ARRAY') { 7677: $numbub = scalar(@{$analysis->{$part_id.'.foils'}}); 7678: } 7679: } 7680: if (ref($analysis->{$part_id.'.shown'}) eq 'ARRAY') { 7681: $numshown = scalar(@{$analysis->{$part_id.'.shown'}}); 7682: } 7683: my $bubbles_per_line = 10; 7684: my $inner_bubble_lines = int($numbub/$bubbles_per_line); 7685: if (($numbub % $bubbles_per_line) != 0) { 7686: $inner_bubble_lines++; 7687: } 7688: for (my $i=0; $i<$numshown; $i++) { 7689: $subdivided_bubble_lines{$response_number} .= 7690: $inner_bubble_lines.','; 7691: } 7692: $subdivided_bubble_lines{$response_number} =~ s/,$//; 7693: $lines = $numshown * $inner_bubble_lines; 7694: } else { 7695: $lines = $analysis->{"$part_id.bubble_lines"}; 7696: } 7697: 7698: $first_bubble_line{$response_number} = $bubble_line; 7699: $bubble_lines_per_response{$response_number} = $lines; 7700: $responsetype_per_response{$response_number} = 7701: $analysis->{$part_id.'.type'}; 7702: $response_number++; 7703: 7704: $bubble_line += $lines; 7705: $total_lines += $lines; 7706: } 7707: } 7708: } 7709: &Apache::lonnet::delenv('scantron.'); 7710: 7711: &save_bubble_lines(); 7712: $env{'form.scantron_maxbubble'} = 7713: $total_lines; 7714: return $env{'form.scantron_maxbubble'}; 7715: } 7716: 7717: sub scantron_validate_missingbubbles { 7718: my ($r,$currentphase) = @_; 7719: #get student info 7720: my $classlist=&Apache::loncoursedata::get_classlist(); 7721: my %idmap=&username_to_idmap($classlist); 7722: 7723: #get scantron line setup 7724: my %scantron_config=&get_scantron_config($env{'form.scantron_format'}); 7725: my ($scanlines,$scan_data)=&scantron_getfile(); 7726: my $nav_error; 7727: my $max_bubble=&scantron_get_maxbubble(\$nav_error); 7728: if ($nav_error) { 7729: return(1,$currentphase); 7730: } 7731: if (!$max_bubble) { $max_bubble=2**31; } 7732: for (my $i=0;$i<=$scanlines->{'count'};$i++) { 7733: my $line=&scantron_get_line($scanlines,$scan_data,$i); 7734: if ($line=~/^[\s\cz]*$/) { next; } 7735: my $scan_record=&scantron_parse_scanline($line,$i,\%scantron_config, 7736: $scan_data); 7737: if (!defined($$scan_record{'scantron.missingerror'})) { next; } 7738: my @to_correct; 7739: 7740: # Probably here's where the error is... 7741: 7742: foreach my $missing (@{$$scan_record{'scantron.missingerror'}}) { 7743: my $lastbubble; 7744: if ($missing =~ /^(\d+)\.(\d+)$/) { 7745: my $question = $1; 7746: my $subquestion = $2; 7747: if (!defined($first_bubble_line{$question -1})) { next; } 7748: my $first = $first_bubble_line{$question-1}; 7749: my @subans = split(/,/,$subdivided_bubble_lines{$question-1}); 7750: my $subcount = 1; 7751: while ($subcount<$subquestion) { 7752: $first += $subans[$subcount-1]; 7753: $subcount ++; 7754: } 7755: my $count = $subans[$subquestion-1]; 7756: $lastbubble = $first + $count; 7757: } else { 7758: if (!defined($first_bubble_line{$missing - 1})) { next; } 7759: $lastbubble = $first_bubble_line{$missing - 1} + $bubble_lines_per_response{$missing - 1}; 7760: } 7761: if ($lastbubble > $max_bubble) { next; } 7762: push(@to_correct,$missing); 7763: } 7764: if (@to_correct) { 7765: &scantron_get_correction($r,$i,$scan_record,\%scantron_config, 7766: $line,'missingbubble',\@to_correct); 7767: return (1,$currentphase); 7768: } 7769: 7770: } 7771: return (0,$currentphase+1); 7772: } 7773: 7774: 7775: sub scantron_process_students { 7776: my ($r) = @_; 7777: 7778: my (undef,undef,$sequence)=&Apache::lonnet::decode_symb($env{'form.selectpage'}); 7779: my ($symb)=&get_symb($r); 7780: if (!$symb) { 7781: return ''; 7782: } 7783: my $default_form_data=&defaultFormData($symb); 7784: 7785: my %scantron_config=&get_scantron_config($env{'form.scantron_format'}); 7786: my ($scanlines,$scan_data)=&scantron_getfile(); 7787: my $classlist=&Apache::loncoursedata::get_classlist(); 7788: my %idmap=&username_to_idmap($classlist); 7789: my $navmap=Apache::lonnavmaps::navmap->new(); 7790: unless (ref($navmap)) { 7791: $r->print(&navmap_errormsg()); 7792: return ''; 7793: } 7794: my $map=$navmap->getResourceByUrl($sequence); 7795: my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0); 7796: my (%grader_partids_by_symb,%grader_randomlists_by_symb); 7797: &graders_resources_pass(\@resources,\%grader_partids_by_symb, 7798: \%grader_randomlists_by_symb); 7799: my $resource_error; 7800: foreach my $resource (@resources) { 7801: my $ressymb; 7802: if (ref($resource)) { 7803: $ressymb = $resource->symb(); 7804: } else { 7805: $resource_error = 1; 7806: last; 7807: } 7808: my ($analysis,$parts) = 7809: &scantron_partids_tograde($resource,$env{'request.course.id'}, 7810: $env{'user.name'},$env{'user.domain'},1); 7811: $grader_partids_by_symb{$ressymb} = $parts; 7812: if (ref($analysis) eq 'HASH') { 7813: if (ref($analysis->{'parts_withrandomlist'}) eq 'ARRAY') { 7814: $grader_randomlists_by_symb{$ressymb} = 7815: $analysis->{'parts_withrandomlist'}; 7816: } 7817: } 7818: } 7819: if ($resource_error) { 7820: $r->print(&navmap_errormsg()); 7821: return ''; 7822: } 7823: 7824: my ($uname,$udom); 7825: my $result= <<SCANTRONFORM; 7826: <form method="post" enctype="multipart/form-data" action="/adm/grades" name="scantronupload"> 7827: <input type="hidden" name="command" value="scantron_configphase" /> 7828: $default_form_data 7829: SCANTRONFORM 7830: $r->print($result); 7831: 7832: my @delayqueue; 7833: my (%completedstudents,%scandata); 7834: 7835: my $lock=&Apache::lonnet::set_lock(&mt('Grading bubblesheet exam')); 7836: my $count=&get_todo_count($scanlines,$scan_data); 7837: my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin($r,'Bubblesheet Status', 7838: 'Bubblesheet Progress',$count, 7839: 'inline',undef,'scantronupload'); 7840: &Apache::lonhtmlcommon::Update_PrgWin($r,\%prog_state, 7841: 'Processing first student'); 7842: $r->print('<br />'); 7843: my $start=&Time::HiRes::time(); 7844: my $i=-1; 7845: my $started; 7846: 7847: my $nav_error; 7848: &scantron_get_maxbubble(\$nav_error); # Need the bubble lines array to parse. 7849: if ($nav_error) { 7850: $r->print(&navmap_errormsg()); 7851: return ''; 7852: } 7853: 7854: # If an ssi failed in scantron_get_maxbubble, put an error message out to 7855: # the user and return. 7856: 7857: if ($ssi_error) { 7858: $r->print("</form>"); 7859: &ssi_print_error($r); 7860: $r->print(&show_grading_menu_form($symb)); 7861: &Apache::lonnet::remove_lock($lock); 7862: return ''; # Dunno why the other returns return '' rather than just returning. 7863: } 7864: 7865: my %lettdig = &letter_to_digits(); 7866: my $numletts = scalar(keys(%lettdig)); 7867: 7868: while ($i<$scanlines->{'count'}) { 7869: ($uname,$udom)=('',''); 7870: $i++; 7871: my $line=&scantron_get_line($scanlines,$scan_data,$i); 7872: if ($line=~/^[\s\cz]*$/) { next; } 7873: if ($started) { 7874: &Apache::lonhtmlcommon::Increment_PrgWin($r,\%prog_state, 7875: 'last student'); 7876: } 7877: $started=1; 7878: my $scan_record=&scantron_parse_scanline($line,$i,\%scantron_config, 7879: $scan_data); 7880: unless ($uname=&scantron_find_student($scan_record,$scan_data, 7881: \%idmap,$i)) { 7882: &scantron_add_delay(\@delayqueue,$line, 7883: 'Unable to find a student that matches',1); 7884: next; 7885: } 7886: if (exists $completedstudents{$uname}) { 7887: &scantron_add_delay(\@delayqueue,$line, 7888: 'Student '.$uname.' has multiple sheets',2); 7889: next; 7890: } 7891: ($uname,$udom)=split(/:/,$uname); 7892: 7893: my (%partids_by_symb,$res_error); 7894: foreach my $resource (@resources) { 7895: my $ressymb; 7896: if (ref($resource)) { 7897: $ressymb = $resource->symb(); 7898: } else { 7899: $res_error = 1; 7900: last; 7901: } 7902: if ((exists($grader_randomlists_by_symb{$ressymb})) || 7903: (ref($grader_partids_by_symb{$ressymb}) ne 'ARRAY')) { 7904: my ($analysis,$parts) = 7905: &scantron_partids_tograde($resource,$env{'request.course.id'},$uname,$udom); 7906: $partids_by_symb{$ressymb} = $parts; 7907: } else { 7908: $partids_by_symb{$ressymb} = $grader_partids_by_symb{$ressymb}; 7909: } 7910: } 7911: 7912: if ($res_error) { 7913: &scantron_add_delay(\@delayqueue,$line, 7914: 'An error occurred while grading student '.$uname,2); 7915: next; 7916: } 7917: 7918: &Apache::lonxml::clear_problem_counter(); 7919: &Apache::lonnet::appenv($scan_record); 7920: 7921: if (&scantron_clear_skip($scanlines,$scan_data,$i)) { 7922: &scantron_putfile($scanlines,$scan_data); 7923: } 7924: 7925: my $scancode; 7926: if ((exists($scan_record->{'scantron.CODE'})) && 7927: (&Apache::lonnet::validCODE($scan_record->{'scantron.CODE'}))) { 7928: $scancode = $scan_record->{'scantron.CODE'}; 7929: } else { 7930: $scancode = ''; 7931: } 7932: 7933: if (&grade_student_bubbles($r,$uname,$udom,$scan_record,$scancode, 7934: \@resources,\%partids_by_symb) eq 'ssi_error') { 7935: $ssi_error = 0; # So end of handler error message does not trigger. 7936: $r->print("</form>"); 7937: &ssi_print_error($r); 7938: $r->print(&show_grading_menu_form($symb)); 7939: &Apache::lonnet::remove_lock($lock); 7940: return ''; # Why return ''? Beats me. 7941: } 7942: 7943: $completedstudents{$uname}={'line'=>$line}; 7944: if ($env{'form.verifyrecord'}) { 7945: my $lastpos = $env{'form.scantron_maxbubble'}*$scantron_config{'Qlength'}; 7946: my $studentdata = substr($line,$scantron_config{'Qstart'}-1,$lastpos); 7947: chomp($studentdata); 7948: $studentdata =~ s/\r$//; 7949: my $studentrecord = ''; 7950: my $counter = -1; 7951: foreach my $resource (@resources) { 7952: my $ressymb = $resource->symb(); 7953: ($counter,my $recording) = 7954: &verify_scantron_grading($resource,$udom,$uname,$env{'request.course.id'}, 7955: $counter,$studentdata,$partids_by_symb{$ressymb}, 7956: \%scantron_config,\%lettdig,$numletts); 7957: $studentrecord .= $recording; 7958: } 7959: if ($studentrecord ne $studentdata) { 7960: &Apache::lonxml::clear_problem_counter(); 7961: if (&grade_student_bubbles($r,$uname,$udom,$scan_record,$scancode, 7962: \@resources,\%partids_by_symb) eq 'ssi_error') { 7963: $ssi_error = 0; # So end of handler error message does not trigger. 7964: $r->print("</form>"); 7965: &ssi_print_error($r); 7966: $r->print(&show_grading_menu_form($symb)); 7967: &Apache::lonnet::remove_lock($lock); 7968: delete($completedstudents{$uname}); 7969: return ''; 7970: } 7971: $counter = -1; 7972: $studentrecord = ''; 7973: foreach my $resource (@resources) { 7974: my $ressymb = $resource->symb(); 7975: ($counter,my $recording) = 7976: &verify_scantron_grading($resource,$udom,$uname,$env{'request.course.id'}, 7977: $counter,$studentdata,$partids_by_symb{$ressymb}, 7978: \%scantron_config,\%lettdig,$numletts); 7979: $studentrecord .= $recording; 7980: } 7981: if ($studentrecord ne $studentdata) { 7982: $r->print('<p><span class="LC_warning">'); 7983: if ($scancode eq '') { 7984: $r->print(&mt('Mismatch grading bubblesheet for user: [_1] with ID: [_2].', 7985: $uname.':'.$udom,$scan_record->{'scantron.ID'})); 7986: } else { 7987: $r->print(&mt('Mismatch grading bubblesheet for user: [_1] with ID: [_2] and CODE: [_3].', 7988: $uname.':'.$udom,$scan_record->{'scantron.ID'},$scancode)); 7989: } 7990: $r->print('</span><br />'.&Apache::loncommon::start_data_table()."\n". 7991: &Apache::loncommon::start_data_table_header_row()."\n". 7992: '<th>'.&mt('Source').'</th><th>'.&mt('Bubbled responses').'</th>'. 7993: &Apache::loncommon::end_data_table_header_row()."\n". 7994: &Apache::loncommon::start_data_table_row(). 7995: '<td>'.&mt('Bubblesheet').'</td>'. 7996: '<td><span class="LC_nobreak">'.$studentdata.'</span></td>'. 7997: &Apache::loncommon::end_data_table_row(). 7998: &Apache::loncommon::start_data_table_row(). 7999: '<td>'.&mt('Stored submissions').'</td>'. 8000: '<td><span class="LC_nobreak">'.$studentrecord.'</span></td>'."\n". 8001: &Apache::loncommon::end_data_table_row(). 8002: &Apache::loncommon::end_data_table().'</p>'); 8003: } else { 8004: $r->print('<br /><span class="LC_warning">'. 8005: &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 />'. 8006: &mt("As a consequence, this user's submission history records two tries."). 8007: '</span><br />'); 8008: } 8009: } 8010: } 8011: if (&Apache::loncommon::connection_aborted($r)) { last; } 8012: } continue { 8013: &Apache::lonxml::clear_problem_counter(); 8014: &Apache::lonnet::delenv('scantron.'); 8015: } 8016: &Apache::lonhtmlcommon::Close_PrgWin($r,\%prog_state); 8017: &Apache::lonnet::remove_lock($lock); 8018: # my $lasttime = &Time::HiRes::time()-$start; 8019: # $r->print("<p>took $lasttime</p>"); 8020: 8021: $r->print("</form>"); 8022: $r->print(&show_grading_menu_form($symb)); 8023: return ''; 8024: } 8025: 8026: sub graders_resources_pass { 8027: my ($resources,$grader_partids_by_symb,$grader_randomlists_by_symb) = @_; 8028: if ((ref($resources) eq 'ARRAY') && (ref($grader_partids_by_symb)) && 8029: (ref($grader_randomlists_by_symb) eq 'HASH')) { 8030: foreach my $resource (@{$resources}) { 8031: my $ressymb = $resource->symb(); 8032: my ($analysis,$parts) = 8033: &scantron_partids_tograde($resource,$env{'request.course.id'}, 8034: $env{'user.name'},$env{'user.domain'},1); 8035: $grader_partids_by_symb->{$ressymb} = $parts; 8036: if (ref($analysis) eq 'HASH') { 8037: if (ref($analysis->{'parts_withrandomlist'}) eq 'ARRAY') { 8038: $grader_randomlists_by_symb->{$ressymb} = 8039: $analysis->{'parts_withrandomlist'}; 8040: } 8041: } 8042: } 8043: } 8044: return; 8045: } 8046: 8047: sub grade_student_bubbles { 8048: my ($r,$uname,$udom,$scan_record,$scancode,$resources,$parts) = @_; 8049: if (ref($resources) eq 'ARRAY') { 8050: my $count = 0; 8051: foreach my $resource (@{$resources}) { 8052: my $ressymb = $resource->symb(); 8053: my %form = ('submitted' => 'scantron', 8054: 'grade_target' => 'grade', 8055: 'grade_username' => $uname, 8056: 'grade_domain' => $udom, 8057: 'grade_courseid' => $env{'request.course.id'}, 8058: 'grade_symb' => $ressymb, 8059: 'CODE' => $scancode 8060: ); 8061: if (ref($parts) eq 'HASH') { 8062: if (ref($parts->{$ressymb}) eq 'ARRAY') { 8063: foreach my $part (@{$parts->{$ressymb}}) { 8064: $form{'scantron_questnum_start.'.$part} = 8065: 1+$env{'form.scantron.first_bubble_line.'.$count}; 8066: $count++; 8067: } 8068: } 8069: } 8070: my $result=&ssi_with_retries($resource->src(),$ssi_retries,%form); 8071: return 'ssi_error' if ($ssi_error); 8072: last if (&Apache::loncommon::connection_aborted($r)); 8073: } 8074: } 8075: return; 8076: } 8077: 8078: sub scantron_upload_scantron_data { 8079: my ($r)=@_; 8080: my $dom = $env{'request.role.domain'}; 8081: my $domdesc = &Apache::lonnet::domain($dom,'description'); 8082: $r->print(&Apache::loncommon::coursebrowser_javascript($dom)); 8083: my $select_link=&Apache::loncommon::selectcourse_link('rules','courseid', 8084: 'domainid', 8085: 'coursename',$dom); 8086: my $syllabuslink = '<a href="javascript:ToSyllabus();">'.&mt('Syllabus').'</a>'. 8087: (' 'x2).&mt('(shows course personnel)'); 8088: my $default_form_data=&defaultFormData(&get_symb($r,1)); 8089: my $nofile_alert = &mt('Please use the browse button to select a file from your local directory.'); 8090: my $nocourseid_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."); 8091: $r->print(' 8092: <script type="text/javascript" language="javascript"> 8093: function checkUpload(formname) { 8094: if (formname.upfile.value == "") { 8095: alert("'.$nofile_alert.'"); 8096: return false; 8097: } 8098: if (formname.courseid.value == "") { 8099: alert("'.$nocourseid_alert.'"); 8100: return false; 8101: } 8102: formname.submit(); 8103: } 8104: 8105: function ToSyllabus() { 8106: var cdom = '."'$dom'".'; 8107: var cnum = document.rules.courseid.value; 8108: if (cdom == "" || cdom == null) { 8109: return; 8110: } 8111: if (cnum == "" || cnum == null) { 8112: return; 8113: } 8114: syllwin=window.open("/public/"+cdom+"/"+cnum+"/syllabus","LONCAPASyllabus", 8115: "height=350,width=350,scrollbars=yes,menubar=no"); 8116: return; 8117: } 8118: 8119: </script> 8120: 8121: <h3>'.&mt('Send bubblesheet data to a course').'</h3> 8122: 8123: <form enctype="multipart/form-data" action="/adm/grades" name="rules" method="post"> 8124: '.$default_form_data. 8125: &Apache::lonhtmlcommon::start_pick_box(). 8126: &Apache::lonhtmlcommon::row_title(&mt('Course ID')). 8127: '<input name="courseid" type="text" size="30" />'.$select_link. 8128: &Apache::lonhtmlcommon::row_closure(). 8129: &Apache::lonhtmlcommon::row_title(&mt('Course Name')). 8130: '<input name="coursename" type="text" size="30" />'.$syllabuslink. 8131: &Apache::lonhtmlcommon::row_closure(). 8132: &Apache::lonhtmlcommon::row_title(&mt('Domain')). 8133: '<input name="domainid" type="hidden" />'.$domdesc. 8134: &Apache::lonhtmlcommon::row_closure(). 8135: &Apache::lonhtmlcommon::row_title(&mt('File to upload')). 8136: '<input type="file" name="upfile" size="50" />'. 8137: &Apache::lonhtmlcommon::row_closure(1). 8138: &Apache::lonhtmlcommon::end_pick_box().'<br /> 8139: 8140: <input name="command" value="scantronupload_save" type="hidden" /> 8141: <input type="button" onclick="javascript:checkUpload(this.form);" value="'.&mt('Upload Bubblesheet Data').'" /> 8142: </form> 8143: '); 8144: return ''; 8145: } 8146: 8147: 8148: sub scantron_upload_scantron_data_save { 8149: my($r)=@_; 8150: my ($symb)=&get_symb($r,1); 8151: my $doanotherupload= 8152: '<br /><form action="/adm/grades" method="post">'."\n". 8153: '<input type="hidden" name="command" value="scantronupload" />'."\n". 8154: '<input type="submit" name="submit" value="'.&mt('Do Another Upload').'" />'."\n". 8155: '</form>'."\n"; 8156: if (!&Apache::lonnet::allowed('usc',$env{'form.domainid'}) && 8157: !&Apache::lonnet::allowed('usc', 8158: $env{'form.domainid'}.'_'.$env{'form.courseid'})) { 8159: $r->print(&mt("You are not allowed to upload bubblesheet data to the requested course.")."<br />"); 8160: if ($symb) { 8161: $r->print(&show_grading_menu_form($symb)); 8162: } else { 8163: $r->print($doanotherupload); 8164: } 8165: return ''; 8166: } 8167: my %coursedata=&Apache::lonnet::coursedescription($env{'form.domainid'}.'_'.$env{'form.courseid'}); 8168: my $uploadedfile; 8169: $r->print('<h3>'.&mt("Uploading file to [_1]",$coursedata{'description'}).'</h3>'); 8170: if (length($env{'form.upfile'}) < 2) { 8171: $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>')); 8172: } else { 8173: my $result = 8174: &Apache::lonnet::userfileupload('upfile','','scantron','','','', 8175: $env{'form.courseid'},$env{'form.domainid'}); 8176: if ($result =~ m{^/uploaded/}) { 8177: $r->print(&mt('[_1]Success:[_2] Successfully uploaded [_3] bytes of data into location: [_4]', 8178: '<span class="LC_success">','</span>',(length($env{'form.upfile'})-1), 8179: '<span class="LC_filename">'.$result.'</span>')); 8180: ($uploadedfile) = ($result =~ m{/([^/]+)$}); 8181: $r->print(&validate_uploaded_scantron_file($env{'form.domainid'}, 8182: $env{'form.courseid'},$uploadedfile)); 8183: } else { 8184: $r->print(&mt('[_1]Error:[_2] An error ([_3]) occurred when attempting to upload the file, [_4]', 8185: '<span class="LC_error">','</span>',$result, 8186: '<span class="LC_filename">'.&HTML::Entities::encode($env{'form.upfile.filename'},'<>&"').'</span>')); 8187: } 8188: } 8189: if ($symb) { 8190: $r->print(&scantron_selectphase($r,$uploadedfile)); 8191: } else { 8192: $r->print($doanotherupload); 8193: } 8194: return ''; 8195: } 8196: 8197: sub validate_uploaded_scantron_file { 8198: my ($cdom,$cname,$fname) = @_; 8199: my $scanlines=&Apache::lonnet::getfile('/uploaded/'.$cdom.'/'.$cname.'/'.$fname); 8200: my @lines; 8201: if ($scanlines ne '-1') { 8202: @lines=split("\n",$scanlines,-1); 8203: } 8204: my $output; 8205: if (@lines) { 8206: my (%counts,$max_match_format); 8207: my ($max_match_count,$max_match_pct) = (0,0); 8208: my $classlist = &Apache::loncoursedata::get_classlist($cdom,$cname); 8209: my %idmap = &username_to_idmap($classlist); 8210: foreach my $key (keys(%idmap)) { 8211: my $lckey = lc($key); 8212: $idmap{$lckey} = $idmap{$key}; 8213: } 8214: my %unique_formats; 8215: my @formatlines = &get_scantronformat_file(); 8216: foreach my $line (@formatlines) { 8217: chomp($line); 8218: my @config = split(/:/,$line); 8219: my $idstart = $config[5]; 8220: my $idlength = $config[6]; 8221: if (($idstart ne '') && ($idlength > 0)) { 8222: if (ref($unique_formats{$idstart.':'.$idlength}) eq 'ARRAY') { 8223: push(@{$unique_formats{$idstart.':'.$idlength}},$config[0].':'.$config[1]); 8224: } else { 8225: $unique_formats{$idstart.':'.$idlength} = [$config[0].':'.$config[1]]; 8226: } 8227: } 8228: } 8229: foreach my $key (keys(%unique_formats)) { 8230: my ($idstart,$idlength) = split(':',$key); 8231: %{$counts{$key}} = ( 8232: 'found' => 0, 8233: 'total' => 0, 8234: ); 8235: foreach my $line (@lines) { 8236: next if ($line =~ /^#/); 8237: next if ($line =~ /^[\s\cz]*$/); 8238: my $id = substr($line,$idstart-1,$idlength); 8239: $id = lc($id); 8240: if (exists($idmap{$id})) { 8241: $counts{$key}{'found'} ++; 8242: } 8243: $counts{$key}{'total'} ++; 8244: } 8245: if ($counts{$key}{'total'}) { 8246: my $percent_match = (100*$counts{$key}{'found'})/($counts{$key}{'total'}); 8247: if (($max_match_format eq '') || ($percent_match > $max_match_pct)) { 8248: $max_match_pct = $percent_match; 8249: $max_match_format = $key; 8250: $max_match_count = $counts{$key}{'total'}; 8251: } 8252: } 8253: } 8254: if (ref($unique_formats{$max_match_format}) eq 'ARRAY') { 8255: my $format_descs; 8256: my $numwithformat = @{$unique_formats{$max_match_format}}; 8257: for (my $i=0; $i<$numwithformat; $i++) { 8258: my ($name,$desc) = split(':',$unique_formats{$max_match_format}[$i]); 8259: if ($i<$numwithformat-2) { 8260: $format_descs .= '"<i>'.$desc.'</i>", '; 8261: } elsif ($i==$numwithformat-2) { 8262: $format_descs .= '"<i>'.$desc.'</i>" '.&mt('and').' '; 8263: } elsif ($i==$numwithformat-1) { 8264: $format_descs .= '"<i>'.$desc.'</i>"'; 8265: } 8266: } 8267: my $showpct = sprintf("%.0f",$max_match_pct).'%'; 8268: $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). 8269: '<br />'.&mt('A low percentage of matches results from one of the following:').'<ul>'. 8270: '<li>'.&mt('The file was uploaded to the wrong course').'</li>'. 8271: '<li>'.&mt('The data are not in the format expected for the domain: [_1]', 8272: '<i>'.$cdom.'</i>').'</li>'. 8273: '<li>'.&mt('Students did not bubble their IDs, or mis-bubbled them').'</li>'. 8274: '<li>'.&mt('The course roster is not up to date').'</li>'. 8275: '</ul>'; 8276: } 8277: } else { 8278: $output = '<span class="LC_warning">'.&mt('Uploaded file contained no data').'</span>'; 8279: } 8280: return $output; 8281: } 8282: 8283: sub valid_file { 8284: my ($requested_file)=@_; 8285: foreach my $filename (sort(&scantron_filenames())) { 8286: if ($requested_file eq $filename) { return 1; } 8287: } 8288: return 0; 8289: } 8290: 8291: sub scantron_download_scantron_data { 8292: my ($r)=@_; 8293: my $default_form_data=&defaultFormData(&get_symb($r,1)); 8294: my $cname=$env{'course.'.$env{'request.course.id'}.'.num'}; 8295: my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'}; 8296: my $file=$env{'form.scantron_selectfile'}; 8297: if (! &valid_file($file)) { 8298: $r->print(' 8299: <p> 8300: '.&mt('The requested file name was invalid.').' 8301: </p> 8302: '); 8303: $r->print(&show_grading_menu_form(&get_symb($r,1))); 8304: return; 8305: } 8306: my $orig='/uploaded/'.$cdom.'/'.$cname.'/scantron_orig_'.$file; 8307: my $corrected='/uploaded/'.$cdom.'/'.$cname.'/scantron_corrected_'.$file; 8308: my $skipped='/uploaded/'.$cdom.'/'.$cname.'/scantron_skipped_'.$file; 8309: &Apache::lonnet::allowuploaded('/adm/grades',$orig); 8310: &Apache::lonnet::allowuploaded('/adm/grades',$corrected); 8311: &Apache::lonnet::allowuploaded('/adm/grades',$skipped); 8312: $r->print(' 8313: <p> 8314: '.&mt('[_1]Original[_2] file as uploaded by the scantron office.', 8315: '<a href="'.$orig.'">','</a>').' 8316: </p> 8317: <p> 8318: '.&mt('[_1]Corrections[_2], a file of corrected records that were used in grading.', 8319: '<a href="'.$corrected.'">','</a>').' 8320: </p> 8321: <p> 8322: '.&mt('[_1]Skipped[_2], a file of records that were skipped.', 8323: '<a href="'.$skipped.'">','</a>').' 8324: </p> 8325: '); 8326: $r->print(&show_grading_menu_form(&get_symb($r,1))); 8327: return ''; 8328: } 8329: 8330: sub checkscantron_results { 8331: my ($r) = @_; 8332: my ($symb)=&get_symb($r); 8333: if (!$symb) {return '';} 8334: my $grading_menu_button=&show_grading_menu_form($symb); 8335: my $cid = $env{'request.course.id'}; 8336: my %lettdig = &letter_to_digits(); 8337: my $numletts = scalar(keys(%lettdig)); 8338: my $cnum = $env{'course.'.$cid.'.num'}; 8339: my $cdom = $env{'course.'.$cid.'.domain'}; 8340: my (undef, undef, $sequence) = &Apache::lonnet::decode_symb($env{'form.selectpage'}); 8341: my %record; 8342: my %scantron_config = 8343: &Apache::grades::get_scantron_config($env{'form.scantron_format'}); 8344: my ($scanlines,$scan_data)=&Apache::grades::scantron_getfile(); 8345: my $classlist=&Apache::loncoursedata::get_classlist(); 8346: my %idmap=&Apache::grades::username_to_idmap($classlist); 8347: my $navmap=Apache::lonnavmaps::navmap->new(); 8348: unless (ref($navmap)) { 8349: $r->print(&navmap_errormsg()); 8350: return ''; 8351: } 8352: my $map=$navmap->getResourceByUrl($sequence); 8353: my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0); 8354: my (%grader_partids_by_symb,%grader_randomlists_by_symb); 8355: &graders_resources_pass(\@resources,\%grader_partids_by_symb, \%grader_randomlists_by_symb); 8356: 8357: my ($uname,$udom); 8358: my (%scandata,%lastname,%bylast); 8359: $r->print(' 8360: <form method="post" enctype="multipart/form-data" action="/adm/grades" name="checkscantron">'."\n"); 8361: 8362: my @delayqueue; 8363: my %completedstudents; 8364: 8365: my $count=&Apache::grades::get_todo_count($scanlines,$scan_data); 8366: my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin($r,'Bubblesheet/Submissions Comparison Status', 8367: 'Progress of Bubblesheet Data/Submission Records Comparison',$count, 8368: 'inline',undef,'checkscantron'); 8369: my ($username,$domain,$started); 8370: my $nav_error; 8371: &scantron_get_maxbubble(\$nav_error); # Need the bubble lines array to parse. 8372: if ($nav_error) { 8373: $r->print(&navmap_errormsg()); 8374: return ''; 8375: } 8376: 8377: &Apache::lonhtmlcommon::Update_PrgWin($r,\%prog_state, 8378: 'Processing first student'); 8379: my $start=&Time::HiRes::time(); 8380: my $i=-1; 8381: 8382: while ($i<$scanlines->{'count'}) { 8383: ($username,$domain,$uname)=('','',''); 8384: $i++; 8385: my $line=&Apache::grades::scantron_get_line($scanlines,$scan_data,$i); 8386: if ($line=~/^[\s\cz]*$/) { next; } 8387: if ($started) { 8388: &Apache::lonhtmlcommon::Increment_PrgWin($r,\%prog_state, 8389: 'last student'); 8390: } 8391: $started=1; 8392: my $scan_record= 8393: &Apache::grades::scantron_parse_scanline($line,$i,\%scantron_config, 8394: $scan_data); 8395: unless ($uname=&Apache::grades::scantron_find_student($scan_record,$scan_data, 8396: \%idmap,$i)) { 8397: &Apache::grades::scantron_add_delay(\@delayqueue,$line, 8398: 'Unable to find a student that matches',1); 8399: next; 8400: } 8401: if (exists $completedstudents{$uname}) { 8402: &Apache::grades::scantron_add_delay(\@delayqueue,$line, 8403: 'Student '.$uname.' has multiple sheets',2); 8404: next; 8405: } 8406: my $pid = $scan_record->{'scantron.ID'}; 8407: $lastname{$pid} = $scan_record->{'scantron.LastName'}; 8408: push(@{$bylast{$lastname{$pid}}},$pid); 8409: my $lastpos = $env{'form.scantron_maxbubble'}*$scantron_config{'Qlength'}; 8410: $scandata{$pid} = substr($line,$scantron_config{'Qstart'}-1,$lastpos); 8411: chomp($scandata{$pid}); 8412: $scandata{$pid} =~ s/\r$//; 8413: ($username,$domain)=split(/:/,$uname); 8414: my $counter = -1; 8415: foreach my $resource (@resources) { 8416: my $parts; 8417: my $ressymb = $resource->symb(); 8418: if ((exists($grader_randomlists_by_symb{$ressymb})) || 8419: (ref($grader_partids_by_symb{$ressymb}) ne 'ARRAY')) { 8420: (my $analysis,$parts) = 8421: &scantron_partids_tograde($resource,$env{'request.course.id'},$username,$domain); 8422: } else { 8423: $parts = $grader_partids_by_symb{$ressymb}; 8424: } 8425: ($counter,my $recording) = 8426: &verify_scantron_grading($resource,$domain,$username,$cid,$counter, 8427: $scandata{$pid},$parts, 8428: \%scantron_config,\%lettdig,$numletts); 8429: $record{$pid} .= $recording; 8430: } 8431: } 8432: &Apache::lonhtmlcommon::Close_PrgWin($r,\%prog_state); 8433: $r->print('<br />'); 8434: my ($okstudents,$badstudents,$numstudents,$passed,$failed); 8435: $passed = 0; 8436: $failed = 0; 8437: $numstudents = 0; 8438: foreach my $last (sort(keys(%bylast))) { 8439: if (ref($bylast{$last}) eq 'ARRAY') { 8440: foreach my $pid (sort(@{$bylast{$last}})) { 8441: my $showscandata = $scandata{$pid}; 8442: my $showrecord = $record{$pid}; 8443: $showscandata =~ s/\s/ /g; 8444: $showrecord =~ s/\s/ /g; 8445: if ($scandata{$pid} eq $record{$pid}) { 8446: my $css_class = ($passed % 2)?'LC_odd_row':'LC_even_row'; 8447: $okstudents .= '<tr class="'.$css_class.'">'. 8448: '<td>'.&mt('Bubblesheet').'</td><td>'.$showscandata.'</td><td rowspan="2">'.$last.'</td><td rowspan="2">'.$pid.'</td>'."\n". 8449: '</tr>'."\n". 8450: '<tr class="'.$css_class.'">'."\n". 8451: '<td>Submissions</td><td>'.$showrecord.'</td></tr>'."\n"; 8452: $passed ++; 8453: } else { 8454: my $css_class = ($failed % 2)?'LC_odd_row':'LC_even_row'; 8455: $badstudents .= '<tr class="'.$css_class.'"><td>'.&mt('Bubblesheet').'</td><td><span class="LC_nobreak">'.$scandata{$pid}.'</span></td><td rowspan="2">'.$last.'</td><td rowspan="2">'.$pid.'</td>'."\n". 8456: '</tr>'."\n". 8457: '<tr class="'.$css_class.'">'."\n". 8458: '<td>Submissions</td><td><span class="LC_nobreak">'.$record{$pid}.'</span></td>'."\n". 8459: '</tr>'."\n"; 8460: $failed ++; 8461: } 8462: $numstudents ++; 8463: } 8464: } 8465: } 8466: $r->print('<p>'. 8467: &mt('Comparison of bubblesheet data (including corrections) with corresponding submission records (most recent submission) for [_1][quant,_2,student][_3] ([quant,_4,bubblesheet line] per student).', 8468: '<b>', 8469: $numstudents, 8470: '</b>', 8471: $env{'form.scantron_maxbubble'}). 8472: '</p>' 8473: ); 8474: $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>'); 8475: if ($passed) { 8476: $r->print(&mt('Students with exact correspondence between bubblesheet data and submissions are as follows:').'<br /><br />'); 8477: $r->print(&Apache::loncommon::start_data_table()."\n". 8478: &Apache::loncommon::start_data_table_header_row()."\n". 8479: '<th>'.&mt('Source').'</th><th>'.&mt('Bubble records').'</th><th>'.&mt('Name').'</th><th>'.&mt('ID').'</th>'. 8480: &Apache::loncommon::end_data_table_header_row()."\n". 8481: $okstudents."\n". 8482: &Apache::loncommon::end_data_table().'<br />'); 8483: } 8484: if ($failed) { 8485: $r->print(&mt('Students with differences between bubblesheet data and submissions are as follows:').'<br /><br />'); 8486: $r->print(&Apache::loncommon::start_data_table()."\n". 8487: &Apache::loncommon::start_data_table_header_row()."\n". 8488: '<th>'.&mt('Source').'</th><th>'.&mt('Bubble records').'</th><th>'.&mt('Name').'</th><th>'.&mt('ID').'</th>'. 8489: &Apache::loncommon::end_data_table_header_row()."\n". 8490: $badstudents."\n". 8491: &Apache::loncommon::end_data_table()).'<br />'. 8492: &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.'); 8493: } 8494: $r->print('</form><br />'.$grading_menu_button); 8495: return; 8496: } 8497: 8498: sub verify_scantron_grading { 8499: my ($resource,$domain,$username,$cid,$counter,$scandata,$partids, 8500: $scantron_config,$lettdig,$numletts) = @_; 8501: my ($record,%expected,%startpos); 8502: return ($counter,$record) if (!ref($resource)); 8503: return ($counter,$record) if (!$resource->is_problem()); 8504: my $symb = $resource->symb(); 8505: return ($counter,$record) if (ref($partids) ne 'ARRAY'); 8506: foreach my $part_id (@{$partids}) { 8507: $counter ++; 8508: $expected{$part_id} = 0; 8509: if ($env{"form.scantron.sub_bubblelines.$counter"}) { 8510: my @sub_lines = split(/,/,$env{"form.scantron.sub_bubblelines.$counter"}); 8511: foreach my $item (@sub_lines) { 8512: $expected{$part_id} += $item; 8513: } 8514: } else { 8515: $expected{$part_id} = $env{"form.scantron.bubblelines.$counter"}; 8516: } 8517: $startpos{$part_id} = $env{"form.scantron.first_bubble_line.$counter"}; 8518: } 8519: if ($symb) { 8520: my %recorded; 8521: my (%returnhash) = &Apache::lonnet::restore($symb,$cid,$domain,$username); 8522: if ($returnhash{'version'}) { 8523: my %lasthash=(); 8524: my $version; 8525: for ($version=1;$version<=$returnhash{'version'};$version++) { 8526: foreach my $key (sort(split(/\:/,$returnhash{$version.':keys'}))) { 8527: $lasthash{$key}=$returnhash{$version.':'.$key}; 8528: } 8529: } 8530: foreach my $key (keys(%lasthash)) { 8531: if ($key =~ /\.scantron$/) { 8532: my $value = &unescape($lasthash{$key}); 8533: my ($part_id) = ($key =~ /^resource\.(.+)\.scantron$/); 8534: if ($value eq '') { 8535: for (my $i=0; $i<$expected{$part_id}; $i++) { 8536: for (my $j=0; $j<$scantron_config->{'length'}; $j++) { 8537: $recorded{$part_id} .= $scantron_config->{'Qoff'}; 8538: } 8539: } 8540: } else { 8541: my @tocheck; 8542: my @items = split(//,$value); 8543: if (($scantron_config->{'Qon'} eq 'letter') || 8544: ($scantron_config->{'Qon'} eq 'number')) { 8545: if (@items < $expected{$part_id}) { 8546: my $fragment = substr($scandata,$startpos{$part_id},$expected{$part_id}); 8547: my @singles = split(//,$fragment); 8548: foreach my $pos (@singles) { 8549: if ($pos eq ' ') { 8550: push(@tocheck,$pos); 8551: } else { 8552: my $next = shift(@items); 8553: push(@tocheck,$next); 8554: } 8555: } 8556: } else { 8557: @tocheck = @items; 8558: } 8559: foreach my $letter (@tocheck) { 8560: if ($scantron_config->{'Qon'} eq 'letter') { 8561: if ($letter !~ /^[A-J]$/) { 8562: $letter = $scantron_config->{'Qoff'}; 8563: } 8564: $recorded{$part_id} .= $letter; 8565: } elsif ($scantron_config->{'Qon'} eq 'number') { 8566: my $digit; 8567: if ($letter !~ /^[A-J]$/) { 8568: $digit = $scantron_config->{'Qoff'}; 8569: } else { 8570: $digit = $lettdig->{$letter}; 8571: } 8572: $recorded{$part_id} .= $digit; 8573: } 8574: } 8575: } else { 8576: @tocheck = @items; 8577: for (my $i=0; $i<$expected{$part_id}; $i++) { 8578: my $curr_sub = shift(@tocheck); 8579: my $digit; 8580: if ($curr_sub =~ /^[A-J]$/) { 8581: $digit = $lettdig->{$curr_sub}-1; 8582: } 8583: if ($curr_sub eq 'J') { 8584: $digit += scalar($numletts); 8585: } 8586: for (my $j=0; $j<$scantron_config->{'Qlength'}; $j++) { 8587: if ($j == $digit) { 8588: $recorded{$part_id} .= $scantron_config->{'Qon'}; 8589: } else { 8590: $recorded{$part_id} .= $scantron_config->{'Qoff'}; 8591: } 8592: } 8593: } 8594: } 8595: } 8596: } 8597: } 8598: } 8599: foreach my $part_id (@{$partids}) { 8600: if ($recorded{$part_id} eq '') { 8601: for (my $i=0; $i<$expected{$part_id}; $i++) { 8602: for (my $j=0; $j<$scantron_config->{'Qlength'}; $j++) { 8603: $recorded{$part_id} .= $scantron_config->{'Qoff'}; 8604: } 8605: } 8606: } 8607: $record .= $recorded{$part_id}; 8608: } 8609: } 8610: return ($counter,$record); 8611: } 8612: 8613: sub letter_to_digits { 8614: my %lettdig = ( 8615: A => 1, 8616: B => 2, 8617: C => 3, 8618: D => 4, 8619: E => 5, 8620: F => 6, 8621: G => 7, 8622: H => 8, 8623: I => 9, 8624: J => 0, 8625: ); 8626: return %lettdig; 8627: } 8628: 8629: 8630: #-------- end of section for handling grading scantron forms ------- 8631: # 8632: #------------------------------------------------------------------- 8633: 8634: #-------------------------- Menu interface ------------------------- 8635: # 8636: #--- Show a Grading Menu button - Calls the next routine --- 8637: sub show_grading_menu_form { 8638: my ($symb)=@_; 8639: my $result.='<br /><form action="/adm/grades" method="post">'."\n". 8640: '<input type="hidden" name="symb" value="'.&Apache::lonenc::check_encrypt($symb).'" />'."\n". 8641: '<input type="hidden" name="saveState" value="'.$env{'form.saveState'}.'" />'."\n". 8642: '<input type="hidden" name="command" value="gradingmenu" />'."\n". 8643: '<input type="submit" name="submit" value="'.&mt('Grading Menu').'" />'."\n". 8644: '</form>'."\n"; 8645: return $result; 8646: } 8647: 8648: # -- Retrieve choices for grading form 8649: sub savedState { 8650: my %savedState = (); 8651: if ($env{'form.saveState'}) { 8652: foreach (split(/:/,$env{'form.saveState'})) { 8653: my ($key,$value) = split(/=/,$_,2); 8654: $savedState{$key} = $value; 8655: } 8656: } 8657: return \%savedState; 8658: } 8659: 8660: sub grading_menu { 8661: my ($request) = @_; 8662: my ($symb)=&get_symb($request); 8663: if (!$symb) {return '';} 8664: my $probTitle = &Apache::lonnet::gettitle($symb); 8665: my ($table,undef,$hdgrade) = &showResourceInfo($symb,$probTitle); 8666: 8667: $request->print($table); 8668: my %fields = ('symb'=>&Apache::lonenc::check_encrypt($symb), 8669: 'handgrade'=>$hdgrade, 8670: 'probTitle'=>$probTitle, 8671: 'command'=>'submit_options', 8672: 'saveState'=>"", 8673: 'gradingMenu'=>1, 8674: 'showgrading'=>"yes"); 8675: 8676: my $url1 = &Apache::lonhtmlcommon::build_url('grades/',\%fields); 8677: 8678: $fields{'command'} = 'csvform'; 8679: my $url2 = &Apache::lonhtmlcommon::build_url('grades/',\%fields); 8680: 8681: $fields{'command'} = 'processclicker'; 8682: my $url3 = &Apache::lonhtmlcommon::build_url('grades/',\%fields); 8683: 8684: $fields{'command'} = 'scantron_selectphase'; 8685: my $url4 = &Apache::lonhtmlcommon::build_url('grades/',\%fields); 8686: 8687: my @menu = ({ categorytitle=>'Course Grading', 8688: items =>[ 8689: { linktext => 'Manual Grading/View Submissions', 8690: url => $url1, 8691: permission => 'F', 8692: icon => 'edit-find-replace.png', 8693: linktitle => 'Start the process of hand grading submissions.' 8694: }, 8695: { linktext => 'Upload Scores', 8696: url => $url2, 8697: permission => 'F', 8698: icon => 'uploadscores.png', 8699: linktitle => 'Specify a file containing the class scores for current resource.' 8700: }, 8701: { linktext => 'Process Clicker', 8702: url => $url3, 8703: permission => 'F', 8704: icon => 'addClickerInfoFile.png', 8705: linktitle => 'Specify a file containing the clicker information for this resource.' 8706: }, 8707: { linktext => 'Grade/Manage/Review Bubblesheets', 8708: url => $url4, 8709: permission => 'F', 8710: icon => 'stat.png', 8711: linktitle => 'Grade bubblesheet exams, upload/download bubblesheet data files, and review previously graded bubblesheet exams.' 8712: } 8713: ] 8714: }); 8715: 8716: #$fields{'command'} = 'verify'; 8717: #$url = &Apache::lonhtmlcommon::build_url('grades/',\%fields); 8718: # 8719: # Create the menu 8720: my $Str; 8721: # $Str .= '<h2>'.&mt('Please select a grading task').'</h2>'; 8722: $Str .= '<form method="post" action="" name="gradingMenu">'; 8723: $Str .= '<input type="hidden" name="command" value="" />'. 8724: '<input type="hidden" name="symb" value="'.&Apache::lonenc::check_encrypt($symb).'" />'."\n". 8725: '<input type="hidden" name="handgrade" value="'.$hdgrade.'" />'."\n". 8726: '<input type="hidden" name="probTitle" value="'.$probTitle.'" />'."\n". 8727: '<input type="hidden" name="saveState" value="" />'."\n". 8728: '<input type="hidden" name="gradingMenu" value="1" />'."\n". 8729: '<input type="hidden" name="showgrading" value="yes" />'."\n"; 8730: 8731: $Str .= Apache::lonhtmlcommon::generate_menu(@menu); 8732: #$menudata->{'jscript'} 8733: $Str .='<hr /><input type="button" value="'.&mt('Verify Receipt No.').'" '. 8734: ' onclick="javascript:checkChoice(document.forms.gradingMenu,\'5\',\'verify\')" '. 8735: ' /> '. 8736: &Apache::lonnet::recprefix($env{'request.course.id'}). 8737: '-<input type="text" name="receipt" size="4" onchange="javascript:checkReceiptNo(this.form,\'OK\')" />'; 8738: 8739: $Str .="</form>\n"; 8740: my $receiptalert = &mt("Please enter a receipt number given by a student in the receipt box."); 8741: $request->print(<<GRADINGMENUJS); 8742: <script type="text/javascript" language="javascript"> 8743: function checkChoice(formname,val,cmdx) { 8744: if (val <= 2) { 8745: var cmd = radioSelection(formname.radioChoice); 8746: var cmdsave = cmd; 8747: } else { 8748: cmd = cmdx; 8749: cmdsave = 'submission'; 8750: } 8751: formname.command.value = cmd; 8752: if (val < 5) formname.submit(); 8753: if (val == 5) { 8754: if (!checkReceiptNo(formname,'notOK')) { 8755: return false; 8756: } else { 8757: formname.submit(); 8758: } 8759: } 8760: } 8761: 8762: function checkReceiptNo(formname,nospace) { 8763: var receiptNo = formname.receipt.value; 8764: var checkOpt = false; 8765: if (nospace == "OK" && isNaN(receiptNo)) {checkOpt = true;} 8766: if (nospace == "notOK" && (isNaN(receiptNo) || receiptNo == "")) {checkOpt = true;} 8767: if (checkOpt) { 8768: alert("$receiptalert"); 8769: formname.receipt.value = ""; 8770: formname.receipt.focus(); 8771: return false; 8772: } 8773: return true; 8774: } 8775: </script> 8776: GRADINGMENUJS 8777: &commonJSfunctions($request); 8778: return $Str; 8779: } 8780: 8781: 8782: #--- Displays the submissions first page ------- 8783: sub submit_options { 8784: my ($request) = @_; 8785: my ($symb)=&get_symb($request); 8786: if (!$symb) {return '';} 8787: my $probTitle = &Apache::lonnet::gettitle($symb); 8788: 8789: my $receiptalert = &mt("Please enter a receipt number given by a student in the receipt box."); 8790: $request->print(<<GRADINGMENUJS); 8791: <script type="text/javascript" language="javascript"> 8792: function checkChoice(formname,val,cmdx) { 8793: if (val <= 2) { 8794: var cmd = radioSelection(formname.radioChoice); 8795: var cmdsave = cmd; 8796: } else { 8797: cmd = cmdx; 8798: cmdsave = 'submission'; 8799: } 8800: formname.command.value = cmd; 8801: formname.saveState.value = "saveCmd="+cmdsave+":saveSec="+pullDownSelection(formname.section)+ 8802: ":saveSub="+pullDownSelection(formname.submitonly)+":saveStatus="+pullDownSelection(formname.Status); 8803: if (val < 5) formname.submit(); 8804: if (val == 5) { 8805: if (!checkReceiptNo(formname,'notOK')) { return false;} 8806: formname.submit(); 8807: } 8808: if (val < 7) formname.submit(); 8809: } 8810: 8811: function checkReceiptNo(formname,nospace) { 8812: var receiptNo = formname.receipt.value; 8813: var checkOpt = false; 8814: if (nospace == "OK" && isNaN(receiptNo)) {checkOpt = true;} 8815: if (nospace == "notOK" && (isNaN(receiptNo) || receiptNo == "")) {checkOpt = true;} 8816: if (checkOpt) { 8817: alert("$receiptalert"); 8818: formname.receipt.value = ""; 8819: formname.receipt.focus(); 8820: return false; 8821: } 8822: return true; 8823: } 8824: </script> 8825: GRADINGMENUJS 8826: &commonJSfunctions($request); 8827: my ($table,undef,$hdgrade) = &showResourceInfo($symb,$probTitle); 8828: my $result; 8829: my (undef,$sections) = &getclasslist('all','0'); 8830: my $savedState = &savedState(); 8831: my $saveCmd = ($$savedState{'saveCmd'} eq '' ? 'submission' : $$savedState{'saveCmd'}); 8832: my $saveSec = ($$savedState{'saveSec'} eq '' ? 'all' : $$savedState{'saveSec'}); 8833: my $saveSub = ($$savedState{'saveSub'} eq '' ? 'all' : $$savedState{'saveSub'}); 8834: my $saveStatus = ($$savedState{'saveStatus'} eq '' ? 'Active' : $$savedState{'saveStatus'}); 8835: 8836: # Preselect sections 8837: my $selsec=""; 8838: if (ref($sections)) { 8839: foreach my $section (sort(@$sections)) { 8840: $selsec.='<option value="'.$section.'" '. 8841: ($saveSec eq $section ? 'selected="selected"':'').'>'.$section.'</option>'."\n"; 8842: } 8843: } 8844: 8845: $result.='<form action="/adm/grades" method="post" name="gradingMenu">'."\n". 8846: '<input type="hidden" name="symb" value="'.&Apache::lonenc::check_encrypt($symb).'" />'."\n". 8847: '<input type="hidden" name="handgrade" value="'.$hdgrade.'" />'."\n". 8848: '<input type="hidden" name="probTitle" value="'.$probTitle.'" />'."\n". 8849: '<input type="hidden" name="command" value="" />'."\n". 8850: '<input type="hidden" name="saveState" value="" />'."\n". 8851: '<input type="hidden" name="gradingMenu" value="1" />'."\n". 8852: '<input type="hidden" name="showgrading" value="yes" />'."\n"; 8853: 8854: $result.=' 8855: <h2> 8856: '.&mt('Grade Current Resource').' 8857: </h2> 8858: <div> 8859: '.$table.' 8860: </div> 8861: 8862: <div class="LC_columnSection"> 8863: 8864: <fieldset> 8865: <legend> 8866: '.&mt('Sections').' 8867: </legend> 8868: <select name="section" multiple="multiple" size="5">'."\n"; 8869: $result.= $selsec; 8870: $result.= '<option value="all" '.($saveSec eq 'all' ? 'selected="selected"' : ''). '>all</option></select> '; 8871: $result.=' 8872: </fieldset> 8873: 8874: <fieldset> 8875: <legend> 8876: '.&mt('Groups').' 8877: </legend> 8878: '.&Apache::lonstatistics::GroupSelect('group','multiple',5).' 8879: </fieldset> 8880: 8881: <fieldset> 8882: <legend> 8883: '.&mt('Access Status').' 8884: </legend> 8885: '.&Apache::lonhtmlcommon::StatusOptions($saveStatus,undef,5,undef,'mult').' 8886: </fieldset> 8887: 8888: <fieldset> 8889: <legend> 8890: '.&mt('Submission Status').' 8891: </legend> 8892: <select name="submitonly" size="5"> 8893: <option value="yes" '. ($saveSub eq 'yes' ? 'selected="selected"' : '').'>'.&mt('with submissions').'</option> 8894: <option value="queued" '. ($saveSub eq 'queued' ? 'selected="selected"' : '').'>'.&mt('in grading queue').'</option> 8895: <option value="graded" '. ($saveSub eq 'graded' ? 'selected="selected"' : '').'>'.&mt('with ungraded submissions').'</option> 8896: <option value="incorrect" '.($saveSub eq 'incorrect' ? 'selected="selected"' : '').'>'.&mt('with incorrect submissions').'</option> 8897: <option value="all" '. ($saveSub eq 'all' ? 'selected="selected"' : '').'>'.&mt('with any status').'</option> 8898: </select> 8899: </fieldset> 8900: 8901: </div> 8902: 8903: <br /> 8904: <div> 8905: <div> 8906: <label> 8907: <input type="radio" name="radioChoice" value="submission" '. 8908: ($saveCmd eq 'submission' ? 'checked="checked"' : '').' /> '. 8909: &mt('Select individual students to grade and view submissions.').' 8910: </label> 8911: </div> 8912: <div> 8913: <label> 8914: <input type="radio" name="radioChoice" value="viewgrades" '. 8915: ($saveCmd eq 'viewgrades' ? 'checked="checked"' : '').' /> '. 8916: &mt('Grade all selected students in a grading table.').' 8917: </label> 8918: </div> 8919: <div> 8920: <input type="button" onclick="javascript:checkChoice(this.form,\'2\');" value="'.&mt('Next').' →" /> 8921: </div> 8922: </div> 8923: 8924: 8925: <h2> 8926: '.&mt('Grade Complete Folder for One Student').' 8927: </h2> 8928: <div> 8929: <div> 8930: <label> 8931: <input type="radio" name="radioChoice" value="pickStudentPage" '. 8932: ($saveCmd eq 'pickStudentPage' ? 'checked="checked"' : '').' /> '. 8933: &mt('The <b>complete</b> page/sequence/folder: For one student').' 8934: </label> 8935: </div> 8936: <div> 8937: <input type="button" onclick="javascript:checkChoice(this.form,\'2\');" value="'.&mt('Next').' →" /> 8938: </div> 8939: </div> 8940: </form>'; 8941: $result .= &show_grading_menu_form($symb); 8942: return $result; 8943: } 8944: 8945: sub reset_perm { 8946: undef(%perm); 8947: } 8948: 8949: sub init_perm { 8950: &reset_perm(); 8951: foreach my $test_perm ('vgr','mgr','opa') { 8952: 8953: my $scope = $env{'request.course.id'}; 8954: if (!($perm{$test_perm}=&Apache::lonnet::allowed($test_perm,$scope))) { 8955: 8956: $scope .= '/'.$env{'request.course.sec'}; 8957: if ( $perm{$test_perm}= 8958: &Apache::lonnet::allowed($test_perm,$scope)) { 8959: $perm{$test_perm.'_section'}=$env{'request.course.sec'}; 8960: } else { 8961: delete($perm{$test_perm}); 8962: } 8963: } 8964: } 8965: } 8966: 8967: sub gather_clicker_ids { 8968: my %clicker_ids; 8969: 8970: my $classlist = &Apache::loncoursedata::get_classlist(); 8971: 8972: # Set up a couple variables. 8973: my $username_idx = &Apache::loncoursedata::CL_SNAME(); 8974: my $domain_idx = &Apache::loncoursedata::CL_SDOM(); 8975: my $status_idx = &Apache::loncoursedata::CL_STATUS(); 8976: 8977: foreach my $student (keys(%$classlist)) { 8978: if ($classlist->{$student}->[$status_idx] ne 'Active') { next; } 8979: my $username = $classlist->{$student}->[$username_idx]; 8980: my $domain = $classlist->{$student}->[$domain_idx]; 8981: my $clickers = 8982: (&Apache::lonnet::userenvironment($domain,$username,'clickers'))[1]; 8983: foreach my $id (split(/\,/,$clickers)) { 8984: $id=~s/^[\#0]+//; 8985: $id=~s/[\-\:]//g; 8986: if (exists($clicker_ids{$id})) { 8987: $clicker_ids{$id}.=','.$username.':'.$domain; 8988: } else { 8989: $clicker_ids{$id}=$username.':'.$domain; 8990: } 8991: } 8992: } 8993: return %clicker_ids; 8994: } 8995: 8996: sub gather_adv_clicker_ids { 8997: my %clicker_ids; 8998: my $cnum=$env{'course.'.$env{'request.course.id'}.'.num'}; 8999: my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'}; 9000: my %coursepersonnel=&Apache::lonnet::get_course_adv_roles($cdom.'/'.$cnum); 9001: foreach my $element (sort(keys(%coursepersonnel))) { 9002: foreach my $person (split(/\,/,$coursepersonnel{$element})) { 9003: my ($puname,$pudom)=split(/\:/,$person); 9004: my $clickers = 9005: (&Apache::lonnet::userenvironment($pudom,$puname,'clickers'))[1]; 9006: foreach my $id (split(/\,/,$clickers)) { 9007: $id=~s/^[\#0]+//; 9008: $id=~s/[\-\:]//g; 9009: if (exists($clicker_ids{$id})) { 9010: $clicker_ids{$id}.=','.$puname.':'.$pudom; 9011: } else { 9012: $clicker_ids{$id}=$puname.':'.$pudom; 9013: } 9014: } 9015: } 9016: } 9017: return %clicker_ids; 9018: } 9019: 9020: sub clicker_grading_parameters { 9021: return ('gradingmechanism' => 'scalar', 9022: 'upfiletype' => 'scalar', 9023: 'specificid' => 'scalar', 9024: 'pcorrect' => 'scalar', 9025: 'pincorrect' => 'scalar'); 9026: } 9027: 9028: sub process_clicker { 9029: my ($r)=@_; 9030: my ($symb)=&get_symb($r); 9031: if (!$symb) {return '';} 9032: my $result=&checkforfile_js(); 9033: $env{'form.probTitle'} = &Apache::lonnet::gettitle($symb); 9034: my ($table) = &showResourceInfo($symb,$env{'form.probTitle'}); 9035: $result.=$table; 9036: $result.='<br /><table width="100%" border="0"><tr><td bgcolor="#777777">'."\n"; 9037: $result.='<table width="100%" border="0"><tr bgcolor="#e6ffff"><td>'."\n"; 9038: $result.=' <b>'.&mt('Specify a file containing the clicker information for this resource.'). 9039: '</b></td></tr>'."\n"; 9040: $result.='<tr bgcolor="#ffffe6"><td>'."\n"; 9041: # Attempt to restore parameters from last session, set defaults if not present 9042: my %Saveable_Parameters=&clicker_grading_parameters(); 9043: &Apache::loncommon::restore_course_settings('grades_clicker', 9044: \%Saveable_Parameters); 9045: if (!$env{'form.pcorrect'}) { $env{'form.pcorrect'}=100; } 9046: if (!$env{'form.pincorrect'}) { $env{'form.pincorrect'}=100; } 9047: if (!$env{'form.gradingmechanism'}) { $env{'form.gradingmechanism'}='attendance'; } 9048: if (!$env{'form.upfiletype'}) { $env{'form.upfiletype'}='iclicker'; } 9049: 9050: my %checked; 9051: foreach my $gradingmechanism ('attendance','personnel','specific','given') { 9052: if ($env{'form.gradingmechanism'} eq $gradingmechanism) { 9053: $checked{$gradingmechanism}=' checked="checked"'; 9054: } 9055: } 9056: 9057: my $upload=&mt("Upload File"); 9058: my $type=&mt("Type"); 9059: my $attendance=&mt("Award points just for participation"); 9060: my $personnel=&mt("Correctness determined from response by course personnel"); 9061: my $specific=&mt("Correctness determined from response with clicker ID(s)"); 9062: my $given=&mt("Correctness determined from given list of answers").' '. 9063: '<font size="-2"><tt>('.&mt("Provide comma-separated list. Use '*' for any answer correct, '-' for skip").')</tt></font>'; 9064: my $pcorrect=&mt("Percentage points for correct solution"); 9065: my $pincorrect=&mt("Percentage points for incorrect solution"); 9066: my $selectform=&Apache::loncommon::select_form($env{'form.upfiletype'},'upfiletype', 9067: {'iclicker' => 'i>clicker', 9068: 'interwrite' => 'interwrite PRS'}); 9069: $symb = &Apache::lonenc::check_encrypt($symb); 9070: $result.=<<ENDUPFORM; 9071: <script type="text/javascript"> 9072: function sanitycheck() { 9073: // Accept only integer percentages 9074: document.forms.gradesupload.pcorrect.value=Math.round(document.forms.gradesupload.pcorrect.value); 9075: document.forms.gradesupload.pincorrect.value=Math.round(document.forms.gradesupload.pincorrect.value); 9076: // Find out grading choice 9077: for (i=0; i<document.forms.gradesupload.gradingmechanism.length; i++) { 9078: if (document.forms.gradesupload.gradingmechanism[i].checked) { 9079: gradingchoice=document.forms.gradesupload.gradingmechanism[i].value; 9080: } 9081: } 9082: // By default, new choice equals user selection 9083: newgradingchoice=gradingchoice; 9084: // Not good to give more points for false answers than correct ones 9085: if (Math.round(document.forms.gradesupload.pcorrect.value)<Math.round(document.forms.gradesupload.pincorrect.value)) { 9086: document.forms.gradesupload.pcorrect.value=document.forms.gradesupload.pincorrect.value; 9087: } 9088: // If new choice is attendance only, and old choice was correctness-based, restore defaults 9089: if ((gradingchoice=='attendance') && (document.forms.gradesupload.waschecked.value!='attendance')) { 9090: document.forms.gradesupload.pcorrect.value=100; 9091: document.forms.gradesupload.pincorrect.value=100; 9092: } 9093: // If the values are different, cannot be attendance only 9094: if ((Math.round(document.forms.gradesupload.pcorrect.value)!=Math.round(document.forms.gradesupload.pincorrect.value)) && 9095: (gradingchoice=='attendance')) { 9096: newgradingchoice='personnel'; 9097: } 9098: // Change grading choice to new one 9099: for (i=0; i<document.forms.gradesupload.gradingmechanism.length; i++) { 9100: if (document.forms.gradesupload.gradingmechanism[i].value==newgradingchoice) { 9101: document.forms.gradesupload.gradingmechanism[i].checked=true; 9102: } else { 9103: document.forms.gradesupload.gradingmechanism[i].checked=false; 9104: } 9105: } 9106: // Remember the old state 9107: document.forms.gradesupload.waschecked.value=newgradingchoice; 9108: } 9109: </script> 9110: <form method="post" enctype="multipart/form-data" action="/adm/grades" name="gradesupload"> 9111: <input type="hidden" name="symb" value="$symb" /> 9112: <input type="hidden" name="command" value="processclickerfile" /> 9113: <input type="hidden" name="probTitle" value="$env{'form.probTitle'}" /> 9114: <input type="hidden" name="saveState" value="$env{'form.saveState'}" /> 9115: <input type="file" name="upfile" size="50" /> 9116: <br /><label>$type: $selectform</label> 9117: <br /><label><input type="radio" name="gradingmechanism" value="attendance"$checked{'attendance'} onclick="sanitycheck()" />$attendance </label> 9118: <br /><label><input type="radio" name="gradingmechanism" value="personnel"$checked{'personnel'} onclick="sanitycheck()" />$personnel</label> 9119: <br /><label><input type="radio" name="gradingmechanism" value="specific"$checked{'specific'} onclick="sanitycheck()" />$specific </label> 9120: <input type="text" name="specificid" value="$env{'form.specificid'}" size="20" /> 9121: <br /><label><input type="radio" name="gradingmechanism" value="given"$checked{'given'} onclick="sanitycheck()" />$given </label> 9122: <br /> 9123: <input type="text" name="givenanswer" size="50" /> 9124: <input type="hidden" name="waschecked" value="$env{'form.gradingmechanism'}" /> 9125: <br /><label>$pcorrect: <input type="text" name="pcorrect" size="4" value="$env{'form.pcorrect'}" onchange="sanitycheck()" /></label> 9126: <br /><label>$pincorrect: <input type="text" name="pincorrect" size="4" value="$env{'form.pincorrect'}" onchange="sanitycheck()" /></label> 9127: <br /><input type="button" onclick="javascript:checkUpload(this.form);" value="$upload" /> 9128: </form> 9129: ENDUPFORM 9130: $result.='</td></tr></table>'."\n". 9131: '</td></tr></table><br /><br />'."\n"; 9132: $result.=&show_grading_menu_form($symb); 9133: return $result; 9134: } 9135: 9136: sub process_clicker_file { 9137: my ($r)=@_; 9138: my ($symb)=&get_symb($r); 9139: if (!$symb) {return '';} 9140: 9141: my %Saveable_Parameters=&clicker_grading_parameters(); 9142: &Apache::loncommon::store_course_settings('grades_clicker', 9143: \%Saveable_Parameters); 9144: 9145: my ($result) = &showResourceInfo($symb,$env{'form.probTitle'}); 9146: if (($env{'form.gradingmechanism'} eq 'specific') && ($env{'form.specificid'}!~/\w/)) { 9147: $result.='<span class="LC_error">'.&mt('You need to specify a clicker ID for the correct answer').'</span>'; 9148: return $result.&show_grading_menu_form($symb); 9149: } 9150: if (($env{'form.gradingmechanism'} eq 'given') && ($env{'form.givenanswer'}!~/\S/)) { 9151: $result.='<span class="LC_error">'.&mt('You need to specify the correct answer').'</span>'; 9152: return $result.&show_grading_menu_form($symb); 9153: } 9154: my $foundgiven=0; 9155: if ($env{'form.gradingmechanism'} eq 'given') { 9156: $env{'form.givenanswer'}=~s/^\s*//gs; 9157: $env{'form.givenanswer'}=~s/\s*$//gs; 9158: $env{'form.givenanswer'}=~s/[^a-zA-Z0-9\.\*\-\+]+/\,/g; 9159: $env{'form.givenanswer'}=uc($env{'form.givenanswer'}); 9160: my @answers=split(/\,/,$env{'form.givenanswer'}); 9161: $foundgiven=$#answers+1; 9162: } 9163: my %clicker_ids=&gather_clicker_ids(); 9164: my %correct_ids; 9165: if ($env{'form.gradingmechanism'} eq 'personnel') { 9166: %correct_ids=&gather_adv_clicker_ids(); 9167: } 9168: if ($env{'form.gradingmechanism'} eq 'specific') { 9169: foreach my $correct_id (split(/[\s\,]/,$env{'form.specificid'})) {; 9170: $correct_id=~tr/a-z/A-Z/; 9171: $correct_id=~s/\s//gs; 9172: $correct_id=~s/^[\#0]+//; 9173: $correct_id=~s/[\-\:]//g; 9174: if ($correct_id) { 9175: $correct_ids{$correct_id}='specified'; 9176: } 9177: } 9178: } 9179: if ($env{'form.gradingmechanism'} eq 'attendance') { 9180: $result.=&mt('Score based on attendance only'); 9181: } elsif ($env{'form.gradingmechanism'} eq 'given') { 9182: $result.=&mt('Score based on [_1] ([_2] answers)','<tt>'.$env{'form.givenanswer'}.'</tt>',$foundgiven); 9183: } else { 9184: my $number=0; 9185: $result.='<p><b>'.&mt('Correctness determined by the following IDs').'</b>'; 9186: foreach my $id (sort(keys(%correct_ids))) { 9187: $result.='<br /><tt>'.$id.'</tt> - '; 9188: if ($correct_ids{$id} eq 'specified') { 9189: $result.=&mt('specified'); 9190: } else { 9191: my ($uname,$udom)=split(/\:/,$correct_ids{$id}); 9192: $result.=&Apache::loncommon::plainname($uname,$udom); 9193: } 9194: $number++; 9195: } 9196: $result.="</p>\n"; 9197: if ($number==0) { 9198: $result.='<span class="LC_error">'.&mt('No IDs found to determine correct answer').'</span>'; 9199: return $result.&show_grading_menu_form($symb); 9200: } 9201: } 9202: if (length($env{'form.upfile'}) < 2) { 9203: $result.=&mt('[_1] Error: [_2] The file you attempted to upload, [_3] contained no information. Please check that you entered the correct filename.', 9204: '<span class="LC_error">', 9205: '</span>', 9206: '<span class="LC_filename">'.&HTML::Entities::encode($env{'form.upfile.filename'},'<>&"').'</span>'); 9207: return $result.&show_grading_menu_form($symb); 9208: } 9209: 9210: # Were able to get all the info needed, now analyze the file 9211: 9212: $result.=&Apache::loncommon::studentbrowser_javascript(); 9213: $symb = &Apache::lonenc::check_encrypt($symb); 9214: my $heading=&mt('Scanning clicker file'); 9215: $result.=(<<ENDHEADER); 9216: <br /><table width="100%" border="0"><tr><td bgcolor="#777777"> 9217: <table width="100%" border="0"><tr bgcolor="#e6ffff"><td> 9218: <b>$heading</b></td></tr><tr bgcolor="#ffffe6"><td> 9219: <form method="post" action="/adm/grades" name="clickeranalysis"> 9220: <input type="hidden" name="symb" value="$symb" /> 9221: <input type="hidden" name="command" value="assignclickergrades" /> 9222: <input type="hidden" name="probTitle" value="$env{'form.probTitle'}" /> 9223: <input type="hidden" name="saveState" value="$env{'form.saveState'}" /> 9224: <input type="hidden" name="gradingmechanism" value="$env{'form.gradingmechanism'}" /> 9225: <input type="hidden" name="pcorrect" value="$env{'form.pcorrect'}" /> 9226: <input type="hidden" name="pincorrect" value="$env{'form.pincorrect'}" /> 9227: ENDHEADER 9228: if ($env{'form.gradingmechanism'} eq 'given') { 9229: $result.='<input type="hidden" name="correct:given" value="'.$env{'form.givenanswer'}.'" />'; 9230: } 9231: my %responses; 9232: my @questiontitles; 9233: my $errormsg=''; 9234: my $number=0; 9235: if ($env{'form.upfiletype'} eq 'iclicker') { 9236: ($errormsg,$number)=&iclicker_eval(\@questiontitles,\%responses); 9237: } 9238: if ($env{'form.upfiletype'} eq 'interwrite') { 9239: ($errormsg,$number)=&interwrite_eval(\@questiontitles,\%responses); 9240: } 9241: $result.='<br />'.&mt('Found [_1] question(s)',$number).'<br />'. 9242: '<input type="hidden" name="number" value="'.$number.'" />'. 9243: &mt('Awarding [_1] percent for correct and [_2] percent for incorrect responses', 9244: $env{'form.pcorrect'},$env{'form.pincorrect'}). 9245: '<br />'; 9246: if (($env{'form.gradingmechanism'} eq 'given') && ($number!=$foundgiven)) { 9247: $result.='<span class="LC_error">'.&mt('Number of given answers does not agree with number of questions in file.').'</span>'; 9248: return $result.&show_grading_menu_form($symb); 9249: } 9250: # Remember Question Titles 9251: # FIXME: Possibly need delimiter other than ":" 9252: for (my $i=0;$i<$number;$i++) { 9253: $result.='<input type="hidden" name="question:'.$i.'" value="'. 9254: &HTML::Entities::encode($questiontitles[$i],'"&<>').'" />'; 9255: } 9256: my $correct_count=0; 9257: my $student_count=0; 9258: my $unknown_count=0; 9259: # Match answers with usernames 9260: # FIXME: Possibly need delimiter other than ":" 9261: foreach my $id (keys(%responses)) { 9262: if ($correct_ids{$id}) { 9263: $result.="\n".'<input type="hidden" name="correct:'.$correct_count.':'.$correct_ids{$id}.'" value="'.$responses{$id}.'" />'; 9264: $correct_count++; 9265: } elsif ($clicker_ids{$id}) { 9266: if ($clicker_ids{$id}=~/\,/) { 9267: # More than one user with the same clicker! 9268: $result.="\n<hr />".&mt('Clicker registered more than once').": <tt>".$id."</tt><br />"; 9269: $result.="\n".'<input type="hidden" name="unknown:'.$id.'" value="'.$responses{$id}.'" />'. 9270: "<select name='multi".$id."'>"; 9271: foreach my $reguser (sort(split(/\,/,$clicker_ids{$id}))) { 9272: $result.="<option value='".$reguser."'>".&Apache::loncommon::plainname(split(/\:/,$reguser)).' ('.$reguser.')</option>'; 9273: } 9274: $result.='</select>'; 9275: $unknown_count++; 9276: } else { 9277: # Good: found one and only one user with the right clicker 9278: $result.="\n".'<input type="hidden" name="student:'.$clicker_ids{$id}.'" value="'.$responses{$id}.'" />'; 9279: $student_count++; 9280: } 9281: } else { 9282: $result.="\n<hr />".&mt('Unregistered Clicker')." <tt>".$id."</tt><br />"; 9283: $result.="\n".'<input type="hidden" name="unknown:'.$id.'" value="'.$responses{$id}.'" />'. 9284: "\n".&mt("Username").": <input type='text' name='uname".$id."' /> ". 9285: "\n".&mt("Domain").": ". 9286: &Apache::loncommon::select_dom_form($env{'course.'.$env{'request.course.id'}.'.domain'},'udom'.$id).' '. 9287: &Apache::loncommon::selectstudent_link('clickeranalysis','uname'.$id,'udom'.$id,0,$id); 9288: $unknown_count++; 9289: } 9290: } 9291: $result.='<hr />'. 9292: &mt('Found [_1] registered and [_2] unregistered clickers.',$student_count,$unknown_count); 9293: if (($env{'form.gradingmechanism'} ne 'attendance') && ($env{'form.gradingmechanism'} ne 'given')) { 9294: if ($correct_count==0) { 9295: $errormsg.="Found no correct answers answers for grading!"; 9296: } elsif ($correct_count>1) { 9297: $result.='<br /><span class="LC_warning">'.&mt("Found [_1] entries for grading!",$correct_count).'</span>'; 9298: } 9299: } 9300: if ($number<1) { 9301: $errormsg.="Found no questions."; 9302: } 9303: if ($errormsg) { 9304: $result.='<br /><span class="LC_error">'.&mt($errormsg).'</span>'; 9305: } else { 9306: $result.='<br /><input type="submit" name="finalize" value="'.&mt('Finalize Grading').'" />'; 9307: } 9308: $result.='</form></td></tr></table>'."\n". 9309: '</td></tr></table><br /><br />'."\n"; 9310: return $result.&show_grading_menu_form($symb); 9311: } 9312: 9313: sub iclicker_eval { 9314: my ($questiontitles,$responses)=@_; 9315: my $number=0; 9316: my $errormsg=''; 9317: foreach my $line (split(/[\n\r]/,$env{'form.upfile'})) { 9318: my %components=&Apache::loncommon::record_sep($line); 9319: my @entries=map {$components{$_}} (sort(keys(%components))); 9320: if ($entries[0] eq 'Question') { 9321: for (my $i=3;$i<$#entries;$i+=6) { 9322: $$questiontitles[$number]=$entries[$i]; 9323: $number++; 9324: } 9325: } 9326: if ($entries[0]=~/^\#/) { 9327: my $id=$entries[0]; 9328: my @idresponses; 9329: $id=~s/^[\#0]+//; 9330: for (my $i=0;$i<$number;$i++) { 9331: my $idx=3+$i*6; 9332: $entries[$idx]=~s/[^a-zA-Z0-9\.\*\-\+]+//g; 9333: push(@idresponses,$entries[$idx]); 9334: } 9335: $$responses{$id}=join(',',@idresponses); 9336: } 9337: } 9338: return ($errormsg,$number); 9339: } 9340: 9341: sub interwrite_eval { 9342: my ($questiontitles,$responses)=@_; 9343: my $number=0; 9344: my $errormsg=''; 9345: my $skipline=1; 9346: my $questionnumber=0; 9347: my %idresponses=(); 9348: foreach my $line (split(/[\n\r]/,$env{'form.upfile'})) { 9349: my %components=&Apache::loncommon::record_sep($line); 9350: my @entries=map {$components{$_}} (sort(keys(%components))); 9351: if ($entries[1] eq 'Time') { $skipline=0; next; } 9352: if ($entries[1] eq 'Response') { $skipline=1; } 9353: next if $skipline; 9354: if ($entries[0]!=$questionnumber) { 9355: $questionnumber=$entries[0]; 9356: $$questiontitles[$number]=&mt('Question [_1]',$questionnumber); 9357: $number++; 9358: } 9359: my $id=$entries[4]; 9360: $id=~s/^[\#0]+//; 9361: $id=~s/^v\d*\://i; 9362: $id=~s/[\-\:]//g; 9363: $idresponses{$id}[$number]=$entries[6]; 9364: } 9365: foreach my $id (keys(%idresponses)) { 9366: $$responses{$id}=join(',',@{$idresponses{$id}}); 9367: $$responses{$id}=~s/^\s*\,//; 9368: } 9369: return ($errormsg,$number); 9370: } 9371: 9372: sub assign_clicker_grades { 9373: my ($r)=@_; 9374: my ($symb)=&get_symb($r); 9375: if (!$symb) {return '';} 9376: # See which part we are saving to 9377: my $res_error; 9378: my ($partlist,$handgrade,$responseType) = &response_type($symb,\$res_error); 9379: if ($res_error) { 9380: return &navmap_errormsg(); 9381: } 9382: # FIXME: This should probably look for the first handgradeable part 9383: my $part=$$partlist[0]; 9384: # Start screen output 9385: my ($result) = &showResourceInfo($symb,$env{'form.probTitle'}).'<br />'; 9386: 9387: $result .= &Apache::loncommon::start_data_table(). 9388: &Apache::loncommon::start_data_table_header_row(). 9389: '<th>'.&mt('Assigning grades based on clicker file').'</th>'. 9390: &Apache::loncommon::end_data_table_header_row(). 9391: &Apache::loncommon::start_data_table_row().'<td>'; 9392: 9393: # Get correct result 9394: # FIXME: Possibly need delimiter other than ":" 9395: my @correct=(); 9396: my $gradingmechanism=$env{'form.gradingmechanism'}; 9397: my $number=$env{'form.number'}; 9398: if ($gradingmechanism ne 'attendance') { 9399: foreach my $key (keys(%env)) { 9400: if ($key=~/^form\.correct\:/) { 9401: my @input=split(/\,/,$env{$key}); 9402: for (my $i=0;$i<=$#input;$i++) { 9403: if (($correct[$i]) && ($input[$i]) && 9404: ($correct[$i] ne $input[$i])) { 9405: $result.='<br /><span class="LC_warning">'. 9406: &mt('More than one correct result given for question "[_1]": [_2] versus [_3].', 9407: $env{'form.question:'.$i},$correct[$i],$input[$i]).'</span>'; 9408: } elsif (($input[$i]) || ($input[$i] eq '0')) { 9409: $correct[$i]=$input[$i]; 9410: } 9411: } 9412: } 9413: } 9414: for (my $i=0;$i<$number;$i++) { 9415: if ((!$correct[$i]) && ($correct[$i] ne '0')) { 9416: $result.='<br /><span class="LC_error">'. 9417: &mt('No correct result given for question "[_1]"!', 9418: $env{'form.question:'.$i}).'</span>'; 9419: } 9420: } 9421: $result.='<br />'.&mt("Correct answer: [_1]",join(', ',map { ((($_) || ($_ eq '0'))?$_:'-') } @correct)); 9422: } 9423: # Start grading 9424: my $pcorrect=$env{'form.pcorrect'}; 9425: my $pincorrect=$env{'form.pincorrect'}; 9426: my $storecount=0; 9427: my %users=(); 9428: foreach my $key (keys(%env)) { 9429: my $user=''; 9430: if ($key=~/^form\.student\:(.*)$/) { 9431: $user=$1; 9432: } 9433: if ($key=~/^form\.unknown\:(.*)$/) { 9434: my $id=$1; 9435: if (($env{'form.uname'.$id}) && ($env{'form.udom'.$id})) { 9436: $user=$env{'form.uname'.$id}.':'.$env{'form.udom'.$id}; 9437: } elsif ($env{'form.multi'.$id}) { 9438: $user=$env{'form.multi'.$id}; 9439: } 9440: } 9441: if ($user) { 9442: if ($users{$user}) { 9443: $result.='<br /><span class="LC_warning">'. 9444: &mt("More than one entry found for <tt>[_1]</tt>!",$user). 9445: '</span><br />'; 9446: } 9447: $users{$user}=1; 9448: my @answer=split(/\,/,$env{$key}); 9449: my $sum=0; 9450: my $realnumber=$number; 9451: for (my $i=0;$i<$number;$i++) { 9452: if ($correct[$i] eq '-') { 9453: $realnumber--; 9454: } elsif ($answer[$i]) { 9455: if ($gradingmechanism eq 'attendance') { 9456: $sum+=$pcorrect; 9457: } elsif ($correct[$i] eq '*') { 9458: $sum+=$pcorrect; 9459: } else { 9460: # We actually grade if correct or not 9461: my $increment=$pincorrect; 9462: # Special case: numerical answer "0" 9463: if ($correct[$i] eq '0') { 9464: if ($answer[$i]=~/^[0\.]+$/) { 9465: $increment=$pcorrect; 9466: } 9467: # General numerical answer, both evaluate to something non-zero 9468: } elsif ((1.0*$correct[$i]!=0) && (1.0*$answer[$i]!=0)) { 9469: if (1.0*$correct[$i]==1.0*$answer[$i]) { 9470: $increment=$pcorrect; 9471: } 9472: # Must be just alphanumeric 9473: } elsif ($answer[$i] eq $correct[$i]) { 9474: $increment=$pcorrect; 9475: } 9476: $sum+=$increment; 9477: } 9478: } 9479: } 9480: my $ave=$sum/(100*$realnumber); 9481: # Store 9482: my ($username,$domain)=split(/\:/,$user); 9483: my %grades=(); 9484: $grades{"resource.$part.solved"}='correct_by_override'; 9485: $grades{"resource.$part.awarded"}=$ave; 9486: $grades{"resource.regrader"}="$env{'user.name'}:$env{'user.domain'}"; 9487: my $returncode=&Apache::lonnet::cstore(\%grades,$symb, 9488: $env{'request.course.id'}, 9489: $domain,$username); 9490: if ($returncode ne 'ok') { 9491: $result.="<br /><span class=\"LC_error\">Failed to save student $username:$domain. Message when trying to save was ($returncode)</span>"; 9492: } else { 9493: $storecount++; 9494: } 9495: } 9496: } 9497: # We are done 9498: $result.='<br />'.&mt('Successfully stored grades for [quant,_1,student].',$storecount). 9499: '</td>'. 9500: &Apache::loncommon::end_data_table_row(). 9501: &Apache::loncommon::end_data_table()."<br /><br />\n"; 9502: return $result.&show_grading_menu_form($symb); 9503: } 9504: 9505: sub navmap_errormsg { 9506: return '<div class="LC_error">'. 9507: &mt('An error occurred retrieving information about resources in the course.').'<br />'. 9508: &mt('It is recommended that you [_1]re-initialize the course[_2] and then return to this grading page.','<a href="/adm/roles?selectrole=1&newrole='.$env{'request.role'}.'">','</a>'). 9509: '</div>'; 9510: } 9511: 9512: sub handler { 9513: my $request=$_[0]; 9514: &reset_caches(); 9515: if ($request->header_only) { 9516: &Apache::loncommon::content_type($request,'text/html'); 9517: $request->send_http_header; 9518: return OK; 9519: } 9520: &Apache::loncommon::get_unprocessed_cgi($ENV{'QUERY_STRING'}); 9521: 9522: my $symb=&get_symb($request,1); 9523: my @commands=&Apache::loncommon::get_env_multiple('form.command'); 9524: my $command=$commands[0]; 9525: 9526: if ($#commands > 0) { 9527: &Apache::lonnet::logthis("grades got multiple commands ".join(':',@commands)); 9528: } 9529: 9530: $ssi_error = 0; 9531: my $brcrum = [{href=>"/adm/grades",text=>"Grading"}]; 9532: my $start_page = &Apache::loncommon::start_page('Grading',undef, 9533: {'bread_crumbs' => $brcrum}); 9534: if ($symb eq '' && $command eq '') { 9535: if ($env{'user.adv'}) { 9536: &Apache::loncommon::content_type($request,'text/html'); 9537: $request->send_http_header; 9538: $request->print($start_page); 9539: if (($env{'form.codeone'}) && ($env{'form.codetwo'}) && 9540: ($env{'form.codethree'})) { 9541: my $token=$env{'form.codeone'}.'*'.$env{'form.codetwo'}.'*'. 9542: $env{'form.codethree'}; 9543: my ($tsymb,$tuname,$tudom,$tcrsid)= 9544: &Apache::lonnet::checkin($token); 9545: if ($tsymb) { 9546: my ($map,$id,$url)=&Apache::lonnet::decode_symb($tsymb); 9547: if (&Apache::lonnet::allowed('mgr',$tcrsid)) { 9548: $request->print(&ssi_with_retries('/res/'.$url, $ssi_retries, 9549: ('grade_username' => $tuname, 9550: 'grade_domain' => $tudom, 9551: 'grade_courseid' => $tcrsid, 9552: 'grade_symb' => $tsymb))); 9553: } else { 9554: $request->print('<h3>Not authorized: '.$token.'</h3>'); 9555: } 9556: } else { 9557: $request->print('<h3>Not a valid DocID: '.$token.'</h3>'); 9558: } 9559: } else { 9560: $request->print(&Apache::lonxml::tokeninputfield()); 9561: } 9562: } elsif ($env{'request.course.id'}) { 9563: &init_perm(); 9564: if (!%perm) { 9565: $request->internal_redirect('/adm/quickgrades'); 9566: } else { 9567: &Apache::loncommon::content_type($request,'text/html'); 9568: $request->send_http_header; 9569: $request->print($start_page); 9570: } 9571: } 9572: } else { 9573: &init_perm(); 9574: if (!$env{'request.course.id'}) { 9575: # Not in a course. 9576: $env{'user.error.msg'}="/adm/grades::vgr:0:0:Cannot display grades page outside course context"; 9577: return HTTP_NOT_ACCEPTABLE; 9578: } elsif (!%perm) { 9579: $request->internal_redirect('/adm/quickgrades'); 9580: } 9581: &Apache::loncommon::content_type($request,'text/html'); 9582: $request->send_http_header; 9583: $request->print($start_page); 9584: if ($command eq 'submission' && $perm{'vgr'}) { 9585: ($env{'form.student'} eq '' ? &listStudents($request) : &submission($request,0,0)); 9586: } elsif ($command eq 'pickStudentPage' && $perm{'vgr'}) { 9587: &pickStudentPage($request); 9588: } elsif ($command eq 'displayPage' && $perm{'vgr'}) { 9589: &displayPage($request); 9590: } elsif ($command eq 'gradeByPage' && $perm{'mgr'}) { 9591: &updateGradeByPage($request); 9592: } elsif ($command eq 'processGroup' && $perm{'vgr'}) { 9593: &processGroup($request); 9594: } elsif ($command eq 'gradingmenu' && $perm{'vgr'}) { 9595: $request->print(&grading_menu($request)); 9596: } elsif ($command eq 'submit_options' && $perm{'vgr'}) { 9597: $request->print(&submit_options($request)); 9598: } elsif ($command eq 'viewgrades' && $perm{'vgr'}) { 9599: $request->print(&viewgrades($request)); 9600: } elsif ($command eq 'handgrade' && $perm{'mgr'}) { 9601: $request->print(&processHandGrade($request)); 9602: } elsif ($command eq 'editgrades' && $perm{'mgr'}) { 9603: $request->print(&editgrades($request)); 9604: } elsif ($command eq 'verify' && $perm{'vgr'}) { 9605: $request->print(&verifyreceipt($request)); 9606: } elsif ($command eq 'processclicker' && $perm{'mgr'}) { 9607: $request->print(&process_clicker($request)); 9608: } elsif ($command eq 'processclickerfile' && $perm{'mgr'}) { 9609: $request->print(&process_clicker_file($request)); 9610: } elsif ($command eq 'assignclickergrades' && $perm{'mgr'}) { 9611: $request->print(&assign_clicker_grades($request)); 9612: } elsif ($command eq 'csvform' && $perm{'mgr'}) { 9613: $request->print(&upcsvScores_form($request)); 9614: } elsif ($command eq 'csvupload' && $perm{'mgr'}) { 9615: $request->print(&csvupload($request)); 9616: } elsif ($command eq 'csvuploadmap' && $perm{'mgr'} ) { 9617: $request->print(&csvuploadmap($request)); 9618: } elsif ($command eq 'csvuploadoptions' && $perm{'mgr'}) { 9619: if ($env{'form.associate'} ne 'Reverse Association') { 9620: $request->print(&csvuploadoptions($request)); 9621: } else { 9622: if ( $env{'form.upfile_associate'} ne 'reverse' ) { 9623: $env{'form.upfile_associate'} = 'reverse'; 9624: } else { 9625: $env{'form.upfile_associate'} = 'forward'; 9626: } 9627: $request->print(&csvuploadmap($request)); 9628: } 9629: } elsif ($command eq 'csvuploadassign' && $perm{'mgr'} ) { 9630: $request->print(&csvuploadassign($request)); 9631: } elsif ($command eq 'scantron_selectphase' && $perm{'mgr'}) { 9632: $request->print(&scantron_selectphase($request)); 9633: } elsif ($command eq 'scantron_warning' && $perm{'mgr'}) { 9634: $request->print(&scantron_do_warning($request)); 9635: } elsif ($command eq 'scantron_validate' && $perm{'mgr'}) { 9636: $request->print(&scantron_validate_file($request)); 9637: } elsif ($command eq 'scantron_process' && $perm{'mgr'}) { 9638: $request->print(&scantron_process_students($request)); 9639: } elsif ($command eq 'scantronupload' && 9640: (&Apache::lonnet::allowed('usc',$env{'request.role.domain'})|| 9641: &Apache::lonnet::allowed('usc',$env{'request.course.id'}))) { 9642: $request->print(&scantron_upload_scantron_data($request)); 9643: } elsif ($command eq 'scantronupload_save' && 9644: (&Apache::lonnet::allowed('usc',$env{'request.role.domain'})|| 9645: &Apache::lonnet::allowed('usc',$env{'request.course.id'}))) { 9646: $request->print(&scantron_upload_scantron_data_save($request)); 9647: } elsif ($command eq 'scantron_download' && 9648: &Apache::lonnet::allowed('usc',$env{'request.course.id'})) { 9649: $request->print(&scantron_download_scantron_data($request)); 9650: } elsif ($command eq 'checksubmissions' && $perm{'vgr'}) { 9651: $request->print(&checkscantron_results($request)); 9652: } elsif ($command) { 9653: $request->print('<p class="LC_error">'.&mt('Access Denied ([_1])',$command).'</p>'); 9654: } 9655: } 9656: if ($ssi_error) { 9657: &ssi_print_error($request); 9658: } 9659: $request->print(&Apache::loncommon::end_page()); 9660: &reset_caches(); 9661: return OK; 9662: } 9663: 9664: 1; 9665: 9666: __END__; 9667: 9668: 9669: =head1 NAME 9670: 9671: Apache::grades 9672: 9673: =head1 SYNOPSIS 9674: 9675: Handles the viewing of grades. 9676: 9677: This is part of the LearningOnline Network with CAPA project 9678: described at http://www.lon-capa.org. 9679: 9680: =head1 OVERVIEW 9681: 9682: Do an ssi with retries: 9683: While I'd love to factor out this with the vesrion in lonprintout, 9684: 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 9685: I'm not quite ready to invent (e.g. an ssi_with_retry object). 9686: 9687: At least the logic that drives this has been pulled out into loncommon. 9688: 9689: 9690: 9691: ssi_with_retries - Does the server side include of a resource. 9692: if the ssi call returns an error we'll retry it up to 9693: the number of times requested by the caller. 9694: If we still have a proble, no text is appended to the 9695: output and we set some global variables. 9696: to indicate to the caller an SSI error occurred. 9697: All of this is supposed to deal with the issues described 9698: in LonCAPA BZ 5631 see: 9699: http://bugs.lon-capa.org/show_bug.cgi?id=5631 9700: by informing the user that this happened. 9701: 9702: Parameters: 9703: resource - The resource to include. This is passed directly, without 9704: interpretation to lonnet::ssi. 9705: form - The form hash parameters that guide the interpretation of the resource 9706: 9707: retries - Number of retries allowed before giving up completely. 9708: Returns: 9709: On success, returns the rendered resource identified by the resource parameter. 9710: Side Effects: 9711: The following global variables can be set: 9712: ssi_error - If an unrecoverable error occurred this becomes true. 9713: It is up to the caller to initialize this to false 9714: if desired. 9715: ssi_error_resource - If an unrecoverable error occurred, this is the value 9716: of the resource that could not be rendered by the ssi 9717: call. 9718: ssi_error_message - The error string fetched from the ssi response 9719: in the event of an error. 9720: 9721: 9722: =head1 HANDLER SUBROUTINE 9723: 9724: ssi_with_retries() 9725: 9726: =head1 SUBROUTINES 9727: 9728: =over 9729: 9730: =item scantron_get_correction() : 9731: 9732: Builds the interface screen to interact with the operator to fix a 9733: specific error condition in a specific scanline 9734: 9735: Arguments: 9736: $r - Apache request object 9737: $i - number of the current scanline 9738: $scan_record - hash ref as returned from &scantron_parse_scanline() 9739: $scan_config - hash ref as returned from &get_scantron_config() 9740: $line - full contents of the current scanline 9741: $error - error condition, valid values are 9742: 'incorrectCODE', 'duplicateCODE', 9743: 'doublebubble', 'missingbubble', 9744: 'duplicateID', 'incorrectID' 9745: $arg - extra information needed 9746: For errors: 9747: - duplicateID - paper number that this studentID was seen before on 9748: - duplicateCODE - array ref of the paper numbers this CODE was 9749: seen on before 9750: - incorrectCODE - current incorrect CODE 9751: - doublebubble - array ref of the bubble lines that have double 9752: bubble errors 9753: - missingbubble - array ref of the bubble lines that have missing 9754: bubble errors 9755: 9756: =item scantron_get_maxbubble() : 9757: 9758: Arguments: 9759: $nav_error - Reference to scalar which is a flag to indicate a 9760: failure to retrieve a navmap object. 9761: if $nav_error is set to 1 by scantron_get_maxbubble(), the 9762: calling routine should trap the error condition and display the warning 9763: found in &navmap_errormsg(). 9764: 9765: Returns the maximum number of bubble lines that are expected to 9766: occur. Does this by walking the selected sequence rendering the 9767: resource and then checking &Apache::lonxml::get_problem_counter() 9768: for what the current value of the problem counter is. 9769: 9770: Caches the results to $env{'form.scantron_maxbubble'}, 9771: $env{'form.scantron.bubble_lines.n'}, 9772: $env{'form.scantron.first_bubble_line.n'} and 9773: $env{"form.scantron.sub_bubblelines.n"} 9774: which are the total number of bubble, lines, the number of bubble 9775: lines for response n and number of the first bubble line for response n, 9776: and a comma separated list of numbers of bubble lines for sub-questions 9777: (for optionresponse, matchresponse, and rankresponse items), for response n. 9778: 9779: 9780: =item scantron_validate_missingbubbles() : 9781: 9782: Validates all scanlines in the selected file to not have any 9783: answers that don't have bubbles that have not been verified 9784: to be bubble free. 9785: 9786: =item scantron_process_students() : 9787: 9788: Routine that does the actual grading of the bubblesheet information. 9789: 9790: The parsed scanline hash is added to %env 9791: 9792: Then foreach unskipped scanline it does an &Apache::lonnet::ssi() 9793: foreach resource , with the form data of 9794: 9795: 'submitted' =>'scantron' 9796: 'grade_target' =>'grade', 9797: 'grade_username'=> username of student 9798: 'grade_domain' => domain of student 9799: 'grade_courseid'=> of course 9800: 'grade_symb' => symb of resource to grade 9801: 9802: This triggers a grading pass. The problem grading code takes care 9803: of converting the bubbled letter information (now in %env) into a 9804: valid submission. 9805: 9806: =item scantron_upload_scantron_data() : 9807: 9808: Creates the screen for adding a new bubblesheet data file to a course. 9809: 9810: =item scantron_upload_scantron_data_save() : 9811: 9812: Adds a provided bubble information data file to the course if user 9813: has the correct privileges to do so. 9814: 9815: =item valid_file() : 9816: 9817: Validates that the requested bubble data file exists in the course. 9818: 9819: =item scantron_download_scantron_data() : 9820: 9821: Shows a list of the three internal files (original, corrected, 9822: skipped) for a specific bubblesheet data file that exists in the 9823: course. 9824: 9825: =item scantron_validate_ID() : 9826: 9827: Validates all scanlines in the selected file to not have any 9828: invalid or underspecified student/employee IDs 9829: 9830: =item navmap_errormsg() : 9831: 9832: Returns HTML mark-up inside a <div></div> with a link to re-initialize the course. 9833: Should be called whenever the request to instantiate a navmap object fails. 9834: 9835: =back 9836: 9837: =cut