--- loncom/homework/grades.pm 2003/07/29 14:24:24 1.127
+++ loncom/homework/grades.pm 2007/10/30 01:04:31 1.472
@@ -1,7 +1,7 @@
# The LearningOnline Network with CAPA
# The LON-CAPA Grading handler
#
-# $Id: grades.pm,v 1.127 2003/07/29 14:24:24 ng Exp $
+# $Id: grades.pm,v 1.472 2007/10/30 01:04:31 albertel Exp $
#
# Copyright Michigan State University Board of Trustees
#
@@ -25,16 +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
-# July, H. K. Ng
-#
package Apache::grades;
use strict;
@@ -45,135 +35,403 @@ 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=();
+my %bubble_lines_per_response = (); # no. bubble lines for each response.
+ # index is "symb.part_id"
+
+my %first_bubble_line = (); # First bubble line no. for each bubble.
+
+# Save and restore the bubble lines array to the form env.
+
+
+sub save_bubble_lines {
+ foreach my $line (keys(%bubble_lines_per_response)) {
+ $env{"form.scantron.bubblelines.$line"} = $bubble_lines_per_response{$line};
+ $env{"form.scantron.first_bubble_line.$line"} =
+ $first_bubble_line{$line};
+ }
+}
+
+
+sub restore_bubble_lines {
+ my $line = 0;
+ %bubble_lines_per_response = ();
+ while ($env{"form.scantron.bubblelines.$line"}) {
+ my $value = $env{"form.scantron.bubblelines.$line"};
+ $bubble_lines_per_response{$line} = $value;
+ $first_bubble_line{$line} =
+ $env{"form.scantron.first_bubble_line.$line"};
+ $line++;
+ }
+
+}
+
+# Given the parsed scanline, get the response for
+# 'answer' number n:
+
+sub get_response_bubbles {
+ my ($parsed_line, $response) = @_;
+
+
+ my $bubble_line = $first_bubble_line{$response-1} +1;
+ my $bubble_lines= $bubble_lines_per_response{$response-1};
+
+ my $selected = "";
+
+ for (my $bline = 0; $bline < $bubble_lines; $bline++) {
+ $selected .= $$parsed_line{"scantron.$bubble_line.answer"}.":";
+ $bubble_line++;
+ }
+ return $selected;
+}
+
# ----- 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;
+}
+
#
-# --- Retrieve the parts that matches stores_\d+ from the metadata file.---
+# --- 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 ' Fullname (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,$symb) = shift;
- $symb=($ENV{'form.symb'} ne '' ? $ENV{'form.symb'} : (&Apache::lonnet::symbread($url))) if ($symb eq '');
- my $allkeys = &Apache::lonnet::metadata($url,'keys');
- my %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!!
- my ($value) = &Apache::lonnet::EXT('resource.'.$part.'.handgrade',$symb);
- $handgrade{$part} = $responsetype.':'.($value eq 'yes' ? '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 ='
'.
- '
Current Resource: '.$probTitle.'
'."\n";
- my ($partlist,$handgrade) = &response_type($url);
+ my ($symb,$probTitle,$checkboxes) = @_;
+ my $col=3;
+ if ($checkboxes) { $col=4; }
+ my $result = '
'.&mt('Current Resource').': '.$probTitle.'
'."\n";
+ $result .='
';
+ my ($partlist,$handgrade,$responseType) = &response_type($symb);
my %resptype = ();
my $hdgrade='no';
- for (sort keys(%$handgrade)) {
- my ($responsetype,$handgrade)=split(/:/,$$handgrade{$_});
- my $partID = (split(/_/))[0];
- $resptype{$partID} = $responsetype;
- $hdgrade = $handgrade if ($handgrade eq 'yes');
- $result.='
Part '.$partID.'
'.
- '
Type: '.$responsetype.'
';
+ 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.="
'."\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)=@_;
+ my $key = "$symb\0$uname\0$udom";
+ return $analyze_cache{$key} if (exists($analyze_cache{$key}));
+
+ my (undef,undef,$url)=&Apache::lonnet::decode_symb($symb);
+ $url=&Apache::lonnet::clutter($url);
+ my $subresult=&Apache::lonnet::ssi($url,
+ ('grade_target' => 'analyze'),
+ ('grade_domain' => $udom),
+ ('grade_symb' => $symb),
+ ('grade_courseid' =>
+ $env{'request.course.id'}),
+ ('grade_username' => $uname));
+ (undef,$subresult)=split(/_HASH_REF__/,$subresult,2);
+ my %analyze=&Apache::lonnet::str2hash($subresult);
+ return $analyze_cache{$key} = \%analyze;
+ }
+
+ sub get_order {
+ my ($partid,$respid,$symb,$uname,$udom)=@_;
+ my $analyze = &get_analyze($symb,$uname,$udom);
+ 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,$symb) = @_;
- if ($response eq 'option') {
- my (@IDs,@ans);
- foreach (split(/\&/,&Apache::lonnet::unescape($answer))) {
- my ($optionID,$ans) = split(/=/);
- push @IDs,$optionID.'';
- push @ans,$ans;
+ 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.'
';
}
- my $grayFont = '';
return '
'.
- '
Answer
'.
- (join '
',@ans).'
'.
- '
'.$grayFont.'Option ID
'.$grayFont.
- (join '
'.$grayFont,@IDs).'
'.
- '
';
- }
- if ($response eq 'essay') {
- if (! exists ($ENV{'form.'.$symb})) {
+ '
'.&mt('Answer').'
'.$toprow.'
'.
+ '
'.$grayFont.&mt('Option ID').'
'.
+ $grayFont.$bottomrow.'
'.'
';
+ } 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 '
'.
+ '
'.&mt('Answer').'
'.$toprow.'
'.
+ '
'.$grayFont.&mt('Item ID').'
'.
+ $middlerow.'
'.
+ '
'.$grayFont.&mt('Option ID').'
'.
+ $bottomrow.'
'.'
';
+ } 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 '
'.
+ '
'.&mt('Answer').'
'.$toprow.'
'.
+ '
'.$grayFont.&mt('Option ID').'
'.
+ $grayFont.$bottomrow.'
'.'
';
+ } 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'});
+ $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.
- }
- return '
'.&keywords_highlight($answer).'
';
+ 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 '
';
+ my @grade = grep(/^\Q${version}resource.$respid.$partid.\E[^.]*[.]status$/,
+ keys(%{$record}));
+ foreach my $grade (sort(@grade)) {
+ my ($dim) = ($grade =~/[.]([^.]+)[.]status$/);
+ $result.= '
'.&mt("Dimension: [_1], status [_2] ",
+ $dim, $record->{$grade}).
+ '
';
+ }
+ $result.='
';
+ return $result;
+ }
+ } elsif ( $response =~ m/(?:numerical|formula)/) {
+ $answer =
+ &Apache::loncommon::format_previous_attempt_value('submission',
+ $answer);
}
return $answer;
}
@@ -206,7 +464,8 @@ sub commonJSfunctions {
}
}
} else {
- if (selectOne.selected) return selectOne.value;
+ // only one value it must be the selected one
+ return selectOne.value;
}
}
@@ -216,35 +475,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 = ();
@@ -294,11 +604,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";
@@ -311,7 +621,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.= ' ".&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);
+ }
+ 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
+
+ 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);
+
+ #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
+
+ $r->print("
An error was detected ($error)");
+ if ( $$scan_record{'scantron.PaperID'} =~ /\S/) {
+ $r->print(" for PaperID ".
+ $$scan_record{'scantron.PaperID'}." \n");
+ } else {
+ $r->print(" in scanline $i
".
+ $line."
\n");
+ }
+ my $message="
The ID on the form is ".
+ $$scan_record{'scantron.ID'}." \n".
+ "The name on the paper is ".
+ $$scan_record{'scantron.LastName'}.",".
+ $$scan_record{'scantron.FirstName'}."
";
+
+ $r->print(''."\n");
+ $r->print(''."\n");
+ if ($error =~ /ID$/) {
+ if ($error eq 'incorrectID') {
+ $r->print("The encoded ID is not in the classlist\n");
+ } elsif ($error eq 'duplicateID') {
+ $r->print("The encoded ID has also been used by a previous paper $arg\n");
+ }
+ $r->print($message);
+ $r->print("
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('
There have been no bubbles scanned for some question(s)
\n");
+ $r->print($message);
+ $r->print("
Please indicate which bubble should be used for grading
");
+ $r->print("Some questions have no scanned bubbles\n");
+ $r->print('');
+ foreach my $question (@{$arg}) {
+ my $selected = &get_response_bubbles($scan_record, $question);
+ my @select_array = split(/:/,$selected); # ought to be an array of empties.
+ &scantron_bubble_selector($r,$scan_config,$question, @select_array);
+ }
+ } else {
+ $r->print("\n
");
+ }
+ $r->print("\n
");
+
+}
+
+=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()
+ $quest - number of the bubble line to make a corrector for
+ @lines - array of answer lines.
+
+=cut
+
+sub scantron_bubble_selector {
+ my ($r,$scan_config,$quest,@lines)=@_;
+ my $max=$$scan_config{'Qlength'};
+
+
+ my $scmode=$$scan_config{'Qon'};
+
+ my $bubble_length = scalar(@lines);
+
+
+ if ($scmode eq 'number' || $scmode eq 'letter') { $max=10; }
+
+ my $response = $quest-1;
+ my $lines = $bubble_lines_per_response{$response};
+
+ my $total_lines = $lines*2;
+ my @alphabet=('A'..'Z');
+ $r->print("
');
+
+ }
+
+ if ($l == 0) {
+ my $lspan = $total_lines * 2; # 2 table rows per bubble line.
+
+ $r->print('
');
+
+ }
+
+ $r->print('
');
+
+ # FIXME: This may have to be a bit more clever for
+ # multiline questions (different values e.g..).
+
+ for (my $i=0;$i<$max;$i++) {
+ $r->print("\n".
+ '
");
+ }
+ $r->print('
');
+
+
+ }
+ $r->print('
');
+}
+
+=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'} and
+ $env{'form.scantron.first_bubble_line.n'}
+ which are the total number of bubble, lines, the number of bubble
+ lines for reponse n and number of the first bubble line for response n.
+
+=cut
+
+sub scantron_get_maxbubble {
+ 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);
+
+ &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 = ();
+
+
+ my $response_number = 0;
+ my $bubble_line = 0;
+ foreach my $resource (@resources) {
+ my $symb = $resource->symb();
+ &Apache::lonxml::clear_bubble_lines_for_part();
+ my $result=&Apache::lonnet::ssi($resource->src(),
+ ('symb' => $resource->symb()),
+ ('grade_target' => 'analyze'),
+ ('grade_courseid' => $cid),
+ ('grade_domain' => $udom),
+ ('grade_username' => $uname));
+ my (undef, $an) =
+ split(/_HASH_REF__/,$result, 2);
+
+ my %analysis = &Apache::lonnet::str2hash($an);
+
+
+
+ foreach my $part_id (@{$analysis{'parts'}}) {
+
+
+ my $lines = $analysis{"$part_id.bubble_lines"};;
+
+ # TODO - make this a persistent hash not an array.
+
+
+ $first_bubble_line{$response_number} = $bubble_line;
+ $bubble_lines_per_response{$response_number} = $lines;
+ $response_number++;
+
+ $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'}}) {
+ if ($missing > $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)=split(/___/,$ENV{'form.selectpage'});
- my ($symb,$url)=&get_symb_and_url($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,$url);
+ my $default_form_data=&defaultFormData($symb);
- 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>;
+ 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($ENV{'request.course.fn'}.'.db',$ENV{'request.course.fn'}.'_parms.db',1, 1);
+ 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)." ");
+# $r->print("geto ".scalar(@resources)." ");
my $result= <
@@ -3327,102 +6938,276 @@ SCANTRONFORM
$r->print($result);
my @delayqueue;
- my $totalcorrect;
- my $totalincorrect;
+ my %completedstudents;
+
+ 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);
+
+ &scantron_get_maxbubble(); # Need the bubble lines array to parse.
+
+ 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);
- my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin($r,
- 'Scantron Status','Scantron Progress',scalar(@scanlines));
- foreach my $line (@scanlines) {
- my $studentcorrect;
- my $studentincorrect;
+ &Apache::lonxml::clear_problem_counter();
+ &Apache::lonnet::appenv(%$scan_record);
- 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 (&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++;
- }
+ my %form=('submitted' =>'scantron',
+ 'grade_target' =>'grade',
+ 'grade_username'=>$uname,
+ 'grade_domain' =>$udom,
+ 'grade_courseid'=>$env{'request.course.id'},
+ 'grade_symb' =>$resource->symb());
+ if (exists($scan_record->{'scantron.CODE'})
+ &&
+ &Apache::lonnet::validCODE($scan_record->{'scantron.CODE'})) {
+ $form{'CODE'}=$scan_record->{'scantron.CODE'};
+ } else {
+ $form{'CODE'}='';
+ }
+ my $result=&Apache::lonnet::ssi($resource->src(),%form);
+ if ($result ne '') {
}
- $r->print('
result is'.$result);
- &Apache::lonhomework::showhash(%score);
- # if ($i eq 3) {last;}
+ if (&Apache::loncommon::connection_aborted($r)) { last; }
}
- &Apache::lonnet::delenv('form.counter');
+ $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);
+# 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(<
+ function checkUpload(formname) {
+ if (formname.upfile.value == "") {
+ alert("Please use the browse button to select a file from your local directory.");
+ return false;
+ }
+ formname.submit();
+ }
+
+
+
+$default_form_data
+
+
$select_link
+
Course ID:
+
Course Name:
+
Domain:
$domsel
+
File to upload:
+
+
+
+
+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("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("Doing upload to ".$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("Error: The file you attempted to upload, ".&HTML::Entities::encode($env{'form.upfile.filename'},'<>&"').", contained no information. Please check that you entered the correct filename.");
+ } else {
+ my $result=&Apache::lonnet::finishuserfileupload($env{'form.courseid'},$env{'form.domainid'},'upfile',$fname);
+ if ($result =~ m|^/uploaded/|) {
+ $r->print("Success: Successfully uploaded ".(length($env{'form.upfile'})-1)." bytes of data into location ".$result."");
+ } else {
+ $r->print("Error: An error (".$result.") occurred when attempting to upload the file, ".&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(<
+ The requested file name was invalid.
+
+ERROR
+ $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(<
+ Original file as uploaded by the scantron office.
+
+
+ Corrections, a file of corrected records that were used in grading.
+
+DOWNLOAD
+ $r->print(&show_grading_menu_form(&get_symb($r,1)));
+ 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 ($symb)=@_;
my $result.='
'."\n";
+ $result.=' '.&mt('Specify a file containing the clicker information for this resource').
+ '.
'."\n";
+ $result.='
'."\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') {
+ 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 $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);
+ }
+ 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');
+ } 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
+ 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 corrion(s)',$number).' '.
+ ''.
+ &mt('Awarding [_1] percent for correct and [_2] percent for incorrect responses',
+ $env{'form.pcorrect'},$env{'form.pincorrect'}).
+ ' ';
+# 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".''.
+ "";
+ foreach my $reguser (sort(split(/\,/,$clicker_ids{$id}))) {
+ $result.="';
+ }
+ $result.='';
+ $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') {
+ 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;
+ for (my $i=0;$i<$number;$i++) {
+ if ($answer[$i]) {
+ if ($gradingmechanism eq 'attendance') {
+ $sum+=$pcorrect;
+ } else {
+ if ($answer[$i] eq $correct[$i]) {
+ $sum+=$pcorrect;
+ } else {
+ $sum+=$pincorrect;
+ }
+ }
+ }
+ }
+ my $ave=$sum/(100*$number);
+# 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).
+ '