[system] / trunk / webwork-modperl / lib / WeBWorK / ContentGenerator / GatewayQuiz.pm Repository:
ViewVC logotype

View of /trunk/webwork-modperl/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 3377 - (download) (as text) (annotate)
Thu Jul 14 13:15:27 2005 UTC (7 years, 10 months ago) by glarose
File size: 60014 byte(s)
Preliminary commit of changes to add Gateway module.
This adds to WeBWorK
 - the ability to create versioned, timed problem sets ("gateway tests")
   for which all problems are displayed on a single page ("versioned"
   means that students can get multiple versions of the problem set),
 - the ability to create sets that draw problems from groups of
   problems, and
 - the ability to create sets that require a proctor login to start
   and grade.
Sets can be defined as gateway tests or proctored gateway tests from
the ProblemSetDetail page.

Not quite bug-free yet.  Known bugs include handling of problem values
on the Student Progress page (I think this may be a problem with
changing from sql database format where all entries were 'text' to
sql_single in ver 2.1, where they are integer), and a division by zero
error on the grades page (which may be the same problem).

Tests with a number of attempts per version greater than one haven't
been carefully tested, nor has scoring of gateway tests.

    1 ################################################################################
    2 # WeBWorK Online Homework Delivery System
    3 # Copyright © 2000-2003 The WeBWorK Project, http://openwebwork.sf.net/
    4 # $CVSHeader: webwork2/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm,v 1.9.4.2 2004/07/16 20:06:03 glarose 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 package WeBWorK::ContentGenerator::GatewayQuiz;
   18 use base qw(WeBWorK::ContentGenerator);
   19 
   20 =head1 NAME
   21 
   22 WeBWorK::ContentGenerator::GatewayQuiz - display a quiz of problems on one page,
   23 deal with versioning sets
   24 
   25 =cut
   26 
   27 use strict;
   28 use warnings;
   29 use CGI qw();
   30 use File::Path qw(rmtree);
   31 use WeBWorK::Form;
   32 use WeBWorK::PG;
   33 use WeBWorK::PG::ImageGenerator;
   34 use WeBWorK::PG::IO;
   35 use WeBWorK::Utils qw(writeLog writeCourseLog encodeAnswers decodeAnswers ref2string makeTempDirectory);
   36 use WeBWorK::DB::Utils qw(global2user user2global findDefaults);
   37 use WeBWorK::Timing;
   38 
   39 use WeBWorK::ContentGenerator::Instructor qw(assignSetVersionToUser);
   40 
   41 # template method
   42 sub templateName {
   43     return "gateway";
   44 }
   45 
   46 
   47 ################################################################################
   48 # "can" methods
   49 ################################################################################
   50 
   51 # Subroutines to determine if a user "can" perform an action. Each subroutine is
   52 # called with the following arguments:
   53 #
   54 #     ($self, $User, $PermissionLevel, $EffectiveUser, $Set, $Problem)
   55 
   56 # *** The "can" routines are taken from Problem.pm, with small modifications
   57 # *** to look at number of attempts per version, not per set, and to allow
   58 # *** showing of correct answers after all attempts at a version are used
   59 
   60 sub can_showOldAnswers {
   61   #my ($self, $User, $PermissionLevel, $EffectiveUser, $Set, $Problem) = @_;
   62 
   63   return 1;
   64 }
   65 
   66 # gateway change here: add $submitAnswers as an optional additional argument
   67 #   to be included if it's defined
   68 sub can_showCorrectAnswers {
   69   my ($self, $User, $PermissionLevel, $EffectiveUser, $Set, $Problem,
   70       $submitAnswers) = @_;
   71   my $authz = $self->r->authz;
   72 
   73 # gateway change here to allow correct answers to be viewed after all attempts
   74 #   at a version are exhausted as well as if it's after the answer date
   75 # $addOne allows us to count the current submission
   76   my $addOne = defined( $submitAnswers ) ? $submitAnswers : 0;
   77   my $maxAttempts = $Set->attempts_per_version();
   78   my $attemptsUsed = $Problem->num_correct + $Problem->num_incorrect +
   79       $addOne;
   80 
   81   return ( ( after( $Set->answer_date ) ||
   82        $attemptsUsed >= $maxAttempts ) ||
   83      $authz->hasPermissions($User->user_id,
   84         "show_correct_answers_before_answer_date") )
   85      ;
   86 }
   87 
   88 sub can_showHints {
   89   #my ($self, $User, $PermissionLevel, $EffectiveUser, $Set, $Problem) = @_;
   90 
   91   return 1;
   92 }
   93 
   94 # gateway change here: add $submitAnswers as an optional additional argument
   95 #   to be included if it's defined
   96 sub can_showSolutions {
   97   my ($self, $User, $PermissionLevel, $EffectiveUser, $Set, $Problem,
   98       $submitAnswers) = @_;
   99   my $authz = $self->r->authz;
  100 
  101 # this is the same as can_showCorrectAnswers
  102 # gateway change here to allow correct answers to be viewed after all attempts
  103 #   at a version are exhausted as well as if it's after the answer date
  104 # $addOne allows us to count the current submission
  105   my $addOne = defined( $submitAnswers ) ? $submitAnswers : 0;
  106   my $maxAttempts = $Set->attempts_per_version();
  107   my $attemptsUsed = $Problem->num_correct+$Problem->num_incorrect+$addOne;
  108 
  109   return ( ( after( $Set->answer_date ) ||
  110        $attemptsUsed >= $maxAttempts ) ||
  111      $authz->hasPermissions($User->user_id,
  112         "show_correct_answers_before_answer_date") );
  113 }
  114 
  115 # gateway change here: add $submitAnswers as an optional additional argument
  116 #   to be included if it's defined
  117 # we also allow for a version_last_attempt_time which is the time the set was
  118 #   submitted; if that's present we use that instead of the current time to
  119 #   decide if we can record the answers.  this deals with the time between the
  120 #   submission time and the proctor authorization.
  121 sub can_recordAnswers {
  122   my ($self, $User, $PermissionLevel, $EffectiveUser, $Set, $Problem,
  123       $submitAnswers) = @_;
  124   my $authz = $self->r->authz;
  125 
  126   my $submitTime = ( defined($Set->version_last_attempt_time()) &&
  127          $Set->version_last_attempt_time() ) ?
  128          $Set->version_last_attempt_time() : time();
  129 
  130   if ($User->user_id ne $EffectiveUser->user_id) {
  131     return $authz->hasPermissions($User->user_id, "record_answers_when_acting_as_student");
  132   }
  133   if (before($Set->open_date, $submitTime)) {
  134     return $authz->hasPermissions($User->user_id, "record_answers_before_open_date");
  135   } elsif (between($Set->open_date, $Set->due_date, $submitTime)) {
  136 
  137 # gateway change here; we look at maximum attempts per version, not for the set,
  138 #   to determine the number of attempts allowed
  139 # $addOne allows us to count the current submission
  140       my $addOne = defined( $submitAnswers ) ? $submitAnswers : 0;
  141       my $max_attempts = $Set->attempts_per_version();
  142       my $attempts_used = $Problem->num_correct+$Problem->num_incorrect+$addOne;
  143     if ($max_attempts == -1 or $attempts_used < $max_attempts) {
  144       return $authz->hasPermissions($User->user_id, "record_answers_after_open_date_with_attempts");
  145     } else {
  146       return $authz->hasPermissions($User->user_id, "record_answers_after_open_date_without_attempts");
  147     }
  148   } elsif (between($Set->due_date, $Set->answer_date, $submitTime)) {
  149     return $authz->hasPermissions($User->user_id, "record_answers_after_due_date");
  150   } elsif (after($Set->answer_date, $submitTime)) {
  151     return $authz->hasPermissions($User->user_id, "record_answers_after_answer_date");
  152   }
  153 }
  154 
  155 # gateway change here: add $submitAnswers as an optional additional argument
  156 #   to be included if it's defined
  157 # we also allow for a version_last_attempt_time which is the time the set was
  158 #   submitted; if that's present we use that instead of the current time to
  159 #   decide if we can check the answers.  this deals with the time between the
  160 #   submission time and the proctor authorization.
  161 sub can_checkAnswers {
  162   my ($self, $User, $PermissionLevel, $EffectiveUser, $Set, $Problem,
  163       $submitAnswers) = @_;
  164   my $authz = $self->r->authz;
  165 
  166   my $submitTime = ( defined($Set->version_last_attempt_time()) &&
  167          $Set->version_last_attempt_time() ) ?
  168          $Set->version_last_attempt_time() : time();
  169 
  170   if (before($Set->open_date, $submitTime)) {
  171     return $authz->hasPermissions($User->user_id, "check_answers_before_open_date");
  172   } elsif (between($Set->open_date, $Set->due_date, $submitTime)) {
  173 
  174 # gateway change here; we look at maximum attempts per version, not for the set,
  175 #   to determine the number of attempts allowed
  176 # $addOne allows us to count the current submission
  177       my $addOne = defined( $submitAnswers ) ? $submitAnswers : 0;
  178       my $max_attempts = $Set->attempts_per_version();
  179       my $attempts_used = $Problem->num_correct+$Problem->num_incorrect+$addOne;
  180 
  181     if ($max_attempts == -1 or $attempts_used < $max_attempts) {
  182       return $authz->hasPermissions($User->user_id, "check_answers_after_open_date_with_attempts");
  183     } else {
  184       return $authz->hasPermissions($User->user_id, "check_answers_after_open_date_without_attempts");
  185     }
  186   } elsif (between($Set->due_date, $Set->answer_date, $submitTime)) {
  187     return $authz->hasPermissions($User->user_id, "check_answers_after_due_date");
  188   } elsif (after($Set->answer_date, $submitTime)) {
  189     return $authz->hasPermissions($User->user_id, "check_answers_after_answer_date");
  190   }
  191 }
  192 
  193 # Helper functions for calculating times
  194 # gateway change here: we allow an optional additional argument to use as the
  195 #   time to check rather than time()
  196 sub before  { return (@_==2) ? $_[1] <= $_[0] : time <= $_[0] }
  197 sub after   { return (@_==2) ? $_[1] >= $_[0] : time >= $_[0] }
  198 sub between { my $t = (@_==3) ? $_[2] : time; return $t > $_[0] && $t < $_[1] }
  199 
  200 ################################################################################
  201 # output utilities
  202 ################################################################################
  203 
  204 # subroutine is modified from that in Problem.pm to produce a different
  205 #    table format
  206 sub attemptResults {
  207   my $self = shift;
  208   my $pg = shift;
  209   my $showAttemptAnswers = shift;
  210   my $showCorrectAnswers = shift;
  211   my $showAttemptResults = $showAttemptAnswers && shift;
  212   my $showSummary = shift;
  213   my $showAttemptPreview = shift || 0;
  214 
  215   my $r = $self->{r};
  216   my $setName = $r->urlpath->arg("setID");
  217   my $ce = $self->{ce};
  218   my $root = $ce->{webworkURLs}->{root};
  219   my $courseName = $ce->{courseName};
  220   my @links = ("Homework Sets" , "$root/$courseName", "navUp");
  221   my $tail = "";
  222 
  223   my $problemResult = $pg->{result}; # the overall result of the problem
  224   my @answerNames = @{ $pg->{flags}->{ANSWER_ENTRY_ORDER} };
  225 
  226   my $showMessages = $showAttemptAnswers && grep { $pg->{answers}->{$_}->{ans_message} } @answerNames;
  227 
  228   # present in ver 1.10; why is this checked here?
  229   # return CGI::p(CGI::font({-color=>"red"}, "This problem is not available because the homework set that contains it is not yet open."))
  230   # unless $self->{isOpen};
  231 
  232   my $basename = "equation-" . $self->{set}->psvn. "." . $self->{problem}->problem_id . "-preview";
  233 
  234   # to make grabbing these options easier, we'll pull them out now...
  235   my %imagesModeOptions = %{$ce->{pg}->{displayModeOptions}->{images}};
  236 
  237   my $imgGen = WeBWorK::PG::ImageGenerator->new(
  238     tempDir         => $ce->{webworkDirs}->{tmp},
  239     latex         => $ce->{externalPrograms}->{latex},
  240     dvipng          => $ce->{externalPrograms}->{dvipng},
  241     useCache        => 1,
  242     cacheDir        => $ce->{webworkDirs}->{equationCache},
  243     cacheURL        => $ce->{webworkURLs}->{equationCache},
  244     cacheDB         => $ce->{webworkFiles}->{equationCacheDB},
  245     dvipng_align    => $imagesModeOptions{dvipng_align},
  246     dvipng_depth_db => $imagesModeOptions{dvipng_depth_db},
  247   );
  248 
  249   my %resultsData = ();
  250   $resultsData{'Entered'}  = CGI::td({-class=>"label"}, "Your answer parses as:");
  251   $resultsData{'Preview'}  = CGI::td({-class=>"label"}, "Your answer previews as:");
  252   $resultsData{'Correct'}  = CGI::td({-class=>"label"}, "The correct answer is:");
  253   $resultsData{'Results'}  = CGI::td({-class=>"label"}, "Result:");
  254   $resultsData{'Messages'} = CGI::td({-class=>"label"}, "Messages:");
  255 
  256   my %resultsRows = ();
  257   foreach ( qw( Entered Preview Correct Results Messages ) ) {
  258       $resultsRows{$_} = "";
  259   }
  260 
  261   my $numCorrect = 0;
  262   my $numAns = 0;
  263   foreach my $name (@answerNames) {
  264     my $answerResult  = $pg->{answers}->{$name};
  265     my $studentAnswer = $answerResult->{student_ans}; # original_student_ans
  266     my $preview       = ($showAttemptPreview
  267                           ? $self->previewAnswer($answerResult, $imgGen)
  268                           : "");
  269     my $correctAnswer = $answerResult->{correct_ans};
  270     my $answerScore   = $answerResult->{score};
  271     my $answerMessage = $showMessages ? $answerResult->{ans_message} : "";
  272     #FIXME  --Can we be sure that $answerScore is an integer-- could the problem give partial credit?
  273     $numCorrect += $answerScore > 0;
  274     my $resultString = $answerScore == 1 ? "correct" : "incorrect";
  275 
  276     # get rid of the goofy prefix on the answer names (supposedly, the format
  277     # of the answer names is changeable. this only fixes it for "AnSwEr"
  278     #$name =~ s/^AnSwEr//;
  279 
  280     my $pre = $numAns ? CGI::td("&nbsp;") : "";
  281 
  282     $resultsRows{'Entered'} .= $showAttemptAnswers ?
  283         CGI::Tr( $pre . $resultsData{'Entered'} .
  284            CGI::td({-class=>"output"}, $self->nbsp($studentAnswer))) : "";
  285     $resultsData{'Entered'} = '';
  286     $resultsRows{'Preview'} .= $showAttemptPreview ?
  287         CGI::Tr( $pre . $resultsData{'Preview'} .
  288            CGI::td({-class=>"output"}, $self->nbsp($preview)) ) : "";
  289     $resultsData{'Preview'} = '';
  290     $resultsRows{'Correct'} .= $showCorrectAnswers ?
  291         CGI::Tr( $pre . $resultsData{'Correct'} .
  292            CGI::td({-class=>"output"}, $self->nbsp($correctAnswer)) ) : "";
  293     $resultsData{'Correct'} = '';
  294     $resultsRows{'Results'} .= $showAttemptResults ?
  295         CGI::Tr( $pre . $resultsData{'Results'} .
  296            CGI::td({-class=>"output"}, $self->nbsp($resultString)) )  : "";
  297     $resultsRows{'Results'} = '';
  298     $resultsRows{'Messages'} .= $showMessages ?
  299         CGI::Tr( $pre . $resultsData{'Messages'} .
  300            CGI::td({-class=>"output"}, $self->nbsp($answerMessage)) ) : "";
  301 
  302     $numAns++;
  303   }
  304 
  305   # render equation images
  306   $imgGen->render(refresh => 1);
  307 
  308 # my $numIncorrectNoun = scalar @answerNames == 1 ? "question" : "questions";
  309   my $scorePercent = sprintf("%.0f%%", $problemResult->{score} * 100);
  310 #   FIXME  -- I left the old code in in case we have to back out.
  311 # my $summary = "On this attempt, you answered $numCorrect out of "
  312 #   . scalar @answerNames . " $numIncorrectNoun correct, for a score of $scorePercent.";
  313 
  314   my $summary = "";
  315   if (scalar @answerNames == 1) {
  316       if ($numCorrect == scalar @answerNames) {
  317         $summary .= CGI::div({class=>"gwCorrect"},"This answer is correct.");
  318        } else {
  319          $summary .= CGI::div({class=>"gwIncorrect"},"This answer is NOT correct.");
  320        }
  321   } else {
  322       if ($numCorrect == scalar @answerNames) {
  323         $summary .= CGI::div({class=>"gwCorrect"},"All of these answers are correct.");
  324        } else {
  325          $summary .= CGI::div({class=>"gwIncorrect"},"At least one of these answers is NOT correct.");
  326        }
  327   }
  328 
  329   return
  330 #     CGI::table({-class=>"attemptResults"}, $resultsRows{'Entered'},
  331       CGI::table({-class=>"gwAttemptResults"}, $resultsRows{'Entered'},
  332            $resultsRows{'Preview'}, $resultsRows{'Correct'},
  333            $resultsRows{'Results'}, $resultsRows{'Messages'}) .
  334       ($showSummary ? CGI::p({class=>'emphasis'},$summary) : "");
  335 #   CGI::table({-class=>"attemptResults"}, CGI::Tr(\@tableRows))
  336 #   . ($showSummary ? CGI::p({class=>'emphasis'},$summary) : "");
  337 }
  338 
  339 # *BeginPPM* ###################################################################
  340 # this code taken from Problem.pm; excerpted section ends at *EndPPM*
  341 # modifications are flagged with comments *GW*
  342 
  343 sub viewOptions {
  344   my ($self) = @_;
  345   my $ce = $self->r->ce;
  346 
  347   # don't show options if we don't have anything to show
  348 # return if $self->{invalidSet} or $self->{invalidProblem};
  349 # return unless $self->{isOpen};
  350   return if $self->{invalidSet};
  351 
  352   my $displayMode = $self->{displayMode};
  353   my %must = %{ $self->{must} };
  354   my %can  = %{ $self->{can}  };
  355   my %will = %{ $self->{will} };
  356 
  357   my $optionLine;
  358   $can{showOldAnswers} and $optionLine .= join "",
  359     "Show: &nbsp;".CGI::br(),
  360     CGI::checkbox(
  361       -name    => "showOldAnswers",
  362       -checked => $will{showOldAnswers},
  363       -label   => "Saved answers",
  364     ), "&nbsp;&nbsp;".CGI::br();
  365 
  366   $optionLine and $optionLine .= join "", CGI::br();
  367 
  368   my %display_modes = %{WeBWorK::PG::DISPLAY_MODES()};
  369   my @active_modes = grep { exists $display_modes{$_} }
  370       @{$ce->{pg}->{displayModes}};
  371   my $modeLine = (scalar(@active_modes) > 1) ?
  372     "View&nbsp;equations&nbsp;as:&nbsp;&nbsp;&nbsp;&nbsp;".CGI::br().
  373     CGI::radio_group(
  374       -name    => "displayMode",
  375       -values  => \@active_modes,
  376       -default => $displayMode,
  377       -linebreak=>'true',
  378       -labels  => {
  379         plainText     => "plain",
  380         formattedText => "formatted",
  381         images        => "images",
  382         jsMath        => "jsMath",
  383         asciimath     => "asciimath",
  384       },
  385     ). CGI::br().CGI::hr() : '';
  386 
  387   return CGI::div({-style=>"border: thin groove; padding: 1ex; margin: 2ex align: left"},
  388     $modeLine,
  389     $optionLine,
  390     CGI::submit(-name=>"redisplay", -label=>"Apply Options"),
  391   );
  392 }
  393 
  394 sub previewAnswer {
  395   my ($self, $answerResult, $imgGen) = @_;
  396   my $ce            = $self->r->ce;
  397   my $EffectiveUser = $self->{effectiveUser};
  398   my $set           = $self->{set};
  399   my $problem       = $self->{problem};
  400   my $displayMode   = $self->{displayMode};
  401 
  402   # note: right now, we have to do things completely differently when we are
  403   # rendering math from INSIDE the translator and from OUTSIDE the translator.
  404   # so we'll just deal with each case explicitly here. there's some code
  405   # duplication that can be dealt with later by abstracting out tth/dvipng/etc.
  406 
  407   my $tex = $answerResult->{preview_latex_string};
  408 
  409   return "" unless defined $tex and $tex ne "";
  410 
  411   if ($displayMode eq "plainText") {
  412     return $tex;
  413   } elsif ($displayMode eq "formattedText") {
  414     my $tthCommand = $ce->{externalPrograms}->{tth}
  415       . " -L -f5 -r 2> /dev/null <<END_OF_INPUT; echo > /dev/null\n"
  416       . "\\(".$tex."\\)\n"
  417       . "END_OF_INPUT\n";
  418 
  419     # call tth
  420     my $result = `$tthCommand`;
  421     if ($?) {
  422       return "<b>[tth failed: $? $@]</b>";
  423     } else {
  424       return $result;
  425     }
  426   } elsif ($displayMode eq "images") {
  427     $imgGen->add($tex);
  428   } elsif ($displayMode eq "jsMath") {
  429     return '<DIV CLASS="math">'.$tex.'</DIV>' ;
  430   }
  431 }
  432 
  433 # *EndPPM ######################################################################
  434 
  435 ################################################################################
  436 # Template escape implementations
  437 ################################################################################
  438 
  439 # FIXME need to make $Set and $set be used consistently
  440 
  441 sub pre_header_initialize {
  442     my ($self)     = @_;
  443 
  444     my $r = $self->r;
  445     my $ce = $r->ce;
  446     my $db = $r->db;
  447     my $authz = $r->authz;
  448     my $urlpath = $r->urlpath;
  449 
  450     my $setName = $urlpath->arg("setID");
  451     my $userName = $r->param('user');
  452     my $effectiveUserName = $r->param('effectiveUser');
  453     my $key = $r->param('key');
  454 
  455 # this is a horrible hack to allow use of a javascript link to trigger
  456 # the preview of the page: set previewAnswers to yes if either the
  457 # "previewAnswers" or "previewhack" inputs are set
  458     my $prevOr = $r->param('previewAnswers') || $r->param('previewHack');
  459     $r->param('previewAnswers', $prevOr) if ( defined( $prevOr ) );
  460 
  461     my $User = $db->getUser($userName);
  462     die "record for user $userName (real user) does not exist."
  463   unless defined $User;
  464     my $EffectiveUser = $db->getUser($effectiveUserName);
  465     die "record for user $effectiveUserName (effective user) does not exist."
  466   unless defined $EffectiveUser;
  467 
  468     my $PermissionLevel = $db->getPermissionLevel($userName);
  469     die "permission level record for $userName does not exist (but the " .
  470   "user does? odd...)" unless defined($PermissionLevel);
  471     my $permissionLevel = $PermissionLevel->permission;
  472 
  473 # we could be coming in with $setName = the versioned or nonversioned set
  474 # deal with that first
  475     my $requestedVersion = ( $setName =~ /,v(\d+)$/ ) ? $1 : '';
  476     $setName =~ s/,v\d+$//;
  477 # note that if we're already working with a version we want to be sure to stick
  478 # with that version.  we do this after we've validated that the user is
  479 # assigned the set, below
  480 
  481 ###################################
  482 # gateway content generator tests
  483 ###################################
  484 
  485 # get template set: the non-versioned set that's assigned to the user
  486     my $tmplSet = $db->getMergedSet( $effectiveUserName, $setName );
  487     die( "Set $setName hasn't been assigned to effective user " .
  488    $effectiveUserName ) unless( defined( $tmplSet ) );
  489 
  490 # ok, get the version number if we should be required to stay with a version
  491     $requestedVersion =
  492   $db->getUserSetVersionNumber($effectiveUserName, $setName)
  493   if ( ( $r->param("previewAnswers") || $r->param("checkAnswers") ||
  494          $r->param("submitAnswers") ) && ! $requestedVersion );
  495     die("Requested version 0 when returning to problem?!")
  496   if ( ( $r->param("previewAnswers") || $r->param("checkAnswers") ||
  497          $r->param("submitAnswers") ) && ! $requestedVersion );
  498 
  499 # FIXME should we be more subtle than just die()ing here?  c.f. Problem.pm
  500 #    $self->{invalidSet}  FIXME  (also, if getMergedSet() returns undef for
  501 #    sets not assigned to users, why does Problem.pm resort to the logic
  502 #    (grep /^$setName/, $db->listUserSets($effectiveUserName)) == 0)?
  503     die( "Invalid set $setName requested" )
  504   if ( ! ( $tmplSet->published ||
  505      $authz->hasPermissions($userName,"view_unpublished_sets") ) );
  506 
  507 # if this set isn't a gateway test, we're in the wrong content generator
  508     die("Set $setName isn't a gateway test.  Error in ContentGenerator " .
  509   "call.") if ( ! defined( $tmplSet->assignment_type() ) ||
  510           $tmplSet->assignment_type() !~ /gateway/i );
  511 
  512 # now we know that we're in a gateway test, save the assignment test for
  513 #    the processing of proctor keys for graded proctored tests
  514     $self->{'assignment_type'} = $tmplSet->assignment_type();
  515 
  516 # to test for a proctored test, we need the set version, not the template,
  517 #    which allows for a finished proctored test to be checked as an
  518 #    unproctored test.  so we get the versioned set here
  519     my $set = $db->getMergedVersionedSet($effectiveUserName, $setName,
  520            $requestedVersion);
  521 
  522     unless (defined $set) {
  523   my $userSetClass = $ce->{dbLayout}->{set_user}->{record};
  524   $set = global2user($userSetClass, $db->getGlobalSet($setName));
  525   die "set  $setName  not found."  unless $set;
  526   $set->user_id($effectiveUserName);
  527   $set->psvn('000');
  528   $set->set_id("$setName,v0"); # set to establish the version number only
  529     }
  530     my $setVersionName = $set->set_id();
  531     my ($setVersionNumber) = ($setVersionName =~ /.*,v(\d+)$/);
  532 
  533 # proctor check to be sure that no one is trying to abuse the url path to sneak
  534 #    in the back door on a proctored test
  535 # in the dispatcher we make sure that every call with a proctored url has a
  536 #    valid proctor authentication.  so if we're here either we were called with
  537 #    an unproctored url, or we have a valid proctor authentication.
  538 # this check is to be sure we have a valid proctor authentication for any test
  539 #    that has a proctored assignment type, preventing someone from trying to
  540 #    go to a proctored test with a hacked unproctored URL
  541     if ( ( $requestedVersion && $set->assignment_type() =~ /proctored/i ) ||
  542    ( ! $requestedVersion && $tmplSet->assignment_type() =~ /proctored/i )
  543    ) {
  544 # check against the requested set, if that is the one we're using, or against
  545 #    the template if no version was specified.
  546   die("Set $setName requires a valid proctor login.")
  547       if ( ! WeBWorK::Authen->new($r, $ce, $db)->verifyProctor() );
  548     }
  549 
  550 #################################
  551 # assemble gateway parameters
  552 #################################
  553 
  554 # we get the open/close dates for the gateway from the template set.
  555 # note $isOpen/Closed give the open/close dates for the gateway as a whole
  556     my $isOpen = after($tmplSet->open_date()) ||
  557   $authz->hasPermissions($userName, "view_unopened_sets");
  558 
  559 # FIXME for $isClosed, "record_answers_after_due_date" isn't quite the
  560 #    right description, but it's probably reasonable for our purposes FIXME
  561     my $isClosed = after($tmplSet->due_date()) &&
  562   ! $authz->hasPermissions($userName, "record_answers_after_due_date");
  563 
  564 # to determine if we need a new version, we need to know whether this
  565 #    version exceeds the number of attempts per version.  (among other
  566 #    things,) the number of attempts is a property of the problem, so
  567 #    get a problem to check that.  note that for a gateway/quiz all
  568 #    problems will have the same number of attempts.
  569 # note that this might not be defined, if the set hasn't been versioned
  570 #    to the user yet--this gets fixed when we assign the setVersion
  571     my $Problem =
  572   $db->getMergedVersionedProblem($EffectiveUser->user_id,
  573                $setName, $setVersionName, 1);
  574 
  575 # FIXME: is there any case where $maxAttemptsPerVersion shouldn't be
  576 #    finite?  For the moment we don't deal with this here  FIXME
  577     my $maxAttemptsPerVersion = $tmplSet->attempts_per_version();
  578     my $timeInterval          = $tmplSet->time_interval();
  579     my $versionsPerInterval   = $tmplSet->versions_per_interval();
  580     my $timeLimit             = $tmplSet->version_time_limit();
  581 
  582 # these both work because every problem in the set must have the same
  583 #    submission characteristics
  584     my $currentNumAttempts    = ( defined($Problem) ? $Problem->num_correct() +
  585           $Problem->num_incorrect() : 0 );
  586 
  587 # $maxAttempts turns into the maximum number of versions we can create;
  588 #    if $Problem isn't defined, we can't have made any attempts, so it
  589 #    doesn't matter
  590 # FIXME: I'm using max_attempts == 0, instead of -1; does this matter?
  591     my $maxAttempts           = ( defined($Problem) &&
  592           defined($Problem->max_attempts()) &&
  593           $Problem->max_attempts() != -1 ?
  594           $Problem->max_attempts() : 0 );
  595 
  596 # finding the number of versions per time interval is a little harder.  we
  597 #    interpret the time interval as a rolling interval: that is, if we allow
  598 #    two sets per day, that's two sets in any 24 hour period.  this is
  599 #    probably not what we really want, but it's more extensible to a
  600 #    limitation like "one version per hour", and we can set it to two sets
  601 #    per 12 hours for most "2ce daily" type applications
  602     my $timeNow = time();
  603     my $currentNumVersions = 0;  # this is the number of versions in the last
  604                                  #    time interval
  605     my $totalNumVersions = 0;
  606 
  607     if ( $setVersionNumber ) {
  608   my @setVersions = $db->getUserSetVersions($effectiveUserName,$setName,
  609               $setVersionNumber);
  610   foreach ( @setVersions ) {
  611       $totalNumVersions++;
  612       $currentNumVersions++
  613     if ( $_->version_creation_time() > ($timeNow - $timeInterval) );
  614   }
  615     }
  616 
  617 ####################################
  618 # new version creation conditional
  619 ####################################
  620 
  621     my $versionIsOpen = 0;  # can we do anything to this version?
  622     $timeNow -= 5;          # be safe with $timeNow
  623 
  624     if ( $isOpen && ! $isClosed ) {  # this makes sense, really
  625 
  626 # if no specific version is requested, we can create a new one if
  627 #    need be
  628   if ( ! $requestedVersion ) {
  629       if (
  630      ( ! $maxAttempts || $totalNumVersions < $maxAttempts )
  631      &&
  632      ( $setVersionNumber == 0 ||
  633        (
  634          ( $currentNumAttempts >= $maxAttemptsPerVersion
  635            ||
  636            $timeNow >= $set->due_date )
  637          &&
  638          ( ! $versionsPerInterval
  639            ||
  640            $currentNumVersions < $versionsPerInterval )
  641        )
  642      )
  643      &&
  644      ( $effectiveUserName eq $userName ||
  645        $authz->hasPermissions($effectiveUserName,
  646         "record_answers_when_acting_as_student") )
  647          ) {
  648 
  649     # assign set, get the right name, version number, etc., and redefine
  650     #    the $set and $Problem we're working with
  651     my $setTmpl = $db->getUserSet($effectiveUserName,$setName);
  652     WeBWorK::ContentGenerator::Instructor::assignSetVersionToUser(
  653         $self, $effectiveUserName, $setTmpl);
  654     $setVersionNumber++;
  655     $setVersionName = "$setName,v$setVersionNumber";
  656     $set = $db->getMergedVersionedSet($userName,$setName,
  657               $setVersionNumber);
  658 
  659     $Problem = $db->getMergedVersionedProblem($userName,$setName,
  660                 $setVersionName,1);
  661     # because we're creating this on the fly, it should be published
  662     $set->published(1);
  663     # set up creation time, open and due dates
  664     $set->version_creation_time( $timeNow );
  665     $set->open_date( $timeNow );
  666     $set->due_date( $timeNow+$timeLimit );
  667     $set->answer_date( $timeNow+$timeLimit );
  668     $set->version_last_attempt_time( 0 );
  669     # put this new info into the database.  note that this means that -all- of
  670     #    the merged information gets put back into the database.  as long as
  671     #    the version doesn't have a long lifespan, this is ok...
  672     $db->putVersionedUserSet( $set );
  673 
  674     # we have a new set version, so it's open
  675     $versionIsOpen = 1;
  676 
  677     # also reset the number of attempts for this set; this will be zero
  678     $currentNumAttempts = $Problem->num_correct() +
  679         $Problem->num_incorrect();
  680 
  681       } elsif ( $maxAttempts && $totalNumVersions > $maxAttempts ) {
  682     $self->{invalidSet} = "No new versions of this assignment " .
  683         "are available,\nbecause you have already taken the " .
  684         "maximum number\nallowed.";
  685 
  686       } elsif ( $currentNumAttempts < $maxAttemptsPerVersion &&
  687           $timeNow < $set->due_date() ) {
  688 
  689     if ( between($set->open_date(), $set->due_date(), $timeNow) ) {
  690         $versionIsOpen = 1;
  691     } else {
  692         $versionIsOpen = 0;  # redundant; default is 0
  693         $self->{invalidSet} = "No new versions of this assignment" .
  694       "are available,\nbecause the set is not open or its" .
  695       "time limit has expired.\n";
  696     }
  697 
  698       } elsif ( $versionsPerInterval &&
  699           ( $currentNumVersions >= $versionsPerInterval ) ) {
  700     $self->{invalidSet} = "You have already taken all available " .
  701         "versions of this\ntest in the current time interval.  " .
  702         "You may take the\ntest again after the time interval " .
  703         "has expired.";
  704 
  705       }
  706 
  707   } else {
  708 # (we're still in the $isOpen && ! $isClosed conditional here)
  709 # if a specific version is requested, then we only check to see if it's open
  710       if (
  711      ( $currentNumAttempts < $maxAttemptsPerVersion )
  712      &&
  713      ( $effectiveUserName eq $userName ||
  714        $authz->hasPermissions($effectiveUserName,
  715         "record_answers_when_acting_as_student") )
  716          ) {
  717     if ( between($set->open_date(), $set->due_date(), $timeNow) ) {
  718         $versionIsOpen = 1;
  719     } else {
  720         $versionIsOpen = 0;  # redundant; default is 0
  721     }
  722       }
  723   }
  724 
  725 # set isn't available.
  726     } elsif ( ! $isOpen ) {
  727   $self->{invalidSet} = "This assignment is not open.";
  728 
  729     } elsif ( ! $requestedVersion ) { # closed set, with attempt at a new one
  730   $self->{invalidSet} = "This set is closed.  No new set versions may " .
  731       "be taken.";
  732     }
  733 
  734 
  735 ####################################
  736 # save problem and user data
  737 ####################################
  738 
  739     my $psvn = $set->psvn();
  740     $self->{set} = $set;
  741     $self->{problem} = $Problem;
  742     $self->{requestedVersion} = $requestedVersion;
  743 
  744     $self->{userName} = $userName;
  745     $self->{effectiveUserName} = $effectiveUserName;
  746     $self->{user} = $User;
  747     $self->{effectiveUser}   = $EffectiveUser;
  748     $self->{permissionLevel} = $permissionLevel;
  749 
  750     $self->{isOpen} = $isOpen;
  751     $self->{isClosed} = $isClosed;
  752     $self->{versionIsOpen} = $versionIsOpen;
  753 
  754 ####################################
  755 # form processing
  756 ####################################
  757 
  758 # *BeginPPM* ###################################################################
  759 
  760   # set options from form fields (see comment at top of file for names)
  761     my $displayMode      = $r->param("displayMode") ||
  762                      $ce->{pg}->{options}->{displayMode};
  763     my $redisplay        = $r->param("redisplay");
  764     my $submitAnswers    = $r->param("submitAnswers");
  765     my $checkAnswers     = $r->param("checkAnswers");
  766     my $previewAnswers   = $r->param("previewAnswers");
  767 
  768     my $formFields = { WeBWorK::Form->new_from_paramable($r)->Vars };
  769 
  770     $self->{displayMode}    = $displayMode;
  771     $self->{redisplay}      = $redisplay;
  772     $self->{submitAnswers}  = $submitAnswers;
  773     $self->{checkAnswers}   = $checkAnswers;
  774     $self->{previewAnswers} = $previewAnswers;
  775     $self->{formFields}     = $formFields;
  776 
  777   # get result and send to message
  778     my $success        = $r->param("sucess");
  779     my $failure        = $r->param("failure");
  780     $self->addbadmessage(CGI::p($failure)) if $failure;
  781     $self->addgoodmessage(CGI::p($success)) if $success;
  782 
  783   # now that we've set all the necessary variables quit out if the set or
  784   #    problem is invalid
  785     return if $self->{invalidSet} || $self->{invalidProblem};
  786 
  787 # *EndPPM* #####################################################################
  788 
  789 ####################################
  790 # permissions
  791 ####################################
  792 
  793 # bail without doing anything if the set isn't yet open for this user
  794     return unless $self->{isOpen};
  795 
  796   # what does the user want to do?
  797     my %want =
  798   (showOldAnswers     => $r->param("showOldAnswers") ||
  799                          $ce->{pg}->{options}->{showOldAnswers},
  800      showCorrectAnswers => $r->param("showCorrectAnswers") ||
  801                          $ce->{pg}->{options}->{showCorrectAnswers},
  802    showHints          => $r->param("showHints") ||
  803                    $ce->{pg}->{options}->{showHints},
  804    showSolutions      => $r->param("showSolutions") ||
  805                    $ce->{pg}->{options}->{showSolutions},
  806    recordAnswers      => $submitAnswers,
  807    checkAnswers       => $checkAnswers,
  808    );
  809 
  810   # are certain options enforced?
  811     my %must =
  812   (showOldAnswers     => 0,
  813    showCorrectAnswers => 0,
  814    showHints          => 0,
  815    showSolutions      => 0,
  816    recordAnswers      => ! $authz->hasPermissions($userName,
  817             "avoid_recording_answers"),
  818    checkAnswers       => 0,
  819    );
  820 
  821   # does the user have permission to use certain options?
  822     my @args = ($User, $PermissionLevel, $EffectiveUser, $set, $Problem );
  823     my $sAns = ( $submitAnswers ? 1 : 0 );
  824     my %can =
  825   (showOldAnswers     => $self->can_showOldAnswers(@args),
  826    showCorrectAnswers => $self->can_showCorrectAnswers(@args, $sAns),
  827    showHints          => $self->can_showHints(@args),
  828    showSolutions      => $self->can_showSolutions(@args, $sAns),
  829    recordAnswers      => $self->can_recordAnswers(@args),
  830    checkAnswers       => $self->can_checkAnswers(@args),
  831    recordAnswersNextTime => $self->can_recordAnswers(@args, $sAns),
  832    checkAnswersNextTime  => $self->can_checkAnswers(@args, $sAns),
  833   );
  834 
  835   # final values for options
  836 #     warn("back - next time, " . $can{recordAnswersNextTime} . "\n");
  837     my %will;
  838     foreach (keys %must) {
  839   $will{$_} = $can{$_} && ($must{$_} || $want{$_}) ;
  840     }
  841 
  842   ##### store fields #####
  843 
  844 ## FIXME: the following is present in Problem.pm, but missing here.  how do we
  845 ##   deal with it in the context of multiple problems with possible hints?
  846 ## ##### fix hint/solution options #####
  847 ## $can{showHints}     &&= $pg->{flags}->{hintExists}
  848 ##                     &&= $pg->{flags}->{showHintLimit}<=$pg->{state}->{num_of_incorrect_ans};
  849 ## $can{showSolutions} &&= $pg->{flags}->{solutionExists};
  850 
  851     $self->{want} = \%want;
  852     $self->{must} = \%must;
  853     $self->{can}  = \%can;
  854     $self->{will} = \%will;
  855 
  856 
  857 ####################################
  858 # process problems
  859 ####################################
  860 
  861     my @problemNumbers = $db->listUserProblems($effectiveUserName,
  862                  $setVersionName);
  863     my @problems = ();
  864     my @pg_results = ();
  865 
  866     foreach my $problemNumber (sort {$a<=>$b } @problemNumbers) {
  867   my $ProblemN = $db->getMergedVersionedProblem($effectiveUserName,
  868                   $setName,
  869                   $setVersionName,
  870                   $problemNumber);
  871 
  872     # sticky answers are set up here
  873   if ( not ( $submitAnswers or $previewAnswers or $checkAnswers )
  874        and $will{showOldAnswers} ) {
  875       my %oldAnswers = decodeAnswers( $ProblemN->last_answer );
  876       $formFields->{$_} = $oldAnswers{$_} foreach ( keys %oldAnswers );
  877   }
  878   push( @problems, $ProblemN );
  879 
  880     # this is the actual translation of each problem.  errors are stored in
  881     #    @{$self->{errors}} in each case
  882   my $pg = $self->getProblemHTML( $self->{effectiveUser}, $setVersionName,
  883           $formFields, $ProblemN );
  884   push(@pg_results, $pg);
  885     }
  886     $self->{ra_problems} = \@problems;
  887     $self->{ra_pg_results}=\@pg_results;
  888 
  889 }
  890 
  891 sub path {
  892     my ( $self, $args ) = @_;
  893 
  894     my $r = $self->{r};
  895     my $setName = $r->urlpath->arg("setID");
  896     my $ce = $self->{ce};
  897     my $root = $ce->{webworkURLs}->{root};
  898     my $courseName = $ce->{courseName};
  899 
  900     return $self->pathMacro( $args, "Home" => "$root",
  901            $courseName => "$root/$courseName",
  902            $setName => "" );
  903 }
  904 
  905 sub nav {
  906     my ($self, $args) = @_;
  907 
  908     my $r = $self->{r};
  909     my $setName = $r->urlpath->arg("setID");
  910     my $ce = $self->{ce};
  911     my $root = $ce->{webworkURLs}->{root};
  912     my $courseName = $ce->{courseName};
  913     my @links = ("Problem Sets" , "$root/$courseName", "navUp");
  914     my $tail = "";
  915 
  916     return $self->navMacro($args, $tail, @links);
  917 }
  918 
  919 sub options {
  920   my ($self) = @_;
  921 
  922   return "" if $self->{invalidProblem};
  923   my $sourceFilePathfield = '';
  924         if($self->r->param("sourceFilePath")) {
  925     $sourceFilePathfield = CGI::hidden(-name => "sourceFilePath",
  926                                                    -value => $self->r->param("sourceFilePath"));
  927   }
  928 
  929   return join("",
  930     CGI::start_form("POST", $self->{r}->uri),
  931     $self->hidden_authen_fields,
  932     $sourceFilePathfield,
  933     CGI::hr(),
  934     CGI::start_div({class=>"viewOptions"}),
  935     $self->viewOptions(),
  936     CGI::end_div(),
  937     CGI::end_form()
  938   );
  939 }
  940 
  941 sub body {
  942     my $self = shift();
  943     my $r = $self->r;
  944     my $ce = $r->ce;
  945     my $db = $r->db;
  946     my $authz = $r->authz;
  947     my $urlpath = $r->urlpath;
  948     my $user = $r->param('user');
  949     my $effectiveUser = $r->param('effectiveUser');
  950 
  951     my $timeNow = time();
  952 
  953 #########################################
  954 # preliminary error checking and output
  955 #########################################
  956 
  957 # basic error checking: is the set actually open?
  958     unless ( $self->{isOpen} ) {
  959   return CGI::div({class=>"ResultsWithError"},
  960       CGI::p("This assignment is not open yet, and " .
  961              "therefore is not yet available"));
  962     }
  963 # if we set the invalid flag, we may want this too
  964     if ($self->{invalidSet}) {
  965 # delete any proctor keys that are floating around
  966   if ( $self->{'assignment_type'} eq 'proctored_gateway' ) {
  967       my $proctorID = $r->param('proctor_user');
  968       eval{ $db->deleteKey( "$effectiveUser,$proctorID" ); };
  969       eval{ $db->deleteKey( "$effectiveUser,$proctorID,g" ); };
  970   }
  971 
  972   return CGI::div({class=>"ResultsWithError"},
  973       CGI::p("The selected problem set (" .
  974              $urlpath->arg("setID") . ") is not a valid set" .
  975              " for $effectiveUser."),
  976       CGI::p("This is because: " . $self->{invalidSet}));
  977     }
  978 
  979     my $set = $self->{set};
  980     my $Problem = $self->{problem};
  981     my $permissionLevel = $self->{permissionLevel};
  982     my $submitAnswers = $self->{submitAnswers};
  983     my $checkAnswers = $self->{checkAnswers};
  984     my $previewAnswers = $self->{previewAnswers};
  985     my %want = %{ $self->{want} };
  986     my %can = %{ $self->{can} };
  987     my %must = %{ $self->{must} };
  988     my %will = %{ $self->{will} };
  989     my @problems = @{ $self->{ra_problems} };
  990     my @pg_results = @{ $self->{ra_pg_results} };
  991     my @pg_errors = @{ $self->{errors} };
  992     my $requestedVersion = $self->{requestedVersion};
  993 
  994     my $setVersionName  = $set->set_id;
  995     my ( $setName ) = ( $setVersionName =~ /(.*),v\d+$/ );
  996     my ( $versionNumber ) = ( $setVersionName =~ /.*,v(\d+)$/ );
  997 
  998 # translation errors -- we use the same output routine as Problem.pm, but
  999 #    play around to allow for errors on multiple translations because we
 1000 #    have an array of problems to deal with.
 1001     if ( @pg_errors ) {
 1002   my $errorNum = 1;
 1003   my ( $message, $context ) = ( '', '' );
 1004   foreach ( @pg_errors ) {
 1005 
 1006       $message .= "$errorNum. " if ( @pg_errors > 1 );
 1007       $message .= $_->{message} . CGI::br() . "\n";
 1008 
 1009       $context .= CGI::p( (@pg_errors > 1 ? "$errorNum." : '') .
 1010         $_->{context} ) . "\n\n" . CGI::hr() . "\n\n";
 1011   }
 1012   return $self->errorOutput( $message, $context );
 1013     }
 1014 
 1015 ####################################
 1016 # answer processing
 1017 ####################################
 1018 
 1019     $WeBWorK::timer->continue("begin answer processing")
 1020   if defined($WeBWorK::timer);
 1021 
 1022     my @scoreRecordedMessage = ('') x scalar(@problems);
 1023 
 1024     if ( $submitAnswers ) {
 1025 
 1026 # if we're submitting answers for a proctored exam, we want to delete
 1027 #    the proctor keys that authorized that grading, so that it isn't possible
 1028 #    to just log in and take another proctored test without getting
 1029 #    reauthorized
 1030   if ( $self->{'assignment_type'} eq 'proctored_gateway' ) {
 1031       my $proctorID = $r->param('proctor_user');
 1032       eval{ $db->deleteKey( "$effectiveUser,$proctorID" ); };
 1033     # we should be more subtle than die()ing, but this is a potentially
 1034     #    big problem
 1035       if ( $@ ) {
 1036     die("ERROR RESETTING PROCTOR KEY: $@\n");
 1037       }
 1038       eval{ $db->deleteKey( "$effectiveUser,$proctorID,g" ); };
 1039       if ( $@ ) {
 1040     die("ERROR RESETTING PROCTOR GRADING KEY: $@\n");
 1041       }
 1042   }
 1043 
 1044   foreach my $i ( 0 .. $#problems ) {  # process each problem in g/w
 1045     # this code is essentially that from Problem.pm
 1046       my $pureProblem = $db->getUserProblem( $problems[$i]->user_id,
 1047                $setVersionName,
 1048                $problems[$i]->problem_id );
 1049     # this should be defined unless it's not assigned yet, in which case
 1050     #    we should have die()ed earlier, but what's an extra conditional
 1051     #    between friends?
 1052       if ( defined( $pureProblem ) ) {
 1053         # store answers in problem for sticky answers later
 1054     my %answersToStore;
 1055     my %answerHash = %{$pg_results[$i]->{answers}};
 1056     $answersToStore{$_} =
 1057         $self->{formFields}->{$_} foreach ( keys %answerHash );
 1058   # check for extra answers that slipped by---e.g. for matrices, and get
 1059         #    them from the original input form
 1060     my @extra_answer_names =
 1061         @{ $pg_results[$i]->{flags}->{KEPT_EXTRA_ANSWERS} };
 1062     $answersToStore{$_} =
 1063         $self->{formFields}->{$_} foreach ( @extra_answer_names );
 1064         # now encode all answers
 1065     my @answer_order =
 1066         ( @{$pg_results[$i]->{flags}->{ANSWER_ENTRY_ORDER}},
 1067           @extra_answer_names );
 1068     my $answerString = encodeAnswers( %answersToStore,
 1069               @answer_order );
 1070         # and store the last answer to the database
 1071     $problems[$i]->last_answer( $answerString );
 1072     $pureProblem->last_answer( $answerString );
 1073     my $versioned = 1;
 1074     $db->putUserProblem( $pureProblem, $versioned );
 1075 
 1076         # next, store the state in the database if that makes sense
 1077     if ( $will{recordAnswers} ) {
 1078   $problems[$i]->status($pg_results[$i]->{state}->{recorded_score});
 1079   $problems[$i]->attempted(1);
 1080   $problems[$i]->num_correct($pg_results[$i]->{state}->{num_of_correct_ans});
 1081   $problems[$i]->num_incorrect($pg_results[$i]->{state}->{num_of_incorrect_ans});
 1082   $pureProblem->status($pg_results[$i]->{state}->{recorded_score});
 1083   $pureProblem->attempted(1);
 1084   $pureProblem->num_correct($pg_results[$i]->{state}->{num_of_correct_ans});
 1085   $pureProblem->num_incorrect($pg_results[$i]->{state}->{num_of_incorrect_ans});
 1086 
 1087                     if ( $db->putUserProblem( $pureProblem, $versioned ) ) {
 1088       $scoreRecordedMessage[$i] = "Your score on this " .
 1089           "problem was recorded.";
 1090         } else {
 1091       $scoreRecordedMessage[$i] = "Your score was not " .
 1092           "recorded because there was a failure in storing " .
 1093           "the problem record to the database.";
 1094         }
 1095             # write the transaction log
 1096                     writeLog( $self->{ce}, "transaction",
 1097             $problems[$i]->problem_id . "\t" .
 1098             $problems[$i]->set_id . "\t" .
 1099             $problems[$i]->user_id . "\t" .
 1100             $problems[$i]->source_file . "\t" .
 1101             $problems[$i]->value . "\t" .
 1102             $problems[$i]->max_attempts . "\t" .
 1103             $problems[$i]->problem_seed . "\t" .
 1104             $problems[$i]->status . "\t" .
 1105             $problems[$i]->attempted . "\t" .
 1106             $problems[$i]->last_answer . "\t" .
 1107             $problems[$i]->num_correct . "\t" .
 1108             $problems[$i]->num_incorrect
 1109           );
 1110                 } else {
 1111 
 1112         if ($self->{isClosed}) {
 1113       $scoreRecordedMessage[$i] = "Your score was not " .
 1114           "recorded because this problem set version is " .
 1115           "not open.";
 1116         } elsif ( $problems[$i]->num_correct +
 1117             $problems[$i]->num_incorrect >=
 1118             $set->attempts_per_version ) {
 1119       $scoreRecordedMessage[$i] = "Your score was not " .
 1120           "recorded because you have no attempts " .
 1121           "remaining on this set version.";
 1122         } elsif ( ! $self->{versionIsOpen} ) {
 1123       $scoreRecordedMessage[$i] = "Your score was not " .
 1124           "recorded because you have exceeded the time " .
 1125           "limit for this test.";
 1126         } else {
 1127       $scoreRecordedMessage[$i] = "Your score was not " .
 1128           "recorded.";
 1129         }
 1130     }
 1131       } else {
 1132 # I don't think this should ever happen, because we die() out of the
 1133 #    pre_header_initialize routine when we have the same situation
 1134     $scoreRecordedMessage[$i] = "Your score was not recorded, " .
 1135         "because this problem set has not been assigned to you.";
 1136       }
 1137         # log student answers
 1138       my $answer_log = $self->{ce}->{courseFiles}->{logs}->{'answer_log'};
 1139 
 1140   # this is carried over from Problem.pm
 1141       if ( defined( $answer_log ) && defined( $pureProblem ) ) {
 1142     if ( $submitAnswers ) {
 1143         my $answerString = '';
 1144         my %answerHash = %{ $pg_results[$i]->{answers} };
 1145             # FIXME fix carried over from Problem.pm for "line 552 error"
 1146 
 1147         foreach ( sort keys %answerHash ) {
 1148       my $student_ans =
 1149           $answerHash{$_}->{original_student_ans} || '';
 1150       $answerString .= $student_ans . "\t";
 1151         }
 1152         $answerString = '' unless defined( $answerString );
 1153 
 1154         writeCourseLog( $self->{ce}, "answer_log",
 1155             join("", '|', $problems[$i]->user_id,
 1156                '|', $problems[$i]->set_id,
 1157                '|', $problems[$i]->problem_id,
 1158                '|', "\t$timeNow\t",
 1159                $answerString),
 1160             );
 1161     }
 1162       }
 1163   } # end loop through problems
 1164 
 1165 # warn("in submitanswers conditional\n");
 1166 
 1167     } # end if submitAnswers conditional
 1168     $WeBWorK::timer->continue("end answer processing")
 1169   if defined( $WeBWorK::timer );
 1170 
 1171 # additional set-level database manipulation: this is all for versioned
 1172 #    sets/gateway tests
 1173 # we want to save the time that a set was submitted, and for proctored
 1174 #    tests we want to reset the assignment type after a set is submitted
 1175 #    for the last time so that it's possible to look at it later without
 1176 #    getting proctor authorization
 1177     if ( ( $submitAnswers && $will{recordAnswers} ) ||
 1178    ( ! $can{recordAnswersNextTime} &&
 1179      $set->assignment_type() eq 'proctored_gateway' ) ) {
 1180 
 1181 # warn("in put set conditional\n");
 1182 
 1183   my $setName = $set->set_id();
 1184 
 1185   if ( $submitAnswers && $will{recordAnswers} ) {
 1186       $set->version_last_attempt_time( $timeNow );
 1187   }
 1188   if ( ! $can{recordAnswersNextTime} &&
 1189        $set->assignment_type() eq 'proctored_gateway' ) {
 1190       $set->assignment_type( 'gateway' );
 1191   }
 1192   $db->putVersionedUserSet( $set );
 1193     }
 1194 
 1195 
 1196 
 1197 ####################################
 1198 # output
 1199 ####################################
 1200 
 1201     my $overallScore = -1;  # is there a total score we should be reporting?
 1202     my $totPossible = 0;
 1203     if ( $submitAnswers ) {
 1204   $overallScore = 0;
 1205   foreach ( @pg_results ) {
 1206       $overallScore += $_->{state}->{recorded_score};
 1207 # FIXME  we need to worry about weight, both for score and total possible
 1208 #     $totPossible += $_->value;
 1209       $totPossible++;
 1210   }
 1211     }
 1212 
 1213     if ( $overallScore > -1 ) {
 1214   my $divClass = '';
 1215   my $ansRecorded = 1;
 1216   my $recdMsg = '';
 1217   foreach ( @scoreRecordedMessage ) {
 1218       if ( $_ ne 'Your score on this problem was recorded.' ) {
 1219     $ansRecorded = 0;
 1220     $recdMsg = $_;
 1221     last;
 1222       }
 1223   }
 1224   if ( $ansRecorded ) {
 1225       $divClass = 'ResultsWithoutError';
 1226       $recdMsg = "Your score on this test was recorded.";
 1227   } else {
 1228       $divClass = 'ResultsWithError';
 1229 # inherit saved value from above
 1230       $recdMsg = "Your score on this test was NOT recorded.  " . $recdMsg;
 1231   }
 1232 
 1233   print CGI::div({class=>"$divClass"},
 1234            CGI::strong("Score on this attempt (test number " .
 1235            "$versionNumber) = " .
 1236            "$overallScore / $totPossible"),
 1237            CGI::br(),
 1238            CGI::strong("$recdMsg")),"\n\n";
 1239     }
 1240 
 1241     if ( ! $can{recordAnswersNextTime} ) {
 1242 # if we can't record answers any more, then we're finished with this set
 1243 #   version.  print the appropriate message to that effect.
 1244   print CGI::start_div({class=>"gwMessage"});
 1245   my $mesg = ( $requestedVersion ) ? '' :
 1246       ", because you have used all available attempts on it or " .
 1247       "because its time limit has expired.\n" .
 1248       "To attempt the set again, please try again after the time " .
 1249       "limit between versions has expired.\n";
 1250   print CGI::p(CGI::strong("Note: this set version (number " .
 1251        "$versionNumber) can no longer be submitted for a" .
 1252        " grade"),"\n",$mesg,"\n",
 1253       "You may, however, check your answers to see what you did" .
 1254       " right or wrong."), "\n\n";
 1255   print CGI::end_div();
 1256 
 1257     } else {
 1258 
 1259 # FIXME: This assumes that there IS a time limit!
 1260 # FIXME: We need to drop this out gracefully if there isn't!
 1261 # set up a timer
 1262   my $timeLeft = $set->due_date() - $timeNow;  # this is in seconds
 1263   print CGI::start_div({class=>"gwTiming"});
 1264   print CGI::startform({-name=>"gwtimer", -method=>"POST",
 1265             -action=>$r->uri}), "\n";
 1266   print CGI::hidden({-name=>"gwpagetimeleft", -value=>$timeLeft}), "\n";
 1267   print CGI::strong("Time Remaining:");
 1268   print CGI::textfield({-name=>'gwtime', -default=>0, -size=>8}),
 1269         CGI::strong("min:sec"), CGI::br(), "\n";
 1270   print CGI::endform();
 1271   if ( $timeLeft < 1 ) {
 1272       print CGI::span({-class=>"resultsWithError"},
 1273           CGI::b("You have less than 1 minute to ",
 1274            "complete this test.\n"));
 1275   }
 1276   print CGI::end_div();
 1277 #       print CGI::strong("Time Remaining:
 1278 #                         scalar(localtime($set->open_date())),
 1279 #                         CGI::br(),"\nTime limit : ",
 1280 #                         ($set->version_time_limit()/60),
 1281 #                         " minutes (must be completed by: ",
 1282 #                         scalar(localtime($set->due_date())), ")", CGI::br(),
 1283 #                         "The current time is ", scalar(localtime())), "\n\n";
 1284     }
 1285 
 1286 # this is a brutal hack to get a URL that won't require a proctor login if
 1287 # we've submitted a proctored test for the last time.  above we've reset the
 1288 # assignment_type in this case, so we'll use that to decide if we should
 1289 # give a path to an unproctored test.  note that this substitution leaves
 1290 # unproctored test URLs unchanged
 1291     my $action = $r->uri();
 1292     $action =~ s/proctored_quiz_mode/quiz_mode/
 1293   if ( $set->assignment_type() eq 'gateway' );
 1294 
 1295     print CGI::startform({-name=>"gwquiz", -method=>"POST", -action=>$action}), $self->hidden_authen_fields,
 1296         $self->hidden_proctor_authen_fields;
 1297 
 1298 # FIXME RETURNTO
 1299 # this is a horrible hack to try and let us use a javascript link to
 1300 # trigger previews
 1301     print CGI::hidden({-name=>'previewHack', -value=>''}), CGI::br();
 1302 # and the text for the link
 1303     my $jsprevlink = 'javascript:document.gwquiz.previewHack.value="1";' .
 1304   'document.gwquiz.submit();';
 1305 
 1306 # some links to easily move between problems
 1307     my $jumpLinks = "Jump to problem: ";
 1308     for my $i ( 0 .. $#pg_results ) {
 1309   my $pn = $i+1;
 1310   $jumpLinks .= "/ " . CGI::a({-href=>".", -onclick=>"jumpTo($pn);return false;"}, "$pn") . " /";
 1311     }
 1312     print CGI::p($jumpLinks,"\n");
 1313 
 1314 # print out problems and attempt results, as appropriate
 1315 # note: args to attemptResults are (self,) $pg, $showAttemptAnswers,
 1316 #    $showCorrectAnswers, $showAttemptResults (and-ed with
 1317 #    $showAttemptAnswers), $showSummary, $showAttemptPreview (or-ed with zero)
 1318     my $problemNumber = 0;
 1319 
 1320 # deal with ordering
 1321     my @probOrder = ( 0 .. $#pg_results );
 1322 
 1323 # there's a routine to do this somewhere, I think...
 1324     if ( defined( $set->problem_randorder ) && $set->problem_randorder ) {
 1325   my @newOrder = ();
 1326 # we need to keep the random order the same each time the set is loaded!
 1327 #    this requires either saving the order in the set definition, or being
 1328 #    sure that the random seed that we use is the same each time the same
 1329 #    set is called.  we'll do the latter by setting the seed to the psvn
 1330 #    of the problem set
 1331   srand( $set->psvn );
 1332   while ( @probOrder ) {
 1333       my $i = int(rand(@probOrder));
 1334       push( @newOrder, $probOrder[$i] );
 1335       splice(@probOrder, $i, 1);
 1336   }
 1337   @probOrder = @newOrder;
 1338     }
 1339 
 1340     foreach my $i ( 0 .. $#pg_results ) {
 1341   my $pg = $pg_results[$probOrder[$i]];
 1342   $problemNumber++;
 1343 
 1344   my $recordMessage = '';
 1345   my $resultsTable = '';
 1346 
 1347   if ($pg->{flags}->{showPartialCorrectAnswers} >= 0 && $submitAnswers) {
 1348       if ( $scoreRecordedMessage[$probOrder[$i]] ne
 1349      "Your score on this problem was recorded." ) {
 1350     $recordMessage = CGI::span({class=>"resultsWithError"},
 1351              "ANSWERS NOT RECORDED --",
 1352              $scoreRecordedMessage[$probOrder[$i]]);
 1353 
 1354       }
 1355       $resultsTable =
 1356     $self->attemptResults($pg, 1, $will{showCorrectAnswers},
 1357               $pg->{flags}->{showPartialCorrectAnswers},
 1358               1, 1);
 1359 
 1360   } elsif ( $checkAnswers ) {
 1361       $recordMessage = CGI::span({class=>"resultsWithError"},
 1362          "ANSWERS ONLY CHECKED -- ",
 1363          "ANSWERS NOT RECORDED");
 1364 
 1365       $resultsTable =
 1366     $self->attemptResults($pg, 1, $will{showCorrectAnswers},
 1367               $pg->{flags}->{showPartialCorrectAnswers},
 1368               1, 1);
 1369 
 1370   } elsif ( $previewAnswers ) {
 1371       $recordMessage = CGI::span({class=>"resultsWithError"},
 1372          "PREVIEW ONLY -- ANSWERS NOT RECORDED");
 1373 
 1374       $resultsTable = $self->attemptResults($pg, 1, 0, 0, 0, 1);
 1375 
 1376   }
 1377 
 1378   print CGI::start_div({class=>"gwProblem"});
 1379   my $i1 = $i+1;
 1380   print CGI::a({-name=>"#$i1"},"");
 1381   print CGI::strong("Problem $problemNumber."), "\n", $recordMessage;
 1382   print CGI::p($pg->{body_text}),
 1383         CGI::p($pg->{result}->{msg} ? CGI::b("Note: ") : "",
 1384          CGI::i($pg->{result}->{msg}));
 1385   print CGI::p({class=>"gwPreview"},
 1386          CGI::a({-href=>"$jsprevlink"}, "preview problems"));
 1387 #   print CGI::end_div();
 1388 
 1389   print $resultsTable if $resultsTable;
 1390 
 1391   print CGI::end_div();
 1392 
 1393   print "\n", CGI::hr(), "\n";
 1394     }
 1395     print CGI::p($jumpLinks, "\n");
 1396 
 1397     if ($can{showCorrectAnswers}) {
 1398   print CGI::checkbox(-name    => "showCorrectAnswers",
 1399           -checked => $will{showCorrectAnswers},
 1400           -label   => "Show correct answers",
 1401           );
 1402     }
 1403 #     if ($can{showHints}) {
 1404 #   print CGI::div({style=>"color:red"},
 1405 #            CGI::checkbox(-name    => "showHints",
 1406 #              -checked => $will{showHints},
 1407 #              -label   => "Show Hints",
 1408 #              )
 1409 #            );
 1410 #     }
 1411     if ($can{showSolutions}) {
 1412   print CGI::checkbox(-name    => "showSolutions",
 1413           -checked => $will{showSolutions},
 1414           -label   => "Show Solutions",
 1415           );
 1416     }
 1417 
 1418     if ($can{showCorrectAnswers} or $can{showHints} or $can{showSolutions}) {
 1419   print CGI::br();
 1420     }
 1421 
 1422 # Note: because of the way these things are grouped, the submit/et al buttons
 1423 # in this form are getting put outside of the problem div, while on a regular
 1424 # problem they'd fall inside.  Does this matter?  We shall see.
 1425     print CGI::p( CGI::submit( -name=>"previewAnswers",
 1426              -label=>"Preview Answers" ),
 1427       ($can{recordAnswersNextTime} ?
 1428           CGI::submit( -name=>"submitAnswers",
 1429            -label=>"Grade Gateway" ) : " "),
 1430       ($can{checkAnswersNextTime} && ! $can{recordAnswersNextTime} ?
 1431           CGI::submit( -name=>"checkAnswers",
 1432            -label=>"Check Answers" ) : " ") );
 1433 
 1434     print CGI::endform();
 1435 
 1436 # debugging verbiage
 1437 #     if ( $can{checkAnswersNextTime} ) {
 1438 #   print "Can check answers next time\n";
 1439 #     } else {
 1440 #   print "Can NOT check answers next time\n";
 1441 #     }
 1442 #     if ( $can{recordAnswersNextTime} ) {
 1443 #   print "Can record answers next time\n";
 1444 #     } else {
 1445 #   print "Can NOT record answers next time\n";
 1446 #     }
 1447 
 1448   # we exclude the feedback form from gateway tests.  they can use the feedback
 1449   #   button on the preceding or following pages
 1450 #     my $ce = $r->ce;
 1451 #     my $root = $ce->{webworkURLs}->{root};
 1452 #     my $courseName = $ce->{courseName};
 1453 #     my $feedbackURL = "$root/$courseName/feedback/";
 1454 #     print CGI::startform("POST", $feedbackURL),
 1455 #           $self->hidden_authen_fields,
 1456 #           CGI::hidden("module", __PACKAGE__),
 1457 #           CGI::hidden("set",    $self->{set}->set_id),
 1458 #           CGI::p({-align=>"right"},
 1459 #      CGI::submit(-name=>"feedbackForm", -label=>"Send Feedback")
 1460 #      ),
 1461 #     CGI::endform();
 1462 
 1463     return "";
 1464 
 1465 }
 1466 
 1467 
 1468 ###########################################################################
 1469 # Evaluation utilities
 1470 ############################################################################
 1471 
 1472 sub getProblemHTML {
 1473     my ( $self, $EffectiveUser, $setVersionName, $formFields,
 1474    $mergedProblem, $pgFile ) = @_;
 1475 # in:  $EffectiveUser is the effective user we're working as, $setVersionName
 1476 #      the versioned set name (setID,vN), %$formFields the form fields from
 1477 #      the input form that we need to worry about putting into the HTML we're
 1478 #      generating, and $mergedProblem and $pgFile are what we'd expect.
 1479 #      $pgFile is optional
 1480 # out: the translated problem is returned
 1481 
 1482     my $r = $self->r;
 1483     my $ce = $r->ce;
 1484     my $db = $r->db;
 1485     my $key =  $r->param('key');
 1486 
 1487 # this isn't good because it doesn't include the sticky answers that we
 1488 #    might want.  so off with its head!
 1489 ##    my $formFields = { WeBWorK::Form->new_from_paramable($r)->Vars };
 1490 
 1491     my $permissionLevel = $self->{permissionLevel};
 1492     my $set  = $db->getMergedVersionedSet( $EffectiveUser->user_id,
 1493              $setVersionName );
 1494 
 1495 # should this ever happen?  I think we should have die()ed way earlier than
 1496 #    this if the set doesn't exist, but it can't hurt to try and die() here
 1497 #    too
 1498     die "set $setVersionName for effectiveUser " . $EffectiveUser->user_id .
 1499   " not found." unless $set;
 1500 
 1501     my $psvn = $set->psvn();
 1502     my ($setName) = ($setVersionName =~ /^(.*),v\d+/);
 1503 
 1504     if ( defined($mergedProblem) && $mergedProblem->problem_id ) {
 1505 # nothing needs to be done
 1506 
 1507     } elsif ($pgFile) {
 1508   $mergedProblem =
 1509       WeBWorK::DB::Record::UserProblem->new(
 1510       set_id => $set->set_id,
 1511       problem_id => 0,
 1512       login_id => $EffectiveUser->user_id,
 1513       source_file => $pgFile,
 1514       # the rest of Problem's fields are not needed, i think
 1515     );
 1516     }
 1517 # figure out if we're allowed to get solutions and call PG->new accordingly.
 1518     my $showCorrectAnswers = $self->{will}->{showCorrectAnswers};
 1519     my $showHints          = $self->{will}->{showHints};
 1520     my $showSolutions      = $self->{will}->{showSolutions};
 1521     my $processAnswers     = $self->{will}->{checkAnswers};
 1522 
 1523 # FIXME  I'm not sure that problem_id is what we want here  FIXME
 1524     my $problemNumber = $mergedProblem->problem_id;
 1525 
 1526     my $pg =
 1527   WeBWorK::PG->new(
 1528        $ce,
 1529        $EffectiveUser,
 1530        $key,
 1531        $set,
 1532        $mergedProblem,
 1533        $psvn,
 1534        $formFields,
 1535        { # translation options
 1536            displayMode     => $self->{displayMode},
 1537            showHints       => $showHints,
 1538            showSolutions   => $showSolutions,
 1539            refreshMath2img => $showHints || $showSolutions,
 1540            processAnswers  => 1,
 1541            QUIZ_PREFIX     => 'Q' .
 1542          sprintf("%04d",$problemNumber) . '_',
 1543            },
 1544        );
 1545 
 1546 # FIXME  is problem_id the correct thing in the following two stanzas?
 1547 # FIXME  the original version had "problem number", which is what we want.
 1548 # FIXME  I think problem_id will work, too
 1549     if ($pg->{warnings} ne "") {
 1550   push @{$self->{warnings}}, {
 1551       set     => $setVersionName,
 1552       problem => $mergedProblem->problem_id,
 1553       message => $pg->{warnings},
 1554   };
 1555     }
 1556 
 1557     $self->{errors} = [];  # initialize this to no errors
 1558     if ($pg->{flags}->{error_flag}) {
 1559   push @{$self->{errors}}, {
 1560       set     => $setVersionName,
 1561       problem => $mergedProblem->problem_id,
 1562       message => $pg->{errors},
 1563       context => $pg->{body_text},
 1564   };
 1565   # if there was an error, body_text contains
 1566   # the error context, not TeX code
 1567   $pg->{body_text} = undef;
 1568     }
 1569 
 1570     return    $pg;
 1571 }
 1572 
 1573 ##### output utilities #####
 1574 sub problemListRow($$$) {
 1575   my $self = shift;
 1576   my $set = shift;
 1577   my $Problem = shift;
 1578 
 1579   my $name = $Problem->problem_id;
 1580   my $interactiveURL = "$name/?" . $self->url_authen_args;
 1581   my $interactive = CGI::a({-href=>$interactiveURL}, "Problem $name");
 1582   my $attempts = $Problem->num_correct + $Problem->num_incorrect;
 1583   my $remaining = $Problem->max_attempts < 0
 1584     ? "unlimited"
 1585     : $Problem->max_attempts - $attempts;
 1586   my $status = sprintf("%.0f%%", $Problem->status * 100); # round to whole number
 1587 
 1588   return CGI::Tr(CGI::td({-nowrap=>1}, [
 1589     $interactive,
 1590     $attempts,
 1591     $remaining,
 1592     $status,
 1593   ]));
 1594 }
 1595 # sub nbsp {
 1596 #   my $str = shift;
 1597 #   ($str) ? $str : '&nbsp;';  # returns non-breaking space for empty strings
 1598 # }
 1599 
 1600 ##### logging subroutine ####
 1601 
 1602 
 1603 
 1604 
 1605 1;

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9