--- loncom/homework/grades.pm 2008/12/21 22:01:35 1.539
+++ 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.539 2008/12/21 22:01:35 riegler Exp $
+# $Id: grades.pm,v 1.553 2009/02/24 17:12:19 biermanm Exp $
#
# Copyright Michigan State University Board of Trustees
#
@@ -699,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";
@@ -779,12 +779,12 @@ sub listStudents {
my $getsec = $env{'form.section'} eq '' ? 'all' : $env{'form.section'};
my $getgroup = $env{'form.group'} eq '' ? 'all' : $env{'form.group'};
my $submitonly= $env{'form.submitonly'} eq '' ? 'all' : $env{'form.submitonly'};
- my $viewgrade = $env{'form.showgrading'} eq 'yes' ? &mt('View/Grade/Regrade') : &mt('View');
+ my $viewgrade = $env{'form.showgrading'} eq 'yes' ? 'View/Grade/Regrade' : 'View';
$env{'form.probTitle'} = $env{'form.probTitle'} eq '' ?
&Apache::lonnet::gettitle($symb) : $env{'form.probTitle'};
- my $result=' '.$viewgrade.
- &mt(' 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'));
@@ -889,7 +889,7 @@ LISTJAVASCRIPT
&Apache::lonhtmlcommon::StatusOptions($saveStatus,undef,1,'javascript:reLoadList(this.form);')).' ';
}
- $gradeTable.=&mt('To [_1] 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.',lc($viewgrade)).' '."\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
@@ -1656,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') {
@@ -1669,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".
@@ -2094,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++;
@@ -2234,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".
' '."\n";
- #&mt('Part: [_1] Points: [_2] or [_3] ',$display_part,$radio,$line).
$result.=
&Apache::loncommon::start_data_table_row()."\n".
''.&mt('Part').': '.$display_part.' '.&mt('Points').': '.$radio.' '.&mt('or').' '.$line.' '.
@@ -3427,7 +3426,7 @@ 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/\[Part: \Q$part\E\]//;
my $narrowtext = &mt('Tries');
$display =~ s/Number of Attempts/$narrowtext/;
$header .= ''.&mt('Old').' '.$display.' '.
@@ -5488,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')) {
@@ -6178,12 +6178,18 @@ 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(" ");
@@ -7241,35 +7247,83 @@ sub scantron_get_maxbubble {
my $response_number = 0;
my $bubble_line = 0;
foreach my $resource (@resources) {
- my $symb = $resource->symb();
+ my ($analysis,$parts) = &scantron_partids_tograde($resource,$cid,$uname,$udom);
+ if ((ref($analysis) eq 'HASH') && (ref($parts) eq 'ARRAY')) {
+ foreach my $part_id (@{$parts}) {
+ my $lines;
+
+ # TODO - make this a persistent hash not an array.
+
+ # optionresponse, matchresponse and rankresponse type items
+ # render as separate sub-questions in exam mode.
+ if (($analysis->{$part_id.'.type'} eq 'optionresponse') ||
+ ($analysis->{$part_id.'.type'} eq 'matchresponse') ||
+ ($analysis->{$part_id.'.type'} eq 'rankresponse')) {
+ my ($numbub,$numshown);
+ if ($analysis->{$part_id.'.type'} eq 'optionresponse') {
+ if (ref($analysis->{$part_id.'.options'}) eq 'ARRAY') {
+ $numbub = scalar(@{$analysis->{$part_id.'.options'}});
+ }
+ } elsif ($analysis->{$part_id.'.type'} eq 'matchresponse') {
+ if (ref($analysis->{$part_id.'.items'}) eq 'ARRAY') {
+ $numbub = scalar(@{$analysis->{$part_id.'.items'}});
+ }
+ } elsif ($analysis->{$part_id.'.type'} eq 'rankresponse') {
+ if (ref($analysis->{$part_id.'.foils'}) eq 'ARRAY') {
+ $numbub = scalar(@{$analysis->{$part_id.'.foils'}});
+ }
+ }
+ if (ref($analysis->{$part_id.'.shown'}) eq 'ARRAY') {
+ $numshown = scalar(@{$analysis->{$part_id.'.shown'}});
+ }
+ my $bubbles_per_line = 10;
+ my $inner_bubble_lines = int($numbub/$bubbles_per_line);
+ if (($numbub % $bubbles_per_line) != 0) {
+ $inner_bubble_lines++;
+ }
+ for (my $i=0; $i<$numshown; $i++) {
+ $subdivided_bubble_lines{$response_number} .=
+ $inner_bubble_lines.',';
+ }
+ $subdivided_bubble_lines{$response_number} =~ s/,$//;
+ $lines = $numshown * $inner_bubble_lines;
+ } else {
+ $lines = $analysis->{"$part_id.bubble_lines"};
+ }
- my (@parts,@allparts,@possible_parts);
+ $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++;
- # Need to retrieve part IDs and response IDs because essayresponse,
- # reactionresponse and organicresponse items are not included in
- # $analysis{'parts'} from lonnet::ssi.
- if (ref($resource->parts()) eq 'ARRAY') {
- foreach my $part (@{$resource->parts()}) {
- if (!&Apache::loncommon::check_if_partid_hidden($part,$symb,$udom,$uname)) {
- my @resp_ids = $resource->responseIds($part);
- foreach my $id (@resp_ids) {
- my $part_id = $part.'.'.$id;
- push(@possible_parts,$part_id);
- }
- }
- }
+ $bubble_line += $lines;
+ $total_lines += $lines;
+ }
}
+ }
+ &Apache::lonnet::delenv('scantron.');
+ &save_bubble_lines();
+ $env{'form.scantron_maxbubble'} =
+ $total_lines;
+ return $env{'form.scantron_maxbubble'};
+}
+
+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);
-
- my %analysis = &Apache::lonnet::str2hash($an);
+ my (undef, $an) = split(/_HASH_REF__/,$result, 2);
+ %analysis = &Apache::lonnet::str2hash($an);
if (ref($analysis{'parts'}) eq 'ARRAY') {
foreach my $part (@{$analysis{'parts'}}) {
@@ -7279,82 +7333,10 @@ sub scantron_get_maxbubble {
}
}
}
- # Add part_ids for any essayresponse, reactionresponse or
- # organicresponse items.
- foreach my $part_id (@possible_parts) {
- if (grep(/^\Q$part_id\E$/,@parts)) {
- push(@allparts,$part_id);
- } else {
- if (($analysis{$part_id.'.type'} eq 'essayresponse') ||
- ($analysis{$part_id.'.type'} eq 'reactionresponse') ||
- ($analysis{$part_id.'.type'} eq 'organicresponse')) {
- push(@allparts,$part_id);
- }
- }
- }
-
- foreach my $part_id (@allparts) {
- my $lines;
-
- # TODO - make this a persistent hash not an array.
-
- # optionresponse, matchresponse and rankresponse type items
- # render as separate sub-questions in exam mode.
- if (($analysis{$part_id.'.type'} eq 'optionresponse') ||
- ($analysis{$part_id.'.type'} eq 'matchresponse') ||
- ($analysis{$part_id.'.type'} eq 'rankresponse')) {
- my ($numbub,$numshown);
- if ($analysis{$part_id.'.type'} eq 'optionresponse') {
- if (ref($analysis{$part_id.'.options'}) eq 'ARRAY') {
- $numbub = scalar(@{$analysis{$part_id.'.options'}});
- }
- } elsif ($analysis{$part_id.'.type'} eq 'matchresponse') {
- if (ref($analysis{$part_id.'.items'}) eq 'ARRAY') {
- $numbub = scalar(@{$analysis{$part_id.'.items'}});
- }
- } elsif ($analysis{$part_id.'.type'} eq 'rankresponse') {
- if (ref($analysis{$part_id.'.foils'}) eq 'ARRAY') {
- $numbub = scalar(@{$analysis{$part_id.'.foils'}});
- }
- }
- if (ref($analysis{$part_id.'.shown'}) eq 'ARRAY') {
- $numshown = scalar(@{$analysis{$part_id.'.shown'}});
- }
- my $bubbles_per_line = 10;
- my $inner_bubble_lines = int($numbub/$bubbles_per_line);
- if (($numbub % $bubbles_per_line) != 0) {
- $inner_bubble_lines++;
- }
- for (my $i=0; $i<$numshown; $i++) {
- $subdivided_bubble_lines{$response_number} .=
- $inner_bubble_lines.',';
- }
- $subdivided_bubble_lines{$response_number} =~ s/,$//;
- $lines = $numshown * $inner_bubble_lines;
- } else {
- $lines = $analysis{"$part_id.bubble_lines"};
- }
-
- $first_bubble_line{$response_number} = $bubble_line;
- $bubble_lines_per_response{$response_number} = $lines;
- $responsetype_per_response{$response_number} =
- $analysis{$part_id.'.type'};
- $response_number++;
-
- $bubble_line += $lines;
- $total_lines += $lines;
- }
-
}
- &Apache::lonnet::delenv('scantron\.');
-
- &save_bubble_lines();
- $env{'form.scantron_maxbubble'} =
- $total_lines;
- return $env{'form.scantron_maxbubble'};
+ return (\%analysis,\@parts);
}
-
sub scantron_validate_missingbubbles {
my ($r,$currentphase) = @_;
#get student info
@@ -7426,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= <
@@ -7435,7 +7425,7 @@ 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);
@@ -7444,9 +7434,10 @@ SCANTRONFORM
'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.
@@ -7462,6 +7453,9 @@ SCANTRONFORM
return ''; # Dunno why the other returns return '' rather than just returning.
}
+ my %lettdig = &letter_to_digits();
+ my $numletts = scalar(keys(%lettdig));
+
while ($i<$scanlines->{'count'}) {
($uname,$udom)=('','');
$i++;
@@ -7494,39 +7488,83 @@ SCANTRONFORM
&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=&ssi_with_retries($resource->src(), $ssi_retries, %form);
- if ($ssi_error) {
- $ssi_error = 0; # So end of handler error message does not trigger.
- $r->print("");
- &ssi_print_error($r);
- $r->print(&show_grading_menu_form($symb));
- &Apache::lonnet::remove_lock($lock);
- return ''; # Why return ''? Beats me.
- }
+ 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.
+ }
- if (&Apache::loncommon::connection_aborted($r)) { last; }
- }
$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);
@@ -7538,6 +7576,23 @@ SCANTRONFORM
return '';
}
+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)=@_;
$r->print(&Apache::loncommon::coursebrowser_javascript($env{'request.role.domain'}));
@@ -7692,18 +7747,7 @@ sub checkscantron_results {
if (!$symb) {return '';}
my $grading_menu_button=&show_grading_menu_form($symb);
my $cid = $env{'request.course.id'};
- my %lettdig = (
- A => 1,
- B => 2,
- C => 3,
- D => 4,
- E => 5,
- F => 6,
- G => 7,
- H => 8,
- I => 9,
- J => 0,
- );
+ my %lettdig = &letter_to_digits();
my $numletts = scalar(keys(%lettdig));
my $cnum = $env{'course.'.$cid.'.num'};
my $cdom = $env{'course.'.$cid.'.domain'};
@@ -7717,6 +7761,13 @@ sub checkscantron_results {
my $navmap=Apache::lonnavmaps::navmap->new();
my $map=$navmap->getResourceByUrl($sequence);
my @resources=$navmap->retrieveResources($map,undef,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;
+ }
my (%scandata,%lastname,%bylast);
$r->print('