How To: Build a Wiki with Ruby on Rails – Part 1

Here’s how to build a very simple Ruby on Rails-based wiki engine 1.

1. Create the Rails App

rails wiki
and cd wiki to get inside the app.
Remember: create your database (i.e. wiki_development) and update config/database.ymlappropriately

2. Generate the Scaffolding

There are 3 nouns in this wiki system: People, Pages, Revisions. Let’s create the models for each of them.
run script/generate scaffold Person
Now open up wiki/db/migrate/*_create_people.rb and add:
t.column "name", :string inside the create_table block to store the Person’s name.

Add Person.create :name => "First Person"
after the create_table block to give us an initial Person.

run script/generate scaffold Page
Now open up wiki/db/migrate/*_create_pages.rb and add:
t.column "title", :string inside the create_table to store the Page’s title and add
Page.create :title => "Home_Page" after the create_table block to give us an initial Page.

run script/generate scaffold Revision
Now open up wiki/db/migrate/*_create_revisions.rb and add:

# Reference to the Person that created the Revision
t.column "person_id", :integer

# The contents of the Revision
t.column "contents", :text

# Reference to the Page this Revision belongs to
t.column "page_id", :integer

3. Run the database migrations

rake db:migrate

4. Describe the relationships between the Models

People should be able to create many Revisions.
Open /app/models/person.rb and add – after the first line:
has_many :revisions

Pages should have many Revisions
Open /app/models/page.rb and add – after the first line:
has_many :revisions

Revisions should reflect they belong to both Pages and People
Open /app/models/revision.rb and add – after the first line:

belongs_to :page
belongs_to :person

5. Draw up the Routes

Open up config/routes.rb and update them to handle wiki-fied words. Copy the following after the map.resources block

map.root :controller => 'pages', :action => 'show', :id => 1

map.connect 'pages.:format', :controller => 'pages', :action => 'index'
map.connect 'revisions.:format', :controller => 'revisions', :action => 'index'

map.page_base ':title', :controller => 'pages', :action => 'show'
map.connect ':title.:format', :controller => 'pages', :action => 'show'

map.page_history ':title/history', :controller => 'pages', :action => 'history'
map.connect ':title/history.:format', :controller => 'pages', :action => 'history'

map.page_edit ':title/edit', :controller => 'pages', :action => 'edit'

Now run script/server and load up a browser.
If things are working correctly, you should see simply ‘Edit | Back’ as links

6. Create the Views

Open /app/views/revisions/new.html.erb and add

< %= f.text_area :contents %>

< % fields_for :person do |p| %>


< %= p.text_field :name %>

< % end %>

right after < %= f.error_messages %> for the Revision’s content, the corresponding Page and the author’s name
Loading up http://0.0.0.0:3000/revisions/new should give you the form with the text area, a text field, and a submit button you just created.

Try entering something into the form and click ‘Create’. You should see ‘Revision was successfully created.’ Now, if you pop into your database, you should see the new row in the Revisions table with the contents you entered, but NULL values for person_id and page_id.
Let’s remedy that.

Open /app/controllers/revisions_controller.rb and add this line right after the @revision declaration in both def new and def create

@revision.update_attribute('person_id', Person.find_or_create_by_name(params[:person][:name]).id)

Reload http://0.0.0.0:3000/revisions/new and re-fill out the form. After hitting ‘Create’ you should have another Revision record in your database with some digit in the person_id field

Now, let’s tie the Revision form we created to a Page.
Open up /app/views/revisions/new.html.erb and change the form_for to:
< % form_for @revision, :url => new_revision_path, :html => {:method => :post} do |f| %>

Next, right after < %= f.text_area :contents %> add:
< %= f.hidden_field :page_id, :value => @page.id %>

Now, rename /app/views/revisions/new.html.erb to /app/views/revisions/_new.html.erb. This turns the form into a partial, which is fine, because Revisions only exist within Pages, never on their own.
Next, replace everything in /app/views/pages/edit.html.erb with

<h1>Update "< %= @page.title.gsub('_', ' ') %>"</h1>
< %= render :partial => 'revisions/new' %>

If you load up 0.0.0.0:3000/pages/1/edit right now, you’ll get a ‘Called id for nil’ error. That’s because the Revision partial is referencing @revision, which doesn’t exist in /pages. Yet.
Open up /app/controllers/pages_controller.rb and scroll to ‘def edit’.
Add @revision = @page.revisions.last || Revision.new right after the @page declaration

Refreshing 0.0.0.0:3000/pages/1/edit should now correctly render the form.
Fill it out, hit ‘Create’ and check the database. You should have a new record with a number in the page_id column.

But we can’t see it because 0.0.0.0:3000/pages/1 doesn’t show anything.

Open up /app/views/pages/show.html.erb and add:

<h2>< %= @page.title.gsub('_', ' ') %></h2>
<p>< %= @page.revisions.last.contents %></p>
<p>last edited by < %= @page.revisions.last.person.name %> on < %= time_ago_in_words(@page.revisions.last.created_at) %> ago</p>

to the first line and save.

Refreshing 0.0.0.0:3000/pages/1 should show the text you entered.

7. Update the Model, Controller, Views, and Routes to Acknowledge Wiki-fied Words (separated with an underscore ‘_’)

Open up /app/models/revision.rb and add:

def parsed_contents
contents = self.contents.gsub(/(w*_w*)/) {|match| "<a href='#{match.downcase}'>#{match.gsub('_', ' ')}</a>"}
end

Open up /app/controllers/pages_controllers.rb, find def show and change:
@page = Page.find(params[:id])
to

@page = Page.find_or_create_by_title(params[:title])
return redirect_to page_edit_url(:title => @page.title) if @page.revisions.empty?

Open up /app/controllers/revisions_controllers.rb, find def create and change each :
format.html... line to
format.html { redirect_to page_base_url(:page_title => @revision.page.title) }

Next, scroll down to def edit and change:
@page = Page.find(params[:id])
to
@page = Page.find_by_title(params[:title])

Now, in /app/views/pages/show.html.erb change the Edit link to build the new route:
< %= link_to 'Edit', page_edit_url(:title => @page.title) %> |

Lastly, in config/routes.rb, change
map.root :controller => 'pages', :action => 'show', :id => 1
to
map.root :controller => 'pages', :action => 'show', :title => 'Home_Page'

Restart your script/server and you should have a very basic wiki.

In Part 2, we’ll add formatting.

Want to see it in action? Play around with Wiki.Cullect.com

1. There are leaner Ruby frameworks to build wikis in (Camping or Merb come to mind.). I picked Rails for 2 reasons; My servers and deployment process was already set up for Rails, I wanted to loosely integrate this wiki into another existing Rails-based application (Cullect.com).

McCain & Obama Tax Plans: Small Change

Viveka Weiley redrew Washington Post’s chart of Obama’s and McCain’s respective tax plans.

First off, a caveat: Basing a vote on potential personal financial changes is as one-sided as basing a vote on gender, skin pigment, or hair color. It’s one factor and one that I hope to argue it is a wash to vast majority of Americans. For both plans call for significant cuts for the vast majority of Americans – households making less than $603k/yr (99% of Americans). For those remaining 1%, taxes will either go up or down. I’m part of the 99%, and I suspect you are as well1.

Income (% of Taxpayers) Plan Difference in $ Plan Difference
as % of Income
Favors
< $603k (10) 7,869 1.3 McCain
< $227k (10) 1,591 0.7 McCain
< $161k (10) 410 0.25 McCain
< $111k (10) 281 0.25 Obama
< $ 66k (20) 723 1.1 Obama
< $ 38k (20) 779 2.0 Obama
< $ 19k (20) 548 2.9 Obama

While much attention has been made to how different these plans are at the poles, it surprises me how close the two plans are for the middle 60% of tax payers (<2% delta).

I’m assuming both plans are drafts and would have to pass Congress to be enacted 2. If so, then I assume getting them passed through Congress would change the plans – perhaps even making them more similar.

Does this betray how similar their policies are/will be for the majority of Americans?

1. If you’re not, can I has monie? kthxbye.
2. Confirming we shouldn’t be investing too much in the candidates plans:

“The fact is that presidents have no power to raise or lower taxes. They can propose tax measures or veto them but Congress has the ultimate power to raise or lower taxes” – Walter E. Williams

Asks: Should We Buy a HD TiVo?

Our Series2 TiVo is on its last legs. With each passing day, its over-the-air recordings (we’re a no cable household) are more and more unwatchable, while digital over-the-air recordings are getting more stable. But seriously, TV without TiVo is like email without a spam filter.

Back in January, I replaced our DVD player w/ a Mac Mini. Since then, none of TiVo’s non-TV features have been used (music, podcast, etc) – simply because TiVo’s UI was too much of a PITA compared to Front Row & OS X.

The Mini’s secondary job is to play Netflix disks and does so with far fewer curse words1 than the dedicated DVD player it replaced.

In a world where; there’s a computer already plugged into our TV, NBC is back on iTunes, Hulu.com gaining traction, and the Roku box, dropping $200+ on a HD TiVo for over-the-air programming seems questionable.

What would you do?

UPDATE 12 JUNE 2009
The Tivo HD is now on order. Lifetime service.

1. I won’t get into my complaints about iTunes and Front Row here – they deserve their own post. 😉

RE: Aquarium for your toilet

Finally, Fish-n-Flush is bringing the toilet aquarium to market!

This proves: if an idea’s good enough, and you wait long enough, someone else will do it for you.

The aquarium toilet is one of my longest held It’d-Be-Cool-If ideas. I still remember the moment 15yrs ago when the idea called me. Something about the fish bringing calm and reflection to some often stressful moments. This use case means the tank isn’t the best candidate for the aquarium. I hold the stool-as-aquarium would be more interesting for all involved, but the tank-as-aquarium is close enough to justify me holding onto the idea this long.

Thanks to David Pescovitz at BoingBoing for the pointer.

Look + See: Curation in Eyewear

I just picked up a new pair of glasses from George over at Look + See Eyecare in Minneapolis.

Until I found Look + See, I was weary of eyewear places. It was a classic case of the paradox of choice. Lots of potential options and difficulty discerning differences without trying on every pair in the store.

George and I actively ignored the vast majority of the frames in the store. We may have gone through 5 frames – out of the hundreds on the walls. Five. Cause, really, how many times do you want to say ‘No’?

Plus, picking 1 from the 5 George recommended was easy.

While part of my final bill was for frames and lenses, part is also for George’s expert curation and recommendations. I don’t want discount that. Remember, the fire hose is free….

Workaround for IE Overly Accepting in Rails’ respond_to format

Looks like Microsoft’s Internet Explorer will accept any format a web server is willing to give it.

This doesn’t play nicely with Rails’ 2.0+ respond_to feature. A slick little bit of code that asks the browser what it wants and replies accordingly.

Here’s a conversation between Rails & Firefox

Firefox: “Hey Rails, I want this url”
Rails: “No problem, which format would you like it in?”
Firefox: “HTML, please.”
Rails: “Here you go.”

Here’s the same conversation with Internet Explorer

IE: “Hey Rails, I want this url”
Rails: “No problem, which format would you like it in?”
IE: “Whatcha got?”
Rails: “I’ve got Atom, and…”
IE: (interputting) “OK THANKS!”
Rails: “…um, what? I wasn’t finished, really? ok, here you go.”

I had ordered my code alphabetically, so ‘atom‘ came before ‘html‘, like this:

respond_to do |format|
format.atom
format.html

end

Because IE is so, um, accepting, I’ve needed to put ‘html‘ first:

respond_to do |format|
format.html
format.atom

end

For more on this issue:

What if We Had Just 10% More Energy Producers?

If memory serves, the internet was originally developed as a national defense mechanism. A way to keep communications – in a distributed manner – flowing after a nuclear attack.

Each node a client and a server, a receiver and producer.

Today, not only are the vast majority of Americans online (receivers), but a good chunk – 10% – are actively engaged in making online a better place (producers).

While our communications and communities are distributed, our energy is still centralized.

Broadcast if you will.

From where you’re sitting, can you see the power plant generating the electricity you’re using to read this?

Probably not.

So, we don’t see energy being generated, it’s far away, feedback takes a billing cycle, and our only way to reduce costs is to reduce demand.

And we’re surprised selling energy efficiency to the American public is an uphill battle?

But what if?

What if, just 10% of us were also putting energy back onto the grid.

1 household per block (another take on the block-by-blog idea) sucking down solar, wind, or geothermal. Covering their energy needs and putting the surplus on the grid.

Reducing demand by increasing the number of suppliers – even if they’re only nano-suppliers, cover a few households.

This may even minimize the outages from minor disasters.

The economics are tough from a private citizen doing this on their own, so I wonder – what if this was part of being a energy customer. The energy company finances and maintains the solar panels on your suburban roof. Like sitting in the exit row on an airplane. But for your neighborhood’s electricity.

Hmmmmm.

(citations forthcoming)

Daily Bread: 12 Aug 2008

The boy and I have been making bread (almost) every morning for the past few weeks. I find it a relaxing way to start the morning as he picks at breakfast. The loaf in the photo above, I made this morning.

The simplicity of bread-making is compelling. 4 ingredients: flour, water, yeast, salt.

Separate they don’t taste like much (still, each time we make dough, the boy tastes a licked-finger full of each ingredient).

You’ve probably got those 4 items lying around your kitchen. I did.

No HFCS, no extra flavors. Still, this very simple (and forgiving) recipe makes the best bread I’ve had in a decade. Easy.

Custard-y interior with a hard, crusty exterior.

Perfect for a creamy cheese or a thick swath of Nutella.

And I’ve yet to hit reach 5 min/day, it usually takes fewer.


Artisan Bread in 5 Minutes a Day

My 11 Favorite Eponymous Laws

  • Amara’s law — “We tend to overestimate the effect of a technology in the short run and underestimate the effect in the long run”.
  • Brooks’ law: “Adding manpower to a late software project makes it later.”
  • Conway’s Law : “Any piece of software reflects the organizational structure that produced it.”
  • Edwards’ law: “You cannot apply a technological solution to a sociological problem.”
  • Goodhart’s law: “When a measure becomes a target, it ceases to be a good measure.”
  • Hanlon’s razor: “Never attribute to malice that which can be adequately explained by stupidity.”
  • Heisenberg’s Uncertainty principle: “States that one cannot measure values (with arbitrary precision) of certain conjugate quantities, which are pairs of observables of a single elementary particle. The most familiar of these pairs is the position and momentum.”
  • Keynes’ Law: “Demand creates its own supply.” (The economists’ version of Gibson’s ‘the street has it’s own use for things’)
  • Parkinson’s law: “Work expands so as to fill the time available for its completion.”
  • Sturgeon’s revelation: “90 percent of everything is crap.”
  • Winer’s rule of alternatives: “One way to do something, no matter how flawed that way is, is better than two, no matter how much better the second way is.”Two is more than twice as bad (Note: Thanks Dave!I need to find a proper citation for this. Google wasn’t helpful)

Update 11 Aug 2008
I’m interested in the relationship between Sturgeon’s Relevation and the Pareto Principle (i.e the 80-20 rule).

Update 25 May 2025

Kidlin’s Law
If you write a problem down, you’ve already solved half of it.

Gilbert’s Law
No one’s coming to save you.
When you commit to something, it’s your job to figure it out.
Ownership beats excuses—every time.

Wilson’s Law
Growth leads. Success follows.

Falkland’s Law
If it’s not necessary to decide, don’t.