--- loncom/homework/grades.pm 2008/01/09 14:16:52 1.502
+++ loncom/homework/grades.pm 2008/03/24 19:08:09 1.513.2.1
@@ -1,7 +1,7 @@
# The LearningOnline Network with CAPA
# The LON-CAPA Grading handler
#
-# $Id: grades.pm,v 1.502 2008/01/09 14:16:52 www Exp $
+# $Id: grades.pm,v 1.513.2.1 2008/03/24 19:08:09 raeburn Exp $
#
# Copyright Michigan State University Board of Trustees
#
@@ -47,8 +47,82 @@ use LONCAPA;
use POSIX qw(floor);
+
my %perm=();
+# These variables are used to recover from ssi errors
+
+my $ssi_retries = 5;
+my $ssi_error;
+my $ssi_error_resource;
+my $ssi_error_message;
+
+
+# Do an ssi with retries:
+# While I'd love to factor out this with the vesrion in lonprintout,
+# that would either require a data coupling between modules, which I refuse to perpetuate
+# (there's quite enough of that already), or would require the invention of another infrastructure
+# I'm not quite ready to invent (e.g. an ssi_with_retry object).
+#
+# At least the logic that drives this has been pulled out into loncommon.
+
+
+#
+# ssi_with_retries - Does the server side include of a resource.
+# if the ssi call returns an error we'll retry it up to
+# the number of times requested by the caller.
+# If we still have a proble, no text is appended to the
+# output and we set some global variables.
+# to indicate to the caller an SSI error occured.
+# All of this is supposed to deal with the issues described
+# in LonCAPA BZ 5631 see:
+# http://bugs.lon-capa.org/show_bug.cgi?id=5631
+# by informing the user that this happened.
+#
+# Parameters:
+# resource - The resource to include. This is passed directly, without
+# interpretation to lonnet::ssi.
+# form - The form hash parameters that guide the interpretation of the resource
+#
+# retries - Number of retries allowed before giving up completely.
+# Returns:
+# On success, returns the rendered resource identified by the resource parameter.
+# Side Effects:
+# The following global variables can be set:
+# ssi_error - If an unrecoverable error occured this becomes true.
+# It is up to the caller to initialize this to false
+# if desired.
+# ssi_last_error_resource - If an unrecoverable error occured, this is the value
+# of the resource that could not be rendered by the ssi
+# call.
+# ssi_last_error - The error string fetched from the ssi response
+# in the event of an error.
+#
+sub ssi_with_retries {
+ my ($resource, $retries, %form) = @_;
+ my ($content, $response) = &Apache::loncommon::ssi_with_retries($resource, $retries, %form);
+ if ($response->is_error) {
+ $ssi_error = 1;
+ $ssi_error_resource = $resource;
+ $ssi_error_message = $response->code . " " . $response->message;
+ }
+
+ return $content;
+
+}
+#
+# Prodcuces an ssi retry failure error message to the user:
+#
+
+sub ssi_print_error {
+ my ($r) = @_;
+ $r->print('
Unrecoverable network error
');
+ $r->print('
Unable to perform a resource fetch from a server: ');
+ $r->print("Resource: $ssi_error_resource ");
+ $r->print("Error: $ssi_error_message Try again later.");
+ $r->print('If errors persist, contact LonCAPA support for assistance
');
+}
+
#
# --- Retrieve the parts from the metadata file.---
sub getpartlist {
@@ -201,7 +275,7 @@ sub reset_caches {
my (undef,undef,$url)=&Apache::lonnet::decode_symb($symb);
$url=&Apache::lonnet::clutter($url);
- my $subresult=&Apache::lonnet::ssi($url,
+ my $subresult=&ssi_with_retries($url, $ssi_retries,
('grade_target' => 'analyze'),
('grade_domain' => $udom),
('grade_symb' => $symb),
@@ -3960,31 +4034,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)
-
-
');
+ $r->print(&mt('Validating '.$validate_phases[$currentphase]).' ');
$r->rflush();
my $which="scantron_validate_".$validate_phases[$currentphase];
{
@@ -5963,8 +6113,7 @@ sub scantron_validate_file {
}
if (!$stop) {
my $warning=&scantron_warning_screen('Start Grading');
- $r->print('
-'.&mt('Validation process complete.').'
+ $r->print(&mt('Validation process complete.').'
'.$warning.'
@@ -5981,7 +6130,11 @@ sub scantron_validate_file {
$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."));
@@ -6463,7 +6616,6 @@ sub scantron_validate_ID {
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
@@ -6485,6 +6637,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").
@@ -6580,7 +6736,7 @@ ENDSCRIPT
""));
$r->print("\n
".&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:
@@ -6592,15 +6748,18 @@ ENDSCRIPT
$r->print($message);
$r->print("
".&mt("Please indicate which bubble should be used for grading")."
".&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_questinos is actually a list of line numbers not
+ # The form field scantron_questions is actually a list of line numbers not
# a list of question numbers. Therefore:
#
@@ -6609,14 +6768,50 @@ ENDSCRIPT
$r->print('');
foreach my $question (@{$arg}) {
- &prompt_for_corrections($r, $question, $scan_config, $scan_record);
+ my @linenums = &prompt_for_corrections($r,$question,$scan_config,
+ $scan_record, $error);
+ push (@lines_to_correct,@linenums);
}
+ $r->print(&verify_bubbles_checked(@lines_to_correct));
} else {
$r->print("\n
");
}
$r->print("\n
");
}
+sub verify_bubbles_checked {
+ my (@ansnums) = @_;
+ my $ansnumstr = join('","',@ansnums);
+ my $warning = &mt("A bubble or 'No bubble' selection has not been made for one or more lines.");
+ my $output = (<
+function verify_bubble_radio(form) {
+ var ansnumArray = new Array ("$ansnumstr");
+ var need_bubble_count = 0;
+ for (var i=0; i 1) {
+ var bubble_picked = 0;
+ for (var j=0; j
+ENDSCRIPT
+ return $output;
+}
+
=pod
=item questions_to_line_list
@@ -6635,11 +6830,26 @@ sub questions_to_line_list {
my ($questions) = @_;
my @lines;
- foreach my $question (@{$questions}) {
- my $first = $first_bubble_line{$question-1} + 1;
- my $count = $bubble_lines_per_response{$question-1};
- my $last = $first+$count-1;
- push(@lines, ($first..$last));
+ 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);
}
@@ -6657,33 +6867,74 @@ for multi and missing bubble cases).
$question - The question number to prompt for.
$scan_config - The scantron file configuration hash.
$scan_record - Reference to the hash that has the the parsed scanlines.
+ $error - Type of error
Implicit inputs:
%bubble_lines_per_response - Starting line numbers for each question.
Numbered from 0 (but question numbers are from
1.
%first_bubble_line - Starting bubble line for each question.
+ %subdivided_bubble_lines - optionresponse, matchresponse and rankresponse
+ type problems render as separate sub-questions,
+ in exam mode. This hash contains a
+ comma-separated list of the lines per
+ sub-question.
+ %responsetype_per_response - essayresponse, formularesponse,
+ stringresponse, imageresponse, reactionresponse,
+ and organicresponse type problem parts can have
+ multiple lines per response if the weight
+ assigned exceeds 10. In this case, only
+ one bubble per line is permitted, but more
+ than one line might contain bubbles, e.g.
+ bubbling of: line 1 - J, line 2 - J,
+ line 3 - B would assign 22 points.
=cut
sub prompt_for_corrections {
- my ($r, $question, $scan_config, $scan_record) = @_;
-
- my $lines = $bubble_lines_per_response{$question-1};
- my $current_line = $first_bubble_line{$question-1} + 1 ;
-
+ 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. Select at most one bubble in a single line and select 'No Bubble' in all the other lines. ")." ");
+ $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,
- split('', $selected));
+ my $selected = $$scan_record{"scantron.$current_line.answer"};
+ &scantron_bubble_selector($r,$scan_config,$current_line,
+ $questionnum,$error,split('', $selected));
+ push (@linenums,$current_line);
$current_line++;
}
if ($lines > 1) {
$r->print(" ");
}
+ return @linenums;
}
=pod
@@ -6697,34 +6948,46 @@ sub prompt_for_corrections {
$r - Apache request object
$scan_config - hash from &get_scantron_config()
$line - Number of the line being displayed.
+ $questionnum - Question number (may include subquestion)
+ $error - Type of error.
@selected - Array of bubbles picked on this line.
=cut
sub scantron_bubble_selector {
- my ($r,$scan_config,$line,@selected)=@_;
+ my ($r,$scan_config,$line,$questionnum,$error,@selected)=@_;
my $max=$$scan_config{'Qlength'};
my $scmode=$$scan_config{'Qon'};
if ($scmode eq 'number' || $scmode eq 'letter') { $max=10; }
my @alphabet=('A'..'Z');
- $r->print("
');
+ $r->print(&Apache::loncommon::end_data_table_row().
+ &Apache::loncommon::end_data_table());
}
=pod
@@ -6904,7 +7167,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++) {
@@ -6931,14 +7193,17 @@ sub scantron_validate_doublebubble {
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'}
+ $env{'form.scantron.bubble_lines.n'},
+ $env{'form.scantron.first_bubble_line.n'} and
+ $env{"form.scantron.sub_bubblelines.n"}
which are the total number of bubble, lines, the number of bubble
- lines for reponse n and number of the first bubble line for response n.
+ lines for response n and number of the first bubble line for response n,
+ and a comma separated list of numbers of bubble lines for sub-questions
+ (for optionresponse, matchresponse, and rankresponse items), for response n.
=cut
-sub scantron_get_maxbubble {
+sub scantron_get_maxbubble {
if (defined($env{'form.scantron_maxbubble'}) &&
$env{'form.scantron_maxbubble'}) {
&restore_bubble_lines();
@@ -6960,14 +7225,29 @@ 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();
- my $result=&Apache::lonnet::ssi($resource->src(),
- ('symb' => $resource->symb()),
+ my $symb = $resource->symb();
+ # Need to retrieve part IDs and response IDs because essayresponse,
+ # reactionresponse and organicresponse items are not included in
+ # $analysis{'parts'} from lonnet::ssi.
+ my %possible_part_ids;
+ 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) {
+ $possible_part_ids{$part.'.'.$id} = 1;
+ }
+ }
+ }
+ }
+ my $result=&ssi_with_retries($resource->src(), $ssi_retries,
+ ('symb' => $symb),
('grade_target' => 'analyze'),
('grade_courseid' => $cid),
('grade_domain' => $udom),
@@ -6975,21 +7255,72 @@ sub scantron_get_maxbubble {
my (undef, $an) =
split(/_HASH_REF__/,$result, 2);
- my %analysis = &Apache::lonnet::str2hash($an);
-
-
+ my @parts;
- foreach my $part_id (@{$analysis{'parts'}}) {
-
- my $lines = $analysis{"$part_id.bubble_lines"};;
+ my %analysis = &Apache::lonnet::str2hash($an);
+ if (ref($analysis{'parts'}) eq 'ARRAY') {
+ foreach my $part (@{$analysis{'parts'}}) {
+ my ($id,$respid) = split(/\./,$part);
+ if (!&Apache::loncommon::check_if_partid_hidden($id,$symb,$udom,$uname)) {
+ push(@parts,$part);
+ }
+ }
+ }
+ # Add part_ids for any essayresponse items.
+ foreach my $part_id (keys(%possible_part_ids)) {
+ if (($analysis{$part_id.'.type'} eq 'essayresponse') ||
+ ($analysis{$part_id.'.type'} eq 'reactionresponse') ||
+ ($analysis{$part_id.'.type'} eq 'organicresponse')) {
+ if (!grep(/^\Q$part_id\E$/,@parts)) {
+ push (@parts,$part_id);
+ }
+ }
+ }
+ foreach my $part_id (@parts) {
+ my $lines = $analysis{"$part_id.bubble_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($numshown/$bubbles_per_line);
+ if (($numshown % $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/,$//;
+ }
- $first_bubble_line{$response_number} = $bubble_line;
- $bubble_lines_per_response{$response_number} = $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;
@@ -7037,7 +7368,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) {
@@ -7076,9 +7425,12 @@ sub scantron_validate_missingbubbles {
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'});
@@ -7110,6 +7462,17 @@ SCANTRONFORM
my ($uname,$udom,$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));
+ return ''; # Dunno why the other returns return '' rather than just returning.
+ }
while ($i<$scanlines->{'count'}) {
($uname,$udom)=('','');
@@ -7158,10 +7521,16 @@ SCANTRONFORM
$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));
+ return ''; # Why return ''? Beats me.
}
- my $result=&Apache::lonnet::ssi($resource->src(),%form);
- if ($result ne '') {
- }
+
if (&Apache::loncommon::connection_aborted($r)) { last; }
}
$completedstudents{$uname}={'line'=>$line};
@@ -7463,14 +7832,12 @@ sub grading_menu {
$menudata->{'url'}.'" >'.
$menudata->{'name'}."\n";
} else {
- $Str .='