Ruby on Rails: Creating a 10 minute wiki
Further to a previous article, explaining the virtues of a wiki for open communication, here is a very basic (very basic indeed) implementation of a wiki, in Rails, that can work as a very useful baseline for growing your own bespoke tool.
I shall assume that you have knowledge of creating a Rails application, configuring the database and running generators etc.
Source code is included, for download, at the end of the article.
Wiki Project
After creating your wiki project (i.e. rails wiki) and your database (mine is wiki_development) you need to create your model. I am a fan of using migrations to do this, so I shall create the application’s only model, WikiPage.
ruby script/generate model WikiPage
Our wiki pages will have a url, some text and, for good measure, time stamp columns. That is all. This dedscribes our entire application.
Here is my migration
001_create_wiki_pages.rb:class CreateWikiPages < ActiveRecord::Migration def self.up create_table :wiki_pages do |t| t.column "page_url", :string t.column "page_text", :text t.column "created_at", :datetime t.column "updated_at", :datetime end end def self.down drop_table :wiki_pages end end
When you run rake db:migrate, assuming everything is wired up correctly, your database should contain one table.
I am going to use a controller called wiki to perform the ‘heavy’ lifting of the application.
ruby script/generate controller wiki
I know that my wiki controller will need to perform at least two different tasks: show and edit.
I shall define the following in my controller:
def index redirect_to :action => :browse end def browse end def edit end
both the browse and edit actions will need to load a wiki page, based on the url. Urls are going to be of the form: /wiki/browse/MyPageName, so params[:id] is what we’re interested in. If the id is missing, we’ll default to a page called start.
I’ll add this method to my controller:def page_url url = params[:id] ||= "Start" end
Naturally, the browse and edit actions will want to load a WikiPage object based on the supplied URL. However, the way to create a new page in a wiki is to browse to a URL that doesn’t yet exist, and then edit that page. This means that we need to handle missing pages by ‘pretending’ they exist in both actions. We’ll create a get_current_page method, to avoid repeating ourselves:
private def get_current_page current_page = WikiPage.find(:first, :conditions => 'page_url = \'' + page_url + '\'' ) if (current_page == nil) current_page = WikiPage.new current_page.page_url = page_url current_page.page_text = '' end return current_page end
I can now flesh out the actions to load a WikiPage (for browse) and to save a wiki page on post back (for edit):
def browse @wiki_page = get_current_page end def edit @wiki_page = get_current_page if (request.post?) @wiki_page.update_attributes(params[:wiki_page]) @wiki_page.save! redirect_to :action => :browse, :id => @wiki_page.page_url end end
That’s all very basic stuff, and you’ll know most it, because it is what you learned from Agile Web Development with Rails.
Our HTML files, once populated to contain the minimum support for our functionality (and a small measure of style), look like this:
views/layouts/wiki.rhtml:<html> <head> <style type="text/css"> #page_title { border-bottom:dotted 1px #dadada; margin-bottom:18px; } #page_content { margin:12px 0px; padding:0px 8px; border:dotted 1px #aaa; border-bottom:dotted 1px #aaa; } #page_content h1 { margin:4px 0px; } #page_content textarea { width:100%; border:none; } #page_options { padding:4px; font-family:verdana; font-size:8pt; } #page_meta_data { padding:4px; font-family:verdana; font-size:8pt; color:#ccc; } #page_options a:link, #page_options a:visited { color:#555; text-decoration:none; } #page_options a:hover, #page_options a:active { color:red; text-decoration:underline; } a:link, a:visited { color:navy; } a:hover, a:active { color:purple; } </style> </head> <body> <%= yield %> </body> </html>
<div id="page_content"> <h1 id="page_title"><%= @wiki_page.page_url %></h1> <%= @wiki_page.processed_page_text %> </div> <div id="page_options"> <%= link_to("home", :action => :index) %> <%= link_to("edit", :action => :edit, :id => @wiki_page.page_url) %> </div> <div id="page_meta_data"> Created at <%= @wiki_page.created_at %><br /> Last updated at <%= @wiki_page.updated_at %><br /> </div>
<h1>Editing '<%= @wiki_page.page_url %>'</h1> <form method="post"> <div id="page_content"> <%= text_area(:wiki_page, :page_text) %> </div> <div id="page_options"> <input type="submit" value="Save" /> <input type="button" value="Cancel" onclick="location = '/wiki/browse/<%= @wiki_page.page_url %>';" /> </div> </form>
OK, one final thing to do. In browse I don’t display the plain text of the wiki page. I call a non-existent method in WikiPage, processed_page_text. This is because our wiki should retain things like paragraph breaks without requiring the user to enter raw HTML. There is also another convention in simple wikis, in that if you smash two capitalized words together, LikeThis, that they will automatically become a link, LikeThis. It’s crude but effective.
I added the following method to my WikiPage model that both formats text, using textile, and performs the processing of words containing two or more capital letters:
/models/wiki_page.rbrequire 'redcloth' def processed_page_text r = RedCloth.new self.page_text.gsub(/(([A-Z][a-z]*)([A-Z][a-z]*)+)/, '<a href="/wiki/browse/\1">\1</a>') return r.to_html end
When you fire the application up and point at the browse controller, you should see an emply Start page. You can edit this page, grow your wiki and modify it to meet your own requirements!
