[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 5373 - (download) (as text) (annotate)
Sun Aug 19 02:01:57 2007 UTC (12 years, 3 months ago) by dpvc
File size: 13339 byte(s)
Normalized comments and headers to that they will format their POD
documentation properly.  (I know that the POD processing was supposed
to strip off the initial #, but that doesn't seem to happen, so I've
added a space throughout.)

    1 
    2 =head1 ProblemRandomize();
    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 =cut
   98 
   99 sub _problemRandomize_init {
  100   sub ProblemRandomize {new problemRandomize(@_)}
  101   PG_restricted_eval(<<'  end_eval');
  102     sub install_problem_grader {
  103       return $PG_FLAGS{problemRandomize}->useGrader(@_) if $PG_FLAGS{problemRandomize};
  104       &{$problemRandomize::installGrader}(@_); # call cached version
  105     }
  106   end_eval
  107 }
  108 
  109 ######################################################################
  110 
  111 package problemRandomize;
  112 
  113 #
  114 #  The state data that is stored between invocations of
  115 #  the problem.
  116 #
  117 our %defaultStatus = (
  118   seed => $main::problemSeed,  # original seed
  119   answers => "",               # list of answer names
  120   ans_rule_count => 0,         # number of unnamed answers
  121 );
  122 
  123 #
  124 #  Cache original grader installer (so we can override it).
  125 #
  126 our $installGrader = \&main::install_problem_grader;
  127 
  128 #
  129 #  Create new problemRandomize object from user's data
  130 #  and initialize it.
  131 #
  132 sub new {
  133   my $self = shift; my $class = ref($self) || $self;
  134   my $pr = bless {
  135     when => "correct",
  136     onlyAfterDue => 1,
  137     style => "Button",
  138     label => undef,
  139     buttonLabel => "Get a new version of this problem",
  140     checkboxLabel => "Get a new version of this problem",
  141     inputLabel => "Set random seed to:",
  142     grader => $main::PG_FLAGS{PROBLEM_GRADER_TO_USE} || \&main::avg_problem_grader,
  143     random => $main::PG_random_generator,
  144     status => {},
  145     @_
  146   }, $class;
  147   $pr->{style} = uc(substr($pr->{style},0,1)) . lc(substr($pr->{style},1));
  148   $pr->{when} = lc($pr->{when});
  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("_reseed");
  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->{_reseed} || ($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   $status->{seed} = ($main::inputs_ref->{_reseed} || seed());
  191 }
  192 
  193 sub seed {substr(time,5,5)}
  194 
  195 ##################################################
  196 
  197 #
  198 #  Return the HTML for the "re-randomize" checkbox.
  199 #
  200 sub randomizeCheckbox {
  201   my $self = shift;
  202   my $label = shift || $self->{checkboxLabel};
  203   $label = "<b>$label</b> (when you submit your answers).";
  204   my $par = shift; $par = ($par ? $main::PAR : '');
  205   $self->{reseedInserted} = 1;
  206   $par . '<input type="checkbox" name="_reseed" value="'.seed().'" />' . $label;
  207 }
  208 
  209 #
  210 #  Return the HTML for the "next part" button.
  211 #
  212 sub randomizeButton {
  213   my $self = shift;
  214   my $label = quoteHTML(shift || $self->{buttonLabel});
  215   my $par = shift; $par = ($par ? $main::PAR : '');
  216   $par . qq!<input type="submit" name="submitAnswers" value="$label" !
  217        .  q!onclick="document.getElementById('_reseed').value=!.seed().'" />';
  218 }
  219 
  220 #
  221 #  Return the HTML for the "problem seed" input box
  222 #
  223 sub randomizeInput {
  224   my $self = shift;
  225   my $label = quoteHTML(shift || $self->{inputLabel});
  226   my $par = shift; $par = ($par ? main::PAR : '');
  227   $par . qq!<input type="submit" name="submitAnswers" value="$label" !
  228        .  q!onclick="document.getElementById('_reseed').value=document.getElementById('_seed').value" />!
  229        . qq!<input name="_seed" id="_seed" value="$self->{status}{seed}" size="10">!;
  230 }
  231 
  232 #
  233 #  Return the raw HTML provided
  234 #
  235 sub randomizeHTML {shift; shift}
  236 
  237 ##################################################
  238 
  239 #
  240 #  Encode all the status information so that it can be
  241 #  maintained as the student submits answers.  Since this
  242 #  state information includes things like the score from
  243 #  the previous parts, it is "encrypted" using a dumb
  244 #  hex encoding (making it harder for a student to recognize
  245 #  it as valuable data if they view the page source).
  246 #
  247 sub encode {
  248   my $self = shift; my $status = shift || $self->{status};
  249   my @data = (); my $data = "";
  250   foreach my $id (main::lex_sort(keys(%defaultStatus))) {push(@data,$status->{$id})}
  251   foreach my $c (split(//,join('|',@data))) {$data .= toHex($c)}
  252   return $data;
  253 }
  254 
  255 #
  256 #  Decode the data and break it into the status hash.
  257 #
  258 sub decode {
  259   my $self = shift; my $status = shift || $main::inputs_ref->{_status};
  260   return {%defaultStatus} unless $status;
  261   my @data = (); foreach my $hex (split(/(..)/,$status)) {push(@data,fromHex($hex)) if $hex ne ''}
  262   @data = split('\\|',join('',@data)); $status = {%defaultStatus};
  263   foreach my $id (main::lex_sort(keys(%defaultStatus))) {$status->{$id} = shift(@data)}
  264   return $status;
  265 }
  266 
  267 
  268 #
  269 #  Hex encoding is shifted by 10 to obfuscate it further.
  270 #  (shouldn't be a problem since the status will be made of
  271 #  printable characters, so they are all above ASCII 32)
  272 #
  273 sub toHex {main::spf(ord(shift)-10,"%X")}
  274 sub fromHex {main::spf(hex(shift)+10,"%c")}
  275 
  276 
  277 #
  278 #  Make sure the data can be properly preserved within
  279 #  an HTML <INPUT TYPE="HIDDEN"> tag.
  280 #
  281 sub quoteHTML {
  282   my $string = shift;
  283   $string =~ s/&/\&amp;/g; $string =~ s/"/\&quot;/g;
  284   $string =~ s/>/\&gt;/g;  $string =~ s/</\&lt;/g;
  285   return $string;
  286 }
  287 
  288 ##################################################
  289 
  290 #
  291 #  Set the grader for this part to the specified one.
  292 #
  293 sub useGrader {
  294   my $self = shift;
  295   $self->{grader} = shift;
  296 }
  297 
  298 #
  299 #  The custom grader that does the work of computing the scores
  300 #  and saving the data.
  301 #
  302 sub grader {
  303   my $self = $main::PG_FLAGS{problemRandomize};
  304 
  305   #
  306   #  Call the original grader
  307   #
  308   $self->{grader} = \&problemRandomize::resetGrader if $self->{isReset};
  309   my ($result,$state) = &{$self->{grader}}(@_);
  310   shift; shift; my %options = @_;
  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   $result->{type} = "problemRandomize ($result->{type})";
  320 
  321   #
  322   #  Conditions for when to show the reseed message
  323   #
  324   my $inputs = $main::inputs_ref;
  325   my $isSubmit = $inputs->{submitAnswers} || $inputs->{previewAnswers} || $inputs->{checkAnswers};
  326   my $score = ($isSubmit || $self->{isReset} ? $result->{score} : $state->{recorded_score});
  327   my $isWhen = ($self->{when} eq 'always' ||
  328      ($self->{when} eq 'correct' && $score >= 1 && !$main::inputs_ref->{previewAnswers}));
  329   my $okDate = (!$self->{onlyAfterDue} || time >= $main::dueDate);
  330 
  331   #
  332   #  Add the problemRandomize message and data
  333   #
  334   if ($isWhen && !$okDate) {
  335     $result->{msg} .= "</i><br /><b>Note:</b> <i>" if $result->{msg};
  336     $result->{msg} .= "You can get a new version of this problem after the due date.";
  337   }
  338   if (!$result->{msg}) {
  339     # hack to remove unwanted "<b>Note: </b>" from the problem
  340     #  (it is inserted automatically by Problem.pm when {msg} is non-emtpy).
  341     $result->{msg} .= '<script>var bb = document.getElementsByTagName("b");'
  342                    .  'bb[bb.length-1].style.display="none"</script>';
  343   }
  344   $result->{msg} .= qq!<input type="hidden" name="_status" value="$data" />!;
  345 
  346   #
  347   #  Include the "randomize" checkbox, button, or whatever.
  348   #
  349   if ($isWhen && $okDate) {
  350     my $method = "randomize".$self->{style};
  351     $result->{msg} .= $self->$method($self->{label},1).'<br/>';
  352   }
  353 
  354   #
  355   #  Don't show the summary section if the problem is being reset.
  356   #
  357   if ($self->{isReset} && $isSubmit) {
  358     $result->{msg} .= "<style>.problemHeader {display:none}</style>";
  359     $state->{state_summary_msg} =
  360        "<b>Note:</b> This is a new (re-randomized) version of the problem.".$main::BR.
  361        "If you come back to it later, it may revert to its original version.".$main::BR.
  362        "Hardcopy will always print the original version of the problem.";
  363   }
  364 
  365   #
  366   #  Make sure we don't go on unless the next button really is checked
  367   #
  368   $result->{msg} .= '<input type="hidden" name="_reseed" id="_reseed" value="0" />'
  369     unless $self->{reseedInserted};
  370 
  371   return ($result,$state);
  372 }
  373 
  374 #
  375 #  Fake grader for when the problem is reset
  376 #
  377 sub resetGrader {
  378   my $answers = shift;
  379   my $state = shift;
  380   my %options = @_;
  381   my $result = {
  382     score => 0,
  383     msg => '',
  384     errors => '',
  385     type => 'problemRandomize (reset)',
  386   };
  387   return ($result,$state);
  388 }
  389 
  390 1;

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9