--- loncom/homework/grades.pm 2008/02/04 13:46:35 1.505 +++ loncom/homework/grades.pm 2008/12/21 22:01:35 1.539 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # The LON-CAPA Grading handler # -# $Id: grades.pm,v 1.505 2008/02/04 13:46:35 raeburn Exp $ +# $Id: grades.pm,v 1.539 2008/12/21 22:01:35 riegler Exp $ # # Copyright Michigan State University Board of Trustees # @@ -26,6 +26,8 @@ # http://www.lon-capa.org/ # + + package Apache::grades; use strict; use Apache::style; @@ -47,8 +49,51 @@ use LONCAPA; use POSIX qw(floor); + my %perm=(); +# These variables are used to recover from ssi errors + +my $ssi_retries = 5; +my $ssi_error; +my $ssi_error_resource; +my $ssi_error_message; + + +sub ssi_with_retries { + my ($resource, $retries, %form) = @_; + my ($content, $response) = &Apache::loncommon::ssi_with_retries($resource, $retries, %form); + if ($response->is_error) { + $ssi_error = 1; + $ssi_error_resource = $resource; + $ssi_error_message = $response->code . " " . $response->message; + } + + return $content; + +} +# +# Prodcuces an ssi retry failure error message to the user: +# + +sub ssi_print_error { + my ($r) = @_; + my $helpurl = &Apache::loncommon::top_nav_help('Helpdesk'); + $r->print(' +<br /> +<h2>'.&mt('An unrecoverable network error occurred:').'</h2> +<p> +'.&mt('Unable to retrieve a resource from a server:').'<br /> +'.&mt('Resource:').' '.$ssi_error_resource.'<br /> +'.&mt('Error:').' '.$ssi_error_message.' +</p> +<p>'. +&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 />'. +&mt('If the error persists, please contact the [_1] for assistance.',$helpurl). +'</p>'); + return; +} + # # --- Retrieve the parts from the metadata file.--- sub getpartlist { @@ -157,8 +202,8 @@ sub showResourceInfo { my %resptype = (); my $hdgrade='no'; my %partsseen; - foreach my $partID (sort keys(%$responseType)) { - foreach my $resID (sort keys(%{ $responseType->{$partID} })) { + foreach my $partID (sort(keys(%$responseType))) { + foreach my $resID (sort(keys(%{ $responseType->{$partID} }))) { my $handgrade=$$handgrade{$partID.'_'.$resID}; my $responsetype = $responseType->{$partID}->{$resID}; $hdgrade = $handgrade if ($handgrade eq 'yes'); @@ -172,9 +217,9 @@ sub showResourceInfo { $partsseen{$partID}=1; } my $display_part=&get_display_part($partID,$symb); - $result.='<td>'.&mt('<b>Part: </b>[_1]',$display_part).' <span class="LC_internal_info">'. - $resID.'</span></td>'. - '<td>'.&mt('<b>Type: </b>[_1]',$responsetype).'</td></tr>'; + $result.='<td><b>'.&mt('Part').': </b>'.$display_part. + ' <span class="LC_internal_info">'.$resID.'</span></td>'. + '<td><b>'.&mt('Type').': </b>'.$responsetype.'</td></tr>'; # '<td>'.&mt('<b>Handgrade: </b>[_1]',$handgrade).'</td></tr>'; } } @@ -195,27 +240,28 @@ sub reset_caches { } sub get_analyze { - my ($symb,$uname,$udom)=@_; + my ($symb,$uname,$udom,$no_increment)=@_; my $key = "$symb\0$uname\0$udom"; return $analyze_cache{$key} if (exists($analyze_cache{$key})); my (undef,undef,$url)=&Apache::lonnet::decode_symb($symb); $url=&Apache::lonnet::clutter($url); - my $subresult=&Apache::lonnet::ssi($url, - ('grade_target' => 'analyze'), - ('grade_domain' => $udom), - ('grade_symb' => $symb), - ('grade_courseid' => - $env{'request.course.id'}), - ('grade_username' => $uname)); + my $subresult=&ssi_with_retries($url, $ssi_retries, + ('grade_target' => 'analyze', + 'grade_domain' => $udom, + 'grade_symb' => $symb, + 'grade_courseid' => + $env{'request.course.id'}, + 'grade_username' => $uname, + 'grade_noincrement' => $no_increment)); (undef,$subresult)=split(/_HASH_REF__/,$subresult,2); my %analyze=&Apache::lonnet::str2hash($subresult); return $analyze_cache{$key} = \%analyze; } sub get_order { - my ($partid,$respid,$symb,$uname,$udom)=@_; - my $analyze = &get_analyze($symb,$uname,$udom); + my ($partid,$respid,$symb,$uname,$udom,$no_increment)=@_; + my $analyze = &get_analyze($symb,$uname,$udom,$no_increment); return $analyze->{"$partid.$respid.shown"}; } @@ -733,20 +779,20 @@ sub listStudents { my $getsec = $env{'form.section'} eq '' ? 'all' : $env{'form.section'}; my $getgroup = $env{'form.group'} eq '' ? 'all' : $env{'form.group'}; my $submitonly= $env{'form.submitonly'} eq '' ? 'all' : $env{'form.submitonly'}; - my $viewgrade = $env{'form.showgrading'} eq 'yes' ? 'View/Grade/Regrade' : 'View'; + my $viewgrade = $env{'form.showgrading'} eq 'yes' ? &mt('View/Grade/Regrade') : &mt('View'); $env{'form.probTitle'} = $env{'form.probTitle'} eq '' ? &Apache::lonnet::gettitle($symb) : $env{'form.probTitle'}; - my $result='<h3><span class="LC_info"> '. - &mt($viewgrade.' Submissions for a Student or a Group of Students') + my $result='<h3><span class="LC_info"> '.$viewgrade. + &mt(' Submissions for a Student or a Group of Students') .'</span></h3>'; my ($table,undef,$hdgrade,$partlist,$handgrade) = &showResourceInfo($symb,$env{'form.probTitle'},($env{'form.showgrading'} eq 'yes')); my %lt = ( 'multiple' => - "Please select a student or group of students before clicking on the Next button.", + &mt("Please select a student or group of students before clicking on the Next button."), 'single' => - "Please select the student before clicking on the Next button.", + &mt("Please select the student before clicking on the Next button."), ); %lt = &Apache::lonlocal::texthash(%lt); $request->print(<<LISTJAVASCRIPT); @@ -791,17 +837,15 @@ LISTJAVASCRIPT "\n".$table; $gradeTable .= - ' '. - &mt('<b>View Problem Text: </b>[_1]', + ' <b>'.&mt('View Problem Text').': </b>'. '<label><input type="radio" name="vProb" value="no" checked="checked" /> '.&mt('no').' </label>'."\n". '<label><input type="radio" name="vProb" value="yes" /> '.&mt('one student').' </label>'."\n". - '<label><input type="radio" name="vProb" value="all" /> '.&mt('all students').' </label>').'<br />'."\n"; + '<label><input type="radio" name="vProb" value="all" /> '.&mt('all students').' </label><br />'."\n"; $gradeTable .= - ' '. - &mt('<b>View Answer: </b>[_1]', + ' <b>'.&mt('View Answer').': </b>'. '<label><input type="radio" name="vAns" value="no" /> '.&mt('no').' </label>'."\n". '<label><input type="radio" name="vAns" value="yes" /> '.&mt('one student').' </label>'."\n". - '<label><input type="radio" name="vAns" value="all" checked="checked" /> '.&mt('all students').' </label>').'<br />'."\n"; + '<label><input type="radio" name="vAns" value="all" checked="checked" /> '.&mt('all students').' </label><br />'."\n"; my $submission_options; if ($env{'form.handgrade'} eq 'yes' && scalar(@$partlist) > 1) { @@ -817,18 +861,16 @@ LISTJAVASCRIPT '<label><input type="radio" name="lastSub" value="datesub" /> '.&mt('by dates and submissions').' </label>'."\n". '<label><input type="radio" name="lastSub" value="all" /> '.&mt('all details').'</label>'; $gradeTable .= - ' '. - &mt('<b>Submissions: </b>[_1]',$submission_options).'<br />'."\n"; + ' <b>'.&mt('Submissions').': </b>'.$submission_options.'<br />'."\n"; $gradeTable .= - ' '. - &mt('<b>Grading Increments:</b> [_1]', + ' <b>'.&mt('Grading Increments').': </b>'. '<select name="increment">'. '<option value="1">'.&mt('Whole Points').'</option>'. '<option value=".5">'.&mt('Half Points').'</option>'. '<option value=".25">'.&mt('Quarter Points').'</option>'. '<option value=".1">'.&mt('Tenths of a Point').'</option>'. - '</select>'); + '</select>'; $gradeTable .= &build_section_inputs(). @@ -847,15 +889,14 @@ LISTJAVASCRIPT &Apache::lonhtmlcommon::StatusOptions($saveStatus,undef,1,'javascript:reLoadList(this.form);')).'<br />'; } - $gradeTable.=&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.').'<br />'."\n". + $gradeTable.=&mt('To [_1] 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.',lc($viewgrade)).'<br />'."\n". '<input type="hidden" name="command" value="processGroup" />'."\n"; # checkall buttons $gradeTable.=&check_script('gradesub', 'stuinfo'); $gradeTable.='<input type="button" '."\n". 'onClick="javascript:checkSelect(this.form.stuinfo);" '."\n". - 'value="'.&mt('Next->').'" /> <br />'."\n"; + 'value="'.&mt('Next').' →" /> <br />'."\n"; $gradeTable.=&check_buttons(); $gradeTable.='<label><input type="checkbox" name="checkPlag" checked="checked" />'.&mt('Check For Plagiarism').'</label>'; my ($classlist, undef, $fullname) = &getclasslist($getsec,'1',$getgroup); @@ -947,7 +988,7 @@ LISTJAVASCRIPT ' '.$section.($group ne '' ?'/'.$group:'').'</td>'."\n"; if ($env{'form.showgrading'} eq 'yes' && $submitonly ne 'all') { - foreach (sort keys(%status)) { + foreach (sort(keys(%status))) { next if ($_ =~ /^resource.*?submitted_by$/); $gradeTable.='<td align="center"> '.&mt($status{$_}).' </td>'."\n"; } @@ -975,7 +1016,7 @@ LISTJAVASCRIPT $gradeTable.=&Apache::loncommon::end_data_table()."\n". '<input type="button" '. 'onClick="javascript:checkSelect(this.form.stuinfo);" '. - 'value="'.&mt('Next->').'" /></form>'."\n"; + 'value="'.&mt('Next').' →" /></form>'."\n"; if ($ctr == 0) { my $num_students=(scalar(keys(%$fullname))); if ($num_students eq 0) { @@ -1071,6 +1112,7 @@ sub processGroup { #--- Javascript to handle the submission page functionality --- sub sub_page_js { my $request = shift; + my $alertmsg = &mt('A number equal or greater than 0 is expected. Entered value = '); $request->print(<<SUBJAVASCRIPT); <script type="text/javascript" language="javascript"> function updateRadio(formname,id,weight) { @@ -1081,7 +1123,7 @@ sub sub_page_js { gradeBox.value = pts; var resetbox = false; if (isNaN(pts) || pts < 0) { - alert("A number equal or greater than 0 is expected. Entered value = "+pts); + alert("$alertmsg"+pts); for (var i=0; i<radioButton.length; i++) { if (radioButton[i].checked) { gradeBox.value = i; @@ -1326,6 +1368,7 @@ INNERJS my $docopen=&Apache::lonhtmlcommon::javascript_docopen(); $docopen=~s/^document\.//; + my $alertmsg = &mt('Please select a word or group of words from document and then click this link.'); $request->print(<<SUBJAVASCRIPT); <script type="text/javascript" language="javascript"> @@ -1358,7 +1401,7 @@ INNERJS else return; var cleantxt = txt.replace(new RegExp('([\\f\\n\\r\\t\\v ])+', 'g')," "); if (cleantxt=="") { - alert("Please select a word or group of words from document and then click this link."); + alert("$alertmsg"); return; } var nret = prompt("Add selection to keyword list? Edit if desired.",cleantxt); @@ -1597,7 +1640,7 @@ sub gradeBox { my $radio.='<table border="0"><tr>'."\n"; # display radio buttons in a nice table 10 across while ($thisweight<=$wgt) { - $radio.= '<td><span style="white-space: nowrap;"><label><input type="radio" name="RADVAL'.$counter.'_'.$partid.'" '. + $radio.= '<td><span class="LC_nobreak"><label><input type="radio" name="RADVAL'.$counter.'_'.$partid.'" '. 'onclick="javascript:writeBox(this.form,\''.$counter.'_'.$partid.'\','. $thisweight.')" value="'.$thisweight.'" '. ($score eq $thisweight ? 'checked="checked"':'').' /> '.$thisweight."</label></span></td>\n"; @@ -1759,9 +1802,9 @@ sub download_all_link { join("\n",&Apache::loncommon::get_env_multiple('form.vPart')); my $identifier = &Apache::loncommon::get_cgi_id(); - &Apache::lonnet::appenv('cgi.'.$identifier.'.students' => $all_students, - 'cgi.'.$identifier.'.symb' => $symb, - 'cgi.'.$identifier.'.parts' => $parts,); + &Apache::lonnet::appenv({'cgi.'.$identifier.'.students' => $all_students, + 'cgi.'.$identifier.'.symb' => $symb, + 'cgi.'.$identifier.'.parts' => $parts,}); $r->print('<a href="/cgi-bin/multidownload.pl?'.$identifier.'">'. &mt('Download All Submitted Documents').'</a>'); return @@ -2010,7 +2053,7 @@ KEYWORDS $lastsubonly.="\n".'<div class="LC_grade_submission_part"><b>Part:</b> '. $display_part.' <span class="LC_internal_info">( ID '.$respid. ' )</span> '. - '<span class="LC_warning">'.&mt('Nothing submitted - no attempts').'</span><br /><br /></div>'; + '<span class="LC_warning">'.&mt('Nothing submitted - no attempts.').'</span><br /><br /></div>'; next; } foreach my $submission (@$string) { @@ -2142,8 +2185,8 @@ KEYWORDS $seen{$partid}++; next if ($$handgrade{$part_resp} ne 'yes' && $env{'form.lastSub'} eq 'hdgrade'); - push @partlist,$partid; - push @gradePartRespid,$partid.'.'.$respid; + push(@partlist,$partid); + push(@gradePartRespid,$partid.'.'.$respid); $request->print(&gradeBox($request,$symb,$uname,$udom,$counter,$partid,\%record)); } $request->print('</div></div>'); @@ -2288,7 +2331,7 @@ sub get_last_submission { } if (!@string) { $string[0] = - '<span class="LC_warning">Nothing submitted - no attempts.</span>'; + '<span class="LC_warning">'.&mt('Nothing submitted - no attempts.').'</span>'; } return (\@string,\$timestamp); } @@ -2470,7 +2513,7 @@ sub processHandGrade { my (@parsedlist,@nextlist); my ($nextflg) = 0; - foreach (sort + foreach my $item (sort { if (lc($$fullname{$a}) ne lc($$fullname{$b})) { return (lc($$fullname{$a}) cmp lc($$fullname{$b})); @@ -2478,12 +2521,12 @@ sub processHandGrade { return $a cmp $b; } (keys(%$fullname))) { if ($nextflg == 1 && $button =~ /Next$/) { - push @parsedlist,$_; + push(@parsedlist,$item); } - $nextflg = 1 if ($_ eq $laststu); + $nextflg = 1 if ($item eq $laststu); if ($button eq 'Previous') { - last if ($_ eq $firststu); - push @parsedlist,$_; + last if ($item eq $firststu); + push(@parsedlist,$item); } } $ctr = 0; @@ -2506,11 +2549,11 @@ sub processHandGrade { my $submitted = 0; my $ungraded = 0; my $incorrect = 0; - foreach (keys(%status)) { - $submitted = 1 if ($status{$_} ne 'nothing'); - $ungraded = 1 if ($status{$_} =~ /^ungraded/); - $incorrect = 1 if ($status{$_} =~ /^incorrect/); - my ($foo,$partid,$foo1) = split(/\./,$_); + foreach my $item (keys(%status)) { + $submitted = 1 if ($status{$item} ne 'nothing'); + $ungraded = 1 if ($status{$item} =~ /^ungraded/); + $incorrect = 1 if ($status{$item} =~ /^incorrect/); + my ($foo,$partid,$foo1) = split(/\./,$item); if ($status{'resource.'.$partid.'.submitted_by'} ne '') { $submitted = 0; } @@ -2521,7 +2564,7 @@ sub processHandGrade { next if (!$ungraded && ($submitonly eq 'graded')); next if (!$incorrect && $submitonly eq 'incorrect'); } - push @nextlist,$student if ($ctr < $ntstu); + push(@nextlist,$student) if ($ctr < $ntstu); last if ($ctr == $ntstu); $ctr++; } @@ -2529,7 +2572,7 @@ sub processHandGrade { $ctr = 0; my $total = scalar(@nextlist)-1; - foreach (sort @nextlist) { + foreach (sort(@nextlist)) { my ($uname,$udom,$submitter) = split(/:/); $env{'form.student'} = $uname; $env{'form.userdom'} = $udom; @@ -2575,7 +2618,7 @@ sub saveHandGrade { } } elsif ($dropMenu eq 'reset status' && exists($record{'resource.'.$new_part.'.solved'})) { #don't bother if no old records -> no attempts - foreach my $key (keys (%record)) { + foreach my $key (keys(%record)) { if ($key=~/^resource\.\Q$new_part\E\./) { $newrecord{$key} = ''; } } $newrecord{'resource.'.$new_part.'.regrader'}= @@ -2610,7 +2653,7 @@ sub saveHandGrade { &handback_files($request,$symb,$stuname,$domain,$newflg,$new_part,\%newrecord); next; } else { - push @parts_graded, $new_part; + push(@parts_graded,$new_part); } if ($record{'resource.'.$new_part.'.awarded'} ne $partial) { $newrecord{'resource.'.$new_part.'.awarded'} = $partial; @@ -2637,7 +2680,7 @@ sub saveHandGrade { $record{'resource.'.$new_part.'.solved'} eq 'incorrect_by_override' || $dropMenu eq 'reset status') { - push (@version_parts,$new_part); + push(@version_parts,$new_part); } } my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; @@ -2685,7 +2728,7 @@ sub check_and_remove_from_queue { sub handback_files { my ($request,$symb,$stuname,$domain,$newflg,$new_part,$newrecord) = @_; - my $portfolio_root = &propath($domain,$stuname).'/userfiles/portfolio'; + my $portfolio_root = '/userfiles/portfolio'; my ($partlist,$handgrade,$responseType) = &response_type($symb); my @part_response_id = &flatten_responseType($responseType); @@ -2703,7 +2746,8 @@ sub handback_files { my ($answer_name,$answer_ver,$answer_ext) = &file_name_version_ext($answer_file); my ($portfolio_path) = ($directory =~ /^.+$stuname\/portfolio(.*)/); - my @dir_list = &Apache::lonnet::dirlist($portfolio_path,$domain,$stuname,$portfolio_root); + my $getpropath = 1; + my @dir_list = &Apache::lonnet::dirlist($portfolio_root.$portfolio_path,$domain,$stuname,$getpropath); my $version = &get_next_version($answer_name, $answer_ext, \@dir_list); # fix file name my ($save_file_name) = (($directory.$answer_name.".$version.".$answer_ext) =~ /^.+\/${stuname}\/(.*)/); @@ -2711,8 +2755,10 @@ sub handback_files { $newflg.'_'.$part_resp.'_returndoc'.$file_counter, $save_file_name); if ($result !~ m|^/uploaded/|) { - $request->print('<span class="LC_error">An error occurred ('.$result. - ') while trying to upload '.$newflg.'_'.$part_resp.'_returndoc'.$file_counter.'</span><br />'); + $request->print('<br /><span class="LC_error">'. + &mt('An error occurred ([_1]) while trying to upload [_2].', + $result,$newflg.'_'.$part_resp.'_returndoc'.$file_counter). + '</span>'); } else { # mark the file as read only my @files = ($save_file_name); @@ -2809,7 +2855,7 @@ sub decrement_aggs { if ($aggtries == $totaltries) { $decrement{'users'} = 1; } - foreach my $type (keys (%decrement)) { + foreach my $type (keys(%decrement)) { $$aggregate{$symb."\0".$part."\0".$type} = -$decrement{$type}; } return; @@ -2839,8 +2885,7 @@ sub version_portfiles { my $version_parts = join('|',@$v_flag); my @returned_keys; my $parts = join('|', @$parts_graded); - my $portfolio_root = &propath($domain,$stu_name). - '/userfiles/portfolio'; + my $portfolio_root = '/userfiles/portfolio'; foreach my $key (keys(%$record)) { my $new_portfiles; if ($key =~ /^resource\.($version_parts)\./ && $key =~ /\.portfiles$/ ) { @@ -2851,7 +2896,8 @@ sub version_portfiles { my ($directory,$answer_file) =($file =~ /^(.*?)([^\/]*)$/); my ($answer_name,$answer_ver,$answer_ext) = &file_name_version_ext($answer_file); - my @dir_list = &Apache::lonnet::dirlist($directory,$domain,$stu_name,$portfolio_root); + my $getpropath = 1; + my @dir_list = &Apache::lonnet::dirlist($portfolio_root.$directory,$domain,$stu_name,$getpropath); my $version = &get_next_version($answer_name, $answer_ext, \@dir_list); my $new_answer = &version_selected_portfile($domain, $stu_name, $directory, $answer_file, $version); if ($new_answer ne 'problem getting file') { @@ -2931,6 +2977,7 @@ sub file_name_version_ext { sub viewgrades_js { my ($request) = shift; + my $alertmsg = &mt('A number equal or greater than 0 is expected. Entered value = '); $request->print(<<VIEWJAVASCRIPT); <script type="text/javascript" language="javascript"> function writePoint(partid,weight,point) { @@ -2939,7 +2986,7 @@ sub viewgrades_js { if (point == "textval") { point = document.classgrade["TEXTVAL_"+partid].value; if (isNaN(point) || parseFloat(point) < 0) { - alert("A number equal or greater than 0 is expected. Entered value = "+parseFloat(point)); + alert("$alertmsg"+parseFloat(point)); var resetbox = false; for (var i=0; i<radioButton.length; i++) { if (radioButton[i].checked) { @@ -3037,7 +3084,7 @@ sub viewgrades_js { var weight = document.classgrade["weight_"+partid].value; if (isNaN(point) || parseFloat(point) < 0) { - alert("A number equal or greater than 0 is expected. Entered value = "+parseFloat(point)); + alert("$alertmsg"+parseFloat(point)); textbox.value = ""; return; } @@ -3129,15 +3176,15 @@ sub viewgrades { my $sectionClass; my $section_display = join (", ",&Apache::loncommon::get_env_multiple('form.section')); if ($env{'form.section'} eq 'all') { - $sectionClass='Class'; + $sectionClass=&mt('Class'); } elsif ($env{'form.section'} eq 'none') { - $sectionClass='Students in no Section'; + $sectionClass=&mt('Students in no Section'); } else { - $sectionClass='Students in Section(s) [_1]'; + $sectionClass=&mt('Students in Section(s) [_1]'); } $result.= '<h3>'. - &mt("Assign Common Grade To $sectionClass",$section_display).'</h3>'; + &mt("Assign Common Grade to [_1]",$sectionClass,$section_display).'</h3>'; $result.= &Apache::loncommon::start_data_table(); #radio buttons/text box for assigning points for a section or class. #handles different parts of a problem @@ -3169,8 +3216,8 @@ sub viewgrades { my $line = '<input type="text" name="TEXTVAL_'. $partid.'" size="4" '.'onChange="javascript:writePoint(\''. $partid.'\','.$weight{$partid}.',\'textval\')" /> /'. - $weight{$partid}.' (problem weight)</td>'."\n"; - $line.= '<td><select name="SELVAL_'.$partid.'"'. + $weight{$partid}.' '.&mt('(problem weight)').'</td>'."\n"; + $line.= '<td><b>'.&mt('Grade Status').':</b><select name="SELVAL_'.$partid.'"'. 'onChange="javascript:writeRadText(\''.$partid.'\','. $weight{$partid}.')"> '. '<option selected="selected"> </option>'. @@ -3183,9 +3230,10 @@ sub viewgrades { $line.='<input type="hidden" name="weight_'. $partid.'" value="'.$weight{$partid}.'" />'."\n"; + #&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). $result.= &Apache::loncommon::start_data_table_row()."\n". - &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). + '<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>'. &Apache::loncommon::end_data_table_row()."\n"; $ctsparts++; } @@ -3196,8 +3244,8 @@ sub viewgrades { #table listing all the students in a section/class #header of table - $result.= '<h3>'.&mt('Assign Grade to Specific Students in '.$sectionClass, - $section_display).'</h3>'; + $result.= '<h3>'.&mt('Assign Grade to Specific Students in ').$sectionClass, + $section_display.'</h3>'; $result.= &Apache::loncommon::start_data_table(). &Apache::loncommon::start_data_table_header_row(). '<th>'.&mt('No.').'</th>'. @@ -3207,10 +3255,11 @@ sub viewgrades { my @partids = (); foreach my $part (@parts) { my $display=&Apache::lonnet::metadata($url,$part.'.display'); - $display =~ s|^Number of Attempts|Tries<br />|; # makes the column narrower + my $narrowtext = &mt('Tries'); + $display =~ s|^Number of Attempts|$narrowtext <br />|; # makes the column narrower if (!$display) { $display = &Apache::lonnet::metadata($url,$part.'.name'); } my ($partid) = &split_part_type($part); - push(@partids, $partid); + push(@partids,$partid); my $display_part=&get_display_part($partid,$symb); if ($display =~ /^Partial Credit Factor/) { $result.='<th>'. @@ -3364,7 +3413,7 @@ sub editgrades { my $header; while ($ctr < $env{'form.totalparts'}) { my $partid = $env{'form.partid_'.$ctr}; - push @partid,$partid; + push(@partid,$partid); $weight{$partid} = $env{'form.weight_'.$partid}; $ctr++; } @@ -3379,9 +3428,10 @@ sub editgrades { if ($type eq 'awarded' || $type eq 'solved') { next; } my $display=&Apache::lonnet::metadata($url,$stores.'.display'); $display =~ s/\[Part: (\w)+\]//; - $display =~ s/Number of Attempts/Tries/; - $header .= '<th align="center">'.&mt('Old '.$display).'</th>'. - '<th align="center">'.&mt('New '.$display).'</th>'; + my $narrowtext = &mt('Tries'); + $display =~ s/Number of Attempts/$narrowtext/; + $header .= '<th align="center">'.&mt('Old').' '.$display.'</th>'. + '<th align="center">'.&mt('New').' '.$display.'</th>'; $columns{$partid}+=2; } } @@ -3723,11 +3773,12 @@ ENDPICK } sub checkforfile_js { + my $alertmsg = &mt('Please use the browse button to select a file from your local directory.'); my $result =<<CSVFORMJS; <script type="text/javascript" language="javascript"> function checkUpload(formname) { if (formname.upfile.value == "") { - alert("Please use the browse button to select a file from your local directory."); + alert("$alertmsg"); return false; } formname.submit(); @@ -3747,8 +3798,8 @@ sub upcsvScores_form { $result.=$table; $result.='<br /><table width="100%" border="0"><tr><td bgcolor="#777777">'."\n"; $result.='<table width="100%" border="0"><tr bgcolor="#e6ffff"><td>'."\n"; - $result.=' <b>'.&mt('Specify a file containing the class scores for current resource'). - '.</b></td></tr>'."\n"; + $result.=' <b>'.&mt('Specify a file containing the class scores for current resource.'). + '</b></td></tr>'."\n"; $result.='<tr bgcolor=#ffffe6><td>'."\n"; my $upload=&mt("Upload Scores"); my $upfile_select=&Apache::loncommon::upfile_select_html(); @@ -3960,31 +4011,31 @@ sub csvuploadassign { $grades{$store_key}=$entries{$fields{$dest}}; } } - if (! %grades) { push(@skipped,"$username:$domain no data to save"); } - $grades{"resource.regrader"}="$env{'user.name'}:$env{'user.domain'}"; - my $result=&Apache::lonnet::cstore(\%grades,$symb, + if (! %grades) { + push(@skipped,&mt("[_1]: no data to save","$username:$domain")); + } else { + $grades{"resource.regrader"}="$env{'user.name'}:$env{'user.domain'}"; + my $result=&Apache::lonnet::cstore(\%grades,$symb, $env{'request.course.id'}, $domain,$username); - if ($result eq 'ok') { - $request->print('.'); - } else { - $request->print("<p> - <span class=\"LC_error\"> - Failed to save student $username:$domain. - Message when trying to save was ($result) - </span> - </p>" ); - } - $request->rflush(); - $countdone++; + if ($result eq 'ok') { + $request->print('.'); + } else { + $request->print("<p><span class=\"LC_error\">". + &mt("Failed to save data for student [_1]. Message when trying to save was: [_2]", + "$username:$domain",$result)."</span></p>"); + } + $request->rflush(); + $countdone++; + } } - $request->print("<br />Saved $countdone students\n"); + $request->print('<br /><span class="LC_info">'.&mt("Saved [_1] students",$countdone)."</span>\n"); if (@skipped) { - $request->print('<p><h4><b>Skipped Students</b></h4></p>'); + $request->print('<p><span class="LC_warning">'.&mt('Skipped Students').'</span></p>'); foreach my $student (@skipped) { $request->print("$student<br />\n"); } } if (@notallowed) { - $request->print('<p><span class="LC_error">Students Not Allowed to Modify</span></p>'); + $request->print('<p><span class="LC_error">'.&mt('Students Not Allowed to Modify').'</span></p>'); foreach my $student (@notallowed) { $request->print("$student<br />\n"); } } $request->print("<br />\n"); @@ -4001,12 +4052,13 @@ sub csvuploadassign { sub pickStudentPage { my ($request) = shift; + my $alertmsg = &mt('Please select the student you wish to grade.'); $request->print(<<LISTJAVASCRIPT); <script type="text/javascript" language="javascript"> function checkPickOne(formname) { if (radioSelection(formname.student) == null) { - alert("Please select the student you wish to grade."); + alert("$alertmsg"); return; } ptr = pullDownSelection(formname.selectpage); @@ -4041,7 +4093,7 @@ LISTJAVASCRIPT $ctr++; } $select.= '</select>'; - $result.=&mt(' <b>Problems from:</b> [_1]',$select)."<br />\n"; + $result.=' <b>'.&mt('Problems from').':</b> '.$select."<br />\n"; $ctr=0; foreach (@$titles) { @@ -4056,13 +4108,13 @@ LISTJAVASCRIPT my $options = '<label><input type="radio" name="vProb" value="no" checked="checked" /> '.&mt('no').' </label>'."\n". '<label><input type="radio" name="vProb" value="yes" /> '.&mt('yes').' </label>'."<br />\n"; - $result.=' '.&mt('<b>View Problems Text: </b> [_1]',$options); + $result.=' <b>'.&mt('View Problem Text').': </b>'.$options; $options = '<label><input type="radio" name="lastSub" value="none" /> '.&mt('none').' </label>'."\n". '<label><input type="radio" name="lastSub" value="datesub" checked="checked" /> '.&mt('by dates and submissions').'</label>'."\n". '<label><input type="radio" name="lastSub" value="all" /> '.&mt('all details').' </label>'."\n"; - $result.=' '.&mt('<b>Submission Details: </b>[_1]',$options); + $result.=' <b>'.&mt('Submissions').': </b>'.$options; $result.=&build_section_inputs(); my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status')); @@ -4071,12 +4123,10 @@ LISTJAVASCRIPT '<input type="hidden" name="symb" value="'.&Apache::lonenc::check_encrypt($symb).'" />'."\n". '<input type="hidden" name="saveState" value="'.$env{'form.saveState'}.'" />'."<br />\n"; - $result.=' '.&mt('<b>Use CODE: [_1] </b>', - '<input type="text" name="CODE" value="" />'). - '<br />'."\n"; + $result.=' <b>'.&mt('Use CODE').': </b> <input type="text" name="CODE" value="" /> <br />'."\n"; $result.=' <input type="button" '. - 'onClick="javascript:checkPickOne(this.form);" value="'.&mt('Next->').'" /><br />'."\n"; + 'onClick="javascript:checkPickOne(this.form);" value="'.&mt('Next').' →" /><br />'."\n"; $request->print($result); @@ -4115,7 +4165,7 @@ LISTJAVASCRIPT } $studentTable.=&Apache::loncommon::end_data_table()."\n"; $studentTable.='<input type="button" '. - 'onClick="javascript:checkPickOne(this.form);" value="'.&mt('Next->').'" /></form>'."\n"; + 'onClick="javascript:checkPickOne(this.form);" value="'.&mt('Next').' →" /></form>'."\n"; $studentTable.=&show_grading_menu_form($symb); $request->print($studentTable); @@ -4252,7 +4302,7 @@ sub displayPage { # $request->print('match='.$1."<br />\n"); # } # $companswer =~ s|<table border=\"1\">|<table border=\"0\">|g; - $studentTable.=' <b>'.$title.'</b> <br /> '.&mt('<b>Correct answer:</b><br />[_1]',$companswer); + $studentTable.=' <b>'.$title.'</b> <br /> <b>'.&mt('Correct answer').':</b><br />'.$companswer; } my %record = &Apache::lonnet::restore($symbx,$env{'request.course.id'},$udom,$uname); @@ -4322,10 +4372,11 @@ sub displaySubByDates { my %orders; $mark{'correct_by_student'} = $checkIcon; if (!exists($$record{'1:timestamp'})) { - return '<br /> <span class="LC_warning">'.&mt('Nothing submitted - no attempts').'</span><br />'; + return '<br /> <span class="LC_warning">'.&mt('Nothing submitted - no attempts.').'</span><br />'; } my $interaction; + my $no_increment = 1; for ($version=1;$version<=$$record{'version'};$version++) { my $timestamp = &Apache::lonlocal::locallocaltime($$record{$version.':timestamp'}); @@ -4369,7 +4420,8 @@ sub displaySubByDates { if (!exists($orders{$partid})) { $orders{$partid}={}; } if (!exists($orders{$partid}->{$responseId})) { $orders{$partid}->{$responseId}= - &get_order($partid,$responseId,$symb,$uname,$udom); + &get_order($partid,$responseId,$symb,$uname,$udom, + $no_increment); } $displaySub[0].='</b> '. &cleanRecord($$record{$version.':'.$matchKey},$responseType,$symb,$partid,$responseId,$record,$orders{$partid}->{$responseId},"$version:",$uname,$udom).'<br />'; @@ -4422,12 +4474,12 @@ sub updateGradeByPage { my ($uname,$udom) = split(/:/,$env{'form.student'}); my $usec=$classlist->{$env{'form.student'}}[5]; if (!&canmodify($usec)) { - $request->print('<span class="LC_warning">Unable to modify requested student.('.$env{'form.student'}.'</span>'); + $request->print('<span class="LC_warning">'.&mt('Unable to modify requested student ([_1])',$env{'form.student'}).'</span>'); $request->print(&show_grading_menu_form($env{'form.symb'})); return; } my $result='<h3><span class="LC_info"> '.$env{'form.title'}.'</span></h3>'; - $result.='<h3> Student: '.&nameUserString(undef,$env{'form.fullname'},$uname,$udom). + $result.='<h3> '.&mt('Student: ').&nameUserString(undef,$env{'form.fullname'},$uname,$udom). '</h3>'."\n"; $request->print($result); @@ -4436,7 +4488,7 @@ sub updateGradeByPage { my ($mapUrl, $id, $resUrl) = &Apache::lonnet::decode_symb( $env{'form.page'}); my $map = $navmap->getResourceByUrl($resUrl); # add to navmaps if (!$map) { - $request->print('<span class="LC_warning">Unable to grade requested sequence. ('.$resUrl.')</span>'); + $request->print('<span class="LC_warning">'.&mt('Unable to grade requested sequence ([_1]).',$resUrl).'</span>'); my ($symb)=&get_symb($request); $request->print(&show_grading_menu_form($symb)); return; @@ -4468,8 +4520,8 @@ sub updateGradeByPage { &Apache::loncommon::start_data_table_row(). '<td align="center" valign="top" >'.$prob. (scalar(@{$parts}) == 1 ? '' - : '<br />('.&mt('[quant,_1, parts]',scalar(@{$parts})) - ).')</td>'; + : '<br />('.&mt('[quant,_1, part]',scalar(@{$parts})) + .')').'</td>'; $studentTable.='<td valign="top"> <b>'.$title.'</b> </td>'; my %newrecord=(); @@ -4513,10 +4565,10 @@ sub updateGradeByPage { } my $display_part=&get_display_part($partid,$curRes->symb()); my $oldstatus = $env{'form.solved'.$question.'_'.$partid}; - $displayPts[0].=' <b>Part:</b> '.$display_part.' = '. + $displayPts[0].=' <b>'.&mt('Part').':</b> '.$display_part.' = '. (($oldstatus eq 'excused') ? 'excused' : $oldpts). ' <br />'; - $displayPts[1].=' <b>Part:</b> '.$display_part.' = '. + $displayPts[1].=' <b>'.&mt('Part').':</b> '.$display_part.' = '. (($score eq 'excused') ? 'excused' : $newpts). ' <br />'; $question++; @@ -4565,9 +4617,9 @@ sub updateGradeByPage { $studentTable.=&Apache::loncommon::end_data_table(); $studentTable.=&show_grading_menu_form($env{'form.symb'}); - my $grademsg=($changeflag == 0 ? 'No score was changed or updated.' : - 'The scores were changed for '. - $changeflag.' problem'.($changeflag == 1 ? '.' : 's.')); + my $grademsg=($changeflag == 0 ? &mt('No score was changed or updated.') : + &mt('The scores were changed for [quant,_1,problem].', + $changeflag)); $request->print($grademsg.$studentTable); return ''; @@ -4681,9 +4733,10 @@ my %bubble_lines_per_response; # no. my %first_bubble_line; # First bubble line no. for each bubble. -my %subdivided_bubble_lines; # no. bubble lines for optionresponse - # or matchresponse where an individual - # response can have multiple lines +my %subdivided_bubble_lines; # no. bubble lines for optionresponse, + # matchresponse or rankresponse, where + # an individual response can have multiple + # lines my %responsetype_per_response; # responsetype for each response @@ -4750,8 +4803,9 @@ sub get_response_bubbles { sub scantron_filenames { my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'}; my $cname=$env{'course.'.$env{'request.course.id'}.'.num'}; + my $getpropath = 1; my @files=&Apache::lonnet::dirlist('userfiles',$cdom,$cname, - &propath($cdom,$cname)); + $getpropath); my @possiblenames; foreach my $filename (sort(@files)) { ($filename)=split(/&/,$filename); @@ -4794,19 +4848,76 @@ sub scantron_uploads { =cut sub scantron_scantab { - my $fh=Apache::File->new($Apache::lonnet::perlvar{'lonTabDir'}.'/scantronformat.tab'); my $result='<select name="scantron_format">'."\n"; $result.='<option></option>'."\n"; - foreach my $line (<$fh>) { - my ($name,$descrip)=split(/:/,$line); - if ($name =~ /^\#/) { next; } - $result.='<option value="'.$name.'">'.$descrip.'</option>'."\n"; + my @lines = &get_scantronformat_file(); + if (@lines > 0) { + foreach my $line (@lines) { + next if (($line =~ /^\#/) || ($line eq '')); + my ($name,$descrip)=split(/:/,$line); + $result.='<option value="'.$name.'">'.$descrip.'</option>'."\n"; + } } $result.='</select>'."\n"; - return $result; } +=pod + +=item get_scantronformat_file + + Returns an array containing lines from the scantron format file for + the domain of the course. + + If a url for a custom.tab file is listed in domain's configuration.db, + lines are from this file. + + Otherwise, if a default.tab has been published in RES space by the + domainconfig user, lines are from this file. + + Otherwise, fall back to getting lines from the legacy file on the + local server: /home/httpd/lonTabs/default_scantronformat.tab + +=cut + +sub get_scantronformat_file { + my $cdom= $env{'course.'.$env{'request.course.id'}.'.domain'}; + my %domconfig = &Apache::lonnet::get_dom('configuration',['scantron'],$cdom); + my $gottab = 0; + my @lines; + if (ref($domconfig{'scantron'}) eq 'HASH') { + if ($domconfig{'scantron'}{'scantronformat'} ne '') { + my $formatfile = &Apache::lonnet::getfile($Apache::lonnet::perlvar{'lonDocRoot'}.$domconfig{'scantron'}{'scantronformat'}); + if ($formatfile ne '-1') { + @lines = split("\n",$formatfile,-1); + $gottab = 1; + } + } + } + if (!$gottab) { + my $confname = $cdom.'-domainconfig'; + my $default = $Apache::lonnet::perlvar{'lonDocRoot'}.'/res/'.$cdom.'/'.$confname.'/default.tab'; + my $formatfile = &Apache::lonnet::getfile($default); + if ($formatfile ne '-1') { + @lines = split("\n",$formatfile,-1); + $gottab = 1; + } + } + if (!$gottab) { + my @domains = &Apache::lonnet::current_machine_domains(); + if (grep(/^\Q$cdom\E$/,@domains)) { + my $fh=Apache::File->new($Apache::lonnet::perlvar{'lonTabDir'}.'/scantronformat.tab'); + @lines = <$fh>; + close($fh); + } else { + my $fh=Apache::File->new($Apache::lonnet::perlvar{'lonTabDir'}.'/default_scantronformat.tab'); + @lines = <$fh>; + close($fh); + } + } + return @lines; +} + =pod =item scantron_CODElist @@ -4839,11 +4950,11 @@ sub scantron_CODElist { =cut sub scantron_CODEunique { - my $result='<span style="white-space: nowrap;"> + my $result='<span class="LC_nobreak"> <label><input type="radio" name="scantron_CODEunique" value="yes" checked="checked" />'.&mt('Yes').' </label> </span> - <span style="white-space: nowrap;"> + <span class="LC_nobreak"> <label><input type="radio" name="scantron_CODEunique" value="no" />'.&mt('No').' </label> </span>'; @@ -4880,6 +4991,8 @@ sub scantron_selectphase { my $CODE_unique=&scantron_CODEunique(); my $result; + $ssi_error = 0; + # Chunk of form to prompt for a file to grade and how: $result.= ' @@ -5000,8 +5113,37 @@ sub scantron_selectphase { '); &Apache::lonpickcode::code_list($r,2); + + $r->print('<br /><form method="post" name="checkscantron">'. + $default_form_data."\n". + &Apache::loncommon::start_data_table('LC_scantron_action')."\n". + &Apache::loncommon::start_data_table_header_row()."\n". + '<th colspan="2"> + '.&mt('Review scantron data and submissions for a previously graded folder/sequence')."\n". + '</th>'."\n". + &Apache::loncommon::end_data_table_header_row()."\n". + &Apache::loncommon::start_data_table_row()."\n". + '<td> '.&mt('Graded folder/sequence:').' </td>'."\n". + '<td> '.$sequence_selector.' </td>'. + &Apache::loncommon::end_data_table_row()."\n". + &Apache::loncommon::start_data_table_row()."\n". + '<td> '.&mt('Filename of scoring office file:').' </td>'."\n". + '<td> '.$file_selector.' </td>'."\n". + &Apache::loncommon::end_data_table_row()."\n". + &Apache::loncommon::start_data_table_row()."\n". + '<td> '.&mt('Format of data file:').' </td>'."\n". + '<td> '.$format_selector.' </td>'."\n". + &Apache::loncommon::end_data_table_row()."\n". + &Apache::loncommon::start_data_table_row()."\n". + '<td colspan="2">'."\n". + '<input type="hidden" name="command" value="checksubmissions" />'."\n". + '<input type="submit" value="'.&mt('Review Scantron Data and Submission Records').'" />'."\n". + '</td>'."\n". + &Apache::loncommon::end_data_table_row()."\n". + &Apache::loncommon::end_data_table()."\n". + '</form><br />'); $r->print($grading_menu_button); - return + return; } =pod @@ -5063,10 +5205,10 @@ sub scantron_selectphase { sub get_scantron_config { my ($which) = @_; - my $fh=Apache::File->new($Apache::lonnet::perlvar{'lonTabDir'}.'/scantronformat.tab'); + my @lines = &get_scantronformat_file(); my %config; #FIXME probably should move to XML it has already gotten a bit much now - foreach my $line (<$fh>) { + foreach my $line (@lines) { my ($name,$descrip)=split(/:/,$line); if ($name ne $which ) { next; } chomp($line); @@ -5457,7 +5599,10 @@ sub scantron_validator_lettnum { my $occurrences = 0; if (($responsetype_per_response{$questnum-1} eq 'essayresponse') || ($responsetype_per_response{$questnum-1} eq 'formularesponse') || - ($responsetype_per_response{$questnum-1} eq 'stringresponse')) { + ($responsetype_per_response{$questnum-1} eq 'stringresponse') || + ($responsetype_per_response{$questnum-1} eq 'imageresponse') || + ($responsetype_per_response{$questnum-1} eq 'reactionresponse') || + ($responsetype_per_response{$questnum-1} eq 'organicresponse')) { my @singlelines = split('',$currquest); foreach my $entry (@singlelines) { $occurrences = &occurence_count($entry,$matchon); @@ -5524,6 +5669,10 @@ sub scantron_validator_positional { # If the split only gives us one element.. the full length of the # answer string, no bubbles are filled in: + if ($answers_needed eq '') { + return; + } + if (length($array[0]) eq $$scantron_config{'Qlength'}*$answers_needed) { for (my $ans=0; $ans<$answers_needed; $ans++ ) { $record->{"scantron.$ansnum.answer"}=''; @@ -5552,7 +5701,10 @@ sub scantron_validator_positional { # if (($responsetype_per_response{$questnum-1} eq 'essayresponse') || ($responsetype_per_response{$questnum-1} eq 'formularesponse') || - ($responsetype_per_response{$questnum-1} eq 'stringresponse')) { + ($responsetype_per_response{$questnum-1} eq 'stringresponse') || + ($responsetype_per_response{$questnum-1} eq 'imageresponse') || + ($responsetype_per_response{$questnum-1} eq 'reactionresponse') || + ($responsetype_per_response{$questnum-1} eq 'organicresponse')) { my $doubleerror = 0; while (($currquest >= $$scantron_config{'Qlength'}) && (!$doubleerror)) { @@ -6026,8 +6178,7 @@ sub scantron_validate_file { } if (!$stop) { my $warning=&scantron_warning_screen('Start Grading'); - $r->print(' -<b>'.&mt('Validation process complete.').'<b><br /> + $r->print(&mt('Validation process complete.').'<br /> '.$warning.' <input type="submit" name="submit" value="'.&mt('Start Grading').'" /> <input type="hidden" name="command" value="scantron_process" /> @@ -6039,15 +6190,15 @@ sub scantron_validate_file { } if ($stop) { if ($validate_phases[$currentphase] eq 'sequence') { - $r->print('<input type="submit" name="submit" value="'.&mt('Ignore ->').' " />'); + $r->print('<input type="submit" name="submit" value="'.&mt('Ignore').' → " />'); $r->print(' '.&mt('this error').' <br />'); $r->print(" <p>".&mt("Or click the 'Grading Menu' button to start over.")."</p>"); } else { if ($validate_phases[$currentphase] eq 'doublebubble' || $validate_phases[$currentphase] eq 'missingbubbles') { - $r->print('<input type="button" name="submitbutton" value="'.&mt('Continue ->').'" onclick="javascript:verify_bubble_radio(this.form)" />'); + $r->print('<input type="button" name="submitbutton" value="'.&mt('Continue').' →" onclick="javascript:verify_bubble_radio(this.form)" />'); } else { - $r->print('<input type="submit" name="submit" value="'.&mt('Continue ->').'" />'); + $r->print('<input type="submit" name="submit" value="'.&mt('Continue').' →" />'); } $r->print(' '.&mt('using corrected info').' <br />'); $r->print("<input type='submit' value='".&mt("Skip")."' name='scantron_skip_record' />"); @@ -6425,14 +6576,7 @@ sub scantron_validate_sequence { return (0,$currentphase+1); } -=pod - -=item scantron_validate_ID - Validates all scanlines in the selected file to not have any - invalid or underspecified student IDs - -=cut sub scantron_validate_ID { my ($r,$currentphase) = @_; @@ -6498,35 +6642,6 @@ sub scantron_validate_ID { return (0,$currentphase+1); } -=pod - -=item scantron_get_correction - - Builds the interface screen to interact with the operator to fix a - specific error condition in a specific scanline - - Arguments: - $r - Apache request object - $i - number of the current scanline - $scan_record - hash ref as returned from &scantron_parse_scanline() - $scan_config - hash ref as returned from &get_scantron_config() - $line - full contents of the current scanline - $error - error condition, valid values are - 'incorrectCODE', 'duplicateCODE', - 'doublebubble', 'missingbubble', - 'duplicateID', 'incorrectID' - $arg - extra information needed - For errors: - - duplicateID - paper number that this studentID was seen before on - - duplicateCODE - array ref of the paper numbers this CODE was - seen on before - - incorrectCODE - current incorrect CODE - - doublebubble - array ref of the bubble lines that have double - bubble errors - - missingbubble - array ref of the bubble lines that have missing - bubble errors - -=cut sub scantron_get_correction { my ($r,$i,$scan_record,$scan_config,$line,$error,$arg)=@_; @@ -6664,7 +6779,7 @@ ENDSCRIPT foreach my $question (@{$arg}) { my @linenums = &prompt_for_corrections($r,$question,$scan_config, $scan_record, $error); - push (@lines_to_correct,@linenums); + push(@lines_to_correct,@linenums); } $r->print(&verify_bubbles_checked(@lines_to_correct)); } elsif ($error eq 'missingbubble') { @@ -6684,7 +6799,7 @@ ENDSCRIPT foreach my $question (@{$arg}) { my @linenums = &prompt_for_corrections($r,$question,$scan_config, $scan_record, $error); - push (@lines_to_correct,@linenums); + push(@lines_to_correct,@linenums); } $r->print(&verify_bubbles_checked(@lines_to_correct)); } else { @@ -6762,7 +6877,7 @@ sub questions_to_line_list { $first = $first_bubble_line{$question-1} + 1; $count = $bubble_lines_per_response{$question-1}; } - my $last = $first+$count-1; + $last = $first+$count-1; push(@lines, ($first..$last)); } return join(',', @lines); @@ -6788,13 +6903,14 @@ for multi and missing bubble cases). Numbered from 0 (but question numbers are from 1. %first_bubble_line - Starting bubble line for each question. - %subdivided_bubble_lines - optionresponse and matchresponse type - problems render as separate sub-questions, + %subdivided_bubble_lines - optionresponse, matchresponse and rankresponse + type problems render as separate sub-questions, in exam mode. This hash contains a comma-separated list of the lines per sub-question. - %responsetype_per_response - essayresponse, forumalaresponse, and - stringresponse type problem parts can have + %responsetype_per_response - essayresponse, formularesponse, + stringresponse, imageresponse, reactionresponse, + and organicresponse type problem parts can have multiple lines per response if the weight assigned exceeds 10. In this case, only one bubble per line is permitted, but more @@ -6828,7 +6944,10 @@ sub prompt_for_corrections { $r->print(&mt('The group of bubble lines below responds to a single question.').'<br />'); if (($responsetype_per_response{$question-1} eq 'essayresponse') || ($responsetype_per_response{$question-1} eq 'formularesponse') || - ($responsetype_per_response{$question-1} eq 'stringresponse')) { + ($responsetype_per_response{$question-1} eq 'stringresponse') || + ($responsetype_per_response{$question-1} eq 'imageresponse') || + ($responsetype_per_response{$question-1} eq 'reactionresponse') || + ($responsetype_per_response{$question-1} eq 'organicresponse')) { $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 scantron sheets.",$lines).'<br /><br />'.&mt('A non-zero score can be assigned to the student during scantron 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 />'); } else { $r->print(&mt("Select at most one bubble in a single line and select 'No Bubble' in all the other lines. ")."<br />"); @@ -6838,7 +6957,7 @@ sub prompt_for_corrections { my $selected = $$scan_record{"scantron.$current_line.answer"}; &scantron_bubble_selector($r,$scan_config,$current_line, $questionnum,$error,split('', $selected)); - push (@linenums,$current_line); + push(@linenums,$current_line); $current_line++; } if ($lines > 1) { @@ -7054,7 +7173,7 @@ sub scantron_validate_CODE { $line,'duplicateCODE',$usedCODEs{$CODE}); return(1,$currentphase); } - push (@{$usedCODEs{$CODE}},$$scan_record{'scantron.PaperID'}); + push(@{$usedCODEs{$CODE}},$$scan_record{'scantron.PaperID'}); } return (0,$currentphase+1); } @@ -7093,25 +7212,6 @@ sub scantron_validate_doublebubble { return (0,$currentphase+1); } -=pod - -=item scantron_get_maxbubble - - Returns the maximum number of bubble lines that are expected to - occur. Does this by walking the selected sequence rendering the - resource and then checking &Apache::lonxml::get_problem_counter() - for what the current value of the problem counter is. - - Caches the results to $env{'form.scantron_maxbubble'}, - $env{'form.scantron.bubble_lines.n'}, - $env{'form.scantron.first_bubble_line.n'} and - $env{"form.scantron.sub_bubblelines.n"} - which are the total number of bubble, lines, the number of bubble - lines for response n and number of the first bubble line for response n, - and a comma separated list of numbers of bubble lines for sub-questions - (for optionresponse items only), for response n. - -=cut sub scantron_get_maxbubble { if (defined($env{'form.scantron_maxbubble'}) && @@ -7141,51 +7241,68 @@ sub scantron_get_maxbubble { my $response_number = 0; my $bubble_line = 0; foreach my $resource (@resources) { - # Need to retrieve part IDs and response IDs because essayresponse - # items are not included in $analysis{'parts'} from lonnet::ssi. - my %possible_part_ids; - if (ref($resource->parts()) eq 'ARRAY') { + my $symb = $resource->symb(); + + my (@parts,@allparts,@possible_parts); + + # Need to retrieve part IDs and response IDs because essayresponse, + # reactionresponse and organicresponse items are not included in + # $analysis{'parts'} from lonnet::ssi. + if (ref($resource->parts()) eq 'ARRAY') { foreach my $part (@{$resource->parts()}) { - my @resp_ids = $resource->responseIds($part); - foreach my $id (@resp_ids) { - $possible_part_ids{$part.'.'.$id} = 1; + if (!&Apache::loncommon::check_if_partid_hidden($part,$symb,$udom,$uname)) { + my @resp_ids = $resource->responseIds($part); + foreach my $id (@resp_ids) { + my $part_id = $part.'.'.$id; + push(@possible_parts,$part_id); + } } } } - my $result=&Apache::lonnet::ssi($resource->src(), - ('symb' => $resource->symb()), - ('grade_target' => 'analyze'), - ('grade_courseid' => $cid), - ('grade_domain' => $udom), - ('grade_username' => $uname)); - my (undef, $an) = - split(/_HASH_REF__/,$result, 2); - my @parts; + my $result=&ssi_with_retries($resource->src(), $ssi_retries, + ('symb' => $symb, + 'grade_target' => 'analyze', + 'grade_courseid' => $cid, + 'grade_domain' => $udom, + 'grade_username' => $uname)); + my (undef, $an) = + split(/_HASH_REF__/,$result, 2); my %analysis = &Apache::lonnet::str2hash($an); if (ref($analysis{'parts'}) eq 'ARRAY') { - @parts = @{$analysis{'parts'}}; + foreach my $part (@{$analysis{'parts'}}) { + my ($id,$respid) = split(/\./,$part); + if (!&Apache::loncommon::check_if_partid_hidden($id,$symb,$udom,$uname)) { + push(@parts,$part); + } + } } - # Add part_ids for any essayresponse items. - foreach my $part_id (keys(%possible_part_ids)) { - if ($analysis{$part_id.'.type'} eq 'essayresponse') { - if (!grep(/^\Q$part_id\E$/,@parts)) { - push (@parts,$part_id); + # Add part_ids for any essayresponse, reactionresponse or + # organicresponse items. + foreach my $part_id (@possible_parts) { + if (grep(/^\Q$part_id\E$/,@parts)) { + push(@allparts,$part_id); + } else { + if (($analysis{$part_id.'.type'} eq 'essayresponse') || + ($analysis{$part_id.'.type'} eq 'reactionresponse') || + ($analysis{$part_id.'.type'} eq 'organicresponse')) { + push(@allparts,$part_id); } } } - foreach my $part_id (@parts) { - my $lines = $analysis{"$part_id.bubble_lines"}; + foreach my $part_id (@allparts) { + my $lines; # TODO - make this a persistent hash not an array. - # optionresponse and matchresponse type items render as - # separate sub-questions in exam mode. + # optionresponse, matchresponse and rankresponse type items + # render as separate sub-questions in exam mode. if (($analysis{$part_id.'.type'} eq 'optionresponse') || - ($analysis{$part_id.'.type'} eq 'matchresponse')) { + ($analysis{$part_id.'.type'} eq 'matchresponse') || + ($analysis{$part_id.'.type'} eq 'rankresponse')) { my ($numbub,$numshown); if ($analysis{$part_id.'.type'} eq 'optionresponse') { if (ref($analysis{$part_id.'.options'}) eq 'ARRAY') { @@ -7195,13 +7312,17 @@ sub scantron_get_maxbubble { if (ref($analysis{$part_id.'.items'}) eq 'ARRAY') { $numbub = scalar(@{$analysis{$part_id.'.items'}}); } + } elsif ($analysis{$part_id.'.type'} eq 'rankresponse') { + if (ref($analysis{$part_id.'.foils'}) eq 'ARRAY') { + $numbub = scalar(@{$analysis{$part_id.'.foils'}}); + } } if (ref($analysis{$part_id.'.shown'}) eq 'ARRAY') { $numshown = scalar(@{$analysis{$part_id.'.shown'}}); } my $bubbles_per_line = 10; - my $inner_bubble_lines = int($numshown/$bubbles_per_line); - if (($numshown % $bubbles_per_line) != 0) { + my $inner_bubble_lines = int($numbub/$bubbles_per_line); + if (($numbub % $bubbles_per_line) != 0) { $inner_bubble_lines++; } for (my $i=0; $i<$numshown; $i++) { @@ -7209,6 +7330,9 @@ sub scantron_get_maxbubble { $inner_bubble_lines.','; } $subdivided_bubble_lines{$response_number} =~ s/,$//; + $lines = $numshown * $inner_bubble_lines; + } else { + $lines = $analysis{"$part_id.bubble_lines"}; } $first_bubble_line{$response_number} = $bubble_line; @@ -7230,15 +7354,6 @@ sub scantron_get_maxbubble { return $env{'form.scantron_maxbubble'}; } -=pod - -=item scantron_validate_missingbubbles - - Validates all scanlines in the selected file to not have any - answers that don't have bubbles that have not been verified - to be bubble free. - -=cut sub scantron_validate_missingbubbles { my ($r,$currentphase) = @_; @@ -7293,35 +7408,15 @@ sub scantron_validate_missingbubbles { return (0,$currentphase+1); } -=pod - -=item scantron_process_students - - Routine that does the actual grading of the bubble sheet information. - - The parsed scanline hash is added to %env - - Then foreach unskipped scanline it does an &Apache::lonnet::ssi() - foreach resource , with the form data of - - 'submitted' =>'scantron' - 'grade_target' =>'grade', - 'grade_username'=> username of student - 'grade_domain' => domain of student - 'grade_courseid'=> of course - 'grade_symb' => symb of resource to grade - - This triggers a grading pass. The problem grading code takes care - of converting the bubbled letter information (now in %env) into a - valid submission. - -=cut sub scantron_process_students { my ($r) = @_; + my (undef,undef,$sequence)=&Apache::lonnet::decode_symb($env{'form.selectpage'}); my ($symb)=&get_symb($r); - if (!$symb) {return '';} + if (!$symb) { + return ''; + } my $default_form_data=&defaultFormData($symb); my %scantron_config=&get_scantron_config($env{'form.scantron_format'}); @@ -7342,6 +7437,7 @@ SCANTRONFORM my @delayqueue; my %completedstudents; + my $lock=&Apache::lonnet::set_lock(&mt('Grading bubblesheet exam')); my $count=&get_todo_count($scanlines,$scan_data); my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin($r,'Scantron Status', 'Scantron Progress',$count, @@ -7353,6 +7449,18 @@ SCANTRONFORM my ($uname,$udom,$started); &scantron_get_maxbubble(); # Need the bubble lines array to parse. + + + # If an ssi failed in scantron_get_maxbubble, put an error message out to + # the user and return. + + if ($ssi_error) { + $r->print("</form>"); + &ssi_print_error($r); + $r->print(&show_grading_menu_form($symb)); + &Apache::lonnet::remove_lock($lock); + return ''; # Dunno why the other returns return '' rather than just returning. + } while ($i<$scanlines->{'count'}) { ($uname,$udom)=('',''); @@ -7380,7 +7488,7 @@ SCANTRONFORM ($uname,$udom)=split(/:/,$uname); &Apache::lonxml::clear_problem_counter(); - &Apache::lonnet::appenv(%$scan_record); + &Apache::lonnet::appenv($scan_record); if (&scantron_clear_skip($scanlines,$scan_data,$i)) { &scantron_putfile($scanlines,$scan_data); @@ -7401,10 +7509,17 @@ SCANTRONFORM $form{'CODE'}=$scan_record->{'scantron.CODE'}; } else { $form{'CODE'}=''; + } + my $result=&ssi_with_retries($resource->src(), $ssi_retries, %form); + if ($ssi_error) { + $ssi_error = 0; # So end of handler error message does not trigger. + $r->print("</form>"); + &ssi_print_error($r); + $r->print(&show_grading_menu_form($symb)); + &Apache::lonnet::remove_lock($lock); + return ''; # Why return ''? Beats me. } - my $result=&Apache::lonnet::ssi($resource->src(),%form); - if ($result ne '') { - } + if (&Apache::loncommon::connection_aborted($r)) { last; } } $completedstudents{$uname}={'line'=>$line}; @@ -7414,6 +7529,7 @@ SCANTRONFORM &Apache::lonnet::delenv('scantron\.'); } &Apache::lonhtmlcommon::Close_PrgWin($r,\%prog_state); + &Apache::lonnet::remove_lock($lock); # my $lasttime = &Time::HiRes::time()-$start; # $r->print("<p>took $lasttime</p>"); @@ -7422,14 +7538,6 @@ SCANTRONFORM return ''; } -=pod - -=item scantron_upload_scantron_data - - Creates the screen for adding a new bubble sheet data file to a course. - -=cut - sub scantron_upload_scantron_data { my ($r)=@_; $r->print(&Apache::loncommon::coursebrowser_javascript($env{'request.role.domain'})); @@ -7443,7 +7551,7 @@ sub scantron_upload_scantron_data { <script type="text/javascript" language="javascript"> function checkUpload(formname) { if (formname.upfile.value == "") { - alert("Please use the browse button to select a file from your local directory."); + alert("'.&mt('Please use the browse button to select a file from your local directory.').'"); return false; } formname.submit(); @@ -7470,14 +7578,6 @@ sub scantron_upload_scantron_data { return ''; } -=pod - -=item scantron_upload_scantron_data_save - - Adds a provided bubble information data file to the course if user - has the correct privileges to do so. - -=cut sub scantron_upload_scantron_data_save { my($r)=@_; @@ -7539,14 +7639,6 @@ sub scantron_upload_scantron_data_save { return ''; } -=pod - -=item valid_file - - Validates that the requested bubble data file exists in the course. - -=cut - sub valid_file { my ($requested_file)=@_; foreach my $filename (sort(&scantron_filenames())) { @@ -7555,16 +7647,6 @@ sub valid_file { return 0; } -=pod - -=item scantron_download_scantron_data - - Shows a list of the three internal files (original, corrected, - skipped) for a specific bubble sheet data file that exists in the - course. - -=cut - sub scantron_download_scantron_data { my ($r)=@_; my $default_form_data=&defaultFormData(&get_symb($r,1)); @@ -7604,11 +7686,271 @@ sub scantron_download_scantron_data { return ''; } -=pod +sub checkscantron_results { + my ($r) = @_; + my ($symb)=&get_symb($r); + if (!$symb) {return '';} + my $grading_menu_button=&show_grading_menu_form($symb); + my $cid = $env{'request.course.id'}; + my %lettdig = ( + A => 1, + B => 2, + C => 3, + D => 4, + E => 5, + F => 6, + G => 7, + H => 8, + I => 9, + J => 0, + ); + my $numletts = scalar(keys(%lettdig)); + my $cnum = $env{'course.'.$cid.'.num'}; + my $cdom = $env{'course.'.$cid.'.domain'}; + my (undef, undef, $sequence) = &Apache::lonnet::decode_symb($env{'form.selectpage'}); + my %record; + my %scantron_config = + &Apache::grades::get_scantron_config($env{'form.scantron_format'}); + my ($scanlines,$scan_data)=&Apache::grades::scantron_getfile(); + my $classlist=&Apache::loncoursedata::get_classlist(); + my %idmap=&Apache::grades::username_to_idmap($classlist); + my $navmap=Apache::lonnavmaps::navmap->new(); + my $map=$navmap->getResourceByUrl($sequence); + my @resources=$navmap->retrieveResources($map,undef,1,0); + my (%scandata,%lastname,%bylast); + $r->print(' +<form method="post" enctype="multipart/form-data" action="/adm/grades" name="checkscantron">'."\n"); -=back + my @delayqueue; + my %completedstudents; + + my $count=&Apache::grades::get_todo_count($scanlines,$scan_data); + my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin($r,'Scantron/Submissions Comparison Status', + 'Progress of Scantron Data/Submission Records Comparison',$count, + 'inline',undef,'checkscantron'); + my ($username,$domain,$uname,$started); + + &Apache::grades::scantron_get_maxbubble(); # Need the bubble lines array to parse. + + &Apache::lonhtmlcommon::Update_PrgWin($r,\%prog_state, + 'Processing first student'); + my $start=&Time::HiRes::time(); + my $i=-1; + + while ($i<$scanlines->{'count'}) { + ($username,$domain,$uname)=('','',''); + $i++; + my $line=&Apache::grades::scantron_get_line($scanlines,$scan_data,$i); + if ($line=~/^[\s\cz]*$/) { next; } + if ($started) { + &Apache::lonhtmlcommon::Increment_PrgWin($r,\%prog_state, + 'last student'); + } + $started=1; + my $scan_record= + &Apache::grades::scantron_parse_scanline($line,$i,\%scantron_config, + $scan_data); + unless ($uname=&Apache::grades::scantron_find_student($scan_record,$scan_data, + \%idmap,$i)) { + &Apache::grades::scantron_add_delay(\@delayqueue,$line, + 'Unable to find a student that matches',1); + next; + } + if (exists $completedstudents{$uname}) { + &Apache::grades::scantron_add_delay(\@delayqueue,$line, + 'Student '.$uname.' has multiple sheets',2); + next; + } + my $pid = $scan_record->{'scantron.ID'}; + $lastname{$pid} = $scan_record->{'scantron.LastName'}; + push(@{$bylast{$lastname{$pid}}},$pid); + my $lastpos = $env{'form.scantron_maxbubble'}*$scantron_config{'Qlength'}; + $scandata{$pid} = substr($line,$scantron_config{'Qstart'}-1,$lastpos); + chomp($scandata{$pid}); + $scandata{$pid} =~ s/\r$//; + ($username,$domain)=split(/:/,$uname); + my $counter = -1; + my (%expected,%startpos); + foreach my $resource (@resources) { + next if (!$resource->is_problem()); + my $symb = $resource->symb(); + my $partsref = $resource->parts(); + my @parts; + my @part_ids = (); + if (ref($partsref) eq 'ARRAY') { + @parts = @{$partsref}; + foreach my $part (@parts) { + my @resp_ids = $resource->responseIds($part); + foreach my $resp (@resp_ids) { + $counter ++; + my $part_id = $part.'.'.$resp; + $expected{$part_id} = 0; + push(@part_ids,$part_id); + if ($env{"form.scantron.sub_bubblelines.$counter"}) { + my @sub_lines = split(/,/,$env{"form.scantron.sub_bubblelines.$counter"}); + foreach my $item (@sub_lines) { + $expected{$part_id} += $item; + } + } else { + $expected{$part_id} = $env{"form.scantron.bubblelines.$counter"}; + } + $startpos{$part_id} = $env{"form.scantron.first_bubble_line.$counter"}; + } + } + } + if ($symb) { + my %recorded; + my (%returnhash) = + &Apache::lonnet::restore($symb,$cid,$domain,$username); + if ($returnhash{'version'}) { + my %lasthash=(); + my $version; + for ($version=1;$version<=$returnhash{'version'};$version++) { + foreach my $key (sort(split(/\:/,$returnhash{$version.':keys'}))) { + $lasthash{$key}=$returnhash{$version.':'.$key}; + } + } + foreach my $key (keys(%lasthash)) { + if ($key =~ /\.scantron$/) { + my $value = &unescape($lasthash{$key}); + my ($part_id) = ($key =~ /^resource\.(.+)\.scantron$/); + if ($value eq '') { + for (my $i=0; $i<$expected{$part_id}; $i++) { + for (my $j=0; $j<$scantron_config{'length'}; $j++) { + $recorded{$part_id} .= $; + } + } + } else { + my @tocheck; + my @items = split(//,$value); + if (($scantron_config{'Qon'} eq 'letter') || + ($scantron_config{'Qon'} eq 'number')) { + if (@items < $expected{$part_id}) { + my $fragment = substr($scandata{$pid},$startpos{$part_id},$expected{$part_id}); + my @singles = split(//,$fragment); + foreach my $pos (@singles) { + if ($pos eq ' ') { + push(@tocheck,$pos); + } else { + my $next = shift(@items); + push(@tocheck,$next); + } + } + } else { + @tocheck = @items; + } + foreach my $letter (@tocheck) { + if ($scantron_config{'Qon'} eq 'letter') { + if ($letter !~ /^[A-J]$/) { + $letter = $scantron_config{'Qoff'}; + } + $recorded{$part_id} .= $letter; + } elsif ($scantron_config{'Qon'} eq 'number') { + my $digit; + if ($letter !~ /^[A-J]$/) { + $digit = $scantron_config{'Qoff'}; + } else { + $digit = $lettdig{$letter}; + } + $recorded{$part_id} .= $digit; + } + } + } else { + @tocheck = @items; + for (my $i=0; $i<$expected{$part_id}; $i++) { + my $curr_sub = shift(@tocheck); + my $digit; + if ($curr_sub =~ /^[A-J]$/) { + $digit = $lettdig{$curr_sub}-1; + } + if ($curr_sub eq 'J') { + $digit += scalar($numletts); + } + for (my $j=0; $j<$scantron_config{'Qlength'}; $j++) { + if ($j == $digit) { + $recorded{$part_id} .= $scantron_config{'Qon'}; + } else { + $recorded{$part_id} .= $scantron_config{'Qoff'}; + } + } + } + } + } + } + } + } + foreach my $part_id (@part_ids) { + if ($recorded{$part_id} eq '') { + for (my $i=0; $i<$expected{$part_id}; $i++) { + for (my $j=0; $j<$scantron_config{'Qlength'}; $j++) { + $recorded{$part_id} .= $scantron_config{'Qoff'}; + } + } + } + $record{$pid} .= $recorded{$part_id}; + } + } + } + } + &Apache::lonhtmlcommon::Close_PrgWin($r,\%prog_state); + $r->print('<br />'); + my ($okstudents,$badstudents,$numstudents,$passed,$failed); + $passed = 0; + $failed = 0; + $numstudents = 0; + foreach my $last (sort(keys(%bylast))) { + if (ref($bylast{$last}) eq 'ARRAY') { + foreach my $pid (sort(@{$bylast{$last}})) { + my $showscandata = $scandata{$pid}; + my $showrecord = $record{$pid}; + $showscandata =~ s/\s/ /g; + $showrecord =~ s/\s/ /g; + if ($scandata{$pid} eq $record{$pid}) { + my $css_class = ($passed % 2)?'LC_odd_row':'LC_even_row'; + $okstudents .= '<tr class="'.$css_class.'">'. +'<td>'.&mt('Scantron').'</td><td>'.$showscandata.'</td><td rowspan="2">'.$last.'</td><td rowspan="2">'.$pid.'</td>'."\n". +'</tr>'."\n". +'<tr class="'.$css_class.'">'."\n". +'<td>Submissions</td><td>'.$showrecord.'</td></tr>'."\n"; + $passed ++; + } else { + my $css_class = ($failed % 2)?'LC_odd_row':'LC_even_row'; + $badstudents .= '<tr class="'.$css_class.'"><td>'.&mt('Scantron').'</td><td><span class="LC_nobreak">'.$scandata{$pid}.'</span></td><td rowspan="2">'.$last.'</td><td rowspan="2">'.$pid.'</td>'."\n". +'</tr>'."\n". +'<tr class="'.$css_class.'">'."\n". +'<td>Submissions</td><td><span class="LC_nobreak">'.$record{$pid}.'</span></td>'."\n". +'</tr>'."\n"; + $failed ++; + } + $numstudents ++; + } + } + } + $r->print('<p>'.&mt('Comparison of scantron data (including corrections) with corresponding submission records (most recent submission) for <b>[quant,_1,student]</b> ([_2] scantron lines/student).',$numstudents,$env{'form.scantron_maxbubble'}).'</p>'); + $r->print('<p>'.&mt('Exact matches for <b>[quant,_1,student]</b>.',$passed).'<br />'.&mt('Discrepancies detected for <b>[quant,_1,student]</b>.',$failed).'</p>'); + if ($passed) { + $r->print(&mt('Students with exact correspondence between scantron data and submissions are as follows:').'<br /><br />'); + $r->print(&Apache::loncommon::start_data_table()."\n". + &Apache::loncommon::start_data_table_header_row()."\n". + '<th>'.&mt('Source').'</th><th>'.&mt('Bubble records').'</th><th>'.&mt('Name').'</th><th>'.&mt('ID').'</th>'. + &Apache::loncommon::end_data_table_header_row()."\n". + $okstudents."\n". + &Apache::loncommon::end_data_table().'<br />'); + } + if ($failed) { + $r->print(&mt('Students with differences between scantron data and submissions are as follows:').'<br /><br />'); + $r->print(&Apache::loncommon::start_data_table()."\n". + &Apache::loncommon::start_data_table_header_row()."\n". + '<th>'.&mt('Source').'</th><th>'.&mt('Bubble records').'</th><th>'.&mt('Name').'</th><th>'.&mt('ID').'</th>'. + &Apache::loncommon::end_data_table_header_row()."\n". + $badstudents."\n". + &Apache::loncommon::end_data_table()).'<br />'. + &mt('Differences can occur if submissions were modified using manual grading after a scantron grading pass.').'<br />'.&mt('If unexpected discrepancies were detected, it is recommended that you inspect the original scantron sheets.'); + } + $r->print('</form><br />'.$grading_menu_button); + return; +} -=cut #-------- end of section for handling grading scantron forms ------- # @@ -7655,36 +7997,49 @@ sub grading_menu { 'saveState'=>"", 'gradingMenu'=>1, 'showgrading'=>"yes"); - my $url = &Apache::lonhtmlcommon::build_url('grades/',\%fields); - my @menu = ({ url => $url, - name => &mt('Manual Grading/View Submissions'), - short_description => - &mt('Start the process of hand grading submissions.'), - }); + + my $url1 = &Apache::lonhtmlcommon::build_url('grades/',\%fields); + $fields{'command'} = 'csvform'; - $url = &Apache::lonhtmlcommon::build_url('grades/',\%fields); - push (@menu, { url => $url, - name => &mt('Upload Scores'), - short_description => - &mt('Specify a file containing the class scores for current resource.')}); + my $url2 = &Apache::lonhtmlcommon::build_url('grades/',\%fields); + $fields{'command'} = 'processclicker'; - $url = &Apache::lonhtmlcommon::build_url('grades/',\%fields); - push (@menu, { url => $url, - name => &mt('Process Clicker'), - short_description => - &mt('Specify a file containing the clicker information for this resource.')}); + my $url3 = &Apache::lonhtmlcommon::build_url('grades/',\%fields); + $fields{'command'} = 'scantron_selectphase'; - $url = &Apache::lonhtmlcommon::build_url('grades/',\%fields); - push (@menu, { url => $url, - name => &mt('Grade/Manage Scantron Forms'), - short_description => - &mt('')}); - $fields{'command'} = 'verify'; - $url = &Apache::lonhtmlcommon::build_url('grades/',\%fields); - push (@menu, { url => "", - name => &mt('Verify Receipt'), - short_description => - &mt('')}); + my $url4 = &Apache::lonhtmlcommon::build_url('grades/',\%fields); + + my @menu = ({ categorytitle=>'Course Grading', + items =>[ + { linktext => 'Manual Grading/View Submissions', + url => $url1, + permission => 'F', + icon => 'edit-find-replace.png', + linktitle => 'Start the process of hand grading submissions.' + }, + { linktext => 'Upload Scores', + url => $url2, + permission => 'F', + icon => 'uploadscores.png', + linktitle => 'Specify a file containing the class scores for current resource.' + }, + { linktext => 'Process Clicker', + url => $url3, + permission => 'F', + icon => 'addClickerInfoFile.png', + linktitle => 'Specify a file containing the clicker information for this resource.' + }, + { linktext => 'Grade/Manage/Review Scantron Forms', + url => $url4, + permission => 'F', + icon => 'stat.png', + linktitle => 'Grade scantron exams, upload/download scantron data files, and review previously graded scantron exams.' + } + ] + }); + + #$fields{'command'} = 'verify'; + #$url = &Apache::lonhtmlcommon::build_url('grades/',\%fields); # # Create the menu my $Str; @@ -7698,27 +8053,16 @@ sub grading_menu { '<input type="hidden" name="gradingMenu" value="1" />'."\n". '<input type="hidden" name="showgrading" value="yes" />'."\n"; - foreach my $menudata (@menu) { - if ($menudata->{'name'} ne &mt('Verify Receipt')) { - $Str .=' <h3><a '. - $menudata->{'jscript'}. - ' href="'. - $menudata->{'url'}.'" >'. - $menudata->{'name'}."</a></h3>\n"; - } else { - $Str .=' <h3><input type="button" value="'.&mt('Verify Receipt').'" '. - $menudata->{'jscript'}. - ' onClick="javascript:checkChoice(document.forms.gradingMenu,\'5\',\'verify\')" '. - ' /></h3>'; - $Str .= (' 'x8). - &mt(' receipt: [_1]', - &Apache::lonnet::recprefix($env{'request.course.id'}). - '-<input type="text" name="receipt" size="4" onChange="javascript:checkReceiptNo(this.form,\'OK\')" />'); - } - $Str .= ' '.(' 'x8).$menudata->{'short_description'}. - "\n"; - } + $Str .= Apache::lonhtmlcommon::generate_menu(@menu); + #$menudata->{'jscript'} + $Str .='<hr /><input type="button" value="'.&mt('Verify Receipt').'" '. + ' onClick="javascript:checkChoice(document.forms.gradingMenu,\'5\',\'verify\')" '. + ' /> '. + &Apache::lonnet::recprefix($env{'request.course.id'}). + '-<input type="text" name="receipt" size="4" onChange="javascript:checkReceiptNo(this.form,\'OK\')" />'; + $Str .="</form>\n"; + my $receiptalert = &mt("Please enter a receipt number given by a student in the receipt box."); $request->print(<<GRADINGMENUJS); <script type="text/javascript" language="javascript"> function checkChoice(formname,val,cmdx) { @@ -7746,7 +8090,7 @@ sub grading_menu { if (nospace == "OK" && isNaN(receiptNo)) {checkOpt = true;} if (nospace == "notOK" && (isNaN(receiptNo) || receiptNo == "")) {checkOpt = true;} if (checkOpt) { - alert("Please enter a receipt number given by a student in the receipt box."); + alert("$receiptalert"); formname.receipt.value = ""; formname.receipt.focus(); return false; @@ -7767,6 +8111,7 @@ sub submit_options { if (!$symb) {return '';} my $probTitle = &Apache::lonnet::gettitle($symb); + my $receiptalert = &mt("Please enter a receipt number given by a student in the receipt box."); $request->print(<<GRADINGMENUJS); <script type="text/javascript" language="javascript"> function checkChoice(formname,val,cmdx) { @@ -7794,7 +8139,7 @@ sub submit_options { if (nospace == "OK" && isNaN(receiptNo)) {checkOpt = true;} if (nospace == "notOK" && (isNaN(receiptNo) || receiptNo == "")) {checkOpt = true;} if (checkOpt) { - alert("Please enter a receipt number given by a student in the receipt box."); + alert("$receiptalert"); formname.receipt.value = ""; formname.receipt.focus(); return false; @@ -7813,6 +8158,15 @@ GRADINGMENUJS my $saveSub = ($$savedState{'saveSub'} eq '' ? 'all' : $$savedState{'saveSub'}); my $saveStatus = ($$savedState{'saveStatus'} eq '' ? 'Active' : $$savedState{'saveStatus'}); + # Preselect sections + my $selsec=""; + if (ref($sections)) { + foreach my $section (sort(@$sections)) { + $selsec.='<option value="'.$section.'" '. + ($saveSec eq $section ? 'selected="selected"':'').'>'.$section.'</option>'."\n"; + } + } + $result.='<form action="/adm/grades" method="post" name="gradingMenu">'."\n". '<input type="hidden" name="symb" value="'.&Apache::lonenc::check_encrypt($symb).'" />'."\n". '<input type="hidden" name="handgrade" value="'.$hdgrade.'" />'."\n". @@ -7823,102 +8177,91 @@ GRADINGMENUJS '<input type="hidden" name="showgrading" value="yes" />'."\n"; $result.=' - <div class="LC_grade_select_mode"> - <div class="LC_grade_select_mode_current"> - <h2> - '.&mt('Grade Current Resource').' - </h2> - <div class="LC_grade_select_mode_body"> - <div class="LC_grades_resource_info"> - '.$table.' - </div> - <div class="LC_grade_select_mode_selector"> - <div class="LC_grade_select_mode_selector_header"> - '.&mt('Sections').' - </div> - <div class="LC_grade_select_mode_selector_body"> - <select name="section" multiple="multiple" size="5">'."\n"; - if (ref($sections)) { - foreach my $section (sort (@$sections)) { - $result.='<option value="'.$section.'" '. - ($saveSec eq $section ? 'selected="selected"':'').'>'.$section.'</option>'."\n"; - } - } +<h2> + '.&mt('Grade Current Resource').' +</h2> +<div> + '.$table.' +</div> + +<div class="LC_columnSection"> + + <fieldset> + <legend> + '.&mt('Sections').' + </legend> + <select name="section" multiple="multiple" size="5">'."\n"; + $result.= $selsec; $result.= '<option value="all" '.($saveSec eq 'all' ? 'selected="selected"' : ''). '>all</option></select> '; $result.=' - </div> - </div> - <div class="LC_grade_select_mode_selector"> - <div class="LC_grade_select_mode_selector_header"> - '.&mt('Groups').' - </div> - <div class="LC_grade_select_mode_selector_body"> - '.&Apache::lonstatistics::GroupSelect('group','multiple',5).' - </div> - </div> - <div class="LC_grade_select_mode_selector"> - <div class="LC_grade_select_mode_selector_header"> - '.&mt('Access Status').' - </div> - <div class="LC_grade_select_mode_selector_body"> - '.&Apache::lonhtmlcommon::StatusOptions($saveStatus,undef,5,undef,'mult').' - </div> - </div> - <div class="LC_grade_select_mode_selector"> - <div class="LC_grade_select_mode_selector_header"> - '.&mt('Submission Status').' - </div> - <div class="LC_grade_select_mode_selector_body"> - <select name="submitonly" size="5"> + </fieldset> + + <fieldset> + <legend> + '.&mt('Groups').' + </legend> + '.&Apache::lonstatistics::GroupSelect('group','multiple',5).' + </fieldset> + + <fieldset> + <legend> + '.&mt('Access Status').' + </legend> + '.&Apache::lonhtmlcommon::StatusOptions($saveStatus,undef,5,undef,'mult').' + </fieldset> + + <fieldset> + <legend> + '.&mt('Submission Status').' + </legend> + <select name="submitonly" size="5"> <option value="yes" '. ($saveSub eq 'yes' ? 'selected="selected"' : '').'>'.&mt('with submissions').'</option> <option value="queued" '. ($saveSub eq 'queued' ? 'selected="selected"' : '').'>'.&mt('in grading queue').'</option> <option value="graded" '. ($saveSub eq 'graded' ? 'selected="selected"' : '').'>'.&mt('with ungraded submissions').'</option> <option value="incorrect" '.($saveSub eq 'incorrect' ? 'selected="selected"' : '').'>'.&mt('with incorrect submissions').'</option> <option value="all" '. ($saveSub eq 'all' ? 'selected="selected"' : '').'>'.&mt('with any status').'</option> - </select> - </div> - </div> - <div class="LC_grade_select_mode_type_body"> - <div class="LC_grade_select_mode_type"> + </select> + </fieldset> + +</div> + +<br /> + <div> + <div> <label> <input type="radio" name="radioChoice" value="submission" '. ($saveCmd eq 'submission' ? 'checked="checked"' : '').' /> '. &mt('Select individual students to grade and view submissions.').' </label> </div> - <div class="LC_grade_select_mode_type"> + <div> <label> <input type="radio" name="radioChoice" value="viewgrades" '. ($saveCmd eq 'viewgrades' ? 'checked="checked"' : '').' /> '. &mt('Grade all selected students in a grading table.').' </label> </div> - <div class="LC_grade_select_mode_type"> - <input type="button" onClick="javascript:checkChoice(this.form,\'2\');" value="'.&mt('Next->').'" /> + <div> + <input type="button" onClick="javascript:checkChoice(this.form,\'2\');" value="'.&mt('Next').' →" /> </div> </div> - </div> - </div> - <div class="LC_grade_select_mode_page"> + + <h2> '.&mt('Grade Complete Folder for One Student').' </h2> - <div class="LC_grades_select_mode_body"> - <div class="LC_grade_select_mode_type_body"> - <div class="LC_grade_select_mode_type"> + <div> + <div> <label> <input type="radio" name="radioChoice" value="pickStudentPage" '. ($saveCmd eq 'pickStudentPage' ? 'checked="checked"' : '').' /> '. &mt('The <b>complete</b> page/sequence/folder: For one student').' </label> </div> - <div class="LC_grade_select_mode_type"> - <input type="button" onClick="javascript:checkChoice(this.form,\'2\');" value="'.&mt('Next->').'" /> + <div> + <input type="button" onClick="javascript:checkChoice(this.form,\'2\');" value="'.&mt('Next').' →" /> </div> - </div> </div> - </div> - </div> </form>'; $result .= &show_grading_menu_form($symb); return $result; @@ -8017,8 +8360,8 @@ sub process_clicker { $result.=$table; $result.='<br /><table width="100%" border="0"><tr><td bgcolor="#777777">'."\n"; $result.='<table width="100%" border="0"><tr bgcolor="#e6ffff"><td>'."\n"; - $result.=' <b>'.&mt('Specify a file containing the clicker information for this resource'). - '.</b></td></tr>'."\n"; + $result.=' <b>'.&mt('Specify a file containing the clicker information for this resource.'). + '</b></td></tr>'."\n"; $result.='<tr bgcolor=#ffffe6><td>'."\n"; # Attempt to restore parameters from last session, set defaults if not present my %Saveable_Parameters=&clicker_grading_parameters(); @@ -8030,7 +8373,7 @@ sub process_clicker { if (!$env{'form.upfiletype'}) { $env{'form.upfiletype'}='iclicker'; } my %checked; - foreach my $gradingmechanism ('attendance','personnel','specific') { + foreach my $gradingmechanism ('attendance','personnel','specific','given') { if ($env{'form.gradingmechanism'} eq $gradingmechanism) { $checked{$gradingmechanism}="checked='checked'"; } @@ -8041,6 +8384,8 @@ sub process_clicker { my $attendance=&mt("Award points just for participation"); my $personnel=&mt("Correctness determined from response by course personnel"); my $specific=&mt("Correctness determined from response with clicker ID(s)"); + my $given=&mt("Correctness determined from given list of answers").' '. + '<font size="-2"><tt>('.&mt("Provide comma-separated list. Use '*' for any answer correct, '-' for skip").')</tt></font>'; my $pcorrect=&mt("Percentage points for correct solution"); my $pincorrect=&mt("Percentage points for incorrect solution"); my $selectform=&Apache::loncommon::select_form($env{'form.upfiletype'},'upfiletype', @@ -8098,6 +8443,9 @@ function sanitycheck() { <br /><label><input type="radio" name="gradingmechanism" value="personnel" $checked{'personnel'} onClick="sanitycheck()" />$personnel</label> <br /><label><input type="radio" name="gradingmechanism" value="specific" $checked{'specific'} onClick="sanitycheck()" />$specific </label> <input type="text" name="specificid" value="$env{'form.specificid'}" size="20" /> +<br /><label><input type="radio" name="gradingmechanism" value="given" $checked{'given'} onClick="sanitycheck()" />$given </label> +<br /> +<input type="text" name="givenanswer" size="50" /> <input type="hidden" name="waschecked" value="$env{'form.gradingmechanism'}" /> <br /><label>$pcorrect: <input type="text" name="pcorrect" size="4" value="$env{'form.pcorrect'}" onChange="sanitycheck()" /></label> <br /><label>$pincorrect: <input type="text" name="pincorrect" size="4" value="$env{'form.pincorrect'}" onChange="sanitycheck()" /></label> @@ -8124,6 +8472,19 @@ sub process_clicker_file { $result.='<span class="LC_error">'.&mt('You need to specify a clicker ID for the correct answer').'</span>'; return $result.&show_grading_menu_form($symb); } + if (($env{'form.gradingmechanism'} eq 'given') && ($env{'form.givenanswer'}!~/\S/)) { + $result.='<span class="LC_error">'.&mt('You need to specify the correct answer').'</span>'; + return $result.&show_grading_menu_form($symb); + } + my $foundgiven=0; + if ($env{'form.gradingmechanism'} eq 'given') { + $env{'form.givenanswer'}=~s/^\s*//gs; + $env{'form.givenanswer'}=~s/\s*$//gs; + $env{'form.givenanswer'}=~s/[^a-zA-Z0-9\.\*\-]+/\,/g; + $env{'form.givenanswer'}=uc($env{'form.givenanswer'}); + my @answers=split(/\,/,$env{'form.givenanswer'}); + $foundgiven=$#answers+1; + } my %clicker_ids=&gather_clicker_ids(); my %correct_ids; if ($env{'form.gradingmechanism'} eq 'personnel') { @@ -8142,6 +8503,8 @@ sub process_clicker_file { } if ($env{'form.gradingmechanism'} eq 'attendance') { $result.=&mt('Score based on attendance only'); + } elsif ($env{'form.gradingmechanism'} eq 'given') { + $result.=&mt('Score based on [_1] ([_2] answers)','<tt>'.$env{'form.givenanswer'}.'</tt>',$foundgiven); } else { my $number=0; $result.='<p><b>'.&mt('Correctness determined by the following IDs').'</b>'; @@ -8187,6 +8550,9 @@ sub process_clicker_file { <input type="hidden" name="pcorrect" value="$env{'form.pcorrect'}" /> <input type="hidden" name="pincorrect" value="$env{'form.pincorrect'}" /> ENDHEADER + if ($env{'form.gradingmechanism'} eq 'given') { + $result.='<input type="hidden" name="correct:given" value="'.$env{'form.givenanswer'}.'" />'; + } my %responses; my @questiontitles; my $errormsg=''; @@ -8202,6 +8568,10 @@ ENDHEADER &mt('Awarding [_1] percent for correct and [_2] percent for incorrect responses', $env{'form.pcorrect'},$env{'form.pincorrect'}). '<br />'; + if (($env{'form.gradingmechanism'} eq 'given') && ($number!=$foundgiven)) { + $result.='<span class="LC_error">'.&mt('Number of given answers does not agree with number of questions in file.').'</span>'; + return $result.&show_grading_menu_form($symb); + } # Remember Question Titles # FIXME: Possibly need delimiter other than ":" for (my $i=0;$i<$number;$i++) { @@ -8245,7 +8615,7 @@ ENDHEADER } $result.='<hr />'. &mt('Found [_1] registered and [_2] unregistered clickers.',$student_count,$unknown_count); - if ($env{'form.gradingmechanism'} ne 'attendance') { + if (($env{'form.gradingmechanism'} ne 'attendance') && ($env{'form.gradingmechanism'} ne 'given')) { if ($correct_count==0) { $errormsg.="Found no correct answers answers for grading!"; } elsif ($correct_count>1) { @@ -8316,7 +8686,7 @@ sub interwrite_eval { $id=~s/[\-\:]//g; $idresponses{$id}[$number]=$entries[6]; } - foreach my $id (keys %idresponses) { + foreach my $id (keys(%idresponses)) { $$responses{$id}=join(',',@{$idresponses{$id}}); $$responses{$id}=~s/^\s*\,//; } @@ -8390,10 +8760,15 @@ ENDHEADER if ($user) { my @answer=split(/\,/,$env{$key}); my $sum=0; + my $realnumber=$number; for (my $i=0;$i<$number;$i++) { if ($answer[$i]) { if ($gradingmechanism eq 'attendance') { $sum+=$pcorrect; + } elsif ($answer[$i] eq '*') { + $sum+=$pcorrect; + } elsif ($answer[$i] eq '-') { + $realnumber--; } else { if ($answer[$i] eq $correct[$i]) { $sum+=$pcorrect; @@ -8403,7 +8778,7 @@ ENDHEADER } } } - my $ave=$sum/(100*$number); + my $ave=$sum/(100*$realnumber); # Store my ($username,$domain)=split(/\:/,$user); my %grades=(); @@ -8446,8 +8821,10 @@ sub handler { &Apache::lonnet::logthis("grades got multiple commands ".join(':',@commands)); } - - $request->print(&Apache::loncommon::start_page('Grading')); + $ssi_error = 0; + my $brcrum = [{href=>"/adm/grades",text=>"Grading"}]; + $request->print(&Apache::loncommon::start_page('Grading',undef, + {'bread_crumbs' => $brcrum})); if ($symb eq '' && $command eq '') { if ($env{'user.adv'}) { if (($env{'form.codeone'}) && ($env{'form.codetwo'}) && @@ -8459,7 +8836,7 @@ sub handler { if ($tsymb) { my ($map,$id,$url)=&Apache::lonnet::decode_symb($tsymb); if (&Apache::lonnet::allowed('mgr',$tcrsid)) { - $request->print(&Apache::lonnet::ssi_body('/res/'.$url, + $request->print(&ssi_with_retries('/res/'.$url, $ssi_retries, ('grade_username' => $tuname, 'grade_domain' => $tudom, 'grade_courseid' => $tcrsid, @@ -8542,10 +8919,15 @@ sub handler { } elsif ($command eq 'scantron_download' && &Apache::lonnet::allowed('usc',$env{'request.course.id'})) { $request->print(&scantron_download_scantron_data($request)); + } elsif ($command eq 'checksubmissions' && $perm{'vgr'}) { + $request->print(&checkscantron_results($request)); } elsif ($command) { $request->print("Access Denied ($command)"); } } + if ($ssi_error) { + &ssi_print_error($request); + } $request->print(&Apache::loncommon::end_page()); &reset_caches(); return ''; @@ -8554,3 +8936,162 @@ sub handler { 1; __END__; + + +=head1 NAME + +Apache::grades + +=head1 SYNOPSIS + +Handles the viewing of grades. + +This is part of the LearningOnline Network with CAPA project +described at http://www.lon-capa.org. + +=head1 OVERVIEW + +Do an ssi with retries: +While I'd love to factor out this with the vesrion in lonprintout, +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 +I'm not quite ready to invent (e.g. an ssi_with_retry object). + +At least the logic that drives this has been pulled out into loncommon. + + + +ssi_with_retries - Does the server side include of a resource. + if the ssi call returns an error we'll retry it up to + the number of times requested by the caller. + If we still have a proble, no text is appended to the + output and we set some global variables. + to indicate to the caller an SSI error occurred. + All of this is supposed to deal with the issues described + in LonCAPA BZ 5631 see: + http://bugs.lon-capa.org/show_bug.cgi?id=5631 + by informing the user that this happened. + +Parameters: + resource - The resource to include. This is passed directly, without + interpretation to lonnet::ssi. + form - The form hash parameters that guide the interpretation of the resource + + retries - Number of retries allowed before giving up completely. +Returns: + On success, returns the rendered resource identified by the resource parameter. +Side Effects: + The following global variables can be set: + ssi_error - If an unrecoverable error occurred this becomes true. + It is up to the caller to initialize this to false + if desired. + ssi_error_resource - If an unrecoverable error occurred, this is the value + of the resource that could not be rendered by the ssi + call. + ssi_error_message - The error string fetched from the ssi response + in the event of an error. + + +=head1 HANDLER SUBROUTINE + +ssi_with_retries() + +=head1 SUBROUTINES + +=over + +=item scantron_get_correction() : + + Builds the interface screen to interact with the operator to fix a + specific error condition in a specific scanline + + Arguments: + $r - Apache request object + $i - number of the current scanline + $scan_record - hash ref as returned from &scantron_parse_scanline() + $scan_config - hash ref as returned from &get_scantron_config() + $line - full contents of the current scanline + $error - error condition, valid values are + 'incorrectCODE', 'duplicateCODE', + 'doublebubble', 'missingbubble', + 'duplicateID', 'incorrectID' + $arg - extra information needed + For errors: + - duplicateID - paper number that this studentID was seen before on + - duplicateCODE - array ref of the paper numbers this CODE was + seen on before + - incorrectCODE - current incorrect CODE + - doublebubble - array ref of the bubble lines that have double + bubble errors + - missingbubble - array ref of the bubble lines that have missing + bubble errors + +=item scantron_get_maxbubble() : + + Returns the maximum number of bubble lines that are expected to + occur. Does this by walking the selected sequence rendering the + resource and then checking &Apache::lonxml::get_problem_counter() + for what the current value of the problem counter is. + + Caches the results to $env{'form.scantron_maxbubble'}, + $env{'form.scantron.bubble_lines.n'}, + $env{'form.scantron.first_bubble_line.n'} and + $env{"form.scantron.sub_bubblelines.n"} + which are the total number of bubble, lines, the number of bubble + lines for response n and number of the first bubble line for response n, + and a comma separated list of numbers of bubble lines for sub-questions + (for optionresponse, matchresponse, and rankresponse items), for response n. + + +=item scantron_validate_missingbubbles() : + + Validates all scanlines in the selected file to not have any + answers that don't have bubbles that have not been verified + to be bubble free. + +=item scantron_process_students() : + + Routine that does the actual grading of the bubble sheet information. + + The parsed scanline hash is added to %env + + Then foreach unskipped scanline it does an &Apache::lonnet::ssi() + foreach resource , with the form data of + + 'submitted' =>'scantron' + 'grade_target' =>'grade', + 'grade_username'=> username of student + 'grade_domain' => domain of student + 'grade_courseid'=> of course + 'grade_symb' => symb of resource to grade + + This triggers a grading pass. The problem grading code takes care + of converting the bubbled letter information (now in %env) into a + valid submission. + +=item scantron_upload_scantron_data() : + + Creates the screen for adding a new bubble sheet data file to a course. + +=item scantron_upload_scantron_data_save() : + + Adds a provided bubble information data file to the course if user + has the correct privileges to do so. + +=item valid_file() : + + Validates that the requested bubble data file exists in the course. + +=item scantron_download_scantron_data() : + + Shows a list of the three internal files (original, corrected, + skipped) for a specific bubble sheet data file that exists in the + course. + +=item scantron_validate_ID() : + + Validates all scanlines in the selected file to not have any + invalid or underspecified student IDs + +=back + +=cut