--- trunk/webwork2/lib/WeBWorK/PG.pm 2002/07/25 21:45:29 440 +++ trunk/webwork2/lib/WeBWorK/PG.pm 2003/01/07 21:19:38 692 @@ -1,46 +1,58 @@ ################################################################################ -# WeBWorK mod_perl (c) 1995-2002 WeBWorK Team, Univeristy of Rochester +# WeBWorK mod_perl (c) 2000-2002 WeBWorK Project # $Id$ ################################################################################ package WeBWorK::PG; +=head1 NAME + +WeBWorK::PG - Wrap the action of the PG Translator in an easy-to-use API. + +=cut + use strict; use warnings; -use WeBWorK::Utils qw(readFile formatDateTime); +use File::Path qw(rmtree); +use File::Temp qw(tempdir); use WeBWorK::DB::Classlist; use WeBWorK::DB::WW; use WeBWorK::PG::Translator; +use WeBWorK::Problem; +use WeBWorK::Utils qw(readFile formatDateTime writeTimingLogEntry); sub new($$$$$$$$) { my $invocant = shift; my $class = ref($invocant) || $invocant; my ( $courseEnv, - $userName, + $user, $key, - $setName, - $problemNumber, + $set, + $problem, + $psvn, + $formFields, # in CGI::Vars format $translationOptions, # hashref containing options for the # translator, such as whether to show # hints and the display mode to use - $formFields, # in CGI::Vars format ) = @_; - # get database information - my $classlist = WeBWorK::DB::Classlist->new($courseEnv); - my $wwdb = WeBWorK::DB::WW->new($courseEnv); - my $user = $classlist->getUser($userName); - my $set = $wwdb->getSet($userName, $setName); - my $problem = $wwdb->getProblem($userName, $setName, $problemNumber); - my $psvn = $wwdb->getPSVN($userName, $setName); + # write timing log entry + writeTimingLogEntry($courseEnv, "WeBWorK::PG::new", + "user=".$user->id.",problem=".$courseEnv->{courseName}."/".$set->id."/".$problem->id.",mode=".$translationOptions->{displayMode}, + "begin"); + + # install a local warn handler to collect warnings + my $warnings = ""; + local $SIG{__WARN__} = sub { $warnings .= shift } + if $courseEnv->{pg}->{options}->{catchWarnings}; # create a Translator - warn "PG: creating a Translator\n"; + #warn "PG: creating a Translator\n"; my $translator = WeBWorK::PG::Translator->new; # set the directory hash - warn "PG: setting the directory hash\n"; + #warn "PG: setting the directory hash\n"; $translator->rh_directories({ courseScriptsDirectory => $courseEnv->{webworkDirs}->{macros}, macroDirectory => $courseEnv->{courseDirs}->{macros}, @@ -49,95 +61,150 @@ }); # evaluate modules and "extra packages" - warn "PG: evaluating modules and \"extra packages\"\n"; + #warn "PG: evaluating modules and \"extra packages\"\n"; my @modules = @{ $courseEnv->{pg}->{modules} }; - foreach my $module_packages (@modules) { - # the first item in $module_packages is the main package - $translator->evaluate_modules(shift @$module_packages); + foreach my $module_packages_ref (@modules) { + my ($module, @extra_packages) = @$module_packages_ref; + # the first item is the main package + $translator->evaluate_modules($module); # the remaining items are "extra" packages - $translator->load_extra_packages(@$module_packages); + $translator->load_extra_packages(@extra_packages); } # set the environment (from defineProblemEnvir) - warn "PG: setting the environment (from defineProblemEnvir)\n"; - $translator->environment(defineProblemEnvir( - $courseEnv, $user, $key, $set, $problem, $psvn, $formFields, $translationOptions)); + #warn "PG: setting the environment (from defineProblemEnvir)\n"; + my $envir = defineProblemEnvir( + $courseEnv, + $user, + $key, + $set, + $problem, + $psvn, + $formFields, + $translationOptions, + ); + $translator->environment($envir); # initialize the Translator - warn "PG: initializing the Translator\n"; + #warn "PG: initializing the Translator\n"; $translator->initialize(); - # load PG.pl and dangerousMacros.pl using unrestricted_load + # load IO.pl, PG.pl, and dangerousMacros.pl using unrestricted_load # i'd like to change this at some point to have the same sort of interface to global.conf # that the module loading does -- have a list of macros to load unrestrictedly. - warn "PG: loading PG.pl and dangerousMacros.pl using unrestricted_load\n"; - my $pg_pl = $courseEnv->{webworkDirs}->{macros} . "/PG.pl"; - my $dangerousMacros_pl = $courseEnv->{webworkDirs}->{macros} . "/dangerousMacros.pl"; - my $err = $translator->unrestricted_load($pg_pl); - warn "Error while loading $pg_pl: $err" if $err; - $err = $translator->unrestricted_load($dangerousMacros_pl); - warn "Error while loading $dangerousMacros_pl: $err" if $err; + #warn "PG: loading IO.pl, PG.pl, and dangerousMacros.pl using unrestricted_load\n"; + foreach (qw(IO.pl PG.pl dangerousMacros.pl)) { + my $macroPath = $courseEnv->{webworkDirs}->{macros} . "/$_"; + my $err = $translator->unrestricted_load($macroPath); + warn "Error while loading $macroPath: $err" if $err; + } + #my $pg_pl = $courseEnv->{webworkDirs}->{macros} . "/PG.pl"; + #my $dangerousMacros_pl = $courseEnv->{webworkDirs}->{macros} . "/dangerousMacros.pl"; + #my $io_pl = $courseEnv->{webworkDirs}->{macros} . "/IO.pl"; + #my $err = $translator->unrestricted_load($pg_pl); + #warn "Error while loading $pg_pl: $err" if $err; + #$err = $translator->unrestricted_load($dangerousMacros_pl); + #warn "Error while loading $dangerousMacros_pl: $err" if $err; + #$err = $translator->unrestricted_load($io_pl); + #warn "Error while loading $io_pl: $err" if $err; # set the opcode mask (using default values) - warn "PG: setting the opcode mask (using default values)\n"; + #warn "PG: setting the opcode mask (using default values)\n"; $translator->set_mask(); # store the problem source - warn "PG: storing the problem source\n"; - my $sourceFile = $courseEnv->{courseDirs}->{templates}."/".$problem->source_file; - $translator->source_string(readFile($sourceFile)); + #warn "PG: storing the problem source\n"; + my $sourceFile = $problem->source_file; + $sourceFile = $courseEnv->{courseDirs}->{templates}."/".$sourceFile + unless ($sourceFile =~ /^\//); + eval { $translator->source_string(readFile($sourceFile)) }; + if ($@) { + # well, we couldn't get the problem source, for some reason. + return bless { + translator => $translator, + head_text => "", + body_text => < {}, + result => {}, + state => {}, + errors => "Failed to read the problem source file.", + warnings => $warnings, + flags => {error_flag => 1}, + }, $class; + } # install a safety filter (&safetyFilter) - warn "PG: installing a safety filter\n"; + #warn "PG: installing a safety filter\n"; $translator->rf_safety_filter(\&safetyFilter); + # write timing log entry -- the translator is now all set up + writeTimingLogEntry($courseEnv, "WeBWorK::PG::new", + "initialized", + "intermediate"); + # translate the PG source into text - warn "PG: translating the PG source into text\n"; + #warn "PG: translating the PG source into text\n"; $translator->translate(); - # [in Problem.pm and processProblem8.pl, "install a grader" is here] + # after we're done translating, we may have to clean up after the translator. + # for example, 'images' mode uses a tempdir for dvipng's temp files. We have + # to remove it. + if ($translationOptions->{displayMode} eq 'images' && $envir->{dvipngTempDir}) { + rmtree($envir->{dvipngTempDir}, 0, 0); + } - # process student answers - warn "PG: processing student answers\n"; - $translator->process_answers($formFields); - - # retrieve the problem state and give it to the translator - warn "PG: retrieving the problem state and giving it to the translator\n"; - $translator->rh_problem_state({ - recorded_score => $problem->status, - num_of_correct_ans => $problem->num_correct, - num_of_incorrect_ans => $problem->num_incorrect, - }); + my ($result, $state); # we'll need these on the other side of the if block! + if ($translationOptions->{processAnswers}) { + + # process student answers + #warn "PG: processing student answers\n"; + $translator->process_answers($formFields); + + # retrieve the problem state and give it to the translator + #warn "PG: retrieving the problem state and giving it to the translator\n"; + $translator->rh_problem_state({ + recorded_score => $problem->status, + num_of_correct_ans => $problem->num_correct, + num_of_incorrect_ans => $problem->num_incorrect, + }); + + # determine an entry order -- the ANSWER_ENTRY_ORDER flag is built by + # the PG macro package (PG.pl) + #warn "PG: determining an entry order\n"; + my @answerOrder = + $translator->rh_flags->{ANSWER_ENTRY_ORDER} + ? @{ $translator->rh_flags->{ANSWER_ENTRY_ORDER} } + : keys %{ $translator->rh_evaluated_answers }; + + # install a grader -- use the one specified in the problem, + # or fall back on the default from the course environment. + # (two magic strings are accepted, to avoid having to + # reference code when it would be difficult.) + #warn "PG: installing a grader\n"; + my $grader = $translator->rh_flags->{PROBLEM_GRADER_TO_USE} + || $courseEnv->{pg}->{options}->{grader}; + $grader = $translator->rf_std_problem_grader + if $grader eq "std_problem_grader"; + $grader = $translator->rf_avg_problem_grader + if $grader eq "avg_problem_grader"; + die "Problem grader $grader is not a CODE reference." + unless ref $grader eq "CODE"; + $translator->rf_problem_grader($grader); + + # grade the problem + #warn "PG: grading the problem\n"; + ($result, $state) = $translator->grade_problem( + answers_submitted => $translationOptions->{processAnswers}, + ANSWER_ENTRY_ORDER => \@answerOrder, + ); + + } - # determine an entry order -- the ANSWER_ENTRY_ORDER flag is built by - # the PG macro package (PG.pl) - warn "PG: determining an entry order\n"; - my @answerOrder = - $translator->rh_flags->{ANSWER_ENTRY_ORDER} - ? @{ $translator->rh_flags->{ANSWER_ENTRY_ORDER} } - : keys %{ $translator->rh_evaluated_answers }; - - # install a grader -- use the one specified in the problem, - # or fall back on the default from the course environment. - # (two magic strings are accepted, to avoid having to - # reference code when it would be difficult.) - warn "PG: installing a grader\n"; - my $grader = $translator->rh_flags->{PROBLEM_GRADER_TO_USE} - || $courseEnv->{pg}->{options}->{grader}; - $grader = $translator->rf_std_problem_grader - if $grader eq "std_problem_grader"; - $grader = $translator->rf_avg_problem_grader - if $grader eq "avg_problem_grader"; - die "Problem grader $grader is not a CODE reference." - unless ref $grader eq "CODE"; - $translator->rf_problem_grader($grader); - - # grade the problem - warn "PG: grading the problem\n"; - my ($result, $state) = $translator->grade_problem( - answers_submitted => $translationOptions->{processAnswers}, - ANSWER_ENTRY_ORDER => \@answerOrder, - ); + # write timing log entry + writeTimingLogEntry($courseEnv, "WeBWorK::PG::new", "", "end"); # return an object which contains the translator and the results of # the translation process. this is DIFFERENT from the "format expected @@ -149,8 +216,8 @@ answers => $translator->rh_evaluated_answers, result => $result, state => $state, - errors => $translator->errors, # *** what is this doing? - warnings => undef, # *** gotta catch warnings eventually... + errors => $translator->errors, + warnings => $warnings, flags => $translator->rh_flags, }, $class; } @@ -176,7 +243,8 @@ # any changes are noted by "ADDED:" or "REMOVED:" # Vital state information - # ADDED: displayHintsQ, displaySolutionsQ + # ADDED: displayHintsQ, displaySolutionsQ, refreshMath2img, + # texDisposition $envir{psvn} = $psvn; $envir{psvnNumber} = $envir{psvn}; @@ -188,9 +256,10 @@ $envir{displayMode} = translateDisplayModeNames($options->{displayMode}); $envir{languageMode} = $envir{displayMode}; $envir{outputMode} = $envir{displayMode}; - $envir{displayHintsQ} = $options->{hints}; - $envir{displaySolutionsQ} = $options->{solutions}; + $envir{displayHintsQ} = $options->{showHints}; + $envir{displaySolutionsQ} = $options->{showSolutions}; $envir{refreshMath2img} = $options->{refreshMath2img}; + $envir{texDisposition} = "pdf"; # in webwork-modperl, we use pdflatex # Problem Information # ADDED: courseName @@ -201,7 +270,7 @@ $envir{formattedDueDate} = formatDateTime($envir{dueDate}); $envir{answerDate} = $set->answer_date; $envir{formattedAnswerDate} = formatDateTime($envir{answerDate}); - $envir{numOfAttempts} = $problem->num_correct + $problem->num_incorrect; + $envir{numOfAttempts} = ($problem->num_correct || 0) + ($problem->num_incorrect || 0); $envir{problemValue} = $problem->value; $envir{sessionKey} = $key; $envir{courseName} = $courseEnv->{courseName}; @@ -224,25 +293,40 @@ $envir{inputs_ref} = $formFields; # External Programs + # ADDED: externalLaTeXPath, externalDvipngPath, + # externalGif2EpsPath, externalPng2EpsPath $envir{externalTTHPath} = $courseEnv->{externalPrograms}->{tth}; - $envir{externalMath2imgPath} = $courseEnv->{externalPrograms}->{math2img}; + $envir{externalLaTeXPath} = $courseEnv->{externalPrograms}->{latex}; + $envir{externalDvipngPath} = $courseEnv->{externalPrograms}->{dvipng}; + $envir{externalGif2EpsPath} = $courseEnv->{externalPrograms}->{gif2eps}; + $envir{externalPng2EpsPath} = $courseEnv->{externalPrograms}->{png2eps}; + $envir{externalGif2PngPath} = $courseEnv->{externalPrograms}->{gif2png}; # Directories and URLs # REMOVED: courseName + # ADDED: dvipngTempDir $envir{cgiDirectory} = undef; $envir{cgiURL} = undef; $envir{classDirectory} = undef; $envir{courseScriptsDirectory} = $courseEnv->{webworkDirs}->{macros}."/"; $envir{htmlDirectory} = $courseEnv->{courseDirs}->{html}."/"; - $envir{htmlURL} = $courseEnv->{courseURLs}->{html}; + $envir{htmlURL} = $courseEnv->{courseURLs}->{html}."/"; $envir{macroDirectory} = $courseEnv->{courseDirs}->{macros}."/"; $envir{templateDirectory} = $courseEnv->{courseDirs}->{templates}."/"; $envir{tempDirectory} = $courseEnv->{courseDirs}->{html_temp}."/"; - $envir{tempURL} = $courseEnv->{courseURLs}->{html_temp}; + $envir{tempURL} = $courseEnv->{courseURLs}->{html_temp}."/"; $envir{scriptDirectory} = undef; - $envir{webworkDocsURL} = $courseEnv->{webworkURLs}->{docs}; + $envir{webworkDocsURL} = $courseEnv->{webworkURLs}->{docs}."/"; + $envir{dvipngTempDir} = $options->{displayMode} eq 'images' + ? tempdir("webwork-dvipng-XXXXXXXX", DIR => $envir{tempDirectory}) + : undef; + + # Information for sending mail + + $envir{mailSmtpServer} = $courseEnv->{mail}->{smtpServer}; + $envir{mailSmtpSender} = $courseEnv->{mail}->{smtpSender}; # Default values for evaluating answers @@ -259,6 +343,7 @@ sub translateDisplayModeNames($) { my $name = shift; return { + tex => "TeX", plainText => "HTML", formattedText => "HTML_tth", images => "HTML_img" @@ -279,7 +364,7 @@ } # replace ^ with ** (for exponentiation) # $answer =~ s/\^/**/g; - # Return if forbidden characters are found + # Return if forbidden characters are found unless ($answer =~ /^[a-zA-Z0-9_\-\+ \t\/@%\*\.\n^\(\)]+$/ ) { $answer =~ tr/a-zA-Z0-9_\-\+ \t\/@%\*\.\n^\(\)/#/c; $errorno = "
There are forbidden characters in your answer: $submittedAnswer
"; @@ -293,18 +378,16 @@ __END__ -=head1 NAME - -WeBWorK::PG - Wrap the action of the PG Translator in an easy-to-use API - =head1 SYNOPSIS $pg = WeBWorK::PG->new( - $courseEnv, # a WeBWorK::CourseEnvironment object - $userName, + $courseEnv, # a WeBWorK::CourseEnvironment object + $user, # a WeBWorK::User object $sessionKey, - $setName, - $problemNumber, + $set, # a WeBWorK::Set object + $problem, # a WeBWorK::Problem object + $psvn, + $formFields # in &WeBWorK::Form::Vars format { # translation options displayMode => "images", # (plainText|formattedText|images) showHints => 1, # (0|1) @@ -312,7 +395,6 @@ refreshMath2img => 0, # (0|1) processAnswers => 1, # (0|1) }, - $formFields # in WeBWorK::Form::Vars format ); $translator = $pg->{translator}; # WeBWorK::PG::Translator @@ -335,7 +417,7 @@ =over -=item new (ENVIRONMENT, USER, KEY, SET, PROBLEM, OPTIONS, FIELDS) +=item new (ENVIRONMENT, USER, KEY, SET, PROBLEM, PSVN, FIELDS, OPTIONS) The C method creates a translator, initializes it using the parameters specified, translates a PG file, and processes answers. It returns a reference @@ -353,7 +435,7 @@ =item USER -the name of the user for whom to render +a WeBWorK::User object =item KEY @@ -361,11 +443,23 @@ =item SET -the name of the problem set from which to get the problem +a WeBWorK::Set object =item PROBLEM -the number of the problem to render +a WeBWorK::Problem object. The contents of the source_file field can specify a +PG file either by absolute path or path relative to the "templates" directory. +I + +=item PSVN + +the problem set version number + +=item FIELDS + +a reference to a hash (as returned by &WeBWorK::Form::Vars) containing form +fields submitted by a problem processor. The translator will look for fields +like "AnSwEr[0-9]" containing submitted student answers. =item OPTIONS @@ -396,12 +490,6 @@ =back -=item FIELDS - -a reference to a hash (as returned by &WeBWorK::Form::Vars) containing form -fields submitted by a problem processor. The translator will look for fields -like "AnSwEr[0-9]" containing submitted student answers. - =back =head2 RETURN VALUE