[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 6272 - (download) (as text) (annotate)
Thu May 20 12:11:23 2010 UTC (2 years, 11 months ago) by gage
File size: 42397 byte(s)
Insure that error files can be read from the web even if the files are served using
lighttpd and port 8080 instead of the main server.
This adds group readable and executable permissions to the working directory for creating the tex files.

    1 ################################################################################
    2 # WeBWorK Online Homework Delivery System
    3 # Copyright © 2000-2007 The WeBWorK Project, http://openwebwork.sf.net/
    4 # $CVSHeader: webwork2/lib/WeBWorK/ContentGenerator/Hardcopy.pm,v 1.102 2009/09/25 00:39:49 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 decodeAnswers/;
   43 use PGrandom;
   44 
   45 =head1 CONFIGURATION VARIABLES
   46 
   47 =over
   48 
   49 =item $PreserveTempFiles
   50 
   51 If true, don't delete temporary files.
   52 
   53 =cut
   54 
   55 our $PreserveTempFiles = 0 unless defined $PreserveTempFiles;
   56 
   57 =back
   58 
   59 =cut
   60 
   61 our $HC_DEFAULT_FORMAT = "pdf"; # problems if this is not an allowed format for the user...
   62 our %HC_FORMATS = (
   63   tex => { name => "TeX Source", subr => "generate_hardcopy_tex" },
   64   pdf => { name => "Adobe PDF",  subr => "generate_hardcopy_pdf" },
   65 );
   66 
   67 # custom fields used in $self hash
   68 # FOR HEAVEN'S SAKE, PLEASE KEEP THIS UP-TO-DATE!
   69 #
   70 # final_file_url
   71 #   contains the URL of the final hardcopy file generated
   72 #   set by generate_hardcopy(), used by pre_header_initialize() and body()
   73 #
   74 # temp_file_map
   75 #   reference to a hash mapping temporary file names to URL
   76 #   set by pre_header_initialize(), used by body()
   77 #
   78 # hardcopy_errors
   79 #   reference to array containing HTML strings describing generation errors (and warnings)
   80 #   used by add_errors(), get_errors(), get_errors_ref()
   81 #
   82 # at_least_one_problem_rendered_without_error
   83 #   set to a true value by write_problem_tex if it is able to sucessfully render
   84 #   a problem. checked by generate_hardcopy to determine whether to continue
   85 #   with the generation process.
   86 #
   87 # versioned
   88 #   set to a true value in write_set_tex if the set_id indicates that
   89 #   the set being rendered is a versioned set; this is used in
   90 #   write_problem_tex to determine which problem merging routine from
   91 #   DB.pm to use, and to indicate what problem number in a versioned
   92 #   test we're on
   93 #
   94 # mergedSets
   95 #   a reference to a hash { userID!setID => setObject }, where setID is
   96 #   either the set id or the fake versioned set id "setName,vN" depending
   97 #   on whether the set is a versioned set or not.  this may include the
   98 #   sets for which the hardcopy is being generated (or may not), depending
   99 #   on whether they were needed to determine the required permissions for
  100 #   generating a hardcopy
  101 #
  102 # canShowScore
  103 #   a reference to a hash { userID!setID => boolean }, where setID is either
  104 #   the set id or the fake versioned set id "setName,vN" depending on whether
  105 #   the set is a versioned set or not, and the value of the boolean is
  106 #   determined by the corresponding userSet's value of hide_score and the
  107 #   current time
  108 
  109 ################################################################################
  110 # UI subroutines
  111 ################################################################################
  112 
  113 sub pre_header_initialize {
  114   my ($self) = @_;
  115   my $r = $self->r;
  116   my $ce = $r->ce;
  117   my $db = $r->db;
  118   my $authz = $r->authz;
  119 
  120   my $userID = $r->param("user");
  121   my $eUserID = $r->param("effectiveUser");
  122   my @setIDs = $r->param("selected_sets");
  123   my @userIDs = $r->param("selected_users");
  124   my $hardcopy_format = $r->param("hardcopy_format");
  125   my $generate_hardcopy = $r->param("generate_hardcopy");
  126   my $send_existing_hardcopy = $r->param("send_existing_hardcopy");
  127   my $final_file_url = $r->param("final_file_url");
  128 
  129   # if there's an existing hardcopy file that can be sent, get set up to do that
  130   if ($send_existing_hardcopy) {
  131     $self->reply_with_redirect($final_file_url);
  132     $self->{final_file_url} = $final_file_url;
  133     $self->{send_hardcopy} = 1;
  134     return;
  135   }
  136 
  137   # this should never happen, but apparently it did once (see bug #714), so we check for it
  138   die "Parameter 'user' not defined -- this should never happen" unless defined $userID;
  139 
  140   if ($generate_hardcopy) {
  141     my $validation_failed = 0;
  142 
  143     # set default format
  144     $hardcopy_format = $HC_DEFAULT_FORMAT unless defined $hardcopy_format;
  145 
  146     # make sure format is valid
  147     unless (grep { $_ eq $hardcopy_format } keys %HC_FORMATS) {
  148       $self->addbadmessage("'$hardcopy_format' is not a valid hardcopy format.");
  149       $validation_failed = 1;
  150     }
  151 
  152     # make sure we are allowed to generate hardcopy in this format
  153     unless ($authz->hasPermissions($userID, "download_hardcopy_format_$hardcopy_format")) {
  154       $self->addbadmessage("You do not have permission to generate hardcopy in $hardcopy_format format.");
  155       $validation_failed = 1;
  156     }
  157 
  158     # is there at least one user and set selected?
  159     unless (@userIDs) {
  160       $self->addbadmessage("Please select at least one user and try again.");
  161       $validation_failed = 1;
  162     }
  163 
  164 # when students don't select any sets the size of @setIDs is 1 with a null character in $setIDs[0].
  165 # when professors don't select any sets the size of @setIDs is 0.
  166 # the following test "unless ((@setIDs) and ($setIDs[0] =~ /\S+/))" catches both cases and prevents
  167 # warning messages in the case of a professor's empty array.
  168     unless ((@setIDs) and ($setIDs[0] =~ /\S+/)) {
  169       $self->addbadmessage("Please select at least one set and try again.");
  170       $validation_failed = 1;
  171     }
  172 
  173     # is the user allowed to request multiple sets/users at a time?
  174     my $perm_multiset = $authz->hasPermissions($userID, "download_hardcopy_multiset");
  175     my $perm_multiuser = $authz->hasPermissions($userID, "download_hardcopy_multiuser");
  176 
  177     my $perm_viewhidden = $authz->hasPermissions($userID, "view_hidden_work");
  178     my $perm_viewfromip = $authz->hasPermissions($userID, "view_ip_restricted_sets");
  179 
  180     if (@setIDs > 1 and not $perm_multiset) {
  181       $self->addbadmessage("You are not permitted to generate hardcopy for multiple sets. Please select a single set and try again.");
  182       $validation_failed = 1;
  183     }
  184     if (@userIDs > 1 and not $perm_multiuser) {
  185       $self->addbadmessage("You are not permitted to generate hardcopy for multiple users. Please select a single user and try again.");
  186       $validation_failed = 1;
  187     }
  188     if (@userIDs and $userIDs[0] ne $eUserID and not $perm_multiuser) {
  189       $self->addbadmessage("You are not permitted to generate hardcopy for other users.");
  190       $validation_failed = 1;
  191       # FIXME -- download_hardcopy_multiuser controls both whether a user can generate hardcopy
  192       # that contains sets for multiple users AND whether she can generate hardcopy that contains
  193       # sets for users other than herself. should these be separate permission levels?
  194     }
  195 
  196     # to check if the set has a "hide_work" flag, or if we aren't
  197     #    allowed to view the set from the user's IP address, we
  198     #    need the userset objects; if we've not failed validation
  199     #    yet, get those to check on this
  200     my %canShowScore = ();
  201     my %mergedSets = ();
  202     unless ($validation_failed ) {
  203       foreach my $sid ( @setIDs ) {
  204         my($s,undef,$v) = ($sid =~ /([^,]+)(,v(\d+))?$/);
  205         foreach my $uid ( @userIDs ) {
  206           if ( $perm_viewhidden && $perm_viewfromip ) {
  207             $canShowScore{"$uid!$sid"} = 1;
  208           } else {
  209             my $userSet;
  210             if ( defined($v) ) {
  211               $userSet = $db->getMergedSetVersion($uid,$s,$v);
  212             } else {
  213               $userSet = $db->getMergedSet($uid,$s);
  214             }
  215             $mergedSets{"$uid!$sid"} = $userSet;
  216             if ( ! $perm_viewhidden &&
  217                  defined( $userSet->hide_work ) &&
  218                  ( $userSet->hide_work eq 'Y' ||
  219                    ( $userSet->hide_work eq 'BeforeAnswerDate' &&
  220                time < $userSet->answer_date ) ) ) {
  221               $validation_failed = 1;
  222               $self->addbadmessage("You are not permitted to generate a hardcopy for a set with hidden work.");
  223               last;
  224             }
  225 
  226             if ( $authz->invalidIPAddress($userSet) ) {
  227               $validation_failed = 1;
  228               $self->addbadmessage("You are not allowed to generate a " .
  229                        "hardcopy for " . $userSet->set_id .
  230                        " from your IP address, " .
  231                        $r->connection->remote_ip . ".");
  232               last;
  233             }
  234 
  235             $canShowScore{"$uid!$sid"} =
  236                 ( ! defined( $userSet->hide_score ) ||
  237                   $userSet->hide_score eq '' ) ||
  238               ( $userSet ->hide_score eq 'N' ||
  239                 ( $userSet->hide_score eq 'BeforeAnswerDate' &&
  240                   time >= $userSet->answer_date ) );
  241 #   die("hide_score = ", $userSet->hide_score, "; canshow{$uid!$sid} = ", (($canShowScore{"$uid!$sid"})?"True":"False"), "\n");
  242 
  243           }
  244           last if $validation_failed;
  245         }
  246       }
  247     }
  248 
  249     unless ($validation_failed) {
  250       $self->{canShowScore} = \%canShowScore;
  251       $self->{mergedSets} = \%mergedSets;
  252       my ($final_file_url, %temp_file_map) = $self->generate_hardcopy($hardcopy_format, \@userIDs, \@setIDs);
  253       if ($self->get_errors) {
  254         # store the URLs in self hash so that body() can make a link to it
  255         $self->{final_file_url} = $final_file_url;
  256         $self->{temp_file_map} = \%temp_file_map;
  257       } else {
  258         # send the file only
  259         $self->reply_with_redirect($final_file_url);
  260       }
  261     }
  262   }
  263 }
  264 
  265 sub body {
  266   my ($self) = @_;
  267   my $userID = $self->r->param("user");
  268   my $perm_view_errors = $self->r->authz->hasPermissions($userID, "download_hardcopy_view_errors");
  269   $perm_view_errors = (defined($perm_view_errors) ) ? $perm_view_errors : 0;
  270   if (my $num = $self->get_errors) {
  271     my $final_file_url = $self->{final_file_url};
  272     my %temp_file_map = %{$self->{temp_file_map}};
  273     if($perm_view_errors) {
  274       my $errors_str = $num > 1 ? "errors" : "error";
  275       print CGI::p("$num $errors_str occured while generating hardcopy:");
  276 
  277       print CGI::ul(CGI::li($self->get_errors_ref));
  278     }
  279 
  280     if ($final_file_url) {
  281       print CGI::p(
  282         "A hardcopy file was generated, but it may not be complete or correct.",
  283         "Please check that no problems are missing and that they are all legible." ,
  284         "If not, please inform your instructor.<br />",
  285         CGI::a({href=>$final_file_url}, "Download Hardcopy"),
  286       );
  287     } else {
  288       print CGI::p(
  289         "WeBWorK was unable to generate a paper copy of this homework set.  Please inform your instructor. "
  290       );
  291 
  292     }
  293     if($perm_view_errors) {
  294       if (%temp_file_map) {
  295         print CGI::start_p();
  296         print "You can also examine the following temporary files: ";
  297         my $first = 1;
  298         while (my ($temp_file_name, $temp_file_url) = each %temp_file_map) {
  299           if ($first) {
  300             $first = 0;
  301           } else {
  302             print ", ";
  303           }
  304           print CGI::a({href=>$temp_file_url}, " $temp_file_name");
  305         }
  306         print CGI::end_p();
  307       }
  308     }
  309 
  310     print CGI::hr();
  311   }
  312 
  313   # don't display the retry form if there are errors and the user doesn't have permission to view the errors.
  314   unless ($self->get_errors and not $perm_view_errors) {
  315     $self->display_form();
  316   }
  317   ''; # return a blank
  318 }
  319 
  320 sub display_form {
  321   my ($self) = @_;
  322   my $r = $self->r;
  323   my $db = $r->db;
  324   my $authz = $r->authz;
  325   my $userID = $r->param("user");
  326   my $eUserID = $r->param("effectiveUser");
  327 
  328   # first time we show up here, fill in some values
  329   unless ($r->param("in_hc_form")) {
  330     # if a set was passed in via the path_info, add that to the list of sets.
  331     my $singleSet = $r->urlpath->arg("setID");
  332     if (defined $singleSet and $singleSet ne "") {
  333       my @selected_sets = $r->param("selected_sets");
  334       $r->param("selected_sets" => [ @selected_sets, $singleSet]) unless grep { $_ eq $singleSet } @selected_sets;
  335     }
  336 
  337     # if no users are selected, select the effective user
  338     my @selected_users = $r->param("selected_users");
  339     unless (@selected_users) {
  340       $r->param("selected_users" => $eUserID);
  341     }
  342   }
  343 
  344   my $perm_multiset = $authz->hasPermissions($userID, "download_hardcopy_multiset");
  345   my $perm_multiuser = $authz->hasPermissions($userID, "download_hardcopy_multiuser");
  346   my $perm_texformat = $authz->hasPermissions($userID, "download_hardcopy_format_tex");
  347   my $perm_unopened = $authz->hasPermissions($userID, "view_unopened_sets");
  348   my $perm_unpublished = $authz->hasPermissions($userID, "view_unpublished_sets");
  349 
  350   # get formats
  351   my @formats;
  352   foreach my $format (keys %HC_FORMATS) {
  353     push @formats, $format if $authz->hasPermissions($userID, "download_hardcopy_format_$format");
  354   }
  355 
  356   # get format names hash for radio buttons
  357   my %format_labels = map { $_ => $HC_FORMATS{$_}{name} || $_ } @formats;
  358 
  359   # get users for selection
  360   my @Users;
  361   if ($perm_multiuser) {
  362     # if we're allowed to select multiple users, get all the users
  363     # DBFIXME shouldn't need to pass list of users, should use iterator for results?
  364     @Users = $db->getUsers($db->listUsers);
  365   } else {
  366     # otherwise, we get our own record only
  367     @Users = $db->getUser($eUserID);
  368   }
  369 
  370   # get sets for selection
  371   # DBFIXME should use WHERE clause to filter on open_date and published, rather then getting all
  372   my @globalSetIDs;
  373   my @GlobalSets;
  374   if ($perm_multiuser) {
  375     # if we're allowed to select sets for multiple users, get all sets.
  376     @globalSetIDs = $db->listGlobalSets;
  377     @GlobalSets = $db->getGlobalSets(@globalSetIDs);
  378   } else {
  379     # otherwise, only get the sets assigned to the effective user.
  380     # note that we are getting GlobalSets, but using the list of UserSets assigned to the
  381     # effective user. this is because if we pass UserSets  to ScrollingRecordList it will
  382     # give us composite IDs back, which is a pain in the ass to deal with.
  383     @globalSetIDs = $db->listUserSets($eUserID);
  384     @GlobalSets = $db->getGlobalSets(@globalSetIDs);
  385   }
  386   # we also want to get the versioned sets for this user
  387   # FIXME: this is another place where we assume that there is a
  388   #    one-to-one correspondence between assignment_type =~ gateway
  389   #    and versioned sets.  I think we really should have a
  390   #    "is_versioned" flag on set objects instead.
  391   my @versionedSets = grep {$_->assignment_type =~ /gateway/} @GlobalSets;
  392   my @SetVersions = ();
  393   foreach my $v (@versionedSets) {
  394     my @usv = map { [$eUserID, $v->set_id, $_] } ( $db->listSetVersions( $eUserID, $v->set_id ) );
  395     push( @SetVersions, $db->getSetVersions( @usv ) );
  396   }
  397   # FIXME: this is a hideous, horrible hack.  the identifying key for
  398   #    a global set is the set_id.  those for a set version are the
  399   #    set_id and version_id.  but this means that we have trouble
  400   #    displaying them both together in HTML::scrollingRecordList.
  401   #    so we brutally play tricks with the set_id here, which probably
  402   #    is not very robust, and certainly is aesthetically displeasing.
  403   #    yuck.
  404   foreach ( @SetVersions ) {
  405     $_->set_id($_->set_id . ",v" . $_->version_id);
  406   }
  407 
  408   # filter out unwanted sets
  409   my @WantedGlobalSets;
  410   foreach my $i (0 .. $#GlobalSets) {
  411     my $Set = $GlobalSets[$i];
  412     unless (defined $Set) {
  413       warn "\$GlobalSets[$i] (ID $globalSetIDs[$i]) not defined -- skipping";
  414       next;
  415     }
  416     next unless $Set->open_date <= time or $perm_unopened;
  417     next unless $Set->published or $perm_unpublished;
  418     # also skip gateway sets, for which we have to have a
  419     #    version to print something
  420     next if $Set->assignment_type =~ /gateway/;
  421     push @WantedGlobalSets, $Set;
  422   }
  423 
  424   my $scrolling_user_list = scrollingRecordList({
  425     name => "selected_users",
  426     request => $r,
  427     default_sort => "lnfn",
  428     default_format => "lnfn_uid",
  429     default_filters => ["all"],
  430     size => 20,
  431     multiple => $perm_multiuser,
  432   }, @Users);
  433 
  434   my $scrolling_set_list = scrollingRecordList({
  435     name => "selected_sets",
  436     request => $r,
  437     default_sort => "set_id",
  438     default_format => "sid",
  439     default_filters => ["all"],
  440     size => 20,
  441     multiple => $perm_multiset,
  442   }, @WantedGlobalSets, @SetVersions );
  443 
  444   # we change the text a little bit depending on whether the user has multiuser privileges
  445   my $ss = $perm_multiuser ? "s" : "";
  446   my $aa = $perm_multiuser ? " " : " a ";
  447   my $phrase_for_privileged_users = $perm_multiuser ? "to privileged users or" : "";
  448   my $button_label = $perm_multiuser ? "Generate hardcopy for selected sets and selected users" :"Generate hardcopy";
  449 
  450 #   print CGI::start_p();
  451 #   print "Select the homework set$ss for which to generate${aa}hardcopy version$ss.";
  452 #   if ($authz->hasPermissions($userID, "download_hardcopy_multiuser")) {
  453 #     print "You may also select multiple users from the users list. You will receive hardcopy for each (set, user) pair.";
  454 #   }
  455 #   print CGI::end_p();
  456 
  457   print CGI::start_form(-method=>"POST", -action=>$r->uri);
  458   print $self->hidden_authen_fields();
  459   print CGI::hidden("in_hc_form", 1);
  460 
  461   if ($perm_multiuser and $perm_multiset) {
  462     print CGI::p("Select the homework sets for which to generate hardcopy versions. You may"
  463           ." also select multiple users from the users list. You will receive hardcopy"
  464           ." for each (set, user) pair.");
  465 
  466     print CGI::table({class=>"FormLayout"},
  467       CGI::Tr({},
  468         CGI::th("Users"),
  469         CGI::th("Sets"),
  470       ),
  471       CGI::Tr({},
  472         CGI::td($scrolling_user_list),
  473         CGI::td($scrolling_set_list),
  474       ),
  475     );
  476   } else { # single user mode
  477     #FIXME -- do a better job of getting the set and the user when in the single set mode
  478     my $selected_set_id = $r->param("selected_sets");
  479     $selected_set_id = '' unless defined $selected_set_id;
  480 
  481     my $selected_user_id = $Users[0]->user_id;
  482     print CGI::hidden("selected_sets",   $selected_set_id ),
  483           CGI::hidden( "selected_users", $selected_user_id);
  484 
  485           # make display for versioned sets a bit nicer
  486     $selected_set_id =~ s/,v(\d+)$/ (test $1)/;
  487 
  488     print CGI::p("Download hardcopy of set ", $selected_set_id, " for ", $Users[0]->first_name, " ",$Users[0]->last_name,"?");
  489 
  490   }
  491   print CGI::table({class=>"FormLayout"},
  492     CGI::Tr({},
  493       CGI::td({colspan=>2, class=>"ButtonRow"},
  494         CGI::small("You may choose to show any of the following data. Correct answers and solutions are only
  495                     available $phrase_for_privileged_users after the answer date of the homework set."),
  496         CGI::br(),
  497         CGI::b("Show:"), " ",
  498         CGI::checkbox(
  499           -name    => "showCorrectAnswers",
  500           -checked => scalar($r->param("showCorrectAnswers")) || 0,
  501           -label   => "Correct answers",
  502         ),
  503         CGI::checkbox(
  504           -name    => "showHints",
  505           -checked => scalar($r->param("showHints")) || 0,
  506           -label   => "Hints",
  507         ),
  508         CGI::checkbox(
  509           -name    => "showSolutions",
  510           -checked => scalar($r->param("showSolutions")) || 0,
  511           -label   => "Solutions",
  512         ),
  513       ),
  514     ),
  515     CGI::Tr({},
  516       CGI::td({colspan=>2, class=>"ButtonRow"},
  517         CGI::b("Hardcopy Format:"), " ",
  518         CGI::radio_group(
  519           -name    => "hardcopy_format",
  520           -values  => \@formats,
  521           -default => scalar($r->param("hardcopy_format")) || $HC_DEFAULT_FORMAT,
  522           -labels  => \%format_labels,
  523         ),
  524       ),
  525     ),
  526     CGI::Tr({},
  527       CGI::td({colspan=>2, class=>"ButtonRow"},
  528         CGI::submit(
  529           -name => "generate_hardcopy",
  530           -value => $button_label,
  531           #-style => "width: 45ex",
  532         ),
  533       ),
  534     ),
  535   );
  536 
  537   print CGI::end_form();
  538 
  539   return "";
  540 }
  541 
  542 ################################################################################
  543 # harddcopy generating subroutines
  544 ################################################################################
  545 
  546 sub generate_hardcopy {
  547   my ($self, $format, $userIDsRef, $setIDsRef) = @_;
  548   my $r = $self->r;
  549   my $ce = $r->ce;
  550   my $db = $r->db;
  551   my $authz = $r->authz;
  552 
  553   my $courseID = $r->urlpath->arg("courseID");
  554   my $userID = $r->param("user");
  555   my $eUserID = $r->param("effectiveUser");
  556 
  557   # we want to make the temp directory web-accessible, for error reporting
  558   # use mkpath to ensure it exists (mkpath is pretty much ``mkdir -p'')
  559   my $temp_dir_parent_path = $ce->{courseDirs}{html_temp} . "/hardcopy";
  560   eval { mkpath($temp_dir_parent_path) };
  561   if ($@) {
  562     die "Couldn't create hardcopy directory $temp_dir_parent_path: $@";
  563   }
  564 
  565   # create a randomly-named working directory in the hardcopy directory
  566   my $temp_dir_path = eval { tempdir("work.XXXXXXXX", DIR => $temp_dir_parent_path) };
  567   if ($@) {
  568     $self->add_errors("Couldn't create temporary working directory: ".CGI::code(CGI::escapeHTML($@)));
  569     return;
  570   }
  571   # make sure the directory can be read by other daemons e.g. lighttpd
  572   chmod 0755, $temp_dir_path;
  573 
  574 
  575   # do some error checking
  576   unless (-e $temp_dir_path) {
  577     $self->add_errors("Temporary directory '".CGI::code(CGI::escapeHTML($temp_dir_path))
  578       ."' does not exist, but creation didn't fail. This shouldn't happen.");
  579     return;
  580   }
  581   unless (-w $temp_dir_path) {
  582     $self->add_errors("Temporary directory '".CGI::code(CGI::escapeHTML($temp_dir_path))
  583       ."' is not writeable.");
  584     $self->delete_temp_dir($temp_dir_path);
  585     return;
  586   }
  587 
  588   my $tex_file_name = "hardcopy.tex";
  589   my $tex_file_path = "$temp_dir_path/$tex_file_name";
  590 
  591   # write TeX
  592   my $open_result = open my $FH, ">", $tex_file_path;
  593   unless ($open_result) {
  594     $self->add_errors("Failed to open file '".CGI::code(CGI::escapeHTML($tex_file_path))
  595       ."' for writing: ".CGI::code(CGI::escapeHTML($!)));
  596     $self->delete_temp_dir($temp_dir_path);
  597     return;
  598   }
  599   $self->write_multiuser_tex($FH, $userIDsRef, $setIDsRef);
  600   close $FH;
  601 
  602   # if no problems got rendered successfully, we can't continue
  603   unless ($self->{at_least_one_problem_rendered_without_error}) {
  604     $self->add_errors("No problems rendered. Can't continue.");
  605     $self->delete_temp_dir($temp_dir_path);
  606     return;
  607   }
  608 
  609   # if no hardcopy.tex file was generated, fail now
  610   unless (-e "$temp_dir_path/hardcopy.tex") {
  611     $self->add_errors("'".CGI::code("hardcopy.tex")."' not written to temporary directory '"
  612       .CGI::code(CGI::escapeHTML($temp_dir_path))."'. Can't continue.");
  613     $self->delete_temp_dir($temp_dir_path);
  614     return;
  615   }
  616 
  617   # determine base name of final file
  618   my $final_file_user = @$userIDsRef > 1 ? "multiuser" : $userIDsRef->[0];
  619   my $final_file_set = @$setIDsRef > 1 ? "multiset" : $setIDsRef->[0];
  620   my $final_file_basename = "$courseID.$final_file_user.$final_file_set";
  621 
  622   # call format subroutine
  623   # $final_file_name is the name of final hardcopy file
  624   # @temp_files is a list of temporary files of interest used by the subroutine
  625   # (all are relative to $temp_dir_path)
  626   my $format_subr = $HC_FORMATS{$format}{subr};
  627   my ($final_file_name, @temp_files) = $self->$format_subr($temp_dir_path, $final_file_basename);
  628   my $final_file_path = "$temp_dir_path/$final_file_name";
  629 
  630   #warn "final_file_name=$final_file_name\n";
  631   #warn "temp_files=@temp_files\n";
  632 
  633   # calculate URLs for each temp file of interest
  634   # makeTempDirectory's interface forces us to reverse-engineer the name of the temp dir from the path
  635   my $temp_dir_parent_url = $ce->{courseURLs}{html_temp} . "/hardcopy";
  636   (my $temp_dir_url = $temp_dir_path) =~ s/^$temp_dir_parent_path/$temp_dir_parent_url/;
  637   my %temp_file_map;
  638   foreach my $temp_file_name (@temp_files) {
  639     $temp_file_map{$temp_file_name} = "$temp_dir_url/$temp_file_name";
  640   }
  641 
  642   my $final_file_url;
  643 
  644   # make sure final file exists
  645   unless (-e $final_file_path) {
  646     $self->add_errors("Final hardcopy file '".CGI::code(CGI::escapeHTML($final_file_path))
  647       ."' not found after calling '".CGI::code(CGI::escapeHTML($format_subr))."': "
  648       .CGI::code(CGI::escapeHTML($!)));
  649     return $final_file_url, %temp_file_map;
  650   }
  651 
  652   # try to move the hardcopy file out of the temp directory
  653   # set $final_file_url accordingly
  654   my $final_file_final_path = "$temp_dir_parent_path/$final_file_name";
  655   my $mv_cmd = "2>&1 " . $ce->{externalPrograms}{mv} . " " . shell_quote($final_file_path, $final_file_final_path);
  656   my $mv_out = readpipe $mv_cmd;
  657   if ($?) {
  658     $self->add_errors("Failed to move hardcopy file '".CGI::code(CGI::escapeHTML($final_file_name))
  659       ."' from '".CGI::code(CGI::escapeHTML($temp_dir_path))."' to '"
  660       .CGI::code(CGI::escapeHTML($temp_dir_parent_path))."':".CGI::br()
  661       .CGI::pre(CGI::escapeHTML($mv_out)));
  662     $final_file_url = "$temp_dir_url/$final_file_name";
  663   } else {
  664     $final_file_url = "$temp_dir_parent_url/$final_file_name";
  665   }
  666 
  667   # remove the temp directory if there are no errors
  668   unless ($self->get_errors or $PreserveTempFiles) {
  669     $self->delete_temp_dir($temp_dir_path);
  670   }
  671 
  672   warn "Preserved temporary files in directory '$temp_dir_path'.\n" if $PreserveTempFiles;
  673 
  674   return $final_file_url, %temp_file_map;
  675 }
  676 
  677 # helper function to remove temp dirs
  678 sub delete_temp_dir {
  679   my ($self, $temp_dir_path) = @_;
  680 
  681   my $rm_cmd = "2>&1 " . $self->r->ce->{externalPrograms}{rm} . " -rf " . shell_quote($temp_dir_path);
  682   my $rm_out = readpipe $rm_cmd;
  683   if ($?) {
  684     $self->add_errors("Failed to remove temporary directory '".CGI::code(CGI::escapeHTML($temp_dir_path))."':"
  685       .CGI::br().CGI::pre($rm_out));
  686     return 0;
  687   } else {
  688     return 1;
  689   }
  690 }
  691 
  692 # format subroutines
  693 #
  694 # assume that TeX source is located at $temp_dir_path/hardcopy.tex
  695 # the generated file will being with $final_file_basename
  696 # first element of return value is the name of the generated file (relative to $temp_dir_path)
  697 # rest of return value elements are names of temporary files that may be of interest in the
  698 #   case of an error, relative to $temp_dir_path. these are returned whether or not an error
  699 #   actually occured.
  700 
  701 sub generate_hardcopy_tex {
  702   my ($self, $temp_dir_path, $final_file_basename) = @_;
  703 
  704   my $final_file_name;
  705 
  706   # try to rename tex file
  707   my $src_name = "hardcopy.tex";
  708   my $dest_name = "$final_file_basename.tex";
  709   my $mv_cmd = "2>&1 " . $self->r->ce->{externalPrograms}{mv} . " " . shell_quote("$temp_dir_path/$src_name", "$temp_dir_path/$dest_name");
  710   my $mv_out = readpipe $mv_cmd;
  711   if ($?) {
  712     $self->add_errors("Failed to rename '".CGI::code(CGI::escapeHTML($src_name))."' to '"
  713       .CGI::code(CGI::escapeHTML($dest_name))."' in directory '"
  714       .CGI::code(CGI::escapeHTML($temp_dir_path))."':".CGI::br()
  715       .CGI::pre(CGI::escapeHTML($mv_out)));
  716     $final_file_name = $src_name;
  717   } else {
  718     $final_file_name = $dest_name;
  719   }
  720 
  721   return $final_file_name;
  722 }
  723 
  724 sub generate_hardcopy_pdf {
  725   my ($self, $temp_dir_path, $final_file_basename) = @_;
  726 
  727   # call pdflatex - we don't want to chdir in the mod_perl process, as
  728   # that might step on the feet of other things (esp. in Apache 2.0)
  729   my $pdflatex_cmd = "cd " . shell_quote($temp_dir_path) . " && "
  730     . $self->r->ce->{externalPrograms}{pdflatex}
  731     . " >pdflatex.stdout 2>pdflatex.stderr hardcopy";
  732   if (my $rawexit = system $pdflatex_cmd) {
  733     my $exit = $rawexit >> 8;
  734     my $signal = $rawexit & 127;
  735     my $core = $rawexit & 128;
  736     $self->add_errors("Failed to convert TeX to PDF with command '"
  737       .CGI::code(CGI::escapeHTML($pdflatex_cmd))."' (exit=$exit signal=$signal core=$core).");
  738 
  739     # read hardcopy.log and report first error
  740     my $hardcopy_log = "$temp_dir_path/hardcopy.log";
  741     if (-e $hardcopy_log) {
  742       if (open my $LOG, "<", $hardcopy_log) {
  743         my $line;
  744         while ($line = <$LOG>) {
  745           last if $line =~ /^!\s+/;
  746         }
  747         my $first_error = $line;
  748         while ($line = <$LOG>) {
  749           last if $line =~ /^!\s+/;
  750           $first_error .= $line;
  751         }
  752         close $LOG;
  753         if (defined $first_error) {
  754           $self->add_errors("First error in TeX log is:".CGI::br().
  755             CGI::pre(CGI::escapeHTML($first_error)));
  756         } else {
  757           $self->add_errors("No errors encoundered in TeX log.");
  758         }
  759       } else {
  760         $self->add_errors("Could not read TeX log: ".CGI::code(CGI::escapeHTML($!)));
  761       }
  762     } else {
  763       $self->add_errors("No TeX log was found.");
  764     }
  765   }
  766 
  767   my $final_file_name;
  768 
  769   # try rename the pdf file
  770   my $src_name = "hardcopy.pdf";
  771   my $dest_name = "$final_file_basename.pdf";
  772   my $mv_cmd = "2>&1 " . $self->r->ce->{externalPrograms}{mv} . " " . shell_quote("$temp_dir_path/$src_name", "$temp_dir_path/$dest_name");
  773   my $mv_out = readpipe $mv_cmd;
  774   if ($?) {
  775     $self->add_errors("Failed to rename '".CGI::code(CGI::escapeHTML($src_name))."' to '"
  776       .CGI::code(CGI::escapeHTML($dest_name))."' in directory '"
  777       .CGI::code(CGI::escapeHTML($temp_dir_path))."':".CGI::br()
  778       .CGI::pre(CGI::escapeHTML($mv_out)));
  779     $final_file_name = $src_name;
  780   } else {
  781     $final_file_name = $dest_name;
  782   }
  783 
  784   return $final_file_name, qw/hardcopy.tex hardcopy.log hardcopy.aux pdflatex.stdout pdflatex.stderr/;
  785 }
  786 
  787 ################################################################################
  788 # TeX aggregating subroutines
  789 ################################################################################
  790 
  791 sub write_multiuser_tex {
  792   my ($self, $FH, $userIDsRef, $setIDsRef) = @_;
  793   my $r = $self->r;
  794   my $ce = $r->ce;
  795 
  796   my @userIDs = @$userIDsRef;
  797   my @setIDs = @$setIDsRef;
  798 
  799   # get snippets
  800   my $preamble = $ce->{webworkFiles}->{hardcopySnippets}->{preamble};
  801   my $postamble = $ce->{webworkFiles}->{hardcopySnippets}->{postamble};
  802   my $divider = $ce->{webworkFiles}->{hardcopySnippets}->{userDivider};
  803 
  804   # write preamble
  805   $self->write_tex_file($FH, $preamble);
  806 
  807   # write section for each user
  808   while (defined (my $userID = shift @userIDs)) {
  809     $self->write_multiset_tex($FH, $userID, @setIDs);
  810     $self->write_tex_file($FH, $divider) if @userIDs; # divide users, but not after the last user
  811   }
  812 
  813   # write postamble
  814   $self->write_tex_file($FH, $postamble);
  815 }
  816 
  817 sub write_multiset_tex {
  818   my ($self, $FH, $targetUserID, @setIDs) = @_;
  819   my $r = $self->r;
  820   my $ce = $r->ce;
  821   my $db = $r->db;
  822 
  823   # get user record
  824   my $TargetUser = $db->getUser($targetUserID); # checked
  825   unless ($TargetUser) {
  826     $self->add_errors("Can't generate hardcopy for user '".CGI::code(CGI::escapeHTML($targetUserID))."' -- no such user exists.\n");
  827     return;
  828   }
  829 
  830   # get set divider
  831   my $divider = $ce->{webworkFiles}->{hardcopySnippets}->{setDivider};
  832 
  833   # write each set
  834   while (defined (my $setID = shift @setIDs)) {
  835     $self->write_set_tex($FH, $TargetUser, $setID);
  836     $self->write_tex_file($FH, $divider) if @setIDs; # divide sets, but not after the last set
  837   }
  838 }
  839 
  840 sub write_set_tex {
  841   my ($self, $FH, $TargetUser, $setID) = @_;
  842   my $r = $self->r;
  843   my $ce = $r->ce;
  844   my $db = $r->db;
  845   my $authz  = $r->authz;
  846   my $userID = $r->param("user");
  847 
  848   # we may already have the MergedSet from checking hide_work and
  849   #    hide_score in pre_header_initialize; check to see if that's true,
  850   #    and otherwise, get the set.
  851   my %mergedSets = %{$self->{mergedSets}};
  852   my $uid = $TargetUser->user_id;
  853   my $MergedSet;
  854   my $versioned = 0;
  855   if ( defined( $mergedSets{"$uid!$setID"} ) ) {
  856     $MergedSet = $mergedSets{"$uid!$setID"};
  857     $versioned = ($setID =~ /,v(\d+)$/) ? $1 : 0;
  858   } else {
  859     if ( $setID =~ /(.+),v(\d+)$/ ) {
  860       $setID = $1;
  861       $versioned = $2;
  862     }
  863     if ( $versioned ) {
  864       $MergedSet = $db->getMergedSetVersion($TargetUser->user_id, $setID, $versioned);
  865     } else {
  866       $MergedSet = $db->getMergedSet($TargetUser->user_id, $setID); # checked
  867     }
  868   }
  869   # save versioned info for use in write_problem_tex
  870   $self->{versioned} = $versioned;
  871 
  872   unless ($MergedSet) {
  873     $self->add_errors("Can't generate hardcopy for set ''".CGI::code(CGI::escapeHTML($setID))
  874       ."' for user '".CGI::code(CGI::escapeHTML($TargetUser->user_id))
  875       ."' -- set is not assigned to that user.");
  876     return;
  877   }
  878 
  879   # see if the *real* user is allowed to access this problem set
  880   if ($MergedSet->open_date > time and not $authz->hasPermissions($userID, "view_unopened_sets")) {
  881     $self->add_errors("Can't generate hardcopy for set '".CGI::code(CGI::escapeHTML($setID))
  882       ."' for user '".CGI::code(CGI::escapeHTML($TargetUser->user_id))
  883       ."' -- set is not yet open.");
  884     return;
  885   }
  886   if (not $MergedSet->published and not $authz->hasPermissions($userID, "view_unpublished_sets")) {
  887     $self->addbadmessage("Can't generate hardcopy for set '".CGI::code(CGI::escapeHTML($setID))
  888       ."' for user '".CGI::code(CGI::escapeHTML($TargetUser->user_id))
  889       ."' -- set has not been published.");
  890     return;
  891   }
  892 
  893   # get snippets
  894   my $header = $MergedSet->hardcopy_header
  895     ? $MergedSet->hardcopy_header
  896     : $ce->{webworkFiles}->{hardcopySnippets}->{setHeader};
  897   my $footer = $ce->{webworkFiles}->{hardcopySnippets}->{setFooter};
  898   my $divider = $ce->{webworkFiles}->{hardcopySnippets}->{problemDivider};
  899 
  900   # get list of problem IDs
  901   # DBFIXME use ORDER BY in database
  902   my @problemIDs = sort { $a <=> $b } $db->listUserProblems($MergedSet->user_id, $MergedSet->set_id);
  903 
  904   # for versioned sets (gateways), we might have problems in a random
  905   # order; reset the order of the problemIDs if this is the case
  906   if ( defined( $MergedSet->problem_randorder ) &&
  907        $MergedSet->problem_randorder ) {
  908     my @newOrder = ();
  909 
  910   # to set the same order each time we set the random seed to the psvn,
  911   # and to avoid messing with the system random number generator we use
  912   # our own PGrandom object
  913     my $pgrand = PGrandom->new();
  914     $pgrand->srand( $MergedSet->psvn );
  915     while ( @problemIDs ) {
  916       my $i = int($pgrand->rand(scalar(@problemIDs)));
  917       push( @newOrder, $problemIDs[$i] );
  918       splice(@problemIDs, $i, 1);
  919     }
  920     @problemIDs = @newOrder;
  921   }
  922 
  923 
  924   # write set header
  925   $self->write_problem_tex($FH, $TargetUser, $MergedSet, 0, $header); # 0 => pg file specified directly
  926 
  927   # write each problem
  928   # for versioned problem sets (gateway tests) we like to include
  929   #   problem numbers
  930   my $i = 1;
  931   while (my $problemID = shift @problemIDs) {
  932     $self->write_tex_file($FH, $divider);
  933     $self->{versioned} = $i if $versioned;
  934     $self->write_problem_tex($FH, $TargetUser, $MergedSet, $problemID);
  935     $i++;
  936   }
  937 
  938   # write footer
  939   $self->write_problem_tex($FH, $TargetUser, $MergedSet, 0, $footer); # 0 => pg file specified directly
  940 }
  941 
  942 sub write_problem_tex {
  943   my ($self, $FH, $TargetUser, $MergedSet, $problemID, $pgFile) = @_;
  944   my $r = $self->r;
  945   my $ce = $r->ce;
  946   my $db = $r->db;
  947   my $authz  = $r->authz;
  948   my $userID = $r->param("user");
  949   my $versioned = $self->{versioned};
  950   my %canShowScore = %{$self->{canShowScore}};
  951 
  952   my @errors;
  953 
  954   # get problem record
  955   my $MergedProblem;
  956   if ($problemID) {
  957     # a non-zero problem ID was given -- load that problem
  958           # we use $versioned to determine which merging routine to use
  959     if ( $versioned ) {
  960       $MergedProblem = $db->getMergedProblemVersion($MergedSet->user_id, $MergedSet->set_id, $MergedSet->version_id, $problemID);
  961 
  962     } else {
  963       $MergedProblem = $db->getMergedProblem($MergedSet->user_id, $MergedSet->set_id, $problemID); # checked
  964     }
  965 
  966     # handle nonexistent problem
  967     unless ($MergedProblem) {
  968       $self->add_errors("Can't generate hardcopy for problem '"
  969         .CGI::code(CGI::escapeHTML($problemID))."' in set '"
  970         .CGI::code(CGI::escapeHTML($MergedSet->set_id))
  971         ."' for user '".CGI::code(CGI::escapeHTML($MergedSet->user_id))
  972         ."' -- problem does not exist in that set or is not assigned to that user.");
  973       return;
  974     }
  975   } elsif ($pgFile) {
  976     # otherwise, we try an explicit PG file
  977     $MergedProblem = $db->newUserProblem(
  978       user_id => $MergedSet->user_id,
  979       set_id => $MergedSet->set_id,
  980       problem_id => 0,
  981       source_file => $pgFile,
  982     );
  983     die "newUserProblem failed -- WTF?" unless $MergedProblem; # this should never happen
  984   } else {
  985     # this shouldn't happen -- error out for real
  986     die "write_problem_tex needs either a non-zero \$problemID or a \$pgFile";
  987   }
  988 
  989   # figure out if we're allowed to get correct answers, hints, and solutions
  990   # (eventually, we'd like to be able to use the same code as Problem)
  991   my $versionName = $MergedSet->set_id .
  992     (( $versioned ) ?  ",v" . $MergedSet->version_id : '');
  993   my $showCorrectAnswers  = $r->param("showCorrectAnswers") || 0;
  994   my $showHints           = $r->param("showHints")          || 0;
  995   my $showSolutions       = $r->param("showSolutions")      || 0;
  996   unless( ( $authz->hasPermissions($userID, "show_correct_answers_before_answer_date") or
  997       ( time > $MergedSet->answer_date or
  998         ( $versioned &&
  999           $MergedProblem->num_correct +
 1000           $MergedProblem->num_incorrect >=
 1001           $MergedSet->attempts_per_version &&
 1002           $MergedSet->due_date == $MergedSet->answer_date ) ) ) &&
 1003     ( $canShowScore{$MergedSet->user_id . "!$versionName"} ) ) {
 1004     $showCorrectAnswers = 0;
 1005     $showSolutions      = 0;
 1006   }
 1007 
 1008   # FIXME -- there can be a problem if the $siteDefaults{timezone} is not defined?  Why is this?
 1009   # why does it only occur with hardcopy?
 1010 
 1011   # we need an additional translation option for versioned sets; also,
 1012   #   for versioned sets include old answers in the set if we're also
 1013   #   asking for the answers
 1014   my $transOpts =
 1015     { # translation options
 1016       displayMode     => "tex",
 1017       showHints       => $showHints          ? 1 : 0, # insure that this value is numeric
 1018       showSolutions   => $showSolutions      ? 1 : 0, # (or what? -sam)
 1019       processAnswers  => $showCorrectAnswers ? 1 : 0,
 1020       permissionLevel => $db->getPermissionLevel($userID)->permission,
 1021     };
 1022   my $formFields = { };
 1023   if ( $versioned && $MergedProblem->problem_id != 0 ) {
 1024     $transOpts->{QUIZ_PREFIX} = 'Q' . sprintf("%04d",$MergedProblem->problem_id()) . '_';
 1025     if ( $showCorrectAnswers ) {
 1026       my %oldAnswers = decodeAnswers($MergedProblem->last_answer);
 1027       $formFields->{$_} = $oldAnswers{$_} foreach (keys %oldAnswers);
 1028       print $FH "%% decoded old answers, saved. (keys = " . join(',', keys(%oldAnswers)) . "\n";
 1029     }
 1030   }
 1031 
 1032 # warn("problem ", $MergedProblem->problem_id, ": source = ", $MergedProblem->source_file, "\n");
 1033 
 1034   my $pg = WeBWorK::PG->new(
 1035     $ce,
 1036     $TargetUser,
 1037     scalar($r->param('key')), # avoid multiple-values problem
 1038     $MergedSet,
 1039     $MergedProblem,
 1040     $MergedSet->psvn,
 1041     $formFields, # no form fields!
 1042     $transOpts,
 1043   );
 1044 
 1045   # only bother to generate this info if there were warnings or errors
 1046   my $edit_url;
 1047   my $problem_name;
 1048   my $problem_desc;
 1049   if ($pg->{warnings} ne "" or $pg->{flags}->{error_flag}) {
 1050     my $edit_urlpath = $r->urlpath->newFromModule(
 1051       "WeBWorK::ContentGenerator::Instructor::PGProblemEditor",
 1052       courseID  => $r->urlpath->arg("courseID"),
 1053       setID     => $MergedProblem->set_id,
 1054       problemID => $MergedProblem->problem_id,
 1055     );
 1056 
 1057     if ($MergedProblem->problem_id == 0) {
 1058       # link for an fake problem (like a header file)
 1059       $edit_url = $self->systemLink($edit_urlpath,
 1060         params => {
 1061           sourceFilePath => $MergedProblem->source_file,
 1062           problemSeed    => $MergedProblem->problem_seed,
 1063         },
 1064       );
 1065     } else {
 1066       # link for a real problem
 1067       $edit_url = $self->systemLink($edit_urlpath);
 1068     }
 1069 
 1070     if ($MergedProblem->problem_id == 0) {
 1071       $problem_name = "snippet";
 1072       $problem_desc = $problem_name." '".$MergedProblem->source_file
 1073         ."' for set '".$MergedProblem->set_id."' and user '"
 1074         .$MergedProblem->user_id."'";
 1075     } else {
 1076       $problem_name = "problem";
 1077       $problem_desc = $problem_name." '".$MergedProblem->problem_id
 1078         ."' in set '".$MergedProblem->set_id."' for user '"
 1079         .$MergedProblem->user_id."'";
 1080     }
 1081   }
 1082 
 1083   # deal with PG warnings
 1084   if ($pg->{warnings} ne "") {
 1085     $self->add_errors(CGI::a({href=>$edit_url, target=>"WW_Editor"}, "[edit]")
 1086       ." Warnings encountered while processing $problem_desc. "
 1087       ."Error text:".CGI::br().CGI::pre(CGI::escapeHTML($pg->{warnings}))
 1088     );
 1089   }
 1090 
 1091   # deal with PG errors
 1092   if ($pg->{flags}->{error_flag}) {
 1093     $self->add_errors(CGI::a({href=>$edit_url, target=>"WW_Editor"}, "[edit]")
 1094       ." Errors encountered while processing $problem_desc. "
 1095       ."This $problem_name has been omitted from the hardcopy. "
 1096       ."Error text:".CGI::br().CGI::pre(CGI::escapeHTML($pg->{errors}))
 1097     );
 1098     return;
 1099   }
 1100 
 1101   # if we got here, there were no errors (because errors cause a return above)
 1102   $self->{at_least_one_problem_rendered_without_error} = 1;
 1103 
 1104   print $FH "{\\bf Problem $versioned.}\n"
 1105     if ( $versioned && $MergedProblem->problem_id != 0 );
 1106   print $FH $pg->{body_text};
 1107 
 1108   my @ans_entry_order = defined($pg->{flags}->{ANSWER_ENTRY_ORDER}) ? @{$pg->{flags}->{ANSWER_ENTRY_ORDER}} : ( );
 1109 
 1110   # write the list of student answers if we're working with a versioned
 1111   #   set and showing the correct answers
 1112   if ( $versioned && $showCorrectAnswers &&
 1113        $MergedProblem->problem_id != 0 && @ans_entry_order ) {
 1114     my $stuAnswers = "\\par{\\small{\\it Answer submitted:}\n" .
 1115       "\\vspace{-\\parskip}\\begin{itemize}\n";
 1116     for my $ansName ( @ans_entry_order ) {
 1117       my $stuAns = $pg->{answers}->{$ansName}->{original_student_ans};
 1118       my $recScore = $pg->{state}->{recorded_score};
 1119       my $corrMsg = '';
 1120       if ( $recScore == 1 ) {
 1121         $corrMsg = ' (correct)';
 1122       } elsif ( $recScore == 0 ) {
 1123         $corrMsg = ' (incorrect)';
 1124       } else {
 1125         $corrMsg = " (score $recScore)";
 1126       }
 1127       $stuAnswers .= "\\item\\begin{verbatim}$stuAns$corrMsg\\end{verbatim}\n";
 1128     }
 1129 
 1130     $stuAnswers .= "\\end{itemize}}\\par\n";
 1131     print $FH $stuAnswers;
 1132   }
 1133 
 1134   # write the list of correct answers is appropriate; ANSWER_ENTRY_ORDER
 1135   #   isn't defined for versioned sets?  this seems odd FIXME  GWCHANGE
 1136   if ($showCorrectAnswers && $MergedProblem->problem_id != 0 && @ans_entry_order) {
 1137     my $correctTeX = "\\par{\\small{\\it Correct Answers:}\n"
 1138       . "\\vspace{-\\parskip}\\begin{itemize}\n";
 1139 
 1140     foreach my $ansName (@ans_entry_order) {
 1141       my $correctAnswer = $pg->{answers}->{$ansName}->{correct_ans};
 1142       $correctTeX .= "\\item\\begin{verbatim}$correctAnswer\\end{verbatim}\n";
 1143       # FIXME: What about vectors (where TeX will complain about < and > outside of math mode)?
 1144     }
 1145 
 1146     $correctTeX .= "\\end{itemize}}\\par\n";
 1147 
 1148     print $FH $correctTeX;
 1149   }
 1150 }
 1151 
 1152 sub write_tex_file {
 1153   my ($self, $FH, $file) = @_;
 1154 
 1155   my $tex = eval { readFile($file) };
 1156   if ($@) {
 1157     $self->add_errors("Failed to include TeX file '".CGI::code(CGI::escapeHTML($file))."': "
 1158       .CGI::escapeHTML($@));
 1159   } else {
 1160     print $FH $tex;
 1161   }
 1162 }
 1163 
 1164 ################################################################################
 1165 # utilities
 1166 ################################################################################
 1167 
 1168 sub add_errors {
 1169   my ($self, @errors) = @_;
 1170   push @{$self->{hardcopy_errors}}, @errors;
 1171 }
 1172 
 1173 sub get_errors {
 1174   my ($self) = @_;
 1175   return $self->{hardcopy_errors} ? @{$self->{hardcopy_errors}} : ();
 1176 }
 1177 
 1178 sub get_errors_ref {
 1179   my ($self) = @_;
 1180   return $self->{hardcopy_errors};
 1181 }
 1182 
 1183 1;

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9