diff --git a/lib/WeBWorK/PG/Translator.pm b/lib/WeBWorK/PG/Translator.pm index 50a38f126..2c195ad78 100644 --- a/lib/WeBWorK/PG/Translator.pm +++ b/lib/WeBWorK/PG/Translator.pm @@ -1088,46 +1088,71 @@ sub rf_avg_problem_grader { } sub avg_problem_grader { - my ($rh_evaluated_answers, $rh_problem_state, %form_options) = @_; + my ($answers, $problem_state, %form_options) = @_; - my %evaluated_answers = %{$rh_evaluated_answers}; + my %problem_result = (score => 0, errors => '', type => 'avg_problem_grader', msg => ''); - # By default the old problem state is simply passed back out again. - my %problem_state = %$rh_problem_state; + $problem_result{msg} = maketext('You can earn partial credit on this problem.') if keys %$answers > 1; - # Initial setup of the answer - my $total = 0; - my %problem_result = ( - score => 0, - errors => '', - type => 'avg_problem_grader', - msg => '', - ); + # Return unless answers have been submitted. + return (\%problem_result, $problem_state) unless $form_options{answers_submitted} == 1; + + my %credit; + + # Get the score for each answer (error if can't recognize the answer format). + for my $ans_name (keys %$answers) { + if (ref($answers->{$ans_name}) =~ m/^(HASH|AnswerHash)$/) { + $credit{$ans_name} = $answers->{$ans_name}{score} // 0; + } else { + $problem_result{error} = "Error: Answer $ans_name is not a hash: $answers->{$ans_name}"; + die "Error: Answer |$ans_name| is not a hash reference\n" + . $answers->{$ans_name} + . "\nThis probably means that the answer evaluator for this answer is not working correctly."; + } + } - my $count = keys %evaluated_answers; - $problem_result{msg} = 'You can earn partial credit on this problem.' if $count > 1; + # Mark any optional answers as correct, if the goal answers are right and the optional answers are blank. + for my $ans_name (keys %$answers) { + if ($credit{$ans_name} == 1 && defined $answers->{$ans_name}{credit}) { + for my $credit_name ( + ref($answers->{$ans_name}{credit}) eq 'ARRAY' + ? @{ $answers->{$ans_name}{credit} } + : $answers->{$ans_name}{credit}) + { + if (!defined $answers->{$credit_name}{student_ans} + || $answers->{$credit_name}{student_ans} =~ m/^\s*$/) + { + $answers->{$credit_name}{score} = 1; + $answers->{$credit_name}{ans_message} = + maketext('This answer was marked correct because the primary answer is correct.'); + $credit{$credit_name} = 1; + } + } + } + } - return (\%problem_result, \%problem_state) unless $form_options{answers_submitted} == 1; + my ($score, $total) = (0, 0); - # Answers have been submitted -- process them. - for my $ans_name (keys %evaluated_answers) { - $total += $evaluated_answers{$ans_name}{score}; + # Add up the weighted scores + for my $ans_name (keys %$answers) { + my $weight = $answers->{$ans_name}{weight} // 1; + $total += $weight; + $score += $weight * $credit{$ans_name}; } - # Calculate score rounded to three places to avoid roundoff problems - $problem_result{score} = $count ? $total / $count : 0; - $problem_state{recorded_score} //= 0; + $problem_result{score} = $total ? $score / $total : 0; - # Increase recorded score if the current score is greater. - $problem_state{recorded_score} = $problem_result{score} - if $problem_result{score} > $problem_state{recorded_score}; + ++$problem_state->{num_of_correct_ans} if $score == $total; + ++$problem_state->{num_of_incorrect_ans} if $score < $total; + $problem_state->{recorded_score} //= 0; - ++$problem_state{num_of_correct_ans} if $total == $count; - ++$problem_state{num_of_incorrect_ans} if $total < $count; + # Increase recorded score if the current score is greater. + $problem_state->{recorded_score} = $problem_result{score} + if $problem_result{score} > $problem_state->{recorded_score}; - warn "Error in grading this problem the total $total is larger than $count" if $total > $count; + warn "Error in grading this problem: The score $score is larger than the total $total." if $score > $total; - return (\%problem_result, \%problem_state); + return (\%problem_result, $problem_state); } =head2 post_process_content diff --git a/macros/core/PGanswermacros.pl b/macros/core/PGanswermacros.pl index c2dbb469b..9e9a0bd05 100644 --- a/macros/core/PGanswermacros.pl +++ b/macros/core/PGanswermacros.pl @@ -1587,62 +1587,142 @@ sub std_problem_grader2 { =head3 C -This grader gives a grade depending on how many questions from the problem are correct. (The highest -grade is the one that is kept. One can never lower the recorded grade on a problem by repeating it.) -Many professors (and almost all students :-) ) prefer this grader. +This grader gives a "weighted" average score to the problem and is the default +grader. + +The grader can be selected by calling install_problem_grader(~~&avg_problem_grader); +However, since this is the default grader, that is not necessary to use this +grader. + +Each answer is assigned a weight (the default is 1). The score is then the sum +of the product of the weights and scores for the correct answers divided by the +total of the weights for all answers. (To assign weights as percentages, use +integers that add up to 100. For example, use 40 and 60 for the weights for two +answers.) Assign weights to answers using the C option C<< weight => n >>. +For example, in PGML create the answer rule with + + [_]{$answer}{10}{ cmp_options => { weight => 40 } } + +With the classic C method call + + ANS($answer->cmp(weight => 40); + +This grader also allows for one "goal" answer that is answered correctly to +automatically give credit for one or more other "optional" answers. This way, if +there are several "optional" answers leading up to the "goal" answer, and the +student produces the "goal" answer by some other means and does not answer the +"optional" answers, the student can be given full credit for the problem anyway. +To use this feature use the C option of the C method for the "goal" +answer. For example, C<< credit => $answer1Name >> or C<< credit => [ +$answer1Name, $answer2Name, ... ] >>, where C<$answer1Name>, C<$answer2Name>, +etc., are the names of the "optional" answers that will be given credit if the +"goal" answer is correct. Note that the other answers must be assigned names +either by calling C and C, or by creating the answer +rule in PGML with C<[_]{$answer1}{15}{$answer1Name}>, for example. The answer +names should be generated by calling C (for example, +C<$answer1Name = NEW_ANS_NAME()>) rather than being made up. Otherwise the +problem will fail to work in many situations (for example, in tests). For +example, to set this up in PGML use + + BEGIN_PGML + Optional Answer 1: [_]{$answer1}{10}{$answer1Name = NEW_ANS_NAME()} + + Optional Answer 2: [_]{$answer2}{10}{$answer2Name = NEW_ANS_NAME()} + + Goal: [_]{$answer3}{10}{ cmp_options => { credit => [ $answer1Name, $answer2Name ] } } + END_PGML + +Note that the C and C options can be used together. For example: + + BEING_PGML + Optional Answer: [_]{$optional}{10}{$optionalName = NEW_ANS_NAME()}{{ weight => 20 }} + + Goal: [_]{$goalAnswer}{10}{ cmp_options => { credit => $optionalName, weight => 80 } } + END_PGML + +This way, if the "optional" answer is correct but the "goal" answer is not, the +problem score will be 20%, but if the "goal" answer is correct, the problem +score will be 100%. + +One caveat to keep in mind is that credit is given to an "optional" answer ONLY +if the answer is left blank (or is actually correct). Credit will NOT be given +if an "optional" answer is incorrect, even if the "goal" answer IS correct. + +When credit is given to an "optional" answer due to the "goal" answer being +correct, a message will be added to the "optional" answer stating, "This answer +was marked correct because the primary answer is correct." + =cut sub avg_problem_grader { - my ($rh_evaluated_answers, $rh_problem_state, %form_options) = @_; + my ($answers, $problem_state, %form_options) = @_; - my %evaluated_answers = %{$rh_evaluated_answers}; + my %problem_result = (score => 0, errors => '', type => 'avg_problem_grader', msg => ''); - # By default the old problem state is simply passed back out again. - my %problem_state = %$rh_problem_state; - - # Initial setup of the answer. - my $total = 0; - my %problem_result = ( - score => 0, - errors => '', - type => 'avg_problem_grader', - msg => '', - ); - my $count = keys %evaluated_answers; - $problem_result{msg} = maketext('You can earn partial credit on this problem.') if $count > 1; + $problem_result{msg} = maketext('You can earn partial credit on this problem.') if keys %$answers > 1; # Return unless answers have been submitted. - return (\%problem_result, \%problem_state) unless $form_options{answers_submitted} == 1; + return (\%problem_result, $problem_state) unless $form_options{answers_submitted} == 1; - # Answers have been submitted -- process them. - for my $ans_name (keys %evaluated_answers) { - if (ref $evaluated_answers{$ans_name} eq 'HASH' || ref $evaluated_answers{$ans_name} eq 'AnswerHash') { - $total += $evaluated_answers{$ans_name}{score} // 0; + my %credit; + + # Get the score for each answer (error if can't recognize the answer format). + for my $ans_name (keys %$answers) { + if (ref($answers->{$ans_name}) =~ m/^(HASH|AnswerHash)$/) { + $credit{$ans_name} = $answers->{$ans_name}{score} // 0; } else { + $problem_result{error} = "Error: Answer $ans_name is not a hash: $answers->{$ans_name}"; die "Error: Answer |$ans_name| is not a hash reference\n" - . $evaluated_answers{$ans_name} - . 'This probably means that the answer evaluator for this answer is not working correctly.'; - $problem_result{error} = "Error: Answer $ans_name is not a hash: $evaluated_answers{$ans_name}"; + . $answers->{$ans_name} + . "\nThis probably means that the answer evaluator for this answer is not working correctly."; } } - # Calculate the score. - $problem_result{score} = $total / $count if $count; + # Mark any optional answers as correct, if the goal answers are right and the optional answers are blank. + for my $ans_name (keys %$answers) { + if ($credit{$ans_name} == 1 && defined $answers->{$ans_name}{credit}) { + for my $credit_name ( + ref($answers->{$ans_name}{credit}) eq 'ARRAY' + ? @{ $answers->{$ans_name}{credit} } + : $answers->{$ans_name}{credit}) + { + if (!defined $answers->{$credit_name}{student_ans} + || $answers->{$credit_name}{student_ans} =~ m/^\s*$/) + { + $answers->{$credit_name}{score} = 1; + $answers->{$credit_name}{ans_message} = + maketext('This answer was marked correct because the primary answer is correct.'); + $credit{$credit_name} = 1; + } + } + } + } - ++$problem_state{num_of_correct_ans} if $total == $count; - ++$problem_state{num_of_incorrect_ans} if $total < $count; - $problem_state{recorded_score} //= 0; + my ($score, $total) = (0, 0); + + # Add up the weighted scores + for my $ans_name (keys %$answers) { + my $weight = $answers->{$ans_name}{weight} // 1; + $total += $weight; + $score += $weight * $credit{$ans_name}; + } + + $problem_result{score} = $total ? $score / $total : 0; + + ++$problem_state->{num_of_correct_ans} if $score == $total; + ++$problem_state->{num_of_incorrect_ans} if $score < $total; + $problem_state->{recorded_score} //= 0; # Increase recorded score if the current score is greater. - $problem_state{recorded_score} = $problem_result{score} - if $problem_result{score} > $problem_state{recorded_score}; + $problem_state->{recorded_score} = $problem_result{score} + if $problem_result{score} > $problem_state->{recorded_score}; - warn "Error in grading this problem the total $total is larger than $count" if $total > $count; + warn "Error in grading this problem: The score $score is larger than the total $total." if $score > $total; - return (\%problem_result, \%problem_state); + return (\%problem_result, $problem_state); } =head2 Utility subroutines