Parent Directory
|
Revision Log
forward-port from rel-2-2-dev: (update copyright date range -- 2000-2006. this is probably overkill, since there are some files that were created after 2000 and some files that were last modified before 2006.)
1 ################################################################################ 2 # WeBWorK Online Homework Delivery System 3 # Copyright © 2000-2006 The WeBWorK Project, http://openwebwork.sf.net/ 4 # $CVSHeader: webwork2/lib/WeBWorK/ContentGenerator/Hardcopy.pm,v 1.74 2005/12/16 18:19:00 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::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 use Apache::Constants qw/:common REDIRECT/; 30 use CGI qw//; 31 use String::ShellQuote; 32 use WeBWorK::DB::Utils qw/user2global/; 33 use WeBWorK::Debug; 34 use WeBWorK::Form; 35 use WeBWorK::HTML::ScrollingRecordList qw/scrollingRecordList/; 36 use WeBWorK::PG; 37 use WeBWorK::Utils qw/readFile makeTempDirectory surePathToFile/; 38 39 =head1 CONFIGURATION VARIABLES 40 41 =over 42 43 =item $PreserveTempFiles 44 45 If true, don't delete temporary files. 46 47 =cut 48 49 our $PreserveTempFiles = 0 unless defined $PreserveTempFiles; 50 51 =back 52 53 =cut 54 55 our $HC_DEFAULT_FORMAT = "pdf"; # problems if this is not an allowed format for the user... 56 our %HC_FORMATS = ( 57 tex => { name => "TeX Source", subr => "generate_hardcopy_tex" }, 58 pdf => { name => "Adobe PDF", subr => "generate_hardcopy_pdf" }, 59 ); 60 61 # custom fields used in $self hash 62 # FOR HEAVEN'S SAKE, PLEASE KEEP THIS UP-TO-DATE! 63 # 64 # final_file_url 65 # contains the URL of the final hardcopy file generated 66 # set by generate_hardcopy(), used by pre_header_initialize() and body() 67 # 68 # temp_file_map 69 # reference to a hash mapping temporary file names to URL 70 # set by pre_header_initialize(), used by body() 71 # 72 # hardcopy_errors 73 # reference to array containing HTML strings describing generation errors (and warnings) 74 # used by add_errors(), get_errors(), get_errors_ref() 75 # 76 # at_least_one_problem_rendered_without_error 77 # set to a true value by write_problem_tex if it is able to sucessfully render 78 # a problem. checked by generate_hardcopy to determine whether to continue 79 # with the generation process. 80 81 ################################################################################ 82 # UI subroutines 83 ################################################################################ 84 85 sub pre_header_initialize { 86 my ($self) = @_; 87 my $r = $self->r; 88 my $ce = $r->ce; 89 my $db = $r->db; 90 my $authz = $r->authz; 91 92 my $userID = $r->param("user"); 93 my $eUserID = $r->param("effectiveUser"); 94 my @setIDs = $r->param("selected_sets"); 95 my @userIDs = $r->param("selected_users"); 96 my $hardcopy_format = $r->param("hardcopy_format"); 97 my $generate_hardcopy = $r->param("generate_hardcopy"); 98 my $send_existing_hardcopy = $r->param("send_existing_hardcopy"); 99 my $final_file_url = $r->param("final_file_url"); 100 101 # if there's an existing hardcopy file that can be sent, get set up to do that 102 if ($send_existing_hardcopy) { 103 $self->reply_with_redirect($final_file_url); 104 $self->{final_file_url} = $final_file_url; 105 $self->{send_hardcopy} = 1; 106 return; 107 } 108 109 # this should never happen, but apparently it did once (see bug #714), so we check for it 110 die "Parameter 'user' not defined -- this should never happen" unless defined $userID; 111 112 if ($generate_hardcopy) { 113 my $validation_failed = 0; 114 115 # set default format 116 $hardcopy_format = $HC_DEFAULT_FORMAT unless defined $hardcopy_format; 117 118 # make sure format is valid 119 unless (grep { $_ eq $hardcopy_format } keys %HC_FORMATS) { 120 $self->addbadmessage("'$hardcopy_format' is not a valid hardcopy format."); 121 $validation_failed = 1; 122 } 123 124 # make sure we are allowed to generate hardcopy in this format 125 unless ($authz->hasPermissions($userID, "download_hardcopy_format_$hardcopy_format")) { 126 $self->addbadmessage("You do not have permission to generate hardcopy in $hardcopy_format format."); 127 $validation_failed = 1; 128 } 129 130 # is there at least one user and set selected? 131 unless (@userIDs) { 132 $self->addbadmessage("Please select at least one user and try again."); 133 $validation_failed = 1; 134 } 135 unless (@setIDs) { 136 $self->addbadmessage("Please select at least one set and try again."); 137 $validation_failed = 1; 138 } 139 140 # is the user allowed to request multiple sets/users at a time? 141 my $perm_multiset = $authz->hasPermissions($userID, "download_hardcopy_multiset"); 142 my $perm_multiuser = $authz->hasPermissions($userID, "download_hardcopy_multiuser"); 143 144 if (@setIDs > 1 and not $perm_multiset) { 145 $self->addbadmessage("You are not permitted to generate hardcopy for multiple sets. Please select a single set and try again."); 146 $validation_failed = 1; 147 } 148 if (@userIDs > 1 and not $perm_multiuser) { 149 $self->addbadmessage("You are not permitted to generate hardcopy for multiple users. Please select a single user and try again."); 150 $validation_failed = 1; 151 } 152 if (@userIDs and $userIDs[0] ne $eUserID and not $perm_multiuser) { 153 $self->addbadmessage("You are not permitted to generate hardcopy for other users."); 154 $validation_failed = 1; 155 # FIXME -- download_hardcopy_multiuser controls both whether a user can generate hardcopy 156 # that contains sets for multiple users AND whether she can generate hardcopy that contains 157 # sets for users other than herself. should these be separate permission levels? 158 } 159 160 unless ($validation_failed) { 161 my ($final_file_url, %temp_file_map) = $self->generate_hardcopy($hardcopy_format, \@userIDs, \@setIDs); 162 if ($self->get_errors) { 163 # store the URLs in self hash so that body() can make a link to it 164 $self->{final_file_url} = $final_file_url; 165 $self->{temp_file_map} = \%temp_file_map; 166 } else { 167 # send the file only 168 $self->reply_with_redirect($final_file_url); 169 } 170 } 171 } 172 } 173 174 sub body { 175 my ($self) = @_; 176 177 if (my $num = $self->get_errors) { 178 my $final_file_url = $self->{final_file_url}; 179 my %temp_file_map = %{$self->{temp_file_map}}; 180 181 my $errors_str = $num > 1 ? "errors" : "error"; 182 print CGI::p("$num $errors_str occured while generating hardcopy:"); 183 184 print CGI::ul(CGI::li($self->get_errors_ref)); 185 186 if ($final_file_url) { 187 print CGI::p( 188 "A hardcopy file was generated, but it may not be complete or correct: ", 189 CGI::a({href=>$final_file_url}, "Download Hardcopy") 190 ); 191 } 192 193 if (%temp_file_map) { 194 print CGI::start_p(); 195 print "You can also examine the following temporary files: "; 196 my $first = 1; 197 while (my ($temp_file_name, $temp_file_url) = each %temp_file_map) { 198 if ($first) { 199 $first = 0; 200 } else { 201 print ", "; 202 } 203 print CGI::a({href=>$temp_file_url}, " $temp_file_name"); 204 } 205 print CGI::end_p(); 206 } 207 208 print CGI::hr(); 209 } 210 211 $self->display_form(); 212 } 213 214 sub display_form { 215 my ($self) = @_; 216 my $r = $self->r; 217 my $db = $r->db; 218 my $authz = $r->authz; 219 my $userID = $r->param("user"); 220 my $eUserID = $r->param("effectiveUser"); 221 222 # first time we show up here, fill in some values 223 unless ($r->param("in_hc_form")) { 224 # if a set was passed in via the path_info, add that to the list of sets. 225 my $singleSet = $r->urlpath->arg("setID"); 226 if (defined $singleSet and $singleSet ne "") { 227 my @selected_sets = $r->param("selected_sets"); 228 $r->param("selected_sets" => [ @selected_sets, $singleSet]) unless grep { $_ eq $singleSet } @selected_sets; 229 } 230 231 # if no users are selected, select the effective user 232 my @selected_users = $r->param("selected_users"); 233 unless (@selected_users) { 234 $r->param("selected_users" => $eUserID); 235 } 236 } 237 238 my $perm_multiset = $authz->hasPermissions($userID, "download_hardcopy_multiset"); 239 my $perm_multiuser = $authz->hasPermissions($userID, "download_hardcopy_multiuser"); 240 my $perm_texformat = $authz->hasPermissions($userID, "download_hardcopy_format_tex"); 241 my $perm_unopened = $authz->hasPermissions($userID, "view_unopened_sets"); 242 my $perm_unpublished = $authz->hasPermissions($userID, "view_unpublished_sets"); 243 244 # get formats 245 my @formats; 246 foreach my $format (keys %HC_FORMATS) { 247 push @formats, $format if $authz->hasPermissions($userID, "download_hardcopy_format_$format"); 248 } 249 250 # get format names hash for radio buttons 251 my %format_labels = map { $_ => $HC_FORMATS{$_}{name} || $_ } @formats; 252 253 # get users for selection 254 my @Users; 255 if ($perm_multiuser) { 256 # if we're allowed to select multiple users, get all the users 257 @Users = $db->getUsers($db->listUsers); 258 } else { 259 # otherwise, we get our own record only 260 @Users = $db->getUser($eUserID); 261 } 262 263 # get sets for selection 264 my @globalSetIDs; 265 my @GlobalSets; 266 if ($perm_multiuser) { 267 # if we're allowed to select sets for multiple users, get all sets. 268 @globalSetIDs = $db->listGlobalSets; 269 @GlobalSets = $db->getGlobalSets(@globalSetIDs); 270 } else { 271 # otherwise, only get the sets assigned to the effective user. 272 # note that we are getting GlobalSets, but using the list of UserSets assigned to the 273 # effective user. this is because if we pass UserSets to ScrollingRecordList it will 274 # give us composite IDs back, which is a pain in the ass to deal with. 275 @globalSetIDs = $db->listUserSets($eUserID); 276 @GlobalSets = $db->getGlobalSets(@globalSetIDs); 277 } 278 279 # filter out unwanted sets 280 my @WantedGlobalSets; 281 foreach my $i (0 .. $#GlobalSets) { 282 my $Set = $GlobalSets[$i]; 283 unless (defined $Set) { 284 warn "\$GlobalSets[$i] (ID $globalSetIDs[$i]) not defined -- skipping"; 285 next; 286 } 287 next unless $Set->open_date <= time or $perm_unopened; 288 next unless $Set->published or $perm_unpublished; 289 push @WantedGlobalSets, $Set; 290 } 291 292 my $scrolling_user_list = scrollingRecordList({ 293 name => "selected_users", 294 request => $r, 295 default_sort => "lnfn", 296 default_format => "lnfn_uid", 297 default_filters => ["all"], 298 size => 20, 299 multiple => $perm_multiuser, 300 }, @Users); 301 302 my $scrolling_set_list = scrollingRecordList({ 303 name => "selected_sets", 304 request => $r, 305 default_sort => "set_id", 306 default_format => "set_id", 307 default_filters => ["all"], 308 size => 20, 309 multiple => $perm_multiset, 310 }, @WantedGlobalSets); 311 312 # we change the text a little bit depending on whether the user has multiuser privileges 313 my $ss = $perm_multiuser ? "s" : ""; 314 my $aa = $perm_multiuser ? " " : " a "; 315 my $phrase_for_privileged_users = $perm_multiuser ? "to privileged users or" : ""; 316 my $button_label = $perm_multiuser ? "Generate hardcopy for selected sets and selected users" :"Generate hardcopy"; 317 318 # print CGI::start_p(); 319 # print "Select the homework set$ss for which to generate${aa}hardcopy version$ss."; 320 # if ($authz->hasPermissions($userID, "download_hardcopy_multiuser")) { 321 # print "You may also select multiple users from the users list. You will receive hardcopy for each (set, user) pair."; 322 # } 323 # print CGI::end_p(); 324 325 print CGI::start_form(-method=>"POST", -action=>$r->uri); 326 print $self->hidden_authen_fields(); 327 print CGI::hidden("in_hc_form", 1); 328 329 if ($perm_multiuser and $perm_multiset) { 330 print CGI::p("Select the homework sets for which to generate hardcopy versions. You may" 331 ." also select multiple users from the users list. You will receive hardcopy" 332 ." for each (set, user) pair."); 333 334 print CGI::table({class=>"FormLayout"}, 335 CGI::Tr( 336 CGI::th("Users"), 337 CGI::th("Sets"), 338 ), 339 CGI::Tr( 340 CGI::td($scrolling_user_list), 341 CGI::td($scrolling_set_list), 342 ), 343 ); 344 } else { # single user mode 345 #FIXME -- do a better job of getting the set and the user when in the single set mode 346 my $selected_set_id = $r->param("selected_sets"); 347 my $selected_user_id = $Users[0]->user_id; 348 print CGI::hidden("selected_sets", $selected_set_id ), 349 CGI::hidden( "selected_users", $selected_user_id); 350 351 print CGI::p("Download hardcopy of set ", $selected_set_id, " for ", $Users[0]->first_name, " ",$Users[0]->last_name,"?"); 352 353 } 354 print CGI::table({class=>"FormLayout"}, 355 CGI::Tr( 356 CGI::td({colspan=>2, class=>"ButtonRow"}, 357 CGI::small("You may choose to show any of the following data. Correct answers and solutions are only available $phrase_for_privileged_users after the answer date of the homework set."), 358 CGI::br(), 359 CGI::b("Show:"), " ", 360 CGI::checkbox( 361 -name => "showCorrectAnswers", 362 -checked => scalar($r->param("showCorrectAnswers")) || 0, 363 -label => "Correct answers", 364 ), 365 CGI::checkbox( 366 -name => "showHints", 367 -checked => scalar($r->param("showHints")) || 0, 368 -label => "Hints", 369 ), 370 CGI::checkbox( 371 -name => "showSolutions", 372 -checked => scalar($r->param("showSolutions")) || 0, 373 -label => "Solutions", 374 ), 375 ), 376 ), 377 CGI::Tr( 378 CGI::td({colspan=>2, class=>"ButtonRow"}, 379 CGI::b("Hardcopy Format:"), " ", 380 CGI::radio_group( 381 -name => "hardcopy_format", 382 -values => \@formats, 383 -default => scalar($r->param("hardcopy_format")) || $HC_DEFAULT_FORMAT, 384 -labels => \%format_labels, 385 ), 386 ), 387 ), 388 CGI::Tr( 389 CGI::td({colspan=>2, class=>"ButtonRow"}, 390 CGI::submit( 391 -name => "generate_hardcopy", 392 -value => $button_label, 393 #-style => "width: 45ex", 394 ), 395 ), 396 ), 397 ); 398 399 print CGI::end_form(); 400 401 return ""; 402 } 403 404 ################################################################################ 405 # harddcopy generating subroutines 406 ################################################################################ 407 408 sub generate_hardcopy { 409 my ($self, $format, $userIDsRef, $setIDsRef) = @_; 410 my $r = $self->r; 411 my $ce = $r->ce; 412 my $db = $r->db; 413 my $authz = $r->authz; 414 415 my $courseID = $r->urlpath->arg("courseID"); 416 my $userID = $r->param("user"); 417 my $eUserID = $r->param("effectiveUser"); 418 419 # we want to make the temp directory web-accessible, for error reporting 420 my $temp_dir_parent_path = $ce->{courseDirs}{html_temp} . "/hardcopy"; 421 #FIXME 422 # ensure that .../hardcopy exists 423 unless (-w $temp_dir_parent_path) { 424 mkdir "$temp_dir_parent_path" 425 or die "Failed to create course directory $temp_dir_parent_path: $!\n"; 426 } 427 my $temp_dir_path = eval { makeTempDirectory($temp_dir_parent_path, "work") }; 428 if ($@) { 429 $self->add_errors(CGI::escapeHTML($@)); 430 return; 431 } 432 433 # do some error checking 434 unless (-e $temp_dir_path) { 435 $self->add_errors("Temporary directory '".CGI::code(CGI::escapeHTML($temp_dir_path)) 436 ."' does not exist, but creation didn't fail. This shouldn't happen."); 437 return; 438 } 439 unless (-w $temp_dir_path) { 440 $self->add_errors("Temporary directory '".CGI::code(CGI::escapeHTML($temp_dir_path)) 441 ."' is not writeable."); 442 $self->delete_temp_dir($temp_dir_path); 443 return; 444 } 445 446 my $tex_file_name = "hardcopy.tex"; 447 my $tex_file_path = "$temp_dir_path/$tex_file_name"; 448 449 # write TeX 450 my $open_result = open my $FH, ">", $tex_file_path; 451 unless ($open_result) { 452 $self->add_errors("Failed to open file '".CGI::code(CGI::escapeHTML($tex_file_path)) 453 ."' for writing: ".CGI::code(CGI::escapeHTML($!))); 454 $self->delete_temp_dir($temp_dir_path); 455 return; 456 } 457 $self->write_multiuser_tex($FH, $userIDsRef, $setIDsRef); 458 close $FH; 459 460 # if no problems got rendered successfully, we can't continue 461 unless ($self->{at_least_one_problem_rendered_without_error}) { 462 $self->add_errors("No problems rendered. Can't continue."); 463 $self->delete_temp_dir($temp_dir_path); 464 return; 465 } 466 467 # if no hardcopy.tex file was generated, fail now 468 unless (-e "$temp_dir_path/hardcopy.tex") { 469 $self->add_errors("'".CGI::code("hardcopy.tex")."' not written to temporary directory '" 470 .CGI::code(CGI::escapeHTML($temp_dir_path))."'. Can't continue."); 471 $self->delete_temp_dir($temp_dir_path); 472 return; 473 } 474 475 # determine base name of final file 476 my $final_file_user = @$userIDsRef > 1 ? "multiuser" : $userIDsRef->[0]; 477 my $final_file_set = @$setIDsRef > 1 ? "multiset" : $setIDsRef->[0]; 478 my $final_file_basename = "$courseID.$final_file_user.$final_file_set"; 479 480 # call format subroutine 481 # $final_file_name is the name of final hardcopy file 482 # @temp_files is a list of temporary files of interest used by the subroutine 483 # (all are relative to $temp_dir_path) 484 my $format_subr = $HC_FORMATS{$format}{subr}; 485 my ($final_file_name, @temp_files) = $self->$format_subr($temp_dir_path, $final_file_basename); 486 my $final_file_path = "$temp_dir_path/$final_file_name"; 487 488 #warn "final_file_name=$final_file_name\n"; 489 #warn "temp_files=@temp_files\n"; 490 491 # calculate URLs for each temp file of interest 492 # makeTempDirectory's interface forces us to reverse-engineer the name of the temp dir from the path 493 my $temp_dir_parent_url = $ce->{courseURLs}{html_temp} . "/hardcopy"; 494 (my $temp_dir_url = $temp_dir_path) =~ s/^$temp_dir_parent_path/$temp_dir_parent_url/; 495 my %temp_file_map; 496 foreach my $temp_file_name (@temp_files) { 497 $temp_file_map{$temp_file_name} = "$temp_dir_url/$temp_file_name"; 498 } 499 500 my $final_file_url; 501 502 # make sure final file exists 503 unless (-e $final_file_path) { 504 $self->add_errors("Final hardcopy file '".CGI::code(CGI::escapeHTML($final_file_path)) 505 ."' not found after calling '".CGI::code(CGI::escapeHTML($format_subr))."': " 506 .CGI::code(CGI::escapeHTML($!))); 507 return $final_file_url, %temp_file_map; 508 } 509 510 # try to move the hardcopy file out of the temp directory 511 # set $final_file_url accordingly 512 my $final_file_final_path = "$temp_dir_parent_path/$final_file_name"; 513 my $mv_cmd = "2>&1 /bin/mv " . shell_quote($final_file_path, $final_file_final_path); 514 my $mv_out = readpipe $mv_cmd; 515 if ($?) { 516 $self->add_errors("Failed to move hardcopy file '".CGI::code(CGI::escapeHTML($final_file_name)) 517 ."' from '".CGI::code(CGI::escapeHTML($temp_dir_path))."' to '" 518 .CGI::code(CGI::escapeHTML($temp_dir_parent_path))."':".CGI::br() 519 .CGI::pre(CGI::escapeHTML($mv_out))); 520 $final_file_url = "$temp_dir_url/$final_file_name"; 521 } else { 522 $final_file_url = "$temp_dir_parent_url/$final_file_name"; 523 } 524 525 # remove the temp directory if there are no errors 526 unless ($self->get_errors or $PreserveTempFiles) { 527 $self->delete_temp_dir($temp_dir_path); 528 } 529 530 warn "Preserved temporary files in directory '$temp_dir_path'.\n" if $PreserveTempFiles; 531 532 return $final_file_url, %temp_file_map; 533 } 534 535 # helper function to remove temp dirs 536 sub delete_temp_dir { 537 my ($self, $temp_dir_path) = @_; 538 539 my $rm_cmd = "2>&1 /bin/rm -rf " . shell_quote($temp_dir_path); 540 my $rm_out = readpipe $rm_cmd; 541 if ($?) { 542 $self->add_errors("Failed to remove temporary directory '".CGI::code(CGI::escapeHTML($temp_dir_path))."':" 543 .CGI::br().CGI::pre($rm_out)); 544 return 0; 545 } else { 546 return 1; 547 } 548 } 549 550 # format subroutines 551 # 552 # assume that TeX source is located at $temp_dir_path/hardcopy.tex 553 # the generated file will being with $final_file_basename 554 # first element of return value is the name of the generated file (relative to $temp_dir_path) 555 # rest of return value elements are names of temporary files that may be of interest in the 556 # case of an error, relative to $temp_dir_path. these are returned whether or not an error 557 # actually occured. 558 559 sub generate_hardcopy_tex { 560 my ($self, $temp_dir_path, $final_file_basename) = @_; 561 562 my $final_file_name; 563 564 # try to rename tex file 565 my $src_name = "hardcopy.tex"; 566 my $dest_name = "$final_file_basename.tex"; 567 my $mv_cmd = "2>&1 /bin/mv " . shell_quote("$temp_dir_path/$src_name", "$temp_dir_path/$dest_name"); 568 my $mv_out = readpipe $mv_cmd; 569 if ($?) { 570 $self->add_errors("Failed to rename '".CGI::code(CGI::escapeHTML($src_name))."' to '" 571 .CGI::code(CGI::escapeHTML($dest_name))."' in directory '" 572 .CGI::code(CGI::escapeHTML($temp_dir_path))."':".CGI::br() 573 .CGI::pre(CGI::escapeHTML($mv_out))); 574 $final_file_name = $src_name; 575 } else { 576 $final_file_name = $dest_name; 577 } 578 579 return $final_file_name; 580 } 581 582 sub generate_hardcopy_pdf { 583 my ($self, $temp_dir_path, $final_file_basename) = @_; 584 585 # call pdflatex - we don't want to chdir in the mod_perl process, as 586 # that might step on the feet of other things (esp. in Apache 2.0) 587 my $pdflatex_cmd = "cd " . shell_quote($temp_dir_path) . " && " 588 . $self->r->ce->{externalPrograms}{pdflatex} 589 . " >pdflatex.stdout 2>pdflatex.stderr hardcopy"; 590 if (system $pdflatex_cmd) { 591 $self->add_errors("Failed to convert TeX to PDF with command '" 592 .CGI::code(CGI::escapeHTML($pdflatex_cmd))."'."); 593 594 # read hardcopy.log and report first error 595 my $hardcopy_log = "$temp_dir_path/hardcopy.log"; 596 if (-e $hardcopy_log) { 597 if (open my $LOG, "<", $hardcopy_log) { 598 my $line; 599 while ($line = <$LOG>) { 600 last if $line =~ /^!\s+/; 601 } 602 my $first_error = $line; 603 while ($line = <$LOG>) { 604 last if $line =~ /^!\s+/; 605 $first_error .= $line; 606 } 607 close $LOG; 608 if (defined $first_error) { 609 $self->add_errors("First error in TeX log is:".CGI::br(). 610 CGI::pre(CGI::escapeHTML($first_error))); 611 } else { 612 $self->add_errors("No errors encoundered in TeX log."); 613 } 614 } else { 615 $self->add_errors("Could not read TeX log: ".CGI::code(CGI::escapeHTML($!))); 616 } 617 } else { 618 $self->add_errors("No TeX log was found."); 619 } 620 } 621 622 my $final_file_name; 623 624 # try rename the pdf file 625 my $src_name = "hardcopy.pdf"; 626 my $dest_name = "$final_file_basename.pdf"; 627 my $mv_cmd = "2>&1 /bin/mv " . shell_quote("$temp_dir_path/$src_name", "$temp_dir_path/$dest_name"); 628 my $mv_out = readpipe $mv_cmd; 629 if ($?) { 630 $self->add_errors("Failed to rename '".CGI::code(CGI::escapeHTML($src_name))."' to '" 631 .CGI::code(CGI::escapeHTML($dest_name))."' in directory '" 632 .CGI::code(CGI::escapeHTML($temp_dir_path))."':".CGI::br() 633 .CGI::pre(CGI::escapeHTML($mv_out))); 634 $final_file_name = $src_name; 635 } else { 636 $final_file_name = $dest_name; 637 } 638 639 return $final_file_name, qw/hardcopy.tex hardcopy.log hardcopy.aux pdflatex.stdout pdflatex.stderr/; 640 } 641 642 ################################################################################ 643 # TeX aggregating subroutines 644 ################################################################################ 645 646 sub write_multiuser_tex { 647 my ($self, $FH, $userIDsRef, $setIDsRef) = @_; 648 my $r = $self->r; 649 my $ce = $r->ce; 650 651 my @userIDs = @$userIDsRef; 652 my @setIDs = @$setIDsRef; 653 654 # get snippets 655 my $preamble = $ce->{webworkFiles}->{hardcopySnippets}->{preamble}; 656 my $postamble = $ce->{webworkFiles}->{hardcopySnippets}->{postamble}; 657 my $divider = $ce->{webworkFiles}->{hardcopySnippets}->{userDivider}; 658 659 # write preamble 660 $self->write_tex_file($FH, $preamble); 661 662 # write section for each user 663 while (defined (my $userID = shift @userIDs)) { 664 $self->write_multiset_tex($FH, $userID, @setIDs); 665 $self->write_tex_file($FH, $divider) if @userIDs; # divide users, but not after the last user 666 } 667 668 # write postamble 669 $self->write_tex_file($FH, $postamble); 670 } 671 672 sub write_multiset_tex { 673 my ($self, $FH, $targetUserID, @setIDs) = @_; 674 my $r = $self->r; 675 my $ce = $r->ce; 676 my $db = $r->db; 677 678 # get user record 679 my $TargetUser = $db->getUser($targetUserID); # checked 680 unless ($TargetUser) { 681 $self->add_errors("Can't generate hardcopy for user '".CGI::code(CGI::escapeHTML($targetUserID))."' -- no such user exists.\n"); 682 return; 683 } 684 685 # get set divider 686 my $divider = $ce->{webworkFiles}->{hardcopySnippets}->{setDivider}; 687 688 # write each set 689 while (defined (my $setID = shift @setIDs)) { 690 $self->write_set_tex($FH, $TargetUser, $setID); 691 $self->write_tex_file($FH, $divider) if @setIDs; # divide sets, but not after the last set 692 } 693 } 694 695 sub write_set_tex { 696 my ($self, $FH, $TargetUser, $setID) = @_; 697 my $r = $self->r; 698 my $ce = $r->ce; 699 my $db = $r->db; 700 my $authz = $r->authz; 701 my $userID = $r->param("user"); 702 703 # get set record 704 my $MergedSet = $db->getMergedSet($TargetUser->user_id, $setID); # checked 705 unless ($MergedSet) { 706 $self->add_errors("Can't generate hardcopy for set ''".CGI::code(CGI::escapeHTML($setID)) 707 ."' for user '".CGI::code(CGI::escapeHTML($TargetUser->user_id)) 708 ."' -- set is not assigned to that user."); 709 return; 710 } 711 712 # see if the *real* user is allowed to access this problem set 713 if ($MergedSet->open_date > time and not $authz->hasPermissions($userID, "view_unopened_sets")) { 714 $self->add_errors("Can't generate hardcopy for set '".CGI::code(CGI::escapeHTML($setID)) 715 ."' for user '".CGI::code(CGI::escapeHTML($TargetUser->user_id)) 716 ."' -- set is not yet open."); 717 return; 718 } 719 if (not $MergedSet->published and not $authz->hasPermissions($userID, "view_unpublished_sets")) { 720 $self->addbadmessage("Can't generate hardcopy for set '".CGI::code(CGI::escapeHTML($setID)) 721 ."' for user '".CGI::code(CGI::escapeHTML($TargetUser->user_id)) 722 ."' -- set has not been published."); 723 return; 724 } 725 726 # get snippets 727 my $header = $MergedSet->hardcopy_header 728 ? $MergedSet->hardcopy_header 729 : $ce->{webworkFiles}->{hardcopySnippets}->{setHeader}; 730 my $footer = $ce->{webworkFiles}->{hardcopySnippets}->{setFooter}; 731 my $divider = $ce->{webworkFiles}->{hardcopySnippets}->{problemDivider}; 732 733 # get list of problem IDs 734 my @problemIDs = sort { $a <=> $b } $db->listUserProblems($MergedSet->user_id, $MergedSet->set_id); 735 736 # write set header 737 $self->write_problem_tex($FH, $TargetUser, $MergedSet, 0, $header); # 0 => pg file specified directly 738 739 # write each problem 740 while (my $problemID = shift @problemIDs) { 741 $self->write_tex_file($FH, $divider); 742 $self->write_problem_tex($FH, $TargetUser, $MergedSet, $problemID); 743 } 744 745 # write footer 746 $self->write_problem_tex($FH, $TargetUser, $MergedSet, 0, $footer); # 0 => pg file specified directly 747 } 748 749 sub write_problem_tex { 750 my ($self, $FH, $TargetUser, $MergedSet, $problemID, $pgFile) = @_; 751 my $r = $self->r; 752 my $ce = $r->ce; 753 my $db = $r->db; 754 my $authz = $r->authz; 755 my $userID = $r->param("user"); 756 757 my @errors; 758 759 # get problem record 760 my $MergedProblem; 761 if ($problemID) { 762 # a non-zero problem ID was given -- load that problem 763 $MergedProblem = $db->getMergedProblem($MergedSet->user_id, $MergedSet->set_id, $problemID); # checked 764 765 # handle nonexistent problem 766 unless ($MergedProblem) { 767 $self->add_errors("Can't generate hardcopy for problem '" 768 .CGI::code(CGI::escapeHTML($problemID))."' in set '" 769 .CGI::code(CGI::escapeHTML($MergedSet->set_id)) 770 ."' for user '".CGI::code(CGI::escapeHTML($MergedSet->user_id)) 771 ."' -- problem does not exist in that set or is not assigned to that user."); 772 return; 773 } 774 } elsif ($pgFile) { 775 # otherwise, we try an explicit PG file 776 $MergedProblem = $db->newUserProblem( 777 user_id => $MergedSet->user_id, 778 set_id => $MergedSet->set_id, 779 problem_id => 0, 780 source_file => $pgFile, 781 ); 782 die "newUserProblem failed -- WTF?" unless $MergedProblem; # this should never happen 783 } else { 784 # this shouldn't happen -- error out for real 785 die "write_problem_tex needs either a non-zero \$problemID or a \$pgFile"; 786 } 787 788 # figure out if we're allowed to get correct answers, hints, and solutions 789 # (eventually, we'd like to be able to use the same code as Problem) 790 my $showCorrectAnswers = $r->param("showCorrectAnswers") || 0; 791 my $showHints = $r->param("showHints") || 0; 792 my $showSolutions = $r->param("showSolutions") || 0; 793 unless ($authz->hasPermissions($userID, "show_correct_answers_before_answer_date") or time > $MergedSet->answer_date) { 794 $showCorrectAnswers = 0; 795 $showSolutions = 0; 796 } 797 798 # FIXME -- there can be a problem if the $siteDefaults{timezone} is not defined? Why is this? 799 # why does it only occur with hardcopy? 800 my $pg = WeBWorK::PG->new( 801 $ce, 802 $TargetUser, 803 scalar($r->param('key')), # avoid multiple-values problem 804 $MergedSet, 805 $MergedProblem, 806 $MergedSet->psvn, 807 {}, # no form fields! 808 { # translation options 809 displayMode => "tex", 810 showHints => $showHints ? 1 : 0, # insure that this value is numeric 811 showSolutions => $showSolutions ? 1 : 0, # (or what? -sam) 812 processAnswers => $showCorrectAnswers ? 1 : 0, 813 }, 814 ); 815 816 # only bother to generate this info if there were warnings or errors 817 my $edit_url; 818 my $problem_name; 819 my $problem_desc; 820 if ($pg->{warnings} ne "" or $pg->{flags}->{error_flag}) { 821 my $edit_urlpath = $r->urlpath->newFromModule( 822 "WeBWorK::ContentGenerator::Instructor::PGProblemEditor", 823 courseID => $r->urlpath->arg("courseID"), 824 setID => $MergedProblem->set_id, 825 problemID => $MergedProblem->problem_id, 826 ); 827 828 if ($MergedProblem->problem_id == 0) { 829 # link for an fake problem (like a header file) 830 $edit_url = $self->systemLink($edit_urlpath, 831 params => { 832 sourceFilePath => $MergedProblem->source_file, 833 problemSeed => $MergedProblem->problem_seed, 834 }, 835 ); 836 } else { 837 # link for a real problem 838 $edit_url = $self->systemLink($edit_urlpath); 839 } 840 841 if ($MergedProblem->problem_id == 0) { 842 $problem_name = "snippet"; 843 $problem_desc = $problem_name." '".$MergedProblem->source_file 844 ."' for set '".$MergedProblem->set_id."' and user '" 845 .$MergedProblem->user_id."'"; 846 } else { 847 $problem_name = "problem"; 848 $problem_desc = $problem_name." '".$MergedProblem->problem_id 849 ."' in set '".$MergedProblem->set_id."' for user '" 850 .$MergedProblem->user_id."'"; 851 } 852 } 853 854 # deal with PG warnings 855 if ($pg->{warnings} ne "") { 856 $self->add_errors(CGI::a({href=>$edit_url}, "[edit]") 857 ."Warnings encountered while processing $problem_desc. " 858 ."Error text:".CGI::br().CGI::pre(CGI::escapeHTML($pg->{warnings})) 859 ); 860 } 861 862 # deal with PG errors 863 if ($pg->{flags}->{error_flag}) { 864 $self->add_errors(CGI::a({href=>$edit_url}, "[edit]") 865 ."Errors encountered while processing $problem_desc. " 866 ."This $problem_name has been omitted from the hardcopy. " 867 ."Error text:".CGI::br().CGI::pre(CGI::escapeHTML($pg->{errors})) 868 ); 869 return; 870 } 871 872 # if we got here, there were no errors (because errors cause a return above) 873 $self->{at_least_one_problem_rendered_without_error} = 1; 874 875 print $FH $pg->{body_text}; 876 877 # write the list of correct answers is appropriate 878 my @ans_entry_order = @{$pg->{flags}->{ANSWER_ENTRY_ORDER}}; 879 if ($showCorrectAnswers && $MergedProblem->problem_id != 0 && @ans_entry_order) { 880 my $correctTeX = "\\par{\\small{\\it Correct Answers:}\n" 881 . "\\vspace{-\\parskip}\\begin{itemize}\n"; 882 883 foreach my $ansName (@ans_entry_order) { 884 my $correctAnswer = $pg->{answers}->{$ansName}->{correct_ans}; 885 $correctTeX .= "\\item\\begin{verbatim}$correctAnswer\\end{verbatim}\n"; 886 # FIXME: What about vectors (where TeX will complain about < and > outside of math mode)? 887 } 888 889 $correctTeX .= "\\end{itemize}}\\par\n"; 890 891 print $FH $correctTeX; 892 } 893 } 894 895 sub write_tex_file { 896 my ($self, $FH, $file) = @_; 897 898 my $tex = eval { readFile($file) }; 899 if ($@) { 900 $self->add_errors("Failed to include TeX file '".CGI::code(CGI::escapeHTML($file))."': " 901 .CGI::escapeHTML($@)); 902 } else { 903 print $FH $tex; 904 } 905 } 906 907 ################################################################################ 908 # utilities 909 ################################################################################ 910 911 sub add_errors { 912 my ($self, @errors) = @_; 913 push @{$self->{hardcopy_errors}}, @errors; 914 } 915 916 sub get_errors { 917 my ($self) = @_; 918 return $self->{hardcopy_errors} ? @{$self->{hardcopy_errors}} : (); 919 } 920 921 sub get_errors_ref { 922 my ($self) = @_; 923 return $self->{hardcopy_errors}; 924 } 925 926 1;
| aubreyja at gmail dot com | ViewVC Help |
| Powered by ViewVC 1.0.9 |