[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 818 - (download) (as text) (annotate)
Fri Apr 25 04:51:37 2003 UTC (10 years ago) by sh002i
File size: 15837 byte(s)
modev more content generators over to the new WWDBv2 API. fixed some
bugs.
-sam

    1 ################################################################################
    2 # WeBWorK mod_perl (c) 2000-2002 WeBWorK Project
    3 # $Id$
    4 ################################################################################
    5 
    6 package WeBWorK::ContentGenerator::Hardcopy;
    7 
    8 =head1 NAME
    9 
   10 WeBWorK::ContentGenerator::Hardcopy - generate a PDF version of one or more
   11 problem sets.
   12 
   13 =cut
   14 
   15 use strict;
   16 use warnings;
   17 use base qw(WeBWorK::ContentGenerator);
   18 use CGI qw();
   19 use File::Path qw(rmtree);
   20 use File::Temp qw(tempdir);
   21 use WeBWorK::Form;
   22 use WeBWorK::Utils qw(readFile);
   23 
   24 sub go {
   25   my ($self, $singleSet) = @_;
   26 
   27   my $r = $self->{r};
   28   my $ce = $self->{ce};
   29   my $db = $self->{db};
   30   my @sets = $r->param("hcSet");
   31   my @users = $r->param("hcUser");
   32 
   33   # add singleSet to the list of sets
   34   if (length $singleSet > 0) {
   35     $singleSet =~ s/^set//;
   36     unshift @sets, $singleSet unless grep { $_ eq $singleSet } @sets;
   37   }
   38 
   39   # default user is the effectiveUser
   40   unless (@users) {
   41     unshift @users, $r->param("effectiveUser");
   42   }
   43 
   44   $self->{user}            = $db->getUser($r->param("user"));
   45   $self->{permissionLevel} = $db->getPermissionLevel($r->param("user"))->permission();
   46   $self->{effectiveUser}   = $db->getUser($r->param("effectiveUser"));
   47   $self->{sets}  = \@sets;
   48   $self->{users} = \@users;
   49   $self->{errors}   = [];
   50   $self->{warnings} = [];
   51 
   52   # security checks
   53   my $multiSet = $self->{permissionLevel} > 0;
   54   my $multiUser = $self->{permissionLevel} > 0;
   55   if (@sets > 1 and not $multiSet) {
   56     $self->{generationError} = ["SIMPLE", "You are not permitted to generate hardcopy for multiple sets. Please select a single set and try again."];
   57   }
   58   if (@users > 1 and not $multiUser) {
   59     $self->{generationError} = ["SIMPLE", "You are not permitted to generate hardcopy for multiple users. Please select a single user and try again."];
   60   }
   61   if ($users[0] ne $self->{effectiveUser}->id and not $multiUser) {
   62     $self->{generationError} = ["SIMPLE", "You are not permitted to generate hardcopy for other users."];
   63   }
   64 
   65   unless ($self->{generationError}) {
   66     if ($r->param("generateHardcopy")) {
   67       my ($tempDir, $fileName) = eval { $self->generateHardcopy() };
   68       if ($@) {
   69         $self->{generationError} = $@;
   70       } else {
   71         my $filePath = "$tempDir/$fileName";
   72 
   73         $r->content_type("application/x-pdf");
   74         # as per RFC2183:
   75         $r->header_out("Content-Disposition", "attachment; filename=$fileName");
   76         $r->send_http_header();
   77 
   78         local *INPUTFILE;
   79         open INPUTFILE, "<", $filePath
   80           or die "Failed to read $filePath: $!";
   81         my $buf;
   82         while (read INPUTFILE, $buf, 16384) {
   83           print $buf;
   84         }
   85         close INPUTFILE;
   86 
   87         return;
   88       }
   89     }
   90   }
   91 
   92   $r->content_type("text/html");
   93   $r->send_http_header();
   94   $self->template($ce->{templates}->{system}, $singleSet);
   95 }
   96 
   97 # -----
   98 
   99 sub path {
  100   my ($self, undef, $args) = @_;
  101 
  102   my $ce = $self->{ce};
  103   my $root = $ce->{webworkURLs}->{root};
  104   my $courseName = $ce->{courseName};
  105   return $self->pathMacro($args,
  106     "Home" => "$root",
  107     $courseName => "$root/$courseName",
  108     "Hardcopy Generator" => "",
  109   );
  110 }
  111 
  112 sub title {
  113   return "Hardcopy Generator";
  114 }
  115 
  116 sub body {
  117   my $self = shift;
  118 
  119   if ($self->{generationError}) {
  120     if (ref $self->{generationError} eq "ARRAY") {
  121       my ($disposition, @rest) = @{$self->{generationError}};
  122       if ($disposition eq "PGFAIL") {
  123         print $self->multiErrorOutput(@{$self->{errors}});
  124         return "";
  125       } elsif ($disposition eq "FAIL") {
  126         print $self->errorOutput(@rest);
  127         return "";
  128       } elsif ($disposition eq "RETRY") {
  129         print $self->errorOutput(@rest);
  130       } else { # a "simple" error
  131         print CGI::p(CGI::font({-color=>"red"}, @rest));
  132       }
  133     } else {
  134       # not something we were expecting...
  135       die $self->{generationError};
  136     }
  137   }
  138   $self->displayForm();
  139 }
  140 
  141 sub multiErrorOutput($@) {
  142   my ($self, @errors) = @_;
  143 
  144   print CGI::h2("Software Errors");
  145   print CGI::p(<<EOF);
  146 WeBWorK has encountered one or more software errors while attempting to process these sets.
  147 It is likely that there are error(s) in the problem itself.
  148 If you are a student, contact your professor to have the error(s) corrected.
  149 If you are a professor, please consut the error output below for more informaiton.
  150 EOF
  151   foreach my $error (@errors) {
  152     print CGI::h3("Set: ", $error->{set}, ", Problem: ", $error->{problem});
  153     print CGI::h4("Error messages"), CGI::blockquote(CGI::pre($error->{message}));
  154     print CGI::h4("Error context"), CGI::blockquote(CGI::pre($error->{context}));
  155   }
  156 }
  157 
  158 # -----
  159 
  160 sub displayForm($) {
  161   my $self = shift;
  162   my $r = $self->{r};
  163   my $db = $self->{db};
  164 
  165   print CGI::start_p(), "Select the problem sets for which to generate hardcopy versions.";
  166   if ($self->{permissionLevel} > 0) {
  167     print "You may also select multiple users from the users list. You will receive hardcopy for each (set, user) pair.";
  168   }
  169   print CGI::end_p();
  170 
  171   print CGI::start_form(-method=>"POST", -action=>$r->uri);
  172   print $self->hidden_authen_fields();
  173   print CGI::h3("Options");
  174   print CGI::p("You may choose to show any of the following data. Correct answers and solutions are only available to privileged users or after the answer date of the problem set.");
  175   print CGI::p(
  176     CGI::checkbox(
  177       -name    => "showCorrectAnswers",
  178       -checked => $r->param("showCorrectAnswers") || 0,
  179       -label   => "Correct answers",
  180     ), CGI::br(),
  181     CGI::checkbox(
  182       -name    => "showHints",
  183       -checked => $r->param("showHints") || 0,
  184       -label   => "Hints",
  185     ), CGI::br(),
  186     CGI::checkbox(
  187       -name    => "showSolutions",
  188       -checked => $r->param("showSolutions") || 0,
  189       -label   => "Solutions",
  190     ),
  191   );
  192   print CGI::start_table({-width=>"100%"}), CGI::start_Tr({-valign=>"top"});
  193 
  194   my $multiSet = $self->{permissionLevel} > 0;
  195   my $multiUser = $self->{permissionLevel} > 0;
  196   my $preOpenSets = $self->{permissionLevel} > 0;
  197 
  198   # set selection menu
  199   {
  200     print CGI::start_td();
  201     print CGI::h3("Sets");
  202     print CGI::start_table();
  203     my @sets;
  204     push @sets, $db->getGlobalUserSet($self->{effectiveUser}->id, $_)
  205       foreach ($db->listUserSets($self->{effectiveUser}->id));
  206     @sets = sort { $a->set_id cmp $b->set_id } @sets;
  207     foreach my $set (@sets) {
  208       my $checked = grep { $_ eq $set->set_id } @{$self->{sets}};
  209       my $control;
  210       if (time < $set->open_date and not $preOpenSets) {
  211         $control = "";
  212       } else {
  213         if ($multiSet) {
  214           $control = CGI::checkbox(
  215             -name=>"hcSet",
  216             -value=>$set->set_id,
  217             -label=>"",
  218             -checked=>$checked
  219           );
  220         } else {
  221           $control = CGI::radio_group(
  222             -name=>"hcSet",
  223             -values=>[$set->set_id],
  224             -default=>($checked ? $set->set_id : "-"),
  225             -labels=>{$set->set_id => ""}
  226           );
  227         }
  228       }
  229       print CGI::Tr(CGI::td([
  230         $control,
  231         $set->set_id,
  232       ]));
  233     }
  234     print CGI::end_table();
  235     print CGI::end_td();
  236   }
  237 
  238   # user selection menu
  239   if ($multiUser) {
  240     print CGI::start_td();
  241     print CGI::h3("Users");
  242     print CGI::start_table();
  243     #print CGI::Tr(
  244     # CGI::td(CGI::checkbox(-name=>"hcAllUsers", -value=>"1", -label=>"")),
  245     # CGI::td({-colspan=>"2"}, "All Users"),
  246     #);
  247     #print CGI::Tr(CGI::td({-colspan=>"3"}, "&nbsp;"));
  248     my @users;
  249     push @users, $self->{cldb}->getUser($_)
  250       foreach ($self->{cldb}->listUsers());
  251     @users = sort { $a->last_name cmp $b->last_name } @users;
  252     foreach my $user (@users) {
  253       my $checked = grep { $_ eq $user->id } @{$self->{users}};
  254       print CGI::Tr(CGI::td([
  255         CGI::checkbox(-name=>"hcUser", -value=>$user->id, -label=>"", -checked=>$checked),
  256         $user->id,
  257         $user->last_name.", ".$user->first_name,
  258       ]));
  259     }
  260     print CGI::end_table();
  261     print CGI::end_td();
  262   }
  263 
  264   print CGI::end_Tr(), CGI::end_table();
  265   print CGI::p({-align=>"center"},
  266     CGI::submit(-name=>"generateHardcopy", -label=>"Generate Hardcopy"));
  267   print CGI::end_form();
  268 
  269   return "";
  270 }
  271 
  272 sub generateHardcopy($) {
  273   my $self = shift;
  274   my @sets = @{$self->{sets}};
  275   my @users = @{$self->{users}};
  276   my $multiSet = $self->{permissionLevel} > 0;
  277   my $multiUser = $self->{permissionLevel} > 0;
  278   # sanity checks
  279   unless (@sets) {
  280     die ["RETRY", "No sets were specified."];
  281   }
  282   unless (@users) {
  283     die ["RETRY", "No users were specified."];
  284   }
  285 
  286   # determine where hardcopy is going to go
  287   #my $tempDir = $self->{ce}->{courseDirs}->{html_temp} . "/hardcopy";
  288   my $tempDir = tempdir("webwork-hardcopy-XXXXXXXX", TMPDIR => 1);
  289 
  290   # make sure tempDir exists
  291   #unless (-e $tempDir) {
  292   # if (system "mkdir", "-p", $tempDir) {
  293   #   die ["FAIL", "Failed to mkdir $tempDir", $!];
  294   # }
  295   #}
  296 
  297   # determine name of PDF file
  298   my $courseName = $self->{ce}->{courseName};
  299   my $fileNameSet = (@sets > 1 ? "multiset" : $sets[0]);
  300   my $fileNameUser = (@users > 1 ? "multiuser" : $users[0]);
  301   my $fileName = "$courseName.$fileNameUser.$fileNameSet.pdf";
  302 
  303   # for each user ... generate TeX for each set
  304   my $tex;
  305   foreach my $user (@users) {
  306     $tex .= $self->getMultiSetTeX(@sets);
  307   }
  308 
  309   # deal with PG errors
  310   if (@{$self->{errors}}) {
  311     die ["PGFAIL"];
  312   }
  313 
  314   # "try" to generate pdf
  315   eval { $self->latex2pdf($tex, $tempDir, $fileName) };
  316   if ($@) {
  317     die ["FAIL", "Failed to generate PDF from tex", $@];
  318   }
  319 
  320   return $tempDir, $fileName;
  321 }
  322 
  323 # -----
  324 
  325 sub latex2pdf {
  326   # this is a little ad-hoc function which I will replace with a LaTeX
  327   # module at some point (or put it in Utils).
  328   my ($self, $tex, $fileBase, $fileName) = @_;
  329   my $finalFile = "$fileBase/$fileName";
  330   my $ce = $self->{ce};
  331 
  332   # create a temporary directory for tex to shit in
  333   my $wd = tempdir("webwork-hardcopy-XXXXXXXX", TMPDIR => 1);
  334   my $texFile = "$wd/hardcopy.tex";
  335   my $pdfFile = "$wd/hardcopy.pdf";
  336   my $logFile = "$wd/hardcopy.log";
  337 
  338   # write the tex file
  339   local *TEX;
  340   open TEX, ">", $texFile or die "Failed to open $texFile: $!\n";
  341   print TEX $tex;
  342   close TEX;
  343 
  344   # call pdflatex - we don't want to chdir in the mod_perl process, as
  345   # that might step on the feet of other things (esp. in Apache 2.0)
  346   my $pdflatex = $ce->{externalPrograms}->{pdflatex};
  347   my $pdflatexResult = system "cd $wd && $pdflatex $texFile";
  348   if ($pdflatexResult) {
  349     # something bad happened
  350     my $textErrorMessage = "Call to $pdflatex failed: $!\n";
  351     if (-e $logFile) {
  352       $textErrorMessage .= "pdflatex ran, but did not succeed. This suggests an error in the TeX\n";
  353       $textErrorMessage .= "version of one of the problems, or a problem with the pdflatex system.\n";
  354       my $logFileContents = eval { readFile($logFile) };
  355       if ($@) {
  356         $textErrorMessage .= "Additionally, the pdflatex log file could not be read, though it exists.\n";
  357       } else {
  358         $textErrorMessage .= "The contents of the TeX log are as follows:\n\n";
  359         $textErrorMessage .= "$logFileContents\n\n";
  360       }
  361     } else {
  362       $textErrorMessage .= "No log file was created, suggesting that pdflatex never ran. Check the WeBWorK\n";
  363       $textErrorMessage .= "configuration to ensure that the path to pdflatex is correct.\n";
  364     }
  365     die $textErrorMessage;
  366   }
  367 
  368   if (-e $pdfFile) {
  369     # move resulting PDF file to appropriate location
  370     system "/bin/mv", $pdfFile, $finalFile and die "Failed to mv: $!\n";
  371   }
  372 
  373   # remove temporary directory
  374   rmtree($wd, 0, 1);
  375 
  376   -e $finalFile or die "Failed to create $finalFile for no apparent reason.\n";
  377 }
  378 
  379 # -----
  380 
  381 sub texBlockComment(@) { return "\n".("%"x80)."\n%% ".join("", @_)."\n".("%"x80)."\n\n"; }
  382 
  383 sub getMultiSetTeX {
  384   my ($self, @sets) = @_;
  385   my $ce = $self->{ce};
  386   my $tex = "";
  387 
  388   # the document preamble
  389   $tex .= $self->texInclude($ce->{webworkFiles}->{hardcopySnippets}->{preamble});
  390 
  391   while (defined (my $setName = shift @sets)) {
  392     $tex .= $self->getSetTeX($setName);
  393     if (@sets) {
  394       # divide sets, but not after the last set
  395       $tex .= $self->texInclude($ce->{webworkFiles}->{hardcopySnippets}->{setDivider});
  396     }
  397   }
  398 
  399   # the document postamble
  400   $tex .= $self->texInclude($ce->{webworkFiles}->{hardcopySnippets}->{postamble});
  401 
  402   return $tex;
  403 }
  404 
  405 sub getSetTeX {
  406   my ($self, $setName) = @_;
  407   my $ce = $self->{ce};
  408   my $db = $self->{db};
  409   my $effectiveUserName = $self->{effectiveUser}->id;
  410   my @problemNumbers = sort { $a <=> $b }
  411     $db->listUserProblems($effectiveUserName, $setName);
  412 
  413   # get header and footer
  414   my $setHeader = $db->getGlobalUserSet($effectiveUserName, $setName)->set_header
  415     || $ce->{webworkFiles}->{hardcopySnippets}->{setHeader};
  416   # database doesn't support the following yet :(
  417   #my $setFooter = $wwdb->getGlobalUserSet($effectiveUserName, $setName)->set_footer
  418   # || $ce->{webworkFiles}->{hardcopySnippets}->{setFooter};
  419   # so we don't allow per-set customization, which is probably okay :)
  420   my $setFooter = $ce->{webworkFiles}->{hardcopySnippets}->{setFooter};
  421 
  422   my $tex = "";
  423 
  424   # render header
  425   $tex .= texBlockComment("BEGIN $setName : $setHeader");
  426   $tex .= $self->getProblemTeX($setName, 0, $setHeader);
  427 
  428   # render each problem
  429   while (my $problemNumber = shift @problemNumbers) {
  430     $tex .= texBlockComment("BEGIN $setName : $problemNumber");
  431     $tex .= $self->getProblemTeX($setName, $problemNumber);
  432     if (@problemNumbers) {
  433       # divide problems, but not after the last problem
  434       $tex .= $self->texInclude($ce->{webworkFiles}->{hardcopySnippets}->{problemDivider});
  435     }
  436   }
  437 
  438   # render footer
  439   $tex .= texBlockComment("BEGIN $setName : $setFooter");
  440   $tex .= $self->getProblemTeX($setName, 0, $setFooter);
  441 
  442   return $tex;
  443 }
  444 
  445 sub getProblemTeX {
  446   my ($self, $setName, $problemNumber, $pgFile) = @_;
  447   my $r = $self->{r};
  448   my $ce = $self->{ce};
  449   my $db = $self->{db};
  450 
  451   my $effectiveUser = $self->{effectiveUser};
  452   my $permissionLevel = $self->{permissionLevel};
  453   my $set  = $db->getGlobalUserSet($effectiveUser->id, $setName);
  454   my $psvn = $set->psvn();
  455 
  456   # decide what to do about problem number
  457   my $problem;
  458   if ($problemNumber) {
  459     $problem = $db->getGlobalUserProblem($effectiveUser->id, $setName, $problemNumber);
  460   } elsif ($pgFile) {
  461     $problem = WeBWorK::DB::Record::UserProblem->new(
  462       set_id => $set->set_id,
  463       problem_id => 0,
  464       login_id => $effectiveUser->id,
  465       source_file => $pgFile,
  466       # the rest of Problem's fields are not needed, i think
  467     );
  468   }
  469 
  470   # figure out if we're allowed to get solutions and call PG->new accordingly.
  471   my $showCorrectAnswers = $r->param("showCorrectAnswers") || 0;
  472   my $showHints          = $r->param("showHints") || 0;
  473   my $showSolutions      = $r->param("showSolutions") || 0;
  474   unless ($permissionLevel > 0 or time > $set->answer_date) {
  475     $showCorrectAnswers = 0;
  476     $showSolutions      = 0;
  477   }
  478 
  479   my $pg = WeBWorK::PG->new(
  480     $ce,
  481     $effectiveUser,
  482     $r->param('key'),
  483     $set,
  484     $problem,
  485     $psvn,
  486     {}, # no form fields!
  487     { # translation options
  488       displayMode     => "tex",
  489       showHints       => $showHints,
  490       showSolutions   => $showSolutions,
  491       processAnswers  => $showCorrectAnswers,
  492     },
  493   );
  494 
  495   if ($pg->{warnings} ne "") {
  496     push @{$self->{warnings}}, {
  497       set     => $setName,
  498       problem => $problemNumber,
  499       message => $pg->{warnings},
  500     };
  501   }
  502 
  503   if ($pg->{flags}->{error_flag}) {
  504     push @{$self->{errors}}, {
  505       set     => $setName,
  506       problem => $problemNumber,
  507       message => $pg->{errors},
  508       context => $pg->{body_text},
  509     };
  510     # if there was an error, body_text contains
  511     # the error context, not TeX code
  512     $pg->{body_text} = undef;
  513   } else {
  514     # append list of correct answers to body text
  515     if ($showCorrectAnswers && $problemNumber != 0) {
  516       my $correctTeX = "Correct Answers:\\par\\begin{itemize}\n";
  517       foreach my $ansName (@{$pg->{flags}->{ANSWER_ENTRY_ORDER}}) {
  518         my $correctAnswer = $pg->{answers}->{$ansName}->{correct_ans};
  519         $correctAnswer =~ s/\^/\\\^\{\}/g;
  520         $correctAnswer =~ s/\_/\\\_/g;
  521         $correctTeX .= "\\item $correctAnswer\n";
  522       }
  523       $correctTeX .= "\\end{itemize} \\par\n";
  524       $pg->{body_text} .= $correctTeX;
  525     }
  526   }
  527   warn "BODY TEXT=\n", $pg->{body_text}, "\n\n";
  528   return $pg->{body_text};
  529 }
  530 
  531 sub texInclude {
  532   my ($self, $texFile) = @_;
  533   my $tex = "";
  534 
  535   $tex .= texBlockComment("BEGIN: $texFile");
  536   eval {
  537     $tex .= readFile($texFile)
  538   };
  539   if ($@) {
  540     $tex .= texBlockComment($@);
  541   }
  542 
  543   return $tex;
  544 }
  545 
  546 1;

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9