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

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9