--- loncom/homework/grades.pm 2005/04/07 22:44:03 1.259 +++ loncom/homework/grades.pm 2010/04/12 13:11:42 1.612 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # The LON-CAPA Grading handler # -# $Id: grades.pm,v 1.259 2005/04/07 22:44:03 banghart Exp $ +# $Id: grades.pm,v 1.612 2010/04/12 13:11:42 www 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; @@ -35,41 +37,85 @@ use Apache::loncommon; use Apache::lonhtmlcommon; use Apache::lonnavmaps; use Apache::lonhomework; +use Apache::lonpickcode; use Apache::loncoursedata; -use Apache::lonmsg qw(:user_normal_msg); +use Apache::lonmsg(); use Apache::Constants qw(:common); use Apache::lonlocal; +use Apache::lonenc; use String::Similarity; +use LONCAPA; + +use POSIX qw(floor); + + -my %oldessays=(); my %perm=(); -# ----- These first few routines are general use routines.---- +# These variables are used to recover from ssi errors + +my $ssi_retries = 5; +my $ssi_error; +my $ssi_error_resource; +my $ssi_error_message; + + +sub ssi_with_retries { + my ($resource, $retries, %form) = @_; + my ($content, $response) = &Apache::loncommon::ssi_with_retries($resource, $retries, %form); + if ($response->is_error) { + $ssi_error = 1; + $ssi_error_resource = $resource; + $ssi_error_message = $response->code . " " . $response->message; + } + + return $content; + +} +# +# Prodcuces an ssi retry failure error message to the user: +# + +sub ssi_print_error { + my ($r) = @_; + my $helpurl = &Apache::loncommon::top_nav_help('Helpdesk'); + $r->print(' +
+

'.&mt('An unrecoverable network error occurred:').'

+

+'.&mt('Unable to retrieve a resource from a server:').'
+'.&mt('Resource:').' '.$ssi_error_resource.'
+'.&mt('Error:').' '.$ssi_error_message.' +

+

'. +&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.').'
'. +&mt('If the error persists, please contact the [_1] for assistance.',$helpurl). +'

'); + return; +} + # # --- Retrieve the parts from the metadata file.--- +# Returns an array of everything that the resources stores away +# + sub getpartlist { - my ($url,$symb) = @_; - my $partorder = &Apache::lonnet::metadata($url, 'partorder'); - my @parts; - if ($partorder) { - for my $part (split (/,/,$partorder)) { - if (!&Apache::loncommon::check_if_partid_hidden($part,$symb)) { - push(@parts, $part); - } - } - } else { - my $metadata = &Apache::lonnet::metadata($url, 'packages'); - foreach (split(/\,/,$metadata)) { - if ($_ =~ /^part_(.*)$/) { - if (!&Apache::loncommon::check_if_partid_hidden($1,$symb)) { - push(@parts, $1); - } - } - } + 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(); + my @metakeys = split(/,/,&Apache::lonnet::metadata($url,'keys')); + my @stores; - foreach my $part (@parts) { - my (@metakeys) = split(/,/,&Apache::lonnet::metadata($url,'keys')); + foreach my $part (@{ $partlist }) { foreach my $key (@metakeys) { if ($key =~ m/^stores_\Q$part\E_/) { push(@stores,$key); } } @@ -77,139 +123,189 @@ sub getpartlist { return @stores; } -# --- Get the symbolic name of a problem and the url -sub get_symb_and_url { - 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 (); - } - } - return ($symb,$url); -} - #--- Format fullname, username:domain if different for display #--- Use anywhere where the student names are listed sub nameUserString { my ($type,$fullname,$uname,$udom) = @_; if ($type eq 'header') { - return ' Fullname (Username)'; + return ' '.&mt('Fullname').' ('.&mt('Username').')'; } else { - return ' '.$fullname.' ('.$uname. - ($env{'user.domain'} eq $udom ? '' : ' ('.$udom.')').')'; + return ' '.$fullname.' ('.$uname. + ($env{'user.domain'} eq $udom ? '' : ' ('.$udom.')').')'; } } #--- 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 ($url,$symb) = shift; - $symb=($env{'form.symb'} ne '' ? $env{'form.symb'} : (&Apache::lonnet::symbread($url))) if ($symb eq ''); - my $allkeys = &Apache::lonnet::metadata($url,'keys'); - my %vPart; - foreach my $partid (&Apache::loncommon::get_env_multiple('form.vPart')) { - $vPart{$partid}=1; - } - my %seen = (); - my (@partlist,%handgrade,%responseType); - foreach (split(/,/,&Apache::lonnet::metadata($url,'packages'))) { - if (/^\w+response_.*/) { - my ($responsetype,$part) = split(/_/,$_,2); - my ($partid,$respid) = split(/_/,$part); - if (&Apache::loncommon::check_if_partid_hidden($partid,$symb)) { - next; - } - if (%vPart && !exists($vPart{$partid})) { - next; - } - $responsetype =~ s/response$//; # make it compatible w/ navmaps - should move to that!! - my ($value) = &Apache::lonnet::EXT('resource.'.$part.'.handgrade',$symb); - $handgrade{$part} = ($value eq 'yes' ? 'yes' : 'no'); - if (!exists($responseType{$partid})) { $responseType{$partid}={}; } - $responseType{$partid}->{$respid}=$responsetype; - next if ($seen{$partid} > 0); - $seen{$partid}++; - push @partlist,$partid; - } + my ($symb,$response_error) = @_; + + my $navmap = Apache::lonnavmaps::navmap->new(); + unless (ref($navmap)) { + if (ref($response_error)) { + $$response_error = 1; + } + return; } - return \@partlist,\%handgrade,\%responseType; + 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')); + my (%response_types,%handgrade); + foreach my $part (@{ $partlist }) { + next if (%vPart && !exists($vPart{$part})); + + my @types = $res->responseType($part); + my @ids = $res->responseIds($part); + for (my $i=0; $i < scalar(@ids); $i++) { + $response_types{$part}{$ids[$i]} = $types[$i]; + $handgrade{$part.'_'.$ids[$i]} = + &Apache::lonnet::EXT('resource.'.$part.'_'.$ids[$i]. + '.handgrade',$symb); + } + } + return ($partlist,\%handgrade,\%response_types); +} + +sub flatten_responseType { + my ($responseType) = @_; + my @part_response_id = + map { + my $part = $_; + map { + [$part,$_] + } sort(keys(%{ $responseType->{$part} })); + } sort(keys(%$responseType)); + return @part_response_id; } sub get_display_part { - my ($partID,$url,$symb)=@_; - if (!defined($symb) || $symb eq '') { - $symb=$env{'form.symb'}; - if ($symb eq '') { $symb=&Apache::lonnet::symbread($url) } - } + my ($partID,$symb)=@_; my $display=&Apache::lonnet::EXT('resource.'.$partID.'.display',$symb); if (defined($display) and $display ne '') { - $display.= " (id $partID)"; + $display.= ' (' + .&mt('Part ID: [_1]',$partID).')'; } else { $display=$partID; } return $display; } -#--- Show resource title -#--- and parts and response type -sub showResourceInfo { - my ($url,$probTitle,$checkboxes) = @_; - my $col=3; - if ($checkboxes) { $col=4; } - my $result =''. - ''."\n"; - my ($partlist,$handgrade,$responseType) = &response_type($url); - my %resptype = (); - my $hdgrade='no'; - my %partsseen; - for my $part_resID (sort keys(%$handgrade)) { - my $handgrade=$$handgrade{$part_resID}; - my ($partID,$resID) = split(/_/,$part_resID); - my $responsetype = $responseType->{$partID}->{$resID}; - $hdgrade = $handgrade if ($handgrade eq 'yes'); - $result.=''; - if ($checkboxes) { - if (exists($partsseen{$partID})) { - $result.=""; - } else { - $result.=""; + +sub reset_caches { + &reset_analyze_cache(); + &reset_perm(); +} + +{ + 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,$no_increment,$add_to_hash)=@_; + my $key = "$symb\0$uname\0$udom"; + 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 %form = ('grade_target' => 'analyze', + 'grade_domain' => $udom, + 'grade_symb' => $symb, + 'grade_courseid' => $env{'request.course.id'}, + 'grade_username' => $uname, + 'grade_noincrement' => $no_increment); + 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,$no_increment)=@_; + my $analyze = &get_analyze($symb,$uname,$udom,$no_increment); + return $analyze->{"$partid.$respid.shown"}; + } + + sub get_radiobutton_correct_foil { + my ($partid,$respid,$symb,$uname,$udom)=@_; + my $analyze = &get_analyze($symb,$uname,$udom); + my $foils = &get_order($partid,$respid,$symb,$uname,$udom); + if (ref($foils) eq 'ARRAY') { + foreach my $foil (@{$foils}) { + if ($analyze->{"$partid.$respid.foil.value.$foil"} eq 'true') { + return $foil; + } } - $partsseen{$partID}=1; } - my $display_part=&get_display_part($partID,$url); - $result.=''. - ''; -# ''; } - $result.='
'.&mt('Current Resource').': '. - $probTitle.'
 Part: '.$display_part.' '. - $resID.'Type: '.$responsetype.'
Handgrade: '.$handgrade.'
'."\n"; - return $result,$responseType,$hdgrade,$partlist,$handgrade; -} + sub scantron_partids_tograde { + my ($resource,$cid,$uname,$udom,$check_for_randomlist) = @_; + 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); + 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); + } -sub get_order { - my ($partid,$respid,$symb,$uname,$udom)=@_; - my (undef,undef,$url)=&Apache::lonnet::decode_symb($symb); - $url=&Apache::lonnet::clutter($url); - my $subresult=&Apache::lonnet::ssi($url, - ('grade_target' => 'analyze'), - ('grade_domain' => $udom), - ('grade_symb' => $symb), - ('grade_courseid' => - $env{'request.course.id'}), - ('grade_username' => $uname)); - (undef,$subresult)=split(/_HASH_REF__/,$subresult,2); - my %analyze=&Apache::lonnet::str2hash($subresult); - return ($analyze{"$partid.$respid.shown"}); } + #--- Clean response type for display -#--- Currently filters option/rank/radiobutton/match/essay response types only. +#--- Currently filters option/rank/radiobutton/match/essay/Task +# response types only. sub cleanRecord { - my ($answer,$response,$symb,$partid,$respid,$record,$order,$version) = @_; - my $grayFont = ''; + my ($answer,$response,$symb,$partid,$respid,$record,$order,$version, + $uname,$udom) = @_; + my $grayFont = ''; if ($response =~ /^(option|rank)$/) { my %answer=&Apache::lonnet::str2hash($answer); my %grading=&Apache::lonnet::str2hash($record->{$version."resource.$partid.$respid.submissiongrading"}); @@ -220,11 +316,11 @@ sub cleanRecord { } else { $toprow.=''.$answer{$foil}.' '; } - $bottomrow.=''.$grayFont.$foil.' '; + $bottomrow.=''.$grayFont.$foil.' '; } return '
'. - ''.$toprow.''. - ''. + ''.$toprow.''. + ''. $grayFont.$bottomrow.''.'
Answer
'.$grayFont.'Option ID
'.&mt('Answer').'
'.$grayFont.&mt('Option ID').'
'; } elsif ($response eq 'match') { my %answer=&Apache::lonnet::str2hash($answer); @@ -235,40 +331,40 @@ sub cleanRecord { my $item=shift(@items); if ($grading{$foil} == 1) { $toprow.=''.$item.' '; - $middlerow.=''.$grayFont.$answer{$foil}.' '; + $middlerow.=''.$grayFont.$answer{$foil}.' '; } else { $toprow.=''.$item.' '; - $middlerow.=''.$grayFont.$answer{$foil}.' '; + $middlerow.=''.$grayFont.$answer{$foil}.' '; } - $bottomrow.=''.$grayFont.$foil.' '; + $bottomrow.=''.$grayFont.$foil.' '; } return '
'. - ''.$toprow.''. - ''. + ''.$toprow.''. + ''. $middlerow.''. - ''. + ''. $bottomrow.''.'
Answer
'.$grayFont.'Item ID
'.&mt('Answer').'
'.$grayFont.&mt('Item ID').'
'.$grayFont.'Option ID
'.$grayFont.&mt('Option ID').'
'; } elsif ($response eq 'radiobutton') { my %answer=&Apache::lonnet::str2hash($answer); my ($toprow,$bottomrow); - my $correct=($order->[0])+1; - for (my $i=1;$i<=$#$order;$i++) { - my $foil=$order->[$i]; + my $correct = + &get_radiobutton_correct_foil($partid,$respid,$symb,$uname,$udom); + foreach my $foil (@$order) { if (exists($answer{$foil})) { - if ($i == $correct) { - $toprow.='true'; + if ($foil eq $correct) { + $toprow.=''.&mt('true').''; } else { - $toprow.='true'; + $toprow.=''.&mt('true').''; } } else { - $toprow.='false'; + $toprow.=''.&mt('false').''; } - $bottomrow.=''.$grayFont.$foil.' '; + $bottomrow.=''.$grayFont.$foil.' '; } return '
'. - ''.$toprow.''. - ''. - $grayFont.$bottomrow.''.'
Answer
'.$grayFont.'Option ID
'; + ''.&mt('Answer').''.$toprow.''. + ''.$grayFont.&mt('Option ID').''. + $bottomrow.''.''; } elsif ($response eq 'essay') { if (! exists ($env{'form.'.$symb})) { my (%keyhash) = &Apache::lonnet::dump('nohist_handgrade', @@ -284,6 +380,45 @@ sub cleanRecord { } $answer =~ s-\n-
-g; return '

'.&keywords_highlight($answer).'
'; + } elsif ( $response eq 'organic') { + my $result='Smile representation: "'.$answer.'"'; + my $jme=$record->{$version."resource.$partid.$respid.molecule"}; + $result.=&Apache::chemresponse::jme_img($jme,$answer,400); + return $result; + } elsif ( $response eq 'Task') { + if ( $answer eq 'SUBMITTED') { + my $files = $record->{$version."resource.$respid.$partid.bridgetask.portfiles"}; + my $result = &Apache::bridgetask::file_list($files,$uname,$udom); + return $result; + } elsif ( grep(/^\Q$version\E.*?\.instance$/, keys(%{$record})) ) { + my @matches = grep(/^\Q$version\E.*?\.instance$/, + keys(%{$record})); + return join('
',($version,@matches)); + + + } else { + my $result = + '

' + .&mt('Overall result: [_1]', + $record->{$version."resource.$respid.$partid.status"}) + .'

'; + + $result .= ''; + return $result; + } + } elsif ( $response =~ m/(?:numerical|formula)/) { + $answer = + &Apache::loncommon::format_previous_attempt_value('submission', + $answer); } return $answer; } @@ -291,8 +426,7 @@ sub cleanRecord { #-- A couple of common js functions sub commonJSfunctions { my $request = shift; - $request->print(< + $request->print(&Apache::lonhtmlcommon::scripttag(< 1) { @@ -320,18 +454,37 @@ sub commonJSfunctions { return selectOne.value; } } - COMMONJSFUNCTIONS } #--- Dumps the class list with usernames,list of sections, #--- section, ids and fullnames for each user. sub getclasslist { - my ($getsec,$filterlist) = @_; - $getsec = $getsec eq '' ? 'all' : $getsec; - my $classlist=&Apache::loncoursedata::get_classlist(); + my ($getsec,$filterlist,$getgroup) = @_; + my @getsec; + my @getgroup; + my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status')); + if (!ref($getsec)) { + if ($getsec ne '' && $getsec ne 'all') { + @getsec=($getsec); + } + } else { + @getsec=@{$getsec}; + } + if (grep(/^all$/,@getsec)) { undef(@getsec); } + if (!ref($getgroup)) { + if ($getgroup ne '' && $getgroup ne 'all') { + @getgroup=($getgroup); + } + } else { + @getgroup=@{$getgroup}; + } + if (grep(/^all$/,@getgroup)) { undef(@getgroup); } + + my ($classlist,$keylist)=&Apache::loncoursedata::get_classlist(); # Bail out if we were unable to get the classlist return if (! defined($classlist)); + &Apache::loncoursedata::get_group_memberships($classlist,$keylist); # my %sections; my %fullnames; @@ -348,18 +501,40 @@ sub getclasslist { $classlist->{$student}->[&Apache::loncoursedata::CL_FULLNAME()]; my $status = $classlist->{$student}->[&Apache::loncoursedata::CL_STATUS()]; + my $group = + $classlist->{$student}->[&Apache::loncoursedata::CL_GROUP()]; # filter students according to status selected - if ($filterlist && $env{'form.Status'} ne 'Any') { - if ($env{'form.Status'} ne $status) { - delete ($classlist->{$student}); + if ($filterlist && (!($stu_status =~ /Any/))) { + if (!($stu_status =~ $status)) { + delete($classlist->{$student}); next; } } + # filter students according to groups selected + my @stu_groups = split(/,/,$group); + if (@getgroup) { + my $exclude = 1; + foreach my $grp (@getgroup) { + foreach my $stu_group (@stu_groups) { + if ($stu_group eq $grp) { + $exclude = 0; + } + } + if (($grp eq 'none') && !$group) { + $exclude = 0; + } + } + if ($exclude) { + delete($classlist->{$student}); + } + } $section = ($section ne '' ? $section : 'none'); if (&canview($section)) { - if ($getsec eq 'all' || $getsec eq $section) { + if (!@getsec || grep(/^\Q$section\E$/,@getsec)) { $sections{$section}++; - $fullnames{$student}=$fullname; + if ($classlist->{$student}) { + $fullnames{$student}=$fullname; + } } else { delete($classlist->{$student}); } @@ -414,7 +589,7 @@ sub canview { #--- Retrieve the grade status of a student for all the parts sub student_gradeStatus { - my ($url,$symb,$udom,$uname,$partlist) = @_; + my ($symb,$udom,$uname,$partlist) = @_; my %record = &Apache::lonnet::restore($symb,$env{'request.course.id'},$udom,$uname); my %partstatus = (); foreach (@$partlist) { @@ -431,20 +606,19 @@ sub student_gradeStatus { # Use by verifyscript and viewgrades # Shows a student's view of problem and submission sub jscriptNform { - my ($url,$symb) = @_; - my $jscript=''."\n"; + "\n"); $jscript.= '
'."\n". - ''."\n". - ''."\n". + ''."\n". ''."\n". - ''."\n". - ''."\n". + ''."\n". ''."\n". ''."\n". ''."\n". @@ -452,6 +626,35 @@ sub jscriptNform { return $jscript; } + + +# Given the score (as a number [0-1] and the weight) what is the final +# point value? This function will round to the nearest tenth, third, +# or quarter if one of those is within the tolerance of .00001. +sub compute_points { + my ($score, $weight) = @_; + + my $tolerance = .00001; + my $points = $score * $weight; + + # Check for nearness to 1/x. + my $check_for_nearness = sub { + my ($factor) = @_; + my $num = ($points * $factor) + $tolerance; + my $floored_num = floor($num); + if ($num - $floored_num < 2 * $tolerance * $factor) { + return $floored_num / $factor; + } + return $points; + }; + + $points = $check_for_nearness->(10); + $points = $check_for_nearness->(3); + $points = $check_for_nearness->(4); + + return $points; +} + #------------------ End of general use routines -------------------- # @@ -459,12 +662,16 @@ sub jscriptNform { # sub most_similar { - my ($uname,$udom,$uessay)=@_; + my ($uname,$udom,$uessay,$old_essays)=@_; # ignore spaces and punctuation $uessay=~s/\W+/ /gs; +# ignore empty submissions (occuring when only files are sent) + + unless ($uessay=~/\w+/s) { return ''; } + # these will be returned. Do not care if not at least 50 percent similar my $limit=0.6; my $sname=''; @@ -472,23 +679,22 @@ sub most_similar { my $scrsid=''; my $sessay=''; # go through all essays ... - foreach my $tkey (keys %oldessays) { - my ($tname,$tdom,$tcrsid)=split(/\./,$tkey); + foreach my $tkey (keys(%$old_essays)) { + my ($tname,$tdom,$tcrsid)=map {&unescape($_)} (split(/\./,$tkey)); # ... except the same student - if (($tname ne $uname) || ($tdom ne $udom)) { - my $tessay=$oldessays{$tkey}; - $tessay=~s/\W+/ /gs; + next if (($tname eq $uname) && ($tdom eq $udom)); + my $tessay=$old_essays->{$tkey}; + $tessay=~s/\W+/ /gs; # String similarity gives up if not even limit - my $tsimilar=&String::Similarity::similarity($uessay,$tessay,$limit); + my $tsimilar=&String::Similarity::similarity($uessay,$tessay,$limit); # Found one - if ($tsimilar>$limit) { - $limit=$tsimilar; - $sname=$tname; - $sdom=$tdom; - $scrsid=$tcrsid; - $sessay=$oldessays{$tkey}; - } - } + if ($tsimilar>$limit) { + $limit=$tsimilar; + $sname=$tname; + $sdom=$tdom; + $scrsid=$tcrsid; + $sessay=$old_essays->{$tkey}; + } } if ($limit>0.6) { return ($sname,$sdom,$scrsid,$sessay,$limit); @@ -501,67 +707,101 @@ sub most_similar { #------------------------------------ Receipt Verification Routines # + +sub initialverifyreceipt { + my ($request,$symb) = @_; + &commonJSfunctions($request); + return ''. + &Apache::lonnet::recprefix($env{'request.course.id'}). + '-'. + ''."\n". + ''. + "
\n"; +} + #--- Check whether a receipt number is valid.--- sub verifyreceipt { - my $request = shift; + my ($request,$symb) = @_; my $courseid = $env{'request.course.id'}; my $receipt = &Apache::lonnet::recprefix($courseid).'-'. $env{'form.receipt'}; $receipt =~ s/[^\-\d]//g; - my $url = $env{'form.url'}; - my $symb = $env{'form.symb'}; - unless ($symb) { - $symb = &Apache::lonnet::symbread($url); - } - my $title.='

Verifying Submission Receipt '. - $receipt.'

'."\n". - 'Resource: '.$env{'form.probTitle'}.'

'."\n"; + my $title.= + '

'. + &mt('Verifying Receipt Number [_1]',$receipt). + '

'."\n"; my ($string,$contents,$matches) = ('','',0); my (undef,undef,$fullname) = &getclasslist('all','0'); my $receiptparts=0; - if ($env{"course.$courseid.receiptalg"} eq 'receipt2') { $receiptparts=1; } + if ($env{"course.$courseid.receiptalg"} eq 'receipt2' || + $env{"course.$courseid.receiptalg"} eq 'receipt3') { $receiptparts=1; } my $parts=['0']; - if ($receiptparts) { ($parts)=&response_type($url,$symb); } - foreach (sort {lc($$fullname{$a}) cmp lc($$fullname{$b}) } keys %$fullname) { + 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(). + ' '.&mt('Fullname').' '."\n". + ' '.&mt('Username').' '."\n". + ' '.&mt('Domain').' '; + if ($receiptparts) { + $header.=' '.&mt('Problem Part').' '; + } + $header.= + &Apache::loncommon::end_data_table_header_row(); + + foreach (sort + { + if (lc($$fullname{$a}) ne lc($$fullname{$b})) { + return (lc($$fullname{$a}) cmp lc($$fullname{$b})); + } + return $a cmp $b; + } (keys(%$fullname))) { my ($uname,$udom)=split(/\:/); foreach my $part (@$parts) { if ($receipt eq &Apache::lonnet::ireceipt($uname,$udom,$courseid,$symb,$part)) { - $contents.=' '."\n". + $contents.= + &Apache::loncommon::start_data_table_row(). + ' '."\n". ''.$$fullname{$_}.' '."\n". + '\');" target="_self">'.$$fullname{$_}.' '."\n". ' '.$uname.' '. ' '.$udom.' '; if ($receiptparts) { $contents.=' '.$part.' '; } - $contents.=''."\n"; + $contents.= + &Apache::loncommon::end_data_table_row()."\n"; $matches++; } } } if ($matches == 0) { - $string = $title.'No match found for the above receipt.'; + $string = $title + .'

' + .&mt('No match found for the above receipt number.') + .'

'; } else { - $string = &jscriptNform($url,$symb).$title. - 'The above receipt matches the following student'. - ($matches <= 1 ? '.' : 's.')."\n". - '
'."\n". - ''."\n". - ''."\n". - ''."\n". - ''; - if ($receiptparts) { - $string.=''; - } - $string.=''."\n".$contents. - '
 Fullname  Username  Domain  Problem Part 
'."\n"; + $string = &jscriptNform($symb).$title. + '

'. + &mt('The above receipt number matches the following [quant,_1,student].',$matches). + '

'. + $header. + $contents. + &Apache::loncommon::end_data_table()."\n"; } - return $string.&show_grading_menu_form($symb,$url); + return $string.&show_grading_menu_form($symb); } #--- This is called by a number of programs. @@ -569,25 +809,28 @@ sub verifyreceipt { #--- Also called directly when one clicks on the subm button # on the problem page. sub listStudents { - my ($request) = shift; + my ($request,$symb) = @_; - my ($symb,$url) = &get_symb_and_url($request); my $cdom = $env{"course.$env{'request.course.id'}.domain"}; my $cnum = $env{"course.$env{'request.course.id'}.num"}; my $getsec = $env{'form.section'} eq '' ? 'all' : $env{'form.section'}; + my $getgroup = $env{'form.group'} eq '' ? 'all' : $env{'form.group'}; my $submitonly= $env{'form.submitonly'} eq '' ? 'all' : $env{'form.submitonly'}; - my $viewgrade = $env{'form.showgrading'} eq 'yes' ? 'View/Grade/Regrade' : 'View'; - $env{'form.probTitle'} = $env{'form.probTitle'} eq '' ? - &Apache::lonnet::gettitle($symb) : $env{'form.probTitle'}; - - my $result='

 '.$viewgrade. - ' Submissions for a Student or a Group of Students

'; - my ($table,undef,$hdgrade,$partlist,$handgrade) = &showResourceInfo($url,$env{'form.probTitle'},($env{'form.showgrading'} eq 'yes')); - - $request->print(< + my $result='

 ' + .&mt("$viewgrade Submissions for a Student or a Group of Students") + .'

'; + + my ($partlist,$handgrade,$responseType) = &response_type($symb +#,$res_error + ); + + 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(&Apache::lonhtmlcommon::scripttag(< LISTJAVASCRIPT &commonJSfunctions($request); $request->print($result); - my $checkhdgrade = ($env{'form.handgrade'} eq 'yes' && scalar(@$partlist) > 1 ) ? 'checked' : ''; - my $checklastsub = $checkhdgrade eq '' ? 'checked' : ''; + my $checkhdgrade = ($env{'form.handgrade'} eq 'yes' && scalar(@$partlist) > 1 ) ? 'checked="checked"' : ''; + my $checklastsub = $checkhdgrade eq '' ? 'checked="checked"' : ''; my $gradeTable='
'. - "\n".$table. - ' View Problem Text: no '."\n". - ' one student '."\n". - ' all students
'."\n". - ' View Answer: no '."\n". - ' one student '."\n". - ' all students
'."\n". - ' Submissions: '."\n"; + "\n"; + + $gradeTable .= &Apache::lonhtmlcommon::start_pick_box(); + $gradeTable .= &Apache::lonhtmlcommon::row_title(&mt('View Problem Text')) + .''."\n" + .''."\n" + .'
'."\n" + .&Apache::lonhtmlcommon::row_closure(); + $gradeTable .= &Apache::lonhtmlcommon::row_title(&mt('View Answer')) + .''."\n" + .''."\n" + .'
'."\n" + .&Apache::lonhtmlcommon::row_closure(); + + my $submission_options; if ($env{'form.handgrade'} eq 'yes' && scalar(@$partlist) > 1) { - $gradeTable.=' essay part only'."\n"; + $submission_options.= + ''."\n"; } - - my $saveStatus = $env{'form.Status'} eq '' ? 'Active' : $env{'form.Status'}; + my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status')); + my $saveStatus = $stu_status eq '' ? 'Active' : $stu_status; $env{'form.Status'} = $saveStatus; + $submission_options.= + ''. + ''."\n". + ''. + ''."\n". + ''. + ''."\n". + ''. + ''; + $gradeTable .= &Apache::lonhtmlcommon::row_title(&mt('Submissions')) + .$submission_options + .&Apache::lonhtmlcommon::row_closure(); + + $gradeTable .= &Apache::lonhtmlcommon::row_title(&mt('Grading Increments')) + .'' + .&Apache::lonhtmlcommon::row_closure(); - $gradeTable.=' last submission only'."\n". - ' last submission & parts info'."\n". - ' by dates and submissions'."\n". - ' all details'."\n". - ''."\n". + $gradeTable .= + &build_section_inputs(). ''."\n". '
'."\n". '
'."\n". ''."\n". - ''."\n". - ''."\n". - ''."\n". + ''."\n". ''."\n"; if (exists($env{'form.gradingMenu'}) && exists($env{'form.Status'})) { - $gradeTable.=''."\n"; + $gradeTable .= ''."\n"; } else { - $gradeTable.='Student Status: '. - &Apache::lonhtmlcommon::StatusOptions($saveStatus,undef,1,'javascript:reLoadList(this.form);').'
'; + $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.
'."\n". - ''."\n"; + $gradeTable .= &Apache::lonhtmlcommon::row_title(&mt('Check For Plagiarism')) + .'' + .&Apache::lonhtmlcommon::row_closure(1) + .&Apache::lonhtmlcommon::end_pick_box(); + + $gradeTable .= '

' + .&mt('To '.lc($viewgrade)." a submission or a group of submissions, click on the check box(es) next to the student's name(s). Then click on the Next button.")."\n" + .'' + .'

'; # checkall buttons $gradeTable.=&check_script('gradesub', 'stuinfo'); $gradeTable.='
'."\n"; + 'onclick="javascript:checkSelect(this.form.stuinfo);" '."\n". + 'value="'.&mt('Next').' →" />
'."\n"; $gradeTable.=&check_buttons(); - $gradeTable.='Check For Plagiarism'; - my ($classlist, undef, $fullname) = &getclasslist($getsec,'1'); - $gradeTable.='
'. - ''; + my ($classlist, undef, $fullname) = &getclasslist($getsec,'1',$getgroup); + $gradeTable.= &Apache::loncommon::start_data_table(). + &Apache::loncommon::start_data_table_header_row(); my $loop = 0; while ($loop < 2) { - $gradeTable.=''. - ''; - if ($env{'form.showgrading'} eq 'yes' && $submitonly ne 'all') { - foreach (sort(@$partlist)) { - my $display_part=&get_display_part((split(/_/))[0],$url,$symb); - $gradeTable.=''; + $gradeTable.=''. + ''; + if ($env{'form.showgrading'} eq 'yes' + && $submitonly ne 'queued' + && $submitonly ne 'all') { + foreach my $part (sort(@$partlist)) { + my $display_part= + &get_display_part((split(/_/,$part))[0],$symb); + $gradeTable.= + ''; } + } elsif ($submitonly eq 'queued') { + $gradeTable.=''; } $loop++; # $gradeTable.='' if ($loop%2 ==1); } - $gradeTable.=''."\n"; + $gradeTable.=&Apache::loncommon::end_data_table_header_row()."\n"; my $ctr = 0; - foreach my $student (sort {lc($$fullname{$a}) cmp lc($$fullname{$b}) } keys %$fullname) { + foreach my $student (sort + { + if (lc($$fullname{$a}) ne lc($$fullname{$b})) { + return (lc($$fullname{$a}) cmp lc($$fullname{$b})); + } + return $a cmp $b; + } + (keys(%$fullname))) { my ($uname,$udom) = split(/:/,$student); + my %status = (); - if ($env{'form.showgrading'} eq 'yes' && $submitonly ne 'all') { - (%status) =&student_gradeStatus($url,$symb,$udom,$uname,$partlist); + + if ($submitonly eq 'queued') { + my %queue_status = + &Apache::bridgetask::get_student_status($symb,$cdom,$cnum, + $udom,$uname); + next if (!defined($queue_status{'gradingqueue'})); + $status{'gradingqueue'} = $queue_status{'gradingqueue'}; + } + + if ($env{'form.showgrading'} eq 'yes' + && $submitonly ne 'queued' + && $submitonly ne 'all') { + (%status) =&student_gradeStatus($symb,$udom,$uname,$partlist); my $submitted = 0; my $graded = 0; my $incorrect = 0; @@ -724,56 +1025,66 @@ LISTJAVASCRIPT $ctr++; my $section = $classlist->{$student}->[&Apache::loncoursedata::CL_SECTION()]; - + my $group = $classlist->{$student}->[&Apache::loncoursedata::CL_GROUP()]; if ( $perm{'vgr'} eq 'F' ) { - $gradeTable.='' if ($ctr%2 ==1); + if ($ctr%2 ==1) { + $gradeTable.= &Apache::loncommon::start_data_table_row(); + } $gradeTable.=''. - ''."\n".''."\n"; + ' '.$section.($group ne '' ?'/'.$group:'').''."\n"; if ($env{'form.showgrading'} eq 'yes' && $submitonly ne 'all') { - foreach (sort keys(%status)) { - next if (/^resource.*?submitted_by$/); - $gradeTable.=''."\n"; + foreach (sort(keys(%status))) { + next if ($_ =~ /^resource.*?submitted_by$/); + $gradeTable.=''."\n"; } } # $gradeTable.='' if ($ctr%2 ==1); - $gradeTable.=''."\n" if ($ctr%2 ==0); + if ($ctr%2 ==0) { + $gradeTable.=&Apache::loncommon::end_data_table_row()."\n"; + } } } if ($ctr%2 ==1) { $gradeTable.=''; - if ($env{'form.showgrading'} eq 'yes' && $submitonly ne 'all') { + if ($env{'form.showgrading'} eq 'yes' + && $submitonly ne 'queued' + && $submitonly ne 'all') { foreach (@$partlist) { $gradeTable.=''; } + } elsif ($submitonly eq 'queued') { + $gradeTable.=''; } - $gradeTable.=''; + $gradeTable.=&Apache::loncommon::end_data_table_row(); } - $gradeTable.='
 No.  Select '.&nameUserString('header').' Section/Group Part: '.$display_part. - ' Status '.&mt('No.').''.&mt('Select').''.&nameUserString('header').' '.&mt('Section/Group').''.&mt('Part: [_1] Status',$display_part).''.&mt('Queue Status').' 
'.$ctr.' '. &nameUserString(undef,$$fullname{$student},$uname,$udom). - ' '.$section.' '.$status{$_}.'  '.&mt($status{$_}).' 
     
'."\n". - '
'."\n"; + $gradeTable.=&Apache::loncommon::end_data_table()."\n". + ''."\n"; if ($ctr == 0) { my $num_students=(scalar(keys(%$fullname))); if ($num_students eq 0) { - $gradeTable='
 There are no students currently enrolled.'; + $gradeTable='
 '.&mt('There are no students currently enrolled.').''; } else { my $submissions='submissions'; if ($submitonly eq 'incorrect') { $submissions = 'incorrect submissions'; } if ($submitonly eq 'graded' ) { $submissions = 'ungraded submissions'; } - $gradeTable='
 '. - 'No '.$submissions.' found for this resource for any students. ('.$num_students. - ' students checked for '.$submissions.')
'; + if ($submitonly eq 'queued' ) { $submissions = 'queued submissions'; } + $gradeTable='
 '. + &mt('No '.$submissions.' found for this resource for any students. ([_1] students checked for '.$submissions.')', + $num_students). + '
'; } } elsif ($ctr == 1) { - $gradeTable =~ s/type=checkbox/type=checkbox checked/; + $gradeTable =~ s/type="checkbox"/type="checkbox" checked="checked"/; } - $gradeTable.=&show_grading_menu_form($symb,$url); + $gradeTable.=&show_grading_menu_form($symb); $request->print($gradeTable); return ''; } @@ -782,7 +1093,7 @@ LISTJAVASCRIPT sub check_script { my ($form, $type)=@_; - my $chkallscript=''."\n"; +'."\n"); return $chkallscript; } sub check_buttons { - my $buttons.=''; - $buttons.=' '; - $buttons.=''; + my $buttons.=''; + $buttons.=' '; + $buttons.=''; $buttons.=' '; return $buttons; } @@ -832,8 +1143,8 @@ sub processGroup { my @stuchecked = &Apache::loncommon::get_env_multiple('form.stuinfo'); my $total = scalar(@stuchecked)-1; - foreach (@stuchecked) { - my ($uname,$udom,$fullname) = split(/:/); + foreach my $student (@stuchecked) { + my ($uname,$udom,$fullname) = split(/:/,$student); $env{'form.student'} = $uname; $env{'form.userdom'} = $udom; $env{'form.fullname'} = $fullname; @@ -851,8 +1162,8 @@ sub processGroup { #--- Javascript to handle the submission page functionality --- sub sub_page_js { my $request = shift; - $request->print(< + my $alertmsg = &mt('A number equal or greater than 0 is expected. Entered value = '); + $request->print(&Apache::lonhtmlcommon::scripttag(< SUBJAVASCRIPT } @@ -1029,10 +1339,81 @@ sub sub_page_kw_js { my $request = shift; my $iconpath = $request->dir_config('lonIconsURL'); &commonJSfunctions($request); + + my $inner_js_msg_central= &Apache::lonhtmlcommon::scripttag(< 1, + 'only_body' => 1, + 'bgcolor' =>'#FFFFFF',}); + my $end_page_msg_central = + &Apache::loncommon::end_page({'js_ready' => 1}); + + + my $start_page_highlight_central = + &Apache::loncommon::start_page('Highlight Central', + $inner_js_highlight_central, + {'js_ready' => 1, + 'only_body' => 1, + 'bgcolor' =>'#FFFFFF',}); + my $end_page_highlight_central = + &Apache::loncommon::end_page({'js_ready' => 1}); + my $docopen=&Apache::lonhtmlcommon::javascript_docopen(); $docopen=~s/^document\.//; - $request->print(< + my $alertmsg = &mt('Please select a word or group of words from document and then click this link.'); + $request->print(&Apache::lonhtmlcommon::scripttag(<"); - pDoc.write("Message Central"); - - pDoc.write(" SUBJAVASCRIPT } +sub get_increment { + my $increment = $env{'form.increment'}; + if ($increment != 1 && $increment != .5 && $increment != .25 && + $increment != .1) { + $increment = 1; + } + return $increment; +} + +sub gradeBox_start { + return ( + &Apache::loncommon::start_data_table() + .&Apache::loncommon::start_data_table_header_row() + .''.&mt('Part').'' + .''.&mt('Points').'' + .' ' + .''.&mt('Assign Grade').'' + .''.&mt('Weight').'' + .''.&mt('Grade Status').'' + .&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 = ''; - + my $checkIcon = ''.&mt('Check Mark').
+	''; my $wgt = &Apache::lonnet::EXT('resource.'.$partid.'.weight',$symb,$udom,$uname); - my $wgtmsg = ($wgt > 0 ? '(problem weight)' : - 'problem weight assigned by computer'); + my $wgtmsg = ($wgt > 0) ? &mt('(problem weight)') + : ''.&mt('problem weight assigned by computer').''; $wgt = ($wgt > 0 ? $wgt : '1'); my $score = ($$record{'resource.'.$partid.'.awarded'} eq '' ? - '' : $$record{'resource.'.$partid.'.awarded'}*$wgt); + '' : &compute_points($$record{'resource.'.$partid.'.awarded'},$wgt)); my $result=''."\n"; - - my $display_part=&get_display_part($partid,undef,$symb); - $result.='
'. - 'Part: '.$display_part.' Points: '."\n"; - + my $display_part= &get_display_part($partid,$symb); + my %last_resets = &get_last_resets($symb,$env{'request.course.id'}, + [$partid]); + my $aggtries = $$record{'resource.'.$partid.'.tries'}; + if ($last_resets{$partid}) { + $aggtries = &get_num_tries($record,$last_resets{$partid},$partid); + } + $result.=&Apache::loncommon::start_data_table_row(); my $ctr = 0; - $result.=''."\n"; # display radio buttons in a nice table 10 across - while ($ctr<=$wgt) { - $result.= '\n"; - $result.=(($ctr+1)%10 == 0 ? '' : ''); + my $thisweight = 0; + my $increment = &get_increment(); + + my $radio.='
'.$ctr."
'."\n"; # display radio buttons in a nice table 10 across + while ($thisweight<=$wgt) { + $radio.= '\n"; + $radio.=(($ctr+1)%10 == 0 ? '' : ''); + $thisweight += $increment; $ctr++; } - $result.='
'; + $radio.='
'; - $result.=' or '."\n"; - $result.=''."\n"; - $result.='/'.$wgt.' '.$wgtmsg. + $line.='/'.$wgt.' '.$wgtmsg. ($$record{'resource.'.$partid.'.solved'} eq 'correct_by_student' ? ' '.$checkIcon : ''). - ' '."\n"; - - $result.=''."\n"; if ($$record{'resource.'.$partid.'.solved'} eq 'excused') { - $result.=''. - ''; + $line.=''. + ''; } else { - $result.=''. - ''; + $line.=''. + ''; } - $result.=''."\n"; - $result.="  \n"; + $line.=''."\n"; + + + $result .= + ''.$display_part.''.$radio.''.&mt('or').''.$line.''; + $result.=&Apache::loncommon::end_data_table_row(); $result.=''."\n". ''."\n". ''."\n"; - $result.=''."\n"; + $$record{'resource.'.$partid.'.solved'}.'" />'."\n". + ''."\n". + ''."\n"; + my $res_error; + $result.=&handback_box($symb,$uname,$udom,$counter,$partid,$record,\$res_error); + if ($res_error) { + return &navmap_errormsg(); + } return $result; } +sub handback_box { + 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); + foreach my $part_response_id (@part_response_id) { + my ($part,$resp) = @{ $part_response_id }; + if ($part eq $partid) { + push(@respids,$resp); + } + } + my $result; + foreach my $respid (@respids) { + my $prefix = $counter.'_'.$partid.'_'.$respid.'_'; + my $files=&get_submitted_files($udom,$uname,$partid,$respid,$record); + next if (!@$files); + my $file_counter = 1; + foreach my $file (@$files) { + if ($file =~ /\/portfolio\//) { + my ($file_path, $file_disp) = ($file =~ m|(.+/)(.+)$|); + my ($name,$version,$ext) = &file_name_version_ext($file_disp); + $file_disp = "$name.$ext"; + $file = $file_path.$file_disp; + $result.=&mt('Return commented version of [_1] to student.', + ''.$file_disp.''); + $result.=''."\n"; + $result.='
'; + $result.='('.&mt('File will be uploaded when you click on Save & Next below.').')
'; + $file_counter++; + } + } + } + return $result; +} + sub show_problem { - my ($request,$symb,$uname,$udom,$removeform,$viewon,$mode) = @_; + my ($request,$symb,$uname,$udom,$removeform,$viewon,$mode,$form) = @_; my $rendered; + my %form = ((ref($form) eq 'HASH')? %{$form} : ()); + &Apache::lonxml::remember_problem_counter(); if ($mode eq 'both' or $mode eq 'text') { $rendered=&Apache::loncommon::get_student_view($symb,$uname,$udom, - $env{'request.course.id'}); + $env{'request.course.id'}, + undef,\%form); } if ($removeform) { $rendered=~s|||g; $rendered=~s|||g; - $rendered=~s|name="submit"|name="would_have_been_submit"|g; + $rendered=~s|(]*name\s*=\s*"?)(\w+)("?)|$1would_have_been_$2$3|g; } my $companswer; if ($mode eq 'both' or $mode eq 'answer') { - $companswer=&Apache::loncommon::get_student_answers($symb,$uname,$udom, - $env{'request.course.id'}); + &Apache::lonxml::restore_problem_counter(); + $companswer= + &Apache::loncommon::get_student_answers($symb,$uname,$udom, + $env{'request.course.id'}, + %form); } if ($removeform) { $companswer=~s|||g; $companswer=~s|||g; $companswer=~s|name="submit"|name="would_have_been_submit"|g; } - my $result.='
'; - $result.=''; - if ($viewon) { - $result.=''; - } + $rendered= + '
' + .'

'.&mt('View of the problem').'

' + .$rendered + .'
'; + $companswer= + '
' + .'

'.&mt('Correct answer').'

' + .$companswer + .'
'; + my $result; if ($mode eq 'both') { - $result.='
'; - if ($mode eq 'both' or $mode eq 'text') { - $result.='View of the problem - '; - } else { - $result.='Correct answer: '; - } - $result.=$env{'form.fullname'}.'
'.$rendered.'
'; - $result.='Correct answer:
'.$companswer; + $result=$rendered.$companswer; } elsif ($mode eq 'text') { - $result.='
'.$rendered; + $result=$rendered; } elsif ($mode eq 'answer') { - $result.='
'.$companswer; + $result=$companswer; } - $result.='
'; - $result.='

'; return $result; } +sub files_exist { + my ($r, $symb) = @_; + my @students = &Apache::loncommon::get_env_multiple('form.stuinfo'); + + foreach my $student (@students) { + my ($uname,$udom,$fullname) = split(/:/,$student); + my %record = &Apache::lonnet::restore($symb,$env{'request.course.id'}, + $udom,$uname); + my ($string,$timestamp)= &get_last_submission(\%record); + foreach my $submission (@$string) { + my ($partid,$respid) = + ($submission =~ /^resource\.([^\.]*)\.([^\.]*)\.submission/); + my $files=&get_submitted_files($udom,$uname,$partid,$respid, + \%record); + return 1 if (@$files); + } + } + return 0; +} + +sub download_all_link { + my ($r,$symb) = @_; + my $all_students = + join("\n", &Apache::loncommon::get_env_multiple('form.stuinfo')); + + my $parts = + 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,}); + $r->print(''. + &mt('Download All Submitted Documents').''); + return +} + +sub build_section_inputs { + my $section_inputs; + if ($env{'form.section'} eq '') { + $section_inputs .= ''."\n"; + } else { + my @sections = &Apache::loncommon::get_env_multiple('form.section'); + foreach my $section (@sections) { + $section_inputs .= ''."\n"; + } + } + return $section_inputs; +} + # --------------------------- show submissions of a student, option to grade sub submission { - my ($request,$counter,$total) = @_; - - (my $url=$env{'form.url'})=~s-^http://($ENV{'SERVER_NAME'}|$ENV{'HTTP_HOST'})--; + my ($request,$counter,$total,$symb) = @_; my ($uname,$udom) = ($env{'form.student'},$env{'form.userdom'}); $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=($env{'form.symb'} ne '' ? $env{'form.symb'} : (&Apache::lonnet::symbread($url))); - if ($symb eq '') { $request->print("Unable to handle ambiguous references:$url:."); return ''; } + my $probtitle=&Apache::lonnet::gettitle($symb); + if ($symb eq '') { $request->print("Unable to handle ambiguous references:."); return ''; } if (!&canview($usec)) { - $request->print('Unable to view requested student.('. - $uname.'@'.$udom.' in section '.$usec.' in course id '. - $env{'request.course.id'}.')'); - $request->print(&show_grading_menu_form($symb,$url)); + $request->print('Unable to view requested student.('. + $uname.':'.$udom.' in section '.$usec.' in course id '. + $env{'request.course.id'}.')'); + $request->print(&show_grading_menu_form($symb)); return; } @@ -1460,24 +1906,19 @@ sub submission { if (!$env{'form.vProb'}) { $env{'form.vProb'} = 'yes'; } if (!$env{'form.vAns'}) { $env{'form.vAns'} = 'yes'; } my $last = ($env{'form.lastSub'} eq 'last' ? 'last' : ''); - my $checkIcon = ''; + my %old_essays; # header info if ($counter == 0) { &sub_page_js($request); &sub_page_kw_js($request) if ($env{'form.handgrade'} eq 'yes'); - $env{'form.probTitle'} = $env{'form.probTitle'} eq '' ? - &Apache::lonnet::gettitle($symb) : $env{'form.probTitle'}; - - $request->print('

 Submission Record

'."\n". - ' Resource: '.$env{'form.probTitle'}.''."\n"); - - if ($env{'form.handgrade'} eq 'no') { - my $checkMark='

 Note: Part(s) graded correct by the computer is marked with a '. - $checkIcon.' symbol.'."\n"; - $request->print($checkMark); + if ($env{'form.handgrade'} eq 'yes' && &files_exist($request, $symb)) { + &download_all_link($request, $symb); } + $request->print('

 '.&mt('Submission Record').'

'); # option to display problem, only once else it cause problems # with the form later since the problem has a form. @@ -1490,9 +1931,10 @@ sub submission { } elsif ($env{'form.vAns'} eq 'yes') { $mode='answer'; } + &Apache::lonxml::clear_problem_counter(); $request->print(&show_problem($request,$symb,$uname,$udom,0,1,$mode)); } - + # kwclr is the only variable that is guaranteed to be non blank # if this subroutine has been called once. my %keyhash = (); @@ -1507,29 +1949,27 @@ sub submission { $env{'form.kwsize'} = $keyhash{$loginuser.'_kwsize'} ne '' ? $keyhash{$loginuser.'_kwsize'} : '0'; $env{'form.kwstyle'} = $keyhash{$loginuser.'_kwstyle'} ne '' ? $keyhash{$loginuser.'_kwstyle'} : ''; $env{'form.msgsub'} = $keyhash{$symb.'_subject'} ne '' ? - $keyhash{$symb.'_subject'} : $env{'form.probTitle'}; + $keyhash{$symb.'_subject'} : $probtitle; $env{'form.savemsgN'} = $keyhash{$symb.'_savemsgN'} ne '' ? $keyhash{$symb.'_savemsgN'} : '0'; } my $overRideScore = $env{'form.overRideScore'} eq '' ? 'no' : $env{'form.overRideScore'}; - - $request->print('
'."\n". + my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status')); + $request->print(''."\n". ''."\n". ''."\n". - ''."\n". + ''."\n". ''."\n". - ''."\n". ''."\n". ''."\n". ''."\n". - ''."\n". - ''."\n". + ''."\n". ''."\n". ''."\n". ''."\n". ''."\n". - ''."\n". - ''."\n". - ''."\n". + &build_section_inputs(). + ''."\n". + ''."\n". ''."\n"); if ($env{'form.handgrade'} eq 'yes') { @@ -1563,24 +2003,49 @@ sub submission { # $request->print(<Keyword Options:  -List    -List    +Paste Selection to List    -Highlight Attribute

+Highlight Attribute

KEYWORDS # # Load the other essays for similarity check # - my $essayurl=&Apache::lonnet::declutter($url); - my ($adom,$aname,$apath)=($essayurl=~/^(\w+)\/(\w+)\/(.*)$/); - $apath=&Apache::lonnet::escape($apath); + my (undef,undef,$essayurl) = &Apache::lonnet::decode_symb($symb); + my ($adom,$aname,$apath)=($essayurl=~/^($LONCAPA::domain_re)\/($LONCAPA::username_re)\/(.*)$/); + $apath=&escape($apath); $apath=~s/\W/\_/gs; - %oldessays=&Apache::lonnet::dump('nohist_essay_'.$apath,$adom,$aname); + %old_essays=&Apache::lonnet::dump('nohist_essay_'.$apath,$adom,$aname); } } +# This is where output for one specific student would start + my $add_class = ($counter%2) ? ' LC_grade_show_user_odd_row' : ''; + $request->print( + "\n\n" + .'
' + .'

'.&nameUserString(undef,$env{'form.fullname'},$uname,$udom).'

' + ."\n" + ); + + # Show additional functions if allowed + if ($perm{'vgr'}) { + $request->print( + &Apache::loncommon::track_student_link( + &mt('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') { - $request->print('


') if ($counter > 0); my $mode; if ($env{'form.vProb'} eq 'all' && $env{'form.vAns'} eq 'all') { $mode='both'; @@ -1589,98 +2054,68 @@ KEYWORDS } elsif ($env{'form.vAns'} eq 'all') { $mode='answer'; } - $request->print(&show_problem($request,$symb,$uname,$udom,1,1,$mode)); + &Apache::lonxml::clear_problem_counter(); + $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($url,$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 ? '' : '
')); - my $result='
'."\n". - '\n"; - if ($$timestamp eq '') { - $lastsubonly.='
'."\n"; - $result.='Fullname: '.&nameUserString(undef,$env{'form.fullname'},$uname,$udom).'
'."\n"; + my $result='
' + .'

'.&mt('Submissions').'

'; $result.=''."\n"; + '" value="'.$env{'form.fullname'}.'" />'."\n"; + if ($env{'form.handgrade'} eq 'no') { + $result.='

' + .&mt('Part(s) graded correct by the computer is marked with a [_1] symbol.',$checkIcon) + ."

\n"; + } # If any part of the problem is an essay-response (handgraded), then check for collaborators - my @col_fullnames; - my ($classlist,$fullname); + my $fullname; + my $col_fullnames = []; if ($env{'form.handgrade'} eq 'yes') { - ($classlist,undef,$fullname) = &getclasslist('all','0'); - for (keys (%$handgrade)) { - my $ncol = &Apache::lonnet::EXT('resource.'.$_. - '.maxcollaborators', - $symb,$udom,$uname); - next if ($ncol <= 0); - s/\_/\./g; - next if ($record{'resource.'.$_.'.collaborators'} eq ''); - my @goodcollaborators = (); - my @badcollaborators = (); - foreach (split(/,?\s+/,$record{'resource.'.$_.'.collaborators'})) { - $_ =~ s/[\$\^\(\)]//g; - next if ($_ eq ''); - my ($co_name,$co_dom) = split /\@|:/,$_; - $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 - my @Matches = grep /^$co_name:$co_dom$/i,keys %$classlist; - if (! scalar(@Matches)) { - push @badcollaborators,$_; - } else { - push @goodcollaborators, @Matches; - } - } - if (scalar(@goodcollaborators) != 0) { - $result.='Collaborators: '; - foreach (@goodcollaborators) { - my ($lastname,$givenn) = split(/,/,$$fullname{$_}); - push @col_fullnames, $givenn.' '.$lastname; - $result.=$$fullname{$_}.'     '; - } - $result.='
'."\n"; - my ($part)=split(/\./,$_); - $result.=''. - "\n"; - } - if (scalar(@badcollaborators) > 0) { - $result.='
'; - $result.='This student has submitted '; - $result.=(scalar(@badcollaborators) == 1) ? 'an invalid collaborator' : 'invalid collaborators'; - $result .= ': '.join(', ',@badcollaborators); - $result .= '
'; - } - if (scalar(@badcollaborators > $ncol)) { - $result .= '
'; - $result .= 'This student has submitted too many '. - 'collaborators. Maximum is '.$ncol.'.'; - $result .= '
'; - } - } + (my $sub_result,$fullname,$col_fullnames)= + &check_collaborators($symb,$uname,$udom,\%record,$handgrade, + $counter); + $result.=$sub_result; } $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 '' ? '' : 'Date Submitted: '. - $$timestamp)."
'.$$string[0]; - } else { + + my $lastsubonly; + + if ($$timestamp eq '') { + $lastsubonly.='
'.$$string[0].'
'; + } else { + $lastsubonly = + '
' + .''.&mt('Date Submitted:').' '.$$timestamp."\n"; + my %seenparts; - for my $part (sort keys(%$handgrade)) { - my ($partid,$respid) = split(/_/,$part); - my $display_part=&get_display_part($partid,$url,$symb); + my @part_response_id = &flatten_responseType($responseType); + foreach my $part (@part_response_id) { + next if ($env{'form.lastSub'} eq 'hdgrade' + && $$handgrade{$$part[0].'_'.$$part[1]} ne 'yes'); + + my ($partid,$respid) = @{ $part }; + my $display_part=&get_display_part($partid,$symb); if ($env{"form.$uname:$udom:$partid:submitted_by"}) { if (exists($seenparts{$partid})) { next; } $seenparts{$partid}=1; @@ -1688,79 +2123,95 @@ KEYWORDS ' Collaborative submission by: '. ''. + '\');" target="_self">'. $$fullname{$env{"form.$uname:$udom:$partid:submitted_by"}}.'
'; $request->print($submitby); next; } my $responsetype = $responseType->{$partid}->{$respid}; if (!exists($record{"resource.$partid.$respid.submission"})) { - $lastsubonly.='
Part: '. - $display_part.' ( ID '.$respid. - ' )   '. - 'Nothing submitted - no attempts

'; + $lastsubonly.="\n".'
'. + ''.&mt('Part: [_1]',$display_part).''. + ' '. + '('.&mt('Part ID: [_1]',$respid).')'. + '   '. + ''.&mt('Nothing submitted - no attempts.').'

'; next; } - foreach (@$string) { - my ($partid,$respid) = /^resource\.([^\.]*)\.([^\.]*)\.submission/; - if ($part ne ($partid.'_'.$respid)) { next; } - my ($ressub,$subval) = split(/:/,$_,2); + foreach my $submission (@$string) { + my ($partid,$respid) = ($submission =~ /^resource\.([^\.]*)\.([^\.]*)\.submission/); + if (join('_',@{$part}) ne ($partid.'_'.$respid)) { next; } + my ($ressub,$hide,$subval) = split(/:/,$submission,3); # Similarity check my $similar=''; if($env{'form.checkPlag'}){ my ($oname,$odom,$ocrsid,$oessay,$osim)= - &most_similar($uname,$udom,$subval); + &most_similar($uname,$udom,$subval,\%old_essays); if ($osim) { $osim=int($osim*100.0); - $similar="

Essay". - " is $osim% similar to an essay by ". - &Apache::loncommon::plainname($oname,$odom). - '

'. - &keywords_highlight($oessay). - '

'; + my %old_course_desc = + &Apache::lonnet::coursedescription($ocrsid, + {'one_time' => 1}); + + if ($hide) { + $similar='
'.&mt("Essay was found to be similar to another essay submitted for this assignment.").'
'. + &mt('As the current submission is for an anonymous survey, no other details are available.').'

'; + } else { + $similar="

". + &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'}). + '

'. + &keywords_highlight($oessay). + '

'; + } } } my $order=&get_order($partid,$respid,$symb,$uname,$udom); if ($env{'form.lastSub'} eq 'lastonly' || ($env{'form.lastSub'} eq 'hdgrade' && - $$handgrade{$part} eq 'yes')) { - my $display_part=&get_display_part($partid,$url,$symb); - $lastsubonly.='
Part: '. - $display_part.' ( ID '.$respid. - ' )   '; - my @files; - if ($record{"resource.$partid.$respid.portfiles"}) { - my $file_url = '/uploaded/'.$udom.'/'.$uname.'/portfolio'; - foreach my $file (split(',',$record{"resource.$partid.$respid.portfiles"})) { - push(@files,$file_url.$file); - - &Apache::lonnet::logthis("found a portfolio file".$record{"resource.$partid.$respid.portfiles"}); - &Apache::lonnet::logthis("uploaded URL file".$record{"resource.$partid.$respid.uploadedurl"}); - } - } - if ($record{"resource.$partid.$respid.uploadedurl"}) { - push(@files,$record{"resource.$partid.$respid.uploadedurl"}); - } - if (@files) { - $lastsubonly.='
Like all files provided by users, this file may contain virusses
'; - foreach my $file (@files) { - &Apache::lonnet::allowuploaded('/adm/grades',$file); - $lastsubonly.='
'.$file.''; - } + $$handgrade{$$part[0].'_'.$$part[1]} eq 'yes')) { + my $display_part=&get_display_part($partid,$symb); + $lastsubonly.='
'. + ''.&mt('Part: [_1]',$display_part).''. + ' '. + '('.&mt('Part ID: [_1]',$respid).')'. + '   '; + my $files=&get_submitted_files($udom,$uname,$partid,$respid,\%record); + if (@$files) { + if ($hide) { + $lastsubonly.='
'.&mt('[quant,_1,file] uploaded to this anonymous survey',scalar(@{$files})); + } else { + $lastsubonly.='
'.&mt('Like all files provided by users, this file may contain viruses').'
'; + foreach my $file (@$files) { + &Apache::lonnet::allowuploaded('/adm/grades',$file); + $lastsubonly.='
'.$file.''; + } + } $lastsubonly.='
'; } - $lastsubonly.='Submitted Answer: '. - &cleanRecord($subval,$responsetype,$symb,$partid, - $respid,\%record,$order); + if ($hide) { + $lastsubonly.=''.&mt('Anonymous Survey').''; + } else { + $lastsubonly.=''.&mt('Submitted Answer:').' '. + &cleanRecord($subval,$responsetype,$symb,$partid, + $respid,\%record,$order,undef,$uname,$udom); + } if ($similar) {$lastsubonly.="

$similar\n";} + $lastsubonly.='
'; } } } + $lastsubonly.=''."\n"; # End: LC_grade_submissions_body } - $lastsubonly.='
'."\n"; $request->print($lastsubonly); - } elsif ($env{'form.lastSub'} eq 'datesub') { - my (undef,$responseType,undef,$parts) = &showResourceInfo($url); + } elsif ($env{'form.lastSub'} eq 'datesub') { +# my (undef,$responseType,undef,$parts) = &showResourceInfo($symb); + my ($parts,$handgrade,$responseType) = &response_type($symb); + $request->print(&displaySubByDates($symb,\%record,$parts,$responseType,$checkIcon,$uname,$udom)); } elsif ($env{'form.lastSub'} =~ /^(last|all)$/) { $request->print(&Apache::loncommon::get_previous_attempt($symb,$uname,$udom, @@ -1771,57 +2222,75 @@ KEYWORDS $request->print(''."\n"); - # return if view submission with no grading option if ($env{'form.showgrading'} eq '' || (!&canmodify($usec))) { my $toGrade.='  '."\n" if (&canmodify($usec)); - $toGrade.='
'."\n"; + 'onclick="javascript:checksubmit(this.form,\'Grade Student\',\'' + .$counter.'\');" target="_self" />  '."\n" if (&canmodify($usec)); + $toGrade.='
'."\n"; if (($env{'form.command'} eq 'submission') || ($env{'form.command'} eq 'processGroup' && $counter == $total)) { - $toGrade.=''.&show_grading_menu_form($symb,$url) + $toGrade.=''.&show_grading_menu_form($symb); } $request->print($toGrade); return; } else { - $request->print(''."\n"); + $request->print(''."\n"); } # essay grading message center if ($env{'form.handgrade'} eq 'yes') { + my $result='
'; + + $result.='
'. + &mt('Send Message').'
'; my ($lastname,$givenn) = split(/,/,$env{'form.fullname'}); my $msgfor = $givenn.' '.$lastname; - if (scalar(@col_fullnames) > 0) { - my $lastone = pop @col_fullnames; - $msgfor .= ', '.(join ', ',@col_fullnames).' and '.$lastone.'.'; + if (scalar(@$col_fullnames) > 0) { + my $lastone = pop(@$col_fullnames); + $msgfor .= ', '.(join ', ',@$col_fullnames).' and '.$lastone.'.'; } $msgfor =~ s/\'/\\'/g; #' stupid emacs - no! javascript - $result=''."\n". + $result.=''."\n". ''."\n"; $result.=' '. - 'Compose Message to student'.(scalar(@col_fullnames) >= 1 ? 's' : '').'  '. + ',\''.$msgfor.'\');" target="_self">'. + &mt('Compose message to student').(scalar(@$col_fullnames) >= 1 ? 's' : '').')'. ''."\n". - '
 (Message will be sent when you click on Save & Next below.)'."\n" - if ($env{'form.handgrade'} eq 'yes'); + '
 ('. + &mt('Message will be sent when you click on Save & Next below.').")\n"; + $result.='
'; $request->print($result); } my %seen = (); my @partlist; my @gradePartRespid; - for (sort keys(%$handgrade)) { - my ($partid,$respid) = split(/_/); + my @part_response_id = &flatten_responseType($responseType); + $request->print( + '
' + .'

'.&mt('Assign Grades').'

' + ); + $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 }); next if ($seen{$partid} > 0); $seen{$partid}++; - next if ($$handgrade{$_} =~ /:no$/ && $env{'form.lastSub'} =~ /^(hdgrade)$/); - push @partlist,$partid; - push @gradePartRespid,$partid.'.'.$respid; - + next if ($$handgrade{$part_resp} ne 'yes' + && $env{'form.lastSub'} eq 'hdgrade'); + push(@partlist,$partid); + push(@gradePartRespid,$partid.'.'.$respid); $request->print(&gradeBox($request,$symb,$uname,$udom,$counter,$partid,\%record)); } + $request->print(&gradeBox_end()); #
+ $request->print(''); + + $request->print(''); + $result=''."\n"; $result.=''."\n"; $ctr++; } - $request->print($result.''."\n"); + $request->print($result.''."\n"); + +# Done with printing info for one student + + $request->print('');#LC_grade_show_user + # print end of form if ($counter == $total) { - my $endform='
'."\n"; - $endform.='  '."\n"; + my $endform='

'."\n"; + $endform.='  '."\n"; my $ntstu =''."\n"; my $nsel = ($env{'form.NTSTU'} ne '' ? $env{'form.NTSTU'} : '1'); - $ntstu =~ s/
'; - $endform.=&show_grading_menu_form($symb,$url); + $ntstu =~ s/
'; + $endform.=&show_grading_menu_form($symb); $request->print($endform); } return ''; } +sub check_collaborators { + my ($symb,$uname,$udom,$record,$handgrade,$counter) = @_; + my ($result,@col_fullnames); + my ($classlist,undef,$fullname) = &getclasslist('all','0'); + foreach my $part (keys(%$handgrade)) { + my $ncol = &Apache::lonnet::EXT('resource.'.$part. + '.maxcollaborators', + $symb,$udom,$uname); + next if ($ncol <= 0); + $part =~ s/\_/\./g; + next if ($record->{'resource.'.$part.'.collaborators'} eq ''); + my (@good_collaborators, @bad_collaborators); + foreach my $possible_collaborator + (split(/,?\s+/,$record->{'resource.'.$part.'.collaborators'})) { + $possible_collaborator =~ s/[\$\^\(\)]//g; + next if ($possible_collaborator eq ''); + 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 + my @matches = grep(/^\Q$co_name\E:\Q$co_dom\E$/i, + keys(%$classlist)); + if (! scalar(@matches)) { + push(@bad_collaborators, $possible_collaborator); + } else { + push(@good_collaborators, @matches); + } + } + if (scalar(@good_collaborators) != 0) { + $result.='
'.&mt('Collaborators: '); + foreach my $name (@good_collaborators) { + my ($lastname,$givenn) = split(/,/,$$fullname{$name}); + push(@col_fullnames, $givenn.' '.$lastname); + $result.=$fullname->{$name}.'     '; + } + $result.='
'."\n"; + my ($part)=split(/\./,$part); + $result.=''. + "\n"; + } + if (scalar(@bad_collaborators) > 0) { + $result.='
'; + $result.=&mt('This student has submitted [quant,_1,invalid collaborator]: [_2]',scalar(@bad_collaborators),join(', ',@bad_collaborators)); + $result .= '
'; + } + if (scalar(@bad_collaborators > $ncol)) { + $result .= '
'; + $result .= &mt('This student has submitted too many '. + 'collaborators. Maximum is [_1].',$ncol); + $result .= '
'; + } + } + return ($result,$fullname,\@col_fullnames); +} + #--- 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); for ($version=1;$version<=$$returnhash{'version'};$version++) { - foreach (sort(split(/\:/,$$returnhash{$version.':keys'}))) { - $lasthash{$_}=$$returnhash{$version.':'.$_}; - $timestamp = scalar(localtime($$returnhash{$version.':timestamp'})); - } - } - foreach ((keys %lasthash)) { - if ($_ =~ /\.submission$/) { - my ($partid,$foo) = split(/submission$/,$_); - my $draft = $lasthash{$partid.'awarddetail'} eq 'DRAFT' ? - 'Draft Copy ' : ''; - push @string, (join(':',$_,$draft.$lasthash{$_})); - } + foreach my $key (sort(split(/\:/, + $$returnhash{$version.':keys'}))) { + $lasthash{$key}=$$returnhash{$version.':'.$key}; + $timestamp = + &Apache::lonlocal::locallocaltime($$returnhash{$version.':timestamp'}); + } + } + my %typeparts; + 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')) { + my ($ign,@parts) = split(/\./,$key); + pop(@parts); + unless ($showsurv) { + my $id = join(',',@parts); + $typeparts{$ign.'.'.$id} = $lasthash{$key}; + } + delete($lasthash{$key}); + } + } + } + my @hidden = keys(%typeparts); + foreach my $key (keys(%lasthash)) { + next if ($key !~ /\.submission$/); + my $hide; + if (@hidden) { + foreach my $id (@hidden) { + if ($key =~ /^\Q$id\E/) { + $hide = 1; + last; + } + } + } + my ($partid,$foo) = split(/submission$/,$key); + my $draft = $lasthash{$partid.'awarddetail'} eq 'DRAFT' ? + 'Draft Copy ' : ''; + push(@string, join(':', $key, $hide, $draft.$lasthash{$key})); } } - @string = $string[0] eq '' ? 'Nothing submitted - no attempts.' : @string; - return \@string,\$timestamp; + if (!@string) { + $string[0] = + ''.&mt('Nothing submitted - no attempts.').''; + } + return (\@string,\$timestamp); } #--- High light keywords, with style choosen by user. @@ -1892,66 +2457,86 @@ sub keywords_highlight { my $styleon = $env{'form.kwstyle'} eq '' ? '' : $env{'form.kwstyle'}; (my $styleoff = $styleon) =~ s/\$styleon$_$styleoff<\/font>/gi; + foreach my $keyword (@keylist) { + $string =~ s/\b\Q$keyword\E(\b|\.)/$styleon$keyword$styleoff<\/font>/gi; } return $string; } #--- Called from submission routine sub processHandGrade { - my ($request) = shift; - my $url = $env{'form.url'}; - my $symb = $env{'form.symb'}; + my ($request,$symb) = @_; + my (undef,undef,$url) = &Apache::lonnet::decode_symb($symb); my $button = $env{'form.gradeOpt'}; my $ngrade = $env{'form.NCT'}; my $ntstu = $env{'form.NTSTU'}; + my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; + my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'}; + if ($button eq 'Save & Next') { my $ctr = 0; while ($ctr < $ngrade) { my ($uname,$udom) = split(/:/,$env{'form.unamedom'.$ctr}); - my ($errorflag,$pts,$wgt) = &saveHandGrade($request,$url,$symb,$uname,$udom,$ctr); + my ($errorflag,$pts,$wgt) = &saveHandGrade($request,$symb,$uname,$udom,$ctr); if ($errorflag eq 'no_score') { $ctr++; next; } if ($errorflag eq 'not_allowed') { - $request->print("Not allowed to modify grades for $uname:$udom"); + $request->print("Not allowed to modify grades for $uname:$udom"); $ctr++; next; } my $includemsg = $env{'form.includemsg'.$ctr}; my ($subject,$message,$msgstatus) = ('','',''); + my $restitle = &Apache::lonnet::gettitle($symb); + my ($feedurl,$showsymb) = + &get_feedurl_and_symb($symb,$uname,$udom); + my $messagetail; if ($includemsg =~ /savemsg|newmsg\Q$ctr\E/) { - $subject = $env{'form.msgsub'} if ($includemsg =~ /^msgsub/); + $subject = $env{'form.msgsub'} if ($includemsg =~ /msgsub/); + unless ($subject=~/\w/) { $subject=&mt('Grading Feedback'); } + $subject.=' ['.$restitle.']'; my (@msgnum) = split(/,/,$includemsg); foreach (@msgnum) { $message.=$env{'form.'.$_} if ($_ =~ /savemsg|newmsg/ && $_ ne ''); } $message =&Apache::lonfeedback::clear_out_html($message); - $message.="\n\nPoint".($pts > 1 ? 's':'').' awarded = '.$pts.' out of '.$wgt; - $message.=" for $env{'form.probTitle'}"; - $msgstatus = &Apache::lonmsg::user_normal_msg ($uname,$udom, - $env{'form.msgsub'},$message); + if ($env{'form.withgrades'.$ctr}) { + $message.="\n\nPoint".($pts > 1 ? 's':'').' awarded = '.$pts.' out of '.$wgt; + $messagetail = " for $restitle"; + } + $msgstatus = + &Apache::lonmsg::user_normal_msg($uname,$udom,$subject, + $message.$messagetail, + undef,$feedurl,undef, + undef,undef,$showsymb, + $restitle); + $request->print('
'.&mt('Sending message to [_1]',$uname.':'.$udom).': '. + $msgstatus); } if ($env{'form.collaborator'.$ctr}) { my @collabstrs=&Apache::loncommon::get_env_multiple("form.collaborator$ctr"); foreach my $collabstr (@collabstrs) { my ($part,@collaborators) = split(/:/,$collabstr); - foreach (@collaborators) { + foreach my $collaborator (@collaborators) { my ($errorflag,$pts,$wgt) = - &saveHandGrade($request,$url,$symb,$_,$udom,$ctr, + &saveHandGrade($request,$symb,$collaborator,$udom,$ctr, $env{'form.unamedom'.$ctr},$part); if ($errorflag eq 'not_allowed') { - $request->print("Not allowed to modify grades for $_:$udom"); + $request->print("".&mt('Not allowed to modify grades for [_1]',"$collaborator:$udom").""); next; - } else { - if ($message ne '') { - $msgstatus = &Apache::lonmsg::user_normal_msg($_,$udom,$env{'form.msgsub'},$message); + } elsif ($message ne '') { + my ($baseurl,$showsymb) = + &get_feedurl_and_symb($symb,$collaborator, + $udom); + if ($env{'form.withgrades'.$ctr}) { + $messagetail = " for $restitle"; } - + $msgstatus = + &Apache::lonmsg::user_normal_msg($collaborator,$udom,$subject,$message.$messagetail,undef,$baseurl,undef,undef,undef,$showsymb,$restitle); } } } @@ -1997,9 +2582,7 @@ sub processHandGrade { $env{'form.savemsgN'} = --$idx; $keyhash{$symb.'_savemsgN'} = $env{'form.savemsgN'}; my $putresult = &Apache::lonnet::put - ('nohist_handgrade',\%keyhash, - $env{'course.'.$env{'request.course.id'}.'.domain'}, - $env{'course.'.$env{'request.course.id'}.'.num'}); + ('nohist_handgrade',\%keyhash,$cdom,$cnum); } # Called by Save & Refresh from Highlight Attribute Window my (undef,undef,$fullname) = &getclasslist($env{'form.section'},'1'); @@ -2023,7 +2606,7 @@ sub processHandGrade { # Go directly to grade student - from submission or link from chart page if ($button eq 'Grade Student') { - (undef,undef,$env{'form.handgrade'},undef,undef) = &showResourceInfo($url); +# (undef,undef,$env{'form.handgrade'},undef,undef) = &showResourceInfo($symb); my $processUser = $env{'form.unamedom'.$env{'form.studentNo'}}; ($env{'form.student'},$env{'form.userdom'}) = split(/:/,$processUser); $env{'form.fullname'} = $$fullname{$processUser}; @@ -2043,33 +2626,54 @@ sub processHandGrade { my (@parsedlist,@nextlist); my ($nextflg) = 0; - foreach (sort {lc($$fullname{$a}) cmp lc($$fullname{$b}) } keys %$fullname) { + foreach my $item (sort + { + if (lc($$fullname{$a}) ne lc($$fullname{$b})) { + return (lc($$fullname{$a}) cmp lc($$fullname{$b})); + } + return $a cmp $b; + } (keys(%$fullname))) { +# FIXME: this is fishy, looks like the button label 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; +# FIXME: this is fishy, looks like the button label @parsedlist = reverse @parsedlist if ($button eq 'Previous'); - my ($partlist) = &response_type($url); + 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); + + if ($submitonly eq 'queued') { + my %queue_status = + &Apache::bridgetask::get_student_status($symb,$cdom,$cnum, + $udom,$uname); + next if (!defined($queue_status{'gradingqueue'})); + } + if ($submitonly =~ /^(yes|graded|incorrect)$/) { # my %record = &Apache::lonnet::restore($symb,$env{'request.course.id'},$udom,$uname); - my %status=&student_gradeStatus($url,$symb,$udom,$uname,$partlist); + my %status=&student_gradeStatus($symb,$udom,$uname,$partlist); 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; } @@ -2080,7 +2684,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++; } @@ -2088,7 +2692,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; @@ -2097,10 +2701,10 @@ sub processHandGrade { $ctr++; } if ($total < 0) { - my $the_end = '

LON-CAPA User Message


'."\n"; - $the_end.='Message: No more students for this section or class.

'."\n"; - $the_end.='Click on the button below to return to the grading menu.

'."\n"; - $the_end.=&show_grading_menu_form ($symb,$url); + my $the_end = '

'.&mt('LON-CAPA User Message').'


'."\n"; + $the_end.=&mt('Message: No more students for this section or class.').'

'."\n"; + $the_end.=&mt('Click on the button below to return to the grading menu.').'

'."\n"; + $the_end.=&show_grading_menu_form($symb); $request->print($the_end); } return ''; @@ -2108,17 +2712,20 @@ sub processHandGrade { #---- Save the score and award for each student, if changed sub saveHandGrade { - my ($request,$url,$symb,$stuname,$domain,$newflg,$submitter,$part) = @_; - my $v_flag=0; + my ($request,$symb,$stuname,$domain,$newflg,$submitter,$part) = @_; + my @version_parts; my $usec = &Apache::lonnet::getsection($domain,$stuname, $env{'request.course.id'}); if (!&canmodify($usec)) { return('not_allowed'); } - my %record = &Apache::lonnet::restore($symb,$env{'request.course.id'},$domain,$stuname); + my %record = &Apache::lonnet::restore($symb,$env{'request.course.id'},$domain,$stuname); my @parts_graded; my %newrecord = (); my ($pts,$wgt) = ('',''); - foreach my $new_part (split(/:/,$env{'form.partlist'.$newflg})) { - #collaborator may vary for different parts + my %aggregate = (); + my $aggregateflag = 0; + my @parts = split(/:/,$env{'form.partlist'.$newflg}); + foreach my $new_part (@parts) { + #collaborator ($submi may vary for different parts if ($submitter && $new_part ne $part) { next; } my $dropMenu = $env{'form.GD_SEL'.$newflg.'_'.$new_part}; if ($dropMenu eq 'excused') { @@ -2127,15 +2734,30 @@ sub saveHandGrade { if (exists($record{'resource.'.$new_part.'.awarded'})) { $newrecord{'resource.'.$new_part.'.awarded'} = ''; } - $newrecord{'resource.'.$new_part.'.regrader'}="$env{'user.name'}:$env{'user.domain'}"; + $newrecord{'resource.'.$new_part.'.regrader'}="$env{'user.name'}:$env{'user.domain'}"; } } 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'}= "$env{'user.name'}:$env{'user.domain'}"; + my $totaltries = $record{'resource.'.$part.'.tries'}; + + my %last_resets = &get_last_resets($symb,$env{'request.course.id'}, + [$new_part]); + my $aggtries =$totaltries; + if ($last_resets{$new_part}) { + $aggtries = &get_num_tries(\%record,$last_resets{$new_part}, + $new_part); + } + + my $solvedstatus = $record{'resource.'.$new_part.'.solved'}; + if ($aggtries > 0) { + &decrement_aggs($symb,$new_part,\%aggregate,$aggtries,$totaltries,$solvedstatus); + $aggregateflag = 1; + } } elsif ($dropMenu eq '') { $pts = ($env{'form.GD_BOX'.$newflg.'_'.$new_part} ne '' ? $env{'form.GD_BOX'.$newflg.'_'.$new_part} : @@ -2148,9 +2770,10 @@ sub saveHandGrade { my $partial= $pts/$wgt; if ($partial eq $record{'resource.'.$new_part.'.awarded'}) { #do not update score for part if not changed. + &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; @@ -2173,79 +2796,301 @@ sub saveHandGrade { "$env{'user.name'}:$env{'user.domain'}"; } # unless problem has been graded, set flag to version the submitted files - unless ($record{'resource.'.$new_part.'.solved'} =~ /^correct_/ || $record{'resource.'.$new_part.'.solved'} eq 'incorrect_by_override') { - $v_flag = 1; - &Apache::lonnet::logthis("v_flag is $v_flag"); - &Apache::lonnet::logthis('the record info is '.$record{'resource.'.$new_part.'.solved'}); - } else { - &Apache::lonnet::logthis('the record info is '.$record{'resource.'.$new_part.'.solved'}); + unless ($record{'resource.'.$new_part.'.solved'} =~ /^correct_/ || + $record{'resource.'.$new_part.'.solved'} eq 'incorrect_by_override' || + $dropMenu eq 'reset status') + { + push(@version_parts,$new_part); } } - if (scalar(keys(%newrecord)) > 0) { - if ($v_flag) { - &Apache::lonnet::logthis("v_flag (setting stuff) is $v_flag"); - &Apache::lonnet::unmark_as_readonly($domain,$stuname,$symb.$env{'request.course.id'}); - &version_portfiles(\%record, \@parts_graded, $env{'request.course.id'}, $symb, $domain, $stuname); + my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; + my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'}; + + if (%newrecord) { + if (@version_parts) { + my @changed_keys = &version_portfiles(\%record, \@parts_graded, + $env{'request.course.id'}, $symb, $domain, $stuname, \@version_parts); + @newrecord{@changed_keys} = @record{@changed_keys}; + foreach my $new_part (@version_parts) { + &handback_files($request,$symb,$stuname,$domain,$newflg, + $new_part,\%newrecord); + } } &Apache::lonnet::cstore(\%newrecord,$symb, $env{'request.course.id'},$domain,$stuname); + &check_and_remove_from_queue(\@parts,\%record,\%newrecord,$symb, + $cdom,$cnum,$domain,$stuname); + } + if ($aggregateflag) { + &Apache::lonnet::cinc('nohist_resourcetracker',\%aggregate, + $cdom,$cnum); + } + return ('',$pts,$wgt); +} + +sub check_and_remove_from_queue { + my ($parts,$record,$newrecord,$symb,$cdom,$cnum,$domain,$stuname) = @_; + my @ungraded_parts; + foreach my $part (@{$parts}) { + if ( $record->{ 'resource.'.$part.'.awarded'} eq '' + && $record->{ 'resource.'.$part.'.solved' } ne 'excused' + && $newrecord->{'resource.'.$part.'.awarded'} eq '' + && $newrecord->{'resource.'.$part.'.solved' } ne 'excused' + ) { + push(@ungraded_parts, $part); + } + } + if ( !@ungraded_parts ) { + &Apache::bridgetask::remove_from_queue('gradingqueue',$symb,$cdom, + $cnum,$domain,$stuname); + } +} + +sub handback_files { + my ($request,$symb,$stuname,$domain,$newflg,$new_part,$newrecord) = @_; + my $portfolio_root = '/userfiles/portfolio'; + my $res_error; + my ($partlist,$handgrade,$responseType) = &response_type($symb,\$res_error); + if ($res_error) { + $request->print('
'.&navmap_errormsg().'
'); + return; + } + 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 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'}; + my ($directory,$answer_file) = + ($env{'form.'.$newflg.'_'.$part_resp.'_origdoc'.$file_counter} =~ /^(.*?)([^\/]*)$/); + my ($answer_name,$answer_ver,$answer_ext) = + &file_name_version_ext($answer_file); + my ($portfolio_path) = ($directory =~ /^.+$stuname\/portfolio(.*)/); + my $getpropath = 1; + my @dir_list = &Apache::lonnet::dirlist($portfolio_root.$portfolio_path,$domain,$stuname,$getpropath); + my $version = &get_next_version($answer_name, $answer_ext, \@dir_list); + # fix file name + my ($save_file_name) = (($directory.$answer_name.".$version.".$answer_ext) =~ /^.+\/${stuname}\/(.*)/); + my $result=&Apache::lonnet::finishuserfileupload($stuname,$domain, + $newflg.'_'.$part_resp.'_returndoc'.$file_counter, + $save_file_name); + if ($result !~ m|^/uploaded/|) { + $request->print('
'. + &mt('An error occurred ([_1]) while trying to upload [_2].', + $result,$newflg.'_'.$part_resp.'_returndoc'.$file_counter). + ''); + } 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); + 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".'
'.$save_file_name."
"; + + } + $request->print("
".$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:
"; + $message .= "".&Apache::lonnet::gettitle($symb)."
"; + $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); + } + } + return; +} + +sub get_feedurl_and_symb { + my ($symb,$uname,$udom) = @_; + my (undef,undef,$url) = &Apache::lonnet::decode_symb($symb); + $url = &Apache::lonnet::clutter($url); + my $encrypturl=&Apache::lonnet::EXT('resource.0.encrypturl', + $symb,$udom,$uname); + if ($encrypturl =~ /^yes$/i) { + &Apache::lonenc::encrypted(\$url,1); + &Apache::lonenc::encrypted(\$symb,1); + } + return ($url,$symb); +} + +sub get_submitted_files { + my ($udom,$uname,$partid,$respid,$record) = @_; + my @files; + if ($$record{"resource.$partid.$respid.portfiles"}) { + my $file_url = '/uploaded/'.$udom.'/'.$uname.'/portfolio'; + foreach my $file (split(',',$$record{"resource.$partid.$respid.portfiles"})) { + push(@files,$file_url.$file); + } } - return '',$pts,$wgt; + if ($$record{"resource.$partid.$respid.uploadedurl"}) { + push(@files,$$record{"resource.$partid.$respid.uploadedurl"}); + } + return (\@files); +} + +# ----------- Provides number of tries since last reset. +sub get_num_tries { + my ($record,$last_reset,$part) = @_; + my $timestamp = ''; + my $num_tries = 0; + if ($$record{'version'}) { + for (my $version=$$record{'version'};$version>=1;$version--) { + if (exists($$record{$version.':resource.'.$part.'.solved'})) { + $timestamp = $$record{$version.':timestamp'}; + if ($timestamp > $last_reset) { + $num_tries ++; + } else { + last; + } + } + } + } + return $num_tries; +} + +# ----------- Determine decrements required in aggregate totals +sub decrement_aggs { + my ($symb,$part,$aggregate,$aggtries,$totaltries,$solvedstatus) = @_; + my %decrement = ( + attempts => 0, + users => 0, + correct => 0 + ); + $decrement{'attempts'} = $aggtries; + if ($solvedstatus =~ /^correct/) { + $decrement{'correct'} = 1; + } + if ($aggtries == $totaltries) { + $decrement{'users'} = 1; + } + foreach my $type (keys(%decrement)) { + $$aggregate{$symb."\0".$part."\0".$type} = -$decrement{$type}; + } + return; +} + +# ----------- Determine timestamps for last reset of aggregate totals for parts +sub get_last_resets { + my ($symb,$courseid,$partids) =@_; + my %last_resets; + my $cdom = $env{'course.'.$courseid.'.domain'}; + my $cname = $env{'course.'.$courseid.'.num'}; + my @keys; + foreach my $part (@{$partids}) { + push(@keys,"$symb\0$part\0resettime"); + } + my %results=&Apache::lonnet::get('nohist_resourcetracker',\@keys, + $cdom,$cname); + foreach my $part (@{$partids}) { + $last_resets{$part}=$results{"$symb\0$part\0resettime"}; + } + return %last_resets; } # ----------- Handles creating versions for portfolio files as answers sub version_portfiles { - my ($record, $parts_graded, $courseid, $symb, $domain, $stuname) = @_; + my ($record, $parts_graded, $courseid, $symb, $domain, $stu_name, $v_flag) = @_; + my $version_parts = join('|',@$v_flag); + my @returned_keys; my $parts = join('|', @$parts_graded); - my $portfolio_root = &Apache::loncommon::propath($domain, - $stuname). - '/userfiles/portfolio'; - foreach my $key(keys %$record) { - #&Apache::lonnet::logthis("key is $key, value is $$record{$key}"); + my $portfolio_root = '/userfiles/portfolio'; + foreach my $key (keys(%$record)) { my $new_portfiles; - if ($key =~ /^resource\.($parts)\./ && $key =~ /\.portfiles$/) { - - my @v_portfiles; - my @portfiles = split(/,/,$$record{$key}); + if ($key =~ /^resource\.($version_parts)\./ && $key =~ /\.portfiles$/ ) { + my @versioned_portfiles; + my @portfiles = split(/\s*,\s*/,$$record{$key}); foreach my $file (@portfiles) { - my ($directory,$answer_file) =($file =~ /^(.*?)([^\/]*$)/); - my $version = 0; - my @answer_file_parts = split(/\./, $answer_file); - my @dir_list = &Apache::lonnet::dirlist($directory,$domain,$stuname,$portfolio_root); - my @file_names; - my @file_name_parts; - foreach my $row (@dir_list) { - @file_names = split(/\&/,$row,2); - @file_name_parts = split(/\./, $file_names[0]); - # ($file_name_parts[scalar @file_name_parts] eq $answer_file_parts[scalar @answer_file_parts]) - if (($file_name_parts[0] eq $answer_file_parts[0]) && - ($file_name_parts[-1] eq $answer_file_parts[-1])) { - # gets here if filename and extension match, regardless of version - if (scalar @file_name_parts == 3) { # a versioned file is found - # so save it for later - if ($file_name_parts[1] > $version) {$version = $file_name_parts[1]}; - } - } - } - $version++; - my $home_server = &Apache::lonnet::homeserver($stuname,$domain,undef); - $env{'form.copy'} = &Apache::lonnet::getfile("/uploaded/$domain/$stuname/portfolio$directory$answer_file"); - if($env{'form.copy'} eq '-1') { - &Apache::lonnet::logthis('problem getting file '.$directory.$answer_file); - } else { - my $copy_result = &Apache::lonnet::finishuserfileupload($stuname,$domain,$home_server,'copy', - '/portfolio'.$directory.$answer_file_parts[0].'.'.$version.'.'.$answer_file_parts[-1]); - push(@v_portfiles, $answer_file_parts[0].'.'.$version.'.'.$answer_file_parts[-1]); - &Apache::lonnet::logthis('copy result is '.$copy_result); - &Apache::lonnet::logthis('answer file is '.$answer_file. - ' becomes '.$answer_file_parts[0].'.'.$version.'.'.$answer_file_parts[-1]); + &Apache::lonnet::unmark_as_readonly($domain,$stu_name,[$symb,$env{'request.course.id'}],$file); + my ($directory,$answer_file) =($file =~ /^(.*?)([^\/]*)$/); + my ($answer_name,$answer_ver,$answer_ext) = + &file_name_version_ext($answer_file); + my $getpropath = 1; + my @dir_list = &Apache::lonnet::dirlist($portfolio_root.$directory,$domain,$stu_name,$getpropath); + my $version = &get_next_version($answer_name, $answer_ext, \@dir_list); + my $new_answer = &version_selected_portfile($domain, $stu_name, $directory, $answer_file, $version); + if ($new_answer ne 'problem getting file') { + push(@versioned_portfiles, $directory.$new_answer); + &Apache::lonnet::mark_as_readonly($domain,$stu_name, + [$directory.$new_answer], + [$symb,$env{'request.course.id'},'graded']); } } - $$record{$key} = join(/,/,@v_portfiles); + $$record{$key} = join(',',@versioned_portfiles); + push(@returned_keys,$key); } } - return 'ok'; - + return (@returned_keys); +} + +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 ($file_version ne '') { + # a versioned file is found so save it for later + if ($file_version > $version) { + $version = $file_version; + } + } + } + } + $version ++; + return($version); +} + +sub version_selected_portfile { + my ($domain,$stu_name,$directory,$file_name,$version) = @_; + my ($answer_name,$answer_ver,$answer_ext) = + &file_name_version_ext($file_name); + my $new_answer; + $env{'form.copy'} = &Apache::lonnet::getfile("/uploaded/$domain/$stu_name/portfolio$directory$file_name"); + if($env{'form.copy'} eq '-1') { + $new_answer = 'problem getting file'; + } else { + $new_answer = $answer_name.'.'.$version.'.'.$answer_ext; + my $copy_result = &Apache::lonnet::finishuserfileupload( + $stu_name,$domain,'copy', + '/portfolio'.$directory.$new_answer); + } + return ($new_answer); +} + +sub file_name_version_ext { + my ($file)=@_; + my @file_parts = split(/\./, $file); + my ($name,$version,$ext); + if (@file_parts > 1) { + $ext=pop(@file_parts); + if (@file_parts > 1 && $file_parts[-1] =~ /^\d+$/) { + $version=pop(@file_parts); + } + $name=join('.',@file_parts); + } else { + $name=join('.',@file_parts); + } + return($name,$version,$ext); } #-------------------------------------------------------------------------------------- @@ -2256,15 +3101,15 @@ sub version_portfiles { sub viewgrades_js { my ($request) = shift; - $request->print(< + my $alertmsg = &mt('A number equal or greater than 0 is expected. Entered value = '); + $request->print(&Apache::lonhtmlcommon::scripttag(< VIEWJAVASCRIPT } #--- show scores for a section or whole class w/ option to change/update a score sub viewgrades { - my ($request) = shift; + my ($request,$symb) = @_; &viewgrades_js($request); - my ($symb,$url) = ($env{'form.symb'},$env{'form.url'}); #need to make sure we have the correct data for later EXT calls, #thus invalidate the cache &Apache::lonnet::devalidatecourseresdata( @@ -2430,151 +3278,207 @@ sub viewgrades { $env{'course.'.$env{'request.course.id'}.'.domain'}); &Apache::lonnet::clear_EXT_cache_status(); - my $result='

'.&mt('Manual Grading').'

'; - $result.='Current Resource: '.$env{'form.probTitle'}.''."\n"; + my $result='

'.&mt('Manual Grading').'

'; #view individual student submission form - called using Javascript viewOneStudent - $result.=&jscriptNform($url,$symb); + $result.=&jscriptNform($symb); #beginning of class grading form + my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status')); $result.= '
'."\n". - ''."\n". - ''."\n". + ''."\n". ''."\n". - ''."\n". + &build_section_inputs(). ''."\n". - ''."\n". - ''."\n"; + ''."\n". - my $sectionClass; + my ($common_header,$specific_header); if ($env{'form.section'} eq 'all') { - $sectionClass='Class '; + $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='Students in no Section '; + $common_header = &mt('Assign Common Grade to Students in no Section'); + $specific_header = &mt('Assign Grade to Specific Students in no Section'); } else { - $sectionClass='Students in Section '.$env{'form.section'}.''; + 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.='

Assign Common Grade To '.$sectionClass; - $result.= '
'."\n". - '
'; + $result.= '

'.$common_header.'

'.&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) = &response_type($url,$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.=''; my %seen = (); - for (sort keys(%$handgrade)) { - my ($partid,$respid) = split (/_/,$_,2); + my @part_response_id = &flatten_responseType($responseType); + foreach my $part_response_id (@part_response_id) { + my ($partid,$respid) = @{ $part_response_id }; + my $part_resp = join('_',@{ $part_response_id }); next if $seen{$partid}; $seen{$partid}++; - my $handgrade=$$handgrade{$_}; + my $handgrade=$$handgrade{$part_resp}; my $wgt = &Apache::lonnet::EXT('resource.'.$partid.'.weight',$symb); $weight{$partid} = $wgt eq '' ? '1' : $wgt; - $result.=''."\n"; - $result.=''."\n"; - my $display_part=&get_display_part($partid,$url,$symb); - $result.=''."\n"; - $result.= ''."\n"; + ''. + ''. + ''. + ''. + ''. + &Apache::loncommon::end_data_table_row()."\n"; $ctsparts++; } - $result.='
Part: '.$display_part.'   Point: '; - $result.=''; + my $display_part=&get_display_part($partid,$symb); + my $radio.='
'; my $ctr = 0; while ($ctr<=$weight{$partid}) { # display radio buttons in a nice table 10 across - $result.= '\n"; + ','.$ctr.')" />'.$ctr."\n"; $result.=(($ctr+1)%10 == 0 ? '' : ''); $ctr++; } - $result.='
'.$ctr."
'; - $result.= '
or /'. - $weight{$partid}.' (problem weight) '. - ''. - ''. - '
'; + $line.=''."\n"; + $line.=''."\n"; + + $result.= + &Apache::loncommon::start_data_table_row()."\n". + ''.&mt('Part:').''.$display_part.''.&mt('Points:').''.$radio.''.&mt('or').''.$line.'
'.'
'.'
'."\n". + $result.=&Apache::loncommon::end_data_table()."\n". ''; - $result.=''; + $result.=''; #table listing all the students in a section/class #header of table - $result.= '

Assign Grade to Specific Students in '.$sectionClass; - $result.= '
'."\n". - ''. - '\n"; - my (@parts) = sort(&getpartlist($url,$symb)); + $result.= '

'.$specific_header.'

'. + &Apache::loncommon::start_data_table(). + &Apache::loncommon::start_data_table_header_row(). + ''. + '\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
|; # makes the column narrower + my $narrowtext = &mt('Tries'); + $display =~ s|^Number of Attempts|$narrowtext
|; # makes the column narrower if (!$display) { $display = &Apache::lonnet::metadata($url,$part.'.name'); } my ($partid) = &split_part_type($part); - my $display_part=&get_display_part($partid,$url,$symb); + push(@partids,$partid); + my $display_part=&get_display_part($partid,$symb); if ($display =~ /^Partial Credit Factor/) { - $result.=''."\n"; + $result.=''."\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
}; + } + my $part_mt = &mt('Part:'); + $display =~s{\[Part: \Q$partid\E\]}{$part_mt $display_part}; } - $display =~ s|Problem Status|Grade Status
|; - $result.=''."\n"; + + $result.=''."\n"; } - $result.=''; + $result.=&Apache::loncommon::end_data_table_header_row(); + + my %last_resets = + &get_last_resets($symb,$env{'request.course.id'},\@partids); #get info for each student #list all the students - with points and grade status my (undef,undef,$fullname) = &getclasslist($env{'form.section'},'1'); my $ctr = 0; - foreach (sort {lc($$fullname{$a}) cmp lc($$fullname{$b}) } keys %$fullname) { + foreach (sort + { + if (lc($$fullname{$a}) ne lc($$fullname{$b})) { + return (lc($$fullname{$a}) cmp lc($$fullname{$b})); + } + return $a cmp $b; + } (keys(%$fullname))) { $ctr++; - $result.=&viewstudentgrade($url,$symb,$env{'request.course.id'}, - $_,$$fullname{$_},\@parts,\%weight,$ctr); + $result.=&viewstudentgrade($symb,$env{'request.course.id'}, + $_,$$fullname{$_},\@parts,\%weight,$ctr,\%last_resets); } - $result.='
 No. '.&nameUserString('header')."'.&mt('No.').''.&nameUserString('header')."Score Part: '.$display_part. - '
(weight = '.$weight{$partid}.')
'. + &mt('Score Part: [_1]
(weight = [_2])', + $display_part,$weight{$partid}).'
'.$display.''.$display.'
'; + $result.=&Apache::loncommon::end_data_table(); $result.=''."\n"; - $result.=''."\n"; + $result.=''."\n"; if (scalar(%$fullname) eq 0) { my $colspan=3+scalar(@parts); - $result='There are no students in section "'.$env{'form.section'}. - '" with enrollment status "'.$env{'form.Status'}.'" to modify or grade.'; + my $section_display = join (", ",&Apache::loncommon::get_env_multiple('form.section')); + my $stu_status = join(' or ',&Apache::loncommon::get_env_multiple('form.Status')); + $result=''. + &mt('There are no students in section(s) [_1] with enrollment status [_2] to modify or grade.', + $section_display, $stu_status). + ''; } - $result.=&show_grading_menu_form($symb,$url); + $result.=&show_grading_menu_form($symb); return $result; } #--- call by previous routine to display each student sub viewstudentgrade { - my ($url,$symb,$courseid,$student,$fullname,$parts,$weight,$ctr) = @_; + my ($symb,$courseid,$student,$fullname,$parts,$weight,$ctr,$last_resets) = @_; my ($uname,$udom) = split(/:/,$student); - $student=~s/:/_/; my %record=&Apache::lonnet::restore($symb,$courseid,$udom,$uname); - my $result=''. + my %aggregates = (); + my $result=&Apache::loncommon::start_data_table_row().''. ''. "\n".$ctr.'  '. ''.$fullname.' '. - '('.$uname.($env{'user.domain'} eq $udom ? '' : ':'.$udom).')'."\n"; + '\');" target="_self">'.$fullname.' '. + '('.$uname.($env{'user.domain'} eq $udom ? '' : ':'.$udom).')'."\n"; + $student=~s/:/_/; # colon doen't work in javascript for names foreach my $apart (@$parts) { my ($part,$type) = &split_part_type($apart); my $score=$record{"resource.$part.$type"}; - $result.=''; + $result.=''; + my ($aggtries,$totaltries); + unless (exists($aggregates{$part})) { + $totaltries = $record{'resource.'.$part.'.tries'}; + + $aggtries = $totaltries; + if ($$last_resets{$part}) { + $aggtries = &get_num_tries(\%record,$$last_resets{$part}, + $part); + } + $result.=''."\n"; + $result.=''."\n"; + $aggregates{$part} = 1; + } if ($type eq 'awarded') { - my $pts = $score eq '' ? '' : $score*$$weight{$part}; + my $pts = $score eq '' ? '' : &compute_points($score,$$weight{$part}); $result.=''."\n"; $result.=''."\n"; } elsif ($type eq 'solved') { my ($status,$foo)=split(/_/,$score,2); @@ -2583,10 +3487,10 @@ sub viewstudentgrade { $part.'_solved_s" value="'.$status.'" />'."\n"; $result.='  \n"; } else { $result.=''."\n"; } } - $result.=''; + $result.=&Apache::loncommon::end_data_table_row(); return $result; } #--- change scores for all the students in a section/class # record does not get update if unchanged sub editgrades { - my ($request) = @_; - - my $symb=$env{'form.symb'}; - my $url =$env{'form.url'}; - my $title='

Current Grade Status

'; - $title.='Current Resource: '.$env{'form.probTitle'}.'
'."\n"; - $title.='Section: '.$env{'form.section'}.''."\n"; - - my $result= '
'."\n"; - $result.= ''. - ''. - '\n"; + my ($request,$symb) = @_; + my $section_display = join (", ",&Apache::loncommon::get_env_multiple('form.section')); + my $title='

'.&mt('Current Grade Status').'

'; + $title.='

'.&mt('Section: [_1]',$section_display).'

'."\n"; + + my $result= &Apache::loncommon::start_data_table(). + &Apache::loncommon::start_data_table_header_row(). + ''. + '\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'); @@ -2631,63 +3533,72 @@ sub editgrades { my %columns = (); my ($i,$ctr,$count,$rec_update) = (0,0,0,0); - my (@parts) = sort(&getpartlist($url,$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 .= ''. - ''; + $header .= ''. + ''; $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 .= ''. - ''; + $display =~ s/\[Part: \Q$part\E\]//; + my $narrowtext = &mt('Tries'); + $display =~ s/Number of Attempts/$narrowtext/; + $header .= ''. + ''; $columns{$partid}+=2; } } foreach my $partid (@partid) { - my $display_part=&get_display_part($partid,$url,$symb); - $result .= ''; + my $display_part=&get_display_part($partid,$symb); + $result .= ''; } - $result .= ''; - $result .= $header; - $result .= ''."\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; my $user = $env{'form.ctr'.$i}; - my $usercolon = $user; - $usercolon =~s/_/:/; - my ($uname,$udom)=split(/_/,$user); + my ($uname,$udom)=split(/:/,$user); my %newrecord; my $updateflag = 0; - $line .= ''; + $line .= ''; my $usec=$classlist->{"$uname:$udom"}[5]; if (!&canmodify($usec)) { my $numcols=scalar(@partid)*4+2; - $noupdate.=$line.""; + push(@noupdate, + $line.""); next; } + my %aggregate = (); + my $aggregateflag = 0; + $user=~s/:/_/; # colon doen't work in javascript for names foreach (@partid) { my $old_aw = $env{'form.GD_'.$user.'_'.$_.'_awarded_s'}; my $old_part_pcr = $old_aw/($weight{$_} ne '0' ? $weight{$_}:1); my $old_part = $old_aw eq '' ? '' : $old_part_pcr; my $old_score = $scoreptr{$env{'form.GD_'.$user.'_'.$_.'_solved_s'}}; - my $awarded = $env{'form.GD_'.$user.'_'.$_.'_awarded'}; my $pcr = $awarded/($weight{$_} ne '0' ? $weight{$_} : 1); my $partial = $awarded eq '' ? '' : $pcr; @@ -2702,14 +3613,22 @@ sub editgrades { my $dropMenu = $env{'form.GD_'.$user.'_'.$_.'_solved'}; $score = 'excused' if (($dropMenu eq 'excused') && ($score ne 'excused')); + $newrecord{'resource.'.$_.'.regrader'}= + "$env{'user.name'}:$env{'user.domain'}"; if ($dropMenu eq 'reset status' && $old_score ne '') { # ignore if no previous attempts => nothing to reset - $newrecord{'resource.'.$_.'.tries'} = 0; + $newrecord{'resource.'.$_.'.tries'} = ''; $newrecord{'resource.'.$_.'.solved'} = ''; $newrecord{'resource.'.$_.'.award'} = ''; - $newrecord{'resource.'.$_.'.awarded'} = 0; - $newrecord{'resource.'.$_.'.regrader'}="$env{'user.name'}:$env{'user.domain'}"; + $newrecord{'resource.'.$_.'.awarded'} = ''; $updateflag = 1; + if ($env{'form.GD_'.$user.'_'.$_.'_aggtries'} > 0) { + my $aggtries = $env{'form.GD_'.$user.'_'.$_.'_aggtries'}; + my $totaltries = $env{'form.GD_'.$user.'_'.$_.'_totaltries'}; + my $solvedstatus = $env{'form.GD_'.$user.'_'.$_.'_solved_s'}; + &decrement_aggs($symb,$_,\%aggregate,$aggtries,$totaltries,$solvedstatus); + $aggregateflag = 1; + } } elsif (!($old_part eq $partial && $old_score eq $score)) { $updateflag = 1; $newrecord{'resource.'.$_.'.awarded'} = $partial if $partial ne ''; @@ -2738,28 +3657,75 @@ sub editgrades { ''; } } - $line.=''."\n"; + $line.="\n"; + + my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; + my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'}; + if ($updateflag) { $count++; &Apache::lonnet::cstore(\%newrecord,$symb,$env{'request.course.id'}, $udom,$uname); - $result.=''.$line; + + if (&Apache::bridgetask::in_queue('gradingqueue',$symb,$cdom, + $cnum,$udom,$uname)) { + # need to figure out if should be in queue. + my %record = + &Apache::lonnet::restore($symb,$env{'request.course.id'}, + $udom,$uname); + my $all_graded = 1; + my $none_graded = 1; + foreach my $part (@parts) { + if ( $record{'resource.'.$part.'.awarded'} eq '' ) { + $all_graded = 0; + } else { + $none_graded = 0; + } + } + + if ($all_graded || $none_graded) { + &Apache::bridgetask::remove_from_queue('gradingqueue', + $symb,$cdom,$cnum, + $udom,$uname); + } + } + + $result.=&Apache::loncommon::start_data_table_row(). + ''.$line. + &Apache::loncommon::end_data_table_row(); $updateCtr++; } else { - $noupdate.=''.$line; + push(@noupdate, + ''.$line); $noupdateCtr++; } + if ($aggregateflag) { + &Apache::lonnet::cinc('nohist_resourcetracker',\%aggregate, + $cdom,$cnum); + } } - if ($noupdate) { + if (@noupdate) { # my $numcols=(scalar(@partid)*(scalar(@parts)-1)*2)+3; my $numcols=scalar(@partid)*4+2; - $result .= ''.$noupdate; - } - $result .= '
 No. '.&nameUserString('header')."'.&mt('No.').''.&nameUserString('header')." Old Score  New Score '.&mt('Old Score').''.&mt('New Score').' Old '.$display.'  New '.$display.' '.&mt('Old').' '.$display.''.&mt('New').' '.$display.'Part: '.$display_part. - ' (Weight = '.$weight{$partid}.')'. + &mt('Part: [_1] (Weight = [_2])',$display_part,$weight{$partid}). + '
'.&nameUserString(undef,$$fullname{$usercolon},$uname,$udom).''.&nameUserString(undef,$$fullname{$user},$uname,$udom).'Not allowed to modify student
". + &mt('Not allowed to modify student')."
'.$awarded.' 
 '.$updateCtr.'  '.$updateCtr.' 
 '.$noupdateCtr.'  '.$noupdateCtr.' 
No Changes Occurred For the Students Below
'."\n". - &show_grading_menu_form ($symb,$url); - my $msg = '
Number of records updated = '.$rec_update. - ' for '.$count.' student'.($count <= 1 ? '' : 's').'.
'. - 'Total number of students = '.$env{'form.total'}.'
'; + $result .= &Apache::loncommon::start_data_table_row('LC_empty_row'). + ''. + &mt('No Changes Occurred For the Students Below'). + ''. + &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 .= &Apache::loncommon::end_data_table(). + &show_grading_menu_form($symb); + my $msg = '

'. + &mt('Number of records updated = [_1] for [quant,_2,student].', + $rec_update,$count).'
'. + ''.&mt('Total number of students = [_1]',$env{'form.total'}). + '

'; return $title.$msg.$result; } @@ -2767,7 +3733,7 @@ sub split_part_type { my ($partstr) = @_; my ($temp,@allparts)=split(/_/,$partstr); my $type=pop(@allparts); - my $part=join('.',@allparts); + my $part=join('_',@allparts); return ($part,$type); } @@ -2782,7 +3748,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(<print(< -

Uploading Class Grades

+

Uploading Class Grades

$result -
+

Identify fields

Total number of records found in file: $distotal
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.
- + @@ -2889,25 +3856,28 @@ to this page if the data selected is ins - -
- ENDPICK + $request->print(&Apache::lonhtmlcommon::scripttag($javascript)); return ''; } sub csvupload_fields { - my ($url,$symb) = @_; - my (@parts) = &getpartlist($url,$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); foreach my $part (sort(@parts)) { my @datum; my $display=&Apache::lonnet::metadata($url,$part.'.display'); @@ -2928,58 +3898,59 @@ sub csvuploadmap_footer { -
+
ENDPICK } -sub upcsvScores_form { - my ($request) = shift; - my ($symb,$url)=&get_symb_and_url($request); - if (!$symb) {return '';} - my $result =< +sub checkforfile_js { + my $alertmsg = &mt('Please use the browse button to select a file from your local directory.'); + my $result = &Apache::lonhtmlcommon::scripttag(< CSVFORMJS - $env{'form.probTitle'} = &Apache::lonnet::gettitle($symb); - my ($table) = &showResourceInfo($url,$env{'form.probTitle'}); - $result.=$table; - $result.='
'."\n"; - $result.=''."\n"; + return $result; +} + +sub upcsvScores_form { + my ($request,$symb) = @_; + if (!$symb) {return '';} + my $result=&checkforfile_js(); + $result.='
'."\n"; - $result.=' Specify a file containing the class scores for current resource'. - '.
'."\n"; + $result.=''."\n"; $result.='
'."\n"; + $result.=' '.&mt('Specify a file containing the class scores for current resource.'). + '
'."\n"; + my $upload=&mt("Upload Scores"); my $upfile_select=&Apache::loncommon::upfile_select_html(); my $ignore=&mt('Ignore First Line'); + $symb = &Apache::lonenc::check_encrypt($symb); $result.=< - - $upfile_select -
-
'."\n"; + $result.=&Apache::loncommon::help_open_topic("Course_Convert_To_CSV", + &mt("How do I create a CSV file from a spreadsheet")) + .'
'."\n"; $result.='


'."\n"; - $result.=&show_grading_menu_form($symb,$url); + $result.=&show_grading_menu_form($symb); return $result; } sub csvuploadmap { - my ($request)= @_; - my ($symb,$url)=&get_symb_and_url($request); + my ($request,$symb)= @_; if (!$symb) {return '';} my $datatoken; @@ -2991,11 +3962,15 @@ sub csvuploadmap { } my @records=&Apache::loncommon::upfile_record_sep(); if ($env{'form.noFirstLine'}) { shift(@records); } - &csvuploadmap_header($request,$symb,$url,$datatoken,$#records+1); + &csvuploadmap_header($request,$symb,$datatoken,$#records+1); my ($i,$keyfields); if (@records) { - my @fields=&csvupload_fields($url,$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, @@ -3006,32 +3981,37 @@ sub csvuploadmap { unshift(@fields,['none','']); $i=&Apache::loncommon::csv_samples_select_table($request,\@records, \@fields); - my %sone=&Apache::loncommon::record_sep($records[0]); - $keyfields=join(',',sort(keys(%sone))); + foreach my $rec (@records) { + my %temp = &Apache::loncommon::record_sep($rec); + if (%temp) { + $keyfields=join(',',sort(keys(%temp))); + last; + } + } } } &csvuploadmap_footer($request,$i,$keyfields); - $request->print(&show_grading_menu_form($symb,$url)); + $request->print(&show_grading_menu_form($symb)); return ''; } sub csvuploadoptions { - my ($request)= @_; - my ($symb,$url)=&get_symb_and_url($request); + my ($request,$symb)= @_; my $checked=(($env{'form.noFirstLine'})?'1':'0'); my $ignore=&mt('Ignore First Line'); $request->print(< -

Uploading Class Grade Options

+

Uploading Class Grade Options

-
+


\n"); - $request->print(&show_grading_menu_form($symb,$url)); + $request->print('
+
'."\n"); + $request->print(&show_grading_menu_form($symb)); return ''; } @@ -3076,9 +4057,9 @@ sub get_fields { } sub csvuploadassign { - my ($request)= @_; - my ($symb,$url)=&get_symb_and_url($request); + my ($request,$symb)= @_; if (!$symb) {return '';} + my $error_msg = ''; &Apache::loncommon::load_tmp_file($request); my @gradedata = &Apache::loncommon::upfile_record_sep(); if ($env{'form.noFirstLine'}) { shift(@gradedata); } @@ -3131,12 +4112,21 @@ sub csvuploadassign { my $part=$1; my $wgt =&Apache::lonnet::EXT('resource.'.$part.'.weight', $symb,$domain,$username); - $entries{$fields{$dest}}=~s/\s//g; - my $pcr=$entries{$fields{$dest}} / $wgt; - my $award='correct_by_override'; - $grades{"resource.$part.awarded"}=$pcr; - $grades{"resource.$part.solved"}=$award; - $points{$part}=1; + if ($wgt) { + $entries{$fields{$dest}}=~s/\s//g; + my $pcr=$entries{$fields{$dest}} / $wgt; + my $award=($pcr == 0) ? 'incorrect_by_override' + : 'correct_by_override'; + $grades{"resource.$part.awarded"}=$pcr; + $grades{"resource.$part.solved"}=$award; + $points{$part}=1; + } else { + $error_msg = "
" . + &mt("Some point values were assigned" + ." for problems with a weight " + ."of zero. These values were " + ."ignored."); + } } else { if ($dest=~/stores_(.*)_awarded/) { if ($points{$1}) {next;} } if ($dest=~/stores_(.*)_solved/) { if ($points{$1}) {next;} } @@ -3146,27 +4136,36 @@ sub csvuploadassign { $grades{$store_key}=$entries{$fields{$dest}}; } } - if (! %grades) { push(@skipped,"$username:$domain no data to store"); } - $grades{"resource.regrader"}="$env{'user.name'}:$env{'user.domain'}"; -# &Apache::lonnet::logthis(" storing ".(join('-',%grades))); - &Apache::lonnet::cstore(\%grades,$symb,$env{'request.course.id'}, - $domain,$username); - $request->print('.'); - $request->rflush(); - $countdone++; + 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("

". + &mt("Failed to save data for student [_1]. Message when trying to save was: [_2]", + "$username:$domain",$result)."

"); + } + $request->rflush(); + $countdone++; + } } - $request->print("
Stored $countdone students\n"); + $request->print('
'.&Apache::lonhtmlcommon::confirm_success(&mt("Saved scores for [quant,_1,student]",$countdone),$countdone==0)); if (@skipped) { - $request->print('Skipped Students

'); - foreach my $student (@skipped) { $request->print("$student
\n"); } + $request->print('
'.&Apache::lonhtmlcommon::confirm_success(&mt('No scores stored for the following username(s):'),1).'
'); + $request->print(join(', ',@skipped)); } if (@notallowed) { - $request->print('

Students Not Allowed to Modify

'); - foreach my $student (@notallowed) { $request->print("$student
\n"); } + $request->print('
'.&Apache::lonhtmlcommon::confirm_success(&mt('Modification of scores not allowed for the following username(s):'),1).'
'); + $request->print(join(', ',@notallowed)); } $request->print("
\n"); - $request->print(&show_grading_menu_form($symb,$url)); - return ''; + $request->print(&show_grading_menu_form($symb)); + return $error_msg; } #------------- end of section for handling csv file upload --------- # @@ -3176,14 +4175,14 @@ sub csvuploadassign { # #--- Select a page/sequence and a student to grade sub pickStudentPage { - my ($request) = shift; + my ($request,$symb) = @_; - $request->print(< + my $alertmsg = &mt('Please select the student you wish to grade.'); + $request->print(&Apache::lonhtmlcommon::scripttag(< LISTJAVASCRIPT &commonJSfunctions($request); - my ($symb,$url) = &get_symb_and_url($request); + my $cdom = $env{"course.$env{'request.course.id'}.domain"}; my $cnum = $env{"course.$env{'request.course.id'}.num"}; my $getsec = $env{'form.section'} eq '' ? 'all' : $env{'form.section'}; - my $result='

 '. - 'Manual Grading by Page or Sequence

'; + my $result='

 '. + &mt('Manual Grading by Page or Sequence').'

'; $result.='
'."\n"; - $result.=' Problems from: '."\n"; my $ctr=0; foreach (@$titles) { my ($minder,$showtitle) = ($_ =~ /(\d+)\.(.*)/); - $result.=''."\n"; $ctr++; } - $result.= ''."
\n"; + $select.= ''; + $result.=' '.&mt('Problems from').': '.$select."
\n"; + $ctr=0; foreach (@$titles) { my ($minder,$showtitle) = ($_ =~ /(\d+)\.(.*)/); @@ -3228,60 +4233,83 @@ LISTJAVASCRIPT $result.=''."\n". ''."\n"; - $result.=' View Problems Text: no '."\n". - ' yes '."
\n"; - - $result.=' Submission Details: '. - ' none'."\n". - ' by dates and submissions'."\n". - ' all details'."\n"; - - $result.=''."\n". - ''."\n". + my $options = + ''."\n". + ''."
\n"; + $result.=' '.&mt('View Problem Text').': '.$options; + + $options = + ''."\n". + ''."\n". + ''."\n"; + $result.=' '.&mt('Submissions').': '.$options; + + $result.=&build_section_inputs(); + my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status')); + $result.=''."\n". ''."\n". - ''."\n". - ''."\n". + ''."\n". ''."
\n"; + $result.=' '.&mt('Use CODE').':
'."\n"; + $result.=' 
'."\n"; + 'onclick="javascript:checkPickOne(this.form);" value="'.&mt('Next').' →" />
'."\n"; $request->print($result); - my $studentTable.=' Select a student you wish to grade and then click on the Next button.
'. - '
'. - ''. - ''. - ''. - ''. - ''; + my $studentTable.=' '.&mt('Select a student you wish to grade and then click on the Next button.').'
'. + &Apache::loncommon::start_data_table(). + &Apache::loncommon::start_data_table_header_row(). + ''. + ''. + ''. + ''. + &Apache::loncommon::end_data_table_header_row(); my (undef,undef,$fullname) = &getclasslist($getsec,'1'); my $ptr = 1; - foreach my $student (sort {lc($$fullname{$a}) cmp lc($$fullname{$b}) } keys %$fullname) { + foreach my $student (sort + { + if (lc($$fullname{$a}) ne lc($$fullname{$b})) { + return (lc($$fullname{$a}) cmp lc($$fullname{$b})); + } + return $a cmp $b; + } (keys(%$fullname))) { my ($uname,$udom) = split(/:/,$student); - $studentTable.=($ptr%2 == 1 ? '' : ''); + $studentTable.=($ptr%2==1 ? &Apache::loncommon::start_data_table_row() + : ''); $studentTable.=''; - $studentTable.='' : ''); + $studentTable.=''.&Apache::loncommon::end_data_table_row() + : ''); $ptr++; } - $studentTable.='
 No.'.&nameUserString('header').' No.'.&nameUserString('header').'
 '.&mt('No.').''.&nameUserString('header').' '.&mt('No.').''.&nameUserString('header').'
'.$ptr.'   ' - .&nameUserString(undef,$$fullname{$student},$uname,$udom)."\n"; - $studentTable.=($ptr%2 == 0 ? '
 \n"; + $studentTable.= + ($ptr%2 == 0 ? '  ' if ($ptr%2 == 0); - $studentTable.='
'."\n"; + if ($ptr%2 == 0) { + $studentTable.='  '. + &Apache::loncommon::end_data_table_row(); + } + $studentTable.=&Apache::loncommon::end_data_table()."\n"; $studentTable.='
'."\n"; + 'onclick="javascript:checkPickOne(this.form);" value="'.&mt('Next').' →" />'."\n"; - $studentTable.=&show_grading_menu_form($symb,$url); + $studentTable.=&show_grading_menu_form($symb); $request->print($studentTable); return ''; } sub getSymbMap { - my ($request) = @_; + 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; @@ -3291,9 +4319,10 @@ sub getSymbMap { 1,0,1); for my $sequence ($navmap->getById('0.0'), @sequences) { if ($navmap->hasResource($sequence, sub { shift->is_problem(); }, 0) ) { - my $title = $minder.'.'.$sequence->compTitle(); - push @titles, $title; # minder in case two titles are identical - $symbx{$title} = $sequence->symb(); + my $title = $minder.'.'. + &HTML::Entities::encode($sequence->compTitle(),'"\'&'); + push(@titles, $title); # minder in case two titles are identical + $symbx{$title} = &HTML::Entities::encode($sequence->symb(),'"\'&'); $minder++; } } @@ -3303,9 +4332,7 @@ sub getSymbMap { # #--- Displays a page/sequence w/wo problems, w/wo submissions sub displayPage { - my ($request) = shift; - - my ($symb,$url) = &get_symb_and_url($request); + my ($request,$symb) = @_; my $cdom = $env{"course.$env{'request.course.id'}.domain"}; my $cnum = $env{"course.$env{'request.course.id'}.num"}; my $getsec = $env{'form.section'} eq '' ? 'all' : $env{'form.section'}; @@ -3322,20 +4349,35 @@ sub displayPage { &Apache::lonnet::clear_EXT_cache_status(); if (!&canview($usec)) { - $request->print('Unable to view requested student.('.$env{'form.student'}.')'); - $request->print(&show_grading_menu_form($symb,$url)); + $request->print(''.&mt('Unable to view requested student. ([_1])',$env{'form.student'}).''); + $request->print(&show_grading_menu_form($symb)); return; } - my $result='

 '.$env{'form.title'}.'

'; - $result.='

 Student: '.&nameUserString(undef,$$fullname{$env{'form.student'}},$uname,$udom). + my $result='

 '.$env{'form.title'}.'

'; + $result.='

 '.&mt('Student: [_1]',&nameUserString(undef,$$fullname{$env{'form.student'}},$uname,$udom)). '

'."\n"; + $env{'form.CODE'} = uc($env{'form.CODE'}); + if (&Apache::lonnet::validCODE(uc($env{'form.CODE'}))) { + $result.='

 '.&mt('CODE: [_1]',$env{'form.CODE'}).'

'."\n"; + } else { + delete($env{'form.CODE'}); + } &sub_page_js($request); $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(''.&mt('Unable to view requested sequence. ([_1])',$resUrl).''); + $request->print(&show_grading_menu_form($symb)); + return; + } my $iterator = $navmap->getIterator($map->map_start(), $map->map_finish()); @@ -3345,21 +4387,27 @@ sub displayPage { ''."\n". ''."\n". ''."\n". - ''."\n". - ''."\n". + ''."\n". ''."\n". ''."\n"; - my $checkIcon = ''; - - $studentTable.=' Note: Problems graded correct by the computer are marked with a '.$checkIcon. - ' symbol.'."\n". - '
'. - ''. - ''. - ''; + if (defined($env{'form.CODE'})) { + $studentTable.= + ''."\n"; + } + my $checkIcon = ''.&mt('Check Mark').
+	''; + + $studentTable.=' '. + &mt('Problems graded correct by the computer are marked with a [_1] symbol.',$checkIcon). + ''."\n". + &Apache::loncommon::start_data_table(). + &Apache::loncommon::start_data_table_header_row(). + ''. + ''. + &Apache::loncommon::end_data_table_header_row(); + &Apache::lonxml::clear_problem_counter(); my ($depth,$question,$prob) = (1,1,1); $iterator->next(); # skip the first BEGIN_MAP my $curRes = $iterator->next(); # for "current resource" @@ -3367,33 +4415,40 @@ sub displayPage { if($curRes == $iterator->BEGIN_MAP) { $depth++; } if($curRes == $iterator->END_MAP) { $depth--; } - if (ref($curRes) && $curRes->is_problem() && !$curRes->randomout) { + if (ref($curRes) && $curRes->is_problem()) { my $parts = $curRes->parts(); my $title = $curRes->compTitle(); my $symbx = $curRes->symb(); - $studentTable.=''; + $studentTable.= + &Apache::loncommon::start_data_table_row(). + ''; $studentTable.='
 Prob.  '.($env{'form.vProb'} eq 'no' ? 'Title' : 'Problem Text').'/Grade
 Prob.  '.($env{'form.vProb'} eq 'no' ? &mt('Title') : &mt('Problem Text')).'/'.&mt('Grade').'
'.$prob. - (scalar(@{$parts}) == 1 ? '' : '
('.scalar(@{$parts}).' parts)').'
'.$prob. + (scalar(@{$parts}) == 1 ? '' + : '
('.&mt('[_1] parts)', + scalar(@{$parts})) + ). + '
'; + my %form = ('CODE' => $env{'form.CODE'},); if ($env{'form.vProb'} eq 'yes' ) { $studentTable.=&show_problem($request,$symbx,$uname,$udom,1, - undef,'both'); + undef,'both',\%form); } else { - my $companswer = &Apache::loncommon::get_student_answers($symbx,$uname,$udom,$env{'request.course.id'}); + my $companswer = &Apache::loncommon::get_student_answers($symbx,$uname,$udom,$env{'request.course.id'},%form); $companswer =~ s|||g; $companswer =~ s|||g; # while ($companswer =~ /()/s) { #\n"); +# $request->print('match='.$1."
\n"); # } # $companswer =~ s||
|g; - $studentTable.=' '.$title.' 
 Correct answer:
'.$companswer; + $studentTable.=' '.$title.' 
 '.&mt('Correct answer').':
'.$companswer; } my %record = &Apache::lonnet::restore($symbx,$env{'request.course.id'},$udom,$uname); if ($env{'form.lastSub'} eq 'datesub') { if ($record{'version'} eq '') { - $studentTable.='
 No recorded submission for this problem
'; + $studentTable.='
 '.&mt('No recorded submission for this problem.').'
'; } else { my %responseType = (); foreach my $partid (@{$parts}) { @@ -3416,11 +4471,13 @@ sub displayPage { } if (&canmodify($usec)) { + $studentTable.=&gradeBox_start(); foreach my $partid (@{$parts}) { $studentTable.=&gradeBox($request,$symbx,$uname,$udom,$question,$partid,\%record); $studentTable.=''."\n"; $question++; } + $studentTable.=&gradeBox_end(); $prob++; } $studentTable.=''; @@ -3429,11 +4486,12 @@ sub displayPage { $curRes = $iterator->next(); } - $studentTable.='
'."\n". - ''. - ''."\n"; - $studentTable.=&show_grading_menu_form($symb,$url); + $studentTable.= + '
'."\n". + ''. + ''."\n"; + $studentTable.=&show_grading_menu_form($symb); $request->print($studentTable); return ''; @@ -3442,63 +4500,105 @@ sub displayPage { sub displaySubByDates { my ($symb,$record,$parts,$responseType,$checkIcon,$uname,$udom) = @_; my $isCODE=0; + my $isTask = ($symb =~/\.task$/); if (exists($record->{'resource.CODE'})) { $isCODE=1; } - my $studentTable='
'. - ''. - ''. - ($isCODE?'':''). - ''. - ''; + my $studentTable=&Apache::loncommon::start_data_table(). + &Apache::loncommon::start_data_table_header_row(). + ''. + ($isCODE?'':''). + ''. + ''. + &Apache::loncommon::end_data_table_header_row(); my ($version); my %mark; my %orders; $mark{'correct_by_student'} = $checkIcon; if (!exists($$record{'1:timestamp'})) { - return '
 Nothing submitted - no attempts
'; + return '
 '.&mt('Nothing submitted - no attempts.').'
'; } + + my $interaction; + my $no_increment = 1; for ($version=1;$version<=$$record{'version'};$version++) { - my $timestamp = scalar(localtime($$record{$version.':timestamp'})); - $studentTable.=''; + my $timestamp = + &Apache::lonlocal::locallocaltime($$record{$version.':timestamp'}); + if (exists($$record{$version.':resource.0.version'})) { + $interaction = $$record{$version.':resource.0.version'}; + } + + my $where = ($isTask ? "$version:resource.$interaction" + : "$version:resource"); + $studentTable.=&Apache::loncommon::start_data_table_row(). + ''; if ($isCODE) { $studentTable.=''; } my @versionKeys = split(/\:/,$$record{$version.':keys'}); my @displaySub = (); foreach my $partid (@{$parts}) { - my @matchKey = sort(grep /^resource\.\Q$partid\E\..*?\.submission$/,@versionKeys); + my $hidden; + if (($$record{$version.':resource.'.$partid.'.type'} eq 'anonsurvey') || + ($$record{$version.':resource.'.$partid.'.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,undef,$symb); + my $display_part=&get_display_part($partid,$symb); foreach my $matchKey (@matchKey) { if (exists($$record{$version.':'.$matchKey}) && $$record{$version.':'.$matchKey} ne '') { - my ($responseId)=($matchKey=~ /^resource\.\Q$partid\E\.(.*?)\.submission$/); - $displaySub[0].='Part: '.$display_part.' '; - $displaySub[0].='(ID '. - $responseId.') '; - if ($$record{"$version:resource.$partid.tries"} eq '') { - $displaySub[0].='Trial not counted'; - } else { - $displaySub[0].='Trial '. - $$record{"$version:resource.$partid.tries"}; - } - my $responseType=$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].='  '. - &cleanRecord($$record{$version.':'.$matchKey},$responseType,$symb,$partid,$responseId,$record,$orders{$partid}->{$responseId},"$version:").'
'; + + my ($responseId)= ($isTask ? ($matchKey=~ /^resource\.(.*?)\.\Q$partid\E\.award$/) + : ($matchKey=~ /^resource\.\Q$partid\E\.(.*?)\.submission$/)); + $displaySub[0].='' + .' ' + .'('.&mt('Part ID: [_1]',$responseId).')' + .'' + .' '; + if ($hidden) { + $displaySub[0].= &mt('Anonymous Survey').''; + } else { + 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' + : $responseType->{$partid}->{$responseId}); + if (!exists($orders{$partid})) { $orders{$partid}={}; } + if (!exists($orders{$partid}->{$responseId})) { + $orders{$partid}->{$responseId}= + &get_order($partid,$responseId,$symb,$uname,$udom, + $no_increment); + } + $displaySub[0].=''; # /nobreak + $displaySub[0].='  '. + &cleanRecord($$record{$version.':'.$matchKey},$responseType,$symb,$partid,$responseId,$record,$orders{$partid}->{$responseId},"$version:",$uname,$udom).'
'; + } } } - if (exists $$record{"$version:resource.$partid.award"}) { - $displaySub[1].='Part: '.$display_part.'  '. - lc($$record{"$version:resource.$partid.award"}).' '. - $mark{$$record{"$version:resource.$partid.solved"}}. + if (exists($$record{"$where.$partid.checkedin"})) { + $displaySub[1].=&mt('Checked in by [_1] into slot [_2]', + $$record{"$where.$partid.checkedin"}, + $$record{"$where.$partid.checkedin.slot"}). + '
'; + } + if (exists $$record{"$where.$partid.award"}) { + $displaySub[1].=''.&mt('Part:').' '.$display_part.'  '. + lc($$record{"$where.$partid.award"}).' '. + $mark{$$record{"$where.$partid.solved"}}. '
'; } - if (exists $$record{"$version:resource.$partid.regrader"}) { - $displaySub[2].=$$record{"$version:resource.$partid.regrader"}. + if (exists $$record{"$where.$partid.regrader"}) { + $displaySub[2].=$$record{"$where.$partid.regrader"}. + ' ('.&mt('Part').': '.$display_part.')'; + } elsif ($$record{"$version:resource.$partid.regrader"} =~ /\S/) { + $displaySub[2].= + $$record{"$version:resource.$partid.regrader"}. ' ('.&mt('Part').': '.$display_part.')'; } } @@ -3508,17 +4608,17 @@ sub displaySubByDates { } $studentTable.=''; - + $studentTable.=' '. + &Apache::loncommon::end_data_table_row(); } - $studentTable.='
Date/TimeCODESubmissionStatus 
'.&mt('Date/Time').''.&mt('CODE').''.&mt('Submission').''.&mt('Status').'
'.$timestamp.''.$timestamp.''.$record->{$version.':resource.CODE'}.''.$displaySub[0].' '.$displaySub[1]; if ($displaySub[2]) { - $studentTable.='Manually graded by '.$displaySub[2]; + $studentTable.=&mt('Manually graded by [_1]',$displaySub[2]); } - $studentTable.=' 
'; + $studentTable.=&Apache::loncommon::end_data_table(); return $studentTable; } sub updateGradeByPage { - my ($request) = shift; + my ($request,$symb) = @_; my $cdom = $env{"course.$env{'request.course.id'}.domain"}; my $cnum = $env{"course.$env{'request.course.id'}.num"}; @@ -3528,29 +4628,40 @@ sub updateGradeByPage { my ($uname,$udom) = split(/:/,$env{'form.student'}); my $usec=$classlist->{$env{'form.student'}}[5]; if (!&canmodify($usec)) { - $request->print('Unable to modify requested student.('.$env{'form.student'}.''); - $request->print(&show_grading_menu_form($env{'form.symb'},$env{'form.url'})); + $request->print(''.&mt('Unable to modify requested student ([_1])',$env{'form.student'}).''); + $request->print(&show_grading_menu_form($env{'form.symb'})); return; } - my $result='

 '.$env{'form.title'}.'

'; - $result.='

 Student: '.&nameUserString(undef,$env{'form.fullname'},$uname,$udom). + my $result='

 '.$env{'form.title'}.'

'; + $result.='

 '.&mt('Student: ').&nameUserString(undef,$env{'form.fullname'},$uname,$udom). '

'."\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(''.&mt('Unable to grade requested sequence ([_1]).',$resUrl).''); + $request->print(&show_grading_menu_form($symb)); + return; + } my $iterator = $navmap->getIterator($map->map_start(), $map->map_finish()); - my $studentTable='
'. - ''. - ''. - ''. - ''. - ''; + my $studentTable= + &Apache::loncommon::start_data_table(). + &Apache::loncommon::start_data_table_header_row(). + ''. + ''. + ''. + ''. + &Apache::loncommon::end_data_table_header_row(); $iterator->next(); # skip the first BEGIN_MAP my $curRes = $iterator->next(); # for "current resource" @@ -3559,16 +4670,22 @@ sub updateGradeByPage { if($curRes == $iterator->BEGIN_MAP) { $depth++; } if($curRes == $iterator->END_MAP) { $depth--; } - if (ref($curRes) && $curRes->is_problem() && !$curRes->randomout) { + if (ref($curRes) && $curRes->is_problem()) { my $parts = $curRes->parts(); my $title = $curRes->compTitle(); my $symbx = $curRes->symb(); - $studentTable.=''; + $studentTable.= + &Apache::loncommon::start_data_table_row(). + ''; $studentTable.=''; my %newrecord=(); my @displayPts=(); + my %aggregate = (); + my $aggregateflag = 0; foreach my $partid (@{$parts}) { my $newpts = $env{'form.GD_BOX'.$question.'_'.$partid}; my $oldpts = $env{'form.oldpts'.$question.'_'.$partid}; @@ -3595,19 +4712,25 @@ sub updateGradeByPage { $newrecord{'resource.'.$partid.'.regrader'} = "$env{'user.name'}:$env{'user.domain'}"; $changeflag++; $newpts = ''; + + my $aggtries = $env{'form.aggtries'.$question.'_'.$partid}; + my $totaltries = $env{'form.totaltries'.$question.'_'.$partid}; + my $solvedstatus = $env{'form.solved'.$question.'_'.$partid}; + if ($aggtries > 0) { + &decrement_aggs($symbx,$partid,\%aggregate,$aggtries,$totaltries,$solvedstatus); + $aggregateflag = 1; + } } - my $display_part=&get_display_part($partid,undef, - $curRes->symb()); + my $display_part=&get_display_part($partid,$curRes->symb()); my $oldstatus = $env{'form.solved'.$question.'_'.$partid}; - $displayPts[0].=' Part: '.$display_part.' = '. + $displayPts[0].=' '.&mt('Part').': '.$display_part.' = '. (($oldstatus eq 'excused') ? 'excused' : $oldpts). - ' 
'; - $displayPts[1].=' Part: '.$display_part.' = '. + ' 
'; + $displayPts[1].=' '.&mt('Part').': '.$display_part.' = '. (($score eq 'excused') ? 'excused' : $newpts). - ' 
'; - + ' 
'; $question++; - next if ($dropMenu eq 'reset status' || ($newpts == $oldpts && $score ne 'excused')); + next if ($dropMenu eq 'reset status' || ($newpts eq $oldpts && $score ne 'excused')); $newrecord{'resource.'.$partid.'.awarded'} = $partial if $partial ne ''; $newrecord{'resource.'.$partid.'.solved'} = $score if $score ne ''; @@ -3617,24 +4740,44 @@ sub updateGradeByPage { $changeflag++; } if (scalar(keys(%newrecord)) > 0) { + my %record = + &Apache::lonnet::restore($symbx,$env{'request.course.id'}, + $udom,$uname); + + if (&Apache::lonnet::validCODE($env{'form.CODE'})) { + $newrecord{'resource.CODE'} = $env{'form.CODE'}; + } elsif (&Apache::lonnet::validCODE($record{'resource.CODE'})) { + $newrecord{'resource.CODE'} = ''; + } &Apache::lonnet::cstore(\%newrecord,$symbx,$env{'request.course.id'}, $udom,$uname); + %record = &Apache::lonnet::restore($symbx, + $env{'request.course.id'}, + $udom,$uname); + &check_and_remove_from_queue($parts,\%record,undef,$symbx, + $cdom,$cnum,$udom,$uname); } + + if ($aggregateflag) { + &Apache::lonnet::cinc('nohist_resourcetracker',\%aggregate, + $env{'course.'.$env{'request.course.id'}.'.domain'}, + $env{'course.'.$env{'request.course.id'}.'.num'}); + } $studentTable.=''. ''. - ''; + &Apache::loncommon::end_data_table_row(); $prob++; } $curRes = $iterator->next(); } - $studentTable.='
 Prob.  Title  Previous Score  New Score 
 '.&mt('Prob.').'  '.&mt('Title').'  '.&mt('Previous Score').'  '.&mt('New Score').' 
'.$prob. - (scalar(@{$parts}) == 1 ? '' : '
('.scalar(@{$parts}).' parts)').'
'.$prob. + (scalar(@{$parts}) == 1 ? '' + : '
('.&mt('[quant,_1, part]',scalar(@{$parts})) + .')').'
 '.$title.' '.$displayPts[0].''.$displayPts[1].'
'; - $studentTable.=&show_grading_menu_form($env{'form.symb'},$env{'form.url'}); - my $grademsg=($changeflag == 0 ? 'No score was changed or updated.' : - 'The scores were changed for '. - $changeflag.' problem'.($changeflag == 1 ? '.' : 's.')); + $studentTable.=&Apache::loncommon::end_data_table(); + $studentTable.=&show_grading_menu_form($env{'form.symb'}); + my $grademsg=($changeflag == 0 ? &mt('No score was changed or updated.') : + &mt('The scores were changed for [quant,_1,problem].', + $changeflag)); $request->print($grademsg.$studentTable); return ''; @@ -3644,29 +4787,102 @@ sub updateGradeByPage { # #------------------------------------------------------------------- -#--------------------Scantron Grading----------------------------------- +#-------------------- Bubblesheet (Scantron) Grading ------------------- # #------ start of section for handling grading by page/sequence --------- +=pod + +=head1 Bubble sheet grading routines + + For this documentation: + + 'scanline' refers to the full line of characters + 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 + + +The overall process is that a scanned in bubble sheet 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 +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/employee ID + +If the CODE option is used that determines the randomization of the +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 + + 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 +file (see &scantron_getfile() for more information) + +After all scanlines are either valid, marked as valid or skipped, then +foreach line foreach problem in the picked sequence, an ssi request is +made that simulates a user submitting their selected letter(s) against +the homework problem. + +=over 4 + + + +=item defaultFormData + + Returns html hidden inputs used to hold context/default values. + + Arguments: + $symb - $symb of the current resource + +=cut + sub defaultFormData { - my ($symb,$url)=@_; - return ' - '."\n". - ''."\n". - ''."\n". - ''."\n"; + my ($symb)=@_; + return ''."\n". + ''; } + +=pod + +=item getSequenceDropDown + + Return html dropdown of possible sequences to grade + + Arguments: + $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 ($request,$symb)=@_; + my ($symb,$map_error)=@_; my $result=''; $result.=""; foreach my $filename (sort(&scantron_filenames())) { - $result.="$filename\n"; + $result.="$filename\n"; } $result.=""; return $result; } +=pod + +=item scantron_scantab + + Returns html drop down of the scantron formats in the scantronformat.tab + file. + +=cut + sub scantron_scantab { - my $fh=Apache::File->new($Apache::lonnet::perlvar{'lonTabDir'}.'/scantronformat.tab'); my $result=''."\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 + + Returns html drop down of the saved CODE lists from current course, + generated from earlier printings. + +=cut + sub scantron_CODElist { my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'}; @@ -3721,173 +5094,289 @@ sub scantron_CODElist { my $namechoice=''; foreach my $name (sort {uc($a) cmp uc($b)} @names) { if ($name =~ /^error: 2 /) { next; } + if ($name =~ /^type\0/) { next; } $namechoice.=''; } $namechoice=''; return $namechoice; } +=pod + +=item scantron_CODEunique + + Returns the html for "Each CODE to be used once" radio. + +=cut + sub scantron_CODEunique { - my $result=' - Yes - - - No - '; + my $result=' + + + + + '; return $result; } +=pod + +=item scantron_selectphase + + Generates the initial screen to start the bubble sheet process. + Allows for - starting a grading run. + - downloading existing scan data (original, corrected + or skipped info) + + - uploading new scan data + + Arguments: + $r - The Apache request object + $file2grade - name of the file that contain the scanned data to score + +=cut + sub scantron_selectphase { - my ($r,$file2grade) = @_; - my ($symb,$url)=&get_symb_and_url($r); + my ($r,$file2grade,$symb) = @_; if (!$symb) {return '';} - my $sequence_selector=&getSequenceDropDown($r,$symb); - my $default_form_data=&defaultFormData($symb,$url); - my $grading_menu_button=&show_grading_menu_form($symb,$url); + my $map_error; + my $sequence_selector=&getSequenceDropDown($symb,\$map_error); + if ($map_error) { + $r->print('
'.&navmap_errormsg().'
'); + return; + } + my $default_form_data=&defaultFormData($symb); + my $grading_menu_button=&show_grading_menu_form($symb); my $file_selector=&scantron_uploads($file2grade); my $format_selector=&scantron_scantab(); my $CODE_selector=&scantron_CODElist(); my $CODE_unique=&scantron_CODEunique(); my $result; - #FIXME allow instructor to be able to download the scantron file - # and to upload it, - $result.= < - -
- - - $default_form_data - - - - - - - - - - - - - - - - - - - - - - - - - - -
-  Specify file and which Folder/Sequence to grade -
Sequence to grade: $sequence_selector
Filename of scoring office file: $file_selector
Format of data file: $format_selector
Saved CODEs to validate against: $CODE_selector
Each CODE is only to be used once: $CODE_unique
Options: - Do only previously skipped records
- Remove all exisiting corrections -
- -
- -
- -SCANTRONFORM - - $r->print($result); + + $ssi_error = 0; if (&Apache::lonnet::allowed('usc',$env{'request.role.domain'}) || &Apache::lonnet::allowed('usc',$env{'request.course.id'})) { - $r->print(< - - - - - - + # Chunk of form to prompt for a scantron file upload. + + $r->print(' +
+ '.&Apache::loncommon::start_data_table('LC_scantron_action').' + '.&Apache::loncommon::start_data_table_header_row().' + + '.&Apache::loncommon::end_data_table_header_row().' + '.&Apache::loncommon::start_data_table_row().' - -
-  Specify a Scantron data file to upload. -
+  '.&mt('Specify a bubblesheet data file to upload.').' + -SCANTRONFORM - my $default_form_data=&defaultFormData(&get_symb_and_url($r,1)); +'); + my $default_form_data=&defaultFormData($symb); my $cdom= $env{'course.'.$env{'request.course.id'}.'.domain'}; my $cnum= $env{'course.'.$env{'request.course.id'}.'.num'}; - $r->print(< + $r->print(&Apache::lonhtmlcommon::scripttag(' function checkUpload(formname) { if (formname.upfile.value == "") { - alert("Please use the browse button to select a file from your local directory."); + alert("'.&mt('Please use the browse button to select a file from your local directory.').'"); return false; } formname.submit(); - } - - -
- $default_form_data - - - - File to upload: + }')); + $r->print(' + + '.$default_form_data.' + + + + '.&mt('File to upload: [_1]','').'
- +
-UPLOAD +'); - $r->print(<print('
- - -SCANTRONFORM - } - $r->print(< -
- - - - - - - - - - - - -
-  Download a scoring office file -
Filename of scoring office file: $file_selector
- -
- -
- -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.= ' +
+
+ + '.$default_form_data.' + '.&Apache::loncommon::start_data_table('LC_scantron_action').' + '.&Apache::loncommon::start_data_table_header_row().' + +  '.&mt('Specify file and which Folder/Sequence to grade').' + + '.&Apache::loncommon::end_data_table_header_row().' + '.&Apache::loncommon::start_data_table_row().' + '.&mt('Sequence to grade:').' '.$sequence_selector.' + '.&Apache::loncommon::end_data_table_row().' + '.&Apache::loncommon::start_data_table_row().' + '.&mt('Filename of bubblesheet data file:').' '.$file_selector.' + '.&Apache::loncommon::end_data_table_row().' + '.&Apache::loncommon::start_data_table_row().' + '.&mt('Format of bubblesheet data file:').' '.$format_selector.' + '.&Apache::loncommon::end_data_table_row().' + '.&Apache::loncommon::start_data_table_row().' + '.&mt('Saved CODEs to validate against:').' '.$CODE_selector.' + '.&Apache::loncommon::end_data_table_row().' + '.&Apache::loncommon::start_data_table_row().' + '.&mt('Each CODE is only to be used once:').' '.$CODE_unique.' + '.&Apache::loncommon::end_data_table_row().' + '.&Apache::loncommon::start_data_table_row().' + '.&mt('Options:').' + +
+
+ + + '.&Apache::loncommon::end_data_table_row().' + '.&Apache::loncommon::start_data_table_row().' + + + + '.&Apache::loncommon::end_data_table_row().' + '.&Apache::loncommon::end_data_table().' +
+'; + + $r->print($result); - $r->print(< -$grading_menu_button -SCANTRONFORM - return + + # Chunk of the form that prompts to view a scoring office file, + # corrected file, skipped records in a file. + + $r->print(' +
+
+ '.$default_form_data.' + + '.&Apache::loncommon::start_data_table('LC_scantron_action').' + '.&Apache::loncommon::start_data_table_header_row().' + +  '.&mt('Download a scoring office file').' + + '.&Apache::loncommon::end_data_table_header_row().' + '.&Apache::loncommon::start_data_table_row().' + '.&mt('Filename of scoring office file: [_1]',$file_selector).' +
+ + '.&Apache::loncommon::end_data_table_row().' + '.&Apache::loncommon::end_data_table().' +
+
+'); + + &Apache::lonpickcode::code_list($r,2); + + $r->print('
'. + $default_form_data."\n". + &Apache::loncommon::start_data_table('LC_scantron_action')."\n". + &Apache::loncommon::start_data_table_header_row()."\n". + ' +  '.&mt('Review bubblesheet data and submissions for a previously graded folder/sequence')."\n". + ''."\n". + &Apache::loncommon::end_data_table_header_row()."\n". + &Apache::loncommon::start_data_table_row()."\n". + ' '.&mt('Graded folder/sequence:').' '."\n". + ' '.$sequence_selector.' '. + &Apache::loncommon::end_data_table_row()."\n". + &Apache::loncommon::start_data_table_row()."\n". + ' '.&mt('Filename of scoring office file:').' '."\n". + ' '.$file_selector.' '."\n". + &Apache::loncommon::end_data_table_row()."\n". + &Apache::loncommon::start_data_table_row()."\n". + ' '.&mt('Format of data file:').' '."\n". + ' '.$format_selector.' '."\n". + &Apache::loncommon::end_data_table_row()."\n". + &Apache::loncommon::start_data_table_row()."\n". + ' '.&mt('Options').' '."\n". + ' '. + &Apache::loncommon::end_data_table_row()."\n". + &Apache::loncommon::start_data_table_row()."\n". + ''."\n". + ''."\n". + ''."\n". + ''."\n". + &Apache::loncommon::end_data_table_row()."\n". + &Apache::loncommon::end_data_table()."\n". + '

'); + $r->print($grading_menu_button); + return; } +=pod + +=item get_scantron_config + + Parse and return the scantron configuration line selected as a + hash of configuration file fields. + + Arguments: + which - the name of the configuration to parse from the file. + + + Returns: + If the named configuration is not in the file, an empty + hash is returned. + a hash with the fields + name - internal name for the this configuration setup + description - text to display to operator that describes this config + CODElocation - if 0 or the string 'none' + - no CODE exists for this config + if -1 || the string 'letter' + - a CODE exists for this config and is + a string of letters + Unsupported value (but planned for future support) + if a positive integer + - The CODE exists as the first n items from + the question section of the form + if the string 'number' + - The CODE exists for this config and is + a string of numbers + 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/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 + the sheet. (usually either 1 or 10) + Qon - either a single character representing the character used + to signal a bubble was chosen in the positional setup, or + the string 'letter' if the letter of the chosen bubble is + in the final, or 'number' if a number representing the + chosen bubble is in the file (1->A 0->J) + Qoff - the character used to represent that a bubble was + left blank + PaperID - if the scanning process generates a unique number for each + sheet scanned the column that this ID number starts in + PaperIDlength - number of columns that comprise the unique ID number + for the sheet of paper + FirstName - column that the first name starts in + FirstNameLength - number of columns that the first name spans + + LastName - column that the last name starts in + LastNameLength - number of columns that the last name spans + +=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); @@ -3900,7 +5389,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]; @@ -3914,6 +5403,25 @@ sub get_scantron_config { return %config; } +=pod + +=item username_to_idmap + + creates a hash keyed by student/employee ID with values of the corresponding + student username:domain. + + Arguments: + + $classlist - reference to the class list hash. This is a hash + keyed by student name:domain whose elements are references + to arrays containing various chunks of information + about the student. (See loncoursedata for more info). + + Returns + %idmap - the constructed hash + +=cut + sub username_to_idmap { my ($classlist)= @_; my %idmap; @@ -3924,6 +5432,49 @@ sub username_to_idmap { return %idmap; } +=pod + +=item scantron_fixup_scanline + + Process a requested correction to a scanline. + + Arguments: + $scantron_config - hash from &get_scantron_config() + $scan_data - hash of correction information + (see &scantron_getfile()) + $line - existing scanline + $whichline - line number of the passed in scanline + $field - type of change to process + (either + 'ID' -> correct the student/employee ID + 'CODE' -> correct the CODE + 'answer' -> fixup the submitted answers) + + $args - hash of additional info, + - 'ID' + 'newid' -> studentID to use in replacement + of existing one + - 'CODE' + 'CODE_ignore_dup' - set to true if duplicates + should be ignored. + 'CODE' - is new code or 'use_unfound' + if the existing unfound code should + be used as is + - '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 + + Side effects: + $scan_data - may be updated + +=cut + + sub scantron_fixup_scanline { my ($scantron_config,$scan_data,$line,$whichline,$field,$args)=@_; if ($field eq 'ID') { @@ -3962,11 +5513,19 @@ 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 { - substr($answer,$args->{'response'},1)=$on; + if ($on eq 'letter') { + my @alphabet=('A'..'Z'); + $answer=$alphabet[$args->{'response'}]; + } elsif ($on eq 'number') { + $answer=$args->{'response'}+1; + if ($answer == 10) { $answer = '0'; } + } else { + 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; @@ -3974,6 +5533,25 @@ sub scantron_fixup_scanline { return $line; } +=pod + +=item scan_data + + Edit or look up an item in the scan_data hash. + + Arguments: + $scan_data - The hash (see scantron_getfile) + $key - shorthand of the key to edit (actual key is + scantronfilename_key). + $data - New value of the hash entry. + $delete - If true, the entry is removed from the hash. + + Returns: + The new value of the hash table field (undefined if deleted). + +=cut + + sub scan_data { my ($scan_data,$key,$value,$delete)=@_; my $filename=$env{'form.scantron_selectfile'}; @@ -3984,13 +5562,108 @@ 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 + + Decodes a scanline from the selected scantron file + + Arguments: + line - The text of the scantron file line to process + whichline - Line number + scantron_config - Hash describing the format of the scantron lines. + scan_data - Hash of extra information about the scanline + (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. + Returns: + Hash containing the result of parsing the scanline + + Keys are all proceeded by the string 'scantron.' + + CODE - the CODE in use for this scanline + useCODE - 1 if the CODE is invalid but it usage has been forced + by the operator + 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/employee ID + PaperID - if used, the ID number printed on the sheet when the + paper was scanned + FirstName - first name from the sheet + LastName - last name from the sheet + + if just_header was not true these key may also exist + + missingerror - a list of bubble ranges that are considered to be answers + to a single question that don't have any bubbles filled in. + Of the form questionnumber:firstbubblenumber:count. + doubleerror - a list of bubble ranges that are considered to be answers + to a single question that have more than one bubble filled in. + Of the form questionnumber::firstbubblenumber:count + + In the above, count is the number of bubble responses in the + input line needed to represent the possible answers to the question. + e.g. a radioresponse with 15 choices in an answer sheet with 10 choices + per line would have count = 2. + + maxquest - the number of the last bubble line that was parsed + + ( starts at 1) + .answer - zero or more letters representing the selected + letters from the scanline for the bubble line + . + if blank there was either no bubble or there where + multiple bubbles, (consult the keys missingerror and + doubleerror if this is an error condition) + +=cut + sub scantron_parse_scanline { - my ($line,$whichline,$scantron_config,$scan_data,$justHeader)=@_; + my ($line,$whichline,$scantron_config,$scan_data,$just_header)=@_; + my %record; - my $questions=substr($line,$$scantron_config{'Qstart'}-1); - my $data=substr($line,0,$$scantron_config{'Qstart'}-1); - if ($$scantron_config{'CODElocation'} ne 0) { - if ($$scantron_config{'CODElocation'} < 0) { + my $lastpos = $env{'form.scantron_maxbubble'}*$$scantron_config{'Qlength'}; + my $questions=substr($line,$$scantron_config{'Qstart'}-1,$lastpos); # Answers + my $data=substr($line,0,$$scantron_config{'Qstart'}-1); # earlier stuff + if (!($$scantron_config{'CODElocation'} eq 0 || + $$scantron_config{'CODElocation'} eq 'none')) { + if ($$scantron_config{'CODElocation'} < 0 || + $$scantron_config{'CODElocation'} eq 'letter' || + $$scantron_config{'CODElocation'} eq 'number') { $record{'scantron.CODE'}=substr($data, $$scantron_config{'CODEstart'}-1, $$scantron_config{'CODElength'}); @@ -4015,63 +5688,248 @@ sub scantron_parse_scanline { $record{'scantron.LastName'}= substr($data,$$scantron_config{'LastName'}-1, $$scantron_config{'LastNamelength'}); - if ($justHeader) { return \%record; } + if ($just_header) { return \%record; } my @alphabet=('A'..'Z'); my $questnum=0; - while ($questions) { - $questnum++; - my $currentquest=substr($questions,0,$$scantron_config{'Qlength'}); - substr($questions,0,$$scantron_config{'Qlength'})=''; - if (length($currentquest) < $$scantron_config{'Qlength'}) { next; } - if ($$scantron_config{'Qon'} eq 'letter') { - if (!$currentquest || $currentquest eq $$scantron_config{'Qoff'} || - $currentquest !~ /^[A-Z]$/) { - $record{"scantron.$questnum.answer"}=''; - if (!&scan_data($scan_data,"$whichline.no_bubble.$questnum")) { - push(@{$record{"scantron.missingerror"}},$questnum); - } - } else { - $record{"scantron.$questnum.answer"}=$currentquest; - } - } elsif ($$scantron_config{'Qon'} eq 'number') { - if (!$currentquest || $currentquest eq $$scantron_config{'Qoff'} || - $currentquest !~ /^\d$/) { - $record{"scantron.$questnum.answer"}=''; - if (!&scan_data($scan_data,"$whichline.no_bubble.$questnum")) { - push(@{$record{"scantron.missingerror"}},$questnum); - } - } else { - $record{"scantron.$questnum.answer"}= - $alphabet[$currentquest-1]; - } - } else { - my @array=split($$scantron_config{'Qon'},$currentquest,-1); - if (length($array[0]) eq $$scantron_config{'Qlength'}) { - $record{"scantron.$questnum.answer"}=''; - if (!&scan_data($scan_data,"$whichline.no_bubble.$questnum")) { - push(@{$record{"scantron.missingerror"}},$questnum); - } - } else { - $record{"scantron.$questnum.answer"}= - $alphabet[length($array[0])]; - } - if (scalar(@array) gt 2) { - push(@{$record{'scantron.doubleerror'}},$questnum); - my @ans=@array; - my $i=length($ans[0]);shift(@ans); - while ($#ans) { - $i+=length($ans[0])+1; - $record{"scantron.$questnum.answer"}.=$alphabet[$i]; - shift(@ans); - } - } - } + my $ansnum =1; # Multiple 'answer lines'/question. + + 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 = $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; } + + if ($subdivided_bubble_lines{$questnum-1} =~ /,/) { + my $subquestnum = 1; + my $subquestions = $currentquest; + my @subanswers_needed = + split(/,/,$subdivided_bubble_lines{$questnum-1}); + 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); + } else { + $ansnum = &scantron_validator_positional($ansnum, + $questnum,$quest_id,$subans,$currsubquest,$whichline, \@alphabet,\%record,$scantron_config,$scan_data); + } + $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); + } else { + $ansnum = &scantron_validator_positional($ansnum,$questnum, + $quest_id,$answers_needed,$currentquest,$whichline, + \@alphabet,\%record,$scantron_config,$scan_data); + } + } } $record{'scantron.maxquest'}=$questnum; return \%record; } +sub scantron_validator_lettnum { + my ($ansnum,$questnum,$quest_id,$answers_needed,$currquest,$whichline, + $alphabet,$record,$scantron_config,$scan_data) = @_; + + # 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. + # + + my $matchon; + if ($$scantron_config{'Qon'} eq 'letter') { + $matchon = '[A-Z]'; + } elsif ($$scantron_config{'Qon'} eq 'number') { + $matchon = '\d'; + } + my $occurrences = 0; + if (($responsetype_per_response{$questnum-1} eq 'essayresponse') || + ($responsetype_per_response{$questnum-1} eq 'formularesponse') || + ($responsetype_per_response{$questnum-1} eq 'stringresponse') || + ($responsetype_per_response{$questnum-1} eq 'imageresponse') || + ($responsetype_per_response{$questnum-1} eq 'reactionresponse') || + ($responsetype_per_response{$questnum-1} eq 'organicresponse')) { + my @singlelines = split('',$currquest); + foreach my $entry (@singlelines) { + $occurrences = &occurence_count($entry,$matchon); + 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; +} + +sub scantron_validator_positional { + my ($ansnum,$questnum,$quest_id,$answers_needed,$currquest, + $whichline,$alphabet,$record,$scantron_config,$scan_data) = @_; + + # 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. + # + + my @array=split($$scantron_config{'Qon'},$currquest,-1); + + # If the split only gives us one element.. the full length of the + # answer string, no bubbles are filled in: + + if ($answers_needed eq '') { + return; + } + + if (length($array[0]) eq $$scantron_config{'Qlength'}*$answers_needed) { + for (my $ans=0; $ans<$answers_needed; $ans++ ) { + $record->{"scantron.$ansnum.answer"}=''; + $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. + # + if (($responsetype_per_response{$questnum-1} eq 'essayresponse') || + ($responsetype_per_response{$questnum-1} eq 'formularesponse') || + ($responsetype_per_response{$questnum-1} eq 'stringresponse') || + ($responsetype_per_response{$questnum-1} eq 'imageresponse') || + ($responsetype_per_response{$questnum-1} eq 'reactionresponse') || + ($responsetype_per_response{$questnum-1} 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 ++; + } + + 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; + } + return $ansnum; +} + +=pod + +=item scantron_add_delay + + Adds an error message that occurred during the grading phase to a + queue of messages to be shown after grading pass is complete + + Arguments: + $delayqueue - arrary ref of hash ref of error messages + $scanline - the scanline that caused the error + $errormesage - the error message + $errorcode - a numeric code for the error + + Side Effects: + updates the $delayqueue to have a new hash ref of the error + +=cut + sub scantron_add_delay { my ($delayqueue,$scanline,$errormessage,$errorcode)=@_; push(@$delayqueue, @@ -4080,6 +5938,24 @@ sub scantron_add_delay { ); } +=pod + +=item scantron_find_student + + Finds the username for the current scanline + + Arguments: + $scantron_record - hash result from scantron_parse_scanline + $scan_data - hash of correction information + (see &scantron_getfile() form more information) + $idmap - hash from &username_to_idmap() + $line - number of current scanline + + Returns: + Either 'username:domain' or undef if unknown + +=cut + sub scantron_find_student { my ($scantron_record,$scan_data,$idmap,$line)=@_; my $scanID=$$scantron_record{'scantron.ID'}; @@ -4094,15 +5970,40 @@ sub scantron_find_student { return undef; } +=pod + +=item scantron_filter + + Filter sub for lonnavmaps, filters out hidden resources if ignore + hidden resources was selected + +=cut + sub scantron_filter { my ($curres)=@_; - # randomout is dysfunctional at best for this purpose - if (ref($curres) && $curres->is_problem()) { #&& !$curres->randomout) { + + if (ref($curres) && $curres->is_problem()) { + # if the user has asked to not have either hidden + # or 'randomout' controlled resources to be graded + # don't include them + if ($env{'form.scantron_options_hidden'} eq 'ignore_hidden' + && $curres->randomout) { + return 0; + } return 1; } return 0; } +=pod + +=item scantron_process_corrections + + Gets correction information out of submitted form data and corrects + the scanline + +=cut + sub scantron_process_corrections { my ($r) = @_; my %scantron_config=&get_scantron_config($env{'form.scantron_format'}); @@ -4148,42 +6049,86 @@ 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("Unable to accept last correction, an error occurred :$errmsg:"); + $r->print("Unable to accept last correction, an error occurred :$errmsg:"); } else { &scantron_put_line($scanlines,$scan_data,$which,$line,$skip); &scantron_putfile($scanlines,$scan_data); } } +=pod + +=item reset_skipping_status + + Forgets the current set of remember skipped scanlines (and thus + reverts back to considering all lines in the + scantron_skipped_ file) + +=cut + sub reset_skipping_status { my ($scanlines,$scan_data)=&scantron_getfile(); &scan_data($scan_data,'remember_skipping',undef,1); &scantron_putfile(undef,$scan_data); } -sub allow_skipping { +=pod + +=item start_skipping + + Marks a scanline to be skipped. + +=cut + +sub start_skipping { my ($scan_data,$i)=@_; my %remembered=split(':',&scan_data($scan_data,'remember_skipping')); - delete($remembered{$i}); + if ($env{'form.scantron_options_redo'} =~ /^redo_/) { + $remembered{$i}=2; + } else { + $remembered{$i}=1; + } &scan_data($scan_data,'remember_skipping',join(':',%remembered)); } +=pod + +=item should_be_skipped + + Checks whether a scanline should be skipped. + +=cut + sub should_be_skipped { - my ($scan_data,$i)=@_; + my ($scanlines,$scan_data,$i)=@_; if ($env{'form.scantron_options_redo'} !~ /^redo_/) { # not redoing old skips + if ($scanlines->{'skipped'}[$i]) { return 1; } return 0; } my %remembered=split(':',&scan_data($scan_data,'remember_skipping')); - if (exists($remembered{$i})) { return 0; } + + if (exists($remembered{$i}) && $remembered{$i} != 2 ) { + return 0; + } return 1; } +=pod + +=item remember_current_skipped + + Discovers what scanlines are in the scantron_skipped_ + file and remembers them into scan_data for later use. + +=cut + sub remember_current_skipped { my ($scanlines,$scan_data)=&scantron_getfile(); my %to_remember; @@ -4192,70 +6137,116 @@ sub remember_current_skipped { $to_remember{$i}=1; } } - &Apache::lonnet::logthis('remembering '.join(':',%to_remember)); + &scan_data($scan_data,'remember_skipping',join(':',%to_remember)); &scantron_putfile(undef,$scan_data); } +=pod + +=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 + something went wrong. + +=cut + sub check_for_error { my ($r,$result)=@_; if ($result ne 'ok' && $result ne 'not_found' ) { - $r->print("An error occured ($result) when trying to Remove the existing corrections."); + $r->print(&mt("An error occurred ([_1]) when trying to remove the existing corrections.",$result)); } } +=pod + +=item scantron_warning_screen + + Interstitial screen to make sure the operator has selected the + correct options before we start the validation phase. + +=cut + sub scantron_warning_screen { my ($button_text)=@_; my $title=&Apache::lonnet::gettitle($env{'form.selectpage'}); - return (<'.&mt('List of CODES to validate against:').''. + $env{'form.scantron_CODElist'}.''; + } + return ('

-Please double check the information - below before clicking on '$button_text' + +'.&mt('Please double check the information below before clicking on \'[_1]\'',&mt($button_text)).'

- - + + +'.$CODElist.'
Sequence To be Graded:$title
Data File that will be used:$env{'form.scantron_selectfile'}
'.&mt('Sequence to be Graded:').''.$title.'
'.&mt('Data File that will be used:').''.$env{'form.scantron_selectfile'}.'
-
-

If this information is correct, please click on '$button_text'.

-

If something is incorrect, please click the 'Grading Menu' button to start over.

+

'.&mt('If this information is correct, please click on \'[_1]\'.',&mt($button_text)).'

+

'.&mt('If something is incorrect, please click the \'Grading Menu\' button to start over.').'


-STUFF +'); } +=pod + +=item scantron_do_warning + + Check if the operator has picked something for all required + fields. Error out if something is missing. + +=cut + sub scantron_do_warning { - my ($r)=@_; - my ($symb,$url)=&get_symb_and_url($r); + my ($r,$symb)=@_; if (!$symb) {return '';} - my $default_form_data=&defaultFormData($symb,$url); + my $default_form_data=&defaultFormData($symb); $r->print(&scantron_form_start().$default_form_data); if ( $env{'form.selectpage'} eq '' || $env{'form.scantron_selectfile'} eq '' || $env{'form.scantron_format'} eq '' ) { - $r->print("

You have forgetten to specify some information. Please go Back and try again.

"); + $r->print("

".&mt('You have forgetten to specify some information. Please go Back and try again.')."

"); if ( $env{'form.selectpage'} eq '') { - $r->print('

You have not selected a Sequence to grade

'); + $r->print('

'.&mt('You have not selected a Sequence to grade').'

'); } if ( $env{'form.scantron_selectfile'} eq '') { - $r->print('

You have not selected a file that contains the student\'s response data.

'); + $r->print('

'.&mt('You have not selected a file that contains the student\'s response data.').'

'); } if ( $env{'form.scantron_format'} eq '') { - $r->print('

You have not selected a the format of the student\'s response data.

'); + $r->print('

'.&mt('You have not selected a the format of the student\'s response data.').'

'); } } else { - my $warning=&scantron_warning_screen('Validate Records'); - $r->print(< + my $warning=&scantron_warning_screen('Grading: Validate Records'); + $r->print(' +'.$warning.' + -STUFF +'); } - $r->print("
".&show_grading_menu_form($symb,$url).""); + $r->print("
".&show_grading_menu_form($symb)); return ''; } +=pod + +=item scantron_form_start + + html hidden input for remembering all selected grading options + +=cut + sub scantron_form_start { my ($max_bubble)=@_; my $result= < + SCANTRONFORM + + my $line = 0; + while (defined($env{"form.scantron.bubblelines.$line"})) { + my $chunk = + ''."\n"; + $chunk .= + ''."\n"; + $chunk .= + ''."\n"; + $chunk .= + ''."\n"; + $result .= $chunk; + $line++; + } return $result; } +=pod + +=item scantron_validate_file + + Dispatch routine for doing validation of a bubble sheet data file. + + Also processes any necessary information resets that need to + occur before validation begins (ignore previous corrections, + restarting the skipped records processing) + +=cut + sub scantron_validate_file { - my ($r) = @_; - my ($symb,$url)=&get_symb_and_url($r); + my ($r,$symb) = @_; if (!$symb) {return '';} - my $default_form_data=&defaultFormData($symb,$url); + my $default_form_data=&defaultFormData($symb); # do the detection of only doing skipped records first befroe we delete - # them when doing the corrections reset + # them when doing the corrections reset if ($env{'form.scantron_options_redo'} ne 'redo_skipped_ready') { &reset_skipping_status(); } if ($env{'form.scantron_options_redo'} eq 'redo_skipped') { &remember_current_skipped(); - &scantron_remove_file('skipped'); $env{'form.scantron_options_redo'}='redo_skipped_ready'; } @@ -4299,14 +6315,20 @@ sub scantron_validate_file { if ($env{'form.scantron_corrections'}) { &scantron_process_corrections($r); } - $r->print("

Gathering neccessary info.

");$r->rflush(); + $r->print('

'.&mt('Gathering necessary information.').'

');$r->rflush(); #get the student pick code ready $r->print(&Apache::loncommon::studentbrowser_javascript()); - my $max_bubble=&scantron_get_maxbubble($r); + my $nav_error; + my $max_bubble=&scantron_get_maxbubble(\$nav_error); + if ($nav_error) { + $r->print(&navmap_errormsg()); + return ''; + } my $result=&scantron_form_start($max_bubble).$default_form_data; $r->print($result); - my @validate_phases=( 'ID', + my @validate_phases=( 'sequence', + 'ID', 'CODE', 'doublebubble', 'missingbubbles'); @@ -4315,9 +6337,10 @@ sub scantron_validate_file { } my $currentphase=$env{'form.validatepass'}; + my $stop=0; while (!$stop && $currentphase < scalar(@validate_phases)) { - $r->print("

Validating ".$validate_phases[$currentphase]."

"); + $r->print(&mt('Validating '.$validate_phases[$currentphase]).'
'); $r->rflush(); my $which="scantron_validate_".$validate_phases[$currentphase]; { @@ -4327,28 +6350,54 @@ sub scantron_validate_file { } if (!$stop) { my $warning=&scantron_warning_screen('Start Grading'); - $r->print(< -$warning - - -STUFF - + $r->print(&mt('Validation process complete.').'
'. + $warning. + &mt('Perform verification for each student after storage of submissions?'). + ' '. + (' 'x3).'
'. + &mt('Grading will take longer if you use verification.').'
'. + &mt("Alternatively, the 'Review bubblesheet data' utility (see grading menu) can be used for all students after grading is complete.").'

'. + ''. + ''."\n"); } else { $r->print(''); $r->print(""); } if ($stop) { - $r->print(''); - $r->print(' using corrected info
'); - $r->print(""); - $r->print(" this scanline saving it for later."); + if ($validate_phases[$currentphase] eq 'sequence') { + $r->print(''); + $r->print(' '.&mt('this error').'
'); + + $r->print("

".&mt("Or click the 'Grading Menu' button to start over.")."

"); + } else { + if ($validate_phases[$currentphase] eq 'doublebubble' || $validate_phases[$currentphase] eq 'missingbubbles') { + $r->print(''); + } else { + $r->print(''); + } + $r->print(' '.&mt('using corrected info').'
'); + $r->print(""); + $r->print(" ".&mt("this scanline saving it for later.")); + } } - $r->print("
".&show_grading_menu_form($symb,$url). - ""); + $r->print("
".&show_grading_menu_form($symb)); return ''; } + +=pod + +=item scantron_remove_file + + Removes the requested bubble sheet data file, makes sure that + scantron_original_ is never removed + + +=cut + sub scantron_remove_file { my ($which)=@_; my $cname=$env{'course.'.$env{'request.course.id'}.'.num'}; @@ -4363,6 +6412,18 @@ sub scantron_remove_file { return &Apache::lonnet::removeuserfile($cname,$cdom,$file); } + +=pod + +=item scantron_remove_scan_data + + Removes all scan_data correction for the requested bubble sheet + 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.) + +=cut + sub scantron_remove_scan_data { my $cname=$env{'course.'.$env{'request.course.id'}.'.num'}; my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'}; @@ -4380,11 +6441,59 @@ 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; } + +=pod + +=item scantron_getfile + + Fetches the requested bubble sheet data file (all 3 versions), and + the scan_data hash + + Arguments: + None + + Returns: + 2 hash references + + - first one has + orig - + corrected - + skipped - each of which points to an array ref of the specified + file broken up into individual lines + count - number of scanlines + + - second is the scan_data hash possible keys are + ($number refers to scanline numbered $number and thus the key affects + only that scanline + $bubline refers to the specific bubble line element and the aspects + refers to that specific bubble line element) + + $number.user - username:domain to use + $number.CODE_ignore_dup + - ignore the duplicate CODE error + $number.useCODE + - use the CODE in the scanline as is + $number.no_bubble.$bubline + - it is valid that there is no bubbled in bubble + at $number $bubline + remember_skipping + - a frozen hash containing keys of $number and values + of either + 1 - we are on a 'do skipped records pass' and plan + on processing this line + 2 - we are on a 'do skipped records pass' and this + scanline has been marked to skip yet again + +=cut + sub scantron_getfile { #FIXME really would prefer a scantron directory my $cname=$env{'course.'.$env{'request.course.id'}.'.num'}; @@ -4417,16 +6526,46 @@ sub scantron_getfile { return (\%scanlines,\%scan_data); } +=pod + +=item lonnet_putfile + + Wrapper routine to call &Apache::lonnet::finishuserfileupload + + Arguments: + $contents - data to store + $filename - filename to store $contents into + + Returns: + result value from &Apache::lonnet::finishuserfileupload + +=cut + sub lonnet_putfile { my ($contents,$filename)=@_; my $docuname=$env{'course.'.$env{'request.course.id'}.'.num'}; my $docudom=$env{'course.'.$env{'request.course.id'}.'.domain'}; - my $docuhome=$env{'course.'.$env{'request.course.id'}.'.home'}; $env{'form.sillywaytopassafilearound'}=$contents; - &Apache::lonnet::finishuserfileupload($docuname,$docudom,$docuhome,'sillywaytopassafilearound',$filename); + &Apache::lonnet::finishuserfileupload($docuname,$docudom,'sillywaytopassafilearound',$filename); } +=pod + +=item scantron_putfile + + Stores the current version of the bubble sheet data files, and the + scan_data hash. (Does not modify the original version only the + corrected and skipped versions. + + Arguments: + $scanlines - hash ref that looks like the first return value from + &scantron_getfile() + $scan_data - hash ref that looks like the second return value from + &scantron_getfile() + +=cut + sub scantron_putfile { my ($scanlines,$scan_data) = @_; #FIXME really would prefer a scantron directory @@ -4447,14 +6586,53 @@ sub scantron_putfile { &Apache::lonnet::put('nohist_scantrondata',$scan_data,$cdom,$cname); } +=pod + +=item scantron_get_line + + Returns the correct version of the scanline + + Arguments: + $scanlines - hash ref that looks like the first return value from + &scantron_getfile() + $scan_data - hash ref that looks like the second return value from + &scantron_getfile() + $i - number of the requested line (starts at 0) + + Returns: + A scanline, (either the original or the corrected one if it + exists), or undef if the requested scanline should be + skipped. (Either because it's an skipped scanline, or it's an + unskipped scanline and we are not doing a 'do skipped scanlines' + pass. + +=cut + sub scantron_get_line { my ($scanlines,$scan_data,$i)=@_; - if (&should_be_skipped($scan_data,$i)) { return undef; } - if ($scanlines->{'skipped'}[$i]) { return undef; } + if (&should_be_skipped($scanlines,$scan_data,$i)) { return undef; } + #if ($scanlines->{'skipped'}[$i]) { return undef; } if ($scanlines->{'corrected'}[$i]) {return $scanlines->{'corrected'}[$i];} return $scanlines->{'orig'}[$i]; } +=pod + +=item scantron_todo_count + + Counts the number of scanlines that need processing. + + Arguments: + $scanlines - hash ref that looks like the first return value from + &scantron_getfile() + $scan_data - hash ref that looks like the second return value from + &scantron_getfile() + + Returns: + $count - number of scanlines to process + +=cut + sub get_todo_count { my ($scanlines,$scan_data)=@_; my $count=0; @@ -4466,16 +6644,122 @@ sub get_todo_count { return $count; } +=pod + +=item scantron_put_line + + Updates the 'corrected' or 'skipped' versions of the bubble sheet + data file. + + Arguments: + $scanlines - hash ref that looks like the first return value from + &scantron_getfile() + $scan_data - hash ref that looks like the second return value from + &scantron_getfile() + $i - line number to update + $newline - contents of the updated scanline + $skip - if true make the line for skipping and update the + 'skipped' file + +=cut + sub scantron_put_line { my ($scanlines,$scan_data,$i,$newline,$skip)=@_; if ($skip) { $scanlines->{'skipped'}[$i]=$newline; - &allow_skipping($scan_data,$i); + &start_skipping($scan_data,$i); return; } $scanlines->{'corrected'}[$i]=$newline; } +=pod + +=item scantron_clear_skip + + Remove a line from the 'skipped' file + + Arguments: + $scanlines - hash ref that looks like the first return value from + &scantron_getfile() + $scan_data - hash ref that looks like the second return value from + &scantron_getfile() + $i - line number to update + +=cut + +sub scantron_clear_skip { + my ($scanlines,$scan_data,$i)=@_; + if (exists($scanlines->{'skipped'}[$i])) { + undef($scanlines->{'skipped'}[$i]); + return 1; + } + return 0; +} + +=pod + +=item scantron_filter_not_exam + + Filter routine used by &Apache::lonnavmaps::retrieveResources(), to + filter out resources that are not marked as 'exam' mode + +=cut + +sub scantron_filter_not_exam { + my ($curres)=@_; + + if (ref($curres) && $curres->is_problem() && !$curres->is_exam()) { + # if the user has asked to not have either hidden + # or 'randomout' controlled resources to be graded + # don't include them + if ($env{'form.scantron_options_hidden'} eq 'ignore_hidden' + && $curres->randomout) { + return 0; + } + return 1; + } + return 0; +} + +=pod + +=item scantron_validate_sequence + + Validates the selected sequence, checking for resource that are + not set to exam mode. + +=cut + +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'}); + + my $map=$navmap->getResourceByUrl($sequence); + + $r->print(''); + if ($env{'form.validate_sequence_exam'} ne 'ignore') { + my @resources= + $navmap->retrieveResources($map,\&scantron_filter_not_exam,1,0); + if (@resources) { + $r->print("

".&mt('Some resources in the sequence currently are not set to exam mode. Grading these resources currently may not work correctly.')."

"); + return (1,$currentphase); + } + } + + return (0,$currentphase+1); +} + + + sub scantron_validate_ID { my ($r,$currentphase) = @_; @@ -4487,6 +6771,13 @@ sub scantron_validate_ID { my %scantron_config=&get_scantron_config($env{'form.scantron_format'}); my ($scanlines,$scan_data)=&scantron_getfile(); + my $nav_error; + &scantron_get_maxbubble(\$nav_error); # 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++) { my $line=&scantron_get_line($scanlines,$scan_data,$i); @@ -4538,37 +6829,43 @@ sub scantron_validate_ID { return (0,$currentphase+1); } + sub scantron_get_correction { my ($r,$i,$scan_record,$scan_config,$line,$error,$arg)=@_; - -#FIXME in the case of a duplicated ID the previous line, probaly need +#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("

An error was detected ($error)"); - if ( defined($$scan_record{'scantron.PaperID'}) ) { - $r->print(" for PaperID ". - $$scan_record{'scantron.PaperID'}." \n"); + if ( $$scan_record{'scantron.PaperID'} =~ /\S/) { + $r->print("

".&mt("An error was detected ($error)". + " for PaperID [_1]", + $$scan_record{'scantron.PaperID'})."

\n"); } else { - $r->print(" in scanline $i
".
-		  $line."
\n"); - } - my $message="

The ID on the form is ". - $$scan_record{'scantron.ID'}."
\n". - "The name on the paper is ". - $$scan_record{'scantron.LastName'}.",". - $$scan_record{'scantron.FirstName'}."

"; + $r->print("

".&mt("An error was detected ($error)". + " in scanline [_1]

[_2]
", + $i,$line)."

\n"); + } + my $message="

".&mt("The ID on the form is [_1]
". + "The name on the paper is [_2],[_3]", + $$scan_record{'scantron.ID'}, + $$scan_record{'scantron.LastName'}, + $$scan_record{'scantron.FirstName'})."

"; $r->print(''."\n"); $r->print(''."\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

\n"); + $r->print("

".&mt("The encoded ID is not in the classlist"). + "

\n"); } elsif ($error eq 'duplicateID') { - $r->print("The encoded ID has also been used by a previous paper $arg

\n"); + $r->print("

".&mt("The encoded ID has also been used by a previous paper [_1]",$arg)."

\n"); } $r->print($message); - $r->print("

How should I handle this?
\n"); + $r->print("

".&mt("How should I handle this?")."
\n"); $r->print("\n

  • "); #FIXME it would be nice if this sent back the user ID and #could do partial userID matches @@ -4581,32 +6878,47 @@ sub scantron_get_correction { $r->print('
  • '); } elsif ($error =~ /CODE$/) { if ($error eq 'incorrectCODE') { - $r->print("

    The encoded CODE is not in the list of possible CODEs

    \n"); + $r->print("

    ".&mt("The encoded CODE is not in the list of possible CODEs.")."

    \n"); } elsif ($error eq 'duplicateCODE') { - $r->print("

    The encoded CODE has also been used by a previous paper ".join(', ',@{$arg}).", and CODEs are supposed to be unique

    \n"); + $r->print("

    ".&mt("The encoded CODE has also been used by a previous paper [_1], and CODEs are supposed to be unique.",join(', ',@{$arg}))."

    \n"); } - $r->print("

    The CODE on the form is '". - $$scan_record{'scantron.CODE'}."'
    \n"); + $r->print("

    ".&mt("The CODE on the form is '[_1]'", + $$scan_record{'scantron.CODE'})."
    \n"); $r->print($message); - $r->print("

    How should I handle this?
    \n"); + $r->print("

    ".&mt("How should I handle this?")."
    \n"); $r->print("\n
    "); my $i=0; - if ($error eq 'incorrectCODE') { + if ($error eq 'incorrectCODE' + && $$scan_record{'scantron.CODE'}=~/\S/ ) { my ($max,$closest)=&scantron_get_closely_matching_CODEs($arg,$$scan_record{'scantron.CODE'}); - foreach my $testcode (@{$closest}) { - my $checked=''; - if (!$i) { $checked=' checked="on" '; } - $r->print(" Use the similar CODE ".$testcode." instead."); - $r->print("\n
    "); - $i++; + if ($closest > 0) { + foreach my $testcode (@{$closest}) { + my $checked=''; + if (!$i) { $checked=' checked="checked"'; } + $r->print(" + + "); + $r->print("\n
    "); + $i++; + } } } - my $checked; if (!$i) { $checked=' checked="on" '; } - $r->print(" Use the CODE ".$$scan_record{'scantron.CODE'}." that is was on the paper, ignoring the error."); - $r->print("\n
    "); + if ($$scan_record{'scantron.CODE'}=~/\S/ ) { + my $checked; if (!$i) { $checked=' checked="checked"'; } + $r->print(" + "); + $r->print("\n
    "); + } - $r->print(< + $r->print(&Apache::lonhtmlcommon::scripttag(< ENDSCRIPT my $href="/adm/pickcode?". - "form=".&Apache::lonnet::escape("scantronupload"). - "&scantron_format=".&Apache::lonnet::escape($env{'form.scantron_format'}). - "&scantron_CODElist=".&Apache::lonnet::escape($env{'form.scantron_CODElist'}). - "&curCODE=".&Apache::lonnet::escape($$scan_record{'scantron.CODE'}). - "&scantron_selectfile=".&Apache::lonnet::escape($env{'form.scantron_selectfile'}); - $r->print(" Select a CODE from the list of all CODEs and use it. Selected CODE is "); - $r->print("\n
    "); - $r->print(" Use as the CODE."); + "form=".&escape("scantronupload"). + "&scantron_format=".&escape($env{'form.scantron_format'}). + "&scantron_CODElist=".&escape($env{'form.scantron_CODElist'}). + "&curCODE=".&escape($$scan_record{'scantron.CODE'}). + "&scantron_selectfile=".&escape($env{'form.scantron_selectfile'}); + if ($env{'form.scantron_CODElist'} =~ /\S/) { + $r->print(" + + ".&mt("Selected CODE is [_1]",'')); + $r->print("\n
    "); + } + $r->print(" + ")); $r->print("\n

    "); } elsif ($error eq 'doublebubble') { - $r->print("

    There have been multiple bubbles scanned for a some question(s)

    \n"); + $r->print("

    ".&mt("There have been multiple bubbles scanned for some question(s)")."

    \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); + $r->print(''); + $line_list.'" />'); $r->print($message); - $r->print("

    Please indicate which bubble should be used for grading

    "); + $r->print("

    ".&mt("Please indicate which bubble should be used for grading")."

    "); foreach my $question (@{$arg}) { - my $selected=$$scan_record{"scantron.$question.answer"}; - &scantron_bubble_selector($r,$scan_config,$question,split('',$selected)); + my @linenums = &prompt_for_corrections($r,$question,$scan_config, + $scan_record, $error); + push(@lines_to_correct,@linenums); } + $r->print(&verify_bubbles_checked(@lines_to_correct)); } elsif ($error eq 'missingbubble') { - $r->print("

    There have been no bubbles scanned for some question(s)

    \n"); + $r->print("

    ".&mt("There have been no bubbles scanned for some question(s)")."

    \n"); $r->print($message); - $r->print("

    Please indicate which bubble should be used for grading

    "); - $r->print("Some questions have no scanned bubbles\n"); + $r->print("

    ".&mt("Please indicate which bubble should be used for grading.")."

    "); + $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); + $r->print(''); + $line_list.'" />'); foreach my $question (@{$arg}) { - my $selected=$$scan_record{"scantron.$question.answer"}; - &scantron_bubble_selector($r,$scan_config,$question); + my @linenums = &prompt_for_corrections($r,$question,$scan_config, + $scan_record, $error); + push(@lines_to_correct,@linenums); } + $r->print(&verify_bubbles_checked(@lines_to_correct)); } else { $r->print("\n
      "); } $r->print("\n
    "); +} +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 = &Apache::lonhtmlcommon::scripttag((< 1) { + var bubble_picked = 0; + for (var j=0; j 1) { + $r->print(&mt('The group of bubble lines below responds to a single question.').'
    '); + if (($responsetype_per_response{$question-1} eq 'essayresponse') || + ($responsetype_per_response{$question-1} eq 'formularesponse') || + ($responsetype_per_response{$question-1} eq 'stringresponse') || + ($responsetype_per_response{$question-1} eq 'imageresponse') || + ($responsetype_per_response{$question-1} eq 'reactionresponse') || + ($responsetype_per_response{$question-1} eq 'organicresponse')) { + $r->print(&mt("Although this particular question type requires handgrading, the instructions for this question in the exam directed students to leave [quant,_1,line] blank on their bubblesheets.",$lines).'

    '.&mt('A non-zero score can be assigned to the student during bubblesheet grading by selecting a bubble in at least one line.').'
    '.&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.').'
    '.&mt("To assign a score of zero for this question, mark all lines as 'No bubble'.").'

    '); + } else { + $r->print(&mt("Select at most one bubble in a single line and select 'No Bubble' in all the other lines. ")."
    "); + } + } + 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("

    "); + } + return @linenums; +} + +=pod + +=item scantron_bubble_selector + + Generates the html radiobuttons to correct a single bubble line + possibly showing the existing the selected bubbles if known + + 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. + +=cut + sub scantron_bubble_selector { - my ($r,$scan_config,$quest,@selected)=@_; + 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') { $max=10; } + my @alphabet=('A'..'Z'); - $r->print(""); + $r->print(&Apache::loncommon::start_data_table(). + &Apache::loncommon::start_data_table_row()); + $r->print(''); for (my $i=0;$i<$max+1;$i++) { - $r->print(''); } - $r->print(''); + $r->print(&Apache::loncommon::end_data_table_row(). + &Apache::loncommon::start_data_table_row()); for (my $i=0;$i<$max;$i++) { - $r->print('"); + $r->print("\n". + '"); } - $r->print(''); - $r->print('
    $quest'.$line.''); + $r->print("\n".''); if ($selected[0] eq $alphabet[$i]) { $r->print('X'); shift(@selected) } else { $r->print(' '); } $r->print('
    '.$alphabet[$i]." No bubble
    '); + my $nobub_checked = ' '; + if ($error eq 'missingbubble') { + $nobub_checked = ' checked = "checked" '; + } + $r->print("\n".''."\n".''); + $r->print(&Apache::loncommon::end_data_table_row(). + &Apache::loncommon::end_data_table()); } +=pod + +=item num_matches + + Counts the number of characters that are the same between the two arguments. + + Arguments: + $orig - CODE from the scanline + $code - CODE to match against + + Returns: + $count - integer count of the number of same characters between the + two arguments + +=cut + sub num_matches { my ($orig,$code) = @_; my @code=split(//,$code); @@ -4686,6 +7229,26 @@ sub num_matches { return $same; } +=pod + +=item scantron_get_closely_matching_CODEs + + Cycles through all CODEs and finds the set that has the greatest + number of same characters as the provided CODE + + Arguments: + $allcodes - hash ref returned by &get_codes() + $CODE - CODE from the current scanline + + Returns: + 2 element list + - first elements is number of how closely matching the best fit is + (5 means best set has 5 matching characters) + - second element is an arrary ref containing the set of valid CODEs + that best fit the passed in CODE + +=cut + sub scantron_get_closely_matching_CODEs { my ($allcodes,$CODE)=@_; my @CODEs; @@ -4696,15 +7259,55 @@ sub scantron_get_closely_matching_CODEs return ($#CODEs,$CODEs[-1]); } +=pod + +=item get_codes + + Builds a hash which has keys of all of the valid CODEs from the selected + set of remembered CODEs. + + Arguments: + $old_name - name of the set of remembered CODEs + $cdom - domain of the course + $cnum - internal course name + + Returns: + %allcodes - keys are the valid CODEs, values are all 1 + +=cut + sub get_codes { - my $old_name=$env{'form.scantron_CODElist'}; - my $cdom =$env{'course.'.$env{'request.course.id'}.'.domain'}; - my $cnum =$env{'course.'.$env{'request.course.id'}.'.num'}; - my %result=&Apache::lonnet::get('CODEs',[$old_name],$cdom,$cnum); - my %allcodes=map {(&Apache::lonprintout::num_to_letters($_),1)} split(',',$result{$old_name}); + my ($old_name, $cdom, $cnum) = @_; + if (!$old_name) { + $old_name=$env{'form.scantron_CODElist'}; + } + if (!$cdom) { + $cdom =$env{'course.'.$env{'request.course.id'}.'.domain'}; + } + if (!$cnum) { + $cnum =$env{'course.'.$env{'request.course.id'}.'.num'}; + } + my %result=&Apache::lonnet::get('CODEs',[$old_name,"type\0$old_name"], + $cdom,$cnum); + my %allcodes; + if ($result{"type\0$old_name"} eq 'number') { + %allcodes=map {($_,1)} split(',',$result{$old_name}); + } else { + %allcodes=map {(&Apache::lonprintout::num_to_letters($_),1)} split(',',$result{$old_name}); + } return %allcodes; } +=pod + +=item scantron_validate_CODE + + Validates all scanlines in the selected file to not have any + invalid or underspecified CODEs and that none of the codes are + duplicated if this was requested. + +=cut + sub scantron_validate_CODE { my ($r,$currentphase) = @_; my %scantron_config=&get_scantron_config($env{'form.scantron_format'}); @@ -4722,6 +7325,13 @@ sub scantron_validate_CODE { my %allcodes=&get_codes(); + my $nav_error; + &scantron_get_maxbubble(\$nav_error); # 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++) { my $line=&scantron_get_line($scanlines,$scan_data,$i); @@ -4751,11 +7361,20 @@ 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); } +=pod + +=item scantron_validate_doublebubble + + Validates all scanlines in the selected file to not have any + bubble lines with multiple bubbles marked. + +=cut + sub scantron_validate_doublebubble { my ($r,$currentphase) = @_; #get student info @@ -4765,6 +7384,13 @@ sub scantron_validate_doublebubble { #get scantron line setup my %scantron_config=&get_scantron_config($env{'form.scantron_format'}); my ($scanlines,$scan_data)=&scantron_getfile(); + my $nav_error; + &scantron_get_maxbubble(\$nav_error); # 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; } @@ -4779,28 +7405,102 @@ sub scantron_validate_doublebubble { return (0,$currentphase+1); } + sub scantron_get_maxbubble { - my ($r)=@_; + my ($nav_error) = @_; if (defined($env{'form.scantron_maxbubble'}) && $env{'form.scantron_maxbubble'}) { + &restore_bubble_lines(); return $env{'form.scantron_maxbubble'}; } - my $navmap=Apache::lonnavmaps::navmap->new(); - my (undef,undef,$sequence)= + + my (undef, undef, $sequence) = &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); - &Apache::lonnet::delenv('form.counter'); + + &Apache::lonxml::clear_problem_counter(); + + 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 = (); + + my $response_number = 0; + my $bubble_line = 0; foreach my $resource (@resources) { - my $result=&Apache::lonnet::ssi($resource->src().'?symb='.&Apache::lonnet::escape($resource->symb())); + my ($analysis,$parts) = &scantron_partids_tograde($resource,$cid,$uname,$udom); + 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_line = 10; + my $inner_bubble_lines = int($numbub/$bubbles_per_line); + if (($numbub % $bubbles_per_line) != 0) { + $inner_bubble_lines++; + } + for (my $i=0; $i<$numshown; $i++) { + $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"}; + } + + $first_bubble_line{$response_number} = $bubble_line; + $bubble_lines_per_response{$response_number} = $lines; + $responsetype_per_response{$response_number} = + $analysis->{$part_id.'.type'}; + $response_number++; + + $bubble_line += $lines; + $total_lines += $lines; + } + } } - &Apache::lonnet::delenv('scantron\.'); - my $envfile=$env{'user.environment'}; - $envfile=~/\/([^\/]+)\.id$/; - $envfile=$1; - &Apache::lonnet::transfer_profile_to_env($r->dir_config('lonIDsDir'), - $envfile); - $env{'form.scantron_maxbubble'}=$env{'form.counter'}-1; + &Apache::lonnet::delenv('scantron.'); + + &save_bubble_lines(); + $env{'form.scantron_maxbubble'} = + $total_lines; return $env{'form.scantron_maxbubble'}; } @@ -4813,7 +7513,11 @@ sub scantron_validate_missingbubbles { #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 $nav_error; + my $max_bubble=&scantron_get_maxbubble(\$nav_error); + if ($nav_error) { + 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); @@ -4822,8 +7526,29 @@ sub scantron_validate_missingbubbles { $scan_data); 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; + if (!defined($first_bubble_line{$question -1})) { next; } + my $first = $first_bubble_line{$question-1}; + my @subans = split(/,/,$subdivided_bubble_lines{$question-1}); + my $subcount = 1; + while ($subcount<$subquestion) { + $first += $subans[$subcount-1]; + $subcount ++; + } + my $count = $subans[$subquestion-1]; + $lastbubble = $first + $count; + } else { + if (!defined($first_bubble_line{$missing - 1})) { next; } + $lastbubble = $first_bubble_line{$missing - 1} + $bubble_lines_per_response{$missing - 1}; + } + if ($lastbubble > $max_bubble) { next; } push(@to_correct,$missing); } if (@to_correct) { @@ -4836,21 +7561,56 @@ sub scantron_validate_missingbubbles { return (0,$currentphase+1); } + sub scantron_process_students { - my ($r) = @_; + my ($r,$symb) = @_; + my (undef,undef,$sequence)=&Apache::lonnet::decode_symb($env{'form.selectpage'}); - my ($symb,$url)=&get_symb_and_url($r); - if (!$symb) {return '';} - my $default_form_data=&defaultFormData($symb,$url); + if (!$symb) { + return ''; + } + my $default_form_data=&defaultFormData($symb); my %scantron_config=&get_scantron_config($env{'form.scantron_format'}); 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 @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0); -# $r->print("geto ".scalar(@resources)."
    "); + my (%grader_partids_by_symb,%grader_randomlists_by_symb); + &graders_resources_pass(\@resources,\%grader_partids_by_symb, + \%grader_randomlists_by_symb); + my $resource_error; + foreach my $resource (@resources) { + my $ressymb; + if (ref($resource)) { + $ressymb = $resource->symb(); + } else { + $resource_error = 1; + last; + } + my ($analysis,$parts) = + &scantron_partids_tograde($resource,$env{'request.course.id'}, + $env{'user.name'},$env{'user.domain'},1); + $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'}; + } + } + } + if ($resource_error) { + $r->print(&navmap_errormsg()); + return ''; + } + + my ($uname,$udom); my $result= < @@ -4859,17 +7619,41 @@ 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, + my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin($r,'Bubblesheet Status', + 'Bubblesheet Progress',$count, 'inline',undef,'scantronupload'); &Apache::lonhtmlcommon::Update_PrgWin($r,\%prog_state, 'Processing first student'); + $r->print('
    '); my $start=&Time::HiRes::time(); my $i=-1; - my ($uname,$udom,$started); + my $started; + + my $nav_error; + &scantron_get_maxbubble(\$nav_error); # 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(""); + &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)); + while ($i<$scanlines->{'count'}) { ($uname,$udom)=('',''); $i++; @@ -4894,161 +7678,416 @@ SCANTRONFORM next; } ($uname,$udom)=split(/:/,$uname); - &Apache::lonnet::delenv('form.counter'); - &Apache::lonnet::appenv(%$scan_record); - - 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'}) && - $scan_record->{'scantron.CODE'}) { - $form{'CODE'}=$scan_record->{'scantron.CODE'}; - } else { - $form{'CODE'}=''; - } - my $result=&Apache::lonnet::ssi($resource->src(),%form); - if ($result ne '') { - &Apache::lonnet::logthis("scantron grading error -> $result"); - &Apache::lonnet::logthis("scantron grading error info name $uname domain $udom course $env{'request.course.id'} url ".$resource->src()); - } - if (&Apache::loncommon::connection_aborted($r)) { last; } + + my (%partids_by_symb,$res_error); + foreach my $resource (@resources) { + 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); + $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); + + if (&scantron_clear_skip($scanlines,$scan_data,$i)) { + &scantron_putfile($scanlines,$scan_data); } + + my $scancode; + if ((exists($scan_record->{'scantron.CODE'})) && + (&Apache::lonnet::validCODE($scan_record->{'scantron.CODE'}))) { + $scancode = $scan_record->{'scantron.CODE'}; + } else { + $scancode = ''; + } + + if (&grade_student_bubbles($r,$uname,$udom,$scan_record,$scancode, + \@resources,\%partids_by_symb) eq 'ssi_error') { + $ssi_error = 0; # So end of handler error message does not trigger. + $r->print(""); + &ssi_print_error($r); + $r->print(&show_grading_menu_form($symb)); + &Apache::lonnet::remove_lock($lock); + return ''; # Why return ''? Beats me. + } + $completedstudents{$uname}={'line'=>$line}; - if (&Apache::loncommon::connection_aborted($r)) { last; } + if ($env{'form.verifyrecord'}) { + my $lastpos = $env{'form.scantron_maxbubble'}*$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 (@resources) { + 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); + $studentrecord .= $recording; + } + if ($studentrecord ne $studentdata) { + &Apache::lonxml::clear_problem_counter(); + if (&grade_student_bubbles($r,$uname,$udom,$scan_record,$scancode, + \@resources,\%partids_by_symb) eq 'ssi_error') { + $ssi_error = 0; # So end of handler error message does not trigger. + $r->print(""); + &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 (@resources) { + 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); + $studentrecord .= $recording; + } + if ($studentrecord ne $studentdata) { + $r->print('

    '); + if ($scancode eq '') { + $r->print(&mt('Mismatch grading bubble sheet for user: [_1] with ID: [_2].', + $uname.':'.$udom,$scan_record->{'scantron.ID'})); + } else { + $r->print(&mt('Mismatch grading bubble sheet for user: [_1] with ID: [_2] and CODE: [_3].', + $uname.':'.$udom,$scan_record->{'scantron.ID'},$scancode)); + } + $r->print('
    '.&Apache::loncommon::start_data_table()."\n". + &Apache::loncommon::start_data_table_header_row()."\n". + ''.&mt('Source').''.&mt('Bubbled responses').''. + &Apache::loncommon::end_data_table_header_row()."\n". + &Apache::loncommon::start_data_table_row(). + ''.&mt('Bubble Sheet').''. + ''.$studentdata.''. + &Apache::loncommon::end_data_table_row(). + &Apache::loncommon::start_data_table_row(). + 'Stored submissions'. + ''.$studentrecord.''."\n". + &Apache::loncommon::end_data_table_row(). + &Apache::loncommon::end_data_table().'

    '); + } else { + $r->print('
    '. + &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'}).'
    '. + &mt("As a consequence, this user's submission history records two tries."). + '

    '); + } + } + } + if (&Apache::loncommon::connection_aborted($r)) { last; } } continue { - &Apache::lonnet::delenv('form.counter'); - &Apache::lonnet::delenv('scantron\.'); + &Apache::lonxml::clear_problem_counter(); + &Apache::lonnet::delenv('scantron.'); } &Apache::lonhtmlcommon::Close_PrgWin($r,\%prog_state); + &Apache::lonnet::remove_lock($lock); # my $lasttime = &Time::HiRes::time()-$start; # $r->print("

    took $lasttime

    "); $r->print(""); - $r->print(&show_grading_menu_form($symb,$url)); + $r->print(&show_grading_menu_form($symb)); return ''; } +sub graders_resources_pass { + my ($resources,$grader_partids_by_symb,$grader_randomlists_by_symb) = @_; + 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); + $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; +} + +sub grade_student_bubbles { + my ($r,$uname,$udom,$scan_record,$scancode,$resources,$parts) = @_; + 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 (ref($parts) eq 'HASH') { + if (ref($parts->{$ressymb}) eq 'ARRAY') { + foreach my $part (@{$parts->{$ressymb}}) { + $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 ($r,$symb)=@_; + 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_and_url($r,1)); - $r->print(< + 'coursename',$dom); + my $syllabuslink = ''.&mt('Syllabus').''. + (' 'x2).&mt('(shows course personnel)'); + 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(&Apache::lonhtmlcommon::scripttag(' 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(); } - -
    -$default_form_data - - - - - - -
    $select_link
    Course ID:
    Course Name:
    Domain: $domsel
    File to upload:
    - - + 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; + } + +')); + $r->print(' +

    '.&mt('Send scanned bubblesheet data to a course').'

    + + +'.$default_form_data. + &Apache::lonhtmlcommon::start_pick_box(). + &Apache::lonhtmlcommon::row_title(&mt('Course ID')). + ''.$select_link. + &Apache::lonhtmlcommon::row_closure(). + &Apache::lonhtmlcommon::row_title(&mt('Course Name')). + ''.$syllabuslink. + &Apache::lonhtmlcommon::row_closure(). + &Apache::lonhtmlcommon::row_title(&mt('Domain')). + ''.$domdesc. + &Apache::lonhtmlcommon::row_closure(). + &Apache::lonhtmlcommon::row_title(&mt('File to upload')). + ''. + &Apache::lonhtmlcommon::row_closure(1). + &Apache::lonhtmlcommon::end_pick_box().'
    + + +
    -UPLOAD +'); return ''; } + sub scantron_upload_scantron_data_save { - my($r)=@_; - my ($symb,$url)=&get_symb_and_url($r,1); + my($r,$symb)=@_; my $doanotherupload= '
    '."\n". ''."\n". - ''."\n". + ''."\n". '
    '."\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.
    "); + $r->print(&mt("You are not allowed to upload bubblesheet data to the requested course.")."
    "); if ($symb) { - $r->print(&show_grading_menu_form($symb,$url)); + $r->print(&show_grading_menu_form($symb)); } else { $r->print($doanotherupload); } return ''; } my %coursedata=&Apache::lonnet::coursedescription($env{'form.domainid'}.'_'.$env{'form.courseid'}); - $r->print("Doing upload to ".$coursedata{'description'}."
    "); - my $home=&Apache::lonnet::homeserver($env{'form.courseid'}, - $env{'form.domainid'}); - 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('

    '.&mt("Uploading file to [_1]",$coursedata{'description'}).'

    '); if (length($env{'form.upfile'}) < 2) { - $r->print("Error: The file you attempted to upload, ".&HTML::Entities::encode($env{'form.upfile.filename'},'<>&"').", contained no information. Please check that you entered the correct filename."); + $r->print(&mt('[_1]Error:[_2] The file you attempted to upload, [_3] contained no information. Please check that you entered the correct filename.','','',''.&HTML::Entities::encode($env{'form.upfile.filename'},'<>&"').'')); } else { - my $result=&Apache::lonnet::finishuserfileupload($env{'form.courseid'},$env{'form.domainid'},$home,'upfile',$fname); - if ($result =~ m|^/uploaded/|) { - $r->print("Success: Successfully uploaded ".(length($env{'form.upfile'})-1)." bytes of data into location ".$result.""); + my $result = + &Apache::lonnet::userfileupload('upfile','','scantron','','','', + $env{'form.courseid'},$env{'form.domainid'}); + if ($result =~ m{^/uploaded/}) { + $r->print(&mt('[_1]Success:[_2] Successfully uploaded [_3] bytes of data into location: [_4]', + '','',(length($env{'form.upfile'})-1), + ''.$result.'')); + ($uploadedfile) = ($result =~ m{/([^/]+)$}); + $r->print(&validate_uploaded_scantron_file($env{'form.domainid'}, + $env{'form.courseid'},$uploadedfile)); } else { - $r->print("Error: An error (".$result.") occurred when attempting to upload the file, ".&HTML::Entities::encode($env{'form.upfile.filename'},'<>&"').""); + $r->print(&mt('[_1]Error:[_2] An error ([_3]) occurred when attempting to upload the file, [_4]', + '','',$result, + ''.&HTML::Entities::encode($env{'form.upfile.filename'},'<>&"').'')); } } if ($symb) { - $r->print(&scantron_selectphase($r,$uploadedfile)); + $r->print(&scantron_selectphase($r,$uploadedfile,$symb)); } else { $r->print($doanotherupload); } return ''; } +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 ($max_match_count,$max_match_pct) = (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; + $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 .= '"'.$desc.'", '; + } elsif ($i==$numwithformat-2) { + $format_descs .= '"'.$desc.'" '.&mt('and').' '; + } elsif ($i==$numwithformat-1) { + $format_descs .= '"'.$desc.'"'; + } + } + my $showpct = sprintf("%.0f",$max_match_pct).'%'; + $output .= '
    '.&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]).',''.$showpct.'',''.$max_match_count.'',$format_descs). + '
    '.&mt('A low percentage of matches results from one of the following:').'
      '. + '
    • '.&mt('The file was uploaded to the wrong course').'
    • '. + '
    • '.&mt('The data are not in the format expected for the domain: [_1]', + ''.$cdom.'').'
    • '. + '
    • '.&mt('Students did not bubble their IDs, or mis-bubbled them').'
    • '. + '
    • '.&mt('The course roster is not up to date').'
    • '. + '
    '; + } + } else { + $output = ''.&mt('Uploaded file contained no data').''; + } + return $output; +} + sub valid_file { my ($requested_file)=@_; foreach my $filename (sort(&scantron_filenames())) { - &Apache::lonnet::logthis("$requested_file $filename"); if ($requested_file eq $filename) { return 1; } } return 0; } sub scantron_download_scantron_data { - my ($r)=@_; - my $default_form_data=&defaultFormData(&get_symb_and_url($r,1)); + my ($r,$symb)=@_; + 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(<print('

    - The requested file name was invalid. + '.&mt('The requested file name was invalid.').'

    -ERROR - $r->print(&show_grading_menu_form(&get_symb_and_url($r,1))); +'); + $r->print(&show_grading_menu_form($symb)); return; } my $orig='/uploaded/'.$cdom.'/'.$cname.'/scantron_orig_'.$file; @@ -5057,21 +8096,316 @@ ERROR &Apache::lonnet::allowuploaded('/adm/grades',$orig); &Apache::lonnet::allowuploaded('/adm/grades',$corrected); &Apache::lonnet::allowuploaded('/adm/grades',$skipped); - $r->print(<print('

    - Original file as uploaded by the scantron office. + '.&mt('[_1]Original[_2] file as uploaded by the scantron office.', + '','').'

    - Corrections, a file of corrected records that were used in grading. + '.&mt('[_1]Corrections[_2], a file of corrected records that were used in grading.', + '','').'

    - Skipped, a file of records that were skipped. + '.&mt('[_1]Skipped[_2], a file of records that were skipped.', + '','').'

    -DOWNLOAD - $r->print(&show_grading_menu_form(&get_symb_and_url($r,1))); +'); + $r->print(&show_grading_menu_form($symb)); return ''; } +sub checkscantron_results { + my ($r,$symb) = @_; + 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 ($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 @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0); + my (%grader_partids_by_symb,%grader_randomlists_by_symb); + &graders_resources_pass(\@resources,\%grader_partids_by_symb, \%grader_randomlists_by_symb); + + my ($uname,$udom); + my (%scandata,%lastname,%bylast); + $r->print(' +
    '."\n"); + + my @delayqueue; + my %completedstudents; + + my $count=&Apache::grades::get_todo_count($scanlines,$scan_data); + my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin($r,'Bubblesheet/Submissions Comparison Status', + 'Progress of Bubblesheet Data/Submission Records Comparison',$count, + 'inline',undef,'checkscantron'); + my ($username,$domain,$started); + my $nav_error; + &scantron_get_maxbubble(\$nav_error); # 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=&Apache::grades::scantron_find_student($scan_record,$scan_data, + \%idmap,$i)) { + &Apache::grades::scantron_add_delay(\@delayqueue,$line, + 'Unable to find a student that matches',1); + next; + } + if (exists $completedstudents{$uname}) { + &Apache::grades::scantron_add_delay(\@delayqueue,$line, + 'Student '.$uname.' has multiple sheets',2); + next; + } + my $pid = $scan_record->{'scantron.ID'}; + $lastname{$pid} = $scan_record->{'scantron.LastName'}; + push(@{$bylast{$lastname{$pid}}},$pid); + my $lastpos = $env{'form.scantron_maxbubble'}*$scantron_config{'Qlength'}; + $scandata{$pid} = substr($line,$scantron_config{'Qstart'}-1,$lastpos); + chomp($scandata{$pid}); + $scandata{$pid} =~ s/\r$//; + ($username,$domain)=split(/:/,$uname); + my $counter = -1; + foreach my $resource (@resources) { + 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); + } 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); + $record{$pid} .= $recording; + } + } + &Apache::lonhtmlcommon::Close_PrgWin($r,\%prog_state); + $r->print('
    '); + 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 .= ''. +''.&mt('Bubblesheet').''.$showscandata.''.$last.''.$pid.''."\n". +''."\n". +''."\n". +'Submissions'.$showrecord.''."\n"; + $passed ++; + } else { + my $css_class = ($failed % 2)?'LC_odd_row':'LC_even_row'; + $badstudents .= ''.&mt('Bubblesheet').''.$scandata{$pid}.''.$last.''.$pid.''."\n". +''."\n". +''."\n". +'Submissions'.$record{$pid}.''."\n". +''."\n"; + $failed ++; + } + $numstudents ++; + } + } + } + $r->print('

    '.&mt('Comparison of bubblesheet data (including corrections) with corresponding submission records (most recent submission) for [quant,_1,student] ([_2] scantron lines/student).',$numstudents,$env{'form.scantron_maxbubble'}).'

    '); + $r->print('

    '.&mt('Exact matches for [quant,_1,student].',$passed).'
    '.&mt('Discrepancies detected for [quant,_1,student].',$failed).'

    '); + if ($passed) { + $r->print(&mt('Students with exact correspondence between bubblesheet data and submissions are as follows:').'

    '); + $r->print(&Apache::loncommon::start_data_table()."\n". + &Apache::loncommon::start_data_table_header_row()."\n". + ''.&mt('Source').''.&mt('Bubble records').''.&mt('Name').''.&mt('ID').''. + &Apache::loncommon::end_data_table_header_row()."\n". + $okstudents."\n". + &Apache::loncommon::end_data_table().'
    '); + } + if ($failed) { + $r->print(&mt('Students with differences between bubblesheet data and submissions are as follows:').'

    '); + $r->print(&Apache::loncommon::start_data_table()."\n". + &Apache::loncommon::start_data_table_header_row()."\n". + ''.&mt('Source').''.&mt('Bubble records').''.&mt('Name').''.&mt('ID').''. + &Apache::loncommon::end_data_table_header_row()."\n". + $badstudents."\n". + &Apache::loncommon::end_data_table()).'
    '. + &mt('Differences can occur if submissions were modified using manual grading after a bubblesheet grading pass.').'
    '.&mt('If unexpected discrepancies were detected, it is recommended that you inspect the original bubblesheets.'); + } + $r->print('

    '.$grading_menu_button); + return; +} + +sub verify_scantron_grading { + my ($resource,$domain,$username,$cid,$counter,$scandata,$partids, + $scantron_config,$lettdig,$numletts) = @_; + 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; + if ($env{"form.scantron.sub_bubblelines.$counter"}) { + my @sub_lines = split(/,/,$env{"form.scantron.sub_bubblelines.$counter"}); + foreach my $item (@sub_lines) { + $expected{$part_id} += $item; + } + } else { + $expected{$part_id} = $env{"form.scantron.bubblelines.$counter"}; + } + $startpos{$part_id} = $env{"form.scantron.first_bubble_line.$counter"}; + } + if ($symb) { + my %recorded; + my (%returnhash) = &Apache::lonnet::restore($symb,$cid,$domain,$username); + if ($returnhash{'version'}) { + my %lasthash=(); + my $version; + for ($version=1;$version<=$returnhash{'version'};$version++) { + foreach my $key (sort(split(/\:/,$returnhash{$version.':keys'}))) { + $lasthash{$key}=$returnhash{$version.':'.$key}; + } + } + foreach my $key (keys(%lasthash)) { + if ($key =~ /\.scantron$/) { + my $value = &unescape($lasthash{$key}); + my ($part_id) = ($key =~ /^resource\.(.+)\.scantron$/); + if ($value eq '') { + for (my $i=0; $i<$expected{$part_id}; $i++) { + for (my $j=0; $j<$scantron_config->{'length'}; $j++) { + $recorded{$part_id} .= $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; +} + + #-------- end of section for handling grading scantron forms ------- # #------------------------------------------------------------------- @@ -5080,173 +8414,798 @@ DOWNLOAD # #--- Show a Grading Menu button - Calls the next routine --- sub show_grading_menu_form { - my ($symb,$url)=@_; + my ($symb)=@_; my $result.='
    '."\n". - ''."\n". - ''."\n". + ''."\n". ''."\n". ''."\n". - ''."\n". + ''."\n". '
    '."\n"; return $result; } -# -- Retrieve choices for grading form -sub savedState { - my %savedState = (); - if ($env{'form.saveState'}) { - foreach (split(/:/,$env{'form.saveState'})) { - my ($key,$value) = split(/=/,$_,2); - $savedState{$key} = $value; - } - } - return \%savedState; -} - -#--- Displays the main menu page ------- -sub gradingmenu { - my ($request) = @_; - my ($symb,$url)=&get_symb_and_url($request); +sub grading_menu { + my ($request,$symb) = @_; if (!$symb) {return '';} - my $probTitle = &Apache::lonnet::gettitle($symb); - $request->print(< - function checkChoice(formname,val,cmdx) { - if (val <= 2) { - var cmd = radioSelection(formname.radioChoice); - var cmdsave = cmd; - } else { - cmd = cmdx; - cmdsave = 'submission'; - } - formname.command.value = cmd; - formname.saveState.value = "saveCmd="+cmdsave+":saveSec="+pullDownSelection(formname.section)+ - ":saveSub="+pullDownSelection(formname.submitonly)+":saveStatus="+pullDownSelection(formname.Status); - if (val < 5) formname.submit(); - if (val == 5) { - if (!checkReceiptNo(formname,'notOK')) { return false;} - formname.submit(); - } - if (val < 7) formname.submit(); - } - - function checkReceiptNo(formname,nospace) { - var receiptNo = formname.receipt.value; - var checkOpt = false; - 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."); - formname.receipt.value = ""; - formname.receipt.focus(); - return false; - } - return true; - } - -GRADINGMENUJS + my %fields = ('symb'=>&Apache::lonenc::check_encrypt($symb), + 'command'=>'individual', + 'gradingMenu'=>1, + 'showgrading'=>"yes"); + + my $url1a = &Apache::lonhtmlcommon::build_url('grades/',\%fields); + + $fields{'command'}='ungraded'; + my $url1b=&Apache::lonhtmlcommon::build_url('grades/',\%fields); + + $fields{'command'}='table'; + my $url1c=&Apache::lonhtmlcommon::build_url('grades/',\%fields); + + $fields{'command'}='all_for_one'; + my $url1d=&Apache::lonhtmlcommon::build_url('grades/',\%fields); + + $fields{'command'} = 'csvform'; + my $url2 = &Apache::lonhtmlcommon::build_url('grades/',\%fields); + + $fields{'command'} = 'processclicker'; + my $url3 = &Apache::lonhtmlcommon::build_url('grades/',\%fields); + + $fields{'command'} = 'scantron_selectphase'; + my $url4 = &Apache::lonhtmlcommon::build_url('grades/',\%fields); + + $fields{'command'} = 'initialverifyreceipt'; + my $url5 = &Apache::lonhtmlcommon::build_url('grades/',\%fields); + + my @menu = ({ categorytitle=>'Hand Grading', + items =>[ + { linktext => 'Select individual students to grade', + url => $url1a, + permission => 'F', + icon => 'edit-find-replace.png', + linktitle => 'Grade current resource for a selection of students.' + }, + { linktext => 'Grade ungraded submissions.', + url => $url1b, + permission => 'F', + icon => 'edit-find-replace.png', + linktitle => 'Grade all submissions that have not been graded yet.' + }, + + { linktext => 'Grading table', + url => $url1c, + permission => 'F', + icon => 'edit-find-replace.png', + linktitle => 'Grade current resource for all students.' + }, + { linktext => 'Grade complete page/sequence/folder for one student', + url => $url1d, + permission => 'F', + icon => 'edit-find-replace.png', + linktitle => 'Grade all resources in current page/sequence/folder for one student.' + }]}, + { categorytitle=>'Automated Grading', + items =>[ + + { 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 scantron exams, upload/download scantron data files, and review previously graded scantron exams.' + }, + { linktext => 'Verify Receipt No.', + url => $url5, + permission => 'F', + icon => 'edit-find-replace.png', + linktitle => 'Verify a system-generated receipt number for correct problem solution.' + } + + ] + }); + + # Create the menu + my $Str; + $Str .= '
    '; + $Str .= ''. + ''."\n". + ''."\n". + ''."\n"; + + $Str .= &Apache::lonhtmlcommon::generate_menu(@menu); + return $Str; +} + + +sub ungraded { + my ($request)=@_; + &submit_options($request); +} + +sub submit_options_sequence { + my ($request,$symb) = @_; + if (!$symb) {return '';} &commonJSfunctions($request); - my $result='

     Manual Grading/View Submission

    '; - my ($table,undef,$hdgrade) = &showResourceInfo($url,$probTitle); - $result.=$table; - my (undef,$sections) = &getclasslist('all','0'); - my $savedState = &savedState(); - my $saveCmd = ($$savedState{'saveCmd'} eq '' ? 'submission' : $$savedState{'saveCmd'}); - my $saveSec = ($$savedState{'saveSec'} eq '' ? 'all' : $$savedState{'saveSec'}); - my $saveSub = ($$savedState{'saveSub'} eq '' ? 'all' : $$savedState{'saveSub'}); - my $saveStatus = ($$savedState{'saveStatus'} eq '' ? 'Active' : $$savedState{'saveStatus'}); + my $result; $result.=''."\n". - ''."\n". - ''."\n". - ''."\n". - ''."\n". - ''."\n". - ''."\n". + ''."\n". + ''."\n". + ''."\n"; + + $result.=' +

    + '.&mt('Grade complete page/sequence/folder for one student').' +

    '. + &selectfield(0). + ' +
    + +
    + +
    '; + $result .= &show_grading_menu_form($symb); + return $result; +} + +sub submit_options_table { + my ($request,$symb) = @_; + if (!$symb) {return '';} + &commonJSfunctions($request); + my $result; + + $result.='
    '."\n". + ''."\n". + ''."\n". + ''."\n"; + + $result.=' +

    + '.&mt('Grading table').' +

    '. + &selectfield(0). + ' +
    + +
    + +
    '; + $result .= &show_grading_menu_form($symb); + return $result; +} + + + +#--- Displays the submissions first page ------- +sub submit_options { + my ($request,$symb) = @_; + if (!$symb) {return '';} + + &commonJSfunctions($request); + my $result; + + $result.='
    '."\n". + ''."\n". ''."\n". ''."\n"; - $result.='
    '."\n". - ''."\n". - '
    '."\n". - ' Select a Grading/Viewing Option
    '."\n"; - - $result.=''; - $result.=''; - - $result.=''."\n"; - - $result.=''."\n"; - - $result.=''."\n"; - - $result.='
    '."\n". - ' '.&mt('Select Section').':   '; - - $result.=&mt('Student Status').':'.&Apache::lonhtmlcommon::StatusOptions($saveStatus,undef,1,undef); - - $result.='
    '. - ' '.''.&mt('Current Resource').': '.&mt('For one or more students'). - '
    '. - ' '. - 'Current Resource: For all students in selected section or course
    '. - ' '. - 'The complete set/page/sequence: For one student

    '. - ''. - '
    '."\n"; - - $result.='
    '; - - $result.=''; - $result.=''."\n"; - - $result.=''."\n"; - - if ((&Apache::lonnet::allowed('mgr',$env{'request.course.id'})) && ($symb)) { - $result.=''."\n"; - } - $result.=''."\n"; + $result.=' +

    + '.&mt('Select individual students to grade').' +

    '.&selectfield(1).' + + + + - $result.='
    '. - ''. - ' '.&mt('scores from file').'
    '. - ' scantron forms
    '. - ''. - ' '.&mt('receipt').': '. - &Apache::lonnet::recprefix($env{'request.course.id'}). - '-'. - '
    '. - ' access times.
    '."\n". - '
    '."\n". - '
    '."\n"; + + '; + $result .= &show_grading_menu_form($symb); return $result; } -sub handler { - my $request=$_[0]; +sub selectfield { + my ($full)=@_; + my $result='
    + +
    + + '.&mt('Sections').' + + '.&Apache::lonstatistics::SectionSelect('section','multiple',5).' +
    + +
    + + '.&mt('Groups').' + + '.&Apache::lonstatistics::GroupSelect('group','multiple',5).' +
    + +
    + + '.&mt('Access Status').' + + '.&Apache::lonhtmlcommon::StatusOptions(undef,undef,5,undef,'mult').' +
    '; + if ($full) { + $result.=' +
    + + '.&mt('Submission Status').' + '. + &Apache::loncommon::select_form('all','submitonly', + (&Apache::lonlocal::texthash( + 'yes' => 'with submissions', + 'queued' => 'in grading queue', + 'graded' => 'with ungraded submissions', + 'incorrect' => 'with incorrect submissions', + 'all' => 'with any status'), + 'select_form_order' => ['yes','queued','graded','incorrect','all'])). + '
    '; + } + $result.='

    '; + return $result; +} +sub reset_perm { undef(%perm); +} + +sub init_perm { + &reset_perm(); + foreach my $test_perm ('vgr','mgr','opa') { + + my $scope = $env{'request.course.id'}; + if (!($perm{$test_perm}=&Apache::lonnet::allowed($test_perm,$scope))) { + + $scope .= '/'.$env{'request.course.sec'}; + if ( $perm{$test_perm}= + &Apache::lonnet::allowed($test_perm,$scope)) { + $perm{$test_perm.'_section'}=$env{'request.course.sec'}; + } else { + delete($perm{$test_perm}); + } + } + } +} + +sub gather_clicker_ids { + my %clicker_ids; + + my $classlist = &Apache::loncoursedata::get_classlist(); + + # Set up a couple variables. + my $username_idx = &Apache::loncoursedata::CL_SNAME(); + my $domain_idx = &Apache::loncoursedata::CL_SDOM(); + my $status_idx = &Apache::loncoursedata::CL_STATUS(); + + foreach my $student (keys(%$classlist)) { + if ($classlist->{$student}->[$status_idx] ne 'Active') { next; } + my $username = $classlist->{$student}->[$username_idx]; + my $domain = $classlist->{$student}->[$domain_idx]; + my $clickers = + (&Apache::lonnet::userenvironment($domain,$username,'clickers'))[1]; + foreach my $id (split(/\,/,$clickers)) { + $id=~s/^[\#0]+//; + $id=~s/[\-\:]//g; + if (exists($clicker_ids{$id})) { + $clicker_ids{$id}.=','.$username.':'.$domain; + } else { + $clicker_ids{$id}=$username.':'.$domain; + } + } + } + return %clicker_ids; +} + +sub gather_adv_clicker_ids { + my %clicker_ids; + my $cnum=$env{'course.'.$env{'request.course.id'}.'.num'}; + my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'}; + my %coursepersonnel=&Apache::lonnet::get_course_adv_roles($cdom.'/'.$cnum); + foreach my $element (sort(keys(%coursepersonnel))) { + foreach my $person (split(/\,/,$coursepersonnel{$element})) { + my ($puname,$pudom)=split(/\:/,$person); + my $clickers = + (&Apache::lonnet::userenvironment($pudom,$puname,'clickers'))[1]; + foreach my $id (split(/\,/,$clickers)) { + $id=~s/^[\#0]+//; + $id=~s/[\-\:]//g; + if (exists($clicker_ids{$id})) { + $clicker_ids{$id}.=','.$puname.':'.$pudom; + } else { + $clicker_ids{$id}=$puname.':'.$pudom; + } + } + } + } + return %clicker_ids; +} + +sub clicker_grading_parameters { + return ('gradingmechanism' => 'scalar', + 'upfiletype' => 'scalar', + 'specificid' => 'scalar', + 'pcorrect' => 'scalar', + 'pincorrect' => 'scalar'); +} + +sub process_clicker { + my ($r,$symb)=@_; + if (!$symb) {return '';} + my $result=&checkforfile_js(); + $result.='
    '."\n"; + $result.=''."\n"; + $result.='
    '."\n"; + $result.=' '.&mt('Specify a file containing the clicker information for this resource.'). + '
    '."\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', + \%Saveable_Parameters); + if (!$env{'form.pcorrect'}) { $env{'form.pcorrect'}=100; } + if (!$env{'form.pincorrect'}) { $env{'form.pincorrect'}=100; } + if (!$env{'form.gradingmechanism'}) { $env{'form.gradingmechanism'}='attendance'; } + if (!$env{'form.upfiletype'}) { $env{'form.upfiletype'}='iclicker'; } + + my %checked; + foreach my $gradingmechanism ('attendance','personnel','specific','given') { + if ($env{'form.gradingmechanism'} eq $gradingmechanism) { + $checked{$gradingmechanism}=' checked="checked"'; + } + } + + my $upload=&mt("Upload File"); + my $type=&mt("Type"); + 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").' '. + '('.&mt("Provide comma-separated list. Use '*' for any answer correct, '-' for skip").')'; + 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')); + $symb = &Apache::lonenc::check_encrypt($symb); + $result.= &Apache::lonhtmlcommon::scripttag(< + + + + +
    +
    +
    +
    + +
    +
        + + +
    +
    +
    +' +ENDUPFORM + $result.='
    '."\n". + '


    '."\n"; + $result.=&show_grading_menu_form($symb); + return $result; +} + +sub process_clicker_file { + my ($r,$symb)=@_; + if (!$symb) {return '';} + + my %Saveable_Parameters=&clicker_grading_parameters(); + &Apache::loncommon::store_course_settings('grades_clicker', + \%Saveable_Parameters); + my $result=''; + if (($env{'form.gradingmechanism'} eq 'specific') && ($env{'form.specificid'}!~/\w/)) { + $result.=''.&mt('You need to specify a clicker ID for the correct answer').''; + return $result.&show_grading_menu_form($symb); + } + if (($env{'form.gradingmechanism'} eq 'given') && ($env{'form.givenanswer'}!~/\S/)) { + $result.=''.&mt('You need to specify the correct answer').''; + 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') { + %correct_ids=&gather_adv_clicker_ids(); + } + if ($env{'form.gradingmechanism'} eq 'specific') { + foreach my $correct_id (split(/[\s\,]/,$env{'form.specificid'})) {; + $correct_id=~tr/a-z/A-Z/; + $correct_id=~s/\s//gs; + $correct_id=~s/^[\#0]+//; + $correct_id=~s/[\-\:]//g; + if ($correct_id) { + $correct_ids{$correct_id}='specified'; + } + } + } + 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)',''.$env{'form.givenanswer'}.'',$foundgiven); + } else { + my $number=0; + $result.='

    '.&mt('Correctness determined by the following IDs').''; + foreach my $id (sort(keys(%correct_ids))) { + $result.='
    '.$id.' - '; + if ($correct_ids{$id} eq 'specified') { + $result.=&mt('specified'); + } else { + my ($uname,$udom)=split(/\:/,$correct_ids{$id}); + $result.=&Apache::loncommon::plainname($uname,$udom); + } + $number++; + } + $result.="

    \n"; + if ($number==0) { + $result.=''.&mt('No IDs found to determine correct answer').''; + 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.', + '', + '', + ''.&HTML::Entities::encode($env{'form.upfile.filename'},'<>&"').''); + return $result.&show_grading_menu_form($symb); + } + +# Were able to get all the info needed, now analyze the file + + $result.=&Apache::loncommon::studentbrowser_javascript(); + $symb = &Apache::lonenc::check_encrypt($symb); + my $heading=&mt('Scanning clicker file'); + $result.=(<
    +
    +$heading
    +
    + + + + + + +ENDHEADER + if ($env{'form.gradingmechanism'} eq 'given') { + $result.=''; + } + my %responses; + my @questiontitles; + my $errormsg=''; + my $number=0; + if ($env{'form.upfiletype'} eq 'iclicker') { + ($errormsg,$number)=&iclicker_eval(\@questiontitles,\%responses); + } + if ($env{'form.upfiletype'} eq 'interwrite') { + ($errormsg,$number)=&interwrite_eval(\@questiontitles,\%responses); + } + $result.='
    '.&mt('Found [_1] question(s)',$number).'
    '. + ''. + &mt('Awarding [_1] percent for correct and [_2] percent for incorrect responses', + $env{'form.pcorrect'},$env{'form.pincorrect'}). + '
    '; + if (($env{'form.gradingmechanism'} eq 'given') && ($number!=$foundgiven)) { + $result.=''.&mt('Number of given answers does not agree with number of questions in file.').''; + return $result.&show_grading_menu_form($symb); + } +# Remember Question Titles +# FIXME: Possibly need delimiter other than ":" + for (my $i=0;$i<$number;$i++) { + $result.='').'" />'; + } + my $correct_count=0; + my $student_count=0; + my $unknown_count=0; +# Match answers with usernames +# FIXME: Possibly need delimiter other than ":" + foreach my $id (keys(%responses)) { + if ($correct_ids{$id}) { + $result.="\n".''; + $correct_count++; + } elsif ($clicker_ids{$id}) { + if ($clicker_ids{$id}=~/\,/) { +# More than one user with the same clicker! + $result.="\n
    ".&mt('Clicker registered more than once').": ".$id."
    "; + $result.="\n".''. + "'; + $unknown_count++; + } else { +# Good: found one and only one user with the right clicker + $result.="\n".''; + $student_count++; + } + } else { + $result.="\n
    ".&mt('Unregistered Clicker')." ".$id."
    "; + $result.="\n".''. + "\n".&mt("Username").":  ". + "\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); + $unknown_count++; + } + } + $result.='
    '. + &mt('Found [_1] registered and [_2] unregistered clickers.',$student_count,$unknown_count); + if (($env{'form.gradingmechanism'} ne 'attendance') && ($env{'form.gradingmechanism'} ne 'given')) { + if ($correct_count==0) { + $errormsg.="Found no correct answers answers for grading!"; + } elsif ($correct_count>1) { + $result.='
    '.&mt("Found [_1] entries for grading!",$correct_count).''; + } + } + if ($number<1) { + $errormsg.="Found no questions."; + } + if ($errormsg) { + $result.='
    '.&mt($errormsg).''; + } else { + $result.='
    '; + } + $result.='
    '."\n". + '


    '."\n"; + return $result.&show_grading_menu_form($symb); +} + +sub iclicker_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[0] eq 'Question') { + for (my $i=3;$i<$#entries;$i+=6) { + $$questiontitles[$number]=$entries[$i]; + $number++; + } + } + if ($entries[0]=~/^\#/) { + my $id=$entries[0]; + my @idresponses; + $id=~s/^[\#0]+//; + for (my $i=0;$i<$number;$i++) { + my $idx=3+$i*6; + push(@idresponses,$entries[$idx]); + } + $$responses{$id}=join(',',@idresponses); + } + } + return ($errormsg,$number); +} + +sub interwrite_eval { + my ($questiontitles,$responses)=@_; + my $number=0; + my $errormsg=''; + my $skipline=1; + my $questionnumber=0; + my %idresponses=(); + 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[1] eq 'Time') { $skipline=0; next; } + if ($entries[1] eq 'Response') { $skipline=1; } + next if $skipline; + if ($entries[0]!=$questionnumber) { + $questionnumber=$entries[0]; + $$questiontitles[$number]=&mt('Question [_1]',$questionnumber); + $number++; + } + my $id=$entries[4]; + $id=~s/^[\#0]+//; + $id=~s/^v\d*\://i; + $id=~s/[\-\:]//g; + $idresponses{$id}[$number]=$entries[6]; + } + foreach my $id (keys(%idresponses)) { + $$responses{$id}=join(',',@{$idresponses{$id}}); + $$responses{$id}=~s/^\s*\,//; + } + return ($errormsg,$number); +} + +sub assign_clicker_grades { + my ($r,$symb)=@_; + if (!$symb) {return '';} +# See which part we are saving to + 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=''; + + my $heading=&mt('Assigning grades based on clicker file'); + $result.=(<
    +
    +$heading
    +ENDHEADER +# Get correct result +# FIXME: Possibly need delimiter other than ":" + my @correct=(); + my $gradingmechanism=$env{'form.gradingmechanism'}; + my $number=$env{'form.number'}; + if ($gradingmechanism ne 'attendance') { + foreach my $key (keys(%env)) { + if ($key=~/^form\.correct\:/) { + my @input=split(/\,/,$env{$key}); + for (my $i=0;$i<=$#input;$i++) { + if (($correct[$i]) && ($input[$i]) && + ($correct[$i] ne $input[$i])) { + $result.='
    '. + &mt('More than one correct result given for question "[_1]": [_2] versus [_3].', + $env{'form.question:'.$i},$correct[$i],$input[$i]).''; + } elsif ($input[$i]) { + $correct[$i]=$input[$i]; + } + } + } + } + for (my $i=0;$i<$number;$i++) { + if (!$correct[$i]) { + $result.='
    '. + &mt('No correct result given for question "[_1]"!', + $env{'form.question:'.$i}).''; + } + } + $result.='
    '.&mt("Correct answer: [_1]",join(', ',map { ($_?$_:'-') } @correct)); + } +# Start grading + my $pcorrect=$env{'form.pcorrect'}; + my $pincorrect=$env{'form.pincorrect'}; + my $storecount=0; + foreach my $key (keys(%env)) { + my $user=''; + if ($key=~/^form\.student\:(.*)$/) { + $user=$1; + } + if ($key=~/^form\.unknown\:(.*)$/) { + my $id=$1; + if (($env{'form.uname'.$id}) && ($env{'form.udom'.$id})) { + $user=$env{'form.uname'.$id}.':'.$env{'form.udom'.$id}; + } elsif ($env{'form.multi'.$id}) { + $user=$env{'form.multi'.$id}; + } + } + if ($user) { + my @answer=split(/\,/,$env{$key}); + my $sum=0; + my $realnumber=$number; + for (my $i=0;$i<$number;$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; + } + } + } + } + my $ave=$sum/(100*$realnumber); +# Store + my ($username,$domain)=split(/\:/,$user); + my %grades=(); + $grades{"resource.$part.solved"}='correct_by_override'; + $grades{"resource.$part.awarded"}=$ave; + $grades{"resource.regrader"}="$env{'user.name'}:$env{'user.domain'}"; + my $returncode=&Apache::lonnet::cstore(\%grades,$symb, + $env{'request.course.id'}, + $domain,$username); + if ($returncode ne 'ok') { + $result.="
    Failed to save student $username:$domain. Message when trying to save was ($returncode)"; + } else { + $storecount++; + } + } + } +# We are done + $result.='
    '.&mt('Successfully stored grades for [quant,_1,student].',$storecount). + '
    '."\n". + '


    '."\n"; + return $result.&show_grading_menu_form($symb); +} + +sub navmap_errormsg { + return '
    '. + &mt('An error occurred retrieving information about resources in the course.').'
    '. + &mt('It is recommended that you [_1]re-initialize the course[_2] and then return to this grading page.','',''). + '
    '; +} + +sub startpage { + my ($r,$symb,$crumbs,$onlyfolderflag) = @_; + unshift(@$crumbs,{href=>"/adm/grades?command=gradingmenu&symb=".&HTML::Entities::encode($symb,'<>&"'),text=>"Grading"}); + $r->print(&Apache::loncommon::start_page('Grading',undef, + {'bread_crumbs' => $crumbs})); + $r->print(&Apache::lonhtmlcommon::resource_info_box($symb,$onlyfolderflag)); +} + +sub handler { + my $request=$_[0]; + &reset_caches(); if ($env{'browser.mathml'}) { &Apache::loncommon::content_type($request,'text/xml'); } else { @@ -5255,143 +9214,325 @@ sub handler { $request->send_http_header; return '' if $request->header_only; &Apache::loncommon::get_unprocessed_cgi($ENV{'QUERY_STRING'}); - my $url=$env{'form.url'}; - my $symb=$env{'form.symb'}; + +# see what command we need to execute + my @commands=&Apache::loncommon::get_env_multiple('form.command'); my $command=$commands[0]; + if ($#commands > 0) { &Apache::lonnet::logthis("grades got multiple commands ".join(':',@commands)); } - if (!$url) { - my ($temp1,$temp2); - ($temp1,$temp2,$env{'form.url'})=&Apache::lonnet::decode_symb($symb); - $url = $env{'form.url'}; - } - &send_header($request); - if ($url eq '' && $symb eq '' && $command eq '') { - if ($env{'user.adv'}) { - if (($env{'form.codeone'}) && ($env{'form.codetwo'}) && - ($env{'form.codethree'})) { - my $token=$env{'form.codeone'}.'*'.$env{'form.codetwo'}.'*'. - $env{'form.codethree'}; - my ($tsymb,$tuname,$tudom,$tcrsid)= - &Apache::lonnet::checkin($token); - 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, - ('grade_username' => $tuname, - 'grade_domain' => $tudom, - 'grade_courseid' => $tcrsid, - 'grade_symb' => $tsymb))); - } else { - $request->print('

    Not authorized: '.$token.'

    '); - } - } else { - $request->print('

    Not a valid DocID: '.$token.'

    '); - } - } else { - $request->print(&Apache::lonxml::tokeninputfield()); - } - } + +# see what the symb is + + my $symb=$env{'form.symb'}; + unless ($symb) { + (my $url=$env{'form.url'}) =~ s-^https*://($ENV{'SERVER_NAME'}|$ENV{'HTTP_HOST'})--; + $symb=&Apache::lonnet::symbread($url); + } + &Apache::lonenc::check_decrypt(\$symb); + + $ssi_error = 0; + if ($symb eq '' && $command eq '') { +# +# Not called from a resource +# + } else { - if (!($perm{'vgr'}=&Apache::lonnet::allowed('vgr',$env{'request.course.id'}))) { - if ($perm{'vgr'}=&Apache::lonnet::allowed('vgr',$env{'request.course.id'}.'/'.$env{'request.course.sec'})) { - $perm{'vgr_section'}=$env{'request.course.sec'}; - } else { - delete($perm{'vgr'}); - } - } - if (!($perm{'mgr'}=&Apache::lonnet::allowed('mgr',$env{'request.course.id'}))) { - if ($perm{'mgr'}=&Apache::lonnet::allowed('mgr',$env{'request.course.id'}.'/'.$env{'request.course.sec'})) { - $perm{'mgr_section'}=$env{'request.course.sec'}; - } else { - delete($perm{'mgr'}); - } - } + &init_perm(); if ($command eq 'submission' && $perm{'vgr'}) { - ($env{'form.student'} eq '' ? &listStudents($request) : &submission($request,0,0)); + &startpage($request,$symb,[{href=>"", text=>"Student Submissions"}]); + ($env{'form.student'} eq '' ? &listStudents($request,$symb) : &submission($request,0,0,$symb)); } elsif ($command eq 'pickStudentPage' && $perm{'vgr'}) { - &pickStudentPage($request); + &startpage($request,$symb); + &pickStudentPage($request,$symb); } elsif ($command eq 'displayPage' && $perm{'vgr'}) { - &displayPage($request); + &startpage($request,$symb); + &displayPage($request,$symb); } elsif ($command eq 'gradeByPage' && $perm{'mgr'}) { - &updateGradeByPage($request); + &startpage($request,$symb); + &updateGradeByPage($request,$symb); } elsif ($command eq 'processGroup' && $perm{'vgr'}) { - &processGroup($request); + &startpage($request,$symb); + &processGroup($request,$symb); } elsif ($command eq 'gradingmenu' && $perm{'vgr'}) { - $request->print(&gradingmenu($request)); + &startpage($request,$symb); + $request->print(&grading_menu($request,$symb)); + } elsif ($command eq 'individual' && $perm{'vgr'}) { + &startpage($request,$symb); + $request->print(&submit_options($request,$symb)); + } elsif ($command eq 'ungraded' && $perm{'vgr'}) { + &startpage($request,$symb); + $request->print(&submit_options($request,$symb)); + } elsif ($command eq 'table' && $perm{'vgr'}) { + &startpage($request,$symb); + $request->print(&submit_options_table($request,$symb)); + } elsif ($command eq 'all_for_one' && $perm{'vgr'}) { + &startpage($request,$symb); + $request->print(&submit_options_sequence($request,$symb)); } elsif ($command eq 'viewgrades' && $perm{'vgr'}) { - $request->print(&viewgrades($request)); + &startpage($request,$symb); + $request->print(&viewgrades($request,$symb)); } elsif ($command eq 'handgrade' && $perm{'mgr'}) { - $request->print(&processHandGrade($request)); + &startpage($request,$symb); + $request->print(&processHandGrade($request,$symb)); } elsif ($command eq 'editgrades' && $perm{'mgr'}) { - $request->print(&editgrades($request)); + &startpage($request,$symb); + $request->print(&editgrades($request,$symb)); + } elsif ($command eq 'initialverifyreceipt' && $perm{'vgr'}) { + &startpage($request,$symb); + $request->print(&initialverifyreceipt($request,$symb)); } elsif ($command eq 'verify' && $perm{'vgr'}) { - $request->print(&verifyreceipt($request)); + &startpage($request,$symb); + $request->print(&verifyreceipt($request,$symb)); + } elsif ($command eq 'processclicker' && $perm{'mgr'}) { + &startpage($request,$symb); + $request->print(&process_clicker($request,$symb)); + } elsif ($command eq 'processclickerfile' && $perm{'mgr'}) { + &startpage($request,$symb); + $request->print(&process_clicker_file($request,$symb)); + } elsif ($command eq 'assignclickergrades' && $perm{'mgr'}) { + &startpage($request,$symb); + $request->print(&assign_clicker_grades($request,$symb)); } elsif ($command eq 'csvform' && $perm{'mgr'}) { - $request->print(&upcsvScores_form($request)); + &startpage($request,$symb); + $request->print(&upcsvScores_form($request,$symb)); } elsif ($command eq 'csvupload' && $perm{'mgr'}) { - $request->print(&csvupload($request)); + &startpage($request,$symb); + $request->print(&csvupload($request,$symb)); } elsif ($command eq 'csvuploadmap' && $perm{'mgr'} ) { - $request->print(&csvuploadmap($request)); + &startpage($request,$symb); + $request->print(&csvuploadmap($request,$symb)); } elsif ($command eq 'csvuploadoptions' && $perm{'mgr'}) { if ($env{'form.associate'} ne 'Reverse Association') { - $request->print(&csvuploadoptions($request)); + &startpage($request,$symb); + $request->print(&csvuploadoptions($request,$symb)); } else { if ( $env{'form.upfile_associate'} ne 'reverse' ) { $env{'form.upfile_associate'} = 'reverse'; } else { $env{'form.upfile_associate'} = 'forward'; } - $request->print(&csvuploadmap($request)); + &startpage($request,$symb); + $request->print(&csvuploadmap($request,$symb)); } } elsif ($command eq 'csvuploadassign' && $perm{'mgr'} ) { - $request->print(&csvuploadassign($request)); + &startpage($request,$symb); + $request->print(&csvuploadassign($request,$symb)); } elsif ($command eq 'scantron_selectphase' && $perm{'mgr'}) { - $request->print(&scantron_selectphase($request)); + &startpage($request,$symb); + $request->print(&scantron_selectphase($request,undef,$symb)); } elsif ($command eq 'scantron_warning' && $perm{'mgr'}) { - $request->print(&scantron_do_warning($request)); + &startpage($request,$symb); + $request->print(&scantron_do_warning($request,$symb)); } elsif ($command eq 'scantron_validate' && $perm{'mgr'}) { - $request->print(&scantron_validate_file($request)); + &startpage($request,$symb); + $request->print(&scantron_validate_file($request,$symb)); } elsif ($command eq 'scantron_process' && $perm{'mgr'}) { - $request->print(&scantron_process_students($request)); + &startpage($request,$symb); + $request->print(&scantron_process_students($request,$symb)); } elsif ($command eq 'scantronupload' && (&Apache::lonnet::allowed('usc',$env{'request.role.domain'})|| &Apache::lonnet::allowed('usc',$env{'request.course.id'}))) { - $request->print(&scantron_upload_scantron_data($request)); + &startpage($request,$symb); + $request->print(&scantron_upload_scantron_data($request,$symb)); } elsif ($command eq 'scantronupload_save' && (&Apache::lonnet::allowed('usc',$env{'request.role.domain'})|| &Apache::lonnet::allowed('usc',$env{'request.course.id'}))) { - $request->print(&scantron_upload_scantron_data_save($request)); + &startpage($request,$symb); + $request->print(&scantron_upload_scantron_data_save($request,$symb)); } elsif ($command eq 'scantron_download' && &Apache::lonnet::allowed('usc',$env{'request.course.id'})) { - $request->print(&scantron_download_scantron_data($request)); + &startpage($request,$symb); + $request->print(&scantron_download_scantron_data($request,$symb)); + } elsif ($command eq 'checksubmissions' && $perm{'vgr'}) { + &startpage($request,$symb); + $request->print(&checkscantron_results($request,$symb)); } elsif ($command) { - $request->print("Access Denied ($command)"); + &startpage($request,$symb); + $request->print('

    '.&mt('Access Denied ([_1])',$command).'

    '); } } - &send_footer($request); + if ($ssi_error) { + &ssi_print_error($request); + } + $request->print(&Apache::loncommon::end_page()); + &reset_caches(); return ''; } -sub send_header { - my ($request)= @_; - $request->print(&Apache::lontexconvert::header()); -# $request->print(" -#"); - $request->print(&Apache::loncommon::bodytag('Grading')); - $request->rflush(); -} - -sub send_footer { - my ($request)= @_; - $request->print(''); -} - 1; __END__; + + +=head1 NAME + +Apache::grades + +=head1 SYNOPSIS + +Handles the viewing of grades. + +This is part of the LearningOnline Network with CAPA project +described at http://www.lon-capa.org. + +=head1 OVERVIEW + +Do an ssi with retries: +While I'd love to factor out this with the vesrion in lonprintout, +that would either require a data coupling between modules, which I refuse to perpetuate (there's quite enough of that already), or would require the invention of another infrastructure +I'm not quite ready to invent (e.g. an ssi_with_retry object). + +At least the logic that drives this has been pulled out into loncommon. + + + +ssi_with_retries - Does the server side include of a resource. + if the ssi call returns an error we'll retry it up to + the number of times requested by the caller. + If we still have a proble, no text is appended to the + output and we set some global variables. + to indicate to the caller an SSI error occurred. + All of this is supposed to deal with the issues described + in LonCAPA BZ 5631 see: + http://bugs.lon-capa.org/show_bug.cgi?id=5631 + by informing the user that this happened. + +Parameters: + resource - The resource to include. This is passed directly, without + interpretation to lonnet::ssi. + form - The form hash parameters that guide the interpretation of the resource + + retries - Number of retries allowed before giving up completely. +Returns: + On success, returns the rendered resource identified by the resource parameter. +Side Effects: + The following global variables can be set: + ssi_error - If an unrecoverable error occurred this becomes true. + It is up to the caller to initialize this to false + if desired. + ssi_error_resource - If an unrecoverable error occurred, this is the value + of the resource that could not be rendered by the ssi + call. + ssi_error_message - The error string fetched from the ssi response + in the event of an error. + + +=head1 HANDLER SUBROUTINE + +ssi_with_retries() + +=head1 SUBROUTINES + +=over + +=item scantron_get_correction() : + + Builds the interface screen to interact with the operator to fix a + specific error condition in a specific scanline + + Arguments: + $r - Apache request object + $i - number of the current scanline + $scan_record - hash ref as returned from &scantron_parse_scanline() + $scan_config - hash ref as returned from &get_scantron_config() + $line - full contents of the current scanline + $error - error condition, valid values are + 'incorrectCODE', 'duplicateCODE', + 'doublebubble', 'missingbubble', + 'duplicateID', 'incorrectID' + $arg - extra information needed + For errors: + - duplicateID - paper number that this studentID was seen before on + - duplicateCODE - array ref of the paper numbers this CODE was + seen on before + - incorrectCODE - current incorrect CODE + - doublebubble - array ref of the bubble lines that have double + bubble errors + - missingbubble - array ref of the bubble lines that have missing + bubble errors + +=item scantron_get_maxbubble() : + + 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(). + + Returns the maximum number of bubble lines that are expected to + occur. Does this by walking the selected sequence rendering the + resource and then checking &Apache::lonxml::get_problem_counter() + for what the current value of the problem counter is. + + Caches the results to $env{'form.scantron_maxbubble'}, + $env{'form.scantron.bubble_lines.n'}, + $env{'form.scantron.first_bubble_line.n'} and + $env{"form.scantron.sub_bubblelines.n"} + which are the total number of bubble, lines, the number of bubble + lines for response n and number of the first bubble line for response n, + and a comma separated list of numbers of bubble lines for sub-questions + (for optionresponse, matchresponse, and rankresponse items), for response n. + + +=item scantron_validate_missingbubbles() : + + Validates all scanlines in the selected file to not have any + answers that don't have bubbles that have not been verified + to be bubble free. + +=item scantron_process_students() : + + Routine that does the actual grading of the bubble sheet information. + + The parsed scanline hash is added to %env + + Then foreach unskipped scanline it does an &Apache::lonnet::ssi() + foreach resource , with the form data of + + 'submitted' =>'scantron' + 'grade_target' =>'grade', + 'grade_username'=> username of student + 'grade_domain' => domain of student + 'grade_courseid'=> of course + 'grade_symb' => symb of resource to grade + + This triggers a grading pass. The problem grading code takes care + of converting the bubbled letter information (now in %env) into a + valid submission. + +=item scantron_upload_scantron_data() : + + Creates the screen for adding a new bubble sheet data file to a course. + +=item scantron_upload_scantron_data_save() : + + Adds a provided bubble information data file to the course if user + has the correct privileges to do so. + +=item valid_file() : + + Validates that the requested bubble data file exists in the course. + +=item scantron_download_scantron_data() : + + Shows a list of the three internal files (original, corrected, + skipped) for a specific bubble sheet data file that exists in the + course. + +=item scantron_validate_ID() : + + Validates all scanlines in the selected file to not have any + invalid or underspecified student/employee IDs + +=item navmap_errormsg() : + + Returns HTML mark-up inside a
    with a link to re-initialize the course. + Should be called whenever the request to instantiate a navmap object fails. + +=back + +=cut 500 Internal Server Error

    Internal Server Error

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

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

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