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