[system] / branches / rel-2-1-patches / webwork-modperl / lib / WeBWorK / ContentGenerator.pm Repository:
ViewVC logotype

Diff of /branches/rel-2-1-patches/webwork-modperl/lib/WeBWorK/ContentGenerator.pm

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

Revision 1872 Revision 1873
1################################################################################ 1################################################################################
2# WeBWorK Online Homework Delivery System 2# WeBWorK Online Homework Delivery System
3# Copyright © 2000-2003 The WeBWorK Project, http://openwebwork.sf.net/ 3# Copyright © 2000-2003 The WeBWorK Project, http://openwebwork.sf.net/
4# $CVSHeader: webwork-modperl/lib/WeBWorK/ContentGenerator.pm,v 1.82 2004/03/10 02:30:32 sh002i Exp $ 4# $CVSHeader: webwork-modperl/lib/WeBWorK/ContentGenerator.pm,v 1.83 2004/03/10 02:51:57 sh002i Exp $
5# 5#
6# This program is free software; you can redistribute it and/or modify it under 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 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 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. 9# version, or (b) the "Artistic License" which comes with this package.
29 my $cg = WeBWorK::ContentGenerator::SomeSubclass->new($r); 29 my $cg = WeBWorK::ContentGenerator::SomeSubclass->new($r);
30 my $result = $cg->go(); 30 my $result = $cg->go();
31 31
32=head1 DESCRIPTION 32=head1 DESCRIPTION
33 33
34FIXME: write this 34WeBWorK::ContentGenerator provides the framework for generating page content.
35"Content generators" are subclasses of this class which provide content for
36particular parts of the system.
37
38Default versions of methods used by the templating system are provided. Several
39useful methods are provided for rendering common output idioms and some
40miscellaneous utilities are provided.
35 41
36=cut 42=cut
37 43
38use strict; 44use strict;
39use warnings; 45use warnings;
40use Apache::Constants qw(:common); 46use Apache::Constants qw(:common);
41use CGI qw(*ul *li); 47use CGI qw(*ul *li);
42use URI::Escape; 48use URI::Escape;
43use WeBWorK::Authz;
44use WeBWorK::DB;
45use WeBWorK::Template qw(template); 49use WeBWorK::Template qw(template);
46 50
47# This is a very unruly file, so I'm going to use very large comments to divide 51################################################################################
48# it into logical sections.
49 52
50=head1 CONSTRUCTOR 53=head1 CONSTRUCTOR
51 54
52=over 55=over
53 56
54=item new($r) 57=item new($r)
55 58
56Create a new instance of a content generator. Supply a WeBWorK::Request object 59Creates a new instance of a content generator. Supply a WeBWorK::Request object
57$r. 60$r.
58 61
59=cut 62=cut
60 63
61sub new { 64sub new {
75=back 78=back
76 79
77=cut 80=cut
78 81
79################################################################################ 82################################################################################
80# Invocation and template processing
81################################################################################
82 83
83=head1 INVOCATION 84=head1 INVOCATION
84 85
85=over 86=over
86 87
87=item go() 88=item go()
88 89
89Render a page, using methods from the particular subclass of ContentGenerator. 90Generates a page, using methods from the particular subclass of ContentGenerator
90go() will call the following methods when invoked: 91that is instantiated. Generatoion is broken up into several steps, to give
92subclasses ample control over the process.
91 93
92=over 94=over
93 95
94=item pre_header_initialize() 96=item 1
95 97
96Give the subclass a chance to do initialization necessary before generating the 98go() will attempt to call the method pre_header_initialize(). This method may be
97HTTP header. 99implemented in subclasses which must do processing before the HTTP header is
100emitted.
98 101
99=item header() 102=item 2
100 103
101This method provides a standard HTTP header with Content-Type text/html. 104go() will attempt to call the method header(). This method emits the HTTP
102Subclasses are welcome to override this for things like an image-creation 105header. It is defined in this class (see below), but may be overridden in
103content generator or a PDF generator. In addition, if header() returns a value, 106subclasses which need to send different header information. For some reason, the
104that will be the value returned by go(). 107return value of header() will be used as the result of this function, if it is
108defined.
105 109
106=item initialize() 110=item 3
107 111
108Let the subclass do post-header initialization. 112At this point, go() will terminate if the request is a HEAD request or if the
113field $self->{noContent} contains a true value.
109 114
110If pre_header_initialize() or header() sets $self->{noContent} to a true value, 115=item 4
111initialize() will not be run and the content or template processing code
112will not be executed. This is probably only desirable if a redirect has been
113issued.
114 116
115=item template() 117If the field $self->{sendFile} is defined, the method sendFile() is called to
118send the specified file to the client, and go() terminates. See below.
116 119
117The layout template is processed. See template() below. 120=item 5
118 121
119If the subclass implements a method named content(), it is called 122go() then attempts to call the method initialize(). This method may be
120instead and no template processing occurs. 123implemented in subclasses which must do processing after the HTTP header is sent
124but before any content is sent.
125
126=item 6
127
128The method content() is called to send the page content to client.
121 129
122=back 130=back
123 131
124=cut 132=cut
125 133
126sub go { 134sub go {
127 my $self = shift; 135 my ($self) = @_;
128 my $r = $self->{r}; 136 my $r = $self->r;
129 my $ce = $r->ce; 137 my $ce = $r->ce;
130 138
131 my $returnValue = OK; 139 my $returnValue = OK;
132 140
133 $self->pre_header_initialize(@_) if $self->can("pre_header_initialize"); 141 $self->pre_header_initialize(@_) if $self->can("pre_header_initialize");
142
134 my $headerReturn = $self->header(@_); 143 my $headerReturn = $self->header(@_);
135 $returnValue = $headerReturn if defined $headerReturn; 144 $returnValue = $headerReturn if defined $headerReturn;
136 return $returnValue if $r->header_only or $self->{noContent}; 145 return $returnValue if $r->header_only or $self->{noContent};
137 146
138 # if the sendFile flag is set, send the file and exit; 147 # if the sendFile flag is set, send the file and exit;
139 if ($self->{sendFile}) { 148 if ($self->{sendFile}) {
140 return $self->sendFile; 149 return $self->sendFile;
141 } 150 }
142 151
143 $self->initialize(@_) if $self->can("initialize"); 152 $self->initialize() if $self->can("initialize");
144 153
145 # A content generator will have a "content" method if it does not
146 # wish to be passed through template processing, but wishes to be
147 # completely responsible for it's own output.
148 if ($self->can("content")) {
149 $self->content(@_); 154 $self->content();
150 } else {
151 # if the content generator specifies a custom template name, use that
152 # field in the $ce->{templates} hash instead of "system" if it exists.
153 my $templateName;
154 if ($self->can("templateName")) {
155 $templateName = $self->templateName;
156 } else {
157 $templateName = "system";
158 }
159 $templateName = "system" unless exists $ce->{templates}->{$templateName};
160 template($ce->{templates}->{$templateName}, $self);
161 }
162 155
163 return $returnValue; 156 return $returnValue;
164} 157}
165 158
166=item sendFile() 159=item sendFile()
160
161Sends the file specified in $self->{sendFile} to the client. $self->{sendFile}
162should be a reference to a hash containing the following fields:
163
164 source => full path to the file to send
165 type => the content type of the file
166 name => the name that the client should give to the file upon download
167
168This method is called internally by go() if the field $self->{sendFile} is
169present.
170
171This mechanism relies on the header() method to send appropriate C<Content-Type>
172and C<Content-Disposition> headers.
173
174This mechanism is fragile and will probably be replaced by something else in the
175future.
167 176
168=cut 177=cut
169 178
170sub sendFile { 179sub sendFile {
171 my ($self) = @_; 180 my ($self) = @_;
183 close $fh; 192 close $fh;
184 193
185 return OK; 194 return OK;
186} 195}
187 196
197=item r()
198
199Returns a reference to the WeBWorK::Request object associated with this
200instance.
201
202=cut
203
204sub r {
205 my ($self) = @_;
206
207 return $self->{r};
208}
209
188=back 210=back
189 211
190=cut 212=cut
191 213
192################################################################################ 214################################################################################
193# Macros used by content generators to render common idioms
194################################################################################
195 215
196# FIXME: some of these should be moved to WeBWorK::HTML:: modules! 216=head1 STANDARD METHODS
197 217
198=head1 HTML MACROS 218The following are the standard content generator methods. Some are defined here,
199 219but may be overridden in a subclass. Others are not defined unless they are
200Macros used by content generators to render common idioms 220defined in a subclass.
201 221
202=over 222=over
203 223
204=item pathMacro($args, @path) 224=item pre_header_initialize()
205 225
206Helper macro for <!--#path--> escape: $args is a hash reference containing the 226Not defined in this package.
207"style", "image", "text", and "textonly" arguments to the escape. @path consists
208of ordered key-value pairs of the form:
209 227
210 "Page Name" => URL 228May be defined by a subclass to perform any processing that must occur before
229the HTTP header is sent.
211 230
212If the page should not have a link associated with it, the URL should be left
213empty. Authentication data is added to the URL so you don't have to. A fully-
214formed path line is returned, suitable for returning by a function implementing
215the #path escape.
216
217=cut 231=cut
218 232
219sub pathMacro { 233#sub pre_header_initialize { }
220 my $self = shift;
221 my %args = %{ shift() };
222 my @path = @_;
223 $args{style} = "text" if $args{textonly};
224 my $sep;
225 if ($args{style} eq "image") {
226 $sep = CGI::img({-src=>$args{image}, -alt=>$args{text}});
227 } else {
228 $sep = $args{text};
229 }
230 my $auth = $self->url_authen_args;
231 my @result;
232 while (@path) {
233 my $name = shift @path;
234 my $url = shift @path;
235 if ($url and not $args{textonly}) {
236 push @result, CGI::a({-href=>"$url?$auth"}, $name);
237 } else {
238 push @result, $name;
239 }
240 }
241
242 return join($sep, @result), "\n";
243}
244
245=item siblingsMacro(@siblings)
246
247=cut
248
249sub siblingsMacro {
250 my $self = shift;
251 my @siblings = @_;
252 my $sep = CGI::br();
253 my $auth = $self->url_authen_args;
254 my @result;
255 while (@siblings) {
256 my $name = shift @siblings;
257 my $url = shift @siblings;
258 push @result, $url
259 ? CGI::a({-href=>"$url?$auth"}, $name)
260 : $name;
261 }
262 return join($sep, @result) . "\n";
263}
264
265=item navMacro($args, $tail)
266
267=cut
268
269sub navMacro {
270 my $self = shift;
271 my %args = %{ shift() };
272 my $tail = shift;
273 my @links = @_;
274 my $auth = $self->url_authen_args;
275 my $ce = $self->{ce};
276 my $prefix = $ce->{webworkURLs}->{htdocs}."/images";
277 my @result;
278 while (@links) {
279 my $name = shift @links;
280 my $url = shift @links;
281 my $img = shift @links;
282 my $html =
283 ($img && $args{style} eq "images")
284 ? CGI::img(
285 {src=>($prefix."/".$img.$args{imagesuffix}),
286 border=>"",
287 alt=>"$name"})
288 : $name;
289 unless($img && !$url) {
290 push @result, $url
291 ? CGI::a({-href=>"$url?$auth$tail"}, $html)
292 : $html;
293 }
294 }
295 return join($args{separator}, @result) . "\n";
296}
297
298=item hidden_fields(@fields)
299
300Return hidden <INPUT> tags for each field mentioned in @fields (or all fields if
301list is empty), taking data from the current request.
302
303=cut
304
305sub hidden_fields($;@) {
306 my $self = shift;
307 my $r = $self->{r};
308 my @fields = @_;
309 @fields or @fields = $r->param;
310 my $courseEnvironment = $self->{ce};
311 my $html = "";
312
313 foreach my $param (@fields) {
314 my $value = $r->param($param);
315 $html .= CGI::input({-type=>"hidden",-name=>"$param",-value=>"$value"});
316 }
317 return $html;
318}
319
320=item hidden_authen_fields()
321
322Use hidden_fields to return hidden <INPUT> tags for request fields used in
323authentication.
324
325=cut
326
327sub hidden_authen_fields($) {
328 my $self = shift;
329 return $self->hidden_fields("user","effectiveUser","key");
330}
331
332=item url_args(@fields)
333
334Return a URL query string (without the leading `?') containing values for each
335field mentioned in @fields, or all fields if list is empty. Data is taken from
336the current request.
337
338=cut
339
340sub url_args($;@) {
341 my $self = shift;
342 my $r = $self->{r};
343 my @fields = @_;
344 @fields or @fields = $r->param; # If no fields are passed in, do them all.
345 my $courseEnvironment = $self->{ce};
346
347 my @pairs;
348 foreach my $param (@fields) {
349 my @values = $r->param($param);
350 foreach my $value (@values) {
351 push @pairs, uri_escape($param) . "=" . uri_escape($value);
352 }
353 }
354
355 return join("&", @pairs);
356}
357
358=item url_authen_args()
359
360Use url_args to return a URL query string for request fields used in
361authentication.
362
363=cut
364
365sub url_authen_args($) {
366 my $self = shift;
367 my $r = $self->{r};
368 return $self->url_args("user","effectiveUser","key");
369}
370
371=item nbsp($string)
372
373If string is the empty string, the HTML entity C< &nbsp; > is returned.
374Otherwise the string is returned.
375
376=cut
377
378sub nbsp {
379 my $self = shift;
380 my $str = shift;
381 ($str =~/\S/) ? $str : '&nbsp;' ; # returns non-breaking space for empty strings
382 # tricky cases: $str =0;
383 # $str is a complex number
384}
385
386=item print_form_data($begin, $middle, $end, $omit)
387
388Return a string containing request fields not matched by $omit, placing $begin
389before each field name, $middle between each field and its value, and $end after
390each value. Values are taken from the current request. $omit is a quoted reguar
391expression.
392
393=cut
394
395sub print_form_data {
396 my ($self, $begin, $middle, $end, $qr_omit) = @_;
397 my $return_string = "";
398 my $r=$self->{r};
399 my @form_data = $r->param;
400 foreach my $name (@form_data) {
401 next if ($qr_omit and $name =~ /$qr_omit/);
402 my @values = $r->param($name);
403 foreach my $variable (qw(begin name middle value end)) {
404 no strict 'refs';
405 ${$variable} = "" unless defined ${$variable};
406 }
407 foreach my $value (@values) {
408 $return_string .= "$begin$name$middle$value$end";
409 }
410 }
411 return $return_string;
412}
413
414=item errorOutput($error, $details)
415
416=cut
417
418sub errorOutput($$$) {
419 my ($self, $error, $details) = @_;
420 return
421 CGI::h3("Software Error"),
422 CGI::p(<<EOF),
423WeBWorK has encountered a software error while attempting to process this
424problem. It is likely that there is an error in the problem itself. If you are
425a student, contact your professor to have the error corrected. If you are a
426professor, please consut the error output below for more informaiton.
427EOF
428 CGI::h3("Error messages"), CGI::p(CGI::tt($error)),
429 CGI::h3("Error context"), CGI::p(CGI::tt($details));
430}
431
432=item warningOutput($warnings)
433
434=cut
435
436sub warningOutput($$) {
437 my ($self, $warnings) = @_;
438
439 my @warnings = split m/\n+/, $warnings;
440
441 return
442 CGI::h3("Software Warnings"),
443 CGI::p(<<EOF),
444WeBWorK has encountered warnings while attempting to process this problem. It
445is likely that this indicates an error or ambiguity in the problem itself. If
446you are a student, contact your professor to have the problem corrected. If you
447are a professor, please consut the warning output below for more informaiton.
448EOF
449 CGI::h3("Warning messages"),
450 CGI::ul(CGI::li(\@warnings)),
451 ;
452}
453
454=item systemLink($urlpath, %options)
455
456Generate a link to another part of the system. $urlpath is WeBWorK::URLPath
457object from which the base path will be taken. %options can consist of:
458
459=over
460
461=item authen
462
463Boolen, whether to include authentication information in the resulting URL. If
464not given, a true value is assumed.
465
466=item realUserID
467
468If C<authen> is true, the current real user ID is replaced with this value.
469
470=item sessionKey
471
472If C<authen> is true, the current session key is replaced with this value.
473
474=item effectiveUserID
475
476If C<authen> is true, the current effective user ID is replaced with this value.
477
478=back
479
480=cut
481
482sub systemLink {
483 my ($self, $urlpath, %options) = @_;
484 my $r = $self->{r};
485
486 my $authen = $options{authen} || 1;
487
488 my $url = $r->location . $urlpath->path;
489
490 if ($authen) {
491 my $realUserID = $options{realUserID} || $r->param("user");
492 my $sessionKey = $options{sessionKey} || $r->param("key");
493 my $effectiveUserID = $options{effectiveUserID} || $r->param("effectiveUser");
494
495 my @params;
496 defined $realUserID and push @params, "user=$realUserID";
497 defined $sessionKey and push @params, "key=$sessionKey";
498 defined $effectiveUserID and push @params, "effectiveUser=$effectiveUserID";
499
500 $url .= "?" . join("&", @params) if @params;
501 }
502
503 return $url;
504}
505
506=back
507
508=cut
509
510################################################################################
511# Generic versions of template escapes
512################################################################################
513
514=head1 THE HEADER METHOD
515
516=over
517 234
518=item header() 235=item header()
519 236
520The C<header> method is defined in WeBWorK::ContentGenerator to generate a 237Defined in this package.
521default C<Content-type> of text/html and send the HTTP header.
522 238
523=back 239Generates and sends a default HTTP header. If the field $self->{sendFile} is
240present, sends the following headers (where TYPE is $self->{sendFile}->{type}
241and NAME is $self->{sendFile}->{name}):
242
243 Content-Type: TYPE
244 Content-Disposition: attachment; filename=NAME
245
246If $self->{sendFile} is not present, sends the following headers:
247
248 Content-Type: text/html
249
250See sendFile() above for more information on the sendFile mechanism.
524 251
525=cut 252=cut
526 253
527sub header { 254sub header {
528 my $self = shift; 255 my $self = shift;
529 my $r = $self->{r}; 256 my $r = $self->r;
530 257
531 if ($self->{sendFile}) { 258 if ($self->{sendFile}) {
532 my $contentType = $self->{sendFile}->{type}; 259 my $contentType = $self->{sendFile}->{type};
533 my $fileName = $self->{sendFile}->{name}; 260 my $fileName = $self->{sendFile}->{name};
534 $r->content_type($contentType); 261 $r->content_type($contentType);
540 267
541 $r->send_http_header(); 268 $r->send_http_header();
542 return OK; 269 return OK;
543} 270}
544 271
545=head1 TEMPLATE ESCAPE METHODS 272=item initialize()
546 273
547Template escape methods are invoked when a 274Not defined in this package.
548C< <!--#escape argument="value" ... -> > construct is encountered in the
549template. The methods can be defined here in ContentGenerator, or in a
550particular subclass. Arguments are passed to the method as a reference to a
551hash.
552 275
553The following template escapes are currently defined: 276May be defined by a subclass to perform any processing that must occur after the
277HTTP header is sent but before any content is sent.
278
279=cut
280
281#sub initialize { }
282
283=item content()
284
285Defined in this package.
286
287Print the content of the generated page.
288
289The implementation in this package uses WeBWorK::Template to define the content
290of the page. See WeBWorK::Template for details.
291
292If a method named templateName() exists, it it called to determine the name of
293the template to use. If not, the default template, "system", is used. The
294location of the template is looked up in the course environment.
295
296=cut
297
298sub content {
299 my ($self) = @_;
300 my $ce = $self->r->ce;
301
302 # if the content generator specifies a custom template name, use that
303 # field in the $ce->{templates} hash instead of "system" if it exists.
304 my $templateName;
305 if ($self->can("templateName")) {
306 $templateName = $self->templateName;
307 } else {
308 $templateName = "system";
309 }
310 $templateName = "system" unless exists $ce->{templates}->{$templateName};
311 template($ce->{templates}->{$templateName}, $self);
312}
313
314=back
315
316=cut
317
318# ------------------------------------------------------------------------------
319
320=head2 Template escape handlers
321
322Template escape handlers are invoked when the template processor encounters a
323matching escape sequence in the template. The escapse sequence's arguments are
324passed to the methods as a reference to a hash.
325
326For more information, refer to WeBWorK::Template.
327
328The following template escapes handlers are defined here or may be defined in
329subclasses. For methods that are not defined in this package, the documentation
330defines the interface and behavior that any subclass implementation must follow.
554 331
555=over 332=over
556 333
557=item head 334=item head()
558 335
336Not defined in this package.
337
559Any tags that should appear in the HEAD of the document. Not defined by default. 338Any tags that should appear in the HEAD of the document.
560 339
340=cut
341
342#sub head { }
343
561=item info 344=item info()
562 345
563Auxiliary information related to the C<body>. Not defined by default. 346Not defined in this package.
564 347
348Auxiliary information related to the content displayed in the C<body>.
349
350=cut
351
352#sub info { }
353
565=item links 354=item links()
566 355
567Links that should appear on every page. Defined in WeBWorK::ContentGenerator by 356Defined in this package.
568default. 357
358Links that should appear on every page.
569 359
570=cut 360=cut
571 361
572sub links { 362sub links {
573 my ($self) = @_; 363 my ($self) = @_;
574 my $r = $self->{r}; 364 my $r = $self->r;
575 my $db = $r->db; 365 my $db = $r->db;
576 my $urlpath = $r->urlpath; 366 my $urlpath = $r->urlpath;
577 367
578 # we're linking to other places in the same course, so grab the courseID from the current path 368 # we're linking to other places in the same course, so grab the courseID from the current path
579 my $courseID = $urlpath->arg("courseID"); 369 my $courseID = $urlpath->arg("courseID");
664 CGI::li(CGI::a({href=>$self->systemLink($logout)}, $logout->name)), 454 CGI::li(CGI::a({href=>$self->systemLink($logout)}, $logout->name)),
665 $iResult, 455 $iResult,
666 ); 456 );
667} 457}
668 458
669## FIXME: drunk code. rewrite.
670## also, this should be structured s.t. subclasses can add items to the links
671## area, i.e. "stacking"
672#sub links {
673# my $self = shift;
674# my @components = @_;
675# my $ce = $self->{ce};
676# my $db = $self->{db};
677# my $userName = $self->{r}->param("user");
678# my $courseName = $ce->{courseName};
679# my $root = $ce->{webworkURLs}->{root};
680#
681# #my $Key = $db->getKey($userName); # checked
682# #my $key = (defiend $key
683# # ? $Key->key()
684# # : "");
685# #
686# #return "" unless defined $key;
687# # This has been replaced by using "#if loggedin" in ur.template.
688#
689# # URLs to parts of the system
690# my $probSets = "$root/$courseName/?" . $self->url_authen_args();
691# my $prefs = "$root/$courseName/options/?" . $self->url_authen_args();
692# my $grades = "$root/$courseName/grades/?" . $self->url_authen_args();
693# my $help = "$ce->{webworkURLs}->{docs}?" . $self->url_authen_args();
694# my $logout = "$root/$courseName/logout/?" . $self->url_authen_args();
695#
696# my $PermissionLevel = $db->getPermissionLevel($userName); # checked
697# my $permLevel = (defined $PermissionLevel
698# ? $PermissionLevel->permission()
699# : 0);
700#
701# return join("",
702# CGI::div( {style=>'font-size:larger'},CGI::a({-href=>$probSets}, "Problem&nbsp;Sets")
703# ),
704# CGI::a({-href=>$prefs}, "User&nbsp;Prefs"), CGI::br(),
705# CGI::a({-href=>$grades}, "Grades"), CGI::br(),
706# CGI::a({-href=>$help,-target=>'_help_'}, "Help"), CGI::br(),
707# CGI::a({-href=>$logout}, "Log Out"), CGI::br(),
708# ($permLevel > 0
709# ? $self->instructor_links(@components) : ""
710# ),
711# );
712#}
713#
714#sub instructor_links {
715# my $self = shift;
716# my @components = @_;
717# my $args = pop(@components); # get hash of option arguments
718# my $courseName = $self->{ce}->{courseName};
719# my $root = $self->{ce}->{webworkURLs}->{root};
720# my $userName = $self->{r}->param("effectiveUser");
721# $userName = $self->{r}->param("user") unless defined $userName;
722# my ($set, $prob) = @components;
723# my $instructor = "$root/$courseName/instructor/?" . $self->url_authen_args();
724# my $sets = "$root/$courseName/instructor/sets/?" . $self->url_authen_args();
725# my $users = "$root/$courseName/instructor/users/?" . $self->url_authen_args();
726# my $email = "$root/$courseName/instructor/send_mail/?" . $self->url_authen_args();
727# my $scoring = "$root/$courseName/instructor/scoring/?" . $self->url_authen_args();
728# my $statsRoot = "$root/$courseName/instructor/stats";
729# my $stats = $statsRoot. '/?'.$self->url_authen_args();
730# my $fileXfer = "$root/$courseName/instructor/files/?" . $self->url_authen_args();
731#
732#
733# # Add direct links to sets e.g. 3:4 for set3 problem 4
734# my $setURL = (defined $set)
735# ? "$root/$courseName/instructor/sets/$set/?" . $self->url_authen_args()
736# : '';
737# my $probURL = (defined $set && defined $prob)
738# ? "$root/$courseName/instructor/pgProblemEditor/$set/$prob?" . $self->url_authen_args()
739# : '';
740#
741# my ($setLink, $problemLink) = ("", "");
742# if ($setURL) {
743# $setLink = "&nbsp;&nbsp;&nbsp;&nbsp;"
744# . CGI::a({-href=>$setURL}, "Set&nbsp;$set")
745# . CGI::br();
746# if ($probURL) {
747# $problemLink = "&nbsp;&nbsp;&nbsp;&nbsp;"
748# . CGI::a({-href=>$probURL}, "Problem&nbsp;$prob")
749# . CGI::br();
750# }
751# }
752#
753# #my $setProb = ($setURL)
754# # ? CGI::a({-href=>$setURL}, $set)
755# # : '';
756# #$setProb .= ':' . CGI::a({-href=>$probURL},$prob) if $setProb && $probURL;
757#
758# return join("",
759# CGI::hr(),
760# CGI::div( {style=>'font-size:larger'},
761# CGI::a({-href=>$instructor}, "Instructor&nbsp;Tools")
762# ),
763# '&nbsp;&nbsp;&nbsp;',CGI::a({-href=>$users}, "User&nbsp;List"), CGI::br(),
764# '&nbsp;&nbsp;&nbsp;',CGI::a({-href=>$sets}, "Set&nbsp;List"), CGI::br(),
765# $setLink,
766# $problemLink,
767# '&nbsp;&nbsp;&nbsp;',CGI::a({-href=>$email}, "Mail&nbsp;Merge"), CGI::br(),
768# '&nbsp;&nbsp;&nbsp;',CGI::a({-href=>$scoring}, "Scoring"), CGI::br(),
769# '&nbsp;&nbsp;&nbsp;',CGI::a({-href=>$stats}, "Statistics"), CGI::br(),
770# (defined($set))
771# ? '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'.CGI::a({-href=>"$statsRoot/set/$set/?".$self->url_authen_args}, "$set").CGI::br()
772# : '',
773# (defined($userName))
774# ? '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'.CGI::a({-href=>"$statsRoot/student/$userName/?".$self->url_authen_args}, "$userName").CGI::br()
775# : '',
776# '&nbsp;&nbsp;&nbsp;',CGI::a({-href=>$fileXfer}, "File&nbsp;Transfer"), CGI::br(),
777# );
778#}
779
780=item loginstatus 459=item loginstatus()
781 460
461Defined in this package.
462
782A notification message announcing the current real user and effective user, a 463Print a notification message announcing the current real user and effective
783link to stop acting as the effective user, and a logout link. Defined in 464user, a link to stop acting as the effective user, and a link to logout.
784WeBWorK::ContentGenerator by default.
785 465
786=cut 466=cut
787 467
788sub loginstatus { 468sub loginstatus {
789 my ($self) = @_; 469 my ($self) = @_;
790 my $r = $self->{r}; 470 my $r = $self->r;
791 my $urlpath = $r->urlpath; 471 my $urlpath = $r->urlpath;
792 472
793 my $key = $r->param("key"); 473 my $key = $r->param("key");
794 474
795 if ($key) { 475 if ($key) {
810 } 490 }
811 491
812 return ""; 492 return "";
813} 493}
814 494
815#sub loginstatus_crap { 495=item nav($args)
816# my $self = shift;
817# my $r = $self->{r};
818# my $ce = $self->{ce};
819#
820# my $user = $r->param("user");
821# my $eUser = $r->param("effectiveUser");
822# my $key = $r->param("key");
823#
824# return "" unless $key;
825#
826# my $exitURL = $r->uri() . "?user=$user&key=$key";
827#
828# my $root = $ce->{webworkURLs}->{root};
829# my $courseID = $ce->{courseName};
830# my $logout = "$root/$courseID/logout/?" . $self->url_authen_args();
831#
832# print CGI::small("User:", "$user");
833#
834# if ($user ne $eUser) {
835# print CGI::br(), CGI::font({-color=>'red'},
836# CGI::small("Acting as:", "$eUser")
837# ),
838# CGI::br(), CGI::a({-href=>$exitURL},
839# CGI::small("Stop Acting")
840# );
841# }
842#
843# print CGI::br(), CGI::a({-href=>$logout}, CGI::small("Log Out"));
844#
845# return "";
846#}
847 496
848=item nav 497Not defined in this package.
849 498
850Links to the previous, next, and parent objects. Not defined by default. 499Links to the previous, next, and parent objects.
500
501$args is a reference to a hash containing the following fields:
851 502
852 style => text|image 503 style => text|image
853 imageprefix => prefix to prepend to base image URL 504 imageprefix => prefix to prepend to base image URL
854 imagesuffix => suffix to append to base image URL 505 imagesuffix => suffix to append to base image URL
855 separator => HTML to place in between links 506 separator => HTML to place in between links
856 507
508If C<style> is "image", image URLs are constructed by prepending C<imageprefix>
509and postpending C<imagesuffix> to the image base names defined by the
510implementor. (Examples of base names include "Prev", "Next", "ProbSet", and
511"Up"). Each concatenated string should form an absolute URL to an image file.
512For example:
513
514 <!--#nav style="images" imageprefix="/webwork2_files/images/nav"
515 imagesuffix=".gif" separator=" "-->
516
517=cut
518
519#sub nav { }
520
857=item options 521=item options()
858 522
859A place for an options form, like the problem display options. Not defined by 523Not defined in this package.
860default.
861 524
862=item path 525Print an auxiliary options form, related to the content displayed in the
526C<body>.
863 527
864"Breadcrubs" from the current page to the root of the virtual hierarchy. Defined 528=item path($args)
865in WeBWorK::ContentGenerator to pull information from the WeBWorK::URLPath. 529
530Defined in this package.
531
532Print "breadcrubs" from the root of the virtual hierarchy to the current page.
533$args is a reference to a hash containing the following fields:
866 534
867 style => type of separator: text|image 535 style => type of separator: text|image
868 image => URL of separator image 536 image => if style=image, URL of image to use as path separator
869 text => text of texual separator (also used for image alt text) 537 text => if style=text, text to use as path separator
870 textonly => suppress links 538 if style=image, the ALT text of each separator image
539 textonly => suppress all HTML, return only plain text
540
541The implementation in this package takes information from the WeBWorK::URLPath
542associated with the current request.
871 543
872=cut 544=cut
873 545
874sub path { 546sub path {
875 my ($self, $args) = @_; 547 my ($self, $args) = @_;
876 my $r = $self->{r}; 548 my $r = $self->r;
877 549
878 my @path; 550 my @path;
879 551
880 my $urlpath = $r->urlpath; 552 my $urlpath = $r->urlpath;
881 do { 553 do {
885 $path[$#path] = ""; # we don't want the last path element to be a link 557 $path[$#path] = ""; # we don't want the last path element to be a link
886 558
887 return $self->pathMacro($args, @path); 559 return $self->pathMacro($args, @path);
888} 560}
889 561
890=item siblings 562=item siblings()
891 563
892Links to siblings of the current object. Not defined by default. 564Not defined in this package.
893 565
566Print links to siblings of the current object.
567
568=cut
569
570#sub siblings { }
571
894=item submiterror 572=item submiterror()
895 573
574Defined in this package.
575
896Any error messages resulting from the last form submission. Defined in 576Print any error messages resulting from the last form submission.
897WeBWorK::ContentGenerator by default. 577
578The implementation in this package prints the value of the field
579$self->{submitError}, if it is present.
898 580
899=cut 581=cut
900 582
901sub submiterror { 583sub submiterror {
902 my ($self) = @_; 584 my ($self) = @_;
905 } else { 587 } else {
906 return ""; 588 return "";
907 } 589 }
908} 590}
909 591
910=item title 592=item title()
911 593
912The title of the current page. Defined in WeBWorK::ContentGenerator to pull 594Defined in this package.
913information from the WeBWorK::URLPath. 595
596Print the title of the current page.
597
598The implementation in this package takes information from the WeBWorK::URLPath
599associated with the current request.
914 600
915=cut 601=cut
916 602
917sub title { 603sub title {
918 my ($self, $args) = @_; 604 my ($self, $args) = @_;
919 my $r = $self->{r}; 605 my $r = $self->r;
920 606
921 return $r->urlpath->name; 607 return $r->urlpath->name;
922} 608}
923 609
924=item warnings 610=item warnings()
925 611
926Any warnings. Not defined by default. 612Defined in this package.
613
614Print accumulated warnings.
615
616The implementation in this package checks for a note in the request named
617"warnings". If present, its contents are formatted and returned.
927 618
928=cut 619=cut
929 620
930sub warnings { 621sub warnings {
931 my ($self) = @_; 622 my ($self) = @_;
932 my $r = $self->{r}; 623 my $r = $self->r;
933 if ($r->notes("warnings")) { 624 if ($r->notes("warnings")) {
934 return $self->warningOutput($r->notes("warnings")); 625 return $self->warningOutput($r->notes("warnings"));
935 } else { 626 } else {
936 return ""; 627 return "";
937 } 628 }
938} 629}
939 630
940=back 631=back
941 632
942=head CONDITIONAL PREDICATES 633=cut
943 634
635# ------------------------------------------------------------------------------
636
637=head2 Conditional predicates
638
944Conditional predicate methods are invoked when the 639Conditional predicate methods are invoked when the C<#if> escape sequence is
945C< <!--#if predicate="value"--> > construct is encountered in the template. If a 640encountered in the template. If a method named C<if_predicate> is defined in
946method named C<if_predicate> is defined in here or in a particular subclass, it 641here or in the instantiated subclass, it is invoked.
947is invoked.
948 642
949The following predicates are currently defined: 643The following predicates are currently defined:
950 644
951=over 645=over
952 646
953=item if_can 647=item if_can($function)
954 648
955will return 1 if the current object->can("do $_[1]") 649If a function named $function is present in the current content generator (or
650any superclass), a true value is returned. Otherwise, a false value is returned.
956 651
957=cut 652The implementation in this package uses the method UNIVERSAL->can(function) to
653arrive at the result.
958 654
655A subclass could redefine this method to, for example, "hide" a method from the
656template:
657
658 sub if_can {
659 my ($self, $arg) = @_;
660
661 if ($arg eq "floobar") {
662 return 0;
663 } else {
664 return $self->SUPER::if_can($arg);
665 }
666 }
667
668=cut
669
959sub if_can ($$) { 670sub if_can {
960 my ($self, $arg) = (@_); 671 my ($self, $arg) = @_;
961 672
962 if ($self->can("$arg")) { 673 return $self->can($arg) ? 1 : 0;
963 return 1;
964 } else {
965 return 0;
966 }
967} 674}
968 675
969=item if_loggedin 676=item if_loggedin($arg)
970 677
971Every content generator is logged in unless it overrides this method to say 678If the user is currently logged in, $arg is returned. Otherwise, the inverse of
972otherwise. 679$arg is returned.
973 680
974=cut 681The implementation in this package always returns $arg, since most content
682generators are only reachable when the user is authenticated. It is up to
683classes that can be reached without logging in to override this method and
684provide the correct behavior.
975 685
686This is suboptimal, and may change in the future.
687
688=cut
689
976sub if_loggedin($$) { 690sub if_loggedin {
977 my ($self, $arg) = (@_); 691 my ($self, $arg) = @_;
978 692
979 return $arg; 693 return $arg;
980} 694}
981 695
982=item if_submiterror 696=item if_submiterror($arg)
983 697
984=cut 698If the last form submission generated an error, $arg is returned. Otherwise, the
699inverse of $arg is returned.
985 700
701The implementation in this package checks for the field $self->{submitError} to
702determine if an error condition is present.
703
704If a subclass uses some other method to classify submission results, this method could be
705redefined to handle that variance:
706
707 sub if_submiterror {
708 my ($self, $arg) = @_;
709
710 my $status = $self->{processReturnValue};
711 if ($status != 0) {
712 return $arg;
713 } else {
714 return !$arg;
715 }
716 }
717
718=cut
719
986sub if_submiterror($$) { 720sub if_submiterror {
987 my ($self, $arg) = @_; 721 my ($self, $arg) = @_;
722
988 if (exists $self->{submitError}) { 723 if (exists $self->{submitError}) {
989 return $arg; 724 return $arg;
990 } else { 725 } else {
991 return !$arg; 726 return !$arg;
992 } 727 }
993} 728}
994 729
995=item if_warnings 730=item if_warnings
996 731
997=cut 732If warnings have been emitted while handling this request, $arg is returned.
733Otherwise, the inverse of $arg is returned.
998 734
735The implementation in this package checks for a note in the request named
736"warnings". This is set by the WARN handler in Apache::WeBWorK when a warning is
737handled.
738
739=cut
740
999sub if_warnings($$) { 741sub if_warnings {
1000 my ($self, $arg) = @_; 742 my ($self, $arg) = @_;
1001 return $self->{r}->notes("warnings") ? $arg : !$arg; 743 my $r = $self->r;
744
745 if ($r->notes("warnings")) {
746 return $arg;
747 } else {
748 !$arg;
749 }
1002} 750}
1003 751
1004=back 752=back
1005 753
1006=cut 754=cut
1007 755
756################################################################################
757
758=head1 HTML MACROS
759
760Various routines are defined in this package for rendering common WeBWorK
761idioms.
762
763FIXME: some of these should be moved to WeBWorK::HTML:: modules!
764
765# ------------------------------------------------------------------------------
766
767=head2 Template escape handler macros
768
769These methods are used by implementations of the escape sequence handlers to
770maintain a consistent style.
771
772=over
773
774=item pathMacro($args, @path)
775
776Helper macro for the C<#path> escape sequence: $args is a hash reference
777containing the "style", "image", "text", and "textonly" arguments to the escape.
778@path consists of ordered key-value pairs of the form:
779
780 "Page Name" => URL
781
782If the page should not have a link associated with it, the URL should be left
783empty. Authentication data is added to each URL so you don't have to. A fully-
784formed path line is returned, suitable for returning by a function implementing
785the C<#path> escape.
786
787FIXME: authentication data probably shouldn't be added here any more, now that
788we have systemLink().
789
790=cut
791
792sub pathMacro {
793 my ($self, $args, @path) = @_;
794 my %args = %$args;
795 $args{style} = "text" if $args{textonly};
796
797 my $auth = $self->url_authen_args;
798 my $sep;
799 if ($args{style} eq "image") {
800 $sep = CGI::img({-src=>$args{image}, -alt=>$args{text}});
801 } else {
802 $sep = $args{text};
803 }
804
805 my @result;
806 while (@path) {
807 my $name = shift @path;
808 my $url = shift @path;
809 if ($url and not $args{textonly}) {
810 push @result, CGI::a({-href=>"$url?$auth"}, $name);
811 } else {
812 push @result, $name;
813 }
814 }
815
816 return join($sep, @result), "\n";
817}
818
819=item siblingsMacro(@siblings)
820
821Helper macro for the C<#siblings> escape sequence. @siblings consists of ordered
822key-value pairs of the form:
823
824 "Sibling Name" => URL
825
826If the sibling should not have a link associated with it, the URL should be left
827empty. Authentication data is added to each URL so you don't have to. A fully-
828formed siblings block is returned, suitable for returning by a function
829implementing the C<#siblings> escape.
830
831FIXME: authentication data probably shouldn't be added here any more, now that
832we have systemLink().
833
834=cut
835
836sub siblingsMacro {
837 my ($self, @siblings) = @_;
838
839 my $auth = $self->url_authen_args;
840 my $sep = CGI::br();
841
842 my @result;
843 while (@siblings) {
844 my $name = shift @siblings;
845 my $url = shift @siblings;
846 push @result, $url
847 ? CGI::a({-href=>"$url?$auth"}, $name)
848 : $name;
849 }
850
851 return join($sep, @result) . "\n";
852}
853
854=item navMacro($args, $tail, @links)
855
856Helper macro for the C<#nav> escape sequence: $args is a hash reference
857containing the "style", "imageprefix", "imagesuffix", and "separator" arguments
858to the escape. @siblings consists of ordered tuples of the form:
859
860 "Link Name", URL, ImageBaseName
861
862If the sibling should not have a link associated with it, the URL should be left
863empty. ImageBaseName is placed between the C<imageprefix> and C<imagesuffix>.
864Authentication data is added to each URL so you don't have to. $tail is appended
865to each URL, after the authentication information. A fully-formed nav line is
866returned, suitable for returning by a function implementing the C<#nav> escape.
867
868=cut
869
870sub navMacro {
871 my ($self, $args, $tail, @links) = @_;
872 my $r = $self->r;
873 my $ce = $r->ce;
874 my %args = %$args;
875
876 my $auth = $self->url_authen_args;
877 my $prefix = $ce->{webworkURLs}->{htdocs}."/images";
878
879 my @result;
880 while (@links) {
881 my $name = shift @links;
882 my $url = shift @links;
883 my $img = shift @links;
884 my $html =
885 ($img && $args{style} eq "images")
886 ? CGI::img(
887 {src=>($prefix."/".$img.$args{imagesuffix}),
888 border=>"",
889 alt=>"$name"})
890 : $name;
891 unless($img && !$url) {
892 push @result, $url
893 ? CGI::a({-href=>"$url?$auth$tail"}, $html)
894 : $html;
895 }
896 }
897
898 return join($args{separator}, @result) . "\n";
899}
900
901=back
902
903=cut
904
905# ------------------------------------------------------------------------------
906
907=head2 Parameter management
908
909Methods for formatting request parameters as hidden form fields or query string
910fragments.
911
912=over
913
914=item hidden_fields(@fields)
915
916Return hidden <INPUT> tags for each field mentioned in @fields (or all fields if
917list is empty), taking data from the current request.
918
919=cut
920
921sub hidden_fields {
922 my ($self, @fields) = @_;
923 my $r = $self->r;
924
925 @fields = $r->param unless @fields;
926
927 my $html = "";
928 foreach my $param (@fields) {
929 my @values = $r->param($param);
930 $html .= CGI::hidden($param, @values);
931 }
932 return $html;
933}
934
935=item hidden_authen_fields()
936
937Use hidden_fields to return hidden <INPUT> tags for request fields used in
938authentication.
939
940=cut
941
942sub hidden_authen_fields {
943 my ($self) = @_;
944
945 return $self->hidden_fields("user", "effectiveUser", "key");
946}
947
948=item url_args(@fields)
949
950Return a URL query string (without the leading `?') containing values for each
951field mentioned in @fields, or all fields if list is empty. Data is taken from
952the current request.
953
954=cut
955
956sub url_args {
957 my ($self, @fields) = @_;
958 my $r = $self->r;
959
960 @fields = $r->param unless @fields;
961
962 my @pairs;
963 foreach my $param (@fields) {
964 my @values = $r->param($param);
965 foreach my $value (@values) {
966 push @pairs, uri_escape($param) . "=" . uri_escape($value);
967 }
968 }
969
970 return join("&", @pairs);
971}
972
973=item url_authen_args()
974
975Use url_args to return a URL query string for request fields used in
976authentication.
977
978=cut
979
980sub url_authen_args {
981 my ($self) = @_;
982
983 return $self->url_args("user", "effectiveUser", "key");
984}
985
986=item print_form_data($begin, $middle, $end, $omit)
987
988Return a string containing every request field not matched by the quoted reguar
989expression $omit, placing $begin before each field name, $middle between each
990field name and its value, and $end after each value. Values are taken from the
991current request.
992
993=cut
994
995sub print_form_data {
996 my ($self, $begin, $middle, $end, $qr_omit) = @_;
997 my $r=$self->r;
998 my @form_data = $r->param;
999
1000 my $return_string = "";
1001 foreach my $name (@form_data) {
1002 next if ($qr_omit and $name =~ /$qr_omit/);
1003 my @values = $r->param($name);
1004 foreach my $variable (qw(begin name middle value end)) {
1005 # FIXME: can this loop be moved out of the enclosing loop?
1006 no strict 'refs';
1007 ${$variable} = "" unless defined ${$variable};
1008 }
1009 foreach my $value (@values) {
1010 $return_string .= "$begin$name$middle$value$end";
1011 }
1012 }
1013
1014 return $return_string;
1015}
1016
1017=back
1018
1019=cut
1020
1021# ------------------------------------------------------------------------------
1022
1023=head2 Utilities
1024
1025=over
1026
1027=item systemLink($urlpath, %options)
1028
1029Generate a link to another part of the system. $urlpath is WeBWorK::URLPath
1030object from which the base path will be taken. %options can consist of:
1031
1032=over
1033
1034=item authen
1035
1036Boolen, whether to include authentication information in the resulting URL. If
1037not given, a true value is assumed.
1038
1039=item realUserID
1040
1041If C<authen> is true, the current real user ID is replaced with this value.
1042
1043=item sessionKey
1044
1045If C<authen> is true, the current session key is replaced with this value.
1046
1047=item effectiveUserID
1048
1049If C<authen> is true, the current effective user ID is replaced with this value.
1050
1051=back
1052
1053=cut
1054
1055sub systemLink {
1056 my ($self, $urlpath, %options) = @_;
1057 my $r = $self->r;
1058
1059 my $authen = $options{authen} || 1;
1060
1061 my $url = $r->location . $urlpath->path;
1062
1063 if ($authen) {
1064 my $realUserID = $options{realUserID} || $r->param("user");
1065 my $sessionKey = $options{sessionKey} || $r->param("key");
1066 my $effectiveUserID = $options{effectiveUserID} || $r->param("effectiveUser");
1067
1068 my @params;
1069 defined $realUserID and push @params, "user=$realUserID";
1070 defined $sessionKey and push @params, "key=$sessionKey";
1071 defined $effectiveUserID and push @params, "effectiveUser=$effectiveUserID";
1072
1073 $url .= "?" . join("&", @params) if @params;
1074 }
1075
1076 return $url;
1077}
1078
1079=item nbsp($string)
1080
1081If string consists of only whitespace, the HTML entity C<&nbsp;> is returned.
1082Otherwise $string is returned.
1083
1084=cut
1085
1086sub nbsp {
1087 my $self = shift;
1088 my $str = shift;
1089 ($str =~/\S/) ? $str : '&nbsp;';
1090}
1091
1092=item errorOutput($error, $details)
1093
1094=cut
1095
1096sub errorOutput($$$) {
1097 my ($self, $error, $details) = @_;
1098 return
1099 CGI::h3("Software Error"),
1100 CGI::p(<<EOF),
1101WeBWorK has encountered a software error while attempting to process this
1102problem. It is likely that there is an error in the problem itself. If you are
1103a student, contact your professor to have the error corrected. If you are a
1104professor, please consut the error output below for more informaiton.
1105EOF
1106 # FIXME: this message shouldn't refer the the "problem" since it is for general error reporting
1107 CGI::h3("Error messages"), CGI::p(CGI::tt($error)),
1108 CGI::h3("Error context"), CGI::p(CGI::tt($details));
1109}
1110
1111=item warningOutput($warnings)
1112
1113=cut
1114
1115sub warningOutput($$) {
1116 my ($self, $warnings) = @_;
1117
1118 my @warnings = split m/\n+/, $warnings;
1119
1120 return
1121 CGI::h3("Software Warnings"),
1122 CGI::p(<<EOF),
1123WeBWorK has encountered warnings while attempting to process this problem. It
1124is likely that this indicates an error or ambiguity in the problem itself. If
1125you are a student, contact your professor to have the problem corrected. If you
1126are a professor, please consut the warning output below for more informaiton.
1127EOF
1128 # FIXME: this message shouldn't refer the the "problem" since it is for general warning reporting
1129 CGI::h3("Warning messages"),
1130 CGI::ul(CGI::li(\@warnings));
1131}
1132
1133=back
1134
1135=head1 AUTHOR
1136
1137Written by Dennis Lambe Jr., malsyned (at) math.rochester.edu and Sam Hathaway,
1138sh002i (at) math.rochester.edu.
1139
1140=cut
1141
10081; 11421;
1009
1010__END__
1011
1012=head1 AUTHOR
1013
1014Written by Dennis Lambe Jr., malsyned (at) math.rochester.edu
1015and Sam Hathaway, sh002i (at) math.rochester.edu.
1016
1017=cut

Legend:
Removed from v.1872  
changed lines
  Added in v.1873

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9