[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 5340 - (download) (as text) (annotate)
Thu Aug 16 03:45:33 2007 UTC (12 years, 3 months ago) by dpvc
File size: 13077 byte(s)
Show the reseed button and message even if the student hasn't
submitted answers (like when they have just loaded the problem).

    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->{when} = lc($pr->{when});
  150   $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   main::RECORD_FORM_LABEL("_reseed");
  162   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   $self->{isReset} = $main::inputs_ref->{_reseed} || ($self->{submit} && $self->{submit} eq $label);
  167   $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   $status->{seed} = ($main::inputs_ref->{_reseed} || seed());
  192 }
  193 
  194 sub seed {substr(time,5,5)}
  195 
  196 ##################################################
  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   $self->{reseedInserted} = 1;
  207   $par . '<input type="checkbox" name="_reseed" value="'.seed().'" />' . $label;
  208 }
  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        .  q!onclick="document.getElementById('_reseed').value=!.seed().'" />';
  219 }
  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   $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 }
  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   $self->{grader} = \&problemRandomize::resetGrader if $self->{isReset};
  310   my ($result,$state) = &{$self->{grader}}(@_);
  311   shift; shift; my %options = @_;
  312 
  313   #
  314   #  Update that state information and encode it.
  315   #
  316   my $status = $self->{status};
  317   $status->{ans_rule_count} = $main::ans_rule_count;
  318   $status->{answers} = join(';',grep(!/${main::QUIZ_PREFIX}${main::ANSWER_PREFIX}/o,keys(%{$_[0]})));
  319   my $data = quoteHTML($self->encode);
  320   $result->{type} = "problemRandomize ($result->{type})";
  321 
  322   #
  323   #  Conditions for when to show the reseed message
  324   #
  325   my $score = ($options{answer_submitted} ? $result->{score} : $state->{recorded_score});
  326   my $isWhen = ($self->{when} eq 'always' ||
  327      ($self->{when} eq 'correct' && $score >= 1 && !$main::inputs_ref->{previewAnswers}));
  328   my $okDate = (!$self->{onlyAfterDue} || time >= $main::dueDate);
  329 
  330   #
  331   #  Add the problemRandomize message and data
  332   #
  333   if ($isWhen && !$okDate) {
  334     $result->{msg} .= "</i><br /><b>Note:</b> <i>" if $result->{msg};
  335     $result->{msg} .= "You can get a new version of this problem after the due date.";
  336   }
  337   if (!$result->{msg}) {
  338     # hack to remove unwanted "<b>Note: </b>" from the problem
  339     #  (it is inserted automatically by Problem.pm when {msg} is non-emtpy).
  340     $result->{msg} .= '<script>var bb = document.getElementsByTagName("b");'
  341                    .  'bb[bb.length-1].style.display="none"</script>';
  342   }
  343   $result->{msg} .= qq!<input type="hidden" name="_status" value="$data" />!;
  344 
  345   #
  346   #  Include the "randomize" checkbox, button, or whatever.
  347   #
  348   if ($isWhen && $okDate) {
  349     my $method = "randomize".$self->{style};
  350     $result->{msg} .= $self->$method($self->{label},1).'<br/>';
  351   }
  352 
  353   #
  354   #  Don't show the summary section if the problem is being reset.
  355   #
  356   if ($self->{isReset}) {
  357     $result->{msg} .= "<style>.problemHeader {display:none}</style>";
  358     $state->{state_summary_msg} =
  359        "<b>Note:</b> This is a new (re-randomized) version of the problem.".$main::BR.
  360        "If you come back to it later, it may revert to its original version.".$main::BR.
  361        "Hardcopy will always print the original version of the problem.";
  362   }
  363 
  364   #
  365   #  Make sure we don't go on unless the next button really is checked
  366   #
  367   $result->{msg} .= '<input type="hidden" name="_reseed" id="_reseed" value="0" />'
  368     unless $self->{reseedInserted};
  369 
  370   return ($result,$state);
  371 }
  372 
  373 #
  374 #  Fake grader for when the problem is reset
  375 #
  376 sub resetGrader {
  377   my $answers = shift;
  378   my $state = shift;
  379   my %options = @_;
  380   my $result = {
  381     score => 0,
  382     msg => '',
  383     errors => '',
  384     type => 'problemRandomize (reset)',
  385   };
  386   return ($result,$state);
  387 }
  388 
  389 1;

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9