[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 6658 - (download) (as text) (annotate)
Thu Dec 30 17:08:43 2010 UTC (2 years, 4 months ago) by gage
File size: 43645 byte(s)
added comments -- cosmetic changes while studying best way
to impliment tikz image generator -- we'll probably 
model it on ImageGenerator rather than on Hardcopy.pm


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

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9