Parent Directory
|
Revision Log
merging with HEAD 6/23/2008 see Value.pm for list of significant changes
1 sub _compoundProblem_init {}; # don't reload this file 2 3 ###################################################################### 4 # 5 # This package implements a method of handling multi-part problems 6 # that show only a single part at any one time. The students can 7 # work on one part at a time, and then when they get it right (or 8 # under other circumstances deterimed by the professor), they can 9 # move on to the next part. Students can not return to earlier parts 10 # once they have been completed. The score for problem as a whole is 11 # made up from the scores on the individual parts, and the relative 12 # weighting of the various parts can be specified by the problem 13 # author. 14 # 15 # To use the compoundProblem library, use 16 # 17 # loadMacros("compoundProblem.pl"); 18 # 19 # at the top of your file, and then create a compoundProblem object 20 # via the command 21 # 22 # $cp = new compoundProblem(options) 23 # 24 # where '$cp' is the name of a variable that you will use to 25 # refer to the compound problem, and 'options' can include: 26 # 27 # parts => n The number of parts in the problem. 28 # Default: 1 29 # 30 # weights => [n1,...,nm] The relative weights to give to each 31 # part in the problem. For example, 32 # weights => [2,1,1] 33 # would cause the first part to be worth 50% 34 # of the points (twice the amount for each of 35 # the other two), while the second and third 36 # part would be worth 25% each. If weights 37 # are not supplied, the parts are weighted 38 # by the number of answer blanks in each part 39 # (and you must provide the total number of 40 # blanks in all the parts by supplying the 41 # totalAnswers option). 42 # 43 # totalAnswers => n The total number of answer blanks in all 44 # the parts put together (this is used when 45 # computing the per-part scores, if part 46 # weights are not provided). 47 # 48 # saveAllAnswers => 0 or 1 Usually, the contents of named answer blanks 49 # from previous parts are made available to 50 # later parts using variables with the 51 # same name as the answer blank. Setting 52 # saveAllAnswers to 1 will cause ALL answer 53 # blanks to be available (via variables 54 # like $AnSwEr1, and so on). 55 # Default: 0 56 # 57 # parserValues => 0 or 1 Determines whether the answers from previous 58 # parts are returned as MathObjects (like 59 # those returned from Real(), Vector(), etc) 60 # or as strings (the unparsed contents of the 61 # student answer). If you intend to use the 62 # previous answers as numbers, for example, 63 # you would want to set this to 1 so that you 64 # would get the final result of any formula 65 # the student typed, rather than the formula 66 # itself as a character string. 67 # Default: 0 68 # 69 # nextVisible => type Tells when the "go on to the next part" option 70 # is available to the student. The possible 71 # types include: 72 # 73 # 'ifCorrect' next is available only when 74 # all the answers are correct. 75 # 76 # 'Always' next is always available 77 # (but remember that students 78 # can't go back once they go 79 # on.) 80 # 81 # 'Never' next is never allowed (the 82 # problem will control going 83 # on to the next part itself). 84 # 85 # Default: 'ifCorrect' 86 # 87 # nextStyle => type Determines the style of "next" indicator to display 88 # (when it is available). The type can be one of: 89 # 90 # 'CheckBox' a checkbox that allows the students 91 # to go on to the next part when they 92 # submit their answers. 93 # 94 # 'Button' a button that submits their answers 95 # and goes on to the next part. 96 # 97 # 'Forced' forces the student to go on to the 98 # next part the next time they submit 99 # answers. 100 # 101 # 'HTML' allows you to provide an arbitrary 102 # HTML string of your own. 103 # 104 # Default: 'Checkbox' 105 # 106 # nextLabel => string Specifies the string to use as the label for the checkbox, 107 # the name of the button, the text of the message indicating 108 # that the next submit will move to the next part, or the 109 # HTML string, depending on the setting of nextStyle above. 110 # 111 # nextNoChange => 0 or 1 Since the students must submit their answers again to go on 112 # to the next part, it is possible for them to change their 113 # answers before they submit, and if nextVisible is 'ifCorrect' 114 # they might go on to the next without having correct answers 115 # stored. This option lets you control whether the answers 116 # are checked against the previous ones before going on to the 117 # next part. If the answers don't match, a warning is issued 118 # and they are not allowed to move on. 119 # Default: 1 120 # 121 # allowReset => 0 or 1 Determines whether a "Go back to the first part" checkbox 122 # is provided on parts 2 and later. This is intended for 123 # the professor during testing of the problem (otherwise 124 # it would be impossible to go back to earlier parts). 125 # Default: 0 126 # 127 # resetLabel => string The string used to label the reset checkbox. 128 # 129 # Once you have created a compoundProblem object, you can use $cp->part to 130 # determine the part that the student is working on, and use 'if' statements 131 # to display the proper information for the given part. The compoundProblem 132 # object takes care of maintaining the data as the parts change. (See the 133 # compoundProblem.pg file for an example of a compound problem.) 134 # 135 # In order to handle the scoring of the problem as a whole when only part is 136 # showing, the compoundProblem object uses its own problem grader to manage 137 # the scores, and calls your own grader from there. The default is to use 138 # the one that was installed before the compoundProblem object was created, 139 # or avg_problem_grader if none was installed. You can specify a different 140 # one using the $cp->useGrader() method (see below). It is important that 141 # you NOT call install_problem_grader() yourself once you have created the 142 # compoundProblem object, as that would disable the special grader, causing 143 # the compound problem to fail to work properly. 144 # 145 # You may call the following methods once you have a compoundProblem: 146 # 147 # $cp->part Returns the part the student is working on. 148 # $cp->part(n) Sets the part to be part n, as long as the 149 # student has finished the preceeding parts. 150 # If not, the part is set to the highest 151 # one the student hasn't completed, and he 152 # can work up to the given part. (The 153 # nextVisible option is set to 'ifCorrect' if 154 # it was 'Never' so that students can go on 155 # once they finish the earlier parts.) 156 # 157 # $cp->useGrader(code_ref) Supplies your own grader to use in 158 # place of the default one. For example: 159 # $cp->useGrader(~~&std_problem_grader); 160 # 161 # $cp->score Returns the (weighted) score for this part. 162 # Note that this is the score shown at the bottom 163 # of the page on which the student pressed submit 164 # (not the score for the answers the student is 165 # submitting -- that is not available until 166 # after the body of the problem has been created). 167 # 168 # $cp->scoreRaw Returns the unweighted score for this part. 169 # 170 # $cp->scoreOverall Returns the overall score for the problem 171 # so far. 172 # 173 # $cp->addAnswers(list) Make additional answer blanks be available 174 # from one part to another. E.g., 175 # $cp->addAnswers('AnSwEr1'); 176 # would make the first unnamed blank be available 177 # in later parts as well. (This command should 178 # be issued only when the part containing the 179 # given answer blank is displayed.) 180 # 181 # $cp->nextCheckbox(label) Returns the HTML string for the "go on to next 182 # part" checkbox so you can use it in the body of 183 # the problem if you wish. This should not be 184 # inserted when the $displayMode is 'TeX'. If the 185 # label is not given or is blank, the default label 186 # is used. 187 # 188 # $cp->nextButton(label) Returns the HTML string for the "go on to next 189 # part" button so you can use it in the body of 190 # the problem if you wish. This should not be 191 # inserted when the $displayMode is 'TeX'. If the 192 # label is not given or is blank, the default label 193 # is used. 194 # 195 # $cp->nextForces(label) Returns the HTML string for the forced "go on to 196 # next part" so you can use it in the body of 197 # the problem if you wish. This should not be 198 # inserted when the $displayMode is 'TeX'. If the 199 # label is not given or is blank, the default label 200 # is used. 201 # 202 # $cp->reset Go back to part 1, clearing the answers 203 # and score. (Best used when debugging problems.) 204 # 205 # $cp->resetCheckbox(label) Returns the HTML string for the reset checkbox 206 # so that you can provide one within the body 207 # of the problem if you wish. This should not be 208 # inserted when the $displayMode is 'TeX'. If the 209 # label is not given or is blank, the default label 210 # will be used. 211 # 212 213 ###################################################################### 214 215 216 package compoundProblem; 217 218 # 219 # The state data that is stored between invocations of 220 # the problem. 221 # 222 our %defaultStatus = ( 223 part => 1, # the current part 224 answers => "", # answer labels from previous parts 225 new_answers => "", # answer labels for THIS part 226 ans_rule_count => 0, # the ans_rule count from previous parts 227 new_ans_rule_count => 0, # the ans_rule count from THIS part 228 score => 0, # the (weighted) score on this part 229 total => 0, # the total on previous parts 230 raw => 0, # raw score on this part 231 ); 232 233 # 234 # Create a new instance of the compound Problem and initialize 235 # it. This includes reading the status from the previous 236 # parts, defining the variables from the answers to previous parts, 237 # and setting up the grader so that the current data can be saved. 238 # 239 sub new { 240 my $self = shift; my $class = ref($self) || $self; 241 my $cp = bless { 242 parts => 1, 243 totalAnswers => undef, 244 weights => undef, # array of weights per part 245 saveAllAnswers => 0, # usually only save named answers 246 parserValues => 0, # make Parser objects from the answers? 247 nextVisible => "ifCorrect", # or "Always" or "Never" 248 nextStyle => "Checkbox", # or "Button", "Forced", or "HTML" 249 nextLabel => undef, # Checkbox text or button name or HTML 250 nextNoChange => 1, # true if answer can't change for new part 251 allowReset => 0, # true to show "back to part 1" button 252 resetLabel => undef, # label for reset button 253 grader => $main::PG_FLAGS{PROBLEM_GRADER_TO_USE} || \&main::avg_problem_grader, 254 @_, 255 status => $defaultStatus, 256 }, $class; 257 die "You must provide either the totalAnswers or weights" 258 unless $cp->{totalAnswers} || $cp->{weights}; 259 $cp->getTotalWeight if $cp->{weights}; 260 main::loadMacros("Parser.pl") if $cp->{parserValues}; 261 $cp->reset if $cp->{allowReset} && $main::inputs_ref->{_reset}; 262 $cp->getStatus; 263 $cp->initPart; 264 return $cp; 265 } 266 267 # 268 # Compute the total of the weights so that the parts can 269 # be properly scaled. 270 # 271 sub getTotalWeight { 272 my $self = shift; 273 $self->{totalWeight} = 0; $self->{totalAnswers} = 1; 274 foreach my $w (@{$self->{weights}}) {$self->{totalWeight} += $w} 275 $self->{totalWeight} = 1 if $self->{totalWeight} == 0; 276 } 277 278 # 279 # Look up the status from the previous invocation 280 # and see if we need to go on to the next part. 281 # 282 sub getStatus { 283 my $self = shift; 284 main::RECORD_FORM_LABEL("_next"); 285 main::RECORD_FORM_LABEL("_status"); 286 $self->{status} = $self->decode; 287 $self->{isNew} = $main::inputs_ref->{_next} || ($main::inputs_ref->{submitAnswers} && 288 $main::inputs_ref->{submitAnswers} eq ($self->{nextLabel} || "Go on to Next Part")); 289 if ($self->{isNew}) { 290 $self->checkAnswers; 291 $self->incrementPart unless $self->{nextNoChange} && $self->{answersChanged}; 292 } 293 } 294 295 # 296 # Initialize the current part by setting the ans_rule 297 # count (so that later parts will get unique answer names), 298 # installing the grader (to save the data), and setting 299 # the variables for previous answers. 300 # 301 sub initPart { 302 my $self = shift; 303 $main::ans_rule_count = $self->{status}{ans_rule_count}; 304 main::install_problem_grader(\&compoundProblem::grader); 305 $main::PG_FLAGS{compoundProblem} = $self; 306 $self->initAnswers($self->{status}{answers}); 307 } 308 309 # 310 # Look through the list of answer labels and set 311 # the variables for them to be the associated student 312 # answer. Make it a Parser value if requested. 313 # Record the value so that is will be available 314 # again on the next invocation. 315 # 316 sub initAnswers { 317 my $self = shift; my $answers = shift; 318 foreach my $id (split(/;/,$answers)) { 319 my $value = $main::inputs_ref->{$id}; $value = '' unless defined($value); 320 if ($self->{parserValues}) { 321 my $parser = Parser::Formula($value); 322 $parser = Parser::Evaluate($parser) if $parser && $parser->isConstant; 323 $value = $parser if $parser; 324 } 325 ${"main::$id"} = $value unless $id =~ m/$main::ANSWER_PREFIX/o; 326 $value = quoteHTML($value); 327 main::TEXT(qq!<input type="hidden" name="$id" value="$value" />!); 328 main::RECORD_FORM_LABEL($id); 329 } 330 } 331 332 # 333 # Look to see is any answers have changed on this 334 # invocation of the problem. 335 # 336 sub checkAnswers { 337 my $self = shift; 338 foreach my $id (keys(%{$main::inputs_ref})) { 339 if ($id =~ m/^previous_(.*)$/) { 340 if ($main::inputs_ref->{$id} ne $main::inputs_ref->{$1}) { 341 $self->{answersChanged} = 1; 342 $self->{isNew} = 0 if $self->{nextNoChange}; 343 return; 344 } 345 } 346 } 347 } 348 349 # 350 # Go on to the next part, updating the status 351 # to include the data from the old part so that 352 # it will be properly preserved when the next 353 # part is showing. 354 # 355 sub incrementPart { 356 my $self = shift; 357 my $status = $self->{status}; 358 if ($status->{part} < $self->{parts}) { 359 $status->{part}++; 360 $status->{answers} .= ';' if $status->{answers}; 361 $status->{answers} .= $status->{new_answers}; 362 $status->{ans_rule_count} = $status->{new_ans_rule_count}; 363 $status->{total} += $status->{score}; 364 $status->{score} = $status->{raw} = 0; 365 $status->{new_answers} = ''; 366 } 367 } 368 369 ###################################################################### 370 371 # 372 # Encode all the status information so that it can be 373 # maintained as the student submits answers. Since this 374 # state information includes things like the score from 375 # the previous parts, it is "encrypted" using a dumb 376 # hex encoding (making it harder for a student to recognize 377 # it as valuable data if they view the page source). 378 # 379 sub encode { 380 my $self = shift; my $status = shift || $self->{status}; 381 my @data = (); my $data = ""; 382 foreach my $id (main::lex_sort(keys(%defaultStatus))) {push(@data,$status->{$id})} 383 foreach my $c (split(//,join('|',@data))) {$data .= toHex($c)} 384 return $data; 385 } 386 387 # 388 # Decode the data and break it into the status hash. 389 # 390 sub decode { 391 my $self = shift; my $status = shift || $main::inputs_ref->{_status}; 392 return {%defaultStatus} unless $status; 393 my @data = (); foreach my $hex (split(/(..)/,$status)) {push(@data,fromHex($hex)) if $hex ne ''} 394 @data = split('\\|',join('',@data)); $status = {%defaultStatus}; 395 foreach my $id (main::lex_sort(keys(%defaultStatus))) {$status->{$id} = shift(@data)} 396 return $status; 397 } 398 399 400 # 401 # Hex encoding is shifted by 10 to obfuscate it further. 402 # (shouldn't be a problem since the status will be made of 403 # printable characters, so they are all above ASCII 32) 404 # 405 sub toHex {main::spf(ord(shift)-10,"%X")} 406 sub fromHex {main::spf(hex(shift)+10,"%c")} 407 408 409 # 410 # Make sure the data can be properly preserved within 411 # an HTML <INPUT TYPE="HIDDEN"> tag. 412 # 413 sub quoteHTML { 414 my $string = shift; 415 $string =~ s/&/\&/g; $string =~ s/"/\"/g; 416 $string =~ s/>/\>/g; $string =~ s/</\</g; 417 return $string; 418 } 419 420 ###################################################################### 421 422 # 423 # Set the grader for this part to the specified one. 424 # 425 sub useGrader { 426 my $self = shift; 427 $self->{grader} = shift; 428 } 429 430 # 431 # Make additional answer blanks from the current part 432 # be preserved for use in future parts. 433 # 434 sub addAnswers { 435 my $self = shift; 436 $self->{extraAnswers} = [] unless $self->{extraAnswers}; 437 push(@{$self->{extraAnswers}},@_); 438 } 439 440 # 441 # Go back to part 1 and clear the answers and scores. 442 # 443 sub reset { 444 my $self = shift; 445 if ($main::inputs_ref->{_status}) { 446 my $status = $self->decode($main::inputs_ref->{_status}); 447 foreach my $id (split(/;/,$status->{answers})) {delete $main::inputs_ref->{$id}} 448 foreach my $id (1..$status->{ans_rule_count}) 449 {delete $main::inputs_ref->{"${main::QUIZ_PREFIX}${main::ANSWER_PREFIX}$id"}} 450 } 451 $main::inputs_ref->{_status} = $self->encode(\%defaultStatus); 452 $main::inputs_ref->{_next} = 0; 453 } 454 455 # 456 # Return the HTML for the "Go back to part 1" checkbox. 457 # 458 sub resetCheckbox { 459 my $self = shift; 460 my $label = shift || " <b>Go back to Part 1</b> (when you submit your answers)."; 461 my $par = shift; $par = ($par ? $main::PAR : ''); 462 qq'$par<input type="checkbox" name="_reset" value="1" />$label'; 463 } 464 465 # 466 # Return the HTML for the "next part" checkbox. 467 # 468 sub nextCheckbox { 469 my $self = shift; 470 my $label = shift || " <b>Go on to next part</b> (when you submit your answers)."; 471 my $par = shift; $par = ($par ? $main::PAR : ''); 472 $self->{nextInserted} = 1; 473 qq!$par<input type="checkbox" name="_next" value="next" />$label!; 474 } 475 476 # 477 # Return the HTML for the "next part" button. 478 # 479 sub nextButton { 480 my $self = shift; 481 my $label = quoteHTML(shift || "Go on to Next Part"); 482 my $par = shift; $par = ($par ? $main::PAR : ''); 483 $par . qq!<input type="submit" name="submitAnswers" value="$label" ! 484 . q!onclick="document.getElementById('_next').value=1" />!; 485 } 486 487 # 488 # Return the HTML for when going to the next part is forced. 489 # 490 sub nextForced { 491 my $self = shift; 492 my $label = shift || "<b>Submit your answers again to go on to the next part.</b>"; 493 $label = $main::PAR . $label if shift; 494 $self->{nextInserted} = 1; 495 qq!$label<input type="hidden" name="_next" id="_next" value="Next" />!; 496 } 497 498 # 499 # Return the raw HTML provided 500 # 501 sub nextHTML {shift; shift} 502 503 ###################################################################### 504 505 # 506 # Return the current part, or try to set the part to the given 507 # part (returns the part actually set, which may be earlier if 508 # the student didn't complete an earlier part). 509 # 510 sub part { 511 my $self = shift; my $status = $self->{status}; 512 my $part = shift; 513 return $status->{part} unless defined $part && $main::displayMode ne 'TeX'; 514 $part = 1 if $part < 1; $part = $self->{parts} if $part > $self->{parts}; 515 if ($part > $status->{part} && !$main::inputs_ref->{_noadvance}) { 516 unless ((lc($self->{nextVisible}) eq 'ifcorrect' && $status->{raw} < 1) || 517 lc($self->{nextVisible}) eq 'never') { 518 $self->initAnswers($status->{new_answers}); 519 $self->incrementPart; $self->{isNew} = 1; 520 } 521 } 522 if ($part != $status->{part}) { 523 main::TEXT('<input type="hidden" name="_noadvance" value="1" />'); 524 $self->{nextVisible} = 'IfCorrect' if lc($self->{nextVisible}) eq 'never'; 525 } 526 return $status->{part}; 527 } 528 529 # 530 # Return the various scores 531 # 532 sub score {shift->{status}{score}} 533 sub scoreRaw {shift->{status}{raw}} 534 sub scoreOverall { 535 my $self = shift; 536 return $self->{status}{score} + $self->{status}{total}; 537 } 538 539 ###################################################################### 540 # 541 # The custom grader that does the work of computing the scores 542 # and saving the data. 543 # 544 sub grader { 545 my $self = $main::PG_FLAGS{compoundProblem}; 546 547 # 548 # Get the answer names and the weight for the current part. 549 # 550 my @answers = keys(%{$_[0]}); 551 my $weight = scalar(@answers)/$self->{totalAnswers}; 552 $weight = $self->{weights}[$self->{status}{part}-1]/$self->{totalWeight} 553 if $self->{weights} && defined($self->{weights}[$self->{status}{part}-1]); 554 @answers = grep(!/$main::ANSWER_PREFIX/o,@answers) unless $self->{saveAllAnswers}; 555 push(@answers,@{$self->{extraAnswers}}) if $self->{extraAnswers}; 556 my $space = '<img src="about:blank" style="height:1px; width:3em; visibility:hidden" />'; 557 558 # 559 # Call the original grader, but put back the old recorded_score 560 # (the grader will have updated it based on the score for the PART, 561 # not the problem as a whole). 562 # 563 my $oldScore = ($_[1])->{recorded_score}; 564 my ($result,$state) = &{$self->{grader}}(@_); 565 $state->{recorded_score} = $oldScore; 566 567 # 568 # Update that state information and encode it. 569 # 570 my $status = $self->{status}; 571 $status->{raw} = $result->{score}; 572 $status->{score} = $result->{score}*$weight; 573 $status->{new_ans_rule_count} = $main::ans_rule_count; 574 $status->{new_answers} = join(';',@answers); 575 my $data = quoteHTML($self->encode); 576 577 # 578 # Update the recorded score 579 # 580 my $newScore = $status->{total} + $status->{score}; 581 $state->{recorded_score} = $newScore if $newScore > $oldScore; 582 $state->{recorded_score} = 0 if $self->{allowReset} && $main::inputs_ref->{_reset}; 583 584 # 585 # Add the compoundProblem message and data 586 # 587 $result->{type} = "compoundProblem ($result->{type})"; 588 $result->{msg} .= '</i><p><b>Note:</b> <i>' if $result->{msg}; 589 $result->{msg} .= 'This problem has more than one part.' 590 . '<br/>'.$space.'<small>Your score for this attempt is for this part only;</small>' 591 . '<br/>'.$space.'<small>your overall score is for all the parts combined.</small>' 592 . qq!<input type="hidden" name="_status" value="$data" />!; 593 594 # 595 # Warn if the answers changed when they shouldn't have 596 # 597 $result->{msg} .= '<p><b>You may not change your answers when going on to the next part!</b>' 598 if $self->{nextNoChange} && $self->{answersChanged}; 599 600 # 601 # Include the "next part" checkbox, button, or whatever. 602 # 603 my $par = 1; 604 if ($self->{parts} > $status->{part} && !$main::inputs_ref->{previewAnswers}) { 605 if (lc($self->{nextVisible}) eq 'always' || 606 (lc($self->{nextVisible}) eq 'ifcorrect' && $result->{score} >= 1)) { 607 my $method = "next".$self->{nextStyle}; $par = 0; 608 $result->{msg} .= $self->$method($self->{nextLabel},1).'<br/>'; 609 } 610 } 611 612 # 613 # Add the reset checkbox, if needed 614 # 615 $result->{msg} .= $self->resetCheckbox($self->{resetLabel},$par) 616 if $self->{allowReset} && $status->{part} > 1; 617 618 # 619 # Make sure we don't go on unless the next button really is checked 620 # 621 $result->{msg} .= '<input type="hidden" name="_next" value="0" />' 622 unless $self->{nextInserted}; 623 624 return ($result,$state); 625 }
| aubreyja at gmail dot com | ViewVC Help |
| Powered by ViewVC 1.0.9 |