[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 6590 - (view) (download) (as text)

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

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9