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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 4860 - (view) (download) (as text)
Original Path: trunk/webwork-modperl/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm

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

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9