source: trunk/lib/OpenGuides.pm @ 524

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

Replace find_within_distance with redirect to supersearch.cgi

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