source: trunk/lib/OpenGuides.pm @ 473

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

Changes since

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