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

Diff of /trunk/webwork-modperl/lib/WeBWorK.pm

Parent Directory Parent Directory | Revision Log Revision Log | View Patch Patch

Revision 1245 Revision 1755
1################################################################################ 1################################################################################
2# WeBWorK mod_perl (c) 2000-2002 WeBWorK Project 2# WeBWorK Online Homework Delivery System
3# $Id$ 3# Copyright © 2000-2003 The WeBWorK Project, http://openwebwork.sf.net/
4# $CVSHeader: webwork-modperl/lib/WeBWorK.pm,v 1.42 2004/01/23 16:49:09 sh002i Exp $
5#
6# This program is free software; you can redistribute it and/or modify it under
7# the terms of either: (a) the GNU General Public License as published by the
8# Free Software Foundation; either version 2, or (at your option) any later
9# version, or (b) the "Artistic License" which comes with this package.
10#
11# This program is distributed in the hope that it will be useful, but WITHOUT
12# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13# FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the
14# Artistic License for more details.
4################################################################################ 15################################################################################
5 16
6package WeBWorK; 17package WeBWorK;
7 18
8=head1 NAME 19=head1 NAME
9 20
10WeBWorK - Dispatch requests to the appropriate ContentGenerator. 21WeBWorK - Dispatch requests to the appropriate content generator.
11 22
23=head1 SYNOPSIS
24
25 my $r = Apache->request;
26 my $result = eval { WeBWorK::dispatch($r) };
27 die "something bad happened: $@" if $@;
28
29=head1 DESCRIPTION
30
31C<WeBWorK> is the dispatcher for the WeBWorK system. Given an Apache request
32object, it performs authentication and determines which subclass of
33C<WeBWorK::ContentGenerator> to call.
34
35=head1 REQUEST FORMAT
36
37 FIXME: write this part
38 summary: the URI controls
39
12=cut 40=cut
41
42BEGIN { $main::VERSION = "2.0"; }
43
44
45my $timingON = 1;
13 46
14use strict; 47use strict;
15use warnings; 48use warnings;
16use Apache::Constants qw(:common REDIRECT); 49use Apache::Constants qw(:common REDIRECT DONE);
17use Apache::Request; 50use Apache::Request;
18use WeBWorK::Authen; 51use WeBWorK::Authen;
19use WeBWorK::Authz; 52use WeBWorK::Authz;
20use WeBWorK::ContentGenerator::Feedback;
21use WeBWorK::ContentGenerator::GatewayQuiz;
22use WeBWorK::ContentGenerator::Hardcopy;
23use WeBWorK::ContentGenerator::Instructor::Assigner;
24use WeBWorK::ContentGenerator::Instructor::Index;
25use WeBWorK::ContentGenerator::Instructor::PGProblemEditor;
26use WeBWorK::ContentGenerator::Instructor::ProblemList;
27use WeBWorK::ContentGenerator::Instructor::ProblemSetEditor;
28use WeBWorK::ContentGenerator::Instructor::ProblemSetList;
29use WeBWorK::ContentGenerator::Instructor::UserList;
30use WeBWorK::ContentGenerator::Instructor::UserList;
31use WeBWorK::ContentGenerator::Login;
32use WeBWorK::ContentGenerator::Logout;
33use WeBWorK::ContentGenerator::Options;
34use WeBWorK::ContentGenerator::Problem;
35use WeBWorK::ContentGenerator::ProblemSet;
36use WeBWorK::ContentGenerator::ProblemSets;
37use WeBWorK::ContentGenerator::Test;
38use WeBWorK::CourseEnvironment; 53use WeBWorK::CourseEnvironment;
39use WeBWorK::DB; 54use WeBWorK::DB;
40use WeBWorK::Timing; 55use WeBWorK::Timing;
56use WeBWorK::Upload;
57use WeBWorK::Utils qw(runtime_use);
41 58
42#sub dispatch($) { 59=head1 THE C<&dispatch> FUNCTION
43# print STDERR "Executing &WeBWorK::dispatch\n"; 60
44# return DECLINED; 61The C<&dispatch> function takes an Apache request object (REQUEST) and returns
45#} 62an apache status code. Below is an overview of its operation:
46#1; 63
47#__END__ 64=over
65
66=cut
48 67
49sub dispatch($) { 68sub dispatch($) {
50 my ($apache) = @_; 69 my ($apache) = @_;
51 my $r = Apache::Request->new($apache); 70 my $r = Apache::Request->new($apache);
52 # have to deal with unpredictable GET or POST data, and sift 71 # have to deal with unpredictable GET or POST data, and sift
59 $path_info =~ s!/+!/!g; # strip multiple forward slashes 78 $path_info =~ s!/+!/!g; # strip multiple forward slashes
60 my $current_uri = $r->uri; 79 my $current_uri = $r->uri;
61 my $args = $r->args; 80 my $args = $r->args;
62 81
63 my ($urlRoot) = $current_uri =~ m/^(.*)$path_info/; 82 my ($urlRoot) = $current_uri =~ m/^(.*)$path_info/;
64 83
84=item Ensure that the URI ends with a "/"
85
86Parts of WeBWorK assume that the current URI of a request ends with a "/". If
87this is not the case, a redirection is issued to add the "/". This action will
88discard any POST data associated with the request, so it is essential that all
89POST requests include a "/" at the end of the URI.
90
91=cut
92
65 # If it's a valid WeBWorK URI, it ends in a /. This is assumed 93 # If it's a valid WeBWorK URI, it ends in a /. This is assumed
66 # alllll over the place. 94 # alllll over the place.
67 unless (substr($current_uri,-1) eq '/') { 95 unless (substr($current_uri,-1) eq '/') {
68 $r->header_out(Location => "$current_uri/" . ($args ? "?$args" : "")); 96 $r->header_out(Location => "$current_uri/" . ($args ? "?$args" : ""));
69 return REDIRECT; 97 return REDIRECT;
70 # *** any post data gets lost here -- fix that. 98 # *** any post data gets lost here -- fix that.
71 # (actually, it's not a problem, since all URLs generated 99 # (actually, it's not a problem, since all URLs generated
72 # from within the system have trailing slashes, and we don't 100 # from within the system have trailing slashes, and we don't
73 # need POST data from outside the system anyway!) 101 # need POST data from outside the system anyway!)
74 } 102 }
75 103
76 # Create the @components array, which contains the path specified in the URL 104 # Create the @components array, which contains the path specified in the URL
77 my($junk, @components) = split "/", $path_info; 105 my($junk, @components) = split "/", $path_info;
78 my $webwork_root = $r->dir_config('webwork_root'); # From a PerlSetVar in httpd.conf 106 my $webwork_root = $r->dir_config('webwork_root'); # From a PerlSetVar in httpd.conf
79 my $pg_root = $r->dir_config('pg_root'); # From a PerlSetVar in httpd.conf 107 my $pg_root = $r->dir_config('pg_root'); # From a PerlSetVar in httpd.conf
80 my $course = shift @components; 108 my $course = shift @components;
81 109
110=item Read the course environment
111
112C<WeBWorK::CourseEnvironment> is used to read the F<global.conf> configuration
113file. If a course name was given in the request's URI, it is passed to
114C<WeBWorK::CourseEnvironment>. In this case, the course-specific configuration
115file (usually F<course.conf>) is also read by C<WeBWorK::CourseEnvironment> at
116this point.
117
118See also L<WeBWorK::CourseEnvironment>.
119
120=cut
121
82 # Try to get the course environment. 122 # Try to get the course environment.
83 my $ce = eval {WeBWorK::CourseEnvironment->new($webwork_root, $urlRoot, $pg_root, $course);}; 123 my $ce = eval {WeBWorK::CourseEnvironment->new($webwork_root, $urlRoot, $pg_root, $course);};
84 if ($@) { # If there was an error getting the requested course 124 if ($@) { # If there was an error getting the requested course
85 die "Failed to read course environment for $course: $@"; 125 die "Failed to read course environment for $course: $@";
86 } 126 }
87 127
128=item If no course was given, go to the site home page
129
130If the URI did not include the name of a course, a redirection is issued to the
131site home page, given but the course environemnt variable
132C<$ce-E<gt>{webworkURLs}-E<gt>{home}>.
133
134=cut
135
88 # If no course was specified, redirect to the home URL 136 # If no course was specified, redirect to the home URL
89 unless (defined $course) { 137 unless (defined $course) {
90 $r->header_out(Location => $ce->{webworkURLs}->{home}); 138 $r->header_out(Location => $ce->{webworkURLs}->{home});
91 return REDIRECT; 139 return REDIRECT;
92 } 140 }
93 141
142=item If the given course does not exist, fail
143
144If the URI did include the name of a course, but the course directory was not
145found, an exception is thrown.
146
147=cut
148
94 # Freak out if the requested course doesn't exist. For now, this is just a 149 # Freak out if the requested course doesn't exist. For now, this is just a
95 # check to see if the course directory exists. 150 # check to see if the course directory exists.
96 my $courseDir = $ce->{webworkDirs}->{courses} . "/$course"; 151 my $courseDir = $ce->{webworkDirs}->{courses} . "/$course";
97 unless (-e $courseDir) { 152 unless (-e $courseDir) {
98 die "Course directory for $course ($courseDir) not found. Perhaps the course does not exist?"; 153 die "Course directory for $course ($courseDir) not found. Perhaps the course does not exist?";
99 } 154 }
100 155
156=item Initialize the database system
157
158A C<WeBWorK::DB> object is created from the current course environment.
159
160See also L<WeBWorK::DB>.
161
162=cut
163
101 # Bring up a connection to the database (for Authen/Authz, and eventually 164 # Bring up a connection to the database (for Authen/Authz, and eventually
102 # to be passed to content generators, when we clean this file up). 165 # to be passed to content generators, when we clean this file up).
103 my $db = WeBWorK::DB->new($ce); 166 my $db = WeBWorK::DB->new($ce->{dbLayout});
104 167
168=item Capture any uploads
169
170Before checking authentication, we store any uploads sent by the client
171and replace them with parameters referencing the stored uploads.
172
173=cut
174
175 my @uploads = $r->upload;
176 foreach my $u (@uploads) {
177 # make sure it's a "real" upload
178 next unless $u->filename;
179
180 # store the upload
181 my $upload = WeBWorK::Upload->store($u,
182 dir => $ce->{webworkDirs}->{uploadCache}
183 );
184
185 # store the upload ID and hash in the file upload field
186 my $id = $upload->id;
187 my $hash = $upload->hash;
188 $r->param($u->name => "$id $hash");
189 }
190
191=item Check authentication
192
193Use C<WeBWorK::Authen> to verify that the remote user has authenticated.
194
195See also L<WeBWorK::Authen>.
196
197=cut
198
105 ### Begin dispatching ### 199 ### Begin dispatching ###
106 200
107 #my $dispatchTimer = WeBWorK::Timing->new(__PACKAGE__."::dispatch"); 201 my $contentGenerator = "";
108 #$dispatchTimer->start; 202 my @arguments = ();
109 203
110 my $result;
111 # WeBWorK::Authen::verify erases the passwd field and sets the key field 204 # WeBWorK::Authen::verify erases the passwd field and sets the key field
112 # if login is successful. 205 # if login is successful.
113 if (!WeBWorK::Authen->new($r, $ce, $db)->verify) { 206 if (!WeBWorK::Authen->new($r, $ce, $db)->verify) {
114 $result = WeBWorK::ContentGenerator::Login->new($r, $ce, $db)->go; 207 $contentGenerator = "WeBWorK::ContentGenerator::Login";
208 @arguments = ();
209 }
115 } else { 210 else {
211
212=item Determine if the user is allowed to set C<effectiveUser>
213
214Use C<WeBWorK::Authz> to determine if the user is allowed to set
215C<effectiveUser>. If so, set it to the requested value (or set it to the real
216user name if no value is supplied). If not, set it to the real user name.
217
218See also L<WeBWorK::Authz>.
219
220=cut
221
116 # After we are authenticated, there are some things that need to be 222 # After we are authenticated, there are some things that need to be
117 # sorted out, Authorization-wize, before we start dispatching to individual 223 # sorted out, Authorization-wize, before we start dispatching to individual
118 # content generators. 224 # content generators.
119 my $user = $r->param("user"); 225 my $user = $r->param("user");
120 my $effectiveUser = $r->param("effectiveUser") || $user; 226 my $effectiveUser = $r->param("effectiveUser") || $user;
227 my $authz = WeBWorK::Authz->new($r, $ce, $db);
121 my $su_authorized = WeBWorK::Authz->new($r, $ce, $db)->hasPermissions($user, "become_student", $effectiveUser); 228 my $su_authorized = $authz->hasPermissions($user, "become_student", $effectiveUser);
122 $effectiveUser = $user unless $su_authorized; 229 $effectiveUser = $user unless $su_authorized;
123 $r->param("effectiveUser", $effectiveUser); 230 $r->param("effectiveUser", $effectiveUser);
124 231
232=item Determine the appropriate subclass of C<WeBWorK::ContentGenerator> to call based on the URI.
233
234The dispatcher implements a virtual heirarchy that looks like this:
235
236 $courseID ($courseID) - list of sets
237 hardcopy (Hardcopy Generator) - generate hardcopy for user/set pairs
238 options (User Options) - change email address and password
239 feedback (Feedback) - send feedback to professor via email
240 logout (Logout) - expire session and erase authentication tokens
241 test (Test) - display request information
242 quiz_mode (Quiz) - "quiz" containing all problems from a set
243 instructor (Instructor Tools) - main menu for instructor tools
244 add_users (Add Users) - to be removed
245 scoring (Scoring Tools) - generate scoring files for problem sets
246 scoringDownload - send a scoring file to the client
247 scoring_totals - ???
248 users (Users) - view/edit users
249 $userID ($userID) - user detail for given user
250 sets (Assigned Sets) - view/edit sets assigned to given user
251 sets (Sets) - list of sets, add new sets, delete existing sets
252 $setID - view/edit the given set
253 problems (Problems) - view/edit problems in the given set
254 $problemID - this is where the pg problem editor SHOULD be
255 users (Users Assigned) - view/edit users to whom the given set is assigned
256 pgProblemEditor (Problem Source) - edit the source of a problem
257 send_mail (Mail Merge) - send mail to users in course
258 show_answers (Answers Submitted) - show submitted answers
259 stats (Statistics) - show statistics
260 files (File Transfer) - transfer files to/from the client
261 $setID ($setID) - list of problems in the given set
262 $problemID ($problemID) - interactive display of problem
263
264=cut
265
125 my $arg = shift @components; 266 my $arg = shift @components;
126 if (!defined $arg) { # We want the list of problem sets 267 if (not defined $arg) { # We want the list of problem sets
127 $result = WeBWorK::ContentGenerator::ProblemSets->new($r, $ce, $db)->go; 268 $contentGenerator = "WeBWorK::ContentGenerator::ProblemSets";
269 @arguments = ();
270 }
128 } elsif ($arg eq "hardcopy") { 271 elsif ($arg eq "hardcopy") {
272 my $setID = shift @components;
273 $contentGenerator = "WeBWorK::ContentGenerator::Hardcopy";
274 @arguments = ($setID);
275 }
276 elsif ($arg eq "options") {
277 $contentGenerator = "WeBWorK::ContentGenerator::Options";
278 @arguments = ();
279 }
280 elsif ($arg eq "feedback") {
281 $contentGenerator = "WeBWorK::ContentGenerator::Feedback";
282 @arguments = ();
283 }
284 elsif ($arg eq "logout") {
285 $contentGenerator = "WeBWorK::ContentGenerator::Logout";
286 @arguments = ();
287 }
288 elsif ($arg eq "test") {
289 $contentGenerator = "WeBWorK::ContentGenerator::Test";
290 @arguments = ();
291 }
292 elsif ($arg eq "quiz_mode" ) {
293 $contentGenerator = "WeBWorK::ContentGenerator::GatewayQuiz";
294 @arguments = @components;
295 }
296 elsif ($arg eq "equation" ) {
297 $contentGenerator = "WeBWorK::ContentGenerator::EquationDisplay";
298 @arguments = @components;
299 }
300 elsif ($arg eq "instructor") {
301 my $instructorArgument = shift @components;
129 302
130 my $hardcopyArgument = shift @components;
131 #$WeBWorK::timer1 = WeBWorK::Timing->new("hardcopy: $hardcopyArgument");
132 #$WeBWorK::timer1->start;
133 $hardcopyArgument = "" unless defined $hardcopyArgument;
134 my $result = WeBWorK::ContentGenerator::Hardcopy->new($r, $ce, $db)->go($hardcopyArgument);
135 #$WeBWorK::timer1 ->stop;
136 #$WeBWorK::timer1 ->save;
137 return $result;
138 } elsif ($arg eq "instructor") {
139 my $instructorArgument = shift @components;
140 if (!defined $instructorArgument) { 303 if (not defined $instructorArgument) {
141 $result = WeBWorK::ContentGenerator::Instructor::Index->new($r, $ce, $db)->go; 304 $contentGenerator = "WeBWorK::ContentGenerator::Instructor::Index";
305 @arguments = ();
306 }
307 elsif ($instructorArgument eq "add_users") {
308 $contentGenerator = "WeBWorK::ContentGenerator::Instructor::AddUsers";
309 @arguments = ();
310 }
311 elsif ($instructorArgument eq "assigner") {
312 $contentGenerator = "WeBWorK::ContentGenerator::Instructor::Assigner";
313 @arguments = ();
314 }
315 elsif ($instructorArgument eq "scoring") {
316 $contentGenerator = "WeBWorK::ContentGenerator::Instructor::Scoring";
317 @arguments = ();
318 }
319# elsif ($instructorArgument eq "scoring_totals") {
320# $contentGenerator = "WeBWorK::ContentGenerator::Instructor::ScoringTotals";
321# @arguments = ();
322# }
323 elsif ($instructorArgument eq "scoringDownload") {
324 $contentGenerator = "WeBWorK::ContentGenerator::Instructor::ScoringDownload";
325 @arguments = ();
326 }
142 } elsif ($instructorArgument eq "users") { 327 elsif ($instructorArgument eq "users") {
143 $result = WeBWorK::ContentGenerator::Instructor::UserList->new($r, $ce, $db)->go; 328 my $userID = shift @components;
329
330 if (defined $userID) {
331 my $userArg = shift @components;
332 if (defined $userArg) {
333 if ($userArg eq "sets") {
334 $contentGenerator = "WeBWorK::ContentGenerator::Instructor::SetsAssignedToUser";
335 @arguments = ($userID);
336 }
337 }
338 else {
339 $contentGenerator = "WeBWorK::ContentGenerator::Instructor::UserDetail";
340 @arguments = ($userID);
341 }
342 }
343 else {
344 $contentGenerator = "WeBWorK::ContentGenerator::Instructor::UserList";
345 @arguments = ();
346 }
347 }
144 } elsif ($instructorArgument eq "sets") { 348 elsif ($instructorArgument eq "sets") {
145 my $setID = shift @components; 349 my $setID = shift @components;
350
146 if (defined $setID) { 351 if (defined $setID) {
147 my $setArg = shift @components; 352 my $setArg = shift @components;
353
148 if (!defined $setArg) { 354 if (defined $setArg) {
149 $result = WeBWorK::ContentGenerator::Instructor::ProblemSetEditor->new($r, $ce, $db)->go($setID);
150 } elsif ($setArg eq "problems") { 355 if ($setArg eq "problems") {
151 $result = WeBWorK::ContentGenerator::Instructor::ProblemList->new($r, $ce, $db)->go($setID); 356 $contentGenerator = "WeBWorK::ContentGenerator::Instructor::ProblemList";
357 @arguments = ($setID);
358 }
152 } elsif ($setArg eq "users") { 359 elsif ($setArg eq "users") {
153 $result = WeBWorK::ContentGenerator::Instructor::Assigner->new($r, $ce, $db)->go($setID); 360 $contentGenerator = "WeBWorK::ContentGenerator::Instructor::UsersAssignedToSet";
361 @arguments = ($setID);
362 }
154 } 363 }
155 } else { 364 else {
156 $result = WeBWorK::ContentGenerator::Instructor::ProblemSetList->new($r, $ce, $db)->go; 365 $contentGenerator = "WeBWorK::ContentGenerator::Instructor::ProblemSetEditor";
366 @arguments = ($setID);
367 }
157 } 368 }
369 else {
370 $contentGenerator = "WeBWorK::ContentGenerator::Instructor::ProblemSetList";
371 @arguments = ();
372
373 }
374 }
158 } elsif ($instructorArgument eq "pgProblemEditor") { 375 elsif ($instructorArgument eq "pgProblemEditor") {
159 $result = WeBWorK::ContentGenerator::Instructor::PGProblemEditor->new($r, $ce, $db)->go(@components); 376 $contentGenerator = "WeBWorK::ContentGenerator::Instructor::PGProblemEditor";
377 @arguments = @components;
378 }
379 elsif ($instructorArgument eq "send_mail") {
380 $contentGenerator = "WeBWorK::ContentGenerator::Instructor::SendMail";
381 @arguments = @components;
382 }
383 elsif ($instructorArgument eq "show_answers") {
384 $contentGenerator = "WeBWorK::ContentGenerator::Instructor::ShowAnswers";
385 @arguments = @components;
386 }
387 elsif ($instructorArgument eq "stats") {
388 $contentGenerator = "WeBWorK::ContentGenerator::Instructor::Stats";
389 @arguments = @components;
390 }
391 elsif ($instructorArgument eq "files") {
392 $contentGenerator = "WeBWorK::ContentGenerator::Instructor::FileXfer";
393 @arguments = @components;
394 }
160 } 395 }
161 } elsif ($arg eq "options") { 396 else {
162 $result = WeBWorK::ContentGenerator::Options->new($r, $ce, $db)->go; 397 # $arg is a set ID
163 } elsif ($arg eq "feedback") {
164 $result = WeBWorK::ContentGenerator::Feedback->new($r, $ce, $db)->go;
165 } elsif ($arg eq "logout") {
166 $result = WeBWorK::ContentGenerator::Logout->new($r, $ce, $db)->go;
167 } elsif ($arg eq "test") {
168 $result = WeBWorK::ContentGenerator::Test->new($r, $ce, $db)->go;
169 } elsif ($arg eq "quiz_mode" ) {
170 # Gateway quiz capability -- very similar to problem set (initially)
171 $result = WeBWorK::ContentGenerator::GatewayQuiz->new($r, $ce, $db)->go(@components);
172 } else { # We've got the name of a problem set.
173 my $problem_set = $arg; 398 my $setID = $arg;
174 my $ps_arg = shift @components; 399 my $problemID = shift @components;
175 400
176 if (!defined $ps_arg) { 401 if (defined $problemID) {
177 # list the problems in the problem set 402 $contentGenerator = "WeBWorK::ContentGenerator::Problem";
178 $result = WeBWorK::ContentGenerator::ProblemSet->new($r, $ce, $db)->go($problem_set); 403 @arguments = ($setID, $problemID);
404 }
179 } else { 405 else {
180 # We've got the name of a problem 406 $contentGenerator = "WeBWorK::ContentGenerator::ProblemSet";
181 my $problem = $ps_arg; 407 @arguments = ($setID);
182
183 $WeBWorK::timer0 = WeBWorK::Timing->new("Problem $course:$problem_set/$problem");
184 $WeBWorK::timer0->start;
185 my $result = WeBWorK::ContentGenerator::Problem->new($r, $ce, $db)->go($problem_set, $problem);
186 $WeBWorK::timer0->stop;
187 $WeBWorK::timer0->save;
188 return $result;
189
190
191 }
192 } 408 }
193 } 409 }
194 410 }
195 #$dispatchTimer->stop; 411
196 412=item Call the selected content generator
413
414Instantiate the selected subclass of content generator and call its C<&go> method. Store the result.
415
416=cut
417
418 my $result;
419 if ($contentGenerator) {
420 runtime_use($contentGenerator);
421 my $cg = $contentGenerator->new($r, $ce, $db);
422 @arguments = () unless @arguments;
423 $WeBWorK::timer = WeBWorK::Timing->new("${contentGenerator}::go(@arguments)") if $timingON == 1;
424 $WeBWorK::timer->start if $timingON == 1;
425
426 $result = $cg->go(@arguments);
427
428 $WeBWorK::timer->stop if $timingON == 1;
429 $WeBWorK::timer->save if $timingON == 1;
430 } else {
431 $result = NOT_FOUND;
432 }
433
434=item Return the result of calling the content generator
435
436The return value of the content generator's C<&go> function is returned.
437
438=cut
439
197 return $result; 440 return $result;
198} 441}
199 442
443=back
444
445=head1 AUTHOR
446
447Written by Dennis Lambe, malsyned at math.rochester.edu. Modified by Sam
448Hathaway, sh002i at math.rochester.edu.
449
450=cut
451
2001; 4521;

Legend:
Removed from v.1245  
changed lines
  Added in v.1755

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9