[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 5337 - (download) (as text) (annotate)
Wed Aug 15 21:45:01 2007 UTC (12 years, 5 months ago) by dpvc
File size: 12212 byte(s)
Fixed a bug where onlyAfterDue was not working correctly.  (The button
showed up when it shouldn't, and would have no effect.)

    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     if (!$self->{onlyAfterDue} || time >= $main::dueDate) {
  335       my $method = "randomize".$self->{style};
  336       $result->{msg} .= $self->$method($self->{label},1).'<br/>';
  337     }
  338   }
  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