]> ruin.nu Git - ndwebbie.git/commitdiff
Implemented a basic Wiki.
authorMichael Andreen <harv@ruin.nu>
Tue, 12 Aug 2008 15:26:18 +0000 (17:26 +0200)
committerMichael Andreen <harv@ruin.nu>
Tue, 12 Aug 2008 15:26:18 +0000 (17:26 +0200)
Wiki controller handles all the wiki action. Redirect /index to /wiki to have an easily-editable front page.

database/wiki-access.sql [new file with mode: 0644]
database/wiki.sql [new file with mode: 0644]
lib/NDWeb/Controller/Root.pm
lib/NDWeb/Controller/Wiki.pm [new file with mode: 0644]
root/lib/site/html.tt2
root/lib/site/leftbar.tt2
root/src/wiki/edit.tt2 [new file with mode: 0644]
root/src/wiki/history.tt2 [new file with mode: 0644]
root/src/wiki/page.tt2 [new file with mode: 0644]
t/controller_Wiki.t [new file with mode: 0644]

diff --git a/database/wiki-access.sql b/database/wiki-access.sql
new file mode 100644 (file)
index 0000000..ff63a0a
--- /dev/null
@@ -0,0 +1,20 @@
+INSERT INTO wiki_namespace_access (namespace,gid,edit,post,moderate) VALUES ('',-1,FALSE,FALSE,FALSE);
+INSERT INTO wiki_namespace_access (namespace,gid,edit,post,moderate) VALUES ('',11,TRUE,TRUE,FALSE);
+INSERT INTO wiki_namespace_access (namespace,gid,edit,post,moderate) VALUES ('',1,TRUE,TRUE,TRUE);
+INSERT INTO wiki_namespace_access (namespace,gid,edit,post,moderate) VALUES ('',3,TRUE,TRUE,TRUE);
+
+INSERT INTO wiki_namespace_access (namespace,gid,edit,post,moderate) VALUES ('Info',-1,FALSE,FALSE,FALSE);
+INSERT INTO wiki_namespace_access (namespace,gid,edit,post,moderate) VALUES ('Info',1,TRUE,TRUE,TRUE);
+INSERT INTO wiki_namespace_access (namespace,gid,edit,post,moderate) VALUES ('Info',3,TRUE,TRUE,TRUE);
+
+INSERT INTO wiki_namespace_access (namespace,gid,edit,post,moderate) VALUES ('Members',2,TRUE,TRUE,FALSE);
+INSERT INTO wiki_namespace_access (namespace,gid,edit,post,moderate) VALUES ('Members',1,TRUE,TRUE,TRUE);
+INSERT INTO wiki_namespace_access (namespace,gid,edit,post,moderate) VALUES ('Members',3,TRUE,TRUE,TRUE);
+
+INSERT INTO wiki_namespace_access (namespace,gid,edit,post,moderate) VALUES ('HC',1,TRUE,TRUE,TRUE);
+INSERT INTO wiki_namespace_access (namespace,gid,edit,post,moderate) VALUES ('HC',3,TRUE,TRUE,TRUE);
+
+INSERT INTO wiki_namespace_access (namespace,gid,edit,post,moderate) VALUES ('Tech',-1,FALSE,FALSE,FALSE);
+INSERT INTO wiki_namespace_access (namespace,gid,edit,post,moderate) VALUES ('Tech',2,TRUE,TRUE,FALSE);
+INSERT INTO wiki_namespace_access (namespace,gid,edit,post,moderate) VALUES ('Tech',1,TRUE,TRUE,TRUE);
+INSERT INTO wiki_namespace_access (namespace,gid,edit,post,moderate) VALUES ('Tech',3,TRUE,TRUE,TRUE);
diff --git a/database/wiki.sql b/database/wiki.sql
new file mode 100644 (file)
index 0000000..1d00c01
--- /dev/null
@@ -0,0 +1,73 @@
+CREATE TABLE wiki_namespaces (
+       namespace VARCHAR(16) PRIMARY KEY
+);
+
+CREATE TABLE wiki_pages (
+       wpid SERIAL PRIMARY KEY,
+       name VARCHAR(255) NOT NULL,
+       namespace TExt NOT NULL REFERENCES wiki_namespaces(namespace) DEFAULT '',
+       textsearch tsvector NOT NULL DEFAULT to_tsvector(''),
+       UNIQUE(namespace,name)
+);
+
+CREATE INDEX wiki_pages_textsearch_index ON wiki_pages USING gin (textsearch);
+
+CREATE TABLE wiki_page_revisions (
+       wpid INTEGER REFERENCES wiki_pages(wpid),
+       wprev SERIAL NOT NULL PRIMARY KEY,
+       parent INTEGER REFERENCES wiki_page_revisions(wprev),
+       text TEXT NOT NULL,
+       comment TEXT NOT NULL,
+       time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
+       uid INTEGER REFERENCES users(uid)
+);
+
+ALTER TABLE wiki_pages ADD COLUMN wprev INTEGER REFERENCES wiki_page_revisions(wprev);
+
+CREATE OR REPLACE FUNCTION update_wiki_page() RETURNS trigger
+AS $$
+DECLARE
+       rec RECORD;
+BEGIN
+       SELECT setweight(to_tsvector(wpr.text), 'D') AS ts
+               INTO STRICT rec
+               FROM wiki_page_revisions wpr
+               WHERE NEW.wprev = wpr.wprev;
+       NEW.textsearch := rec.ts
+               || setweight(to_tsvector(NEW.namespace || ':' || NEW.name), 'A');
+       return NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER update_wiki_page
+       BEFORE UPDATE ON wiki_pages
+       FOR EACH ROW
+       EXECUTE PROCEDURE update_wiki_page();
+
+CREATE TABLE wiki_namespace_access (
+       namespace TEXT NOT NULL REFERENCES wiki_namespaces(namespace),
+       gid INTEGER NOT NULL REFERENCES groups(gid),
+       edit BOOL NOT NULL DEFAULT FALSE,
+       post BOOL NOT NULL DEFAULT FALSE,
+       moderate BOOL NOT NULL DEFAULT FALSE,
+       PRIMARY KEY(gid,namespace)
+);
+
+CREATE TABLE wiki_page_access (
+       wpid INTEGER NOT NULL REFERENCES wiki_pages(wpid),
+       uid INTEGER NOT NULL REFERENCES users(uid),
+       edit BOOL NOT NULL DEFAULT FALSE,
+       moderate BOOL NOT NULL DEFAULT FALSE,
+       PRIMARY KEY(uid,wpid)
+);
+
+INSERT INTO wiki_namespaces VALUES ('');
+INSERT INTO wiki_namespaces VALUES ('Members');
+INSERT INTO wiki_namespaces VALUES ('HC');
+INSERT INTO wiki_namespaces VALUES ('Tech');
+INSERT INTO wiki_namespaces VALUES ('Info');
+
+INSERT INTO wiki_pages (name,namespace) VALUES('Main','Info');
+INSERT INTO wiki_page_revisions (wpid,text,comment,uid) VALUES(1,'Welcome to the main page!', 'First revision', 1);
+UPDATE wiki_pages set wprev = 1 WHERE wpid = 1;
+
index 2b16db2b71cc6435af34a4be89360eda88796963..3eaaf0d009b254d61892ddc9c8b84eb7de0839d5 100644 (file)
@@ -32,6 +32,8 @@ NDWeb::Controller::Root - Root Controller for NDWeb
 
 sub index : Local Path Args(0) {
        my ( $self, $c ) = @_;
+
+       $c->res->redirect($c->uri_for('/wiki'));
 }
 
 sub default : Path {
diff --git a/lib/NDWeb/Controller/Wiki.pm b/lib/NDWeb/Controller/Wiki.pm
new file mode 100644 (file)
index 0000000..d15c609
--- /dev/null
@@ -0,0 +1,222 @@
+package NDWeb::Controller::Wiki;
+
+use strict;
+use warnings;
+use parent 'Catalyst::Controller';
+
+use Text::MediawikiFormat prefix => '/wiki/';
+
+=head1 NAME
+
+NDWeb::Controller::Wiki - Catalyst Controller
+
+=head1 DESCRIPTION
+
+Catalyst Controller.
+
+=head1 METHODS
+
+=cut
+
+
+=head2 index
+
+=cut
+
+sub auto : Priate {
+       my ( $self, $c ) = @_;
+
+       $c->stash(wikiformat => \&wikiformat);
+}
+
+sub index :Path :Args(0) {
+       my ( $self, $c ) = @_;
+
+       push @{$c->req->captures}, ('Info','Main');
+       $c->forward('page');
+       $c->stash(template => 'wiki/page.tt2');
+}
+
+sub page : LocalRegex(^(?:([A-Z]\w*)(?::|%3A))?([A-Z]\w*)$) {
+       my ( $self, $c ) = @_;
+       my $dbh = $c->model;
+
+       $c->forward('findPage');
+       $c->acl_access_denied('test',$c->action,'No edit access for this page')
+               if defined $c->stash->{page}->{view} && !$c->stash->{page}->{view};
+       $c->forward('loadText');
+
+       unless ($c->stash->{page}->{wpid}){
+               $c->stash->{page}->{namespace} = $c->req->captures->[0];
+               $c->stash->{page}->{name} = $c->req->captures->[1];
+               $c->stash->{page}->{fullname} = ($c->stash->{page}->{namespace} ? $c->stash->{page}->{namespace}.':' : '')
+                       . $c->stash->{page}->{name};
+               $c->stash->{page}->{post} = $dbh->selectrow_array(q{SELECT post
+                               FROM wiki_namespace_access
+                               WHERE namespace = COALESCE($1,'') AND post AND gid IN (SELECT groups($2))
+                       },undef,$c->stash->{page}->{namespace}, $c->stash->{UID});
+       }
+       $c->stash(title => $c->stash->{page}->{fullname});
+}
+
+sub edit : LocalRegex(^edit/(?:([A-Z]\w*)(?::|%3A))?([A-Z]\w*)$) {
+       my ( $self, $c ) = @_;
+       my $dbh = $c->model;
+
+       $c->forward('findPage');
+       $c->acl_access_denied('test',$c->action,'No edit access for this page')
+               if defined $c->stash->{page}->{edit} && !$c->stash->{page}->{edit};
+       $c->forward('loadText');
+       $c->forward('findNamespaces');
+
+       unless ($c->stash->{page}->{wpid}){
+               $c->acl_access_denied('test',$c->action,'No edit access for this page')
+                       unless @{$c->stash->{namespaces}};
+               $c->stash->{page}->{namespace} = $c->req->captures->[0];
+               $c->stash->{page}->{name} = $c->req->captures->[1];
+       }
+}
+
+sub history : LocalRegex(^history/(?:([A-Z]\w*)(?::|%3A))?([A-Z]\w*)$) {
+       my ( $self, $c ) = @_;
+       my $dbh = $c->model;
+
+       $c->forward('findPage');
+
+       my $query = $dbh->prepare(q{SELECT wprev,time,username,comment
+               FROM wiki_page_revisions JOIN users u USING (uid)
+               WHERE wpid = $1
+               ORDER BY time DESC
+               });
+       $query->execute($c->stash->{page}->{wpid});
+       $c->stash(revisions => $query->fetchall_arrayref({}) );
+       $c->stash(title => 'History for ' . $c->stash->{page}->{fullname});
+}
+
+sub postedit : Local {
+       my ( $self, $c, $p ) = @_;
+       my $dbh = $c->model;
+
+       eval {
+               $dbh->begin_work;
+
+               my $wpid = $c->req->param('wpid');
+               if ( $wpid eq 'new'){
+                       my $namespace = $dbh->selectrow_array(q{SELECT namespace
+                               FROM wiki_namespace_access
+                               WHERE namespace = $1 AND post AND gid IN (SELECT groups($2))
+                       },undef,$c->req->param('namespace'), $c->stash->{UID});
+
+                       my $query = $dbh->prepare(q{INSERT INTO wiki_pages (namespace,name) VALUES($1,$2) RETURNING wpid});
+                       $query->execute($namespace,$c->req->param('name'));
+                       $wpid = $query->fetchrow;
+               }
+               $c->forward('findPage',[$wpid]);
+               $c->acl_access_denied('test',$c->action,'No edit access for this page')
+                       if defined $c->stash->{page}->{edit} && !$c->stash->{page}->{edit};
+
+               my $query = $dbh->prepare(q{INSERT INTO wiki_page_revisions
+                       (wpid,parent,text,comment,uid) VALUES($1,$2,$3,$4,$5)
+                       RETURNING wprev
+                       });
+               $c->req->params->{parent}||= undef;
+               $query->execute($wpid,$c->req->param('parent'),$c->req->param('text')
+                       ,$c->req->param('comment'),$c->stash->{UID});
+               my $rev = $query->fetchrow;
+               $dbh->do(q{UPDATE wiki_pages SET wprev = $1 WHERE wpid = $2}
+                       ,undef,$rev,$wpid);
+
+               $dbh->commit;
+               $c->res->redirect($c->uri_for($c->stash->{page}->{fullname}));
+               return;
+       } if ($c->req->param('cmd') eq 'Submit');
+
+       if ($@){
+               if ($@ =~ /duplicate key value violates unique constraint "wiki_pages_namespace_key"/){
+                       $c->stash(error => "Page does already exist");
+               }elsif ($@ =~ /value too long for type character varying\(255\)/){
+                       $c->stash(error => 'The name is too long, keep it to max 255 characters');
+               }else{
+                       $c->stash(error => $@);
+               }
+               $dbh->rollback;
+       }
+
+       $c->forward('findPage') if $p;
+       $c->forward('findNamespaces');
+
+       $c->stash->{page}->{namespace} = $c->req->param('namespace');
+       $c->stash->{page}->{name} = $c->req->param('name');
+
+       $c->stash(text => $c->req->param('text'));
+       $c->stash(template => 'wiki/edit.tt2');
+}
+
+sub findPage : Private {
+       my ( $self, $c, $p ) = @_;
+       my $dbh = $c->model;
+
+       my @arguments = ($c->stash->{UID});
+       my $where;
+       if ($p){
+               $where =  q{AND wpid = $2};
+               push @arguments, $p;
+       }else{
+               $where = q{AND (namespace = COALESCE($2,'') AND name = $3)};
+               push @arguments, @{$c->req->captures};
+       }
+
+       my $query = q{SELECT wpid,namespace,name,wprev
+               ,(CASE WHEN namespace <> '' THEN namespace || ':' ELSE '' END) || name AS fullname
+               ,bool_or(COALESCE(wpa.edit,wna.edit)) AS edit
+               ,bool_or(wna.post) AS post
+               ,bool_or(wpa.moderate OR wna.moderate) AS moderate
+               ,bool_or(wpa.wpid IS NOT NULL OR wna.namespace IS NOT NULL) AS view
+               FROM wiki_pages wp
+                       LEFT OUTER JOIN (SELECT * FROM wiki_namespace_access
+                               WHERE gid IN (SELECT groups($1))) wna USING (namespace)
+                       LEFT OUTER JOIN (SELECT * FROM wiki_page_access
+                               WHERE uid =  $1) wpa USING (wpid)
+               WHERE TRUE
+       } . $where . q{ GROUP BY wpid,namespace,name,wprev};
+       $query = $dbh->prepare($query);
+       $query->execute(@arguments);
+
+       my $page = $query->fetchrow_hashref;
+       $c->stash(page => $page);
+}
+
+sub loadText : Private {
+       my ( $self, $c, $p ) = @_;
+       my $dbh = $c->model;
+
+       my $text = $dbh->selectrow_array(q{SELECT text
+               FROM wiki_page_revisions WHERE wprev = $1
+               },undef,$c->stash->{page}->{wprev});
+       $c->stash(text => $text);
+}
+
+sub findNamespaces : Private {
+       my ( $self, $c, $p ) = @_;
+       my $dbh = $c->model;
+
+       my $query = $dbh->prepare(q{SELECT namespace FROM wiki_namespaces
+               WHERE namespace IN (SELECT namespace FROM wiki_namespace_access WHERE post AND gid IN (SELECT groups($1)))
+               ORDER BY namespace
+               });
+       $query->execute($c->stash->{UID});
+       $c->stash(namespaces => $query->fetchall_arrayref({}) );
+}
+
+
+=head1 AUTHOR
+
+Michael Andreen (harv@ruin.nu)
+
+=head1 LICENSE
+
+GPL 2.0, or later
+
+=cut
+
+1;
index 4671c42d0049bcd5412c9e4e2a4177aa2d783088..d1af74a5238b2520d696a92d3f92680a62c34df6 100644 (file)
@@ -1,7 +1,7 @@
 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
 <html>
 <head>
-       <title>[% site.title %]: [% template.title %]</title>
+       <title>[% site.title %]: [% title or template.title %]</title>
        <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
        <link rel="stylesheet" type="text/css" href="/static/default.css">
        <link rel="stylesheet" type="text/css" href="/static/css/[% user.css or "black" %].css">
index ff9a0848c36ac84f06554ed4ca6f3200dc993252..8f42cfe2907aae8d7763d2b658a599c8473392c4 100644 (file)
@@ -1,6 +1,7 @@
 <ul class="linkbar">
-       <li><a href="[% c.check_user_roles("member_menu") ? '/members' : '/index' %]">Main page</a></li>
+       [% IF c.check_user_roles("member_menu") %]<li><a href="/members">Main page</a></li>[% END %]
 
+       <li><a href="/wiki">Wiki</a></li>
        <li><a href="/forum">Forum</a></li>
        <li><a href="/forum/search">Forum search</a></li>
 [% IF user %]
diff --git a/root/src/wiki/edit.tt2 b/root/src/wiki/edit.tt2
new file mode 100644 (file)
index 0000000..884764c
--- /dev/null
@@ -0,0 +1,22 @@
+[% META title = 'Edit wiki page' %]
+<form action="[% c.uri_for('postedit',page.wpid) %]" method="post">
+[% IF page.wpid %]
+       <p><input type="radio" name="wpid" value="[% page.wpid %]" checked>Use old name: [% page.fullname %]
+[% END %]
+[% IF namespaces.size > 0 %]
+       <p><input type="radio" name="wpid" value="new"[% UNLESS page.wpid %] checked[%END%]>Save as new: <select name="namespace">
+       [% FOR n IN namespaces %]
+               <option value="[% n.namespace %]" [%IF n.namespace == page.namespace %]selected[%END%]>[% n.namespace %]</option>
+       [% END %]
+       </select> <input type="text" name="name" value="[% page.name %]"></p>
+[% END %]
+       <div><input type="hidden" name="parent" value="[% page.wprev %]"></div>
+       <p><textarea name="text" cols="80" rows="40">[% text | html %]</textarea></p>
+       <p>Comment: <input style="width: 40em" type="text" name="comment"></p>
+       <p><input type="submit" name="cmd" value="Preview"><input type="submit" name="cmd" value="Submit"></p>
+</form>
+
+<hr>
+
+[% wikiformat(text) %]
+
diff --git a/root/src/wiki/history.tt2 b/root/src/wiki/history.tt2
new file mode 100644 (file)
index 0000000..ba63a53
--- /dev/null
@@ -0,0 +1,20 @@
+[% SET name = page.namespace ? "$page.namespace:" : '' %]
+[% name = "$name$page.name" %]
+<h1><a href="[% c.uri_for(name) %]">[% name %]</a></h1>
+<hr>
+
+<table>
+<tr>
+       <th>Time</th><th>User</th><th>Comment</th>
+</tr>
+[% FOR r IN revisions %]
+<tr>
+       <td>[% r.time %]</td>
+       <td>[% r.username %]</td>
+       <td>[% r.comment %]</td>
+</tr>
+[% END %]
+</table>
+
+<hr>
+<a href="[% c.uri_for('edit',name) %]">Edit this page</a>
diff --git a/root/src/wiki/page.tt2 b/root/src/wiki/page.tt2
new file mode 100644 (file)
index 0000000..97f5cdf
--- /dev/null
@@ -0,0 +1,14 @@
+<h1>[% page.fullname %]</h1>
+<hr>
+
+[% IF page.wpid %]
+[% wikiformat(text) %]
+
+<hr>
+[% IF page.edit %]<a href="[% c.uri_for('edit',page.fullname) %]">Edit this page</a>[% END %]
+<a href="[% c.uri_for('history',page.fullname) %]">View page history</a>
+[% ELSE %]
+
+<p>This page doesn't exist[% IF page.post %], <a href="[% c.uri_for('edit',page.fullname) %]">create it</a>[% END %].</p>
+
+[% END %]
diff --git a/t/controller_Wiki.t b/t/controller_Wiki.t
new file mode 100644 (file)
index 0000000..94e8711
--- /dev/null
@@ -0,0 +1,10 @@
+use strict;
+use warnings;
+use Test::More tests => 3;
+
+BEGIN { use_ok 'Catalyst::Test', 'NDWeb' }
+BEGIN { use_ok 'NDWeb::Controller::Wiki' }
+
+ok( request('/wiki')->is_success, 'Request should succeed' );
+
+