[system] / trunk / pg / macros / problemRandomize.pl Repository:
ViewVC logotype

Annotation of /trunk/pg/macros/problemRandomize.pl

Parent Directory Parent Directory | Revision Log Revision Log


Revision 5337 - (view) (download) (as text)

1 : dpvc 5336
2 :     =pod
3 :    
4 :     ######################################################################
5 :     #
6 :     # This file implements a mechanism for allowing a problem file to be
7 :     # "reseeded" so that the student can do additional versions of the
8 :     # problem. You can control when the reseed message is available,
9 :     # and what style to use for it.
10 :     #
11 :     # To use the problemRandimize library, use
12 :     #
13 :     # loadMacros("problemRandomize.pl");
14 :     #
15 :     # at the top of your problem file, and then create a problemRandomize
16 :     # object with
17 :     #
18 :     # $pr = ProblemRandomize(options);
19 :     #
20 :     # where '$pr' is the name of the variable you will use to refer
21 :     # to the randomized problem (if needed), and 'options' can include:
22 :     #
23 :     # when => type Specifies the condition on which
24 :     # reseeding the problem is allowed.
25 :     # The choices include:
26 :     #
27 :     # "Correct" (only when the problem has
28 :     # been answered correctly.)
29 :     #
30 :     # "Always" (reseeding is always allowed.)
31 :     #
32 :     # Default: "Correct"
33 :     #
34 :     # onlyAfterDue => 0 or 1 Specifies if the reseed option is only
35 :     # allowed after the due date has passed.
36 :     # Default: 1
37 :     #
38 :     # style => type Determines the type of interaction needed
39 :     # to reseed the problem. Types include:
40 :     #
41 :     # "Button" (a button)
42 :     #
43 :     # "Checkbox" (a checkbox plus pressing submit)
44 :     #
45 :     # "Input" (an input box where the seed
46 :     # can be set explicitly)
47 :     #
48 :     # "HTML" (the HTML is given explicitly
49 :     # via the "label" option below)
50 :     #
51 :     # Default: "Button"
52 :     #
53 :     # label => "text" Specifies the text used for the button name,
54 :     # checkbox label, input box label, or raw HTML
55 :     # used for the reseed mechanism.
56 :     #
57 :     # The problemRandomize library installs a special grader that handles determining
58 :     # when the reseed option will be available. It also redefines install_problem_grader
59 :     # so that it will not overwrite the one installed by the library (it is stored so
60 :     # that it can be called internally by the problemRandomize library's grader).
61 :     #
62 :     # Note that the problem will store the new problem seed only if the student can
63 :     # submit saved answers (i.e., only before the due date). After the due date,
64 :     # the student can get new versions, but the problem will revert to the original
65 :     # version when they come back to the problem later. Since the default is only
66 :     # to allow reseeding afer the due date, the reseeding will not be sticky by default.
67 :     # Hardcopy ALWAYS produces the original version of the problem, regardless of
68 :     # the seed saved by the student.
69 :     #
70 :     # Examples:
71 :     #
72 :     # ProblemRandomize(); # use all defaults
73 :     # ProblemRandomize(when=>"Always"); # always can reseed (after due date)
74 :     # ProblemRandomize(onlyAfterDue=>0); # can reseed whenever correct
75 :     # ProblemRandomize(when=>"always",onlyAfterDue=>0); # always can reseed
76 :     #
77 :     # ProblemRandomize(style=>"Input"); # use an input box to set the seed
78 :     #
79 :     # For problems that include "PGcourse.pl" in their loadMacros() calls, you can
80 :     # use that file to provide reseed buttons for ALL problems simply by including
81 :     #
82 :     # loadMacros("problemRandomize.pl");
83 :     # ProblemRandomize();
84 :     #
85 :     # in PGcourse.pl. You can make the ProblemRandomize() be dependent on the set
86 :     # number or the set or the login ID or whatever. For example
87 :     #
88 :     # loadMacros("problemRandomize.pl");
89 :     # ProblemRandomize(when=>"always",onlyAfterDue=>0,style=>"Input")
90 :     # if $studentLogin eq "dpvc";
91 :     #
92 :     # would enable reseeding at any time for the user called "dpvc" (presumably a
93 :     # professor). You can test $probNum and $setNumber to make reseeding available
94 :     # only for specific sets or problems within a set.
95 :     #
96 :    
97 :    
98 :     =cut
99 :    
100 :     sub _problemRandomize_init {
101 :     sub ProblemRandomize {new problemRandomize(@_)}
102 :     PG_restricted_eval(<<' end_eval');
103 :     sub install_problem_grader {
104 :     return $PG_FLAGS{problemRandomize}->useGrader(@_) if $PG_FLAGS{problemRandomize};
105 :     &{$problemRandomize::installGrader}(@_); # call cached version
106 :     }
107 :     end_eval
108 :     }
109 :    
110 :     ######################################################################
111 :    
112 :     package problemRandomize;
113 :    
114 :     #
115 :     # The state data that is stored between invocations of
116 :     # the problem.
117 :     #
118 :     our %defaultStatus = (
119 :     seed => $main::problemSeed, # original seed
120 :     answers => "", # list of answer names
121 :     ans_rule_count => 0, # number of unnamed answers
122 :     );
123 :    
124 :     #
125 :     # Cache original grader installer (so we can override it).
126 :     #
127 :     our $installGrader = \&main::install_problem_grader;
128 :    
129 :     #
130 :     # Create new problemRandomize object from user's data
131 :     # and initialize it.
132 :     #
133 :     sub new {
134 :     my $self = shift; my $class = ref($self) || $self;
135 :     my $pr = bless {
136 :     when => "Correct",
137 :     onlyAfterDue => 1,
138 :     style => "Button",
139 :     label => undef,
140 :     buttonLabel => "Get a new version of this problem",
141 :     checkboxLabel => "Get a new version of this problem",
142 :     inputLabel => "Set random seed to:",
143 :     grader => $main::PG_FLAGS{PROBLEM_GRADER_TO_USE} || \&main::avg_problem_grader,
144 :     random => $main::PG_random_generator,
145 :     status => {},
146 :     @_
147 :     }, $class;
148 :     $pr->{style} = uc(substr($pr->{style},0,1)) . lc(substr($pr->{style},1));
149 :     $pr->getStatus;
150 :     $pr->initProblem;
151 :     return $pr;
152 :     }
153 :    
154 :     #
155 :     # Look up the status from the previous invocation
156 :     # and check to see if a rerandomization is requested
157 :     #
158 :     sub getStatus {
159 :     my $self = shift;
160 :     main::RECORD_FORM_LABEL("_randomize");
161 :     main::RECORD_FORM_LABEL("_status");
162 :     my $label = $self->{label} || $self->{lc($self->{style})."Label"};
163 :     $self->{status} = $self->decode;
164 :     $self->{submit} = $main::inputs_ref->{submitAnswers};
165 :     $self->{isReset} = $main::inputs_ref->{_randomize} || ($self->{submit} && $self->{submit} eq $label);
166 :     $self->{isReset} = 0 unless !$self->{onlyAfterDue} || time >= $main::dueDate;
167 :     }
168 :    
169 :     #
170 :     # Initialize the current problem
171 :     #
172 :     sub initProblem {
173 :     my $self = shift;
174 :     $main::PG_FLAGS{PROBLEM_GRADER_TO_USE} = \&problemRandomize::grader;
175 :     $main::PG_FLAGS{problemRandomize} = $self;
176 :     $self->reset if $self->{isReset};
177 :     $self->{random}->srand($self->{status}{seed});
178 :     }
179 :    
180 :     #
181 :     # Clear the answers and re-randomize the seed
182 :     #
183 :     sub reset {
184 :     my $self = shift;
185 :     my $status = $self->{status};
186 :     foreach my $id (split(/;/,$status->{answers})) {delete $main::inputs_ref->{$id}}
187 :     foreach my $id (1..$status->{ans_rule_count})
188 :     {delete $main::inputs_ref->{"${main::QUIZ_PREFIX}${main::ANSWER_PREFIX}$id"}}
189 :     $main::inputs_ref->{_status} = $self->encode(\%defaultStatus);
190 :     $main::inputs_ref->{_randomize} = 1;
191 :     $status->{seed} = ($main::inputs_ref->{_seed} || substr(time,5,5));
192 :     }
193 :    
194 :     ##################################################
195 :    
196 :     #
197 :     # Return the HTML for the "re-randomize" checkbox.
198 :     #
199 :     sub randomizeCheckbox {
200 :     my $self = shift;
201 :     my $label = shift || $self->{checkboxLabel};
202 :     $label = "<b>$label</b> (when you submit your answers).";
203 :     my $par = shift; $par = ($par ? $main::PAR : '');
204 :     $self->{randomizeInserted} = 1;
205 :     $par . '<input type="checkbox" name="_randomize" value="1" />' . $label;
206 :     }
207 :    
208 :     #
209 :     # Return the HTML for the "next part" button.
210 :     #
211 :     sub randomizeButton {
212 :     my $self = shift;
213 :     my $label = quoteHTML(shift || $self->{buttonLabel});
214 :     my $par = shift; $par = ($par ? $main::PAR : '');
215 :     $par . qq!<input type="submit" name="submitAnswers" value="$label" !
216 :     . q!onclick="document.getElementById('_randomize').value=1" />!;
217 :     }
218 :    
219 :     #
220 :     # Return the HTML for the "problem seed" input box
221 :     #
222 :     sub randomizeInput {
223 :     my $self = shift;
224 :     my $label = quoteHTML(shift || $self->{inputLabel});
225 :     my $par = shift; $par = ($par ? main::PAR : '');
226 :     $par . qq!<input type="submit" name="submitAnswers" value="$label">!
227 :     . qq!<input name="_seed" value="$self->{status}{seed}" size="10">!;
228 :     }
229 :    
230 :     #
231 :     # Return the raw HTML provided
232 :     #
233 :     sub randomizeHTML {shift; shift}
234 :    
235 :     ##################################################
236 :    
237 :     #
238 :     # Encode all the status information so that it can be
239 :     # maintained as the student submits answers. Since this
240 :     # state information includes things like the score from
241 :     # the previous parts, it is "encrypted" using a dumb
242 :     # hex encoding (making it harder for a student to recognize
243 :     # it as valuable data if they view the page source).
244 :     #
245 :     sub encode {
246 :     my $self = shift; my $status = shift || $self->{status};
247 :     my @data = (); my $data = "";
248 :     foreach my $id (main::lex_sort(keys(%defaultStatus))) {push(@data,$status->{$id})}
249 :     foreach my $c (split(//,join('|',@data))) {$data .= toHex($c)}
250 :     return $data;
251 :     }
252 :    
253 :     #
254 :     # Decode the data and break it into the status hash.
255 :     #
256 :     sub decode {
257 :     my $self = shift; my $status = shift || $main::inputs_ref->{_status};
258 :     return {%defaultStatus} unless $status;
259 :     my @data = (); foreach my $hex (split(/(..)/,$status)) {push(@data,fromHex($hex)) if $hex ne ''}
260 :     @data = split('\\|',join('',@data)); $status = {%defaultStatus};
261 :     foreach my $id (main::lex_sort(keys(%defaultStatus))) {$status->{$id} = shift(@data)}
262 :     return $status;
263 :     }
264 :    
265 :    
266 :     #
267 :     # Hex encoding is shifted by 10 to obfuscate it further.
268 :     # (shouldn't be a problem since the status will be made of
269 :     # printable characters, so they are all above ASCII 32)
270 :     #
271 :     sub toHex {main::spf(ord(shift)-10,"%X")}
272 :     sub fromHex {main::spf(hex(shift)+10,"%c")}
273 :    
274 :    
275 :     #
276 :     # Make sure the data can be properly preserved within
277 :     # an HTML <INPUT TYPE="HIDDEN"> tag.
278 :     #
279 :     sub quoteHTML {
280 :     my $string = shift;
281 :     $string =~ s/&/\&amp;/g; $string =~ s/"/\&quot;/g;
282 :     $string =~ s/>/\&gt;/g; $string =~ s/</\&lt;/g;
283 :     return $string;
284 :     }
285 :    
286 :     ##################################################
287 :    
288 :     #
289 :     # Set the grader for this part to the specified one.
290 :     #
291 :     sub useGrader {
292 :     my $self = shift;
293 :     $self->{grader} = shift;
294 :     }
295 :    
296 :     #
297 :     # The custom grader that does the work of computing the scores
298 :     # and saving the data.
299 :     #
300 :     sub grader {
301 :     my $self = $main::PG_FLAGS{problemRandomize};
302 :    
303 :     #
304 :     # Call the original grader
305 :     #
306 :     my ($result,$state) = &{$self->{grader}}(@_);
307 :    
308 :     #
309 :     # Update that state information and encode it.
310 :     #
311 :     my $status = $self->{status};
312 :     $status->{ans_rule_count} = $main::ans_rule_count;
313 :     $status->{answers} = join(';',grep(!/${main::QUIZ_PREFIX}${main::ANSWER_PREFIX}/o,keys(%{$_[0]})));
314 :     my $data = quoteHTML($self->encode);
315 :    
316 :     #
317 :     # Add the problemRandomize message and data
318 :     #
319 :     $result->{type} = "problemRandomize ($result->{type})";
320 :     if (!$result->{msg}) {
321 :     # hack to remove unwanted "<b>Note: </b>" from the problem
322 :     # (it is inserted automatically by Problem.pm when {msg} is non-emtpy).
323 :     $result->{msg} .= '<script>var bb = document.getElementsByTagName("b");'
324 :     . 'bb[bb.length-1].style.display="none"</script>';
325 :     }
326 :     $result->{msg} .= qq!<input type="hidden" name="_status" value="$data" />!;
327 :    
328 :     #
329 :     # Include the "randomize" checkbox, button, or whatever.
330 :     #
331 :     if (lc($self->{when}) eq 'always' ||
332 :     (lc($self->{when}) eq 'correct' && $result->{score} >= 1 &&
333 :     !$main::inputs_ref->{previewAnswers})) {
334 : dpvc 5337 if (!$self->{onlyAfterDue} || time >= $main::dueDate) {
335 :     my $method = "randomize".$self->{style};
336 :     $result->{msg} .= $self->$method($self->{label},1).'<br/>';
337 :     }
338 : dpvc 5336 }
339 :    
340 :     #
341 :     # Don't show the summary section if the problem is being reset.
342 :     #
343 :     if ($self->{isReset}) {
344 :     $result->{msg} .= "<style>.problemHeader {display:none}</style>";
345 :     $state->{state_summary_msg} =
346 :     "<b>Note:</b> This is a new (re-randomized) version of the problem.".$main::BR.
347 :     "If you come back to it later, it may revert to its original version.".$main::BR.
348 :     "Hardcopy will always print the original version of the problem.";
349 :     }
350 :    
351 :     #
352 :     # Make sure we don't go on unless the next button really is checked
353 :     #
354 :     $result->{msg} .= '<input type="hidden" name="_randomize" value="0" />'
355 :     unless $self->{randomizeInserted};
356 :    
357 :     return ($result,$state);
358 :     }
359 :    
360 :     1;

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9