[system] / trunk / webwork-modperl / lib / WeBWorK / ContentGenerator / Hardcopy.pm Repository:
ViewVC logotype

View of /trunk/webwork-modperl/lib/WeBWorK/ContentGenerator/Hardcopy.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 4277 - (download) (as text) (annotate)
Mon Jul 17 21:51:12 2006 UTC (6 years, 10 months ago) by gage
File size: 31945 byte(s)
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