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