@@ -1587,62 +1587,142 @@ sub std_problem_grader2 {
1587
1587
1588
1588
=head3 C<avg_problem_grader >
1589
1589
1590
- This grader gives a grade depending on how many questions from the problem are correct. (The highest
1591
- grade is the one that is kept. One can never lower the recorded grade on a problem by repeating it.)
1592
- Many professors (and almost all students :-) ) prefer this grader.
1590
+ This grader gives a "weighted" average score to the problem and is the default
1591
+ grader.
1592
+
1593
+ The grader can be selected by calling
1593
1594
1594
1595
install_problem_grader(~~&avg_problem_grader);
1595
1596
1597
+ However, since this is the default grader, that is not necessary to use this
1598
+ grader.
1599
+
1600
+ Each answer is assigned a weight (the default is 1). The score is then the sum
1601
+ of the product of the weights and scores for the correct answers divided by the
1602
+ total of the weights for all answers. (To assign weights as percentages, use
1603
+ integers that add up to 100. For example, use 40 and 60 for the weights for two
1604
+ answers.) Assign weights to answers using the C<cmp > option C<< weight => n >>.
1605
+ For example, in PGML create the answer rule with
1606
+
1607
+ [_]{$answer}{10}{ cmp_options => { weight => 40 } }
1608
+
1609
+ With the classic C<ANS > method call
1610
+
1611
+ ANS($answer->cmp(weight => 40);
1612
+
1613
+ This grader also allows for one "goal" answer that is answered correctly to
1614
+ automatically give credit for one or more other "optional" answers. This way, if
1615
+ there are several "optional" answers leading up to the "goal" answer, and the
1616
+ student produces the "goal" answer by some other means and does not answer the
1617
+ "optional" answers, the student can be given full credit for the problem anyway.
1618
+ To use this feature use the C<credit > option of the C<cmp > method for the "goal"
1619
+ answer. For example, C<< credit => $answer1Name >> or C<< credit => [
1620
+ $answer1Name, $answer2Name, ... ] >>, where C<$answer1Name > , C<$answer2Name > ,
1621
+ etc., are the names of the "optional" answers that will be given credit if the
1622
+ "goal" answer is correct. Note that the other answers must be assigned names
1623
+ either by calling C<NAMED_ANS_RULE > and C<NAMED_ANS > , or by creating the answer
1624
+ rule in PGML with C<[_]{$answer1}{15}{$answer1Name} > , for example. The answer
1625
+ names should be generated by calling C<NEW_ANS_NAME > (for example,
1626
+ C<$answer1Name = NEW_ANS_NAME() > ) rather than being made up. Otherwise the
1627
+ problem will fail to work in many situations (for example, in tests). For
1628
+ example, to set this up in PGML use
1629
+
1630
+ BEGIN_PGML
1631
+ Optional Answer 1: [_]{$answer1}{10}{$answer1Name = NEW_ANS_NAME()}
1632
+
1633
+ Optional Answer 2: [_]{$answer2}{10}{$answer2Name = NEW_ANS_NAME()}
1634
+
1635
+ Goal: [_]{$answer3}{10}{ cmp_options => { credit => [ $answer1Name, $answer2Name ] } }
1636
+ END_PGML
1637
+
1638
+ Note that the C<credit > and C<weight > options can be used together. For example:
1639
+
1640
+ BEING_PGML
1641
+ Optional Answer: [_]{$optional}{10}{$optionalName = NEW_ANS_NAME()}{{ weight => 20 }}
1642
+
1643
+ Goal: [_]{$goalAnswer}{10}{ cmp_options => { credit => $optionalName, weight => 80 } }
1644
+ END_PGML
1645
+
1646
+ This way, if the "optional" answer is correct but the "goal" answer is not, the
1647
+ problem score will be 20%, but if the "goal" answer is correct, the problem
1648
+ score will be 100%.
1649
+
1650
+ One caveat to keep in mind is that credit is given to an "optional" answer ONLY
1651
+ if the answer is left blank (or is actually correct). Credit will NOT be given
1652
+ if an "optional" answer is incorrect, even if the "goal" answer IS correct.
1653
+
1654
+ When credit is given to an "optional" answer due to the "goal" answer being
1655
+ correct, a message will be added to the "optional" answer stating, "This answer
1656
+ was marked correct because the primary answer is correct."
1657
+
1596
1658
=cut
1597
1659
1598
1660
sub avg_problem_grader {
1599
- my ($rh_evaluated_answers , $rh_problem_state , %form_options ) = @_ ;
1661
+ my ($answers , $problem_state , %form_options ) = @_ ;
1600
1662
1601
- my %evaluated_answers = %{ $rh_evaluated_answers } ;
1663
+ my %problem_result = ( score => 0, errors => ' ' , type => ' avg_problem_grader ' , msg => ' ' ) ;
1602
1664
1603
- # By default the old problem state is simply passed back out again.
1604
- my %problem_state = %$rh_problem_state ;
1605
-
1606
- # Initial setup of the answer.
1607
- my $total = 0;
1608
- my %problem_result = (
1609
- score => 0,
1610
- errors => ' ' ,
1611
- type => ' avg_problem_grader' ,
1612
- msg => ' ' ,
1613
- );
1614
- my $count = keys %evaluated_answers ;
1615
- $problem_result {msg } = maketext(' You can earn partial credit on this problem.' ) if $count > 1;
1665
+ $problem_result {msg } = maketext(' You can earn partial credit on this problem.' ) if keys %$answers > 1;
1616
1666
1617
1667
# Return unless answers have been submitted.
1618
- return (\%problem_result , \ % problem_state ) unless $form_options {answers_submitted } == 1;
1668
+ return (\%problem_result , $ problem_state ) unless $form_options {answers_submitted } == 1;
1619
1669
1620
- # Answers have been submitted -- process them.
1621
- for my $ans_name (keys %evaluated_answers ) {
1622
- if (ref $evaluated_answers {$ans_name } eq ' HASH' || ref $evaluated_answers {$ans_name } eq ' AnswerHash' ) {
1623
- $total += $evaluated_answers {$ans_name }{score } // 0;
1670
+ my %credit ;
1671
+
1672
+ # Get the score for each answer (error if can't recognize the answer format).
1673
+ for my $ans_name (keys %$answers ) {
1674
+ if (ref ($answers -> {$ans_name }) =~ m / ^(HASH|AnswerHash)$ / ) {
1675
+ $credit {$ans_name } = $answers -> {$ans_name }{score } // 0;
1624
1676
} else {
1677
+ $problem_result {error } = " Error: Answer $ans_name is not a hash: $answers ->{$ans_name }" ;
1625
1678
die " Error: Answer |$ans_name | is not a hash reference\n "
1626
- . $evaluated_answers {$ans_name }
1627
- . ' This probably means that the answer evaluator for this answer is not working correctly.' ;
1628
- $problem_result {error } = " Error: Answer $ans_name is not a hash: $evaluated_answers {$ans_name }" ;
1679
+ . $answers -> {$ans_name }
1680
+ . " \n This probably means that the answer evaluator for this answer is not working correctly." ;
1629
1681
}
1630
1682
}
1631
1683
1632
- # Calculate the score.
1633
- $problem_result {score } = $total / $count if $count ;
1684
+ # Mark any optional answers as correct, if the goal answers are right and the optional answers are blank.
1685
+ for my $ans_name (keys %$answers ) {
1686
+ if ($credit {$ans_name } == 1 && defined $answers -> {$ans_name }{credit }) {
1687
+ for my $credit_name (
1688
+ ref ($answers -> {$ans_name }{credit }) eq ' ARRAY'
1689
+ ? @{ $answers -> {$ans_name }{credit } }
1690
+ : $answers -> {$ans_name }{credit })
1691
+ {
1692
+ if (!defined $answers -> {$credit_name }{student_ans }
1693
+ || $answers -> {$credit_name }{student_ans } =~ m / ^\s *$ / )
1694
+ {
1695
+ $answers -> {$credit_name }{score } = 1;
1696
+ $answers -> {$credit_name }{ans_message } =
1697
+ maketext(' This answer was marked correct because the primary answer is correct.' );
1698
+ $credit {$credit_name } = 1;
1699
+ }
1700
+ }
1701
+ }
1702
+ }
1634
1703
1635
- ++$problem_state {num_of_correct_ans } if $total == $count ;
1636
- ++$problem_state {num_of_incorrect_ans } if $total < $count ;
1637
- $problem_state {recorded_score } //= 0;
1704
+ my ($score , $total ) = (0, 0);
1705
+
1706
+ # Add up the weighted scores
1707
+ for my $ans_name (keys %$answers ) {
1708
+ my $weight = $answers -> {$ans_name }{weight } // 1;
1709
+ $total += $weight ;
1710
+ $score += $weight * $credit {$ans_name };
1711
+ }
1712
+
1713
+ $problem_result {score } = $total ? $score / $total : 0;
1714
+
1715
+ ++$problem_state -> {num_of_correct_ans } if $score == $total ;
1716
+ ++$problem_state -> {num_of_incorrect_ans } if $score < $total ;
1717
+ $problem_state -> {recorded_score } //= 0;
1638
1718
1639
1719
# Increase recorded score if the current score is greater.
1640
- $problem_state {recorded_score } = $problem_result {score }
1641
- if $problem_result {score } > $problem_state {recorded_score };
1720
+ $problem_state -> {recorded_score } = $problem_result {score }
1721
+ if $problem_result {score } > $problem_state -> {recorded_score };
1642
1722
1643
- warn " Error in grading this problem the total $total is larger than $count " if $total > $count ;
1723
+ warn " Error in grading this problem: The score $score is larger than the total $total . " if $score > $total ;
1644
1724
1645
- return (\%problem_result , \ % problem_state );
1725
+ return (\%problem_result , $ problem_state );
1646
1726
}
1647
1727
1648
1728
=head2 Utility subroutines
0 commit comments