Parent Directory
|
Revision Log
replace calls to PG_FLAGS with calls to
$main::PG->{flags}
This will work for now at least.
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 images_created => 0, # the image count from the precious parts 229 new_images_created => 0, # the image count from THIS part 230 imageName => "", # name of images_created image file 231 score => 0, # the (weighted) score on this part 232 total => 0, # the total on previous parts 233 raw => 0, # raw score on this part 234 ); 235 236 # 237 # Create a new instance of the compound Problem and initialize 238 # it. This includes reading the status from the previous 239 # parts, defining the variables from the answers to previous parts, 240 # and setting up the grader so that the current data can be saved. 241 # 242 sub new { 243 my $self = shift; my $class = ref($self) || $self; 244 my $cp = bless { 245 parts => 1, 246 totalAnswers => undef, 247 weights => undef, # array of weights per part 248 saveAllAnswers => 0, # usually only save named answers 249 parserValues => 0, # make Parser objects from the answers? 250 nextVisible => "ifCorrect", # or "Always" or "Never" 251 nextStyle => "Checkbox", # or "Button", "Forced", or "HTML" 252 nextLabel => undef, # Checkbox text or button name or HTML 253 nextNoChange => 1, # true if answer can't change for new part 254 allowReset => 0, # true to show "back to part 1" button 255 resetLabel => undef, # label for reset button 256 grader => $main::PG->{flags}->{PROBLEM_GRADER_TO_USE} || \&main::avg_problem_grader, 257 @_, 258 status => $defaultStatus, 259 }, $class; 260 die "You must provide either the totalAnswers or weights" 261 unless $cp->{totalAnswers} || $cp->{weights}; 262 $cp->getTotalWeight if $cp->{weights}; 263 main::loadMacros("Parser.pl") if $cp->{parserValues}; 264 $cp->reset if $cp->{allowReset} && $main::inputs_ref->{_reset}; 265 $cp->getStatus; 266 $cp->initPart; 267 return $cp; 268 } 269 270 # 271 # Compute the total of the weights so that the parts can 272 # be properly scaled. 273 # 274 sub getTotalWeight { 275 my $self = shift; 276 $self->{totalWeight} = 0; $self->{totalAnswers} = 1; 277 foreach my $w (@{$self->{weights}}) {$self->{totalWeight} += $w} 278 $self->{totalWeight} = 1 if $self->{totalWeight} == 0; 279 } 280 281 # 282 # Look up the status from the previous invocation 283 # and see if we need to go on to the next part. 284 # 285 sub getStatus { 286 my $self = shift; 287 main::RECORD_FORM_LABEL("_next"); 288 main::RECORD_FORM_LABEL("_status"); 289 $self->{status} = $self->decode; 290 $self->{isNew} = $main::inputs_ref->{_next} || ($main::inputs_ref->{submitAnswers} && 291 $main::inputs_ref->{submitAnswers} eq ($self->{nextLabel} || "Go on to Next Part")); 292 if ($self->{isNew}) { 293 $self->checkAnswers; 294 $self->incrementPart unless $self->{nextNoChange} && $self->{answersChanged}; 295 } 296 } 297 298 # 299 # Initialize the current part by setting the ans_rule 300 # count (so that later parts will get unique answer names), 301 # installing the grader (to save the data), and setting 302 # the variables for previous answers. 303 # 304 sub initPart { 305 my $self = shift; 306 $main::ans_rule_count = $self->{status}{ans_rule_count}; 307 $main::images_created{$self->{status}{imageName}} = $self->{status}{images_created} 308 if $self->{status}{imageName}; 309 main::install_problem_grader(\&compoundProblem::grader); 310 $main::PG->{flags}->{compoundProblem} = $self; 311 $self->initAnswers($self->{status}{answers}); 312 } 313 314 # 315 # Look through the list of answer labels and set 316 # the variables for them to be the associated student 317 # answer. Make it a Parser value if requested. 318 # Record the value so that is will be available 319 # again on the next invocation. 320 # 321 sub initAnswers { 322 my $self = shift; my $answers = shift; 323 foreach my $id (split(/;/,$answers)) { 324 my $value = $main::inputs_ref->{$id}; $value = '' unless defined($value); 325 if ($self->{parserValues}) { 326 my $parser = Parser::Formula($value); 327 $parser = Parser::Evaluate($parser) if $parser && $parser->isConstant; 328 $value = $parser if $parser; 329 } 330 ${"main::$id"} = $value unless $id =~ m/$main::ANSWER_PREFIX/o; 331 $value = quoteHTML($value); 332 main::TEXT(qq!<input type="hidden" name="$id" value="$value" />!); 333 main::RECORD_FORM_LABEL($id); 334 } 335 } 336 337 # 338 # Look to see is any answers have changed on this 339 # invocation of the problem. 340 # 341 sub checkAnswers { 342 my $self = shift; 343 foreach my $id (keys(%{$main::inputs_ref})) { 344 if ($id =~ m/^previous_(.*)$/) { 345 if ($main::inputs_ref->{$id} ne $main::inputs_ref->{$1}) { 346 $self->{answersChanged} = 1; 347 $self->{isNew} = 0 if $self->{nextNoChange}; 348 return; 349 } 350 } 351 } 352 } 353 354 # 355 # Go on to the next part, updating the status 356 # to include the data from the old part so that 357 # it will be properly preserved when the next 358 # part is showing. 359 # 360 sub incrementPart { 361 my $self = shift; 362 my $status = $self->{status}; 363 if ($status->{part} < $self->{parts}) { 364 $status->{part}++; 365 $status->{answers} .= ';' if $status->{answers}; 366 $status->{answers} .= $status->{new_answers}; 367 $status->{ans_rule_count} = $status->{new_ans_rule_count}; 368 $status->{images_created} = $status->{new_images_created}; 369 $status->{total} += $status->{score}; 370 $status->{score} = $status->{raw} = 0; 371 $status->{new_answers} = ''; 372 } 373 } 374 375 ###################################################################### 376 377 # 378 # Encode all the status information so that it can be 379 # maintained as the student submits answers. Since this 380 # state information includes things like the score from 381 # the previous parts, it is "encrypted" using a dumb 382 # hex encoding (making it harder for a student to recognize 383 # it as valuable data if they view the page source). 384 # 385 sub encode { 386 my $self = shift; my $status = shift || $self->{status}; 387 my @data = (); my $data = ""; 388 foreach my $id (main::lex_sort(keys(%defaultStatus))) {push(@data,$status->{$id})} 389 foreach my $c (split(//,join('|',@data))) {$data .= toHex($c)} 390 return $data; 391 } 392 393 # 394 # Decode the data and break it into the status hash. 395 # 396 sub decode { 397 my $self = shift; my $status = shift || $main::inputs_ref->{_status}; 398 return {%defaultStatus} unless $status; 399 my @data = (); foreach my $hex (split(/(..)/,$status)) {push(@data,fromHex($hex)) if $hex ne ''} 400 @data = split('\\|',join('',@data)); $status = {%defaultStatus}; 401 if (scalar(@data) == 8) { 402 # insert imageName, images_created, new_images_created, if missing 403 splice(@data,2,0,"",0); splice(@data,6,0,0); 404 } 405 foreach my $id (main::lex_sort(keys(%defaultStatus))) {$status->{$id} = shift(@data)} 406 return $status; 407 } 408 409 410 # 411 # Hex encoding is shifted by 10 to obfuscate it further. 412 # (shouldn't be a problem since the status will be made of 413 # printable characters, so they are all above ASCII 32) 414 # 415 sub toHex {main::spf(ord(shift)-10,"%X")} 416 sub fromHex {main::spf(hex(shift)+10,"%c")} 417 418 419 # 420 # Make sure the data can be properly preserved within 421 # an HTML <INPUT TYPE="HIDDEN"> tag. 422 # 423 sub quoteHTML { 424 my $string = shift; 425 $string =~ s/&/\&/g; $string =~ s/"/\"/g; 426 $string =~ s/>/\>/g; $string =~ s/</\</g; 427 return $string; 428 } 429 430 ###################################################################### 431 432 # 433 # Set the grader for this part to the specified one. 434 # 435 sub useGrader { 436 my $self = shift; 437 $self->{grader} = shift; 438 } 439 440 # 441 # Make additional answer blanks from the current part 442 # be preserved for use in future parts. 443 # 444 sub addAnswers { 445 my $self = shift; 446 $self->{extraAnswers} = [] unless $self->{extraAnswers}; 447 push(@{$self->{extraAnswers}},@_); 448 } 449 450 # 451 # Go back to part 1 and clear the answers and scores. 452 # 453 sub reset { 454 my $self = shift; 455 if ($main::inputs_ref->{_status}) { 456 my $status = $self->decode($main::inputs_ref->{_status}); 457 foreach my $id (split(/;/,$status->{answers})) {delete $main::inputs_ref->{$id}} 458 foreach my $id (1..$status->{ans_rule_count}) 459 {delete $main::inputs_ref->{"${main::QUIZ_PREFIX}${main::ANSWER_PREFIX}$id"}} 460 } 461 $main::inputs_ref->{_status} = $self->encode(\%defaultStatus); 462 $main::inputs_ref->{_next} = 0; 463 } 464 465 # 466 # Return the HTML for the "Go back to part 1" checkbox. 467 # 468 sub resetCheckbox { 469 my $self = shift; 470 my $label = shift || " <b>Go back to Part 1</b> (when you submit your answers)."; 471 my $par = shift; $par = ($par ? $main::PAR : ''); 472 qq'$par<input type="checkbox" name="_reset" value="1" />$label'; 473 } 474 475 # 476 # Return the HTML for the "next part" checkbox. 477 # 478 sub nextCheckbox { 479 my $self = shift; 480 my $label = shift || " <b>Go on to next part</b> (when you submit your answers)."; 481 my $par = shift; $par = ($par ? $main::PAR : ''); 482 $self->{nextInserted} = 1; 483 qq!$par<input type="checkbox" name="_next" value="next" />$label!; 484 } 485 486 # 487 # Return the HTML for the "next part" button. 488 # 489 sub nextButton { 490 my $self = shift; 491 my $label = quoteHTML(shift || "Go on to Next Part"); 492 my $par = shift; $par = ($par ? $main::PAR : ''); 493 $par . qq!<input type="submit" name="submitAnswers" value="$label" ! 494 . q!onclick="document.getElementById('_next').value=1" />!; 495 } 496 497 # 498 # Return the HTML for when going to the next part is forced. 499 # 500 sub nextForced { 501 my $self = shift; 502 my $label = shift || "<b>Submit your answers again to go on to the next part.</b>"; 503 $label = $main::PAR . $label if shift; 504 $self->{nextInserted} = 1; 505 qq!$label<input type="hidden" name="_next" id="_next" value="Next" />!; 506 } 507 508 # 509 # Return the raw HTML provided 510 # 511 sub nextHTML {shift; shift} 512 513 ###################################################################### 514 515 # 516 # Return the current part, or try to set the part to the given 517 # part (returns the part actually set, which may be earlier if 518 # the student didn't complete an earlier part). 519 # 520 sub part { 521 my $self = shift; my $status = $self->{status}; 522 my $part = shift; 523 return $status->{part} unless defined $part && $main::displayMode ne 'TeX'; 524 $part = 1 if $part < 1; $part = $self->{parts} if $part > $self->{parts}; 525 if ($part > $status->{part} && !$main::inputs_ref->{_noadvance}) { 526 unless ((lc($self->{nextVisible}) eq 'ifcorrect' && $status->{raw} < 1) || 527 lc($self->{nextVisible}) eq 'never') { 528 $self->initAnswers($status->{new_answers}); 529 $self->incrementPart; $self->{isNew} = 1; 530 } 531 } 532 if ($part != $status->{part}) { 533 main::TEXT('<input type="hidden" name="_noadvance" value="1" />'); 534 $self->{nextVisible} = 'IfCorrect' if lc($self->{nextVisible}) eq 'never'; 535 } 536 return $status->{part}; 537 } 538 539 # 540 # Return the various scores 541 # 542 sub score {shift->{status}{score}} 543 sub scoreRaw {shift->{status}{raw}} 544 sub scoreOverall { 545 my $self = shift; 546 return $self->{status}{score} + $self->{status}{total}; 547 } 548 549 ###################################################################### 550 # 551 # The custom grader that does the work of computing the scores 552 # and saving the data. 553 # 554 sub grader { 555 my $self = $main::PG->{flags}->{compoundProblem}; 556 # 557 # Get the answer names and the weight for the current part. 558 # 559 my @answers = keys(%{$_[0]}); 560 my $weight = scalar(@answers)/$self->{totalAnswers}; 561 $weight = $self->{weights}[$self->{status}{part}-1]/$self->{totalWeight} 562 if $self->{weights} && defined($self->{weights}[$self->{status}{part}-1]); 563 @answers = grep(!/$main::ANSWER_PREFIX/o,@answers) unless $self->{saveAllAnswers}; 564 push(@answers,@{$self->{extraAnswers}}) if $self->{extraAnswers}; 565 my $space = '<img src="about:blank" style="height:1px; width:3em; visibility:hidden" />'; 566 567 # 568 # Call the original grader, but put back the old recorded_score 569 # (the grader will have updated it based on the score for the PART, 570 # not the problem as a whole). 571 # 572 my $oldScore = ($_[1])->{recorded_score}; 573 my ($result,$state) = &{$self->{grader}}(@_); 574 $state->{recorded_score} = $oldScore; 575 576 # 577 # Update that state information and encode it. 578 # 579 my $status = $self->{status}; 580 $status->{raw} = $result->{score}; 581 $status->{score} = $result->{score}*$weight; 582 $status->{new_ans_rule_count} = $main::ans_rule_count; 583 if (defined(%main::images_created)) { 584 $status->{imageName} = (keys %main::images_created)[0]; 585 $status->{new_images_created} = $main::images_created{$status->{imageName}}; 586 } 587 $status->{new_answers} = join(';',@answers); 588 my $data = quoteHTML($self->encode); 589 590 # 591 # Update the recorded score 592 # 593 my $newScore = $status->{total} + $status->{score}; 594 $state->{recorded_score} = $newScore if $newScore > $oldScore; 595 $state->{recorded_score} = 0 if $self->{allowReset} && $main::inputs_ref->{_reset}; 596 597 # 598 # Add the compoundProblem message and data 599 # 600 $result->{type} = "compoundProblem ($result->{type})"; 601 $result->{msg} .= '</i><p><b>Note:</b> <i>' if $result->{msg}; 602 $result->{msg} .= 'This problem has more than one part.' 603 . '<br/>'.$space.'<small>Your score for this attempt is for this part only;</small>' 604 . '<br/>'.$space.'<small>your overall score is for all the parts combined.</small>' 605 . qq!<input type="hidden" name="_status" value="$data" />!; 606 607 # 608 # Warn if the answers changed when they shouldn't have 609 # 610 $result->{msg} .= '<p><b>You may not change your answers when going on to the next part!</b>' 611 if $self->{nextNoChange} && $self->{answersChanged}; 612 613 # 614 # Include the "next part" checkbox, button, or whatever. 615 # 616 my $par = 1; 617 if ($self->{parts} > $status->{part} && !$main::inputs_ref->{previewAnswers}) { 618 if (lc($self->{nextVisible}) eq 'always' || 619 (lc($self->{nextVisible}) eq 'ifcorrect' && $result->{score} >= 1)) { 620 my $method = "next".$self->{nextStyle}; $par = 0; 621 $result->{msg} .= $self->$method($self->{nextLabel},1).'<br/>'; 622 } 623 } 624 625 # 626 # Add the reset checkbox, if needed 627 # 628 $result->{msg} .= $self->resetCheckbox($self->{resetLabel},$par) 629 if $self->{allowReset} && $status->{part} > 1; 630 631 # 632 # Make sure we don't go on unless the next button really is checked 633 # 634 $result->{msg} .= '<input type="hidden" name="_next" value="0" />' 635 unless $self->{nextInserted}; 636 637 return ($result,$state); 638 } 639 640 1;
| aubreyja at gmail dot com | ViewVC Help |
| Powered by ViewVC 1.0.9 |