Parent Directory
|
Revision Log
merge changes from trunk
1 ################################################################################ 2 # WeBWorK Online Homework Delivery System 3 # Copyright © 2000-2007 The WeBWorK Project, http://openwebwork.sf.net/ 4 # $CVSHeader: webwork2/lib/WeBWorK/ContentGenerator/Hardcopy.pm,v 1.102 2009/09/25 00:39:49 gage 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::Hardcopy; 18 use base qw(WeBWorK::ContentGenerator); 19 20 =head1 NAME 21 22 WeBWorK::ContentGenerator::Hardcopy - generate printable versions of one or more 23 problem sets. 24 25 =cut 26 27 use strict; 28 use warnings; 29 30 #use Apache::Constants qw/:common REDIRECT/; 31 #use CGI qw(-nosticky ); 32 use WeBWorK::CGI; 33 34 use File::Path; 35 use File::Temp qw/tempdir/; 36 use String::ShellQuote; 37 use WeBWorK::DB::Utils qw/user2global/; 38 use WeBWorK::Debug; 39 use WeBWorK::Form; 40 use WeBWorK::HTML::ScrollingRecordList qw/scrollingRecordList/; 41 use WeBWorK::PG; 42 use WeBWorK::Utils qw/readFile decodeAnswers/; 43 use PGrandom; 44 45 =head1 CONFIGURATION VARIABLES 46 47 =over 48 49 =item $PreserveTempFiles 50 51 If true, don't delete temporary files. 52 53 =cut 54 55 our $PreserveTempFiles = 0 unless defined $PreserveTempFiles; 56 57 =back 58 59 =cut 60 61 our $HC_DEFAULT_FORMAT = "pdf"; # problems if this is not an allowed format for the user... 62 our %HC_FORMATS = ( 63 tex => { name => "TeX Source", subr => "generate_hardcopy_tex" }, 64 pdf => { name => "Adobe PDF", subr => "generate_hardcopy_pdf" }, 65 tikz =>{ name => "TIKZ PDF file", subr => "generate_hardcopy_tigz"}, 66 ); 67 68 # custom fields used in $self hash 69 # FOR HEAVEN'S SAKE, PLEASE KEEP THIS UP-TO-DATE! 70 # 71 # final_file_url 72 # contains the URL of the final hardcopy file generated 73 # set by generate_hardcopy(), used by pre_header_initialize() and body() 74 # 75 # temp_file_map 76 # reference to a hash mapping temporary file names to URL 77 # set by pre_header_initialize(), used by body() 78 # 79 # hardcopy_errors 80 # reference to array containing HTML strings describing generation errors (and warnings) 81 # used by add_errors(), get_errors(), get_errors_ref() 82 # 83 # at_least_one_problem_rendered_without_error 84 # set to a true value by write_problem_tex if it is able to sucessfully render 85 # a problem. checked by generate_hardcopy to determine whether to continue 86 # with the generation process. 87 # 88 # versioned 89 # set to a true value in write_set_tex if the set_id indicates that 90 # the set being rendered is a versioned set; this is used in 91 # write_problem_tex to determine which problem merging routine from 92 # DB.pm to use, and to indicate what problem number in a versioned 93 # test we're on 94 # 95 # mergedSets 96 # a reference to a hash { userID!setID => setObject }, where setID is 97 # either the set id or the fake versioned set id "setName,vN" depending 98 # on whether the set is a versioned set or not. this may include the 99 # sets for which the hardcopy is being generated (or may not), depending 100 # on whether they were needed to determine the required permissions for 101 # generating a hardcopy 102 # 103 # canShowScore 104 # a reference to a hash { userID!setID => boolean }, where setID is either 105 # the set id or the fake versioned set id "setName,vN" depending on whether 106 # the set is a versioned set or not, and the value of the boolean is 107 # determined by the corresponding userSet's value of hide_score and the 108 # current time 109 110 ################################################################################ 111 # UI subroutines 112 ################################################################################ 113 114 sub pre_header_initialize { 115 my ($self) = @_; 116 my $r = $self->r; 117 my $ce = $r->ce; 118 my $db = $r->db; 119 my $authz = $r->authz; 120 121 my $userID = $r->param("user"); 122 my $eUserID = $r->param("effectiveUser"); 123 my @setIDs = $r->param("selected_sets"); 124 my @userIDs = $r->param("selected_users"); 125 my $hardcopy_format = $r->param("hardcopy_format"); 126 my $generate_hardcopy = $r->param("generate_hardcopy"); 127 my $send_existing_hardcopy = $r->param("send_existing_hardcopy"); 128 my $final_file_url = $r->param("final_file_url"); 129 130 # if there's an existing hardcopy file that can be sent, get set up to do that 131 if ($send_existing_hardcopy) { 132 $self->reply_with_redirect($final_file_url); 133 $self->{final_file_url} = $final_file_url; 134 $self->{send_hardcopy} = 1; 135 return; 136 } 137 138 # this should never happen, but apparently it did once (see bug #714), so we check for it 139 die "Parameter 'user' not defined -- this should never happen" unless defined $userID; 140 141 if ($generate_hardcopy) { 142 my $validation_failed = 0; 143 144 # set default format 145 $hardcopy_format = $HC_DEFAULT_FORMAT unless defined $hardcopy_format; 146 147 # make sure format is valid 148 unless (grep { $_ eq $hardcopy_format } keys %HC_FORMATS) { 149 $self->addbadmessage("'$hardcopy_format' is not a valid hardcopy format."); 150 $validation_failed = 1; 151 } 152 153 # make sure we are allowed to generate hardcopy in this format 154 unless ($authz->hasPermissions($userID, "download_hardcopy_format_$hardcopy_format")) { 155 $self->addbadmessage("You do not have permission to generate hardcopy in $hardcopy_format format."); 156 $validation_failed = 1; 157 } 158 159 # is there at least one user and set selected? 160 unless (@userIDs) { 161 $self->addbadmessage("Please select at least one user and try again."); 162 $validation_failed = 1; 163 } 164 165 # when students don't select any sets the size of @setIDs is 1 with a null character in $setIDs[0]. 166 # when professors don't select any sets the size of @setIDs is 0. 167 # the following test "unless ((@setIDs) and ($setIDs[0] =~ /\S+/))" catches both cases and prevents 168 # warning messages in the case of a professor's empty array. 169 unless ((@setIDs) and ($setIDs[0] =~ /\S+/)) { 170 $self->addbadmessage("Please select at least one set and try again."); 171 $validation_failed = 1; 172 } 173 174 # is the user allowed to request multiple sets/users at a time? 175 my $perm_multiset = $authz->hasPermissions($userID, "download_hardcopy_multiset"); 176 my $perm_multiuser = $authz->hasPermissions($userID, "download_hardcopy_multiuser"); 177 178 my $perm_viewhidden = $authz->hasPermissions($userID, "view_hidden_work"); 179 my $perm_viewfromip = $authz->hasPermissions($userID, "view_ip_restricted_sets"); 180 181 if (@setIDs > 1 and not $perm_multiset) { 182 $self->addbadmessage("You are not permitted to generate hardcopy for multiple sets. Please select a single set and try again."); 183 $validation_failed = 1; 184 } 185 if (@userIDs > 1 and not $perm_multiuser) { 186 $self->addbadmessage("You are not permitted to generate hardcopy for multiple users. Please select a single user and try again."); 187 $validation_failed = 1; 188 } 189 if (@userIDs and $userIDs[0] ne $eUserID and not $perm_multiuser) { 190 $self->addbadmessage("You are not permitted to generate hardcopy for other users."); 191 $validation_failed = 1; 192 # FIXME -- download_hardcopy_multiuser controls both whether a user can generate hardcopy 193 # that contains sets for multiple users AND whether she can generate hardcopy that contains 194 # sets for users other than herself. should these be separate permission levels? 195 } 196 197 # to check if the set has a "hide_work" flag, or if we aren't 198 # allowed to view the set from the user's IP address, we 199 # need the userset objects; if we've not failed validation 200 # yet, get those to check on this 201 my %canShowScore = (); 202 my %mergedSets = (); 203 unless ($validation_failed ) { 204 foreach my $sid ( @setIDs ) { 205 my($s,undef,$v) = ($sid =~ /([^,]+)(,v(\d+))?$/); 206 foreach my $uid ( @userIDs ) { 207 if ( $perm_viewhidden && $perm_viewfromip ) { 208 $canShowScore{"$uid!$sid"} = 1; 209 } else { 210 my $userSet; 211 if ( defined($v) ) { 212 $userSet = $db->getMergedSetVersion($uid,$s,$v); 213 } else { 214 $userSet = $db->getMergedSet($uid,$s); 215 } 216 $mergedSets{"$uid!$sid"} = $userSet; 217 if ( ! $perm_viewhidden && 218 defined( $userSet->hide_work ) && 219 ( $userSet->hide_work eq 'Y' || 220 ( $userSet->hide_work eq 'BeforeAnswerDate' && 221 time < $userSet->answer_date ) ) ) { 222 $validation_failed = 1; 223 $self->addbadmessage("You are not permitted to generate a hardcopy for a set with hidden work."); 224 last; 225 } 226 227 if ( $authz->invalidIPAddress($userSet) ) { 228 $validation_failed = 1; 229 $self->addbadmessage("You are not allowed to generate a " . 230 "hardcopy for " . $userSet->set_id . 231 " from your IP address, " . 232 $r->connection->remote_ip . "."); 233 last; 234 } 235 236 $canShowScore{"$uid!$sid"} = 237 ( ! defined( $userSet->hide_score ) || 238 $userSet->hide_score eq '' ) || 239 ( $userSet ->hide_score eq 'N' || 240 ( $userSet->hide_score eq 'BeforeAnswerDate' && 241 time >= $userSet->answer_date ) ); 242 # die("hide_score = ", $userSet->hide_score, "; canshow{$uid!$sid} = ", (($canShowScore{"$uid!$sid"})?"True":"False"), "\n"); 243 244 } 245 last if $validation_failed; 246 } 247 } 248 } 249 250 unless ($validation_failed) { 251 $self->{canShowScore} = \%canShowScore; 252 $self->{mergedSets} = \%mergedSets; 253 my ($final_file_url, %temp_file_map) = $self->generate_hardcopy($hardcopy_format, \@userIDs, \@setIDs); 254 if ($self->get_errors) { 255 # store the URLs in self hash so that body() can make a link to it 256 $self->{final_file_url} = $final_file_url; 257 $self->{temp_file_map} = \%temp_file_map; 258 } else { 259 # send the file only 260 $self->reply_with_redirect($final_file_url); 261 } 262 } 263 } 264 } 265 266 sub body { 267 my ($self) = @_; 268 my $userID = $self->r->param("user"); 269 my $perm_view_errors = $self->r->authz->hasPermissions($userID, "download_hardcopy_view_errors"); 270 $perm_view_errors = (defined($perm_view_errors) ) ? $perm_view_errors : 0; 271 if (my $num = $self->get_errors) { 272 my $final_file_url = $self->{final_file_url}; 273 my %temp_file_map = %{$self->{temp_file_map}}; 274 if($perm_view_errors) { 275 my $errors_str = $num > 1 ? "errors" : "error"; 276 print CGI::p("$num $errors_str occured while generating hardcopy:"); 277 278 print CGI::ul(CGI::li($self->get_errors_ref)); 279 } 280 281 if ($final_file_url) { 282 print CGI::p( 283 "A hardcopy file was generated, but it may not be complete or correct.", 284 "Please check that no problems are missing and that they are all legible." , 285 "If not, please inform your instructor.<br />", 286 CGI::a({href=>$final_file_url}, "Download Hardcopy"), 287 ); 288 } else { 289 print CGI::p( 290 "WeBWorK was unable to generate a paper copy of this homework set. Please inform your instructor. " 291 ); 292 293 } 294 if($perm_view_errors) { 295 if (%temp_file_map) { 296 print CGI::start_p(); 297 print "You can also examine the following temporary files: "; 298 my $first = 1; 299 while (my ($temp_file_name, $temp_file_url) = each %temp_file_map) { 300 if ($first) { 301 $first = 0; 302 } else { 303 print ", "; 304 } 305 print CGI::a({href=>$temp_file_url}, " $temp_file_name"); 306 } 307 print CGI::end_p(); 308 } 309 } 310 311 print CGI::hr(); 312 } 313 314 # don't display the retry form if there are errors and the user doesn't have permission to view the errors. 315 unless ($self->get_errors and not $perm_view_errors) { 316 $self->display_form(); 317 } 318 ''; # return a blank 319 } 320 321 sub display_form { 322 my ($self) = @_; 323 my $r = $self->r; 324 my $db = $r->db; 325 my $authz = $r->authz; 326 my $userID = $r->param("user"); 327 my $eUserID = $r->param("effectiveUser"); 328 329 # first time we show up here, fill in some values 330 unless ($r->param("in_hc_form")) { 331 # if a set was passed in via the path_info, add that to the list of sets. 332 my $singleSet = $r->urlpath->arg("setID"); 333 if (defined $singleSet and $singleSet ne "") { 334 my @selected_sets = $r->param("selected_sets"); 335 $r->param("selected_sets" => [ @selected_sets, $singleSet]) unless grep { $_ eq $singleSet } @selected_sets; 336 } 337 338 # if no users are selected, select the effective user 339 my @selected_users = $r->param("selected_users"); 340 unless (@selected_users) { 341 $r->param("selected_users" => $eUserID); 342 } 343 } 344 345 my $perm_multiset = $authz->hasPermissions($userID, "download_hardcopy_multiset"); 346 my $perm_multiuser = $authz->hasPermissions($userID, "download_hardcopy_multiuser"); 347 my $perm_texformat = $authz->hasPermissions($userID, "download_hardcopy_format_tex"); 348 my $perm_unopened = $authz->hasPermissions($userID, "view_unopened_sets"); 349 my $perm_view_hidden = $authz->hasPermissions($userID, "view_hidden_sets"); 350 351 # get formats 352 my @formats; 353 foreach my $format (keys %HC_FORMATS) { 354 push @formats, $format if $authz->hasPermissions($userID, "download_hardcopy_format_$format"); 355 } 356 357 # get format names hash for radio buttons 358 my %format_labels = map { $_ => $HC_FORMATS{$_}{name} || $_ } @formats; 359 360 # get users for selection 361 my @Users; 362 if ($perm_multiuser) { 363 # if we're allowed to select multiple users, get all the users 364 # DBFIXME shouldn't need to pass list of users, should use iterator for results? 365 @Users = $db->getUsers($db->listUsers); 366 } else { 367 # otherwise, we get our own record only 368 @Users = $db->getUser($eUserID); 369 } 370 371 # get sets for selection 372 # DBFIXME should use WHERE clause to filter on open_date and visible, rather then getting all 373 my @globalSetIDs; 374 my @GlobalSets; 375 if ($perm_multiuser) { 376 # if we're allowed to select sets for multiple users, get all sets. 377 @globalSetIDs = $db->listGlobalSets; 378 @GlobalSets = $db->getGlobalSets(@globalSetIDs); 379 } else { 380 # otherwise, only get the sets assigned to the effective user. 381 # note that we are getting GlobalSets, but using the list of UserSets assigned to the 382 # effective user. this is because if we pass UserSets to ScrollingRecordList it will 383 # give us composite IDs back, which is a pain in the ass to deal with. 384 @globalSetIDs = $db->listUserSets($eUserID); 385 @GlobalSets = $db->getGlobalSets(@globalSetIDs); 386 } 387 # we also want to get the versioned sets for this user 388 # FIXME: this is another place where we assume that there is a 389 # one-to-one correspondence between assignment_type =~ gateway 390 # and versioned sets. I think we really should have a 391 # "is_versioned" flag on set objects instead. 392 my @versionedSets = grep {$_->assignment_type =~ /gateway/} @GlobalSets; 393 my @SetVersions = (); 394 foreach my $v (@versionedSets) { 395 my @usv = map { [$eUserID, $v->set_id, $_] } ( $db->listSetVersions( $eUserID, $v->set_id ) ); 396 push( @SetVersions, $db->getSetVersions( @usv ) ); 397 } 398 # FIXME: this is a hideous, horrible hack. the identifying key for 399 # a global set is the set_id. those for a set version are the 400 # set_id and version_id. but this means that we have trouble 401 # displaying them both together in HTML::scrollingRecordList. 402 # so we brutally play tricks with the set_id here, which probably 403 # is not very robust, and certainly is aesthetically displeasing. 404 # yuck. 405 foreach ( @SetVersions ) { 406 $_->set_id($_->set_id . ",v" . $_->version_id); 407 } 408 409 # filter out unwanted sets 410 my @WantedGlobalSets; 411 foreach my $i (0 .. $#GlobalSets) { 412 my $Set = $GlobalSets[$i]; 413 unless (defined $Set) { 414 warn "\$GlobalSets[$i] (ID $globalSetIDs[$i]) not defined -- skipping"; 415 next; 416 } 417 next unless $Set->open_date <= time or $perm_unopened; 418 next unless $Set->visible or $perm_view_hidden; 419 # also skip gateway sets, for which we have to have a 420 # version to print something 421 next if $Set->assignment_type =~ /gateway/; 422 push @WantedGlobalSets, $Set; 423 } 424 425 my $scrolling_user_list = scrollingRecordList({ 426 name => "selected_users", 427 request => $r, 428 default_sort => "lnfn", 429 default_format => "lnfn_uid", 430 default_filters => ["all"], 431 size => 20, 432 multiple => $perm_multiuser, 433 }, @Users); 434 435 my $scrolling_set_list = scrollingRecordList({ 436 name => "selected_sets", 437 request => $r, 438 default_sort => "set_id", 439 default_format => "sid", 440 default_filters => ["all"], 441 size => 20, 442 multiple => $perm_multiset, 443 }, @WantedGlobalSets, @SetVersions ); 444 445 # we change the text a little bit depending on whether the user has multiuser privileges 446 my $ss = $perm_multiuser ? "s" : ""; 447 my $aa = $perm_multiuser ? " " : " a "; 448 my $phrase_for_privileged_users = $perm_multiuser ? "to privileged users or" : ""; 449 my $button_label = $perm_multiuser ? "Generate hardcopy for selected sets and selected users" :"Generate hardcopy"; 450 451 # print CGI::start_p(); 452 # print "Select the homework set$ss for which to generate${aa}hardcopy version$ss."; 453 # if ($authz->hasPermissions($userID, "download_hardcopy_multiuser")) { 454 # print "You may also select multiple users from the users list. You will receive hardcopy for each (set, user) pair."; 455 # } 456 # print CGI::end_p(); 457 458 print CGI::start_form(-method=>"POST", -action=>$r->uri); 459 print $self->hidden_authen_fields(); 460 print CGI::hidden("in_hc_form", 1); 461 462 if ($perm_multiuser and $perm_multiset) { 463 print CGI::p("Select the homework sets for which to generate hardcopy versions. You may" 464 ." also select multiple users from the users list. You will receive hardcopy" 465 ." for each (set, user) pair."); 466 467 print CGI::table({class=>"FormLayout"}, 468 CGI::Tr({}, 469 CGI::th("Users"), 470 CGI::th("Sets"), 471 ), 472 CGI::Tr({}, 473 CGI::td($scrolling_user_list), 474 CGI::td($scrolling_set_list), 475 ), 476 ); 477 } else { # single user mode 478 #FIXME -- do a better job of getting the set and the user when in the single set mode 479 my $selected_set_id = $r->param("selected_sets"); 480 $selected_set_id = '' unless defined $selected_set_id; 481 482 my $selected_user_id = $Users[0]->user_id; 483 print CGI::hidden("selected_sets", $selected_set_id ), 484 CGI::hidden( "selected_users", $selected_user_id); 485 486 # make display for versioned sets a bit nicer 487 $selected_set_id =~ s/,v(\d+)$/ (test $1)/; 488 489 print CGI::p("Download hardcopy of set ", $selected_set_id, " for ", $Users[0]->first_name, " ",$Users[0]->last_name,"?"); 490 491 } 492 print CGI::table({class=>"FormLayout"}, 493 CGI::Tr({}, 494 CGI::td({colspan=>2, class=>"ButtonRow"}, 495 CGI::small("You may choose to show any of the following data. Correct answers and solutions are only 496 available $phrase_for_privileged_users after the answer date of the homework set."), 497 CGI::br(), 498 CGI::b("Show:"), " ", 499 CGI::checkbox( 500 -name => "printStudentAnswers", 501 -checked => defined($r->param("printStudentAnswers"))? $r->param("printStudentAnswers") : 1, # checked by default 502 -label => "Student answers", 503 ), 504 CGI::checkbox( 505 -name => "showCorrectAnswers", 506 -checked => scalar($r->param("showCorrectAnswers")) || 0, 507 -label => "Correct answers", 508 ), 509 CGI::checkbox( 510 -name => "showHints", 511 -checked => scalar($r->param("showHints")) || 0, 512 -label => "Hints", 513 ), 514 CGI::checkbox( 515 -name => "showSolutions", 516 -checked => scalar($r->param("showSolutions")) || 0, 517 -label => "Solutions", 518 ), 519 ), 520 ), 521 CGI::Tr({}, 522 CGI::td({colspan=>2, class=>"ButtonRow"}, 523 CGI::b("Hardcopy Format:"), " ", 524 CGI::radio_group( 525 -name => "hardcopy_format", 526 -values => \@formats, 527 -default => scalar($r->param("hardcopy_format")) || $HC_DEFAULT_FORMAT, 528 -labels => \%format_labels, 529 ), 530 ), 531 ), 532 CGI::Tr({}, 533 CGI::td({colspan=>2, class=>"ButtonRow"}, 534 CGI::submit( 535 -name => "generate_hardcopy", 536 -value => $button_label, 537 #-style => "width: 45ex", 538 ), 539 ), 540 ), 541 ); 542 543 print CGI::end_form(); 544 545 return ""; 546 } 547 548 ################################################################################ 549 # harddcopy generating subroutines 550 ################################################################################ 551 552 sub generate_hardcopy { 553 my ($self, $format, $userIDsRef, $setIDsRef) = @_; 554 my $r = $self->r; 555 my $ce = $r->ce; 556 my $db = $r->db; 557 my $authz = $r->authz; 558 559 my $courseID = $r->urlpath->arg("courseID"); 560 my $userID = $r->param("user"); 561 my $eUserID = $r->param("effectiveUser"); 562 563 # we want to make the temp directory web-accessible, for error reporting 564 # use mkpath to ensure it exists (mkpath is pretty much ``mkdir -p'') 565 my $temp_dir_parent_path = $ce->{courseDirs}{html_temp} . "/hardcopy"; 566 eval { mkpath($temp_dir_parent_path) }; 567 if ($@) { 568 die "Couldn't create hardcopy directory $temp_dir_parent_path: $@"; 569 } 570 571 # create a randomly-named working directory in the hardcopy directory 572 my $temp_dir_path = eval { tempdir("work.XXXXXXXX", DIR => $temp_dir_parent_path) }; 573 if ($@) { 574 $self->add_errors("Couldn't create temporary working directory: ".CGI::code(CGI::escapeHTML($@))); 575 return; 576 } 577 # make sure the directory can be read by other daemons e.g. lighttpd 578 chmod 0755, $temp_dir_path; 579 580 581 # do some error checking 582 unless (-e $temp_dir_path) { 583 $self->add_errors("Temporary directory '".CGI::code(CGI::escapeHTML($temp_dir_path)) 584 ."' does not exist, but creation didn't fail. This shouldn't happen."); 585 return; 586 } 587 unless (-w $temp_dir_path) { 588 $self->add_errors("Temporary directory '".CGI::code(CGI::escapeHTML($temp_dir_path)) 589 ."' is not writeable."); 590 $self->delete_temp_dir($temp_dir_path); 591 return; 592 } 593 594 my $tex_file_name = "hardcopy.tex"; 595 my $tex_file_path = "$temp_dir_path/$tex_file_name"; 596 597 ####################################### 598 # create TeX file (callback write_multiuser_tex, or ??) 599 ####################################### 600 601 my $open_result = open my $FH, ">", $tex_file_path; 602 unless ($open_result) { 603 $self->add_errors("Failed to open file '".CGI::code(CGI::escapeHTML($tex_file_path)) 604 ."' for writing: ".CGI::code(CGI::escapeHTML($!))); 605 $self->delete_temp_dir($temp_dir_path); 606 return; 607 } 608 $self->write_multiuser_tex($FH, $userIDsRef, $setIDsRef); 609 close $FH; 610 611 # if no problems got rendered successfully, we can't continue 612 unless ($self->{at_least_one_problem_rendered_without_error}) { 613 $self->add_errors("No problems rendered. Can't continue."); 614 $self->delete_temp_dir($temp_dir_path); 615 return; 616 } 617 618 # if no hardcopy.tex file was generated, fail now 619 unless (-e "$temp_dir_path/hardcopy.tex") { 620 $self->add_errors("'".CGI::code("hardcopy.tex")."' not written to temporary directory '" 621 .CGI::code(CGI::escapeHTML($temp_dir_path))."'. Can't continue."); 622 $self->delete_temp_dir($temp_dir_path); 623 return; 624 } 625 626 ############################################## 627 # end creation of TeX file 628 ############################################## 629 630 # determine base name of final file 631 my $final_file_user = @$userIDsRef > 1 ? "multiuser" : $userIDsRef->[0]; 632 my $final_file_set = @$setIDsRef > 1 ? "multiset" : $setIDsRef->[0]; 633 my $final_file_basename = "$courseID.$final_file_user.$final_file_set"; 634 635 ############################################### 636 # call format subroutine (call back) 637 ############################################### 638 # $final_file_name is the name of final hardcopy file 639 # @temp_files is a list of temporary files of interest used by the subroutine 640 # (all are relative to $temp_dir_path) 641 my $format_subr = $HC_FORMATS{$format}{subr}; 642 my ($final_file_name, @temp_files) = $self->$format_subr($temp_dir_path, $final_file_basename); 643 644 my $final_file_path = "$temp_dir_path/$final_file_name"; 645 646 #warn "final_file_name=$final_file_name\n"; 647 #warn "temp_files=@temp_files\n"; 648 649 ################################################ 650 # calculate URLs for each temp file of interest 651 ################################################# 652 # makeTempDirectory's interface forces us to reverse-engineer the name of the temp dir from the path 653 my $temp_dir_parent_url = $ce->{courseURLs}{html_temp} . "/hardcopy"; 654 (my $temp_dir_url = $temp_dir_path) =~ s/^$temp_dir_parent_path/$temp_dir_parent_url/; 655 my %temp_file_map; 656 foreach my $temp_file_name (@temp_files) { 657 $temp_file_map{$temp_file_name} = "$temp_dir_url/$temp_file_name"; 658 } 659 660 my $final_file_url; 661 662 ################################################## 663 # make sure final file exists 664 ################################################## 665 # returns undefined unless $final_file_path points to a file 666 unless (-e $final_file_path) { 667 $self->add_errors("Final hardcopy file '".CGI::code(CGI::escapeHTML($final_file_path)) 668 ."' not found after calling '".CGI::code(CGI::escapeHTML($format_subr))."': " 669 .CGI::code(CGI::escapeHTML($!))); 670 return $final_file_url, %temp_file_map; 671 } 672 673 ################################################## 674 # try to move the hardcopy file out of the temp directory 675 ################################################## 676 677 # set $final_file_url accordingly 678 my $final_file_final_path = "$temp_dir_parent_path/$final_file_name"; 679 my $mv_cmd = "2>&1 " . $ce->{externalPrograms}{mv} . " " . shell_quote($final_file_path, $final_file_final_path); 680 my $mv_out = readpipe $mv_cmd; 681 if ($?) { 682 $self->add_errors("Failed to move hardcopy file '".CGI::code(CGI::escapeHTML($final_file_name)) 683 ."' from '".CGI::code(CGI::escapeHTML($temp_dir_path))."' to '" 684 .CGI::code(CGI::escapeHTML($temp_dir_parent_path))."':".CGI::br() 685 .CGI::pre(CGI::escapeHTML($mv_out))); 686 $final_file_url = "$temp_dir_url/$final_file_name"; 687 } else { 688 $final_file_url = "$temp_dir_parent_url/$final_file_name"; 689 } 690 691 ################################################## 692 # remove the temp directory if there are no errors 693 ################################################## 694 695 unless ($self->get_errors or $PreserveTempFiles) { 696 $self->delete_temp_dir($temp_dir_path); 697 } 698 699 warn "Preserved temporary files in directory '$temp_dir_path'.\n" if $PreserveTempFiles; 700 701 return $final_file_url, %temp_file_map; 702 } 703 704 # helper function to remove temp dirs 705 sub delete_temp_dir { 706 my ($self, $temp_dir_path) = @_; 707 708 my $rm_cmd = "2>&1 " . $self->r->ce->{externalPrograms}{rm} . " -rf " . shell_quote($temp_dir_path); 709 my $rm_out = readpipe $rm_cmd; 710 if ($?) { 711 $self->add_errors("Failed to remove temporary directory '".CGI::code(CGI::escapeHTML($temp_dir_path))."':" 712 .CGI::br().CGI::pre($rm_out)); 713 return 0; 714 } else { 715 return 1; 716 } 717 } 718 719 # format subroutines 720 # 721 # assume that TeX source is located at $temp_dir_path/hardcopy.tex 722 # the generated file will being with $final_file_basename 723 # first element of return value is the name of the generated file (relative to $temp_dir_path) 724 # rest of return value elements are names of temporary files that may be of interest in the 725 # case of an error, relative to $temp_dir_path. these are returned whether or not an error 726 # actually occured. 727 728 sub generate_hardcopy_tex { 729 my ($self, $temp_dir_path, $final_file_basename) = @_; 730 731 my $final_file_name; 732 733 # try to rename tex file 734 my $src_name = "hardcopy.tex"; 735 my $dest_name = "$final_file_basename.tex"; 736 my $mv_cmd = "2>&1 " . $self->r->ce->{externalPrograms}{mv} . " " . shell_quote("$temp_dir_path/$src_name", "$temp_dir_path/$dest_name"); 737 my $mv_out = readpipe $mv_cmd; 738 if ($?) { 739 $self->add_errors("Failed to rename '".CGI::code(CGI::escapeHTML($src_name))."' to '" 740 .CGI::code(CGI::escapeHTML($dest_name))."' in directory '" 741 .CGI::code(CGI::escapeHTML($temp_dir_path))."':".CGI::br() 742 .CGI::pre(CGI::escapeHTML($mv_out))); 743 $final_file_name = $src_name; 744 } else { 745 $final_file_name = $dest_name; 746 } 747 748 return $final_file_name; 749 } 750 751 sub generate_hardcopy_pdf { 752 my ($self, $temp_dir_path, $final_file_basename) = @_; 753 754 # call pdflatex - we don't want to chdir in the mod_perl process, as 755 # that might step on the feet of other things (esp. in Apache 2.0) 756 my $pdflatex_cmd = "cd " . shell_quote($temp_dir_path) . " && " 757 . $self->r->ce->{externalPrograms}{pdflatex} 758 . " >pdflatex.stdout 2>pdflatex.stderr hardcopy"; 759 if (my $rawexit = system $pdflatex_cmd) { 760 my $exit = $rawexit >> 8; 761 my $signal = $rawexit & 127; 762 my $core = $rawexit & 128; 763 $self->add_errors("Failed to convert TeX to PDF with command '" 764 .CGI::code(CGI::escapeHTML($pdflatex_cmd))."' (exit=$exit signal=$signal core=$core)."); 765 766 # read hardcopy.log and report first error 767 my $hardcopy_log = "$temp_dir_path/hardcopy.log"; 768 if (-e $hardcopy_log) { 769 if (open my $LOG, "<", $hardcopy_log) { 770 my $line; 771 while ($line = <$LOG>) { 772 last if $line =~ /^!\s+/; 773 } 774 my $first_error = $line; 775 while ($line = <$LOG>) { 776 last if $line =~ /^!\s+/; 777 $first_error .= $line; 778 } 779 close $LOG; 780 if (defined $first_error) { 781 $self->add_errors("First error in TeX log is:".CGI::br(). 782 CGI::pre(CGI::escapeHTML($first_error))); 783 } else { 784 $self->add_errors("No errors encoundered in TeX log."); 785 } 786 } else { 787 $self->add_errors("Could not read TeX log: ".CGI::code(CGI::escapeHTML($!))); 788 } 789 } else { 790 $self->add_errors("No TeX log was found."); 791 } 792 } 793 794 my $final_file_name; 795 796 # try rename the pdf file 797 my $src_name = "hardcopy.pdf"; 798 my $dest_name = "$final_file_basename.pdf"; 799 my $mv_cmd = "2>&1 " . $self->r->ce->{externalPrograms}{mv} . " " . shell_quote("$temp_dir_path/$src_name", "$temp_dir_path/$dest_name"); 800 my $mv_out = readpipe $mv_cmd; 801 if ($?) { 802 $self->add_errors("Failed to rename '".CGI::code(CGI::escapeHTML($src_name))."' to '" 803 .CGI::code(CGI::escapeHTML($dest_name))."' in directory '" 804 .CGI::code(CGI::escapeHTML($temp_dir_path))."':".CGI::br() 805 .CGI::pre(CGI::escapeHTML($mv_out))); 806 $final_file_name = $src_name; 807 } else { 808 $final_file_name = $dest_name; 809 } 810 811 return $final_file_name, qw/hardcopy.tex hardcopy.log hardcopy.aux pdflatex.stdout pdflatex.stderr/; 812 } 813 814 ################################################################################ 815 # TeX aggregating subroutines 816 ################################################################################ 817 818 sub write_multiuser_tex { 819 my ($self, $FH, $userIDsRef, $setIDsRef) = @_; 820 my $r = $self->r; 821 my $ce = $r->ce; 822 823 my @userIDs = @$userIDsRef; 824 my @setIDs = @$setIDsRef; 825 826 # get snippets 827 my $preamble = $ce->{webworkFiles}->{hardcopySnippets}->{preamble}; 828 my $postamble = $ce->{webworkFiles}->{hardcopySnippets}->{postamble}; 829 my $divider = $ce->{webworkFiles}->{hardcopySnippets}->{userDivider}; 830 831 # write preamble 832 $self->write_tex_file($FH, $preamble); 833 834 # write section for each user 835 while (defined (my $userID = shift @userIDs)) { 836 $self->write_multiset_tex($FH, $userID, @setIDs); 837 $self->write_tex_file($FH, $divider) if @userIDs; # divide users, but not after the last user 838 } 839 840 # write postamble 841 $self->write_tex_file($FH, $postamble); 842 } 843 844 sub write_multiset_tex { 845 my ($self, $FH, $targetUserID, @setIDs) = @_; 846 my $r = $self->r; 847 my $ce = $r->ce; 848 my $db = $r->db; 849 850 # get user record 851 my $TargetUser = $db->getUser($targetUserID); # checked 852 unless ($TargetUser) { 853 $self->add_errors("Can't generate hardcopy for user '".CGI::code(CGI::escapeHTML($targetUserID))."' -- no such user exists.\n"); 854 return; 855 } 856 857 # get set divider 858 my $divider = $ce->{webworkFiles}->{hardcopySnippets}->{setDivider}; 859 860 # write each set 861 while (defined (my $setID = shift @setIDs)) { 862 $self->write_set_tex($FH, $TargetUser, $setID); 863 $self->write_tex_file($FH, $divider) if @setIDs; # divide sets, but not after the last set 864 } 865 } 866 867 sub write_set_tex { 868 my ($self, $FH, $TargetUser, $setID) = @_; 869 my $r = $self->r; 870 my $ce = $r->ce; 871 my $db = $r->db; 872 my $authz = $r->authz; 873 my $userID = $r->param("user"); 874 875 # we may already have the MergedSet from checking hide_work and 876 # hide_score in pre_header_initialize; check to see if that's true, 877 # and otherwise, get the set. 878 my %mergedSets = %{$self->{mergedSets}}; 879 my $uid = $TargetUser->user_id; 880 my $MergedSet; 881 my $versioned = 0; 882 if ( defined( $mergedSets{"$uid!$setID"} ) ) { 883 $MergedSet = $mergedSets{"$uid!$setID"}; 884 $versioned = ($setID =~ /,v(\d+)$/) ? $1 : 0; 885 } else { 886 if ( $setID =~ /(.+),v(\d+)$/ ) { 887 $setID = $1; 888 $versioned = $2; 889 } 890 if ( $versioned ) { 891 $MergedSet = $db->getMergedSetVersion($TargetUser->user_id, $setID, $versioned); 892 } else { 893 $MergedSet = $db->getMergedSet($TargetUser->user_id, $setID); # checked 894 } 895 } 896 # save versioned info for use in write_problem_tex 897 $self->{versioned} = $versioned; 898 899 unless ($MergedSet) { 900 $self->add_errors("Can't generate hardcopy for set ''".CGI::code(CGI::escapeHTML($setID)) 901 ."' for user '".CGI::code(CGI::escapeHTML($TargetUser->user_id)) 902 ."' -- set is not assigned to that user."); 903 return; 904 } 905 906 # see if the *real* user is allowed to access this problem set 907 if ($MergedSet->open_date > time and not $authz->hasPermissions($userID, "view_unopened_sets")) { 908 $self->add_errors("Can't generate hardcopy for set '".CGI::code(CGI::escapeHTML($setID)) 909 ."' for user '".CGI::code(CGI::escapeHTML($TargetUser->user_id)) 910 ."' -- set is not yet open."); 911 return; 912 } 913 if (not $MergedSet->visible and not $authz->hasPermissions($userID, "view_hidden_sets")) { 914 $self->addbadmessage("Can't generate hardcopy for set '".CGI::code(CGI::escapeHTML($setID)) 915 ."' for user '".CGI::code(CGI::escapeHTML($TargetUser->user_id)) 916 ."' -- set is not visible to students."); 917 return; 918 } 919 920 # get snippets 921 my $header = $MergedSet->hardcopy_header 922 ? $MergedSet->hardcopy_header 923 : $ce->{webworkFiles}->{hardcopySnippets}->{setHeader}; 924 my $footer = $ce->{webworkFiles}->{hardcopySnippets}->{setFooter}; 925 my $divider = $ce->{webworkFiles}->{hardcopySnippets}->{problemDivider}; 926 927 # get list of problem IDs 928 # DBFIXME use ORDER BY in database 929 my @problemIDs = sort { $a <=> $b } $db->listUserProblems($MergedSet->user_id, $MergedSet->set_id); 930 931 # for versioned sets (gateways), we might have problems in a random 932 # order; reset the order of the problemIDs if this is the case 933 if ( defined( $MergedSet->problem_randorder ) && 934 $MergedSet->problem_randorder ) { 935 my @newOrder = (); 936 937 # to set the same order each time we set the random seed to the psvn, 938 # and to avoid messing with the system random number generator we use 939 # our own PGrandom object 940 my $pgrand = PGrandom->new(); 941 $pgrand->srand( $MergedSet->psvn ); 942 while ( @problemIDs ) { 943 my $i = int($pgrand->rand(scalar(@problemIDs))); 944 push( @newOrder, $problemIDs[$i] ); 945 splice(@problemIDs, $i, 1); 946 } 947 @problemIDs = @newOrder; 948 } 949 950 951 # write set header 952 $self->write_problem_tex($FH, $TargetUser, $MergedSet, 0, $header); # 0 => pg file specified directly 953 954 # write each problem 955 # for versioned problem sets (gateway tests) we like to include 956 # problem numbers 957 my $i = 1; 958 while (my $problemID = shift @problemIDs) { 959 $self->write_tex_file($FH, $divider); 960 $self->{versioned} = $i if $versioned; 961 $self->write_problem_tex($FH, $TargetUser, $MergedSet, $problemID); 962 $i++; 963 } 964 965 # write footer 966 $self->write_problem_tex($FH, $TargetUser, $MergedSet, 0, $footer); # 0 => pg file specified directly 967 } 968 969 sub write_problem_tex { 970 my ($self, $FH, $TargetUser, $MergedSet, $problemID, $pgFile) = @_; 971 my $r = $self->r; 972 my $ce = $r->ce; 973 my $db = $r->db; 974 my $authz = $r->authz; 975 my $userID = $r->param("user"); 976 my $versioned = $self->{versioned}; 977 my %canShowScore = %{$self->{canShowScore}}; 978 979 my @errors; 980 981 # get problem record 982 my $MergedProblem; 983 if ($problemID) { 984 # a non-zero problem ID was given -- load that problem 985 # we use $versioned to determine which merging routine to use 986 if ( $versioned ) { 987 $MergedProblem = $db->getMergedProblemVersion($MergedSet->user_id, $MergedSet->set_id, $MergedSet->version_id, $problemID); 988 989 } else { 990 $MergedProblem = $db->getMergedProblem($MergedSet->user_id, $MergedSet->set_id, $problemID); # checked 991 } 992 993 # handle nonexistent problem 994 unless ($MergedProblem) { 995 $self->add_errors("Can't generate hardcopy for problem '" 996 .CGI::code(CGI::escapeHTML($problemID))."' in set '" 997 .CGI::code(CGI::escapeHTML($MergedSet->set_id)) 998 ."' for user '".CGI::code(CGI::escapeHTML($MergedSet->user_id)) 999 ."' -- problem does not exist in that set or is not assigned to that user."); 1000 return; 1001 } 1002 } elsif ($pgFile) { 1003 # otherwise, we try an explicit PG file 1004 $MergedProblem = $db->newUserProblem( 1005 user_id => $MergedSet->user_id, 1006 set_id => $MergedSet->set_id, 1007 problem_id => 0, 1008 source_file => $pgFile, 1009 num_correct => 0, 1010 num_incorrect => 0, 1011 num_correct => 0, 1012 num_incorrect => 0, 1013 ); 1014 die "newUserProblem failed -- WTF?" unless $MergedProblem; # this should never happen 1015 } else { 1016 # this shouldn't happen -- error out for real 1017 die "write_problem_tex needs either a non-zero \$problemID or a \$pgFile"; 1018 } 1019 1020 # figure out if we're allowed to get correct answers, hints, and solutions 1021 # (eventually, we'd like to be able to use the same code as Problem) 1022 my $versionName = $MergedSet->set_id . 1023 (( $versioned ) ? ",v" . $MergedSet->version_id : ''); 1024 1025 my $showCorrectAnswers = $r->param("showCorrectAnswers") || 0; 1026 my $printStudentAnswers = $r->param("printStudentAnswers") || 0; 1027 my $showHints = $r->param("showHints") || 0; 1028 my $showSolutions = $r->param("showSolutions") || 0; 1029 1030 1031 unless( ( $authz->hasPermissions($userID, "show_correct_answers_before_answer_date") or 1032 ( time > $MergedSet->answer_date or 1033 ( $versioned && 1034 $MergedProblem->num_correct + 1035 $MergedProblem->num_incorrect >= 1036 $MergedSet->attempts_per_version && 1037 $MergedSet->due_date == $MergedSet->answer_date ) ) ) && 1038 ( $canShowScore{$MergedSet->user_id . "!$versionName"} ) ) { 1039 $showCorrectAnswers = 0; 1040 $showSolutions = 0; 1041 } 1042 1043 # FIXME -- there can be a problem if the $siteDefaults{timezone} is not defined? Why is this? 1044 # why does it only occur with hardcopy? 1045 1046 # we need an additional translation option for versioned sets; also, 1047 # for versioned sets include old answers in the set if we're also 1048 # asking for the answers 1049 my $transOpts = 1050 { # translation options 1051 displayMode => "tex", 1052 showHints => $showHints ? 1 : 0, # insure that this value is numeric 1053 showSolutions => $showSolutions ? 1 : 0, # (or what? -sam) 1054 processAnswers => ($showCorrectAnswers || $printStudentAnswers) ? 1 : 0, 1055 permissionLevel => $db->getPermissionLevel($userID)->permission, 1056 }; 1057 1058 if ( $versioned && $MergedProblem->problem_id != 0 ) { 1059 1060 1061 $transOpts->{QUIZ_PREFIX} = 'Q' . sprintf("%04d",$MergedProblem->problem_id()) . '_'; 1062 1063 } 1064 my $formFields = { }; 1065 if ( $showCorrectAnswers ||$printStudentAnswers ) { 1066 my %oldAnswers = decodeAnswers($MergedProblem->last_answer); 1067 $formFields->{$_} = $oldAnswers{$_} foreach (keys %oldAnswers); 1068 print $FH "%% decoded old answers, saved. (keys = " . join(',', keys(%oldAnswers)) . "\n"; 1069 } 1070 1071 # warn("problem ", $MergedProblem->problem_id, ": source = ", $MergedProblem->source_file, "\n"); 1072 1073 my $pg = WeBWorK::PG->new( 1074 $ce, 1075 $TargetUser, 1076 scalar($r->param('key')), # avoid multiple-values problem 1077 $MergedSet, 1078 $MergedProblem, 1079 $MergedSet->psvn, 1080 $formFields, # no form fields! 1081 $transOpts, 1082 ); 1083 1084 # only bother to generate this info if there were warnings or errors 1085 my $edit_url; 1086 my $problem_name; 1087 my $problem_desc; 1088 if ($pg->{warnings} ne "" or $pg->{flags}->{error_flag}) { 1089 my $edit_urlpath = $r->urlpath->newFromModule( 1090 "WeBWorK::ContentGenerator::Instructor::PGProblemEditor", 1091 courseID => $r->urlpath->arg("courseID"), 1092 setID => $MergedProblem->set_id, 1093 problemID => $MergedProblem->problem_id, 1094 ); 1095 1096 if ($MergedProblem->problem_id == 0) { 1097 # link for an fake problem (like a header file) 1098 $edit_url = $self->systemLink($edit_urlpath, 1099 params => { 1100 sourceFilePath => $MergedProblem->source_file, 1101 problemSeed => $MergedProblem->problem_seed, 1102 }, 1103 ); 1104 } else { 1105 # link for a real problem 1106 $edit_url = $self->systemLink($edit_urlpath); 1107 } 1108 1109 if ($MergedProblem->problem_id == 0) { 1110 $problem_name = "snippet"; 1111 $problem_desc = $problem_name." '".$MergedProblem->source_file 1112 ."' for set '".$MergedProblem->set_id."' and user '" 1113 .$MergedProblem->user_id."'"; 1114 } else { 1115 $problem_name = "problem"; 1116 $problem_desc = $problem_name." '".$MergedProblem->problem_id 1117 ."' in set '".$MergedProblem->set_id."' for user '" 1118 .$MergedProblem->user_id."'"; 1119 } 1120 } 1121 1122 # deal with PG warnings 1123 if ($pg->{warnings} ne "") { 1124 $self->add_errors(CGI::a({href=>$edit_url, target=>"WW_Editor"}, "[edit]") 1125 ." Warnings encountered while processing $problem_desc. " 1126 ."Error text:".CGI::br().CGI::pre(CGI::escapeHTML($pg->{warnings})) 1127 ); 1128 } 1129 1130 # deal with PG errors 1131 if ($pg->{flags}->{error_flag}) { 1132 $self->add_errors(CGI::a({href=>$edit_url, target=>"WW_Editor"}, "[edit]") 1133 ." Errors encountered while processing $problem_desc. " 1134 ."This $problem_name has been omitted from the hardcopy. " 1135 ."Error text:".CGI::br().CGI::pre(CGI::escapeHTML($pg->{errors})) 1136 ); 1137 return; 1138 } 1139 1140 # if we got here, there were no errors (because errors cause a return above) 1141 $self->{at_least_one_problem_rendered_without_error} = 1; 1142 1143 print $FH "{\\bf Problem $versioned.}\n" 1144 if ( $versioned && $MergedProblem->problem_id != 0 ); 1145 print $FH $pg->{body_text}; 1146 1147 my @ans_entry_order = defined($pg->{flags}->{ANSWER_ENTRY_ORDER}) ? @{$pg->{flags}->{ANSWER_ENTRY_ORDER}} : ( ); 1148 1149 # print the list of student answers if it is requested 1150 if ( $printStudentAnswers && 1151 $MergedProblem->problem_id != 0 && @ans_entry_order ) { 1152 my $recScore = $pg->{state}->{recorded_score}; 1153 my $corrMsg = ''; 1154 if ( $recScore == 1 ) { 1155 $corrMsg = ' (correct)'; 1156 } elsif ( $recScore == 0 ) { 1157 $corrMsg = ' (incorrect)'; 1158 } else { 1159 $corrMsg = " (score $recScore)"; 1160 } 1161 my $stuAnswers = "\\par{\\small{\\it Answer(s) submitted:}\n" . 1162 "\\vspace{-\\parskip}\\begin{itemize}\n"; 1163 for my $ansName ( @ans_entry_order ) { 1164 my $stuAns = $pg->{answers}->{$ansName}->{original_student_ans}; 1165 $stuAnswers .= "\\item\\begin{verbatim}$stuAns\\end{verbatim}\n"; 1166 1167 $stuAnswers .= "\\end{itemize}}$corrMsg\\par\n"; 1168 print $FH $stuAnswers; 1169 } 1170 1171 # write the list of correct answers is appropriate; ANSWER_ENTRY_ORDER 1172 # isn't defined for versioned sets? this seems odd FIXME GWCHANGE 1173 if ($showCorrectAnswers && $MergedProblem->problem_id != 0 && @ans_entry_order) { 1174 my $correctTeX = "\\par{\\small{\\it Correct Answers:}\n" 1175 . "\\vspace{-\\parskip}\\begin{itemize}\n"; 1176 1177 foreach my $ansName (@ans_entry_order) { 1178 my $correctAnswer = $pg->{answers}->{$ansName}->{correct_ans}; 1179 $correctTeX .= "\\item\\begin{verbatim}$correctAnswer\\end{verbatim}\n"; 1180 # FIXME: What about vectors (where TeX will complain about < and > outside of math mode)? 1181 } 1182 1183 $correctTeX .= "\\end{itemize}}\\par\n"; 1184 1185 print $FH $correctTeX; 1186 } 1187 } 1188 1189 sub write_tex_file { 1190 my ($self, $FH, $file) = @_; 1191 1192 my $tex = eval { readFile($file) }; 1193 if ($@) { 1194 $self->add_errors("Failed to include TeX file '".CGI::code(CGI::escapeHTML($file))."': " 1195 .CGI::escapeHTML($@)); 1196 } else { 1197 print $FH $tex; 1198 } 1199 } 1200 1201 ################################################################################ 1202 # utilities 1203 ################################################################################ 1204 1205 sub add_errors { 1206 my ($self, @errors) = @_; 1207 push @{$self->{hardcopy_errors}}, @errors; 1208 } 1209 1210 sub get_errors { 1211 my ($self) = @_; 1212 return $self->{hardcopy_errors} ? @{$self->{hardcopy_errors}} : (); 1213 } 1214 1215 sub get_errors_ref { 1216 my ($self) = @_; 1217 return $self->{hardcopy_errors}; 1218 } 1219 1220 1;
| aubreyja at gmail dot com | ViewVC Help |
| Powered by ViewVC 1.0.9 |