[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 7041 - (download) (as text) (annotate)
Sat Sep 10 02:39:37 2011 UTC (20 months, 1 week ago) by gage
File size: 87686 byte(s)
May have fixed problem where gateway quiz labels don't show up.

It may be that the interactive link should also be wrapped in a label.
  

    1 ################################################################################
    2 # WeBWorK Online Homework Delivery System
    3 # Copyright © 2000-2007 The WeBWorK Project, http://openwebwork.sf.net/
    4 # $CVSHeader: webwork2/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm,v 1.54 2008/07/01 13:12:56 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(-nosticky );
   30 use WeBWorK::CGI;
   31 use File::Path qw(rmtree);
   32 use WeBWorK::Form;
   33 use WeBWorK::PG;
   34 use WeBWorK::PG::ImageGenerator;
   35 use WeBWorK::PG::IO;
   36 use WeBWorK::Utils qw(writeLog writeCourseLog encodeAnswers decodeAnswers
   37   ref2string makeTempDirectory path_is_subdir sortByName before after
   38   between);  # use the ContentGenerator formatDateTime, not the version in Utils
   39 use WeBWorK::DB::Utils qw(global2user user2global);
   40 use WeBWorK::Utils::Tasks qw(fake_set fake_set_version fake_problem);
   41 use WeBWorK::Debug;
   42 use WeBWorK::ContentGenerator::Instructor qw(assignSetVersionToUser);
   43 use PGrandom;
   44 
   45 # template method
   46 sub templateName {
   47   return "gateway";
   48 }
   49 
   50 
   51 ################################################################################
   52 # "can" methods
   53 ################################################################################
   54 
   55 # Subroutines to determine if a user "can" perform an action. Each subroutine is
   56 # called with the following arguments:
   57 #
   58 #     ($self, $User, $PermissionLevel, $EffectiveUser, $Set, $Problem)
   59 
   60 # *** The "can" routines are taken from Problem.pm, with small modifications
   61 # *** to look at number of attempts per version, not per set, and to allow
   62 # *** showing of correct answers after all attempts at a version are used
   63 
   64 sub can_showOldAnswers {
   65   my ($self, $User, $PermissionLevel, $EffectiveUser, $Set, $Problem, $tmplSet ) = @_;
   66   my $authz = $self->r->authz;
   67 # we'd like to use "! $Set->hide_work()", but that hides students' work
   68 # as they're working on the set, which isn't quite right.  so use instead:
   69   return( before( $Set->due_date() ) ||
   70 
   71     $authz->hasPermissions($User->user_id,"view_hidden_work") ||
   72     ( $Set->hide_work() eq 'N' ||
   73       ( $Set->hide_work() eq 'BeforeAnswerDate' && time > $tmplSet->answer_date ) ) );
   74 }
   75 
   76 # gateway change here: add $submitAnswers as an optional additional argument
   77 #   to be included if it's defined
   78 sub can_showCorrectAnswers {
   79   my ($self, $User, $PermissionLevel, $EffectiveUser, $Set, $Problem,
   80       $tmplSet, $submitAnswers) = @_;
   81   my $authz = $self->r->authz;
   82 
   83 # gateway change here to allow correct answers to be viewed after all attempts
   84 #   at a version are exhausted as well as if it's after the answer date
   85 # $addOne allows us to count the current submission
   86   my $addOne = defined( $submitAnswers ) ? $submitAnswers : 0;
   87   my $maxAttempts = $Set->attempts_per_version() || 0;
   88   my $attemptsUsed = $Problem->num_correct + $Problem->num_incorrect +
   89       $addOne;
   90 
   91 # this is complicated by trying to address hiding scores by problem---that
   92 #    is, if $set->hide_score_by_problem and $set->hide_score are both set,
   93 #    then we should allow scores to be shown, but not show the score on
   94 #    any individual problem.  to deal with this, we make
   95 #    can_showCorrectAnswers give the least restrictive view of hiding, and
   96 #    then filter scores for the problems themselves later
   97   my $canShowScores = ( $Set->hide_score eq 'N' ||
   98             $Set->hide_score_by_problem eq 'Y' ||
   99             ( $Set->hide_score eq 'BeforeAnswerDate' &&
  100         after($tmplSet->answer_date) ) );
  101 
  102   return ( ( ( after( $Set->answer_date ) ||
  103          ( $attemptsUsed >= $maxAttempts &&
  104            $Set->due_date() == $Set->answer_date() ) ) ||
  105        $authz->hasPermissions($User->user_id,
  106         "show_correct_answers_before_answer_date") ) &&
  107      ( $authz->hasPermissions($User->user_id, "view_hidden_work") ||
  108        $canShowScores ) );
  109 }
  110 
  111 sub can_showHints {
  112   #my ($self, $User, $PermissionLevel, $EffectiveUser, $Set, $Problem) = @_;
  113 
  114   return 1;
  115 }
  116 
  117 # gateway change here: add $submitAnswers as an optional additional argument
  118 #   to be included if it's defined
  119 sub can_showSolutions {
  120   my ($self, $User, $PermissionLevel, $EffectiveUser, $Set, $Problem,
  121       $tmplSet, $submitAnswers) = @_;
  122   my $authz = $self->r->authz;
  123 
  124 # this is the same as can_showCorrectAnswers
  125 # gateway change here to allow correct answers to be viewed after all attempts
  126 #   at a version are exhausted as well as if it's after the answer date
  127 # $addOne allows us to count the current submission
  128   my $addOne = defined( $submitAnswers ) ? $submitAnswers : 0;
  129   my $maxAttempts = $Set->attempts_per_version()||1;
  130   my $attemptsUsed = $Problem->num_correct+$Problem->num_incorrect+$addOne;
  131 
  132 # this is complicated by trying to address hiding scores by problem---that
  133 #    is, if $set->hide_score_by_problem and $set->hide_score are both set,
  134 #    then we should allow scores to be shown, but not show the score on
  135 #    any individual problem.  to deal with this, we make can_showSolutions
  136 #    give the least restrictive view of hiding, and then filter scores for
  137 #    the problems themselves later
  138   my $canShowScores = ( $Set->hide_score eq 'N' ||
  139             $Set->hide_score_by_problem eq 'Y' ||
  140             ( $Set->hide_score eq 'BeforeAnswerDate' &&
  141         after($tmplSet->answer_date) ) );
  142 
  143   return ( ( ( after( $Set->answer_date ) ||
  144          ( $attemptsUsed >= $maxAttempts &&
  145            $Set->due_date() == $Set->answer_date() ) ) ||
  146        $authz->hasPermissions($User->user_id,
  147         "show_correct_answers_before_answer_date") ) &&
  148      ( $authz->hasPermissions($User->user_id, "view_hidden_work") ||
  149        $canShowScores ) );
  150 }
  151 
  152 # gateway change here: add $submitAnswers as an optional additional argument
  153 #   to be included if it's defined
  154 # we also allow for a version_last_attempt_time which is the time the set was
  155 #   submitted; if that's present we use that instead of the current time to
  156 #   decide if we can record the answers.  this deals with the time between the
  157 #   submission time and the proctor authorization.
  158 sub can_recordAnswers {
  159   my ($self, $User, $PermissionLevel, $EffectiveUser, $Set, $Problem,
  160       $tmplSet, $submitAnswers) = @_;
  161   my $authz = $self->r->authz;
  162 
  163 # easy first case: never record answers for undefined sets
  164   return 0 if ( $Set->set_id eq "Undefined_Set" );
  165 
  166   my $timeNow = ( defined($self->{timeNow}) ) ? $self->{timeNow} : time();
  167    # get the sag time after the due date in which we'll still grade the test
  168   my $grace = $self->{ce}->{gatewayGracePeriod};
  169 
  170   my $submitTime = ( defined($Set->version_last_attempt_time()) &&
  171          $Set->version_last_attempt_time() ) ?
  172          $Set->version_last_attempt_time() : $timeNow;
  173 
  174   if ($User->user_id ne $EffectiveUser->user_id) {
  175     my $recordAsOther = $authz->hasPermissions($User->user_id, "record_answers_when_acting_as_student");
  176     my $recordVersionsAsOther = $authz->hasPermissions($User->user_id, "record_set_version_answers_when_acting_as_student");
  177 
  178     if ( $recordAsOther ) {
  179       return $recordAsOther;
  180     } elsif ( ! $recordVersionsAsOther ) {
  181       return $recordVersionsAsOther;
  182     }
  183     ## if we're not allowed to record answers as another user,
  184     ##    return that permission.  if we're allowed to record
  185     ##    only set version answers, then we allow that between
  186     ##    the open and close dates, and so drop out of this
  187     ##    conditional to the usual one.
  188     ## it isn't clear if this is the correct behavior, but I
  189     ##    think it's probably reasonable.
  190   }
  191 
  192   if (before($Set->open_date, $submitTime)) {
  193     #    warn("case 0\n");
  194     return $authz->hasPermissions($User->user_id, "record_answers_before_open_date");
  195   } elsif (between($Set->open_date, ($Set->due_date + $grace), $submitTime)) {
  196 
  197 # gateway change here; we look at maximum attempts per version, not for the set,
  198 #   to determine the number of attempts allowed
  199 # $addOne allows us to count the current submission
  200       my $addOne = ( defined( $submitAnswers ) && $submitAnswers ) ?
  201     1 : 0;
  202       my $max_attempts = $Set->attempts_per_version() || 0;
  203       my $attempts_used = $Problem->num_correct+$Problem->num_incorrect+$addOne;
  204     if ($max_attempts == -1 or $attempts_used < $max_attempts) {
  205       return $authz->hasPermissions($User->user_id, "record_answers_after_open_date_with_attempts");
  206     } else {
  207       return $authz->hasPermissions($User->user_id, "record_answers_after_open_date_without_attempts");
  208     }
  209   } elsif (between(($Set->due_date + $grace), $Set->answer_date, $submitTime)) {
  210     return $authz->hasPermissions($User->user_id, "record_answers_after_due_date");
  211   } elsif (after($Set->answer_date, $submitTime)) {
  212     return $authz->hasPermissions($User->user_id, "record_answers_after_answer_date");
  213   }
  214 }
  215 
  216 # gateway change here: add $submitAnswers as an optional additional argument
  217 #   to be included if it's defined
  218 # we also allow for a version_last_attempt_time which is the time the set was
  219 #   submitted; if that's present we use that instead of the current time to
  220 #   decide if we can check the answers.  this deals with the time between the
  221 #   submission time and the proctor authorization.
  222 sub can_checkAnswers {
  223   my ($self, $User, $PermissionLevel, $EffectiveUser, $Set, $Problem,
  224       $tmplSet, $submitAnswers) = @_;
  225   my $authz = $self->r->authz;
  226 
  227   my $timeNow = ( defined($self->{timeNow}) ) ? $self->{timeNow} : time();
  228    # get the sag time after the due date in which we'll still grade the test
  229   my $grace = $self->{ce}->{gatewayGracePeriod};
  230 
  231   my $submitTime = ( defined($Set->version_last_attempt_time()) &&
  232          $Set->version_last_attempt_time() ) ?
  233          $Set->version_last_attempt_time() : $timeNow;
  234 
  235   # this is further complicated by trying to address hiding scores by
  236   #    problem---that is, if $set->hide_score_by_problem and
  237   #    $set->hide_score are both set, then we should allow scores to
  238   #    be shown, but not show the score on any individual problem.
  239   #    to deal with this, we use the least restrictive view of hiding
  240   #    here, and then filter for the problems themselves later
  241   my $canShowScores = ( $Set->hide_score eq 'N' ||
  242             $Set->hide_score_by_problem eq 'Y' ||
  243             ( $Set->hide_score eq 'BeforeAnswerDate' &&
  244         after($tmplSet->answer_date) ) );
  245 
  246   if (before($Set->open_date, $submitTime)) {
  247     return $authz->hasPermissions($User->user_id, "check_answers_before_open_date");
  248   } elsif (between($Set->open_date, ($Set->due_date + $grace), $submitTime)) {
  249 
  250 # gateway change here; we look at maximum attempts per version, not for the set,
  251 #   to determine the number of attempts allowed
  252 # $addOne allows us to count the current submission
  253       my $addOne = (defined( $submitAnswers ) && $submitAnswers) ?
  254     1 : 0;
  255       my $max_attempts = $Set->attempts_per_version()||1;
  256       my $attempts_used = $Problem->num_correct+$Problem->num_incorrect+$addOne;
  257 
  258     if ($max_attempts == -1 or $attempts_used < $max_attempts) {
  259       return ( $authz->hasPermissions($User->user_id, "check_answers_after_open_date_with_attempts") &&
  260          ( $authz->hasPermissions($User->user_id, "view_hidden_work") ||
  261            $canShowScores ) );
  262     } else {
  263       return ( $authz->hasPermissions($User->user_id, "check_answers_after_open_date_without_attempts") &&
  264          ( $authz->hasPermissions($User->user_id, "view_hidden_work") ||
  265            $canShowScores ) );
  266     }
  267   } elsif (between(($Set->due_date + $grace), $Set->answer_date, $submitTime)) {
  268     return ( $authz->hasPermissions($User->user_id, "check_answers_after_due_date")  &&
  269        ( $authz->hasPermissions($User->user_id, "view_hidden_work") ||
  270          $canShowScores ) );
  271   } elsif (after($Set->answer_date, $submitTime)) {
  272     return ( $authz->hasPermissions($User->user_id, "check_answers_after_answer_date") &&
  273        ( $authz->hasPermissions($User->user_id, "view_hidden_work") ||
  274          $canShowScores ) );
  275   }
  276 }
  277 
  278 sub can_showScore {
  279   my ($self, $User, $PermissionLevel, $EffectiveUser, $Set, $Problem,
  280       $tmplSet, $submitAnswers) = @_;
  281   my $authz = $self->r->authz;
  282 
  283   my $timeNow = ( defined($self->{timeNow}) ) ? $self->{timeNow} : time();
  284 
  285   # address hiding scores by problem
  286   my $canShowScores = ( $Set->hide_score eq 'N' ||
  287             $Set->hide_score_by_problem eq 'Y' ||
  288             ( $Set->hide_score eq 'BeforeAnswerDate' &&
  289         after($tmplSet->answer_date) ) );
  290 
  291   return( $authz->hasPermissions($User->user_id,"view_hidden_work") ||
  292     $canShowScores );
  293 }
  294 
  295 ################################################################################
  296 # output utilities
  297 ################################################################################
  298 
  299 # subroutine is modified from that in Problem.pm to produce a different
  300 #    table format
  301 sub attemptResults {
  302   my $self = shift;
  303   my $pg = shift;
  304   my $showAttemptAnswers = shift;
  305   my $showCorrectAnswers = shift;
  306   my $showAttemptResults = $showAttemptAnswers && shift;
  307   my $showSummary = shift;
  308   my $showAttemptPreview = shift || 0;
  309 
  310   my $r = $self->{r};
  311   my $setName = $r->urlpath->arg("setID");
  312   my $ce = $self->{ce};
  313   my $root = $ce->{webworkURLs}->{root};
  314   my $courseName = $ce->{courseName};
  315   my @links = ("Homework Sets" , "$root/$courseName", "navUp");
  316   my $tail = "";
  317 
  318   my $problemResult = $pg->{result}; # the overall result of the problem
  319   my @answerNames = @{ $pg->{flags}->{ANSWER_ENTRY_ORDER} };
  320 
  321   my $showMessages = $showAttemptAnswers && grep { $pg->{answers}->{$_}->{ans_message} } @answerNames;
  322 
  323   # present in ver 1.10; why is this checked here?
  324   # return CGI::p(CGI::font({-color=>"red"}, "This problem is not available because the homework set that contains it is not yet open."))
  325   # unless $self->{isOpen};
  326 
  327   my $basename = "equation-" . $self->{set}->psvn. "." . $self->{problem}->problem_id . "-preview";
  328 
  329   # to make grabbing these options easier, we'll pull them out now...
  330   my %imagesModeOptions = %{$ce->{pg}->{displayModeOptions}->{images}};
  331 
  332   my $imgGen = WeBWorK::PG::ImageGenerator->new(
  333     tempDir         => $ce->{webworkDirs}->{tmp},
  334     latex         => $ce->{externalPrograms}->{latex},
  335     dvipng          => $ce->{externalPrograms}->{dvipng},
  336     useCache        => 1,
  337     cacheDir        => $ce->{webworkDirs}->{equationCache},
  338     cacheURL        => $ce->{webworkURLs}->{equationCache},
  339     cacheDB         => $ce->{webworkFiles}->{equationCacheDB},
  340     dvipng_align    => $imagesModeOptions{dvipng_align},
  341     dvipng_depth_db => $imagesModeOptions{dvipng_depth_db},
  342   );
  343 
  344   my %resultsData = ();
  345   $resultsData{'Entered'}  = CGI::td({-class=>"label"}, "Your answer parses as:");
  346   $resultsData{'Preview'}  = CGI::td({-class=>"label"}, "Your answer previews as:");
  347   $resultsData{'Correct'}  = CGI::td({-class=>"label"}, "The correct answer is:");
  348   $resultsData{'Results'}  = CGI::td({-class=>"label"}, "Result:");
  349   $resultsData{'Messages'} = CGI::td({-class=>"label"}, "Messages:");
  350 
  351   my %resultsRows = ();
  352   foreach ( qw( Entered Preview Correct Results Messages ) ) {
  353       $resultsRows{$_} = "";
  354   }
  355 
  356   my $numCorrect = 0;
  357   my $numAns = 0;
  358   foreach my $name (@answerNames) {
  359     my $answerResult  = $pg->{answers}->{$name};
  360     my $studentAnswer = $answerResult->{student_ans}; # original_student_ans
  361     my $preview       = ($showAttemptPreview
  362                           ? $self->previewAnswer($answerResult, $imgGen)
  363                           : "");
  364     my $correctAnswer = $answerResult->{correct_ans};
  365     my $answerScore   = $answerResult->{score};
  366     my $answerMessage = $showMessages ? $answerResult->{ans_message} : "";
  367     #FIXME  --Can we be sure that $answerScore is an integer-- could the problem give partial credit?
  368     $numCorrect += $answerScore > 0;
  369     my $resultString = $answerScore == 1 ? "correct" : "incorrect";
  370 
  371     # get rid of the goofy prefix on the answer names (supposedly, the format
  372     # of the answer names is changeable. this only fixes it for "AnSwEr"
  373     #$name =~ s/^AnSwEr//;
  374 
  375     my $pre = $numAns ? CGI::td("&nbsp;") : "";
  376 
  377     $resultsRows{'Entered'} .= $showAttemptAnswers ?
  378         CGI::Tr( $pre . $resultsData{'Entered'} .
  379            CGI::td({-class=>"output"}, $self->nbsp($studentAnswer))) : "";
  380     $resultsData{'Entered'} = '';
  381     $resultsRows{'Preview'} .= $showAttemptPreview ?
  382         CGI::Tr( $pre . $resultsData{'Preview'} .
  383            CGI::td({-class=>"output"}, $self->nbsp($preview)) ) : "";
  384     $resultsData{'Preview'} = '';
  385     $resultsRows{'Correct'} .= $showCorrectAnswers ?
  386         CGI::Tr( $pre . $resultsData{'Correct'} .
  387            CGI::td({-class=>"output"}, $self->nbsp($correctAnswer)) ) : "";
  388     $resultsData{'Correct'} = '';
  389     $resultsRows{'Results'} .= $showAttemptResults ?
  390         CGI::Tr( $pre . $resultsData{'Results'} .
  391            CGI::td({-class=>"output"}, $self->nbsp($resultString)) )  : "";
  392     $resultsData{'Results'} = '';
  393     $resultsRows{'Messages'} .= $showMessages ?
  394         CGI::Tr( $pre . $resultsData{'Messages'} .
  395            CGI::td({-class=>"output"}, $self->nbsp($answerMessage)) ) : "";
  396 
  397     $numAns++;
  398   }
  399 
  400   # render equation images
  401   $imgGen->render(refresh => 1);
  402 
  403 # my $numIncorrectNoun = scalar @answerNames == 1 ? "question" : "questions";
  404   my $scorePercent = sprintf("%.0f%%", $problemResult->{score} * 100);
  405 #   FIXME  -- I left the old code in in case we have to back out.
  406 # my $summary = "On this attempt, you answered $numCorrect out of "
  407 #   . scalar @answerNames . " $numIncorrectNoun correct, for a score of $scorePercent.";
  408 
  409   my $summary = "";
  410   if (scalar @answerNames == 1) {
  411       if ($numCorrect == scalar @answerNames) {
  412         $summary .= CGI::div({class=>"gwCorrect"},"This answer is correct.");
  413        } else {
  414          $summary .= CGI::div({class=>"gwIncorrect"},"This answer is NOT correct.");
  415        }
  416   } else {
  417       if ($numCorrect == scalar @answerNames) {
  418         $summary .= CGI::div({class=>"gwCorrect"},"All of these answers are correct.");
  419        } else {
  420          $summary .= CGI::div({class=>"gwIncorrect"},"At least one of these answers is NOT correct.");
  421        }
  422   }
  423 
  424   return
  425 #     CGI::table({-class=>"attemptResults"}, $resultsRows{'Entered'},
  426       CGI::table({-class=>"gwAttemptResults"}, $resultsRows{'Entered'},
  427            $resultsRows{'Preview'}, $resultsRows{'Correct'},
  428            $resultsRows{'Results'}, $resultsRows{'Messages'}) .
  429       ($showSummary ? CGI::p({class=>'attemptResultsSummary'},$summary) : "");
  430 #   CGI::table({-class=>"attemptResults"}, CGI::Tr(\@tableRows))
  431 #   . ($showSummary ? CGI::p({class=>'emphasis'},$summary) : "");
  432 }
  433 
  434 # *BeginPPM* ###################################################################
  435 # this code taken from Problem.pm; excerpted section ends at *EndPPM*
  436 # modifications are flagged with comments *GW*
  437 
  438 sub previewAnswer {
  439   my ($self, $answerResult, $imgGen) = @_;
  440   my $ce            = $self->r->ce;
  441   my $EffectiveUser = $self->{effectiveUser};
  442   my $set           = $self->{set};
  443   my $problem       = $self->{problem};
  444   my $displayMode   = $self->{displayMode};
  445 
  446   # note: right now, we have to do things completely differently when we are
  447   # rendering math from INSIDE the translator and from OUTSIDE the translator.
  448   # so we'll just deal with each case explicitly here. there's some code
  449   # duplication that can be dealt with later by abstracting out tth/dvipng/etc.
  450 
  451   my $tex = $answerResult->{preview_latex_string};
  452 
  453   return "" unless defined $tex and $tex ne "";
  454 
  455   if ($displayMode eq "plainText") {
  456     return $tex;
  457   } elsif ($displayMode eq "formattedText") {
  458     my $tthCommand = $ce->{externalPrograms}->{tth}
  459       . " -L -f5 -r 2> /dev/null <<END_OF_INPUT; echo > /dev/null\n"
  460       . "\\(".$tex."\\)\n"
  461       . "END_OF_INPUT\n";
  462 
  463     # call tth
  464     my $result = `$tthCommand`;
  465     if ($?) {
  466       return "<b>[tth failed: $? $@]</b>";
  467     } else {
  468       return $result;
  469     }
  470   } elsif ($displayMode eq "images") {
  471     $imgGen->add($tex);
  472   } elsif ($displayMode eq "MathJax") {
  473     return '<span class="MathJax_Preview">[math]</span><script type="math/tex; mode=display">'.$tex.'</script>';
  474   } elsif ($displayMode eq "jsMath") {
  475     $tex =~ s/&/&amp;/g; $tex =~ s/</&lt;/g; $tex =~ s/>/&gt;/g;
  476     return '<SPAN CLASS="math">\\displaystyle{'.$tex.'}</SPAN>';
  477   }
  478 }
  479 
  480 # *EndPPM ######################################################################
  481 
  482 ################################################################################
  483 # Template escape implementations
  484 ################################################################################
  485 
  486 # FIXME need to make $Set and $set be used consistently
  487 
  488 sub pre_header_initialize {
  489   my ($self)     = @_;
  490 
  491   my $r = $self->r;
  492   my $ce = $r->ce;
  493   my $db = $r->db;
  494   my $authz = $r->authz;
  495   my $urlpath = $r->urlpath;
  496 
  497   my $setName = $urlpath->arg("setID");
  498   my $userName = $r->param('user');
  499   my $effectiveUserName = $r->param('effectiveUser');
  500   my $key = $r->param('key');
  501 
  502   # should we allow a new version to be created when
  503   #    acting as a user?
  504   my $verCreateOK = ( defined( $r->param('createnew_ok') ) ) ?
  505     $r->param('createnew_ok') : 0;
  506 
  507   # user checks
  508   my $User = $db->getUser($userName);
  509   die "record for user $userName (real user) does not exist."
  510     unless defined $User;
  511   my $EffectiveUser = $db->getUser($effectiveUserName);
  512   die "record for user $effectiveUserName (effective user) does " .
  513     "not exist." unless defined $EffectiveUser;
  514 
  515   my $PermissionLevel = $db->getPermissionLevel($userName);
  516   die "permission level record for $userName does not exist (but the " .
  517     "user does? odd...)" unless defined($PermissionLevel);
  518   my $permissionLevel = $PermissionLevel->permission;
  519 
  520 # we could be coming in with $setName = the versioned or nonversioned set
  521 # deal with that first
  522   my $requestedVersion = ( $setName =~ /,v(\d+)$/ ) ? $1 : 0;
  523   $setName =~ s/,v\d+$//;
  524 # note that if we're already working with a version we want to be sure to stick
  525 # with that version.  we do this after we've validated that the user is
  526 # assigned the set, below
  527 
  528 ###################################
  529 # gateway set and problem collection
  530 ###################################
  531 
  532 # we need the template (user) set, the merged set-version, and a
  533 #    problem from the set to be able to test whether we're creating a
  534 #    new set version.  assemble these
  535   my ( $tmplSet, $set, $Problem ) = ( 0, 0, 0 );
  536 
  537 # if the set comes in as "Undefined_Set", then we're trying/editing a
  538 #    single problem in a set, and so create a fake set with which to work
  539 #    if the user has the authorization to do that.
  540   if ( $setName eq "Undefined_Set" ) {
  541 
  542     # make sure these are defined
  543     $requestedVersion = 1;
  544     $self->{assignment_type} = 'gateway';
  545 
  546     if ( ! $authz->hasPermissions($userName,
  547                 "modify_problem_sets") ) {
  548       $self->{invalidSet} = "You do not have the " .
  549         "authorization level required to view/" .
  550         "edit undefined sets.";
  551 
  552       # define these so that we can drop through
  553       #    to report the error in body()
  554       $tmplSet = fake_set( $db );
  555       $set     = fake_set_version( $db );
  556       $Problem = fake_problem( $db );
  557     } else {
  558   # in this case we're creating a fake set from the input, so
  559   #    the input must include a source file.
  560       if ( ! $r->param("sourceFilePath") ) {
  561         $self->{invalidSet} = "An Undefined_Set " .
  562           "was requested, but no source " .
  563           "file for the contained problem " .
  564           "was provided.";
  565 
  566         # define these so that we can drop through
  567         #    to report the error in body()
  568         $tmplSet = fake_set( $db );
  569         $set     = fake_set_version( $db );
  570         $Problem = fake_problem( $db );
  571 
  572       } else {
  573         my $sourceFPath = $r->param("sourceFilePath");
  574         die("sourceFilePath is unsafe!") unless
  575           path_is_subdir($sourceFPath,
  576             $ce->{courseDirs}->{templates},
  577             1);
  578 
  579         $tmplSet = fake_set( $db );
  580         $set     = fake_set_version( $db );
  581         $Problem = fake_problem( $db );
  582 
  583         $tmplSet->assignment_type( "gateway" );
  584         $tmplSet->attempts_per_version( 0 );
  585         $tmplSet->time_interval( 0 );
  586         $tmplSet->versions_per_interval(1);
  587         $tmplSet->version_time_limit( 0 );
  588         $tmplSet->version_creation_time( time() );
  589         $tmplSet->problem_randorder( 0 );
  590         $tmplSet->problems_per_page( 1 );
  591         $tmplSet->hide_score('N');
  592         $tmplSet->hide_score_by_problem('N');
  593         $tmplSet->hide_work('N');
  594         $tmplSet->time_limit_cap('0');
  595         $tmplSet->restrict_ip('No');
  596 
  597         $set->assignment_type( "gateway" );
  598         $set->time_interval( 0 );
  599         $set->versions_per_interval(1);
  600         $set->version_time_limit( 0 );
  601         $set->version_creation_time( time() );
  602         $set->time_limit_cap('0');
  603 
  604         $Problem->problem_id(1);
  605         $Problem->source_file($sourceFPath);
  606         $Problem->user_id($effectiveUserName);
  607         $Problem->value(1);
  608         $Problem->problem_seed( $r->param("problemSeed") ) if ( $r->param("problemSeed") );
  609       }
  610     }
  611   } else {
  612 
  613 # get template set: the non-versioned set that's assigned to the user
  614 #    if this fails/failed in authz->checkSet, then $self->{invalidSet} is
  615 #    set
  616     $tmplSet = $db->getMergedSet( $effectiveUserName, $setName );
  617 
  618   # now we know that we're in a gateway test, save the assignment test
  619   #    for the processing of proctor keys for graded proctored tests;
  620   #    if we failed to get the set from the database, we store a fake
  621   #    value here to be able to continue
  622     $self->{'assignment_type'} = $tmplSet->assignment_type() ||
  623       'gateway';
  624 
  625   # next, get the latest (current) version of the set if we don't have a
  626   #     requested version number
  627     my @allVersionIds = $db->listSetVersions($effectiveUserName,
  628                $setName);
  629     my $latestVersion = (@allVersionIds ? $allVersionIds[-1] : 0);
  630 
  631   # double check that any requested version makes sense
  632     $requestedVersion = $latestVersion
  633       if ( $requestedVersion !~ /^\d+$/ ||
  634            $requestedVersion > $latestVersion ||
  635            $requestedVersion < 0 );
  636 
  637     die("No requested version when returning to problem?!")
  638       if ( ( $r->param("previewAnswers") ||
  639              $r->param("checkAnswers") ||
  640              $r->param("submitAnswers") ||
  641              $r->param("newPage") ) && ! $requestedVersion );
  642 
  643   # to test for a proctored test, we need the set version, not the
  644   #    template, to allow a finished proctored test to be checked as an
  645   #    unproctored test.  so we get the versioned set here
  646     if ( $requestedVersion ) {
  647   # if a specific set version was requested, it was stored in the $authz
  648   #    object when we did the set check
  649       $set = $db->getMergedSetVersion($effectiveUserName,
  650               $setName,
  651               $requestedVersion);
  652     } elsif ( $latestVersion ) {
  653   # otherwise, if there's a current version, which we take to be the
  654   #    latest version taken, we use that
  655       $set = $db->getMergedSetVersion($effectiveUserName,
  656               $setName,
  657               $latestVersion);
  658     } else {
  659   # and if neither of those work, get a dummy set so that we have
  660   #    something to work with
  661       my $userSetClass = $ce->{dbLayout}->{set_version}->{record};
  662 # FIXME RETURN TO: should this be global2version?
  663       $set = global2user($userSetClass,
  664              $db->getGlobalSet($setName));
  665       die "set  $setName  not found."  unless $set;
  666       $set->user_id($effectiveUserName);
  667       $set->psvn('000');
  668       $set->set_id("$setName");  # redundant?
  669       $set->version_id(0);
  670     }
  671   }
  672   my $setVersionNumber = ($set) ? $set->version_id() : 0;
  673 
  674   #################################
  675   # assemble gateway parameters
  676   #################################
  677 
  678   # we get the open/close dates for the gateway from the template set.
  679   #    note $isOpen/Closed give the open/close dates for the gateway
  680   #    as a whole (that is, the merged user|global set).  because the
  681   #    set could be bad (if $self->{invalidSet}), we check ->open_date
  682   #    before actually testing the date
  683   my $isOpen = $tmplSet && $tmplSet->open_date &&
  684     ( after($tmplSet->open_date()) ||
  685       $authz->hasPermissions($userName, "view_unopened_sets") );
  686 
  687   # FIXME for $isClosed, "record_answers_after_due_date" isn't quite
  688   #    the right description, but it seems reasonable
  689   my $isClosed = $tmplSet && $tmplSet->due_date &&
  690     ( after($tmplSet->due_date()) &&
  691       ! $authz->hasPermissions($userName, "record_answers_after_due_date") );
  692 
  693   # to determine if we need a new version, we need to know whether this
  694   #    version exceeds the number of attempts per version.  (among other
  695   #    things,) the number of attempts is a property of the problem, so
  696   #    get a problem to check that.  note that for a gateway/quiz all
  697   #    problems will have the same number of attempts.  This means that
  698   #    if the set doesn't have any problems we're up a creek, so check
  699   #    for that here and bail if it's the case
  700   my @setPNum = $setName eq "Undefined_Set" ? ( 1 ) :
  701     $db->listUserProblems($EffectiveUser->user_id, $setName);
  702   die("Set $setName contains no problems.") if ( ! @setPNum );
  703 
  704   # if we assigned a fake problem above, $Problem is already defined.
  705   #    otherwise, we get the Problem, or define it to be undefined if
  706   #    the set hasn't been versioned to the user yet--this gets fixed
  707   #    when we assign the setVersion
  708   if ( ! $Problem ) {
  709     $Problem = $setVersionNumber ?
  710       $db->getMergedProblemVersion($EffectiveUser->user_id,
  711         $setName, $setVersionNumber, $setPNum[0]) :
  712         undef;
  713   }
  714 
  715   # note that having $maxAttemptsPerVersion set to an infinite/0 value is
  716   #    nonsensical; if we did that, why have versions? (might want to do it for one individual?)
  717   my $maxAttemptsPerVersion = $tmplSet->attempts_per_version() || 0;
  718   my $timeInterval          = $tmplSet->time_interval() || 0;
  719   my $versionsPerInterval   = $tmplSet->versions_per_interval() || 0;
  720   my $timeLimit             = $tmplSet->version_time_limit() || 0;
  721 
  722   # what happens if someone didn't set one of these?  I think this can
  723   # happen if we're handed a malformed set, where the values in the
  724   # database are null.
  725   $timeInterval = 0 if (! defined($timeInterval) || $timeInterval eq '');
  726   $versionsPerInterval = 0 if (! defined($versionsPerInterval) ||
  727              $versionsPerInterval eq '');
  728 
  729   # every problem in the set must have the same submission characteristics
  730   my $currentNumAttempts    = ( defined($Problem) &&
  731               $Problem->num_correct() ne '' ) ?
  732               $Problem->num_correct() +
  733               $Problem->num_incorrect() : 0;
  734 
  735   # $maxAttempts turns into the maximum number of versions we can create;
  736   #    if $Problem isn't defined, we can't have made any attempts, so it
  737   #    doesn't matter
  738   my $maxAttempts           = ( defined($Problem) &&
  739               defined($Problem->max_attempts()) &&
  740               $Problem->max_attempts() ) ?
  741               $Problem->max_attempts() : -1;
  742 
  743   # finding the number of versions per time interval is a little harder.
  744   #    we interpret the time interval as a rolling interval: that is,
  745   #    if we allow two sets per day, that's two sets in any 24 hour
  746   #    period.  this is probably not what we really want, but it's
  747   #    more extensible to a limitation like "one version per hour",
  748   #    and we can set it to two sets per 12 hours for most "2ce daily"
  749   #    type applications
  750   my $timeNow = time();
  751   my $grace = $ce->{gatewayGracePeriod};
  752 
  753   my $currentNumVersions = 0;  # this is the number of versions in the
  754                                #    time interval
  755   my $totalNumVersions = 0;
  756 
  757   # we don't need to check this if $self->{invalidSet} is already set,
  758   #    or if we're working with an Undefined_Set
  759   if ( $setVersionNumber && ! $self->{invalidSet} &&
  760        $setName ne "Undefined_Set" ) {
  761     my @setVersionIDs = $db->listSetVersions($effectiveUserName, $setName);
  762     my @setVersions = $db->getSetVersions(map {[$effectiveUserName, $setName,, $_]} @setVersionIDs);
  763     foreach ( @setVersions ) {
  764       $totalNumVersions++;
  765       $currentNumVersions++
  766           if ( ! $timeInterval ||
  767          $_->version_creation_time() > ($timeNow - $timeInterval) );
  768     }
  769   }
  770 
  771   ####################################
  772   # new version creation conditional
  773   ####################################
  774 
  775   my $versionIsOpen = 0;  # can we do anything to this version?
  776 
  777   # recall $isOpen = timeNow > openDate [for the merged userset] and
  778   #    $isClosed = timeNow > dueDate [for the merged userset]
  779   #    again, if $self->{invalidSet} is already set, we don't need to
  780   #    to check this
  781   if ( $isOpen && ! $isClosed && ! $self->{invalidSet} ) {
  782 
  783   # if no specific version is requested, we can create a new one if
  784   #    need be
  785     if ( ! $requestedVersion ) {
  786       if ( ( $maxAttempts == -1 ||
  787              $totalNumVersions < $maxAttempts )
  788            &&
  789            ( $setVersionNumber == 0 ||
  790              (
  791          ( $currentNumAttempts>=$maxAttemptsPerVersion
  792            ||
  793            $timeNow >= $set->due_date + $grace )
  794          &&
  795          ( ! $versionsPerInterval
  796            ||
  797            $currentNumVersions < $versionsPerInterval )
  798          )
  799            )
  800            &&
  801            ( $effectiveUserName eq $userName ||
  802               ( $authz->hasPermissions($userName, "record_answers_when_acting_as_student") ||
  803           ( $authz->hasPermissions($userName, "create_new_set_version_when_acting_as_student") && $verCreateOK ) ) )
  804 
  805          ) {
  806         # assign set, get the right name, version
  807         #    number, etc., and redefine the $set
  808         #    and $Problem we're working with
  809         my $setTmpl = $db->getUserSet($effectiveUserName,$setName);
  810         WeBWorK::ContentGenerator::Instructor::assignSetVersionToUser($self, $effectiveUserName, $setTmpl);
  811         $setVersionNumber++;
  812 
  813         # get a clean version of the set to save,
  814         #    and the merged version to use in the
  815         #    rest of the routine
  816         my $cleanSet = $db->getSetVersion(
  817           $effectiveUserName, $setName,
  818           $setVersionNumber);
  819         $set = $db->getMergedSetVersion(
  820           $effectiveUserName, $setName,
  821           $setVersionNumber );
  822 
  823         $Problem = $db->getMergedProblemVersion(
  824           $effectiveUserName, $setName,
  825           $setVersionNumber, 1);
  826 
  827         # because we're creating this on the fly,
  828         #    it should be visible
  829         $set->visible(1);
  830         # set up creation time, open and due dates
  831         my $ansOffset = $set->answer_date() -
  832           $set->due_date();
  833         $set->version_creation_time( $timeNow );
  834         $set->open_date( $timeNow );
  835         # figure out the due date, taking into account
  836         #    any time limit cap
  837         my $dueTime =
  838             ( $set->time_limit_cap &&
  839               $timeNow+$timeLimit > $set->due_date ) ?
  840               $set->due_date : $timeNow+$timeLimit;
  841         $set->due_date( $dueTime );
  842         $set->answer_date($set->due_date + $ansOffset);
  843         $set->version_last_attempt_time( 0 );
  844 
  845         # put this new info into the database.  we
  846         #    put back that data which we need for the
  847         #    version, and leave blank any information
  848         #    that we'd like to inherit from the user
  849         #    set or global set.  we set the data which
  850         #    determines if a set is open, because we
  851         #    don't want the set version to reopen after
  852         #    it's complete
  853         $cleanSet->version_creation_time( $set->version_creation_time );
  854         $cleanSet->open_date( $set->open_date );
  855         $cleanSet->due_date( $set->due_date );
  856         $cleanSet->answer_date( $set->answer_date );
  857         $cleanSet->version_last_attempt_time( $set->version_last_attempt_time );
  858         $cleanSet->version_time_limit( $set->version_time_limit );
  859         $cleanSet->attempts_per_version( $set->attempts_per_version );
  860         $cleanSet->assignment_type( $set->assignment_type );
  861         $db->putSetVersion( $cleanSet );
  862 
  863         # we have a new set version, so it's open
  864         $versionIsOpen = 1;
  865 
  866         # also reset the number of attempts for this
  867         #    set to zero
  868         $currentNumAttempts = 0;
  869 
  870       } elsif ( $maxAttempts != -1 &&
  871           $totalNumVersions > $maxAttempts ) {
  872         $self->{invalidSet} = "No new versions of " .
  873           "this assignment are available,\n" .
  874           "because you have already taken the " .
  875           "maximum number\nallowed.";
  876 
  877       } elsif ( $effectiveUserName ne $userName &&
  878           $authz->hasPermissions($userName, "create_new_set_version_when_acting_as_student") ) {
  879         $self->{invalidSet} = "User " .
  880           "$effectiveUserName is being acted " .
  881           "as.  If you continue, you will " .
  882           "create a new version of this set " .
  883           "for that user, which will count " .
  884           "against their allowed maximum " .
  885           "number of versions for the current " .
  886           "time interval.  IN GENERAL, THIS " .
  887           "IS NOT WHAT YOU WANT TO DO.  " .
  888           "Please be sure that you want to " .
  889           "do this before clicking the \"" .
  890           "Create new set version\" link " .
  891           "below.  Alternately, PRESS THE " .
  892           "\"BACK\" BUTTON and continue.";
  893         $self->{invalidVersionCreation} = 1;
  894 
  895       } elsif ( $effectiveUserName ne $userName ) {
  896         $self->{invalidSet} = "User " .
  897           "$effectiveUserName is being acted " .
  898           "as.  When acting as another user, " .
  899           "new versions of the set cannot be " .
  900           "created.";
  901         $self->{invalidVersionCreation} = 2;
  902 
  903       } elsif ($currentNumAttempts < $maxAttemptsPerVersion &&
  904          $timeNow < $set->due_date() + $grace ) {
  905         if ( between($set->open_date(),
  906                $set->due_date() + $grace,
  907                $timeNow) ) {
  908           $versionIsOpen = 1;
  909         } else {
  910           $versionIsOpen = 0;  # redundant
  911           $self->{invalidSet} = "No new " .
  912             " versions of this assignment" .
  913             " are available,\nbecause the" .
  914             " set is not open or its time" .
  915             " limit has expired.\n";
  916         }
  917 
  918       } elsif ($versionsPerInterval &&
  919          ($currentNumVersions >= $versionsPerInterval)){
  920         $self->{invalidSet} = "You have already taken" .
  921           " all available versions of this\n" .
  922           "test in the current time interval.  " .
  923           "You may take the\ntest again after " .
  924           "the time interval has expired.";
  925 
  926       }
  927 
  928     } else {
  929     # (we're still in the $isOpen && ! $isClosed conditional here)
  930     #    if a specific version is requested, then we only check to
  931     #    see if it's open
  932       if (
  933            ( $currentNumAttempts < $maxAttemptsPerVersion )
  934            &&
  935            ( $effectiveUserName eq $userName ||
  936              $authz->hasPermissions($userName,
  937                   "record_set_version_answers_when_acting_as_student") )
  938          ) {
  939         if ( between($set->open_date(),
  940                $set->due_date() + $grace,
  941                $timeNow) ) {
  942           $versionIsOpen = 1;
  943         } else {
  944           $versionIsOpen = 0;  # redundant
  945         }
  946       }
  947     }
  948 
  949   # closed set, with attempt at a new one
  950   } elsif ( ! $self->{invalidSet} && ! $requestedVersion ) {
  951     $self->{invalidSet} = "This set is closed.  No new set " .
  952       "versions may be taken.";
  953   }
  954 
  955 
  956   ####################################
  957   # save problem and user data
  958   ####################################
  959 
  960   my $psvn = $set->psvn();
  961   $self->{tmplSet} = $tmplSet;
  962   $self->{set} = $set;
  963   $self->{problem} = $Problem;
  964   $self->{requestedVersion} = $requestedVersion;
  965 
  966   $self->{userName} = $userName;
  967   $self->{effectiveUserName} = $effectiveUserName;
  968   $self->{user} = $User;
  969   $self->{effectiveUser}   = $EffectiveUser;
  970   $self->{permissionLevel} = $permissionLevel;
  971 
  972   $self->{isOpen} = $isOpen;
  973   $self->{isClosed} = $isClosed;
  974   $self->{versionIsOpen} = $versionIsOpen;
  975 
  976   $self->{timeNow} = $timeNow;
  977 
  978   ####################################
  979   # form processing
  980   ####################################
  981 
  982   # this is the same as the following, but doesn't appear in Problem.pm
  983   my $newPage = $r->param("newPage");
  984   $self->{newPage} = $newPage;
  985 
  986   # also get the current page, if it's given
  987   my $currentPage = $r->param("currentPage") || 1;
  988 
  989   # this is a hack manage previewing a page.  we set previewAnswers to
  990   # yes if either of the following are true:
  991   #  1. the "previewAnswers" input is set (the "preview" button was
  992   #     clicked), or
  993   #  2. the "previewHack" input is set (a preview link was used)
  994   my $prevOr = $r->param('previewAnswers') || $r->param('previewHack');
  995   $r->param('previewAnswers', $prevOr) if ( defined( $prevOr ) );
  996 
  997         # [This section lifted from Problem.pm] ##############################
  998 
  999   # set options from form fields (see comment at top of file for names)
 1000   my $displayMode      = $r->param("displayMode") ||
 1001     $ce->{pg}->{options}->{displayMode};
 1002   my $redisplay        = $r->param("redisplay");
 1003   my $submitAnswers    = $r->param("submitAnswers");
 1004   my $checkAnswers     = $r->param("checkAnswers");
 1005   my $previewAnswers   = $r->param("previewAnswers");
 1006 
 1007   my $formFields = { WeBWorK::Form->new_from_paramable($r)->Vars };
 1008 
 1009   $self->{displayMode}    = $displayMode;
 1010   $self->{redisplay}      = $redisplay;
 1011   $self->{submitAnswers}  = $submitAnswers;
 1012   $self->{checkAnswers}   = $checkAnswers;
 1013   $self->{previewAnswers} = $previewAnswers;
 1014   $self->{formFields}     = $formFields;
 1015 
 1016   # now that we've set all the necessary variables quit out if the set or
 1017   #    problem is invalid
 1018 
 1019   return if $self->{invalidSet} || $self->{invalidProblem};
 1020 
 1021   # [End lifted section] ###############################################
 1022 
 1023   ####################################
 1024   # permissions
 1025   ####################################
 1026 
 1027   # bail without doing anything if the set isn't yet open for this user
 1028   if ( ! ( $self->{isOpen} ||
 1029      $authz->hasPermissions($userName,"view_unopened_sets") ) ) {
 1030     $self->{invalidSet} = "This set is not yet open.";
 1031     return;
 1032   }
 1033 
 1034   # what does the user want to do?
 1035   my %want =
 1036       (showOldAnswers     => $r->param("showOldAnswers") ||
 1037            $ce->{pg}->{options}->{showOldAnswers},
 1038        showCorrectAnswers => ($r->param("showCorrectAnswers") ||
 1039                              $ce->{pg}->{options}->{showCorrectAnswers}) &&
 1040                                    ($submitAnswers || $checkAnswers),
 1041        showHints          => $r->param("showHints") ||
 1042                        $ce->{pg}->{options}->{showHints},
 1043        showSolutions      => ($r->param("showSolutions") ||
 1044                        $ce->{pg}->{options}->{showSolutions}) &&
 1045                                    ($submitAnswers || $checkAnswers),
 1046        recordAnswers      => $submitAnswers,
 1047   # we also want to check answers if we were checking answers and are
 1048   #    switching between pages
 1049        checkAnswers       => $checkAnswers,
 1050        );
 1051 
 1052   # are certain options enforced?
 1053   my %must =
 1054       (showOldAnswers     => 0,
 1055        showCorrectAnswers => 0,
 1056        showHints          => 0,
 1057        showSolutions      => 0,
 1058        recordAnswers      => ! $authz->hasPermissions($userName,
 1059                   "avoid_recording_answers"),
 1060        checkAnswers       => 0,
 1061        );
 1062 
 1063   # does the user have permission to use certain options?
 1064   my @args = ($User, $PermissionLevel, $EffectiveUser, $set, $Problem,
 1065         $tmplSet);
 1066   my $sAns = ( $submitAnswers ? 1 : 0 );
 1067   my %can =
 1068       (showOldAnswers     => $self->can_showOldAnswers(@args),
 1069        showCorrectAnswers => $self->can_showCorrectAnswers(@args, $sAns),
 1070        showHints          => $self->can_showHints(@args),
 1071        showSolutions      => $self->can_showSolutions(@args, $sAns),
 1072        recordAnswers      => $self->can_recordAnswers(@args),
 1073        checkAnswers       => $self->can_checkAnswers(@args),
 1074        recordAnswersNextTime => $self->can_recordAnswers(@args, $sAns),
 1075        checkAnswersNextTime  => $self->can_checkAnswers(@args, $sAns),
 1076        showScore          => $self->can_showScore(@args),
 1077        );
 1078 
 1079   # final values for options
 1080   my %will;
 1081   foreach (keys %must) {
 1082     $will{$_} = $can{$_} && ($must{$_} || $want{$_}) ;
 1083   }
 1084 
 1085   ##### store fields #####
 1086 
 1087 ## FIXME: the following is present in Problem.pm, but missing here.  how do we
 1088 ##   deal with it in the context of multiple problems with possible hints?
 1089 ## ##### fix hint/solution options #####
 1090 ## $can{showHints}     &&= $pg->{flags}->{hintExists}
 1091 ##                     &&= $pg->{flags}->{showHintLimit}<=$pg->{state}->{num_of_incorrect_ans};
 1092 ##    $can{showSolutions} &&= $pg->{flags}->{solutionExists};
 1093 
 1094   $self->{want} = \%want;
 1095   $self->{must} = \%must;
 1096   $self->{can}  = \%can;
 1097   $self->{will} = \%will;
 1098 
 1099 
 1100   ####################################
 1101   # set up problem numbering and multipage variables
 1102   ####################################
 1103 
 1104   my @problemNumbers;
 1105   if ( $setName eq "Undefined_Set" ) {
 1106     @problemNumbers = ( 1 );
 1107   } else {
 1108     @problemNumbers = $db->listProblemVersions($effectiveUserName,
 1109                  $setName,
 1110                  $setVersionNumber);
 1111   }
 1112 
 1113   # to speed up processing of long (multi-page) tests, we want to only
 1114   #    translate those problems that are being submitted or are currently
 1115   #    being displayed.  so work out here which problems are on the
 1116   #    current page.
 1117   my ( $numPages, $pageNumber, $numProbPerPage ) = ( 1, 0, 0 );
 1118   my ( $startProb, $endProb ) = ( 0, $#problemNumbers );
 1119 
 1120   # update startProb and endProb for multipage tests
 1121   if ( defined($set->problems_per_page) && $set->problems_per_page ) {
 1122     $numProbPerPage = $set->problems_per_page;
 1123     $pageNumber = ($newPage) ? $newPage : $currentPage;
 1124 
 1125     $numPages = scalar(@problemNumbers)/$numProbPerPage;
 1126     $numPages = int($numPages) + 1 if (int($numPages) != $numPages);
 1127 
 1128     $startProb = ($pageNumber - 1)*$numProbPerPage;
 1129     $startProb = 0 if ( $startProb < 0 ||
 1130             $startProb > $#problemNumbers );
 1131     $endProb = ($startProb + $numProbPerPage > $#problemNumbers) ?
 1132         $#problemNumbers : $startProb + $numProbPerPage - 1;
 1133   }
 1134 
 1135 
 1136   # set up problem list for randomly ordered tests
 1137   my @probOrder = (0..$#problemNumbers);
 1138 
 1139   # there's a routine to do this somewhere, I think...
 1140   if ( $set->problem_randorder ) {
 1141     my @newOrder = ();
 1142   # we need to keep the random order the same each time the set is loaded!
 1143   #    this requires either saving the order in the set definition, or
 1144   #    being sure that the random seed that we use is the same each time
 1145   #    the same set is called.  we'll do the latter by setting the seed
 1146   #    to the psvn of the problem set.  we use a local PGrandom object
 1147   #    to avoid mucking with the system seed.
 1148     my $pgrand = PGrandom->new();
 1149     $pgrand->srand( $set->psvn );
 1150     while ( @probOrder ) {
 1151       my $i = int($pgrand->rand(scalar(@probOrder)));
 1152       push( @newOrder, $probOrder[$i] );
 1153       splice(@probOrder, $i, 1);
 1154     }
 1155     @probOrder = @newOrder;
 1156   }
 1157   # now $probOrder[i] = the problem number, numbered from zero, that's
 1158   #    displayed in the ith position on the test
 1159 
 1160   # make a list of those problems we're displaying
 1161   my @probsToDisplay = ();
 1162   for ( my $i=0; $i<@probOrder; $i++ ) {
 1163     push(@probsToDisplay, $probOrder[$i])
 1164         if ( $i >= $startProb && $i <= $endProb );
 1165   }
 1166 
 1167   ####################################
 1168   # process problems
 1169   ####################################
 1170 
 1171   my @problems = ();
 1172   my @pg_results = ();
 1173   # pg errors are stored here; initialize it to empty to start
 1174   $self->{errors} = [ ];
 1175 
 1176   # process the problems as needed
 1177   my @mergedProblems;
 1178   if ( $setName eq "Undefined_Set" ) {
 1179     @mergedProblems = ( $Problem );
 1180   } else {
 1181     @mergedProblems = $db->getAllMergedProblemVersions($effectiveUserName, $setName, $setVersionNumber);
 1182   }
 1183 
 1184   foreach my $problemNumber (sort {$a<=>$b } @problemNumbers) {
 1185 
 1186     # pIndex numbers from zero
 1187     my $pIndex = $problemNumber - 1;
 1188     if ( ! defined( $mergedProblems[$pIndex] ) ) {
 1189       $self->{invalidSet} = "One or more of the problems " .
 1190         "in this set have not been assigned to you.";
 1191       return;
 1192     }
 1193     my $ProblemN = $mergedProblems[$pIndex];
 1194 
 1195     # sticky answers are set up here
 1196     if ( not ( $submitAnswers or $previewAnswers or $checkAnswers or
 1197          $newPage ) and $will{showOldAnswers} ) {
 1198 
 1199       my %oldAnswers = decodeAnswers( $ProblemN->last_answer);
 1200       $formFields->{$_} = $oldAnswers{$_} foreach ( keys %oldAnswers );
 1201     }
 1202     push( @problems, $ProblemN );
 1203 
 1204     # if we don't have to translate this problem, just save the
 1205     #    problem number
 1206     my $pg = $problemNumber;
 1207     # this is the actual translation of each problem.  errors are
 1208     #    stored in @{$self->{errors}} in each case
 1209     if ( (grep /^$pIndex$/, @probsToDisplay) || $submitAnswers ) {
 1210       $pg = $self->getProblemHTML($self->{effectiveUser},
 1211                 $set, $formFields,
 1212                 $ProblemN);
 1213     }
 1214     push(@pg_results, $pg);
 1215   }
 1216   $self->{ra_problems} = \@problems;
 1217   $self->{ra_pg_results}=\@pg_results;
 1218 
 1219   $self->{startProb} = $startProb;
 1220   $self->{endProb} = $endProb;
 1221   $self->{numPages} = $numPages;
 1222   $self->{pageNumber} = $pageNumber;
 1223   $self->{ra_probOrder} = \@probOrder;
 1224 }
 1225 
 1226 sub path {
 1227   my ( $self, $args ) = @_;
 1228 
 1229   my $r = $self->{r};
 1230   my $setName = $r->urlpath->arg("setID");
 1231   my $ce = $self->{ce};
 1232   my $root = $ce->{webworkURLs}->{root};
 1233   my $courseName = $ce->{courseName};
 1234 
 1235   return $self->pathMacro( $args, "Home" => "$root",
 1236          $courseName => "$root/$courseName",
 1237          $setName => "" );
 1238 }
 1239 
 1240 sub nav {
 1241   my ($self, $args) = @_;
 1242 
 1243   my $r = $self->{r};
 1244   my $setName = $r->urlpath->arg("setID");
 1245   my $ce = $self->{ce};
 1246   my $root = $ce->{webworkURLs}->{root};
 1247   my $courseName = $ce->{courseName};
 1248   my @links = ("Problem Sets" , "$root/$courseName", "navUp");
 1249   my $tail = "";
 1250 
 1251   return $self->navMacro($args, $tail, @links);
 1252 }
 1253 
 1254 sub options {
 1255   my ($self) = @_;
 1256   #warn "doing options in GatewayQuiz";
 1257 
 1258   # don't show options if we don't have anything to show
 1259   return if $self->{invalidSet} or $self->{invalidProblem};
 1260   return unless $self->{isOpen};
 1261 
 1262   my $displayMode = $self->{displayMode};
 1263   my %can = %{ $self->{can} };
 1264 
 1265   my @options_to_show = "displayMode";
 1266   push @options_to_show, "showOldAnswers" if $can{showOldAnswers};
 1267   push @options_to_show, "showHints" if $can{showHints};
 1268   push @options_to_show, "showSolutions" if $can{showSolutions};
 1269 
 1270   return $self->optionsMacro(
 1271     options_to_show => \@options_to_show,
 1272   );
 1273 }
 1274 
 1275 sub body {
 1276   my $self = shift();
 1277   my $r = $self->r;
 1278   my $ce = $r->ce;
 1279   my $db = $r->db;
 1280   my $authz = $r->authz;
 1281   my $urlpath = $r->urlpath;
 1282   my $user = $r->param('user');
 1283   my $effectiveUser = $r->param('effectiveUser');
 1284 
 1285   # report everything with the same time that we started with
 1286   my $timeNow = $self->{timeNow};
 1287   my $grace = $ce->{gatewayGracePeriod};
 1288 
 1289   #########################################
 1290   # preliminary error checking and output
 1291   #########################################
 1292 
 1293   # if $self->{invalidSet} is set, then we have an error and should
 1294   #    just bail with the appropriate error message
 1295 
 1296   if ($self->{invalidSet} || $self->{invalidProblem}) {
 1297       # delete any proctor keys that are floating around
 1298     if ( $self->{'assignment_type'} eq 'proctored_gateway' ) {
 1299       my $proctorID = $r->param('proctor_user');
 1300       if ( $proctorID ) {
 1301         eval{ $db->deleteKey("$effectiveUser,$proctorID"); };
 1302         eval{ $db->deleteKey("$effectiveUser,$proctorID,g"); };
 1303       }
 1304     }
 1305 
 1306     my $newlink = '';
 1307     my $usernote = '';
 1308     if ( defined( $self->{invalidVersionCreation} ) &&
 1309          $self->{invalidVersionCreation} == 1 ) {
 1310       my $gwpage = $urlpath->newFromModule($urlpath->module,$r,
 1311         courseID=>$urlpath->arg("courseID"),
 1312         setID=>$urlpath->arg("setID"));
 1313       my $link = $self->systemLink( $gwpage,
 1314         params=>{effectiveUser => $effectiveUser,
 1315            user => $user,
 1316            createnew_ok => 1} );
 1317       $newlink = CGI::p(CGI::a({href=>$link},
 1318         "Create new set version."));
 1319       $usernote = " (acted as by $user)";
 1320     } elsif ( defined( $self->{invalidVersionCreation} ) &&
 1321         $self->{invalidVersionCreation} == 2 ) {
 1322       $usernote = " (acted as by $user)";
 1323     }
 1324 
 1325     return CGI::div({class=>"ResultsWithError"},
 1326         CGI::p("The selected problem set (" .
 1327                $urlpath->arg("setID") . ") is not " .
 1328                "a valid set for $effectiveUser" .
 1329                "$usernote:"),
 1330         CGI::p($self->{invalidSet}),
 1331         $newlink);
 1332   }
 1333 
 1334   my $tmplSet = $self->{tmplSet};
 1335   my $set = $self->{set};
 1336   my $Problem = $self->{problem};
 1337   my $permissionLevel = $self->{permissionLevel};
 1338   my $submitAnswers = $self->{submitAnswers};
 1339   my $checkAnswers = $self->{checkAnswers};
 1340   my $previewAnswers = $self->{previewAnswers};
 1341   my $newPage = $self->{newPage};
 1342   my %want = %{ $self->{want} };
 1343   my %can = %{ $self->{can} };
 1344   my %must = %{ $self->{must} };
 1345   my %will = %{ $self->{will} };
 1346 
 1347   my @problems = @{ $self->{ra_problems} };
 1348   my @pg_results = @{ $self->{ra_pg_results} };
 1349   my @pg_errors = @{ $self->{errors} };
 1350   my $requestedVersion = $self->{requestedVersion};
 1351 
 1352   my $startProb = $self->{startProb};
 1353   my $endProb = $self->{endProb};
 1354   my $numPages = $self->{numPages};
 1355   my $pageNumber = $self->{pageNumber};
 1356   my @probOrder = @{$self->{ra_probOrder}};
 1357 
 1358   my $setName  = $set->set_id;
 1359   my $versionNumber = $set->version_id;
 1360   my $setVName = "$setName,v$versionNumber";
 1361   my $numProbPerPage = $set->problems_per_page;
 1362 
 1363   # translation errors -- we use the same output routine as Problem.pm,
 1364   #    but play around to allow for errors on multiple translations
 1365   #    because we have an array of problems to deal with.
 1366   if ( @pg_errors ) {
 1367     my $errorNum = 1;
 1368     my ( $message, $context ) = ( '', '' );
 1369     foreach ( @pg_errors ) {
 1370 
 1371       $message .= "$errorNum. " if ( @pg_errors > 1 );
 1372       $message .= $_->{message} . CGI::br() . "\n";
 1373 
 1374       $context .= CGI::p((@pg_errors > 1? "$errorNum.": '') .
 1375              $_->{context} ) . "\n\n" .
 1376              CGI::hr() . "\n\n";
 1377     }
 1378     return $self->errorOutput( $message, $context );
 1379   }
 1380 
 1381   ####################################
 1382   # answer processing
 1383   ####################################
 1384 
 1385   debug("begin answer processing");
 1386 
 1387   my @scoreRecordedMessage = ('') x scalar(@problems);
 1388 
 1389   ####################################
 1390   # save results to database as appropriate
 1391   ####################################
 1392 
 1393   if ( $submitAnswers || ( ($previewAnswers || $newPage) &&
 1394          $can{recordAnswers} ) ) {
 1395     # if we're submitting answers, we have to save the problems
 1396     #    to the database.
 1397     # if we're previewing or switching pages and can still
 1398     #    record answers, we save the last answer for future
 1399     #    reference
 1400 
 1401     # first, if we're submitting answers for a proctored exam,
 1402     #    we want to delete the proctor keys that authorized
 1403     #    that grading, so that it isn't possible to just log
 1404     #    in and take another proctored test without getting
 1405     #    reauthorized
 1406     if ( $submitAnswers &&
 1407          $self->{'assignment_type'} eq 'proctored_gateway' ) {
 1408       my $proctorID = $r->param('proctor_user');
 1409 
 1410       # if we don't have attempts left, delete all
 1411       #    proctor keys for this user
 1412       if ( $set->attempts_per_version - 1 -
 1413            $Problem->num_correct - $Problem->num_incorrect
 1414            <= 0 ) {
 1415         eval{ $db->deleteAllProctorKeys( $effectiveUser ); };
 1416       } else {
 1417         # otherwise, delete only the grading key
 1418         eval{ $db->deleteKey("$effectiveUser,$proctorID,g"); };
 1419         # in this case we may have a past, login,
 1420         #    proctor key that we can keep so that
 1421         #    we don't have to get another login to
 1422         #    continue working the test
 1423         if ( $r->param("past_proctor_user") &&
 1424              $r->param("past_proctor_key") ) {
 1425           $r->param("proctor_user", $r->param("past_proctor_user"));
 1426           $r->param("proctor_key", $r->param("past_proctor_key"));
 1427         }
 1428       }
 1429       # this is unsubtle, but we'd rather not have bogus
 1430       #    keys sitting around
 1431       if ( $@ ) {
 1432         die("ERROR RESETTING PROCTOR GRADING KEY(S): $@\n");
 1433       }
 1434 
 1435     }
 1436 
 1437     my @pureProblems = $db->getAllProblemVersions($effectiveUser,
 1438                     $setName,
 1439                     $versionNumber);
 1440     foreach my $i ( 0 .. $#problems ) {  # process each problem
 1441       # this code is essentially that from Problem.pm
 1442       my $pureProblem = $pureProblems[$i];
 1443 
 1444       # store answers in problem for sticky answers later
 1445       my %answersToStore;
 1446 
 1447       # we have to be a little careful about getting the
 1448       #    answers that we're saving, because we don't have
 1449       #    a pg_results object for all problems if we're not
 1450       #    submitting
 1451       my %answerHash = ();
 1452       my @answer_order = ();
 1453       if ( ref( $pg_results[$i] ) ) {
 1454         %answerHash = %{$pg_results[$i]->{answers}};
 1455         $answersToStore{$_} = $self->{formFields}->{$_}
 1456           foreach (keys %answerHash);
 1457         # check for extra answers that slipped
 1458         #    by---e.g. for matrices, and get them
 1459         #    from the original input form
 1460         my @extra_answer_names =
 1461             @{ $pg_results[$i]->{flags}->{KEPT_EXTRA_ANSWERS} };
 1462         $answersToStore{$_} =
 1463             $self->{formFields}->{$_} foreach (@extra_answer_names);
 1464         @answer_order =
 1465             ( @{$pg_results[$i]->{flags}->{ANSWER_ENTRY_ORDER}},
 1466               @extra_answer_names );
 1467       } else {
 1468         my $prefix = sprintf('Q%04d_',$i+1);
 1469         my @fields = sort grep {/^$prefix/} (keys %{$self->{formFields}});
 1470         %answersToStore = map {$_ => $self->{formFields}->{$_}} @fields;
 1471         @answer_order = @fields;
 1472       }
 1473       my $answerString = encodeAnswers( %answersToStore,
 1474                 @answer_order );
 1475       # and get the last answer
 1476       $problems[$i]->last_answer( $answerString );
 1477       $pureProblem->last_answer( $answerString );
 1478 
 1479       # next, store the state in the database if that makes
 1480       #    sense
 1481       if ( $submitAnswers && $will{recordAnswers} ) {
 1482   $problems[$i]->status($pg_results[$i]->{state}->{recorded_score});
 1483   $problems[$i]->attempted(1);
 1484   $problems[$i]->num_correct($pg_results[$i]->{state}->{num_of_correct_ans});
 1485   $problems[$i]->num_incorrect($pg_results[$i]->{state}->{num_of_incorrect_ans});
 1486   $pureProblem->status($pg_results[$i]->{state}->{recorded_score});
 1487   $pureProblem->attempted(1);
 1488   $pureProblem->num_correct($pg_results[$i]->{state}->{num_of_correct_ans});
 1489   $pureProblem->num_incorrect($pg_results[$i]->{state}->{num_of_incorrect_ans});
 1490 
 1491         if ( $db->putProblemVersion( $pureProblem ) ) {
 1492           $scoreRecordedMessage[$i] = "Your " .
 1493             "score on this problem was " .
 1494             "recorded.";
 1495         } else {
 1496           $scoreRecordedMessage[$i] = "Your " .
 1497             "score was not recorded " .
 1498             "because there was a failure " .
 1499             "in storing the problem " .
 1500             "record to the database.";
 1501         }
 1502         # write the transaction log
 1503         writeLog( $self->{ce}, "transaction",
 1504             $problems[$i]->problem_id . "\t" .
 1505             $problems[$i]->set_id . "\t" .
 1506             $problems[$i]->user_id . "\t" .
 1507             $problems[$i]->source_file . "\t" .
 1508             $problems[$i]->value . "\t" .
 1509             $problems[$i]->max_attempts . "\t" .
 1510             $problems[$i]->problem_seed . "\t" .
 1511             $problems[$i]->status . "\t" .
 1512             $problems[$i]->attempted . "\t" .
 1513             $problems[$i]->last_answer . "\t" .
 1514             $problems[$i]->num_correct . "\t" .
 1515             $problems[$i]->num_incorrect
 1516             );
 1517       } elsif ( $submitAnswers ) {
 1518         # this is the case where we submitted answers
 1519         #    but can't save them; report an error
 1520         #    message
 1521 
 1522         if ($self->{isClosed}) {
 1523           $scoreRecordedMessage[$i] = "Your " .
 1524             "score was not recorded " .
 1525             "because this problem set " .
 1526             "version is not open.";
 1527         } elsif ( $problems[$i]->num_correct +
 1528             $problems[$i]->num_incorrect >=
 1529             $set->attempts_per_version ) {
 1530           $scoreRecordedMessage[$i] = "Your " .
 1531             "score was not recorded " .
 1532             "because you have no " .
 1533             "attempts remaining on this " .
 1534             "set version.";
 1535         } elsif ( ! $self->{versionIsOpen} ) {
 1536           my $endTime = ( $set->version_last_attempt_time ) ? $set->version_last_attempt_time : $timeNow;
 1537           if ($endTime > $set->due_date &&
 1538               $endTime < $set->due_date + $grace){
 1539             $endTime = $set->due_date;
 1540           }
 1541           my $elapsed =
 1542               int(($endTime - $set->open_date)/0.6 + 0.5)/100;
 1543           # we assume that allowed is an even
 1544           #    number of minutes
 1545           my $allowed = ($set->due_date - $set->open_date)/60;
 1546           $scoreRecordedMessage[$i] = "Your " .
 1547             "score was not recorded " .
 1548             "because you have exceeded " .
 1549             "the time limit for this " .
 1550             "test. (Time taken: $elapsed " .
 1551             "min; allowed: $allowed min.)";
 1552         } else {
 1553           $scoreRecordedMessage[$i] = "Your " .
 1554             "score was not recorded.";
 1555         }
 1556       } else {
 1557         # finally, we must be previewing or switching
 1558         #    pages.  save only the last answer for the
 1559         #    problems
 1560         $db->putProblemVersion( $pureProblem );
 1561       }
 1562     } # end loop through problems
 1563 
 1564     ## finally, log student answers if we're submitting,
 1565     ##    previewing, or changing pages, provided that we can
 1566     ##    record answers.  note that this will log an overtime
 1567     ##    submission (or any case where someone submits the
 1568     ##    test, or spoofs a request to submit a test)
 1569 
 1570     my $answer_log =
 1571       $self->{ce}->{courseFiles}->{logs}->{'answer_log'};
 1572 
 1573     # this is carried over from Problem.pm
 1574     if ( defined( $answer_log ) ) {
 1575       foreach my $i ( 0 .. $#problems ) {
 1576         my $answerString = '';
 1577         my $scores = '';
 1578         # note that we store these answers in the
 1579         #    order that they are presented, not the
 1580         #    actual problem order
 1581         if ( ref( $pg_results[$probOrder[$i]] ) ) {
 1582           my %answerHash = %{ $pg_results[$probOrder[$i]]->{answers} };
 1583           foreach ( sortByName(undef, keys %answerHash) ) {
 1584             my $sAns = defined($answerHash{$_}->{original_student_ans}) ? $answerHash{$_}->{original_student_ans} : '';
 1585             $answerString .= $sAns . "\t";
 1586             $scores .= $answerHash{$_}->{score}>=1 ? "1" : "0" if ( $submitAnswers );
 1587           }
 1588         } else {
 1589           my $prefix = sprintf('Q%04d_', ($probOrder[$i]+1));
 1590           my @fields = sort grep {/^$prefix/} (keys %{$self->{formFields}});
 1591           foreach ( @fields ) {
 1592             $answerString .= $self->{formFields}->{$_} . "\t";
 1593             $scores .= $self->{formFields}->{"probstatus" . ($probOrder[$i]+1)} >= 1 ? "1" : "0" if ( $submitAnswers );
 1594           }
 1595         }
 1596         $answerString =~ s/\t+$/\t/;
 1597 
 1598         my $answerPrefix;
 1599         if ( $submitAnswers ) {
 1600           $answerPrefix = "[submit] ";
 1601         } elsif ( $previewAnswers ) {
 1602           $answerPrefix = "[preview] ";
 1603         } else {
 1604           $answerPrefix = "[newPage] ";
 1605         }
 1606 
 1607         if ( ! $answerString ||
 1608              $answerString =~ /^\t$/ ) {
 1609           $answerString = "$answerPrefix" .
 1610             "No answer entered\t";
 1611         } else {
 1612           $answerString = "$answerPrefix" .
 1613             "$answerString";
 1614         }
 1615 
 1616         writeCourseLog( $self->{ce}, "answer_log",
 1617             join("", '|',
 1618                  $problems[$i]->user_id,
 1619                  '|', $setVName,
 1620                  '|', ($i+1), '|', $scores,
 1621                  "\t$timeNow\t",
 1622                  "$answerString"),
 1623             );
 1624       }
 1625     }
 1626   }
 1627   debug("end answer processing");
 1628 
 1629   # additional set-level database manipulation: we want to save the time
 1630   #    that a set was submitted, and for proctored tests we want to reset
 1631   #    the assignment type after a set is submitted for the last time so
 1632   #    that it's possible to look at it later without getting proctor
 1633   #    authorization
 1634   if ( ( $submitAnswers &&
 1635          ( $will{recordAnswers} ||
 1636      ( ! $set->version_last_attempt_time() &&
 1637        $timeNow > $set->due_date + $grace ) ) ) ||
 1638        ( ! $can{recordAnswersNextTime} &&
 1639          $set->assignment_type() eq 'proctored_gateway' ) ) {
 1640 
 1641     my $setName = $set->set_id();
 1642 
 1643   # save the submission time if we're recording the answer, or if the
 1644   #     first submission occurs after the due_date
 1645     if ( $submitAnswers &&
 1646          ( $will{recordAnswers} ||
 1647            ( ! $set->version_last_attempt_time() &&
 1648        $timeNow > $set->due_date + $grace ) ) ) {
 1649       $set->version_last_attempt_time( $timeNow );
 1650     }
 1651     if ( ! $can{recordAnswersNextTime} &&
 1652          $set->assignment_type() eq 'proctored_gateway' ) {
 1653       $set->assignment_type( 'gateway' );
 1654     }
 1655   # again, we save only parameters that are determine access to the
 1656   #    set version
 1657     my $cleanSet = $db->getSetVersion($effectiveUser,
 1658             $setName,
 1659             $versionNumber);
 1660     $cleanSet->assignment_type( $set->assignment_type );
 1661     $cleanSet->version_last_attempt_time( $set->version_last_attempt_time );
 1662     $db->putSetVersion( $cleanSet );
 1663   }
 1664 
 1665 
 1666   ####################################
 1667   # output
 1668   ####################################
 1669 
 1670   # some convenient output variables
 1671   my $canShowProblemScores = $can{showScore} &&
 1672       ($set->hide_score eq 'N' || $set->hide_score_by_problem eq 'N' ||
 1673        $authz->hasPermissions($user, "view_hidden_work"));
 1674   my $canShowWork = $authz->hasPermissions($user, "view_hidden_work") || ($set->hide_work eq 'N' || ($set->hide_work eq 'BeforeAnswerDate' && $timeNow>$tmplSet->answer_date));
 1675 
 1676   # for nicer answer checking on multi-page tests, we want to keep
 1677   #    track of any changes that someone made to a different page,
 1678   #    and what their score was.  we use @probStatus to do this.  we
 1679   #    initialize this to any known scores, and then update this when
 1680   #    calculating the score for checked or submitted tests
 1681   my @probStatus = ();
 1682   # we also figure out recorded score for the set, if any, and score
 1683   #    on this attempt
 1684   my $recordedScore = 0;
 1685   my $totPossible = 0;
 1686   foreach ( @problems ) {
 1687     my $pv = ( $_->value() ) ? $_->value() : 1;
 1688     $totPossible += $pv;
 1689     $recordedScore += $_->status*$pv if (defined($_->status));
 1690     push( @probStatus, ($r->param("probstatus" . $_->problem_id) ||
 1691             $_->status || 0) );
 1692   }
 1693 
 1694   # to get the attempt score, we have to figure out what the score on
 1695   #    each part of each problem is, and multiply the total for the
 1696   #    problem by the weight (value) of the problem.  to make things
 1697   #    even more interesting, we are avoiding translating all of the
 1698   #    problems when checking answers
 1699   my $attemptScore = 0;
 1700 
 1701   if ( $submitAnswers || $checkAnswers ) {
 1702     my $i=0;
 1703     foreach my $pg ( @pg_results ) {
 1704       my $pValue = $problems[$i]->value() ? $problems[$i]->value() : 1;
 1705       my $pScore = 0;
 1706       my $numParts = 0;
 1707       if ( ref( $pg ) ) {  # then we have a pg object
 1708         foreach (@{$pg->{flags}->{ANSWER_ENTRY_ORDER}}){
 1709           $pScore += $pg->{answers}->{$_}->{score};
 1710           $numParts++;
 1711         }
 1712         $probStatus[$i] = $pScore/($numParts>0 ? $numParts : 1);
 1713 
 1714       } else {
 1715         # if we don't have a pg object, use any known
 1716         #    problem status (this defaults to zero)
 1717         $pScore = $probStatus[$i];
 1718       }
 1719       $attemptScore += $pScore*$pValue/($numParts > 0 ? $numParts : 1);
 1720       $i++;
 1721     }
 1722   }
 1723 
 1724   # we want to print elapsed and allowed times; allowed is easy
 1725   my $allowed = sprintf( "%.0f", 10*($set->due_date - $set->open_date)/6 )/100;
 1726   # elapsed is a little harder; we're counting to the last submission
 1727   #    time, or to the current time if the test hasn't been submitted,
 1728   #    and if the submission fell in the grace period round it to the
 1729   #    due_date
 1730   my $exceededAllowedTime = 0;
 1731   my $endTime = ( $set->version_last_attempt_time ) ?
 1732       $set->version_last_attempt_time : $timeNow;
 1733   if ( $endTime > $set->due_date && $endTime < $set->due_date + $grace ) {
 1734     $endTime = $set->due_date;
 1735   } elsif ( $endTime > $set->due_date ) {
 1736     $exceededAllowedTime = 1;
 1737   }
 1738   my $elapsedTime = int(($endTime - $set->open_date)/0.6 + 0.5)/100;
 1739 
 1740   # also get number of remaining attempts (important for sets with
 1741   #    multiple attempts per version)
 1742   my $numLeft = ($set->attempts_per_version ||0 )- $Problem->num_correct -
 1743     $Problem->num_incorrect -
 1744     ($submitAnswers && $will{recordAnswers} ? 1 : 0);
 1745   my $attemptNumber = $Problem->num_correct + $Problem->num_incorrect;
 1746 
 1747   # a handy noun for when referring to a test
 1748   my $testNoun = (( $set->attempts_per_version || 0 ) > 1) ? "submission" : "test";
 1749   my $testNounNum = ( ( $set->attempts_per_version ||0 ) > 1 ) ?
 1750     "submission (test " : "test (";
 1751 
 1752   ##### start output of test headers:
 1753   ##### display information about recorded and checked scores
 1754   if ( $submitAnswers ) {
 1755     # the distinction between $can{recordAnswers} and ! $can{} has
 1756     #    been dealt with above and recorded in @scoreRecordedMessage
 1757     my $divClass = 'ResultsWithoutError';
 1758     my $recdMsg = '';
 1759     foreach ( @scoreRecordedMessage ) {
 1760       if ($_ ne 'Your score on this problem was recorded.') {
 1761         $recdMsg = $_;
 1762         $divClass = 'ResultsWithError';
 1763         last;
 1764       }
 1765     }
 1766     print CGI::start_div({class=>$divClass});
 1767 
 1768     if ( $recdMsg ) {
 1769       # then there was an error when saving the results
 1770       print CGI::strong("Your score on this $testNounNum ",
 1771             "$versionNumber) was NOT recorded.  ",
 1772             $recdMsg), CGI::br();
 1773     } else {
 1774       # no error; print recorded message
 1775       print CGI::strong("Your score on this $testNounNum ",
 1776             "$versionNumber) WAS recorded."),
 1777       CGI::br();
 1778 
 1779       # and show the score if we're allowed to do that
 1780       if ( $can{showScore} ) {
 1781         print CGI::strong("Your score on this " .
 1782               "$testNoun is ",
 1783               "$attemptScore/$totPossible.");
 1784       } else {
 1785         my $when =
 1786           ($set->hide_score eq 'BeforeAnswerDate')
 1787           ? ' until ' . ($self->formatDateTime($set->answer_date) )
 1788           : '';
 1789         print CGI::br() .
 1790           "(Your score on this $testNoun " .
 1791           "is not available$when.)";
 1792       }
 1793     }
 1794 
 1795     # finally, if there is another, recorded message, print that
 1796     #    too so that we know what's going on
 1797     print CGI::end_div();
 1798     if ( $set->attempts_per_version > 1 && $attemptNumber > 1 &&
 1799          $recordedScore != $attemptScore && $can{showScore} ) {
 1800       print CGI::start_div({class=>'gwMessage'});
 1801       print "The recorded score for this test is ",
 1802         "$recordedScore/$totPossible.";
 1803       print CGI::end_div();
 1804     }
 1805 
 1806   } elsif ( $checkAnswers ) {
 1807     if ( $can{showScore} ) {
 1808       print CGI::start_div({class=>'gwMessage'});
 1809       print CGI::strong("Your score on this (checked, not ",
 1810             "recorded) submission is ",
 1811             "$attemptScore/$totPossible."),
 1812         CGI::br();
 1813       print "The recorded score for this test is " .
 1814         "$recordedScore/$totPossible.  ";
 1815       print CGI::end_div();
 1816     }
 1817   }
 1818 
 1819   ##### remaining output of test headers:
 1820   ##### display timer or information about elapsed time, "printme" link,
 1821   ##### and information about any recorded score if not submitAnswers or
 1822   ##### checkAnswers
 1823   if ( $can{recordAnswersNextTime} ) {
 1824 
 1825     # print timer
 1826     # FIXME: in the long run, we want to allow a test to not be
 1827     #    timed.  This does not allow for that possibility
 1828     my $timeLeft = $set->due_date() - $timeNow;  # this is in secs
 1829     print CGI::div({-id=>"gwTimer"},"\n");
 1830     print CGI::startform({-name=>"gwTimeData", -method=>"POST",
 1831               -action=>$r->uri});
 1832     print CGI::hidden({-name=>"serverTime", -value=>$timeNow}),
 1833       "\n";
 1834     print CGI::hidden({-name=>"serverDueTime",
 1835            -value=>$set->due_date()}), "\n";
 1836     print CGI::endform();
 1837 
 1838     if ( $timeLeft < 1 && $timeLeft > 0 &&
 1839          ! $authz->hasPermissions($user, "record_answers_when_acting_as_student")) {
 1840       print CGI::span({-class=>"resultsWithError"},
 1841           CGI::b("You have less than 1 minute ",
 1842                  "to complete this test.\n"));
 1843     } elsif ( $timeLeft <= 0 &&
 1844         ! $authz->hasPermissions($user, "record_answers_when_acting_as_student") ) {
 1845       print CGI::span({-class=>"resultsWithError"},
 1846           CGI::b("You are out of time.  ",
 1847                  "Press grade now!\n"));
 1848     }
 1849     # if there are multiple attempts per version, indicate the
 1850     #    number remaining, and if we've submitted a multiple
 1851     #    attempt multi-page test, show the score on the previous
 1852     #    submission
 1853     if ( $set->attempts_per_version > 1 ) {
 1854       print CGI::em("You have $numLeft attempt(s) remaining ",
 1855               "on this test.");
 1856       if ( $numLeft < $set->attempts_per_version &&
 1857            $numPages > 1 &&
 1858            $can{showScore} ) {
 1859         print CGI::start_div({-id=>"gwScoreSummary"}),
 1860           CGI::strong({},"Score summary for " .
 1861                 "last submit:");
 1862         print CGI::start_table({"border"=>0,
 1863               "cellpadding"=>0,
 1864               "cellspacing"=>0});
 1865         print CGI::Tr({},CGI::th({-align=>"left"},
 1866                ["Prob","","Status","",
 1867                 "Result"]));
 1868         for ( my $i=0; $i<@probStatus; $i++ ) {
 1869           print CGI::Tr({},
 1870             CGI::td({},[($i+1),"",int(100*$probStatus[$probOrder[$i]]+0.5) . "%","", $probStatus[$probOrder[$i]] == 1 ? "Correct" : "Incorrect"]));
 1871         }
 1872         print CGI::end_table(), CGI::end_div();
 1873       }
 1874     }
 1875   } else {
 1876     print CGI::start_div({class=>'gwMessage'});
 1877 
 1878     if ( ! $checkAnswers && ! $submitAnswers ) {
 1879 
 1880       if ( $can{showScore} ) {
 1881         my $scMsg = "Your recorded score on this " .
 1882           "(test number $versionNumber) is " .
 1883           "$recordedScore/$totPossible";
 1884         if ( $exceededAllowedTime &&
 1885              $recordedScore == 0 ) {
 1886           $scMsg .= ", because you exceeded " .
 1887             "the allowed time.";
 1888         } else {
 1889           $scMsg .= ".  ";
 1890         }
 1891         print CGI::strong($scMsg), CGI::br();
 1892       }
 1893     }
 1894 
 1895     if ( $set->version_last_attempt_time ) {
 1896       print "Time taken on test: $elapsedTime min " .
 1897         "($allowed min allowed).";
 1898     } elsif ( $exceededAllowedTime && $recordedScore != 0 ) {
 1899       print "(This test is overtime because it was not " .
 1900         "submitted in the allowed time.)";
 1901     }
 1902     print CGI::end_div();
 1903 
 1904     if ( $canShowWork && $set->set_id ne "Undefined_Set" ) {
 1905       print "The test (which is number $versionNumber) may " .
 1906         "no longer be submitted for a grade";
 1907       print "" . (($can{showScore}) ? ", but you may still " .
 1908             "check your answers." : ".") ;
 1909 
 1910       # print a "printme" link if we're allowed to see our
 1911       #    work
 1912       my $link = $ce->{webworkURLs}->{root} . '/' .
 1913         $ce->{courseName} . '/hardcopy/' .
 1914         $set->set_id . ',v' . $set->version_id . '/?' .
 1915         $self->url_authen_args;
 1916       my $printmsg = CGI::div({-class=>'gwPrintMe'},
 1917             CGI::a({-href=>$link},
 1918                    "Print Test"));
 1919       print $printmsg;
 1920     }
 1921   }
 1922 
 1923   # this is a hack to get a URL that won't require a proctor login if
 1924   #    we've submitted a proctored test for the last time.  above we've
 1925   #    reset the assignment_type in this case, so we'll use that to
 1926   #    decide if we should give a path to an unproctored test.
 1927   my $action = $r->uri();
 1928   $action =~ s/proctored_quiz_mode/quiz_mode/
 1929     if ( $set->assignment_type() eq 'gateway' );
 1930   # we also want to be sure that if we're in a set, the 'action' in the
 1931   #    form points us to the same set.
 1932   my $setname = $set->set_id;
 1933   my $setvnum = $set->version_id;
 1934   $action =~ s/(quiz_mode\/$setname)\/?$/$1,v$setvnum\//;  #"
 1935 
 1936   # now, we print out the rest of the page if we're not hiding submitted
 1937   #    answers
 1938   if ( ! $can{recordAnswersNextTime} && ! $canShowWork ) {
 1939     my $when = ( $set->hide_work eq 'BeforeAnswerDate' )
 1940       ? ' until ' . ($self->formatDateTime($set->answer_date))
 1941       : '';
 1942     print CGI::start_div({class=>"gwProblem"});
 1943     print CGI::strong("Completed results for this assignment are " .
 1944           "not available$when.");
 1945     print CGI::end_div();
 1946 
 1947   # else: we're not hiding answers
 1948   } else {
 1949 
 1950     print CGI::startform({-name=>"gwquiz", -method=>"POST",
 1951               -action=>$action}),
 1952       $self->hidden_authen_fields,
 1953       $self->hidden_proctor_authen_fields;
 1954 
 1955   # hacks to use a javascript link to trigger previews and jump to
 1956   #    subsequent pages of a multipage test
 1957     print CGI::hidden({-name=>'previewHack', -value=>''}),
 1958       CGI::br();
 1959     if ( $numProbPerPage && $numPages > 1 ) {
 1960       print CGI::hidden({-name=>'newPage', -value=>''});
 1961       print CGI::hidden({-name=>'currentPage',
 1962              -value=>$pageNumber});
 1963     }
 1964 
 1965   # the link for a preview; for a multipage test, this also needs to
 1966   #    keep track of what page we're on
 1967     my $jsprevlink = 'javascript:document.gwquiz.previewHack.value="1";';
 1968     $jsprevlink .= "document.gwquiz.newPage.value=\"$pageNumber\";"
 1969       if ( $numProbPerPage && $numPages > 1 );
 1970     $jsprevlink .= 'document.gwquiz.submit();';
 1971 
 1972   # set up links between problems and, for multi-page tests, pages
 1973     my $jumpLinks = '';
 1974     my $probRow = [ CGI::b("Problem") ];
 1975     for my $i ( 0 .. $#pg_results ) {
 1976 
 1977       my $pn = $i + 1;
 1978       if ( $i >= $startProb && $i <= $endProb ) {
 1979         push(@$probRow, CGI::b(" [ ")) if ($i == $startProb);
 1980         push( @$probRow, " &nbsp;" .
 1981               CGI::a({-href=>".",
 1982                 -onclick=>"jumpTo($pn);return false;"},
 1983                "$pn") . "&nbsp; " );
 1984         push(@$probRow, CGI::b(" ] ")) if ($i == $endProb);
 1985       } elsif ( ! ($i % $numProbPerPage) ) {
 1986         push(@$probRow, " &nbsp;&nbsp; ",
 1987              " &nbsp;&nbsp; ", " &nbsp;&nbsp; ");
 1988       }
 1989     }
 1990     if ( $numProbPerPage && $numPages > 1 ) {
 1991       my $pageRow = [ CGI::td([ CGI::b('Jump to: '),
 1992               CGI::b('Page '),
 1993               CGI::b(' [ ' ) ]) ];
 1994       for my $i ( 1 .. $numPages ) {
 1995         my $pn = ( $i == $pageNumber ) ? $i :
 1996             CGI::a({-href=>'javascript:' .
 1997             "document.gwquiz.newPage.value=\"$i\";" .
 1998             'document.gwquiz.submit();'},
 1999              "&nbsp;$i&nbsp;");
 2000 
 2001         my $colspan =  0;
 2002         if ( $i == $pageNumber ) {
 2003           $colspan =
 2004               ($#pg_results - ($i-1)*$numProbPerPage > $numProbPerPage) ?
 2005               $numProbPerPage :
 2006               $#pg_results - ($i-1)*$numProbPerPage + 1;
 2007         } else {
 2008           $colspan = 1;
 2009         }
 2010         push( @$pageRow, CGI::td({-colspan=>$colspan,
 2011                 -align=>'center'},
 2012                $pn) );
 2013         push( @$pageRow, CGI::td( [CGI::b(' ] '),
 2014                  CGI::b(' [ ')] ) )
 2015           if ( $i != $numPages );
 2016       }
 2017       push( @$pageRow, CGI::td(CGI::b(' ] ')) );
 2018       unshift( @$probRow, ' &nbsp; ' );
 2019       $jumpLinks = CGI::table( CGI::Tr(@$pageRow),
 2020              CGI::Tr( CGI::td($probRow) ) );
 2021     } else {
 2022       unshift( @$probRow, CGI::b('Jump to: ') );
 2023       $jumpLinks = CGI::table( CGI::Tr( CGI::td($probRow) ) );
 2024     }
 2025 
 2026     print $jumpLinks,"\n";
 2027 
 2028   # print out problems and attempt results, as appropriate
 2029   # note: args to attemptResults are (self,) $pg, $showAttemptAnswers,
 2030   #    $showCorrectAnswers, $showAttemptResults (and-ed with
 2031   #    $showAttemptAnswers), $showSummary, $showAttemptPreview (or-ed
 2032   #    with zero)
 2033     my $problemNumber = 0;
 2034 
 2035     foreach my $i ( 0 .. $#pg_results ) {
 2036       my $pg = $pg_results[$probOrder[$i]];
 2037       $problemNumber++;
 2038 
 2039       if ( $i >= $startProb && $i <= $endProb ) {
 2040 
 2041         my $recordMessage = '';
 2042         my $resultsTable = '';
 2043 
 2044         if ($pg->{flags}->{showPartialCorrectAnswers}>=0 && $submitAnswers){
 2045           if ( $scoreRecordedMessage[$probOrder[$i]] ne
 2046                "Your score on this problem was recorded." ) {
 2047             $recordMessage = CGI::span({class=>"resultsWithError"},
 2048                      "ANSWERS NOT RECORDED --",
 2049                      $scoreRecordedMessage[$probOrder[$i]]);
 2050 
 2051           }
 2052           $resultsTable =
 2053               $self->attemptResults($pg, 1, $will{showCorrectAnswers},
 2054                   $pg->{flags}->{showPartialCorrectAnswers} && $canShowProblemScores,
 2055                   $canShowProblemScores, 1);
 2056 
 2057         } elsif ( $checkAnswers ) {
 2058           $recordMessage = CGI::span({class=>"resultsWithError"},
 2059                    "ANSWERS ONLY CHECKED -- ",
 2060                    "ANSWERS NOT RECORDED");
 2061 
 2062           $resultsTable =
 2063               $self->attemptResults($pg, 1, $will{showCorrectAnswers},
 2064                   $pg->{flags}->{showPartialCorrectAnswers} && $canShowProblemScores,
 2065                   $canShowProblemScores, 1);
 2066 
 2067         } elsif ( $previewAnswers ) {
 2068           $recordMessage =
 2069               CGI::span({class=>"resultsWithError"},
 2070                   "PREVIEW ONLY -- ANSWERS NOT RECORDED");
 2071           $resultsTable = $self->attemptResults($pg, 1, 0, 0, 0, 1);
 2072 
 2073         }
 2074 
 2075         print CGI::start_div({class=>"gwProblem"});
 2076         my $i1 = $i+1;
 2077         my $pv = $problems[$probOrder[$i]]->value() ? $problems[$probOrder[$i]]->value() : 1;
 2078         my $points = ($pv > 1) ?
 2079           " (" . $pv . " points)" :
 2080           " (1 point)";
 2081         print CGI::a({-name=>"#$i1"},"");
 2082         print CGI::strong("Problem $problemNumber."),
 2083           "$points\n", $recordMessage;
 2084         print CGI::p($pg->{body_text}),
 2085         CGI::p($pg->{result}->{msg} ?
 2086                CGI::b("Note: ") : "",
 2087                CGI::i($pg->{result}->{msg}));
 2088         print CGI::p({class=>"gwPreview"},
 2089                CGI::a({-href=>"$jsprevlink"},
 2090                 "preview problems"));
 2091 
 2092         print $resultsTable if $resultsTable;
 2093 
 2094         print CGI::end_div();
 2095         # finally, store the problem status for
 2096         #    continued attempts recording
 2097         my $pNum = $probOrder[$i] + 1;
 2098         print CGI::hidden({-name=>"probstatus$pNum",
 2099                -value=>$probStatus[$probOrder[$i]]});
 2100 
 2101         print "\n", CGI::hr(), "\n";
 2102       } else {
 2103         my $i1 = $i+1;
 2104         # keep the jump to anchors so that jumping to
 2105         #    problem number 6 still works, even if
 2106         #    we're viewing only problems 5-7, etc.
 2107         print CGI::a({-name=>"#$i1"},""), "\n";
 2108         # and print out hidden fields with the current
 2109         #    last answers
 2110         my $curr_prefix = 'Q' . sprintf("%04d", $probOrder[$i]+1) . '_';
 2111         my @curr_fields = grep /^$curr_prefix/, keys %{$self->{formFields}};
 2112         foreach my $curr_field ( @curr_fields ) {
 2113           print CGI::hidden({-name=>$curr_field,
 2114                  -value=>$self->{formFields}->{$curr_field}});
 2115         }
 2116         # finally, store the problem status for
 2117         #    continued attempts recording
 2118         my $pNum = $probOrder[$i] + 1;
 2119         print CGI::hidden({-name=>"probstatus$pNum",
 2120                -value=>$probStatus[$probOrder[$i]]});
 2121 #       my $probid = 'Q' . sprintf("%04d", $probOrder[$i]+1) . "_AnSwEr1";
 2122 #       my $probval = $self->{formFields}->{$probid};
 2123 #       print CGI::hidden({-name=>$probid, -value=>$probval}), "\n";
 2124       }
 2125     }
 2126     print CGI::p($jumpLinks, "\n");
 2127     print "\n",CGI::hr(), "\n";
 2128 
 2129     if ($can{showCorrectAnswers}) {
 2130       print CGI::checkbox(-name   =>"showCorrectAnswers",
 2131 #       -checked => $will{showCorrectAnswers},
 2132               -checked=>$want{showCorrectAnswers},
 2133               -label  =>"Show correct answers",
 2134               );
 2135     }
 2136 #     if ($can{showHints}) {
 2137 #   print CGI::div({style=>"color:red"},
 2138 #            CGI::checkbox(-name    => "showHints",
 2139 #              -checked => $will{showHints},
 2140 #              -label   => "Show Hints",
 2141 #              )
 2142 #            );
 2143 #     }
 2144     if ($can{showSolutions}) {
 2145       print CGI::checkbox(-name    => "showSolutions",
 2146               -checked => $will{showSolutions},
 2147               -label   => "Show Solutions",
 2148               );
 2149     }
 2150 
 2151     if ($can{showCorrectAnswers} or $can{showHints} or
 2152         $can{showSolutions}) {
 2153       print CGI::br();
 2154     }
 2155 
 2156     print CGI::p( CGI::submit( -name=>"previewAnswers",
 2157              -label=>"Preview Test" ),
 2158             ($can{recordAnswersNextTime} ?
 2159              CGI::submit( -name=>"submitAnswers",
 2160               -label=>"Grade Test" ) : " "),
 2161             ($can{checkAnswersNextTime} && ! $can{recordAnswersNextTime} ?
 2162              CGI::submit( -name=>"checkAnswers",
 2163               -label=>"Check Test" ) : " "),
 2164             ($numProbPerPage && $numPages > 1 &&
 2165              $can{recordAnswersNextTime} ? CGI::br() .
 2166              CGI::em("Note: grading the test grades " .
 2167                CGI::b("all") . " problems, not just those " .
 2168                "on this page.") : " ") );
 2169 
 2170     ## save the source file, etc., if we're trying an undefined
 2171     ##    set
 2172     # print( CGI::hidden(
 2173     #        -name   => 'sourceFilePath',
 2174     #        -value  =>  $self->{problem}->{source_file}
 2175     #       ))  if defined($self->{problem}->{source_file});
 2176     print( CGI::hidden(
 2177            -name   => 'sourceFilePath',
 2178            -value  =>  $r->param("sourceFilePath")
 2179           ))  if defined($r->param("sourceFilePath"));
 2180     print( CGI::hidden(
 2181            -name   => 'problemSeed',
 2182            -value  =>  $r->param("problemSeed")
 2183           ))  if defined($r->param("problemSeed"));
 2184 
 2185     print CGI::endform();
 2186   }
 2187 
 2188   # finally, put in a show answers option if appropriate
 2189   # print answer inspection button
 2190   if ($authz->hasPermissions($user, "view_answers")) {
 2191     my $pastAnswersPage = $urlpath->newFromModule("WeBWorK::ContentGenerator::Instructor::ShowAnswers", $r, courseID => $ce->{courseName});
 2192     my $showPastAnswersURL = $self->systemLink($pastAnswersPage, authen => 0); # no authen info for form action
 2193     print "\n", CGI::start_form(-method=>"POST",-action=>$showPastAnswersURL,-target=>"WW_Info"),"\n",
 2194       $self->hidden_authen_fields,"\n",
 2195       CGI::hidden(-name => 'courseID',  -value=>$ce->{courseName}), "\n",
 2196       CGI::hidden(-name => 'problemID', -value=>($startProb+1)), "\n",
 2197       CGI::hidden(-name => 'setID',  -value=>$setVName), "\n",
 2198       CGI::hidden(-name => 'studentUser',    -value=>$effectiveUser), "\n",
 2199       CGI::p( {-align=>"left"},
 2200         CGI::submit(-name => 'action',  -value=>'Show Past Answers')
 2201         ), "\n",
 2202       CGI::endform();
 2203   }
 2204 
 2205 # debugging verbiage
 2206 #     if ( $can{checkAnswersNextTime} ) {
 2207 #   print "Can check answers next time\n";
 2208 #     } else {
 2209 #   print "Can NOT check answers next time\n";
 2210 #     }
 2211 #     if ( $can{recordAnswersNextTime} ) {
 2212 #   print "Can record answers next time\n";
 2213 #     } else {
 2214 #   print "Can NOT record answers next time\n";
 2215 #     }
 2216 
 2217   # we exclude the feedback form from gateway tests.  they can use the feedback
 2218   #   button on the preceding or following pages
 2219 #     my $ce = $r->ce;
 2220 #     my $root = $ce->{webworkURLs}->{root};
 2221 #     my $courseName = $ce->{courseName};
 2222 #     my $feedbackURL = "$root/$courseName/feedback/";
 2223 #     print CGI::startform("POST", $feedbackURL),
 2224 #           $self->hidden_authen_fields,
 2225 #           CGI::hidden("module", __PACKAGE__),
 2226 #           CGI::hidden("set",    $self->{set}->set_id),
 2227 #           CGI::p({-align=>"right"},
 2228 #      CGI::submit(-name=>"feedbackForm", -label=>"Send Feedback")
 2229 #      ),
 2230 #     CGI::endform();
 2231 
 2232   return "";
 2233 
 2234 }
 2235 
 2236 
 2237 ###########################################################################
 2238 # Evaluation utilities
 2239 ############################################################################
 2240 
 2241 sub getProblemHTML {
 2242   my ( $self, $EffectiveUser, $set, $formFields,
 2243        $mergedProblem, $pgFile ) = @_;
 2244 # in:  $EffectiveUser is the effective user we're working as, $set is the
 2245 #      merged set version, %$formFields the form fields from the input form
 2246 #      that we need to worry about putting into the HTML we're generating,
 2247 #      and $mergedProblem and $pgFile are what we'd expect.
 2248 #      $pgFile is optional
 2249 # out: the translated problem is returned
 2250 
 2251   my $r = $self->r;
 2252   my $ce = $r->ce;
 2253   my $db = $r->db;
 2254   my $key =  $r->param('key');
 2255   my $setName = $set->set_id;
 2256   my $setVersionNumber = $set->version_id;
 2257   my $permissionLevel = $self->{permissionLevel};
 2258   my $psvn = $set->psvn();
 2259 
 2260   if ( defined($mergedProblem) && $mergedProblem->problem_id ) {
 2261 # nothing needs to be done
 2262 
 2263   } elsif ($pgFile) {
 2264     $mergedProblem =
 2265         WeBWorK::DB::Record::ProblemVersion->new(
 2266           set_id => $setName,
 2267           version_id => $setVersionNumber,
 2268           problem_id => 0,
 2269           login_id => $EffectiveUser->user_id,
 2270           source_file => $pgFile,
 2271       # the rest of Problem's fields are not needed, i think
 2272                    );
 2273   }
 2274 # figure out if we're allowed to get solutions and call PG->new accordingly.
 2275   my $showCorrectAnswers = $self->{will}->{showCorrectAnswers};
 2276   my $showHints          = $self->{will}->{showHints};
 2277   my $showSolutions      = $self->{will}->{showSolutions};
 2278   my $processAnswers     = $self->{will}->{checkAnswers};
 2279 
 2280 # FIXME  I'm not sure that problem_id is what we want here  FIXME
 2281   my $problemNumber = $mergedProblem->problem_id;
 2282 
 2283   my $pg =
 2284       WeBWorK::PG->new(
 2285        $ce,
 2286        $EffectiveUser,
 2287        $key,
 2288        $set,
 2289        $mergedProblem,
 2290        $psvn,
 2291        $formFields,
 2292        { # translation options
 2293            displayMode     => $self->{displayMode},
 2294            showHints       => $showHints,
 2295            showSolutions   => $showSolutions,
 2296            refreshMath2img => $showHints || $showSolutions,
 2297            processAnswers  => 1,
 2298            QUIZ_PREFIX     => 'Q' .
 2299          sprintf("%04d",$problemNumber) . '_',
 2300            },
 2301            );
 2302 
 2303 # FIXME  is problem_id the correct thing in the following two stanzas?
 2304 # FIXME  the original version had "problem number", which is what we want.
 2305 # FIXME  I think problem_id will work, too
 2306   if ($pg->{warnings} ne "") {
 2307     push @{$self->{warnings}}, {
 2308       set     => "$setName,v$setVersionNumber",
 2309       problem => $mergedProblem->problem_id,
 2310       message => $pg->{warnings},
 2311     };
 2312   }
 2313 
 2314   if ($pg->{flags}->{error_flag}) {
 2315     push @{$self->{errors}}, {
 2316       set     => "$setName,v$setVersionNumber",
 2317       problem => $mergedProblem->problem_id,
 2318       message => $pg->{errors},
 2319       context => $pg->{body_text},
 2320     };
 2321   # if there was an error, body_text contains
 2322   # the error context, not TeX code
 2323     $pg->{body_text} = undef;
 2324   }
 2325 
 2326   return    $pg;
 2327 }
 2328 
 2329 ##### output utilities #####
 2330 sub problemListRow($$$) {
 2331   my $self = shift;
 2332   my $set = shift;
 2333   my $Problem = shift;
 2334 
 2335   my $name = $Problem->problem_id;
 2336   my $interactiveURL = "$name/?" . $self->url_authen_args;
 2337   my $interactive = CGI::a({-href=>$interactiveURL}, "Problem $name");
 2338   my $attempts = $Problem->num_correct + $Problem->num_incorrect;
 2339   my $remaining = $Problem->max_attempts < 0
 2340     ? "unlimited"
 2341     : $Problem->max_attempts - $attempts;
 2342   my $status = sprintf("%.0f%%", $Problem->status * 100); # round to whole number
 2343 
 2344   return CGI::Tr(CGI::td({-nowrap=>1}, [
 2345     $interactive,
 2346     $attempts,
 2347     $remaining,
 2348     $status,
 2349   ]));
 2350 }
 2351 # sub nbsp {
 2352 #   my $str = shift;
 2353 #   ($str) ? $str : '&nbsp;';  # returns non-breaking space for empty strings
 2354 # }
 2355 
 2356 ##### logging subroutine ####
 2357 
 2358 
 2359 
 2360 
 2361 1;

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9