[system] / trunk / pg / macros / compoundProblem.pl Repository: Repository Listing bbplugincoursesdistsnplrochestersystemwww

# View of /trunk/pg/macros/compoundProblem.pl

Mon Dec 22 05:18:26 2008 UTC (11 years, 1 month ago) by dpvc
File size: 26613 byte(s)
```Added code to save/restore the images_created counts so that
on-the-fly graphics in more than one part will work properly.
Old data format is updated automatically to the new one, so problems
should continue to work even if they are already in use by students.
```

```    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 #
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
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
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
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
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 #
158 #                                place of the default one.  For example:
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 #
174 #                                from one part to another.  E.g.,
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
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,
247     weights => undef,            # array of weights per part
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
257     @_,
258     status => \$defaultStatus,
259   }, \$class;
260   die "You must provide either the totalAnswers or weights"
262   \$cp->getTotalWeight if \$cp->{weights};
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}) {
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};
310   \$main::PG_FLAGS{compoundProblem} = \$self;
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 #
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 #
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}) {
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}++;
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;
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/&/\&amp;/g; \$string =~ s/"/\&quot;/g;
426   \$string =~ s/>/\&gt;/g;  \$string =~ s/</\&lt;/g;
427   return \$string;
428 }
429
430 ######################################################################
431
432 #
433 #  Set the grader for this part to the specified one.
434 #
436   my \$self = shift;
438 }
439
440 #
442 #  be preserved for use in future parts.
443 #
445   my \$self = shift;
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})
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') {
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 #
555   my \$self = \$main::PG_FLAGS{compoundProblem};
556
557   #
558   #  Get the answer names and the weight for the current part.
559   #
562   \$weight = \$self->{weights}[\$self->{status}{part}-1]/\$self->{totalWeight}
563     if \$self->{weights} && defined(\$self->{weights}[\$self->{status}{part}-1]);
566   my \$space = '<img src="about:blank" style="height:1px; width:3em; visibility:hidden" />';
567
568   #
569   #  Call the original grader, but put back the old recorded_score
570   #  (the grader will have updated it based on the score for the PART,
571   #  not the problem as a whole).
572   #
573   my \$oldScore = (\$_[1])->{recorded_score};
575   \$state->{recorded_score} = \$oldScore;
576
577   #
578   #  Update that state information and encode it.
579   #
580   my \$status = \$self->{status};
581   \$status->{raw}   = \$result->{score};
582   \$status->{score} = \$result->{score}*\$weight;
583   \$status->{new_ans_rule_count} = \$main::ans_rule_count;
584   if (defined(%main::images_created)) {
585     \$status->{imageName} = (keys %main::images_created)[0];
586     \$status->{new_images_created} = \$main::images_created{\$status->{imageName}};
587   }
589   my \$data = quoteHTML(\$self->encode);
590
591   #
592   #  Update the recorded score
593   #
594   my \$newScore = \$status->{total} + \$status->{score};
595   \$state->{recorded_score} = \$newScore if \$newScore > \$oldScore;
596   \$state->{recorded_score} = 0 if \$self->{allowReset} && \$main::inputs_ref->{_reset};
597
598   #
599   #  Add the compoundProblem message and data
600   #
601   \$result->{type} = "compoundProblem (\$result->{type})";
602   \$result->{msg} .= '</i><p><b>Note:</b> <i>' if \$result->{msg};
603   \$result->{msg} .= 'This problem has more than one part.'
604                  .  '<br/>'.\$space.'<small>Your score for this attempt is for this part only;</small>'
605      .  '<br/>'.\$space.'<small>your overall score is for all the parts combined.</small>'
606                  .  qq!<input type="hidden" name="_status" value="\$data" />!;
607
608   #
609   #  Warn if the answers changed when they shouldn't have
610   #
611   \$result->{msg} .= '<p><b>You may not change your answers when going on to the next part!</b>'
613
614   #
615   #  Include the "next part" checkbox, button, or whatever.
616   #
617   my \$par = 1;
618   if (\$self->{parts} > \$status->{part} && !\$main::inputs_ref->{previewAnswers}) {
619     if (lc(\$self->{nextVisible}) eq 'always' ||
620        (lc(\$self->{nextVisible}) eq 'ifcorrect' && \$result->{score} >= 1)) {
621       my \$method = "next".\$self->{nextStyle}; \$par = 0;
622       \$result->{msg} .= \$self->\$method(\$self->{nextLabel},1).'<br/>';
623     }
624   }
625
626   #
627   #  Add the reset checkbox, if needed
628   #
629   \$result->{msg} .= \$self->resetCheckbox(\$self->{resetLabel},\$par)
630     if \$self->{allowReset} && \$status->{part} > 1;
631
632   #
633   #  Make sure we don't go on unless the next button really is checked
634   #
635   \$result->{msg} .= '<input type="hidden" name="_next" value="0" />'
636     unless \$self->{nextInserted};
637
638   return (\$result,\$state);
639 }
640
641 1;
```