[system] / branches / rel-2-4-patches / webwork-modperl / lib / WeBWorK / ContentGenerator / GatewayQuiz.pm Repository:
ViewVC logotype

View of /branches/rel-2-4-patches/webwork-modperl/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 3485 - (download) (as text) (annotate)
Fri Aug 12 02:47:30 2005 UTC (7 years, 9 months ago) by sh002i
Original Path: trunk/webwork-modperl/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm
File size: 59889 byte(s)
added HiRes timing data to WeBWorK::Debug, removed WeBWorK::Timing. all
existing calls to the WeBWorK::Timing methods now pass the same messages
to debug().

added an option to WeBWorK::Debug to allow only certain subroutines to
log debug messages, in addition to the existing option to bar certain
subroutines from doing so.

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

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9