--- loncom/homework/grades.pm 2007/06/22 22:50:30 1.417
+++ loncom/homework/grades.pm 2007/11/03 00:08:09 1.477
@@ -1,7 +1,7 @@
# The LearningOnline Network with CAPA
# The LON-CAPA Grading handler
#
-# $Id: grades.pm,v 1.417 2007/06/22 22:50:30 albertel Exp $
+# $Id: grades.pm,v 1.477 2007/11/03 00:08:09 albertel Exp $
#
# Copyright Michigan State University Board of Trustees
#
@@ -35,6 +35,7 @@ use Apache::loncommon;
use Apache::lonhtmlcommon;
use Apache::lonnavmaps;
use Apache::lonhomework;
+use Apache::lonpickcode;
use Apache::loncoursedata;
use Apache::lonmsg();
use Apache::Constants qw(:common);
@@ -45,36 +46,104 @@ 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 from the metadata file.---
sub getpartlist {
my ($symb) = @_;
- my (undef,undef,$url) = &Apache::lonnet::decode_symb($symb);
- my $partorder = &Apache::lonnet::metadata($url, 'partorder');
- my @parts;
- if ($partorder) {
- for my $part (split (/,/,$partorder)) {
- if (!&Apache::loncommon::check_if_partid_hidden($part,$symb)) {
- push(@parts, $part);
- }
- }
- } else {
- my $metadata = &Apache::lonnet::metadata($url, 'packages');
- foreach (split(/\,/,$metadata)) {
- if ($_ =~ /^part_(.*)$/) {
- if (!&Apache::loncommon::check_if_partid_hidden($1,$symb)) {
- push(@parts, $1);
- }
- }
- }
- }
+
+ my $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 (@parts) {
- my (@metakeys) = split(/,/,&Apache::lonnet::metadata($url,'keys'));
+ foreach my $part (@{ $partlist }) {
foreach my $key (@metakeys) {
if ($key =~ m/^stores_\Q$part\E_/) { push(@stores,$key); }
}
@@ -93,6 +162,7 @@ sub get_symb {
return ();
}
}
+ &Apache::lonenc::check_decrypt(\$symb);
return ($symb);
}
@@ -194,22 +264,54 @@ sub showResourceInfo {
return $result,$responseType,$hdgrade,$partlist,$handgrade;
}
+sub reset_caches {
+ &reset_analyze_cache();
+ &reset_perm();
+}
-sub get_order {
- my ($partid,$respid,$symb,$uname,$udom)=@_;
- my (undef,undef,$url)=&Apache::lonnet::decode_symb($symb);
- $url=&Apache::lonnet::clutter($url);
- my $subresult=&Apache::lonnet::ssi($url,
- ('grade_target' => 'analyze'),
- ('grade_domain' => $udom),
- ('grade_symb' => $symb),
- ('grade_courseid' =>
- $env{'request.course.id'}),
- ('grade_username' => $uname));
- (undef,$subresult)=split(/_HASH_REF__/,$subresult,2);
- my %analyze=&Apache::lonnet::str2hash($subresult);
- return ($analyze{"$partid.$respid.shown"});
+{
+ 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/rank/radiobutton/match/essay/Task
# response types only.
@@ -230,8 +332,8 @@ sub cleanRecord {
$bottomrow.='
'.$grayFont.$foil.' ';
}
return ''.
- 'Answer '.$toprow.' '.
- ''.$grayFont.'Option ID '.
+ ''.&mt('Answer').' '.$toprow.' '.
+ ''.$grayFont.&mt('Option ID').' '.
$grayFont.$bottomrow.' '.'
';
} elsif ($response eq 'match') {
my %answer=&Apache::lonnet::str2hash($answer);
@@ -250,31 +352,31 @@ sub cleanRecord {
$bottomrow.=''.$grayFont.$foil.' ';
}
return ''.
- 'Answer '.$toprow.' '.
- ''.$grayFont.'Item ID '.
+ ''.&mt('Answer').' '.$toprow.' '.
+ ''.$grayFont.&mt('Item ID').' '.
$middlerow.' '.
- ''.$grayFont.'Option ID '.
+ ''.$grayFont.&mt('Option ID').' '.
$bottomrow.' '.'
';
} elsif ($response eq 'radiobutton') {
my %answer=&Apache::lonnet::str2hash($answer);
my ($toprow,$bottomrow);
- my $correct=($order->[0])+1;
- for (my $i=1;$i<=$#$order;$i++) {
- my $foil=$order->[$i];
+ my $correct =
+ &get_radiobutton_correct_foil($partid,$respid,$symb,$uname,$udom);
+ foreach my $foil (@$order) {
if (exists($answer{$foil})) {
- if ($i == $correct) {
- $toprow.='true ';
+ if ($foil eq $correct) {
+ $toprow.=''.&mt('true').' ';
} else {
- $toprow.='true ';
+ $toprow.=''.&mt('true').' ';
}
} else {
- $toprow.='false ';
+ $toprow.=''.&mt('false').' ';
}
$bottomrow.=''.$grayFont.$foil.' ';
}
return ''.
- 'Answer '.$toprow.' '.
- ''.$grayFont.'Option ID '.
+ ''.&mt('Answer').' '.$toprow.' '.
+ ''.$grayFont.&mt('Option ID').' '.
$grayFont.$bottomrow.' '.'
';
} elsif ($response eq 'essay') {
if (! exists ($env{'form.'.$symb})) {
@@ -326,7 +428,10 @@ sub cleanRecord {
$result.='';
return $result;
}
-
+ } elsif ( $response =~ m/(?:numerical|formula)/) {
+ $answer =
+ &Apache::loncommon::format_previous_attempt_value('submission',
+ $answer);
}
return $answer;
}
@@ -370,8 +475,10 @@ COMMONJSFUNCTIONS
#--- Dumps the class list with usernames,list of sections,
#--- section, ids and fullnames for each user.
sub getclasslist {
- my ($getsec,$filterlist) = @_;
+ 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);
@@ -380,10 +487,19 @@ sub getclasslist {
@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=&Apache::loncoursedata::get_classlist();
+ 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;
@@ -400,18 +516,40 @@ sub getclasslist {
$classlist->{$student}->[&Apache::loncoursedata::CL_FULLNAME()];
my $status =
$classlist->{$student}->[&Apache::loncoursedata::CL_STATUS()];
+ my $group =
+ $classlist->{$student}->[&Apache::loncoursedata::CL_GROUP()];
# filter students according to status selected
- if ($filterlist && $env{'form.Status'} ne 'Any') {
- if ($env{'form.Status'} ne $status) {
- delete ($classlist->{$student});
+ if ($filterlist && (!($stu_status =~ /Any/))) {
+ if (!($stu_status =~ $status)) {
+ delete($classlist->{$student});
next;
}
}
+ # filter students according to groups selected
+ my @stu_groups = split(/,/,$group);
+ if (@getgroup) {
+ my $exclude = 1;
+ foreach my $grp (@getgroup) {
+ foreach my $stu_group (@stu_groups) {
+ if ($stu_group eq $grp) {
+ $exclude = 0;
+ }
+ }
+ if (($grp eq 'none') && !$group) {
+ $exclude = 0;
+ }
+ }
+ if ($exclude) {
+ delete($classlist->{$student});
+ }
+ }
$section = ($section ne '' ? $section : 'none');
if (&canview($section)) {
if (!@getsec || grep(/^\Q$section\E$/,@getsec)) {
$sections{$section}++;
- $fullnames{$student}=$fullname;
+ if ($classlist->{$student}) {
+ $fullnames{$student}=$fullname;
+ }
} else {
delete($classlist->{$student});
}
@@ -484,6 +622,7 @@ sub student_gradeStatus {
# Shows a student's view of problem and submission
sub jscriptNform {
my ($symb) = @_;
+ my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status'));
my $jscript=''."\n";
$jscript.= ''."\n";
@@ -890,7 +1032,7 @@ LISTJAVASCRIPT
' students checked for '.$submissions.') ';
}
} elsif ($ctr == 1) {
- $gradeTable =~ s/type=checkbox/type=checkbox checked/;
+ $gradeTable =~ s/type="checkbox"/type="checkbox" checked="checked"/;
}
$gradeTable.=&show_grading_menu_form($symb);
$request->print($gradeTable);
@@ -1342,43 +1484,43 @@ INNERJS
pDoc.write("");
+ pDoc.write("<\\/form>");
pDoc.write('$end_page_msg_central');
pDoc.close();
}
@@ -1427,32 +1569,32 @@ INNERJS
hDoc.$docopen;
hDoc.write('$start_page_highlight_central');
hDoc.write("");
- hDoc.write(" Keyword Highlight Options ");
+ hDoc.write(" Keyword Highlight Options<\\/span><\\/h3> ");
hDoc.write("");
hDoc.write("");
- hDoc.write("
");
+ hDoc.write("<\\/table>");
+ hDoc.write("<\\/td><\\/tr><\\/table> ");
hDoc.write(" ");
hDoc.write(" ");
- hDoc.write(" ");
+ hDoc.write("<\\/form>");
hDoc.write('$end_page_highlight_central');
hDoc.close();
}
@@ -1477,13 +1619,13 @@ sub gradeBox {
'" src="'.$request->dir_config('lonIconsURL').
'/check.gif" height="16" border="0" />';
my $wgt = &Apache::lonnet::EXT('resource.'.$partid.'.weight',$symb,$udom,$uname);
- my $wgtmsg = ($wgt > 0 ? '(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 '' ?
'' : &compute_points($$record{'resource.'.$partid.'.awarded'},$wgt));
my $result=' '."\n";
- my $display_part=&get_display_part($partid,$symb);
+ 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'};
@@ -1565,7 +1707,7 @@ sub handback_box {
''.$file_disp.' ');
$result.=' '."\n";
$result.=' ';
- $result.='(File will be uploaded when you click on Save & Next below.) ';
+ $result.='(File will be uploaded when you click on Save & Next below.) ';
$file_counter++;
}
}
@@ -1601,27 +1743,27 @@ sub show_problem {
$companswer=~s|||g;
$companswer=~s|name="submit"|name="would_have_been_submit"|g;
}
- my $result.='';
- $result.='';
- if ($viewon) {
- $result.=' ';
- if ($mode eq 'both' or $mode eq 'text') {
- $result.='View of the problem - ';
- } else {
- $result.='Correct answer: ';
- }
- $result.=$env{'form.fullname'}.' ';
- }
+ $rendered=
+ ''.
+ $rendered.
+ '
';
+ $companswer=
+ ''.
+ $companswer.
+ '
';
+ my $result;
if ($mode eq 'both') {
- $result.=''.$rendered.' ';
- $result.='Correct answer: '.$companswer;
+ $result=$rendered.$companswer;
} elsif ($mode eq 'text') {
- $result.=' '.$rendered;
+ $result=$rendered;
} elsif ($mode eq 'answer') {
- $result.=' '.$companswer;
+ $result=$companswer;
}
- $result.='
';
- $result.='
';
+ $result=''.$result.'
';
return $result;
}
@@ -1662,15 +1804,26 @@ sub download_all_link {
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 ($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 ''; }
@@ -1690,6 +1843,7 @@ sub submission {
'" src="'.$request->dir_config('lonIconsURL').
'/check.gif" height="16" border="0" />';
+ my %old_essays;
# header info
if ($counter == 0) {
&sub_page_js($request);
@@ -1702,12 +1856,6 @@ sub submission {
$request->print(' Submission Record '."\n".
' Resource: '.$env{'form.probTitle'}.' '."\n");
- if ($env{'form.handgrade'} eq 'no') {
- my $checkMark=' Note: Part(s) graded correct by the computer is marked with a '.
- $checkIcon.' symbol.'."\n";
- $request->print($checkMark);
- }
-
# 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.vAns'} eq 'yes') {
@@ -1722,7 +1870,7 @@ sub submission {
&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 = ();
@@ -1741,22 +1889,22 @@ sub submission {
$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".
+ &build_section_inputs().
' '."\n".
' '."\n".
' print("\n\n".
+ ''.
+ '
'.&nameUserString(undef,$env{'form.fullname'},$uname,$udom).'
'.
+ '
'."\n");
+
if ($env{'form.vProb'} eq 'all' or $env{'form.vAns'} eq 'all') {
- $request->print('
') if ($counter > 0);
my $mode;
if ($env{'form.vProb'} eq 'all' && $env{'form.vAns'} eq 'all') {
$mode='both';
@@ -1819,7 +1973,7 @@ KEYWORDS
$mode='answer';
}
&Apache::lonxml::clear_problem_counter();
- $request->print(&show_problem($request,$symb,$uname,$udom,1,1,$mode));
+ $request->print(&show_problem($request,$symb,$uname,$udom,1,1,$mode,{'request.prefix' => 'ctr'.$counter}));
}
my %record = &Apache::lonnet::restore($symb,$env{'request.course.id'},$udom,$uname);
@@ -1827,71 +1981,31 @@ KEYWORDS
# Display student info
$request->print(($counter == 0 ? '' : '
'));
- my $result='
'."\n".
- ''."\n";
-
- $result.='Fullname: '.&nameUserString(undef,$env{'form.fullname'},$uname,$udom).' '."\n";
+ my $result='';
+
+ $result.=''."\n");
# print student answer/submission
# Options are (1) Handgaded submission only
# (2) Last submission, includes submission that is not handgraded
@@ -1900,12 +2014,14 @@ KEYWORDS
# (4) The whole record for this student
if ($env{'form.lastSub'} =~ /^(lastonly|hdgrade)$/) {
my ($string,$timestamp)= &get_last_submission(\%record);
- my $lastsubonly=''.
- ($$timestamp eq '' ? '' : 'Date Submitted: '.
- $$timestamp)."
\n";
+
+ my $lastsubonly;
+
if ($$timestamp eq '') {
- $lastsubonly.=''.$$string[0];
+ $lastsubonly.=''.$$string[0].'
';
} else {
+ $lastsubonly = ' Date Submitted: '.$$timestamp."\n";
+
my %seenparts;
my @part_response_id = &flatten_responseType($responseType);
foreach my $part (@part_response_id) {
@@ -1928,26 +2044,35 @@ KEYWORDS
}
my $responsetype = $responseType->{$partid}->{$respid};
if (!exists($record{"resource.$partid.$respid.submission"})) {
- $lastsubonly.='
Part: '.
+ $lastsubonly.="\n".'Part: '.
$display_part.' ( ID '.$respid.
' ) '.
- 'Nothing submitted - no attempts ';
+ ''.&mt('Nothing submitted - no attempts').'
';
next;
}
- foreach (@$string) {
- my ($partid,$respid) = /^resource\.([^\.]*)\.([^\.]*)\.submission/;
+ foreach my $submission (@$string) {
+ my ($partid,$respid) = ($submission =~ /^resource\.([^\.]*)\.([^\.]*)\.submission/);
if (join('_',@{$part}) ne ($partid.'_'.$respid)) { next; }
- my ($ressub,$subval) = split(/:/,$_,2);
+ my ($ressub,$subval) = split(/:/,$submission,2);
# Similarity check
my $similar='';
if($env{'form.checkPlag'}){
my ($oname,$odom,$ocrsid,$oessay,$osim)=
- &most_similar($uname,$udom,$subval);
+ &most_similar($uname,$udom,$subval,\%old_essays);
if ($osim) {
$osim=int($osim*100.0);
- $similar="Essay".
- " is $osim% similar to an essay by ".
- &Apache::loncommon::plainname($oname,$odom).
+ 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).
' ';
@@ -1958,31 +2083,32 @@ KEYWORDS
($env{'form.lastSub'} eq 'hdgrade' &&
$$handgrade{$$part[0].'_'.$$part[1]} eq 'yes')) {
my $display_part=&get_display_part($partid,$symb);
- $lastsubonly.='Part: '.
+ $lastsubonly.='Part: '.
$display_part.'
( ID '.$respid.
' ) ';
my $files=&get_submitted_files($udom,$uname,$partid,$respid,\%record);
if (@$files) {
- $lastsubonly.='
Like all files provided by users, this file may contain virusses ';
+ $lastsubonly.='
'.&mt('Like all files provided by users, this file may contain virusses').' ';
my $file_counter = 0;
foreach my $file (@$files) {
- $file_counter ++;
+ $file_counter++;
&Apache::lonnet::allowuploaded('/adm/grades',$file);
$lastsubonly.='
'.$file.'';
}
$lastsubonly.='
';
}
- $lastsubonly.='
Submitted Answer: '.
+ $lastsubonly.='
'.&mt('Submitted Answer:').' '.
&cleanRecord($subval,$responsetype,$symb,$partid,
$respid,\%record,$order);
if ($similar) {$lastsubonly.="
$similar\n";}
+ $lastsubonly.='
';
}
}
}
+ $lastsubonly.=''."\n";
}
- $lastsubonly.=''."\n";
$request->print($lastsubonly);
- } elsif ($env{'form.lastSub'} eq 'datesub') {
+ } 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)$/) {
@@ -1994,13 +2120,12 @@ KEYWORDS
$request->print(' '."\n");
-
# return if view submission with no grading option
if ($env{'form.showgrading'} eq '' || (!&canmodify($usec))) {
my $toGrade.=' '."\n" if (&canmodify($usec));
- $toGrade.='
'."\n";
+ $toGrade.='
'."\n";
if (($env{'form.command'} eq 'submission') ||
($env{'form.command'} eq 'processGroup' && $counter == $total)) {
$toGrade.=''.&show_grading_menu_form($symb);
@@ -2008,45 +2133,45 @@ KEYWORDS
$request->print($toGrade);
return;
} else {
- $request->print(''."\n");
+ $request->print('
'."\n");
}
# essay grading message center
if ($env{'form.handgrade'} eq 'yes') {
+ my $result='';
+
+ $result.='
';
my ($lastname,$givenn) = split(/,/,$env{'form.fullname'});
my $msgfor = $givenn.' '.$lastname;
- if (scalar(@col_fullnames) > 0) {
- my $lastone = pop @col_fullnames;
- $msgfor .= ', '.(join ', ',@col_fullnames).' and '.$lastone.'.';
+ if (scalar(@$col_fullnames) > 0) {
+ my $lastone = pop(@$col_fullnames);
+ $msgfor .= ', '.(join ', ',@$col_fullnames).' and '.$lastone.'.';
}
$msgfor =~ s/\'/\\'/g; #' stupid emacs - no! javascript
- $result='
'."\n".
+ $result.='
'."\n".
'
'."\n";
$result.='
'.
- &mt('Compose message to student').(scalar(@col_fullnames) >= 1 ? 's' : '').' ('.
+ &mt('Compose message to student').(scalar(@$col_fullnames) >= 1 ? 's' : '').' ('.
&mt('incl. grades').' )'.
' '."\n".
' ('.
- &mt('Message will be sent when you click on Save & Next below.').")\n";
+ &mt('Message will be sent when you click on Save & Next below.').")\n";
+ $result.=' ';
$request->print($result);
}
- if ($perm{'vgr'}) {
- $request->print(' '.
- &Apache::loncommon::track_student_link(&mt('View recent activity'),
- $uname,$udom,'check'));
- }
- if ($perm{'opa'}) {
- $request->print(' '.
- &Apache::loncommon::pprmlink(&mt('Set/Change parameters'),
- $uname,$udom,$symb,'check'));
- }
my %seen = ();
my @partlist;
my @gradePartRespid;
my @part_response_id = &flatten_responseType($responseType);
+ $request->print(''.
+
+ ''.
+ '
');
foreach my $part_response_id (@part_response_id) {
my ($partid,$respid) = @{ $part_response_id };
my $part_resp = join('_',@{ $part_response_id });
@@ -2058,6 +2183,21 @@ KEYWORDS
push @gradePartRespid,$partid.'.'.$respid;
$request->print(&gradeBox($request,$symb,$uname,$udom,$counter,$partid,\%record));
}
+ $request->print('
');
+
+ $request->print('');
+ if ($perm{'vgr'}) {
+ $request->print(
+ &Apache::loncommon::track_student_link(&mt('View recent activity'),
+ $uname,$udom,'check'));
+ }
+ if ($perm{'opa'}) {
+ $request->print(
+ &Apache::loncommon::pprmlink(&mt('Set/Change parameters'),
+ $uname,$udom,$symb,'check'));
+ }
+ $request->print('
');
+
$result=' '."\n";
$result.=' '."\n";
$ctr++;
}
- $request->print($result.''."\n");
+ $request->print($result.''."\n");
+
+# Done with printing info for one student
+
+ $request->print('');#LC_grade_show_user_body
+ $request->print('');#LC_grade_show_user
+
# print end of form
if ($counter == $total) {
@@ -2097,6 +2243,62 @@ KEYWORDS
return '';
}
+sub check_collaborators {
+ my ($symb,$uname,$udom,$record,$handgrade,$counter) = @_;
+ my ($result,@col_fullnames);
+ my ($classlist,undef,$fullname) = &getclasslist('all','0');
+ foreach my $part (keys(%$handgrade)) {
+ my $ncol = &Apache::lonnet::EXT('resource.'.$part.
+ '.maxcollaborators',
+ $symb,$udom,$uname);
+ next if ($ncol <= 0);
+ $part =~ s/\_/\./g;
+ next if ($record->{'resource.'.$part.'.collaborators'} eq '');
+ my (@good_collaborators, @bad_collaborators);
+ foreach my $possible_collaborator
+ (split(/,?\s+/,$record->{'resource.'.$part.'.collaborators'})) {
+ $possible_collaborator =~ s/[\$\^\(\)]//g;
+ next if ($possible_collaborator eq '');
+ my ($co_name,$co_dom) = split(/\@|:/,$possible_collaborator);
+ $co_dom = $udom if (! defined($co_dom) || $co_dom =~ /^domain$/i);
+ next if ($co_name eq $uname && $co_dom eq $udom);
+ # Doing this grep allows 'fuzzy' specification
+ my @matches = grep(/^\Q$co_name\E:\Q$co_dom\E$/i,
+ keys(%$classlist));
+ if (! scalar(@matches)) {
+ push(@bad_collaborators, $possible_collaborator);
+ } else {
+ push(@good_collaborators, @matches);
+ }
+ }
+ if (scalar(@good_collaborators) != 0) {
+ $result.=' '.&mt('Collaborators: ');
+ foreach my $name (@good_collaborators) {
+ my ($lastname,$givenn) = split(/,/,$$fullname{$name});
+ push(@col_fullnames, $givenn.' '.$lastname);
+ $result.=$fullname->{$name}.' ';
+ }
+ $result.=' '."\n";
+ my ($part)=split(/\./,$part);
+ $result.=' '.
+ "\n";
+ }
+ if (scalar(@bad_collaborators) > 0) {
+ $result.='';
+ $result.=&mt('This student has submitted [quant,_1,invalid collaborator]: [_2]',scalar(@bad_collaborators),join(', ',@bad_collaborators));
+ $result .= '
';
+ }
+ if (scalar(@bad_collaborators > $ncol)) {
+ $result .= '';
+ $result .= &mt('This student has submitted too many '.
+ 'collaborators. Maximum is [_1].',$ncol);
+ $result .= '
';
+ }
+ }
+ return ($result,$fullname,\@col_fullnames);
+}
+
#--- Retrieve the last submission for all the parts
sub get_last_submission {
my ($returnhash)=@_;
@@ -2168,18 +2370,10 @@ sub processHandGrade {
}
my $includemsg = $env{'form.includemsg'.$ctr};
my ($subject,$message,$msgstatus) = ('','','');
- my $restitle = &Apache::lonnet::gettitle($symb);
- my $encrypturl=&Apache::lonnet::EXT('resource.0.encrypturl',
- $symb,$udom,$uname);
- my ($feedurl,$baseurl,$showsymb,$messagetail);
- $feedurl = &Apache::lonnet::clutter($url);
- if ($encrypturl =~ /^yes$/i) {
- $baseurl = &Apache::lonenc::encrypted($feedurl,1);
- $showsymb = &Apache::lonenc::encrypted($symb,1);
- } else {
- $baseurl = $feedurl;
- $showsymb = $symb;
- }
+ my $restitle = &Apache::lonnet::gettitle($symb);
+ my ($feedurl,$showsymb) =
+ &get_feedurl_and_symb($symb,$uname,$udom);
+ my $messagetail;
if ($includemsg =~ /savemsg|newmsg\Q$ctr\E/) {
$subject = $env{'form.msgsub'} if ($includemsg =~ /msgsub/);
unless ($subject=~/\w/) { $subject=&mt('Grading Feedback'); }
@@ -2192,12 +2386,12 @@ sub processHandGrade {
if ($env{'form.withgrades'.$ctr}) {
$message.="\n\nPoint".($pts > 1 ? 's':'').' awarded = '.$pts.' out of '.$wgt;
$messagetail = " for $env{'form.probTitle'} ";
+ $feedurl."?symb=$showsymb\">$env{'form.probTitle'}";
}
$msgstatus =
&Apache::lonmsg::user_normal_msg($uname,$udom,$subject,
$message.$messagetail,
- undef,$baseurl,undef,
+ undef,$feedurl,undef,
undef,undef,$showsymb,
$restitle);
$request->print(' '.&mt('Sending message to [_1]:[_2]',$uname,$udom).': '.
@@ -2214,26 +2408,16 @@ sub processHandGrade {
if ($errorflag eq 'not_allowed') {
$request->print("".&mt('Not allowed to modify grades for [_1]',"$collaborator:$udom")." ");
next;
- } else {
- if ($message ne '') {
- $encrypturl=
- &Apache::lonnet::EXT('resource.0.encrypturl',
- $symb,$udom,$collaborator);
- if ($encrypturl =~ /^yes$/i) {
- $baseurl = &Apache::lonenc::encrypted($feedurl,1);
- $showsymb = &Apache::lonenc::encrypted($symb,1);
- } else {
- $baseurl = $feedurl;
- $showsymb = $symb;
- }
- if ($env{'form.withgrades'.$ctr}) {
- $messagetail = " for $env{'form.probTitle'} ";
-
- }
- $msgstatus =
- &Apache::lonmsg::user_normal_msg($collaborator,$udom,$subject,$message.$messagetail,undef,$baseurl,undef,undef,undef,$showsymb,$restitle);
}
+ $msgstatus =
+ &Apache::lonmsg::user_normal_msg($collaborator,$udom,$subject,$message.$messagetail,undef,$baseurl,undef,undef,undef,$showsymb,$restitle);
}
}
}
@@ -2587,28 +2771,31 @@ sub handback_files {
$message .= "".&Apache::lonnet::gettitle($symb)." ";
$message .= ' The returned file(s) are named: '. $file_msg;
$message .= " and can be found in your portfolio space.";
- my $url = (&Apache::lonnet::decode_symb($symb))[2];
- my $feedurl = &Apache::lonnet::clutter($url);
- my $encrypturl=&Apache::lonnet::EXT('resource.0.encrypturl',
- $symb,$domain,$stuname);
- my ($baseurl,$showsymb);
- if ($encrypturl =~ /^yes$/i) {
- $baseurl = &Apache::lonenc::encrypted($feedurl,1);
- $showsymb = &Apache::lonenc::encrypted($symb,1);
- } else {
- $baseurl = $feedurl;
- $showsymb = $symb;
- }
+ my ($feedurl,$showsymb) =
+ &get_feedurl_and_symb($symb,$domain,$stuname);
my $restitle = &Apache::lonnet::gettitle($symb);
my $msgstatus =
&Apache::lonmsg::user_normal_msg($stuname,$domain,$subject.
' (File Returned) ['.$restitle.']',$message,undef,
- $baseurl,undef,undef,undef,$showsymb,$restitle);
+ $feedurl,undef,undef,undef,$showsymb,$restitle);
}
}
return;
}
+sub get_feedurl_and_symb {
+ my ($symb,$uname,$udom) = @_;
+ my (undef,undef,$url) = &Apache::lonnet::decode_symb($symb);
+ $url = &Apache::lonnet::clutter($url);
+ my $encrypturl=&Apache::lonnet::EXT('resource.0.encrypturl',
+ $symb,$udom,$uname);
+ if ($encrypturl =~ /^yes$/i) {
+ &Apache::lonenc::encrypted(\$url,1);
+ &Apache::lonenc::encrypted(\$symb,1);
+ }
+ return ($url,$symb);
+}
+
sub get_submitted_files {
my ($udom,$uname,$partid,$respid,$record) = @_;
my @files;
@@ -2747,7 +2934,6 @@ sub version_selected_portfile {
my $new_answer;
$env{'form.copy'} = &Apache::lonnet::getfile("/uploaded/$domain/$stu_name/portfolio$directory$file_name");
if($env{'form.copy'} eq '-1') {
- &Apache::lonnet::logthis('problem getting file '.$file_name);
$new_answer = 'problem getting file';
} else {
$new_answer = $answer_name.'.'.$version.'.'.$answer_ext;
@@ -2968,31 +3154,31 @@ sub viewgrades {
$result.=&jscriptNform($symb);
#beginning of class grading form
+ my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status'));
$result.= ' '."\n".
- ' '."\n".
+ ' '."\n".
' '."\n".
- ' '."\n".
+ &build_section_inputs().
' '."\n".
- ' '."\n".
+ ' '."\n".
' '."\n";
my $sectionClass;
+ my $section_display = join (", ",&Apache::loncommon::get_env_multiple('form.section'));
if ($env{'form.section'} eq 'all') {
$sectionClass='Class ';
} elsif ($env{'form.section'} eq 'none') {
- $sectionClass='Students in no Section ';
+ $sectionClass=&mt('Students in no Section').'';
} else {
- $sectionClass='Students in Section '.$env{'form.section'}.'';
+ $sectionClass=&mt('Students in Section(s) [_1]',$section_display).'';
}
- $result.='Assign Common Grade To '.$sectionClass;
- $result.= ' '."\n".
- '';
+ $result.=''.&mt('Assign Common Grade To [_1]',$sectionClass);
+ $result.= &Apache::loncommon::start_data_table();
#radio buttons/text box for assigning points for a section or class.
#handles different parts of a problem
my ($partlist,$handgrade,$responseType) = &response_type($symb);
my %weight = ();
my $ctsparts = 0;
- $result.=' ';
my %seen = ();
my @part_response_id = &flatten_responseType($responseType);
foreach my $part_response_id (@part_response_id) {
@@ -3004,12 +3190,14 @@ sub viewgrades {
my $wgt = &Apache::lonnet::EXT('resource.'.$partid.'.weight',$symb);
$weight{$partid} = $wgt eq '' ? '1' : $wgt;
+ $result.=&Apache::loncommon::start_data_table_row().'';
$result.=' '."\n";
$result.=' '."\n";
my $display_part=&get_display_part($partid,$symb);
- $result.=' Part: '.$display_part.' Point: ';
+ $result.=
+ 'Part: '.$display_part.' Point: ';
$result.=''.'
'.'
'."\n".
+ $result.=&Apache::loncommon::end_data_table()."\n".
' ';
$result.=' ';
+ 'onClick="javascript:resetEntry('.$ctsparts.');" />';
#table listing all the students in a section/class
#header of table
$result.= 'Assign Grade to Specific Students in '.$sectionClass;
- $result.= ' '."\n".
- ' No. '.
- ''.&nameUserString('header')." \n";
+ $result.= &Apache::loncommon::start_data_table().
+ &Apache::loncommon::start_data_table_header_row().
+ 'No. '.
+ ''.&nameUserString('header')." \n";
my (@parts) = sort(&getpartlist($symb));
my (undef,undef,$url)=&Apache::lonnet::decode_symb($symb);
my @partids = ();
@@ -3055,16 +3244,16 @@ sub viewgrades {
push(@partids, $partid);
my $display_part=&get_display_part($partid,$symb);
if ($display =~ /^Partial Credit Factor/) {
- $result.='Score Part: '.$display_part.
- ' (weight = '.$weight{$partid}.') '."\n";
+ $result.='Score Part: '.$display_part.
+ ' (weight = '.$weight{$partid}.') '."\n";
next;
} else {
$display =~s/\[Part: \Q$partid\E\]/Part:<\/b> $display_part/;
}
$display =~ s|Problem Status|Grade Status |;
- $result.=''.$display.' '."\n";
+ $result.=''.$display.' '."\n";
}
- $result.=' ';
+ $result.=&Apache::loncommon::end_data_table_header_row();
my %last_resets =
&get_last_resets($symb,$env{'request.course.id'},\@partids);
@@ -3084,14 +3273,18 @@ sub viewgrades {
$result.=&viewstudentgrade($symb,$env{'request.course.id'},
$_,$$fullname{$_},\@parts,\%weight,$ctr,\%last_resets);
}
- $result.='
';
+ $result.=&Apache::loncommon::end_data_table();
$result.=' '."\n";
$result.=' '."\n";
if (scalar(%$fullname) eq 0) {
my $colspan=3+scalar(@parts);
- $result='There are no students in section "'.$env{'form.section'}.
- '" with enrollment status "'.$env{'form.Status'}.'" to modify or grade. ';
+ my $section_display = join (", ",&Apache::loncommon::get_env_multiple('form.section'));
+ my $stu_status = join(' or ',&Apache::loncommon::get_env_multiple('form.Status'));
+ $result=''.
+ &mt('There are no students in section(s) [_1] with enrollment status [_2] to modify or grade',
+ $section_display, $stu_status).
+ ' ';
}
$result.=&show_grading_menu_form($symb);
return $result;
@@ -3103,7 +3296,7 @@ sub viewstudentgrade {
my ($uname,$udom) = split(/:/,$student);
my %record=&Apache::lonnet::restore($symb,$courseid,$udom,$uname);
my %aggregates = ();
- my $result=' '.
+ my $result=&Apache::loncommon::start_data_table_row().' '.
' '.
"\n".$ctr.' '.
' '."\n";
}
}
- $result.=' ';
+ $result.=&Apache::loncommon::end_data_table_row();
return $result;
}
@@ -3168,15 +3361,15 @@ sub editgrades {
my ($request) = @_;
my $symb=&get_symb($request);
- my $title='Current Grade Status ';
- $title.='Current Resource: '.$env{'form.probTitle'}.' '."\n";
- $title.='Section: '.$env{'form.section'}.' '."\n";
-
- my $result= ''."\n";
- $result.= ''.
- ' No. '.
- ''.&nameUserString('header')." \n";
-
+ my $section_display = join (", ",&Apache::loncommon::get_env_multiple('form.section'));
+ my $title=''.&mt('Current Grade Status').' ';
+ $title.=''.&mt('Current Resource: [_1]',$env{'form.probTitle'}).' '."\n";
+ $title.=''.&mt('Section: [_1]',$section_display).' '."\n";
+
+ my $result= &Apache::loncommon::start_data_table().
+ &Apache::loncommon::start_data_table_header_row().
+ ''.&mt('No.').' '.
+ ''.&nameUserString('header')." \n";
my %scoreptr = (
'correct' =>'correct_by_override',
'incorrect'=>'incorrect_by_override',
@@ -3201,8 +3394,8 @@ sub editgrades {
}
my (undef,undef,$url) = &Apache::lonnet::decode_symb($symb);
foreach my $partid (@partid) {
- $header .= ' Old Score '.
- ' New Score ';
+ $header .= 'Old Score '.
+ 'New Score ';
$columns{$partid}=2;
foreach my $stores (@parts) {
my ($part,$type) = &split_part_type($stores);
@@ -3211,22 +3404,23 @@ sub editgrades {
my $display=&Apache::lonnet::metadata($url,$stores.'.display');
$display =~ s/\[Part: (\w)+\]//;
$display =~ s/Number of Attempts/Tries/;
- $header .= ' Old '.$display.' '.
- ' New '.$display.' ';
+ $header .= 'Old '.$display.' '.
+ 'New '.$display.' ';
$columns{$partid}+=2;
}
}
foreach my $partid (@partid) {
my $display_part=&get_display_part($partid,$symb);
- $result .= 'Part: '.$display_part.
- ' (Weight = '.$weight{$partid}.') ';
+ $result .= 'Part: '.$display_part.
+ ' (Weight = '.$weight{$partid}.') ';
}
- $result .= ' ';
- $result .= $header;
- $result .= ' '."\n";
- my $noupdate;
+ $result .= &Apache::loncommon::end_data_table_header_row().
+ &Apache::loncommon::start_data_table_header_row().
+ $header.
+ &Apache::loncommon::end_data_table_header_row();
+ my @noupdate;
my ($updateCtr,$noupdateCtr) = (1,1);
for ($i=0; $i<$env{'form.total'}; $i++) {
my $line;
@@ -3238,7 +3432,8 @@ sub editgrades {
my $usec=$classlist->{"$uname:$udom"}[5];
if (!&canmodify($usec)) {
my $numcols=scalar(@partid)*4+2;
- $noupdate.=$line."Not allowed to modify student ";
+ push(@noupdate,
+ $line."Not allowed to modify student ");
next;
}
my %aggregate = ();
@@ -3307,7 +3502,7 @@ sub editgrades {
''.$awarded.' ';
}
}
- $line.=''."\n";
+ $line.="\n";
my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'};
my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'};
@@ -3340,10 +3535,13 @@ sub editgrades {
}
}
- $result.=' '.$updateCtr.' '.$line;
+ $result.=&Apache::loncommon::start_data_table_row().
+ ' '.$updateCtr.' '.$line.
+ &Apache::loncommon::end_data_table_row();
$updateCtr++;
} else {
- $noupdate.=' '.$noupdateCtr.' '.$line;
+ push(@noupdate,
+ ' '.$noupdateCtr.' '.$line);
$noupdateCtr++;
}
if ($aggregateflag) {
@@ -3351,16 +3549,24 @@ sub editgrades {
$cdom,$cnum);
}
}
- if ($noupdate) {
+ if (@noupdate) {
# my $numcols=(scalar(@partid)*(scalar(@parts)-1)*2)+3;
my $numcols=scalar(@partid)*4+2;
- $result .= 'No Changes Occurred For the Students Below '.$noupdate;
+ $result .= &Apache::loncommon::start_data_table_row('LC_empty_row').
+ 'No Changes Occurred For the Students Below '.
+ &Apache::loncommon::end_data_table_row();
+ foreach my $line (@noupdate) {
+ $result.=
+ &Apache::loncommon::start_data_table_row().
+ $line.
+ &Apache::loncommon::end_data_table_row();
+ }
}
- $result .= '
'."\n".
- &show_grading_menu_form ($symb);
- my $msg = 'Number of records updated = '.$rec_update.
+ $result .= &Apache::loncommon::end_data_table().
+ &show_grading_menu_form($symb);
+ my $msg = ' Number of records updated = '.$rec_update.
' for '.$count.' student'.($count <= 1 ? '' : 's').'. '.
- 'Total number of students = '.$env{'form.total'}.' ';
+ 'Total number of students = '.$env{'form.total'}.'
';
return $title.$msg.$result;
}
@@ -3368,7 +3574,7 @@ sub split_part_type {
my ($partstr) = @_;
my ($temp,@allparts)=split(/_/,$partstr);
my $type=pop(@allparts);
- my $part=join('.',@allparts);
+ my $part=join('_',@allparts);
return ($part,$type);
}
@@ -3471,6 +3677,7 @@ sub csvuploadmap_header {
my ($result) = &showResourceInfo($symb,$env{'form.probTitle'});
my $checked=(($env{'form.noFirstLine'})?' checked="checked"':'');
my $ignore=&mt('Ignore First Line');
+ $symb = &Apache::lonenc::check_encrypt($symb);
$request->print(<
Uploading Class Grades
@@ -3565,6 +3772,7 @@ sub upcsvScores_form {
my $upload=&mt("Upload Scores");
my $upfile_select=&Apache::loncommon::upfile_select_html();
my $ignore=&mt('Ignore First Line');
+ $symb = &Apache::lonenc::check_encrypt($symb);
$result.=<
@@ -3750,7 +3958,8 @@ sub csvuploadassign {
if ($wgt) {
$entries{$fields{$dest}}=~s/\s//g;
my $pcr=$entries{$fields{$dest}} / $wgt;
- my $award='correct_by_override';
+ my $award=($pcr == 0) ? 'incorrect_by_override'
+ : 'correct_by_override';
$grades{"resource.$part.awarded"}=$pcr;
$grades{"resource.$part.solved"}=$award;
$points{$part}=1;
@@ -3772,7 +3981,6 @@ sub csvuploadassign {
}
if (! %grades) { push(@skipped,"$username:$domain no data to save"); }
$grades{"resource.regrader"}="$env{'user.name'}:$env{'user.domain'}";
-# &Apache::lonnet::logthis(" storing ".(join('-',%grades)));
my $result=&Apache::lonnet::cstore(\%grades,$symb,
$env{'request.course.id'},
$domain,$username);
@@ -3839,7 +4047,7 @@ LISTJAVASCRIPT
$result.=''."\n";
$result.=' Problems from: '."\n";
- my ($titles,$symbx) = &getSymbMap($request);
+ my ($titles,$symbx) = &getSymbMap();
my ($curpage) =&Apache::lonnet::decode_symb($symb);
# my ($curpage,$mapId) =&Apache::lonnet::decode_symb($symb);
# my $type=($curpage =~ /\.(page|sequence)/);
@@ -3869,11 +4077,12 @@ LISTJAVASCRIPT
' none '."\n".
' by dates and submissions '."\n".
' all details '."\n";
-
- $result.=' '."\n".
- ' '."\n".
+
+ $result.=&build_section_inputs();
+ my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status'));
+ $result.=' '."\n".
' '."\n".
- ' '."\n".
+ ' '."\n".
' '." \n";
$result.=' '.&mt('Use CODE:').' '.
@@ -3921,7 +4130,6 @@ LISTJAVASCRIPT
}
sub getSymbMap {
- my ($request) = @_;
my $navmap = Apache::lonnavmaps::navmap->new();
my %symbx = ();
@@ -3997,7 +4205,7 @@ sub displayPage {
' '."\n".
' '."\n".
' '."\n".
- ' '."\n".
+ ' '."\n".
' '."\n".
' '."\n";
@@ -4102,31 +4310,33 @@ sub displaySubByDates {
my $isCODE=0;
my $isTask = ($symb =~/\.task$/);
if (exists($record->{'resource.CODE'})) { $isCODE=1; }
- my $studentTable=''.
- ''.
- 'Date/Time '.
- ($isCODE?'CODE ':'').
- 'Submission '.
- 'Status ';
+ my $studentTable=&Apache::loncommon::start_data_table().
+ &Apache::loncommon::start_data_table_header_row().
+ ''.&mt('Date/Time').' '.
+ ($isCODE?''.&mt('CODE').' ':'').
+ ''.&mt('Submission').' '.
+ ''.&mt('Status').' '.
+ &Apache::loncommon::end_data_table_header_row();
my ($version);
my %mark;
my %orders;
$mark{'correct_by_student'} = $checkIcon;
if (!exists($$record{'1:timestamp'})) {
- return ' Nothing submitted - no attempts ';
+ return ' '.&mt('Nothing submitted - no attempts').' ';
}
my $interaction;
for ($version=1;$version<=$$record{'version'};$version++) {
- my $timestamp = scalar(localtime($$record{$version.':timestamp'}));
+ my $timestamp =
+ &Apache::lonlocal::locallocaltime($$record{$version.':timestamp'});
if (exists($$record{$version.':resource.0.version'})) {
$interaction = $$record{$version.':resource.0.version'};
}
my $where = ($isTask ? "$version:resource.$interaction"
: "$version:resource");
- #&Apache::lonnet::logthis(" got $where");
- $studentTable.=''.$timestamp.' ';
+ $studentTable.=&Apache::loncommon::start_data_table_row().
+ ''.$timestamp.' ';
if ($isCODE) {
$studentTable.=''.$record->{$version.':resource.CODE'}.' ';
}
@@ -4145,15 +4355,14 @@ sub displaySubByDates {
my ($responseId)= ($isTask ? ($matchKey=~ /^resource\.(.*?)\.\Q$partid\E\.award$/)
: ($matchKey=~ /^resource\.\Q$partid\E\.(.*?)\.submission$/));
- #&Apache::lonnet::logthis("match $matchKey $responseId (".$$record{$version.':'.$matchKey});
- $displaySub[0].='Part: '.$display_part.' ';
- $displaySub[0].='(ID '.
+ $displaySub[0].=''.&mt('Part:').' '.$display_part.' ';
+ $displaySub[0].='('.&mt('ID').' '.
$responseId.') ';
if ($$record{"$where.$partid.tries"} eq '') {
- $displaySub[0].='Trial not counted';
+ $displaySub[0].=&mt('Trial not counted');
} else {
- $displaySub[0].='Trial '.
- $$record{"$where.$partid.tries"};
+ $displaySub[0].=&mt('Trial [_1]',
+ $$record{"$where.$partid.tries"});
}
my $responseType=($isTask ? 'Task'
: $responseType->{$partid}->{$responseId});
@@ -4193,12 +4402,12 @@ sub displaySubByDates {
}
$studentTable.=' '.$displaySub[0].' '.$displaySub[1];
if ($displaySub[2]) {
- $studentTable.='Manually graded by '.$displaySub[2];
+ $studentTable.=&mt('Manually graded by [_1]',$displaySub[2]);
}
- $studentTable.=' ';
-
+ $studentTable.=' '.
+ &Apache::loncommon::end_data_table_row();
}
- $studentTable.='
';
+ $studentTable.=&Apache::loncommon::end_data_table();
return $studentTable;
}
@@ -4366,18 +4575,88 @@ sub updateGradeByPage {
#
#------ start of section for handling grading by page/sequence ---------
+=pod
+
+=head1 Bubble sheet grading routines
+
+ For this documentation:
+
+ 'scanline' refers to the full line of characters
+ from the file that we are parsing that represents one entire sheet
+
+ 'bubble line' refers to the data
+ representing the line of bubbles that are on the physical bubble sheet
+
+
+The overall process is that a scanned in bubble sheet data is uploaded
+into a course. When a user wants to grade, they select a
+sequence/folder of resources, a file of bubble sheet info, and pick
+one of the predefined configurations for what each scanline looks
+like.
+
+Next each scanline is checked for any errors of either 'missing
+bubbles' (it's an error because it may have been mis-scanned
+because too light bubbling), 'double bubble' (each bubble line should
+have no more that one letter picked), invalid or duplicated CODE,
+invalid student ID
+
+If the CODE option is used that determines the randomization of the
+homework problems, either way the student ID is looked up into a
+username:domain.
+
+During the validation phase the instructor can choose to skip scanlines.
+
+After the validation phase, there are now 3 bubble sheet files
+
+ scantron_original_filename (unmodified original file)
+ scantron_corrected_filename (file where the corrected information has replaced the original information)
+ scantron_skipped_filename (contains the exact text of scanlines that where skipped)
+
+Also there is a separate hash nohist_scantrondata that contains extra
+correction information that isn't representable in the bubble sheet
+file (see &scantron_getfile() for more information)
+
+After all scanlines are either valid, marked as valid or skipped, then
+foreach line foreach problem in the picked sequence, an ssi request is
+made that simulates a user submitting their selected letter(s) against
+the homework problem.
+
+=over 4
+
+
+
+=item defaultFormData
+
+ Returns html hidden inputs used to hold context/default values.
+
+ Arguments:
+ $symb - $symb of the current resource
+
+=cut
+
sub defaultFormData {
my ($symb)=@_;
- return '
- '."\n".
+ return ' '."\n".
' '."\n".
' '."\n";
}
+
+=pod
+
+=item getSequenceDropDown
+
+ Return html dropdown of possible sequences to grade
+
+ Arguments:
+ $symb - $symb of the current resource
+
+=cut
+
sub getSequenceDropDown {
- my ($request,$symb)=@_;
+ my ($symb)=@_;
my $result=''."\n";
- my ($titles,$symbx) = &getSymbMap($request);
+ my ($titles,$symbx) = &getSymbMap();
my ($curpage)=&Apache::lonnet::decode_symb($symb);
my $ctr=0;
foreach (@$titles) {
@@ -4391,6 +4670,15 @@ sub getSequenceDropDown {
return $result;
}
+
+=pod
+
+=item scantron_filenames
+
+ Returns a list of the scantron files in the current course
+
+=cut
+
sub scantron_filenames {
my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'};
my $cname=$env{'course.'.$env{'request.course.id'}.'.num'};
@@ -4406,6 +4694,17 @@ sub scantron_filenames {
return @possiblenames;
}
+=pod
+
+=item scantron_uploads
+
+ Returns html drop-down list of scantron files in current course.
+
+ Arguments:
+ $file2grade - filename to set as selected in the dropdown
+
+=cut
+
sub scantron_uploads {
my ($file2grade) = @_;
my $result= '';
@@ -4417,6 +4716,15 @@ sub scantron_uploads {
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";
@@ -4431,6 +4739,15 @@ sub scantron_scantab {
return $result;
}
+=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'};
@@ -4445,23 +4762,48 @@ sub scantron_CODElist {
return $namechoice;
}
+=pod
+
+=item scantron_CODEunique
+
+ Returns the html for "Each CODE to be used once" radio.
+
+=cut
+
sub scantron_CODEunique {
my $result='
Yes
+ value="yes" checked="checked" />'.&mt('Yes').'
No
+ value="no" />'.&mt('No').'
';
return $result;
}
+=pod
+
+=item scantron_selectphase
+
+ Generates the initial screen to start the bubble sheet process.
+ Allows for - starting a grading run.
+ - downloading existing scan data (original, corrected
+ or skipped info)
+
+ - uploading new scan data
+
+ Arguments:
+ $r - The Apache request object
+ $file2grade - name of the file that contain the scanned data to score
+
+=cut
+
sub scantron_selectphase {
my ($r,$file2grade) = @_;
my ($symb)=&get_symb($r);
if (!$symb) {return '';}
- my $sequence_selector=&getSequenceDropDown($r,$symb);
+ 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);
@@ -4469,8 +4811,9 @@ sub scantron_selectphase {
my $CODE_selector=&scantron_CODElist();
my $CODE_unique=&scantron_CODEunique();
my $result;
- #FIXME allow instructor to be able to download the scantron file
- # and to upload it,
+
+ # Chunk of form to prompt for a file to grade and how:
+
$result.= <
@@ -4503,7 +4846,7 @@ sub scantron_selectphase {
Options:
Do only previously skipped records
- Remove all exisiting corrections
+ Remove all existing corrections
Skip hidden resources when grading
@@ -4523,6 +4866,8 @@ SCANTRONFORM
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(<
@@ -4568,6 +4913,10 @@ UPLOAD
SCANTRONFORM
}
+
+ # Chunk of the form that prompts to view a scoring office file,
+ # corrected file, skipped records in a file.
+
$r->print(<
@@ -4594,14 +4943,70 @@ SCANTRONFORM
SCANTRONFORM
- $r->print(<
-$grading_menu_button
-SCANTRONFORM
-
+ $r->print('');
+ &Apache::lonpickcode::code_list($r,2);
+ $r->print('
');
+ $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');
@@ -4634,6 +5039,25 @@ sub get_scantron_config {
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;
@@ -4644,8 +5068,50 @@ 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
+
+ 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');
@@ -4702,6 +5168,25 @@ sub scantron_fixup_scanline {
return $line;
}
+=pod
+
+=item scan_data
+
+ Edit or look up an item in the scan_data hash.
+
+ Arguments:
+ $scan_data - The hash (see scantron_getfile)
+ $key - shorthand of the key to edit (actual key is
+ scantronfilename_key).
+ $data - New value of the hash entry.
+ $delete - If true, the entry is removed from the hash.
+
+ Returns:
+ The new value of the hash table field (undefined if deleted).
+
+=cut
+
+
sub scan_data {
my ($scan_data,$key,$value,$delete)=@_;
my $filename=$env{'form.scantron_selectfile'};
@@ -4712,11 +5197,69 @@ sub scan_data {
return $scan_data->{$filename.'_'.$key};
}
+=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,$whichline,$scantron_config,$scan_data,$justHeader)=@_;
+ my ($line,$whichline,$scantron_config,$scan_data,$just_header)=@_;
+
my %record;
- my $questions=substr($line,$$scantron_config{'Qstart'}-1);
- my $data=substr($line,0,$$scantron_config{'Qstart'}-1);
+ 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 ||
@@ -4746,72 +5289,168 @@ sub scantron_parse_scanline {
$record{'scantron.LastName'}=
substr($data,$$scantron_config{'LastName'}-1,
$$scantron_config{'LastNamelength'});
- if ($justHeader) { return \%record; }
+ if ($just_header) { return \%record; }
my @alphabet=('A'..'Z');
my $questnum=0;
- while ($questions) {
+ 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;
+
+
+
$questnum++;
- my $currentquest=substr($questions,0,$$scantron_config{'Qlength'});
- substr($questions,0,$$scantron_config{'Qlength'})='';
- if (length($currentquest) < $$scantron_config{'Qlength'}) { next; }
+ my $currentquest = substr($questions,0,$answer_length);
+ $questions = substr($questions,0,$answer_length)='';
+ if (length($currentquest) < $answer_length) { next; }
+
+ # Qon letter implies for each slot in currentquest we have:
+ # ? or * for doubles a letter in A-Z for a bubble and
+ # about anything else (esp. a value of Qoff for missing
+ # bubbles.
+
+
if ($$scantron_config{'Qon'} eq 'letter') {
- if ($currentquest eq '?'
- || $currentquest eq '*') {
+
+ if ($currentquest =~ /\?/
+ || $currentquest =~ /\*/
+ || (&occurence_count($currentquest, "[A-Z]") > 1)) {
push(@{$record{'scantron.doubleerror'}},$questnum);
- $record{"scantron.$questnum.answer"}='';
+ for (my $ans = 0; $ans < $answers_needed; $ans++) {
+ my $bubble = substr($currentquest, $ans, 1);
+ if ($bubble =~ /[A-Z]/ ) {
+ $record{"scantron.$ansnum.answer"} = $bubble;
+ } else {
+ $record{"scantron.$ansnum.answer"}='';
+ }
+ $ansnum++;
+ }
+
} elsif (!defined($currentquest)
- || $currentquest eq $$scantron_config{'Qoff'}
- || $currentquest !~ /^[A-Z]$/) {
- $record{"scantron.$questnum.answer"}='';
+ || (&occurence_count($currentquest, $$scantron_config{'Qoff'}) == length($currentquest))
+ || (&occurence_count($currentquest, "[A-Z]") == 0)) {
+ for (my $ans = 0; $ans < $answers_needed; $ans++ ) {
+ $record{"scantron.$ansnum.answer"}='';
+ $ansnum++;
+
+ }
if (!&scan_data($scan_data,"$whichline.no_bubble.$questnum")) {
push(@{$record{"scantron.missingerror"}},$questnum);
+ # $ansnum += $answers_needed;
}
} else {
- $record{"scantron.$questnum.answer"}=$currentquest;
+ for (my $ans = 0; $ans < $answers_needed; $ans++) {
+ $record{"scantron.$ansnum.answer"} = substr($currentquest, $ans, 1);
+ $ansnum++;
+ }
}
+
+ # Qon 'number' implies each slot gives a digit that indexes the
+ # the bubbles filled or Qoff or a non number for unbubbled lines.
+ # and *? for double bubbles on a line.
+ # these answers are also stored as letters.
+
} elsif ($$scantron_config{'Qon'} eq 'number') {
- if ($currentquest eq '?'
- || $currentquest eq '*') {
+ if ($currentquest =~ /\?/
+ || $currentquest =~ /\*/
+ || (&occurence_count($currentquest, '\d') > 1)) {
push(@{$record{'scantron.doubleerror'}},$questnum);
- $record{"scantron.$questnum.answer"}='';
+ for (my $ans = 0; $ans < $answers_needed; $ans++) {
+ my $bubble = substr($currentquest, $ans, 1);
+ if ($bubble =~ /\d/) {
+ $record{"scantron.$ansnum.answer"} = $alphabet[$bubble];
+ } else {
+ $record{"scantron.$ansnum.answer"}=' ';
+ }
+ $ansnum++;
+ }
+
} elsif (!defined($currentquest)
- || $currentquest eq $$scantron_config{'Qoff'}
- || $currentquest !~ /^\d$/) {
- $record{"scantron.$questnum.answer"}='';
+ || (&occurence_count($currentquest,$$scantron_config{'Qoff'}) == length($currentquest))
+ || (&occurence_count($currentquest, '\d') == 0)) {
+ for (my $ans = 0; $ans < $answers_needed; $ans++ ) {
+ $record{"scantron.$ansnum.answer"}='';
+ $ansnum++;
+
+ }
if (!&scan_data($scan_data,"$whichline.no_bubble.$questnum")) {
push(@{$record{"scantron.missingerror"}},$questnum);
+ $ansnum += $answers_needed;
}
+
} else {
- # wrap zero back to J
- if ($currentquest eq '0') {
- $record{"scantron.$questnum.answer"}=
- $alphabet[9];
- } else {
- $record{"scantron.$questnum.answer"}=
- $alphabet[$currentquest-1];
+ $currentquest = &digits_to_letters($currentquest);
+ for (my $ans =0; $ans < $answers_needed; $ans++) {
+ $record{"scantron.$ansnum.answer"} = substr($currentquest, $ans, 1);
+ $ansnum++;
}
}
} else {
+
+ # 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'},$currentquest,-1);
- if (length($array[0]) eq $$scantron_config{'Qlength'}) {
- $record{"scantron.$questnum.answer"}='';
+
+ # If the split only giveas us one element.. the full length of the
+ # answser string, no bubbles are filled in:
+
+ 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.$questnum")) {
push(@{$record{"scantron.missingerror"}},$questnum);
}
- } else {
- $record{"scantron.$questnum.answer"}=
- $alphabet[length($array[0])];
+ } elsif (scalar(@array) lt 2) {
+
+ my $location = length($array[0]);
+ my $line_num = $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++;
+ }
}
- if (scalar(@array) gt 2) {
+ # If there's more than one instance of a bubble character
+ # That's a double bubble; with positional notation we can
+ # record all the bubbles filled in as well as the
+ # fact this response consists of multiple bubbles.
+ #
+ else {
push(@{$record{'scantron.doubleerror'}},$questnum);
+
+ my $first_answer = $ansnum;
+ for (my $ans =0; $ans < $answers_needed; $ans++) {
+ my $item = $first_answer+$ans;
+ $record{"scantron.$item.answer"} = '';
+ }
+
my @ans=@array;
- my $i=length($ans[0]);shift(@ans);
+ my $i=0;
+ my $increment = 0;
while ($#ans) {
- $i+=length($ans[0])+1;
- $record{"scantron.$questnum.answer"}.=$alphabet[$i];
+ $i+=length($ans[0]) + $increment;
+ my $line = int($i/$$scantron_config{'Qlength'} + $first_answer);
+ my $bubble = $i%$$scantron_config{'Qlength'};
+ $record{"scantron.$line.answer"}.=$alphabet[$bubble];
shift(@ans);
+ $increment = 1;
}
+ $ansnum += $answers_needed;
}
}
}
@@ -4819,6 +5458,24 @@ sub scantron_parse_scanline {
return \%record;
}
+=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,
@@ -4827,6 +5484,24 @@ sub scantron_add_delay {
);
}
+=pod
+
+=item scantron_find_student
+
+ Finds the username for the current scanline
+
+ Arguments:
+ $scantron_record - hash result from scantron_parse_scanline
+ $scan_data - hash of correction information
+ (see &scantron_getfile() form more information)
+ $idmap - hash from &username_to_idmap()
+ $line - number of current scanline
+
+ Returns:
+ Either 'username:domain' or undef if unknown
+
+=cut
+
sub scantron_find_student {
my ($scantron_record,$scan_data,$idmap,$line)=@_;
my $scanID=$$scantron_record{'scantron.ID'};
@@ -4841,6 +5516,15 @@ sub scantron_find_student {
return undef;
}
+=pod
+
+=item scantron_filter
+
+ Filter sub for lonnavmaps, filters out hidden resources if ignore
+ hidden resources was selected
+
+=cut
+
sub scantron_filter {
my ($curres)=@_;
@@ -4857,6 +5541,15 @@ sub scantron_filter {
return 0;
}
+=pod
+
+=item scantron_process_corrections
+
+ Gets correction information out of submitted form data and corrects
+ the scanline
+
+=cut
+
sub scantron_process_corrections {
my ($r) = @_;
my %scantron_config=&get_scantron_config($env{'form.scantron_format'});
@@ -4914,12 +5607,30 @@ sub scantron_process_corrections {
}
}
+=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'));
@@ -4931,6 +5642,14 @@ sub start_skipping {
&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_/) {
@@ -4946,6 +5665,15 @@ sub should_be_skipped {
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;
@@ -4959,6 +5687,16 @@ sub remember_current_skipped {
&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' ) {
@@ -4966,6 +5704,15 @@ sub check_for_error {
}
}
+=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'});
@@ -4998,6 +5745,15 @@ $CODElist
STUFF
}
+=pod
+
+=item scantron_do_warning
+
+ Check if the operator has picked something for all required
+ fields. Error out if something is missing.
+
+=cut
+
sub scantron_do_warning {
my ($r)=@_;
my ($symb)=&get_symb($r);
@@ -5029,6 +5785,14 @@ STUFF
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";
+ $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);
@@ -5053,7 +5839,7 @@ sub scantron_validate_file {
my $default_form_data=&defaultFormData($symb);
# do the detection of only doing skipped records first befroe we delete
- # them when doing the corrections reset
+ # them when doing the corrections reset
if ($env{'form.scantron_options_redo'} ne 'redo_skipped_ready') {
&reset_skipping_status();
}
@@ -5072,7 +5858,7 @@ sub scantron_validate_file {
if ($env{'form.scantron_corrections'}) {
&scantron_process_corrections($r);
}
- $r->print("Gathering neccessary info.
");$r->rflush();
+ $r->print("Gathering necessary info.
");$r->rflush();
#get the student pick code ready
$r->print(&Apache::loncommon::studentbrowser_javascript());
my $max_bubble=&scantron_get_maxbubble();
@@ -5089,6 +5875,7 @@ sub scantron_validate_file {
}
my $currentphase=$env{'form.validatepass'};
+
my $stop=0;
while (!$stop && $currentphase < scalar(@validate_phases)) {
$r->print(" Validating ".$validate_phases[$currentphase]."
");
@@ -5129,6 +5916,17 @@ STUFF
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'};
@@ -5143,6 +5941,18 @@ sub scantron_remove_file {
return &Apache::lonnet::removeuserfile($cname,$cdom,$file);
}
+
+=pod
+
+=item scantron_remove_scan_data
+
+ Removes all scan_data correction for the requested bubble sheet
+ data file. (In the case that both the are doing skipped records we need
+ to remember the old skipped lines for the time being so that element
+ persists for a while.)
+
+=cut
+
sub scantron_remove_scan_data {
my $cname=$env{'course.'.$env{'request.course.id'}.'.num'};
my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'};
@@ -5165,6 +5975,51 @@ sub scantron_remove_scan_data {
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'};
@@ -5197,6 +6052,21 @@ sub scantron_getfile {
return (\%scanlines,\%scan_data);
}
+=pod
+
+=item lonnet_putfile
+
+ Wrapper routine to call &Apache::lonnet::finishuserfileupload
+
+ Arguments:
+ $contents - data to store
+ $filename - filename to store $contents into
+
+ Returns:
+ result value from &Apache::lonnet::finishuserfileupload
+
+=cut
+
sub lonnet_putfile {
my ($contents,$filename)=@_;
my $docuname=$env{'course.'.$env{'request.course.id'}.'.num'};
@@ -5206,6 +6076,22 @@ sub lonnet_putfile {
}
+=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
@@ -5226,6 +6112,28 @@ sub scantron_putfile {
&Apache::lonnet::put('nohist_scantrondata',$scan_data,$cdom,$cname);
}
+=pod
+
+=item scantron_get_line
+
+ Returns the correct version of the scanline
+
+ Arguments:
+ $scanlines - hash ref that looks like the first return value from
+ &scantron_getfile()
+ $scan_data - hash ref that looks like the second return value from
+ &scantron_getfile()
+ $i - number of the requested line (starts at 0)
+
+ Returns:
+ A scanline, (either the original or the corrected one if it
+ exists), or undef if the requested scanline should be
+ skipped. (Either because it's an skipped scanline, or it's an
+ unskipped scanline and we are not doing a 'do skipped scanlines'
+ pass.
+
+=cut
+
sub scantron_get_line {
my ($scanlines,$scan_data,$i)=@_;
if (&should_be_skipped($scanlines,$scan_data,$i)) { return undef; }
@@ -5234,6 +6142,23 @@ sub scantron_get_line {
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;
@@ -5245,6 +6170,25 @@ sub get_todo_count {
return $count;
}
+=pod
+
+=item scantron_put_line
+
+ Updates the 'corrected' or 'skipped' versions of the bubble sheet
+ data file.
+
+ Arguments:
+ $scanlines - hash ref that looks like the first return value from
+ &scantron_getfile()
+ $scan_data - hash ref that looks like the second return value from
+ &scantron_getfile()
+ $i - line number to update
+ $newline - contents of the updated scanline
+ $skip - if true make the line for skipping and update the
+ 'skipped' file
+
+=cut
+
sub scantron_put_line {
my ($scanlines,$scan_data,$i,$newline,$skip)=@_;
if ($skip) {
@@ -5255,6 +6199,21 @@ sub scantron_put_line {
$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])) {
@@ -5264,6 +6223,15 @@ sub scantron_clear_skip {
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)=@_;
@@ -5280,6 +6248,15 @@ sub scantron_filter_not_exam {
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) = @_;
@@ -5303,6 +6280,15 @@ sub scantron_validate_sequence {
return (0,$currentphase+1);
}
+=pod
+
+=item scantron_validate_ID
+
+ Validates all scanlines in the selected file to not have any
+ invalid or underspecified student IDs
+
+=cut
+
sub scantron_validate_ID {
my ($r,$currentphase) = @_;
@@ -5313,6 +6299,8 @@ sub scantron_validate_ID {
#get scantron line setup
my %scantron_config=&get_scantron_config($env{'form.scantron_format'});
my ($scanlines,$scan_data)=&scantron_getfile();
+
+ &scantron_get_maxbubble(); # parse needs the bubble_lines.. array.
my %found=('ids'=>{},'usernames'=>{});
for (my $i=0;$i<=$scanlines->{'count'};$i++) {
@@ -5365,10 +6353,40 @@ sub scantron_validate_ID {
return (0,$currentphase+1);
}
+=pod
+
+=item scantron_get_correction
+
+ Builds the interface screen to interact with the operator to fix a
+ specific error condition in a specific scanline
+
+ Arguments:
+ $r - Apache request object
+ $i - number of the current scanline
+ $scan_record - hash ref as returned from &scantron_parse_scanline()
+ $scan_config - hash ref as returned from &get_scantron_config()
+ $line - full contents of the current scanline
+ $error - error condition, valid values are
+ 'incorrectCODE', 'duplicateCODE',
+ 'doublebubble', 'missingbubble',
+ 'duplicateID', 'incorrectID'
+ $arg - extra information needed
+ For errors:
+ - duplicateID - paper number that this studentID was seen before on
+ - duplicateCODE - array ref of the paper numbers this CODE was
+ seen on before
+ - incorrectCODE - current incorrect CODE
+ - doublebubble - array ref of the bubble lines that have double
+ bubble errors
+ - missingbubble - array ref of the bubble lines that have missing
+ bubble errors
+
+=cut
+
sub scantron_get_correction {
my ($r,$i,$scan_record,$scan_config,$line,$error,$arg)=@_;
-#FIXME in the case of a duplicated ID the previous line, probaly need
+#FIXME in the case of a duplicated ID the previous line, probably need
#to show both the current line and the previous one and allow skipping
#the previous one or the current one
@@ -5467,8 +6485,10 @@ ENDSCRIPT
$r->print($message);
$r->print("Please indicate which bubble should be used for grading
");
foreach my $question (@{$arg}) {
- my $selected=$$scan_record{"scantron.$question.answer"};
- &scantron_bubble_selector($r,$scan_config,$question,split('',$selected));
+ my $selected = &get_response_bubbles($scan_record, $question);
+ my @select_array = split(/:/,$selected);
+ &scantron_bubble_selector($r,$scan_config,$question,
+ @select_array);
}
} elsif ($error eq 'missingbubble') {
$r->print("There have been no bubbles scanned for some question(s)
\n");
@@ -5478,8 +6498,9 @@ ENDSCRIPT
$r->print(' ');
foreach my $question (@{$arg}) {
- my $selected=$$scan_record{"scantron.$question.answer"};
- &scantron_bubble_selector($r,$scan_config,$question);
+ 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");
@@ -5488,32 +6509,98 @@ ENDSCRIPT
}
+=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,@selected)=@_;
+ 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("');
+
+ }
+
+ $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".
+ ' '.$alphabet[$i]." ");
+ }
+ $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);
@@ -5525,6 +6612,26 @@ sub num_matches {
return $same;
}
+=pod
+
+=item scantron_get_closely_matching_CODEs
+
+ Cycles through all CODEs and finds the set that has the greatest
+ number of same characters as the provided CODE
+
+ Arguments:
+ $allcodes - hash ref returned by &get_codes()
+ $CODE - CODE from the current scanline
+
+ Returns:
+ 2 element list
+ - first elements is number of how closely matching the best fit is
+ (5 means best set has 5 matching characters)
+ - second element is an arrary ref containing the set of valid CODEs
+ that best fit the passed in CODE
+
+=cut
+
sub scantron_get_closely_matching_CODEs {
my ($allcodes,$CODE)=@_;
my @CODEs;
@@ -5535,6 +6642,23 @@ sub scantron_get_closely_matching_CODEs
return ($#CODEs,$CODEs[-1]);
}
+=pod
+
+=item get_codes
+
+ Builds a hash which has keys of all of the valid CODEs from the selected
+ set of remembered CODEs.
+
+ Arguments:
+ $old_name - name of the set of remembered CODEs
+ $cdom - domain of the course
+ $cnum - internal course name
+
+ Returns:
+ %allcodes - keys are the valid CODEs, values are all 1
+
+=cut
+
sub get_codes {
my ($old_name, $cdom, $cnum) = @_;
if (!$old_name) {
@@ -5557,6 +6681,16 @@ sub get_codes {
return %allcodes;
}
+=pod
+
+=item scantron_validate_CODE
+
+ Validates all scanlines in the selected file to not have any
+ invalid or underspecified CODEs and that none of the codes are
+ duplicated if this was requested.
+
+=cut
+
sub scantron_validate_CODE {
my ($r,$currentphase) = @_;
my %scantron_config=&get_scantron_config($env{'form.scantron_format'});
@@ -5574,6 +6708,8 @@ sub scantron_validate_CODE {
my %allcodes=&get_codes();
+ &scantron_get_maxbubble(); # parse needs the lines per response array.
+
my ($scanlines,$scan_data)=&scantron_getfile();
for (my $i=0;$i<=$scanlines->{'count'};$i++) {
my $line=&scantron_get_line($scanlines,$scan_data,$i);
@@ -5608,6 +6744,15 @@ sub scantron_validate_CODE {
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
@@ -5617,6 +6762,9 @@ sub scantron_validate_doublebubble {
#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; }
@@ -5631,32 +6779,100 @@ sub scantron_validate_doublebubble {
return (0,$currentphase+1);
}
+=pod
+
+=item scantron_get_maxbubble
+
+ Returns the maximum number of bubble lines that are expected to
+ occur. Does this by walking the selected sequence rendering the
+ resource and then checking &Apache::lonxml::get_problem_counter()
+ for what the current value of the problem counter is.
+
+ Caches the results to $env{'form.scantron_maxbubble'},
+ $env{'form.scantron.bubble_lines.n'} 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 $navmap=Apache::lonnavmaps::navmap->new();
- my (undef,undef,$sequence)=
+ my (undef, undef, $sequence) =
&Apache::lonnet::decode_symb($env{'form.selectpage'});
+ my $navmap=Apache::lonnavmaps::navmap->new();
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()));
+ ('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\.');
- $env{'form.scantron_maxbubble'} =
- &Apache::lonxml::get_problem_counter()-1;
+ &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
@@ -5675,6 +6891,9 @@ sub scantron_validate_missingbubbles {
$scan_data);
if (!defined($$scan_record{'scantron.missingerror'})) { next; }
my @to_correct;
+
+ # Probably here's where the error is...
+
foreach my $missing (@{$$scan_record{'scantron.missingerror'}}) {
if ($missing > $max_bubble) { next; }
push(@to_correct,$missing);
@@ -5689,6 +6908,30 @@ sub scantron_validate_missingbubbles {
return (0,$currentphase+1);
}
+=pod
+
+=item scantron_process_students
+
+ Routine that does the actual grading of the bubble sheet information.
+
+ The parsed scanline hash is added to %env
+
+ Then foreach unskipped scanline it does an &Apache::lonnet::ssi()
+ foreach resource , with the form data of
+
+ 'submitted' =>'scantron'
+ 'grade_target' =>'grade',
+ 'grade_username'=> username of student
+ 'grade_domain' => domain of student
+ 'grade_courseid'=> of course
+ 'grade_symb' => symb of resource to grade
+
+ This triggers a grading pass. The problem grading code takes care
+ of converting the bubbled letter information (now in %env) into a
+ valid submission.
+
+=cut
+
sub scantron_process_students {
my ($r) = @_;
my (undef,undef,$sequence)=&Apache::lonnet::decode_symb($env{'form.selectpage'});
@@ -5723,6 +6966,9 @@ SCANTRONFORM
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++;
@@ -5773,8 +7019,6 @@ SCANTRONFORM
}
my $result=&Apache::lonnet::ssi($resource->src(),%form);
if ($result ne '') {
- &Apache::lonnet::logthis("scantron grading error -> $result");
- &Apache::lonnet::logthis("scantron grading error info name $uname domain $udom course $env{'request.course.id'} url ".$resource->src());
}
if (&Apache::loncommon::connection_aborted($r)) { last; }
}
@@ -5793,6 +7037,14 @@ SCANTRONFORM
return '';
}
+=pod
+
+=item scantron_upload_scantron_data
+
+ Creates the screen for adding a new bubble sheet data file to a course.
+
+=cut
+
sub scantron_upload_scantron_data {
my ($r)=@_;
$r->print(&Apache::loncommon::coursebrowser_javascript($env{'request.role.domain'}));
@@ -5829,6 +7081,15 @@ 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);
@@ -5884,6 +7145,14 @@ sub scantron_upload_scantron_data_save {
return '';
}
+=pod
+
+=item valid_file
+
+ Validates that the requested bubble data file exists in the course.
+
+=cut
+
sub valid_file {
my ($requested_file)=@_;
foreach my $filename (sort(&scantron_filenames())) {
@@ -5892,6 +7161,16 @@ sub valid_file {
return 0;
}
+=pod
+
+=item scantron_download_scantron_data
+
+ Shows a list of the three internal files (original, corrected,
+ skipped) for a specific bubble sheet data file that exists in the
+ course.
+
+=cut
+
sub scantron_download_scantron_data {
my ($r)=@_;
my $default_form_data=&defaultFormData(&get_symb($r,1));
@@ -5928,6 +7207,12 @@ DOWNLOAD
return '';
}
+=pod
+
+=back
+
+=cut
+
#-------- end of section for handling grading scantron forms -------
#
#-------------------------------------------------------------------
@@ -5938,7 +7223,7 @@ DOWNLOAD
sub show_grading_menu_form {
my ($symb)=@_;
my $result.=' '."\n".
- ' '."\n".
+ ' '."\n".
' '."\n".
' '."\n".
' '."\n".
@@ -5958,8 +7243,127 @@ 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 Scantron Forms'),
+ short_description =>
+ &mt('')});
+ $fields{'command'} = 'verify';
+ $url = &Apache::lonhtmlcommon::build_url('grades/',\%fields);
+ push (@menu, { url => "",
+ name => &mt('Verify Receipt'),
+ short_description =>
+ &mt('')});
+ #
+ # 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 .=' \n";
+ } else {
+ $Str .=' {'jscript'}.
+ ' onClick="javascript:checkChoice(document.forms.gradingMenu,\'5\',\'verify\')" '.
+ ' /> ';
+ $Str .= (' 'x8).
+ ' receipt: '.&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)=&get_symb($request);
if (!$symb) {return '';}
@@ -6002,9 +7406,8 @@ sub gradingmenu {
GRADINGMENUJS
&commonJSfunctions($request);
- my $result=' Manual Grading/View Submission ';
my ($table,undef,$hdgrade) = &showResourceInfo($symb,$probTitle);
- $result.=$table;
+ my $result;
my (undef,$sections) = &getclasslist('all','0');
my $savedState = &savedState();
my $saveCmd = ($$savedState{'saveCmd'} eq '' ? 'submission' : $$savedState{'saveCmd'});
@@ -6013,7 +7416,7 @@ GRADINGMENUJS
my $saveStatus = ($$savedState{'saveStatus'} eq '' ? 'Active' : $$savedState{'saveStatus'});
$result.=''."\n".
- ' '."\n".
+ ' '."\n".
' '."\n".
' '."\n".
' '."\n".
@@ -6021,88 +7424,104 @@ GRADINGMENUJS
' '."\n".
' '."\n";
- $result.=''."\n".
- ''."\n".
- ' Select a Grading/Viewing Option '."\n".
- ''."\n";
-
- $result.=''."\n";
-
- $result.=' ';
-
- $result.=''."\n".
- '
'."\n".
- '
'."\n";
+ $result.='
+
+
+
+
+
+ '.&Apache::lonstatistics::GroupSelect('group','multiple',5).'
+
+
+
+
+
+ '.&Apache::lonhtmlcommon::StatusOptions($saveStatus,undef,5,undef,'mult').'
+
+
+
+
+
+
+ '.&mt('with submissions').'
+ '.&mt('in grading queue').'
+ '.&mt('with ungraded submissions').'
+ '.&mt('with incorrect submissions').'
+ '.&mt('with any status').'
+
+
+
+
+
+
+
+
+ '.&mt('Grade Complete Folder for One Student').'
+
+
+
+
+ ';
return $result;
}
@@ -6136,15 +7555,17 @@ sub gather_clicker_ids {
# Set up a couple variables.
my $username_idx = &Apache::loncoursedata::CL_SNAME();
my $domain_idx = &Apache::loncoursedata::CL_SDOM();
+ my $status_idx = &Apache::loncoursedata::CL_STATUS();
foreach my $student (keys(%$classlist)) {
-
+ if ($classlist->{$student}->[$status_idx] ne 'Active') { next; }
my $username = $classlist->{$student}->[$username_idx];
my $domain = $classlist->{$student}->[$domain_idx];
my $clickers =
(&Apache::lonnet::userenvironment($domain,$username,'clickers'))[1];
foreach my $id (split(/\,/,$clickers)) {
$id=~s/^[\#0]+//;
+ $id=~s/[\-\:]//g;
if (exists($clicker_ids{$id})) {
$clicker_ids{$id}.=','.$username.':'.$domain;
} else {
@@ -6167,6 +7588,7 @@ sub gather_adv_clicker_ids {
(&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 {
@@ -6223,8 +7645,9 @@ sub process_clicker {
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'));
-
+ ('iclicker' => 'i>clicker',
+ 'interwrite' => 'interwrite PRS'));
+ $symb = &Apache::lonenc::check_encrypt($symb);
$result.=<
function sanitycheck() {
@@ -6272,9 +7695,9 @@ function sanitycheck() {
$type: $selectform
-$attendance:
-$personnel:
-$specific:
+ $attendance
+ $personnel
+ $specific
$pcorrect:
@@ -6312,6 +7735,7 @@ sub process_clicker_file {
$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';
}
@@ -6349,6 +7773,7 @@ sub process_clicker_file {
# 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.=(<
@@ -6370,8 +7795,13 @@ ENDHEADER
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'}).
' ';
@@ -6391,8 +7821,21 @@ ENDHEADER
$result.="\n".' ';
$correct_count++;
} elsif ($clicker_ids{$id}) {
- $result.="\n".' ';
- $student_count++;
+ 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.="".&Apache::loncommon::plainname(split(/\:/,$reguser)).' ('.$reguser.') ';
+ }
+ $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".' '.
@@ -6412,6 +7855,9 @@ ENDHEADER
$result.=''.&mt("Found [_1] entries for grading!",$correct_count).' ';
}
}
+ if ($number<1) {
+ $errormsg.="Found no questions.";
+ }
if ($errormsg) {
$result.=''.&mt($errormsg).' ';
} else {
@@ -6449,6 +7895,37 @@ sub iclicker_eval {
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);
@@ -6501,8 +7978,19 @@ ENDHEADER
my $pincorrect=$env{'form.pincorrect'};
my $storecount=0;
foreach my $key (keys(%env)) {
+ my $user='';
if ($key=~/^form\.student\:(.*)$/) {
- my $user=$1;
+ $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++) {
@@ -6544,8 +8032,7 @@ ENDHEADER
sub handler {
my $request=$_[0];
-
- &reset_perm();
+ &reset_caches();
if ($env{'browser.mathml'}) {
&Apache::loncommon::content_type($request,'text/xml');
} else {
@@ -6557,9 +8044,12 @@ sub handler {
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));
}
+
+
$request->print(&Apache::loncommon::start_page('Grading'));
if ($symb eq '' && $command eq '') {
if ($env{'user.adv'}) {
@@ -6600,7 +8090,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'}) {
@@ -6658,6 +8150,7 @@ sub handler {
}
}
$request->print(&Apache::loncommon::end_page());
+ &reset_caches();
return '';
}