[system] / branches / gage_dev / webwork2 / lib / WeBWorK / ContentGenerator / Hardcopy.pm Repository:
ViewVC logotype

View of /branches/gage_dev/webwork2/lib/WeBWorK/ContentGenerator/Hardcopy.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 3973 - (download) (as text) (annotate)
Wed Jan 25 23:13:56 2006 UTC (7 years, 3 months ago) by sh002i
Original Path: trunk/webwork2/lib/WeBWorK/ContentGenerator/Hardcopy.pm
File size: 31678 byte(s)
forward-port from rel-2-2-dev: (update copyright date range -- 2000-2006.
this is probably overkill, since there are some files that were created
after 2000 and some files that were last modified before 2006.)

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

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9