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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 5336 - (download) (as text) (annotate)
Wed Aug 15 21:32:05 2007 UTC (12 years, 5 months ago) by dpvc
File size: 12142 byte(s)
Provides a mechanism for reseeding problems under specific
conditions.  For example, when the student has gotten the problem
right and it is past the due date, he or she can reseed the problem.
Other criteria are also available.

See the comments at the top of the file for more details.

    1 
    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     my $method = "randomize".$self->{style};
  335     $result->{msg} .= $self->$method($self->{label},1).'<br/>';
  336   }
  337 
  338   #
  339   #  Don't show the summary section if the problem is being reset.
  340   #
  341   if ($self->{isReset}) {
  342     $result->{msg} .= "<style>.problemHeader {display:none}</style>";
  343     $state->{state_summary_msg} =
  344        "<b>Note:</b> This is a new (re-randomized) version of the problem.".$main::BR.
  345        "If you come back to it later, it may revert to its original version.".$main::BR.
  346        "Hardcopy will always print the original version of the problem.";
  347   }
  348 
  349   #
  350   #  Make sure we don't go on unless the next button really is checked
  351   #
  352   $result->{msg} .= '<input type="hidden" name="_randomize" value="0" />'
  353     unless $self->{randomizeInserted};
  354 
  355   return ($result,$state);
  356 }
  357 
  358 1;

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9