--- loncom/homework/grades.pm 2007/10/26 00:39:09 1.467 +++ loncom/homework/grades.pm 2015/03/10 17:01:24 1.596.2.12.2.33 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # The LON-CAPA Grading handler # -# $Id: grades.pm,v 1.467 2007/10/26 00:39:09 albertel Exp $ +# $Id: grades.pm,v 1.596.2.12.2.33 2015/03/10 17:01:24 raeburn 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; @@ -38,105 +40,74 @@ use Apache::lonhomework; use Apache::lonpickcode; use Apache::loncoursedata; use Apache::lonmsg(); -use Apache::Constants qw(:common); +use Apache::Constants qw(:common :http); use Apache::lonlocal; use Apache::lonenc; +use Apache::bridgetask(); use String::Similarity; use LONCAPA; use POSIX qw(floor); + my %perm=(); -my %bubble_lines_per_response = (); # no. bubble lines for each response. - # index is "symb.part_id" +my %old_essays=(); -my %first_bubble_line = (); # First bubble line no. for each bubble. +# These variables are used to recover from ssi errors -# Save and restore the bubble lines array to the form env. +my $ssi_retries = 5; +my $ssi_error; +my $ssi_error_resource; +my $ssi_error_message; -sub save_bubble_lines { - foreach my $line (keys(%bubble_lines_per_response)) { - $env{"form.scantron.bubblelines.$line"} = $bubble_lines_per_response{$line}; - $env{"form.scantron.first_bubble_line.$line"} = - $first_bubble_line{$line}; - } -} - - -sub restore_bubble_lines { - my $line = 0; - %bubble_lines_per_response = (); - while ($env{"form.scantron.bubblelines.$line"}) { - my $value = $env{"form.scantron.bubblelines.$line"}; - $bubble_lines_per_response{$line} = $value; - $first_bubble_line{$line} = - $env{"form.scantron.first_bubble_line.$line"}; - $line++; +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; } -} - -# Given the parsed scanline, get the response for -# 'answer' number n: + return $content; -sub get_response_bubbles { - my ($parsed_line, $response) = @_; - - - my $bubble_line = $first_bubble_line{$response-1} +1; - my $bubble_lines= $bubble_lines_per_response{$response-1}; - - my $selected = ""; - - for (my $bline = 0; $bline < $bubble_lines; $bline++) { - $selected .= $$parsed_line{"scantron.$bubble_line.answer"}.":"; - $bubble_line++; - } - return $selected; } +# +# Prodcuces an ssi retry failure error message to the user: +# - -# ----- These first few routines are general use routines.---- - -# Return the number of occurences of a pattern in a string. - -sub occurence_count { - my ($string, $pattern) = @_; - - my @matches = ($string =~ /$pattern/g); - - return scalar(@matches); -} - - -# Take a string known to have digits and convert all the -# digits into letters in the range J,A..I. - -sub digits_to_letters { - my ($input) = @_; - - my @alphabet = ('J', 'A'..'I'); - - my @input = split(//, $input); - my $output =''; - for (my $i = 0; $i < scalar(@input); $i++) { - if ($input[$i] =~ /\d/) { - $output .= $alphabet[$input[$i]]; - } else { - $output .= $input[$i]; - } - } - return $output; +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 { - my ($symb) = @_; + my ($symb,$errorref) = @_; my $navmap = Apache::lonnavmaps::navmap->new(); + unless (ref($navmap)) { + if (ref($errorref)) { + $$errorref = 'navmap'; + return; + } + } my $res = $navmap->getBySymb($symb); my $partlist = $res->parts(); my $url = $res->src(); @@ -154,13 +125,16 @@ sub getpartlist { # --- Get the symbolic name of a problem and the url sub get_symb { my ($request,$silent) = @_; - (my $url=$env{'form.url'}) =~ s-^http://($ENV{'SERVER_NAME'}|$ENV{'HTTP_HOST'})--; - my $symb=($env{'form.symb'} ne '' ? $env{'form.symb'} : (&Apache::lonnet::symbread($url))); - if ($symb eq '') { - if (!$silent) { - $request->print("Unable to handle ambiguous references:$url:."); - return (); - } + my $symb=$env{'form.symb'}; + unless ($symb) { + (my $url=$env{'form.url'}) =~ s-^http://($ENV{'SERVER_NAME'}|$ENV{'HTTP_HOST'})--; + $symb = &Apache::lonnet::symbread($url); + if ($symb eq '') { + if (!$silent) { + $request->print(&mt("Unable to handle ambiguous references: [_1].",$url)); + return (); + } + } } &Apache::lonenc::check_decrypt(\$symb); return ($symb); @@ -171,7 +145,7 @@ sub get_symb { sub nameUserString { my ($type,$fullname,$uname,$udom) = @_; if ($type eq 'header') { - return '<b> Fullname </b><span class="LC_internal_info">(Username)</span>'; + return '<b> '.&mt('Fullname').' </b><span class="LC_internal_info">('.&mt('Username').')</span>'; } else { return ' '.$fullname.'<span class="LC_internal_info"> ('.$uname. ($env{'user.domain'} eq $udom ? '' : ' ('.$udom.')').')</span>'; @@ -181,10 +155,20 @@ sub nameUserString { #--- Get the partlist and the response type for a given problem. --- #--- Indicate if a response type is coded handgraded or not. --- sub response_type { - my ($symb) = shift; + my ($symb,$response_error) = @_; my $navmap = Apache::lonnavmaps::navmap->new(); + unless (ref($navmap)) { + if (ref($response_error)) { + $$response_error = 1; + } + return; + } my $res = $navmap->getBySymb($symb); + unless (ref($res)) { + $$response_error = 1; + return; + } my $partlist = $res->parts(); my %vPart = map { $_ => 1 } (&Apache::loncommon::get_env_multiple('form.vPart')); @@ -220,7 +204,8 @@ sub get_display_part { my ($partID,$symb)=@_; my $display=&Apache::lonnet::EXT('resource.'.$partID.'.display',$symb); if (defined($display) and $display ne '') { - $display.= " (<span class=\"LC_internal_info\">id $partID</span>)"; + $display.= ' (<span class="LC_internal_info">' + .&mt('Part ID: [_1]',$partID).'</span>)'; } else { $display=$partID; } @@ -230,86 +215,171 @@ sub get_display_part { #--- Show resource title #--- and parts and response type sub showResourceInfo { - my ($symb,$probTitle,$checkboxes) = @_; - my $col=3; - if ($checkboxes) { $col=4; } + my ($symb,$probTitle,$checkboxes,$res_error) = @_; my $result = '<h3>'.&mt('Current Resource').': '.$probTitle.'</h3>'."\n"; - $result .='<table border="0">'; - my ($partlist,$handgrade,$responseType) = &response_type($symb); + my ($partlist,$handgrade,$responseType) = &response_type($symb,$res_error); + if (ref($res_error)) { + if ($$res_error) { + return; + } + } + $result.=&Apache::loncommon::start_data_table() + .&Apache::loncommon::start_data_table_header_row(); + if ($checkboxes) { + $result.='<th> </th>'; + } + $result.='<th>'.&mt('Problem Part').'</th>' + .'<th>'.&mt('Res. ID').'</th>' + .'<th>'.&mt('Type').'</th>' + .&Apache::loncommon::end_data_table_header_row(); my %resptype = (); my $hdgrade='no'; my %partsseen; - 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'); - $result.='<tr>'; - if ($checkboxes) { - if (exists($partsseen{$partID})) { - $result.="<td> </td>"; - } else { - $result.="<td><input type='checkbox' name='vPart' value='$partID' checked='checked' /></td>"; - } - $partsseen{$partID}=1; - } - my $display_part=&get_display_part($partID,$symb); - $result.='<td><b>Part: </b>'.$display_part.' <span class="LC_internal_info">'. - $resID.'</span></td>'. - '<td><b>Type: </b>'.$responsetype.'</td></tr>'; -# '<td><b>Handgrade: </b>'.$handgrade.'</td></tr>'; - } + 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'); + $result.=&Apache::loncommon::start_data_table_row(); + if ($checkboxes) { + if (exists($partsseen{$partID})) { + $result.="<td> </td>"; + } else { + $result.="<td><input type='checkbox' name='vPart' value='$partID' checked='checked' /></td>"; + } + $partsseen{$partID}=1; + } + my $display_part=&get_display_part($partID,$symb); + $result.='<td>'.$display_part.'</td>' + .'<td>'.'<span class="LC_internal_info">'.$resID.'</span></td>' + .'<td>'.&mt($responsetype).'</td>' +# .'<td><b>'.&mt('Handgrade: [_1]',$handgrade).'</b></td>' + .&Apache::loncommon::end_data_table_row(); + } } - $result.='</table>'."\n"; + $result.=&Apache::loncommon::end_data_table(); return $result,$responseType,$hdgrade,$partlist,$handgrade; } sub reset_caches { &reset_analyze_cache(); &reset_perm(); + &reset_old_essays(); } { my %analyze_cache; + my %analyze_cache_formkeys; sub reset_analyze_cache { undef(%analyze_cache); + undef(%analyze_cache_formkeys); } sub get_analyze { - my ($symb,$uname,$udom)=@_; + my ($symb,$uname,$udom,$no_increment,$add_to_hash,$type,$trial,$rndseed,$bubbles_per_row)=@_; my $key = "$symb\0$uname\0$udom"; - return $analyze_cache{$key} if (exists($analyze_cache{$key})); + if ($type eq 'randomizetry') { + if ($trial ne '') { + $key .= "\0".$trial; + } + } + if (exists($analyze_cache{$key})) { + my $getupdate = 0; + if (ref($add_to_hash) eq 'HASH') { + foreach my $item (keys(%{$add_to_hash})) { + if (ref($analyze_cache_formkeys{$key}) eq 'HASH') { + if (!exists($analyze_cache_formkeys{$key}{$item})) { + $getupdate = 1; + last; + } + } else { + $getupdate = 1; + } + } + } + if (!$getupdate) { + return $analyze_cache{$key}; + } + } my (undef,undef,$url)=&Apache::lonnet::decode_symb($symb); $url=&Apache::lonnet::clutter($url); - my $subresult=&Apache::lonnet::ssi($url, - ('grade_target' => 'analyze'), - ('grade_domain' => $udom), - ('grade_symb' => $symb), - ('grade_courseid' => - $env{'request.course.id'}), - ('grade_username' => $uname)); + my %form = ('grade_target' => 'analyze', + 'grade_domain' => $udom, + 'grade_symb' => $symb, + 'grade_courseid' => $env{'request.course.id'}, + 'grade_username' => $uname, + 'grade_noincrement' => $no_increment); + if ($bubbles_per_row ne '') { + $form{'bubbles_per_row'} = $bubbles_per_row; + } + if ($type eq 'randomizetry') { + $form{'grade_questiontype'} = $type; + if ($rndseed ne '') { + $form{'grade_rndseed'} = $rndseed; + } + } + if (ref($add_to_hash)) { + %form = (%form,%{$add_to_hash}); + } + my $subresult=&ssi_with_retries($url, $ssi_retries,%form); (undef,$subresult)=split(/_HASH_REF__/,$subresult,2); my %analyze=&Apache::lonnet::str2hash($subresult); + if (ref($add_to_hash) eq 'HASH') { + $analyze_cache_formkeys{$key} = $add_to_hash; + } else { + $analyze_cache_formkeys{$key} = {}; + } return $analyze_cache{$key} = \%analyze; } sub get_order { - my ($partid,$respid,$symb,$uname,$udom)=@_; - my $analyze = &get_analyze($symb,$uname,$udom); + my ($partid,$respid,$symb,$uname,$udom,$no_increment,$type,$trial,$rndseed)=@_; + my $analyze = &get_analyze($symb,$uname,$udom,$no_increment,undef,$type,$trial,$rndseed); return $analyze->{"$partid.$respid.shown"}; } sub get_radiobutton_correct_foil { - my ($partid,$respid,$symb,$uname,$udom)=@_; - my $analyze = &get_analyze($symb,$uname,$udom); - foreach my $foil (@{&get_order($partid,$respid,$symb,$uname,$udom)}) { - if ($analyze->{"$partid.$respid.foil.value.$foil"} eq 'true') { - return $foil; + my ($partid,$respid,$symb,$uname,$udom,$type,$trial,$rndseed)=@_; + my $analyze = &get_analyze($symb,$uname,$udom,undef,undef,$type,$trial,$rndseed); + my $foils = &get_order($partid,$respid,$symb,$uname,$udom,undef,$type,$trial,$rndseed); + if (ref($foils) eq 'ARRAY') { + foreach my $foil (@{$foils}) { + if ($analyze->{"$partid.$respid.foil.value.$foil"} eq 'true') { + return $foil; + } } } } + + sub scantron_partids_tograde { + my ($resource,$cid,$uname,$udom,$check_for_randomlist,$bubbles_per_row) = @_; + my (%analysis,@parts); + if (ref($resource)) { + my $symb = $resource->symb(); + my $add_to_form; + if ($check_for_randomlist) { + $add_to_form = { 'check_parts_withrandomlist' => 1,}; + } + my $analyze = + &get_analyze($symb,$uname,$udom,undef,$add_to_form, + undef,undef,undef,$bubbles_per_row); + if (ref($analyze) eq 'HASH') { + %analysis = %{$analyze}; + } + if (ref($analysis{'parts'}) eq 'ARRAY') { + foreach my $part (@{$analysis{'parts'}}) { + my ($id,$respid) = split(/\./,$part); + if (!&Apache::loncommon::check_if_partid_hidden($id,$symb,$udom,$uname)) { + push(@parts,$part); + } + } + } + } + return (\%analysis,\@parts); + } + } #--- Clean response type for display @@ -317,10 +387,12 @@ sub reset_caches { # response types only. sub cleanRecord { my ($answer,$response,$symb,$partid,$respid,$record,$order,$version, - $uname,$udom) = @_; + $uname,$udom,$type,$trial,$rndseed) = @_; my $grayFont = '<span class="LC_internal_info">'; if ($response =~ /^(option|rank)$/) { my %answer=&Apache::lonnet::str2hash($answer); + my @answer = %answer; + %answer = map {&HTML::Entities::encode($_, '"<>&')} @answer; my %grading=&Apache::lonnet::str2hash($record->{$version."resource.$partid.$respid.submissiongrading"}); my ($toprow,$bottomrow); foreach my $foil (@$order) { @@ -334,9 +406,11 @@ sub cleanRecord { return '<blockquote><table border="1">'. '<tr valign="top"><td>'.&mt('Answer').'</td>'.$toprow.'</tr>'. '<tr valign="top"><td>'.$grayFont.&mt('Option ID').'</span></td>'. - $grayFont.$bottomrow.'</tr>'.'</table></blockquote>'; + $bottomrow.'</tr></table></blockquote>'; } elsif ($response eq 'match') { my %answer=&Apache::lonnet::str2hash($answer); + my @answer = %answer; + %answer = map {&HTML::Entities::encode($_, '"<>&')} @answer; my %grading=&Apache::lonnet::str2hash($record->{$version."resource.$partid.$respid.submissiongrading"}); my @items=&Apache::lonnet::str2array($record->{$version."resource.$partid.$respid.submissionitems"}); my ($toprow,$middlerow,$bottomrow); @@ -356,12 +430,12 @@ sub cleanRecord { '<tr valign="top"><td>'.$grayFont.&mt('Item ID').'</span></td>'. $middlerow.'</tr>'. '<tr valign="top"><td>'.$grayFont.&mt('Option ID').'</span></td>'. - $bottomrow.'</tr>'.'</table></blockquote>'; + $bottomrow.'</tr></table></blockquote>'; } elsif ($response eq 'radiobutton') { my %answer=&Apache::lonnet::str2hash($answer); my ($toprow,$bottomrow); my $correct = - &get_radiobutton_correct_foil($partid,$respid,$symb,$uname,$udom); + &get_radiobutton_correct_foil($partid,$respid,$symb,$uname,$udom,$type,$trial,$rndseed); foreach my $foil (@$order) { if (exists($answer{$foil})) { if ($foil eq $correct) { @@ -377,7 +451,7 @@ sub cleanRecord { return '<blockquote><table border="1">'. '<tr valign="top"><td>'.&mt('Answer').'</td>'.$toprow.'</tr>'. '<tr valign="top"><td>'.$grayFont.&mt('Option ID').'</span></td>'. - $grayFont.$bottomrow.'</tr>'.'</table></blockquote>'; + $bottomrow.'</tr></table></blockquote>'; } elsif ($response eq 'essay') { if (! exists ($env{'form.'.$symb})) { my (%keyhash) = &Apache::lonnet::dump('nohist_handgrade', @@ -391,10 +465,10 @@ sub cleanRecord { $env{'form.kwstyle'} = $keyhash{$loginuser.'_kwstyle'} ne '' ? $keyhash{$loginuser.'_kwstyle'} : ''; $env{'form.'.$symb} = 1; # so that we don't have to read it from disk for multiple sub of the same prob. } - $answer =~ s-\n-<br />-g; return '<br /><br /><blockquote><tt>'.&keywords_highlight($answer).'</tt></blockquote>'; } elsif ( $response eq 'organic') { - my $result='Smile representation: "<tt>'.$answer.'</tt>"'; + my $result=&mt('Smile representation: [_1]', + '"<tt>'.&HTML::Entities::encode($answer, '"<>&').'</tt>"'); my $jme=$record->{$version."resource.$partid.$respid.molecule"}; $result.=&Apache::chemresponse::jme_img($jme,$answer,400); return $result; @@ -428,12 +502,14 @@ sub cleanRecord { $result.='</ul>'; return $result; } - } elsif ( $response =~ m/(?:numerical|formula)/) { + } elsif ( $response =~ m/(?:numerical|formula|custom)/) { + # Respect multiple input fields, see Bug #5409 $answer = &Apache::loncommon::format_previous_attempt_value('submission', $answer); + return $answer; } - return $answer; + return &HTML::Entities::encode($answer, '"<>&'); } #-- A couple of common js functions @@ -678,7 +754,11 @@ sub compute_points { # sub most_similar { - my ($uname,$udom,$uessay,$old_essays)=@_; + my ($uname,$udom,$symb,$uessay)=@_; + + unless ($symb) { return ''; } + + unless (ref($old_essays{$symb}) eq 'HASH') { return ''; } # ignore spaces and punctuation @@ -686,7 +766,7 @@ sub most_similar { # ignore empty submissions (occuring when only files are sent) - unless ($uessay=~/\w+/) { return ''; } + unless ($uessay=~/\w+/s) { return ''; } # these will be returned. Do not care if not at least 50 percent similar my $limit=0.6; @@ -695,11 +775,11 @@ sub most_similar { my $scrsid=''; my $sessay=''; # go through all essays ... - foreach my $tkey (keys(%$old_essays)) { + foreach my $tkey (keys(%{$old_essays{$symb}})) { my ($tname,$tdom,$tcrsid)=map {&unescape($_)} (split(/\./,$tkey)); # ... except the same student next if (($tname eq $uname) && ($tdom eq $udom)); - my $tessay=$old_essays->{$tkey}; + my $tessay=$old_essays{$symb}{$tkey}; $tessay=~s/\W+/ /gs; # String similarity gives up if not even limit my $tsimilar=&String::Similarity::similarity($uessay,$tessay,$limit); @@ -709,7 +789,7 @@ sub most_similar { $sname=$tname; $sdom=$tdom; $scrsid=$tcrsid; - $sessay=$old_essays->{$tkey}; + $sessay=$old_essays{$symb}{$tkey}; } } if ($limit>0.6) { @@ -733,9 +813,12 @@ sub verifyreceipt { $receipt =~ s/[^\-\d]//g; my ($symb) = &get_symb($request); - my $title.='<h3><span class="LC_info">Verifying Submission Receipt '. - $receipt.'</h3></span>'."\n". - '<h4><b>Resource: </b>'.$env{'form.probTitle'}.'</h4><br /><br />'."\n"; + my $title.= + '<h3><span class="LC_info">'. + &mt('Verifying Receipt No. [_1]',$receipt). + '</span></h3>'."\n". + '<h4>'.&mt('[_1]Resource: [_2]','<b>','</b>'.$env{'form.probTitle'}). + '</h4>'."\n"; my ($string,$contents,$matches) = ('','',0); my (undef,undef,$fullname) = &getclasslist('all','0'); @@ -744,7 +827,26 @@ sub verifyreceipt { if ($env{"course.$courseid.receiptalg"} eq 'receipt2' || $env{"course.$courseid.receiptalg"} eq 'receipt3') { $receiptparts=1; } my $parts=['0']; - if ($receiptparts) { ($parts)=&response_type($symb); } + if ($receiptparts) { + my $res_error; + ($parts)=&response_type($symb,\$res_error); + if ($res_error) { + return &navmap_errormsg(); + } + } + + my $header = + &Apache::loncommon::start_data_table(). + &Apache::loncommon::start_data_table_header_row(). + '<th> '.&mt('Fullname').' </th>'."\n". + '<th> '.&mt('Username').' </th>'."\n". + '<th> '.&mt('Domain').' </th>'; + if ($receiptparts) { + $header.='<th> '.&mt('Problem Part').' </th>'; + } + $header.= + &Apache::loncommon::end_data_table_header_row(); + foreach (sort { if (lc($$fullname{$a}) ne lc($$fullname{$b})) { @@ -755,7 +857,9 @@ sub verifyreceipt { my ($uname,$udom)=split(/\:/); foreach my $part (@$parts) { if ($receipt eq &Apache::lonnet::ireceipt($uname,$udom,$courseid,$symb,$part)) { - $contents.='<tr bgcolor="#ffffe6"><td> '."\n". + $contents.= + &Apache::loncommon::start_data_table_row(). + '<td> '."\n". '<a href="javascript:viewOneStudent(\''.$uname.'\',\''.$udom. '\');" target="_self">'.$$fullname{$_}.'</a> </td>'."\n". '<td> '.$uname.' </td>'. @@ -763,28 +867,26 @@ sub verifyreceipt { if ($receiptparts) { $contents.='<td> '.$part.' </td>'; } - $contents.='</tr>'."\n"; + $contents.= + &Apache::loncommon::end_data_table_row()."\n"; $matches++; } } } if ($matches == 0) { - $string = $title.'No match found for the above receipt.'; + $string = $title + .'<p class="LC_warning">' + .&mt('No match found for the above receipt number.') + .'</p>'; } else { $string = &jscriptNform($symb).$title. - 'The above receipt matches the following student'. - ($matches <= 1 ? '.' : 's.')."\n". - '<table border="0"><tr><td bgcolor="#777777">'."\n". - '<table border="0"><tr bgcolor="#e6ffff">'."\n". - '<td><b> Fullname </b></td>'."\n". - '<td><b> Username </b></td>'."\n". - '<td><b> Domain </b></td>'; - if ($receiptparts) { - $string.='<td> Problem Part </td>'; - } - $string.='</tr>'."\n".$contents. - '</table></td></tr></table>'."\n"; + '<p>'. + &mt('The above receipt number matches the following [quant,_1,student].',$matches). + '</p>'. + $header. + $contents. + &Apache::loncommon::end_data_table()."\n"; } return $string.&show_grading_menu_form($symb); } @@ -806,11 +908,16 @@ sub listStudents { $env{'form.probTitle'} = $env{'form.probTitle'} eq '' ? &Apache::lonnet::gettitle($symb) : $env{'form.probTitle'}; - my $result='<h3><span class="LC_info"> '.$viewgrade. - ' Submissions for a Student or a Group of Students</span></h3>'; + my $result='<h3><span class="LC_info"> ' + .&mt("$viewgrade 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 = &Apache::lonlocal::texthash ( + 'multiple' => '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.', + ); $request->print(<<LISTJAVASCRIPT); <script type="text/javascript" language="javascript"> function checkSelect(checkBox) { @@ -822,15 +929,15 @@ sub listStudents { ctr++; } } - sense = "a student or group of students"; + sense = '$lt{'multiple'}'; } else { if (checkBox.checked) { ctr = 1; } - sense = "the student"; + sense = '$lt{'single'}'; } if (ctr == 0) { - alert("Please select "+sense+" before clicking on the Next button."); + alert(sense); return false; } document.gradesub.submit(); @@ -850,30 +957,55 @@ LISTJAVASCRIPT my $checkhdgrade = ($env{'form.handgrade'} eq 'yes' && scalar(@$partlist) > 1 ) ? 'checked="checked"' : ''; my $checklastsub = $checkhdgrade eq '' ? 'checked="checked"' : ''; my $gradeTable='<form action="/adm/grades" method="post" name="gradesub">'. - "\n".$table. - ' <b>View Problem Text: </b><label><input type="radio" name="vProb" value="no" checked="checked" /> no </label>'."\n". - '<label><input type="radio" name="vProb" value="yes" /> one student </label>'."\n". - '<label><input type="radio" name="vProb" value="all" /> all students </label><br />'."\n". - ' <b>View Answer: </b><label><input type="radio" name="vAns" value="no" /> no </label>'."\n". - '<label><input type="radio" name="vAns" value="yes" /> one student </label>'."\n". - '<label><input type="radio" name="vAns" value="all" checked="checked" /> all students </label><br />'."\n". - ' <b>Submissions: </b>'."\n"; + "\n".$table; + + $gradeTable .= &Apache::lonhtmlcommon::start_pick_box(); + $gradeTable .= &Apache::lonhtmlcommon::row_title(&mt('View Problem Text')) + .'<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" + .&Apache::lonhtmlcommon::row_closure(); + $gradeTable .= &Apache::lonhtmlcommon::row_title(&mt('View Answer')) + .'<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" + .&Apache::lonhtmlcommon::row_closure(); + + my $submission_options; if ($env{'form.handgrade'} eq 'yes' && scalar(@$partlist) > 1) { - $gradeTable.='<label><input type="radio" name="lastSub" value="hdgrade" '.$checkhdgrade.' /> essay part only </label>'."\n"; + $submission_options.= + '<label><input type="radio" name="lastSub" value="hdgrade" '.$checkhdgrade.' /> '.&mt('essay part only').' </label>'."\n"; } my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status')); my $saveStatus = $stu_status eq '' ? 'Active' : $stu_status; $env{'form.Status'} = $saveStatus; - $gradeTable.='<label><input type="radio" name="lastSub" value="lastonly" '.$checklastsub.' /> last submission only </label>'."\n". - '<label><input type="radio" name="lastSub" value="last" /> last submission & parts info </label>'."\n". - '<label><input type="radio" name="lastSub" value="datesub" /> by dates and submissions </label>'."\n". - '<label><input type="radio" name="lastSub" value="all" /> all details</label><br />'."\n". - ' <b>Grading Increments:</b> <select name="increment">'. - '<option value="1">Whole Points</option>'. - '<option value=".5">Half Points</option>'. - '<option value=".25">Quarter Points</option>'. - '<option value=".1">Tenths of a Point</option>'. - '</select>'. + $submission_options.= + '<span class="LC_nobreak">'. + '<label><input type="radio" name="lastSub" value="lastonly" '.$checklastsub.' /> '. + &mt('last submission only').' </label></span>'."\n". + '<span class="LC_nobreak">'. + '<label><input type="radio" name="lastSub" value="last" /> '. + &mt('last submission & parts info').' </label></span>'."\n". + '<span class="LC_nobreak">'. + '<label><input type="radio" name="lastSub" value="datesub" /> '. + &mt('by dates and submissions').'</label></span>'."\n". + '<span class="LC_nobreak">'. + '<label><input type="radio" name="lastSub" value="all" /> '. + &mt('all details').'</label></span>'; + $gradeTable .= &Apache::lonhtmlcommon::row_title(&mt('Submissions')) + .$submission_options + .&Apache::lonhtmlcommon::row_closure(); + + $gradeTable .= &Apache::lonhtmlcommon::row_title(&mt('Grading Increments')) + .'<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>' + .&Apache::lonhtmlcommon::row_closure(); + + $gradeTable .= &build_section_inputs(). '<input type="hidden" name="submitonly" value="'.$submitonly.'" />'."\n". '<input type="hidden" name="handgrade" value="'.$env{'form.handgrade'}.'" /><br />'."\n". @@ -884,45 +1016,53 @@ LISTJAVASCRIPT '<input type="hidden" name="saveStatusOld" value="'.$saveStatus.'" />'."\n"; if (exists($env{'form.gradingMenu'}) && exists($env{'form.Status'})) { - $gradeTable.='<input type="hidden" name="Status" value="'.$stu_status.'" />'."\n"; + $gradeTable .= '<input type="hidden" name="Status" value="'.$stu_status.'" />'."\n"; } else { - $gradeTable.='<b>Student Status:</b> '. - &Apache::lonhtmlcommon::StatusOptions($saveStatus,undef,1,'javascript:reLoadList(this.form);').'<br />'; + $gradeTable .= &Apache::lonhtmlcommon::row_title(&mt('Student Status')) + .&Apache::lonhtmlcommon::StatusOptions( + $saveStatus,undef,1,'javascript:reLoadList(this.form);') + .&Apache::lonhtmlcommon::row_closure(); } - $gradeTable.='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". - '<input type="hidden" name="command" value="processGroup" />'."\n"; + $gradeTable .= &Apache::lonhtmlcommon::row_title(&mt('Check For Plagiarism')) + .'<input type="checkbox" name="checkPlag" checked="checked" />' + .&Apache::lonhtmlcommon::row_closure(1) + .&Apache::lonhtmlcommon::end_pick_box(); + + $gradeTable .= '<p>' + .&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" + .'<input type="hidden" name="command" value="processGroup" />' + .'</p>'; # checkall buttons $gradeTable.=&check_script('gradesub', 'stuinfo'); $gradeTable.='<input type="button" '."\n". - 'onClick="javascript:checkSelect(this.form.stuinfo);" '."\n". - 'value="Next->" /> <br />'."\n"; + 'onclick="javascript:checkSelect(this.form.stuinfo);" '."\n". + 'value="'.&mt('Next').' →" /> <br />'."\n"; $gradeTable.=&check_buttons(); - $gradeTable.='<label><input type="checkbox" name="checkPlag" checked="checked" />Check For Plagiarism</label>'; my ($classlist, undef, $fullname) = &getclasslist($getsec,'1',$getgroup); - $gradeTable.='<table border="0"><tr><td bgcolor="#777777">'. - '<table border="0"><tr bgcolor="#e6ffff">'; + $gradeTable.= &Apache::loncommon::start_data_table(). + &Apache::loncommon::start_data_table_header_row(); my $loop = 0; while ($loop < 2) { - $gradeTable.='<td><b> No.</b> </td><td><b> Select </b></td>'. - '<td>'.&nameUserString('header').' Section/Group</td>'; + $gradeTable.='<th>'.&mt('No.').'</th><th>'.&mt('Select').'</th>'. + '<th>'.&nameUserString('header').' '.&mt('Section/Group').'</th>'; if ($env{'form.showgrading'} eq 'yes' && $submitonly ne 'queued' && $submitonly ne 'all') { - foreach (sort(@$partlist)) { - my $display_part=&get_display_part((split(/_/))[0],$symb); - $gradeTable.='<td><b> Part: '.$display_part. - ' Status </b></td>'; + foreach my $part (sort(@$partlist)) { + my $display_part= + &get_display_part((split(/_/,$part))[0],$symb); + $gradeTable.= + '<th>'.&mt('Part: [_1] Status',$display_part).'</th>'; } } elsif ($submitonly eq 'queued') { - $gradeTable.='<td><b> '.&mt('Queue Status').' </b></td>'; + $gradeTable.='<th>'.&mt('Queue Status').' </th>'; } $loop++; # $gradeTable.='<td></td>' if ($loop%2 ==1); } - $gradeTable.='</tr>'."\n"; + $gradeTable.=&Apache::loncommon::end_data_table_header_row()."\n"; my $ctr = 0; foreach my $student (sort @@ -978,22 +1118,26 @@ LISTJAVASCRIPT my $section = $classlist->{$student}->[&Apache::loncoursedata::CL_SECTION()]; my $group = $classlist->{$student}->[&Apache::loncoursedata::CL_GROUP()]; if ( $perm{'vgr'} eq 'F' ) { - $gradeTable.='<tr bgcolor="#ffffe6">' if ($ctr%2 ==1); + if ($ctr%2 ==1) { + $gradeTable.= &Apache::loncommon::start_data_table_row(); + } $gradeTable.='<td align="right">'.$ctr.' </td>'. - '<td align="center"><label><input type=checkbox name="stuinfo" value="'. + '<td align="center"><label><input type="checkbox" name="stuinfo" value="'. $student.':'.$$fullname{$student}.':::SECTION'.$section. ') " /> </label></td>'."\n".'<td>'. &nameUserString(undef,$$fullname{$student},$uname,$udom). - ' '.$section.'/'.$group.'</td>'."\n"; + ' '.$section.($group ne '' ?'/'.$group:'').'</td>'."\n"; if ($env{'form.showgrading'} eq 'yes' && $submitonly ne 'all') { - foreach (sort keys(%status)) { - next if (/^resource.*?submitted_by$/); - $gradeTable.='<td align="center"> '.$status{$_}.' </td>'."\n"; + foreach (sort(keys(%status))) { + next if ($_ =~ /^resource.*?submitted_by$/); + $gradeTable.='<td align="center"> '.&mt($status{$_}).' </td>'."\n"; } } # $gradeTable.='<td></td>' if ($ctr%2 ==1); - $gradeTable.='</tr>'."\n" if ($ctr%2 ==0); + if ($ctr%2 ==0) { + $gradeTable.=&Apache::loncommon::end_data_table_row()."\n"; + } } } if ($ctr%2 ==1) { @@ -1007,28 +1151,29 @@ LISTJAVASCRIPT } elsif ($submitonly eq 'queued') { $gradeTable.='<td> </td>'; } - $gradeTable.='</tr>'; + $gradeTable.=&Apache::loncommon::end_data_table_row(); } - $gradeTable.='</table></td></tr></table>'."\n". - '<input type="button" '. - 'onClick="javascript:checkSelect(this.form.stuinfo);" '. - 'value="Next->" /></form>'."\n"; + $gradeTable.=&Apache::loncommon::end_data_table()."\n". + '<input type="button" '. + 'onclick="javascript:checkSelect(this.form.stuinfo);" '. + 'value="'.&mt('Next').' →" /></form>'."\n"; if ($ctr == 0) { my $num_students=(scalar(keys(%$fullname))); if ($num_students eq 0) { - $gradeTable='<br /> <span class="LC_warning">There are no students currently enrolled.</span>'; + $gradeTable='<br /> <span class="LC_warning">'.&mt('There are no students currently enrolled.').'</span>'; } else { my $submissions='submissions'; if ($submitonly eq 'incorrect') { $submissions = 'incorrect submissions'; } if ($submitonly eq 'graded' ) { $submissions = 'ungraded submissions'; } if ($submitonly eq 'queued' ) { $submissions = 'queued submissions'; } $gradeTable='<br /> <span class="LC_warning">'. - 'No '.$submissions.' found for this resource for any students. ('.$num_students. - ' students checked for '.$submissions.')</span><br />'; + &mt('No '.$submissions.' found for this resource for any students. ([quant,_1,student] checked for '.$submissions.')', + $num_students). + '</span><br />'; } } elsif ($ctr == 1) { - $gradeTable =~ s/type=checkbox/type=checkbox checked/; + $gradeTable =~ s/type="checkbox"/type="checkbox" checked="checked"/; } $gradeTable.=&show_grading_menu_form($symb); $request->print($gradeTable); @@ -1075,9 +1220,9 @@ sub check_script { } sub check_buttons { - my $buttons.='<input type="button" onclick="checkall()" value="Check All" />'; - $buttons.='<input type="button" onclick="uncheckall()" value="Uncheck All" /> '; - $buttons.='<input type="button" onclick="checksec()" value="Check Section/Group" />'; + my $buttons.='<input type="button" onclick="checkall()" value="'.&mt('Check All').'" />'; + $buttons.='<input type="button" onclick="uncheckall()" value="'.&mt('Uncheck All').'" /> '; + $buttons.='<input type="button" onclick="checksec()" value="'.&mt('Check Section/Group').'" />'; $buttons.='<input type="text" size="5" name="chksec" /> '; return $buttons; } @@ -1108,6 +1253,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) { @@ -1118,7 +1264,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; @@ -1226,10 +1372,8 @@ sub sub_page_js { } } } - } } - } if (val == "Grade Student") { formname.showgrading.value = "yes"; @@ -1363,12 +1507,38 @@ INNERJS my $docopen=&Apache::lonhtmlcommon::javascript_docopen(); $docopen=~s/^document\.//; + my %lt = &Apache::lonlocal::texthash( + keyw => 'Keywords list, separated by a space. Add/delete to list if desired.', + plse => 'Please select a word or group of words from document and then click this link.', + adds => 'Add selection to keyword list? Edit if desired.', + comp => 'Compose Message for: ', + incl => 'Include', + type => 'Type', + subj => 'Subject', + mesa => 'Message', + new => 'New', + save => 'Save', + canc => 'Cancel', + kehi => 'Keyword Highlight Options', + txtc => 'Text Color', + font => 'Font Size', + fnst => 'Font Style', + col1 => 'red', + col2 => 'green', + col3 => 'blue', + siz1 => 'normal', + siz2 => '+1', + siz3 => '+2', + sty1 => 'normal', + sty2 => 'italic', + sty3 => 'bold', + ); $request->print(<<SUBJAVASCRIPT); <script type="text/javascript" language="javascript"> //===================== Show list of keywords ==================== function keywords(formname) { - var nret = prompt("Keywords list, separated by a space. Add/delete to list if desired.",formname.keywords.value); + var nret = prompt("$lt{'keyw'}",formname.keywords.value); if (nret==null) return; formname.keywords.value = nret; @@ -1395,10 +1565,10 @@ 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("$lt{'plse'}"); return; } - var nret = prompt("Add selection to keyword list? Edit if desired.",cleantxt); + var nret = prompt("$lt{'adds'}",cleantxt); if (nret==null) return; document.SCORE.keywords.value = document.SCORE.keywords.value+" "+nret; if (document.SCORE.keywords.value != "") { @@ -1462,17 +1632,15 @@ INNERJS function savedMsgHeader(Nmsg,usrctr,fullname) { var height = 70*Nmsg+250; - var scrollbar = "no"; if (height > 600) { height = 600; - scrollbar = "yes"; } var xpos = (screen.width-600)/2; xpos = (xpos < 0) ? '0' : xpos; var ypos = (screen.height-height)/2-30; ypos = (ypos < 0) ? '0' : ypos; - pWin = window.open('', 'MessageCenter', 'resizable=yes,toolbar=no,location=no,scrollbars='+scrollbar+',screenx='+xpos+',screeny='+ypos+',width=600,height='+height); + pWin = window.open('', 'MessageCenter', 'resizable=yes,toolbar=no,location=no,scrollbars=yes,screenx='+xpos+',screeny='+ypos+',width=700,height='+height); pWin.focus(); pDoc = pWin.document; pDoc.$docopen; @@ -1480,16 +1648,16 @@ INNERJS pDoc.write("<form action=\\"inactive\\" name=\\"msgcenter\\">"); pDoc.write("<input value=\\""+usrctr+"\\" name=\\"usrctr\\" type=\\"hidden\\">"); - pDoc.write("<h3><span class=\\"LC_info\\"> Compose Message for \"+fullname+\"<\\/span><\\/h3><br /><br />"); + pDoc.write("<h3><span class=\\"LC_info\\"> $lt{'comp'}\"+fullname+\"<\\/span><\\/h3><br /><br />"); - pDoc.write("<table border=0 width=100%><tr><td bgcolor=\\"#777777\\">"); - pDoc.write("<table border=0 width=100%><tr bgcolor=\\"#ddffff\\">"); - pDoc.write("<td><b>Type<\\/b><\\/td><td><b>Include<\\/b><\\/td><td><b>Message<\\/td><\\/tr>"); + pDoc.write('<table border="0" width="100%"><tr><td bgcolor="#777777">'); + pDoc.write('<table border="0" width="100%"><tr bgcolor="#DDFFFF">'); + pDoc.write("<td><b>$lt{'type'}<\\/b><\\/td><td><b>$lt{'incl'}<\\/b><\\/td><td><b>$lt{'mesa'}<\\/td><\\/tr>"); } function displaySubject(msg,shwsel) { pDoc = pWin.document; pDoc.write("<tr bgcolor=\\"#ffffdd\\">"); - pDoc.write("<td>Subject<\\/td>"); + pDoc.write("<td>$lt{'subj'}<\\/td>"); pDoc.write("<td align=\\"center\\"><input name=\\"subchk\\" type=\\"checkbox\\"" +shwsel+"><\\/td>"); pDoc.write("<td><input name=\\"msgsub\\" type=\\"text\\" value=\\""+msg+"\\"size=\\"60\\" maxlength=\\"80\\"><\\/td><\\/tr>"); } @@ -1505,7 +1673,7 @@ INNERJS function newMsg(newmsg,shwsel) { pDoc = pWin.document; pDoc.write("<tr bgcolor=\\"#ffffdd\\">"); - pDoc.write("<td align=\\"center\\">New<\\/td>"); + pDoc.write("<td align=\\"center\\">$lt{'new'}<\\/td>"); pDoc.write("<td align=\\"center\\"><input name=\\"newmsgchk\\" type=\\"checkbox\\"" +shwsel+"><\\/td>"); pDoc.write("<td><textarea name=\\"newmsg\\" cols=\\"60\\" rows=\\"3\\" onchange=\\"javascript:this.form.newmsgchk.checked=true\\" >"+newmsg+"<\\/textarea><\\/td><\\/tr>"); } @@ -1514,8 +1682,8 @@ INNERJS pDoc = pWin.document; pDoc.write("<\\/table>"); pDoc.write("<\\/td><\\/tr><\\/table> "); - pDoc.write("<input type=\\"button\\" value=\\"Save\\" onClick=\\"javascript:checkInput()\\"> "); - pDoc.write("<input type=\\"button\\" value=\\"Cancel\\" onClick=\\"self.close()\\"><br /><br />"); + pDoc.write("<input type=\\"button\\" value=\\"$lt{'save'}\\" onclick=\\"javascript:checkInput()\\"> "); + pDoc.write("<input type=\\"button\\" value=\\"$lt{'canc'}\\" onclick=\\"self.close()\\"><br /><br />"); pDoc.write("<\\/form>"); pDoc.write('$end_page_msg_central'); pDoc.close(); @@ -1529,25 +1697,34 @@ INNERJS var redsel = ""; var grnsel = ""; var blusel = ""; - if (kwclr=="red") {var redsel="checked"}; - if (kwclr=="green") {var grnsel="checked"}; - if (kwclr=="blue") {var blusel="checked"}; + var txtcol1 = "$lt{'col1'}"; + var txtcol2 = "$lt{'col2'}"; + var txtcol3 = "$lt{'col3'}"; + var txtsiz1 = "$lt{'siz1'}"; + var txtsiz2 = "$lt{'siz2'}"; + var txtsiz3 = "$lt{'siz3'}"; + var txtsty1 = "$lt{'sty1'}"; + var txtsty2 = "$lt{'sty2'}"; + var txtsty3 = "$lt{'sty3'}"; + if (kwclr=="red") {var redsel="checked='checked'"}; + if (kwclr=="green") {var grnsel="checked='checked'"}; + if (kwclr=="blue") {var blusel="checked='checked'"}; var sznsel = ""; var sz1sel = ""; var sz2sel = ""; - if (kwsize=="0") {var sznsel="checked"}; - if (kwsize=="+1") {var sz1sel="checked"}; - if (kwsize=="+2") {var sz2sel="checked"}; + if (kwsize=="0") {var sznsel="checked='checked'"}; + if (kwsize=="+1") {var sz1sel="checked='checked'"}; + if (kwsize=="+2") {var sz2sel="checked='checked'"}; var synsel = ""; var syisel = ""; var sybsel = ""; - if (kwstyle=="") {var synsel="checked"}; - if (kwstyle=="<i>") {var syisel="checked"}; - if (kwstyle=="<b>") {var sybsel="checked"}; + if (kwstyle=="") {var synsel="checked='checked'"}; + if (kwstyle=="<i>") {var syisel="checked='checked'"}; + if (kwstyle=="<b>") {var sybsel="checked='checked'"}; highlightCentral(); - highlightbody('red','red',redsel,'0','normal',sznsel,'','normal',synsel); - highlightbody('green','green',grnsel,'+1','+1',sz1sel,'<i>','italic',syisel); - highlightbody('blue','blue',blusel,'+2','+2',sz2sel,'<b>','bold',sybsel); + highlightbody('red',txtcol1,redsel,'0',txtsiz1,sznsel,'',txtsty1,synsel); + highlightbody('green',txtcol2,grnsel,'+1',txtsiz2,sz1sel,'<i>',txtsty2,syisel); + highlightbody('blue',txtcol3,blusel,'+2',txtsiz3,sz2sel,'<b>',txtsty3,sybsel); highlightend(); return; } @@ -1565,31 +1742,29 @@ INNERJS hDoc.$docopen; hDoc.write('$start_page_highlight_central'); hDoc.write("<form action=\\"inactive\\" name=\\"hlCenter\\">"); - hDoc.write("<h3><span class=\\"LC_info\\"> Keyword Highlight Options<\\/span><\\/h3><br /><br />"); + hDoc.write("<h1>$lt{'kehi'}<\\/h1>"); - hDoc.write("<table border=0 width=100%><tr><td bgcolor=\\"#777777\\">"); - hDoc.write("<table border=0 width=100%><tr bgcolor=\\"#ddffff\\">"); - hDoc.write("<td><b>Text Color<\\/b><\\/td><td><b>Font Size<\\/b><\\/td><td><b>Font Style<\\/td><\\/tr>"); + hDoc.write('<table border="0" width="100%"><tr style="background-color:#A1D676">'); + hDoc.write("<th>$lt{'txtc'}<\\/th><th>$lt{'font'}<\\/th><th>$lt{'fnst'}<\\/th><\\/tr>"); } function highlightbody(clrval,clrtxt,clrsel,szval,sztxt,szsel,syval,sytxt,sysel) { var hDoc = hwdWin.document; - hDoc.write("<tr bgcolor=\\"#ffffdd\\">"); + hDoc.write("<tr>"); hDoc.write("<td align=\\"left\\">"); - hDoc.write("<input name=\\"kwdclr\\" type=\\"radio\\" value=\\""+clrval+"\\" "+clrsel+"> "+clrtxt+"<\\/td>"); + hDoc.write("<input name=\\"kwdclr\\" type=\\"radio\\" value=\\""+clrval+"\\" "+clrsel+" \\/> "+clrtxt+"<\\/td>"); hDoc.write("<td align=\\"left\\">"); - hDoc.write("<input name=\\"kwdsize\\" type=\\"radio\\" value=\\""+szval+"\\" "+szsel+"> "+sztxt+"<\\/td>"); + hDoc.write("<input name=\\"kwdsize\\" type=\\"radio\\" value=\\""+szval+"\\" "+szsel+" \\/> "+sztxt+"<\\/td>"); hDoc.write("<td align=\\"left\\">"); - hDoc.write("<input name=\\"kwdstyle\\" type=\\"radio\\" value=\\""+syval+"\\" "+sysel+"> "+sytxt+"<\\/td>"); + hDoc.write("<input name=\\"kwdstyle\\" type=\\"radio\\" value=\\""+syval+"\\" "+sysel+" \\/> "+sytxt+"<\\/td>"); hDoc.write("<\\/tr>"); } function highlightend() { var hDoc = hwdWin.document; - hDoc.write("<\\/table>"); - hDoc.write("<\\/td><\\/tr><\\/table> "); - hDoc.write("<input type=\\"button\\" value=\\"Save\\" onClick=\\"javascript:updateChoice(1)\\"> "); - hDoc.write("<input type=\\"button\\" value=\\"Cancel\\" onClick=\\"self.close()\\"><br /><br />"); + hDoc.write("<\\/table><br \\/>"); + hDoc.write("<input type=\\"button\\" value=\\"$lt{'save'}\\" onclick=\\"javascript:updateChoice(1)\\" \\/> "); + hDoc.write("<input type=\\"button\\" value=\\"$lt{'canc'}\\" onclick=\\"self.close()\\" \\/><br /><br />"); hDoc.write("<\\/form>"); hDoc.write('$end_page_highlight_central'); hDoc.close(); @@ -1608,19 +1783,37 @@ sub get_increment { return $increment; } +sub gradeBox_start { + return ( + &Apache::loncommon::start_data_table() + .&Apache::loncommon::start_data_table_header_row() + .'<th>'.&mt('Part').'</th>' + .'<th>'.&mt('Points').'</th>' + .'<th> </th>' + .'<th>'.&mt('Assign Grade').'</th>' + .'<th>'.&mt('Weight').'</th>' + .'<th>'.&mt('Grade Status').'</th>' + .&Apache::loncommon::end_data_table_header_row() + ); +} + +sub gradeBox_end { + return ( + &Apache::loncommon::end_data_table() + ); +} #--- displays the grading box, used in essay type problem and grading by page/sequence sub gradeBox { my ($request,$symb,$uname,$udom,$counter,$partid,$record) = @_; my $checkIcon = '<img alt="'.&mt('Check Mark'). - '" src="'.$request->dir_config('lonIconsURL'). - '/check.gif" height="16" border="0" />'; + '" src="'.&Apache::loncommon::lonhttpdurl($request->dir_config('lonIconsURL').'/check.gif').'" height="16" border="0" />'; my $wgt = &Apache::lonnet::EXT('resource.'.$partid.'.weight',$symb,$udom,$uname); my $wgtmsg = ($wgt > 0) ? &mt('(problem weight)') : '<span class="LC_info">'.&mt('problem weight assigned by computer').'</span>'; $wgt = ($wgt > 0 ? $wgt : '1'); my $score = ($$record{'resource.'.$partid.'.awarded'} eq '' ? '' : &compute_points($$record{'resource.'.$partid.'.awarded'},$wgt)); - my $result='<input type="hidden" name="WGT'.$counter.'_'.$partid.'" value="'.$wgt.'" />'."\n"; + my $data_WGT='<input type="hidden" name="WGT'.$counter.'_'.$partid.'" value="'.$wgt.'" />'."\n"; my $display_part= &get_display_part($partid,$symb); my %last_resets = &get_last_resets($symb,$env{'request.course.id'}, [$partid]); @@ -1628,41 +1821,45 @@ sub gradeBox { if ($last_resets{$partid}) { $aggtries = &get_num_tries($record,$last_resets{$partid},$partid); } - $result.='<table border="0"><tr><td>'. - '<b>Part: </b>'.$display_part.' <b>Points: </b></td><td>'."\n"; + my $result=&Apache::loncommon::start_data_table_row(); my $ctr = 0; my $thisweight = 0; my $increment = &get_increment(); - $result.='<table border="0"><tr>'."\n"; # display radio buttons in a nice table 10 across + + my $radio.='<table border="0"><tr>'."\n"; # display radio buttons in a nice table 10 across while ($thisweight<=$wgt) { - $result.= '<td><span style="white-space: nowrap;"><label><input type="radio" name="RADVAL'.$counter.'_'.$partid.'" '. - 'onclick="javascript:writeBox(this.form,\''.$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"; - $result.=(($ctr+1)%10 == 0 ? '</tr><tr>' : ''); + $radio.=(($ctr+1)%10 == 0 ? '</tr><tr>' : ''); $thisweight += $increment; $ctr++; } - $result.='</tr></table>'; - $result.='</td><td> <b>or</b> </td>'."\n"; - $result.='<td><input type="text" name="GD_BOX'.$counter.'_'.$partid.'"'. + $radio.='</tr></table>'; + + my $line.='<input type="text" name="GD_BOX'.$counter.'_'.$partid.'"'. ($score ne ''? ' value = "'.$score.'"':'').' size="4" '. - 'onChange="javascript:updateRadio(this.form,\''.$counter.'_'.$partid.'\','. + 'onchange="javascript:updateRadio(this.form,\''.$counter.'_'.$partid.'\','. $wgt.')" /></td>'."\n"; - $result.='<td>/'.$wgt.' '.$wgtmsg. + $line.='<td>/'.$wgt.' '.$wgtmsg. ($$record{'resource.'.$partid.'.solved'} eq 'correct_by_student' ? ' '.$checkIcon : ''). - ' </td><td>'."\n"; - $result.='<select name="GD_SEL'.$counter.'_'.$partid.'" '. - 'onChange="javascript:clearRadBox(this.form,\''.$counter.'_'.$partid.'\')" >'."\n"; + ' </td>'."\n"; + $line.='<td><select name="GD_SEL'.$counter.'_'.$partid.'" '. + 'onchange="javascript:clearRadBox(this.form,\''.$counter.'_'.$partid.'\')" >'."\n"; if ($$record{'resource.'.$partid.'.solved'} eq 'excused') { - $result.='<option></option>'. - '<option selected="selected">excused</option>'; + $line.='<option></option>'. + '<option value="excused" selected="selected">'.&mt('excused').'</option>'; } else { - $result.='<option selected="selected"></option>'. - '<option>excused</option>'; + $line.='<option selected="selected"></option>'. + '<option value="excused" >'.&mt('excused').'</option>'; } - $result.='<option>reset status</option></select>'."\n"; - $result.=" \n"; + $line.='<option value="reset status">'.&mt('reset status').'</option></select>'."\n"; + + + $result .= + '<td>'.$data_WGT.$display_part.'</td><td>'.$radio.'</td><td>'.&mt('or').'</td><td>'.$line.'</td>'; + $result.=&Apache::loncommon::end_data_table_row().'<td colspan="6">'; $result.='<input type="hidden" name="stores'.$counter.'_'.$partid.'" value="" />'."\n". '<input type="hidden" name="oldpts'.$counter.'_'.$partid.'" value="'.$score.'" />'."\n". '<input type="hidden" name="solved'.$counter.'_'.$partid.'" value="'. @@ -1671,16 +1868,20 @@ sub gradeBox { $$record{'resource.'.$partid.'.tries'}.'" />'."\n". '<input type="hidden" name="aggtries'.$counter.'_'.$partid.'" value="'. $aggtries.'" />'."\n"; - $result.='</td></tr></table>'."\n"; - $result.=&handback_box($symb,$uname,$udom,$counter,$partid,$record); + my $res_error; + $result.=&handback_box($symb,$uname,$udom,$counter,$partid,$record,\$res_error); + $result.='</td>'.&Apache::loncommon::end_data_table_row(); + if ($res_error) { + return &navmap_errormsg(); + } return $result; } sub handback_box { - my ($symb,$uname,$udom,$counter,$partid,$record) = @_; - my ($partlist,$handgrade,$responseType) = &response_type($symb); + my ($symb,$uname,$udom,$counter,$partid,$record,$res_error) = @_; + my ($partlist,$handgrade,$responseType) = &response_type($symb,$res_error); my (@respids); - my @part_response_id = &flatten_responseType($responseType); + my @part_response_id = &flatten_responseType($responseType); foreach my $part_response_id (@part_response_id) { my ($part,$resp) = @{ $part_response_id }; if ($part eq $partid) { @@ -1692,9 +1893,10 @@ sub handback_box { my $prefix = $counter.'_'.$partid.'_'.$respid.'_'; my $files=&get_submitted_files($udom,$uname,$partid,$respid,$record); next if (!@$files); - my $file_counter = 1; + my $file_counter = 0; foreach my $file (@$files) { if ($file =~ /\/portfolio\//) { + $file_counter++; my ($file_path, $file_disp) = ($file =~ m|(.+/)(.+)$|); my ($name,$version,$ext) = &file_name_version_ext($file_disp); $file_disp = "$name.$ext"; @@ -1702,11 +1904,14 @@ sub handback_box { $result.=&mt('Return commented version of [_1] to student.', '<span class="LC_filename">'.$file_disp.'</span>'); $result.='<input type="file" name="'.$prefix.'returndoc'.$file_counter.'" />'."\n"; - $result.='<input type="hidden" name="'.$prefix.'origdoc'.$file_counter.'" value="'.$file.'" /><br />'; - $result.='(File will be uploaded when you click on Save & Next below.)<br />'; - $file_counter++; + $result.='<input type="hidden" name="'.$prefix.'origdoc'.$file_counter.'" value="'.$file.'" /><br />'."\n"; } } + if ($file_counter) { + $result .= '<input type="hidden" name="'.$prefix.'countreturndoc" value="'.$file_counter.'" />'."\n". + '<span class="LC_info">'. + '('.&mt('File(s) will be uploaded when you click on Save & Next below.',$file_counter).')</span><br /><br />'; + } } return $result; } @@ -1739,27 +1944,37 @@ sub show_problem { $companswer=~s|</form>||g; $companswer=~s|name="submit"|name="would_have_been_submit"|g; } - my $result.='<table border="0" width="100%"><tr><td bgcolor="#777777">'; - $result.='<table border="0" width="100%">'; - if ($viewon) { - $result.='<tr><td bgcolor="#e6ffff"><b> '; - if ($mode eq 'both' or $mode eq 'text') { - $result.='View of the problem - '; - } else { - $result.='Correct answer: '; - } - $result.=$env{'form.fullname'}.'</b></td></tr>'; + my $renderheading = &mt('View of the problem'); + my $answerheading = &mt('Correct answer'); + if (($uname ne $env{'user.name'}) || ($udom ne $env{'user.domain'})) { + my $stu_fullname = $env{'form.fullname'}; + if ($stu_fullname eq '') { + $stu_fullname = &Apache::loncommon::plainname($uname,$udom,'lastname'); + } + my $forwhom = &nameUserString(undef,$stu_fullname,$uname,$udom); + if ($forwhom ne '') { + $renderheading = &mt('View of the problem for[_1]',$forwhom); + $answerheading = &mt('Correct answer for[_1]',$forwhom); + } } + $rendered= + '<div class="LC_Box">' + .'<h3 class="LC_hcell">'.$renderheading.'</h3>' + .$rendered + .'</div>'; + $companswer= + '<div class="LC_Box">' + .'<h3 class="LC_hcell">'.$answerheading.'</h3>' + .$companswer + .'</div>'; + my $result; if ($mode eq 'both') { - $result.='<tr><td bgcolor="#ffffff">'.$rendered.'<br />'; - $result.='<b>Correct answer:</b><br />'.$companswer; + $result=$rendered.$companswer; } elsif ($mode eq 'text') { - $result.='<tr><td bgcolor="#ffffff">'.$rendered; + $result=$rendered; } elsif ($mode eq 'answer') { - $result.='<tr><td bgcolor="#ffffff">'.$companswer; + $result=$companswer; } - $result.='</td></tr></table>'; - $result.='</td></tr></table><br />'; return $result; } @@ -1792,9 +2007,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 @@ -1820,13 +2035,16 @@ sub submission { $udom = ($udom eq '' ? $env{'user.domain'} : $udom); #has form.userdom changed for a student? my $usec = &Apache::lonnet::getsection($udom,$uname,$env{'request.course.id'}); $env{'form.fullname'} = &Apache::loncommon::plainname($uname,$udom,'lastname') if $env{'form.fullname'} eq ''; - my $symb = &get_symb($request); + my ($symb) = &get_symb($request); if ($symb eq '') { $request->print("Unable to handle ambiguous references:."); return ''; } if (!&canview($usec)) { - $request->print('<span class="LC_warning">Unable to view requested student.('. - $uname.':'.$udom.' in section '.$usec.' in course id '. - $env{'request.course.id'}.')</span>'); + $request->print( + '<span class="LC_warning">'. + &mt('Unable to view requested student.'). + ' '.&mt('([_1] in section [_2] in course id [_3])', + $uname.':'.$udom,$usec,$env{'request.course.id'}). + '</span>'); $request->print(&show_grading_menu_form($symb)); return; } @@ -1839,7 +2057,6 @@ sub submission { '" src="'.$request->dir_config('lonIconsURL'). '/check.gif" height="16" border="0" />'; - my %old_essays; # header info if ($counter == 0) { &sub_page_js($request); @@ -1849,14 +2066,8 @@ sub submission { if ($env{'form.handgrade'} eq 'yes' && &files_exist($request, $symb)) { &download_all_link($request, $symb); } - $request->print('<h3> <span class="LC_info">Submission Record</span></h3>'."\n". - '<h4> <b>Resource: </b>'.$env{'form.probTitle'}.'</h4>'."\n"); - - if ($env{'form.handgrade'} eq 'no') { - my $checkMark='<br /><br /> <b>Note:</b> Part(s) graded correct by the computer is marked with a '. - $checkIcon.' symbol.'."\n"; - $request->print($checkMark); - } + $request->print('<h3> <span class="LC_info">'.&mt('Submission Record').'</span></h3>'."\n". + '<h4> '.&mt('[_1]Resource: [_2]','<b>','</b>'.$env{'form.probTitle'}).'</h4>'."\n"); # option to display problem, only once else it cause problems # with the form later since the problem has a form. @@ -1873,7 +2084,7 @@ sub submission { $request->print(&show_problem($request,$symb,$uname,$udom,0,1,$mode)); } - # kwclr is the only variable that is guaranteed to be non blank + # kwclr is the only variable that is guaranteed not to be blank # if this subroutine has been called once. my %keyhash = (); if ($env{'form.kwclr'} eq '' && $env{'form.handgrade'} eq 'yes') { @@ -1937,16 +2148,29 @@ sub submission { $request->print($prnmsg); if ($env{'form.handgrade'} eq 'yes' && $env{'form.showgrading'} eq 'yes') { + + my %lt = &Apache::lonlocal::texthash( + keyh => 'Keyword Highlighting for Essays', + keyw => 'Keyword Options', + list => 'List', + past => 'Paste Selection to List', + high => 'Highlight Attribute', + ); # # Print out the keyword options line # - $request->print(<<KEYWORDS); - <b>Keyword Options:</b> -<a href="javascript:keywords(document.SCORE);" target="_self">List</a> -<a href="#" onMouseDown="javascript:getSel(); return false" - CLASS="page">Paste Selection to List</a> -<a href="javascript:kwhighlight();" target="_self">Highlight Attribute</a><br /><br /> -KEYWORDS + $request->print( + '<div class="LC_columnSection">' + .'<fieldset><legend>'.$lt{'keyh'}.'</legend>' + .&Apache::lonhtmlcommon::funclist_from_array( + ['<a href="javascript:keywords(document.SCORE);" target="_self">'.$lt{'list'}.'</a>', + '<a href="#" onmousedown="javascript:getSel(); return false" + class="page">'.$lt{'past'}.'</a>', + '<a href="javascript:kwhighlight();" target="_self">'.$lt{'high'}.'</a>'], + {legend => $lt{'keyw'}}) + .'</fieldset></div>' + ); + # # Load the other essays for similarity check # @@ -1954,16 +2178,36 @@ KEYWORDS my ($adom,$aname,$apath)=($essayurl=~/^($LONCAPA::domain_re)\/($LONCAPA::username_re)\/(.*)$/); $apath=&escape($apath); $apath=~s/\W/\_/gs; - %old_essays=&Apache::lonnet::dump('nohist_essay_'.$apath,$adom,$aname); + &init_old_essays($symb,$apath,$adom,$aname); } } # This is where output for one specific student would start - my $bgcolor='#DDEEDD'; - if ($counter%2) { $bgcolor='#DDDDEE'; } - $request->print("\n\n". - '<p><table border="2"><tr><th bgcolor="'.$bgcolor.'">'.$env{'form.fullname'}.'</th></tr><tr><td bgcolor="'.$bgcolor.'">'); + my $add_class = ($counter%2) ? ' LC_grade_show_user_odd_row' : ''; + $request->print( + "\n\n" + .'<div class="LC_grade_show_user'.$add_class.'">' + .'<h2>'.&nameUserString(undef,$env{'form.fullname'},$uname,$udom).'</h2>' + ."\n" + ); + + # Show additional functions if allowed + if ($perm{'vgr'}) { + $request->print( + &Apache::loncommon::track_student_link( + 'View recent activity', + $uname,$udom,'check') + .' ' + ); + } + if ($perm{'opa'}) { + $request->print( + &Apache::loncommon::pprmlink( + &mt('Set/Change parameters'), + $uname,$udom,$symb,'check')); + } + # Show Problem if ($env{'form.vProb'} eq 'all' or $env{'form.vAns'} eq 'all') { my $mode; if ($env{'form.vProb'} eq 'all' && $env{'form.vAns'} eq 'all') { @@ -1974,20 +2218,29 @@ KEYWORDS $mode='answer'; } &Apache::lonxml::clear_problem_counter(); - $request->print(&show_problem($request,$symb,$uname,$udom,1,1,$mode)); + $request->print(&show_problem($request,$symb,$uname,$udom,1,1,$mode,{'request.prefix' => 'ctr'.$counter})); } my %record = &Apache::lonnet::restore($symb,$env{'request.course.id'},$udom,$uname); - my ($partlist,$handgrade,$responseType) = &response_type($symb); + my $res_error; + my ($partlist,$handgrade,$responseType) = &response_type($symb,\$res_error); + if ($res_error) { + $request->print(&navmap_errormsg()); + return; + } # Display student info $request->print(($counter == 0 ? '' : '<br />')); - my $result='<table border="0" width="100%"><tr><td bgcolor="#777777">'."\n". - '<table border="0" width="100%"><tr bgcolor="#edffff"><td>'."\n"; - $result.='<b>Fullname: </b>'.&nameUserString(undef,$env{'form.fullname'},$uname,$udom).'<br />'."\n"; + my $result='<div class="LC_Box">' + .'<h3 class="LC_hcell">'.&mt('Submissions').'</h3>'; $result.='<input type="hidden" name="name'.$counter. - '" value="'.$env{'form.fullname'}.'" />'."\n"; + '" value="'.$env{'form.fullname'}.'" />'."\n"; + if ($env{'form.handgrade'} eq 'no') { + $result.='<p class="LC_info">' + .&mt('Part(s) graded correct by the computer is marked with a [_1] symbol.',$checkIcon) + ."</p>\n"; + } # If any part of the problem is an essay-response (handgraded), then check for collaborators my $fullname; @@ -2001,19 +2254,23 @@ KEYWORDS $request->print($result."\n"); # print student answer/submission - # Options are (1) Handgaded submission only + # Options are (1) Handgraded submission only # (2) Last submission, includes submission that is not handgraded # (for multi-response type part) # (3) Last submission plus the parts info # (4) The whole record for this student - if ($env{'form.lastSub'} =~ /^(lastonly|hdgrade)$/) { + my ($string,$timestamp)= &get_last_submission(\%record); - my $lastsubonly=''. - ($$timestamp eq '' ? '' : '<b>Date Submitted:</b> '. - $$timestamp)."</td></tr>\n"; - if ($$timestamp eq '') { - $lastsubonly.='<tr><td bgcolor="#ffffe6">'.$$string[0]; - } else { + + my $lastsubonly; + + if ($$timestamp eq '') { + $lastsubonly.='<div class="LC_grade_submissions_body">'.$$string[0].'</div>'; + } else { + $lastsubonly = + '<div class="LC_grade_submissions_body">' + .'<b>'.&mt('Date Submitted:').'</b> '.$$timestamp."\n"; + my %seenparts; my @part_response_id = &flatten_responseType($responseType); foreach my $part (@part_response_id) { @@ -2025,99 +2282,139 @@ KEYWORDS if ($env{"form.$uname:$udom:$partid:submitted_by"}) { if (exists($seenparts{$partid})) { next; } $seenparts{$partid}=1; - my $submitby='<b>Part:</b> '.$display_part. - ' <b>Collaborative submission by:</b> '. - '<a href="javascript:viewSubmitter(\''. - $env{"form.$uname:$udom:$partid:submitted_by"}. - '\');" target="_self">'. - $$fullname{$env{"form.$uname:$udom:$partid:submitted_by"}}.'</a><br />'; - $request->print($submitby); + $request->print( + '<b>'.&mt('Part: [_1]',$display_part).'</b>'. + ' <b>'.&mt('Collaborative submission by: [_1]', + '<a href="javascript:viewSubmitter(\''. + $env{"form.$uname:$udom:$partid:submitted_by"}. + '\');" target="_self">'. + $$fullname{$env{"form.$uname:$udom:$partid:submitted_by"}}.'</a>'). + '<br />'); next; } my $responsetype = $responseType->{$partid}->{$respid}; if (!exists($record{"resource.$partid.$respid.submission"})) { - $lastsubonly.='<tr><td bgcolor="#ffffe6"><b>Part:</b> '. - $display_part.' <span class="LC_internal_info">( ID '.$respid. - ' )</span> '. - '<span class="LC_warning">Nothing submitted - no attempts</span><br /><br />'; + $lastsubonly.="\n".'<div class="LC_grade_submission_part">'. + '<b>'.&mt('Part: [_1]',$display_part).'</b>'. + ' <span class="LC_internal_info">'. + '('.&mt('Response ID: [_1]',$respid).')'. + '</span> '. + '<span class="LC_warning">'.&mt('Nothing submitted - no attempts.').'</span><br /><br /></div>'; next; } - foreach (@$string) { - my ($partid,$respid) = /^resource\.([^\.]*)\.([^\.]*)\.submission/; + foreach my $submission (@$string) { + my ($partid,$respid) = ($submission =~ /^resource\.([^\.]*)\.([^\.]*)\.submission/); if (join('_',@{$part}) ne ($partid.'_'.$respid)) { next; } - my ($ressub,$subval) = split(/:/,$_,2); + my ($ressub,$hide,$draft,$subval) = split(/:/,$submission,4); # Similarity check my $similar=''; - if($env{'form.checkPlag'}){ + my ($type,$trial,$rndseed); + if ($hide eq 'rand') { + $type = 'randomizetry'; + $trial = $record{"resource.$partid.tries"}; + $rndseed = $record{"resource.$partid.rndseed"}; + } + if ($env{'form.checkPlag'}) { my ($oname,$odom,$ocrsid,$oessay,$osim)= - &most_similar($uname,$udom,$subval,\%old_essays); + &most_similar($uname,$udom,$symb,$subval); if ($osim) { $osim=int($osim*100.0); my %old_course_desc = &Apache::lonnet::coursedescription($ocrsid, {'one_time' => 1}); - $similar="<hr /><h3><span class=\"LC_warning\">". - &mt('Essay is [_1]% similar to an essay by [_2] ([_3]:[_4]) in course [_5] (course id [_6]:[_7])', - $osim, - &Apache::loncommon::plainname($oname,$odom), - $oname,$odom, - $old_course_desc{'description'}, - $old_course_desc{'num'}, - $old_course_desc{'domain'}). - '</span></h3><blockquote><i>'. - &keywords_highlight($oessay). - '</i></blockquote><hr />'; + if ($hide eq 'anon') { + $similar='<hr /><span class="LC_warning">'.&mt("Essay was found to be similar to another essay submitted for this assignment.").'<br />'. + &mt('As the current submission is for an anonymous survey, no other details are available.').'</span><hr />'; + } else { + $similar="<hr /><h3><span class=\"LC_warning\">". + &mt('Essay is [_1]% similar to an essay by [_2] in course [_3] (course id [_4]:[_5])', + $osim, + &Apache::loncommon::plainname($oname,$odom).' ('.$oname.':'.$odom.')', + $old_course_desc{'description'}, + $old_course_desc{'num'}, + $old_course_desc{'domain'}). + '</span></h3><blockquote><i>'. + &keywords_highlight($oessay). + '</i></blockquote><hr />'; + } } } - my $order=&get_order($partid,$respid,$symb,$uname,$udom); - if ($env{'form.lastSub'} eq 'lastonly' || - ($env{'form.lastSub'} eq 'hdgrade' && - $$handgrade{$$part[0].'_'.$$part[1]} eq 'yes')) { + my $order=&get_order($partid,$respid,$symb,$uname,$udom, + undef,$type,$trial,$rndseed); + if ($env{'form.lastSub'} eq 'lastonly' || $env{'form.lastSub'} eq 'datesub' || $env{'form.lastSub'} =~ /^(last|all)$/ || ($env{'form.lastSub'} eq 'hdgrade' && + $$handgrade{$$part[0].'_'.$$part[1]} eq 'yes')) { my $display_part=&get_display_part($partid,$symb); - $lastsubonly.='<tr><td bgcolor="#ffffe6"><b>Part:</b> '. - $display_part.' <span class="LC_internal_info">( ID '.$respid. - ' )</span> '; + $lastsubonly.='<div class="LC_grade_submission_part">'. + '<b>'.&mt('Part: [_1]',$display_part).'</b>'. + ' <span class="LC_internal_info">'. + '('.&mt('Response ID: [_1]',$respid).')'. + '</span> '; my $files=&get_submitted_files($udom,$uname,$partid,$respid,\%record); if (@$files) { - $lastsubonly.='<br /><span class="LC_warning">Like all files provided by users, this file may contain virusses</span><br />'; - my $file_counter = 0; - foreach my $file (@$files) { - $file_counter ++; - &Apache::lonnet::allowuploaded('/adm/grades',$file); - $lastsubonly.='<br /><a href="'.$file.'?rawmode=1" target="lonGRDs"><img src="'.&Apache::loncommon::icon($file).'" border=0"> '.$file.'</a>'; - } + if ($hide eq 'anon') { + $lastsubonly.='<br />'.&mt('[quant,_1,file] uploaded to this anonymous survey',scalar(@{$files})); + } else { + $lastsubonly.='<br /><br />'.'<b>'.&mt('Submitted Files:').'</b>' + .'<br /><span class="LC_warning">'; + if(@$files == 1) { + $lastsubonly .= &mt('Like all files provided by users, this file may contain viruses!'); + } else { + $lastsubonly .= &mt('Like all files provided by users, these files may contain viruses!'); + } + $lastsubonly .= '</span>'; + + foreach my $file (@$files) { + &Apache::lonnet::allowuploaded('/adm/grades',$file); + $lastsubonly.='<br /><a href="'.$file.'?rawmode=1" target="lonGRDs"><img src="'.&Apache::loncommon::icon($file).'" border="0" alt="" /> '.$file.'</a>'; + } + } $lastsubonly.='<br />'; } - $lastsubonly.='<b>Submitted Answer: </b>'. - &cleanRecord($subval,$responsetype,$symb,$partid, - $respid,\%record,$order); + if ($hide eq 'anon') { + $lastsubonly.='<br /><b>'.&mt('Anonymous Survey').'</b>'; + } else { + $lastsubonly.='<br /><b>'.&mt('Submitted Answer:').' </b>'; + if ($draft) { + $lastsubonly.= ' <span class="LC_warning">'.&mt('Draft Copy').'</span>'; + } + $subval = + &cleanRecord($subval,$responsetype,$symb,$partid, + $respid,\%record,$order,undef,$uname,$udom,$type,$trial,$rndseed); + if ($responsetype eq 'essay') { + $subval =~ s{\n}{<br />}g; + } + $lastsubonly.=$subval."\n"; + } if ($similar) {$lastsubonly.="<br /><br />$similar\n";} + $lastsubonly.='</div>'; } } } + $lastsubonly.='</div>'."\n"; # End: LC_grade_submissions_body } - $lastsubonly.='</td></tr><tr bgcolor="#ffffff"><td>'."\n"; $request->print($lastsubonly); - } elsif ($env{'form.lastSub'} eq 'datesub') { + if ($env{'form.lastSub'} eq 'datesub') { my (undef,$responseType,undef,$parts) = &showResourceInfo($symb); $request->print(&displaySubByDates($symb,\%record,$parts,$responseType,$checkIcon,$uname,$udom)); - } elsif ($env{'form.lastSub'} =~ /^(last|all)$/) { + } + if ($env{'form.lastSub'} =~ /^(last|all)$/) { + my $identifier = (&canmodify($usec)? $counter : ''); $request->print(&Apache::loncommon::get_previous_attempt($symb,$uname,$udom, $env{'request.course.id'}, $last,'.submission', - 'Apache::grades::keywords_highlight')); + 'Apache::grades::keywords_highlight', + $usec,$identifier)); } $request->print('<input type="hidden" name="unamedom'.$counter.'" value="'.$uname.':' .$udom.'" />'."\n"); - # return if view submission with no grading option if ($env{'form.showgrading'} eq '' || (!&canmodify($usec))) { my $toGrade.='<input type="button" value="Grade Student" '. - 'onClick="javascript:checksubmit(this.form,\'Grade Student\',\'' + 'onclick="javascript:checksubmit(this.form,\'Grade Student\',\'' .$counter.'\');" target="_self" /> '."\n" if (&canmodify($usec)); - $toGrade.='</td></tr></table></td></tr></table>'."\n"; + $toGrade.='</div>'."\n"; if (($env{'form.command'} eq 'submission') || ($env{'form.command'} eq 'processGroup' && $counter == $total)) { $toGrade.='</form>'.&show_grading_menu_form($symb); @@ -2125,11 +2422,15 @@ KEYWORDS $request->print($toGrade); return; } else { - $request->print('</td></tr></table></td></tr></table>'."\n"); + $request->print('</div>'."\n"); } # essay grading message center if ($env{'form.handgrade'} eq 'yes') { + my $result='<div class="LC_grade_message_center">'; + + $result.='<div class="LC_grade_message_center_header">'. + &mt('Send Message').'</div><div class="LC_grade_message_center_body">'; my ($lastname,$givenn) = split(/,/,$env{'form.fullname'}); my $msgfor = $givenn.' '.$lastname; if (scalar(@$col_fullnames) > 0) { @@ -2137,33 +2438,29 @@ KEYWORDS $msgfor .= ', '.(join ', ',@$col_fullnames).' and '.$lastone.'.'; } $msgfor =~ s/\'/\\'/g; #' stupid emacs - no! javascript - $result='<input type="hidden" name="includemsg'.$counter.'" value="" />'."\n". + $result.='<input type="hidden" name="includemsg'.$counter.'" value="" />'."\n". '<input type="hidden" name="newmsg'.$counter.'" value="" />'."\n"; $result.=' <a href="javascript:msgCenter(document.SCORE,'.$counter. ',\''.$msgfor.'\');" target="_self">'. - &mt('Compose message to student').(scalar(@$col_fullnames) >= 1 ? 's' : '').'</a><label> ('. + &mt('Compose message to student'.(scalar(@$col_fullnames) >= 1 ? 's' : '')).'</a><label> ('. &mt('incl. grades').' <input type="checkbox" name="withgrades'.$counter.'" /></label>)'. - '<img src="'.$request->dir_config('lonIconsURL'). + ' <img src="'.$request->dir_config('lonIconsURL'). '/mailbkgrd.gif" width="14" height="10" name="mailicon'.$counter.'" />'."\n". '<br /> ('. - &mt('Message will be sent when you click on Save & Next below.').")\n"; + &mt('Message will be sent when you click on Save & Next below.').")\n"; + $result.='</div></div>'; $request->print($result); } - if ($perm{'vgr'}) { - $request->print('<br />'. - &Apache::loncommon::track_student_link(&mt('View recent activity'), - $uname,$udom,'check')); - } - if ($perm{'opa'}) { - $request->print('<br />'. - &Apache::loncommon::pprmlink(&mt('Set/Change parameters'), - $uname,$udom,$symb,'check')); - } my %seen = (); my @partlist; my @gradePartRespid; my @part_response_id = &flatten_responseType($responseType); + $request->print( + '<div class="LC_Box">' + .'<h3 class="LC_hcell">'.&mt('Assign Grades').'</h3>' + ); + $request->print(&gradeBox_start()); foreach my $part_response_id (@part_response_id) { my ($partid,$respid) = @{ $part_response_id }; my $part_resp = join('_',@{ $part_response_id }); @@ -2171,10 +2468,16 @@ 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(&gradeBox_end()); # </div> + $request->print('</div>'); + + $request->print('<div class="LC_grade_info_links">'); + $request->print('</div>'); + $result='<input type="hidden" name="partlist'.$counter. '" value="'.(join ":",@partlist).'" />'."\n"; $result.='<input type="hidden" name="gradePartRespid'. @@ -2185,18 +2488,18 @@ KEYWORDS $partlist[$ctr].'" />'."\n"; $ctr++; } - $request->print($result.'</td></tr></table></td></tr></table>'."\n"); + $request->print($result.''."\n"); # Done with printing info for one student - $request->print('</td></tr></table></p>'); + $request->print('</div>');#LC_grade_show_user # print end of form if ($counter == $total) { - my $endform='<table border="0"><tr><td>'."\n"; - $endform.='<input type="button" value="Save & Next" '. - 'onClick="javascript:checksubmit(this.form,\'Save & Next\','. + my $endform='<br /><hr /><table border="0"><tr><td>'."\n"; + $endform.='<input type="button" value="'.&mt('Save & Next').'" '. + 'onclick="javascript:checksubmit(this.form,\'Save & Next\','. $total.','.scalar(@partlist).');" target="_self" /> '."\n"; my $ntstu ='<select name="NTSTU">'. '<option>1</option><option>2</option>'. @@ -2204,15 +2507,17 @@ KEYWORDS '<option>7</option><option>10</option></select>'."\n"; my $nsel = ($env{'form.NTSTU'} ne '' ? $env{'form.NTSTU'} : '1'); $ntstu =~ s/<option>$nsel</<option selected="selected">$nsel</; - $endform.=$ntstu.'student(s) '; - $endform.='<input type="button" value="Previous" '. - 'onClick="javascript:checksubmit(this.form,\'Previous\');" target="_self" /> '."\n". - '<input type="button" value="Next" '. - 'onClick="javascript:checksubmit(this.form,\'Next\');" target="_self" /> '; - $endform.='(Next and Previous (student) do not save the scores.)'."\n" ; + $endform.=&mt('[_1]student(s)',$ntstu); + $endform.=' <input type="button" value="'.&mt('Previous').'" '. + 'onclick="javascript:checksubmit(this.form,\'Previous\');" target="_self" /> '."\n". + '<input type="button" value="'.&mt('Next').'" '. + 'onclick="javascript:checksubmit(this.form,\'Next\');" target="_self" /> '; + $endform.='<span class="LC_warning">'. + &mt('(Next and Previous (student) do not save the scores.)'). + '</span>'."\n" ; $endform.="<input type='hidden' value='".&get_increment(). "' name='increment' />"; - $endform.='</td><tr></table></form>'; + $endform.='</td></tr></table></form>'; $endform.=&show_grading_menu_form($symb); $request->print($endform); } @@ -2232,10 +2537,10 @@ sub check_collaborators { next if ($record->{'resource.'.$part.'.collaborators'} eq ''); my (@good_collaborators, @bad_collaborators); foreach my $possible_collaborator - (split(/,?\s+/,$record->{'resource.'.$part.'.collaborators'})) { + (split(/[,;\s]+/,$record->{'resource.'.$part.'.collaborators'})) { $possible_collaborator =~ s/[\$\^\(\)]//g; next if ($possible_collaborator eq ''); - my ($co_name,$co_dom) = split(/\@|:/,$possible_collaborator); + my ($co_name,$co_dom) = split(/:/,$possible_collaborator); $co_dom = $udom if (! defined($co_dom) || $co_dom =~ /^domain$/i); next if ($co_name eq $uname && $co_dom eq $udom); # Doing this grep allows 'fuzzy' specification @@ -2248,13 +2553,13 @@ sub check_collaborators { } } if (scalar(@good_collaborators) != 0) { - $result.='<br />'.&mt('Collaborators: '); + $result.='<br />'.&mt('Collaborators:').'<ol>'; foreach my $name (@good_collaborators) { my ($lastname,$givenn) = split(/,/,$$fullname{$name}); push(@col_fullnames, $givenn.' '.$lastname); - $result.=$fullname->{$name}.' '; + $result.='<li>'.$fullname->{$name}.'</li>'; } - $result.='<br />'."\n"; + $result.='</ol><br />'."\n"; my ($part)=split(/\./,$part); $result.='<input type="hidden" name="collaborator'.$counter. '" value="'.$part.':'.(join ':',@good_collaborators).'" />'. @@ -2278,7 +2583,7 @@ sub check_collaborators { #--- Retrieve the last submission for all the parts sub get_last_submission { my ($returnhash)=@_; - my (@string,$timestamp); + my (@string,$timestamp,%lasthidden); if ($$returnhash{'version'}) { my %lasthash=(); my ($version); @@ -2287,21 +2592,64 @@ sub get_last_submission { $$returnhash{$version.':keys'}))) { $lasthash{$key}=$$returnhash{$version.':'.$key}; $timestamp = - scalar(localtime($$returnhash{$version.':timestamp'})); + &Apache::lonlocal::locallocaltime($$returnhash{$version.':timestamp'}); } } + my (%typeparts,%randombytry); + my $showsurv = + &Apache::lonnet::allowed('vas',$env{'request.course.id'}); + foreach my $key (sort(keys(%lasthash))) { + if ($key =~ /\.type$/) { + if (($lasthash{$key} eq 'anonsurvey') || + ($lasthash{$key} eq 'anonsurveycred') || + ($lasthash{$key} eq 'randomizetry')) { + my ($ign,@parts) = split(/\./,$key); + pop(@parts); + my $id = join('.',@parts); + if ($lasthash{$key} eq 'randomizetry') { + $randombytry{$ign.'.'.$id} = $lasthash{$key}; + } else { + unless ($showsurv) { + $typeparts{$ign.'.'.$id} = $lasthash{$key}; + } + } + delete($lasthash{$key}); + } + } + } + my @hidden = keys(%typeparts); + my @randomize = keys(%randombytry); foreach my $key (keys(%lasthash)) { next if ($key !~ /\.submission$/); - + my $hide; + if (@hidden) { + foreach my $id (@hidden) { + if ($key =~ /^\Q$id\E/) { + $hide = 'anon'; + last; + } + } + } + unless ($hide) { + if (@randomize) { + foreach my $id (@randomize) { + if ($key =~ /^\Q$id\E/) { + $hide = 'rand'; + last; + } + } + } + } my ($partid,$foo) = split(/submission$/,$key); - my $draft = $lasthash{$partid.'awarddetail'} eq 'DRAFT' ? - '<span class="LC_warning">Draft Copy</span> ' : ''; - push(@string, join(':', $key, $draft.$lasthash{$key})); + my $draft = $lasthash{$partid.'awarddetail'} eq 'DRAFT' ? 1: 0; + push(@string, join(':', $key, $hide, $draft, ( + ref($lasthash{$key}) eq 'ARRAY' ? + join(',', @{$lasthash{$key}}) : $lasthash{$key}) )); } } 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); } @@ -2319,10 +2667,191 @@ sub keywords_highlight { return $string; } +# For Tasks provide a mechanism to display previous version for one specific student + +sub show_previous_task_version { + my ($request,$symb) = @_; + if ($symb eq '') { + $request->print( + '<span class="LC_error">'. + &mt('Unable to handle ambiguous references.'). + '</span>'); + return ''; + } + my ($uname,$udom) = ($env{'form.student'},$env{'form.userdom'}); + my $usec = &Apache::lonnet::getsection($udom,$uname,$env{'request.course.id'}); + if (!&canview($usec)) { + $request->print('<span class="LC_warning">'. + &mt('Unable to view previous version for requested student.'). + ' '.&mt('([_1] in section [_2] in course id [_3])', + $uname.':'.$udom,$usec,$env{'request.course.id'}). + '</span>'); + return; + } + my $mode = 'both'; + my $isTask = ($symb =~/\.task$/); + if ($isTask) { + if ($env{'form.previousversion'} =~ /^\d+$/) { + if ($env{'form.fullname'} eq '') { + $env{'form.fullname'} = + &Apache::loncommon::plainname($uname,$udom,'lastname'); + } + my $probtitle=&Apache::lonnet::gettitle($symb); + $request->print("\n\n". + '<div class="LC_grade_show_user">'. + '<h2>'.&nameUserString(undef,$env{'form.fullname'},$uname,$udom). + '</h2>'."\n"); + &Apache::lonxml::clear_problem_counter(); + $request->print(&show_problem($request,$symb,$uname,$udom,1,1,$mode, + {'previousversion' => $env{'form.previousversion'} })); + $request->print("\n</div>"); + } + } + return; +} + +sub choose_task_version_form { + my ($symb,$uname,$udom,$nomenu) = @_; + my $isTask = ($symb =~/\.task$/); + my ($current,$version,$result,$js,$displayed,$rowtitle); + if ($isTask) { + my %record = &Apache::lonnet::restore($symb,$env{'request.course.id'}, + $udom,$uname); + if (($record{'resource.0.version'} eq '') || + ($record{'resource.0.version'} < 2)) { + return ($record{'resource.0.version'}, + $record{'resource.0.version'},$result,$js); + } else { + $current = $record{'resource.0.version'}; + } + if ($env{'form.previousversion'}) { + $displayed = $env{'form.previousversion'}; + $rowtitle = &mt('Choose another version:') + } else { + $displayed = $current; + $rowtitle = &mt('Show earlier version:'); + } + $result = '<div class="LC_left_float">'; + my $list; + my $numversions = 0; + for (my $i=1; $i<=$record{'resource.0.version'}; $i++) { + if ($i == $current) { + if (!$env{'form.previousversion'} || $nomenu) { + next; + } else { + $list .= '<option value="'.$i.'">'.&mt('Current').'</option>'."\n"; + $numversions ++; + } + } elsif (defined($record{'resource.'.$i.'.0.status'})) { + unless ($i == $env{'form.previousversion'}) { + $numversions ++; + } + $list .= '<option value="'.$i.'">'.$i.'</option>'."\n"; + } + } + if ($numversions) { + $symb = &HTML::Entities::encode($symb,'<>"&'); + $result .= + '<form name="getprev" method="post" action=""'. + ' onsubmit="return previousVersion('."'$uname','$udom','$symb','$displayed'".');">'. + &Apache::loncommon::start_data_table(). + &Apache::loncommon::start_data_table_row(). + '<th align="left">'.$rowtitle.'</th>'. + '<td><select name="version">'. + '<option>'.&mt('Select').'</option>'. + $list. + '</select></td>'. + &Apache::loncommon::end_data_table_row(); + unless ($nomenu) { + $result .= &Apache::loncommon::start_data_table_row(). + '<th align="left">'.&mt('Open in new window').'</th>'. + '<td><span class="LC_nobreak">'. + '<label><input type="radio" name="prevwin" value="1" />'. + &mt('Yes').'</label>'. + '<label><input type="radio" name="prevwin" value="0" checked="checked" />'.&mt('No').'</label>'. + '</span></td>'. + &Apache::loncommon::end_data_table_row(); + } + $result .= + &Apache::loncommon::start_data_table_row(). + '<th align="left"> </th>'. + '<td>'. + '<input type="submit" name="prevsub" value="'.&mt('Display').'" />'. + '</td>'. + &Apache::loncommon::end_data_table_row(). + &Apache::loncommon::end_data_table(). + '</form>'; + $js = &previous_display_javascript($nomenu,$current); + } elsif ($displayed && $nomenu) { + $result .= '<a href="javascript:window.close()">'.&mt('Close window').'</a>'; + } else { + $result .= &mt('No previous versions to show for this student'); + } + $result .= '</div>'; + } + return ($current,$displayed,$result,$js); +} + +sub previous_display_javascript { + my ($nomenu,$current) = @_; + my $js = <<"JSONE"; +<script type="text/javascript"> +// <![CDATA[ +function previousVersion(uname,udom,symb) { + var current = '$current'; + var version = document.getprev.version.options[document.getprev.version.selectedIndex].value; + var prevstr = new RegExp("^\\\\d+\$"); + if (!prevstr.test(version)) { + return false; + } + var url = ''; + if (version == current) { + url = '/adm/grades?student='+uname+'&userdom='+udom+'&symb='+symb+'&command=submission'; + } else { + url = '/adm/grades?student='+uname+'&userdom='+udom+'&symb='+symb+'&command=versionsub&previousversion='+version; + } +JSONE + if ($nomenu) { + $js .= <<"JSTWO"; + document.location.href = url; +JSTWO + } else { + $js .= <<"JSTHREE"; + var newwin = 0; + for (var i=0; i<document.getprev.prevwin.length; i++) { + if (document.getprev.prevwin[i].checked == true) { + newwin = document.getprev.prevwin[i].value; + } + } + if (newwin == 1) { + var options = 'height=600,width=800,resizable=yes,scrollbars=yes,location=no,menubar=no,toolbar=no'; + url = url+'&inhibitmenu=yes'; + if (typeof(previousWin) == 'undefined' || previousWin.closed) { + previousWin = window.open(url,'',options,1); + } else { + previousWin.location.href = url; + } + previousWin.focus(); + return false; + } else { + document.location.href = url; + return false; + } +JSTHREE + } + $js .= <<"ENDJS"; + return false; +} +// ]]> +</script> +ENDJS + +} + #--- Called from submission routine sub processHandGrade { my ($request) = shift; - my $symb = &get_symb($request); + my ($symb) = &get_symb($request); my (undef,undef,$url) = &Apache::lonnet::decode_symb($symb); my $button = $env{'form.gradeOpt'}; my $ngrade = $env{'form.NCT'}; @@ -2334,16 +2863,26 @@ sub processHandGrade { my $ctr = 0; while ($ctr < $ngrade) { my ($uname,$udom) = split(/:/,$env{'form.unamedom'.$ctr}); - my ($errorflag,$pts,$wgt) = &saveHandGrade($request,$symb,$uname,$udom,$ctr); + my ($errorflag,$pts,$wgt,$numhidden) = + &saveHandGrade($request,$symb,$uname,$udom,$ctr); if ($errorflag eq 'no_score') { $ctr++; next; } if ($errorflag eq 'not_allowed') { - $request->print("<span class=\"LC_warning\">Not allowed to modify grades for $uname:$udom</span>"); + $request->print( + '<span class="LC_error">' + .&mt('Not allowed to modify grades for [_1]',"$uname:$udom") + .'</span>'); $ctr++; next; } + if ($numhidden) { + $request->print( + '<span class="LC_info">' + .&mt('For [_1]: [quant,_2,transaction] hidden',"$uname:$udom",$numhidden) + .'</span><br />'); + } my $includemsg = $env{'form.includemsg'.$ctr}; my ($subject,$message,$msgstatus) = ('','',''); my $restitle = &Apache::lonnet::gettitle($symb); @@ -2370,8 +2909,8 @@ sub processHandGrade { undef,$feedurl,undef, undef,undef,$showsymb, $restitle); - $request->print('<br />'.&mt('Sending message to [_1]:[_2]',$uname,$udom).': '. - $msgstatus); + $request->print('<br />'.&mt('Sending message to [_1]',$uname.':'.$udom).': '. + $msgstatus.'<br />'); } if ($env{'form.collaborator'.$ctr}) { my @collabstrs=&Apache::loncommon::get_env_multiple("form.collaborator$ctr"); @@ -2483,7 +3022,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})); @@ -2491,17 +3030,22 @@ 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; @parsedlist = reverse @parsedlist if ($button eq 'Previous'); - my ($partlist) = &response_type($symb); + my $res_error; + my ($partlist) = &response_type($symb,\$res_error); + if ($res_error) { + $request->print(&navmap_errormsg()); + return; + } foreach my $student (@parsedlist) { my $submitonly=$env{'form.submitonly'}; my ($uname,$udom) = split(/:/,$student); @@ -2519,11 +3063,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; } @@ -2534,7 +3078,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++; } @@ -2542,7 +3086,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; @@ -2551,9 +3095,9 @@ sub processHandGrade { $ctr++; } if ($total < 0) { - my $the_end = '<h3><span class="LC_info">LON-CAPA User Message</span></h3><br />'."\n"; - $the_end.='<b>Message: </b> No more students for this section or class.<br /><br />'."\n"; - $the_end.='Click on the button below to return to the grading menu.<br /><br />'."\n"; + my $the_end = '<h3><span class="LC_info">'.&mt('LON-CAPA User Message').'</span></h3><br />'."\n"; + $the_end.='<p>'.&mt('[_1]Message:[_2] No more students for this section or class.','<b>','</b>').'</p>'."\n"; + $the_end.=&mt('Click on the button below to return to the grading menu.').'<br /><br />'."\n"; $the_end.=&show_grading_menu_form($symb); $request->print($the_end); } @@ -2570,9 +3114,14 @@ sub saveHandGrade { my %record = &Apache::lonnet::restore($symb,$env{'request.course.id'},$domain,$stuname); my @parts_graded; my %newrecord = (); - my ($pts,$wgt) = ('',''); + my ($pts,$wgt,$totchg) = ('','',0); my %aggregate = (); my $aggregateflag = 0; + if ($env{'form.HIDE'.$newflg}) { + my ($version,$parts) = split(/:/,$env{'form.HIDE'.$newflg},2); + my $numchgs = &makehidden($version,$parts,\%record,$symb,$domain,$stuname,1); + $totchg += $numchgs; + } my @parts = split(/:/,$env{'form.partlist'.$newflg}); foreach my $new_part (@parts) { #collaborator ($submi may vary for different parts @@ -2588,7 +3137,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'}= @@ -2623,7 +3172,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; @@ -2650,7 +3199,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'}; @@ -2675,7 +3224,37 @@ sub saveHandGrade { &Apache::lonnet::cinc('nohist_resourcetracker',\%aggregate, $cdom,$cnum); } - return ('',$pts,$wgt); + return ('',$pts,$wgt,$totchg); +} + +sub makehidden { + my ($version,$parts,$record,$symb,$domain,$stuname,$tolog) = @_; + return unless (ref($record) eq 'HASH'); + my %modified; + my $numchanged = 0; + if (exists($record->{$version.':keys'})) { + my $partsregexp = $parts; + $partsregexp =~ s/,/|/g; + foreach my $key (split(/\:/,$record->{$version.':keys'})) { + if ($key =~ /^resource\.(?:$partsregexp)\.([^\.]+)$/) { + my $item = $1; + unless (($item eq 'solved') || ($item =~ /^award(|msg|ed)$/)) { + $modified{$key} = $record->{$version.':'.$key}; + } + } elsif ($key =~ m{^(resource\.(?:$partsregexp)\.[^\.]+\.)(.+)$}) { + $modified{$1.'hidden'.$2} = $record->{$version.':'.$key}; + } elsif ($key =~ /^(ip|timestamp|host)$/) { + $modified{$key} = $record->{$version.':'.$key}; + } + } + if (keys(%modified)) { + if (&Apache::lonnet::putstore($env{'request.course.id'},$symb,$version,\%modified, + $domain,$stuname,$tolog) eq 'ok') { + $numchanged ++; + } + } + } + return $numchanged; } sub check_and_remove_from_queue { @@ -2698,64 +3277,86 @@ 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 ($partlist,$handgrade,$responseType) = &response_type($symb); - + my $portfolio_root = '/userfiles/portfolio'; + my $res_error; + my ($partlist,$handgrade,$responseType) = &response_type($symb,\$res_error); + if ($res_error) { + $request->print('<br />'.&navmap_errormsg().'<br />'); + return; + } + my @handedback; + my $file_msg; my @part_response_id = &flatten_responseType($responseType); foreach my $part_response_id (@part_response_id) { my ($part_id,$resp_id) = @{ $part_response_id }; my $part_resp = join('_',@{ $part_response_id }); - if (($env{'form.'.$newflg.'_'.$part_resp.'_returndoc1'}) && ($new_part == $part_id)) { + if (($env{'form.'.$newflg.'_'.$part_resp.'_countreturndoc'} =~ /^\d+$/) & ($new_part eq $part_id)) { + for (my $counter=1; $counter<=$env{'form.'.$newflg.'_'.$part_resp.'_countreturndoc'}; $counter++) { # if multiple files are uploaded names will be 'returndoc2','returndoc3' - my $file_counter = 1; - my $file_msg; - while ($env{'form.'.$newflg.'_'.$part_resp.'_returndoc'.$file_counter}) { - my $fname=$env{'form.'.$newflg.'_'.$part_resp.'_returndoc'.$file_counter.'.filename'}; + if ($env{'form.'.$newflg.'_'.$part_resp.'_returndoc'.$counter}) { + my $fname=$env{'form.'.$newflg.'_'.$part_resp.'_returndoc'.$counter.'.filename'}; my ($directory,$answer_file) = - ($env{'form.'.$newflg.'_'.$part_resp.'_origdoc'.$file_counter} =~ /^(.*?)([^\/]*)$/); + ($env{'form.'.$newflg.'_'.$part_resp.'_origdoc'.$counter} =~ /^(.*?)([^\/]*)$/); 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 $version = &get_next_version($answer_name, $answer_ext, \@dir_list); - # fix file name + my $getpropath = 1; + my ($dir_list,$listerror) = + &Apache::lonnet::dirlist($portfolio_root.$portfolio_path, + $domain,$stuname,$getpropath); + my $version = &get_next_version($answer_name,$answer_ext,$dir_list); + # fix filename my ($save_file_name) = (($directory.$answer_name.".$version.".$answer_ext) =~ /^.+\/${stuname}\/(.*)/); my $result=&Apache::lonnet::finishuserfileupload($stuname,$domain, - $newflg.'_'.$part_resp.'_returndoc'.$file_counter, + $newflg.'_'.$part_resp.'_returndoc'.$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'.$counter). + '</span>'); } else { # mark the file as read only - my @files = ($save_file_name); - my @what = ($symb,$env{'request.course.id'},'handback'); - &Apache::lonnet::mark_as_readonly($domain,$stuname,\@files,\@what); + push(@handedback,$save_file_name); if (exists($$newrecord{"resource.$new_part.$resp_id.handback"})) { $$newrecord{"resource.$new_part.$resp_id.handback"}.=','; } $$newrecord{"resource.$new_part.$resp_id.handback"} .= $save_file_name; - $file_msg.= "\n".'<br /><span class="LC_filename"><a href="/uploaded/'."$domain/$stuname/".$save_file_name.'">'.$save_file_name."</a></span><br />"; + $file_msg.='<span class="LC_filename"><a href="/uploaded/'."$domain/$stuname/".$save_file_name.'">'.$save_file_name."</a></span> <br />"; } - $request->print("<br />".$fname." will be the uploaded file name"); - $request->print(" ".$env{'form.'.$newflg.'_'.$part_resp.'_origdoc'.$file_counter}); - $file_counter++; - } - my $subject = "File Handed Back by Instructor "; - my $message = "A file has been returned that was originally submitted in reponse to: <br />"; - $message .= "<strong>".&Apache::lonnet::gettitle($symb)."</strong><br />"; - $message .= ' The returned file(s) are named: '. $file_msg; - $message .= " and can be found in your portfolio space."; - my ($feedurl,$showsymb) = - &get_feedurl_and_symb($symb,$domain,$stuname); - my $restitle = &Apache::lonnet::gettitle($symb); - my $msgstatus = - &Apache::lonmsg::user_normal_msg($stuname,$domain,$subject. - ' (File Returned) ['.$restitle.']',$message,undef, - $feedurl,undef,undef,undef,$showsymb,$restitle); + $request->print('<br />'.&mt('[_1] will be the uploaded filename [_2]','<span class="LC_info">'.$fname.'</span>','<span class="LC_filename">'.$env{'form.'.$newflg.'_'.$part_resp.'_origdoc'.$counter}.'</span>')); + } } } + } + if (@handedback > 0) { + $request->print('<br />'); + my @what = ($symb,$env{'request.course.id'},'handback'); + &Apache::lonnet::mark_as_readonly($domain,$stuname,\@handedback,\@what); + my $user_lh = &Apache::loncommon::user_lang($stuname,$domain,$env{'request.course.id'}); + my ($subject,$message); + if (scalar(@handedback) == 1) { + $subject = &mt_user($user_lh,'File Handed Back by Instructor'); + } else { + $subject = &mt_user($user_lh,'Files Handed Back by Instructor'); + $message = &mt_user($user_lh,'Files have been returned that were originally submitted in response to: '); + } + $message .= "<p><strong>".&Apache::lonnet::gettitle($symb)." </strong></p>"; + $message .= &mt_user($user_lh,'The returned file(s) are named: [_1]',"<br />$file_msg <br />"). + &mt_user($user_lh,'The file(s) can be found in your [_1]portfolio[_2].','<a href="/adm/portfolio">','</a>'); + my ($feedurl,$showsymb) = + &get_feedurl_and_symb($symb,$domain,$stuname); + my $restitle = &Apache::lonnet::gettitle($symb); + $subject .= ' '.&mt_user($user_lh,'(File Returned)').' ['.$restitle.']'; + my $msgstatus = + &Apache::lonmsg::user_normal_msg($stuname,$domain,$subject, + $message,undef,$feedurl,undef,undef,undef,$showsymb, + $restitle); + if ($msgstatus) { + $request->print(&mt('Notification message status: [_1]','<span class="LC_info">'.$msgstatus.'</span>').'<br />'); + } + } return; } @@ -2822,7 +3423,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; @@ -2852,8 +3453,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$/ ) { @@ -2864,8 +3464,11 @@ 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 $version = &get_next_version($answer_name, $answer_ext, \@dir_list); + my $getpropath = 1; + my ($dir_list,$listerror) = + &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') { push(@versioned_portfiles, $directory.$new_answer); @@ -2884,21 +3487,24 @@ sub version_portfiles { sub get_next_version { my ($answer_name, $answer_ext, $dir_list) = @_; my $version; - foreach my $row (@$dir_list) { - my ($file) = split(/\&/,$row,2); - my ($file_name,$file_version,$file_ext) = - &file_name_version_ext($file); - if (($file_name eq $answer_name) && - ($file_ext eq $answer_ext)) { - # gets here if filename and extension match, regardless of version + if (ref($dir_list) eq 'ARRAY') { + foreach my $row (@{$dir_list}) { + my ($file) = split(/\&/,$row,2); + my ($file_name,$file_version,$file_ext) = + &file_name_version_ext($file); + if (($file_name eq $answer_name) && + ($file_ext eq $answer_ext)) { + # gets here if filename and extension match, + # regardless of version if ($file_version ne '') { - # a versioned file is found so save it for later - if ($file_version > $version) { - $version = $file_version; + # a versioned file is found so save it for later + if ($file_version > $version) { + $version = $file_version; + } } } } - } + } $version ++; return($version); } @@ -2944,6 +3550,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) { @@ -2952,7 +3559,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) { @@ -3050,7 +3657,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; } @@ -3124,7 +3731,7 @@ sub viewgrades { &Apache::lonnet::clear_EXT_cache_status(); my $result='<h3><span class="LC_info">'.&mt('Manual Grading').'</span></h3>'; - $result.='<h4><b>Current Resource: </b>'.$env{'form.probTitle'}.'</h4>'."\n"; + $result.='<h4><b>'.&mt('Current Resource').':</b> '.$env{'form.probTitle'}.'</h4>'."\n"; #view individual student submission form - called using Javascript viewOneStudent $result.=&jscriptNform($symb); @@ -3139,24 +3746,28 @@ sub viewgrades { '<input type="hidden" name="Status" value="'.$env{'stu_status'}.'" />'."\n". '<input type="hidden" name="probTitle" value="'.$env{'form.probTitle'}.'" />'."\n"; - my $sectionClass; - my $section_display = join (", ",&Apache::loncommon::get_env_multiple('form.section')); + my ($common_header,$specific_header); if ($env{'form.section'} eq 'all') { - $sectionClass='Class </h3>'; + $common_header = &mt('Assign Common Grade to Class'); + $specific_header = &mt('Assign Grade to Specific Students in Class'); } elsif ($env{'form.section'} eq 'none') { - $sectionClass=&mt('Students in no Section').'</h3>'; + $common_header = &mt('Assign Common Grade to Students in no Section'); + $specific_header = &mt('Assign Grade to Specific Students in no Section'); } else { - $sectionClass=&mt('Students in Section(s) [_1]',$section_display).'</h3>'; + my $section_display = join (", ",&Apache::loncommon::get_env_multiple('form.section')); + $common_header = &mt('Assign Common Grade to Students in Section(s) [_1]',$section_display); + $specific_header = &mt('Assign Grade to Specific Students in Section(s) [_1]',$section_display); } - $result.='<h3>'.&mt('Assign Common Grade To [_1]',$sectionClass); - $result.= '<table border=0><tr><td bgcolor="#777777">'."\n". - '<table border=0><tr bgcolor="#ffffdd"><td>'; + $result.= '<h3>'.$common_header.'</h3>'.&Apache::loncommon::start_data_table(); #radio buttons/text box for assigning points for a section or class. #handles different parts of a problem - my ($partlist,$handgrade,$responseType) = &response_type($symb); + my $res_error; + my ($partlist,$handgrade,$responseType) = &response_type($symb,\$res_error); + if ($res_error) { + return &navmap_errormsg(); + } my %weight = (); my $ctsparts = 0; - $result.='<table border="0">'; my %seen = (); my @part_response_id = &flatten_responseType($responseType); foreach my $part_response_id (@part_response_id) { @@ -3168,67 +3779,86 @@ sub viewgrades { my $wgt = &Apache::lonnet::EXT('resource.'.$partid.'.weight',$symb); $weight{$partid} = $wgt eq '' ? '1' : $wgt; - $result.='<input type="hidden" name="partid_'. - $ctsparts.'" value="'.$partid.'" />'."\n"; - $result.='<input type="hidden" name="weight_'. - $partid.'" value="'.$weight{$partid}.'" />'."\n"; my $display_part=&get_display_part($partid,$symb); - $result.='<tr><td><b>Part:</b> '.$display_part.' <b>Point:</b> </td><td>'; - $result.='<table border="0"><tr>'; + my $radio.='<table border="0"><tr>'; my $ctr = 0; while ($ctr<=$weight{$partid}) { # display radio buttons in a nice table 10 across - $result.= '<td><label><input type="radio" name="RADVAL_'.$partid.'" '. + $radio.= '<td><label><input type="radio" name="RADVAL_'.$partid.'" '. 'onclick="javascript:writePoint(\''.$partid.'\','.$weight{$partid}. ','.$ctr.')" />'.$ctr."</label></td>\n"; $result.=(($ctr+1)%10 == 0 ? '</tr><tr>' : ''); $ctr++; } - $result.='</tr></table>'; - $result.= '</td><td><b> or </b><input type="text" name="TEXTVAL_'. - $partid.'" size="4" '.'onChange="javascript:writePoint(\''. + $radio.='</tr></table>'; + my $line = '<input type="text" name="TEXTVAL_'. + $partid.'" size="4" '.'onchange="javascript:writePoint(\''. $partid.'\','.$weight{$partid}.',\'textval\')" /> /'. - $weight{$partid}.' (problem weight)</td>'."\n"; - $result.= '</td><td><select name="SELVAL_'.$partid.'"'. - 'onChange="javascript:writeRadText(\''.$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>'. - '<option>excused</option>'. - '<option>reset status</option></select></td>'. - '<td><label><input type="checkbox" name="FORCE_'.$partid.'" /> Override "Correct"</label></td></tr>'."\n"; + '<option value="excused">'.&mt('excused').'</option>'. + '<option value="reset status">'.&mt('reset status').'</option>'. + '</select></td>'. + '<td><label><input type="checkbox" name="FORCE_'.$partid.'" />'.&mt('Override "Correct"').'</label>'; + $line.='<input type="hidden" name="partid_'. + $ctsparts.'" value="'.$partid.'" />'."\n"; + $line.='<input type="hidden" name="weight_'. + $partid.'" value="'.$weight{$partid}.'" />'."\n"; + + $result.= + &Apache::loncommon::start_data_table_row()."\n". + '<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++; } - $result.='</table>'.'</td></tr></table>'.'</td></tr></table>'."\n". + $result.=&Apache::loncommon::end_data_table()."\n". '<input type="hidden" name="totalparts" value="'.$ctsparts.'" />'; - $result.='<input type="button" value="Revert to Default" '. - 'onClick="javascript:resetEntry('.$ctsparts.');" target="_self" />'; + $result.='<input type="button" value="'.&mt('Revert to Default').'" '. + 'onclick="javascript:resetEntry('.$ctsparts.');" />'; #table listing all the students in a section/class #header of table - $result.= '<h3>Assign Grade to Specific Students in '.$sectionClass; - $result.= '<table border=0><tr><td bgcolor="#777777">'."\n". - '<table border=0><tr bgcolor="#deffff"><td> <b>No.</b> </td>'. - '<td>'.&nameUserString('header')."</td>\n"; - my (@parts) = sort(&getpartlist($symb)); + $result.= '<h3>'.$specific_header.'</h3>'. + &Apache::loncommon::start_data_table(). + &Apache::loncommon::start_data_table_header_row(). + '<th>'.&mt('No.').'</th>'. + '<th>'.&nameUserString('header')."</th>\n"; + my $partserror; + my (@parts) = sort(&getpartlist($symb,\$partserror)); + if ($partserror) { + return &navmap_errormsg(); + } my (undef,undef,$url)=&Apache::lonnet::decode_symb($symb); 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.='<td><b>Score Part:</b> '.$display_part. - ' <br /><b>(weight = '.$weight{$partid}.')</b></td>'."\n"; + $result.='<th>'. + &mt('Score Part: [_1][_2](weight = [_3])', + $display_part,'<br />',$weight{$partid}).'</th>'."\n"; next; + } else { - $display =~s/\[Part: \Q$partid\E\]/Part:<\/b> $display_part/; + if ($display =~ /Problem Status/) { + my $grade_status_mt = &mt('Grade Status'); + $display =~ s{Problem Status}{$grade_status_mt<br />}; + } + my $part_mt = &mt('Part:'); + $display =~s{\[Part: \Q$partid\E\]}{$part_mt $display_part}; } - $display =~ s|Problem Status|Grade Status<br />|; - $result.='<td><b>'.$display.'</td>'."\n"; + + $result.='<th>'.$display.'</th>'."\n"; } - $result.='</tr>'; + $result.=&Apache::loncommon::end_data_table_header_row(); my %last_resets = &get_last_resets($symb,$env{'request.course.id'},\@partids); @@ -3248,16 +3878,16 @@ sub viewgrades { $result.=&viewstudentgrade($symb,$env{'request.course.id'}, $_,$$fullname{$_},\@parts,\%weight,$ctr,\%last_resets); } - $result.='</table></td></tr></table>'; + $result.=&Apache::loncommon::end_data_table(); $result.='<input type="hidden" name="total" value="'.$ctr.'" />'."\n"; - $result.='<input type="button" value="Save" '. - 'onClick="javascript:submit();" target="_self" /></form>'."\n"; + $result.='<input type="button" value="'.&mt('Save').'" '. + 'onclick="javascript:submit();" target="_self" /></form>'."\n"; if (scalar(%$fullname) eq 0) { my $colspan=3+scalar(@parts); my $section_display = join (", ",&Apache::loncommon::get_env_multiple('form.section')); my $stu_status = join(' or ',&Apache::loncommon::get_env_multiple('form.Status')); $result='<span class="LC_warning">'. - &mt('There are no students in section(s) [_1] with enrollment status [_2] to modify or grade', + &mt('There are no students in section(s) [_1] with enrollment status [_2] to modify or grade.', $section_display, $stu_status). '</span>'; } @@ -3271,7 +3901,7 @@ sub viewstudentgrade { my ($uname,$udom) = split(/:/,$student); my %record=&Apache::lonnet::restore($symb,$courseid,$udom,$uname); my %aggregates = (); - my $result='<tr bgcolor="#ffffdd"><td align="right">'. + my $result=&Apache::loncommon::start_data_table_row().'<td align="right">'. '<input type="hidden" name="ctr'.($ctr-1).'" value="'.$student.'" />'. "\n".$ctr.' </td><td> '. '<a href="javascript:viewOneStudent(\''.$uname.'\',\''.$udom. @@ -3303,7 +3933,7 @@ sub viewstudentgrade { 'GD_'.$student.'_'.$part.'_awarded_s" value="'.$pts.'" />'."\n"; $result.='<input type="text" name="'. 'GD_'.$student.'_'.$part.'_awarded" '. - 'onChange="javascript:changeSelect(\''.$part.'\',\''.$student. + 'onchange="javascript:changeSelect(\''.$part.'\',\''.$student. '\')" value="'.$pts.'" size="4" /></td>'."\n"; } elsif ($type eq 'solved') { my ($status,$foo)=split(/_/,$score,2); @@ -3312,10 +3942,10 @@ sub viewstudentgrade { $part.'_solved_s" value="'.$status.'" />'."\n"; $result.=' <select name="'. 'GD_'.$student.'_'.$part.'_solved" '. - 'onChange="javascript:changeOneScore(\''.$part.'\',\''.$student.'\')" >'."\n"; - $result.= (($status eq 'excused') ? '<option> </option><option selected="selected">excused</option>' - : '<option selected="selected"> </option><option>excused</option>')."\n"; - $result.='<option>reset status</option>'; + 'onchange="javascript:changeOneScore(\''.$part.'\',\''.$student.'\')" >'."\n"; + $result.= (($status eq 'excused') ? '<option> </option><option selected="selected" value="excused">'.&mt('excused').'</option>' + : '<option selected="selected"> </option><option value="excused">'.&mt('excused').'</option>')."\n"; + $result.='<option value="reset status">'.&mt('reset status').'</option>'; $result.="</select> </td>\n"; } else { $result.='<input type="hidden" name="'. @@ -3326,7 +3956,7 @@ sub viewstudentgrade { 'value="'.$score.'" size="4" /></td>'."\n"; } } - $result.='</tr>'; + $result.=&Apache::loncommon::end_data_table_row(); return $result; } @@ -3335,22 +3965,22 @@ sub viewstudentgrade { sub editgrades { my ($request) = @_; - my $symb=&get_symb($request); + my ($symb)=&get_symb($request); my $section_display = join (", ",&Apache::loncommon::get_env_multiple('form.section')); - my $title='<h3><span class="LC_info">'.&mt('Current Grade Status').'</span></h3>'; - $title.='<h4>'.&mt('<b>Current Resource: </b>[_1]',$env{'form.probTitle'}).'</h4><br />'."\n"; - $title.='<h4>'.&mt('<b>Section: </b>[_1]',$section_display).'</h4>'."\n"; - - my $result= '<table border="0"><tr><td bgcolor="#777777">'."\n"; - $result.= '<table border="0"><tr bgcolor="#deffff">'. - '<td rowspan=2 valign="center"> <b>No.</b> </td>'. - '<td rowspan=2 valign="center">'.&nameUserString('header')."</td>\n"; + my $title='<h2>'.&mt('Current Grade Status').'</h2>'; + $title.='<h4><b>'.&mt('Current Resource').':</b> '.$env{'form.probTitle'}.'</h4>'."\n"; + $title.='<h4><b>'.&mt('Section:').'</b> '.$section_display.'</h4>'."\n"; + my $result= &Apache::loncommon::start_data_table(). + &Apache::loncommon::start_data_table_header_row(). + '<th rowspan="2" valign="middle">'.&mt('No.').'</th>'. + '<th rowspan="2" valign="middle">'.&nameUserString('header')."</th>\n"; my %scoreptr = ( 'correct' =>'correct_by_override', 'incorrect'=>'incorrect_by_override', 'excused' =>'excused', 'ungraded' =>'ungraded_attempted', + 'credited' =>'credit_attempted', 'nothing' => '', ); my ($classlist,undef,$fullname) = &getclasslist($env{'form.section'},'0'); @@ -3360,42 +3990,48 @@ sub editgrades { my %columns = (); my ($i,$ctr,$count,$rec_update) = (0,0,0,0); - my (@parts) = sort(&getpartlist($symb)); + my $partserror; + my (@parts) = sort(&getpartlist($symb,\$partserror)); + if ($partserror) { + return &navmap_errormsg(); + } 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++; } my (undef,undef,$url) = &Apache::lonnet::decode_symb($symb); foreach my $partid (@partid) { - $header .= '<td align="center"> <b>Old Score</b> </td>'. - '<td align="center"> <b>New Score</b> </td>'; + $header .= '<th align="center">'.&mt('Old Score').'</th>'. + '<th align="center">'.&mt('New Score').'</th>'; $columns{$partid}=2; foreach my $stores (@parts) { my ($part,$type) = &split_part_type($stores); if ($part !~ m/^\Q$partid\E/) { next;} 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 .= '<td align="center"> <b>Old '.$display.'</b> </td>'. - '<td align="center"> <b>New '.$display.'</b> </td>'; + $display =~ s/\[Part: \Q$part\E\]//; + 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; } } foreach my $partid (@partid) { my $display_part=&get_display_part($partid,$symb); - $result .= '<td colspan="'.$columns{$partid}. - '" align="center"><b>Part:</b> '.$display_part. - ' (Weight = '.$weight{$partid}.')</td>'; + $result .= '<th colspan="'.$columns{$partid}.'" align="center">'. + &mt('Part: [_1] (Weight = [_2])',$display_part,$weight{$partid}). + '</th>'; } - $result .= '</tr><tr bgcolor="#deffff">'; - $result .= $header; - $result .= '</tr>'."\n"; - my $noupdate; + $result .= &Apache::loncommon::end_data_table_header_row(). + &Apache::loncommon::start_data_table_header_row(). + $header. + &Apache::loncommon::end_data_table_header_row(); + my @noupdate; my ($updateCtr,$noupdateCtr) = (1,1); for ($i=0; $i<$env{'form.total'}; $i++) { my $line; @@ -3407,7 +4043,9 @@ sub editgrades { my $usec=$classlist->{"$uname:$udom"}[5]; if (!&canmodify($usec)) { my $numcols=scalar(@partid)*4+2; - $noupdate.=$line."<td colspan=\"$numcols\"><span class=\"LC_warning\">Not allowed to modify student</span></td></tr>"; + push(@noupdate, + $line."<td colspan=\"$numcols\"><span class=\"LC_warning\">". + &mt('Not allowed to modify student')."</span></td></tr>"); next; } my %aggregate = (); @@ -3476,7 +4114,7 @@ sub editgrades { '<td align="center">'.$awarded.' </td>'; } } - $line.='</tr>'."\n"; + $line.="\n"; my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'}; @@ -3509,10 +4147,13 @@ sub editgrades { } } - $result.='<tr bgcolor="#ffffde"><td align="right"> '.$updateCtr.' </td>'.$line; + $result.=&Apache::loncommon::start_data_table_row(). + '<td align="right"> '.$updateCtr.' </td>'.$line. + &Apache::loncommon::end_data_table_row(); $updateCtr++; } else { - $noupdate.='<tr bgcolor="#ffffde"><td align="right"> '.$noupdateCtr.' </td>'.$line; + push(@noupdate, + '<td align="right"> '.$noupdateCtr.' </td>'.$line); $noupdateCtr++; } if ($aggregateflag) { @@ -3520,16 +4161,28 @@ sub editgrades { $cdom,$cnum); } } - if ($noupdate) { + if (@noupdate) { # my $numcols=(scalar(@partid)*(scalar(@parts)-1)*2)+3; my $numcols=scalar(@partid)*4+2; - $result .= '<tr bgcolor="#ffffff"><td align="center" colspan="'.$numcols.'">No Changes Occurred For the Students Below</td></tr><tr bgcolor="#ffffde">'.$noupdate; + $result .= &Apache::loncommon::start_data_table_row('LC_empty_row'). + '<td align="center" colspan="'.$numcols.'">'. + &mt('No Changes Occurred For the Students Below'). + '</td>'. + &Apache::loncommon::end_data_table_row(); + foreach my $line (@noupdate) { + $result.= + &Apache::loncommon::start_data_table_row(). + $line. + &Apache::loncommon::end_data_table_row(); + } } - $result .= '</table></td></tr></table>'."\n". - &show_grading_menu_form ($symb); - my $msg = '<br /><b>Number of records updated = '.$rec_update. - ' for '.$count.' student'.($count <= 1 ? '' : 's').'.</b><br />'. - '<b>Total number of students = '.$env{'form.total'}.'</b><br />'; + $result .= &Apache::loncommon::end_data_table(). + &show_grading_menu_form($symb); + my $msg = '<p><b>'. + &mt('Number of records updated = [_1] for [quant,_2,student].', + $rec_update,$count).'</b><br />'. + '<b>'.&mt('Total number of students = [_1]',$env{'form.total'}). + '</b></p>'; return $title.$msg.$result; } @@ -3552,7 +4205,7 @@ sub split_part_type { # #--- Javascript to handle csv upload sub csvupload_javascript_reverse_associate { - my $error1=&mt('You need to specify the username or ID'); + my $error1=&mt('You need to specify the username or the student/employee ID'); my $error2=&mt('You need to specify at least one grading field'); return(<<ENDPICK); function verify(vf) { @@ -3592,7 +4245,7 @@ ENDPICK } sub csvupload_javascript_forward_associate { - my $error1=&mt('You need to specify the username or ID'); + my $error1=&mt('You need to specify the username or the student/employee ID'); my $error2=&mt('You need to specify at least one grading field'); return(<<ENDPICK); function verify(vf) { @@ -3650,7 +4303,7 @@ $result Total number of records found in file: $distotal <hr /> Enter as many fields as you can. The system will inform you and bring you back to this page if the data selected is insufficient to run your class.<hr /> -<input type="button" value="Reverse Association" onClick="javascript:this.form.associate.value='Reverse Association';submit(this.form);" /> +<input type="button" value="Reverse Association" onclick="javascript:this.form.associate.value='Reverse Association';submit(this.form);" /> <label><input type="checkbox" name="noFirstLine" $checked />$ignore</label> <input type="hidden" name="associate" value="" /> <input type="hidden" name="phase" value="three" /> @@ -3673,9 +4326,15 @@ ENDPICK } sub csvupload_fields { - my ($symb) = @_; - my (@parts) = &getpartlist($symb); - my @fields=(['ID','Student ID'], + my ($symb,$errorref) = @_; + my (@parts) = &getpartlist($symb,$errorref); + if (ref($errorref)) { + if ($$errorref) { + return; + } + } + + my @fields=(['ID','Student/Employee ID'], ['username','Student Username'], ['domain','Student Domain']); my (undef,undef,$url) = &Apache::lonnet::decode_symb($symb); @@ -3695,21 +4354,23 @@ sub csvupload_fields { sub csvuploadmap_footer { my ($request,$i,$keyfields) =@_; + my $buttontext = &mt('Assign Grades'); $request->print(<<ENDPICK); </table> <input type="hidden" name="nfields" value="$i" /> <input type="hidden" name="keyfields" value="$keyfields" /> -<input type="button" onClick="javascript:verify(this.form)" value="Assign Grades" /><br /> +<input type="button" onclick="javascript:verify(this.form)" value="$buttontext" /><br /> </form> 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(); @@ -3729,9 +4390,9 @@ 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.='<tr bgcolor=#ffffe6><td>'."\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(); my $ignore=&mt('Ignore First Line'); @@ -3743,7 +4404,7 @@ sub upcsvScores_form { <input type="hidden" name="probTitle" value="$env{'form.probTitle'}" /> <input type="hidden" name="saveState" value="$env{'form.saveState'}" /> $upfile_select -<br /><input type="button" onClick="javascript:checkUpload(this.form);" value="$upload" /> +<br /><input type="button" onclick="javascript:checkUpload(this.form);" value="$upload" /> <label><input type="checkbox" name="noFirstLine" />$ignore</label> </form> ENDUPFORM @@ -3773,8 +4434,12 @@ sub csvuploadmap { &csvuploadmap_header($request,$symb,$datatoken,$#records+1); my ($i,$keyfields); if (@records) { - my @fields=&csvupload_fields($symb); - + my $fieldserror; + my @fields=&csvupload_fields($symb,\$fieldserror); + if ($fieldserror) { + $request->print(&navmap_errormsg()); + return; + } if ($env{'form.upfile_associate'} eq 'reverse') { &Apache::loncommon::csv_print_samples($request,\@records); $i=&Apache::loncommon::csv_print_select_table($request,\@records, @@ -3838,7 +4503,7 @@ ENDPICK } # FIXME do a check for any duplicated user ids... # FIXME do a check for any invalid user ids?... - $request->print('<input type="submit" value="Assign Grades" /><br /> + $request->print('<input type="submit" value="'.&mt('Assign Grades').'" /><br /> <hr /></form>'."\n"); $request->print(&show_grading_menu_form($symb)); return ''; @@ -3875,6 +4540,7 @@ sub csvuploadassign { my ($classlist) = &getclasslist('all',0); my @notallowed; my @skipped; + my @warnings; my $countdone=0; foreach my $grade (@gradedata) { my %entries=&Apache::loncommon::record_sep($grade); @@ -3923,6 +4589,9 @@ sub csvuploadassign { my $pcr=$entries{$fields{$dest}} / $wgt; my $award=($pcr == 0) ? 'incorrect_by_override' : 'correct_by_override'; + if ($pcr>1) { + push(@warnings,&mt("[_1]: point value larger than weight","$username:$domain")); + } $grades{"resource.$part.awarded"}=$pcr; $grades{"resource.$part.solved"}=$award; $points{$part}=1; @@ -3942,32 +4611,41 @@ 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('.'); +# Remove from grading queue + &Apache::bridgetask::remove_from_queue('gradingqueue',$symb, + $env{'course.'.$env{'request.course.id'}.'.domain'}, + $env{'course.'.$env{'request.course.id'}.'.num'}, + $domain,$username); + } 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 />'.&Apache::lonhtmlcommon::confirm_success(&mt("Saved scores for [quant,_1,student]",$countdone),$countdone==0)); + if (@warnings) { + $request->print('<br />'.&Apache::lonhtmlcommon::confirm_success(&mt('Warnings generated for the following saved scores:'),1).'<br />'); + $request->print(join(', ',@warnings)); } - $request->print("<br />Saved $countdone students\n"); if (@skipped) { - $request->print('<p><h4><b>Skipped Students</b></h4></p>'); - foreach my $student (@skipped) { $request->print("$student<br />\n"); } + $request->print('<br />'.&Apache::lonhtmlcommon::confirm_success(&mt('No scores stored for the following username(s):'),1).'<br />'); + $request->print(join(', ',@skipped)); } if (@notallowed) { - $request->print('<p><span class="LC_error">Students Not Allowed to Modify</span></p>'); - foreach my $student (@notallowed) { $request->print("$student<br />\n"); } + $request->print('<br />'.&Apache::lonhtmlcommon::confirm_success(&mt('Modification of scores not allowed for the following username(s):'),1).'<br />'); + $request->print(join(', ',@notallowed)); } $request->print("<br />\n"); $request->print(&show_grading_menu_form($symb)); @@ -3983,12 +4661,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); @@ -4006,23 +4685,30 @@ LISTJAVASCRIPT my $getsec = $env{'form.section'} eq '' ? 'all' : $env{'form.section'}; my $result='<h3><span class="LC_info"> '. - 'Manual Grading by Page or Sequence</span></h3>'; + &mt('Manual Grading by Page or Sequence').'</span></h3>'; $result.='<form action="/adm/grades" method="post" name="displayPage">'."\n"; - $result.=' <b>Problems from:</b> <select name="selectpage">'."\n"; - my ($titles,$symbx) = &getSymbMap(); + my $map_error; + my ($titles,$symbx) = &getSymbMap($map_error); + if ($map_error) { + $request->print(&navmap_errormsg()); + return; + } my ($curpage) =&Apache::lonnet::decode_symb($symb); # my ($curpage,$mapId) =&Apache::lonnet::decode_symb($symb); # my $type=($curpage =~ /\.(page|sequence)/); + my $select = '<select name="selectpage">'."\n"; my $ctr=0; foreach (@$titles) { my ($minder,$showtitle) = ($_ =~ /(\d+)\.(.*)/); - $result.='<option value="'.$ctr.'" '. + $select.='<option value="'.$ctr.'" '. ($$symbx{$_} =~ /$curpage$/ ? 'selected="selected"' : ''). '>'.$showtitle.'</option>'."\n"; $ctr++; } - $result.= '</select>'."<br />\n"; + $select.= '</select>'; + $result.=' <b>'.&mt('Problems from').':</b> '.$select."<br />\n"; + $ctr=0; foreach (@$titles) { my ($minder,$showtitle) = ($_ =~ /(\d+)\.(.*)/); @@ -4033,13 +4719,16 @@ LISTJAVASCRIPT $result.='<input type="hidden" name="page" />'."\n". '<input type="hidden" name="title" />'."\n"; - $result.=' <b>View Problems Text: </b><label><input type="radio" name="vProb" value="no" checked="checked" /> no </label>'."\n". - '<label><input type="radio" name="vProb" value="yes" /> yes </label>'."<br />\n"; - - $result.=' <b>Submission Details: </b>'. - '<label><input type="radio" name="lastSub" value="none" /> none</label>'."\n". - '<label><input type="radio" name="lastSub" value="datesub" checked="checked" /> by dates and submissions</label>'."\n". - '<label><input type="radio" name="lastSub" value="all" /> all details</label>'."\n"; + 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.=' <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.=' <b>'.&mt('Submissions').': </b>'.$options; $result.=&build_section_inputs(); my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status')); @@ -4048,21 +4737,21 @@ 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.=' <b>'.&mt('Use CODE:').' </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="Next->" /><br />'."\n"; + 'onclick="javascript:checkPickOne(this.form);" value="'.&mt('Next').' →" /><br />'."\n"; $request->print($result); - my $studentTable.=' <b>Select a student you wish to grade and then click on the Next button.</b><br />'. - '<table border="0"><tr><td bgcolor="#777777">'. - '<table border="0"><tr bgcolor="#e6ffff">'. - '<td align="right"> <b>No.</b></td>'. - '<td>'.&nameUserString('header').'</td>'. - '<td align="right"> <b>No.</b></td>'. - '<td>'.&nameUserString('header').'</td></tr>'; + my $studentTable.=' <b>'.&mt('Select a student you wish to grade and then click on the Next button.').'</b><br />'. + &Apache::loncommon::start_data_table(). + &Apache::loncommon::start_data_table_header_row(). + '<th align="right"> '.&mt('No.').'</th>'. + '<th>'.&nameUserString('header').'</th>'. + '<th align="right"> '.&mt('No.').'</th>'. + '<th>'.&nameUserString('header').'</th>'. + &Apache::loncommon::end_data_table_header_row(); my (undef,undef,$fullname) = &getclasslist($getsec,'1'); my $ptr = 1; @@ -4074,17 +4763,23 @@ LISTJAVASCRIPT return $a cmp $b; } (keys(%$fullname))) { my ($uname,$udom) = split(/:/,$student); - $studentTable.=($ptr%2 == 1 ? '<tr bgcolor="#ffffe6">' : '</td>'); + $studentTable.=($ptr%2==1 ? &Apache::loncommon::start_data_table_row() + : '</td>'); $studentTable.='<td align="right">'.$ptr.' </td>'; $studentTable.='<td> <label><input type="radio" name="student" value="'.$student.'" /> ' .&nameUserString(undef,$$fullname{$student},$uname,$udom)."</label>\n"; - $studentTable.=($ptr%2 == 0 ? '</td></tr>' : ''); + $studentTable.= + ($ptr%2 == 0 ? '</td>'.&Apache::loncommon::end_data_table_row() + : ''); $ptr++; } - $studentTable.='</td><td> </td><td> </td></tr>' if ($ptr%2 == 0); - $studentTable.='</table></td></tr></table>'."\n"; + if ($ptr%2 == 0) { + $studentTable.='</td><td> </td><td> </td>'. + &Apache::loncommon::end_data_table_row(); + } + $studentTable.=&Apache::loncommon::end_data_table()."\n"; $studentTable.='<input type="button" '. - 'onClick="javascript:checkPickOne(this.form);"value="Next->" /></form>'."\n"; + 'onclick="javascript:checkPickOne(this.form);" value="'.&mt('Next').' →" /></form>'."\n"; $studentTable.=&show_grading_menu_form($symb); $request->print($studentTable); @@ -4093,8 +4788,14 @@ LISTJAVASCRIPT } sub getSymbMap { + my ($map_error) = @_; my $navmap = Apache::lonnavmaps::navmap->new(); - + unless (ref($navmap)) { + if (ref($map_error)) { + $$map_error = 'navmap'; + } + return; + } my %symbx = (); my @titles = (); my $minder = 0; @@ -4136,15 +4837,19 @@ sub displayPage { &Apache::lonnet::clear_EXT_cache_status(); if (!&canview($usec)) { - $request->print('<span class="LC_warning">Unable to view requested student.('.$env{'form.student'}.')</span>'); - $request->print(&show_grading_menu_form($symb)); - return; + $request->print('<span class="LC_warning">'. + &mt('Unable to view requested student. ([_1])', + $env{'form.student'}). + '</span>'); + $request->print(&show_grading_menu_form($symb)); + return; } my $result='<h3><span class="LC_info"> '.$env{'form.title'}.'</span></h3>'; - $result.='<h3> Student: '.&nameUserString(undef,$$fullname{$env{'form.student'}},$uname,$udom). + $result.='<h3> '.&mt('Student: [_1]',&nameUserString(undef,$$fullname{$env{'form.student'}},$uname,$udom)). '</h3>'."\n"; - if (&Apache::lonnet::validCODE($env{'form.CODE'})) { - $result.='<h3> CODE: '.$env{'form.CODE'}.'</h3>'."\n"; + $env{'form.CODE'} = uc($env{'form.CODE'}); + if (&Apache::lonnet::validCODE(uc($env{'form.CODE'}))) { + $result.='<h3> '.&mt('CODE: [_1]',$env{'form.CODE'}).'</h3>'."\n"; } else { delete($env{'form.CODE'}); } @@ -4152,10 +4857,15 @@ sub displayPage { $request->print($result); my $navmap = Apache::lonnavmaps::navmap->new(); + unless (ref($navmap)) { + $request->print(&navmap_errormsg()); + $request->print(&show_grading_menu_form($symb)); + return; + } 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 view requested sequence. ('.$resUrl.')</span>'); + $request->print('<span class="LC_warning">'.&mt('Unable to view requested sequence. ([_1])',$resUrl).'</span>'); $request->print(&show_grading_menu_form($symb)); return; } @@ -4177,15 +4887,16 @@ sub displayPage { '<input type="hidden" name="CODE" value="'.$env{'form.CODE'}.'" />'."\n"; } my $checkIcon = '<img alt="'.&mt('Check Mark'). - '" src="'.$request->dir_config('lonIconsURL'). - '/check.gif" height="16" border="0" />'; + '" src="'.&Apache::loncommon::lonhttpdurl($request->dir_config('lonIconsURL').'/check.gif').'" height="16" border="0" />'; - $studentTable.=' <b>Note:</b> Problems graded correct by the computer are marked with a '.$checkIcon. - ' symbol.'."\n". - '<table border="0"><tr><td bgcolor="#777777">'. - '<table border="0"><tr bgcolor="#e6ffff">'. - '<td align="center"><b> Prob. </b></td>'. - '<td><b> '.($env{'form.vProb'} eq 'no' ? 'Title' : 'Problem Text').'/Grade</b></td></tr>'; + $studentTable.=' <span class="LC_info">'. + &mt('Problems graded correct by the computer are marked with a [_1] symbol.',$checkIcon). + '</span>'."\n". + &Apache::loncommon::start_data_table(). + &Apache::loncommon::start_data_table_header_row(). + '<th align="center"> Prob. </th>'. + '<th> '.($env{'form.vProb'} eq 'no' ? &mt('Title') : &mt('Problem Text')).'/'.&mt('Grade').'</th>'. + &Apache::loncommon::end_data_table_header_row(); &Apache::lonxml::clear_problem_counter(); my ($depth,$question,$prob) = (1,1,1); @@ -4199,8 +4910,14 @@ sub displayPage { my $parts = $curRes->parts(); my $title = $curRes->compTitle(); my $symbx = $curRes->symb(); - $studentTable.='<tr bgcolor="#ffffe6"><td align="center" valign="top" >'.$prob. - (scalar(@{$parts}) == 1 ? '' : '<br />('.scalar(@{$parts}).' parts)').'</td>'; + $studentTable.= + &Apache::loncommon::start_data_table_row(). + '<td align="center" valign="top" >'.$prob. + (scalar(@{$parts}) == 1 ? '' + : '<br />('.&mt('[_1]parts', + scalar(@{$parts}).' ').')' + ). + '</td>'; $studentTable.='<td valign="top">'; my %form = ('CODE' => $env{'form.CODE'},); if ($env{'form.vProb'} eq 'yes' ) { @@ -4215,14 +4932,14 @@ sub displayPage { # $request->print('match='.$1."<br />\n"); # } # $companswer =~ s|<table border=\"1\">|<table border=\"0\">|g; - $studentTable.=' <b>'.$title.'</b> <br /> <b>Correct answer:</b><br />'.$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); if ($env{'form.lastSub'} eq 'datesub') { if ($record{'version'} eq '') { - $studentTable.='<br /> <span class="LC_warning">No recorded submission for this problem</span><br />'; + $studentTable.='<br /> <span class="LC_warning">'.&mt('No recorded submission for this problem.').'</span><br />'; } else { my %responseType = (); foreach my $partid (@{$parts}) { @@ -4239,17 +4956,21 @@ sub displayPage { } } elsif ($env{'form.lastSub'} eq 'all') { my $last = ($env{'form.lastSub'} eq 'last' ? 'last' : ''); + my $identifier = (&canmodify($usec)? $prob : ''); $studentTable.=&Apache::loncommon::get_previous_attempt($symbx,$uname,$udom, $env{'request.course.id'}, - '','.submission'); + '','.submission',undef, + $usec,$identifier); } if (&canmodify($usec)) { + $studentTable.=&gradeBox_start(); foreach my $partid (@{$parts}) { $studentTable.=&gradeBox($request,$symbx,$uname,$udom,$question,$partid,\%record); $studentTable.='<input type="hidden" name="q_'.$question.'" value="'.$partid.'" />'."\n"; $question++; } + $studentTable.=&gradeBox_end(); $prob++; } $studentTable.='</td></tr>'; @@ -4258,10 +4979,11 @@ sub displayPage { $curRes = $iterator->next(); } - $studentTable.='</table></td></tr></table>'."\n". - '<input type="button" value="Save" '. - 'onClick="javascript:checkSubmitPage(this.form,'.$question.');" />'. - '</form>'."\n"; + $studentTable.= + '</table>'."\n". + '<input type="button" value="'.&mt('Save').'" '. + 'onclick="javascript:checkSubmitPage(this.form,'.$question.');" />'. + '</form>'."\n"; $studentTable.=&show_grading_menu_form($symb); $request->print($studentTable); @@ -4277,6 +4999,7 @@ sub displaySubByDates { &Apache::loncommon::start_data_table_header_row(). '<th>'.&mt('Date/Time').'</th>'. ($isCODE?'<th>'.&mt('CODE').'</th>':''). + ($isTask?'<th>'.&mt('Version').'</th>':''). '<th>'.&mt('Submission').'</th>'. '<th>'.&mt('Status').'</th>'. &Apache::loncommon::end_data_table_header_row(); @@ -4285,17 +5008,21 @@ 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; + my %lastrndseed; for ($version=1;$version<=$$record{'version'};$version++) { my $timestamp = &Apache::lonlocal::locallocaltime($$record{$version.':timestamp'}); if (exists($$record{$version.':resource.0.version'})) { $interaction = $$record{$version.':resource.0.version'}; } - + if ($isTask && $env{'form.previousversion'}) { + next unless ($interaction == $env{'form.previousversion'}); + } my $where = ($isTask ? "$version:resource.$interaction" : "$version:resource"); $studentTable.=&Apache::loncommon::start_data_table_row(). @@ -4303,49 +5030,75 @@ sub displaySubByDates { if ($isCODE) { $studentTable.='<td>'.$record->{$version.':resource.CODE'}.'</td>'; } + if ($isTask) { + $studentTable.='<td>'.$interaction.'</td>'; + } my @versionKeys = split(/\:/,$$record{$version.':keys'}); my @displaySub = (); foreach my $partid (@{$parts}) { + my ($hidden,$type); + $type = $$record{$version.':resource.'.$partid.'.type'}; + if (($type eq 'anonsurvey') || ($type eq 'anonsurveycred')) { + $hidden = 1; + } my @matchKey = ($isTask ? sort(grep /^resource\.\d+\.\Q$partid\E\.award$/,@versionKeys) : sort(grep /^resource\.\Q$partid\E\..*?\.submission$/,@versionKeys)); - # next if ($$record{"$version:resource.$partid.solved"} eq ''); my $display_part=&get_display_part($partid,$symb); foreach my $matchKey (@matchKey) { if (exists($$record{$version.':'.$matchKey}) && $$record{$version.':'.$matchKey} ne '') { - + my ($responseId)= ($isTask ? ($matchKey=~ /^resource\.(.*?)\.\Q$partid\E\.award$/) : ($matchKey=~ /^resource\.\Q$partid\E\.(.*?)\.submission$/)); - $displaySub[0].='<b>'.&mt('Part:').'</b> '.$display_part.' '; - $displaySub[0].='<span class="LC_internal_info">('.&mt('ID').' '. - $responseId.')</span> <b>'; - if ($$record{"$where.$partid.tries"} eq '') { - $displaySub[0].=&mt('Trial not counted'); - } else { - $displaySub[0].=&mt('Trial [_1]', + $displaySub[0].='<span class="LC_nobreak">'; + $displaySub[0].='<b>'.&mt('Part: [_1]',$display_part).'</b>' + .' <span class="LC_internal_info">' + .'('.&mt('Response ID: [_1]',$responseId).')' + .'</span>' + .' <b>'; + if ($hidden) { + $displaySub[0].= &mt('Anonymous Survey').'</b>'; + } else { + my ($trial,$rndseed,$newvariation); + if ($type eq 'randomizetry') { + $trial = $$record{"$where.$partid.tries"}; + $rndseed = $$record{"$where.$partid.rndseed"}; + } + if ($$record{"$where.$partid.tries"} eq '') { + $displaySub[0].=&mt('Trial not counted'); + } else { + $displaySub[0].=&mt('Trial: [_1]', $$record{"$where.$partid.tries"}); - } - my $responseType=($isTask ? 'Task' + if ($rndseed || $lastrndseed{$partid}) { + if ($rndseed ne $lastrndseed{$partid}) { + $newvariation = ' ('.&mt('New variation this try').')'; + } + } + } + my $responseType=($isTask ? 'Task' : $responseType->{$partid}->{$responseId}); - if (!exists($orders{$partid})) { $orders{$partid}={}; } - if (!exists($orders{$partid}->{$responseId})) { - $orders{$partid}->{$responseId}= - &get_order($partid,$responseId,$symb,$uname,$udom); - } - $displaySub[0].='</b> '. - &cleanRecord($$record{$version.':'.$matchKey},$responseType,$symb,$partid,$responseId,$record,$orders{$partid}->{$responseId},"$version:",$uname,$udom).'<br />'; + if (!exists($orders{$partid})) { $orders{$partid}={}; } + if ((!exists($orders{$partid}->{$responseId})) || ($trial)) { + $orders{$partid}->{$responseId}= + &get_order($partid,$responseId,$symb,$uname,$udom, + $no_increment,$type,$trial,$rndseed); + } + $displaySub[0].='</b>'.$newvariation.'</span>'; # /nobreak + $displaySub[0].=' '. + &cleanRecord($$record{$version.':'.$matchKey},$responseType,$symb,$partid,$responseId,$record,$orders{$partid}->{$responseId},"$version:",$uname,$udom,$type,$trial,$rndseed).'<br />'; + } } } if (exists($$record{"$where.$partid.checkedin"})) { - $displaySub[1].='Checked in by '. - $$record{"$where.$partid.checkedin"}.' into slot '. - $$record{"$where.$partid.checkedin.slot"}. - '<br />'; + $displaySub[1].=&mt('Checked in by [_1] into slot [_2]', + $$record{"$where.$partid.checkedin"}, + $$record{"$where.$partid.checkedin.slot"}). + '<br />'; } if (exists $$record{"$where.$partid.award"}) { - $displaySub[1].='<b>Part:</b> '.$display_part.' '. + $displaySub[1].='<b>'.&mt('Part:').'</b> '.$display_part.' '. lc($$record{"$where.$partid.award"}).' '. $mark{$$record{"$where.$partid.solved"}}. '<br />'; @@ -4385,21 +5138,26 @@ 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); + my $navmap = Apache::lonnavmaps::navmap->new(); + unless (ref($navmap)) { + $request->print(&navmap_errormsg()); + return; + } 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; @@ -4407,16 +5165,18 @@ sub updateGradeByPage { my $iterator = $navmap->getIterator($map->map_start(), $map->map_finish()); - my $studentTable='<table border="0"><tr><td bgcolor="#777777">'. - '<table border="0"><tr bgcolor="#e6ffff">'. - '<td align="center"><b> Prob. </b></td>'. - '<td><b> Title </b></td>'. - '<td><b> Previous Score </b></td>'. - '<td><b> New Score </b></td></tr>'; + my $studentTable= + &Apache::loncommon::start_data_table(). + &Apache::loncommon::start_data_table_header_row(). + '<th align="center"> '.&mt('Prob.').' </th>'. + '<th> '.&mt('Title').' </th>'. + '<th> '.&mt('Previous Score').' </th>'. + '<th> '.&mt('New Score').' </th>'. + &Apache::loncommon::end_data_table_header_row(); $iterator->next(); # skip the first BEGIN_MAP my $curRes = $iterator->next(); # for "current resource" - my ($depth,$question,$prob,$changeflag)= (1,1,1,0); + my ($depth,$question,$prob,$changeflag,$hideflag)= (1,1,1,0,0); while ($depth > 0) { if($curRes == $iterator->BEGIN_MAP) { $depth++; } if($curRes == $iterator->END_MAP) { $depth--; } @@ -4425,14 +5185,24 @@ sub updateGradeByPage { my $parts = $curRes->parts(); my $title = $curRes->compTitle(); my $symbx = $curRes->symb(); - $studentTable.='<tr bgcolor="#ffffe6"><td align="center" valign="top" >'.$prob. - (scalar(@{$parts}) == 1 ? '' : '<br />('.scalar(@{$parts}).' parts)').'</td>'; + $studentTable.= + &Apache::loncommon::start_data_table_row(). + '<td align="center" valign="top" >'.$prob. + (scalar(@{$parts}) == 1 ? '' + : '<br />('.&mt('[quant,_1,part]',scalar(@{$parts})) + .')').'</td>'; $studentTable.='<td valign="top"> <b>'.$title.'</b> </td>'; my %newrecord=(); my @displayPts=(); my %aggregate = (); my $aggregateflag = 0; + if ($env{'form.HIDE'.$prob}) { + my %record = &Apache::lonnet::restore($symbx,$env{'request.course.id'},$udom,$uname); + my ($version,$parts) = split(/:/,$env{'form.HIDE'.$prob},2); + my $numchgs = &makehidden($version,$parts,\%record,$symbx,$udom,$uname,1); + $hideflag += $numchgs; + } foreach my $partid (@{$parts}) { my $newpts = $env{'form.GD_BOX'.$question.'_'.$partid}; my $oldpts = $env{'form.oldpts'.$question.'_'.$partid}; @@ -4470,10 +5240,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++; @@ -4513,19 +5283,22 @@ sub updateGradeByPage { $studentTable.='<td valign="top">'.$displayPts[0].'</td>'. '<td valign="top">'.$displayPts[1].'</td>'. - '</tr>'; + &Apache::loncommon::end_data_table_row(); $prob++; } $curRes = $iterator->next(); } - $studentTable.='</td></tr></table></td></tr></table>'; + $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.')); - $request->print($grademsg.$studentTable); + my $grademsg=($changeflag == 0 ? &mt('No score was changed or updated.') : + &mt('The scores were changed for [quant,_1,problem].', + $changeflag).'<br />'); + my $hidemsg=($hideflag == 0 ? '' : + &mt('Submissions were marked "hidden" for [quant,_1,transaction].', + $hideflag).'<br />'); + $request->print($hidemsg.$grademsg.$studentTable); return ''; } @@ -4534,7 +5307,7 @@ sub updateGradeByPage { # #------------------------------------------------------------------- -#--------------------Scantron Grading----------------------------------- +#-------------------- Bubblesheet (Scantron) Grading ------------------- # #------ start of section for handling grading by page/sequence --------- @@ -4548,35 +5321,35 @@ sub updateGradeByPage { from the file that we are parsing that represents one entire sheet 'bubble line' refers to the data - representing the line of bubbles that are on the physical bubble sheet + representing the line of bubbles that are on the physical bubblesheet -The overall process is that a scanned in bubble sheet data is uploaded +The overall process is that a scanned in bubblesheet data is uploaded into a course. When a user wants to grade, they select a -sequence/folder of resources, a file of bubble sheet info, and pick +sequence/folder of resources, a file of bubblesheet info, and pick one of the predefined configurations for what each scanline looks like. Next each scanline is checked for any errors of either 'missing bubbles' (it's an error because it may have been mis-scanned because too light bubbling), 'double bubble' (each bubble line should -have no more that one letter picked), invalid or duplicated CODE, -invalid student ID +have no more than one letter picked), invalid or duplicated CODE, +invalid student/employee ID If the CODE option is used that determines the randomization of the -homework problems, either way the student ID is looked up into a +homework problems, either way the student/employee ID is looked up into a username:domain. During the validation phase the instructor can choose to skip scanlines. -After the validation phase, there are now 3 bubble sheet files +After the validation phase, there are now 3 bubblesheet files scantron_original_filename (unmodified original file) scantron_corrected_filename (file where the corrected information has replaced the original information) scantron_skipped_filename (contains the exact text of scanlines that where skipped) Also there is a separate hash nohist_scantrondata that contains extra -correction information that isn't representable in the bubble sheet +correction information that isn't representable in the bubblesheet file (see &scantron_getfile() for more information) After all scanlines are either valid, marked as valid or skipped, then @@ -4612,14 +5385,19 @@ sub defaultFormData { Return html dropdown of possible sequences to grade Arguments: - $symb - $symb of the current resource + $symb - $symb of the current resource + $map_error - ref to scalar which will container error if + $navmap object is unavailable in &getSymbMap(). =cut sub getSequenceDropDown { - my ($symb)=@_; + my ($symb,$map_error)=@_; my $result='<select name="selectpage">'."\n"; - my ($titles,$symbx) = &getSymbMap(); + my ($titles,$symbx) = &getSymbMap($map_error); + if (ref($map_error)) { + return if ($$map_error); + } my ($curpage)=&Apache::lonnet::decode_symb($symb); my $ctr=0; foreach (@$titles) { @@ -4633,6 +5411,61 @@ sub getSequenceDropDown { return $result; } +my %bubble_lines_per_response; # no. bubble lines for each response. + # key is zero-based index - 0, 1, 2 ... + +my %first_bubble_line; # First bubble line no. for each bubble. + +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 + +my %masterseq_id_responsenum; # src_id (e.g., 12.3_0.11 etc.) for each + # numbered response. Needed when randomorder + # or randompick are in use. Key is ID, value + # is response number. + +# Save and restore the bubble lines array to the form env. + + +sub save_bubble_lines { + foreach my $line (keys(%bubble_lines_per_response)) { + $env{"form.scantron.bubblelines.$line"} = $bubble_lines_per_response{$line}; + $env{"form.scantron.first_bubble_line.$line"} = + $first_bubble_line{$line}; + $env{"form.scantron.sub_bubblelines.$line"} = + $subdivided_bubble_lines{$line}; + $env{"form.scantron.responsetype.$line"} = + $responsetype_per_response{$line}; + } + foreach my $resid (keys(%masterseq_id_responsenum)) { + my $line = $masterseq_id_responsenum{$resid}; + $env{"form.scantron.residpart.$line"} = $resid; + } +} + + +sub restore_bubble_lines { + my $line = 0; + %bubble_lines_per_response = (); + %masterseq_id_responsenum = (); + while ($env{"form.scantron.bubblelines.$line"}) { + my $value = $env{"form.scantron.bubblelines.$line"}; + $bubble_lines_per_response{$line} = $value; + $first_bubble_line{$line} = + $env{"form.scantron.first_bubble_line.$line"}; + $subdivided_bubble_lines{$line} = + $env{"form.scantron.sub_bubblelines.$line"}; + $responsetype_per_response{$line} = + $env{"form.scantron.responsetype.$line"}; + my $id = $env{"form.scantron.residpart.$line"}; + $masterseq_id_responsenum{$id} = $line; + $line++; + } +} =pod @@ -4645,14 +5478,17 @@ sub getSequenceDropDown { sub scantron_filenames { my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'}; my $cname=$env{'course.'.$env{'request.course.id'}.'.num'}; - my @files=&Apache::lonnet::dirlist('userfiles',$cdom,$cname, - &propath($cdom,$cname)); + my $getpropath = 1; + my ($dirlist,$listerror) = &Apache::lonnet::dirlist('userfiles',$cdom, + $cname,$getpropath); my @possiblenames; - foreach my $filename (sort(@files)) { - ($filename)=split(/&/,$filename); - if ($filename!~/^scantron_orig_/) { next ; } - $filename=~s/^scantron_orig_//; - push(@possiblenames,$filename); + if (ref($dirlist) eq 'ARRAY') { + foreach my $filename (sort(@{$dirlist})) { + ($filename)=split(/&/,$filename); + if ($filename!~/^scantron_orig_/) { next ; } + $filename=~s/^scantron_orig_//; + push(@possiblenames,$filename); + } } return @possiblenames; } @@ -4689,19 +5525,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 @@ -4734,11 +5627,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>'; @@ -4749,7 +5642,7 @@ sub scantron_CODEunique { =item scantron_selectphase - Generates the initial screen to start the bubble sheet process. + Generates the initial screen to start the bubblesheet process. Allows for - starting a grading run. - downloading existing scan data (original, corrected or skipped info) @@ -4766,7 +5659,12 @@ sub scantron_selectphase { my ($r,$file2grade) = @_; my ($symb)=&get_symb($r); if (!$symb) {return '';} - my $sequence_selector=&getSequenceDropDown($symb); + my $map_error; + my $sequence_selector=&getSequenceDropDown($symb,\$map_error); + if ($map_error) { + $r->print('<br />'.&navmap_errormsg().'<br />'); + return; + } my $default_form_data=&defaultFormData($symb); my $grading_menu_button=&show_grading_menu_form($symb); my $file_selector=&scantron_uploads($file2grade); @@ -4775,142 +5673,163 @@ sub scantron_selectphase { my $CODE_unique=&scantron_CODEunique(); my $result; - # Chunk of form to prompt for a file to grade and how: - - $result.= <<SCANTRONFORM; - <table width="100%" border="0"> - <tr> - <form method="post" enctype="multipart/form-data" action="/adm/grades" name="scantron_process"> - <td bgcolor="#777777"> - <input type="hidden" name="command" value="scantron_warning" /> - $default_form_data - <table width="100%" border="0"> - <tr bgcolor="#e6ffff"> - <td colspan="2"> - <b>Specify file and which Folder/Sequence to grade</b> - </td> - </tr> - <tr bgcolor="#ffffe6"> - <td> Sequence to grade: </td><td> $sequence_selector </td> - </tr> - <tr bgcolor="#ffffe6"> - <td> Filename of scoring office file: </td><td> $file_selector </td> - </tr> - <tr bgcolor="#ffffe6"> - <td> Format of data file: </td><td> $format_selector </td> - </tr> - <tr bgcolor="#ffffe6"> - <td> Saved CODEs to validate against: </td><td> $CODE_selector</td> - </tr> - <tr bgcolor="#ffffe6"> - <td> Each CODE is only to be used once:</td><td> $CODE_unique </td> - </tr> - <tr bgcolor="#ffffe6"> - <td> Options: </td> - <td> - <label><input type="checkbox" name="scantron_options_redo" value="redo_skipped"/> Do only previously skipped records</label> <br /> - <label><input type="checkbox" name="scantron_options_ignore" value="ignore_corrections"/> Remove all existing corrections</label> <br /> - <label><input type="checkbox" name="scantron_options_hidden" value="ignore_hidden"/> Skip hidden resources when grading</label> - </td> - </tr> - <tr bgcolor="#ffffe6"> - <td colspan="2"> - <input type="submit" value="Grading: Validate Scantron Records" /> - </td> - </tr> - </table> - </td> - </form> - </tr> -SCANTRONFORM - - $r->print($result); + $ssi_error = 0; if (&Apache::lonnet::allowed('usc',$env{'request.role.domain'}) || &Apache::lonnet::allowed('usc',$env{'request.course.id'})) { - # Chunk of form to prompt for a scantron file upload. + # Chunk of form to prompt for a scantron file upload. - $r->print(<<SCANTRONFORM); - <tr> - <td bgcolor="#777777"> - <table width="100%" border="0"> - <tr bgcolor="#e6ffff"> + $r->print(' + <br /> + '.&Apache::loncommon::start_data_table('LC_scantron_action').' + '.&Apache::loncommon::start_data_table_header_row().' + <th> + '.&mt('Specify a bubblesheet data file to upload.').' + </th> + '.&Apache::loncommon::end_data_table_header_row().' + '.&Apache::loncommon::start_data_table_row().' <td> - <b>Specify a Scantron data file to upload.</b> - </td> - </tr> - <tr bgcolor="#ffffe6"> - <td> -SCANTRONFORM +'); my $default_form_data=&defaultFormData(&get_symb($r,1)); my $cdom= $env{'course.'.$env{'request.course.id'}.'.domain'}; my $cnum= $env{'course.'.$env{'request.course.id'}.'.num'}; - $r->print(<<UPLOAD); + $r->print(' <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."); - return false; - } - formname.submit(); + if (formname.upfile.value == "") { + alert("'.&mt('Please use the browse button to select a file from your local directory.').'"); + return false; + } + formname.submit(); } </script> - <form enctype='multipart/form-data' action='/adm/grades' name='rules' method='post'> - $default_form_data - <input name='courseid' type='hidden' value='$cnum' /> - <input name='domainid' type='hidden' value='$cdom' /> - <input name='command' value='scantronupload_save' type='hidden' /> - File to upload:<input type="file" name="upfile" size="50" /> + <form enctype="multipart/form-data" action="/adm/grades" name="rules" method="post"> + '.$default_form_data.' + <input name="courseid" type="hidden" value="'.$cnum.'" /> + <input name="domainid" type="hidden" value="'.$cdom.'" /> + <input name="command" value="scantronupload_save" type="hidden" /> + '.&mt('File to upload: [_1]','<input type="file" name="upfile" size="50" />').' <br /> - <input type="button" onClick="javascript:checkUpload(this.form);" value="Upload Scantron Data" /> + <input type="button" onclick="javascript:checkUpload(this.form);" value="'.&mt('Upload Bubblesheet Data').'" /> </form> -UPLOAD +'); - $r->print(<<SCANTRONFORM); + $r->print(' </td> - </tr> - </table> - </td> - </tr> -SCANTRONFORM + '.&Apache::loncommon::end_data_table_row().' + '.&Apache::loncommon::end_data_table().' +'); } + # Chunk of form to prompt for a file to grade and how: + + $result.= ' + <br /> + <form method="post" enctype="multipart/form-data" action="/adm/grades" name="scantron_process"> + <input type="hidden" name="command" value="scantron_warning" /> + '.$default_form_data.' + '.&Apache::loncommon::start_data_table('LC_scantron_action').' + '.&Apache::loncommon::start_data_table_header_row().' + <th colspan="2"> + '.&mt('Specify file and which Folder/Sequence to grade').' + </th> + '.&Apache::loncommon::end_data_table_header_row().' + '.&Apache::loncommon::start_data_table_row().' + <td> '.&mt('Sequence to grade:').' </td><td> '.$sequence_selector.' </td> + '.&Apache::loncommon::end_data_table_row().' + '.&Apache::loncommon::start_data_table_row().' + <td> '.&mt('Filename of bubblesheet data file:').' </td><td> '.$file_selector.' </td> + '.&Apache::loncommon::end_data_table_row().' + '.&Apache::loncommon::start_data_table_row().' + <td> '.&mt('Format of bubblesheet data file:').' </td><td> '.$format_selector.' </td> + '.&Apache::loncommon::end_data_table_row().' + '.&Apache::loncommon::start_data_table_row().' + <td> '.&mt('Saved CODEs to validate against:').' </td><td> '.$CODE_selector.' </td> + '.&Apache::loncommon::end_data_table_row().' + '.&Apache::loncommon::start_data_table_row().' + <td> '.&mt('Each CODE is only to be used once:').'</td><td> '.$CODE_unique.' </td> + '.&Apache::loncommon::end_data_table_row().' + '.&Apache::loncommon::start_data_table_row().' + <td> '.&mt('Options:').' </td> + <td> + <label><input type="checkbox" name="scantron_options_redo" value="redo_skipped"/> '.&mt('Do only previously skipped records').'</label> <br /> + <label><input type="checkbox" name="scantron_options_ignore" value="ignore_corrections"/> '.&mt('Remove all existing corrections').'</label> <br /> + <label><input type="checkbox" name="scantron_options_hidden" value="ignore_hidden"/> '.&mt('Skip hidden resources when grading').'</label> + </td> + '.&Apache::loncommon::end_data_table_row().' + '.&Apache::loncommon::start_data_table_row().' + <td colspan="2"> + <input type="submit" value="'.&mt('Grading: Validate Bubblesheet Records').'" /> + </td> + '.&Apache::loncommon::end_data_table_row().' + '.&Apache::loncommon::end_data_table().' + </form> +'; + + $r->print($result); + # Chunk of the form that prompts to view a scoring office file, # corrected file, skipped records in a file. - $r->print(<<SCANTRONFORM); - <tr> - <form action='/adm/grades' name='scantron_download'> - <td bgcolor="#777777"> - $default_form_data - <input type="hidden" name="command" value="scantron_download" /> - <table width="100%" border="0"> - <tr bgcolor="#e6ffff"> - <td colspan="2"> - <b>Download a scoring office file</b> - </td> - </tr> - <tr bgcolor="#ffffe6"> - <td> Filename of scoring office file: </td><td> $file_selector </td> - </tr> - <tr bgcolor="#ffffe6"> - <td colspan="2"> - <input type="submit" value="Download: Show List of Associated Files" /> - </td> - </tr> - </table> - </td> - </form> - </tr> -SCANTRONFORM + $r->print(' + <br /> + <form action="/adm/grades" name="scantron_download"> + '.$default_form_data.' + <input type="hidden" name="command" value="scantron_download" /> + '.&Apache::loncommon::start_data_table('LC_scantron_action').' + '.&Apache::loncommon::start_data_table_header_row().' + <th> + '.&mt('Download a scoring office file').' + </th> + '.&Apache::loncommon::end_data_table_header_row().' + '.&Apache::loncommon::start_data_table_row().' + <td> '.&mt('Filename of scoring office file: [_1]',$file_selector).' + <br /> + <input type="submit" value="'.&mt('Download: Show List of Associated Files').'" /> + '.&Apache::loncommon::end_data_table_row().' + '.&Apache::loncommon::end_data_table().' + </form> + <br /> +'); - $r->print('<tr><td bgcolor="#777777">'); &Apache::lonpickcode::code_list($r,2); - $r->print('</td></tr></table>'); + + $r->print('<br /><form method="post" name="checkscantron" action="">'. + $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 bubblesheet 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> '.&mt('Options').' </td>'."\n". + '<td> <label><input type="checkbox" name="scantron_options_hidden" value="ignore_hidden"/> '.&mt('Skip hidden resources').'</label></td>'. + &Apache::loncommon::end_data_table_row()."\n". + &Apache::loncommon::start_data_table_row()."\n". + '<td colspan="2">'."\n". + '<input type="hidden" name="command" value="checksubmissions" />'."\n". + '<input type="submit" value="'.&mt('Review Bubblesheet 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 @@ -4945,8 +5864,8 @@ SCANTRONFORM CODEstart - (only matter if a CODE exists) column in the line where the CODE starts CODElength - length of the CODE - IDstart - column where the student ID number starts - IDlength - length of the student ID info + IDstart - column where the student/employee ID starts + IDlength - length of the student/employee ID info Qstart - column where the information from the bubbled 'questions' start Qlength - number of columns comprising a single bubble line from @@ -4967,15 +5886,17 @@ SCANTRONFORM LastName - column that the last name starts in LastNameLength - number of columns that the last name spans + BubblesPerRow - number of bubbles available in each row used to + bubble an answer. (If not specified, 10 assumed). =cut 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); @@ -4988,7 +5909,7 @@ sub get_scantron_config { $config{'IDstart'}=$config[5]; $config{'IDlength'}=$config[6]; $config{'Qstart'}=$config[7]; - $config{'Qlength'}=$config[8]; + $config{'Qlength'}=$config[8]; $config{'Qoff'}=$config[9]; $config{'Qon'}=$config[10]; $config{'PaperID'}=$config[11]; @@ -4997,6 +5918,7 @@ sub get_scantron_config { $config{'FirstNamelength'}=$config[14]; $config{'LastName'}=$config[15]; $config{'LastNamelength'}=$config[16]; + $config{'BubblesPerRow'}=$config[17]; last; } return %config; @@ -5006,7 +5928,7 @@ sub get_scantron_config { =item username_to_idmap - creates a hash keyed by student id with values of the corresponding + creates a hash keyed by student/employee ID with values of the corresponding student username:domain. Arguments: @@ -5025,8 +5947,17 @@ sub username_to_idmap { my ($classlist)= @_; my %idmap; foreach my $student (keys(%$classlist)) { - $idmap{$classlist->{$student}->[&Apache::loncoursedata::CL_ID]}= - $student; + my $id = $classlist->{$student}->[&Apache::loncoursedata::CL_ID]; + unless ($id eq '') { + if (!exists($idmap{$id})) { + $idmap{$id} = $student; + } else { + my $status = $classlist->{$student}->[&Apache::loncoursedata::CL_STATUS]; + if ($status eq 'Active') { + $idmap{$id} = $student; + } + } + } } return %idmap; } @@ -5045,7 +5976,7 @@ sub username_to_idmap { $whichline - line number of the passed in scanline $field - type of change to process (either - 'ID' -> correct the student ID number + 'ID' -> correct the student/employee ID 'CODE' -> correct the CODE 'answer' -> fixup the submitted answers) @@ -5062,6 +5993,8 @@ sub username_to_idmap { - 'answer' 'response' - new answer or 'none' if blank 'question' - the bubble line to change + 'questionnum' - the question identifier, + may include subquestion. Returns: $line - the modified scanline @@ -5074,7 +6007,6 @@ sub username_to_idmap { sub scantron_fixup_scanline { my ($scantron_config,$scan_data,$line,$whichline,$field,$args)=@_; - if ($field eq 'ID') { if (length($args->{'newid'}) > $$scantron_config{'IDlength'}) { return ($line,1,'New value too large'); @@ -5111,7 +6043,7 @@ sub scantron_fixup_scanline { my $answer=${off}x$length; if ($args->{'response'} eq 'none') { &scan_data($scan_data, - "$whichline.no_bubble.".$args->{'question'},'1'); + "$whichline.no_bubble.".$args->{'questionnum'},'1'); } else { if ($on eq 'letter') { my @alphabet=('A'..'Z'); @@ -5123,7 +6055,7 @@ sub scantron_fixup_scanline { substr($answer,$args->{'response'},1)=$on; } &scan_data($scan_data, - "$whichline.no_bubble.".$args->{'question'},undef,'1'); + "$whichline.no_bubble.".$args->{'questionnum'},undef,'1'); } my $where=$length*($args->{'question'}-1)+$scantron_config->{'Qstart'}; substr($line,$where-1,$length)=$answer; @@ -5160,6 +6092,39 @@ sub scan_data { return $scan_data->{$filename.'_'.$key}; } +# ----- These first few routines are general use routines.---- + +# Return the number of occurences of a pattern in a string. + +sub occurence_count { + my ($string, $pattern) = @_; + + my @matches = ($string =~ /$pattern/g); + + return scalar(@matches); +} + + +# Take a string known to have digits and convert all the +# digits into letters in the range J,A..I. + +sub digits_to_letters { + my ($input) = @_; + + my @alphabet = ('J', 'A'..'I'); + + my @input = split(//, $input); + my $output =''; + for (my $i = 0; $i < scalar(@input); $i++) { + if ($input[$i] =~ /\d/) { + $output .= $alphabet[$input[$i]]; + } else { + $output .= $input[$i]; + } + } + return $output; +} + =pod =item scantron_parse_scanline @@ -5174,6 +6139,27 @@ sub scan_data { (see scantron_getfile for more information) just_header - True if should not process question answers but only the stuff to the left of the answers. + randomorder - True if randomorder in use + randompick - True if randompick in use + sequence - Exam folder URL + master_seq - Ref to array containing symbs in exam folder + symb_to_resource - Ref to hash of symbs for resources in exam folder + (corresponding values are resource objects) + partids_by_symb - Ref to hash of symb -> array ref of partIDs + orderedforcode - Ref to hash of arrays. keys are CODEs and values + are refs to an array of resource objects, ordered + according to order used for CODE, when randomorder + and or randompick are in use. + respnumlookup - Ref to hash mapping question numbers in bubble lines + for current line to question number used for same question + in "Master Sequence" (as seen by Course Coordinator). + startline - Ref to hash where key is question number (0 is first) + and value is number of first bubble line for current + student or code-based randompick and/or randomorder. + totalref - Ref of scalar used to score total number of bubble + lines needed for responses in a scan line (used when + randompick in use. + Returns: Hash containing the result of parsing the scanline @@ -5185,7 +6171,7 @@ sub scan_data { CODE_ignore_dup - 1 if the CODE is a duplicated use when unique CODEs were selected, but the usage has been forced by the operator - ID - student ID + ID - student/employee ID PaperID - if used, the ID number printed on the sheet when the paper was scanned FirstName - first name from the sheet @@ -5218,10 +6204,12 @@ sub scan_data { =cut sub scantron_parse_scanline { - my ($line,$whichline,$scantron_config,$scan_data,$just_header)=@_; + my ($line,$whichline,$scantron_config,$scan_data,$just_header,$idmap, + $randomorder,$randompick,$sequence,$master_seq,$symb_to_resource, + $partids_by_symb,$orderedforcode,$respnumlookup,$startline,$totalref)=@_; + my %record; - my $questions=substr($line,$$scantron_config{'Qstart'}-1); # Answers - my $data=substr($line,0,$$scantron_config{'Qstart'}-1); # earlier stuff + my $data=substr($line,0,$$scantron_config{'Qstart'}-1); # stuff before answers if (!($$scantron_config{'CODElocation'} eq 0 || $$scantron_config{'CODElocation'} eq 'none')) { if ($$scantron_config{'CODElocation'} < 0 || @@ -5257,166 +6245,317 @@ sub scantron_parse_scanline { my $questnum=0; my $ansnum =1; # Multiple 'answer lines'/question. - while ($questions) { - my $answers_needed = $bubble_lines_per_response{$questnum}; - my $answer_length = $$scantron_config{'Qlength'} * $answers_needed; - - - - $questnum++; - my $currentquest = substr($questions,0,$answer_length); - $questions = substr($questions,0,$answer_length)=''; - if (length($currentquest) < $answer_length) { next; } - - # Qon letter implies for each slot in currentquest we have: - # ? or * for doubles a letter in A-Z for a bubble and - # about anything else (esp. a value of Qoff for missing - # bubbles. - - - if ($$scantron_config{'Qon'} eq 'letter') { - - if ($currentquest =~ /\?/ - || $currentquest =~ /\*/ - || (&occurence_count($currentquest, "[A-Z]") > 1)) { - push(@{$record{'scantron.doubleerror'}},$questnum); - for (my $ans = 0; $ans < $answers_needed; $ans++) { - my $bubble = substr($currentquest, $ans, 1); - if ($bubble =~ /[A-Z]/ ) { - $record{"scantron.$ansnum.answer"} = $bubble; - } else { - $record{"scantron.$ansnum.answer"}=''; - } - $ansnum++; - } - - } elsif (!defined($currentquest) - || (&occurence_count($currentquest, $$scantron_config{'Qoff'}) == length($currentquest)) - || (&occurence_count($currentquest, "[A-Z]") == 0)) { - for (my $ans = 0; $ans < $answers_needed; $ans++ ) { - $record{"scantron.$ansnum.answer"}=''; - $ansnum++; - - } - if (!&scan_data($scan_data,"$whichline.no_bubble.$questnum")) { - push(@{$record{"scantron.missingerror"}},$questnum); - $ansnum += $answers_needed; - } - - } else { - for (my $ans = 0; $ans < $answers_needed; $ans++) { - $record{"scantron.$ansnum.answer"} = substr($currentquest, $ans, 1); - $ansnum++; - } - } + my $lastpos = $env{'form.scantron_maxbubble'}*$$scantron_config{'Qlength'}; + if ($randompick || $randomorder) { + my $total = &get_respnum_lookups($sequence,$scan_data,$idmap,$line,\%record, + $master_seq,$symb_to_resource, + $partids_by_symb,$orderedforcode, + $respnumlookup,$startline); + if ($total) { + $lastpos = $total*$$scantron_config{'Qlength'}; + } + if (ref($totalref)) { + $$totalref = $total; + } + } + my $questions=substr($line,$$scantron_config{'Qstart'}-1,$lastpos); # Answers + chomp($questions); # Get rid of any trailing \n. + $questions =~ s/\r$//; # Get rid of trailing \r too (MAC or Win uploads). + while (length($questions)) { + my $answers_needed; + if (($randompick || $randomorder) && (ref($respnumlookup) eq 'HASH')) { + $answers_needed = $bubble_lines_per_response{$respnumlookup->{$questnum}}; + } else { + $answers_needed = $bubble_lines_per_response{$questnum}; + } + my $answer_length = ($$scantron_config{'Qlength'} * $answers_needed) + || 1; + $questnum++; + my $quest_id = $questnum; + my $currentquest = substr($questions,0,$answer_length); + $questions = substr($questions,$answer_length); + if (length($currentquest) < $answer_length) { next; } + + my $subdivided; + if (($randompick || $randomorder) && (ref($respnumlookup) eq 'HASH')) { + $subdivided = $subdivided_bubble_lines{$respnumlookup->{$questnum-1}}; + } else { + $subdivided = $subdivided_bubble_lines{$questnum-1}; + } + if ($subdivided =~ /,/) { + my $subquestnum = 1; + my $subquestions = $currentquest; + my @subanswers_needed = split(/,/,$subdivided); + foreach my $subans (@subanswers_needed) { + my $subans_length = + ($$scantron_config{'Qlength'} * $subans) || 1; + my $currsubquest = substr($subquestions,0,$subans_length); + $subquestions = substr($subquestions,$subans_length); + $quest_id = "$questnum.$subquestnum"; + if (($$scantron_config{'Qon'} eq 'letter') || + ($$scantron_config{'Qon'} eq 'number')) { + $ansnum = &scantron_validator_lettnum($ansnum, + $questnum,$quest_id,$subans,$currsubquest,$whichline, + \@alphabet,\%record,$scantron_config,$scan_data, + $randomorder,$randompick,$respnumlookup); + } else { + $ansnum = &scantron_validator_positional($ansnum, + $questnum,$quest_id,$subans,$currsubquest,$whichline, + \@alphabet,\%record,$scantron_config,$scan_data, + $randomorder,$randompick,$respnumlookup); + } + $subquestnum ++; + } + } else { + if (($$scantron_config{'Qon'} eq 'letter') || + ($$scantron_config{'Qon'} eq 'number')) { + $ansnum = &scantron_validator_lettnum($ansnum,$questnum, + $quest_id,$answers_needed,$currentquest,$whichline, + \@alphabet,\%record,$scantron_config,$scan_data, + $randomorder,$randompick,$respnumlookup); + } else { + $ansnum = &scantron_validator_positional($ansnum,$questnum, + $quest_id,$answers_needed,$currentquest,$whichline, + \@alphabet,\%record,$scantron_config,$scan_data, + $randomorder,$randompick,$respnumlookup); + } + } + } + $record{'scantron.maxquest'}=$questnum; + return \%record; +} - # Qon 'number' implies each slot gives a digit that indexes the - # the bubbles filled or Qoff or a non number for unbubbled lines. - # and *? for double bubbles on a line. - # these answers are also stored as letters. - - } elsif ($$scantron_config{'Qon'} eq 'number') { - if ($currentquest =~ /\?/ - || $currentquest =~ /\*/ - || (&occurence_count($currentquest, '\d') > 1)) { - push(@{$record{'scantron.doubleerror'}},$questnum); - for (my $ans = 0; $ans < $answers_needed; $ans++) { - my $bubble = substr($currentquest, $ans, 1); - if ($bubble =~ /\d/) { - $record{"scantron.$ansnum.answer"} = $alphabet[$bubble]; - } else { - $record{"scantron.$ansnum.answer"}=' '; - } - $ansnum++; - } +sub get_master_seq { + my ($resources,$master_seq,$symb_to_resource) = @_; + return unless ((ref($resources) eq 'ARRAY') && (ref($master_seq) eq 'ARRAY') && + (ref($symb_to_resource) eq 'HASH')); + my $resource_error; + foreach my $resource (@{$resources}) { + my $ressymb; + if (ref($resource)) { + $ressymb = $resource->symb(); + push(@{$master_seq},$ressymb); + $symb_to_resource->{$ressymb} = $resource; + } else { + $resource_error = 1; + last; + } + } + return $resource_error; +} - } elsif (!defined($currentquest) - || (&occurence_count($currentquest,$$scantron_config{'Qoff'}) == length($currentquest)) - || (&occurence_count($currentquest, '\d') == 0)) { - for (my $ans = 0; $ans < $answers_needed; $ans++ ) { - $record{"scantron.$ansnum.answer"}=''; - $ansnum++; +sub get_respnum_lookups { + my ($sequence,$scan_data,$idmap,$line,$record,$master_seq,$symb_to_resource, + $partids_by_symb,$orderedforcode,$respnumlookup,$startline) = @_; + return unless ((ref($record) eq 'HASH') && (ref($master_seq) eq 'ARRAY') && + (ref($symb_to_resource) eq 'HASH') && (ref($partids_by_symb) eq 'HASH') && + (ref($orderedforcode) eq 'HASH') && (ref($respnumlookup) eq 'HASH') && + (ref($startline) eq 'HASH')); + my ($user,$scancode); + if ((exists($record->{'scantron.CODE'})) && + (&Apache::lonnet::validCODE($record->{'scantron.CODE'}))) { + $scancode = $record->{'scantron.CODE'}; + } else { + $user = &scantron_find_student($record,$scan_data,$idmap,$line); + } + my @mapresources = + &users_order($user,$scancode,$sequence,$master_seq,$symb_to_resource, + $orderedforcode); + my $total = 0; + my $count = 0; + foreach my $resource (@mapresources) { + my $id = $resource->id(); + my $symb = $resource->symb(); + if (ref($partids_by_symb->{$symb}) eq 'ARRAY') { + foreach my $partid (@{$partids_by_symb->{$symb}}) { + my $respnum = $masterseq_id_responsenum{$id.'_'.$partid}; + if ($respnum ne '') { + $respnumlookup->{$count} = $respnum; + $startline->{$count} = $total; + $total += $bubble_lines_per_response{$respnum}; + $count ++; + } + } + } + } + return $total; +} - } - if (!&scan_data($scan_data,"$whichline.no_bubble.$questnum")) { - push(@{$record{"scantron.missingerror"}},$questnum); - $ansnum += $answers_needed; - } +sub scantron_validator_lettnum { + my ($ansnum,$questnum,$quest_id,$answers_needed,$currquest,$whichline, + $alphabet,$record,$scantron_config,$scan_data,$randomorder, + $randompick,$respnumlookup) = @_; + + # Qon 'letter' implies for each slot in currquest we have: + # ? or * for doubles, a letter in A-Z for a bubble, and + # about anything else (esp. a value of Qoff) for missing + # bubbles. + # + # Qon 'number' implies each slot gives a digit that indexes the + # bubbles filled, or Qoff, or a non-number for unbubbled lines, + # and * or ? for double bubbles on a single line. + # - } else { - $currentquest = &digits_to_letters($currentquest); - for (my $ans =0; $ans < $answers_needed; $ans++) { - $record{"scantron.$ansnum.answer"} = substr($currentquest, $ans, 1); - $ansnum++; - } - } - } else { + my $matchon; + if ($$scantron_config{'Qon'} eq 'letter') { + $matchon = '[A-Z]'; + } elsif ($$scantron_config{'Qon'} eq 'number') { + $matchon = '\d'; + } + my $occurrences = 0; + my $responsenum = $questnum-1; + if (($randompick || $randomorder) && (ref($respnumlookup) eq 'HASH')) { + $responsenum = $respnumlookup->{$questnum-1} + } + if (($responsetype_per_response{$responsenum} eq 'essayresponse') || + ($responsetype_per_response{$responsenum} eq 'formularesponse') || + ($responsetype_per_response{$responsenum} eq 'stringresponse') || + ($responsetype_per_response{$responsenum} eq 'imageresponse') || + ($responsetype_per_response{$responsenum} eq 'reactionresponse') || + ($responsetype_per_response{$responsenum} eq 'organicresponse')) { + my @singlelines = split('',$currquest); + foreach my $entry (@singlelines) { + $occurrences = &occurence_count($entry,$matchon); + if ($occurrences > 1) { + last; + } + } + } else { + $occurrences = &occurence_count($currquest,$matchon); + } + if (($currquest =~ /\?/ || $currquest =~ /\*/) || ($occurrences > 1)) { + push(@{$record->{'scantron.doubleerror'}},$quest_id); + for (my $ans=0; $ans<$answers_needed; $ans++) { + my $bubble = substr($currquest,$ans,1); + if ($bubble =~ /$matchon/ ) { + if ($$scantron_config{'Qon'} eq 'number') { + if ($bubble == 0) { + $bubble = 10; + } + $record->{"scantron.$ansnum.answer"} = + $alphabet->[$bubble-1]; + } else { + $record->{"scantron.$ansnum.answer"} = $bubble; + } + } else { + $record->{"scantron.$ansnum.answer"}=''; + } + $ansnum++; + } + } elsif (!defined($currquest) + || (&occurence_count($currquest, $$scantron_config{'Qoff'}) == length($currquest)) + || (&occurence_count($currquest,$matchon) == 0)) { + for (my $ans=0; $ans<$answers_needed; $ans++ ) { + $record->{"scantron.$ansnum.answer"}=''; + $ansnum++; + } + if (!&scan_data($scan_data,"$whichline.no_bubble.$quest_id")) { + push(@{$record->{'scantron.missingerror'}},$quest_id); + } + } else { + if ($$scantron_config{'Qon'} eq 'number') { + $currquest = &digits_to_letters($currquest); + } + for (my $ans=0; $ans<$answers_needed; $ans++) { + my $bubble = substr($currquest,$ans,1); + $record->{"scantron.$ansnum.answer"} = $bubble; + $ansnum++; + } + } + return $ansnum; +} - # Otherwise there's a positional notation; - # each bubble line requires Qlength items, and there are filled in - # bubbles for each case where there 'Qon' characters. - # +sub scantron_validator_positional { + my ($ansnum,$questnum,$quest_id,$answers_needed,$currquest, + $whichline,$alphabet,$record,$scantron_config,$scan_data, + $randomorder,$randompick,$respnumlookup) = @_; - my @array=split($$scantron_config{'Qon'},$currentquest,-1); + # Otherwise there's a positional notation; + # each bubble line requires Qlength items, and there are filled in + # bubbles for each case where there 'Qon' characters. + # - # If the split only giveas us one element.. the full length of the - # answser string, no bubbles are filled in: + my @array=split($$scantron_config{'Qon'},$currquest,-1); - if (length($array[0]) eq $$scantron_config{'Qlength'}*$answers_needed) { - for (my $ans = 0; $ans < $answers_needed; $ans++ ) { - $record{"scantron.$ansnum.answer"}=''; - $ansnum++; + # If the split only gives us one element.. the full length of the + # answer string, no bubbles are filled in: - } - if (!&scan_data($scan_data,"$whichline.no_bubble.$questnum")) { - push(@{$record{"scantron.missingerror"}},$questnum); - } - } elsif (scalar(@array) lt 2) { + if ($answers_needed eq '') { + return; + } - my $location = length($array[0]); - my $line_num = $location / $$scantron_config{'Qlength'}; - my $bubble = $alphabet[$location % $$scantron_config{'Qlength'}]; + if (length($array[0]) eq $$scantron_config{'Qlength'}*$answers_needed) { + for (my $ans=0; $ans<$answers_needed; $ans++ ) { + $record->{"scantron.$ansnum.answer"}=''; + $ansnum++; + } + if (!&scan_data($scan_data,"$whichline.no_bubble.$quest_id")) { + push(@{$record->{"scantron.missingerror"}},$quest_id); + } + } elsif (scalar(@array) == 2) { + my $location = length($array[0]); + my $line_num = int($location / $$scantron_config{'Qlength'}); + my $bubble = $alphabet->[$location % $$scantron_config{'Qlength'}]; + for (my $ans=0; $ans<$answers_needed; $ans++) { + if ($ans eq $line_num) { + $record->{"scantron.$ansnum.answer"} = $bubble; + } else { + $record->{"scantron.$ansnum.answer"} = ' '; + } + $ansnum++; + } + } else { + # If there's more than one instance of a bubble character + # That's a double bubble; with positional notation we can + # record all the bubbles filled in as well as the + # fact this response consists of multiple bubbles. + # + my $responsenum = $questnum-1; + if (($randompick || $randomorder) && (ref($respnumlookup) eq 'HASH')) { + $responsenum = $respnumlookup->{$questnum-1} + } + if (($responsetype_per_response{$responsenum} eq 'essayresponse') || + ($responsetype_per_response{$responsenum} eq 'formularesponse') || + ($responsetype_per_response{$responsenum} eq 'stringresponse') || + ($responsetype_per_response{$responsenum} eq 'imageresponse') || + ($responsetype_per_response{$responsenum} eq 'reactionresponse') || + ($responsetype_per_response{$responsenum} eq 'organicresponse')) { + my $doubleerror = 0; + while (($currquest >= $$scantron_config{'Qlength'}) && + (!$doubleerror)) { + my $currline = substr($currquest,0,$$scantron_config{'Qlength'}); + $currquest = substr($currquest,$$scantron_config{'Qlength'}); + my @currarray = split($$scantron_config{'Qon'},$currline,-1); + if (length(@currarray) > 2) { + $doubleerror = 1; + } + } + if ($doubleerror) { + push(@{$record->{'scantron.doubleerror'}},$quest_id); + } + } else { + push(@{$record->{'scantron.doubleerror'}},$quest_id); + } + my $item = $ansnum; + for (my $ans=0; $ans<$answers_needed; $ans++) { + $record->{"scantron.$item.answer"} = ''; + $item ++; + } - for (my $ans = 0; $ans < $answers_needed; $ans++) { - if ($ans eq $line_num) { - $record{"scantron.$ansnum.answer"} = $bubble; - } else { - $record{"scantron.$ansnum.answer"} = ' '; - } - $ansnum++; - } - } - # If there's more than one instance of a bubble character - # That's a double bubble; with positional notation we can - # record all the bubbles filled in as well as the - # fact this response consists of multiple bubbles. - # - else { - push(@{$record{'scantron.doubleerror'}},$questnum); - - my $first_answer = $ansnum; - for (my $ans =0; $ans < $answers_needed; $ans++) { - my $item = $first_answer+$ans; - $record{"scantron.$item.answer"} = ''; - } - - my @ans=@array; - my $i=0; - my $increment = 0; - while ($#ans) { - $i+=length($ans[0]) + $increment; - my $line = int($i/$$scantron_config{'Qlength'} + $first_answer); - my $bubble = $i%$$scantron_config{'Qlength'}; - $record{"scantron.$line.answer"}.=$alphabet[$bubble]; - shift(@ans); - $increment = 1; - } - $ansnum += $answers_needed; - } - } + my @ans=@array; + my $i=0; + my $increment = 0; + while ($#ans) { + $i+=length($ans[0]) + $increment; + my $line = int($i/$$scantron_config{'Qlength'} + $ansnum); + my $bubble = $i%$$scantron_config{'Qlength'}; + $record->{"scantron.$line.answer"}.=$alphabet->[$bubble]; + shift(@ans); + $increment = 1; + } + $ansnum += $answers_needed; } - $record{'scantron.maxquest'}=$questnum; - return \%record; + return $ansnum; } =pod @@ -5556,12 +6695,17 @@ sub scantron_process_corrections { &scantron_fixup_scanline(\%scantron_config,$scan_data,$line, $which,'answer', { 'question'=>$question, - 'response'=>$env{"form.scantron_correct_Q_$question"}}); + 'response'=>$env{"form.scantron_correct_Q_$question"}, + 'questionnum'=>$env{"form.scantron_questionnum_Q_$question"}}); if ($err) { last; } } } if ($err) { - $r->print("<span class=\"LC_warning\">Unable to accept last correction, an error occurred :$errmsg:</span>"); + $r->print( + '<p class="LC_error">' + .&mt('Unable to accept last correction, an error occurred: [_1]', + $errmsg) + .'</p>'); } else { &scantron_put_line($scanlines,$scan_data,$which,$line,$skip); &scantron_putfile($scanlines,$scan_data); @@ -5653,7 +6797,7 @@ sub remember_current_skipped { =item check_for_error Checks if there was an error when attempting to remove a specific - scantron_.. bubble sheet data file. Prints out an error if + scantron_.. bubblesheet data file. Prints out an error if something went wrong. =cut @@ -5661,7 +6805,7 @@ sub remember_current_skipped { sub check_for_error { my ($r,$result)=@_; if ($result ne 'ok' && $result ne 'not_found' ) { - $r->print("An error occurred ($result) when trying to Remove the existing corrections."); + $r->print(&mt("An error occurred ([_1]) when trying to remove the existing corrections.",$result)); } } @@ -5683,27 +6827,33 @@ sub scantron_warning_screen { $scantron_config{'CODEstart'} && $scantron_config{'CODElength'}) { $CODElist=$env{'form.scantron_CODElist'}; - if ($env{'form.scantron_CODElist'} eq '') { $CODElist='<span class="LC_warning">None</span>'; } + if ($env{'form.scantron_CODElist'} eq '') { $CODElist='<span class="LC_warning">'.&mt('None').'</span>'; } $CODElist= - '<tr><td><b>List of CODES to validate against:</b></td><td><tt>'. + '<tr><td><b>'.&mt('List of CODES to validate against:').'</b></td><td><tt>'. $env{'form.scantron_CODElist'}.'</tt></td></tr>'; } - return (<<STUFF); + my $lastbubblepoints; + if ($env{'form.scantron_lastbubblepoints'} ne '') { + $lastbubblepoints = + '<tr><td><b>'.&mt('Hand-graded items: points from last bubble in row').'</b></td><td><tt>'. + $env{'form.scantron_lastbubblepoints'}.'</tt></td></tr>'; + } + return (' <p> -<span class="LC_warning">Please double check the information - below before clicking on '$button_text'</span> +<span class="LC_warning"> +'.&mt("Please double check the information below before clicking on '[_1]'",&mt($button_text)).'</span> </p> <table> -<tr><td><b>Sequence to be Graded:</b></td><td>$title</td></tr> -<tr><td><b>Data File that will be used:</b></td><td><tt>$env{'form.scantron_selectfile'}</tt></td></tr> -$CODElist +<tr><td><b>'.&mt('Sequence to be Graded:').'</b></td><td>'.$title.'</td></tr> +<tr><td><b>'.&mt('Data File that will be used:').'</b></td><td><tt>'.$env{'form.scantron_selectfile'}.'</tt></td></tr> +'.$CODElist.$lastbubblepoints.' </table> <br /> -<p> If this information is correct, please click on '$button_text'.</p> -<p> If something is incorrect, please click the 'Grading Menu' button to start over.</p> +<p> '.&mt("If this information is correct, please click on '[_1]'.",&mt($button_text)).'</p> +<p> '.&mt("If something is incorrect, please click the 'Grading Menu' button to start over.").'</p> <br /> -STUFF +'); } =pod @@ -5724,23 +6874,24 @@ sub scantron_do_warning { if ( $env{'form.selectpage'} eq '' || $env{'form.scantron_selectfile'} eq '' || $env{'form.scantron_format'} eq '' ) { - $r->print("<p>You have forgetten to specify some information. Please go Back and try again.</p>"); + $r->print("<p>".&mt('You have forgotten to specify some information. Please go Back and try again.')."</p>"); if ( $env{'form.selectpage'} eq '') { - $r->print('<p><span class="LC_error">You have not selected a Sequence to grade</span></p>'); + $r->print('<p><span class="LC_error">'.&mt('You have not selected a Sequence to grade').'</span></p>'); } if ( $env{'form.scantron_selectfile'} eq '') { - $r->print('<p><span class="LC_error">You have not selected a file that contains the student\'s response data.</span></p>'); + $r->print('<p><span class="LC_error">'.&mt("You have not selected a file that contains the student's response data.").'</span></p>'); } if ( $env{'form.scantron_format'} eq '') { - $r->print('<p><span class="LC_error">You have not selected a the format of the student\'s response data.</span></p>'); + $r->print('<p><span class="LC_error">'.&mt("You have not selected the format of the student's response data.").'</span></p>'); } } else { my $warning=&scantron_warning_screen('Grading: Validate Records'); - $r->print(<<STUFF); -$warning -<input type="submit" name="submit" value="Grading: Validate Records" /> + my $bubbledbyhand=&hand_bubble_option(); + $r->print(' +'.$warning.$bubbledbyhand.' +<input type="submit" name="submit" value="'.&mt('Grading: Validate Records').'" /> <input type="hidden" name="command" value="scantron_validate" /> -STUFF +'); } $r->print("</form><br />".&show_grading_menu_form($symb)); return ''; @@ -5775,9 +6926,15 @@ SCANTRONFORM '<input type="hidden" name="scantron.bubblelines.'.$line.'" value="'.$env{"form.scantron.bubblelines.$line"}.'" />'."\n"; $chunk .= '<input type="hidden" name="scantron.first_bubble_line.'.$line.'" value="'.$env{"form.scantron.first_bubble_line.$line"}.'" />'."\n"; + $chunk .= + '<input type="hidden" name="scantron.sub_bubblelines.'.$line.'" value="'.$env{"form.scantron.sub_bubblelines.$line"}.'" />'."\n"; + $chunk .= + '<input type="hidden" name="scantron.responsetype.'.$line.'" value="'.$env{"form.scantron.responsetype.$line"}.'" />'."\n"; + $chunk .= + '<input type="hidden" name="scantron.residpart.'.$line.'" value="'.$env{"form.scantron.residpart.$line"}.'" />'."\n"; $result .= $chunk; $line++; - } + } return $result; } @@ -5785,7 +6942,7 @@ SCANTRONFORM =item scantron_validate_file - Dispatch routine for doing validation of a bubble sheet data file. + Dispatch routine for doing validation of a bubblesheet data file. Also processes any necessary information resets that need to occur before validation begins (ignore previous corrections, @@ -5799,7 +6956,7 @@ sub scantron_validate_file { if (!$symb) {return '';} my $default_form_data=&defaultFormData($symb); - # do the detection of only doing skipped records first befroe we delete + # do the detection of only doing skipped records first before we delete # them when doing the corrections reset if ($env{'form.scantron_options_redo'} ne 'redo_skipped_ready') { &reset_skipping_status(); @@ -5819,11 +6976,20 @@ sub scantron_validate_file { if ($env{'form.scantron_corrections'}) { &scantron_process_corrections($r); } - $r->print("<p>Gathering necessary info.</p>");$r->rflush(); + $r->print('<p>'.&mt('Gathering necessary information.').'</p>');$r->rflush(); #get the student pick code ready $r->print(&Apache::loncommon::studentbrowser_javascript()); - my $max_bubble=&scantron_get_maxbubble(); + my $nav_error; + my %scantron_config=&get_scantron_config($env{'form.scantron_format'}); + my $max_bubble=&scantron_get_maxbubble(\$nav_error,\%scantron_config); + if ($nav_error) { + $r->print(&navmap_errormsg()); + return ''; + } my $result=&scantron_form_start($max_bubble).$default_form_data; + if ($env{'form.scantron_lastbubblepoints'} ne '') { + $result .= '<input type="hidden" name="scantron_lastbubblepoints" value="'.$env{'form.scantron_lastbubblepoints'}.'" />'; + } $r->print($result); my @validate_phases=( 'sequence', @@ -5839,8 +7005,9 @@ sub scantron_validate_file { my $stop=0; while (!$stop && $currentphase < scalar(@validate_phases)) { - $r->print("<p> Validating ".$validate_phases[$currentphase]."</p>"); + $r->print(&mt('Validating '.$validate_phases[$currentphase]).'<br />'); $r->rflush(); + my $which="scantron_validate_".$validate_phases[$currentphase]; { no strict 'refs'; @@ -5849,28 +7016,37 @@ sub scantron_validate_file { } if (!$stop) { my $warning=&scantron_warning_screen('Start Grading'); - $r->print(<<STUFF); -Validation process complete.<br /> -$warning -<input type="submit" name="submit" value="Start Grading" /> -<input type="hidden" name="command" value="scantron_process" /> -STUFF - + $r->print(&mt('Validation process complete.').'<br />'. + $warning. + &mt('Perform verification for each student after storage of submissions?'). + ' <span class="LC_nobreak"><label>'. + '<input type="radio" name="verifyrecord" value="1" />'.&mt('Yes').'</label>'. + (' 'x3).'<label>'. + '<input type="radio" name="verifyrecord" value="0" checked="checked" />'.&mt('No'). + '</label></span><br />'. + &mt('Grading will take longer if you use verification.').'<br />'. + &mt("Alternatively, the 'Review bubblesheet data' utility (see grading menu) can be used for all students after grading is complete.").'<br /><br />'. + '<input type="submit" name="submit" value="'.&mt('Start Grading').'" />'. + '<input type="hidden" name="command" value="scantron_process" />'."\n"); } else { $r->print('<input type="hidden" name="command" value="scantron_validate" />'); $r->print("<input type='hidden' name='validatepass' value='".$currentphase."' />"); } if ($stop) { if ($validate_phases[$currentphase] eq 'sequence') { - $r->print('<input type="submit" name="submit" value="Ignore -> " />'); - $r->print(' this error <br />'); + $r->print('<input type="submit" name="submit" value="'.&mt('Ignore').' → " />'); + $r->print(' '.&mt('this error').' <br />'); - $r->print(" <p>Or click the 'Grading Menu' button to start over.</p>"); + $r->print(" <p>".&mt("Or click the 'Grading Menu' button to start over.")."</p>"); } else { - $r->print('<input type="submit" name="submit" value="Continue ->" />'); - $r->print(' using corrected info <br />'); - $r->print("<input type='submit' value='Skip' name='scantron_skip_record' />"); - $r->print(" this scanline saving it for later."); + 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)" />'); + } else { + $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' />"); + $r->print(" ".&mt("this scanline saving it for later.")); } } $r->print(" </form><br />".&show_grading_menu_form($symb)); @@ -5882,7 +7058,7 @@ STUFF =item scantron_remove_file - Removes the requested bubble sheet data file, makes sure that + Removes the requested bubblesheet data file, makes sure that scantron_original_<filename> is never removed @@ -5907,7 +7083,7 @@ sub scantron_remove_file { =item scantron_remove_scan_data - Removes all scan_data correction for the requested bubble sheet + Removes all scan_data correction for the requested bubblesheet data file. (In the case that both the are doing skipped records we need to remember the old skipped lines for the time being so that element persists for a while.) @@ -5931,7 +7107,10 @@ sub scantron_remove_scan_data { } my $result; if (@todelete) { - $result=&Apache::lonnet::del('nohist_scantrondata',\@todelete,$cdom,$cname); + $result = &Apache::lonnet::del('nohist_scantrondata', + \@todelete,$cdom,$cname); + } else { + $result = 'ok'; } return $result; } @@ -5941,7 +7120,7 @@ sub scantron_remove_scan_data { =item scantron_getfile - Fetches the requested bubble sheet data file (all 3 versions), and + Fetches the requested bubblesheet data file (all 3 versions), and the scan_data hash Arguments: @@ -6041,7 +7220,7 @@ sub lonnet_putfile { =item scantron_putfile - Stores the current version of the bubble sheet data files, and the + Stores the current version of the bubblesheet data files, and the scan_data hash. (Does not modify the original version only the corrected and skipped versions. @@ -6135,7 +7314,7 @@ sub get_todo_count { =item scantron_put_line - Updates the 'corrected' or 'skipped' versions of the bubble sheet + Updates the 'corrected' or 'skipped' versions of the bubblesheet data file. Arguments: @@ -6222,6 +7401,10 @@ sub scantron_validate_sequence { my ($r,$currentphase) = @_; my $navmap=Apache::lonnavmaps::navmap->new(); + unless (ref($navmap)) { + $r->print(&navmap_errormsg()); + return (1,$currentphase); + } my (undef,undef,$sequence)= &Apache::lonnet::decode_symb($env{'form.selectpage'}); @@ -6233,7 +7416,12 @@ sub scantron_validate_sequence { my @resources= $navmap->retrieveResources($map,\&scantron_filter_not_exam,1,0); if (@resources) { - $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>"); + $r->print('<p class="LC_warning">' + .&mt('Some resources in the sequence currently are not set to' + .' exam mode. Grading these resources currently may not' + .' work correctly.') + .'</p>' + ); return (1,$currentphase); } } @@ -6241,14 +7429,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) = @_; @@ -6260,8 +7441,13 @@ sub scantron_validate_ID { #get scantron line setup my %scantron_config=&get_scantron_config($env{'form.scantron_format'}); my ($scanlines,$scan_data)=&scantron_getfile(); - - &scantron_get_maxbubble(); # parse needs the bubble_lines.. array. + + my $nav_error; + &scantron_get_maxbubble(\$nav_error,\%scantron_config); # parse needs the bubble_lines.. array. + if ($nav_error) { + $r->print(&navmap_errormsg()); + return(1,$currentphase); + } my %found=('ids'=>{},'usernames'=>{}); for (my $i=0;$i<=$scanlines->{'count'};$i++) { @@ -6314,87 +7500,74 @@ 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)=@_; - + my ($r,$i,$scan_record,$scan_config,$line,$error,$arg, + $randomorder,$randompick,$respnumlookup,$startline)=@_; #FIXME in the case of a duplicated ID the previous line, probably need #to show both the current line and the previous one and allow skipping #the previous one or the current one - $r->print("<p><b>An error was detected ($error)</b>"); if ( $$scan_record{'scantron.PaperID'} =~ /\S/) { - $r->print(" for PaperID <tt>". - $$scan_record{'scantron.PaperID'}."</tt> \n"); + $r->print( + '<p class="LC_warning">' + .&mt('An error was detected ([_1]) for PaperID [_2]', + "<b>$error</b>", + '<tt>'.$$scan_record{'scantron.PaperID'}.'</tt>') + ."</p> \n"); } else { - $r->print(" in scanline $i <pre>". - $line."</pre> \n"); - } - my $message="<p>The ID on the form is <tt>". - $$scan_record{'scantron.ID'}."</tt><br />\n". - "The name on the paper is ". - $$scan_record{'scantron.LastName'}.",". - $$scan_record{'scantron.FirstName'}."</p>"; + $r->print( + '<p class="LC_warning">' + .&mt('An error was detected ([_1]) in scanline [_2] [_3]', + "<b>$error</b>", $i, "<pre>$line</pre>") + ."</p> \n"); + } + my $message = + '<p>' + .&mt('The ID on the form is [_1]', + "<tt>$$scan_record{'scantron.ID'}</tt>") + .'<br />' + .&mt('The name on the paper is [_1], [_2]', + $$scan_record{'scantron.LastName'}, + $$scan_record{'scantron.FirstName'}) + .'</p>'; $r->print('<input type="hidden" name="scantron_corrections" value="'.$error.'" />'."\n"); $r->print('<input type="hidden" name="scantron_line" value="'.$i.'" />'."\n"); + # Array populated for doublebubble or + my @lines_to_correct; # missingbubble errors to build javascript + # to validate radio button checking + if ($error =~ /ID$/) { if ($error eq 'incorrectID') { - $r->print("The encoded ID is not in the classlist</p>\n"); + $r->print('<p class="LC_warning">'.&mt("The encoded ID is not in the classlist"). + "</p>\n"); } elsif ($error eq 'duplicateID') { - $r->print("The encoded ID has also been used by a previous paper $arg</p>\n"); + $r->print('<p class="LC_warning">'.&mt("The encoded ID has also been used by a previous paper [_1]",$arg)."</p>\n"); } $r->print($message); - $r->print("<p>How should I handle this? <br /> \n"); + $r->print("<p>".&mt("How should I handle this?")." <br /> \n"); $r->print("\n<ul><li> "); #FIXME it would be nice if this sent back the user ID and #could do partial userID matches $r->print(&Apache::loncommon::selectstudent_link('scantronupload', 'scantron_username','scantron_domain')); $r->print(": <input type='text' name='scantron_username' value='' />"); - $r->print("\n@". + $r->print("\n:\n". &Apache::loncommon::select_dom_form($env{'request.role.domain'},'scantron_domain')); $r->print('</li>'); } elsif ($error =~ /CODE$/) { if ($error eq 'incorrectCODE') { - $r->print("</p><p>The encoded CODE is not in the list of possible CODEs</p>\n"); + $r->print('<p class="LC_warning">'.&mt("The encoded CODE is not in the list of possible CODEs.")."</p>\n"); } elsif ($error eq 'duplicateCODE') { - $r->print("</p><p>The encoded CODE has also been used by a previous paper ".join(', ',@{$arg}).", and CODEs are supposed to be unique</p>\n"); + $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"); } - $r->print("<p>The CODE on the form is <tt>'". - $$scan_record{'scantron.CODE'}."'</tt><br />\n"); + $r->print("<p>".&mt('The CODE on the form is [_1]', + "<tt>'$$scan_record{'scantron.CODE'}'</tt>") + ."</p>\n"); $r->print($message); - $r->print("<p>How should I handle this? <br /> \n"); + $r->print("<p>".&mt("How should I handle this?")."</p>\n"); $r->print("\n<br /> "); my $i=0; if ($error eq 'incorrectCODE' @@ -6403,16 +7576,27 @@ sub scantron_get_correction { if ($closest > 0) { foreach my $testcode (@{$closest}) { my $checked=''; - if (!$i) { $checked=' checked="checked" '; } - $r->print("<label><input type='radio' name='scantron_CODE_resolution' value='use_closest_$i' $checked /> Use the similar CODE <b><tt>".$testcode."</tt></b> instead.</label><input type='hidden' name='scantron_CODE_closest_$i' value='$testcode' />"); + if (!$i) { $checked=' checked="checked"'; } + $r->print(" + <label> + <input type='radio' name='scantron_CODE_resolution' value='use_closest_$i'$checked /> + ".&mt("Use the similar CODE [_1] instead.", + "<b><tt>".$testcode."</tt></b>")." + </label> + <input type='hidden' name='scantron_CODE_closest_$i' value='$testcode' />"); $r->print("\n<br />"); $i++; } } } if ($$scan_record{'scantron.CODE'}=~/\S/ ) { - my $checked; if (!$i) { $checked=' checked="checked" '; } - $r->print("<label><input type='radio' name='scantron_CODE_resolution' value='use_unfound' $checked /> Use the CODE <b><tt>".$$scan_record{'scantron.CODE'}."</tt></b> that is was on the paper, ignoring the error.</label>"); + my $checked; if (!$i) { $checked=' checked="checked"'; } + $r->print(" + <label> + <input type='radio' name='scantron_CODE_resolution' value='use_unfound'$checked /> + ".&mt("Use the CODE [_1] that was on the paper, ignoring the error.", + "<b><tt>".$$scan_record{'scantron.CODE'}."</tt></b>")." + </label>"); $r->print("\n<br />"); } @@ -6434,115 +7618,344 @@ ENDSCRIPT "&curCODE=".&escape($$scan_record{'scantron.CODE'}). "&scantron_selectfile=".&escape($env{'form.scantron_selectfile'}); if ($env{'form.scantron_CODElist'} =~ /\S/) { - $r->print("<label><input type='radio' name='scantron_CODE_resolution' value='use_found' /> <a target='_blank' href='$href'>Select</a> a CODE from the list of all CODEs and use it.</label> Selected CODE is <input readonly='true' type='text' size='8' name='scantron_CODE_selectedvalue' onfocus=\"javascript:change_radio('use_found')\" onchange=\"javascript:change_radio('use_found')\" />"); + $r->print(" + <label> + <input type='radio' name='scantron_CODE_resolution' value='use_found' /> + ".&mt("[_1]Select[_2] a CODE from the list of all CODEs and use it.", + "<a target='_blank' href='$href'>","</a>")." + </label> + ".&mt("Selected CODE is [_1]",'<input readonly="readonly" type="text" size="8" name="scantron_CODE_selectedvalue" onfocus="javascript:change_radio(\'use_found\')" onchange="javascript:change_radio(\'use_found\')" />')); $r->print("\n<br />"); } - $r->print("<label><input type='radio' name='scantron_CODE_resolution' value='use_typed' /> Use </label><input type='text' size='8' name='scantron_CODE_newvalue' onfocus=\"javascript:change_radio('use_typed')\" onkeypress=\"javascript:change_radio('use_typed')\" /> as the CODE."); + $r->print(" + <label> + <input type='radio' name='scantron_CODE_resolution' value='use_typed' /> + ".&mt("Use [_1] as the CODE.", + "</label><input type='text' size='8' name='scantron_CODE_newvalue' onfocus=\"javascript:change_radio('use_typed')\" onkeypress=\"javascript:change_radio('use_typed')\" />")); $r->print("\n<br /><br />"); } elsif ($error eq 'doublebubble') { - $r->print("<p>There have been multiple bubbles scanned for a some question(s)</p>\n"); + $r->print('<p class="LC_warning">'.&mt("There have been multiple bubbles scanned for some question(s)")."</p>\n"); + + # The form field scantron_questions is acutally a list of line numbers. + # represented by this form so: + + my $line_list = &questions_to_line_list($arg,$randomorder,$randompick, + $respnumlookup,$startline); + $r->print('<input type="hidden" name="scantron_questions" value="'. - join(',',@{$arg}).'" />'); + $line_list.'" />'); $r->print($message); - $r->print("<p>Please indicate which bubble should be used for grading</p>"); + $r->print("<p>".&mt("Please indicate which bubble should be used for grading")."</p>"); foreach my $question (@{$arg}) { - my $selected = &get_response_bubbles($scan_record, $question); - my @select_array = split(/:/,$selected); - &scantron_bubble_selector($r,$scan_config,$question, - @select_array); + my @linenums = &prompt_for_corrections($r,$question,$scan_config, + $scan_record, $error, + $randomorder,$randompick, + $respnumlookup,$startline); + push(@lines_to_correct,@linenums); } + $r->print(&verify_bubbles_checked(@lines_to_correct)); } elsif ($error eq 'missingbubble') { - $r->print("<p>There have been <b>no</b> bubbles scanned for some question(s)</p>\n"); + $r->print('<p class="LC_warning">'.&mt("There have been [_1]no[_2] bubbles scanned for some question(s)",'<b>','</b>')."</p>\n"); $r->print($message); - $r->print("<p>Please indicate which bubble should be used for grading</p>"); - $r->print("Some questions have no scanned bubbles\n"); + $r->print("<p>".&mt("Please indicate which bubble should be used for grading.")."</p>"); + $r->print(&mt("Some questions have no scanned bubbles.")."\n"); + + # The form field scantron_questions is actually a list of line numbers not + # a list of question numbers. Therefore: + # + + my $line_list = &questions_to_line_list($arg,$randomorder,$randompick, + $respnumlookup,$startline); + $r->print('<input type="hidden" name="scantron_questions" value="'. - join(',',@{$arg}).'" />'); + $line_list.'" />'); foreach my $question (@{$arg}) { - my $selected = &get_response_bubbles($scan_record, $question); - &scantron_bubble_selector($r,$scan_config,$question); + my @linenums = &prompt_for_corrections($r,$question,$scan_config, + $scan_record, $error, + $randomorder,$randompick, + $respnumlookup,$startline); + push(@lines_to_correct,@linenums); } + $r->print(&verify_bubbles_checked(@lines_to_correct)); } else { $r->print("\n<ul>"); } $r->print("\n</li></ul>"); +} +sub verify_bubbles_checked { + my (@ansnums) = @_; + my $ansnumstr = join('","',@ansnums); + my $warning = &mt("A bubble or 'No bubble' selection has not been made for one or more lines."); + my $output = (<<ENDSCRIPT); +<script type="text/javascript"> +function verify_bubble_radio(form) { + var ansnumArray = new Array ("$ansnumstr"); + var need_bubble_count = 0; + for (var i=0; i<ansnumArray.length; i++) { + if (form.elements["scantron_correct_Q_"+ansnumArray[i]].length > 1) { + var bubble_picked = 0; + for (var j=0; j<form.elements["scantron_correct_Q_"+ansnumArray[i]].length; j++) { + if (form.elements["scantron_correct_Q_"+ansnumArray[i]][j].checked == true) { + bubble_picked = 1; + } + } + if (bubble_picked == 0) { + need_bubble_count ++; + } + } + } + if (need_bubble_count) { + alert("$warning"); + return; + } + form.submit(); +} +</script> +ENDSCRIPT + return $output; } =pod -=item scantron_bubble_selector - - Generates the html radiobuttons to correct a single bubble line - possibly showing the existing the selected bubbles if known +=item questions_to_line_list - Arguments: - $r - Apache request object - $scan_config - hash from &get_scantron_config() - $quest - number of the bubble line to make a corrector for - $lines - array of answer lines. +Converts a list of questions into a string of comma separated +line numbers in the answer sheet used by the questions. This is +used to fill in the scantron_questions form field. -=cut + Arguments: + questions - Reference to an array of questions. + randomorder - True if randomorder in use. + randompick - True if randompick in use. + respnumlookup - Reference to HASH mapping question numbers in bubble lines + for current line to question number used for same question + in "Master Seqence" (as seen by Course Coordinator). + startline - Reference to hash where key is question number (0 is first) + and key is number of first bubble line for current student + or code-based randompick and/or randomorder. -sub scantron_bubble_selector { - my ($r,$scan_config,$quest,@lines)=@_; - my $max=$$scan_config{'Qlength'}; +=cut - my $scmode=$$scan_config{'Qon'}; +sub questions_to_line_list { + my ($questions,$randomorder,$randompick,$respnumlookup,$startline) = @_; + my @lines; + + foreach my $item (@{$questions}) { + my $question = $item; + my ($first,$count,$last); + if ($item =~ /^(\d+)\.(\d+)$/) { + $question = $1; + my $subquestion = $2; + my $responsenum = $question-1; + if (($randomorder || $randompick) && (ref($respnumlookup) eq 'HASH')) { + $responsenum = $respnumlookup->{$question-1}; + if (ref($startline) eq 'HASH') { + $first = $startline->{$question-1} + 1; + } + } else { + $first = $first_bubble_line{$responsenum} + 1; + } + my @subans = split(/,/,$subdivided_bubble_lines{$responsenum}); + my $subcount = 1; + while ($subcount<$subquestion) { + $first += $subans[$subcount-1]; + $subcount ++; + } + $count = $subans[$subquestion-1]; + } else { + my $responsenum = $question-1; + if (($randomorder || $randompick) && (ref($respnumlookup) eq 'HASH')) { + $responsenum = $respnumlookup->{$question-1}; + if (ref($startline) eq 'HASH') { + $first = $startline->{$question-1} + 1; + } + } else { + $first = $first_bubble_line{$responsenum} + 1; + } + $count = $bubble_lines_per_response{$responsenum}; + } + $last = $first+$count-1; + push(@lines, ($first..$last)); + } + return join(',', @lines); +} - my $bubble_length = scalar(@lines); +=pod +=item prompt_for_corrections - if ($scmode eq 'number' || $scmode eq 'letter') { $max=10; } +Prompts for a potentially multiline correction to the +user's bubbling (factors out common code from scantron_get_correction +for multi and missing bubble cases). - my $response = $quest-1; - my $lines = $bubble_lines_per_response{$response}; + Arguments: + $r - Apache request object. + $question - The question number to prompt for. + $scan_config - The scantron file configuration hash. + $scan_record - Reference to the hash that has the the parsed scanlines. + $error - Type of error + $randomorder - True if randomorder in use. + $randompick - True if randompick in use. + $respnumlookup - Reference to HASH mapping question numbers in bubble lines + for current line to question number used for same question + in "Master Seqence" (as seen by Course Coordinator). + $startline - Reference to hash where key is question number (0 is first) + and value is number of first bubble line for current student + or code-based randompick and/or randomorder. + + Implicit inputs: + %bubble_lines_per_response - Starting line numbers for each question. + Numbered from 0 (but question numbers are from + 1. + %first_bubble_line - Starting bubble line for each question. + %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, 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 + than one line might contain bubbles, e.g. + bubbling of: line 1 - J, line 2 - J, + line 3 - B would assign 22 points. - my $total_lines = $lines*2; - my @alphabet=('A'..'Z'); - $r->print("<table border='1'><tr><td rowspan='".$total_lines."'>$quest</td>"); +=cut - for (my $l = 0; $l < $lines; $l++) { - if ($l != 0) { - $r->print('<tr>'); - } - my @selected = split(//,$lines[$l]); - for (my $i=0;$i<$max;$i++) { - $r->print("\n".'<td align="center">'); - if ($selected[0] eq $alphabet[$i]) { - $r->print('X'); - shift(@selected) ; - } else { - $r->print(' '); - } - $r->print('</td>'); - - } +sub prompt_for_corrections { + my ($r, $question, $scan_config, $scan_record, $error, $randomorder, + $randompick, $respnumlookup, $startline) = @_; + my ($current_line,$lines); + my @linenums; + my $questionnum = $question; + my ($first,$responsenum); + if ($question =~ /^(\d+)\.(\d+)$/) { + $question = $1; + my $subquestion = $2; + if (($randomorder || $randompick) && (ref($respnumlookup) eq 'HASH')) { + $responsenum = $respnumlookup->{$question-1}; + if (ref($startline) eq 'HASH') { + $first = $startline->{$question-1}; + } + } else { + $responsenum = $question-1; + $first = $first_bubble_line{$responsenum}; + } + $current_line = $first + 1 ; + my @subans = split(/,/,$subdivided_bubble_lines{$responsenum}); + my $subcount = 1; + while ($subcount<$subquestion) { + $current_line += $subans[$subcount-1]; + $subcount ++; + } + $lines = $subans[$subquestion-1]; + } else { + if (($randomorder || $randompick) && (ref($respnumlookup) eq 'HASH')) { + $responsenum = $respnumlookup->{$question-1}; + if (ref($startline) eq 'HASH') { + $first = $startline->{$question-1}; + } + } else { + $responsenum = $question-1; + $first = $first_bubble_line{$responsenum}; + } + $current_line = $first + 1; + $lines = $bubble_lines_per_response{$responsenum}; + } + if ($lines > 1) { + $r->print(&mt('The group of bubble lines below responds to a single question.').'<br />'); + if (($responsetype_per_response{$responsenum} eq 'essayresponse') || + ($responsetype_per_response{$responsenum} eq 'formularesponse') || + ($responsetype_per_response{$responsenum} eq 'stringresponse') || + ($responsetype_per_response{$responsenum} eq 'imageresponse') || + ($responsetype_per_response{$responsenum} eq 'reactionresponse') || + ($responsetype_per_response{$responsenum} eq 'organicresponse')) { + $r->print(&mt("Although this particular question type requires handgrading, the instructions for this question in the bubblesheet 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 />'); + } else { + $r->print(&mt("Select at most one bubble in a single line and select 'No Bubble' in all the other lines. ")."<br />"); + } + } + for (my $i =0; $i < $lines; $i++) { + my $selected = $$scan_record{"scantron.$current_line.answer"}; + &scantron_bubble_selector($r,$scan_config,$current_line, + $questionnum,$error,split('', $selected)); + push(@linenums,$current_line); + $current_line++; + } + if ($lines > 1) { + $r->print("<hr /><br />"); + } + return @linenums; +} - if ($l == 0) { - my $lspan = $total_lines * 2; # 2 table rows per bubble line. +=pod - $r->print('<td rowspan='.$lspan.'><label><input type="radio" name="scantron_correct_Q_'. - $quest.'" value="none" /> No bubble </label></td>'); - - } +=item scantron_bubble_selector + + Generates the html radiobuttons to correct a single bubble line + possibly showing the existing the selected bubbles if known - $r->print('</tr><tr>'); + Arguments: + $r - Apache request object + $scan_config - hash from &get_scantron_config() + $line - Number of the line being displayed. + $questionnum - Question number (may include subquestion) + $error - Type of error. + @selected - Array of bubbles picked on this line. - # FIXME: This may have to be a bit more clever for - # multiline questions (different values e.g..). +=cut - for (my $i=0;$i<$max;$i++) { - $r->print("\n". - '<td><label><input type="radio" name="scantron_correct_Q_'. - $quest.'" value="'.$i.'" />'.$alphabet[$i]."</label></td>"); - } - $r->print('</tr>'); +sub scantron_bubble_selector { + my ($r,$scan_config,$line,$questionnum,$error,@selected)=@_; + my $max=$$scan_config{'Qlength'}; - + my $scmode=$$scan_config{'Qon'}; + if ($scmode eq 'number' || $scmode eq 'letter') { + if (($$scan_config{'BubblesPerRow'} =~ /^\d+$/) && + ($$scan_config{'BubblesPerRow'} > 0)) { + $max=$$scan_config{'BubblesPerRow'}; + if (($scmode eq 'number') && ($max > 10)) { + $max = 10; + } elsif (($scmode eq 'letter') && $max > 26) { + $max = 26; + } + } else { + $max = 10; + } } - $r->print('</table>'); + + my @alphabet=('A'..'Z'); + $r->print(&Apache::loncommon::start_data_table(). + &Apache::loncommon::start_data_table_row()); + $r->print('<td rowspan="2" class="LC_leftcol_header">'.$line.'</td>'); + for (my $i=0;$i<$max+1;$i++) { + $r->print("\n".'<td align="center">'); + if ($selected[0] eq $alphabet[$i]) { $r->print('X'); shift(@selected) } + else { $r->print(' '); } + $r->print('</td>'); + } + $r->print(&Apache::loncommon::end_data_table_row(). + &Apache::loncommon::start_data_table_row()); + for (my $i=0;$i<$max;$i++) { + $r->print("\n". + '<td><label><input type="radio" name="scantron_correct_Q_'. + $line.'" value="'.$i.'" />'.$alphabet[$i]."</label></td>"); + } + my $nobub_checked = ' '; + if ($error eq 'missingbubble') { + $nobub_checked = ' checked = "checked" '; + } + $r->print("\n".'<td><label><input type="radio" name="scantron_correct_Q_'. + $line.'" value="none"'.$nobub_checked.'/>'.&mt('No bubble'). + '</label>'."\n".'<input type="hidden" name="scantron_questionnum_Q_'. + $line.'" value="'.$questionnum.'" /></td>'); + $r->print(&Apache::loncommon::end_data_table_row(). + &Apache::loncommon::end_data_table()); } =pod @@ -6668,7 +8081,12 @@ sub scantron_validate_CODE { my %allcodes=&get_codes(); - &scantron_get_maxbubble(); # parse needs the lines per response array. + my $nav_error; + &scantron_get_maxbubble(\$nav_error,\%scantron_config); # parse needs the lines per response array. + if ($nav_error) { + $r->print(&navmap_errormsg()); + return(1,$currentphase); + } my ($scanlines,$scan_data)=&scantron_getfile(); for (my $i=0;$i<=$scanlines->{'count'};$i++) { @@ -6699,7 +8117,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); } @@ -6718,45 +8136,69 @@ sub scantron_validate_doublebubble { #get student info my $classlist=&Apache::loncoursedata::get_classlist(); my %idmap=&username_to_idmap($classlist); + my (undef,undef,$sequence)= + &Apache::lonnet::decode_symb($env{'form.selectpage'}); #get scantron line setup my %scantron_config=&get_scantron_config($env{'form.scantron_format'}); my ($scanlines,$scan_data)=&scantron_getfile(); - &scantron_get_maxbubble(); # parse needs the bubble line array. + my $navmap = Apache::lonnavmaps::navmap->new(); + unless (ref($navmap)) { + $r->print(&navmap_errormsg()); + return(1,$currentphase); + } + my $map=$navmap->getResourceByUrl($sequence); + my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0); + my ($randomorder,$randompick,@master_seq,%symb_to_resource,%grader_partids_by_symb, + %grader_randomlists_by_symb,%orderedforcode,%respnumlookup,%startline); + my $bubbles_per_row = &bubblesheet_bubbles_per_row(\%scantron_config); + + my $nav_error; + if (ref($map)) { + $randomorder = $map->randomorder(); + $randompick = $map->randompick(); + if ($randomorder || $randompick) { + $nav_error = &get_master_seq(\@resources,\@master_seq,\%symb_to_resource); + if ($nav_error) { + $r->print(&navmap_errormsg()); + return(1,$currentphase); + } + &graders_resources_pass(\@resources,\%grader_partids_by_symb, + \%grader_randomlists_by_symb,$bubbles_per_row); + } + } else { + $r->print(&navmap_errormsg()); + return(1,$currentphase); + } + + &scantron_get_maxbubble(\$nav_error,\%scantron_config); # parse needs the bubble line array. + if ($nav_error) { + $r->print(&navmap_errormsg()); + return(1,$currentphase); + } for (my $i=0;$i<=$scanlines->{'count'};$i++) { my $line=&scantron_get_line($scanlines,$scan_data,$i); if ($line=~/^[\s\cz]*$/) { next; } my $scan_record=&scantron_parse_scanline($line,$i,\%scantron_config, - $scan_data); + $scan_data,undef,\%idmap,$randomorder, + $randompick,$sequence,\@master_seq, + \%symb_to_resource,\%grader_partids_by_symb, + \%orderedforcode,\%respnumlookup,\%startline); if (!defined($$scan_record{'scantron.doubleerror'})) { next; } &scantron_get_correction($r,$i,$scan_record,\%scantron_config,$line, 'doublebubble', - $$scan_record{'scantron.doubleerror'}); + $$scan_record{'scantron.doubleerror'}, + $randomorder,$randompick,\%respnumlookup,\%startline); return (1,$currentphase); } 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'} and - $env{'form.scantron.first_bubble_line.n'} - which are the total number of bubble, lines, the number of bubble - lines for reponse n and number of the first bubble line for response n. - -=cut -sub scantron_get_maxbubble { +sub scantron_get_maxbubble { + my ($nav_error,$scantron_config) = @_; if (defined($env{'form.scantron_maxbubble'}) && $env{'form.scantron_maxbubble'}) { &restore_bubble_lines(); @@ -6767,55 +8209,91 @@ sub scantron_get_maxbubble { &Apache::lonnet::decode_symb($env{'form.selectpage'}); my $navmap=Apache::lonnavmaps::navmap->new(); + unless (ref($navmap)) { + if (ref($nav_error)) { + $$nav_error = 1; + } + return; + } my $map=$navmap->getResourceByUrl($sequence); my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0); + my $bubbles_per_row = &bubblesheet_bubbles_per_row($scantron_config); &Apache::lonxml::clear_problem_counter(); - my $uname = $env{'form.student'}; - my $udom = $env{'form.userdom'}; + my $uname = $env{'user.name'}; + my $udom = $env{'user.domain'}; my $cid = $env{'request.course.id'}; my $total_lines = 0; %bubble_lines_per_response = (); %first_bubble_line = (); + %subdivided_bubble_lines = (); + %responsetype_per_response = (); + %masterseq_id_responsenum = (); - my $response_number = 0; my $bubble_line = 0; foreach my $resource (@resources) { - my $symb = $resource->symb(); - &Apache::lonxml::clear_bubble_lines_for_part(); - 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 %analysis = &Apache::lonnet::str2hash($an); - - - - foreach my $part_id (@{$analysis{'parts'}}) { - - - my $lines = $analysis{"$part_id.bubble_lines"};; - - # TODO - make this a persistent hash not an array. - - - $first_bubble_line{$response_number} = $bubble_line; - $bubble_lines_per_response{$response_number} = $lines; - $response_number++; + my $resid = $resource->id(); + my ($analysis,$parts) = &scantron_partids_tograde($resource,$cid,$uname, + $udom,undef,$bubbles_per_row); + if ((ref($analysis) eq 'HASH') && (ref($parts) eq 'ARRAY')) { + foreach my $part_id (@{$parts}) { + my $lines; + + # TODO - make this a persistent hash not an array. + + # 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 'rankresponse')) { + my ($numbub,$numshown); + if ($analysis->{$part_id.'.type'} eq 'optionresponse') { + if (ref($analysis->{$part_id.'.options'}) eq 'ARRAY') { + $numbub = scalar(@{$analysis->{$part_id.'.options'}}); + } + } elsif ($analysis->{$part_id.'.type'} eq 'matchresponse') { + 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_row = + &bubblesheet_bubbles_per_row($scantron_config); + my $inner_bubble_lines = int($numbub/$bubbles_per_row); + if (($numbub % $bubbles_per_row) != 0) { + $inner_bubble_lines++; + } + for (my $i=0; $i<$numshown; $i++) { + $subdivided_bubble_lines{$response_number} .= + $inner_bubble_lines.','; + } + $subdivided_bubble_lines{$response_number} =~ s/,$//; + $lines = $numshown * $inner_bubble_lines; + } else { + $lines = $analysis->{"$part_id.bubble_lines"}; + } - $bubble_line += $lines; - $total_lines += $lines; - } + $first_bubble_line{$response_number} = $bubble_line; + $bubble_lines_per_response{$response_number} = $lines; + $responsetype_per_response{$response_number} = + $analysis->{$part_id.'.type'}; + $masterseq_id_responsenum{$resid.'_'.$part_id} = $response_number; + $response_number++; + $bubble_line += $lines; + $total_lines += $lines; + } + } } - &Apache::lonnet::delenv('scantron\.'); + &Apache::lonnet::delenv('scantron.'); &save_bubble_lines(); $env{'form.scantron_maxbubble'} = @@ -6823,41 +8301,123 @@ 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 bubblesheet_bubbles_per_row { + my ($scantron_config) = @_; + my $bubbles_per_row; + if (ref($scantron_config) eq 'HASH') { + $bubbles_per_row = $scantron_config->{'BubblesPerRow'}; + } + if ((!$bubbles_per_row) || ($bubbles_per_row < 1)) { + $bubbles_per_row = 10; + } + return $bubbles_per_row; +} sub scantron_validate_missingbubbles { my ($r,$currentphase) = @_; #get student info my $classlist=&Apache::loncoursedata::get_classlist(); my %idmap=&username_to_idmap($classlist); + my (undef,undef,$sequence)= + &Apache::lonnet::decode_symb($env{'form.selectpage'}); #get scantron line setup my %scantron_config=&get_scantron_config($env{'form.scantron_format'}); my ($scanlines,$scan_data)=&scantron_getfile(); - my $max_bubble=&scantron_get_maxbubble(); + + my $navmap = Apache::lonnavmaps::navmap->new(); + unless (ref($navmap)) { + $r->print(&navmap_errormsg()); + return(1,$currentphase); + } + + my $map=$navmap->getResourceByUrl($sequence); + my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0); + my ($randomorder,$randompick,@master_seq,%symb_to_resource,%grader_partids_by_symb, + %grader_randomlists_by_symb,%orderedforcode,%respnumlookup,%startline); + my $bubbles_per_row = &bubblesheet_bubbles_per_row(\%scantron_config); + + my $nav_error; + if (ref($map)) { + $randomorder = $map->randomorder(); + $randompick = $map->randompick(); + if ($randomorder || $randompick) { + $nav_error = &get_master_seq(\@resources,\@master_seq,\%symb_to_resource); + if ($nav_error) { + $r->print(&navmap_errormsg()); + return(1,$currentphase); + } + &graders_resources_pass(\@resources,\%grader_partids_by_symb, + \%grader_randomlists_by_symb,$bubbles_per_row); + } + } else { + $r->print(&navmap_errormsg()); + return(1,$currentphase); + } + + + my $max_bubble=&scantron_get_maxbubble(\$nav_error,\%scantron_config); + if ($nav_error) { + $r->print(&navmap_errormsg()); + return(1,$currentphase); + } + if (!$max_bubble) { $max_bubble=2**31; } for (my $i=0;$i<=$scanlines->{'count'};$i++) { my $line=&scantron_get_line($scanlines,$scan_data,$i); if ($line=~/^[\s\cz]*$/) { next; } - my $scan_record=&scantron_parse_scanline($line,$i,\%scantron_config, - $scan_data); + my $scan_record = + &scantron_parse_scanline($line,$i,\%scantron_config,$scan_data,undef,\%idmap, + $randomorder,$randompick,$sequence,\@master_seq, + \%symb_to_resource,\%grader_partids_by_symb, + \%orderedforcode,\%respnumlookup,\%startline); if (!defined($$scan_record{'scantron.missingerror'})) { next; } my @to_correct; + + # Probably here's where the error is... + foreach my $missing (@{$$scan_record{'scantron.missingerror'}}) { - if ($missing > $max_bubble) { next; } + my $lastbubble; + if ($missing =~ /^(\d+)\.(\d+)$/) { + my $question = $1; + my $subquestion = $2; + my ($first,$responsenum); + if ($randomorder || $randompick) { + $responsenum = $respnumlookup{$question-1}; + $first = $startline{$question-1}; + } else { + $responsenum = $question-1; + $first = $first_bubble_line{$responsenum}; + } + if (!defined($first)) { next; } + my @subans = split(/,/,$subdivided_bubble_lines{$responsenum}); + my $subcount = 1; + while ($subcount<$subquestion) { + $first += $subans[$subcount-1]; + $subcount ++; + } + my $count = $subans[$subquestion-1]; + $lastbubble = $first + $count; + } else { + my ($first,$responsenum); + if ($randomorder || $randompick) { + $responsenum = $respnumlookup{$missing-1}; + $first = $startline{$missing-1}; + } else { + $responsenum = $missing-1; + $first = $first_bubble_line{$responsenum}; + } + if (!defined($first)) { next; } + $lastbubble = $first + $bubble_lines_per_response{$responsenum}; + } + if ($lastbubble > $max_bubble) { next; } push(@to_correct,$missing); } if (@to_correct) { &scantron_get_correction($r,$i,$scan_record,\%scantron_config, - $line,'missingbubble',\@to_correct); + $line,'missingbubble',\@to_correct, + $randomorder,$randompick,\%respnumlookup, + \%startline); return (1,$currentphase); } @@ -6865,45 +8425,85 @@ 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 hand_bubble_option { + my (undef, undef, $sequence) = + &Apache::lonnet::decode_symb($env{'form.selectpage'}); + return if ($sequence eq ''); + my $navmap = Apache::lonnavmaps::navmap->new(); + unless (ref($navmap)) { + return; + } + my $needs_hand_bubbles; + my $map=$navmap->getResourceByUrl($sequence); + my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0); + foreach my $res (@resources) { + if (ref($res)) { + if ($res->is_problem()) { + my $partlist = $res->parts(); + foreach my $part (@{ $partlist }) { + my @types = $res->responseType($part); + if (grep(/^(chem|essay|image|formula|math|string|functionplot)$/,@types)) { + $needs_hand_bubbles = 1; + last; + } + } + } + } + } + if ($needs_hand_bubbles) { + my %scantron_config=&get_scantron_config($env{'form.scantron_format'}); + my $bubbles_per_row = &bubblesheet_bubbles_per_row(\%scantron_config); + return &mt('The sequence to be graded contains response types which are handgraded.').'<p>'. + &mt('If you have already graded these by bubbling sheets to indicate points awarded, [_1]what point value is assigned to a filled last bubble in each row?','<br />'). + '<label><input type="radio" name="scantron_lastbubblepoints" value="'.$bubbles_per_row.'" checked="checked" />'.&mt('[quant,_1,point]',$bubbles_per_row).'</label> '.&mt('or').' '. + '<label><input type="radio" name="scantron_lastbubblepoints" value="0" />'.&mt('0 points').'</label></p>'; + } + return; +} 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'}); + my $bubbles_per_row = &bubblesheet_bubbles_per_row(\%scantron_config); my ($scanlines,$scan_data)=&scantron_getfile(); my $classlist=&Apache::loncoursedata::get_classlist(); my %idmap=&username_to_idmap($classlist); my $navmap=Apache::lonnavmaps::navmap->new(); + unless (ref($navmap)) { + $r->print(&navmap_errormsg()); + return ''; + } my $map=$navmap->getResourceByUrl($sequence); + my ($randomorder,$randompick,@master_seq,%symb_to_resource,%grader_partids_by_symb, + %grader_randomlists_by_symb); + if (ref($map)) { + $randomorder = $map->randomorder(); + $randompick = $map->randompick(); + } else { + $r->print(&navmap_errormsg()); + return ''; + } + my $nav_error; my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0); -# $r->print("geto ".scalar(@resources)."<br />"); + if ($randomorder || $randompick) { + $nav_error = &get_master_seq(\@resources,\@master_seq,\%symb_to_resource); + if ($nav_error) { + $r->print(&navmap_errormsg()); + return ''; + } + } + &graders_resources_pass(\@resources,\%grader_partids_by_symb, + \%grader_randomlists_by_symb,$bubbles_per_row); + + my ($uname,$udom); my $result= <<SCANTRONFORM; <form method="post" enctype="multipart/form-data" action="/adm/grades" name="scantronupload"> <input type="hidden" name="command" value="scantron_configphase" /> @@ -6912,19 +8512,38 @@ SCANTRONFORM $r->print($result); my @delayqueue; - my %completedstudents; + my (%completedstudents,%scandata); + 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, - 'inline',undef,'scantronupload'); + my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin($r,$count); &Apache::lonhtmlcommon::Update_PrgWin($r,\%prog_state, 'Processing first student'); + $r->print('<br />'); my $start=&Time::HiRes::time(); my $i=-1; - my ($uname,$udom,$started); + my $started; - &scantron_get_maxbubble(); # Need the bubble lines array to parse. + &scantron_get_maxbubble(\$nav_error,\%scantron_config); # Need the bubble lines array to parse. + if ($nav_error) { + $r->print(&navmap_errormsg()); + return ''; + } + + # 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. + } + + my %lettdig = &letter_to_digits(); + my $numletts = scalar(keys(%lettdig)); + my %orderedforcode; while ($i<$scanlines->{'count'}) { ($uname,$udom)=('',''); @@ -6936,8 +8555,15 @@ SCANTRONFORM 'last student'); } $started=1; + my %respnumlookup = (); + my %startline = (); + my $total; my $scan_record=&scantron_parse_scanline($line,$i,\%scantron_config, - $scan_data); + $scan_data,undef,\%idmap,$randomorder, + $randompick,$sequence,\@master_seq, + \%symb_to_resource,\%grader_partids_by_symb, + \%orderedforcode,\%respnumlookup,\%startline, + \$total); unless ($uname=&scantron_find_student($scan_record,$scan_data, \%idmap,$i)) { &scantron_add_delay(\@delayqueue,$line, @@ -6949,43 +8575,164 @@ SCANTRONFORM 'Student '.$uname.' has multiple sheets',2); next; } + my $usec = $classlist->{$uname}->[&Apache::loncoursedata::CL_SECTION]; + my $user = $uname.':'.$usec; ($uname,$udom)=split(/:/,$uname); + my $scancode; + if ((exists($scan_record->{'scantron.CODE'})) && + (&Apache::lonnet::validCODE($scan_record->{'scantron.CODE'}))) { + $scancode = $scan_record->{'scantron.CODE'}; + } else { + $scancode = ''; + } + + my @mapresources = @resources; + if ($randomorder || $randompick) { + @mapresources = + &users_order($user,$scancode,$sequence,\@master_seq,\%symb_to_resource, + \%orderedforcode); + } + my (%partids_by_symb,$res_error); + foreach my $resource (@mapresources) { + my $ressymb; + if (ref($resource)) { + $ressymb = $resource->symb(); + } else { + $res_error = 1; + last; + } + if ((exists($grader_randomlists_by_symb{$ressymb})) || + (ref($grader_partids_by_symb{$ressymb}) ne 'ARRAY')) { + my ($analysis,$parts) = + &scantron_partids_tograde($resource,$env{'request.course.id'}, + $uname,$udom,undef,$bubbles_per_row); + $partids_by_symb{$ressymb} = $parts; + } else { + $partids_by_symb{$ressymb} = $grader_partids_by_symb{$ressymb}; + } + } + + if ($res_error) { + &scantron_add_delay(\@delayqueue,$line, + 'An error occurred while grading student '.$uname,2); + next; + } + &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); } - my $i=0; - foreach my $resource (@resources) { - $i++; - my %form=('submitted' =>'scantron', - 'grade_target' =>'grade', - 'grade_username'=>$uname, - 'grade_domain' =>$udom, - 'grade_courseid'=>$env{'request.course.id'}, - 'grade_symb' =>$resource->symb()); - if (exists($scan_record->{'scantron.CODE'}) - && - &Apache::lonnet::validCODE($scan_record->{'scantron.CODE'})) { - $form{'CODE'}=$scan_record->{'scantron.CODE'}; - } else { - $form{'CODE'}=''; - } - my $result=&Apache::lonnet::ssi($resource->src(),%form); - if ($result ne '') { - } - if (&Apache::loncommon::connection_aborted($r)) { last; } - } + if (&grade_student_bubbles($r,$uname,$udom,$scan_record,$scancode, + \@mapresources,\%partids_by_symb, + $bubbles_per_row,$randomorder,$randompick, + \%respnumlookup,\%startline) + eq 'ssi_error') { + $ssi_error = 0; # So end of handler error message does not trigger. + $r->print("</form>"); + &ssi_print_error($r); + $r->print(&show_grading_menu_form($symb)); + &Apache::lonnet::remove_lock($lock); + return ''; # Why return ''? Beats me. + } + + if (($scancode) && ($randomorder || $randompick)) { + my $parmresult = + &Apache::lonparmset::storeparm_by_symb($symb, + '0_examcode',2,$scancode, + 'string_examcode',$uname, + $udom); + } $completedstudents{$uname}={'line'=>$line}; - if (&Apache::loncommon::connection_aborted($r)) { last; } + if ($env{'form.verifyrecord'}) { + my $lastpos = $env{'form.scantron_maxbubble'}*$scantron_config{'Qlength'}; + if ($randompick) { + if ($total) { + $lastpos = $total*$scantron_config{'Qlength'}; + } + } + + my $studentdata = substr($line,$scantron_config{'Qstart'}-1,$lastpos); + chomp($studentdata); + $studentdata =~ s/\r$//; + my $studentrecord = ''; + my $counter = -1; + foreach my $resource (@mapresources) { + my $ressymb = $resource->symb(); + ($counter,my $recording) = + &verify_scantron_grading($resource,$udom,$uname,$env{'request.course.id'}, + $counter,$studentdata,$partids_by_symb{$ressymb}, + \%scantron_config,\%lettdig,$numletts,$randomorder, + $randompick,\%respnumlookup,\%startline); + $studentrecord .= $recording; + } + if ($studentrecord ne $studentdata) { + &Apache::lonxml::clear_problem_counter(); + if (&grade_student_bubbles($r,$uname,$udom,$scan_record,$scancode, + \@mapresources,\%partids_by_symb, + $bubbles_per_row,$randomorder,$randompick, + \%respnumlookup,\%startline) + eq 'ssi_error') { + $ssi_error = 0; # So end of handler error message does not trigger. + $r->print("</form>"); + &ssi_print_error($r); + $r->print(&show_grading_menu_form($symb)); + &Apache::lonnet::remove_lock($lock); + delete($completedstudents{$uname}); + return ''; + } + $counter = -1; + $studentrecord = ''; + foreach my $resource (@mapresources) { + my $ressymb = $resource->symb(); + ($counter,my $recording) = + &verify_scantron_grading($resource,$udom,$uname,$env{'request.course.id'}, + $counter,$studentdata,$partids_by_symb{$ressymb}, + \%scantron_config,\%lettdig,$numletts, + $randomorder,$randompick,\%respnumlookup, + \%startline); + $studentrecord .= $recording; + } + if ($studentrecord ne $studentdata) { + $r->print('<p><span class="LC_warning">'); + if ($scancode eq '') { + $r->print(&mt('Mismatch grading bubblesheet for user: [_1] with ID: [_2].', + $uname.':'.$udom,$scan_record->{'scantron.ID'})); + } else { + $r->print(&mt('Mismatch grading bubblesheet for user: [_1] with ID: [_2] and CODE: [_3].', + $uname.':'.$udom,$scan_record->{'scantron.ID'},$scancode)); + } + $r->print('</span><br />'.&Apache::loncommon::start_data_table()."\n". + &Apache::loncommon::start_data_table_header_row()."\n". + '<th>'.&mt('Source').'</th><th>'.&mt('Bubbled responses').'</th>'. + &Apache::loncommon::end_data_table_header_row()."\n". + &Apache::loncommon::start_data_table_row(). + '<td>'.&mt('Bubblesheet').'</td>'. + '<td><span class="LC_nobreak" style="white-space: pre;"><tt>'.$studentdata.'</tt></span></td>'. + &Apache::loncommon::end_data_table_row(). + &Apache::loncommon::start_data_table_row(). + '<td>'.&mt('Stored submissions').'</td>'. + '<td><span class="LC_nobreak" style="white-space: pre;"><tt>'.$studentrecord.'</tt></span></td>'."\n". + &Apache::loncommon::end_data_table_row(). + &Apache::loncommon::end_data_table().'</p>'); + } else { + $r->print('<br /><span class="LC_warning">'. + &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 />'. + &mt("As a consequence, this user's submission history records two tries."). + '</span><br />'); + } + } + } + if (&Apache::loncommon::connection_aborted($r)) { last; } } continue { &Apache::lonxml::clear_problem_counter(); - &Apache::lonnet::delenv('scantron\.'); + &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>"); @@ -6994,58 +8741,199 @@ SCANTRONFORM return ''; } +sub graders_resources_pass { + my ($resources,$grader_partids_by_symb,$grader_randomlists_by_symb, + $bubbles_per_row) = @_; + if ((ref($resources) eq 'ARRAY') && (ref($grader_partids_by_symb)) && + (ref($grader_randomlists_by_symb) eq 'HASH')) { + foreach my $resource (@{$resources}) { + my $ressymb = $resource->symb(); + my ($analysis,$parts) = + &scantron_partids_tograde($resource,$env{'request.course.id'}, + $env{'user.name'},$env{'user.domain'}, + 1,$bubbles_per_row); + $grader_partids_by_symb->{$ressymb} = $parts; + if (ref($analysis) eq 'HASH') { + if (ref($analysis->{'parts_withrandomlist'}) eq 'ARRAY') { + $grader_randomlists_by_symb->{$ressymb} = + $analysis->{'parts_withrandomlist'}; + } + } + } + } + return; +} + =pod -=item scantron_upload_scantron_data +=item users_order - Creates the screen for adding a new bubble sheet data file to a course. + Returns array of resources in current map, ordered based on either CODE, + if this is a CODEd exam, or based on student's identity if this is a + "NAMEd" exam. + + Should be used when randomorder and/or randompick applied when the + corresponding exam was printed, prior to students completing bubblesheets + for the version of the exam the student received. =cut +sub users_order { + my ($user,$scancode,$mapurl,$master_seq,$symb_to_resource,$orderedforcode) = @_; + my @mapresources; + unless ((ref($master_seq) eq 'ARRAY') && (ref($symb_to_resource) eq 'HASH')) { + return @mapresources; + } + if ($scancode) { + if ((ref($orderedforcode) eq 'HASH') && (ref($orderedforcode->{$scancode}) eq 'ARRAY')) { + @mapresources = @{$orderedforcode->{$scancode}}; + } else { + $env{'form.CODE'} = $scancode; + my $actual_seq = + &Apache::lonprintout::master_seq_to_person_seq($mapurl, + $master_seq, + $user,$scancode,1); + if (ref($actual_seq) eq 'ARRAY') { + @mapresources = map { $symb_to_resource->{$_}; } @{$actual_seq}; + if (ref($orderedforcode) eq 'HASH') { + if (@mapresources > 0) { + $orderedforcode->{$scancode} = \@mapresources; + } + } + } + delete($env{'form.CODE'}); + } + } else { + my $actual_seq = + &Apache::lonprintout::master_seq_to_person_seq($mapurl, + $master_seq, + $user,undef,1); + if (ref($actual_seq) eq 'ARRAY') { + @mapresources = + map { $symb_to_resource->{$_}; } @{$actual_seq}; + } + } + return @mapresources; +} + +sub grade_student_bubbles { + my ($r,$uname,$udom,$scan_record,$scancode,$resources,$parts,$bubbles_per_row, + $randomorder,$randompick,$respnumlookup,$startline) = @_; + my $uselookup = 0; + if (($randomorder || $randompick) && (ref($respnumlookup) eq 'HASH') && + (ref($startline) eq 'HASH')) { + $uselookup = 1; + } + + if (ref($resources) eq 'ARRAY') { + my $count = 0; + foreach my $resource (@{$resources}) { + my $ressymb = $resource->symb(); + my %form = ('submitted' => 'scantron', + 'grade_target' => 'grade', + 'grade_username' => $uname, + 'grade_domain' => $udom, + 'grade_courseid' => $env{'request.course.id'}, + 'grade_symb' => $ressymb, + 'CODE' => $scancode + ); + if ($bubbles_per_row ne '') { + $form{'bubbles_per_row'} = $bubbles_per_row; + } + if ($env{'form.scantron_lastbubblepoints'} ne '') { + $form{'scantron_lastbubblepoints'} = $env{'form.scantron_lastbubblepoints'}; + } + if (ref($parts) eq 'HASH') { + if (ref($parts->{$ressymb}) eq 'ARRAY') { + foreach my $part (@{$parts->{$ressymb}}) { + if ($uselookup) { + $form{'scantron_questnum_start.'.$part} = $startline->{$count} + 1; + } else { + $form{'scantron_questnum_start.'.$part} = + 1+$env{'form.scantron.first_bubble_line.'.$count}; + } + $count++; + } + } + } + my $result=&ssi_with_retries($resource->src(),$ssi_retries,%form); + return 'ssi_error' if ($ssi_error); + last if (&Apache::loncommon::connection_aborted($r)); + } + } + return; +} + sub scantron_upload_scantron_data { my ($r)=@_; - $r->print(&Apache::loncommon::coursebrowser_javascript($env{'request.role.domain'})); + my $dom = $env{'request.role.domain'}; + my $domdesc = &Apache::lonnet::domain($dom,'description'); + $r->print(&Apache::loncommon::coursebrowser_javascript($dom)); my $select_link=&Apache::loncommon::selectcourse_link('rules','courseid', 'domainid', - 'coursename'); - my $domsel=&Apache::loncommon::select_dom_form($env{'request.role.domain'}, - 'domainid'); - my $default_form_data=&defaultFormData(&get_symb($r,1)); - $r->print(<<UPLOAD); + 'coursename',$dom); + my $syllabuslink = '<a href="javascript:ToSyllabus();">'.&mt('Syllabus').'</a>'. + (' 'x2).&mt('(shows course personnel)'); + my ($symb) = &get_symb($r,1); + my $default_form_data=&defaultFormData($symb); + my $nofile_alert = &mt('Please use the browse button to select a file from your local directory.'); + 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."); + $r->print(' <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("'.$nofile_alert.'"); return false; } + if (formname.courseid.value == "") { + alert("'.$nocourseid_alert.'"); + return false; + } formname.submit(); } + + function ToSyllabus() { + var cdom = '."'$dom'".'; + var cnum = document.rules.courseid.value; + if (cdom == "" || cdom == null) { + return; + } + if (cnum == "" || cnum == null) { + return; + } + syllwin=window.open("/public/"+cdom+"/"+cnum+"/syllabus","LONCAPASyllabus", + "height=350,width=350,scrollbars=yes,menubar=no"); + return; + } + </script> -<form enctype='multipart/form-data' action='/adm/grades' name='rules' method='post'> -$default_form_data -<table> -<tr><td>$select_link </td></tr> -<tr><td>Course ID: </td><td><input name='courseid' type='text' /> </td></tr> -<tr><td>Course Name: </td><td><input name='coursename' type='text' /></td></tr> -<tr><td>Domain: </td><td>$domsel </td></tr> -<tr><td>File to upload:</td><td><input type="file" name="upfile" size="50" /></td></tr> -</table> -<input name='command' value='scantronupload_save' type='hidden' /> -<input type="button" onClick="javascript:checkUpload(this.form);" value="Upload Scantron Data" /> +<h3>'.&mt('Send bubblesheet data to a course').'</h3> + +<form enctype="multipart/form-data" action="/adm/grades" name="rules" method="post"> +'.$default_form_data. + &Apache::lonhtmlcommon::start_pick_box(). + &Apache::lonhtmlcommon::row_title(&mt('Course ID')). + '<input name="courseid" type="text" size="30" />'.$select_link. + &Apache::lonhtmlcommon::row_closure(). + &Apache::lonhtmlcommon::row_title(&mt('Course Name')). + '<input name="coursename" type="text" size="30" />'.$syllabuslink. + &Apache::lonhtmlcommon::row_closure(). + &Apache::lonhtmlcommon::row_title(&mt('Domain')). + '<input name="domainid" type="hidden" />'.$domdesc. + &Apache::lonhtmlcommon::row_closure(). + &Apache::lonhtmlcommon::row_title(&mt('File to upload')). + '<input type="file" name="upfile" size="50" />'. + &Apache::lonhtmlcommon::row_closure(1). + &Apache::lonhtmlcommon::end_pick_box().'<br /> + +<input name="command" value="scantronupload_save" type="hidden" /> +<input type="button" onclick="javascript:checkUpload(this.form);" value="'.&mt('Upload Bubblesheet Data').'" /> </form> -UPLOAD +'); 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)=@_; @@ -7053,12 +8941,12 @@ sub scantron_upload_scantron_data_save { my $doanotherupload= '<br /><form action="/adm/grades" method="post">'."\n". '<input type="hidden" name="command" value="scantronupload" />'."\n". - '<input type="submit" name="submit" value="Do Another Upload" />'."\n". + '<input type="submit" name="submit" value="'.&mt('Do Another Upload').'" />'."\n". '</form>'."\n"; if (!&Apache::lonnet::allowed('usc',$env{'form.domainid'}) && !&Apache::lonnet::allowed('usc', $env{'form.domainid'}.'_'.$env{'form.courseid'})) { - $r->print("You are not allowed to upload Scantron data to the requested course.<br />"); + $r->print(&mt("You are not allowed to upload bubblesheet data to the requested course.")."<br />"); if ($symb) { $r->print(&show_grading_menu_form($symb)); } else { @@ -7067,31 +8955,32 @@ sub scantron_upload_scantron_data_save { return ''; } my %coursedata=&Apache::lonnet::coursedescription($env{'form.domainid'}.'_'.$env{'form.courseid'}); - $r->print("Doing upload to ".$coursedata{'description'}." <br />"); - my $fname=$env{'form.upfile.filename'}; - #FIXME - #copied from lonnet::userfileupload() - #make that function able to target a specified course - # Replace Windows backslashes by forward slashes - $fname=~s/\\/\//g; - # Get rid of everything but the actual filename - $fname=~s/^.*\/([^\/]+)$/$1/; - # Replace spaces by underscores - $fname=~s/\s+/\_/g; - # Replace all other weird characters by nothing - $fname=~s/[^\w\.\-]//g; - # See if there is anything left - unless ($fname) { return 'error: no uploaded file'; } - my $uploadedfile=$fname; - $fname='scantron_orig_'.$fname; + my $uploadedfile; + $r->print('<p>'.&mt("Uploading file to [_1]",$coursedata{'description'}).'</p>'); if (length($env{'form.upfile'}) < 2) { - $r->print("<span class=\"LC_error\">Error:</span> The file you attempted to upload, <tt>".&HTML::Entities::encode($env{'form.upfile.filename'},'<>&"')."</tt>, contained no information. Please check that you entered the correct filename."); + $r->print( + &Apache::lonhtmlcommon::confirm_success( + &mt('The file: [_1] you attempted to upload contained no information. Please check that you entered the correct filename.', + '<span class="LC_filename">'.&HTML::Entities::encode($env{'form.upfile.filename'},'<>&"').'</span>'),1)); } else { - my $result=&Apache::lonnet::finishuserfileupload($env{'form.courseid'},$env{'form.domainid'},'upfile',$fname); - if ($result =~ m|^/uploaded/|) { - $r->print("<span class=\"LC_success\">Success:</span> Successfully uploaded ".(length($env{'form.upfile'})-1)." bytes of data into location <tt>".$result."</tt>"); + my $result = + &Apache::lonnet::userfileupload('upfile','','scantron','','','', + $env{'form.courseid'},$env{'form.domainid'}); + if ($result =~ m{^/uploaded/}) { + $r->print( + &Apache::lonhtmlcommon::confirm_success(&mt('Upload successful')).'<br />'. + &mt('Uploaded [_1] bytes of data into location: [_2]', + (length($env{'form.upfile'})-1), + '<span class="LC_filename">'.$result.'</span>')); + ($uploadedfile) = ($result =~ m{/([^/]+)$}); + $r->print(&validate_uploaded_scantron_file($env{'form.domainid'}, + $env{'form.courseid'},$uploadedfile)); } else { - $r->print("<span class=\"LC_error\">Error:</span> An error (".$result.") occurred when attempting to upload the file, <tt>".&HTML::Entities::encode($env{'form.upfile.filename'},'<>&"')."</tt>"); + $r->print( + &Apache::lonhtmlcommon::confirm_success(&mt('Upload failed'),1).'<br />'. + &mt('An error ([_1]) occurred when attempting to upload the file: [_2]', + $result, + '<span class="LC_filename">'.&HTML::Entities::encode($env{'form.upfile.filename'},'<>&"').'</span>')); } } if ($symb) { @@ -7102,13 +8991,115 @@ sub scantron_upload_scantron_data_save { return ''; } -=pod - -=item valid_file - - Validates that the requested bubble data file exists in the course. - -=cut +sub validate_uploaded_scantron_file { + my ($cdom,$cname,$fname) = @_; + my $scanlines=&Apache::lonnet::getfile('/uploaded/'.$cdom.'/'.$cname.'/'.$fname); + my @lines; + if ($scanlines ne '-1') { + @lines=split("\n",$scanlines,-1); + } + my $output; + if (@lines) { + my (%counts,$max_match_format); + my ($found_match_count,$max_match_count,$max_match_pct) = (0,0,0); + my $classlist = &Apache::loncoursedata::get_classlist($cdom,$cname); + my %idmap = &username_to_idmap($classlist); + foreach my $key (keys(%idmap)) { + my $lckey = lc($key); + $idmap{$lckey} = $idmap{$key}; + } + my %unique_formats; + my @formatlines = &get_scantronformat_file(); + foreach my $line (@formatlines) { + chomp($line); + my @config = split(/:/,$line); + my $idstart = $config[5]; + my $idlength = $config[6]; + if (($idstart ne '') && ($idlength > 0)) { + if (ref($unique_formats{$idstart.':'.$idlength}) eq 'ARRAY') { + push(@{$unique_formats{$idstart.':'.$idlength}},$config[0].':'.$config[1]); + } else { + $unique_formats{$idstart.':'.$idlength} = [$config[0].':'.$config[1]]; + } + } + } + foreach my $key (keys(%unique_formats)) { + my ($idstart,$idlength) = split(':',$key); + %{$counts{$key}} = ( + 'found' => 0, + 'total' => 0, + ); + foreach my $line (@lines) { + next if ($line =~ /^#/); + next if ($line =~ /^[\s\cz]*$/); + my $id = substr($line,$idstart-1,$idlength); + $id = lc($id); + if (exists($idmap{$id})) { + $counts{$key}{'found'} ++; + } + $counts{$key}{'total'} ++; + } + if ($counts{$key}{'total'}) { + my $percent_match = (100*$counts{$key}{'found'})/($counts{$key}{'total'}); + if (($max_match_format eq '') || ($percent_match > $max_match_pct)) { + $max_match_pct = $percent_match; + $max_match_format = $key; + $found_match_count = $counts{$key}{'found'}; + $max_match_count = $counts{$key}{'total'}; + } + } + } + if (ref($unique_formats{$max_match_format}) eq 'ARRAY') { + my $format_descs; + my $numwithformat = @{$unique_formats{$max_match_format}}; + for (my $i=0; $i<$numwithformat; $i++) { + my ($name,$desc) = split(':',$unique_formats{$max_match_format}[$i]); + if ($i<$numwithformat-2) { + $format_descs .= '"<i>'.$desc.'</i>", '; + } elsif ($i==$numwithformat-2) { + $format_descs .= '"<i>'.$desc.'</i>" '.&mt('and').' '; + } elsif ($i==$numwithformat-1) { + $format_descs .= '"<i>'.$desc.'</i>"'; + } + } + my $showpct = sprintf("%.0f",$max_match_pct).'%'; + $output .= '<br />'; + if ($found_match_count == $max_match_count) { + # 100% matching entries + $output .= &Apache::lonhtmlcommon::confirm_success( + &mt('Comparison of student IDs: [_1] matching ([quant,_2,entry,entries])', + '<b>'.$showpct.'</b>',$found_match_count)).'<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); + } else { + # Not all entries matching? -> Show warning and additional info + $output .= + &Apache::lonhtmlcommon::confirm_success( + &mt('Comparison of student IDs: [_1] matching ([_2]/[quant,_3,entry,entries])', + '<b>'.$showpct.'</b>',$found_match_count,$max_match_count).'<br />'. + &mt('Not all entries could be matched!'),1).'<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). + '<p class="LC_info">'. + &mt('A low percentage of matches results from one of the following:'). + '</p><ul>'. + '<li>'.&mt('The file was uploaded to the wrong course.').'</li>'. + '<li>'.&mt('The data is not in the format expected for the domain: [_1]', + '<i>'.$cdom.'</i>').'</li>'. + '<li>'.&mt('Students did not bubble their IDs, or mis-bubbled them').'</li>'. + '<li>'.&mt('The course roster is not up to date.').'</li>'. + '</ul>'; + } + } + } else { + $output = '<p class="LC_warning">'.&mt('Uploaded file contained no data').'</p>'; + } + return $output; +} sub valid_file { my ($requested_file)=@_; @@ -7118,29 +9109,20 @@ 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)); + my ($symb) = &get_symb($r,1); + my $default_form_data=&defaultFormData($symb); my $cname=$env{'course.'.$env{'request.course.id'}.'.num'}; my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'}; my $file=$env{'form.scantron_selectfile'}; if (! &valid_file($file)) { - $r->print(<<ERROR); + $r->print(' <p> - The requested file name was invalid. + '.&mt('The requested filename was invalid.').' </p> -ERROR - $r->print(&show_grading_menu_form(&get_symb($r,1))); +'); + $r->print(&show_grading_menu_form($symb)); return; } my $orig='/uploaded/'.$cdom.'/'.$cname.'/scantron_orig_'.$file; @@ -7149,26 +9131,373 @@ ERROR &Apache::lonnet::allowuploaded('/adm/grades',$orig); &Apache::lonnet::allowuploaded('/adm/grades',$corrected); &Apache::lonnet::allowuploaded('/adm/grades',$skipped); - $r->print(<<DOWNLOAD); + $r->print(' <p> - <a href="$orig">Original</a> file as uploaded by the scantron office. + '.&mt('[_1]Original[_2] file as uploaded by bubblesheet scanning office.', + '<a href="'.$orig.'">','</a>').' </p> <p> - <a href="$corrected">Corrections</a>, a file of corrected records that were used in grading. + '.&mt('[_1]Corrections[_2], a file of corrected records that were used in grading.', + '<a href="'.$corrected.'">','</a>').' </p> <p> - <a href="$skipped">Skipped</a>, a file of records that were skipped. + '.&mt('[_1]Skipped[_2], a file of records that were skipped.', + '<a href="'.$skipped.'">','</a>').' </p> -DOWNLOAD - $r->print(&show_grading_menu_form(&get_symb($r,1))); +'); + $r->print(&show_grading_menu_form($symb)); 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 = &letter_to_digits(); + 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 $bubbles_per_row = &bubblesheet_bubbles_per_row(\%scantron_config); + 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(); + unless (ref($navmap)) { + $r->print(&navmap_errormsg()); + return ''; + } + my $map=$navmap->getResourceByUrl($sequence); + my ($randomorder,$randompick,@master_seq,%symb_to_resource,%grader_partids_by_symb, + %grader_randomlists_by_symb,%orderedforcode); + if (ref($map)) { + $randomorder=$map->randomorder(); + $randompick=$map->randompick(); + } + my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0); + my $nav_error = &get_master_seq(\@resources,\@master_seq,\%symb_to_resource); + if ($nav_error) { + $r->print(&navmap_errormsg()); + return ''; + } + &graders_resources_pass(\@resources,\%grader_partids_by_symb, + \%grader_randomlists_by_symb,$bubbles_per_row); + my ($uname,$udom); + 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=&get_todo_count($scanlines,$scan_data); + my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin($r,$count); + my ($username,$domain,$started); + &scantron_get_maxbubble(\$nav_error,\%scantron_config); # Need the bubble lines array to parse. + if ($nav_error) { + $r->print(&navmap_errormsg()); + return ''; + } + + &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=&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 $usec = $classlist->{$uname}->[&Apache::loncoursedata::CL_SECTION]; + my $user = $uname.':'.$usec; + ($username,$domain)=split(/:/,$uname); + + my $scancode; + if ((exists($scan_record->{'scantron.CODE'})) && + (&Apache::lonnet::validCODE($scan_record->{'scantron.CODE'}))) { + $scancode = $scan_record->{'scantron.CODE'}; + } else { + $scancode = ''; + } + + my @mapresources = @resources; + my $lastpos = $env{'form.scantron_maxbubble'}*$scantron_config{'Qlength'}; + my %respnumlookup=(); + my %startline=(); + if ($randomorder || $randompick) { + @mapresources = + &users_order($user,$scancode,$sequence,\@master_seq,\%symb_to_resource, + \%orderedforcode); + my $total = &get_respnum_lookups($sequence,$scan_data,\%idmap,$line, + $scan_record,\@master_seq,\%symb_to_resource, + \%grader_partids_by_symb,\%orderedforcode, + \%respnumlookup,\%startline); + if ($randompick && $total) { + $lastpos = $total*$scantron_config{'Qlength'}; + } + } + $scandata{$pid} = substr($line,$scantron_config{'Qstart'}-1,$lastpos); + chomp($scandata{$pid}); + $scandata{$pid} =~ s/\r$//; + + my $counter = -1; + foreach my $resource (@mapresources) { + my $parts; + my $ressymb = $resource->symb(); + if ((exists($grader_randomlists_by_symb{$ressymb})) || + (ref($grader_partids_by_symb{$ressymb}) ne 'ARRAY')) { + (my $analysis,$parts) = + &scantron_partids_tograde($resource,$env{'request.course.id'}, + $username,$domain,undef, + $bubbles_per_row); + } else { + $parts = $grader_partids_by_symb{$ressymb}; + } + ($counter,my $recording) = + &verify_scantron_grading($resource,$domain,$username,$cid,$counter, + $scandata{$pid},$parts, + \%scantron_config,\%lettdig,$numletts, + $randomorder,$randompick, + \%respnumlookup,\%startline); + $record{$pid} .= $recording; + } + } + &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('Bubblesheet').'</td><td>'.$showscandata.'</td><td rowspan="2">'.$last.'</td><td rowspan="2">'.$pid.'</td>'."\n". +'</tr>'."\n". +'<tr class="'.$css_class.'">'."\n". +'<td>'.&mt('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('Bubblesheet').'</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>'.&mt('Submissions').'</td><td><span class="LC_nobreak">'.$record{$pid}.'</span></td>'."\n". +'</tr>'."\n"; + $failed ++; + } + $numstudents ++; + } + } + } + $r->print('<p>'. + &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).', + '<b>', + $numstudents, + '</b>', + $env{'form.scantron_maxbubble'}). + '</p>' + ); + $r->print('<p>' + .&mt('Exact matches for [_1][quant,_2,student][_3].','<b>',$passed,'</b>') + .'<br />' + .&mt('Discrepancies detected for [_1][quant,_2,student][_3].','<b>',$failed,'</b>') + .'</p>'); + if ($passed) { + $r->print(&mt('Students with exact correspondence between bubblesheet 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 bubblesheet 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 bubblesheet grading pass.').'<br />'.&mt('If unexpected discrepancies were detected, it is recommended that you inspect the original bubblesheets.'); + } + $r->print('</form><br />'.$grading_menu_button); + return; +} + +sub verify_scantron_grading { + my ($resource,$domain,$username,$cid,$counter,$scandata,$partids, + $scantron_config,$lettdig,$numletts,$randomorder,$randompick, + $respnumlookup,$startline) = @_; + my ($record,%expected,%startpos); + return ($counter,$record) if (!ref($resource)); + return ($counter,$record) if (!$resource->is_problem()); + my $symb = $resource->symb(); + return ($counter,$record) if (ref($partids) ne 'ARRAY'); + foreach my $part_id (@{$partids}) { + $counter ++; + $expected{$part_id} = 0; + my $respnum = $counter; + if ($randomorder || $randompick) { + $respnum = $respnumlookup->{$counter}; + $startpos{$part_id} = $startline->{$counter} + 1; + } else { + $startpos{$part_id} = $env{"form.scantron.first_bubble_line.$counter"}; + } + if ($env{"form.scantron.sub_bubblelines.$respnum"}) { + my @sub_lines = split(/,/,$env{"form.scantron.sub_bubblelines.$respnum"}); + foreach my $item (@sub_lines) { + $expected{$part_id} += $item; + } + } else { + $expected{$part_id} = $env{"form.scantron.bubblelines.$respnum"}; + } + } + 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} .= $scantron_config->{'Qoff'}; + } + } + } 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,$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 (@{$partids}) { + 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 .= $recorded{$part_id}; + } + } + return ($counter,$record); +} + +sub letter_to_digits { + my %lettdig = ( + A => 1, + B => 2, + C => 3, + D => 4, + E => 5, + F => 6, + G => 7, + H => 8, + I => 9, + J => 0, + ); + return %lettdig; +} -=cut #-------- end of section for handling grading scantron forms ------- # @@ -7183,7 +9512,7 @@ sub show_grading_menu_form { '<input type="hidden" name="symb" value="'.&Apache::lonenc::check_encrypt($symb).'" />'."\n". '<input type="hidden" name="saveState" value="'.$env{'form.saveState'}.'" />'."\n". '<input type="hidden" name="command" value="gradingmenu" />'."\n". - '<input type="submit" name="submit" value="Grading Menu" />'."\n". + '<input type="submit" name="submit" value="'.&mt('Grading Menu').'" />'."\n". '</form>'."\n"; return $result; } @@ -7200,6 +9529,13 @@ sub savedState { return \%savedState; } +#--- Href with symb and command --- + +sub href_symb_cmd { + my ($symb,$cmd)=@_; + return '/adm/grades?symb='.&HTML::Entities::encode(&Apache::lonenc::check_encrypt($symb),'<>&"').'&command='.$cmd; +} + sub grading_menu { my ($request) = @_; my ($symb)=&get_symb($request); @@ -7215,36 +9551,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 Bubblesheets', + url => $url4, + permission => 'F', + icon => 'stat.png', + linktitle => 'Grade bubblesheet exams, upload/download bubblesheet data files, and review previously graded bubblesheet exams.' + } + ] + }); + + #$fields{'command'} = 'verify'; + #$url = &Apache::lonhtmlcommon::build_url('grades/',\%fields); # # Create the menu my $Str; @@ -7253,32 +9602,21 @@ sub grading_menu { $Str .= '<input type="hidden" name="command" value="" />'. '<input type="hidden" name="symb" value="'.&Apache::lonenc::check_encrypt($symb).'" />'."\n". '<input type="hidden" name="handgrade" value="'.$hdgrade.'" />'."\n". - '<input type="hidden" name="probTitle" value="'.$probTitle.'" ue="" />'."\n". + '<input type="hidden" name="probTitle" value="'.$probTitle.'" />'."\n". '<input type="hidden" name="saveState" value="" />'."\n". '<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="Verify Receipt" '. - $menudata->{'jscript'}. - ' onClick="javascript:checkChoice(document.forms.gradingMenu,\'5\',\'verify\')" '. - ' /></h3>'; - $Str .= (' 'x8). - ' receipt: '.&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 .="</dl>\n"; + $Str .= Apache::lonhtmlcommon::generate_menu(@menu); + #$menudata->{'jscript'} + $Str .='<hr /><input type="button" value="'.&mt('Verify Receipt No.').'" '. + ' 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) { @@ -7306,7 +9644,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; @@ -7327,6 +9665,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) { @@ -7354,7 +9693,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; @@ -7364,9 +9703,8 @@ sub submit_options { </script> GRADINGMENUJS &commonJSfunctions($request); - my $result='<h3> <span class="LC_info">Manual Grading/View Submission</span></h3>'; my ($table,undef,$hdgrade) = &showResourceInfo($symb,$probTitle); - $result.=$table; + my $result; my (undef,$sections) = &getclasslist('all','0'); my $savedState = &savedState(); my $saveCmd = ($$savedState{'saveCmd'} eq '' ? 'submission' : $$savedState{'saveCmd'}); @@ -7374,6 +9712,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". @@ -7383,102 +9730,94 @@ GRADINGMENUJS '<input type="hidden" name="gradingMenu" value="1" />'."\n". '<input type="hidden" name="showgrading" value="yes" />'."\n"; - $result.='<table border="0"><tr><td bgcolor=#777777>'."\n". - '<table width="100%" border="0"><tr bgcolor="#e6ffff"><td>'."\n". - ' <b>Select a Grading/Viewing Option</b></td></tr>'."\n". - '<tr bgcolor="#ffffe6" valign="top"><td>'."\n"; - - $result.='<table width="100%" border="0">'; - $result.='<tr bgcolor="#ffffe6" valign="top">'."\n"; - $result.='<td><b>'.&mt('Sections').'</b></td>'; - $result.='<td><b>'.&mt('Groups').'</b></td>'; - $result.='<td><b>'.&mt('Access Status').'</td>'."\n"; - $result.='<td><b>'.&mt('Submission Status').'</td>'."\n"; - $result.='</tr>'; - $result.='<tr bgcolor="#ffffe6" valign="top"><td>'."\n". - ' <select name="section" multiple="multiple" size="3">'."\n"; - if (ref($sections)) { - foreach (sort (@$sections)) { - $result.='<option value="'.$_.'" '. - ($saveSec eq $_ ? 'selected="selected"':'').'>'.$_.'</option>'."\n"; - } - } + $result.=' +<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.= '</td><td>'."\n"; - $result.= &Apache::lonstatistics::GroupSelect('group','multiple',3); - $result.='</td><td>'."\n"; - $result.=&Apache::lonhtmlcommon::StatusOptions($saveStatus,undef,3,undef,'mult'); - - $result.='</td>'; - $result.='<td><select name="submitonly" size="3">'. - '<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></td></tr>'; - - $result.='<tr bgcolor="#ffffe6"valign="top"><td colspan="3"><label>'. - '<input type="radio" name="radioChoice" value="submission" '. - ($saveCmd eq 'submission' ? 'checked="checked"' : '').' /> '.'<b>'.&mt('Current Resource').':</b> '.&mt('For one or more students'). - '</label> </td></tr>'."\n"; - - $result.='<tr bgcolor="#ffffe6"valign="top"><td colspan="3">'. - '<label><input type="radio" name="radioChoice" value="viewgrades" '. - ($saveCmd eq 'viewgrades' ? 'checked="checked"' : '').' /> '. - '<b>Current Resource:</b> For all students in selected section or course</label></td></tr>'."\n"; - - $result.='<tr bgcolor="#ffffe6"><td colspan="3"><br />'. - '<input type="button" onClick="javascript:checkChoice(this.form,\'2\');" value="Next->" />'. - '</td></tr>'."\n"; - - - $result.='<tr bgcolor="#ffffe6" valign="top"><td colspan="3">'. - '<br /><label><input type="radio" name="radioChoice" value="pickStudentPage" '. - ($saveCmd eq 'pickStudentPage' ? 'checked="checked"' : '').' /> '. - 'The <b>complete</b> set/page/sequence/folder: For one student</label></td></tr>'."\n"; - - $result.='<tr bgcolor="#ffffe6"><td colspan="3"><br />'. - '<input type="button" onClick="javascript:checkChoice(this.form,\'2\');" value="Next->" />'. - '</td></tr></table>'."\n"; - - $result.='</td>'; #<td valign="top">'; - -# $result.='<table width="100%" border="0">'; -# $result.='<tr bgcolor="#ffffe6"><td>'. -# '<input type="button" onClick="javascript:checkChoice(this.form,\'3\',\'csvform\');" value="'.&mt('Upload').'" />'. -# ' '.&mt('scores from file').' </td></tr>'."\n"; -# -# $result.='<tr bgcolor="#ffffe6"><td>'. -# '<input type="button" onClick="javascript:checkChoice(this.form,\'6\',\'processclicker\');" value="'.&mt('Process').'" />'. -# ' '.&mt('clicker file').' </td></tr>'."\n"; -# -# $result.='<tr bgcolor="#ffffe6"valign="top"><td colspan="2">'. -# '<input type="button" onClick="javascript:checkChoice(this.form,\'4\',\'scantron_selectphase\');'. -# '" value="'.&mt('Grade').'" /> scantron forms</td></tr>'."\n"; -# -# if ((&Apache::lonnet::allowed('mgr',$env{'request.course.id'})) && ($symb)) { -# $result.='<tr bgcolor="#ffffe6"valign="top"><td>'. -# '<input type="button" onClick="javascript:checkChoice(this.form,\'5\',\'verify\');" value="'.&mt('Verify').'" />'. -# ' '.&mt('receipt').': '. -# &Apache::lonnet::recprefix($env{'request.course.id'}). -# '-<input type="text" name="receipt" size="4" onChange="javascript:checkReceiptNo(this.form,\'OK\')" />'. -# '</td></tr>'."\n"; -# } -# $result.='<tr bgcolor="#ffffe6"valign="top"><td colspan="2">'. -# '<input type="button" onClick="javascript:this.form.action=\'/adm/helper/resettimes.helper\';this.form.submit();'. -# '" value="'.&mt('Manage').'" /> access times.</td></tr>'."\n"; -# $result.='<tr bgcolor="#ffffe6"valign="top"><td colspan="2">'. -# '<input type="button" onClick="javascript:this.form.command.value=\'codelist\';this.form.action=\'/adm/pickcode\';this.form.submit();'. -# '" value="'.&mt('View').'" /> saved CODEs.</td></tr>'."\n"; -# -# $result.='</table>'."\n".'</td>'; - $result.= '</tr></table>'."\n". - '</td></tr></table></form>'."\n"; + $result.=' + </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> + </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> + <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> + <input type="button" onclick="javascript:checkChoice(this.form,\'2\');" value="'.&mt('Next').' →" /> + </div> + </div> + + + <h2> + '.&mt('Grade Complete Folder for One Student').' + </h2> + <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> + <input type="button" onclick="javascript:checkChoice(this.form,\'2\');" value="'.&mt('Next').' →" /> + </div> + </div> + </form>'; + $result .= &show_grading_menu_form($symb); return $result; } @@ -7504,6 +9843,21 @@ sub init_perm { } } +sub init_old_essays { + my ($symb,$apath,$adom,$aname) = @_; + if ($symb ne '') { + my %essays = &Apache::lonnet::dump('nohist_essay_'.$apath,$adom,$aname); + if (keys(%essays) > 0) { + $old_essays{$symb} = \%essays; + } + } + return; +} + +sub reset_old_essays { + undef(%old_essays); +} + sub gather_clicker_ids { my %clicker_ids; @@ -7575,9 +9929,9 @@ 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.='<tr bgcolor=#ffffe6><td>'."\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(); &Apache::loncommon::restore_course_settings('grades_clicker', @@ -7588,9 +9942,9 @@ 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'"; + $checked{$gradingmechanism}=' checked="checked"'; } } @@ -7599,11 +9953,14 @@ 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', - ('iclicker' => 'i>clicker', - 'interwrite' => 'interwrite PRS')); + {'iclicker' => 'i>clicker', + 'interwrite' => 'interwrite PRS', + 'turning' => 'Turning Technologies'}); $symb = &Apache::lonenc::check_encrypt($symb); $result.=<<ENDUPFORM; <script type="text/javascript"> @@ -7652,14 +10009,17 @@ function sanitycheck() { <input type="hidden" name="saveState" value="$env{'form.saveState'}" /> <input type="file" name="upfile" size="50" /> <br /><label>$type: $selectform</label> -<br /><label><input type="radio" name="gradingmechanism" value="attendance" $checked{'attendance'} onClick="sanitycheck()" />$attendance </label> -<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> +<br /><label><input type="radio" name="gradingmechanism" value="attendance"$checked{'attendance'} onclick="sanitycheck()" />$attendance </label> +<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> -<br /><input type="button" onClick="javascript:checkUpload(this.form);" value="$upload" /> +<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> +<br /><input type="button" onclick="javascript:checkUpload(this.form);" value="$upload" /> </form> ENDUPFORM $result.='</td></tr></table>'."\n". @@ -7682,6 +10042,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') { @@ -7700,6 +10073,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>'; @@ -7714,16 +10089,18 @@ sub process_clicker_file { $number++; } $result.="</p>\n"; - if ($number==0) { - $result.='<span class="LC_error">'.&mt('No IDs found to determine correct answer').'</span>'; - return $result.&show_grading_menu_form($symb); - } + if ($number==0) { + $result .= + &Apache::lonhtmlcommon::confirm_success( + &mt('No IDs found to determine correct answer'),1); + return $result,.&show_grading_menu_form($symb); + } } if (length($env{'form.upfile'}) < 2) { - $result.=&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>'); + $result .= + &Apache::lonhtmlcommon::confirm_success( + &mt('The file: [_1] you attempted to upload contained no information. Please check that you entered the correct filename.', + '<span class="LC_filename">'.&HTML::Entities::encode($env{'form.upfile.filename'},'<>&"').'</span>'),1); return $result.&show_grading_menu_form($symb); } @@ -7735,7 +10112,7 @@ sub process_clicker_file { $result.=(<<ENDHEADER); <br /><table width="100%" border="0"><tr><td bgcolor="#777777"> <table width="100%" border="0"><tr bgcolor="#e6ffff"><td> -<b>$heading</b></td></tr><tr bgcolor=#ffffe6><td> +<b>$heading</b></td></tr><tr bgcolor="#ffffe6"><td> <form method="post" action="/adm/grades" name="clickeranalysis"> <input type="hidden" name="symb" value="$symb" /> <input type="hidden" name="command" value="assignclickergrades" /> @@ -7745,6 +10122,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=''; @@ -7755,13 +10135,18 @@ ENDHEADER if ($env{'form.upfiletype'} eq 'interwrite') { ($errormsg,$number)=&interwrite_eval(\@questiontitles,\%responses); } + if ($env{'form.upfiletype'} eq 'turning') { + ($errormsg,$number)=&turning_eval(\@questiontitles,\%responses); + } $result.='<br />'.&mt('Found [_1] question(s)',$number).'<br />'. '<input type="hidden" name="number" value="'.$number.'" />'. - &mt('Awarding [_1] percent for corrion(s)',$number).'<br />'. - '<input type="hidden" name="number" value="'.$number.'" />'. &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++) { @@ -7799,15 +10184,15 @@ ENDHEADER "\n".&mt("Username").": <input type='text' name='uname".$id."' /> ". "\n".&mt("Domain").": ". &Apache::loncommon::select_dom_form($env{'course.'.$env{'request.course.id'}.'.domain'},'udom'.$id).' '. - &Apache::loncommon::selectstudent_link('clickeranalysis','uname'.$id,'udom'.$id); + &Apache::loncommon::selectstudent_link('clickeranalysis','uname'.$id,'udom'.$id,0,$id); $unknown_count++; } } $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!"; + $errormsg.="Found no correct answers for grading!"; } elsif ($correct_count>1) { $result.='<br /><span class="LC_warning">'.&mt("Found [_1] entries for grading!",$correct_count).'</span>'; } @@ -7844,6 +10229,7 @@ sub iclicker_eval { $id=~s/^[\#0]+//; for (my $i=0;$i<$number;$i++) { my $idx=3+$i*6; + $entries[$idx]=~s/[^a-zA-Z0-9\.\*\-\+]+//g; push(@idresponses,$entries[$idx]); } $$responses{$id}=join(',',@idresponses); @@ -7876,30 +10262,60 @@ 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*\,//; } return ($errormsg,$number); } +sub turning_eval { + my ($questiontitles,$responses)=@_; + my $number=0; + my $errormsg=''; + foreach my $line (split(/[\n\r]/,$env{'form.upfile'})) { + my %components=&Apache::loncommon::record_sep($line); + my @entries=map {$components{$_}} (sort(keys(%components))); + if ($#entries>$number) { $number=$#entries; } + my $id=$entries[0]; + my @idresponses; + $id=~s/^[\#0]+//; + unless ($id) { next; } + for (my $idx=1;$idx<=$#entries;$idx++) { + $entries[$idx]=~s/\,/\;/g; + $entries[$idx]=~s/[^a-zA-Z0-9\.\*\-\+\;]+//g; + push(@idresponses,$entries[$idx]); + } + $$responses{$id}=join(',',@idresponses); + } + for (my $i=1; $i<=$number; $i++) { + $$questiontitles[$i]=&mt('Question [_1]',$i); + } + return ($errormsg,$number); +} + sub assign_clicker_grades { my ($r)=@_; my ($symb)=&get_symb($r); if (!$symb) {return '';} # See which part we are saving to - my ($partlist,$handgrade,$responseType) = &response_type($symb); + my $res_error; + my ($partlist,$handgrade,$responseType) = &response_type($symb,\$res_error); + if ($res_error) { + return &navmap_errormsg(); + } # FIXME: This should probably look for the first handgradeable part my $part=$$partlist[0]; # Start screen output my ($result) = &showResourceInfo($symb,$env{'form.probTitle'}); - my $heading=&mt('Assigning grades based on clicker file'); - $result.=(<<ENDHEADER); -<br /><table width="100%" border="0"><tr><td bgcolor="#777777"> -<table width="100%" border="0"><tr bgcolor="#e6ffff"><td> -<b>$heading</b></td></tr><tr bgcolor=#ffffe6><td> -ENDHEADER + $result .= '<br />'. + &Apache::loncommon::start_data_table(). + &Apache::loncommon::start_data_table_header_row(). + '<th>'.&mt('Assigning grades based on clicker file').'</th>'. + &Apache::loncommon::end_data_table_header_row(). + &Apache::loncommon::start_data_table_row().'<td>'; + # Get correct result # FIXME: Possibly need delimiter other than ":" my @correct=(); @@ -7915,25 +10331,26 @@ ENDHEADER $result.='<br /><span class="LC_warning">'. &mt('More than one correct result given for question "[_1]": [_2] versus [_3].', $env{'form.question:'.$i},$correct[$i],$input[$i]).'</span>'; - } elsif ($input[$i]) { + } elsif (($input[$i]) || ($input[$i] eq '0')) { $correct[$i]=$input[$i]; } } } } for (my $i=0;$i<$number;$i++) { - if (!$correct[$i]) { + if ((!$correct[$i]) && ($correct[$i] ne '0')) { $result.='<br /><span class="LC_error">'. &mt('No correct result given for question "[_1]"!', $env{'form.question:'.$i}).'</span>'; } } - $result.='<br />'.&mt("Correct answer: [_1]",join(', ',map { ($_?$_:'-') } @correct)); + $result.='<br />'.&mt("Correct answer: [_1]",join(', ',map { ((($_) || ($_ eq '0'))?$_:'-') } @correct)); } # Start grading my $pcorrect=$env{'form.pcorrect'}; my $pincorrect=$env{'form.pincorrect'}; my $storecount=0; + my %users=(); foreach my $key (keys(%env)) { my $user=''; if ($key=~/^form\.student\:(.*)$/) { @@ -7947,23 +10364,46 @@ ENDHEADER $user=$env{'form.multi'.$id}; } } - if ($user) { + if ($user) { + if ($users{$user}) { + $result.='<br /><span class="LC_warning">'. + &mt('More than one entry found for [_1]!','<tt>'.$user.'</tt>'). + '</span><br />'; + } + $users{$user}=1; my @answer=split(/\,/,$env{$key}); my $sum=0; + my $realnumber=$number; for (my $i=0;$i<$number;$i++) { - if ($answer[$i]) { + if ($correct[$i] eq '-') { + $realnumber--; + } elsif ($answer[$i]) { if ($gradingmechanism eq 'attendance') { $sum+=$pcorrect; + } elsif ($correct[$i] eq '*') { + $sum+=$pcorrect; } else { - if ($answer[$i] eq $correct[$i]) { - $sum+=$pcorrect; - } else { - $sum+=$pincorrect; +# We actually grade if correct or not + my $increment=$pincorrect; +# Special case: numerical answer "0" + if ($correct[$i] eq '0') { + if ($answer[$i]=~/^[0\.]+$/) { + $increment=$pcorrect; + } +# General numerical answer, both evaluate to something non-zero + } elsif ((1.0*$correct[$i]!=0) && (1.0*$answer[$i]!=0)) { + if (1.0*$correct[$i]==1.0*$answer[$i]) { + $increment=$pcorrect; + } +# Must be just alphanumeric + } elsif ($answer[$i] eq $correct[$i]) { + $increment=$pcorrect; } + $sum+=$increment; } } } - my $ave=$sum/(100*$number); + my $ave=$sum/(100*$realnumber); # Store my ($username,$domain)=split(/\:/,$user); my %grades=(); @@ -7981,23 +10421,43 @@ ENDHEADER } } # We are done - $result.='<br />'.&mt('Successfully stored grades for [_1] student(s).',$storecount). - '</td></tr></table>'."\n". - '</td></tr></table><br /><br />'."\n"; + $result.='<br />'.&mt('Successfully stored grades for [quant,_1,student].',$storecount). + '</td>'. + &Apache::loncommon::end_data_table_row(). + &Apache::loncommon::end_data_table()."<br /><br />\n"; return $result.&show_grading_menu_form($symb); } +sub navmap_errormsg { + return '<div class="LC_error">'. + &mt('An error occurred retrieving information about resources in the course.').'<br />'. + &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>'). + '</div>'; +} + +sub startpage { + my ($r,$symb,$crumbs,$onlyfolderflag,$nodisplayflag,$stuvcurrent,$stuvdisp,$nomenu,$js) = @_; + if ($nomenu) { + $r->print(&Apache::loncommon::start_page("Student's Version",$js,{'only_body' => '1'})); + } else { + $r->print(&Apache::loncommon::start_page('Grading',$js, + {'bread_crumbs' => $crumbs})); + } + unless ($nodisplayflag) { + $r->print(&Apache::lonhtmlcommon::resource_info_box($symb,$onlyfolderflag,$stuvcurrent,$stuvdisp)); + } +} + sub handler { my $request=$_[0]; &reset_caches(); - if ($env{'browser.mathml'}) { - &Apache::loncommon::content_type($request,'text/xml'); - } else { - &Apache::loncommon::content_type($request,'text/html'); + if ($request->header_only) { + &Apache::loncommon::content_type($request,'text/html'); + $request->send_http_header; + return OK; } - $request->send_http_header; - return '' if $request->header_only; &Apache::loncommon::get_unprocessed_cgi($ENV{'QUERY_STRING'}); + my $symb=&get_symb($request,1); my @commands=&Apache::loncommon::get_env_multiple('form.command'); my $command=$commands[0]; @@ -8006,10 +10466,15 @@ 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"}]; + my $start_page = &Apache::loncommon::start_page('Grading',undef, + {'bread_crumbs' => $brcrum}); if ($symb eq '' && $command eq '') { if ($env{'user.adv'}) { + &Apache::loncommon::content_type($request,'text/html'); + $request->send_http_header; + $request->print($start_page); if (($env{'form.codeone'}) && ($env{'form.codetwo'}) && ($env{'form.codethree'})) { my $token=$env{'form.codeone'}.'*'.$env{'form.codetwo'}.'*'. @@ -8019,7 +10484,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, @@ -8033,11 +10498,58 @@ sub handler { } else { $request->print(&Apache::lonxml::tokeninputfield()); } - } + } elsif ($env{'request.course.id'}) { + &init_perm(); + if (!%perm) { + $request->internal_redirect('/adm/quickgrades'); + return OK; + } else { + &Apache::loncommon::content_type($request,'text/html'); + $request->send_http_header; + $request->print($start_page); + } + } } else { - &init_perm(); + &init_perm(); + if (!$env{'request.course.id'}) { + unless ((&Apache::lonnet::allowed('usc',$env{'request.role.domain'})) && + ($command =~ /^scantronupload/)) { + # Not in a course. + $env{'user.error.msg'}="/adm/grades::vgr:0:0:Cannot display grades page outside course context"; + return HTTP_NOT_ACCEPTABLE; + } + } elsif (!%perm) { + $request->internal_redirect('/adm/quickgrades'); + } + &Apache::loncommon::content_type($request,'text/html'); + $request->send_http_header; + unless ((($command eq 'submission' || $command eq 'versionsub')) && ($perm{'vgr'})) { + $request->print($start_page); + } if ($command eq 'submission' && $perm{'vgr'}) { + my ($stuvcurrent,$stuvdisp,$versionform,$js); + if (($env{'form.student'} ne '') && ($env{'form.userdom'} ne '')) { + ($stuvcurrent,$stuvdisp,$versionform,$js) = + &choose_task_version_form($symb,$env{'form.student'}, + $env{'form.userdom'}); + } + &startpage($request,$symb,[{href=>"", text=>"Student Submissions"}],undef,undef,$stuvcurrent,$stuvdisp,undef,$js); + if ($versionform) { + $request->print($versionform); + } + $request->print('<br clear="all" />'); ($env{'form.student'} eq '' ? &listStudents($request) : &submission($request,0,0)); + } elsif ($command eq 'versionsub' && $perm{'vgr'}) { + my ($stuvcurrent,$stuvdisp,$versionform,$js) = + &choose_task_version_form($symb,$env{'form.student'}, + $env{'form.userdom'}, + $env{'form.inhibitmenu'}); + &startpage($request,$symb,[{href=>"", text=>"Previous Student Version"}],undef,undef,$stuvcurrent,$stuvdisp,$env{'form.inhibitmenu'},$js); + if ($versionform) { + $request->print($versionform); + } + $request->print('<br clear="all" />'); + $request->print(&show_previous_task_version($request,$symb)); } elsif ($command eq 'pickStudentPage' && $perm{'vgr'}) { &pickStudentPage($request); } elsif ($command eq 'displayPage' && $perm{'vgr'}) { @@ -8102,15 +10614,203 @@ 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)"); + $request->print('<p class="LC_error">'.&mt('Access Denied ([_1])',$command).'</p>'); } } + if ($ssi_error) { + &ssi_print_error($request); + } $request->print(&Apache::loncommon::end_page()); &reset_caches(); - return ''; + return OK; } 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 problem, 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 LON-CAPA 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 + + $randomorder - True if exam folder has randomorder set + $randompick - True if exam folder has randompick set + $respnumlookup - Reference to HASH mapping question numbers in bubble lines + for current line to question number used for same question + in "Master Seqence" (as seen by Course Coordinator). + $startline - Reference to hash where key is question number (0 is first) + and value is number of first bubble line for current student + or code-based randompick and/or randomorder. + + +=item scantron_get_maxbubble() : + + Arguments: + $nav_error - Reference to scalar which is a flag to indicate a + failure to retrieve a navmap object. + if $nav_error is set to 1 by scantron_get_maxbubble(), the + calling routine should trap the error condition and display the warning + found in &navmap_errormsg(). + + $scantron_config - Reference to bubblesheet format configuration hash. + + 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 bubblesheet 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 bubblesheet 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 bubblesheet 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/employee IDs + +=item navmap_errormsg() : + + Returns HTML mark-up inside a <div></div> with a link to re-initialize the course. + Should be called whenever the request to instantiate a navmap object fails. + +=back + +=cut