[system] / branches / rel-2-2-dev / webwork-modperl / lib / WeBWorK / ContentGenerator / Hardcopy.pm Repository:
ViewVC logotype

View of /branches/rel-2-2-dev/webwork-modperl/lib/WeBWorK/ContentGenerator/Hardcopy.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 4340 - (download) (as text) (annotate)
Thu Aug 3 17:13:02 2006 UTC (6 years, 9 months ago) by sh002i
File size: 31789 byte(s)
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