From 2c934746290ce8baa0b38425e1fd74d63a2a12f5 Mon Sep 17 00:00:00 2001 From: Michael Andreen Date: Tue, 12 Aug 2008 17:26:18 +0200 Subject: [PATCH] Implemented a basic Wiki. Wiki controller handles all the wiki action. Redirect /index to /wiki to have an easily-editable front page. --- database/wiki-access.sql | 20 ++++ database/wiki.sql | 73 ++++++++++++ lib/NDWeb/Controller/Root.pm | 2 + lib/NDWeb/Controller/Wiki.pm | 222 +++++++++++++++++++++++++++++++++++ root/lib/site/html.tt2 | 2 +- root/lib/site/leftbar.tt2 | 3 +- root/src/wiki/edit.tt2 | 22 ++++ root/src/wiki/history.tt2 | 20 ++++ root/src/wiki/page.tt2 | 14 +++ t/controller_Wiki.t | 10 ++ 10 files changed, 386 insertions(+), 2 deletions(-) create mode 100644 database/wiki-access.sql create mode 100644 database/wiki.sql create mode 100644 lib/NDWeb/Controller/Wiki.pm create mode 100644 root/src/wiki/edit.tt2 create mode 100644 root/src/wiki/history.tt2 create mode 100644 root/src/wiki/page.tt2 create mode 100644 t/controller_Wiki.t diff --git a/database/wiki-access.sql b/database/wiki-access.sql new file mode 100644 index 0000000..ff63a0a --- /dev/null +++ b/database/wiki-access.sql @@ -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 index 0000000..1d00c01 --- /dev/null +++ b/database/wiki.sql @@ -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; + diff --git a/lib/NDWeb/Controller/Root.pm b/lib/NDWeb/Controller/Root.pm index 2b16db2..3eaaf0d 100644 --- a/lib/NDWeb/Controller/Root.pm +++ b/lib/NDWeb/Controller/Root.pm @@ -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 index 0000000..d15c609 --- /dev/null +++ b/lib/NDWeb/Controller/Wiki.pm @@ -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; diff --git a/root/lib/site/html.tt2 b/root/lib/site/html.tt2 index 4671c42..d1af74a 100644 --- a/root/lib/site/html.tt2 +++ b/root/lib/site/html.tt2 @@ -1,7 +1,7 @@ - [% site.title %]: [% template.title %] + [% site.title %]: [% title or template.title %] diff --git a/root/lib/site/leftbar.tt2 b/root/lib/site/leftbar.tt2 index ff9a084..8f42cfe 100644 --- a/root/lib/site/leftbar.tt2 +++ b/root/lib/site/leftbar.tt2 @@ -1,6 +1,7 @@