--- loncom/homework/grades.pm 2003/07/21 13:32:49 1.121 +++ loncom/homework/grades.pm 2008/12/18 13:19:17 1.528.2.2 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # The LON-CAPA Grading handler # -# $Id: grades.pm,v 1.121 2003/07/21 13:32:49 ng Exp $ +# $Id: grades.pm,v 1.528.2.2 2008/12/18 13:19:17 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -25,15 +25,6 @@ # # http://www.lon-capa.org/ # -# 2/9,2/13 Guy Albertelli -# 6/8 Gerd Kortemeyer -# 7/26 H.K. Ng -# 8/20 Gerd Kortemeyer -# Year 2002 -# June-August H.K. Ng -# Year 2003 -# February, March H.K. Ng -# package Apache::grades; use strict; @@ -44,116 +35,405 @@ 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; + + +# 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. + + # -# --- Retrieve the parts that matches stores_\d+ from the metadata file.--- +# 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. +# +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.--- sub getpartlist { - my ($url) = @_; - my @parts =(); - my (@metakeys) = split(/,/,&Apache::lonnet::metadata($url,'keys')); - foreach my $key (@metakeys) { - if ( $key =~ m/stores_(\w+)_.*/) { - push(@parts,$key); + my ($symb) = @_; + + my $navmap = Apache::lonnavmaps::navmap->new(); + 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 (@{ $partlist }) { + foreach my $key (@metakeys) { + if ($key =~ m/^stores_\Q$part\E_/) { push(@stores,$key); } } } - return @parts; + return @stores; } # --- Get the symbolic name of a problem and the url -sub get_symb_and_url { - my ($request) = @_; - (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 '') { $request->print("Unable to handle ambiguous references:$url:."); return ''; } - return ($symb,$url); -} - -# --- Retrieve the fullname for a user. Return lastname, first middle --- -# --- Generation is attached next to the lastname if it exists. --- -sub get_fullname { - my ($uname,$udom) = @_; - my %name=&Apache::lonnet::get('environment', ['lastname','generation', - 'firstname','middlename'], - $udom,$uname); - my $fullname; - my ($tmp) = keys(%name); - if ($tmp !~ /^(con_lost|error|no_such_host)/i) { - $fullname = &Apache::loncoursedata::ProcessFullName - (@name{qw/lastname generation firstname middlename/}); +sub get_symb { + my ($request,$silent) = @_; + (my $url=$env{'form.url'}) =~ s-^http://($ENV{'SERVER_NAME'}|$ENV{'HTTP_HOST'})--; + my $symb=($env{'form.symb'} ne '' ? $env{'form.symb'} : (&Apache::lonnet::symbread($url))); + if ($symb eq '') { + if (!$silent) { + $request->print("Unable to handle ambiguous references:$url:."); + return (); + } + } + &Apache::lonenc::check_decrypt(\$symb); + return ($symb); +} + +#--- 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 ' '.&mt('Fullname').' ('.&mt('Username').')'; } else { - &Apache::lonnet::logthis('grades.pm: no name data for '.$uname. - '@'.$udom.':'.$tmp); + return ' '.$fullname.' ('.$uname. + ($env{'user.domain'} eq $udom ? '' : ' ('.$udom.')').')'; } - return $fullname; } #--- 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) = shift; - my $allkeys = &Apache::lonnet::metadata($url,'keys'); - my %seen = (); - my (@partlist,%handgrade); - foreach (split(/,/,&Apache::lonnet::metadata($url,'packages'))) { - if (/^\w+response_\w+.*/) { - my ($responsetype,$part) = split(/_/,$_,2); - my ($partid,$respid) = split(/_/,$part); - $responsetype =~ s/response$//; # make it compatible w/ navmaps - should move to that!! - $handgrade{$part} = $responsetype.':'.($allkeys =~ /parameter_$part\_handgrade/ ? 'yes' : 'no'); - next if ($seen{$partid} > 0); - $seen{$partid}++; - push @partlist,$partid; - } + my ($symb) = shift; + + my $navmap = Apache::lonnavmaps::navmap->new(); + my $res = $navmap->getBySymb($symb); + 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,$symb)=@_; + my $display=&Apache::lonnet::EXT('resource.'.$partID.'.display',$symb); + if (defined($display) and $display ne '') { + $display.= " (id $partID)"; + } else { + $display=$partID; } - return \@partlist,\%handgrade; + return $display; } #--- Show resource title #--- and parts and response type sub showResourceInfo { - my ($url,$probTitle) = @_; - my $result =''. - ''."\n"; - my ($partlist,$handgrade) = &response_type($url); - my ($resptype,$hdgrade)=('','no'); - for (sort keys(%$handgrade)) { - my ($responsetype,$handgrade)=split(/:/,$$handgrade{$_}); - $resptype = $responsetype; - $hdgrade = $handgrade if ($handgrade eq 'yes'); - $result.=''. - ''; -# ''; + my ($symb,$probTitle,$checkboxes) = @_; + my $col=3; + if ($checkboxes) { $col=4; } + my $result = '

'.&mt('Current Resource').': '.$probTitle.'

'."\n"; + $result .='
Current Resource: '.$probTitle.'
Part '.(split(/_/))[0].'Type: '.$responsetype.'
Handgrade: '.$handgrade.'
'; + my ($partlist,$handgrade,$responseType) = &response_type($symb); + my %resptype = (); + my $hdgrade='no'; + my %partsseen; + foreach my $partID (sort(keys(%$responseType))) { + foreach my $resID (sort(keys(%{ $responseType->{$partID} }))) { + my $handgrade=$$handgrade{$partID.'_'.$resID}; + my $responsetype = $responseType->{$partID}->{$resID}; + $hdgrade = $handgrade if ($handgrade eq 'yes'); + $result.=''; + if ($checkboxes) { + if (exists($partsseen{$partID})) { + $result.=""; + } else { + $result.=""; + } + $partsseen{$partID}=1; + } + my $display_part=&get_display_part($partID,$symb); + $result.=''. + ''; +# ''; + } } $result.='
 '.&mt('Part: [_1]',$display_part).' '. + $resID.''.&mt('Type: [_1]',$responsetype).'
'.&mt('Handgrade: [_1]',$handgrade).'
'."\n"; - return $result,$resptype,$hdgrade,$partlist,$handgrade; + return $result,$responseType,$hdgrade,$partlist,$handgrade; +} + +sub reset_caches { + &reset_analyze_cache(); + &reset_perm(); +} + +{ + my %analyze_cache; + + sub reset_analyze_cache { + undef(%analyze_cache); + } + + sub get_analyze { + my ($symb,$uname,$udom,$no_increment)=@_; + my $key = "$symb\0$uname\0$udom"; + return $analyze_cache{$key} if (exists($analyze_cache{$key})); + + my (undef,undef,$url)=&Apache::lonnet::decode_symb($symb); + $url=&Apache::lonnet::clutter($url); + my $subresult=&ssi_with_retries($url, $ssi_retries, + ('grade_target' => 'analyze', + 'grade_domain' => $udom, + 'grade_symb' => $symb, + 'grade_courseid' => + $env{'request.course.id'}, + 'grade_username' => $uname, + 'grade_noincrement' => $no_increment)); + (undef,$subresult)=split(/_HASH_REF__/,$subresult,2); + my %analyze=&Apache::lonnet::str2hash($subresult); + return $analyze_cache{$key} = \%analyze; + } + + sub get_order { + my ($partid,$respid,$symb,$uname,$udom,$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); + foreach my $foil (@{&get_order($partid,$respid,$symb,$uname,$udom)}) { + if ($analyze->{"$partid.$respid.foil.value.$foil"} eq 'true') { + return $foil; + } + } + } } #--- Clean response type for display -#--- Currently filters option response type only. +#--- Currently filters option/rank/radiobutton/match/essay/Task +# response types only. sub cleanRecord { - my ($answer,$response) = @_; - if ($response eq 'option') { - my (@IDs,@ans); - foreach (split(/\&/,&Apache::lonnet::unescape($answer))) { - my ($optionID,$ans) = split(/=/); - push @IDs,$optionID.''; - push @ans,$ans; - } - my $grayFont = ''; - return ''. - ''. - ''. - '
Answer'. - (join '',@ans).'
'.$grayFont.'Option ID'.$grayFont. - (join ''.$grayFont,@IDs).'
'; + 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"}); + my ($toprow,$bottomrow); + foreach my $foil (@$order) { + if ($grading{$foil} == 1) { + $toprow.=''.$answer{$foil}.' '; + } else { + $toprow.=''.$answer{$foil}.' '; + } + $bottomrow.=''.$grayFont.$foil.' '; + } + return '
'. + ''.$toprow.''. + ''. + $grayFont.$bottomrow.''.'
'.&mt('Answer').'
'.$grayFont.&mt('Option ID').'
'; + } elsif ($response eq 'match') { + my %answer=&Apache::lonnet::str2hash($answer); + my %grading=&Apache::lonnet::str2hash($record->{$version."resource.$partid.$respid.submissiongrading"}); + my @items=&Apache::lonnet::str2array($record->{$version."resource.$partid.$respid.submissionitems"}); + my ($toprow,$middlerow,$bottomrow); + foreach my $foil (@$order) { + my $item=shift(@items); + if ($grading{$foil} == 1) { + $toprow.=''.$item.' '; + $middlerow.=''.$grayFont.$answer{$foil}.' '; + } else { + $toprow.=''.$item.' '; + $middlerow.=''.$grayFont.$answer{$foil}.' '; + } + $bottomrow.=''.$grayFont.$foil.' '; + } + return '
'. + ''.$toprow.''. + ''. + $middlerow.''. + ''. + $bottomrow.''.'
'.&mt('Answer').'
'.$grayFont.&mt('Item ID').'
'.$grayFont.&mt('Option ID').'
'; + } elsif ($response eq 'radiobutton') { + my %answer=&Apache::lonnet::str2hash($answer); + my ($toprow,$bottomrow); + my $correct = + &get_radiobutton_correct_foil($partid,$respid,$symb,$uname,$udom); + foreach my $foil (@$order) { + if (exists($answer{$foil})) { + if ($foil eq $correct) { + $toprow.=''.&mt('true').''; + } else { + $toprow.=''.&mt('true').''; + } + } else { + $toprow.=''.&mt('false').''; + } + $bottomrow.=''.$grayFont.$foil.' '; + } + return '
'. + ''.$toprow.''. + ''. + $grayFont.$bottomrow.''.'
'.&mt('Answer').'
'.$grayFont.&mt('Option ID').'
'; + } elsif ($response eq 'essay') { + if (! exists ($env{'form.'.$symb})) { + my (%keyhash) = &Apache::lonnet::dump('nohist_handgrade', + $env{'course.'.$env{'request.course.id'}.'.domain'}, + $env{'course.'.$env{'request.course.id'}.'.num'}); + + my $loginuser = $env{'user.name'}.':'.$env{'user.domain'}; + $env{'form.keywords'} = $keyhash{$symb.'_keywords'} ne '' ? $keyhash{$symb.'_keywords'} : ''; + $env{'form.kwclr'} = $keyhash{$loginuser.'_kwclr'} ne '' ? $keyhash{$loginuser.'_kwclr'} : 'red'; + $env{'form.kwsize'} = $keyhash{$loginuser.'_kwsize'} ne '' ? $keyhash{$loginuser.'_kwsize'} : '0'; + $env{'form.kwstyle'} = $keyhash{$loginuser.'_kwstyle'} ne '' ? $keyhash{$loginuser.'_kwstyle'} : ''; + $env{'form.'.$symb} = 1; # so that we don't have to read it from disk for multiple sub of the same prob. + } + $answer =~ s-\n-
-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; } @@ -186,7 +466,8 @@ sub commonJSfunctions { } } } else { - if (selectOne.selected) return selectOne.value; + // only one value it must be the selected one + return selectOne.value; } } @@ -196,35 +477,86 @@ 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; - foreach (keys(%$classlist)) { - # the following undefs are for 'domain', and 'username' respectively. - my (undef,undef,$end,$start,$id,$section,$fullname,$status)= - @{$classlist->{$_}}; + foreach my $student (keys(%$classlist)) { + my $end = + $classlist->{$student}->[&Apache::loncoursedata::CL_END()]; + my $start = + $classlist->{$student}->[&Apache::loncoursedata::CL_START()]; + my $id = + $classlist->{$student}->[&Apache::loncoursedata::CL_ID()]; + my $section = + $classlist->{$student}->[&Apache::loncoursedata::CL_SECTION()]; + my $fullname = + $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->{$_}); + if ($filterlist && (!($stu_status =~ /Any/))) { + if (!($stu_status =~ $status)) { + delete($classlist->{$student}); next; } } - $section = ($section ne '' ? $section : 'no'); + # 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{$_}=$fullname; + if ($classlist->{$student}) { + $fullnames{$student}=$fullname; + } } else { - delete($classlist->{$_}); + delete($classlist->{$student}); } } else { - delete($classlist->{$_}); + delete($classlist->{$student}); } } my %seen = (); @@ -274,11 +606,11 @@ sub canview { #--- Retrieve the grade status of a student for all the parts sub student_gradeStatus { - my ($url,$symb,$udom,$uname,$partlist) = @_; - my %record = &Apache::lonnet::restore($symb,$ENV{'request.course.id'},$udom,$uname); + my ($symb,$udom,$uname,$partlist) = @_; + my %record = &Apache::lonnet::restore($symb,$env{'request.course.id'},$udom,$uname); my %partstatus = (); foreach (@$partlist) { - my ($status,$foo) = split(/_/,$record{"resource.$_.solved"},2); + my ($status,undef) = split(/_/,$record{"resource.$_.solved"},2); $status = 'nothing' if ($status eq ''); $partstatus{$_} = $status; my $subkey = "resource.$_.submitted_by"; @@ -291,7 +623,8 @@ sub student_gradeStatus { # Use by verifyscript and viewgrades # Shows a student's view of problem and submission sub jscriptNform { - my ($url,$symb) = @_; + my ($symb) = @_; + my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status')); my $jscript=''."\n"; $jscript.= '
'."\n". - ''."\n". - ''."\n". - ''."\n". - ''."\n". + ''."\n". + ''."\n". + ''."\n". + ''."\n". ''."\n". ''."\n". ''."\n". @@ -311,6 +644,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 -------------------- # @@ -318,12 +680,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+/) { return ''; } + # these will be returned. Do not care if not at least 50 percent similar my $limit=0.6; my $sname=''; @@ -331,23 +697,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); @@ -364,51 +729,79 @@ sub most_similar { sub verifyreceipt { my $request = shift; - my $courseid = $ENV{'request.course.id'}; - my $receipt = unpack("%32C*",$Apache::lonnet::perlvar{'lonHostID'}).'-'. - $ENV{'form.receipt'}; + 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 ($symb) = &get_symb($request); - my $title.='

Verifying Submission Receipt '. - $receipt.'

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

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

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

'."\n". + '

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

'."\n"; my ($string,$contents,$matches) = ('','',0); my (undef,undef,$fullname) = &getclasslist('all','0'); - - foreach (sort {lc($$fullname{$a}) cmp lc($$fullname{$b}) } keys %$fullname) { + + my $receiptparts=0; + if ($env{"course.$courseid.receiptalg"} eq 'receipt2' || + $env{"course.$courseid.receiptalg"} eq 'receipt3') { $receiptparts=1; } + my $parts=['0']; + if ($receiptparts) { ($parts)=&response_type($symb); } + + 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(/\:/); - if ($receipt eq - &Apache::lonnet::ireceipt($uname,$udom,$courseid,$symb)) { - $contents.=' '."\n". - ''.$$fullname{$_}.' '."\n". - ' '.$uname.' '. - ' '.$udom.' '."\n"; - - $matches++; + foreach my $part (@$parts) { + if ($receipt eq &Apache::lonnet::ireceipt($uname,$udom,$courseid,$symb,$part)) { + $contents.= + &Apache::loncommon::start_data_table_row(). + ' '."\n". + ''.$$fullname{$_}.' '."\n". + ' '.$uname.' '. + ' '.$udom.' '; + if ($receiptparts) { + $contents.=' '.$part.' '; + } + $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.'); } else { - $string = &jscriptNform($url,$symb).$title. - 'The above receipt matches the following student'. - ($matches <= 1 ? '.' : 's.')."\n". - '
'."\n". - ''."\n". - ''."\n". - ''."\n". - ''."\n". + $string = &jscriptNform($symb).$title. + '

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

'. + $header. $contents. - '
 Fullname  Username  Domain 
'."\n"; + &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. @@ -418,22 +811,28 @@ sub verifyreceipt { sub listStudents { my ($request) = shift; - 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 $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,$resptype,$hdgrade,$partlist,$handgrade) = &showResourceInfo($url,$ENV{'form.probTitle'}); - $result.=$table; - + my ($symb) = &get_symb($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='

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

'; + + my ($table,undef,$hdgrade,$partlist,$handgrade) = &showResourceInfo($symb,$env{'form.probTitle'},($env{'form.showgrading'} eq 'yes')); + + my %lt = ( 'multiple' => + "Please select a student or group of students before clicking on the Next button.", + 'single' => + "Please select the student before clicking on the Next button.", + ); + %lt = &Apache::lonlocal::texthash(%lt); $request->print(< function checkSelect(checkBox) { @@ -445,15 +844,15 @@ sub listStudents { ctr++; } } - sense = "a student or group of students"; + sense = '$lt{'multiple'}'; } else { if (checkBox.checked) { ctr = 1; } - sense = "the student"; + sense = '$lt{'single'}'; } if (ctr == 0) { - alert("Please select "+sense+" before clicking on the $viewgrade button."); + alert(sense); return false; } document.gradesub.submit(); @@ -470,143 +869,278 @@ LISTJAVASCRIPT &commonJSfunctions($request); $request->print($result); - my $checkhdgrade = ($ENV{'form.handgrade'} eq 'yes' && scalar(@$partlist) > 1 ) ? 'checked' : ''; - my $checklastsub = $checkhdgrade eq '' ? 'checked' : ''; - my $gradeTable=''."\n". - ' View Problem Text: no '."\n". - ' one student '."\n". - ' all students
'."\n". - ' Submissions: '."\n"; - if ($ENV{'form.handgrade'} eq 'yes' && scalar(@$partlist) > 1) { - $gradeTable.=' essay part only'."\n"; - } - - my $saveStatus = $ENV{'form.Status'} eq '' ? 'Active' : $ENV{'form.Status'}; - $ENV{'form.Status'} = $saveStatus; - - $gradeTable.=' last sub only'."\n". - ' last sub & parts info'."\n". - ' all details'."\n". - ''."\n". + my $checkhdgrade = ($env{'form.handgrade'} eq 'yes' && scalar(@$partlist) > 1 ) ? 'checked="checked"' : ''; + my $checklastsub = $checkhdgrade eq '' ? 'checked="checked"' : ''; + my $gradeTable=''. + "\n".$table; + + $gradeTable .= + ' '. + &mt('View Problem Text: [_1]', + ''."\n". + ''."\n". + '').'
'."\n"; + $gradeTable .= + ' '. + &mt('View Answer: [_1]', + ''."\n". + ''."\n". + '').'
'."\n"; + + my $submission_options; + if ($env{'form.handgrade'} eq 'yes' && scalar(@$partlist) > 1) { + $submission_options.= + ''."\n"; + } + 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 .= + ' '. + &mt('Submissions: [_1]',$submission_options).'
'."\n"; + + $gradeTable .= + ' '. + &mt('Grading Increments: [_1]', + ''); + + $gradeTable .= + &build_section_inputs(). ''."\n". - ''."\n". - '
'."\n". - '
'."\n". - ''."\n". - ''."\n". - ''."\n". - ''."\n". + '
'."\n". + '
'."\n". + ''."\n". + ''."\n". + ''."\n". ''."\n"; - $gradeTable.='Student Status: '. - &Apache::lonhtmlcommon::StatusOptions($saveStatus,undef,1,'javascript:reLoadList(this.form);').'
'; + if (exists($env{'form.gradingMenu'}) && exists($env{'form.Status'})) { + $gradeTable.=''."\n"; + } else { + $gradeTable.=&mt('Student Status: [_1]', + &Apache::lonhtmlcommon::StatusOptions($saveStatus,undef,1,'javascript:reLoadList(this.form);')).'
'; + } - $gradeTable.='To '.lc($viewgrade).' a submission, click on the check box next to the student\'s name. Then '."\n". - 'click on the '.$viewgrade.' button. To view the submissions for a group of students, click'."\n". - ' on the check boxes for the group of students.
'."\n". + $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". ''."\n"; + +# checkall buttons + $gradeTable.=&check_script('gradesub', 'stuinfo'); $gradeTable.=''."\n"; - - my (undef, undef, $fullname) = &getclasslist($getsec,'1'); - $gradeTable.='
'. - ''; + 'value="'.&mt('Next->').'" />
'."\n"; + $gradeTable.=&check_buttons(); + $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)) { - $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); - my $statusflg = ''; + + 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; foreach (keys(%status)) { - $statusflg = 1 if ($status{$_} ne 'nothing'); + $submitted = 1 if ($status{$_} ne 'nothing'); + $graded = 1 if ($status{$_} =~ /^ungraded/); + $incorrect = 1 if ($status{$_} =~ /^incorrect/); + my ($foo,$partid,$foo1) = split(/\./,$_); if ($status{'resource.'.$partid.'.submitted_by'} ne '') { - $statusflg = ''; + $submitted = 0; + my ($part)=split(/\./,$partid); $gradeTable.=''; } } - next if ($statusflg eq '' && $submitonly eq 'yes'); + + next if (!$submitted && ($submitonly eq 'yes' || + $submitonly eq 'incorrect' || + $submitonly eq 'graded')); + next if (!$graded && ($submitonly eq 'graded')); + next if (!$incorrect && $submitonly eq 'incorrect'); } $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); - $gradeTable.=''."\n". - ''."\n"; - - if ($ENV{'form.showgrading'} eq 'yes' && $submitonly ne 'all') { - foreach (sort keys(%status)) { - next if (/^resource.*?submitted_by$/); - $gradeTable.=''."\n"; + if ($ctr%2 ==1) { + $gradeTable.= &Apache::loncommon::start_data_table_row(); + } + $gradeTable.=''. + ''."\n".''."\n"; + + if ($env{'form.showgrading'} eq 'yes' && $submitonly ne 'all') { + foreach (sort(keys(%status))) { + next if ($_ =~ /^resource.*?submitted_by$/); + $gradeTable.=''."\n"; } } - $gradeTable.=''."\n" if ($ctr%2 ==0); +# $gradeTable.='' if ($ctr%2 ==1); + 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') { + $gradeTable.=''; + 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.='
 Select  Fullname '. - '(Username)  Part '.(split(/_/))[0].' Status '.&mt('No.').''.&mt('Select').''.&nameUserString('header').' '.&mt('Section/Group').''.&mt('Part: [_1] Status',$display_part).''.&mt('Queue Status').' 
 '.$$fullname{$student}.' '."\n". - '('.$uname.') '.$status{$_}.' '.$ctr.' '. + &nameUserString(undef,$$fullname{$student},$uname,$udom). + ' '.$section.($group ne '' ?'/'.$group:'').' '.&mt($status{$_}).' 
       
'. + $gradeTable.=&Apache::loncommon::end_data_table()."\n". ''."\n"; + 'value="'.&mt('Next->').'" />'."\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 { - $gradeTable='
 '. - 'No submissions found for this resource for any students. ('.$num_students. - ' checked for submissions
'; + my $submissions='submissions'; + if ($submitonly eq 'incorrect') { $submissions = 'incorrect submissions'; } + if ($submitonly eq 'graded' ) { $submissions = 'ungraded submissions'; } + if ($submitonly eq 'queued' ) { $submissions = 'queued submissions'; } + $gradeTable='
 '. + &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 ''; } #---- Called from the listStudents routine + +sub check_script { + my ($form, $type)=@_; + my $chkallscript=''."\n"; + return $chkallscript; +} + +sub check_buttons { + my $buttons.=''; + $buttons.=' '; + $buttons.=''; + $buttons.=' '; + return $buttons; +} + # Displays the submissions for one student or a group of students sub processGroup { my ($request) = shift; my $ctr = 0; - my @stuchecked = (ref($ENV{'form.stuinfo'}) ? @{$ENV{'form.stuinfo'}} - : ($ENV{'form.stuinfo'})); + my @stuchecked = &Apache::loncommon::get_env_multiple('form.stuinfo'); my $total = scalar(@stuchecked)-1; - foreach (@stuchecked) { - my ($uname,$udom,$fullname) = split(/:/); - $ENV{'form.student'} = $uname; - $ENV{'form.userdom'} = $udom; - $ENV{'form.fullname'} = $fullname; + foreach my $student (@stuchecked) { + my ($uname,$udom,$fullname) = split(/:/,$student); + $env{'form.student'} = $uname; + $env{'form.userdom'} = $udom; + $env{'form.fullname'} = $fullname; &submission($request,$ctr,$total); $ctr++; } @@ -624,9 +1158,9 @@ sub sub_page_js { $request->print(< function updateRadio(formname,id,weight) { - var gradeBox = eval("formname.GD_BOX"+id); - var radioButton = eval("formname.RADVAL"+id); - var oldpts = eval("formname.oldpts"+id+".value"); + var gradeBox = formname["GD_BOX"+id]; + var radioButton = formname["RADVAL"+id]; + var oldpts = formname["oldpts"+id].value; var pts = checkSolved(formname,id) == 'update' ? gradeBox.value : oldpts; gradeBox.value = pts; var resetbox = false; @@ -648,7 +1182,7 @@ sub sub_page_js { var resp = confirm("You entered a value ("+pts+ ") greater than the weight for the part. Accept?"); if (resp == false) { - gradeBox.value = ""; + gradeBox.value = oldpts; return; } } @@ -660,18 +1194,17 @@ sub sub_page_js { } } updateSelect(formname,id); - var stores = eval("formname.stores"+id); - stores.value = "0"; + formname["stores"+id].value = "0"; } function writeBox(formname,id,pts) { - var gradeBox = eval("formname.GD_BOX"+id); + var gradeBox = formname["GD_BOX"+id]; if (checkSolved(formname,id) == 'update') { gradeBox.value = pts; } else { - var oldpts = eval("formname.oldpts"+id+".value"); + var oldpts = formname["oldpts"+id].value; gradeBox.value = oldpts; - var radioButton = eval("formname.RADVAL"+id); + var radioButton = formname["RADVAL"+id]; for (var i=0; idir_config('lonIconsURL'); &commonJSfunctions($request); + + my $inner_js_msg_central=< + function checkInput() { + opener.document.SCORE.msgsub.value = opener.checkEntities(document.msgcenter.msgsub.value); + var nmsg = opener.document.SCORE.savemsgN.value; + var usrctr = document.msgcenter.usrctr.value; + var newval = opener.document.SCORE["newmsg"+usrctr]; + newval.value = opener.checkEntities(document.msgcenter.newmsg.value); + + var msgchk = ""; + if (document.msgcenter.subchk.checked) { + msgchk = "msgsub,"; + } + var includemsg = 0; + for (var i=1; i<=nmsg; i++) { + var opnmsg = opener.document.SCORE["savemsg"+i]; + var frmmsg = document.msgcenter["msg"+i]; + opnmsg.value = opener.checkEntities(frmmsg.value); + var showflg = opener.document.SCORE["shownOnce"+i]; + showflg.value = "1"; + var chkbox = document.msgcenter["msgn"+i]; + if (chkbox.checked) { + msgchk += "savemsg"+i+","; + includemsg = 1; + } + } + if (document.msgcenter.newmsgchk.checked) { + msgchk += "newmsg"+usrctr; + includemsg = 1; + } + imgformname = opener.document.SCORE["mailicon"+usrctr]; + imgformname.src = "$iconpath/"+((includemsg) ? "mailto.gif" : "mailbkgrd.gif"); + var includemsg = opener.document.SCORE["includemsg"+usrctr]; + includemsg.value = msgchk; + + self.close() + + } + +INNERJS + + my $inner_js_highlight_central=< + function updateChoice(flag) { + opener.document.SCORE.kwclr.value = opener.radioSelection(document.hlCenter.kwdclr); + opener.document.SCORE.kwsize.value = opener.radioSelection(document.hlCenter.kwdsize); + opener.document.SCORE.kwstyle.value = opener.radioSelection(document.hlCenter.kwdstyle); + opener.document.SCORE.refresh.value = "on"; + if (opener.document.SCORE.keywords.value!=""){ + opener.document.SCORE.submit(); + } + self.close() + } + +INNERJS + + my $start_page_msg_central = + &Apache::loncommon::start_page('Message Central',$inner_js_msg_central, + {'js_ready' => 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(< //===================== Show list of keywords ==================== - function keywords(keyform) { - var nret = prompt("Keywords list, separated by a space. Add/delete to list if desired.",keyform.value); + function keywords(formname) { + var nret = prompt("Keywords list, separated by a space. Add/delete to list if desired.",formname.keywords.value); if (nret==null) return; - keyform.value = nret; + formname.keywords.value = nret; - document.SCORE.refresh.value = "on"; - if (document.SCORE.keywords.value != "") { - document.SCORE.submit(); + if (formname.keywords.value != "") { + formname.refresh.value = "on"; + formname.submit(); } return; } @@ -839,10 +1447,9 @@ sub sub_page_kw_js { } var nret = prompt("Add selection to keyword list? Edit if desired.",cleantxt); if (nret==null) return; - var curlist = document.SCORE.keywords.value; - document.SCORE.keywords.value = curlist+" "+nret; - document.SCORE.refresh.value = "on"; + document.SCORE.keywords.value = document.SCORE.keywords.value+" "+nret; if (document.SCORE.keywords.value != "") { + document.SCORE.refresh.value = "on"; document.SCORE.submit(); } return; @@ -859,21 +1466,23 @@ sub sub_page_kw_js { var Nmsg = msgform.savemsgN.value; savedMsgHeader(Nmsg,usrctr,fullname); var subject = msgform.msgsub.value; - var rtrchk = eval("document.SCORE.includemsg"+usrctr); - var msgchk = rtrchk.value; + var msgchk = document.SCORE["includemsg"+usrctr].value; re = /msgsub/; var shwsel = ""; if (re.test(msgchk)) { shwsel = "checked" } - displaySubject(subject,shwsel); + subject = (document.SCORE.shownSub.value == 0 ? checkEntities(subject) : subject); + displaySubject(checkEntities(subject),shwsel); for (var i=1; i<=Nmsg; i++) { - var testpt = "savemsg"+i+","; - re = /testpt/; + var testmsg = "savemsg"+i+","; + re = new RegExp(testmsg,"g"); shwsel = ""; if (re.test(msgchk)) { shwsel = "checked" } - var message = eval("document.SCORE.savemsg"+i+".value"); - displaySavedMsg(i,message,shwsel); + var message = document.SCORE["savemsg"+i].value; + message = (document.SCORE["shownOnce"+i].value == 0 ? checkEntities(message) : message); + displaySavedMsg(i,message,shwsel); //I do not get it. w/o checkEntities on saved messages, + //any < is already converted to <, etc. However, only once!! } - newmsg = eval("document.SCORE.newmsg"+usrctr+".value"); + newmsg = document.SCORE["newmsg"+usrctr].value; shwsel = ""; re = /newmsg/; if (re.test(msgchk)) { shwsel = "checked" } @@ -882,6 +1491,22 @@ sub sub_page_kw_js { return; } + function checkEntities(strx) { + if (strx.length == 0) return strx; + var orgStr = ["&", "<", ">", '"']; + var newStr = ["&", "<", ">", """]; + var counter = 0; + while (counter < 4) { + strx = strReplace(strx,orgStr[counter],newStr[counter]); + counter++; + } + return strx; + } + + function strReplace(strx, orgStr, newStr) { + return strx.split(orgStr).join(newStr); + } + function savedMsgHeader(Nmsg,usrctr,fullname) { var height = 70*Nmsg+250; var scrollbar = "no"; @@ -889,99 +1514,58 @@ sub sub_page_kw_js { height = 600; scrollbar = "yes"; } -// if (window.pWin) {window.pWin.close(); window.pWin=null} var xpos = (screen.width-600)/2; xpos = (xpos < 0) ? '0' : xpos; var ypos = (screen.height-height)/2-30; ypos = (ypos < 0) ? '0' : ypos; - pWin = window.open('', 'MessageCenter', 'toolbar=no,location=no,scrollbars='+scrollbar+',screenx='+xpos+',screeny='+ypos+',width=600,height='+height); + pWin = window.open('', 'MessageCenter', 'resizable=yes,toolbar=no,location=no,scrollbars='+scrollbar+',screenx='+xpos+',screeny='+ypos+',width=600,height='+height); pWin.focus(); pDoc = pWin.document; - pDoc.write(""); - 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; +} + #--- 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"; - - $result.='
'. - 'Part '.$partid.' 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.=''; my $ctr = 0; - $result.='
'."\n"; # display radio buttons in a nice table 10 across - while ($ctr<=$wgt) { - $result.= '\n"; - $result.=(($ctr+1)%10 == 0 ? '' : ''); + $thisweight.')" value="'.$thisweight.'" '. + ($score eq $thisweight ? 'checked="checked"':'').' /> '.$thisweight."\n"; + $radio.=(($ctr+1)%10 == 0 ? '' : ''); + $thisweight += $increment; $ctr++; } - $result.='
'.$ctr."
'; + $radio.='
'; - $result.=' or '."\n"; - $result.=''."\n"; - $result.='/'.$wgt.' '.$wgtmsg. + $line.='/'.$wgt.' '.$wgtmsg. ($$record{'resource.'.$partid.'.solved'} eq 'correct_by_student' ? ' '.$checkIcon : ''). ' '."\n"; - - $result.=''."\n"; + $line.=''. + ''; } else { - $result.=''. - ''."\n"; + $line.=''. + ''; } - $result.="  \n"; + $line.=''."\n"; + + + $result .= + &mt('Part:[_1]Points:[_2]or[_3]',$display_part,$radio,$line); + + + $result.=''."\n"; $result.=''."\n". ''."\n". ''."\n"; - $result.=''."\n"; + $$record{'resource.'.$partid.'.solved'}.'" />'."\n". + ''."\n". + ''."\n"; + $result.=&handback_box($symb,$uname,$udom,$counter,$partid,$record); return $result; } +sub handback_box { + my ($symb,$uname,$udom,$counter,$partid,$record) = @_; + my ($partlist,$handgrade,$responseType) = &response_type($symb); + 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) = @_; - my $rendered=&Apache::loncommon::get_student_view($symb,$uname,$udom, - $ENV{'request.course.id'}); + 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'}, + 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') { + &Apache::lonxml::restore_problem_counter(); + $companswer= + &Apache::loncommon::get_student_answers($symb,$uname,$udom, + $env{'request.course.id'}, + %form); } - my $companswer=&Apache::loncommon::get_student_answers($symb,$uname,$udom, - $ENV{'request.course.id'}); if ($removeform) { $companswer=~s|||g; $companswer=~s|||g; - $rendered=~s|name="submit"|name="would_have_been_submit"|g; + $companswer=~s|name="submit"|name="would_have_been_submit"|g; + } + $rendered= + '
'. + &mt('View of the problem'). + '
'. + $rendered. + '
'; + $companswer= + '
'. + &mt('Correct answer'). + '
'. + $companswer. + '
'; + my $result; + if ($mode eq 'both') { + $result=$rendered.$companswer; + } elsif ($mode eq 'text') { + $result=$rendered; + } elsif ($mode eq 'answer') { + $result=$companswer; } - my $result.='
'; - $result.=''; - $result.='' if ($viewon); - $result.='
View of the problem - '.$ENV{'form.fullname'}. - '
'.$rendered.'
'; - $result.='Correct answer:
'.$companswer; - $result.='
'; - $result.='

'; + $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 ($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'} = &get_fullname ($uname,$udom) 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 ($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 = &get_symb($request); + if ($symb eq '') { $request->print("Unable to handle ambiguous references:."); return ''; } if (!&canview($usec)) { - $request->print('Unable to view requested student.('. - $uname.$udom.$usec.$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; } - $ENV{'form.lastSub'} = ($ENV{'form.lastSub'} eq '' ? 'all' : $ENV{'form.lastSub'}); - my $last = ($ENV{'form.lastSub'} eq 'last' ? 'last' : ''); + if (!$env{'form.lastSub'}) { $env{'form.lastSub'} = 'datesub'; } + 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 = ''.&mt('Check Mark').
+	''; + 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 $checkIcon = ''; - my $checkMark='

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

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

'."\n". + '

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

'."\n"); # option to display problem, only once else it cause problems # with the form later since the problem has a form. - if ($ENV{'form.vProb'} eq 'yes' or !$ENV{'form.vProb'}) { - $request->print(&show_problem($request,$symb,$uname,$udom,0,1)); + if ($env{'form.vProb'} eq 'yes' or $env{'form.vAns'} eq 'yes') { + my $mode; + if ($env{'form.vProb'} eq 'yes' && $env{'form.vAns'} eq 'yes') { + $mode='both'; + } elsif ($env{'form.vProb'} eq 'yes') { + $mode='text'; + } 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 = (); - if ($ENV{'form.kwclr'} eq '' && $ENV{'form.handgrade'} eq 'yes') { + if ($env{'form.kwclr'} eq '' && $env{'form.handgrade'} eq 'yes') { %keyhash = &Apache::lonnet::dump('nohist_handgrade', - $ENV{'course.'.$ENV{'request.course.id'}.'.domain'}, - $ENV{'course.'.$ENV{'request.course.id'}.'.num'}); + $env{'course.'.$env{'request.course.id'}.'.domain'}, + $env{'course.'.$env{'request.course.id'}.'.num'}); - my $loginuser = $ENV{'user.name'}.':'.$ENV{'user.domain'}; - $ENV{'form.keywords'} = $keyhash{$symb.'_keywords'} ne '' ? $keyhash{$symb.'_keywords'} : ''; - $ENV{'form.kwclr'} = $keyhash{$loginuser.'_kwclr'} ne '' ? $keyhash{$loginuser.'_kwclr'} : 'red'; - $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'}; - $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 $loginuser = $env{'user.name'}.':'.$env{'user.domain'}; + $env{'form.keywords'} = $keyhash{$symb.'_keywords'} ne '' ? $keyhash{$symb.'_keywords'} : ''; + $env{'form.kwclr'} = $keyhash{$loginuser.'_kwclr'} ne '' ? $keyhash{$loginuser.'_kwclr'} : 'red'; + $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'}; + $env{'form.savemsgN'} = $keyhash{$symb.'_savemsgN'} ne '' ? $keyhash{$symb.'_savemsgN'} : '0'; + } + my $overRideScore = $env{'form.overRideScore'} eq '' ? 'no' : $env{'form.overRideScore'}; + 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". - ''."\n". - ''."\n". - ''."\n". - ''."\n". - ''."\n". - ''."\n". - ''."\n". + ''."\n". + ''."\n". + ''."\n". + ''."\n". + ''."\n". + &build_section_inputs(). + ''."\n". + ''."\n". ''."\n"); + ' value="'.($env{'form.NTSTU'} ne '' ? $env{'form.NTSTU'} : $total+1).'" />'."\n"); + if ($env{'form.handgrade'} eq 'yes') { + $request->print(''."\n". + ''."\n". + ''."\n". + ''."\n". + ''."\n". + ''."\n". + ''."\n"); + foreach my $partid (&Apache::loncommon::get_env_multiple('form.vPart')) { + $request->print(''."\n"); + } + } my ($cts,$prnmsg) = (1,''); - while ($cts <= $ENV{'form.savemsgN'}) { + while ($cts <= $env{'form.savemsgN'}) { $prnmsg.=''."\n"; + '" />'."\n". + ''."\n"; $cts++; } $request->print($prnmsg); - if ($ENV{'form.handgrade'} eq 'yes' && $ENV{'form.showgrading'} eq 'yes') { + if ($env{'form.handgrade'} eq 'yes' && $env{'form.showgrading'} eq 'yes') { # # Print out the keyword options line # $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); } } - if ($ENV{'form.vProb'} eq 'all') { - $request->print('


') if ($counter > 0); - $request->print(&show_problem($request,$symb,$uname,$udom,1,1)); +# 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"); + + if ($env{'form.vProb'} eq 'all' or $env{'form.vAns'} eq 'all') { + my $mode; + if ($env{'form.vProb'} eq 'all' && $env{'form.vAns'} eq 'all') { + $mode='both'; + } elsif ($env{'form.vProb'} eq 'all' ) { + $mode='text'; + } elsif ($env{'form.vAns'} eq 'all') { + $mode='answer'; + } + &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) = &response_type($url); + + my %record = &Apache::lonnet::restore($symb,$env{'request.course.id'},$udom,$uname); + my ($partlist,$handgrade,$responseType) = &response_type($symb); # Display student info $request->print(($counter == 0 ? '' : '
')); - my $result='
'."\n". - '\n"; - if ($$timestamp eq '') { - $lastsubonly.='
'."\n"; - - $result.='Fullname: '.$ENV{'form.fullname'}. - '   Username: '.$uname. - ($ENV{'user.domain'} eq $udom ? '' : ' ('.$udom.')').'
'."\n"; + my $result='
'; + + $result.='
'; + $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); - 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"; - $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 $fullname; + my $col_fullnames = []; + if ($env{'form.handgrade'} eq 'yes') { + (my $sub_result,$fullname,$col_fullnames)= + &check_collaborators($symb,$uname,$udom,\%record,$handgrade, + $counter); + $result.=$sub_result; } $request->print($result."\n"); - + $request->print('
'."\n"); # print student answer/submission # Options are (1) Handgaded 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)$/) { - if ($ENV{'form.'.$uname.':'.$udom.':submitted_by'}) { - my $submitby=''. - 'Collaborative submission by: '. - ''. - $$fullname{$ENV{'form.'.$uname.':'.$udom.':submitted_by'}}.''; - $request->print($submitby); - } else { - my ($string,$timestamp)= &get_last_submission (\%record); - my $lastsubonly=''. - ($$timestamp eq '' ? '' : 'Date Submitted: '. - $$timestamp)."
'.$$string[0]; - } else { - for my $part (sort keys(%$handgrade)) { - my ($responsetype,$foo) = split(/:/,$$handgrade{$part}); - my ($partid,$respid) = split(/_/,$part); - if (!exists($record{'resource.'.$partid.'.'.$respid.'.submission'})) { - $lastsubonly.='
Part '. - $partid.' ( ID '.$respid. - ' )   Nothing submitted

'; - } else { - foreach (@$string) { - my ($partid,$respid) = /^resource\.(\w+)\.(\w+)\.submission/; - if ($part eq ($partid.'_'.$respid)) { - my ($ressub,$subval) = split(/:/,$_,2); - # Similarity check - my $similar=''; - my ($oname,$odom,$ocrsid,$oessay,$osim)=&most_similar($uname,$udom,$subval); - if ($osim) { - $osim=int($osim*100.0); - $similar='

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

'. - &keywords_highlight($oessay).'

'; - } - $lastsubonly.='
Part '. - $partid.' ( ID '.$respid. - ' )   '. - ($record{"resource.$partid.$respid.uploadedurl"}? - ' File uploaded by student '. - 'Like all files provided by users, '. - 'this file may contain virusses
':''). - 'Submitted Answer: '.($responsetype =~ /^(essay|option)$/ ? - '
' : ''). - &cleanRecord(&keywords_highlight($subval),$responsetype). - ($responsetype =~ /^(essay|option)$/ ? '

' : - '

').$similar."\n" - if ($ENV{'form.lastSub'} eq 'lastonly' || - ($ENV{'form.lastSub'} eq 'hdgrade' && - $$handgrade{$part} =~ /:yes$/)); + if ($env{'form.lastSub'} =~ /^(lastonly|hdgrade)$/) { + my ($string,$timestamp)= &get_last_submission(\%record); + + my $lastsubonly; + + if ($$timestamp eq '') { + $lastsubonly.='
'.$$string[0].'
'; + } else { + $lastsubonly = '
Date Submitted: '.$$timestamp."\n"; + + my %seenparts; + 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; + my $submitby='Part: '.$display_part. + ' Collaborative submission by: '. + ''. + $$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.="\n".'
Part: '. + $display_part.' ( ID '.$respid. + ' )   '. + ''.&mt('Nothing submitted - no attempts').'

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

". + &mt('Essay is [_1]% similar to an essay by [_2] ([_3]:[_4]) in course [_5] (course id [_6]:[_7])', + $osim, + &Apache::loncommon::plainname($oname,$odom), + $oname,$odom, + $old_course_desc{'description'}, + $old_course_desc{'num'}, + $old_course_desc{'domain'}). + '

'. + &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[0].'_'.$$part[1]} eq 'yes')) { + my $display_part=&get_display_part($partid,$symb); + $lastsubonly.='
Part: '. + $display_part.' ( ID '.$respid. + ' )   '; + my $files=&get_submitted_files($udom,$uname,$partid,$respid,\%record); + if (@$files) { + $lastsubonly.='
'.&mt('Like all files provided by users, this file may contain virusses').'
'; + my $file_counter = 0; + foreach my $file (@$files) { + $file_counter++; + &Apache::lonnet::allowuploaded('/adm/grades',$file); + $lastsubonly.='
'.$file.''; } + $lastsubonly.='
'; } + $lastsubonly.=''.&mt('Submitted Answer:').' '. + &cleanRecord($subval,$responsetype,$symb,$partid, + $respid,\%record,$order); + if ($similar) {$lastsubonly.="

$similar\n";} + $lastsubonly.='
'; } } } - $lastsubonly.='
'."\n"; - $request->print($lastsubonly); + $lastsubonly.=''."\n"; } - } else { + $request->print($lastsubonly); + } elsif ($env{'form.lastSub'} eq 'datesub') { + my (undef,$responseType,undef,$parts) = &showResourceInfo($symb); + $request->print(&displaySubByDates($symb,\%record,$parts,$responseType,$checkIcon,$uname,$udom)); + } elsif ($env{'form.lastSub'} =~ /^(last|all)$/) { $request->print(&Apache::loncommon::get_previous_attempt($symb,$uname,$udom, - $ENV{'request.course.id'}, + $env{'request.course.id'}, $last,'.submission', 'Apache::grades::keywords_highlight')); } $request->print(''."\n"); - # return if view submission with no grading option - if ($ENV{'form.showgrading'} eq '' || (!&canmodify($usec))) { + if ($env{'form.showgrading'} eq '' || (!&canmodify($usec))) { my $toGrade.='  '."\n" if (&canmodify($usec)); - $toGrade.='
'."\n"; - $toGrade.=&show_grading_menu_form($symb,$url) - if (($ENV{'form.command'} eq 'submission') || - ($ENV{'form.command'} eq 'processGroup' && $counter == $total)); - $request = print($toGrade); + .$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); + } + $request->print($toGrade); return; + } else { + $request->print('
'."\n"); } # essay grading message center - if ($ENV{'form.handgrade'} eq 'yes') { - my ($lastname,$givenn) = split(/,/,$ENV{'form.fullname'}); + 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". + $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; - for (sort keys(%$handgrade)) { - my ($partid,$respid) = split(/_/); + my @gradePartRespid; + my @part_response_id = &flatten_responseType($responseType); + $request->print('
'. + + '
'. + &mt('Assign Grades').'
'. + '
'); + 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; - + 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('
'); + + $request->print(''); + $result=''."\n"; + $result.=''."\n" if ($counter == 0); my $ctr = 0; while ($ctr < scalar(@partlist)) { $result.=''."\n"; $ctr++; } - $request->print($result.''."\n"); + $request->print($result.''."\n"); + +# Done with printing info for one student + + $request->print('');#LC_grade_show_user_body + $request->print('');#LC_grade_show_user + # print end of form if ($counter == $total) { my $endform='
'."\n"; - $endform.='  '."\n"; + $total.','.scalar(@partlist).');" target="_self" />  '."\n"; my $ntstu =''."\n"; - my $nsel = ($ENV{'form.NTSTU'} ne '' ? $ENV{'form.NTSTU'} : '1'); - $ntstu =~ s/
'; - $endform.=&show_grading_menu_form($symb,$url); + my $nsel = ($env{'form.NTSTU'} ne '' ? $env{'form.NTSTU'} : '1'); + $ntstu =~ s/\n"; } - closedir(DIR); $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'}; + my @names=&Apache::lonnet::getkeys('CODEs',$cdom,$cnum); + 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=' + + + + + '; 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) = @_; - my ($symb,$url)=&get_symb_and_url($r); + my ($r,$file2grade) = @_; + my ($symb)=&get_symb($r); 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 $file_selector=&scantron_uploads(); + my $sequence_selector=&getSequenceDropDown($symb); + 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; - $result.= < - - $default_form_data - - - - -
- - - - - - - - + + $ssi_error = 0; + + # 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().' + + '.&Apache::loncommon::end_data_table_header_row().' + '.&Apache::loncommon::start_data_table_row().' + + '.&Apache::loncommon::end_data_table_row().' + '.&Apache::loncommon::start_data_table_row().' + + '.&Apache::loncommon::end_data_table_row().' + '.&Apache::loncommon::start_data_table_row().' + + '.&Apache::loncommon::end_data_table_row().' + '.&Apache::loncommon::start_data_table_row().' + + '.&Apache::loncommon::end_data_table_row().' + '.&Apache::loncommon::start_data_table_row().' + + '.&Apache::loncommon::end_data_table_row().' + '.&Apache::loncommon::start_data_table_row().' + - - + '.&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); + + if (&Apache::lonnet::allowed('usc',$env{'request.role.domain'}) || + &Apache::lonnet::allowed('usc',$env{'request.course.id'})) { + + # Chunk of form to prompt for a scantron file upload. + + $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 file location and which Folder/Sequence to grade -
- Sequence to grade: $sequence_selector -
+  '.&mt('Specify file and which Folder/Sequence to grade').' + '.&mt('Sequence to grade:').' '.$sequence_selector.' '.&mt('Filename of scoring office file:').' '.$file_selector.' '.&mt('Format of data file:').' '.$format_selector.' '.&mt('Saved CODEs to validate against:').' '.$CODE_selector.' '.&mt('Each CODE is only to be used once:').' '.$CODE_unique.' '.&mt('Options:').' - Filename of scoring office file: $file_selector +
+
+
+ + +  '.&mt('Specify a Scantron data file to upload.').' + - Format of data file: $format_selector -
-
- - -$grading_menu_button -SCANTRONFORM +'); + my $default_form_data=&defaultFormData(&get_symb($r,1)); + my $cdom= $env{'course.'.$env{'request.course.id'}.'.domain'}; + my $cnum= $env{'course.'.$env{'request.course.id'}.'.num'}; + $r->print(' + - return $result; +
+ '.$default_form_data.' + + + + '.&mt('File to upload: [_1]','').' +
+ +
+'); + + $r->print(' + + '.&Apache::loncommon::end_data_table_row().' + '.&Apache::loncommon::end_data_table().' +'); + } + + # 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 scantron 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". + ''."\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 ID number starts + IDlength - length of the student 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; - foreach my $line (<$fh>) { + #FIXME probably should move to XML it has already gotten a bit much now + foreach my $line (@lines) { my ($name,$descrip)=split(/:/,$line); if ($name ne $which ) { next; } chomp($line); @@ -3153,14 +5258,39 @@ 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]; + $config{'PaperIDlength'}=$config[12]; + $config{'FirstName'}=$config[13]; + $config{'FirstNamelength'}=$config[14]; + $config{'LastName'}=$config[15]; + $config{'LastNamelength'}=$config[16]; last; } return %config; } +=pod + +=item username_to_idmap + + creates a hash keyed by student 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; @@ -3171,81 +5301,2256 @@ 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 ID number + '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') { + if (length($args->{'newid'}) > $$scantron_config{'IDlength'}) { + return ($line,1,'New value too large'); + } + if (length($args->{'newid'}) < $$scantron_config{'IDlength'}) { + $args->{'newid'}=sprintf('%-'.$$scantron_config{'IDlength'}.'s', + $args->{'newid'}); + } + substr($line,$$scantron_config{'IDstart'}-1, + $$scantron_config{'IDlength'})=$args->{'newid'}; + if ($args->{'newid'}=~/^\s*$/) { + &scan_data($scan_data,"$whichline.user", + $args->{'username'}.':'.$args->{'domain'}); + } + } elsif ($field eq 'CODE') { + if ($args->{'CODE_ignore_dup'}) { + &scan_data($scan_data,"$whichline.CODE_ignore_dup",'1'); + } + &scan_data($scan_data,"$whichline.useCODE",'1'); + if ($args->{'CODE'} ne 'use_unfound') { + if (length($args->{'CODE'}) > $$scantron_config{'CODElength'}) { + return ($line,1,'New CODE value too large'); + } + if (length($args->{'CODE'}) < $$scantron_config{'CODElength'}) { + $args->{'CODE'}=sprintf('%-'.$$scantron_config{'CODElength'}.'s',$args->{'CODE'}); + } + substr($line,$$scantron_config{'CODEstart'}-1, + $$scantron_config{'CODElength'})=$args->{'CODE'}; + } + } elsif ($field eq 'answer') { + my $length=$scantron_config->{'Qlength'}; + my $off=$scantron_config->{'Qoff'}; + my $on=$scantron_config->{'Qon'}; + my $answer=${off}x$length; + if ($args->{'response'} eq 'none') { + &scan_data($scan_data, + "$whichline.no_bubble.".$args->{'questionnum'},'1'); + } else { + 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->{'questionnum'},undef,'1'); + } + my $where=$length*($args->{'question'}-1)+$scantron_config->{'Qstart'}; + substr($line,$where-1,$length)=$answer; + } + 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'}; + if (defined($value)) { + $scan_data->{$filename.'_'.$key} = $value; + } + if ($delete) { delete($scan_data->{$filename.'_'.$key}); } + 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 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,$scantron_config)=@_; + 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) { - $record{'scantron.CODE'}=substr($data,$$scantron_config{'CODEstart'}-1, + my $questions=substr($line,$$scantron_config{'Qstart'}-1); # 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'}); + if (&scan_data($scan_data,"$whichline.useCODE")) { + $record{'scantron.useCODE'}=1; + } + if (&scan_data($scan_data,"$whichline.CODE_ignore_dup")) { + $record{'scantron.CODE_ignore_dup'}=1; + } } else { #FIXME interpret first N questions } } $record{'scantron.ID'}=substr($data,$$scantron_config{'IDstart'}-1, $$scantron_config{'IDlength'}); + $record{'scantron.PaperID'}= + substr($data,$$scantron_config{'PaperID'}-1, + $$scantron_config{'PaperIDlength'}); + $record{'scantron.FirstName'}= + substr($data,$$scantron_config{'FirstName'}-1, + $$scantron_config{'FirstNamelength'}); + $record{'scantron.LastName'}= + substr($data,$$scantron_config{'LastName'}-1, + $$scantron_config{'LastNamelength'}); + 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; } - my (@array)=split(/$$scantron_config{'Qon'}/,$currentquest); - if (scalar(@array) gt 2) { - #FIXME do something intelligent with double bubbles - Apache->request->print("
Wha!!!
".scalar(@array).
-				   '-'.$currentquest.'-'.$questnum.'

'); - } - if (length($array[0]) eq $$scantron_config{'Qlength'}) { - $record{"scantron.$questnum.answer"}=''; - } else { - $record{"scantron.$questnum.answer"}=$alphabet[length($array[0])]; - } + 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, + {'line' => $scanline, 'emsg' => $errormessage, + 'ecode' => $errorcode } + ); } +=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,$idmap)=@_; + my ($scantron_record,$scan_data,$idmap,$line)=@_; my $scanID=$$scantron_record{'scantron.ID'}; + if ($scanID =~ /^\s*$/) { + return &scan_data($scan_data,"$line.user"); + } foreach my $id (keys(%$idmap)) { - Apache->request->print('
checking studnet -'.$id.'- againt -'.$scanID.'- 
'); - if (lc($id) eq lc($scanID)) { Apache->request->print('success');return $$idmap{$id}; } + if (lc($id) eq lc($scanID)) { + return $$idmap{$id}; + } } 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)=@_; - 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; } -sub scantron_process_students { +=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 (undef,undef,$sequence)=split(/___/,$ENV{'form.selectpage'}); - my ($symb,$url)=&get_symb_and_url($r); + my %scantron_config=&get_scantron_config($env{'form.scantron_format'}); + my ($scanlines,$scan_data)=&scantron_getfile(); + my $classlist=&Apache::loncoursedata::get_classlist(); + my $which=$env{'form.scantron_line'}; + my $line=&scantron_get_line($scanlines,$scan_data,$which); + my ($skip,$err,$errmsg); + if ($env{'form.scantron_skip_record'}) { + $skip=1; + } elsif ($env{'form.scantron_corrections'} =~ /^(duplicate|incorrect)ID$/) { + my $newstudent=$env{'form.scantron_username'}.':'. + $env{'form.scantron_domain'}; + my $newid=$classlist->{$newstudent}->[&Apache::loncoursedata::CL_ID]; + ($line,$err,$errmsg)= + &scantron_fixup_scanline(\%scantron_config,$scan_data,$line,$which, + 'ID',{'newid'=>$newid, + 'username'=>$env{'form.scantron_username'}, + 'domain'=>$env{'form.scantron_domain'}}); + } elsif ($env{'form.scantron_corrections'} =~ /^(duplicate|incorrect)CODE$/) { + my $resolution=$env{'form.scantron_CODE_resolution'}; + my $newCODE; + my %args; + if ($resolution eq 'use_unfound') { + $newCODE='use_unfound'; + } elsif ($resolution eq 'use_found') { + $newCODE=$env{'form.scantron_CODE_selectedvalue'}; + } elsif ($resolution eq 'use_typed') { + $newCODE=$env{'form.scantron_CODE_newvalue'}; + } elsif ($resolution =~ /^use_closest_(\d+)/) { + $newCODE=$env{"form.scantron_CODE_closest_$1"}; + } + if ($env{'form.scantron_corrections'} eq 'duplicateCODE') { + $args{'CODE_ignore_dup'}=1; + } + $args{'CODE'}=$newCODE; + ($line,$err,$errmsg)= + &scantron_fixup_scanline(\%scantron_config,$scan_data,$line,$which, + 'CODE',\%args); + } elsif ($env{'form.scantron_corrections'} =~ /^(missing|double)bubble$/) { + foreach my $question (split(',',$env{'form.scantron_questions'})) { + ($line,$err,$errmsg)= + &scantron_fixup_scanline(\%scantron_config,$scan_data,$line, + $which,'answer', + { 'question'=>$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:"); + } 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); +} + +=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')); + 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 ($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}) && $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; + for (my $i=0;$i<=$scanlines->{'count'};$i++) { + if ($scanlines->{'skipped'}[$i]) { + $to_remember{$i}=1; + } + } + + &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(&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'}); + my %scantron_config=&get_scantron_config($env{'form.scantron_format'}); + my $CODElist; + if ($scantron_config{'CODElocation'} && + $scantron_config{'CODEstart'} && + $scantron_config{'CODElength'}) { + $CODElist=$env{'form.scantron_CODElist'}; + if ($env{'form.scantron_CODElist'} eq '') { $CODElist='None'; } + $CODElist= + ''.&mt('List of CODES to validate against:').''. + $env{'form.scantron_CODElist'}.''; + } + return (' +

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

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

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

+ +
+'); +} + +=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)=&get_symb($r); 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("

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

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

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

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

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

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

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

'); + } + } else { + my $warning=&scantron_warning_screen('Grading: Validate Records'); + $r->print(' +'.$warning.' + + +'); + } + $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)=&get_symb($r); + if (!$symb) {return '';} + my $default_form_data=&defaultFormData($symb); + + # do the detection of only doing skipped records first befroe we delete + # 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(); + $env{'form.scantron_options_redo'}='redo_skipped_ready'; + } + + if ($env{'form.scantron_options_ignore'} eq 'ignore_corrections') { + &check_for_error($r,&scantron_remove_file('corrected')); + &check_for_error($r,&scantron_remove_file('skipped')); + &check_for_error($r,&scantron_remove_scan_data()); + $env{'form.scantron_options_ignore'}='done'; + } + + if ($env{'form.scantron_corrections'}) { + &scantron_process_corrections($r); + } + $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(); + my $result=&scantron_form_start($max_bubble).$default_form_data; + $r->print($result); + + my @validate_phases=( 'sequence', + 'ID', + 'CODE', + 'doublebubble', + 'missingbubbles'); + if (!$env{'form.validatepass'}) { + $env{'form.validatepass'} = 0; + } + my $currentphase=$env{'form.validatepass'}; + + + my $stop=0; + while (!$stop && $currentphase < scalar(@validate_phases)) { + $r->print(&mt('Validating '.$validate_phases[$currentphase]).'
'); + $r->rflush(); + my $which="scantron_validate_".$validate_phases[$currentphase]; + { + no strict 'refs'; + ($stop,$currentphase)=&$which($r,$currentphase); + } + } + if (!$stop) { + my $warning=&scantron_warning_screen('Start Grading'); + $r->print(&mt('Validation process complete.').'
+'.$warning.' + + +'); + + } else { + $r->print(''); + $r->print(""); + } + if ($stop) { + 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)); + 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'}; + my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'}; + my $file='scantron_'; + if ($which eq 'corrected' || $which eq 'skipped') { + $file.=$which.'_'; + } else { + return 'refused'; + } + $file.=$env{'form.scantron_selectfile'}; + 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'}; + my @keys=&Apache::lonnet::getkeys('nohist_scantrondata',$cdom,$cname); + my @todelete; + my $filename=$env{'form.scantron_selectfile'}; + foreach my $key (@keys) { + if ($key=~/^\Q$filename\E_/) { + if ($env{'form.scantron_options_redo'} eq 'redo_skipped_ready' && + $key=~/remember_skipping/) { + next; + } + push(@todelete,$key); + } + } + my $result; + if (@todelete) { + $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'}; + my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'}; + my $lines; + $lines=&Apache::lonnet::getfile('/uploaded/'.$cdom.'/'.$cname.'/'. + 'scantron_orig_'.$env{'form.scantron_selectfile'}); + my %scanlines; + $scanlines{'orig'}=[(split("\n",$lines,-1))]; + my $temp=$scanlines{'orig'}; + $scanlines{'count'}=$#$temp; + + $lines=&Apache::lonnet::getfile('/uploaded/'.$cdom.'/'.$cname.'/'. + 'scantron_corrected_'.$env{'form.scantron_selectfile'}); + if ($lines eq '-1') { + $scanlines{'corrected'}=[]; + } else { + $scanlines{'corrected'}=[(split("\n",$lines,-1))]; + } + $lines=&Apache::lonnet::getfile('/uploaded/'.$cdom.'/'.$cname.'/'. + 'scantron_skipped_'.$env{'form.scantron_selectfile'}); + if ($lines eq '-1') { + $scanlines{'skipped'}=[]; + } else { + $scanlines{'skipped'}=[(split("\n",$lines,-1))]; + } + my @tmp=&Apache::lonnet::dump('nohist_scantrondata',$cdom,$cname); + if ($tmp[0] =~ /^(error:|no_such_host)/) { @tmp=(); } + my %scan_data = @tmp; + 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'}; + $env{'form.sillywaytopassafilearound'}=$contents; + &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 + my $cname=$env{'course.'.$env{'request.course.id'}.'.num'}; + my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'}; + if ($scanlines) { + my $prefix='scantron_'; +# no need to update orig, shouldn't change +# &lonnet_putfile(join("\n",@{$scanlines->{'orig'}}),$prefix.'orig_'. +# $env{'form.scantron_selectfile'}); + &lonnet_putfile(join("\n",@{$scanlines->{'corrected'}}), + $prefix.'corrected_'. + $env{'form.scantron_selectfile'}); + &lonnet_putfile(join("\n",@{$scanlines->{'skipped'}}), + $prefix.'skipped_'. + $env{'form.scantron_selectfile'}); + } + &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($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; + for (my $i=0;$i<=$scanlines->{'count'};$i++) { + my $line=&scantron_get_line($scanlines,$scan_data,$i); + if ($line=~/^[\s\cz]*$/) { next; } + $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; + &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 - my %scantron_config=&get_scantron_config($ENV{'form.scantron_format'}); - my $scanlines=Apache::File->new($Apache::lonnet::perlvar{'lonScansDir'}."/$ENV{'form.scantron_selectfile'}"); - my @scanlines=<$scanlines>; + 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(); + 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); +} + +=pod + +=item scantron_validate_ID + + Validates all scanlines in the selected file to not have any + invalid or underspecified student IDs + +=cut + +sub scantron_validate_ID { + my ($r,$currentphase) = @_; + + #get student info my $classlist=&Apache::loncoursedata::get_classlist(); my %idmap=&username_to_idmap($classlist); - my $navmap=Apache::lonnavmaps::navmap->new($ENV{'request.course.fn'}.'.db',$ENV{'request.course.fn'}.'_parms.db',1, 1); + + #get scantron line setup + my %scantron_config=&get_scantron_config($env{'form.scantron_format'}); + my ($scanlines,$scan_data)=&scantron_getfile(); + + &scantron_get_maxbubble(); # parse needs the bubble_lines.. array. + + my %found=('ids'=>{},'usernames'=>{}); + for (my $i=0;$i<=$scanlines->{'count'};$i++) { + my $line=&scantron_get_line($scanlines,$scan_data,$i); + if ($line=~/^[\s\cz]*$/) { next; } + my $scan_record=&scantron_parse_scanline($line,$i,\%scantron_config, + $scan_data); + my $id=$$scan_record{'scantron.ID'}; + my $found; + foreach my $checkid (keys(%idmap)) { + if (lc($checkid) eq lc($id)) { $found=$checkid;last; } + } + if ($found) { + my $username=$idmap{$found}; + if ($found{'ids'}{$found}) { + &scantron_get_correction($r,$i,$scan_record,\%scantron_config, + $line,'duplicateID',$found); + return(1,$currentphase); + } elsif ($found{'usernames'}{$username}) { + &scantron_get_correction($r,$i,$scan_record,\%scantron_config, + $line,'duplicateID',$username); + return(1,$currentphase); + } + #FIXME store away line we previously saw the ID on to use above + $found{'ids'}{$found}++; + $found{'usernames'}{$username}++; + } else { + if ($id =~ /^\s*$/) { + my $username=&scan_data($scan_data,"$i.user"); + if (defined($username) && $found{'usernames'}{$username}) { + &scantron_get_correction($r,$i,$scan_record, + \%scantron_config, + $line,'duplicateID',$username); + return(1,$currentphase); + } elsif (!defined($username)) { + &scantron_get_correction($r,$i,$scan_record, + \%scantron_config, + $line,'incorrectID'); + return(1,$currentphase); + } + $found{'usernames'}{$username}++; + } else { + &scantron_get_correction($r,$i,$scan_record,\%scantron_config, + $line,'incorrectID'); + return(1,$currentphase); + } + } + } + + return (0,$currentphase+1); +} + +=pod + +=item scantron_get_correction + + Builds the interface screen to interact with the operator to fix a + specific error condition in a specific scanline + + Arguments: + $r - Apache request object + $i - number of the current scanline + $scan_record - hash ref as returned from &scantron_parse_scanline() + $scan_config - hash ref as returned from &get_scantron_config() + $line - full contents of the current scanline + $error - error condition, valid values are + 'incorrectCODE', 'duplicateCODE', + 'doublebubble', 'missingbubble', + 'duplicateID', 'incorrectID' + $arg - extra information needed + For errors: + - duplicateID - paper number that this studentID was seen before on + - duplicateCODE - array ref of the paper numbers this CODE was + seen on before + - incorrectCODE - current incorrect CODE + - doublebubble - array ref of the bubble lines that have double + bubble errors + - missingbubble - array ref of the bubble lines that have missing + bubble errors + +=cut + +sub scantron_get_correction { + my ($r,$i,$scan_record,$scan_config,$line,$error,$arg)=@_; +#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 + + 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("

".&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("

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

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

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

\n"); + } + $r->print($message); + $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 + $r->print(&Apache::loncommon::selectstudent_link('scantronupload', + 'scantron_username','scantron_domain')); + $r->print(": "); + $r->print("\n@". + &Apache::loncommon::select_dom_form($env{'request.role.domain'},'scantron_domain')); + + $r->print('
  • '); + } elsif ($error =~ /CODE$/) { + if ($error eq 'incorrectCODE') { + $r->print("

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

    \n"); + } elsif ($error eq 'duplicateCODE') { + $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("

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

    ".&mt("How should I handle this?")."
    \n"); + $r->print("\n
    "); + my $i=0; + if ($error eq 'incorrectCODE' + && $$scan_record{'scantron.CODE'}=~/\S/ ) { + my ($max,$closest)=&scantron_get_closely_matching_CODEs($arg,$$scan_record{'scantron.CODE'}); + if ($closest > 0) { + foreach my $testcode (@{$closest}) { + my $checked=''; + if (!$i) { $checked=' checked="checked" '; } + $r->print(" + + "); + $r->print("\n
    "); + $i++; + } + } + } + if ($$scan_record{'scantron.CODE'}=~/\S/ ) { + my $checked; if (!$i) { $checked=' checked="checked" '; } + $r->print(" + "); + $r->print("\n
    "); + } + + $r->print(< +function change_radio(field) { + var slct=document.scantronupload.scantron_CODE_resolution; + var i; + for (i=0;i +ENDSCRIPT + my $href="/adm/pickcode?". + "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("

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

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

    "); + foreach my $question (@{$arg}) { + 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("

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

    \n"); + $r->print($message); + $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(''); + foreach my $question (@{$arg}) { + 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 = (< +function verify_bubble_radio(form) { + var ansnumArray = new Array ("$ansnumstr"); + var need_bubble_count = 0; + for (var i=0; i 1) { + var bubble_picked = 0; + for (var j=0; j +ENDSCRIPT + return $output; +} + +=pod + +=item questions_to_line_list + +Converts a list of questions into a string of comma separated +line numbers in the answer sheet used by the questions. This is +used to fill in the scantron_questions form field. + + Arguments: + questions - Reference to an array of questions. + +=cut + + +sub questions_to_line_list { + my ($questions) = @_; + my @lines; + + foreach my $item (@{$questions}) { + my $question = $item; + my ($first,$count,$last); + if ($item =~ /^(\d+)\.(\d+)$/) { + $question = $1; + my $subquestion = $2; + $first = $first_bubble_line{$question-1} + 1; + my @subans = split(/,/,$subdivided_bubble_lines{$question-1}); + my $subcount = 1; + while ($subcount<$subquestion) { + $first += $subans[$subcount-1]; + $subcount ++; + } + $count = $subans[$subquestion-1]; + } else { + $first = $first_bubble_line{$question-1} + 1; + $count = $bubble_lines_per_response{$question-1}; + } + $last = $first+$count-1; + push(@lines, ($first..$last)); + } + return join(',', @lines); +} + +=pod + +=item prompt_for_corrections + +Prompts for a potentially multiline correction to the +user's bubbling (factors out common code from scantron_get_correction +for multi and missing bubble cases). + + Arguments: + $r - Apache request object. + $question - The question number to prompt for. + $scan_config - The scantron file configuration hash. + $scan_record - Reference to the hash that has the the parsed scanlines. + $error - Type of error + + Implicit inputs: + %bubble_lines_per_response - Starting line numbers for each question. + Numbered from 0 (but question numbers are from + 1. + %first_bubble_line - Starting bubble line for each question. + %subdivided_bubble_lines - optionresponse, matchresponse and rankresponse + type problems render as separate sub-questions, + in exam mode. This hash contains a + comma-separated list of the lines per + sub-question. + %responsetype_per_response - essayresponse, formularesponse, + stringresponse, imageresponse, reactionresponse, + and organicresponse type problem parts can have + multiple lines per response if the weight + assigned exceeds 10. In this case, only + one bubble per line is permitted, but more + than one line might contain bubbles, e.g. + bubbling of: line 1 - J, line 2 - J, + line 3 - B would assign 22 points. + +=cut + +sub prompt_for_corrections { + my ($r, $question, $scan_config, $scan_record, $error) = @_; + my ($current_line,$lines); + my @linenums; + my $questionnum = $question; + if ($question =~ /^(\d+)\.(\d+)$/) { + $question = $1; + $current_line = $first_bubble_line{$question-1} + 1 ; + my $subquestion = $2; + my @subans = split(/,/,$subdivided_bubble_lines{$question-1}); + my $subcount = 1; + while ($subcount<$subquestion) { + $current_line += $subans[$subcount-1]; + $subcount ++; + } + $lines = $subans[$subquestion-1]; + } else { + $current_line = $first_bubble_line{$question-1} + 1 ; + $lines = $bubble_lines_per_response{$question-1}; + } + if ($lines > 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 scantron sheets.",$lines).'

    '.&mt('A non-zero score can be assigned to the student during scantron 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,$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(&Apache::loncommon::start_data_table(). + &Apache::loncommon::start_data_table_row()); + $r->print(''.$line.''); + for (my $i=0;$i<$max+1;$i++) { + $r->print("\n".''); + if ($selected[0] eq $alphabet[$i]) { $r->print('X'); shift(@selected) } + else { $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("\n". + '"); + } + 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); + my @orig=split(//,$orig); + my $same=0; + for (my $i=0;$i{'count'};$i++) { + my $line=&scantron_get_line($scanlines,$scan_data,$i); + if ($line=~/^[\s\cz]*$/) { next; } + my $scan_record=&scantron_parse_scanline($line,$i,\%scantron_config, + $scan_data); + my $CODE=$$scan_record{'scantron.CODE'}; + my $error=0; + if (!&Apache::lonnet::validCODE($CODE)) { + &scantron_get_correction($r,$i,$scan_record, + \%scantron_config, + $line,'incorrectCODE',\%allcodes); + return(1,$currentphase); + } + if (%allcodes && !exists($allcodes{$CODE}) + && !$$scan_record{'scantron.useCODE'}) { + &scantron_get_correction($r,$i,$scan_record, + \%scantron_config, + $line,'incorrectCODE',\%allcodes); + return(1,$currentphase); + } + if (exists($usedCODEs{$CODE}) + && $env{'form.scantron_CODEunique'} eq 'yes' + && !$$scan_record{'scantron.CODE_ignore_dup'}) { + &scantron_get_correction($r,$i,$scan_record, + \%scantron_config, + $line,'duplicateCODE',$usedCODEs{$CODE}); + return(1,$currentphase); + } + 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 + my $classlist=&Apache::loncoursedata::get_classlist(); + my %idmap=&username_to_idmap($classlist); + + #get scantron line setup + my %scantron_config=&get_scantron_config($env{'form.scantron_format'}); + my ($scanlines,$scan_data)=&scantron_getfile(); + &scantron_get_maxbubble(); # parse needs the bubble line array. + + for (my $i=0;$i<=$scanlines->{'count'};$i++) { + my $line=&scantron_get_line($scanlines,$scan_data,$i); + if ($line=~/^[\s\cz]*$/) { next; } + my $scan_record=&scantron_parse_scanline($line,$i,\%scantron_config, + $scan_data); + if (!defined($$scan_record{'scantron.doubleerror'})) { next; } + &scantron_get_correction($r,$i,$scan_record,\%scantron_config,$line, + 'doublebubble', + $$scan_record{'scantron.doubleerror'}); + return (1,$currentphase); + } + return (0,$currentphase+1); +} + +=pod + +=item scantron_get_maxbubble + + Returns the maximum number of bubble lines that are expected to + occur. Does this by walking the selected sequence rendering the + resource and then checking &Apache::lonxml::get_problem_counter() + for what the current value of the problem counter is. + + Caches the results to $env{'form.scantron_maxbubble'}, + $env{'form.scantron.bubble_lines.n'}, + $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. + +=cut + +sub scantron_get_maxbubble { + if (defined($env{'form.scantron_maxbubble'}) && + $env{'form.scantron_maxbubble'}) { + &restore_bubble_lines(); + return $env{'form.scantron_maxbubble'}; + } + + my (undef, undef, $sequence) = + &Apache::lonnet::decode_symb($env{'form.selectpage'}); + + my $navmap=Apache::lonnavmaps::navmap->new(); my $map=$navmap->getResourceByUrl($sequence); my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0); - $r->print("geto ".scalar(@resources)."
    "); + + &Apache::lonxml::clear_problem_counter(); + + my $uname = $env{'form.student'}; + my $udom = $env{'form.userdom'}; + 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 $symb = $resource->symb(); + + my (@parts,@allparts,@possible_parts); + + # Need to retrieve part IDs and response IDs because essayresponse, + # reactionresponse and organicresponse items are not included in + # $analysis{'parts'} from lonnet::ssi. + if (ref($resource->parts()) eq 'ARRAY') { + foreach my $part (@{$resource->parts()}) { + if (!&Apache::loncommon::check_if_partid_hidden($part,$symb,$udom,$uname)) { + my @resp_ids = $resource->responseIds($part); + foreach my $id (@resp_ids) { + my $part_id = $part.'.'.$id; + push(@possible_parts,$part_id); + } + } + } + } + + my $result=&ssi_with_retries($resource->src(), $ssi_retries, + ('symb' => $symb, + 'grade_target' => 'analyze', + 'grade_courseid' => $cid, + 'grade_domain' => $udom, + 'grade_username' => $uname)); + my (undef, $an) = + split(/_HASH_REF__/,$result, 2); + + my %analysis = &Apache::lonnet::str2hash($an); + + if (ref($analysis{'parts'}) eq 'ARRAY') { + foreach my $part (@{$analysis{'parts'}}) { + my ($id,$respid) = split(/\./,$part); + if (!&Apache::loncommon::check_if_partid_hidden($id,$symb,$udom,$uname)) { + push(@parts,$part); + } + } + } + # Add part_ids for any essayresponse, reactionresponse or + # organicresponse items. + foreach my $part_id (@possible_parts) { + if (grep(/^\Q$part_id\E$/,@parts)) { + push(@allparts,$part_id); + } else { + if (($analysis{$part_id.'.type'} eq 'essayresponse') || + ($analysis{$part_id.'.type'} eq 'reactionresponse') || + ($analysis{$part_id.'.type'} eq 'organicresponse')) { + push(@allparts,$part_id); + } + } + } + + foreach my $part_id (@allparts) { + 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\.'); + + &save_bubble_lines(); + $env{'form.scantron_maxbubble'} = + $total_lines; + return $env{'form.scantron_maxbubble'}; +} + +=pod + +=item scantron_validate_missingbubbles + + Validates all scanlines in the selected file to not have any + answers that don't have bubbles that have not been verified + to be bubble free. + +=cut + +sub scantron_validate_missingbubbles { + my ($r,$currentphase) = @_; + #get student info + my $classlist=&Apache::loncoursedata::get_classlist(); + my %idmap=&username_to_idmap($classlist); + + #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(); + if (!$max_bubble) { $max_bubble=2**31; } + for (my $i=0;$i<=$scanlines->{'count'};$i++) { + my $line=&scantron_get_line($scanlines,$scan_data,$i); + if ($line=~/^[\s\cz]*$/) { next; } + my $scan_record=&scantron_parse_scanline($line,$i,\%scantron_config, + $scan_data); + if (!defined($$scan_record{'scantron.missingerror'})) { next; } + my @to_correct; + + # Probably here's where the error is... + + foreach my $missing (@{$$scan_record{'scantron.missingerror'}}) { + 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) { + &scantron_get_correction($r,$i,$scan_record,\%scantron_config, + $line,'missingbubble',\@to_correct); + return (1,$currentphase); + } + + } + return (0,$currentphase+1); +} + +=pod + +=item scantron_process_students + + Routine that does the actual grading of the bubble sheet information. + + The parsed scanline hash is added to %env + + Then foreach unskipped scanline it does an &Apache::lonnet::ssi() + foreach resource , with the form data of + + 'submitted' =>'scantron' + 'grade_target' =>'grade', + 'grade_username'=> username of student + 'grade_domain' => domain of student + 'grade_courseid'=> of course + 'grade_symb' => symb of resource to grade + + This triggers a grading pass. The problem grading code takes care + of converting the bubbled letter information (now in %env) into a + valid submission. + +=cut + +sub scantron_process_students { + my ($r) = @_; + + my (undef,undef,$sequence)=&Apache::lonnet::decode_symb($env{'form.selectpage'}); + my ($symb)=&get_symb($r); + if (!$symb) { + return ''; + } + 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(); + my $map=$navmap->getResourceByUrl($sequence); + my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0); +# $r->print("geto ".scalar(@resources)."
    "); my $result= < @@ -3254,104 +7559,576 @@ SCANTRONFORM $r->print($result); my @delayqueue; - my $totalcorrect; - my $totalincorrect; + my %completedstudents; + + my $lock=&Apache::lonnet::set_lock(&mt('Grading bubblesheet exam')); + my $count=&get_todo_count($scanlines,$scan_data); + my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin($r,'Scantron Status', + 'Scantron Progress',$count, + 'inline',undef,'scantronupload'); + &Apache::lonhtmlcommon::Update_PrgWin($r,\%prog_state, + 'Processing first student'); + my $start=&Time::HiRes::time(); + my $i=-1; + my ($uname,$udom,$started); - my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin($r, - 'Scantron Status','Scantron Progress',scalar(@scanlines)); - foreach my $line (@scanlines) { - my $studentcorrect; - my $studentincorrect; + &scantron_get_maxbubble(); # Need the bubble lines array to parse. + - chomp($line); - my $scan_record=&scantron_parse_scanline($line,\%scantron_config); - my ($uname,$udom); - if ($uname=&scantron_find_student($scan_record,\%idmap)) { - &scantron_add_delay(\@delayqueue,$line, - 'Unable to find a student that matches'); - } - $r->print('
    doing studnet'.$uname.'
    '); - ($uname,$udom)=split(/:/,$uname); - &Apache::lonnet::delenv('form.counter'); - &Apache::lonnet::appenv(%$scan_record); -# &Apache::lonhomework::showhash(%ENV); - $Apache::lonxml::debug=1; - &Apache::lonxml::debug("line is $line"); + # 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. + } + + while ($i<$scanlines->{'count'}) { + ($uname,$udom)=('',''); + $i++; + my $line=&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=&scantron_parse_scanline($line,$i,\%scantron_config, + $scan_data); + unless ($uname=&scantron_find_student($scan_record,$scan_data, + \%idmap,$i)) { + &scantron_add_delay(\@delayqueue,$line, + 'Unable to find a student that matches',1); + next; + } + if (exists $completedstudents{$uname}) { + &scantron_add_delay(\@delayqueue,$line, + 'Student '.$uname.' has multiple sheets',2); + next; + } + ($uname,$udom)=split(/:/,$uname); + + &Apache::lonxml::clear_problem_counter(); + &Apache::lonnet::appenv($scan_record); + + if (&scantron_clear_skip($scanlines,$scan_data,$i)) { + &scantron_putfile($scanlines,$scan_data); + } - my $i=0; + my $i=0; foreach my $resource (@resources) { $i++; - my $result=&Apache::lonnet::ssi($resource->src(), - ('submitted' =>'scantron', - 'grade_target' =>'grade', - 'grade_username'=>$uname, - 'grade_domain' =>$udom, - 'grade_courseid'=>$ENV{'request.course.id'}, - 'grade_symb' =>$resource->symb())); - my %score=&Apache::lonnet::restore($resource->symb(), - $ENV{'request.course.id'}, - $udom,$uname); - foreach my $part ($resource->{PARTS}) { - if ($score{'resource.'.$part.'.solved'} =~ /^correct/) { - $studentcorrect++; - $totalcorrect++; - } else { - $studentincorrect++; - $totalincorrect++; - } - } - $r->print('
    '.
    -		      $resource->symb().'-'.
    -		      $resource->src().'-'.'
    result is'.$result); - &Apache::lonhomework::showhash(%score); - # if ($i eq 3) {last;} - } - &Apache::lonnet::delenv('form.counter'); + my %form=('submitted' =>'scantron', + 'grade_target' =>'grade', + 'grade_username'=>$uname, + 'grade_domain' =>$udom, + 'grade_courseid'=>$env{'request.course.id'}, + 'grade_symb' =>$resource->symb()); + if (exists($scan_record->{'scantron.CODE'}) + && + &Apache::lonnet::validCODE($scan_record->{'scantron.CODE'})) { + $form{'CODE'}=$scan_record->{'scantron.CODE'}; + } else { + $form{'CODE'}=''; + } + my $result=&ssi_with_retries($resource->src(), $ssi_retries, %form); + if ($ssi_error) { + $ssi_error = 0; # So end of handler error message does not trigger. + $r->print(""); + &ssi_print_error($r); + $r->print(&show_grading_menu_form($symb)); + &Apache::lonnet::remove_lock($lock); + return ''; # Why return ''? Beats me. + } + + if (&Apache::loncommon::connection_aborted($r)) { last; } + } + $completedstudents{$uname}={'line'=>$line}; + if (&Apache::loncommon::connection_aborted($r)) { last; } + } continue { + &Apache::lonxml::clear_problem_counter(); &Apache::lonnet::delenv('scantron\.'); - &Apache::lonhtmlcommon::Increment_PrgWin($r,\%prog_state, - 'last student Who got a '.$studentcorrect.' correct and '. - $studentincorrect.' incorrect. The class has gotten '. - $totalcorrect.' correct and '.$totalincorrect.' incorrect'); - last; - #FIXME - #get iterator for $sequence - #foreach question 'submit' the students answer to the server - # through grade target { - # generate data to pass back that includes grade recevied - #} - } - $Apache::lonxml::debug=0; - foreach my $delay (@delayqueue) { - #FIXME - #print out each delayed student with interface to select how - # to repair student provided info - #Expected errors include - # 1 bad/no stuid/username - # 2 invalid bubblings - } + &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)); + return ''; +} + +=pod + +=item scantron_upload_scantron_data + + Creates the screen for adding a new bubble sheet data file to a course. + +=cut + +sub scantron_upload_scantron_data { + my ($r)=@_; + $r->print(&Apache::loncommon::coursebrowser_javascript($env{'request.role.domain'})); + my $select_link=&Apache::loncommon::selectcourse_link('rules','courseid', + 'domainid', + 'coursename'); + my $domsel=&Apache::loncommon::select_dom_form($env{'request.role.domain'}, + 'domainid'); + my $default_form_data=&defaultFormData(&get_symb($r,1)); + $r->print(' + + +
    +'.$default_form_data.' + + + + + + + + + + +
    '.$select_link.'
    '.&mt('Course ID:').'
    '.&mt('Course Name:').'
    '.&mt('Domain:').' '.$domsel.'
    '.&mt('File to upload:').'
    + + +
    +'); + return ''; +} + +=pod + +=item scantron_upload_scantron_data_save + + Adds a provided bubble information data file to the course if user + has the correct privileges to do so. + +=cut + +sub scantron_upload_scantron_data_save { + my($r)=@_; + my ($symb)=&get_symb($r,1); + my $doanotherupload= + '
    '."\n". + ''."\n". + ''."\n". + '
    '."\n"; + if (!&Apache::lonnet::allowed('usc',$env{'form.domainid'}) && + !&Apache::lonnet::allowed('usc', + $env{'form.domainid'}.'_'.$env{'form.courseid'})) { + $r->print(&mt("You are not allowed to upload Scantron data to the requested course.")."
    "); + if ($symb) { + $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(&mt("Doing upload to [_1]",$coursedata{'description'})."
    "); + my $fname=$env{'form.upfile.filename'}; #FIXME - # if delay queue exists 2 submits one to process delayed students one - # to ignore delayed students, possibly saving the delay queue for later - - $navmap->untieHashes(); + #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; + if (length($env{'form.upfile'}) < 2) { + $r->print(&mt("Error: The file you attempted to upload, [_1] 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'},'upfile',$fname); + if ($result =~ m|^/uploaded/|) { + $r->print(&mt("Success: Successfully uploaded [_1] bytes of data into location [_2]", + (length($env{'form.upfile'})-1), + ''.$result."")); + } else { + $r->print(&mt("Error: An error ([_1]) occurred when attempting to upload the file, [_2]", + $result, + ''.&HTML::Entities::encode($env{'form.upfile.filename'},'<>&"')."")); + + } + } + if ($symb) { + $r->print(&scantron_selectphase($r,$uploadedfile)); + } else { + $r->print($doanotherupload); + } + return ''; +} + +=pod + +=item valid_file + + Validates that the requested bubble data file exists in the course. + +=cut + +sub valid_file { + my ($requested_file)=@_; + foreach my $filename (sort(&scantron_filenames())) { + if ($requested_file eq $filename) { return 1; } + } + return 0; } + +=pod + +=item scantron_download_scantron_data + + Shows a list of the three internal files (original, corrected, + skipped) for a specific bubble sheet data file that exists in the + course. + +=cut + +sub scantron_download_scantron_data { + my ($r)=@_; + my $default_form_data=&defaultFormData(&get_symb($r,1)); + my $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(' +

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

    +'); + $r->print(&show_grading_menu_form(&get_symb($r,1))); + return; + } + my $orig='/uploaded/'.$cdom.'/'.$cname.'/scantron_orig_'.$file; + my $corrected='/uploaded/'.$cdom.'/'.$cname.'/scantron_corrected_'.$file; + my $skipped='/uploaded/'.$cdom.'/'.$cname.'/scantron_skipped_'.$file; + &Apache::lonnet::allowuploaded('/adm/grades',$orig); + &Apache::lonnet::allowuploaded('/adm/grades',$corrected); + &Apache::lonnet::allowuploaded('/adm/grades',$skipped); + $r->print(' +

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

    +

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

    +

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

    +'); + $r->print(&show_grading_menu_form(&get_symb($r,1))); + return ''; +} + +sub checkscantron_results { + my ($r) = @_; + my ($symb)=&get_symb($r); + if (!$symb) {return '';} + my $grading_menu_button=&show_grading_menu_form($symb); + my $cid = $env{'request.course.id'}; + my %lettdig = ( + A => 1, + B => 2, + C => 3, + D => 4, + E => 5, + F => 6, + G => 7, + H => 8, + I => 9, + J => 0, + ); + my $numletts = scalar(keys(%lettdig)); + my $cnum = $env{'course.'.$cid.'.num'}; + my $cdom = $env{'course.'.$cid.'.domain'}; + my (undef, undef, $sequence) = &Apache::lonnet::decode_symb($env{'form.selectpage'}); + my %record; + my %scantron_config = + &Apache::grades::get_scantron_config($env{'form.scantron_format'}); + my ($scanlines,$scan_data)=&Apache::grades::scantron_getfile(); + my $classlist=&Apache::loncoursedata::get_classlist(); + my %idmap=&Apache::grades::username_to_idmap($classlist); + my $navmap=Apache::lonnavmaps::navmap->new(); + my $map=$navmap->getResourceByUrl($sequence); + my @resources=$navmap->retrieveResources($map,undef,1,0); + my (%scandata,%lastname,%bylast); + $r->print(' +
    '."\n"); + + my @delayqueue; + my %completedstudents; + + my $count=&Apache::grades::get_todo_count($scanlines,$scan_data); + my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin($r,'Scantron/Submissions Comparison Status', + 'Progress of Scantron Data/Submission Records Comparison',$count, + 'inline',undef,'checkscantron'); + my ($username,$domain,$uname,$started); + + &Apache::grades::scantron_get_maxbubble(); # Need the bubble lines array to parse. + + &Apache::lonhtmlcommon::Update_PrgWin($r,\%prog_state, + 'Processing first student'); + my $start=&Time::HiRes::time(); + my $i=-1; + + while ($i<$scanlines->{'count'}) { + ($username,$domain,$uname)=('','',''); + $i++; + my $line=&Apache::grades::scantron_get_line($scanlines,$scan_data,$i); + if ($line=~/^[\s\cz]*$/) { next; } + if ($started) { + &Apache::lonhtmlcommon::Increment_PrgWin($r,\%prog_state, + 'last student'); + } + $started=1; + my $scan_record= + &Apache::grades::scantron_parse_scanline($line,$i,\%scantron_config, + $scan_data); + unless ($uname=&Apache::grades::scantron_find_student($scan_record,$scan_data, + \%idmap,$i)) { + &Apache::grades::scantron_add_delay(\@delayqueue,$line, + 'Unable to find a student that matches',1); + next; + } + if (exists $completedstudents{$uname}) { + &Apache::grades::scantron_add_delay(\@delayqueue,$line, + 'Student '.$uname.' has multiple sheets',2); + next; + } + my $pid = $scan_record->{'scantron.ID'}; + $lastname{$pid} = $scan_record->{'scantron.LastName'}; + push(@{$bylast{$lastname{$pid}}},$pid); + my $lastpos = $env{'form.scantron_maxbubble'}*$scantron_config{'Qlength'}; + $scandata{$pid} = substr($line,$scantron_config{'Qstart'}-1,$lastpos); + chomp($scandata{$pid}); + $scandata{$pid} =~ s/\r$//; + ($username,$domain)=split(/:/,$uname); + my $counter = -1; + my (%expected,%startpos); + foreach my $resource (@resources) { + next if (!$resource->is_problem()); + my $symb = $resource->symb(); + my $partsref = $resource->parts(); + my @parts; + my @part_ids = (); + if (ref($partsref) eq 'ARRAY') { + @parts = @{$partsref}; + foreach my $part (@parts) { + my @resp_ids = $resource->responseIds($part); + foreach my $resp (@resp_ids) { + $counter ++; + my $part_id = $part.'.'.$resp; + $expected{$part_id} = 0; + push(@part_ids,$part_id); + if ($env{"form.scantron.sub_bubblelines.$counter"}) { + my @sub_lines = split(/,/,$env{"form.scantron.sub_bubblelines.$counter"}); + foreach my $item (@sub_lines) { + $expected{$part_id} += $item; + } + } else { + $expected{$part_id} = $env{"form.scantron.bubblelines.$counter"}; + } + $startpos{$part_id} = $env{"form.scantron.first_bubble_line.$counter"}; + } + } + } + if ($symb) { + my %recorded; + my (%returnhash) = + &Apache::lonnet::restore($symb,$cid,$domain,$username); + if ($returnhash{'version'}) { + my %lasthash=(); + my $version; + for ($version=1;$version<=$returnhash{'version'};$version++) { + foreach my $key (sort(split(/\:/,$returnhash{$version.':keys'}))) { + $lasthash{$key}=$returnhash{$version.':'.$key}; + } + } + foreach my $key (keys(%lasthash)) { + if ($key =~ /\.scantron$/) { + my $value = &unescape($lasthash{$key}); + my ($part_id) = ($key =~ /^resource\.(.+)\.scantron$/); + if ($value eq '') { + for (my $i=0; $i<$expected{$part_id}; $i++) { + for (my $j=0; $j<$scantron_config{'length'}; $j++) { + $recorded{$part_id} .= $; + } + } + } else { + my @tocheck; + my @items = split(//,$value); + if (($scantron_config{'Qon'} eq 'letter') || + ($scantron_config{'Qon'} eq 'number')) { + if (@items < $expected{$part_id}) { + my $fragment = substr($scandata{$pid},$startpos{$part_id},$expected{$part_id}); + my @singles = split(//,$fragment); + foreach my $pos (@singles) { + if ($pos eq ' ') { + push(@tocheck,$pos); + } else { + my $next = shift(@items); + push(@tocheck,$next); + } + } + } else { + @tocheck = @items; + } + foreach my $letter (@tocheck) { + if ($scantron_config{'Qon'} eq 'letter') { + if ($letter !~ /^[A-J]$/) { + $letter = $scantron_config{'Qoff'}; + } + $recorded{$part_id} .= $letter; + } elsif ($scantron_config{'Qon'} eq 'number') { + my $digit; + if ($letter !~ /^[A-J]$/) { + $digit = $scantron_config{'Qoff'}; + } else { + $digit = $lettdig{$letter}; + } + $recorded{$part_id} .= $digit; + } + } + } else { + @tocheck = @items; + for (my $i=0; $i<$expected{$part_id}; $i++) { + my $curr_sub = shift(@tocheck); + my $digit; + if ($curr_sub =~ /^[A-J]$/) { + $digit = $lettdig{$curr_sub}-1; + } + if ($curr_sub eq 'J') { + $digit += scalar($numletts); + } + for (my $j=0; $j<$scantron_config{'Qlength'}; $j++) { + if ($j == $digit) { + $recorded{$part_id} .= $scantron_config{'Qon'}; + } else { + $recorded{$part_id} .= $scantron_config{'Qoff'}; + } + } + } + } + } + } + } + } + foreach my $part_id (@part_ids) { + if ($recorded{$part_id} eq '') { + for (my $i=0; $i<$expected{$part_id}; $i++) { + for (my $j=0; $j<$scantron_config{'Qlength'}; $j++) { + $recorded{$part_id} .= $scantron_config{'Qoff'}; + } + } + } + $record{$pid} .= $recorded{$part_id}; + } + } + } + } + &Apache::lonhtmlcommon::Close_PrgWin($r,\%prog_state); + $r->print('
    '); + 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('Scantron').''.$showscandata.''.$last.''.$pid.''."\n". +''."\n". +''."\n". +'Submissions'.$showrecord.''."\n"; + $passed ++; + } else { + my $css_class = ($failed % 2)?'LC_odd_row':'LC_even_row'; + $badstudents .= ''.&mt('Scantron').''.$scandata{$pid}.''.$last.''.$pid.''."\n". +''."\n". +''."\n". +'Submissions'.$record{$pid}.''."\n". +''."\n"; + $failed ++; + } + $numstudents ++; + } + } + } + $r->print('

    '.&mt('Comparison of scantron 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 scantron 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 scantron 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 scantron grading pass.').'
    '.&mt('If unexpected discrepancies were detected, it is recommended that you inspect the original scantron sheets.'); + } + $r->print('

    '.$grading_menu_button); + return; +} + +=pod + +=back + +=cut + #-------- end of section for handling grading scantron forms ------- # #------------------------------------------------------------------- - #-------------------------- Menu interface ------------------------- # #--- Show a Grading Menu button - Calls the next routine --- sub show_grading_menu_form { - my ($symb,$url)=@_; - my $result.='
    '."\n". - ''."\n". - ''."\n". - ''."\n". + my ($symb)=@_; + my $result.='
    '."\n". + ''."\n". + ''."\n". ''."\n". - ''."\n". + ''."\n". '
    '."\n"; return $result; } @@ -3359,8 +8136,8 @@ sub show_grading_menu_form { # -- Retrieve choices for grading form sub savedState { my %savedState = (); - if ($ENV{'form.saveState'}) { - foreach (split(/:/,$ENV{'form.saveState'})) { + if ($env{'form.saveState'}) { + foreach (split(/:/,$env{'form.saveState'})) { my ($key,$value) = split(/=/,$_,2); $savedState{$key} = $value; } @@ -3368,10 +8145,128 @@ sub savedState { return \%savedState; } -#--- Displays the main menu page ------- -sub gradingmenu { +sub grading_menu { + my ($request) = @_; + my ($symb)=&get_symb($request); + if (!$symb) {return '';} + my $probTitle = &Apache::lonnet::gettitle($symb); + my ($table,undef,$hdgrade) = &showResourceInfo($symb,$probTitle); + + $request->print($table); + my %fields = ('symb'=>&Apache::lonenc::check_encrypt($symb), + 'handgrade'=>$hdgrade, + 'probTitle'=>$probTitle, + 'command'=>'submit_options', + 'saveState'=>"", + 'gradingMenu'=>1, + 'showgrading'=>"yes"); + my $url = &Apache::lonhtmlcommon::build_url('grades/',\%fields); + my @menu = ({ url => $url, + name => &mt('Manual Grading/View Submissions'), + short_description => + &mt('Start the process of hand grading submissions.'), + }); + $fields{'command'} = 'csvform'; + $url = &Apache::lonhtmlcommon::build_url('grades/',\%fields); + push(@menu, { url => $url, + name => &mt('Upload Scores'), + short_description => + &mt('Specify a file containing the class scores for current resource.')}); + $fields{'command'} = 'processclicker'; + $url = &Apache::lonhtmlcommon::build_url('grades/',\%fields); + push(@menu, { url => $url, + name => &mt('Process Clicker'), + short_description => + &mt('Specify a file containing the clicker information for this resource.')}); + $fields{'command'} = 'scantron_selectphase'; + $url = &Apache::lonhtmlcommon::build_url('grades/',\%fields); + push(@menu, { url => $url, + name => &mt('Grade/Manage/Review Scantron Forms'), + short_description => + &mt('Grade scantron exams, upload/download scantron data files, and review previously graded scantron exams.')}); + $fields{'command'} = 'verify'; + $url = &Apache::lonhtmlcommon::build_url('grades/',\%fields); + push(@menu, { url => "", + name => &mt('Verify Receipt'), + short_description => + &mt('')}); + # + # Create the menu + my $Str; + # $Str .= '

    '.&mt('Please select a grading task').'

    '; + $Str .= '
    '; + $Str .= ''. + ''."\n". + ''."\n". + ''."\n". + ''."\n". + ''."\n". + ''."\n"; + + foreach my $menudata (@menu) { + if ($menudata->{'name'} ne &mt('Verify Receipt')) { + $Str .='

    {'jscript'}. + ' href="'. + $menudata->{'url'}.'" >'. + $menudata->{'name'}."

    \n"; + } else { + $Str .='
    {'jscript'}. + ' onClick="javascript:checkChoice(document.forms.gradingMenu,\'5\',\'verify\')" '. + ' /> '. + &Apache::lonnet::recprefix($env{'request.course.id'}). + '-'; + } + $Str .= ' '.(' 'x8).$menudata->{'short_description'}. + "\n"; + } + $Str .="
    \n"; + $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; + if (val < 5) formname.submit(); + if (val == 5) { + if (!checkReceiptNo(formname,'notOK')) { + return false; + } else { + 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 + &commonJSfunctions($request); + return $Str; +} + + +#--- Displays the submissions first page ------- +sub submit_options { my ($request) = @_; - my ($symb,$url)=&get_symb_and_url($request); + my ($symb)=&get_symb($request); if (!$symb) {return '';} my $probTitle = &Apache::lonnet::gettitle($symb); @@ -3387,12 +8282,13 @@ sub gradingmenu { } formname.command.value = cmd; formname.saveState.value = "saveCmd="+cmdsave+":saveSec="+pullDownSelection(formname.section)+ - ":saveSub="+radioSelection(formname.submitonly)+":saveStatus="+pullDownSelection(formname.Status); + ":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) { @@ -3411,9 +8307,8 @@ sub gradingmenu { GRADINGMENUJS &commonJSfunctions($request); - my $result='

     Manual Grading/View Submission

    '; - my ($table,$resptype,$hdgrade) = &showResourceInfo($url,$probTitle); - $result.=$table; + my ($table,undef,$hdgrade) = &showResourceInfo($symb,$probTitle); + my $result; my (undef,$sections) = &getclasslist('all','0'); my $savedState = &savedState(); my $saveCmd = ($$savedState{'saveCmd'} eq '' ? 'submission' : $$savedState{'saveCmd'}); @@ -3422,118 +8317,684 @@ GRADINGMENUJS my $saveStatus = ($$savedState{'saveStatus'} eq '' ? 'Active' : $$savedState{'saveStatus'}); $result.='
    '."\n". - ''."\n". - ''."\n". - ''."\n". + ''."\n". ''."\n". ''."\n". ''."\n". ''."\n". + ''."\n". ''."\n"; - $result.='
    '."\n". - ''."\n". - '
    '."\n". - ' Select a Grading/Viewing Option
    '."\n"; - - $result.=''; - $result.=''; + return %clicker_ids; +} - $result.=''."\n"; - - $result.=''."\n"; - - $result.=''."\n"; - - $result.='
    '."\n". - ' Select Section: '."\n"; if (ref($sections)) { - foreach (sort (@$sections)) {$result.=''."\n";} + foreach my $section (sort(@$sections)) { + $result.=''."\n"; + } + } + $result.= '   '; + $result.=' + + +
    +
    + '.&mt('Groups').' +
    +
    + '.&Apache::lonstatistics::GroupSelect('group','multiple',5).' +
    +
    +
    +
    + '.&mt('Access Status').' +
    +
    + '.&Apache::lonhtmlcommon::StatusOptions($saveStatus,undef,5,undef,'mult').' +
    +
    +
    +
    + '.&mt('Submission Status').' +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    + + +
    +

    + '.&mt('Grade Complete Folder for One Student').' +

    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + + '; + $result .= &show_grading_menu_form($symb); + 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}); + } + } } - $result.= '
    '. - ' '.'Current Resource: For one or more students'. - '
                -->For students with '. - ' submissions or '. - ' for all
    '. - ' '. - '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"; - } +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; +} - $result.='
    '. - ''. - ' scores from file
    '. - ' scantron forms
    '. - ''. - ' submission Receipt no: '.unpack("%32C*",$Apache::lonnet::perlvar{'lonHostID'}). - '-'. - '
    '."\n". - '
    '."\n". - '
    '."\n"; +sub clicker_grading_parameters { + return ('gradingmechanism' => 'scalar', + 'upfiletype' => 'scalar', + 'specificid' => 'scalar', + 'pcorrect' => 'scalar', + 'pincorrect' => 'scalar'); +} + +sub process_clicker { + my ($r)=@_; + my ($symb)=&get_symb($r); + if (!$symb) {return '';} + my $result=&checkforfile_js(); + $env{'form.probTitle'} = &Apache::lonnet::gettitle($symb); + my ($table) = &showResourceInfo($symb,$env{'form.probTitle'}); + $result.=$table; + $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.=< +function sanitycheck() { +// Accept only integer percentages + document.forms.gradesupload.pcorrect.value=Math.round(document.forms.gradesupload.pcorrect.value); + document.forms.gradesupload.pincorrect.value=Math.round(document.forms.gradesupload.pincorrect.value); +// Find out grading choice + for (i=0; i +
    + + + + + +
    +
    +
    +
    + +
    +
        + + +
    +
    +
    +
    +ENDUPFORM + $result.='
    '."\n". + '


    '."\n"; + $result.=&show_grading_menu_form($symb); return $result; } +sub process_clicker_file { + my ($r)=@_; + my ($symb)=&get_symb($r); + if (!$symb) {return '';} + + my %Saveable_Parameters=&clicker_grading_parameters(); + &Apache::loncommon::store_course_settings('grades_clicker', + \%Saveable_Parameters); + + my ($result) = &showResourceInfo($symb,$env{'form.probTitle'}); + 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)=@_; + my ($symb)=&get_symb($r); + if (!$symb) {return '';} +# See which part we are saving to + my ($partlist,$handgrade,$responseType) = &response_type($symb); +# FIXME: This should probably look for the first handgradeable part + my $part=$$partlist[0]; +# Start screen output + my ($result) = &showResourceInfo($symb,$env{'form.probTitle'}); + + my $heading=&mt('Assigning grades based on clicker file'); + $result.=(<
    +
    +$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 ($answer[$i]) { + if ($gradingmechanism eq 'attendance') { + $sum+=$pcorrect; + } elsif ($answer[$i] eq '*') { + $sum+=$pcorrect; + } elsif ($answer[$i] eq '-') { + $realnumber--; + } else { + if ($answer[$i] eq $correct[$i]) { + $sum+=$pcorrect; + } 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 [_1] student(s).',$storecount). + '
    '."\n". + '


    '."\n"; + return $result.&show_grading_menu_form($symb); +} + sub handler { my $request=$_[0]; - - undef(%perm); - if ($ENV{'browser.mathml'}) { - $request->content_type('text/xml'); + &reset_caches(); + if ($env{'browser.mathml'}) { + &Apache::loncommon::content_type($request,'text/xml'); } else { - $request->content_type('text/html'); + &Apache::loncommon::content_type($request,'text/html'); } $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'}; - my $command=$ENV{'form.command'}; - if (!$url) { - my ($temp1,$temp2); - ($temp1,$temp2,$ENV{'form.url'})=split(/___/,$symb); - $url = $ENV{'form.url'}; - } - &send_header($request); - if ($url eq '' && $symb 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 $symb=&get_symb($request,1); + 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)); + } + + $ssi_error = 0; + $request->print(&Apache::loncommon::start_page('Grading')); + if ($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)=split(/\_\_\_/,$tsymb); + my ($map,$id,$url)=&Apache::lonnet::decode_symb($tsymb); if (&Apache::lonnet::allowed('mgr',$tcrsid)) { - $request->print(&Apache::lonnet::ssi_body('/res/'.$url, + $request->print(&ssi_with_retries('/res/'.$url, $ssi_retries, ('grade_username' => $tuname, 'grade_domain' => $tudom, 'grade_courseid' => $tcrsid, @@ -3549,23 +9010,9 @@ sub handler { } } } 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)); + ($env{'form.student'} eq '' ? &listStudents($request) : &submission($request,0,0)); } elsif ($command eq 'pickStudentPage' && $perm{'vgr'}) { &pickStudentPage($request); } elsif ($command eq 'displayPage' && $perm{'vgr'}) { @@ -3575,7 +9022,9 @@ sub handler { } elsif ($command eq 'processGroup' && $perm{'vgr'}) { &processGroup($request); } elsif ($command eq 'gradingmenu' && $perm{'vgr'}) { - $request->print(&gradingmenu($request)); + $request->print(&grading_menu($request)); + } elsif ($command eq 'submit_options' && $perm{'vgr'}) { + $request->print(&submit_options($request)); } elsif ($command eq 'viewgrades' && $perm{'vgr'}) { $request->print(&viewgrades($request)); } elsif ($command eq 'handgrade' && $perm{'mgr'}) { @@ -3584,52 +9033,64 @@ sub handler { $request->print(&editgrades($request)); } elsif ($command eq 'verify' && $perm{'vgr'}) { $request->print(&verifyreceipt($request)); + } elsif ($command eq 'processclicker' && $perm{'mgr'}) { + $request->print(&process_clicker($request)); + } elsif ($command eq 'processclickerfile' && $perm{'mgr'}) { + $request->print(&process_clicker_file($request)); + } elsif ($command eq 'assignclickergrades' && $perm{'mgr'}) { + $request->print(&assign_clicker_grades($request)); } elsif ($command eq 'csvform' && $perm{'mgr'}) { $request->print(&upcsvScores_form($request)); } elsif ($command eq 'csvupload' && $perm{'mgr'}) { $request->print(&csvupload($request)); } elsif ($command eq 'csvuploadmap' && $perm{'mgr'} ) { $request->print(&csvuploadmap($request)); - } elsif ($command eq 'csvuploadassign' && $perm{'mgr'}) { - if ($ENV{'form.associate'} ne 'Reverse Association') { - $request->print(&csvuploadassign($request)); + } elsif ($command eq 'csvuploadoptions' && $perm{'mgr'}) { + if ($env{'form.associate'} ne 'Reverse Association') { + $request->print(&csvuploadoptions($request)); } else { - if ( $ENV{'form.upfile_associate'} ne 'reverse' ) { - $ENV{'form.upfile_associate'} = 'reverse'; + if ( $env{'form.upfile_associate'} ne 'reverse' ) { + $env{'form.upfile_associate'} = 'reverse'; } else { - $ENV{'form.upfile_associate'} = 'forward'; + $env{'form.upfile_associate'} = 'forward'; } $request->print(&csvuploadmap($request)); } + } elsif ($command eq 'csvuploadassign' && $perm{'mgr'} ) { + $request->print(&csvuploadassign($request)); } elsif ($command eq 'scantron_selectphase' && $perm{'mgr'}) { $request->print(&scantron_selectphase($request)); + } elsif ($command eq 'scantron_warning' && $perm{'mgr'}) { + $request->print(&scantron_do_warning($request)); + } elsif ($command eq 'scantron_validate' && $perm{'mgr'}) { + $request->print(&scantron_validate_file($request)); } elsif ($command eq 'scantron_process' && $perm{'mgr'}) { $request->print(&scantron_process_students($request)); + } 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)); + } 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)); + } elsif ($command eq 'scantron_download' && + &Apache::lonnet::allowed('usc',$env{'request.course.id'})) { + $request->print(&scantron_download_scantron_data($request)); + } elsif ($command eq 'checksubmissions' && $perm{'vgr'}) { + $request->print(&checkscantron_results($request)); } elsif ($command) { - $request->print("Access Denied"); + $request->print("Access Denied ($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')); -} - -sub send_footer { - my ($request)= @_; - $request->print(''); - $request->print(&Apache::lontexconvert::footer()); -} - 1; __END__;