source: trunk/lib/OpenGuides.pm @ 547

Last change on this file since 547 was 547, checked in by kake, 17 years ago

Fix bugs with latlong representation.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 26.1 KB
Line 
1package OpenGuides;
2use strict;
3
4use Carp "croak";
5use CGI;
6use CGI::Wiki::Plugin::Diff;
7use CGI::Wiki::Plugin::Locator::UK;
8use OpenGuides::CGI;
9use OpenGuides::Template;
10use OpenGuides::Utils;
11use Time::Piece;
12use URI::Escape;
13
14use vars qw( $VERSION );
15
16$VERSION = '0.43';
17
18=head1 NAME
19
20OpenGuides - A complete web application for managing a collaboratively-written guide to a city or town.
21
22=head1 DESCRIPTION
23
24The OpenGuides software provides the framework for a collaboratively-written
25city guide.  It is similar to a wiki but provides somewhat more structured
26data storage allowing you to annotate wiki pages with information such as
27category, location, and much more.  It provides searching facilities
28including "find me everything within a certain distance of this place".
29Every page includes a link to a machine-readable (RDF) version of the page.
30
31=head1 METHODS
32
33=over
34
35=item B<new>
36
37  my $guide = OpenGuides->new( config => $config );
38
39=cut
40
41sub new {
42    my ($class, %args) = @_;
43    my $self = {};
44    bless $self, $class;
45    my $wiki = OpenGuides::Utils->make_wiki_object( config => $args{config} );
46    $self->{wiki} = $wiki;
47    $self->{config} = $args{config};
48    my $locator = CGI::Wiki::Plugin::Locator::UK->new;
49    $wiki->register_plugin( plugin => $locator );
50    $self->{locator} = $locator;
51    my $differ = CGI::Wiki::Plugin::Diff->new;
52    $wiki->register_plugin( plugin => $differ );
53    $self->{differ} = $differ;
54    return $self;
55}
56
57=item B<wiki>
58
59An accessor, returns the underlying L<CGI::Wiki> object.
60
61=cut
62
63sub wiki {
64    my $self = shift;
65    return $self->{wiki};
66}
67
68=item B<config>
69
70An accessor, returns the underlying L<Config::Tiny> object.
71
72=cut
73
74sub config {
75    my $self = shift;
76    return $self->{config};
77}
78
79=item B<locator>
80
81An accessor, returns the underlying L<CGI::Wiki::Plugin::Locator::UK> object.
82
83=cut
84
85sub locator {
86    my $self = shift;
87    return $self->{locator};
88}
89
90=item B<differ>
91
92An accessor, returns the underlying L<CGI::Wiki::Plugin::Diff> object.
93
94=cut
95
96sub differ {
97    my $self = shift;
98    return $self->{differ};
99}
100
101=item B<display_node>
102
103  # Print node to STDOUT.
104  $guide->display_node(
105                        id      => "Calthorpe Arms",
106                        version => 2,
107                      );
108
109  # Or return output as a string (useful for writing tests).
110  $guide->display_node(
111                        id            => "Calthorpe Arms",
112                        return_output => 1,
113                      );
114
115  # Or return the hash of variables that will be passed to the template
116  # (not including those set additionally by OpenGuides::Template).
117  $guide->display_node(
118                        id             => "Calthorpe Arms",
119                        return_tt_vars => 1,
120                      );
121
122If C<version> is omitted then the latest version will be displayed.
123
124=cut
125
126sub display_node {
127    my ($self, %args) = @_;
128    my $return_output = $args{return_output} || 0;
129    my $version = $args{version};
130    my $id = $args{id} || $self->config->{_}->{home_name};
131    my $wiki = $self->wiki;
132    my $config = $self->config;
133
134    my %tt_vars;
135
136    if ( $id =~ /^(Category|Locale) (.*)$/ ) {
137        my $type = $1;
138        $tt_vars{is_indexable_node} = 1;
139        $tt_vars{index_type} = lc($type);
140        $tt_vars{index_value} = $2;
141        $tt_vars{"rss_".lc($type)."_url"} =
142                           $config->{_}{script_name} . "?action=rss;"
143                           . lc($type) . "=" . lc(CGI->escape($2));
144    }
145
146    my %current_data = $wiki->retrieve_node( $id );
147    my $current_version = $current_data{version};
148    undef $version if ($version && $version == $current_version);
149    my %criteria = ( name => $id );
150    $criteria{version} = $version if $version;#retrieve_node default is current
151
152    my %node_data = $wiki->retrieve_node( %criteria );
153    my $raw = $node_data{content};
154    if ( $raw =~ /^#REDIRECT\s+(.+?)\s*$/ ) {
155        my $redirect = $1;
156        # Strip off enclosing [[ ]] in case this is an extended link.
157        $redirect =~ s/^\[\[//;
158        $redirect =~ s/\]\]\s*$//;
159        # See if this is a valid node, if not then just show the page as-is.
160        if ( $wiki->node_exists($redirect) ) {
161            my $output = $self->redirect_to_node($redirect);
162            return $output if $return_output;
163            print $output;
164            exit 0;
165        }
166    }
167    my $content    = $wiki->format($raw);
168    my $modified   = $node_data{last_modified};
169    my %metadata   = %{$node_data{metadata}};
170
171    my %metadata_vars = OpenGuides::Template->extract_metadata_vars(
172                            wiki     => $wiki,
173                            config   => $config,
174                            metadata => $node_data{metadata} );
175
176    %tt_vars = (
177                 %tt_vars,
178                 %metadata_vars,
179                 content       => $content,
180                 last_modified => $modified,
181                 version       => $node_data{version},
182                 node          => $id,
183                 language      => $config->{_}->{default_language},
184               );
185
186
187    # We've undef'ed $version above if this is the current version.
188    $tt_vars{current} = 1 unless $version;
189
190    if ($id eq "RecentChanges") {
191        my $minor_edits = $self->get_cookie( "show_minor_edits_in_rc" );
192        my %recent_changes;
193        my $q = CGI->new;
194        my $since = $q->param("since");
195        if ( $since ) {
196            $tt_vars{since} = $since;
197            my $t = localtime($since); # overloaded by Time::Piece
198            $tt_vars{since_string} = $t->strftime;
199            my %criteria = ( since => $since );
200            $criteria{metadata_was} = { edit_type => "Normal edit" }
201              unless $minor_edits;
202            my @rc = $self->{wiki}->list_recent_changes( %criteria );
203
204            @rc = map {
205                {
206                  name        => CGI->escapeHTML($_->{name}),
207                  last_modified => CGI->escapeHTML($_->{last_modified}),
208                  version     => CGI->escapeHTML($_->{version}),
209                  comment     => CGI->escapeHTML($_->{metadata}{comment}[0]),
210                  username    => CGI->escapeHTML($_->{metadata}{username}[0]),
211                  host        => CGI->escapeHTML($_->{metadata}{host}[0]),
212                  username_param => CGI->escape($_->{metadata}{username}[0]),
213                  edit_type   => CGI->escapeHTML($_->{metadata}{edit_type}[0]),
214                  url         => "$config->{_}->{script_name}?"
215          . CGI->escape($wiki->formatter->node_name_to_node_param($_->{name})),
216                }
217                       } @rc;
218            if ( scalar @rc ) {
219                $recent_changes{since} = \@rc;
220            }
221        } else {
222            for my $days ( [0, 1], [1, 7], [7, 14], [14, 30] ) {
223                my %criteria = ( between_days => $days );
224                $criteria{metadata_was} = { edit_type => "Normal edit" }
225                  unless $minor_edits;
226                my @rc = $self->{wiki}->list_recent_changes( %criteria );
227
228                @rc = map {
229                {
230                  name        => CGI->escapeHTML($_->{name}),
231                  last_modified => CGI->escapeHTML($_->{last_modified}),
232                  version     => CGI->escapeHTML($_->{version}),
233                  comment     => CGI->escapeHTML($_->{metadata}{comment}[0]),
234                  username    => CGI->escapeHTML($_->{metadata}{username}[0]),
235                  host        => CGI->escapeHTML($_->{metadata}{host}[0]),
236                  username_param => CGI->escape($_->{metadata}{username}[0]),
237                  edit_type   => CGI->escapeHTML($_->{metadata}{edit_type}[0]),
238                  url         => "$config->{_}->{script_name}?"
239          . CGI->escape($wiki->formatter->node_name_to_node_param($_->{name})),
240                }
241                           } @rc;
242                if ( scalar @rc ) {
243                    $recent_changes{$days->[1]} = \@rc;
244                }
245            }
246        }
247        $tt_vars{recent_changes} = \%recent_changes;
248        my %processing_args = (
249                                id            => $id,
250                                template      => "recent_changes.tt",
251                                tt_vars       => \%tt_vars,
252                               );
253        if ( !$since && $self->get_cookie("track_recent_changes_views") ) {
254            my $cookie =
255               OpenGuides::CGI->make_recent_changes_cookie(config => $config );
256            $processing_args{cookies} = $cookie;
257            $tt_vars{last_viewed} = OpenGuides::CGI->get_last_recent_changes_visit_from_cookie( config => $config );
258        }
259        return %tt_vars if $args{return_tt_vars};
260        my $output = $self->process_template( %processing_args );
261        return $output if $return_output;
262        print $output;
263    } elsif ( $id eq $self->config->{_}->{home_name} ) {
264        my @recent = $wiki->list_recent_changes(
265            last_n_changes => 10,
266            metadata_was   => { edit_type => "Normal edit" },
267        );
268        @recent = map { {name          => CGI->escapeHTML($_->{name}),
269                         last_modified => CGI->escapeHTML($_->{last_modified}),
270                         comment       => CGI->escapeHTML($_->{metadata}{comment}[0]),
271                         username      => CGI->escapeHTML($_->{metadata}{username}[0]),
272                         url           => "$config->{_}->{script_name}?"
273          . CGI->escape($wiki->formatter->node_name_to_node_param($_->{name})) }
274                       } @recent;
275        $tt_vars{recent_changes} = \@recent;
276        return %tt_vars if $args{return_tt_vars};
277        my $output = $self->process_template(
278                                              id            => $id,
279                                              template      => "home_node.tt",
280                                              tt_vars       => \%tt_vars,
281                                            );
282        return $output if $return_output;
283        print $output;
284    } else {
285        return %tt_vars if $args{return_tt_vars};
286        my $output = $self->process_template(
287                                              id            => $id,
288                                              template      => "node.tt",
289                                              tt_vars       => \%tt_vars,
290                                            );
291        return $output if $return_output;
292        print $output;
293    }
294}
295
296=item B<display_diffs>
297
298  $guide->display_diffs(
299                         id            => "Home Page",
300                         version       => 6,
301                         other_version => 5,
302                       );
303
304  # Or return output as a string (useful for writing tests).
305  my $output = $guide->display_diffs(
306                                      id            => "Home Page",
307                                      version       => 6,
308                                      other_version => 5,
309                                      return_output => 1,
310                                    );
311
312  # Or return the hash of variables that will be passed to the template
313  # (not including those set additionally by OpenGuides::Template).
314  my %vars = $guide->display_diffs(
315                                    id             => "Home Page",
316                                    version        => 6,
317                                    other_version  => 5,
318                                    return_tt_vars => 1,
319                                  );
320
321=cut
322
323sub display_diffs {
324    my ($self, %args) = @_;
325    my %diff_vars = $self->differ->differences(
326                                        node          => $args{id},
327                                        left_version  => $args{version},
328                                        right_version => $args{other_version},
329                                              );
330    $diff_vars{not_deletable} = 1;
331    $diff_vars{not_editable} = 1;
332    $diff_vars{deter_robots} = 1;
333    return %diff_vars if $args{return_tt_vars};
334    my $output = $self->process_template(
335                                          id       => $args{id},
336                                          template => "differences.tt",
337                                          tt_vars  => \%diff_vars
338                                        );
339    return $output if $args{return_output};
340    print $output;
341}
342
343=item B<find_within_distance>
344
345  $guide->find_within_distance(
346                                id => $node,
347                                metres => $q->param("distance_in_metres")
348                              );
349
350=cut
351
352sub find_within_distance {
353    my ($self, %args) = @_;
354    my $node = $args{id};
355    my $metres = $args{metres};
356    my %data = $self->wiki->retrieve_node( $node );
357    my $lat = $data{metadata}{latitude}[0];
358    my $long = $data{metadata}{longitude}[0];
359    my $script_url = $self->config->{_}{script_url};
360    print CGI->redirect( $script_url . "supersearch.cgi?lat=$lat;long=$long;distance_in_metres=$metres" );
361}
362
363=item B<show_index>
364
365  $guide->show_index(
366                      type   => "category",
367                      value  => "pubs",
368                    );
369
370  # RDF version.
371  $guide->show_index(
372                      type   => "locale",
373                      value  => "Holborn",
374                      format => "rdf",
375                    );
376
377  # Or return output as a string (useful for writing tests).
378  $guide->show_index(
379                      type          => "category",
380                      value         => "pubs",
381                      return_output => 1,
382                    );
383
384=cut
385
386sub show_index {
387    my ($self, %args) = @_;
388    my $wiki = $self->wiki;
389    my $formatter = $wiki->formatter;
390    my %tt_vars;
391    my @selnodes;
392
393    if ( $args{type} and $args{value} ) {
394        if ( $args{type} eq "fuzzy_title_match" ) {
395            my %finds = $wiki->fuzzy_title_match( $args{value} );
396            @selnodes = sort { $finds{$a} <=> $finds{$b} } keys %finds;
397            $tt_vars{criterion} = {
398                type  => $args{type},  # for RDF version
399                value => $args{value}, # for RDF version
400                name  => CGI->escapeHTML("Fuzzy Title Match on '$args{value}'"),
401                not_editable => 1
402            };
403        } else {
404            @selnodes = $wiki->list_nodes_by_metadata(
405                metadata_type  => $args{type},
406                metadata_value => $args{value},
407                ignore_case    => 1,
408            );
409            my $name = ucfirst($args{type}) . " $args{value}" ;
410            my $url = $self->config->{_}->{script_name}
411                      . "?"
412                      . ucfirst( $args{type} )
413                      . "_"
414                      . uri_escape(
415                              $formatter->node_name_to_node_param($args{value})
416                                  );
417            $tt_vars{criterion} = {
418                type  => $args{type},
419                value => $args{value}, # for RDF version
420                name  => CGI->escapeHTML( $name ),
421                url   => $url,
422                not_editable => 1
423            };
424        }
425    } else {
426        @selnodes = $wiki->list_all_nodes();
427    }
428
429    my @nodes = map { { name      => $_,
430                        node_data => { $wiki->retrieve_node( name => $_ ) },
431                        param     => $formatter->node_name_to_node_param($_) }
432                    } sort @selnodes;
433
434    $tt_vars{nodes} = \@nodes;
435
436    my ($template, %conf);
437
438    if ( $args{format} and $args{format} eq "rdf" ) {
439        $template = "rdf_index.tt";
440        $conf{content_type} = "text/plain";
441    } else {
442        $template = "site_index.tt";
443    }
444
445    %conf = (
446              %conf,
447              node        => "$args{type} index", # KLUDGE
448              template    => $template,
449              tt_vars     => \%tt_vars,
450    );
451
452    my $output = $self->process_template( %conf );
453    return $output if $args{return_output};
454    print $output;
455}
456
457=item B<list_all_versions>
458
459  $guide->list_all_versions ( id => "Home Page" );
460
461  # Or return output as a string (useful for writing tests).
462  $guide->list_all_versions (
463                              id            => "Home Page",
464                              return_output => 1,
465                            );
466
467  # Or return the hash of variables that will be passed to the template
468  # (not including those set additionally by OpenGuides::Template).
469  $guide->list_all_versions (
470                              id             => "Home Page",
471                              return_tt_vars => 1,
472                            );
473
474=cut
475
476sub list_all_versions {
477    my ($self, %args) = @_;
478    my $return_output = $args{return_output} || 0;
479    my $node = $args{id};
480    my %curr_data = $self->wiki->retrieve_node($node);
481    my $curr_version = $curr_data{version};
482    croak "This is the first version" unless $curr_version > 1;
483    my @history;
484    for my $version ( 1 .. $curr_version ) {
485        my %node_data = $self->wiki->retrieve_node( name    => $node,
486                                                    version => $version );
487        # $node_data{version} will be zero if this version was deleted.
488        push @history, {
489            version  => CGI->escapeHTML( $version ),
490            modified => CGI->escapeHTML( $node_data{last_modified} ),
491            username => CGI->escapeHTML( $node_data{metadata}{username}[0] ),
492            comment  => CGI->escapeHTML( $node_data{metadata}{comment}[0] ),
493                       } if $node_data{version};
494    }
495    @history = reverse @history;
496    my %tt_vars = ( node          => $node,
497                    version       => $curr_version,
498                    not_deletable => 1,
499                    not_editable  => 1,
500                    deter_robots  => 1,
501                    history       => \@history );
502    return %tt_vars if $args{return_tt_vars};
503    my $output = $self->process_template(
504                                          id       => $node,
505                                          template => "node_history.tt",
506                                          tt_vars  => \%tt_vars,
507                                        );
508    return $output if $return_output;
509    print $output;
510}
511
512=item B<commit_node>
513
514  $guide->commit_node(
515                       id      => $node,
516                       cgi_obj => $q,
517                     );
518
519As with other methods, parameters C<return_tt_vars> and
520C<return_output> can be used to return these things instead of
521printing the output to STDOUT.
522
523=cut
524
525sub commit_node {
526    my ($self, %args) = @_;
527    my $node = $args{id};
528    my $q = $args{cgi_obj};
529    my $return_output = $args{return_output};
530    my $wiki = $self->wiki;
531    my $config = $self->config;
532
533    my $content  = $q->param("content");
534    $content =~ s/\r\n/\n/gs;
535    my $checksum = $q->param("checksum");
536
537    my %metadata = OpenGuides::Template->extract_metadata_vars(
538        wiki    => $wiki,
539        config  => $config,
540        cgi_obj => $q
541    );
542
543    $metadata{opening_hours_text} = $q->param("hours_text") || "";
544
545    # Pick out the unmunged versions of lat/long if they're set.
546    # (If they're not, it means they weren't munged in the first place.)
547    $metadata{latitude} = delete $metadata{latitude_unmunged}
548        if $metadata{latitude_unmunged};
549    $metadata{longitude} = delete $metadata{longitude_unmunged}
550        if $metadata{longitude_unmunged};
551
552    # Check to make sure all the indexable nodes are created
553    foreach my $type (qw(Category Locale)) {
554        my $lctype = lc($type);
555        foreach my $index (@{$metadata{$lctype}}) {
556            $index =~ s/(.*)/\u$1/;
557            my $node = $type . " " . $index;
558            # Uppercase the node name before checking for existence
559            $node =~ s/ (\S+)/ \u$1/g;
560            unless ( $wiki->node_exists($node) ) {
561                my $category = $type eq "Category" ? "Category" : "Locales";
562                $wiki->write_node( $node,
563                                   "\@INDEX_LINK [[$node]]",
564                                   undef,
565                                   { username => "Auto Create",
566                                     comment  => "Auto created $lctype stub page",
567                                     category => $category
568                                   }
569                );
570            }
571        }
572    }
573       
574    foreach my $var ( qw( username comment edit_type ) ) {
575        $metadata{$var} = $q->param($var) || "";
576    }
577    $metadata{host} = $ENV{REMOTE_ADDR};
578
579    # CGI::Wiki::Plugin::RSS::ModWiki wants "major_change" to be set.
580    $metadata{major_change} = ( $metadata{edit_type} eq "Normal edit" )
581                            ? 1
582                            : 0;
583
584    my $written = $wiki->write_node($node, $content, $checksum, \%metadata );
585
586    if ($written) {
587        my $output = $self->redirect_to_node($node);
588        return $output if $return_output;
589        print $output;
590    } else {
591        my %node_data = $wiki->retrieve_node($node);
592        my %tt_vars = ( checksum       => $node_data{checksum},
593                        new_content    => $content,
594                        stored_content => $node_data{content} );
595        foreach my $mdvar ( keys %metadata ) {
596            if ($mdvar eq "locales") {
597                $tt_vars{"stored_$mdvar"} = $node_data{metadata}{locale};
598                $tt_vars{"new_$mdvar"}    = $metadata{locale};
599            } elsif ($mdvar eq "categories") {
600                $tt_vars{"stored_$mdvar"} = $node_data{metadata}{category};
601                $tt_vars{"new_$mdvar"}    = $metadata{category};
602            } elsif ($mdvar eq "username" or $mdvar eq "comment"
603                      or $mdvar eq "edit_type" ) {
604                $tt_vars{$mdvar} = $metadata{$mdvar};
605            } else {
606                $tt_vars{"stored_$mdvar"} = $node_data{metadata}{$mdvar}[0];
607                $tt_vars{"new_$mdvar"}    = $metadata{$mdvar};
608            }
609        }
610        return %tt_vars if $args{return_tt_vars};
611        my $output = $self->process_template(
612                                              id       => $node,
613                                              template => "edit_conflict.tt",
614                                              tt_vars  => \%tt_vars,
615                                            );
616        return $output if $args{return_output};
617        print $output;
618    }
619}
620
621
622=item B<delete_node>
623
624  $guide->delete_node(
625                       id       => "FAQ",
626                       version  => 15,
627                       password => "beer",
628                     );
629
630C<version> is optional - if it isn't supplied then all versions of the
631node will be deleted; in other words the node will be entirely
632removed.
633
634If C<password> is not supplied then a form for entering the password
635will be displayed.
636
637=cut
638
639sub delete_node {
640    my ($self, %args) = @_;
641    my $node = $args{id} or croak "No node ID supplied for deletion";
642
643    my %tt_vars = (
644                    not_editable  => 1,
645                    not_deletable => 1,
646                    deter_robots  => 1,
647                  );
648    $tt_vars{delete_version} = $args{version} || "";
649
650    my $password = $args{password};
651
652    if ($password) {
653        if ($password ne $self->config->{_}->{admin_pass}) {
654            print $self->process_template(
655                                     id       => $node,
656                                     template => "delete_password_wrong.tt",
657                                     tt_vars  => \%tt_vars,
658                                   );
659        } else {
660            $self->wiki->delete_node(
661                                      name    => $node,
662                                      version => $args{version},
663                                    );
664            # Check whether any versions of this node remain.
665            my %check = $self->wiki->retrieve_node( name => $node );
666            $tt_vars{other_versions_remain} = 1 if $check{version};
667            print $self->process_template(
668                                     id       => $node,
669                                     template => "delete_done.tt",
670                                     tt_vars  => \%tt_vars,
671                                   );
672        }
673    } else {
674        print $self->process_template(
675                                 id       => $node,
676                                 template => "delete_confirm.tt",
677                                 tt_vars  => \%tt_vars,
678                               );
679    }
680}
681
682sub process_template {
683    my ($self, %args) = @_;
684    my %output_conf = ( wiki     => $self->wiki,
685                        config   => $self->config,
686                        node     => $args{id},
687                        template => $args{template},
688                        vars     => $args{tt_vars},
689                        cookies  => $args{cookies},
690    );
691    if ( $args{content_type} ) {
692        $output_conf{content_type} = "";
693        my $output = "Content-Type: $args{content_type}\n\n"
694                     . OpenGuides::Template->output( %output_conf );
695    } else {
696        return OpenGuides::Template->output( %output_conf );
697    }
698}
699
700sub redirect_to_node {
701    my ($self, $node) = @_;
702    my $script_url = $self->config->{_}->{script_url};
703    my $script_name = $self->config->{_}->{script_name};
704    my $formatter = $self->wiki->formatter;
705    my $param = $formatter->node_name_to_node_param( $node );
706    return CGI->redirect( "$script_url$script_name?$param" );
707}
708
709sub get_cookie {
710    my $self = shift;
711    my $config = $self->config;
712    my $pref_name = shift or return "";
713    my %cookie_data = OpenGuides::CGI->get_prefs_from_cookie(config=>$config);
714    return $cookie_data{$pref_name};
715}
716
717
718=back
719
720=head1 BUGS AND CAVEATS
721
722At the moment, the location data uses a United-Kingdom-specific module,
723so the location features might not work so well outside the UK.
724
725=head1 SEE ALSO
726
727=over 4
728
729=item * L<http://london.openguides.org/|The Open Guide to London>, the first and biggest OpenGuides site.
730
731=item * L<http://openguides.org/|The OpenGuides website>, with a list of all live OpenGuides installs.
732
733=item * L<CGI::Wiki>, the Wiki toolkit which does the heavy lifting for OpenGuides
734
735=back
736
737=head1 FEEDBACK
738
739If you have a question, a bug report, or a patch, or you're interested
740in joining the development team, please contact openguides-dev@openguides.org
741(moderated mailing list, will reach all current developers but you'll have
742to wait for your post to be approved) or kake@earth.li (a real person who
743may take a little while to reply to your mail if she's busy).
744
745=head1 AUTHOR
746
747The OpenGuides Project (openguides-dev@openguides.org)
748
749=head1 COPYRIGHT
750
751     Copyright (C) 2003-4 The OpenGuides Project.  All Rights Reserved.
752
753The OpenGuides distribution is free software; you can redistribute it
754and/or modify it under the same terms as Perl itself.
755
756=head1 CREDITS
757
758Programming by Dominic Hargreaves, Earle Martin, Kake Pugh, and Ivor
759Williams.  Testing and bug reporting by Billy Abbott, Jody Belka,
760Kerry Bosworth, Simon Cozens, Cal Henderson, Steve Jolly, and Bob
761Walker (among others).  Much of the Module::Build stuff copied from
762the Siesta project L<http://siesta.unixbeard.net/>
763
764=cut
765
7661;
Note: See TracBrowser for help on using the repository browser.