[system] / trunk / webwork2 / lib / WeBWorK / ContentGenerator / GatewayQuiz.pm Repository:
ViewVC logotype

View of /trunk/webwork2/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 4202 - (download) (as text) (annotate)
Sat Jul 8 14:07:35 2006 UTC (6 years, 10 months ago) by gage
File size: 63040 byte(s)
Try using the -nosticky pragma to see if this fixes the problem.

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

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9