[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 5556 - (download) (as text) (annotate)
Thu Oct 4 16:40:49 2007 UTC (12 years, 4 months ago) by sh002i
File size: 13550 byte(s)
added standard copyright/license header

    1 ################################################################################
    2 # WeBWorK Online Homework Delivery System
    3 # Copyright  2000-2007 The WeBWorK Project, http://openwebwork.sf.net/
    4 # $CVSHeader: webwork2/lib/WeBWorK.pm,v 1.100 2007/08/13 22:59:53 sh002i 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 $PG_FLAGS{problemRandomize}->useGrader(@_) if $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     label => undef,
  184     buttonLabel => "Get a new version of this problem",
  185     checkboxLabel => "Get a new version of this problem",
  186     inputLabel => "Set random seed to:",
  187     grader => $main::PG_FLAGS{PROBLEM_GRADER_TO_USE} || \&main::avg_problem_grader,
  188     random => $main::PG_random_generator,
  189     status => {},
  190     @_
  191   }, $class;
  192   $pr->{style} = uc(substr($pr->{style},0,1)) . lc(substr($pr->{style},1));
  193   $pr->{when} = lc($pr->{when});
  194   $pr->getStatus;
  195   $pr->initProblem;
  196   return $pr;
  197 }
  198 
  199 #
  200 #  Look up the status from the previous invocation
  201 #  and check to see if a rerandomization is requested
  202 #
  203 sub getStatus {
  204   my $self = shift;
  205   main::RECORD_FORM_LABEL("_reseed");
  206   main::RECORD_FORM_LABEL("_status");
  207   my $label = $self->{label} || $self->{lc($self->{style})."Label"};
  208   $self->{status} = $self->decode;
  209   $self->{submit} = $main::inputs_ref->{submitAnswers};
  210   $self->{isReset} = $main::inputs_ref->{_reseed} || ($self->{submit} && $self->{submit} eq $label);
  211   $self->{isReset} = 0 unless !$self->{onlyAfterDue} || time >= $main::dueDate;
  212 }
  213 
  214 #
  215 #  Initialize the current problem
  216 #
  217 sub initProblem {
  218   my $self = shift;
  219   $main::PG_FLAGS{PROBLEM_GRADER_TO_USE} = \&problemRandomize::grader;
  220   $main::PG_FLAGS{problemRandomize} = $self;
  221   $self->reset if $self->{isReset};
  222   $self->{random}->srand($self->{status}{seed});
  223 }
  224 
  225 #
  226 #  Clear the answers and re-randomize the seed
  227 #
  228 sub reset {
  229   my $self = shift;
  230   my $status = $self->{status};
  231   foreach my $id (split(/;/,$status->{answers})) {delete $main::inputs_ref->{$id}}
  232   foreach my $id (1..$status->{ans_rule_count})
  233     {delete $main::inputs_ref->{"${main::QUIZ_PREFIX}${main::ANSWER_PREFIX}$id"}}
  234   $main::inputs_ref->{_status} = $self->encode(\%defaultStatus);
  235   $status->{seed} = ($main::inputs_ref->{_reseed} || seed());
  236 }
  237 
  238 sub seed {substr(time,5,5)}
  239 
  240 ##################################################
  241 
  242 #
  243 #  Return the HTML for the "re-randomize" checkbox.
  244 #
  245 sub randomizeCheckbox {
  246   my $self = shift;
  247   my $label = shift || $self->{checkboxLabel};
  248   $label = "<b>$label</b> (when you submit your answers).";
  249   my $par = shift; $par = ($par ? $main::PAR : '');
  250   $self->{reseedInserted} = 1;
  251   $par . '<input type="checkbox" name="_reseed" value="'.seed().'" />' . $label;
  252 }
  253 
  254 #
  255 #  Return the HTML for the "next part" button.
  256 #
  257 sub randomizeButton {
  258   my $self = shift;
  259   my $label = quoteHTML(shift || $self->{buttonLabel});
  260   my $par = shift; $par = ($par ? $main::PAR : '');
  261   $par . qq!<input type="submit" name="submitAnswers" value="$label" !
  262        .  q!onclick="document.getElementById('_reseed').value=!.seed().'" />';
  263 }
  264 
  265 #
  266 #  Return the HTML for the "problem seed" input box
  267 #
  268 sub randomizeInput {
  269   my $self = shift;
  270   my $label = quoteHTML(shift || $self->{inputLabel});
  271   my $par = shift; $par = ($par ? main::PAR : '');
  272   $par . qq!<input type="submit" name="submitAnswers" value="$label" !
  273        .  q!onclick="document.getElementById('_reseed').value=document.getElementById('_seed').value" />!
  274        . qq!<input name="_seed" id="_seed" value="$self->{status}{seed}" size="10">!;
  275 }
  276 
  277 #
  278 #  Return the raw HTML provided
  279 #
  280 sub randomizeHTML {shift; shift}
  281 
  282 ##################################################
  283 
  284 #
  285 #  Encode all the status information so that it can be
  286 #  maintained as the student submits answers.  Since this
  287 #  state information includes things like the score from
  288 #  the previous parts, it is "encrypted" using a dumb
  289 #  hex encoding (making it harder for a student to recognize
  290 #  it as valuable data if they view the page source).
  291 #
  292 sub encode {
  293   my $self = shift; my $status = shift || $self->{status};
  294   my @data = (); my $data = "";
  295   foreach my $id (main::lex_sort(keys(%defaultStatus))) {push(@data,$status->{$id})}
  296   foreach my $c (split(//,join('|',@data))) {$data .= toHex($c)}
  297   return $data;
  298 }
  299 
  300 #
  301 #  Decode the data and break it into the status hash.
  302 #
  303 sub decode {
  304   my $self = shift; my $status = shift || $main::inputs_ref->{_status};
  305   return {%defaultStatus} unless $status;
  306   my @data = (); foreach my $hex (split(/(..)/,$status)) {push(@data,fromHex($hex)) if $hex ne ''}
  307   @data = split('\\|',join('',@data)); $status = {%defaultStatus};
  308   foreach my $id (main::lex_sort(keys(%defaultStatus))) {$status->{$id} = shift(@data)}
  309   return $status;
  310 }
  311 
  312 
  313 #
  314 #  Hex encoding is shifted by 10 to obfuscate it further.
  315 #  (shouldn't be a problem since the status will be made of
  316 #  printable characters, so they are all above ASCII 32)
  317 #
  318 sub toHex {main::spf(ord(shift)-10,"%X")}
  319 sub fromHex {main::spf(hex(shift)+10,"%c")}
  320 
  321 
  322 #
  323 #  Make sure the data can be properly preserved within
  324 #  an HTML <INPUT TYPE="HIDDEN"> tag.
  325 #
  326 sub quoteHTML {
  327   my $string = shift;
  328   $string =~ s/&/\&amp;/g; $string =~ s/"/\&quot;/g;
  329   $string =~ s/>/\&gt;/g;  $string =~ s/</\&lt;/g;
  330   return $string;
  331 }
  332 
  333 ##################################################
  334 
  335 #
  336 #  Set the grader for this part to the specified one.
  337 #
  338 sub useGrader {
  339   my $self = shift;
  340   $self->{grader} = shift;
  341 }
  342 
  343 #
  344 #  The custom grader that does the work of computing the scores
  345 #  and saving the data.
  346 #
  347 sub grader {
  348   my $self = $main::PG_FLAGS{problemRandomize};
  349 
  350   #
  351   #  Call the original grader
  352   #
  353   $self->{grader} = \&problemRandomize::resetGrader if $self->{isReset};
  354   my ($result,$state) = &{$self->{grader}}(@_);
  355   shift; shift; my %options = @_;
  356 
  357   #
  358   #  Update that state information and encode it.
  359   #
  360   my $status = $self->{status};
  361   $status->{ans_rule_count} = $main::ans_rule_count;
  362   $status->{answers} = join(';',grep(!/${main::QUIZ_PREFIX}${main::ANSWER_PREFIX}/o,keys(%{$_[0]})));
  363   my $data = quoteHTML($self->encode);
  364   $result->{type} = "problemRandomize ($result->{type})";
  365 
  366   #
  367   #  Conditions for when to show the reseed message
  368   #
  369   my $inputs = $main::inputs_ref;
  370   my $isSubmit = $inputs->{submitAnswers} || $inputs->{previewAnswers} || $inputs->{checkAnswers};
  371   my $score = ($isSubmit || $self->{isReset} ? $result->{score} : $state->{recorded_score});
  372   my $isWhen = ($self->{when} eq 'always' ||
  373      ($self->{when} eq 'correct' && $score >= 1 && !$main::inputs_ref->{previewAnswers}));
  374   my $okDate = (!$self->{onlyAfterDue} || time >= $main::dueDate);
  375 
  376   #
  377   #  Add the problemRandomize message and data
  378   #
  379   if ($isWhen && !$okDate) {
  380     $result->{msg} .= "</i><br /><b>Note:</b> <i>" if $result->{msg};
  381     $result->{msg} .= "You can get a new version of this problem after the due date.";
  382   }
  383   if (!$result->{msg}) {
  384     # hack to remove unwanted "<b>Note: </b>" from the problem
  385     #  (it is inserted automatically by Problem.pm when {msg} is non-emtpy).
  386     $result->{msg} .= '<script>var bb = document.getElementsByTagName("b");'
  387                    .  'bb[bb.length-1].style.display="none"</script>';
  388   }
  389   $result->{msg} .= qq!<input type="hidden" name="_status" value="$data" />!;
  390 
  391   #
  392   #  Include the "randomize" checkbox, button, or whatever.
  393   #
  394   if ($isWhen && $okDate) {
  395     my $method = "randomize".$self->{style};
  396     $result->{msg} .= $self->$method($self->{label},1).'<br/>';
  397   }
  398 
  399   #
  400   #  Don't show the summary section if the problem is being reset.
  401   #
  402   if ($self->{isReset} && $isSubmit) {
  403     $result->{msg} .= "<style>.problemHeader {display:none}</style>";
  404     $state->{state_summary_msg} =
  405        "<b>Note:</b> This is a new (re-randomized) version of the problem.".$main::BR.
  406        "If you come back to it later, it may revert to its original version.".$main::BR.
  407        "Hardcopy will always print the original version of the problem.";
  408   }
  409 
  410   #
  411   #  Make sure we don't go on unless the next button really is checked
  412   #
  413   $result->{msg} .= '<input type="hidden" name="_reseed" id="_reseed" value="0" />'
  414     unless $self->{reseedInserted};
  415 
  416   return ($result,$state);
  417 }
  418 
  419 #
  420 #  Fake grader for when the problem is reset
  421 #
  422 sub resetGrader {
  423   my $answers = shift;
  424   my $state = shift;
  425   my %options = @_;
  426   my $result = {
  427     score => 0,
  428     msg => '',
  429     errors => '',
  430     type => 'problemRandomize (reset)',
  431   };
  432   return ($result,$state);
  433 }
  434 
  435 1;

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9