--- loncom/homework/grades.pm 2007/11/16 07:56:15 1.492
+++ loncom/homework/grades.pm 2009/02/24 17:12:19 1.553
@@ -1,7 +1,7 @@
# The LearningOnline Network with CAPA
# The LON-CAPA Grading handler
#
-# $Id: grades.pm,v 1.492 2007/11/16 07:56:15 albertel Exp $
+# $Id: grades.pm,v 1.553 2009/02/24 17:12:19 biermanm Exp $
#
# Copyright Michigan State University Board of Trustees
#
@@ -26,6 +26,8 @@
# http://www.lon-capa.org/
#
+
+
package Apache::grades;
use strict;
use Apache::style;
@@ -47,88 +49,49 @@ use LONCAPA;
use POSIX qw(floor);
-my %perm=();
-my %bubble_lines_per_response = (); # no. bubble lines for each response.
- # index is "symb.part_id"
-
-my %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++;
- }
-
-}
+my %perm=();
-# Given the parsed scanline, get the response for
-# 'answer' number n:
+# These variables are used to recover from ssi errors
-sub get_response_bubbles {
- my ($parsed_line, $response) = @_;
+my $ssi_retries = 5;
+my $ssi_error;
+my $ssi_error_resource;
+my $ssi_error_message;
- 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++;
+sub ssi_with_retries {
+ my ($resource, $retries, %form) = @_;
+ my ($content, $response) = &Apache::loncommon::ssi_with_retries($resource, $retries, %form);
+ if ($response->is_error) {
+ $ssi_error = 1;
+ $ssi_error_resource = $resource;
+ $ssi_error_message = $response->code . " " . $response->message;
}
- return $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 $content;
- return scalar(@matches);
}
+#
+# Prodcuces an ssi retry failure error message to the user:
+#
-
-# Take a string known to have digits and convert all the
-# digits into letters in the range J,A..I.
-
-sub digits_to_letters {
- my ($input) = @_;
-
- my @alphabet = ('J', 'A'..'I');
-
- my @input = split(//, $input);
- my $output ='';
- for (my $i = 0; $i < scalar(@input); $i++) {
- if ($input[$i] =~ /\d/) {
- $output .= $alphabet[$input[$i]];
- } else {
- $output .= $input[$i];
- }
- }
- return $output;
+sub ssi_print_error {
+ my ($r) = @_;
+ my $helpurl = &Apache::loncommon::top_nav_help('Helpdesk');
+ $r->print('
+
+
'.&mt('An unrecoverable network error occurred:').'
+
+'.&mt('Unable to retrieve a resource from a server:').'
+'.&mt('Resource:').' '.$ssi_error_resource.'
+'.&mt('Error:').' '.$ssi_error_message.'
+
+'.
+&mt('It is recommended that you try again later, as this error may mean the server was just temporarily unavailable, or is down for maintenance.').' '.
+&mt('If the error persists, please contact the [_1] for assistance.',$helpurl).
+'
');
+ return;
}
#
@@ -239,8 +202,8 @@ sub showResourceInfo {
my %resptype = ();
my $hdgrade='no';
my %partsseen;
- foreach my $partID (sort keys(%$responseType)) {
- foreach my $resID (sort keys(%{ $responseType->{$partID} })) {
+ foreach my $partID (sort(keys(%$responseType))) {
+ foreach my $resID (sort(keys(%{ $responseType->{$partID} }))) {
my $handgrade=$$handgrade{$partID.'_'.$resID};
my $responsetype = $responseType->{$partID}->{$resID};
$hdgrade = $handgrade if ($handgrade eq 'yes');
@@ -254,9 +217,9 @@ sub showResourceInfo {
$partsseen{$partID}=1;
}
my $display_part=&get_display_part($partID,$symb);
- $result.=''.&mt('Part: [_1]',$display_part).' '.
- $resID.' '.
- ''.&mt('Type: [_1]',$responsetype).' ';
+ $result.=''.&mt('Part').': '.$display_part.
+ ' '.$resID.' '.
+ ''.&mt('Type').': '.$responsetype.' ';
# ''.&mt('Handgrade: [_1]',$handgrade).' ';
}
}
@@ -277,27 +240,28 @@ sub reset_caches {
}
sub get_analyze {
- my ($symb,$uname,$udom)=@_;
+ my ($symb,$uname,$udom,$no_increment)=@_;
my $key = "$symb\0$uname\0$udom";
return $analyze_cache{$key} if (exists($analyze_cache{$key}));
my (undef,undef,$url)=&Apache::lonnet::decode_symb($symb);
$url=&Apache::lonnet::clutter($url);
- my $subresult=&Apache::lonnet::ssi($url,
- ('grade_target' => 'analyze'),
- ('grade_domain' => $udom),
- ('grade_symb' => $symb),
- ('grade_courseid' =>
- $env{'request.course.id'}),
- ('grade_username' => $uname));
+ my $subresult=&ssi_with_retries($url, $ssi_retries,
+ ('grade_target' => 'analyze',
+ 'grade_domain' => $udom,
+ 'grade_symb' => $symb,
+ 'grade_courseid' =>
+ $env{'request.course.id'},
+ 'grade_username' => $uname,
+ 'grade_noincrement' => $no_increment));
(undef,$subresult)=split(/_HASH_REF__/,$subresult,2);
my %analyze=&Apache::lonnet::str2hash($subresult);
return $analyze_cache{$key} = \%analyze;
}
sub get_order {
- my ($partid,$respid,$symb,$uname,$udom)=@_;
- my $analyze = &get_analyze($symb,$uname,$udom);
+ my ($partid,$respid,$symb,$uname,$udom,$no_increment)=@_;
+ my $analyze = &get_analyze($symb,$uname,$udom,$no_increment);
return $analyze->{"$partid.$respid.shown"};
}
@@ -735,7 +699,7 @@ sub verifyreceipt {
my $title.=
''.
- &mt('Verifying Submission Receipt [_1]',$receipt).
+ &mt('Verifying Receipt No. [_1]',$receipt).
' '."\n".
''.&mt('Resource: [_1]',$env{'form.probTitle'}).
' '."\n";
@@ -819,16 +783,16 @@ sub listStudents {
$env{'form.probTitle'} = $env{'form.probTitle'} eq '' ?
&Apache::lonnet::gettitle($symb) : $env{'form.probTitle'};
- my $result=' '.
- &mt($viewgrade.' Submissions for a Student or a Group of Students')
+ my $result=' '
+ .&mt("$viewgrade Submissions for a Student or a Group of Students")
.' ';
my ($table,undef,$hdgrade,$partlist,$handgrade) = &showResourceInfo($symb,$env{'form.probTitle'},($env{'form.showgrading'} eq 'yes'));
my %lt = ( 'multiple' =>
- "Please select a student or group of students before clicking on the Next button.",
+ &mt("Please select a student or group of students before clicking on the Next button."),
'single' =>
- "Please select the student before clicking on the Next button.",
+ &mt("Please select the student before clicking on the Next button."),
);
%lt = &Apache::lonlocal::texthash(%lt);
$request->print(<View Problem Text: [_1]',
+ ' '.&mt('View Problem Text').': '.
' '.&mt('no').' '."\n".
' '.&mt('one student').' '."\n".
- ' '.&mt('all students').' ').' '."\n";
+ ' '.&mt('all students').' '."\n";
$gradeTable .=
- ' '.
- &mt('View Answer: [_1]',
+ ' '.&mt('View Answer').': '.
' '.&mt('no').' '."\n".
' '.&mt('one student').' '."\n".
- ' '.&mt('all students').' ').' '."\n";
+ ' '.&mt('all students').' '."\n";
my $submission_options;
if ($env{'form.handgrade'} eq 'yes' && scalar(@$partlist) > 1) {
@@ -899,18 +861,16 @@ LISTJAVASCRIPT
' '.&mt('by dates and submissions').' '."\n".
' '.&mt('all details').' ';
$gradeTable .=
- ' '.
- &mt('Submissions: [_1]',$submission_options).' '."\n";
+ ' '.&mt('Submissions').': '.$submission_options.' '."\n";
$gradeTable .=
- ' '.
- &mt('Grading Increments: [_1]',
+ ' '.&mt('Grading Increments').': '.
''.
''.&mt('Whole Points').' '.
''.&mt('Half Points').' '.
''.&mt('Quarter Points').' '.
''.&mt('Tenths of a Point').' '.
- ' ');
+ '';
$gradeTable .=
&build_section_inputs().
@@ -929,15 +889,14 @@ LISTJAVASCRIPT
&Apache::lonhtmlcommon::StatusOptions($saveStatus,undef,1,'javascript:reLoadList(this.form);')).' ';
}
- $gradeTable.=&mt('To '.lc($viewgrade).' a submission or a group of submissions, click on the check box(es) '.
- 'next to the student\'s name(s). Then click on the Next button.').' '."\n".
+ $gradeTable.=&mt('To '.lc($viewgrade)." a submission or a group of submissions, click on the check box(es) next to the student's name(s). Then click on the Next button.").' '."\n".
' '."\n";
# checkall buttons
$gradeTable.=&check_script('gradesub', 'stuinfo');
$gradeTable.=' '."\n";
+ 'value="'.&mt('Next').' →" /> '."\n";
$gradeTable.=&check_buttons();
$gradeTable.=' '.&mt('Check For Plagiarism').' ';
my ($classlist, undef, $fullname) = &getclasslist($getsec,'1',$getgroup);
@@ -1029,7 +988,7 @@ LISTJAVASCRIPT
' '.$section.($group ne '' ?'/'.$group:'').''."\n";
if ($env{'form.showgrading'} eq 'yes' && $submitonly ne 'all') {
- foreach (sort keys(%status)) {
+ foreach (sort(keys(%status))) {
next if ($_ =~ /^resource.*?submitted_by$/);
$gradeTable.=' '.&mt($status{$_}).' '."\n";
}
@@ -1057,7 +1016,7 @@ LISTJAVASCRIPT
$gradeTable.=&Apache::loncommon::end_data_table()."\n".
' '."\n";
+ 'value="'.&mt('Next').' →" />'."\n";
if ($ctr == 0) {
my $num_students=(scalar(keys(%$fullname)));
if ($num_students eq 0) {
@@ -1153,6 +1112,7 @@ sub processGroup {
#--- Javascript to handle the submission page functionality ---
sub sub_page_js {
my $request = shift;
+ my $alertmsg = &mt('A number equal or greater than 0 is expected. Entered value = ');
$request->print(<
function updateRadio(formname,id,weight) {
@@ -1163,7 +1123,7 @@ sub sub_page_js {
gradeBox.value = pts;
var resetbox = false;
if (isNaN(pts) || pts < 0) {
- alert("A number equal or greater than 0 is expected. Entered value = "+pts);
+ alert("$alertmsg"+pts);
for (var i=0; iprint(<
@@ -1440,7 +1401,7 @@ INNERJS
else return;
var cleantxt = txt.replace(new RegExp('([\\f\\n\\r\\t\\v ])+', 'g')," ");
if (cleantxt=="") {
- alert("Please select a word or group of words from document and then click this link.");
+ alert("$alertmsg");
return;
}
var nret = prompt("Add selection to keyword list? Edit if desired.",cleantxt);
@@ -1679,7 +1640,7 @@ sub gradeBox {
my $radio.=''."\n"; # display radio buttons in a nice table 10 across
while ($thisweight<=$wgt) {
- $radio.= ' '.$thisweight." \n";
@@ -1695,7 +1656,7 @@ sub gradeBox {
$wgt.')" />'."\n";
$line.='/'.$wgt.' '.$wgtmsg.
($$record{'resource.'.$partid.'.solved'} eq 'correct_by_student' ? ' '.$checkIcon : '').
- ' '."\n";
+ ' '.&mt('Grade Status').': '."\n";
$line.=''."\n";
if ($$record{'resource.'.$partid.'.solved'} eq 'excused') {
@@ -1708,9 +1669,9 @@ sub gradeBox {
$line.=''.&mt('reset status').' '."\n";
+ #&mt('Part: [_1] Points: [_2] or [_3] ',$display_part,$radio,$line);
$result .=
- &mt('Part: [_1] Points: [_2] or [_3] ',$display_part,$radio,$line);
-
+ ''.&mt('Part').': '.$display_part.' '.&mt('Points').': '.$radio.' '.&mt('or').' '.$line.' '.
$result.='
'."\n";
$result.=' '."\n".
@@ -1841,9 +1802,9 @@ sub download_all_link {
join("\n",&Apache::loncommon::get_env_multiple('form.vPart'));
my $identifier = &Apache::loncommon::get_cgi_id();
- &Apache::lonnet::appenv('cgi.'.$identifier.'.students' => $all_students,
- 'cgi.'.$identifier.'.symb' => $symb,
- 'cgi.'.$identifier.'.parts' => $parts,);
+ &Apache::lonnet::appenv({'cgi.'.$identifier.'.students' => $all_students,
+ 'cgi.'.$identifier.'.symb' => $symb,
+ 'cgi.'.$identifier.'.parts' => $parts,});
$r->print(''.
&mt('Download All Submitted Documents').' ');
return
@@ -2092,7 +2053,7 @@ KEYWORDS
$lastsubonly.="\n".'Part: '.
$display_part.' ( ID '.$respid.
' ) '.
- ''.&mt('Nothing submitted - no attempts').'
';
+ ''.&mt('Nothing submitted - no attempts.').' ';
next;
}
foreach my $submission (@$string) {
@@ -2133,7 +2094,7 @@ KEYWORDS
' ) ';
my $files=&get_submitted_files($udom,$uname,$partid,$respid,\%record);
if (@$files) {
- $lastsubonly.=''.&mt('Like all files provided by users, this file may contain virusses').' ';
+ $lastsubonly.=''.&mt('Like all files provided by users, this file may contain viruses').' ';
my $file_counter = 0;
foreach my $file (@$files) {
$file_counter++;
@@ -2224,8 +2185,8 @@ KEYWORDS
$seen{$partid}++;
next if ($$handgrade{$part_resp} ne 'yes'
&& $env{'form.lastSub'} eq 'hdgrade');
- push @partlist,$partid;
- push @gradePartRespid,$partid.'.'.$respid;
+ push(@partlist,$partid);
+ push(@gradePartRespid,$partid.'.'.$respid);
$request->print(&gradeBox($request,$symb,$uname,$udom,$counter,$partid,\%record));
}
$request->print('');
@@ -2273,7 +2234,7 @@ KEYWORDS
'7 10 '."\n";
my $nsel = ($env{'form.NTSTU'} ne '' ? $env{'form.NTSTU'} : '1');
$ntstu =~ s/$nsel $nsel;
- $endform.=&mt('[_1]student(s)',$ntstu);
+ $endform.=&mt('[quant,_1,student]',$ntstu);
$endform.=' '."\n".
' Nothing submitted - no attempts.';
+ ''.&mt('Nothing submitted - no attempts.').' ';
}
return (\@string,\$timestamp);
}
@@ -2552,7 +2513,7 @@ sub processHandGrade {
my (@parsedlist,@nextlist);
my ($nextflg) = 0;
- foreach (sort
+ foreach my $item (sort
{
if (lc($$fullname{$a}) ne lc($$fullname{$b})) {
return (lc($$fullname{$a}) cmp lc($$fullname{$b}));
@@ -2560,12 +2521,12 @@ sub processHandGrade {
return $a cmp $b;
} (keys(%$fullname))) {
if ($nextflg == 1 && $button =~ /Next$/) {
- push @parsedlist,$_;
+ push(@parsedlist,$item);
}
- $nextflg = 1 if ($_ eq $laststu);
+ $nextflg = 1 if ($item eq $laststu);
if ($button eq 'Previous') {
- last if ($_ eq $firststu);
- push @parsedlist,$_;
+ last if ($item eq $firststu);
+ push(@parsedlist,$item);
}
}
$ctr = 0;
@@ -2588,11 +2549,11 @@ sub processHandGrade {
my $submitted = 0;
my $ungraded = 0;
my $incorrect = 0;
- foreach (keys(%status)) {
- $submitted = 1 if ($status{$_} ne 'nothing');
- $ungraded = 1 if ($status{$_} =~ /^ungraded/);
- $incorrect = 1 if ($status{$_} =~ /^incorrect/);
- my ($foo,$partid,$foo1) = split(/\./,$_);
+ foreach my $item (keys(%status)) {
+ $submitted = 1 if ($status{$item} ne 'nothing');
+ $ungraded = 1 if ($status{$item} =~ /^ungraded/);
+ $incorrect = 1 if ($status{$item} =~ /^incorrect/);
+ my ($foo,$partid,$foo1) = split(/\./,$item);
if ($status{'resource.'.$partid.'.submitted_by'} ne '') {
$submitted = 0;
}
@@ -2603,7 +2564,7 @@ sub processHandGrade {
next if (!$ungraded && ($submitonly eq 'graded'));
next if (!$incorrect && $submitonly eq 'incorrect');
}
- push @nextlist,$student if ($ctr < $ntstu);
+ push(@nextlist,$student) if ($ctr < $ntstu);
last if ($ctr == $ntstu);
$ctr++;
}
@@ -2611,7 +2572,7 @@ sub processHandGrade {
$ctr = 0;
my $total = scalar(@nextlist)-1;
- foreach (sort @nextlist) {
+ foreach (sort(@nextlist)) {
my ($uname,$udom,$submitter) = split(/:/);
$env{'form.student'} = $uname;
$env{'form.userdom'} = $udom;
@@ -2657,7 +2618,7 @@ sub saveHandGrade {
}
} elsif ($dropMenu eq 'reset status'
&& exists($record{'resource.'.$new_part.'.solved'})) { #don't bother if no old records -> no attempts
- foreach my $key (keys (%record)) {
+ foreach my $key (keys(%record)) {
if ($key=~/^resource\.\Q$new_part\E\./) { $newrecord{$key} = ''; }
}
$newrecord{'resource.'.$new_part.'.regrader'}=
@@ -2692,7 +2653,7 @@ sub saveHandGrade {
&handback_files($request,$symb,$stuname,$domain,$newflg,$new_part,\%newrecord);
next;
} else {
- push @parts_graded, $new_part;
+ push(@parts_graded,$new_part);
}
if ($record{'resource.'.$new_part.'.awarded'} ne $partial) {
$newrecord{'resource.'.$new_part.'.awarded'} = $partial;
@@ -2719,7 +2680,7 @@ sub saveHandGrade {
$record{'resource.'.$new_part.'.solved'} eq 'incorrect_by_override' ||
$dropMenu eq 'reset status')
{
- push (@version_parts,$new_part);
+ push(@version_parts,$new_part);
}
}
my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'};
@@ -2767,7 +2728,7 @@ sub check_and_remove_from_queue {
sub handback_files {
my ($request,$symb,$stuname,$domain,$newflg,$new_part,$newrecord) = @_;
- my $portfolio_root = &propath($domain,$stuname).'/userfiles/portfolio';
+ my $portfolio_root = '/userfiles/portfolio';
my ($partlist,$handgrade,$responseType) = &response_type($symb);
my @part_response_id = &flatten_responseType($responseType);
@@ -2785,7 +2746,8 @@ sub handback_files {
my ($answer_name,$answer_ver,$answer_ext) =
&file_name_version_ext($answer_file);
my ($portfolio_path) = ($directory =~ /^.+$stuname\/portfolio(.*)/);
- my @dir_list = &Apache::lonnet::dirlist($portfolio_path,$domain,$stuname,$portfolio_root);
+ my $getpropath = 1;
+ my @dir_list = &Apache::lonnet::dirlist($portfolio_root.$portfolio_path,$domain,$stuname,$getpropath);
my $version = &get_next_version($answer_name, $answer_ext, \@dir_list);
# fix file name
my ($save_file_name) = (($directory.$answer_name.".$version.".$answer_ext) =~ /^.+\/${stuname}\/(.*)/);
@@ -2793,8 +2755,10 @@ sub handback_files {
$newflg.'_'.$part_resp.'_returndoc'.$file_counter,
$save_file_name);
if ($result !~ m|^/uploaded/|) {
- $request->print('An error occurred ('.$result.
- ') while trying to upload '.$newflg.'_'.$part_resp.'_returndoc'.$file_counter.' ');
+ $request->print(''.
+ &mt('An error occurred ([_1]) while trying to upload [_2].',
+ $result,$newflg.'_'.$part_resp.'_returndoc'.$file_counter).
+ ' ');
} else {
# mark the file as read only
my @files = ($save_file_name);
@@ -2891,7 +2855,7 @@ sub decrement_aggs {
if ($aggtries == $totaltries) {
$decrement{'users'} = 1;
}
- foreach my $type (keys (%decrement)) {
+ foreach my $type (keys(%decrement)) {
$$aggregate{$symb."\0".$part."\0".$type} = -$decrement{$type};
}
return;
@@ -2921,8 +2885,7 @@ sub version_portfiles {
my $version_parts = join('|',@$v_flag);
my @returned_keys;
my $parts = join('|', @$parts_graded);
- my $portfolio_root = &propath($domain,$stu_name).
- '/userfiles/portfolio';
+ my $portfolio_root = '/userfiles/portfolio';
foreach my $key (keys(%$record)) {
my $new_portfiles;
if ($key =~ /^resource\.($version_parts)\./ && $key =~ /\.portfiles$/ ) {
@@ -2933,7 +2896,8 @@ sub version_portfiles {
my ($directory,$answer_file) =($file =~ /^(.*?)([^\/]*)$/);
my ($answer_name,$answer_ver,$answer_ext) =
&file_name_version_ext($answer_file);
- my @dir_list = &Apache::lonnet::dirlist($directory,$domain,$stu_name,$portfolio_root);
+ my $getpropath = 1;
+ my @dir_list = &Apache::lonnet::dirlist($portfolio_root.$directory,$domain,$stu_name,$getpropath);
my $version = &get_next_version($answer_name, $answer_ext, \@dir_list);
my $new_answer = &version_selected_portfile($domain, $stu_name, $directory, $answer_file, $version);
if ($new_answer ne 'problem getting file') {
@@ -3013,6 +2977,7 @@ sub file_name_version_ext {
sub viewgrades_js {
my ($request) = shift;
+ my $alertmsg = &mt('A number equal or greater than 0 is expected. Entered value = ');
$request->print(<
function writePoint(partid,weight,point) {
@@ -3021,7 +2986,7 @@ sub viewgrades_js {
if (point == "textval") {
point = document.classgrade["TEXTVAL_"+partid].value;
if (isNaN(point) || parseFloat(point) < 0) {
- alert("A number equal or greater than 0 is expected. Entered value = "+parseFloat(point));
+ alert("$alertmsg"+parseFloat(point));
var resetbox = false;
for (var i=0; i'.
- &mt("Assign Common Grade To $sectionClass",$section_display).' ';
+ &mt("Assign Common Grade to [_1]",$sectionClass,$section_display).'';
$result.= &Apache::loncommon::start_data_table();
#radio buttons/text box for assigning points for a section or class.
#handles different parts of a problem
@@ -3251,8 +3216,8 @@ sub viewgrades {
my $line = ' /'.
- $weight{$partid}.' (problem weight)'."\n";
- $line.= ''."\n";
+ $line.= ''.&mt('Grade Status').': '.
' '.
@@ -3267,7 +3232,7 @@ sub viewgrades {
$result.=
&Apache::loncommon::start_data_table_row()."\n".
- &mt('Part: [_1] Points: [_2] or [_3] ',$display_part,$radio,$line).
+ ''.&mt('Part').': '.$display_part.' '.&mt('Points').': '.$radio.' '.&mt('or').' '.$line.' '.
&Apache::loncommon::end_data_table_row()."\n";
$ctsparts++;
}
@@ -3278,8 +3243,8 @@ sub viewgrades {
#table listing all the students in a section/class
#header of table
- $result.= ''.&mt('Assign Grade to Specific Students in '.$sectionClass,
- $section_display).' ';
+ $result.= ''.&mt('Assign Grade to Specific Students in ').$sectionClass,
+ $section_display.' ';
$result.= &Apache::loncommon::start_data_table().
&Apache::loncommon::start_data_table_header_row().
''.&mt('No.').' '.
@@ -3289,10 +3254,11 @@ sub viewgrades {
my @partids = ();
foreach my $part (@parts) {
my $display=&Apache::lonnet::metadata($url,$part.'.display');
- $display =~ s|^Number of Attempts|Tries |; # makes the column narrower
+ my $narrowtext = &mt('Tries');
+ $display =~ s|^Number of Attempts|$narrowtext |; # makes the column narrower
if (!$display) { $display = &Apache::lonnet::metadata($url,$part.'.name'); }
my ($partid) = &split_part_type($part);
- push(@partids, $partid);
+ push(@partids,$partid);
my $display_part=&get_display_part($partid,$symb);
if ($display =~ /^Partial Credit Factor/) {
$result.=''.
@@ -3446,7 +3412,7 @@ sub editgrades {
my $header;
while ($ctr < $env{'form.totalparts'}) {
my $partid = $env{'form.partid_'.$ctr};
- push @partid,$partid;
+ push(@partid,$partid);
$weight{$partid} = $env{'form.weight_'.$partid};
$ctr++;
}
@@ -3460,10 +3426,11 @@ sub editgrades {
if ($part !~ m/^\Q$partid\E/) { next;}
if ($type eq 'awarded' || $type eq 'solved') { next; }
my $display=&Apache::lonnet::metadata($url,$stores.'.display');
- $display =~ s/\[Part: (\w)+\]//;
- $display =~ s/Number of Attempts/Tries/;
- $header .= ' '.&mt('Old '.$display).' '.
- ''.&mt('New '.$display).' ';
+ $display =~ s/\[Part: \Q$part\E\]//;
+ my $narrowtext = &mt('Tries');
+ $display =~ s/Number of Attempts/$narrowtext/;
+ $header .= ''.&mt('Old').' '.$display.' '.
+ ''.&mt('New').' '.$display.' ';
$columns{$partid}+=2;
}
}
@@ -3805,11 +3772,12 @@ ENDPICK
}
sub checkforfile_js {
+ my $alertmsg = &mt('Please use the browse button to select a file from your local directory.');
my $result =<
function checkUpload(formname) {
if (formname.upfile.value == "") {
- alert("Please use the browse button to select a file from your local directory.");
+ alert("$alertmsg");
return false;
}
formname.submit();
@@ -3829,8 +3797,8 @@ sub upcsvScores_form {
$result.=$table;
$result.=''."\n";
$result.=''."\n";
- $result.=' '.&mt('Specify a file containing the class scores for current resource').
- '. '."\n";
+ $result.=' '.&mt('Specify a file containing the class scores for current resource.').
+ ' '."\n";
$result.=''."\n";
my $upload=&mt("Upload Scores");
my $upfile_select=&Apache::loncommon::upfile_select_html();
@@ -4042,31 +4010,31 @@ sub csvuploadassign {
$grades{$store_key}=$entries{$fields{$dest}};
}
}
- if (! %grades) { push(@skipped,"$username:$domain no data to save"); }
- $grades{"resource.regrader"}="$env{'user.name'}:$env{'user.domain'}";
- my $result=&Apache::lonnet::cstore(\%grades,$symb,
+ if (! %grades) {
+ push(@skipped,&mt("[_1]: no data to save","$username:$domain"));
+ } else {
+ $grades{"resource.regrader"}="$env{'user.name'}:$env{'user.domain'}";
+ my $result=&Apache::lonnet::cstore(\%grades,$symb,
$env{'request.course.id'},
$domain,$username);
- if ($result eq 'ok') {
- $request->print('.');
- } else {
- $request->print("
-
- Failed to save student $username:$domain.
- Message when trying to save was ($result)
-
-
" );
- }
- $request->rflush();
- $countdone++;
+ if ($result eq 'ok') {
+ $request->print('.');
+ } else {
+ $request->print("".
+ &mt("Failed to save data for student [_1]. Message when trying to save was: [_2]",
+ "$username:$domain",$result)."
");
+ }
+ $request->rflush();
+ $countdone++;
+ }
}
- $request->print(" Saved $countdone students\n");
+ $request->print(''.&mt("Saved [_1] students",$countdone)." \n");
if (@skipped) {
- $request->print('
Skipped Students ');
+ $request->print(''.&mt('Skipped Students').'
');
foreach my $student (@skipped) { $request->print("$student \n"); }
}
if (@notallowed) {
- $request->print('Students Not Allowed to Modify
');
+ $request->print(''.&mt('Students Not Allowed to Modify').'
');
foreach my $student (@notallowed) { $request->print("$student \n"); }
}
$request->print(" \n");
@@ -4083,12 +4051,13 @@ sub csvuploadassign {
sub pickStudentPage {
my ($request) = shift;
+ my $alertmsg = &mt('Please select the student you wish to grade.');
$request->print(<
function checkPickOne(formname) {
if (radioSelection(formname.student) == null) {
- alert("Please select the student you wish to grade.");
+ alert("$alertmsg");
return;
}
ptr = pullDownSelection(formname.selectpage);
@@ -4123,7 +4092,7 @@ LISTJAVASCRIPT
$ctr++;
}
$select.= '';
- $result.=&mt(' Problems from: [_1]',$select)." \n";
+ $result.=' '.&mt('Problems from').': '.$select." \n";
$ctr=0;
foreach (@$titles) {
@@ -4138,13 +4107,13 @@ LISTJAVASCRIPT
my $options =
' '.&mt('no').' '."\n".
' '.&mt('yes').' '." \n";
- $result.=' '.&mt('View Problems Text: [_1]',$options);
+ $result.=' '.&mt('View Problem Text').': '.$options;
$options =
' '.&mt('none').' '."\n".
' '.&mt('by dates and submissions').' '."\n".
' '.&mt('all details').' '."\n";
- $result.=' '.&mt('Submission Details: [_1]',$options);
+ $result.=' '.&mt('Submissions').': '.$options;
$result.=&build_section_inputs();
my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status'));
@@ -4153,12 +4122,10 @@ LISTJAVASCRIPT
' '."\n".
' '." \n";
- $result.=' '.&mt('Use CODE: [_1] ',
- ' ').
- ' '."\n";
+ $result.=' '.&mt('Use CODE').': '."\n";
$result.=' '."\n";
+ 'onClick="javascript:checkPickOne(this.form);" value="'.&mt('Next').' →" /> '."\n";
$request->print($result);
@@ -4197,7 +4164,7 @@ LISTJAVASCRIPT
}
$studentTable.=&Apache::loncommon::end_data_table()."\n";
$studentTable.=' '."\n";
+ 'onClick="javascript:checkPickOne(this.form);" value="'.&mt('Next').' →" />'."\n";
$studentTable.=&show_grading_menu_form($symb);
$request->print($studentTable);
@@ -4256,7 +4223,8 @@ sub displayPage {
my $result=' '.$env{'form.title'}.' ';
$result.=' '.&mt('Student: [_1]',&nameUserString(undef,$$fullname{$env{'form.student'}},$uname,$udom)).
' '."\n";
- if (&Apache::lonnet::validCODE($env{'form.CODE'})) {
+ $env{'form.CODE'} = uc($env{'form.CODE'});
+ if (&Apache::lonnet::validCODE(uc($env{'form.CODE'}))) {
$result.=' '.&mt('CODE: [_1]',$env{'form.CODE'}).' '."\n";
} else {
delete($env{'form.CODE'});
@@ -4333,7 +4301,7 @@ sub displayPage {
# $request->print('match='.$1." \n");
# }
# $companswer =~ s|||g;
- $studentTable.=' '.$title.' '.&mt('Correct answer: [_1]',$companswer);
+ $studentTable.=' '.$title.' '.&mt('Correct answer').': '.$companswer;
}
my %record = &Apache::lonnet::restore($symbx,$env{'request.course.id'},$udom,$uname);
@@ -4403,10 +4371,11 @@ sub displaySubByDates {
my %orders;
$mark{'correct_by_student'} = $checkIcon;
if (!exists($$record{'1:timestamp'})) {
- return ' '.&mt('Nothing submitted - no attempts').' ';
+ return ' '.&mt('Nothing submitted - no attempts.').' ';
}
my $interaction;
+ my $no_increment = 1;
for ($version=1;$version<=$$record{'version'};$version++) {
my $timestamp =
&Apache::lonlocal::locallocaltime($$record{$version.':timestamp'});
@@ -4450,7 +4419,8 @@ sub displaySubByDates {
if (!exists($orders{$partid})) { $orders{$partid}={}; }
if (!exists($orders{$partid}->{$responseId})) {
$orders{$partid}->{$responseId}=
- &get_order($partid,$responseId,$symb,$uname,$udom);
+ &get_order($partid,$responseId,$symb,$uname,$udom,
+ $no_increment);
}
$displaySub[0].=' '.
&cleanRecord($$record{$version.':'.$matchKey},$responseType,$symb,$partid,$responseId,$record,$orders{$partid}->{$responseId},"$version:",$uname,$udom).' ';
@@ -4503,12 +4473,12 @@ sub updateGradeByPage {
my ($uname,$udom) = split(/:/,$env{'form.student'});
my $usec=$classlist->{$env{'form.student'}}[5];
if (!&canmodify($usec)) {
- $request->print('Unable to modify requested student.('.$env{'form.student'}.' ');
+ $request->print(''.&mt('Unable to modify requested student ([_1])',$env{'form.student'}).' ');
$request->print(&show_grading_menu_form($env{'form.symb'}));
return;
}
my $result=' '.$env{'form.title'}.' ';
- $result.=' Student: '.&nameUserString(undef,$env{'form.fullname'},$uname,$udom).
+ $result.=' '.&mt('Student: ').&nameUserString(undef,$env{'form.fullname'},$uname,$udom).
' '."\n";
$request->print($result);
@@ -4517,7 +4487,7 @@ sub updateGradeByPage {
my ($mapUrl, $id, $resUrl) = &Apache::lonnet::decode_symb( $env{'form.page'});
my $map = $navmap->getResourceByUrl($resUrl); # add to navmaps
if (!$map) {
- $request->print('Unable to grade requested sequence. ('.$resUrl.') ');
+ $request->print(''.&mt('Unable to grade requested sequence ([_1]).',$resUrl).' ');
my ($symb)=&get_symb($request);
$request->print(&show_grading_menu_form($symb));
return;
@@ -4549,8 +4519,8 @@ sub updateGradeByPage {
&Apache::loncommon::start_data_table_row().
''.$prob.
(scalar(@{$parts}) == 1 ? ''
- : ' ('.&mt('[quant,_1, parts]',scalar(@{$parts}))
- ).') ';
+ : ' ('.&mt('[quant,_1, part]',scalar(@{$parts}))
+ .')').'';
$studentTable.=' '.$title.' ';
my %newrecord=();
@@ -4594,10 +4564,10 @@ sub updateGradeByPage {
}
my $display_part=&get_display_part($partid,$curRes->symb());
my $oldstatus = $env{'form.solved'.$question.'_'.$partid};
- $displayPts[0].=' Part: '.$display_part.' = '.
+ $displayPts[0].=' '.&mt('Part').': '.$display_part.' = '.
(($oldstatus eq 'excused') ? 'excused' : $oldpts).
' ';
- $displayPts[1].=' Part: '.$display_part.' = '.
+ $displayPts[1].=' '.&mt('Part').': '.$display_part.' = '.
(($score eq 'excused') ? 'excused' : $newpts).
' ';
$question++;
@@ -4646,9 +4616,9 @@ sub updateGradeByPage {
$studentTable.=&Apache::loncommon::end_data_table();
$studentTable.=&show_grading_menu_form($env{'form.symb'});
- my $grademsg=($changeflag == 0 ? 'No score was changed or updated.' :
- 'The scores were changed for '.
- $changeflag.' problem'.($changeflag == 1 ? '.' : 's.'));
+ my $grademsg=($changeflag == 0 ? &mt('No score was changed or updated.') :
+ &mt('The scores were changed for [quant,_1,problem].',
+ $changeflag));
$request->print($grademsg.$studentTable);
return '';
@@ -4757,6 +4727,69 @@ sub getSequenceDropDown {
return $result;
}
+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.
+
+my %subdivided_bubble_lines; # no. bubble lines for optionresponse,
+ # matchresponse or rankresponse, where
+ # an individual response can have multiple
+ # lines
+
+my %responsetype_per_response; # responsetype for each response
+
+# Save and restore the bubble lines array to the form env.
+
+
+sub save_bubble_lines {
+ foreach my $line (keys(%bubble_lines_per_response)) {
+ $env{"form.scantron.bubblelines.$line"} = $bubble_lines_per_response{$line};
+ $env{"form.scantron.first_bubble_line.$line"} =
+ $first_bubble_line{$line};
+ $env{"form.scantron.sub_bubblelines.$line"} =
+ $subdivided_bubble_lines{$line};
+ $env{"form.scantron.responsetype.$line"} =
+ $responsetype_per_response{$line};
+ }
+}
+
+
+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"};
+ $subdivided_bubble_lines{$line} =
+ $env{"form.scantron.sub_bubblelines.$line"};
+ $responsetype_per_response{$line} =
+ $env{"form.scantron.responsetype.$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;
+}
=pod
@@ -4769,8 +4802,9 @@ sub getSequenceDropDown {
sub scantron_filenames {
my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'};
my $cname=$env{'course.'.$env{'request.course.id'}.'.num'};
+ my $getpropath = 1;
my @files=&Apache::lonnet::dirlist('userfiles',$cdom,$cname,
- &propath($cdom,$cname));
+ $getpropath);
my @possiblenames;
foreach my $filename (sort(@files)) {
($filename)=split(/&/,$filename);
@@ -4813,19 +4847,76 @@ sub scantron_uploads {
=cut
sub scantron_scantab {
- my $fh=Apache::File->new($Apache::lonnet::perlvar{'lonTabDir'}.'/scantronformat.tab');
my $result=''."\n";
$result.=' '."\n";
- foreach my $line (<$fh>) {
- my ($name,$descrip)=split(/:/,$line);
- if ($name =~ /^\#/) { next; }
- $result.=''.$descrip.' '."\n";
+ my @lines = &get_scantronformat_file();
+ if (@lines > 0) {
+ foreach my $line (@lines) {
+ next if (($line =~ /^\#/) || ($line eq ''));
+ my ($name,$descrip)=split(/:/,$line);
+ $result.=''.$descrip.' '."\n";
+ }
}
$result.=' '."\n";
-
return $result;
}
+=pod
+
+=item get_scantronformat_file
+
+ Returns an array containing lines from the scantron format file for
+ the domain of the course.
+
+ If a url for a custom.tab file is listed in domain's configuration.db,
+ lines are from this file.
+
+ Otherwise, if a default.tab has been published in RES space by the
+ domainconfig user, lines are from this file.
+
+ Otherwise, fall back to getting lines from the legacy file on the
+ local server: /home/httpd/lonTabs/default_scantronformat.tab
+
+=cut
+
+sub get_scantronformat_file {
+ my $cdom= $env{'course.'.$env{'request.course.id'}.'.domain'};
+ my %domconfig = &Apache::lonnet::get_dom('configuration',['scantron'],$cdom);
+ my $gottab = 0;
+ my @lines;
+ if (ref($domconfig{'scantron'}) eq 'HASH') {
+ if ($domconfig{'scantron'}{'scantronformat'} ne '') {
+ my $formatfile = &Apache::lonnet::getfile($Apache::lonnet::perlvar{'lonDocRoot'}.$domconfig{'scantron'}{'scantronformat'});
+ if ($formatfile ne '-1') {
+ @lines = split("\n",$formatfile,-1);
+ $gottab = 1;
+ }
+ }
+ }
+ if (!$gottab) {
+ my $confname = $cdom.'-domainconfig';
+ my $default = $Apache::lonnet::perlvar{'lonDocRoot'}.'/res/'.$cdom.'/'.$confname.'/default.tab';
+ my $formatfile = &Apache::lonnet::getfile($default);
+ if ($formatfile ne '-1') {
+ @lines = split("\n",$formatfile,-1);
+ $gottab = 1;
+ }
+ }
+ if (!$gottab) {
+ my @domains = &Apache::lonnet::current_machine_domains();
+ if (grep(/^\Q$cdom\E$/,@domains)) {
+ my $fh=Apache::File->new($Apache::lonnet::perlvar{'lonTabDir'}.'/scantronformat.tab');
+ @lines = <$fh>;
+ close($fh);
+ } else {
+ my $fh=Apache::File->new($Apache::lonnet::perlvar{'lonTabDir'}.'/default_scantronformat.tab');
+ @lines = <$fh>;
+ close($fh);
+ }
+ }
+ return @lines;
+}
+
=pod
=item scantron_CODElist
@@ -4858,11 +4949,11 @@ sub scantron_CODElist {
=cut
sub scantron_CODEunique {
- my $result='
+ my $result='
'.&mt('Yes').'
-
+
'.&mt('No').'
';
@@ -4899,6 +4990,8 @@ sub scantron_selectphase {
my $CODE_unique=&scantron_CODEunique();
my $result;
+ $ssi_error = 0;
+
# Chunk of form to prompt for a file to grade and how:
$result.= '
@@ -5019,8 +5112,37 @@ sub scantron_selectphase {
');
&Apache::lonpickcode::code_list($r,2);
+
+ $r->print(' ');
$r->print($grading_menu_button);
- return
+ return;
}
=pod
@@ -5082,10 +5204,10 @@ sub scantron_selectphase {
sub get_scantron_config {
my ($which) = @_;
- my $fh=Apache::File->new($Apache::lonnet::perlvar{'lonTabDir'}.'/scantronformat.tab');
+ my @lines = &get_scantronformat_file();
my %config;
#FIXME probably should move to XML it has already gotten a bit much now
- foreach my $line (<$fh>) {
+ foreach my $line (@lines) {
my ($name,$descrip)=split(/:/,$line);
if ($name ne $which ) { next; }
chomp($line);
@@ -5098,7 +5220,7 @@ sub get_scantron_config {
$config{'IDstart'}=$config[5];
$config{'IDlength'}=$config[6];
$config{'Qstart'}=$config[7];
- $config{'Qlength'}=$config[8];
+ $config{'Qlength'}=$config[8];
$config{'Qoff'}=$config[9];
$config{'Qon'}=$config[10];
$config{'PaperID'}=$config[11];
@@ -5172,6 +5294,8 @@ sub username_to_idmap {
- 'answer'
'response' - new answer or 'none' if blank
'question' - the bubble line to change
+ 'questionnum' - the question identifier,
+ may include subquestion.
Returns:
$line - the modified scanline
@@ -5184,8 +5308,6 @@ sub username_to_idmap {
sub scantron_fixup_scanline {
my ($scantron_config,$scan_data,$line,$whichline,$field,$args)=@_;
-
-
if ($field eq 'ID') {
if (length($args->{'newid'}) > $$scantron_config{'IDlength'}) {
return ($line,1,'New value too large');
@@ -5216,58 +5338,28 @@ sub scantron_fixup_scanline {
$$scantron_config{'CODElength'})=$args->{'CODE'};
}
} elsif ($field eq 'answer') {
- &scantron_get_maxbubble(); # Need the bubble counter info.
- my $length =$scantron_config->{'Qlength'};
+ my $length=$scantron_config->{'Qlength'};
my $off=$scantron_config->{'Qoff'};
my $on=$scantron_config->{'Qon'};
- my $question_number = $args->{'question'} -1;
- my $first_position = $first_bubble_line{$question_number};
- my $bubble_count = $bubble_lines_per_response{$question_number};
- my $bubbles_per_line= $$scantron_config{'Qlength'};
- my $answer=${off}x($bubbles_per_line*$bubble_count);
- my $final_answer;
- if ($$scantron_config{'Qon'} eq 'letter' ||
- $$scantron_config{'Qon'} eq 'number') {
- $bubbles_per_line = 10;
- }
- if (defined $args->{'response'}) {
-
- if ($args->{'response'} eq 'none') {
- &scan_data($scan_data,
- "$whichline.no_bubble.".$args->{'question'},'1');
+ my $answer=${off}x$length;
+ if ($args->{'response'} eq 'none') {
+ &scan_data($scan_data,
+ "$whichline.no_bubble.".$args->{'questionnum'},'1');
+ } else {
+ if ($on eq 'letter') {
+ my @alphabet=('A'..'Z');
+ $answer=$alphabet[$args->{'response'}];
+ } elsif ($on eq 'number') {
+ $answer=$args->{'response'}+1;
+ if ($answer == 10) { $answer = '0'; }
} else {
- my ($bubble_line, $bubble_number) = split(/:/,$args->{'response'});
- if ($on eq 'letter') {
- my @alphabet=('A'..'Z');
- $answer=$alphabet[$bubble_number];
- } elsif ($on eq 'number') {
- $answer= $bubble_number+1;
- if ($answer == 10) { $answer = '0'; }
- } else {
- substr($answer,$bubble_number+$bubble_line*$bubbles_per_line,1)=$on;
- $final_answer = $answer;
- }
- &scan_data($scan_data,
- "$whichline.no_bubble.".$args->{'question'},undef,'1');
-
- # Positional notation already has the right final answer length..
-
- if (($on eq 'letter') || ($on eq 'number')) {
- for (my $l = 0; $l < $bubble_count; $l++) {
- if ($l eq $bubble_line) {
- $final_answer .= $answer;
- } else {
- $final_answer .= ' ';
- }
- }
- }
+ substr($answer,$args->{'response'},1)=$on;
}
- # $where=$length*($args->{'question'}-1)+$scantron_config->{'Qstart'};
- #substr($line,$where-1,$length)=$answer;
- substr($line,
- $scantron_config->{'Qstart'}+$first_position-1,
- $bubbles_per_line*$length) = $final_answer;
+ &scan_data($scan_data,
+ "$whichline.no_bubble.".$args->{'questionnum'},undef,'1');
}
+ my $where=$length*($args->{'question'}-1)+$scantron_config->{'Qstart'};
+ substr($line,$where-1,$length)=$answer;
}
return $line;
}
@@ -5301,6 +5393,39 @@ sub scan_data {
return $scan_data->{$filename.'_'.$key};
}
+# ----- These first few routines are general use routines.----
+
+# Return the number of occurences of a pattern in a string.
+
+sub occurence_count {
+ my ($string, $pattern) = @_;
+
+ my @matches = ($string =~ /$pattern/g);
+
+ return scalar(@matches);
+}
+
+
+# Take a string known to have digits and convert all the
+# digits into letters in the range J,A..I.
+
+sub digits_to_letters {
+ my ($input) = @_;
+
+ my @alphabet = ('J', 'A'..'I');
+
+ my @input = split(//, $input);
+ my $output ='';
+ for (my $i = 0; $i < scalar(@input); $i++) {
+ if ($input[$i] =~ /\d/) {
+ $output .= $alphabet[$input[$i]];
+ } else {
+ $output .= $input[$i];
+ }
+ }
+ return $output;
+}
+
=pod
=item scantron_parse_scanline
@@ -5362,7 +5487,8 @@ sub scantron_parse_scanline {
my ($line,$whichline,$scantron_config,$scan_data,$just_header)=@_;
my %record;
- my $questions=substr($line,$$scantron_config{'Qstart'}-1); # Answers
+ my $lastpos = $env{'form.scantron_maxbubble'}*$$scantron_config{'Qlength'};
+ my $questions=substr($line,$$scantron_config{'Qstart'}-1,$lastpos); # Answers
my $data=substr($line,0,$$scantron_config{'Qstart'}-1); # earlier stuff
if (!($$scantron_config{'CODElocation'} eq 0 ||
$$scantron_config{'CODElocation'} eq 'none')) {
@@ -5403,166 +5529,218 @@ sub scantron_parse_scanline {
$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,$answer_length);
- $questions = substr($questions,$answer_length);
- if (length($currentquest) < $answer_length) { next; }
-
- # Qon letter implies for each slot in currentquest we have:
- # ? or * for doubles a letter in A-Z for a bubble and
- # about anything else (esp. a value of Qoff for missing
- # bubbles.
-
-
- if ($$scantron_config{'Qon'} eq 'letter') {
- if ($currentquest =~ /\?/
- || $currentquest =~ /\*/
- || (&occurence_count($currentquest, "[A-Z]") > 1)) {
- push(@{$record{'scantron.doubleerror'}},$questnum);
- for (my $ans = 0; $ans < $answers_needed; $ans++) {
- my $bubble = substr($currentquest, $ans, 1);
- if ($bubble =~ /[A-Z]/ ) {
- $record{"scantron.$ansnum.answer"} = $bubble;
- } else {
- $record{"scantron.$ansnum.answer"}='';
- }
- $ansnum++;
- }
-
- } elsif (!defined($currentquest)
- || (&occurence_count($currentquest, $$scantron_config{'Qoff'}) == length($currentquest))
- || (&occurence_count($currentquest, "[A-Z]") == 0)) {
- for (my $ans = 0; $ans < $answers_needed; $ans++ ) {
- $record{"scantron.$ansnum.answer"}='';
- $ansnum++;
-
- }
- if (!&scan_data($scan_data,"$whichline.no_bubble.$questnum")) {
- push(@{$record{"scantron.missingerror"}},$questnum);
- # $ansnum += $answers_needed;
- }
- } else {
- for (my $ans = 0; $ans < $answers_needed; $ans++) {
- my $bubble = substr($currentquest, $ans, 1);
- $record{"scantron.$ansnum.answer"} = $bubble;
- $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 =~ /\?/
- || $currentquest =~ /\*/
- || (&occurence_count($currentquest, '\d') > 1)) {
- push(@{$record{'scantron.doubleerror'}},$questnum);
- for (my $ans = 0; $ans < $answers_needed; $ans++) {
- my $bubble = substr($currentquest, $ans, 1);
- if ($bubble =~ /\d/) {
- $record{"scantron.$ansnum.answer"} = $alphabet[$bubble];
- } else {
- $record{"scantron.$ansnum.answer"}=' ';
- }
- $ansnum++;
- }
-
- } elsif (!defined($currentquest)
- || (&occurence_count($currentquest,$$scantron_config{'Qoff'}) == length($currentquest))
- || (&occurence_count($currentquest, '\d') == 0)) {
- for (my $ans = 0; $ans < $answers_needed; $ans++ ) {
- $record{"scantron.$ansnum.answer"}='';
- $ansnum++;
-
- }
- if (!&scan_data($scan_data,"$whichline.no_bubble.$questnum")) {
- push(@{$record{"scantron.missingerror"}},$questnum);
- $ansnum += $answers_needed;
- }
-
- } else {
- $currentquest = &digits_to_letters($currentquest);
- for (my $ans =0; $ans < $answers_needed; $ans++) {
- $record{"scantron.$ansnum.answer"} = substr($currentquest, $ans, 1);
- $ansnum++;
- }
- }
- } else {
+ my $answer_length = ($$scantron_config{'Qlength'} * $answers_needed)
+ || 1;
+ $questnum++;
+ my $quest_id = $questnum;
+ my $currentquest = substr($questions,0,$answer_length);
+ $questions = substr($questions,$answer_length);
+ if (length($currentquest) < $answer_length) { next; }
+
+ if ($subdivided_bubble_lines{$questnum-1} =~ /,/) {
+ my $subquestnum = 1;
+ my $subquestions = $currentquest;
+ my @subanswers_needed =
+ split(/,/,$subdivided_bubble_lines{$questnum-1});
+ foreach my $subans (@subanswers_needed) {
+ my $subans_length =
+ ($$scantron_config{'Qlength'} * $subans) || 1;
+ my $currsubquest = substr($subquestions,0,$subans_length);
+ $subquestions = substr($subquestions,$subans_length);
+ $quest_id = "$questnum.$subquestnum";
+ if (($$scantron_config{'Qon'} eq 'letter') ||
+ ($$scantron_config{'Qon'} eq 'number')) {
+ $ansnum = &scantron_validator_lettnum($ansnum,
+ $questnum,$quest_id,$subans,$currsubquest,$whichline,
+ \@alphabet,\%record,$scantron_config,$scan_data);
+ } else {
+ $ansnum = &scantron_validator_positional($ansnum,
+ $questnum,$quest_id,$subans,$currsubquest,$whichline, \@alphabet,\%record,$scantron_config,$scan_data);
+ }
+ $subquestnum ++;
+ }
+ } else {
+ if (($$scantron_config{'Qon'} eq 'letter') ||
+ ($$scantron_config{'Qon'} eq 'number')) {
+ $ansnum = &scantron_validator_lettnum($ansnum,$questnum,
+ $quest_id,$answers_needed,$currentquest,$whichline,
+ \@alphabet,\%record,$scantron_config,$scan_data);
+ } else {
+ $ansnum = &scantron_validator_positional($ansnum,$questnum,
+ $quest_id,$answers_needed,$currentquest,$whichline,
+ \@alphabet,\%record,$scantron_config,$scan_data);
+ }
+ }
+ }
+ $record{'scantron.maxquest'}=$questnum;
+ return \%record;
+}
- # Otherwise there's a positional notation;
- # each bubble line requires Qlength items, and there are filled in
- # bubbles for each case where there 'Qon' characters.
- #
+sub scantron_validator_lettnum {
+ my ($ansnum,$questnum,$quest_id,$answers_needed,$currquest,$whichline,
+ $alphabet,$record,$scantron_config,$scan_data) = @_;
+
+ # Qon 'letter' implies for each slot in currquest we have:
+ # ? or * for doubles, a letter in A-Z for a bubble, and
+ # about anything else (esp. a value of Qoff) for missing
+ # bubbles.
+ #
+ # Qon 'number' implies each slot gives a digit that indexes the
+ # bubbles filled, or Qoff, or a non-number for unbubbled lines,
+ # and * or ? for double bubbles on a single line.
+ #
- my @array=split($$scantron_config{'Qon'},$currentquest,-1);
+ my $matchon;
+ if ($$scantron_config{'Qon'} eq 'letter') {
+ $matchon = '[A-Z]';
+ } elsif ($$scantron_config{'Qon'} eq 'number') {
+ $matchon = '\d';
+ }
+ my $occurrences = 0;
+ if (($responsetype_per_response{$questnum-1} eq 'essayresponse') ||
+ ($responsetype_per_response{$questnum-1} eq 'formularesponse') ||
+ ($responsetype_per_response{$questnum-1} eq 'stringresponse') ||
+ ($responsetype_per_response{$questnum-1} eq 'imageresponse') ||
+ ($responsetype_per_response{$questnum-1} eq 'reactionresponse') ||
+ ($responsetype_per_response{$questnum-1} eq 'organicresponse')) {
+ my @singlelines = split('',$currquest);
+ foreach my $entry (@singlelines) {
+ $occurrences = &occurence_count($entry,$matchon);
+ if ($occurrences > 1) {
+ last;
+ }
+ }
+ } else {
+ $occurrences = &occurence_count($currquest,$matchon);
+ }
+ if (($currquest =~ /\?/ || $currquest =~ /\*/) || ($occurrences > 1)) {
+ push(@{$record->{'scantron.doubleerror'}},$quest_id);
+ for (my $ans=0; $ans<$answers_needed; $ans++) {
+ my $bubble = substr($currquest,$ans,1);
+ if ($bubble =~ /$matchon/ ) {
+ if ($$scantron_config{'Qon'} eq 'number') {
+ if ($bubble == 0) {
+ $bubble = 10;
+ }
+ $record->{"scantron.$ansnum.answer"} =
+ $alphabet->[$bubble-1];
+ } else {
+ $record->{"scantron.$ansnum.answer"} = $bubble;
+ }
+ } else {
+ $record->{"scantron.$ansnum.answer"}='';
+ }
+ $ansnum++;
+ }
+ } elsif (!defined($currquest)
+ || (&occurence_count($currquest, $$scantron_config{'Qoff'}) == length($currquest))
+ || (&occurence_count($currquest,$matchon) == 0)) {
+ for (my $ans=0; $ans<$answers_needed; $ans++ ) {
+ $record->{"scantron.$ansnum.answer"}='';
+ $ansnum++;
+ }
+ if (!&scan_data($scan_data,"$whichline.no_bubble.$quest_id")) {
+ push(@{$record->{'scantron.missingerror'}},$quest_id);
+ }
+ } else {
+ if ($$scantron_config{'Qon'} eq 'number') {
+ $currquest = &digits_to_letters($currquest);
+ }
+ for (my $ans=0; $ans<$answers_needed; $ans++) {
+ my $bubble = substr($currquest,$ans,1);
+ $record->{"scantron.$ansnum.answer"} = $bubble;
+ $ansnum++;
+ }
+ }
+ return $ansnum;
+}
- # If the split only giveas us one element.. the full length of the
- # answser string, no bubbles are filled in:
+sub scantron_validator_positional {
+ my ($ansnum,$questnum,$quest_id,$answers_needed,$currquest,
+ $whichline,$alphabet,$record,$scantron_config,$scan_data) = @_;
- if (length($array[0]) eq $$scantron_config{'Qlength'}*$answers_needed) {
- for (my $ans = 0; $ans < $answers_needed; $ans++ ) {
- $record{"scantron.$ansnum.answer"}='';
- $ansnum++;
+ # Otherwise there's a positional notation;
+ # each bubble line requires Qlength items, and there are filled in
+ # bubbles for each case where there 'Qon' characters.
+ #
- }
- if (!&scan_data($scan_data,"$whichline.no_bubble.$questnum")) {
- push(@{$record{"scantron.missingerror"}},$questnum);
- }
-
+ my @array=split($$scantron_config{'Qon'},$currquest,-1);
+ # If the split only gives us one element.. the full length of the
+ # answer string, no bubbles are filled in:
- } elsif (scalar(@array) eq 2) {
+ if ($answers_needed eq '') {
+ return;
+ }
- my $location = length($array[0]);
- my $line_num = int($location / $$scantron_config{'Qlength'});
- my $bubble = $alphabet[$location % $$scantron_config{'Qlength'}];
-
+ if (length($array[0]) eq $$scantron_config{'Qlength'}*$answers_needed) {
+ for (my $ans=0; $ans<$answers_needed; $ans++ ) {
+ $record->{"scantron.$ansnum.answer"}='';
+ $ansnum++;
+ }
+ if (!&scan_data($scan_data,"$whichline.no_bubble.$quest_id")) {
+ push(@{$record->{"scantron.missingerror"}},$quest_id);
+ }
+ } elsif (scalar(@array) == 2) {
+ my $location = length($array[0]);
+ my $line_num = int($location / $$scantron_config{'Qlength'});
+ my $bubble = $alphabet->[$location % $$scantron_config{'Qlength'}];
+ for (my $ans=0; $ans<$answers_needed; $ans++) {
+ if ($ans eq $line_num) {
+ $record->{"scantron.$ansnum.answer"} = $bubble;
+ } else {
+ $record->{"scantron.$ansnum.answer"} = ' ';
+ }
+ $ansnum++;
+ }
+ } else {
+ # If there's more than one instance of a bubble character
+ # That's a double bubble; with positional notation we can
+ # record all the bubbles filled in as well as the
+ # fact this response consists of multiple bubbles.
+ #
+ if (($responsetype_per_response{$questnum-1} eq 'essayresponse') ||
+ ($responsetype_per_response{$questnum-1} eq 'formularesponse') ||
+ ($responsetype_per_response{$questnum-1} eq 'stringresponse') ||
+ ($responsetype_per_response{$questnum-1} eq 'imageresponse') ||
+ ($responsetype_per_response{$questnum-1} eq 'reactionresponse') ||
+ ($responsetype_per_response{$questnum-1} eq 'organicresponse')) {
+ my $doubleerror = 0;
+ while (($currquest >= $$scantron_config{'Qlength'}) &&
+ (!$doubleerror)) {
+ my $currline = substr($currquest,0,$$scantron_config{'Qlength'});
+ $currquest = substr($currquest,$$scantron_config{'Qlength'});
+ my @currarray = split($$scantron_config{'Qon'},$currline,-1);
+ if (length(@currarray) > 2) {
+ $doubleerror = 1;
+ }
+ }
+ if ($doubleerror) {
+ push(@{$record->{'scantron.doubleerror'}},$quest_id);
+ }
+ } else {
+ push(@{$record->{'scantron.doubleerror'}},$quest_id);
+ }
+ my $item = $ansnum;
+ for (my $ans=0; $ans<$answers_needed; $ans++) {
+ $record->{"scantron.$item.answer"} = '';
+ $item ++;
+ }
- for (my $ans = 0; $ans < $answers_needed; $ans++) {
- if ($ans eq $line_num) {
- $record{"scantron.$ansnum.answer"} = $bubble;
- } else {
- $record{"scantron.$ansnum.answer"} = ' ';
- }
- $ansnum++;
- }
- }
- # If there's more than one instance of a bubble character
- # That's a double bubble; with positional notation we can
- # record all the bubbles filled in as well as the
- # fact this response consists of multiple bubbles.
- #
- else {
- push(@{$record{'scantron.doubleerror'}},$questnum);
-
- my $first_answer = $ansnum;
- for (my $ans =0; $ans < $answers_needed; $ans++) {
- my $item = $first_answer+$ans;
- $record{"scantron.$item.answer"} = '';
- }
-
- my @ans=@array;
- my $i=0;
- my $increment = 0;
- while ($#ans) {
- $i+=length($ans[0]) + $increment;
- my $line = int($i/$$scantron_config{'Qlength'} + $first_answer);
- my $bubble = $i%$$scantron_config{'Qlength'};
- $record{"scantron.$line.answer"}.=$alphabet[$bubble];
- shift(@ans);
- $increment = 1;
- }
- $ansnum += $answers_needed;
- }
- }
+ my @ans=@array;
+ my $i=0;
+ my $increment = 0;
+ while ($#ans) {
+ $i+=length($ans[0]) + $increment;
+ my $line = int($i/$$scantron_config{'Qlength'} + $ansnum);
+ my $bubble = $i%$$scantron_config{'Qlength'};
+ $record->{"scantron.$line.answer"}.=$alphabet->[$bubble];
+ shift(@ans);
+ $increment = 1;
+ }
+ $ansnum += $answers_needed;
}
- $record{'scantron.maxquest'}=$questnum;
- return \%record;
+ return $ansnum;
}
=pod
@@ -5702,7 +5880,8 @@ sub scantron_process_corrections {
&scantron_fixup_scanline(\%scantron_config,$scan_data,$line,
$which,'answer',
{ 'question'=>$question,
- 'response'=>$env{"form.scantron_correct_Q_$question"}});
+ 'response'=>$env{"form.scantron_correct_Q_$question"},
+ 'questionnum'=>$env{"form.scantron_questionnum_Q_$question"}});
if ($err) { last; }
}
}
@@ -5921,6 +6100,10 @@ SCANTRONFORM
' '."\n";
$chunk .=
' '."\n";
+ $chunk .=
+ ' '."\n";
+ $chunk .=
+ ' '."\n";
$result .= $chunk;
$line++;
}
@@ -5965,7 +6148,7 @@ sub scantron_validate_file {
if ($env{'form.scantron_corrections'}) {
&scantron_process_corrections($r);
}
- $r->print(''.&mt('Gathering necessary info.').'
');$r->rflush();
+ $r->print(''.&mt('Gathering necessary information.').'
');$r->rflush();
#get the student pick code ready
$r->print(&Apache::loncommon::studentbrowser_javascript());
my $max_bubble=&scantron_get_maxbubble();
@@ -5985,7 +6168,7 @@ sub scantron_validate_file {
my $stop=0;
while (!$stop && $currentphase < scalar(@validate_phases)) {
- $r->print(' '.&mt('Validating '.$validate_phases[$currentphase]).'
');
+ $r->print(&mt('Validating '.$validate_phases[$currentphase]).' ');
$r->rflush();
my $which="scantron_validate_".$validate_phases[$currentphase];
{
@@ -5995,25 +6178,34 @@ sub scantron_validate_file {
}
if (!$stop) {
my $warning=&scantron_warning_screen('Start Grading');
- $r->print('
-'.&mt('Validation process complete.').'
-'.$warning.'
-
-
-');
-
+ $r->print(&mt('Validation process complete.').' '.
+ $warning.
+ &mt('Perform verification for each student after storage of submissions?').
+ ' '.
+ ' '.&mt('Yes').' '.
+ (' 'x3).''.
+ ' '.&mt('No').
+ ' '.
+ &mt('Grading will take longer if you use verification.').' '.
+ &mt("Alternatively, the 'Review scantron data' utility (see grading menu) can be used for all students after grading is complete.").' '.
+ ' '.
+ ' '."\n");
} else {
$r->print(' ');
$r->print(" ");
}
if ($stop) {
if ($validate_phases[$currentphase] eq 'sequence') {
- $r->print(' ');
+ $r->print(' ');
$r->print(' '.&mt('this error').' ');
$r->print(" ".&mt("Or click the 'Grading Menu' button to start over.")."
");
} else {
- $r->print(' ');
+ if ($validate_phases[$currentphase] eq 'doublebubble' || $validate_phases[$currentphase] eq 'missingbubbles') {
+ $r->print(' ');
+ } else {
+ $r->print(' ');
+ }
$r->print(' '.&mt('using corrected info').' ');
$r->print(" ");
$r->print(" ".&mt("this scanline saving it for later."));
@@ -6390,14 +6582,7 @@ sub scantron_validate_sequence {
return (0,$currentphase+1);
}
-=pod
-=item scantron_validate_ID
-
- Validates all scanlines in the selected file to not have any
- invalid or underspecified student IDs
-
-=cut
sub scantron_validate_ID {
my ($r,$currentphase) = @_;
@@ -6463,39 +6648,9 @@ 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, probably need
#to show both the current line and the previous one and allow skipping
#the previous one or the current one
@@ -6517,6 +6672,10 @@ sub scantron_get_correction {
$r->print(' '."\n");
$r->print(' '."\n");
+ # Array populated for doublebubble or
+ my @lines_to_correct; # missingbubble errors to build javascript
+ # to validate radio button checking
+
if ($error =~ /ID$/) {
if ($error eq 'incorrectID') {
$r->print("".&mt("The encoded ID is not in the classlist").
@@ -6612,112 +6771,258 @@ ENDSCRIPT
" "));
$r->print("\n ");
} elsif ($error eq 'doublebubble') {
- $r->print("
".&mt("There have been multiple bubbles scanned for a some question(s)")."
\n");
+ $r->print("".&mt("There have been multiple bubbles scanned for some question(s)")."
\n");
+
+ # The form field scantron_questions is acutally a list of line numbers.
+ # represented by this form so:
+
+ my $line_list = &questions_to_line_list($arg);
+
$r->print(' ');
+ $line_list.'" />');
$r->print($message);
$r->print("".&mt("Please indicate which bubble should be used for grading")."
");
foreach my $question (@{$arg}) {
- my $selected = &get_response_bubbles($scan_record, $question);
- my @select_array = split(/:/,$selected);
- &scantron_bubble_selector($r,$scan_config,$question,
- @select_array);
+ my @linenums = &prompt_for_corrections($r,$question,$scan_config,
+ $scan_record, $error);
+ push(@lines_to_correct,@linenums);
}
+ $r->print(&verify_bubbles_checked(@lines_to_correct));
} elsif ($error eq 'missingbubble') {
$r->print("".&mt("There have been no bubbles scanned for some question(s)")."
\n");
$r->print($message);
$r->print("".&mt("Please indicate which bubble should be used for grading.")."
");
- $r->print(&mt("Some questions have no scanned bubbles")."\n");
+ $r->print(&mt("Some questions have no scanned bubbles.")."\n");
+
+ # The form field scantron_questions is actually a list of line numbers not
+ # a list of question numbers. Therefore:
+ #
+
+ my $line_list = &questions_to_line_list($arg);
+
$r->print(' ');
+ $line_list.'" />');
foreach my $question (@{$arg}) {
- my $selected = &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);
+ my @linenums = &prompt_for_corrections($r,$question,$scan_config,
+ $scan_record, $error);
+ push(@lines_to_correct,@linenums);
}
+ $r->print(&verify_bubbles_checked(@lines_to_correct));
} else {
$r->print("\n");
+}
+sub verify_bubbles_checked {
+ my (@ansnums) = @_;
+ my $ansnumstr = join('","',@ansnums);
+ my $warning = &mt("A bubble or 'No bubble' selection has not been made for one or more lines.");
+ my $output = (<
+function verify_bubble_radio(form) {
+ var ansnumArray = new Array ("$ansnumstr");
+ var need_bubble_count = 0;
+ for (var i=0; i 1) {
+ var bubble_picked = 0;
+ for (var j=0; j
+ENDSCRIPT
+ return $output;
}
=pod
-=item scantron_bubble_selector
-
- Generates the html radiobuttons to correct a single bubble line
- possibly showing the existing the selected bubbles if known
+=item questions_to_line_list
- Arguments:
- $r - Apache request object
- $scan_config - hash from &get_scantron_config()
- $quest - number of the bubble line to make a corrector for
- @lines - array of answer lines.
+Converts a list of questions into a string of comma separated
+line numbers in the answer sheet used by the questions. This is
+used to fill in the scantron_questions form field.
-=cut
+ Arguments:
+ questions - Reference to an array of questions.
-sub scantron_bubble_selector {
- my ($r,$scan_config,$quest,@lines)=@_;
- my $max=$$scan_config{'Qlength'};
+=cut
- my $scmode=$$scan_config{'Qon'};
+sub questions_to_line_list {
+ my ($questions) = @_;
+ my @lines;
+
+ foreach my $item (@{$questions}) {
+ my $question = $item;
+ my ($first,$count,$last);
+ if ($item =~ /^(\d+)\.(\d+)$/) {
+ $question = $1;
+ my $subquestion = $2;
+ $first = $first_bubble_line{$question-1} + 1;
+ my @subans = split(/,/,$subdivided_bubble_lines{$question-1});
+ my $subcount = 1;
+ while ($subcount<$subquestion) {
+ $first += $subans[$subcount-1];
+ $subcount ++;
+ }
+ $count = $subans[$subquestion-1];
+ } else {
+ $first = $first_bubble_line{$question-1} + 1;
+ $count = $bubble_lines_per_response{$question-1};
+ }
+ $last = $first+$count-1;
+ push(@lines, ($first..$last));
+ }
+ return join(',', @lines);
+}
- my $bubble_length = scalar(@lines);
+=pod
+=item prompt_for_corrections
- if ($scmode eq 'number' || $scmode eq 'letter') { $max=10; }
+Prompts for a potentially multiline correction to the
+user's bubbling (factors out common code from scantron_get_correction
+for multi and missing bubble cases).
- my $response = $quest-1;
- my $lines = $bubble_lines_per_response{$response};
+ Arguments:
+ $r - Apache request object.
+ $question - The question number to prompt for.
+ $scan_config - The scantron file configuration hash.
+ $scan_record - Reference to the hash that has the the parsed scanlines.
+ $error - Type of error
+
+ Implicit inputs:
+ %bubble_lines_per_response - Starting line numbers for each question.
+ Numbered from 0 (but question numbers are from
+ 1.
+ %first_bubble_line - Starting bubble line for each question.
+ %subdivided_bubble_lines - optionresponse, matchresponse and rankresponse
+ type problems render as separate sub-questions,
+ in exam mode. This hash contains a
+ comma-separated list of the lines per
+ sub-question.
+ %responsetype_per_response - essayresponse, formularesponse,
+ stringresponse, imageresponse, reactionresponse,
+ and organicresponse type problem parts can have
+ multiple lines per response if the weight
+ assigned exceeds 10. In this case, only
+ one bubble per line is permitted, but more
+ than one line might contain bubbles, e.g.
+ bubbling of: line 1 - J, line 2 - J,
+ line 3 - B would assign 22 points.
- my $total_lines = $lines*2;
- my @alphabet=('A'..'Z');
+=cut
- $r->print("$quest ");
+sub prompt_for_corrections {
+ my ($r, $question, $scan_config, $scan_record, $error) = @_;
+ my ($current_line,$lines);
+ my @linenums;
+ my $questionnum = $question;
+ if ($question =~ /^(\d+)\.(\d+)$/) {
+ $question = $1;
+ $current_line = $first_bubble_line{$question-1} + 1 ;
+ my $subquestion = $2;
+ my @subans = split(/,/,$subdivided_bubble_lines{$question-1});
+ my $subcount = 1;
+ while ($subcount<$subquestion) {
+ $current_line += $subans[$subcount-1];
+ $subcount ++;
+ }
+ $lines = $subans[$subquestion-1];
+ } else {
+ $current_line = $first_bubble_line{$question-1} + 1 ;
+ $lines = $bubble_lines_per_response{$question-1};
+ }
+ if ($lines > 1) {
+ $r->print(&mt('The group of bubble lines below responds to a single question.').' ');
+ if (($responsetype_per_response{$question-1} eq 'essayresponse') ||
+ ($responsetype_per_response{$question-1} eq 'formularesponse') ||
+ ($responsetype_per_response{$question-1} eq 'stringresponse') ||
+ ($responsetype_per_response{$question-1} eq 'imageresponse') ||
+ ($responsetype_per_response{$question-1} eq 'reactionresponse') ||
+ ($responsetype_per_response{$question-1} eq 'organicresponse')) {
+ $r->print(&mt("Although this particular question type requires handgrading, the instructions for this question in the exam directed students to leave [quant,_1,line] blank on their scantron sheets.",$lines).' '.&mt('A non-zero score can be assigned to the student during scantron grading by selecting a bubble in at least one line.').' '.&mt('The score for this question will be a sum of the numeric values for the selected bubbles from each line, where A=1 point, B=2 points etc.').' '.&mt("To assign a score of zero for this question, mark all lines as 'No bubble'.").' ');
+ } else {
+ $r->print(&mt("Select at most one bubble in a single line and select 'No Bubble' in all the other lines. ")." ");
+ }
+ }
+ for (my $i =0; $i < $lines; $i++) {
+ my $selected = $$scan_record{"scantron.$current_line.answer"};
+ &scantron_bubble_selector($r,$scan_config,$current_line,
+ $questionnum,$error,split('', $selected));
+ push(@linenums,$current_line);
+ $current_line++;
+ }
+ if ($lines > 1) {
+ $r->print(" ");
+ }
+ return @linenums;
+}
- for (my $l = 0; $l < $lines; $l++) {
- if ($l != 0) {
- $r->print('');
- }
- my @selected = split(//,$lines[$l]);
- for (my $i=0;$i<$max;$i++) {
- $r->print("\n".'');
- if ($selected[0] eq $alphabet[$i]) {
- $r->print('X');
- shift(@selected) ;
- } else {
- $r->print(' ');
- }
- $r->print(' ');
-
- }
+=pod
- if ($l == 0) {
- my $lspan = $total_lines * 2; # 2 table rows per bubble line.
+=item scantron_bubble_selector
+
+ Generates the html radiobuttons to correct a single bubble line
+ possibly showing the existing the selected bubbles if known
- $r->print(' '.&mt('No bubble').' ');
-
- }
+ Arguments:
+ $r - Apache request object
+ $scan_config - hash from &get_scantron_config()
+ $line - Number of the line being displayed.
+ $questionnum - Question number (may include subquestion)
+ $error - Type of error.
+ @selected - Array of bubbles picked on this line.
- $r->print(' ');
+=cut
- # FIXME: This may have to be a bit more clever for
- # multiline questions (different values e.g..).
+sub scantron_bubble_selector {
+ my ($r,$scan_config,$line,$questionnum,$error,@selected)=@_;
+ my $max=$$scan_config{'Qlength'};
- for (my $i=0;$i<$max;$i++) {
- my $value = "$l:$i"; # Relative bubble line #: Bubble in line.
- $r->print("\n".
- ' '.$alphabet[$i]." ");
- }
- $r->print(' ');
+ my $scmode=$$scan_config{'Qon'};
+ if ($scmode eq 'number' || $scmode eq 'letter') { $max=10; }
-
- }
- $r->print('
');
+ my @alphabet=('A'..'Z');
+ $r->print(&Apache::loncommon::start_data_table().
+ &Apache::loncommon::start_data_table_row());
+ $r->print('');
+ for (my $i=0;$i<$max+1;$i++) {
+ $r->print("\n".'');
+ if ($selected[0] eq $alphabet[$i]) { $r->print('X'); shift(@selected) }
+ else { $r->print(' '); }
+ $r->print(' ');
+ }
+ $r->print(&Apache::loncommon::end_data_table_row().
+ &Apache::loncommon::start_data_table_row());
+ for (my $i=0;$i<$max;$i++) {
+ $r->print("\n".
+ ' '.$alphabet[$i]." ");
+ }
+ my $nobub_checked = ' ';
+ if ($error eq 'missingbubble') {
+ $nobub_checked = ' checked = "checked" ';
+ }
+ $r->print("\n".' '.&mt('No bubble').
+ ' '."\n".' ');
+ $r->print(&Apache::loncommon::end_data_table_row().
+ &Apache::loncommon::end_data_table());
}
=pod
@@ -6874,7 +7179,7 @@ sub scantron_validate_CODE {
$line,'duplicateCODE',$usedCODEs{$CODE});
return(1,$currentphase);
}
- push (@{$usedCODEs{$CODE}},$$scan_record{'scantron.PaperID'});
+ push(@{$usedCODEs{$CODE}},$$scan_record{'scantron.PaperID'});
}
return (0,$currentphase+1);
}
@@ -6897,7 +7202,6 @@ 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++) {
@@ -6914,24 +7218,8 @@ 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 {
+sub scantron_get_maxbubble {
if (defined($env{'form.scantron_maxbubble'}) &&
$env{'form.scantron_maxbubble'}) {
&restore_bubble_lines();
@@ -6953,45 +7241,68 @@ sub scantron_get_maxbubble {
my $total_lines = 0;
%bubble_lines_per_response = ();
%first_bubble_line = ();
-
+ %subdivided_bubble_lines = ();
+ %responsetype_per_response = ();
my $response_number = 0;
my $bubble_line = 0;
foreach my $resource (@resources) {
- my $symb = $resource->symb();
- &Apache::lonxml::clear_bubble_lines_for_part();
- my $result=&Apache::lonnet::ssi($resource->src(),
- ('symb' => $resource->symb()),
- ('grade_target' => 'analyze'),
- ('grade_courseid' => $cid),
- ('grade_domain' => $udom),
- ('grade_username' => $uname));
- my (undef, $an) =
- split(/_HASH_REF__/,$result, 2);
-
- my %analysis = &Apache::lonnet::str2hash($an);
-
-
-
- foreach my $part_id (@{$analysis{'parts'}}) {
-
- my $lines = $analysis{"$part_id.bubble_lines"};;
-
-
-
- # TODO - make this a persistent hash not an array.
-
+ my ($analysis,$parts) = &scantron_partids_tograde($resource,$cid,$uname,$udom);
+ if ((ref($analysis) eq 'HASH') && (ref($parts) eq 'ARRAY')) {
+ foreach my $part_id (@{$parts}) {
+ my $lines;
+
+ # TODO - make this a persistent hash not an array.
+
+ # optionresponse, matchresponse and rankresponse type items
+ # render as separate sub-questions in exam mode.
+ if (($analysis->{$part_id.'.type'} eq 'optionresponse') ||
+ ($analysis->{$part_id.'.type'} eq 'matchresponse') ||
+ ($analysis->{$part_id.'.type'} eq 'rankresponse')) {
+ my ($numbub,$numshown);
+ if ($analysis->{$part_id.'.type'} eq 'optionresponse') {
+ if (ref($analysis->{$part_id.'.options'}) eq 'ARRAY') {
+ $numbub = scalar(@{$analysis->{$part_id.'.options'}});
+ }
+ } elsif ($analysis->{$part_id.'.type'} eq 'matchresponse') {
+ if (ref($analysis->{$part_id.'.items'}) eq 'ARRAY') {
+ $numbub = scalar(@{$analysis->{$part_id.'.items'}});
+ }
+ } elsif ($analysis->{$part_id.'.type'} eq 'rankresponse') {
+ if (ref($analysis->{$part_id.'.foils'}) eq 'ARRAY') {
+ $numbub = scalar(@{$analysis->{$part_id.'.foils'}});
+ }
+ }
+ if (ref($analysis->{$part_id.'.shown'}) eq 'ARRAY') {
+ $numshown = scalar(@{$analysis->{$part_id.'.shown'}});
+ }
+ my $bubbles_per_line = 10;
+ my $inner_bubble_lines = int($numbub/$bubbles_per_line);
+ if (($numbub % $bubbles_per_line) != 0) {
+ $inner_bubble_lines++;
+ }
+ for (my $i=0; $i<$numshown; $i++) {
+ $subdivided_bubble_lines{$response_number} .=
+ $inner_bubble_lines.',';
+ }
+ $subdivided_bubble_lines{$response_number} =~ s/,$//;
+ $lines = $numshown * $inner_bubble_lines;
+ } else {
+ $lines = $analysis->{"$part_id.bubble_lines"};
+ }
- $first_bubble_line{$response_number} = $bubble_line;
- $bubble_lines_per_response{$response_number} = $lines;
- $response_number++;
-
- $bubble_line += $lines;
- $total_lines += $lines;
- }
+ $first_bubble_line{$response_number} = $bubble_line;
+ $bubble_lines_per_response{$response_number} = $lines;
+ $responsetype_per_response{$response_number} =
+ $analysis->{$part_id.'.type'};
+ $response_number++;
+ $bubble_line += $lines;
+ $total_lines += $lines;
+ }
+ }
}
- &Apache::lonnet::delenv('scantron\.');
+ &Apache::lonnet::delenv('scantron.');
&save_bubble_lines();
$env{'form.scantron_maxbubble'} =
@@ -6999,15 +7310,32 @@ sub scantron_get_maxbubble {
return $env{'form.scantron_maxbubble'};
}
-=pod
-
-=item scantron_validate_missingbubbles
-
- Validates all scanlines in the selected file to not have any
- answers that don't have bubbles that have not been verified
- to be bubble free.
-
-=cut
+sub scantron_partids_tograde {
+ my ($resource,$cid,$uname,$udom) = @_;
+ my (%analysis,@parts);
+
+ if (ref($resource)) {
+ my $symb = $resource->symb();
+ my $result=&ssi_with_retries($resource->src(), $ssi_retries,
+ ('symb' => $symb,
+ 'grade_target' => 'analyze',
+ 'grade_courseid' => $cid,
+ 'grade_domain' => $udom,
+ 'grade_username' => $uname));
+ my (undef, $an) = split(/_HASH_REF__/,$result, 2);
+ %analysis = &Apache::lonnet::str2hash($an);
+
+ if (ref($analysis{'parts'}) eq 'ARRAY') {
+ foreach my $part (@{$analysis{'parts'}}) {
+ my ($id,$respid) = split(/\./,$part);
+ if (!&Apache::loncommon::check_if_partid_hidden($id,$symb,$udom,$uname)) {
+ push(@parts,$part);
+ }
+ }
+ }
+ }
+ return (\%analysis,\@parts);
+}
sub scantron_validate_missingbubbles {
my ($r,$currentphase) = @_;
@@ -7031,7 +7359,25 @@ sub scantron_validate_missingbubbles {
# Probably here's where the error is...
foreach my $missing (@{$$scan_record{'scantron.missingerror'}}) {
- if ($missing > $max_bubble) { next; }
+ my $lastbubble;
+ if ($missing =~ /^(\d+)\.(\d+)$/) {
+ my $question = $1;
+ my $subquestion = $2;
+ if (!defined($first_bubble_line{$question -1})) { next; }
+ my $first = $first_bubble_line{$question-1};
+ my @subans = split(/,/,$subdivided_bubble_lines{$question-1});
+ my $subcount = 1;
+ while ($subcount<$subquestion) {
+ $first += $subans[$subcount-1];
+ $subcount ++;
+ }
+ my $count = $subans[$subquestion-1];
+ $lastbubble = $first + $count;
+ } else {
+ if (!defined($first_bubble_line{$missing - 1})) { next; }
+ $lastbubble = $first_bubble_line{$missing - 1} + $bubble_lines_per_response{$missing - 1};
+ }
+ if ($lastbubble > $max_bubble) { next; }
push(@to_correct,$missing);
}
if (@to_correct) {
@@ -7044,35 +7390,15 @@ 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'});
my ($symb)=&get_symb($r);
- if (!$symb) {return '';}
+ if (!$symb) {
+ return '';
+ }
my $default_form_data=&defaultFormData($symb);
my %scantron_config=&get_scantron_config($env{'form.scantron_format'});
@@ -7082,6 +7408,14 @@ sub scantron_process_students {
my $navmap=Apache::lonnavmaps::navmap->new();
my $map=$navmap->getResourceByUrl($sequence);
my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0);
+
+ my ($uname,$udom,%partids_by_symb);
+ foreach my $resource (@resources) {
+ my $ressymb = $resource->symb();
+ my ($analysis,$parts) =
+ &scantron_partids_tograde($resource,$env{'request.course.id'},$uname,$udom);
+ $partids_by_symb{$ressymb} = $parts;
+ }
# $r->print("geto ".scalar(@resources)." ");
my $result= <
@@ -7091,19 +7425,36 @@ SCANTRONFORM
$r->print($result);
my @delayqueue;
- my %completedstudents;
+ my (%completedstudents,%scandata);
+ my $lock=&Apache::lonnet::set_lock(&mt('Grading bubblesheet exam'));
my $count=&get_todo_count($scanlines,$scan_data);
my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin($r,'Scantron Status',
'Scantron Progress',$count,
'inline',undef,'scantronupload');
&Apache::lonhtmlcommon::Update_PrgWin($r,\%prog_state,
'Processing first student');
+ $r->print(' ');
my $start=&Time::HiRes::time();
my $i=-1;
- my ($uname,$udom,$started);
+ my $started;
&scantron_get_maxbubble(); # Need the bubble lines array to parse.
+
+
+ # If an ssi failed in scantron_get_maxbubble, put an error message out to
+ # the user and return.
+
+ if ($ssi_error) {
+ $r->print("");
+ &ssi_print_error($r);
+ $r->print(&show_grading_menu_form($symb));
+ &Apache::lonnet::remove_lock($lock);
+ return ''; # Dunno why the other returns return '' rather than just returning.
+ }
+
+ my %lettdig = &letter_to_digits();
+ my $numletts = scalar(keys(%lettdig));
while ($i<$scanlines->{'count'}) {
($uname,$udom)=('','');
@@ -7131,40 +7482,92 @@ SCANTRONFORM
($uname,$udom)=split(/:/,$uname);
&Apache::lonxml::clear_problem_counter();
- &Apache::lonnet::appenv(%$scan_record);
+ &Apache::lonnet::appenv($scan_record);
if (&scantron_clear_skip($scanlines,$scan_data,$i)) {
&scantron_putfile($scanlines,$scan_data);
}
- my $i=0;
- foreach my $resource (@resources) {
- $i++;
- my %form=('submitted' =>'scantron',
- 'grade_target' =>'grade',
- 'grade_username'=>$uname,
- 'grade_domain' =>$udom,
- 'grade_courseid'=>$env{'request.course.id'},
- 'grade_symb' =>$resource->symb());
- if (exists($scan_record->{'scantron.CODE'})
- &&
- &Apache::lonnet::validCODE($scan_record->{'scantron.CODE'})) {
- $form{'CODE'}=$scan_record->{'scantron.CODE'};
- } else {
- $form{'CODE'}='';
- }
- my $result=&Apache::lonnet::ssi($resource->src(),%form);
- if ($result ne '') {
- }
- if (&Apache::loncommon::connection_aborted($r)) { last; }
- }
+ my $scancode;
+ if ((exists($scan_record->{'scantron.CODE'})) &&
+ (&Apache::lonnet::validCODE($scan_record->{'scantron.CODE'}))) {
+ $scancode = $scan_record->{'scantron.CODE'};
+ } else {
+ $scancode = '';
+ }
+
+ if (&grade_student_bubbles($r,$uname,$udom,$scan_record,$scancode,
+ @resources) eq 'ssi_error') {
+ $ssi_error = 0; # So end of handler error message does not trigger.
+ $r->print("");
+ &ssi_print_error($r);
+ $r->print(&show_grading_menu_form($symb));
+ &Apache::lonnet::remove_lock($lock);
+ return ''; # Why return ''? Beats me.
+ }
+
$completedstudents{$uname}={'line'=>$line};
- if (&Apache::loncommon::connection_aborted($r)) { last; }
+ if ($env{'form.verifyrecord'}) {
+ my $lastpos = $env{'form.scantron_maxbubble'}*$scantron_config{'Qlength'};
+ my $studentdata = substr($line,$scantron_config{'Qstart'}-1,$lastpos);
+ chomp($studentdata);
+ $studentdata =~ s/\r$//;
+ my $studentrecord = '';
+ my $counter = -1;
+ foreach my $resource (@resources) {
+ ($counter,my $recording) =
+ &verify_scantron_grading($resource,$udom,$uname,$env{'request.course.id'},
+ $counter,$studentdata,\%partids_by_symb,
+ \%scantron_config,\%lettdig,$numletts);
+ $studentrecord .= $recording;
+ }
+ if ($studentrecord ne $studentdata) {
+ $counter = -1;
+ $studentrecord = '';
+ foreach my $resource (@resources) {
+ ($counter,my $recording) =
+ &verify_scantron_grading($resource,$udom,$uname,$env{'request.course.id'},
+ $counter,$studentdata,\%partids_by_symb,
+ \%scantron_config,\%lettdig,$numletts);
+ $studentrecord .= $recording;
+ }
+ if ($studentrecord ne $studentdata) {
+ $r->print('');
+ if ($scancode eq '') {
+ $r->print(&mt('Mismatch grading bubble sheet for user: [_1] with ID: [_2].',
+ $uname.':'.$udom,$scan_record->{'scantron.ID'}));
+ } else {
+ $r->print(&mt('Mismatch grading bubble sheet for user: [_1] with ID: [_2] and CODE: [_3].',
+ $uname.':'.$udom,$scan_record->{'scantron.ID'},$scancode));
+ }
+ $r->print(' '.&Apache::loncommon::start_data_table()."\n".
+ &Apache::loncommon::start_data_table_header_row()."\n".
+ '
'.&mt('Source').' '.&mt('Bubbled responses').' '.
+ &Apache::loncommon::end_data_table_header_row()."\n".
+ &Apache::loncommon::start_data_table_row().
+ ''.&mt('Bubble Sheet').' '.
+ ''.$studentdata.' '.
+ &Apache::loncommon::end_data_table_row().
+ &Apache::loncommon::start_data_table_row().
+ 'Stored submissions '.
+ ''.$studentrecord.' '."\n".
+ &Apache::loncommon::end_data_table_row().
+ &Apache::loncommon::end_data_table().'');
+ } else {
+ $r->print(''.
+ &mt('A second grading pass was needed for user: [_1] with ID: [_2], because a mismatch was seen on the first pass.',$uname.':'.$udom,$scan_record->{'scantron.ID'}).' '.
+ &mt("As a consequence, this user's submission history records two tries.").
+ ' ');
+ }
+ }
+ }
+ if (&Apache::loncommon::connection_aborted($r)) { last; }
} continue {
&Apache::lonxml::clear_problem_counter();
- &Apache::lonnet::delenv('scantron\.');
+ &Apache::lonnet::delenv('scantron.');
}
&Apache::lonhtmlcommon::Close_PrgWin($r,\%prog_state);
+ &Apache::lonnet::remove_lock($lock);
# my $lasttime = &Time::HiRes::time()-$start;
# $r->print("took $lasttime
");
@@ -7173,13 +7576,22 @@ SCANTRONFORM
return '';
}
-=pod
-
-=item scantron_upload_scantron_data
-
- Creates the screen for adding a new bubble sheet data file to a course.
-
-=cut
+sub grade_student_bubbles {
+ my ($r,$uname,$udom,$scan_record,$scancode,@resources) = @_;
+ foreach my $resource (@resources) {
+ my %form = ('submitted' => 'scantron',
+ 'grade_target' => 'grade',
+ 'grade_username'=> $uname,
+ 'grade_domain' => $udom,
+ 'grade_courseid'=> $env{'request.course.id'},
+ 'grade_symb' => $resource->symb(),
+ 'CODE' => $scancode);
+ my $result=&ssi_with_retries($resource->src(),$ssi_retries,%form);
+ return 'ssi_error' if ($ssi_error);
+ last if (&Apache::loncommon::connection_aborted($r));
+ }
+ return;
+}
sub scantron_upload_scantron_data {
my ($r)=@_;
@@ -7194,7 +7606,7 @@ sub scantron_upload_scantron_data {