################################################################################ # WeBWorK mod_perl (c) 2000-2002 WeBWorK Project # $Id$ ################################################################################ package WeBWorK::ContentGenerator::Hardcopy; =head1 NAME WeBWorK::ContentGenerator::Hardcopy - generate a PDF version of one or more problem sets. =cut use strict; use warnings; use base qw(WeBWorK::ContentGenerator); use CGI qw(); use File::Path qw(rmtree); use File::Temp qw(tempdir); use WeBWorK::DB::Classlist; use WeBWorK::DB::WW; use WeBWorK::Form; use WeBWorK::Utils qw(readFile); sub go { my ($self, $singleSet) = @_; my $r = $self->{r}; my $ce = $self->{courseEnvironment}; my @sets = $r->param("hcSet"); my @users = $r->param("hcUser"); # add singleSet to the list of sets if (length $singleSet > 0) { $singleSet =~ s/^set//; unshift @sets, $singleSet unless grep { $_ eq $singleSet } @sets; } # default user is the effectiveUser unless (@users) { unshift @users, $r->param("effectiveUser"); } $self->{cldb} = WeBWorK::DB::Classlist->new($ce); $self->{authdb} = WeBWorK::DB::Auth->new($ce); $self->{wwdb} = WeBWorK::DB::WW->new($ce); $self->{user} = $self->{cldb}->getUser($r->param("user")); $self->{permissionLevel} = $self->{authdb}->getPermissions($r->param("user")); $self->{effectiveUser} = $self->{cldb}->getUser($r->param("effectiveUser")); $self->{sets} = \@sets; $self->{users} = \@users; $self->{errors} = []; $self->{warnings} = []; # security checks - these have to be put somewhere my $multiSet = $self->{permissionLevel} > 0; my $multiUser = $self->{permissionLevel} > 0; if (@sets > 1 and not $multiSet) { $self->{generationError} = ["SIMPLE", "You are not permitted to generate hardcopy for multiple sets. Please select a single set and try again."]; } if (@users > 1 and not $multiUser) { $self->{generationError} = ["SIMPLE", "You are not permitted to generate hardcopy for multiple users. Please select a single user and try again."]; } if ($users[0] ne $self->{effectiveUser}->id and not $multiUser) { $self->{generationError} = ["SIMPLE", "You are not permitted to generate hardcopy for other users."]; } unless ($self->{generationError}) { if ($r->param("generateHardcopy")) { my ($tempDir, $fileName) = eval { $self->generateHardcopy() }; if ($@) { $self->{generationError} = $@; } else { my $filePath = "$tempDir/$fileName"; $r->content_type("application/x-pdf"); # as per RFC2183: $r->header_out("Content-Disposition", "attachment; filename=$fileName"); $r->send_http_header(); local *INPUTFILE; open INPUTFILE, "<", $filePath or die "Failed to read $filePath: $!"; my $buf; while (read INPUTFILE, $buf, 16384) { print $buf; } close INPUTFILE; return; } } } $r->content_type("text/html"); $r->send_http_header(); $self->template($ce->{templates}->{system}, $singleSet); } # ----- sub path { my ($self, undef, $args) = @_; my $ce = $self->{courseEnvironment}; my $root = $ce->{webworkURLs}->{root}; my $courseName = $ce->{courseName}; return $self->pathMacro($args, "Home" => "$root", $courseName => "$root/$courseName", "Hardcopy Generator" => "", ); } sub title { return "Hardcopy Generator"; } sub body { my $self = shift; if ($self->{generationError}) { if (ref $self->{generationError} eq "ARRAY") { my ($disposition, @rest) = @{$self->{generationError}}; if ($disposition eq "PGFAIL") { print $self->multiErrorOutput(@{$self->{errors}}); return ""; } elsif ($disposition eq "FAIL") { print $self->errorOutput(@rest); return ""; } elsif ($disposition eq "RETRY") { print $self->errorOutput(@rest); } else { # a "simple" error print CGI::p(CGI::font({-color=>"red"}, @rest)); } } else { # not something we were expecting... die $self->{generationError}; } } $self->displayForm(); } sub multiErrorOutput($@) { my ($self, @errors) = @_; print CGI::h2("Software Errors"); print CGI::p(<{set}, ", Problem: ", $error->{problem}); print CGI::h4("Error messages"), CGI::blockquote(CGI::pre($error->{message})); print CGI::h4("Error context"), CGI::blockquote(CGI::pre($error->{context})); } } # ----- sub displayForm($) { my $self = shift; my $r = $self->{r}; print CGI::start_p(), "Select the problem sets for which to generate hardcopy versions."; if ($self->{permissionLevel} > 0) { print "You may also select multiple users from the users list. You will receive hardcopy for each (set, user) pair."; } print CGI::end_p(); print CGI::start_form(-method=>"POST", -action=>$r->uri); print $self->hidden_authen_fields(); print CGI::start_table({-width=>"100%"}), CGI::start_Tr({-valign=>"top"}); my $multiSet = $self->{permissionLevel} > 0; my $multiUser = $self->{permissionLevel} > 0; # set selection menu { print CGI::start_td(); print CGI::h3("Sets"); print CGI::start_table(); my @sets; push @sets, $self->{wwdb}->getSet($self->{effectiveUser}->id, $_) foreach ($self->{wwdb}->getSets($self->{effectiveUser}->id)); @sets = sort { $a->id cmp $b->id } @sets; foreach my $set (@sets) { my $checked = grep { $_ eq $set->id } @{$self->{sets}}; my $control; if (time < $set->open_date) { $control = ""; } else { if ($multiSet) { $control = CGI::checkbox( -name=>"hcSet", -value=>$set->id, -label=>"", -checked=>$checked ); } else { $control = CGI::radio_group( -name=>"hcSet", -values=>[$set->id], -default=>($checked ? $set->id : "-"), -labels=>{$set->id => ""} ); } } print CGI::Tr(CGI::td([ $control, $set->id, ])); } print CGI::end_table(); print CGI::end_td(); } # user selection menu if ($multiUser) { print CGI::start_td(); print CGI::h3("Users"); print CGI::start_table(); #print CGI::Tr( # CGI::td(CGI::checkbox(-name=>"hcAllUsers", -value=>"1", -label=>"")), # CGI::td({-colspan=>"2"}, "All Users"), #); #print CGI::Tr(CGI::td({-colspan=>"3"}, " ")); my @users; push @users, $self->{cldb}->getUser($_) foreach ($self->{cldb}->getUsers()); @users = sort { $a->last_name cmp $b->last_name } @users; foreach my $user (@users) { my $checked = grep { $_ eq $user->id } @{$self->{users}}; print CGI::Tr(CGI::td([ CGI::checkbox(-name=>"hcUser", -value=>$user->id, -label=>"", -checked=>$checked), $user->id, $user->last_name.", ".$user->first_name, ])); } print CGI::end_table(); print CGI::end_td(); } print CGI::end_Tr(), CGI::end_table(); print CGI::p({-align=>"center"}, CGI::submit(-name=>"generateHardcopy", -label=>"Generate Hardcopy")); print CGI::end_form(); return ""; } sub generateHardcopy($) { my $self = shift; my @sets = @{$self->{sets}}; my @users = @{$self->{users}}; my $multiSet = $self->{permissionLevel} > 0; my $multiUser = $self->{permissionLevel} > 0; # sanity checks unless (@sets) { die ["RETRY", "No sets were specified."]; } unless (@users) { die ["RETRY", "No users were specified."]; } # determine where hardcopy is going to go #my $tempDir = $self->{courseEnvironment}->{courseDirs}->{html_temp} . "/hardcopy"; my $tempDir = tempdir("webwork-hardcopy-XXXXXXXX", TMPDIR => 1); # make sure tempDir exists #unless (-e $tempDir) { # if (system "mkdir", "-p", $tempDir) { # die ["FAIL", "Failed to mkdir $tempDir", $!]; # } #} # determine name of PDF file my $courseName = $self->{courseEnvironment}->{courseName}; my $fileNameSet = (@sets > 1 ? "multiset" : $sets[0]); my $fileNameUser = (@users > 1 ? "multiuser" : $users[0]); my $fileName = "$courseName.$fileNameUser.$fileNameSet.pdf"; # for each user ... generate TeX for each set my $tex; foreach my $user (@users) { $tex .= $self->getMultiSetTeX(@sets); } # deal with PG errors if (@{$self->{errors}}) { die ["PGFAIL"]; } # "try" to generate pdf eval { $self->latex2pdf($tex, $tempDir, $fileName) }; if ($@) { die ["FAIL", "Failed to generate PDF from tex", $@]; } return $tempDir, $fileName; } # ----- sub latex2pdf { # this is a little ad-hoc function which I will replace with a LaTeX # module at some point (or put it in Utils). my ($self, $tex, $fileBase, $fileName) = @_; my $finalFile = "$fileBase/$fileName"; my $ce = $self->{courseEnvironment}; # create a temporary directory for tex to shit in my $wd = tempdir("webwork-hardcopy-XXXXXXXX", TMPDIR => 1); my $texFile = "$wd/hardcopy.tex"; my $pdfFile = "$wd/hardcopy.pdf"; my $logFile = "$wd/hardcopy.log"; # write the tex file local *TEX; open TEX, ">", $texFile or die "Failed to open $texFile: $!\n"; print TEX $tex; close TEX; # call pdflatex - we don't want to chdir in the mod_perl process, as # that might step on the feet of other things (esp. in Apache 2.0) my $pdflatex = $ce->{externalPrograms}->{pdflatex}; system "cd $wd && $pdflatex $texFile" and die "Failed to call pdflatex: $!\n"; if (-e $pdfFile) { # move resulting PDF file to appropriate location system "/bin/mv", $pdfFile, $finalFile and die "Failed to mv: $!\n"; } # remove temporary directory rmtree($wd, 0, 1); -e $finalFile or die "Failed to create $finalFile for no apparent reason.\n"; } # ----- sub texBlockComment(@) { return "\n".("%"x80)."\n%% ".join("", @_)."\n".("%"x80)."\n\n"; } sub getMultiSetTeX { my ($self, @sets) = @_; my $ce = $self->{courseEnvironment}; my $tex = ""; # the document preamble $tex .= $self->texInclude($ce->{webworkFiles}->{hardcopySnippets}->{preamble}); while (defined (my $setName = shift @sets)) { $tex .= $self->getSetTeX($setName); if (@sets) { # divide sets, but not after the last set $tex .= $self->texInclude($ce->{webworkFiles}->{hardcopySnippets}->{setDivider}); } } # the document postamble $tex .= $self->texInclude($ce->{webworkFiles}->{hardcopySnippets}->{postamble}); return $tex; } sub getSetTeX { my ($self, $setName) = @_; my $ce = $self->{courseEnvironment}; my $wwdb = $self->{wwdb}; my $effectiveUserName = $self->{effectiveUser}->id; my @problemNumbers = sort { $a <=> $b } $wwdb->getProblems($effectiveUserName, $setName); # get header and footer my $setHeader = $wwdb->getSet($effectiveUserName, $setName)->set_header || $ce->{webworkFiles}->{hardcopySnippets}->{setHeader}; # database doesn't support the following yet :( #my $setFooter = $wwdb->getSet($effectiveUserName, $setName)->set_footer # || $ce->{webworkFiles}->{hardcopySnippets}->{setFooter}; # so we don't allow per-set customization, which is probably okay :) my $setFooter = $ce->{webworkFiles}->{hardcopySnippets}->{setFooter}; my $tex = ""; # render header $tex .= texBlockComment("BEGIN $setName : $setHeader"); $tex .= $self->getProblemTeX($setName, 0, $setHeader); # render each problem while (my $problemNumber = shift @problemNumbers) { $tex .= texBlockComment("BEGIN $setName : $problemNumber"); $tex .= $self->getProblemTeX($setName, $problemNumber); if (@problemNumbers) { # divide problems, but not after the last problem $tex .= $self->texInclude($ce->{webworkFiles}->{hardcopySnippets}->{problemDivider}); } } # render footer $tex .= texBlockComment("BEGIN $setName : $setFooter"); $tex .= $self->getProblemTeX($setName, 0, $setFooter); return $tex; } sub getProblemTeX { my ($self, $setName, $problemNumber, $pgFile) = @_; my $r = $self->{r}; my $ce = $self->{courseEnvironment}; my $wwdb = $self->{wwdb}; my $cldb = $self->{cldb}; my $effectiveUser = $self->{effectiveUser}; my $set = $wwdb->getSet($effectiveUser->id, $setName); my $psvn = $wwdb->getPSVN($effectiveUser->id, $setName); # decide what to do about problem number my $problem; if ($problemNumber) { $problem = $wwdb->getProblem($effectiveUser->id, $setName, $problemNumber); } elsif ($pgFile) { $problem = WeBWorK::Problem->new( id => 0, set_id => $set->id, login_id => $effectiveUser->id, source_file => $pgFile, # the rest of Problem's fields are not needed, i think ); } my $pg = WeBWorK::PG->new( $ce, $effectiveUser, $r->param('key'), $set, $problem, $psvn, {}, # no form fields! { # translation options displayMode => "tex", showHints => 0, showSolutions => 0, processAnswers => 0, }, ); if ($pg->{warnings} ne "") { push @{$self->{warnings}}, { set => $setName, problem => $problemNumber, message => $pg->{warnings}, }; } if ($pg->{flags}->{error_flag}) { push @{$self->{errors}}, { set => $setName, problem => $problemNumber, message => $pg->{errors}, context => $pg->{body_text}, }; # if there was an error, body_text contains # the error context, not TeX code $pg->{body_text} = undef; } return $pg->{body_text}; } sub texInclude { my ($self, $texFile) = @_; my $tex = ""; $tex .= texBlockComment("BEGIN: $texFile"); eval { $tex .= readFile($texFile) }; if ($@) { $tex .= texBlockComment($@); } return $tex; } 1; __END__ sub body { my $self = shift; STUFF: { my $courseName = $self->{courseEnvironment}->{courseName}; my $effectiveUserName = $self->{r}->param("effectiveUser"); my @sets = @{$self->{sets}}; unless (@sets) { print CGI::p("No problem sets were specified."); last STUFF; } # determine where hardcopy is going to go my $tempDir = $self->{courseEnvironment}->{courseDirs}->{html_temp} . "/hardcopy"; my $tempURL = $self->{courseEnvironment}->{courseURLs}->{html_temp} . "/hardcopy"; # make sure tempDir exists unless (-e $tempDir) { if (system "mkdir", "-p", $tempDir) { print CGI::p("An error occured while trying to generate your PDF hardcopy:"); print CGI::blockquote(CGI::pre("Failed to mkdir $tempDir: $!\n")); } } # determine name of PDF file my $fileName; if (@sets > 1) { # multiset output $fileName = "$courseName.$effectiveUserName.multiset.pdf" } elsif (@sets == 1) { # only one set my $setName = $sets[0]; $fileName = "$courseName.$effectiveUserName.$setName.pdf"; } else { $fileName = "$courseName.$effectiveUserName.pdf"; } # determine full URL my $fullURL = "$tempURL/$fileName"; # generate TeX from sets my $tex = $self->getMultiSetTeX(@sets); #print CGI::pre($tex); # check for PG errors (fatal) if (@{$self->{errors}}) { my @errors = @{$self->{errors}}; print CGI::h2("Software Errors"); print CGI::p(<{set}, ", Problem: ", $error->{problem}); print CGI::h4("Error messages"), CGI::blockquote(CGI::pre($error->{message})); print CGI::h4("Error context"), CGI::blockquote(CGI::pre($error->{context})); } last STUFF; } # "try" to generate hardcopy eval { $self->latex2pdf($tex, $tempDir, $fileName) }; if ($@) { print CGI::p("An error occured while trying to generate your PDF hardcopy:"); print CGI::blockquote(CGI::pre($@)); last STUFF; } else { print CGI::p({-align=>"center"}, CGI::big(CGI::a({-href=>$fullURL}, "Download PDF Hardcopy")) ); } # check for PG warnings (non-fatal) if (@{$self->{warnings}}) { my @warnings = @{$self->{warnings}}; print CGI::h2("Software Warnings"); print CGI::p(<{set}, ", Problem: ", $warning->{problem}); print CGI::h4("Warning messages"), CGI::blockquote(CGI::pre($warning->{message})); } } } # feedback form my $ce = $self->{courseEnvironment}; my $root = $ce->{webworkURLs}->{root}; my $courseName = $ce->{courseName}; my $feedbackURL = "$root/$courseName/feedback/"; print CGI::startform("POST", $feedbackURL), $self->hidden_authen_fields, CGI::hidden("module", __PACKAGE__), CGI::p({-align=>"right"}, CGI::submit(-name=>"feedbackForm", -label=>"Send Feedback") ), CGI::endform(); return ""; }