Parent Directory
|
Revision Log
Generalized %can settings in Problem.pm with new permissions. Closes bug #541. See said bug for more details.
1 ################################################################################ 2 # WeBWorK Online Homework Delivery System 3 # Copyright © 2000-2003 The WeBWorK Project, http://openwebwork.sf.net/ 4 # $CVSHeader: webwork2/lib/WeBWorK/ContentGenerator/Problem.pm,v 1.153 2004/07/06 21:23:58 jj 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::Problem; 18 use base qw(WeBWorK::ContentGenerator); 19 20 =head1 NAME 21 22 WeBWorK::ContentGenerator::Problem - Allow a student to interact with a problem. 23 24 =cut 25 26 use strict; 27 use warnings; 28 use CGI qw(); 29 use File::Path qw(rmtree); 30 use WeBWorK::Form; 31 use WeBWorK::PG; 32 use WeBWorK::PG::ImageGenerator; 33 use WeBWorK::PG::IO; 34 use WeBWorK::Utils qw(writeLog writeCourseLog encodeAnswers decodeAnswers ref2string makeTempDirectory); 35 use WeBWorK::DB::Utils qw(global2user user2global findDefaults); 36 use WeBWorK::Timing; 37 38 use WeBWorK::Utils::Tasks qw(fake_set fake_problem); 39 40 ################################################################################ 41 # CGI param interface to this module (up-to-date as of v1.153) 42 ################################################################################ 43 44 # Standard params: 45 # 46 # user - user ID of real user 47 # key - session key 48 # effectiveUser - user ID of effective user 49 # 50 # Integration with PGProblemEditor: 51 # 52 # editMode - if set, indicates alternate problem source location. 53 # can be "temporaryFile" or "savedFile". 54 # 55 # sourceFilePath - path to file to be edited 56 # problemSeed - force problem seed to value 57 # success - success message to display 58 # failure - failure message to display 59 # 60 # Rendering options: 61 # 62 # displayMode - name of display mode to use 63 # 64 # showOldAnswers - request that last entered answer be shown (if allowed) 65 # showCorrectAnswers - request that correct answers be shown (if allowed) 66 # showHints - request that hints be shown (if allowed) 67 # showSolutions - request that solutions be shown (if allowed) 68 # 69 # Problem interaction: 70 # 71 # AnSwEr# - answer blanks in problem 72 # 73 # redisplay - name of the "Redisplay Problem" button 74 # submitAnswers - name of "Submit Answers" button 75 # checkAnswers - name of the "Check Answers" button 76 # previewAnswers - name of the "Preview Answers" button 77 78 ################################################################################ 79 # "can" methods 80 ################################################################################ 81 82 # Subroutines to determine if a user "can" perform an action. Each subroutine is 83 # called with the following arguments: 84 # 85 # ($self, $User, $PermissionLevel, $EffectiveUser, $Set, $Problem) 86 87 sub can_showOldAnswers { 88 #my ($self, $User, $PermissionLevel, $EffectiveUser, $Set, $Problem) = @_; 89 90 return 1; 91 } 92 93 sub can_showCorrectAnswers { 94 my ($self, $User, $PermissionLevel, $EffectiveUser, $Set, $Problem) = @_; 95 my $authz = $self->r->authz; 96 97 return 98 after($Set->answer_date) 99 || 100 $authz->hasPermissions($User->user_id, "show_correct_answers_before_answer_date") 101 ; 102 } 103 104 sub can_showHints { 105 #my ($self, $User, $PermissionLevel, $EffectiveUser, $Set, $Problem) = @_; 106 107 return 1; 108 } 109 110 sub can_showSolutions { 111 my ($self, $User, $PermissionLevel, $EffectiveUser, $Set, $Problem) = @_; 112 my $authz = $self->r->authz; 113 114 return 115 after($Set->answer_date) 116 || 117 $authz->hasPermissions($User->user_id, "show_solutions_before_answer_date") 118 ; 119 } 120 121 sub can_recordAnswers { 122 my ($self, $User, $PermissionLevel, $EffectiveUser, $Set, $Problem) = @_; 123 my $authz = $self->r->authz; 124 if ($User->user_id ne $EffectiveUser->user_id) { 125 return $authz->hasPermissions($User->user_id, "record_answers_when_acting_as_student"); 126 } 127 if (before($Set->open_date)) { 128 return $authz->hasPermissions($User->user_id, "record_answers_before_open_date"); 129 } elsif (between($Set->open_date, $Set->due_date)) { 130 my $max_attempts = $Problem->max_attempts; 131 my $attempts_used = $Problem->num_correct + $Problem->num_incorrect + 1; 132 if ($max_attempts == -1 or $attempts_used < $max_attempts) { 133 return $authz->hasPermissions($User->user_id, "record_answers_after_open_date_with_attempts"); 134 } else { 135 return $authz->hasPermissions($User->user_id, "record_answers_after_open_date_without_attempts"); 136 } 137 } elsif (between($Set->due_date, $Set->close_date)) { 138 return $authz->hasPermissions($User->user_id, "record_answers_after_due_date"); 139 } elsif (after($Set->close_date)) { 140 return $authz->hasPermissions($User->user_id, "record_answers_after_answer_date"); 141 } 142 } 143 144 sub can_checkAnswers { 145 my ($self, $User, $PermissionLevel, $EffectiveUser, $Set, $Problem) = @_; 146 my $authz = $self->r->authz; 147 148 if (before($Set->open_date)) { 149 return $authz->hasPermissions($User->user_id, "check_answers_before_open_date"); 150 } elsif (between($Set->open_date, $Set->due_date)) { 151 my $max_attempts = $Problem->max_attempts; 152 my $attempts_used = $Problem->num_correct + $Problem->num_incorrect + 1; 153 if ($max_attempts == -1 or $attempts_used < $max_attempts) { 154 return $authz->hasPermissions($User->user_id, "check_answers_after_open_date_with_attempts"); 155 } else { 156 return $authz->hasPermissions($User->user_id, "check_answers_after_open_date_without_attempts"); 157 } 158 } elsif (between($Set->due_date, $Set->close_date)) { 159 return $authz->hasPermissions($User->user_id, "check_answers_after_due_date"); 160 } elsif (after($Set->close_date)) { 161 return $authz->hasPermissions($User->user_id, "check_answers_after_answer_date"); 162 } 163 } 164 165 # Helper functions for calculating times 166 sub before { return time < $_[0] } 167 sub after { return time > $_[0] } 168 sub between { my $t = time; return $t > $_[0] && $t < $_[1] } 169 170 ################################################################################ 171 # output utilities 172 ################################################################################ 173 174 sub attemptResults { 175 my $self = shift; 176 my $pg = shift; 177 my $showAttemptAnswers = shift; 178 my $showCorrectAnswers = shift; 179 my $showAttemptResults = $showAttemptAnswers && shift; 180 my $showSummary = shift; 181 my $showAttemptPreview = shift || 0; 182 183 my $ce = $self->r->ce; 184 185 my $problemResult = $pg->{result}; # the overall result of the problem 186 my @answerNames = @{ $pg->{flags}->{ANSWER_ENTRY_ORDER} }; 187 188 my $showMessages = $showAttemptAnswers && grep { $pg->{answers}->{$_}->{ans_message} } @answerNames; 189 190 my $basename = "equation-" . $self->{set}->psvn. "." . $self->{problem}->problem_id . "-preview"; 191 192 # to make grabbing these options easier, we'll pull them out now... 193 my %imagesModeOptions = %{$ce->{pg}->{displayModeOptions}->{images}}; 194 195 my $imgGen = WeBWorK::PG::ImageGenerator->new( 196 tempDir => $ce->{webworkDirs}->{tmp}, 197 latex => $ce->{externalPrograms}->{latex}, 198 dvipng => $ce->{externalPrograms}->{dvipng}, 199 useCache => 1, 200 cacheDir => $ce->{webworkDirs}->{equationCache}, 201 cacheURL => $ce->{webworkURLs}->{equationCache}, 202 cacheDB => $ce->{webworkFiles}->{equationCacheDB}, 203 dvipng_align => $imagesModeOptions{dvipng_align}, 204 dvipng_depth_db => $imagesModeOptions{dvipng_depth_db}, 205 ); 206 207 my $header; 208 #$header .= CGI::th("Part"); 209 $header .= $showAttemptAnswers ? CGI::th("Entered") : ""; 210 $header .= $showAttemptPreview ? CGI::th("Answer Preview") : ""; 211 $header .= $showCorrectAnswers ? CGI::th("Correct") : ""; 212 $header .= $showAttemptResults ? CGI::th("Result") : ""; 213 $header .= $showMessages ? CGI::th("Messages") : ""; 214 my @tableRows = ( $header ); 215 my $numCorrect = 0; 216 foreach my $name (@answerNames) { 217 my $answerResult = $pg->{answers}->{$name}; 218 my $studentAnswer = $answerResult->{student_ans}; # original_student_ans 219 my $preview = ($showAttemptPreview 220 ? $self->previewAnswer($answerResult, $imgGen) 221 : ""); 222 my $correctAnswer = $answerResult->{correct_ans}; 223 my $answerScore = $answerResult->{score}; 224 my $answerMessage = $showMessages ? $answerResult->{ans_message} : ""; 225 #FIXME --Can we be sure that $answerScore is an integer-- could the problem give partial credit? 226 $numCorrect += $answerScore > 0; 227 my $resultString = $answerScore == 1 ? "correct" : "incorrect"; 228 229 # get rid of the goofy prefix on the answer names (supposedly, the format 230 # of the answer names is changeable. this only fixes it for "AnSwEr" 231 #$name =~ s/^AnSwEr//; 232 233 my $row; 234 #$row .= CGI::td($name); 235 $row .= $showAttemptAnswers ? CGI::td($self->nbsp($studentAnswer)) : ""; 236 $row .= $showAttemptPreview ? CGI::td($self->nbsp($preview)) : ""; 237 $row .= $showCorrectAnswers ? CGI::td($self->nbsp($correctAnswer)) : ""; 238 $row .= $showAttemptResults ? CGI::td($self->nbsp($resultString)) : ""; 239 $row .= $showMessages ? CGI::td($self->nbsp($answerMessage)) : ""; 240 push @tableRows, $row; 241 } 242 243 # render equation images 244 $imgGen->render(refresh => 1); 245 246 # my $numIncorrectNoun = scalar @answerNames == 1 ? "question" : "questions"; 247 my $scorePercent = sprintf("%.0f%%", $problemResult->{score} * 100); 248 # FIXME -- I left the old code in in case we have to back out. 249 # my $summary = "On this attempt, you answered $numCorrect out of " 250 # . scalar @answerNames . " $numIncorrectNoun correct, for a score of $scorePercent."; 251 my $summary = ""; 252 if (scalar @answerNames == 1) { 253 if ($numCorrect == scalar @answerNames) { 254 $summary .= CGI::div({class=>"ResultsWithoutError"},"The above answer is correct."); 255 } else { 256 $summary .= CGI::div({class=>"ResultsWithError"},"The above answer is NOT correct."); 257 } 258 } else { 259 if ($numCorrect == scalar @answerNames) { 260 $summary .= CGI::div({class=>"ResultsWithoutError"},"All of the above answers are correct."); 261 } else { 262 $summary .= CGI::div({class=>"ResultsWithError"},"At least one of the above answers is NOT correct."); 263 } 264 } 265 266 return 267 CGI::table({-class=>"attemptResults"}, CGI::Tr(\@tableRows)) 268 . ($showSummary ? CGI::p({class=>'emphasis'},$summary) : ""); 269 } 270 271 sub viewOptions { 272 my ($self) = @_; 273 my $ce = $self->r->ce; 274 275 # don't show options if we don't have anything to show 276 return if $self->{invalidSet} or $self->{invalidProblem}; 277 return unless $self->{isOpen}; 278 279 my $displayMode = $self->{displayMode}; 280 my %must = %{ $self->{must} }; 281 my %can = %{ $self->{can} }; 282 my %will = %{ $self->{will} }; 283 284 my $optionLine; 285 $can{showOldAnswers} and $optionLine .= join "", 286 "Show: ".CGI::br(), 287 CGI::checkbox( 288 -name => "showOldAnswers", 289 -checked => $will{showOldAnswers}, 290 -label => "Saved answers", 291 ), " ".CGI::br(); 292 293 $optionLine and $optionLine .= join "", CGI::br(); 294 295 my %display_modes = %{WeBWorK::PG::DISPLAY_MODES()}; 296 my @active_modes = grep { exists $display_modes{$_} } 297 @{$ce->{pg}->{displayModes}}; 298 my $modeLine = (scalar(@active_modes) > 1) ? 299 "View equations as: ".CGI::br(). 300 CGI::radio_group( 301 -name => "displayMode", 302 -values => \@active_modes, 303 -default => $displayMode, 304 -linebreak=>'true', 305 -labels => { 306 plainText => "plain", 307 formattedText => "formatted", 308 images => "images", 309 jsMath => "jsMath", 310 asciimath => "asciimath", 311 }, 312 ). CGI::br().CGI::hr() : ''; 313 314 return CGI::div({-style=>"border: thin groove; padding: 1ex; margin: 2ex align: left"}, 315 $modeLine, 316 $optionLine, 317 CGI::submit(-name=>"redisplay", -label=>"Apply Options"), 318 ); 319 } 320 321 sub previewAnswer { 322 my ($self, $answerResult, $imgGen) = @_; 323 my $ce = $self->r->ce; 324 my $effectiveUser = $self->{effectiveUser}; 325 my $set = $self->{set}; 326 my $problem = $self->{problem}; 327 my $displayMode = $self->{displayMode}; 328 329 # note: right now, we have to do things completely differently when we are 330 # rendering math from INSIDE the translator and from OUTSIDE the translator. 331 # so we'll just deal with each case explicitly here. there's some code 332 # duplication that can be dealt with later by abstracting out tth/dvipng/etc. 333 334 my $tex = $answerResult->{preview_latex_string}; 335 336 return "" unless defined $tex and $tex ne ""; 337 338 if ($displayMode eq "plainText") { 339 return $tex; 340 } elsif ($displayMode eq "formattedText") { 341 my $tthCommand = $ce->{externalPrograms}->{tth} 342 . " -L -f5 -r 2> /dev/null <<END_OF_INPUT; echo > /dev/null\n" 343 . "\\(".$tex."\\)\n" 344 . "END_OF_INPUT\n"; 345 346 # call tth 347 my $result = `$tthCommand`; 348 if ($?) { 349 return "<b>[tth failed: $? $@]</b>"; 350 } else { 351 return $result; 352 } 353 } elsif ($displayMode eq "images") { 354 $imgGen->add($tex); 355 } elsif ($displayMode eq "jsMath") { 356 return '<DIV CLASS="math">'.$tex.'</DIV>' ; 357 } 358 } 359 360 ################################################################################ 361 # Template escape implementations 362 ################################################################################ 363 364 sub pre_header_initialize { 365 my ($self) = @_; 366 my $r = $self->r; 367 my $ce = $r->ce; 368 my $db = $r->db; 369 my $authz = $r->authz; 370 my $urlpath = $r->urlpath; 371 372 my $setName = $urlpath->arg("setID"); 373 my $problemNumber = $r->urlpath->arg("problemID"); 374 my $userName = $r->param('user'); 375 my $effectiveUserName = $r->param('effectiveUser'); 376 my $key = $r->param('key'); 377 378 my $user = $db->getUser($userName); # checked 379 die "record for user $userName (real user) does not exist." 380 unless defined $user; 381 382 my $effectiveUser = $db->getUser($effectiveUserName); # checked 383 die "record for user $effectiveUserName (effective user) does not exist." 384 unless defined $effectiveUser; 385 386 my $PermissionLevel = $db->getPermissionLevel($userName); # checked 387 die "permission level record for user $userName does not exist (but the user does? odd...)" 388 unless defined $PermissionLevel; 389 my $permissionLevel = $PermissionLevel->permission; 390 391 # obtain the merged set for $effectiveUser 392 my $set = $db->getMergedSet($effectiveUserName, $setName); # checked 393 394 # obtain the merged problem for $effectiveUser 395 my $problem = $db->getMergedProblem($effectiveUserName, $setName, $problemNumber); # checked 396 397 my $editMode = $r->param("editMode"); 398 399 if ($authz->hasPermissions($userName, "modify_problem_sets")) { 400 # professors are allowed to fabricate sets and problems not 401 # assigned to them (or anyone). this allows them to use the 402 # editor to 403 404 # if that is not yet defined obtain the global set, convert 405 # it to a user set, and add fake user data 406 unless (defined $set) { 407 my $userSetClass = $db->{set_user}->{record}; 408 my $globalSet = $db->getGlobalSet($setName); # checked 409 # if the global set doesn't exist either, bail! 410 if(not defined $globalSet) { 411 $set = fake_set($db); 412 } else { 413 $set = global2user($userSetClass, $globalSet); 414 $set->psvn(0); 415 416 # FIXME: This is a temporary fix to fill in the database 417 # We want the published field to contain either 1 or 0 so if it has not been set to 0, default to 1 418 # this will fill in all the empty fields but not change anything that has been specifically set to 1 or 0 419 $globalSet->published("1") unless $globalSet->published eq "0"; 420 $db->putGlobalSet($globalSet); 421 } 422 } 423 424 # if that is not yet defined obtain the global problem, 425 # convert it to a user problem, and add fake user data 426 unless (defined $problem) { 427 my $userProblemClass = $db->{problem_user}->{record}; 428 my $globalProblem = $db->getGlobalProblem($setName, $problemNumber); # checked 429 # if the global problem doesn't exist either, bail! 430 if(not defined $globalProblem) { 431 my $sourceFilePath = $r->param("sourceFilePath"); 432 # These are problems from setmaker. If declared invalid, they won't come up 433 $self->{invalidProblem} = $self->{invalidSet} = 1 unless defined $sourceFilePath; 434 # die "Problem $problemNumber in set $setName does not exist" unless defined $sourceFilePath; 435 $problem = fake_problem($db); 436 $problem->problem_id(1); 437 $problem->source_file($sourceFilePath); 438 $problem->user_id($effectiveUserName); 439 } else { 440 $problem = global2user($userProblemClass, $globalProblem); 441 $problem->user_id($effectiveUserName); 442 $problem->problem_seed(0); 443 $problem->status(0); 444 $problem->attempted(0); 445 $problem->last_answer(""); 446 $problem->num_correct(0); 447 $problem->num_incorrect(0); 448 } 449 } 450 451 # now we're sure we have valid UserSet and UserProblem objects 452 # yay! 453 454 # now deal with possible editor overrides: 455 456 # if the caller is asking to override the source file, and 457 # editMode calls for a temporary file, do so 458 my $sourceFilePath = $r->param("sourceFilePath"); 459 if (defined $sourceFilePath and 460 (not defined $editMode or $editMode eq "temporaryFile")) { 461 $problem->source_file($sourceFilePath); 462 } 463 464 # if the problem does not have a source file or no source file has been passed in 465 # then this is really an invalid problem (probably from a bad URL) 466 $self->{invalidProblem} = not (defined $sourceFilePath or $problem->source_file); 467 468 # if the caller is asking to override the problem seed, do so 469 my $problemSeed = $r->param("problemSeed"); 470 if (defined $problemSeed) { 471 $problem->problem_seed($problemSeed); 472 } 473 474 my $publishedClass = ($set->published) ? "Published" : "Unpublished"; 475 my $publishedText = ($set->published) ? "visible to students." : "hidden from students."; 476 $self->addmessage(CGI::p("This set is " . CGI::font({class=>$publishedClass}, $publishedText))); 477 } else { 478 479 # students can't view problems not assigned to them 480 481 # A set is valid if it exists and if it is either published or the user is privileged. 482 $self->{invalidSet} = ((grep /^$setName/, $db->listUserSets($effectiveUserName)) == 0) 483 || not defined $set 484 || !($set->published || $authz->hasPermissions($userName, "view_unpublished_sets")); 485 $self->{invalidProblem} = ((grep /^$problemNumber/, $db->listUserProblems($effectiveUserName, $setName)) == 0) 486 || not defined $problem 487 || !($set->published || $authz->hasPermissions($userName, "view_unpublished_sets")); 488 489 $self->addbadmessage(CGI::p("This problem will not count towards your grade.")) if $problem and not $problem->value and not $self->{invalidProblem}; 490 } 491 492 $self->{userName} = $userName; 493 $self->{effectiveUserName} = $effectiveUserName; 494 $self->{user} = $user; 495 $self->{effectiveUser} = $effectiveUser; 496 $self->{permissionLevel} = $permissionLevel; 497 $self->{set} = $set; 498 $self->{problem} = $problem; 499 $self->{editMode} = $editMode; 500 501 ##### form processing ##### 502 503 # set options from form fields (see comment at top of file for names) 504 my $displayMode = $r->param("displayMode") || $ce->{pg}->{options}->{displayMode}; 505 my $redisplay = $r->param("redisplay"); 506 my $submitAnswers = $r->param("submitAnswers"); 507 my $checkAnswers = $r->param("checkAnswers"); 508 my $previewAnswers = $r->param("previewAnswers"); 509 510 my $formFields = { WeBWorK::Form->new_from_paramable($r)->Vars }; 511 512 $self->{displayMode} = $displayMode; 513 $self->{redisplay} = $redisplay; 514 $self->{submitAnswers} = $submitAnswers; 515 $self->{checkAnswers} = $checkAnswers; 516 $self->{previewAnswers} = $previewAnswers; 517 $self->{formFields} = $formFields; 518 519 # get result and send to message 520 my $success = $r->param("sucess"); 521 my $failure = $r->param("failure"); 522 $self->addbadmessage(CGI::p($failure)) if $failure; 523 $self->addgoodmessage(CGI::p($success)) if $success; 524 525 # now that we've set all the necessary variables quit out if the set or problem is invalid 526 return if $self->{invalidSet} || $self->{invalidProblem}; 527 528 ##### permissions ##### 529 530 # are we allowed to view this problem? 531 $self->{isOpen} = after($set->open_date) || $authz->hasPermissions($userName, "view_unopened_sets"); 532 return unless $self->{isOpen}; 533 534 # what does the user want to do? 535 my %want = ( 536 showOldAnswers => $r->param("showOldAnswers") || $ce->{pg}->{options}->{showOldAnswers}, 537 showCorrectAnswers => $r->param("showCorrectAnswers") || $ce->{pg}->{options}->{showCorrectAnswers}, 538 showHints => $r->param("showHints") || $ce->{pg}->{options}->{showHints}, 539 showSolutions => $r->param("showSolutions") || $ce->{pg}->{options}->{showSolutions}, 540 recordAnswers => $submitAnswers, 541 checkAnswers => $checkAnswers, 542 ); 543 544 # are certain options enforced? 545 my %must = ( 546 showOldAnswers => 0, 547 showCorrectAnswers => 0, 548 showHints => 0, 549 showSolutions => 0, 550 recordAnswers => ! $authz->hasPermissions($userName, "avoid_recording_answers"), 551 checkAnswers => 0, 552 ); 553 554 # does the user have permission to use certain options? 555 my @args = ($user, $PermissionLevel, $effectiveUser, $set, $problem); 556 my %can = ( 557 showOldAnswers => $self->can_showOldAnswers(@args), 558 showCorrectAnswers => $self->can_showCorrectAnswers(@args), 559 showHints => $self->can_showHints(@args), 560 showSolutions => $self->can_showSolutions(@args), 561 recordAnswers => $self->can_recordAnswers(@args), 562 checkAnswers => $self->can_checkAnswers(@args), 563 ); 564 565 # # does the user have permission to use certain options? 566 # my %can = ( 567 # showOldAnswers => 1, 568 # showCorrectAnswers => canShowCorrectAnswers($permissionLevel, $set->answer_date), 569 # showHints => 1, 570 # showSolutions => canShowSolutions($permissionLevel, $set->answer_date), 571 # recordAnswers => canRecordAnswers($permissionLevel, $set->open_date, $set->due_date, 572 # $problem->max_attempts, $problem->num_correct + $problem->num_incorrect + 1), 573 # # attempts=num_correct+num_incorrect+1, as this happens before updating $problem 574 # checkAnswers => canCheckAnswers($permissionLevel, $set->due_date), 575 # ); 576 # 577 # # more complicated logic for showing check answer button: 578 # # checkAnswers button shows up after due date -- once a student can't record anymore 579 # # checkAnswers button always shows up when an instructor or TA is acting 580 # # as someone else (the $user and $effectiveUserName aren't the same). 581 # $can{checkAnswers} = ( 582 # # $can{recordAnswers} will be false if the due date has passed OR the 583 # # student has used up all of her attempts 584 # ($can{checkAnswers} and not $can{recordAnswers}) 585 # or 586 # ( 587 # # FIXME: this is not the right way to check for this. 588 # # also, canCheckAnswers() will show this button if the permission 589 # # level is positive, which is always true when an instructor is 590 # # acting as a student 591 # defined($userName) 592 # and 593 # defined($effectiveUserName) 594 # and 595 # ($userName ne $effectiveUserName) 596 # ) 597 # ); 598 # 599 # # more complicated logic for showing "submit answer" button: 600 # # We hide the submit answer button if someone is acting as a student 601 # # This prevents errors where you accidently submit the answer for a student 602 # # Not sure whether this a feature or a bug 603 # $can{recordAnswers} = ( 604 # $can{recordAnswers} 605 # and not 606 # ( 607 # # FIXME: this is not the right way to check for this. 608 # defined($userName) 609 # and 610 # defined($effectiveUserName) 611 # and 612 # ($userName ne $effectiveUserName) 613 # ) 614 # ); 615 616 # final values for options 617 my %will; 618 foreach (keys %must) { 619 $will{$_} = $can{$_} && ($want{$_} || $must{$_}); 620 } 621 622 ##### sticky answers ##### 623 624 if (not ($submitAnswers or $previewAnswers or $checkAnswers) and $will{showOldAnswers}) { 625 # do this only if new answers are NOT being submitted 626 my %oldAnswers = decodeAnswers($problem->last_answer); 627 $formFields->{$_} = $oldAnswers{$_} foreach keys %oldAnswers; 628 } 629 630 ##### translation ##### 631 632 $WeBWorK::timer->continue("begin pg processing") if defined($WeBWorK::timer); 633 my $pg = WeBWorK::PG->new( 634 $ce, 635 $effectiveUser, 636 $key, 637 $set, 638 $problem, 639 $set->psvn, # FIXME: this field should be removed 640 $formFields, 641 { # translation options 642 displayMode => $displayMode, 643 showHints => $will{showHints}, 644 showSolutions => $will{showSolutions}, 645 refreshMath2img => $will{showHints} || $will{showSolutions}, 646 processAnswers => 1, 647 }, 648 ); 649 650 $WeBWorK::timer->continue("end pg processing") if defined($WeBWorK::timer); 651 652 ##### fix hint/solution options ##### 653 654 $can{showHints} &&= $pg->{flags}->{hintExists} 655 &&= $pg->{flags}->{showHintLimit}<=$pg->{state}->{num_of_incorrect_ans}; 656 $can{showSolutions} &&= $pg->{flags}->{solutionExists}; 657 658 ##### store fields ##### 659 660 $self->{want} = \%want; 661 $self->{must} = \%must; 662 $self->{can} = \%can; 663 $self->{will} = \%will; 664 $self->{pg} = $pg; 665 } 666 667 sub if_errors($$) { 668 my ($self, $arg) = @_; 669 670 if ($self->{isOpen}) { 671 return $self->{pg}->{flags}->{error_flag} ? $arg : !$arg; 672 } else { 673 return !$arg; 674 } 675 } 676 677 sub head { 678 my ($self) = @_; 679 680 return "" unless $self->{isOpen}; 681 return $self->{pg}->{head_text} if $self->{pg}->{head_text}; 682 } 683 684 sub options { 685 my ($self) = @_; 686 687 return "" if $self->{invalidProblem}; 688 my $sourceFilePathfield = ''; 689 if($self->r->param("sourceFilePath")) { 690 $sourceFilePathfield = CGI::hidden(-name => "sourceFilePath", 691 -value => $self->r->param("sourceFilePath")); 692 } 693 694 return join("", 695 CGI::start_form("POST", $self->{r}->uri), 696 $self->hidden_authen_fields, 697 $sourceFilePathfield, 698 CGI::hr(), 699 CGI::start_div({class=>"viewOptions"}), 700 $self->viewOptions(), 701 CGI::end_div(), 702 CGI::end_form() 703 ); 704 } 705 706 sub siblings { 707 my ($self) = @_; 708 my $r = $self->r; 709 my $db = $r->db; 710 my $urlpath = $r->urlpath; 711 712 # can't show sibling problems if the set is invalid 713 return "" if $self->{invalidSet}; 714 715 my $courseID = $urlpath->arg("courseID"); 716 my $setID = $self->{set}->set_id; 717 my $eUserID = $r->param("effectiveUser"); 718 my @problemIDs = sort { $a <=> $b } $db->listUserProblems($eUserID, $setID); 719 720 print CGI::start_ul({class=>"LinksMenu"}); 721 print CGI::start_li(); 722 print CGI::span({style=>"font-size:larger"}, "Problems"); 723 print CGI::start_ul(); 724 725 foreach my $problemID (@problemIDs) { 726 my $problemPage = $urlpath->newFromModule("WeBWorK::ContentGenerator::Problem", 727 courseID => $courseID, setID => $setID, problemID => $problemID); 728 print CGI::li(CGI::a({href=>$self->systemLink($problemPage, params=>{displayMode => $self->{displayMode}})}, "Problem $problemID")); 729 } 730 731 print CGI::end_ul(); 732 print CGI::end_li(); 733 print CGI::end_ul(); 734 735 return ""; 736 } 737 738 sub nav { 739 my ($self, $args) = @_; 740 my $r = $self->r; 741 my $db = $r->db; 742 my $urlpath = $r->urlpath; 743 744 my $courseID = $urlpath->arg("courseID"); 745 my $setID = $self->{set}->set_id if !($self->{invalidSet}); 746 my $problemID = $self->{problem}->problem_id if !($self->{invalidProblem}); 747 my $eUserID = $r->param("effectiveUser"); 748 749 my ($prevID, $nextID); 750 751 if (!$self->{invalidProblem}) { 752 my @problemIDs = $db->listUserProblems($eUserID, $setID); 753 foreach my $id (@problemIDs) { 754 $prevID = $id if $id < $problemID 755 and (not defined $prevID or $id > $prevID); 756 $nextID = $id if $id > $problemID 757 and (not defined $nextID or $id < $nextID); 758 } 759 } 760 761 my @links; 762 763 if ($prevID) { 764 my $prevPage = $urlpath->newFromModule(__PACKAGE__, 765 courseID => $courseID, setID => $setID, problemID => $prevID); 766 push @links, "Previous Problem", $r->location . $prevPage->path, "navPrev"; 767 } else { 768 push @links, "Previous Problem", "", "navPrev"; 769 } 770 771 push @links, "Problem List", $r->location . $urlpath->parent->path, "navProbList"; 772 773 if ($nextID) { 774 my $nextPage = $urlpath->newFromModule(__PACKAGE__, 775 courseID => $courseID, setID => $setID, problemID => $nextID); 776 push @links, "Next Problem", $r->location . $nextPage->path, "navNext"; 777 } else { 778 push @links, "Next Problem", "", "navNext"; 779 } 780 781 my $tail = "&displayMode=".$self->{displayMode}; 782 return $self->navMacro($args, $tail, @links); 783 } 784 785 sub title { 786 my ($self) = @_; 787 788 # using the url arguments won't break if the set/problem are invalid 789 my $setID = $self->r->urlpath->arg("setID"); 790 my $problemID = $self->r->urlpath->arg("problemID"); 791 792 return "$setID : $problemID"; 793 } 794 795 sub body { 796 my $self = shift; 797 my $r = $self->r; 798 my $ce = $r->ce; 799 my $db = $r->db; 800 my $authz = $r->authz; 801 my $urlpath = $r->urlpath; 802 my $user = $r->param('user'); 803 804 if ($self->{invalidSet}) { 805 return CGI::div({class=>"ResultsWithError"}, 806 CGI::p("The selected problem set (" . $urlpath->arg("setID") . ") is not a valid set for " . $r->param("effectiveUser") . ".")); 807 } 808 809 if ($self->{invalidProblem}) { 810 return CGI::div({class=>"ResultsWithError"}, 811 CGI::p("The selected problem (" . $urlpath->arg("problemID") . ") is not a valid problem for set " . $self->{set}->set_id . ".")); 812 } 813 814 unless ($self->{isOpen}) { 815 return CGI::div({class=>"ResultsWithError"}, 816 CGI::p("This problem is not available because the problem set that contains it is not yet open.")); 817 } 818 # unpack some useful variables 819 my $set = $self->{set}; 820 my $problem = $self->{problem}; 821 my $editMode = $self->{editMode}; 822 my $permissionLevel = $self->{permissionLevel}; 823 my $submitAnswers = $self->{submitAnswers}; 824 my $checkAnswers = $self->{checkAnswers}; 825 my $previewAnswers = $self->{previewAnswers}; 826 my %want = %{ $self->{want} }; 827 my %can = %{ $self->{can} }; 828 my %must = %{ $self->{must} }; 829 my %will = %{ $self->{will} }; 830 my $pg = $self->{pg}; 831 832 my $courseName = $urlpath->arg("courseID"); 833 834 # FIXME: move editor link to top, next to problem number. 835 # format as "[edit]" like we're doing with course info file, etc. 836 # add edit link for set as well. 837 my $editorLink = ""; 838 # if we are here without a real problem set, carry that through 839 my $forced_field = []; 840 $forced_field = ['sourceFilePath' => $r->param("sourceFilePath")] if 841 ($set->set_id eq 'Undefined_Set'); 842 if ($authz->hasPermissions($user, "modify_problem_sets")) { 843 my $editorPage = $urlpath->newFromModule("WeBWorK::ContentGenerator::Instructor::PGProblemEditor", 844 courseID => $courseName, setID => $set->set_id, problemID => $problem->problem_id); 845 my $editorURL = $self->systemLink($editorPage, params=>$forced_field); 846 $editorLink = CGI::a({href=>$editorURL}, "Edit this problem"); 847 } 848 849 ##### translation errors? ##### 850 851 if ($pg->{flags}->{error_flag}) { 852 print $self->errorOutput($pg->{errors}, $pg->{body_text}); 853 print $editorLink; 854 return ""; 855 } 856 857 ##### answer processing ##### 858 $WeBWorK::timer->continue("begin answer processing") if defined($WeBWorK::timer); 859 # if answers were submitted: 860 my $scoreRecordedMessage; 861 my $pureProblem; 862 if ($submitAnswers) { 863 # get a "pure" (unmerged) UserProblem to modify 864 # this will be undefined if the problem has not been assigned to this user 865 $pureProblem = $db->getUserProblem($problem->user_id, $problem->set_id, $problem->problem_id); # checked 866 if (defined $pureProblem) { 867 # store answers in DB for sticky answers 868 my %answersToStore; 869 my %answerHash = %{ $pg->{answers} }; 870 $answersToStore{$_} = $self->{formFields}->{$_} #$answerHash{$_}->{original_student_ans} -- this may have been modified for fields with multiple values. Don't use it!! 871 foreach (keys %answerHash); 872 873 # There may be some more answers to store -- one which are auxiliary entries to a primary answer. Evaluating 874 # matrices works in this way, only the first answer triggers an answer evaluator, the rest are just inputs 875 # however we need to store them. Fortunately they are still in the input form. 876 my @extra_answer_names = @{ $pg->{flags}->{KEPT_EXTRA_ANSWERS}}; 877 $answersToStore{$_} = $self->{formFields}->{$_} foreach (@extra_answer_names); 878 879 # Now let's encode these answers to store them -- append the extra answers to the end of answer entry order 880 my @answer_order = (@{$pg->{flags}->{ANSWER_ENTRY_ORDER}}, @extra_answer_names); 881 my $answerString = encodeAnswers(%answersToStore, 882 @answer_order); 883 884 # store last answer to database 885 $problem->last_answer($answerString); 886 $pureProblem->last_answer($answerString); 887 $db->putUserProblem($pureProblem); 888 889 # store state in DB if it makes sense 890 if ($will{recordAnswers}) { 891 $problem->status($pg->{state}->{recorded_score}); 892 $problem->attempted(1); 893 $problem->num_correct($pg->{state}->{num_of_correct_ans}); 894 $problem->num_incorrect($pg->{state}->{num_of_incorrect_ans}); 895 $pureProblem->status($pg->{state}->{recorded_score}); 896 $pureProblem->attempted(1); 897 $pureProblem->num_correct($pg->{state}->{num_of_correct_ans}); 898 $pureProblem->num_incorrect($pg->{state}->{num_of_incorrect_ans}); 899 if ($db->putUserProblem($pureProblem)) { 900 $scoreRecordedMessage = "Your score was recorded."; 901 } else { 902 $scoreRecordedMessage = "Your score was not recorded because there was a failure in storing the problem record to the database."; 903 } 904 # write to the transaction log, just to make sure 905 writeLog($self->{ce}, "transaction", 906 $problem->problem_id."\t". 907 $problem->set_id."\t". 908 $problem->user_id."\t". 909 $problem->source_file."\t". 910 $problem->value."\t". 911 $problem->max_attempts."\t". 912 $problem->problem_seed."\t". 913 $pureProblem->status."\t". 914 $pureProblem->attempted."\t". 915 $pureProblem->last_answer."\t". 916 $pureProblem->num_correct."\t". 917 $pureProblem->num_incorrect 918 ); 919 } else { 920 if (before($set->open_date) or after($set->due_date)) { 921 $scoreRecordedMessage = "Your score was not recorded because this problem set is closed."; 922 } else { 923 $scoreRecordedMessage = "Your score was not recorded."; 924 } 925 } 926 } else { 927 $scoreRecordedMessage = "Your score was not recorded because this problem has not been assigned to you."; 928 } 929 } 930 931 # logging student answers 932 933 my $answer_log = $self->{ce}->{courseFiles}->{logs}->{'answer_log'}; 934 if ( defined($answer_log ) and defined($pureProblem)) { 935 if ($submitAnswers ) { 936 my $answerString = ""; 937 my %answerHash = %{ $pg->{answers} }; 938 # FIXME this is the line 552 error. make sure original student ans is defined. 939 # The fact that it is not defined is probably due to an error in some answer evaluator. 940 # But I think it is useful to suppress this error message in the log. 941 foreach (sort keys %answerHash) { 942 my $student_ans = $answerHash{$_}->{original_student_ans} ||''; 943 $answerString .= $student_ans."\t" 944 } 945 $answerString = '' unless defined($answerString); # insure string is defined. 946 writeCourseLog($self->{ce}, "answer_log", 947 join("", 948 '|', $problem->user_id, 949 '|', $problem->set_id, 950 '|', $problem->problem_id, 951 '|',"\t", 952 time(),"\t", 953 $answerString, 954 ), 955 ); 956 957 } 958 } 959 960 $WeBWorK::timer->continue("end answer processing") if defined($WeBWorK::timer); 961 962 ##### output ##### 963 964 print CGI::start_div({class=>"problemHeader"}); 965 966 # custom message for editor 967 if ($authz->hasPermissions($user, "modify_problem_sets") and defined $editMode) { 968 if ($editMode eq "temporaryFile") { 969 print CGI::p(CGI::i("Editing temporary file: ", $problem->source_file)); 970 } elsif ($editMode eq "savedFile") { 971 # taken care of in the initialization phase 972 } 973 } 974 975 # attempt summary 976 #FIXME -- the following is a kludge: if showPartialCorrectAnswers is negative don't show anything. 977 # until after the due date 978 # do I need to check $will{showCorrectAnswers} to make preflight work?? 979 if (($pg->{flags}->{showPartialCorrectAnswers} >= 0 and $submitAnswers) ) { 980 # print this if user submitted answers OR requested correct answers 981 982 print $self->attemptResults($pg, 1, 983 $will{showCorrectAnswers}, 984 $pg->{flags}->{showPartialCorrectAnswers}, 1, 1); 985 } elsif ($checkAnswers) { 986 # print this if user previewed answers 987 print CGI::div({class=>'ResultsWithError'},"ANSWERS ONLY CHECKED -- ",CGI::br(),"ANSWERS NOT RECORDED", CGI::br() ); 988 print $self->attemptResults($pg, 1, $will{showCorrectAnswers}, 1, 1, 1); 989 # show attempt answers 990 # show correct answers if asked 991 # show attempt results (correctness) 992 # show attempt previews 993 } elsif ($previewAnswers) { 994 # print this if user previewed answers 995 print CGI::div({class=>'ResultsWithError'},"PREVIEW ONLY -- NOT RECORDED"),CGI::br(),$self->attemptResults($pg, 1, 0, 0, 0, 1); 996 # show attempt answers 997 # don't show correct answers 998 # don't show attempt results (correctness) 999 # show attempt previews 1000 } 1001 1002 print CGI::end_div(); 1003 1004 # main form 1005 print CGI::startform("POST", $r->uri); 1006 print $self->hidden_authen_fields; 1007 1008 print CGI::start_div({class=>"problem"}); 1009 print CGI::p($pg->{body_text}); 1010 print CGI::p(CGI::b("Note: "), CGI::i($pg->{result}->{msg})) if $pg->{result}->{msg}; 1011 print CGI::end_div(); 1012 1013 print CGI::start_p(); 1014 1015 if ($can{showCorrectAnswers}) { 1016 print CGI::checkbox( 1017 -name => "showCorrectAnswers", 1018 -checked => $will{showCorrectAnswers}, 1019 -label => "Show correct answers", 1020 ); 1021 } 1022 if ($can{showHints}) { 1023 print CGI::div({style=>"color:red"}, 1024 CGI::checkbox( 1025 -name => "showHints", 1026 -checked => $will{showHints}, 1027 -label => "Show Hints", 1028 ) 1029 ); 1030 } 1031 if ($can{showSolutions}) { 1032 print CGI::checkbox( 1033 -name => "showSolutions", 1034 -checked => $will{showSolutions}, 1035 -label => "Show Solutions", 1036 ); 1037 } 1038 1039 if ($can{showCorrectAnswers} or $can{showHints} or $can{showSolutions}) { 1040 print CGI::br(); 1041 } 1042 1043 print CGI::submit(-name=>"previewAnswers", -label=>"Preview Answers"); 1044 if ($can{checkAnswers}) { 1045 print CGI::submit(-name=>"checkAnswers", -label=>"Check Answers"); 1046 } 1047 if ($can{recordAnswers}) { 1048 print CGI::submit(-name=>"submitAnswers", -label=>"Submit Answers"); 1049 } 1050 1051 print CGI::end_p(); 1052 1053 print CGI::start_div({class=>"scoreSummary"}); 1054 1055 # score summary 1056 my $attempts = $problem->num_correct + $problem->num_incorrect; 1057 my $attemptsNoun = $attempts != 1 ? "times" : "time"; 1058 my $lastScore = sprintf("%.0f%%", $problem->status * 100); # Round to whole number 1059 my ($attemptsLeft, $attemptsLeftNoun); 1060 if ($problem->max_attempts == -1) { 1061 # unlimited attempts 1062 $attemptsLeft = "unlimited"; 1063 $attemptsLeftNoun = "attempts"; 1064 } else { 1065 $attemptsLeft = $problem->max_attempts - $attempts; 1066 $attemptsLeftNoun = $attemptsLeft == 1 ? "attempt" : "attempts"; 1067 } 1068 1069 my $setClosed = 0; 1070 my $setClosedMessage; 1071 if (before($set->open_date) or after($set->due_date)) { 1072 $setClosed = 1; 1073 $setClosedMessage = "This problem set is closed."; 1074 if ($authz->hasPermissions($user, "view_answers")) { 1075 $setClosedMessage .= " However, since you are a privileged user, additional attempts will be recorded."; 1076 } else { 1077 $setClosedMessage .= " Additional attempts will not be recorded."; 1078 } 1079 } 1080 1081 my $notCountedMessage = ($problem->value) ? "" : "(This problem will not count towards your grade.)"; 1082 print CGI::p( 1083 $submitAnswers ? $scoreRecordedMessage . CGI::br() : "", 1084 "You have attempted this problem $attempts $attemptsNoun.", CGI::br(), 1085 $problem->attempted 1086 ? "Your recorded score is $lastScore. $notCountedMessage" . CGI::br() 1087 : "", 1088 $setClosed ? $setClosedMessage : "You have $attemptsLeft $attemptsLeftNoun remaining." 1089 ); 1090 print CGI::end_div(); 1091 1092 # save state for viewOptions 1093 print CGI::hidden( 1094 -name => "showOldAnswers", 1095 -value => $will{showOldAnswers} 1096 ), 1097 1098 CGI::hidden( 1099 -name => "displayMode", 1100 -value => $self->{displayMode} 1101 ); 1102 print( CGI::hidden( 1103 -name => 'editMode', 1104 -value => $self->{editMode}, 1105 ) 1106 ) if defined($self->{editMode}) and $self->{editMode} eq 'temporaryFile'; 1107 print( CGI::hidden( 1108 -name => 'sourceFilePath', 1109 -value => $self->{problem}->{source_file} 1110 )) if defined($self->{problem}->{source_file}); 1111 1112 print( CGI::hidden( 1113 -name => 'problemSeed', 1114 -value => $r->param("problemSeed") 1115 )) if defined($r->param("problemSeed")); 1116 1117 # end of main form 1118 print CGI::endform(); 1119 1120 print CGI::start_div({class=>"problemFooter"}); 1121 1122 ## arguments for answer inspection button 1123 #my $prof_url = $ce->{webworkURLs}->{oldProf}; 1124 #my $webworkURL = $ce->{webworkURLs}->{root}; 1125 #my $cgi_url = $prof_url; 1126 #$cgi_url=~ s|/[^/]*$||; # clip profLogin.pl 1127 #my $authen_args = $self->url_authen_args(); 1128 #my $showPastAnswersURL = "$webworkURL/$courseName/instructor/show_answers/"; 1129 1130 my $pastAnswersPage = $urlpath->newFromModule("WeBWorK::ContentGenerator::Instructor::ShowAnswers", 1131 courseID => $courseName); 1132 my $showPastAnswersURL = $self->systemLink($pastAnswersPage, authen => 0); # no authen info for form action 1133 1134 # print answer inspection button 1135 if ($authz->hasPermissions($user, "view_answers")) { 1136 print "\n", 1137 CGI::start_form(-method=>"POST",-action=>$showPastAnswersURL,-target=>"information"),"\n", 1138 $self->hidden_authen_fields,"\n", 1139 CGI::hidden(-name => 'courseID', -value=>$courseName), "\n", 1140 CGI::hidden(-name => 'problemID', -value=>$problem->problem_id), "\n", 1141 CGI::hidden(-name => 'setID', -value=>$problem->set_id), "\n", 1142 CGI::hidden(-name => 'studentUser', -value=>$problem->user_id), "\n", 1143 CGI::p( {-align=>"left"}, 1144 CGI::submit(-name => 'action', -value=>'Show Past Answers') 1145 ), "\n", 1146 CGI::endform(); 1147 } 1148 1149 # feedback form url 1150 my $feedbackPage = $urlpath->newFromModule("WeBWorK::ContentGenerator::Feedback", 1151 courseID => $courseName); 1152 my $feedbackURL = $self->systemLink($feedbackPage, authen => 0); # no authen info for form action 1153 1154 #print feedback form 1155 print 1156 CGI::start_form(-method=>"POST", -action=>$feedbackURL),"\n", 1157 $self->hidden_authen_fields,"\n", 1158 CGI::hidden("module", __PACKAGE__),"\n", 1159 CGI::hidden("set", $set->set_id),"\n", 1160 CGI::hidden("problem", $problem->problem_id),"\n", 1161 CGI::hidden("displayMode", $self->{displayMode}),"\n", 1162 CGI::hidden("showOldAnswers", $will{showOldAnswers}),"\n", 1163 CGI::hidden("showCorrectAnswers", $will{showCorrectAnswers}),"\n", 1164 CGI::hidden("showHints", $will{showHints}),"\n", 1165 CGI::hidden("showSolutions", $will{showSolutions}),"\n", 1166 CGI::p({-align=>"left"}, 1167 CGI::submit(-name=>"feedbackForm", -label=>"Email instructor") 1168 ), 1169 CGI::endform(),"\n"; 1170 1171 # FIXME print editor link 1172 print $editorLink; #empty unless it is appropriate to have an editor link. 1173 1174 print CGI::end_div(); 1175 1176 # debugging stuff 1177 if (0) { 1178 print 1179 CGI::hr(), 1180 CGI::h2("debugging information"), 1181 CGI::h3("form fields"), 1182 ref2string($self->{formFields}), 1183 CGI::h3("user object"), 1184 ref2string($self->{user}), 1185 CGI::h3("set object"), 1186 ref2string($set), 1187 CGI::h3("problem object"), 1188 ref2string($problem), 1189 CGI::h3("PG object"), 1190 ref2string($pg, {'WeBWorK::PG::Translator' => 1}); 1191 } 1192 1193 return ""; 1194 } 1195 1196 1;
| aubreyja at gmail dot com | ViewVC Help |
| Powered by ViewVC 1.0.9 |