[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 6590 - (download) (as text) (annotate)
Wed Dec 1 21:29:54 2010 UTC (9 years, 1 month ago) by gage
File size: 13796 byte(s)
changed references to  $ans_rule_count  to ans_rule_count();



    1 ################################################################################
    2 # WeBWorK Online Homework Delivery System
    3 # Copyright  2000-2007 The WeBWorK Project, http://openwebwork.sf.net/
    4 # $CVSHeader: pg/macros/problemRandomize.pl,v 1.12 2009/06/25 23:28:44 gage Exp $
    5 #
    6 # This program is free software; you can redistribute it and/or modify it under
    7 # the terms of either: (a) the GNU General Public License as published by the
    8 # Free Software Foundation; either version 2, or (at your option) any later
    9 # version, or (b) the "Artistic License" which comes with this package.
   10 #
   11 # This program is distributed in the hope that it will be useful, but WITHOUT
   12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
   13 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
   14 # Artistic License for more details.
   15 ################################################################################
   16 
   17 =head1 NAME
   18 
   19 problemRandomize.pl - Reseed a problem so that students can do additional versions for
   20 more practice.
   21 
   22 =head1 DESCRIPTION
   23 
   24 This file implements a mechanism for allowing a problem file to be
   25 "reseeded" so that the student can do additional versions of the
   26 problem.  You can control when the reseed message is available,
   27 and what style to use for it.
   28 
   29 To use the problemRandimize library, use
   30 
   31   loadMacros("problemRandomize.pl");
   32 
   33 at the top of your problem file, and then create a problemRandomize
   34 object with
   35 
   36   $pr = ProblemRandomize(options);
   37 
   38 where '$pr' is the name of the variable you will use to refer
   39 to the randomized problem (if needed), and 'options' can include:
   40 
   41 =over
   42 
   43 =item C<S<< when => type >>>
   44 
   45 Specifies the condition on which
   46 reseeding the problem is allowed.
   47 The choices include:
   48 
   49 =over
   50 
   51 =item *
   52 
   53 C<Correct> - only when the problem has been answered correctly.
   54 
   55 =item *
   56 
   57 C<Always> - reseeding is always allowed.
   58 
   59 =back
   60 
   61 Default: "Correct"
   62 
   63 =item C<S<< onlyAfterDue => 0 or 1 >>>
   64 
   65 Specifies if the reseed option is only
   66 allowed after the due date has passed.
   67 Default:  1
   68 
   69 =item C<S<< style => type >>>
   70 
   71 Determines the type of interaction needed
   72 to reseed the problem.  Types include:
   73 
   74 =over
   75 
   76 =item *
   77 
   78 C<Button> - a button.
   79 
   80 =item *
   81 
   82 C<Checkbox> - a checkbox plus pressing submit.
   83 
   84 =item *
   85 
   86 C<Input> - an input box where the seed can be set explicitly.
   87 
   88 =item *
   89 
   90 C<HTML> - the HTML is given explicitly via the "label" option below.
   91 
   92 =back
   93 
   94 Default:  "Button"
   95 
   96 =item C<S<< label => "text" >>>
   97 
   98 Specifies the text used for the button name,
   99 checkbox label, input box label, or raw HTML
  100 used for the reseed mechanism.
  101 
  102 =back
  103 
  104 The problemRandomize library installs a special grader that handles determining
  105 when the reseed option will be available.  It also redefines install_problem_grader
  106 so that it will not overwrite the one installed by the library (it is stored so
  107 that it can be called internally by the problemRandomize library's grader).
  108 
  109 Note that the problem will store the new problem seed only if the student can
  110 submit saved answers (i.e., only before the due date).  After the due date,
  111 the student can get new versions, but the problem will revert to the original
  112 version when they come back to the problem later.  Since the default is only
  113 to allow reseeding afer the due date, the reseeding will not be sticky by default.
  114 Hardcopy ALWAYS produces the original version of the problem, regardless of
  115 the seed saved by the student.
  116 
  117 Examples:
  118 
  119   ProblemRandomize();                               # use all defaults
  120   ProblemRandomize(when=>"Always");                 # always can reseed (after due date)
  121   ProblemRandomize(onlyAfterDue=>0);                # can reseed whenever correct
  122   ProblemRandomize(when=>"always",onlyAfterDue=>0); # always can reseed
  123   ProblemRandomize(style=>"Input");                 # use an input box to set the seed
  124 
  125 For problems that include "PGcourse.pl" in their loadMacros() calls, you can
  126 use that file to provide reseed buttons for ALL problems simply by including
  127 
  128   loadMacros("problemRandomize.pl");
  129   ProblemRandomize();
  130 
  131 in PGcourse.pl.  You can make the ProblemRandomize() be dependent on the set
  132 number or the set or the login ID or whatever.  For example
  133 
  134   loadMacros("problemRandomize.pl");
  135   ProblemRandomize(when=>"always",onlyAfterDue=>0,style=>"Input")
  136     if $studentLogin eq "dpvc";
  137 
  138 would enable reseeding at any time for the user called "dpvc" (presumably a
  139 professor).  You can test $probNum and $setNumber to make reseeding available
  140 only for specific sets or problems within a set.
  141 
  142 =cut
  143 
  144 sub _problemRandomize_init {
  145   sub ProblemRandomize {new problemRandomize(@_)}
  146   PG_restricted_eval(<<'  end_eval');
  147     sub install_problem_grader {
  148       return $main::PG->{flags}->{problemRandomize}->useGrader(@_) if $main::PG->{flags}->{problemRandomize};
  149       &{$problemRandomize::installGrader}(@_); # call cached version
  150     }
  151   end_eval
  152 }
  153 
  154 ######################################################################
  155 
  156 package problemRandomize;
  157 
  158 #
  159 #  The state data that is stored between invocations of
  160 #  the problem.
  161 #
  162 our %defaultStatus = (
  163   seed => $main::problemSeed,  # original seed
  164   answers => "",               # list of answer names
  165   ans_rule_count => 0,         # number of unnamed answers
  166 );
  167 
  168 #
  169 #  Cache original grader installer (so we can override it).
  170 #
  171 our $installGrader = \&main::install_problem_grader;
  172 
  173 #
  174 #  Create new problemRandomize object from user's data
  175 #  and initialize it.
  176 #
  177 sub new {
  178   my $self = shift; my $class = ref($self) || $self;
  179   my $pr = bless {
  180     when => "correct",
  181     onlyAfterDue => 1,
  182     style => "Button",
  183     styleName => ($main::inputs_ref->{effectiveUser} ne $main::inputs_ref->{user} ? "checkAnswers" : "submitAnswers"),
  184     label => undef,
  185     buttonLabel => "Get a new version of this problem",
  186     checkboxLabel => "Get a new version of this problem",
  187     inputLabel => "Set random seed to:",
  188     grader => $main::PG->{flags}->{PROBLEM_GRADER_TO_USE}  || \&main::avg_problem_grader,  #$main::PG_FLAGS{PROBLEM_GRADER_TO_USE}
  189     random => $main::PG_random_generator,
  190     status => {},
  191     @_
  192   }, $class;
  193   $pr->{style} = uc(substr($pr->{style},0,1)) . lc(substr($pr->{style},1));
  194   $pr->{when} = lc($pr->{when});
  195   $pr->getStatus;
  196   $pr->initProblem;
  197   return $pr;
  198 }
  199 
  200 #
  201 #  Look up the status from the previous invocation
  202 #  and check to see if a rerandomization is requested
  203 #
  204 sub getStatus {
  205   my $self = shift;
  206   main::RECORD_FORM_LABEL("_reseed");
  207   main::RECORD_FORM_LABEL("_status");
  208   my $label = $self->{label} || $self->{lc($self->{style})."Label"};
  209   $self->{status} = $self->decode;
  210   $self->{submit} = $main::inputs_ref->{submitAnswers};
  211   $self->{isReset} = $main::inputs_ref->{_reseed} || ($self->{submit} && $self->{submit} eq $label);
  212   $self->{isReset} = 0 unless !$self->{onlyAfterDue} || time >= $main::dueDate;
  213 }
  214 
  215 #
  216 #  Initialize the current problem
  217 #
  218 sub initProblem {
  219   my $self = shift;
  220   $main::PG->{flags}->{PROBLEM_GRADER_TO_USE} = \&problemRandomize::grader;
  221   $main::PG->{flags}->{problemRandomize} = $self;
  222   $self->reset if $self->{isReset};
  223   $main::problemSeed = $self->{status}{seed};
  224   $self->{random}->srand($self->{status}{seed});
  225 }
  226 
  227 #
  228 #  Clear the answers and re-randomize the seed
  229 #
  230 sub reset {
  231   my $self = shift;
  232   my $status = $self->{status};
  233   foreach my $id (split(/;/,$status->{answers})) {delete $main::inputs_ref->{$id}}
  234   foreach my $id (1..$status->{ans_rule_count})
  235     {delete $main::inputs_ref->{main::ANS_NUM_TO_NAME($id)}}
  236   $main::inputs_ref->{_status} = $self->encode(\%defaultStatus);
  237   $status->{seed} = ($main::inputs_ref->{_reseed} || seed());
  238 }
  239 
  240 sub seed {substr(time,5,5)}
  241 
  242 ##################################################
  243 
  244 #
  245 #  Return the HTML for the "re-randomize" checkbox.
  246 #
  247 sub randomizeCheckbox {
  248   my $self = shift;
  249   my $label = shift || $self->{checkboxLabel};
  250   $label = "<b>$label</b> (when you submit your answers).";
  251   my $par = shift; $par = ($par ? $main::PAR : '');
  252   $self->{reseedInserted} = 1;
  253   $par . '<input type="checkbox" name="_reseed" value="'.seed().'" />' . $label;
  254 }
  255 
  256 #
  257 #  Return the HTML for the "next part" button.
  258 #
  259 sub randomizeButton {
  260   my $self = shift;
  261   my $label = quoteHTML(shift || $self->{buttonLabel});
  262   my $par = shift; $par = ($par ? $main::PAR : '');
  263   $par . qq!<input type="submit" name="$self->{styleName}" value="$label" !
  264        .  q!onclick="document.getElementById('_reseed').value=!.seed().'" />';
  265 }
  266 
  267 #
  268 #  Return the HTML for the "problem seed" input box
  269 #
  270 sub randomizeInput {
  271   my $self = shift;
  272   my $label = quoteHTML(shift || $self->{inputLabel});
  273   my $par = shift; $par = ($par ? main::PAR : '');
  274   $par . qq!<input type="submit" name="$self->{styleName}" value="$label" !
  275        .  q!onclick="document.getElementById('_reseed').value=document.getElementById('_seed').value" />!
  276        . qq!<input name="_seed" id="_seed" value="$self->{status}{seed}" size="10">!;
  277 }
  278 
  279 #
  280 #  Return the raw HTML provided
  281 #
  282 sub randomizeHTML {shift; shift}
  283 
  284 ##################################################
  285 
  286 #
  287 #  Encode all the status information so that it can be
  288 #  maintained as the student submits answers.  Since this
  289 #  state information includes things like the score from
  290 #  the previous parts, it is "encrypted" using a dumb
  291 #  hex encoding (making it harder for a student to recognize
  292 #  it as valuable data if they view the page source).
  293 #
  294 sub encode {
  295   my $self = shift; my $status = shift || $self->{status};
  296   my @data = (); my $data = "";
  297   foreach my $id (main::lex_sort(keys(%defaultStatus))) {push(@data, ($status->{$id}) )}
  298   foreach my $c (split(//,join('|',@data))) {$data .= toHex($c)}
  299   return $data;
  300 }
  301 
  302 #
  303 #  Decode the data and break it into the status hash.
  304 #
  305 sub decode {
  306   my $self = shift; my $status = shift || $main::inputs_ref->{_status};
  307   return {%defaultStatus} unless $status;
  308   my @data = (); foreach my $hex (split(/(..)/,$status)) {push(@data,fromHex($hex)) if $hex ne ''}
  309   @data = split('\\|',join('',@data)); $status = {%defaultStatus};
  310   foreach my $id (main::lex_sort(keys(%defaultStatus))) {$status->{$id} = shift(@data)}
  311   return $status;
  312 }
  313 
  314 
  315 #
  316 #  Hex encoding is shifted by 10 to obfuscate it further.
  317 #  (shouldn't be a problem since the status will be made of
  318 #  printable characters, so they are all above ASCII 32)
  319 #
  320 sub toHex {main::spf(ord(shift)-10,"%X")}
  321 sub fromHex {main::spf(hex(shift)+10,"%c")}
  322 
  323 
  324 #
  325 #  Make sure the data can be properly preserved within
  326 #  an HTML <INPUT TYPE="HIDDEN"> tag.
  327 #
  328 sub quoteHTML {
  329   my $string = shift;
  330   $string =~ s/&/\&amp;/g; $string =~ s/"/\&quot;/g;
  331   $string =~ s/>/\&gt;/g;  $string =~ s/</\&lt;/g;
  332   return $string;
  333 }
  334 
  335 ##################################################
  336 
  337 #
  338 #  Set the grader for this part to the specified one.
  339 #
  340 sub useGrader {
  341   my $self = shift;
  342   $self->{grader} = shift;
  343 }
  344 
  345 #
  346 #  The custom grader that does the work of computing the scores
  347 #  and saving the data.
  348 #
  349 sub grader {
  350   my $self = $main::PG->{flags}->{problemRandomize};
  351 
  352   #
  353   #  Call the original grader
  354   #
  355   $self->{grader} = \&problemRandomize::resetGrader if $self->{isReset};
  356   my ($result,$state) = &{$self->{grader}}(@_);
  357   shift; shift; my %options = @_;
  358 
  359   #
  360   #  Update that state information and encode it.
  361   #
  362   my $status = $self->{status};
  363   $status->{ans_rule_count} = main::ans_rule_count();
  364   $status->{answers} = join(';',grep(!/${main::QUIZ_PREFIX}${main::ANSWER_PREFIX}/o,keys(%{$_[0]})));
  365   my $data = quoteHTML($self->encode);
  366   $result->{type} = "problemRandomize ($result->{type})";
  367 
  368   #
  369   #  Conditions for when to show the reseed message
  370   #
  371   my $inputs = $main::inputs_ref;
  372   my $isSubmit = $inputs->{submitAnswers} || $inputs->{previewAnswers} || $inputs->{checkAnswers};
  373   my $score = ($isSubmit || $self->{isReset} ? $result->{score} : $state->{recorded_score});
  374   my $isWhen = ($self->{when} eq 'always' ||
  375      ($self->{when} eq 'correct' && $score >= 1 && !$main::inputs_ref->{previewAnswers}));
  376   my $okDate = (!$self->{onlyAfterDue} || time >= $main::dueDate);
  377 
  378   #
  379   #  Add the problemRandomize message and data
  380   #
  381   if ($isWhen && !$okDate) {
  382     $result->{msg} .= "</i><br /><b>Note:</b> <i>" if $result->{msg};
  383     $result->{msg} .= "You can get a new version of this problem after the due date.";
  384   }
  385   if (!$result->{msg}) {
  386     # hack to remove unwanted "<b>Note: </b>" from the problem
  387     #  (it is inserted automatically by Problem.pm when {msg} is non-emtpy).
  388     $result->{msg} .= '<script>var bb = document.getElementsByTagName("b");'
  389                    .  'bb[bb.length-1].style.display="none"</script>';
  390   }
  391   $result->{msg} .= qq!<input type="hidden" name="_status" value="$data" />!;
  392 
  393   #
  394   #  Include the "randomize" checkbox, button, or whatever.
  395   #
  396   if ($isWhen && $okDate) {
  397     my $method = "randomize".$self->{style};
  398     $result->{msg} .= $self->$method($self->{label},1).'<br/>';
  399   }
  400 
  401   #
  402   #  Don't show the summary section if the problem is being reset.
  403   #
  404   if ($self->{isReset} && $isSubmit) {
  405     $result->{msg} .= "<style>.problemHeader {display:none}</style>";
  406     $state->{state_summary_msg} =
  407        "<b>Note:</b> This is a new (re-randomized) version of the problem.".$main::BR.
  408        "If you come back to it later, it may revert to its original version.".$main::BR.
  409        "Hardcopy will always print the original version of the problem.";
  410   }
  411 
  412   #
  413   #  Make sure we don't go on unless the next button really is checked
  414   #
  415   $result->{msg} .= '<input type="hidden" name="_reseed" id="_reseed" value="0" />'
  416     unless $self->{reseedInserted};
  417 
  418   return ($result,$state);
  419 }
  420 
  421 #
  422 #  Fake grader for when the problem is reset
  423 #
  424 sub resetGrader {
  425   my $answers = shift;
  426   my $state = shift;
  427   my %options = @_;
  428   my $result = {
  429     score => 0,
  430     msg => '',
  431     errors => '',
  432     type => 'problemRandomize (reset)',
  433   };
  434   return ($result,$state);
  435 }
  436 
  437 1;

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9