[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 5339 - (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 : dpvc 5339 when => "correct",
137 : dpvc 5336 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 : dpvc 5339 $pr->{when} = lc($pr->{when});
150 : dpvc 5336 $pr->getStatus;
151 :     $pr->initProblem;
152 :     return $pr;
153 :     }
154 :    
155 :     #
156 :     # Look up the status from the previous invocation
157 :     # and check to see if a rerandomization is requested
158 :     #
159 :     sub getStatus {
160 :     my $self = shift;
161 : dpvc 5338 main::RECORD_FORM_LABEL("_reseed");
162 : dpvc 5336 main::RECORD_FORM_LABEL("_status");
163 :     my $label = $self->{label} || $self->{lc($self->{style})."Label"};
164 :     $self->{status} = $self->decode;
165 :     $self->{submit} = $main::inputs_ref->{submitAnswers};
166 : dpvc 5338 $self->{isReset} = $main::inputs_ref->{_reseed} || ($self->{submit} && $self->{submit} eq $label);
167 : dpvc 5336 $self->{isReset} = 0 unless !$self->{onlyAfterDue} || time >= $main::dueDate;
168 :     }
169 :    
170 :     #
171 :     # Initialize the current problem
172 :     #
173 :     sub initProblem {
174 :     my $self = shift;
175 :     $main::PG_FLAGS{PROBLEM_GRADER_TO_USE} = \&problemRandomize::grader;
176 :     $main::PG_FLAGS{problemRandomize} = $self;
177 :     $self->reset if $self->{isReset};
178 :     $self->{random}->srand($self->{status}{seed});
179 :     }
180 :    
181 :     #
182 :     # Clear the answers and re-randomize the seed
183 :     #
184 :     sub reset {
185 :     my $self = shift;
186 :     my $status = $self->{status};
187 :     foreach my $id (split(/;/,$status->{answers})) {delete $main::inputs_ref->{$id}}
188 :     foreach my $id (1..$status->{ans_rule_count})
189 :     {delete $main::inputs_ref->{"${main::QUIZ_PREFIX}${main::ANSWER_PREFIX}$id"}}
190 :     $main::inputs_ref->{_status} = $self->encode(\%defaultStatus);
191 : dpvc 5338 $status->{seed} = ($main::inputs_ref->{_reseed} || seed());
192 : dpvc 5336 }
193 :    
194 : dpvc 5338 sub seed {substr(time,5,5)}
195 :    
196 : dpvc 5336 ##################################################
197 :    
198 :     #
199 :     # Return the HTML for the "re-randomize" checkbox.
200 :     #
201 :     sub randomizeCheckbox {
202 :     my $self = shift;
203 :     my $label = shift || $self->{checkboxLabel};
204 :     $label = "<b>$label</b> (when you submit your answers).";
205 :     my $par = shift; $par = ($par ? $main::PAR : '');
206 : dpvc 5338 $self->{reseedInserted} = 1;
207 :     $par . '<input type="checkbox" name="_reseed" value="'.seed().'" />' . $label;
208 : dpvc 5336 }
209 :    
210 :     #
211 :     # Return the HTML for the "next part" button.
212 :     #
213 :     sub randomizeButton {
214 :     my $self = shift;
215 :     my $label = quoteHTML(shift || $self->{buttonLabel});
216 :     my $par = shift; $par = ($par ? $main::PAR : '');
217 :     $par . qq!<input type="submit" name="submitAnswers" value="$label" !
218 : dpvc 5338 . q!onclick="document.getElementById('_reseed').value=!.seed().'" />';
219 : dpvc 5336 }
220 :    
221 :     #
222 :     # Return the HTML for the "problem seed" input box
223 :     #
224 :     sub randomizeInput {
225 :     my $self = shift;
226 :     my $label = quoteHTML(shift || $self->{inputLabel});
227 :     my $par = shift; $par = ($par ? main::PAR : '');
228 : dpvc 5338 $par . qq!<input type="submit" name="submitAnswers" value="$label" !
229 :     . q!onclick="document.getElementById('_reseed').value=document.getElementById('_seed').value" />!
230 :     . qq!<input name="_seed" id="_seed" value="$self->{status}{seed}" size="10">!;
231 : dpvc 5336 }
232 :    
233 :     #
234 :     # Return the raw HTML provided
235 :     #
236 :     sub randomizeHTML {shift; shift}
237 :    
238 :     ##################################################
239 :    
240 :     #
241 :     # Encode all the status information so that it can be
242 :     # maintained as the student submits answers. Since this
243 :     # state information includes things like the score from
244 :     # the previous parts, it is "encrypted" using a dumb
245 :     # hex encoding (making it harder for a student to recognize
246 :     # it as valuable data if they view the page source).
247 :     #
248 :     sub encode {
249 :     my $self = shift; my $status = shift || $self->{status};
250 :     my @data = (); my $data = "";
251 :     foreach my $id (main::lex_sort(keys(%defaultStatus))) {push(@data,$status->{$id})}
252 :     foreach my $c (split(//,join('|',@data))) {$data .= toHex($c)}
253 :     return $data;
254 :     }
255 :    
256 :     #
257 :     # Decode the data and break it into the status hash.
258 :     #
259 :     sub decode {
260 :     my $self = shift; my $status = shift || $main::inputs_ref->{_status};
261 :     return {%defaultStatus} unless $status;
262 :     my @data = (); foreach my $hex (split(/(..)/,$status)) {push(@data,fromHex($hex)) if $hex ne ''}
263 :     @data = split('\\|',join('',@data)); $status = {%defaultStatus};
264 :     foreach my $id (main::lex_sort(keys(%defaultStatus))) {$status->{$id} = shift(@data)}
265 :     return $status;
266 :     }
267 :    
268 :    
269 :     #
270 :     # Hex encoding is shifted by 10 to obfuscate it further.
271 :     # (shouldn't be a problem since the status will be made of
272 :     # printable characters, so they are all above ASCII 32)
273 :     #
274 :     sub toHex {main::spf(ord(shift)-10,"%X")}
275 :     sub fromHex {main::spf(hex(shift)+10,"%c")}
276 :    
277 :    
278 :     #
279 :     # Make sure the data can be properly preserved within
280 :     # an HTML <INPUT TYPE="HIDDEN"> tag.
281 :     #
282 :     sub quoteHTML {
283 :     my $string = shift;
284 :     $string =~ s/&/\&amp;/g; $string =~ s/"/\&quot;/g;
285 :     $string =~ s/>/\&gt;/g; $string =~ s/</\&lt;/g;
286 :     return $string;
287 :     }
288 :    
289 :     ##################################################
290 :    
291 :     #
292 :     # Set the grader for this part to the specified one.
293 :     #
294 :     sub useGrader {
295 :     my $self = shift;
296 :     $self->{grader} = shift;
297 :     }
298 :    
299 :     #
300 :     # The custom grader that does the work of computing the scores
301 :     # and saving the data.
302 :     #
303 :     sub grader {
304 :     my $self = $main::PG_FLAGS{problemRandomize};
305 :    
306 :     #
307 :     # Call the original grader
308 :     #
309 : dpvc 5338 $self->{grader} = \&problemRandomize::resetGrader if $self->{isReset};
310 : dpvc 5336 my ($result,$state) = &{$self->{grader}}(@_);
311 :    
312 :     #
313 :     # Update that state information and encode it.
314 :     #
315 :     my $status = $self->{status};
316 :     $status->{ans_rule_count} = $main::ans_rule_count;
317 :     $status->{answers} = join(';',grep(!/${main::QUIZ_PREFIX}${main::ANSWER_PREFIX}/o,keys(%{$_[0]})));
318 :     my $data = quoteHTML($self->encode);
319 : dpvc 5339 $result->{type} = "problemRandomize ($result->{type})";
320 : dpvc 5336
321 :     #
322 : dpvc 5339 # Conditions for when to show the reseed message
323 :     #
324 :     my $isWhen = ($self->{when} eq 'always' ||
325 :     ($self->{when} eq 'correct' && $result->{score} >= 1 &&
326 :     !$main::inputs_ref->{previewAnswers}));
327 :     my $okDate = (!$self->{onlyAfterDue} || time >= $main::dueDate);
328 :    
329 :     #
330 : dpvc 5336 # Add the problemRandomize message and data
331 :     #
332 : dpvc 5339 if ($isWhen && !$okDate) {
333 :     $result->{msg} .= "</i><br /><b>Note:</b> <i>" if $result->{msg};
334 :     $result->{msg} .= "You can get a new version of this problem after the due date.";
335 :     }
336 : dpvc 5336 if (!$result->{msg}) {
337 :     # hack to remove unwanted "<b>Note: </b>" from the problem
338 :     # (it is inserted automatically by Problem.pm when {msg} is non-emtpy).
339 :     $result->{msg} .= '<script>var bb = document.getElementsByTagName("b");'
340 :     . 'bb[bb.length-1].style.display="none"</script>';
341 :     }
342 :     $result->{msg} .= qq!<input type="hidden" name="_status" value="$data" />!;
343 :    
344 :     #
345 :     # Include the "randomize" checkbox, button, or whatever.
346 :     #
347 : dpvc 5339 if ($isWhen && $okDate) {
348 :     my $method = "randomize".$self->{style};
349 :     $result->{msg} .= $self->$method($self->{label},1).'<br/>';
350 : dpvc 5336 }
351 :    
352 :     #
353 :     # Don't show the summary section if the problem is being reset.
354 :     #
355 :     if ($self->{isReset}) {
356 :     $result->{msg} .= "<style>.problemHeader {display:none}</style>";
357 :     $state->{state_summary_msg} =
358 :     "<b>Note:</b> This is a new (re-randomized) version of the problem.".$main::BR.
359 :     "If you come back to it later, it may revert to its original version.".$main::BR.
360 :     "Hardcopy will always print the original version of the problem.";
361 :     }
362 :    
363 :     #
364 :     # Make sure we don't go on unless the next button really is checked
365 :     #
366 : dpvc 5338 $result->{msg} .= '<input type="hidden" name="_reseed" id="_reseed" value="0" />'
367 :     unless $self->{reseedInserted};
368 : dpvc 5336
369 :     return ($result,$state);
370 :     }
371 :    
372 : dpvc 5338 #
373 :     # Fake grader for when the problem is reset
374 :     #
375 :     sub resetGrader {
376 :     my $answers = shift;
377 :     my $state = shift;
378 :     my %options = @_;
379 :     my $result = {
380 :     score => 0,
381 :     msg => '',
382 :     errors => '',
383 :     type => 'problemRandomize (reset)',
384 :     };
385 :     return ($result,$state);
386 :     }
387 :    
388 : dpvc 5336 1;

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9