[system] / trunk / webwork-modperl / lib / WeBWorK / PG.pm Repository:
ViewVC logotype

View of /trunk/webwork-modperl/lib/WeBWorK/PG.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1144 - (download) (as text) (annotate)
Thu Jun 12 19:31:44 2003 UTC (10 years ago) by sh002i
File size: 19443 byte(s)
replaced call to tempdir with call to makeTempDirectory
-sam

    1 ################################################################################
    2 # WeBWorK mod_perl (c) 2000-2002 WeBWorK Project
    3 # $Id$
    4 ################################################################################
    5 
    6 package WeBWorK::PG;
    7 
    8 =head1 NAME
    9 
   10 WeBWorK::PG - Wrap the action of the PG Translator in an easy-to-use API.
   11 
   12 =cut
   13 
   14 use strict;
   15 use warnings;
   16 use File::Path qw(rmtree);
   17 use WeBWorK::PG::Translator;
   18 use WeBWorK::Utils qw(readFile formatDateTime writeTimingLogEntry makeTempDirectory);
   19 
   20 sub new($$$$$$$$) {
   21   my $invocant = shift;
   22   my $class = ref($invocant) || $invocant;
   23   my (
   24     $courseEnv,
   25     $user,
   26     $key,
   27     $set,
   28     $problem,
   29     $psvn,
   30     $formFields, # in CGI::Vars format
   31     $translationOptions, # hashref containing options for the
   32                          # translator, such as whether to show
   33              # hints and the display mode to use
   34   ) = @_;
   35 
   36   # write timing log entry
   37   writeTimingLogEntry($courseEnv, "WeBWorK::PG::new",
   38     "user=".$user->user_id.",problem=".$courseEnv->{courseName}."/".$set->set_id."/".$problem->problem_id.",mode=".$translationOptions->{displayMode},
   39     "begin");
   40 
   41   # install a local warn handler to collect warnings
   42   my $warnings = "";
   43   local $SIG{__WARN__} = sub { $warnings .= shift }
   44     if $courseEnv->{pg}->{options}->{catchWarnings};
   45 
   46   # create a Translator
   47   #warn "PG: creating a Translator\n";
   48   my $translator = WeBWorK::PG::Translator->new;
   49 
   50   # set the directory hash
   51   #warn "PG: setting the directory hash\n";
   52   $translator->rh_directories({
   53     courseScriptsDirectory => $courseEnv->{webworkDirs}->{macros},
   54     macroDirectory         => $courseEnv->{courseDirs}->{macros},
   55     templateDirectory      => $courseEnv->{courseDirs}->{templates},
   56     tempDirectory          => $courseEnv->{courseDirs}->{html_temp},
   57   });
   58 
   59   # evaluate modules and "extra packages"
   60   #warn "PG: evaluating modules and \"extra packages\"\n";
   61   my @modules = @{ $courseEnv->{pg}->{modules} };
   62   foreach my $module_packages_ref (@modules) {
   63     my ($module, @extra_packages) = @$module_packages_ref;
   64     # the first item is the main package
   65     $translator->evaluate_modules($module);
   66     # the remaining items are "extra" packages
   67     $translator->load_extra_packages(@extra_packages);
   68   }
   69 
   70   # set the environment (from defineProblemEnvir)
   71   #warn "PG: setting the environment (from defineProblemEnvir)\n";
   72   my $envir = defineProblemEnvir(
   73     $courseEnv,
   74     $user,
   75     $key,
   76     $set,
   77     $problem,
   78     $psvn,
   79     $formFields,
   80     $translationOptions,
   81   );
   82   $translator->environment($envir);
   83 
   84   # initialize the Translator
   85   #warn "PG: initializing the Translator\n";
   86   $translator->initialize();
   87 
   88   # load IO.pl, PG.pl, and dangerousMacros.pl using unrestricted_load
   89   # i'd like to change this at some point to have the same sort of interface to global.conf
   90   # that the module loading does -- have a list of macros to load unrestrictedly.
   91   #warn "PG: loading IO.pl, PG.pl, and dangerousMacros.pl using unrestricted_load\n";
   92   foreach (qw(IO.pl PG.pl dangerousMacros.pl)) {
   93     my $macroPath = $courseEnv->{webworkDirs}->{macros} . "/$_";
   94     my $err = $translator->unrestricted_load($macroPath);
   95     warn "Error while loading $macroPath: $err" if $err;
   96   }
   97 
   98   # set the opcode mask (using default values)
   99   #warn "PG: setting the opcode mask (using default values)\n";
  100   $translator->set_mask();
  101 
  102   # store the problem source
  103   #warn "PG: storing the problem source\n";
  104   my $sourceFile = ( defined($translationOptions->{override_problem_source}) ) ?
  105             $translationOptions->{override_problem_source} :
  106             $problem->source_file;
  107   $sourceFile = $courseEnv->{courseDirs}->{templates}."/".$sourceFile
  108     unless ($sourceFile =~ /^\//);
  109   eval { $translator->source_string(readFile($sourceFile)) };
  110   if ($@) {
  111     # well, we couldn't get the problem source, for some reason.
  112     return bless {
  113       translator => $translator,
  114       head_text  => "",
  115       body_text  => <<EOF,
  116 WeBWorK::Utils::readFile($sourceFile) says:
  117 $@
  118 EOF
  119       answers    => {},
  120       result     => {},
  121       state      => {},
  122       errors     => "Failed to read the problem source file.",
  123       warnings   => $warnings,
  124       flags      => {error_flag => 1},
  125     }, $class;
  126   }
  127 
  128   # install a safety filter (&safetyFilter)
  129   #warn "PG: installing a safety filter\n";
  130   $translator->rf_safety_filter(\&safetyFilter);
  131 
  132   # write timing log entry -- the translator is now all set up
  133   writeTimingLogEntry($courseEnv, "WeBWorK::PG::new",
  134     "initialized",
  135     "intermediate");
  136 
  137   # translate the PG source into text
  138   #warn "PG: translating the PG source into text\n";
  139   $translator->translate();
  140 
  141   # after we're done translating, we may have to clean up after the translator.
  142   # for example, 'images' mode uses a tempdir for dvipng's temp files. We have
  143   # to remove it.
  144   if ($translationOptions->{displayMode} eq 'images' && $envir->{dvipngTempDir}) {
  145     rmtree($envir->{dvipngTempDir}, 0, 0);
  146   }
  147 
  148   my ($result, $state); # we'll need these on the other side of the if block!
  149   if ($translationOptions->{processAnswers}) {
  150 
  151     # process student answers
  152     #warn "PG: processing student answers\n";
  153     $translator->process_answers($formFields);
  154 
  155     # retrieve the problem state and give it to the translator
  156     #warn "PG: retrieving the problem state and giving it to the translator\n";
  157     $translator->rh_problem_state({
  158       recorded_score =>       $problem->status,
  159       num_of_correct_ans =>   $problem->num_correct,
  160       num_of_incorrect_ans => $problem->num_incorrect,
  161     });
  162 
  163     # determine an entry order -- the ANSWER_ENTRY_ORDER flag is built by
  164     # the PG macro package (PG.pl)
  165     #warn "PG: determining an entry order\n";
  166     my @answerOrder =
  167       $translator->rh_flags->{ANSWER_ENTRY_ORDER}
  168         ? @{ $translator->rh_flags->{ANSWER_ENTRY_ORDER} }
  169         : keys %{ $translator->rh_evaluated_answers };
  170 
  171     # install a grader -- use the one specified in the problem,
  172     # or fall back on the default from the course environment.
  173     # (two magic strings are accepted, to avoid having to
  174     # reference code when it would be difficult.)
  175     #warn "PG: installing a grader\n";
  176     my $grader = $translator->rh_flags->{PROBLEM_GRADER_TO_USE}
  177       || $courseEnv->{pg}->{options}->{grader};
  178     $grader = $translator->rf_std_problem_grader
  179       if $grader eq "std_problem_grader";
  180     $grader = $translator->rf_avg_problem_grader
  181       if $grader eq "avg_problem_grader";
  182     die "Problem grader $grader is not a CODE reference."
  183       unless ref $grader eq "CODE";
  184     $translator->rf_problem_grader($grader);
  185 
  186     # grade the problem
  187     #warn "PG: grading the problem\n";
  188     ($result, $state) = $translator->grade_problem(
  189       answers_submitted  => $translationOptions->{processAnswers},
  190       ANSWER_ENTRY_ORDER => \@answerOrder,
  191     );
  192 
  193   }
  194 
  195   # write timing log entry
  196   writeTimingLogEntry($courseEnv, "WeBWorK::PG::new", "", "end");
  197 
  198   # return an object which contains the translator and the results of
  199   # the translation process. this is DIFFERENT from the "format expected
  200   # by Webwork.pm (and I believe processProblem8, but check.)"
  201   return bless {
  202     translator => $translator,
  203     head_text  => ${ $translator->r_header },
  204     body_text  => ${ $translator->r_text   },
  205     answers    => $translator->rh_evaluated_answers,
  206     result     => $result,
  207     state      => $state,
  208     errors     => $translator->errors,
  209     warnings   => $warnings,
  210     flags      => $translator->rh_flags,
  211   }, $class;
  212 }
  213 
  214 # -----
  215 
  216 sub defineProblemEnvir($$$$$$$) {
  217   my (
  218     $courseEnv,
  219     $user,
  220     $key,
  221     $set,
  222     $problem,
  223     $psvn,
  224     $formFields,
  225     $options,
  226   ) = @_;
  227 
  228   my %envir;
  229 
  230   # PG environment variables
  231   # from docs/pglanguage/pgreference/environmentvariables as of 06/25/2002
  232   # any changes are noted by "ADDED:" or "REMOVED:"
  233 
  234   # Vital state information
  235   # ADDED: displayHintsQ, displaySolutionsQ, refreshMath2img,
  236   #        texDisposition
  237 
  238   $envir{psvn}              = $psvn;
  239   $envir{psvnNumber}        = $envir{psvn};
  240   $envir{probNum}           = $problem->problem_id;
  241   $envir{questionNumber}    = $envir{probNum};
  242   $envir{fileName}          = $problem->source_file;
  243   $envir{probFileName}      = $envir{fileName};
  244   $envir{problemSeed}       = (defined($options->{override_seed}) ) ? $options->{override_seed} :$problem->problem_seed;
  245   $envir{displayMode}       = translateDisplayModeNames($options->{displayMode});
  246   $envir{languageMode}      = $envir{displayMode};
  247   $envir{outputMode}        = $envir{displayMode};
  248   $envir{displayHintsQ}     = $options->{showHints};
  249   $envir{displaySolutionsQ} = $options->{showSolutions};
  250   $envir{refreshMath2img}   = $options->{refreshMath2img};
  251   $envir{texDisposition}    = "pdf"; # in webwork-modperl, we use pdflatex
  252 
  253   # Problem Information
  254   # ADDED: courseName, formatedDueDate
  255 
  256   $envir{openDate}            = $set->open_date;
  257   $envir{formattedOpenDate}   = formatDateTime($envir{openDate});
  258   $envir{dueDate}             = $set->due_date;
  259   $envir{formattedDueDate}    = formatDateTime($envir{dueDate});
  260   $envir{formatedDueDate}     = $envir{formattedDueDate}; # typo in many header files
  261   $envir{answerDate}          = $set->answer_date;
  262   $envir{formattedAnswerDate} = formatDateTime($envir{answerDate});
  263   $envir{numOfAttempts}       = ($problem->num_correct || 0) + ($problem->num_incorrect || 0);
  264   $envir{problemValue}        = $problem->value;
  265   $envir{sessionKey}          = $key;
  266   $envir{courseName}          = $courseEnv->{courseName};
  267 
  268   # Student Information
  269   # ADDED: studentID
  270 
  271   $envir{sectionName}      = $user->section;
  272   $envir{sectionNumber}    = $envir{sectionName};
  273   $envir{recitationName}   = $user->recitation;
  274   $envir{recitationNumber} = $envir{recitationName};
  275   $envir{setNumber}        = $set->set_id;
  276   $envir{studentLogin}     = $user->user_id;
  277   $envir{studentName}      = $user->first_name . " " . $user->last_name;
  278   $envir{studentID}        = $user->student_id;
  279 
  280   # Answer Information
  281   # REMOVED: refSubmittedAnswers
  282 
  283   $envir{inputs_ref} = $formFields;
  284 
  285   # External Programs
  286   # ADDED: externalLaTeXPath, externalDvipngPath,
  287   #        externalGif2EpsPath, externalPng2EpsPath
  288 
  289   $envir{externalTTHPath}      = $courseEnv->{externalPrograms}->{tth};
  290   $envir{externalLaTeXPath}    = $courseEnv->{externalPrograms}->{latex};
  291   $envir{externalDvipngPath}   = $courseEnv->{externalPrograms}->{dvipng};
  292   $envir{externalGif2EpsPath}  = $courseEnv->{externalPrograms}->{gif2eps};
  293   $envir{externalPng2EpsPath}  = $courseEnv->{externalPrograms}->{png2eps};
  294   $envir{externalGif2PngPath}  = $courseEnv->{externalPrograms}->{gif2png};
  295 
  296   # Directories and URLs
  297   # REMOVED: courseName
  298   # ADDED: dvipngTempDir
  299 
  300   $envir{cgiDirectory}           = undef;
  301   $envir{cgiURL}                 = undef;
  302   $envir{classDirectory}         = undef;
  303   $envir{courseScriptsDirectory} = $courseEnv->{webworkDirs}->{macros}."/";
  304   $envir{htmlDirectory}          = $courseEnv->{courseDirs}->{html}."/";
  305   $envir{htmlURL}                = $courseEnv->{courseURLs}->{html}."/";
  306   $envir{macroDirectory}         = $courseEnv->{courseDirs}->{macros}."/";
  307   $envir{templateDirectory}      = $courseEnv->{courseDirs}->{templates}."/";
  308   $envir{tempDirectory}          = $courseEnv->{courseDirs}->{html_temp}."/";
  309   $envir{tempURL}                = $courseEnv->{courseURLs}->{html_temp}."/";
  310   $envir{scriptDirectory}        = undef;
  311   $envir{webworkDocsURL}         = $courseEnv->{webworkURLs}->{docs}."/";
  312   $envir{dvipngTempDir}          = $options->{displayMode} eq 'images'
  313     ? makeTempDirectory($envir{tempDirectory}, "webwork-dvipng")
  314     : undef;
  315 
  316   # Information for sending mail
  317 
  318   $envir{mailSmtpServer} = $courseEnv->{mail}->{smtpServer};
  319   $envir{mailSmtpSender} = $courseEnv->{mail}->{smtpSender};
  320   $envir{ALLOW_MAIL_TO}  = $courseEnv->{mail}->{allowedRecipients};
  321 
  322   # Default values for evaluating answers
  323 
  324   my $ansEvalDefaults = $courseEnv->{pg}->{ansEvalDefaults};
  325   $envir{$_} = $ansEvalDefaults->{$_} foreach (keys %$ansEvalDefaults);
  326 
  327   # Other things...
  328   $envir{QUIZ_PREFIX}              = $options->{QUIZ_PREFIX}; #used by quizzes
  329   $envir{PROBLEM_GRADER_TO_USE}    = $courseEnv->{pg}->{options}->{grader};
  330 
  331   $envir{PRINT_FILE_NAMES_FOR}     = $courseEnv->{pg}->{specialPGEnvironmentVars}->{PRINT_FILE_NAMES_FOR};
  332 
  333   # variables for interpreting capa problems.
  334   $envir{CAPA_Tools}               = $courseEnv->{pg}->{specialPGEnvironmentVars}->{CAPA_Tools};
  335   $envir{CAPA_MCTools}             = $courseEnv->{pg}->{specialPGEnvironmentVars}->{CAPA_MCTools};
  336   $envir{CAPA_Graphics_URL}        = $courseEnv->{pg}->{specialPGEnvironmentVars}->{CAPA_Graphics_URL};
  337   $envir{CAPA_GraphicsDirectory}   = $courseEnv->{pg}->{specialPGEnvironmentVars}->{CAPA_GraphicsDirectory};
  338 
  339   return \%envir;
  340 }
  341 
  342 sub translateDisplayModeNames($) {
  343   my $name = shift;
  344   return {
  345     tex           => "TeX",
  346     plainText     => "HTML",
  347     formattedText => "HTML_tth",
  348     images        => "HTML_img"
  349   }->{$name};
  350 }
  351 
  352 sub safetyFilter {
  353   my $answer = shift; # accepts one answer and checks it
  354   my $submittedAnswer = $answer;
  355   $answer = '' unless defined $answer;
  356   my ($errorno);
  357   $answer =~ tr/\000-\037/ /;
  358   # Return if answer field is empty
  359   unless ($answer =~ /\S/) {
  360     #$errorno = "<BR>No answer was submitted.";
  361     $errorno = 0;  ## don't report blank answer as error
  362     return ($answer,$errorno);
  363   }
  364   # replace ^ with **    (for exponentiation)
  365   # $answer =~ s/\^/**/g;
  366   # Return if forbidden characters are found
  367   unless ($answer =~ /^[a-zA-Z0-9_\-\+ \t\/@%\*\.\n^\[\]\(\)\,\|]+$/ )  {
  368     $answer =~ tr/a-zA-Z0-9_\-\+ \t\/@%\*\.\n^\(\)/#/c;
  369     $errorno = "<BR>There are forbidden characters in your answer: $submittedAnswer<BR>";
  370     return ($answer,$errorno);
  371   }
  372   $errorno = 0;
  373   return($answer, $errorno);
  374 }
  375 
  376 1;
  377 
  378 __END__
  379 
  380 =head1 SYNOPSIS
  381 
  382  $pg = WeBWorK::PG->new(
  383    $courseEnv,  # a WeBWorK::CourseEnvironment object
  384    $user,       # a WeBWorK::DB::Record::User object
  385    $sessionKey,
  386    $set,        # a WeBWorK::DB::Record::UserSet object
  387    $problem,    # a WeBWorK::DB::Record::UserProblem object
  388    $psvn,
  389    $formFields  # in &WeBWorK::Form::Vars format
  390    { # translation options
  391      displayMode     => "images", # (plainText|formattedText|images)
  392      showHints       => 1,        # (0|1)
  393      showSolutions   => 0,        # (0|1)
  394      refreshMath2img => 0,        # (0|1)
  395      processAnswers  => 1,        # (0|1)
  396    },
  397  );
  398 
  399  $translator = $pg->{translator}; # WeBWorK::PG::Translator
  400  $body       = $pg->{body_text};  # text string
  401  $header     = $pg->{head_text};  # text string
  402  $answerHash = $pg->{answers};    # WeBWorK::PG::AnswerHash
  403  $result     = $pg->{result};     # hash reference
  404  $state      = $pg->{state};      # hash reference
  405  $errors     = $pg->{errors};     # text string
  406  $warnings   = $pg->{warnings};   # text string
  407  $flags      = $pg->{flags};      # hash reference
  408 
  409 =head1 DESCRIPTION
  410 
  411 WeBWorK::PG encapsulates the PG translation process, making multiple calls to
  412 WeBWorK::PG::Translator. Much of the flexibility of the Translator is hidden,
  413 instead making choices that are appropriate for the webwork-modperl system.
  414 
  415 =head1 CONSTRUCTION
  416 
  417 =over
  418 
  419 =item new (ENVIRONMENT, USER, KEY, SET, PROBLEM, PSVN, FIELDS, OPTIONS)
  420 
  421 The C<new> method creates a translator, initializes it using the parameters
  422 specified, translates a PG file, and processes answers. It returns a reference
  423 to a blessed hash containing the results of the translation process.
  424 
  425 =back
  426 
  427 =head2 Parameters
  428 
  429 =over
  430 
  431 =item ENVIRONMENT
  432 
  433 a WeBWorK::CourseEnvironment object
  434 
  435 =item USER
  436 
  437 a WeBWorK::User object
  438 
  439 =item KEY
  440 
  441 the session key of the current session
  442 
  443 =item SET
  444 
  445 a WeBWorK::Set object
  446 
  447 =item PROBLEM
  448 
  449 a WeBWorK::DB::Record::UserProblem object. The contents of the source_file
  450 field can specify a PG file either by absolute path or path relative to the
  451 "templates" directory. I<The caller should remove taint from this value before
  452 passing!>
  453 
  454 =item PSVN
  455 
  456 the problem set version number
  457 
  458 =item FIELDS
  459 
  460 a reference to a hash (as returned by &WeBWorK::Form::Vars) containing form
  461 fields submitted by a problem processor. The translator will look for fields
  462 like "AnSwEr[0-9]" containing submitted student answers.
  463 
  464 =item OPTIONS
  465 
  466 a reference to a hash containing the following data:
  467 
  468 =over
  469 
  470 =item displayMode
  471 
  472 one of "plainText", "formattedText", or "images"
  473 
  474 =item showHints
  475 
  476 boolean, render hints
  477 
  478 =item showSolutions
  479 
  480 boolean, render solutions
  481 
  482 =item refreshMath2img
  483 
  484 boolean, force images created by math2img (in "images" mode) to be recreated,
  485 even if the PG source has not been updated.
  486 
  487 =item processAnswers
  488 
  489 boolean, call answer evaluators and graders
  490 
  491 =back
  492 
  493 =back
  494 
  495 =head2 RETURN VALUE
  496 
  497 The C<new> method returns a blessed hash reference containing the following
  498 fields. More information can be found in the documentation for
  499 WeBWorK::PG::Translator.
  500 
  501 =over
  502 
  503 =item translator
  504 
  505 The WeBWorK::PG::Translator object used to render the problem.
  506 
  507 =item head_text
  508 
  509 HTML code for the E<lt>headE<gt> block of an resulting web page. Used for
  510 JavaScript features.
  511 
  512 =item body_text
  513 
  514 HTML code for the E<lt>bodyE<gt> block of an resulting web page.
  515 
  516 =item answers
  517 
  518 An C<AnswerHash> object containing submitted answers, and results of answer
  519 evaluation.
  520 
  521 =item result
  522 
  523 A hash containing the results of grading the problem.
  524 
  525 =item state
  526 
  527 A hash containing the new problem state.
  528 
  529 =item errors
  530 
  531 A string containing any errors encountered while rendering the problem.
  532 
  533 =item warnings
  534 
  535 A string containing any warnings encountered while rendering the problem.
  536 
  537 =item flags
  538 
  539 A hash containing PG_flags (see the Translator docs).
  540 
  541 =back
  542 
  543 =head1 OPERATION
  544 
  545 WeBWorK::PG goes through the following operations when constructed:
  546 
  547 =over
  548 
  549 =item Get database information
  550 
  551 Retrieve information about the current user, set, and problem from the
  552 database.
  553 
  554 =item Create a translator
  555 
  556 Instantiate a WeBWorK::PG::Translator object.
  557 
  558 =item Set the directory hash
  559 
  560 Set the translator's directory hash (courseScripts, macros, templates, and temp
  561 directories) from the course environment.
  562 
  563 =item Evaluate PG modules
  564 
  565 Using the module list from the course environment (pg->modules), perform a
  566 "use"-like operation to evaluate modules at runtime.
  567 
  568 =item Set the problem environment
  569 
  570 Use data from the user, set, and problem, as well as the course environemnt and
  571 translation options, to set the problem environment.
  572 
  573 =item Initialize the translator
  574 
  575 Call &WeBWorK::PG::Translator::initialize. What more do you want?
  576 
  577 =item Load PG.pl and dangerousMacros.pl
  578 
  579 These macros must be loaded without opcode masking, so they are loaded here.
  580 
  581 =item Set the opcode mask
  582 
  583 Set the opcode mask to the default specified by WeBWorK::PG::Translator.
  584 
  585 =item Load the problem source
  586 
  587 Give the problem source to the translator.
  588 
  589 =item Install a safety filter
  590 
  591 The safety filter is used to preprocess student input before evaluation. The
  592 default safety filter, &WeBWorK::PG::safetyFilter, is used.
  593 
  594 =item Translate the problem source
  595 
  596 Call &WeBWorK::PG::Translator::translate to render the problem source into the
  597 format given by the display mode.
  598 
  599 =item Process student answers
  600 
  601 Use form field inputs to evaluate student answers.
  602 
  603 =item Load the problem state
  604 
  605 Use values from the database to initialize the problem state, so that the
  606 grader will have a point of reference.
  607 
  608 =item Determine an entry order
  609 
  610 Use the ANSWER_ENTRY_ORDER flag to determine the order of answers in the
  611 problem. This is important for problems with dependancies among parts.
  612 
  613 =item Install a grader
  614 
  615 Use the PROBLEM_GRADER_TO_USE flag, or a default from the course environment,
  616 to install a grader.
  617 
  618 =item Grade the problem
  619 
  620 Use the selected grader to grade the problem.
  621 
  622 =back
  623 
  624 =head1 AUTHOR
  625 
  626 Written by Sam Hathaway, sh002i (at) math.rochester.edu.
  627 
  628 =cut

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9