HellOnline

Icon

Eran's blog

Acts as Wiki

A wiki is a very useful thing. In fact many times I’ve wanted to have just one field in a class be a wiki. This behavior is even more desirable with all the Web 2.0 user-generated content type sites coming out lately. The way I see it, Wikis have two useful features:

  1. Revision history – so you don’t have to worry too much about vandalism, opinionated authors, etc.
  2. Markdown or similar filter to make rich content authoring simple and safe.

One of my first additions to WhereAreYouCamping.com (WAYC) was the versioning code. Anybody can edit camp content and so the site is left wide open to vandalism. The code for that is pretty simple


class Camp 'Revision',
:foreign_key => 'camp_id',
:conditions => "field_name = 'description'",
:order => "id DESC"
before_save :revise(‘description’)

The revise function is in charge of creating a new revision if the current value for the description field is different from the previous value. I’m omitting it for brevity.

This is very simple and works pretty well as a simple backup method. You can probably imagine that with some pretty simple code in the Revisions_Controller it easy to show the revision history of a field, diff revisions and revert to a previous one. On the other hand, this solution is lacking author details and a few other niceties but the biggest disadvantage is, of course, lack of DRYness. This method is not at all reusable; in fact additional wikified fields requires much replicated code. Enter acts_as_wiki.

Acts_as_wiki is a mixin (soon to be a plugin, I hope) that lets you do all that with one line of code:

acts_as_wiki [:description, :events], Filters.method(‘markdown’)

Adds revisions (with history, diff and revert) and markdown filtering (or whichever filter you decide to use if at all) to the description field. Here’s a simplified version of the method:


def acts_as_wiki(field, filter = nil)
# remember the filter for this field.
Filter_hash[field.to_s] = filter
# add reader to filtered field content
module_eval "def #{field}_filtered self.get_filtered('#{field}'); end"
# add the revisions relation
has_many("#{field}_revisions".to_sym,
:class_name => 'Revision',
:foreign_key => 'obj_id',
:conditions => "field_name = '#{field}' and obj_type = '#{self.class}'",
:order => "id DESC",
:as => 'obj')
}
before_save Proc.new{|obj| obj.revise(field)}
end

To record the author who created a revision I’ve updated the update_attributes method:


attr :current_author, true

def update_attributes(attributes, author_name = 'unknown')
self.current_author = author_name
# let super handle the actual update
super(attributes)
end

Later in revise() I refer to current_author to get the author’s details. This can be expanded to support a more complex user model. For content filtering I’ve added the get_filtered method which is used by the FIELD_NAME_fieltered method (mentioned above):


def get_filtered(field_name)
content = self.send(field_name)
filter = Filter_hash[field_name.to_s]
if filter
return filter.call(content)
else
return content
end
end

This allows the programmer to continue accessing the raw field value (as stored in the database) using the field name and so requires very little change throughout the application.

Revisions are simple creatures:


create_table "revisions" do |t|
t.column "created_at", :datetime, :null => false
t.column "revised_at", :datetime, :null => false
t.column "content", :text, :default => "", :null => false
t.column "author", :string, :limit => 60
t.column "obj_type", :string
t.column "obj_id", :integer, :null => false
t.column "field_name", :string, :limit => 20
end

Similarly, the Revision model needs very little besides the relationship to its owner:


class Revision 'obj_id', :polymorphic => true
# some helper functions for browsing through revisions
def prev_rev
Revision.find(:first,
:conditions => ['id 'id DESC') rescue nil
end
def next_rev
Revision.find(:first,
:conditions => ['id > ? AND field_name = ? AND obj_type = ? AND obj_id = ?', self.id, self.field_name, self.obj_type, self.obj_id]) rescue nil
end
def last_rev
Revision.find(:first,
:conditions => ['id >= ? AND field_name = ? AND obj_type = ? AND obj_id = ?', self.id, self.field_name, self.obj_type, self.obj_id],
:order => 'id DESC') rescue nil
end
end

The revisions controller is also pretty simple. The basic actions: history, show and revert pretty much write themselves. The diff action is just as easy once you use the diff method from instiki.

And there you have it. The field[s] of your choice can now be a full blown wiki with revision history, markdown, the works. You can see the rest of the code on trac.

Update: I’ve put up a plugin, you can install it using

script/plugin install http://www.hellonline.com/svn/hellonline/plugins/acts_as_wiki

generate the code and migrate your db using


script/generate acts_as_wiki
rake db:migrate

Update 2: Just posted a tutorial on how to use the Acts as Wiki plugin.

Advertisements

Filed under: Projects, Ruby on Rails

One Response - Comments are closed.

  1. […] s. And speaks. And speaks. And speaks…

    « Acts as Wiki

    HowTo: Using Acts as Wiki to Create Smarter Blog

    This […]

%d bloggers like this: