Rails Messaging Tutorial

By NovaWave Solutions, aka manitoba98

Abstract

This guide aims to be a simple, logical tutorial showing how to develop a simple Rails messaging system with all of the trimmings with Ruby on Rails (v2.0.2). This tutorial is intended for beginner to intermediate Rails users. If you've never used Rails before, I suggest you check out any of the excellent introductions out there.

I do take a few shortcuts here and there (I use inline CSS, for instance). For authentication, you may find it valuable to implement the concept of an administrator or use a before_filter so that users get a login page instead of an error message. I've only implemented this on the Inbox (because users are redirected to it automatically after logout).

This guide was created in response to this post on RailsForum.

Table of Contents

Starting Out

I'll assume that you already have Ruby and Rails 2.0 installed on your machine. If not, get them.

We begin by generating the Rails project. I'm going to open the project in TextMate, my editor of choice, but feel free to use yours.


$ rails messenger
create  
create  app/controllers
create  app/helpers
create  app/models
create  app/views/layouts
create  config/environments
create  config/initializers
create  db
 ...
create  log/production.log
create  log/development.log
create  log/test.log
$ cd messenger
$ mate .

The next step is to create the basic models of our application. We'll have a Message model to represent the message sent, and MessageCopy model to represent each individual recipient's copy of the message. We'll have a User model to represent each person who can potentially send or receive messages. Finally, we'll have a Folder model to represent each folder in which a person can place mail, including an inbox.

Let's generate those now.


$ script/generate model message author_id:integer subject:string body:text
exists  app/models/
exists  test/unit/
exists  test/fixtures/
create  app/models/message.rb
create  test/unit/message_test.rb
create  test/fixtures/messages.yml
create  db/migrate
create  db/migrate/001_create_messages.rb
$ script/generate model message_copy recipient_id:integer message_id:integer folder_id:integer
exists  app/models/
exists  test/unit/
exists  test/fixtures/
create  app/models/message_copy.rb
create  test/unit/message_copy_test.rb
create  test/fixtures/message_copies.yml
exists  db/migrate
create  db/migrate/002_create_message_copies.rb
$ script/generate model folder user_id:integer parent_id:integer name:string
exists  app/models/
exists  test/unit/
exists  test/fixtures/
create  app/models/folder.rb
create  test/unit/folder_test.rb
create  test/fixtures/folders.yml
exists  db/migrate
create  db/migrate/003_create_folders.rb

Note that we still haven't written any code. Now we'll install restful_authentication as our user login system. We'll also install scope_out, acts_as_tree and will_paginate, useful plugins we'll use later on. We'll also migrate the database.


$ script/plugin install http://svn.techno-weenie.net/projects/plugins/restful_authentication
...
$ script/generate authenticated user sessions
exists  app/models/
exists  app/controllers/
exists  app/controllers/
exists  app/helpers/
...
exists  db/migrate
create  db/migrate/004_create_users.rb
route  map.resource :session
route  map.resources :users
$ script/plugin install acts_as_tree
...
$ script/plugin install http://scope-out-rails.googlecode.com/svn/trunk/
...
$ script/plugin install svn://errtheblog.com/svn/plugins/will_paginate
...
$ rake db:migrate
...

As restful_authentication suggests, you should now add the line "include AuthenticatedSystem" in your ApplicationController, located in app/controllers/application.rb. Do so.

Okay, now all of our models have been created. We need to tell Rails how they are linked to each other. Fortunately, Rails makes this absolutely trivial. Update your Message model file (app/models/message.rb):

class Message < ActiveRecord::Base
  belongs_to :author, :class_name => "User"
  has_many :message_copies
  has_many :recipients, :through => :message_copies
end

This, thanks to Rails' simple style, is already fairly legible. Each messages belongs to an author (which is a User). It has many copies, and it has many recipients, which can be found through the copies (since each copy belongs to a single recipient).

Next on the list is MessageCopy. Here it is:

class MessageCopy < ActiveRecord::Base
  belongs_to :message
  belongs_to :recipient, :class_name => "User"
  belongs_to :folder
  delegate   :author, :created_at, :subject, :body, :recipients, :to => :message
end

As you can see, each copy belongs to the original message. It also belongs to a single recipient, and a folder owned by that recipient. That last "delegate" line is probably new to you. It's just syntactic sugar that allows us to "forward" the listed attributes to the copy's original message. That allows us to say "@copy.author" instead of "@copy.message.author". It's shorter; that's all.

Next are the folders. We want the folders to be hierarchical; that is, folders can contain other folders.

class Folder < ActiveRecord::Base
  acts_as_tree
  belongs_to :user
  has_many :messages, :class_name => "MessageCopy"
end

Finally, we'll move on to our User model. We'll define associations for both sent and received messages, as well as folders.

require 'digest/sha1'
class User < ActiveRecord::Base
  has_many :sent_messages, :class_name => "Message", :foreign_key => "author_id"
  has_many :received_messages, :class_name => "MessageCopy", :foreign_key => "recipient_id"
  has_many :folders
  
  # (Autogenerated restful_authentication code remains here)
end

Okay, now that we've got that basic logic established, we need to allow messages to actually be sent. In order to do that, we need to modify the Message model to automatically create MessageCopy models for "distribution" to other users. I'll also add an attr_accessible call for security.

class Message < ActiveRecord::Base
  belongs_to :author, :class_name => "User"
  has_many :message_copies
  has_many :recipients, :through => :message_copies
  before_create :prepare_copies
  
  attr_accessor  :to # array of people to send to
  attr_accessible :subject, :body, :to
  
  def prepare_copies
    return if to.blank?
    
    to.each do |recipient|
      recipient = User.find(recipient)
      message_copies.build(:recipient_id => recipient.id, :folder_id => recipient.inbox.id)
    end
  end
end

So this is our first bit of code proper. I'll go ahead and explain what I've done here. I've added a callback which will execute just before the message is created. It loops through each recipient requested and builds a copy for them, filing it in that person's inbox. Seems logical enough. But we haven't defined the "inbox" method yet. We'll do that now. Just before the pregenerated restful_authentication code, add the following to your User model:

  before_create :build_inbox

  def inbox
    folders.find_by_name("Inbox")
  end

  def build_inbox
    folders.build(:name => "Inbox")
  end

This bit of magic allows us to access the user's "inbox" folder, and ensure that one is created for each user. We're just about ready to get started.

Back to Top

Anchors Aweigh!

Okay, so we're ready to generate our controllers.


$ script/generate controller sent index show new
...
$ script/generate controller messages show
...
$ script/generate controller mailbox show
...

As you can see, we're going to use three separate controllers. The first handles the user's sent messages, and maps on to our Message model. The second handles received messages, and maps on to our MessageCopy model. The last controller manages the user's mailbox, allowing the user to show their inbox and other folders. This does indeed map on to the Folder model. For those of you who know what it is, this is a RESTful design.

The first thing you need to do is add the resource routes. In your config/routes.rb file, change it to the following:

ActionController::Routing::Routes.draw do |map|
  map.resources :users, :sent, :messages, :mailbox
  map.resource :session
  
  # Home route leads to inbox
  map.inbox '', :controller => "mailbox", :action => "index"
  
  # Install the default routes as the lowest priority.
  map.connect ':controller/:action/:id'
  map.connect ':controller/:action/:id.:format'
end

Don't forget to remove public/index.html, or the Rails welcome message will continue to appear. Let's get started with the SentController.

class SentController < ApplicationController

  def index
    @messages = current_user.sent_messages.paginate :per_page => 10, :page => params[:page], :order => "created_at DESC"
  end

  def show
    @message = current_user.sent_messages.find(params[:id])
  end

  def new
    @message = current_user.sent_messages.build
  end
  
  def create
    @message = current_user.sent_messages.build(params[:message])
    
    if @message.save
      flash[:notice] = "Message sent."
      redirect_to :action => "index"
    else
      render :action => "new"
    end
  end
end

It's really just a variant on the standard methods you've probably seen, but it's bound to the current user's sent messages. The "index" method also uses will_paginate so that only 10 messages are listed at once.

But to each controller, we must fill in the views. Use these to start with:

=== file: app/views/layouts/application.html.erb ===

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
  <head>
    <title>Rails Messenger</title>
  </head>
  
  <body>
    <h1>Rails Messenger</h1>
    
    <% if flash[:notice] %>
      <p style="color:green"><%= flash[:notice] %></p>
    <% end %>
    
    <% if flash[:error] %>
      <p style="color:red"><%= flash[:error] %></p>
    <% end %>
    
    <% if logged_in? %>
      <p>Welcome, <%=h current_user.login %>. <%= link_to "Logout", session_path, :method => "delete" %></p>
    <% else %>
      <p>You are not logged in. <%= link_to "Register", new_user_path %> or <%= link_to "Login", new_session_path %></p>
    <% end %>
    
    <%= render :partial => "layouts/mailbox_list" if logged_in? %>
    
    <%= yield %>
  </body>
</html>

=== file: app/views/layouts/_mailbox_list.html.erb ===

<div id="mailbox_list" style="border:1px solid #aaa; float:right; margin:1em; padding:1em; width:20%">
  <p><%= link_to "Compose", new_sent_path %></p>
  
  <p><strong>Mailboxes</strong></p>
  <ul>
    <li><%= link_to "Inbox", inbox_path %></li>
    <li><%= link_to "Sent", :controller => "sent", :action => "index" %></li>
  </ul>
</div>

=== file: app/views/sent/new.html.erb ===

<h2>Compose</h2>

<% form_for :message, :url => {:controller => "sent", :action => "create"} do |f| %>

  <p>
    To:<br />
    <select name="message[to][]">
      <%= options_from_collection_for_select(User.find(:all), :id, :login, @message.to) %>
    </select>
  </p>

  <p>Subject: <%= f.text_field :subject %></p>
  <p>Body:<br /> <%= f.text_area :body %></p>
  <p><%= submit_tag "Send" %></p>
<% end %>

At this point, you can actually send messages if you run script/server. Nobody can read them, but you can indeed send them. They'll get stored in the database correctly.

But we do need a way to view those messages. Let's do that now. We'll start with a way to view sent messages, since we've already done that controller.

=== file: app/views/sent/index.html.erb ===

    <h2>Sent Messages</h2>

    <table border="1">
      <tr>
        <th>To</th>
        <th>Subject</th>
        <th>Sent</th>
      </tr>

      <% for message in @messages %>
        <tr>
          <td><%=h message.recipients.map(&:login).to_sentence %></td>
          <td><%= link_to h(message.subject), sent_path(message) %></td>
          <td><%= distance_of_time_in_words(message.created_at, Time.now) %> ago</td>
        </tr>
      <% end %>
    </table>

    <%= will_paginate @messages %>

=== file: app/views/sent/show.html.erb ===

<h2>Sent: <%=h(@message.subject) %></h2>
<p><strong>To:</strong> <%= @message.recipients.map(&:login).to_sentence %></p>
<p><strong>Sent:</strong> <%= @message.created_at.to_s(:long) %></p>

<pre><%=h @message.body %></pre>

You can now send messages and view the messages you've sent. Not bad, eh? But we're still missing the most essential functionality: reading received messages. Let's get on that. Open the MailboxController. We'll code that next.

=== file: app/controllers/mailbox_controller.rb ===
    
class MailboxController < ApplicationController
  def index
    redirect_to new_session_path and return unless logged_in?
    @folder = current_user.inbox
    show
    render :action => "show"
  end

  def show
    @folder ||= current_user.folders.find(params[:id])
    @messages = @folder.messages.paginate :per_page => 10, :page => params[:page], :include => :message, :order => "messages.created_at DESC"
  end
end
=== file: app/vies/mailbox/show.html.erb ===

<h2><%=h @folder.name %></h2>

<table border="1">
  <tr>
    <th>From</th>
    <th>Subject</th>
    <th>Received</th>
  </tr>
  
  <% for message in @messages %>
    <tr>
      <td><%=h message.author.login %></td>
      <td><%= link_to h(message.subject), message_path(message) %></td>
      <td><%= distance_of_time_in_words(message.created_at, Time.now) %> ago</td>
    </tr>
  <% end %>
</table>

<%= will_paginate @messages %>

Okay, so that controller uses a little trick that makes our code a bit more DRY, but it begs explanation. When the user views his/her inbox, the "index" action is called. It sets @folder to the current user's inbox, then calls and renders the show action. The show action will see if @folder is already defined. If it is, it will use that (in this case, the inbox). If not, it will load one from the ID parameter. It will then paginate through the messages in that folder.

We now can list messages in our inbox, but we can't read them. For that, we'll need to dive into the MessagesController.

=== file: app/controllers/messages_controller.rb ===

class MessagesController < ApplicationController
  def show
    @message = current_user.received_messages.find(params[:id])
  end
end

=== file: app/views/messages/show.html.erb ===

<h2><%=h @message.subject %></h2>

<p><strong>From:</strong> <%=h @message.author.login %></p>
<p><strong>To:</strong> <%=h @message.recipients.map(&:login).to_sentence %></p>
<p><strong>Received:</strong> <%= @message.created_at.to_s(:long) %></p>

<pre><%=h @message.body %></pre>

It's alive! You can now send and receive messages: the basic functionality works. We can now move on to "bonus" features, like Reply, more folders, multiple recipients, etc.

Back to Top

Reply

Reply is probably one of the more popular messaging features. We will now implement it in our application. First, we'll alter our routes file to create a "reply" action. We will then write that action, and modify our compose view to work with it.

=== file: config/routes.rb ===
    
ActionController::Routing::Routes.draw do |map|
  map.resources :users, :sent, :mailbox
  map.resources :messages, :member => { :reply => :get }
  map.resource :session
  
  # Home route leads to inbox
  map.inbox '', :controller => "mailbox", :action => "index"
  
  # Install the default routes as the lowest priority.
  map.connect ':controller/:action/:id'
  map.connect ':controller/:action/:id.:format'
end

=== file: app/controllers/messages_controller.rb ===

class MessagesController < ApplicationController
  def show
    @message = current_user.received_messages.find(params[:id])
  end
  
  def reply
    @original = current_user.received_messages.find(params[:id])
    
    subject = @original.subject.sub(/^(Re: )?/, "Re: ")
    body = @original.body.gsub(/^/, "> ")
    @message = current_user.sent_messages.build(:to => [@original.author.id], :subject => subject, :body => body)
    render :template => "sent/new"
  end
end

=== file: app/views/messages/show.html.erb ===

<h2><%=h @message.subject %></h2>

<p><strong>From:</strong> <%=h @message.author.login %></p>
<p><strong>To:</strong> <%=h @message.recipients.map(&:login).to_sentence %></p>
<p><strong>Received:</strong> <%= @message.created_at.to_s(:long) %></p>

<pre><%=h @message.body %></pre>

<p><%= link_to "Reply", reply_message_path(@message) %></p>

There's some regular expression magic there for adding the classic "Re: " tag (but not if it's already there) and adding the similarly-popular indentation style. Try it out, it works. If you want forwarding, it should be fairly logical how to accomplish that. Just omit the bit that specifies the new message's recipients in your forward action, and use "Fwd:" instead of "Re:". Forwarding functionality will be in the final copy at the end of this tutorial, but I won't describe it fully here. Consider it an exercise for the reader.

Back to Top

Multiple Recipients

Another common feature is the ability to send to multiple recipients. This system does actually already support that: only the form doesn't. The simplest way to add that is adding the "multiple" parameter to our <select> tag. But personally, I'm not a big fan of that widget. I'll show you how to implement a scrolling checklist instead.

Okay, so let's go back to our compose view (app/views/sent/new.html.erb). We'll take out that ugly select tag and replace it to a call to "checklist", a helper method we'll define. It takes parameters similar to collection_select.

=== file: app/views/sent/new.html.erb ===
    
<h2>Compose</h2>

<% form_for :message, :url => {:controller => "sent", :action => "create"} do |f| %>

  <p>
    To:<br />
    <%= checklist "message[to][]", User.find(:all), :id, :login, @message.to %>
  </p>

  <p>Subject: <%= f.text_field :subject %></p>
  <p>Body:<br /> <%= f.text_area :body %></p>
  <p><%= submit_tag "Send" %></p>
<% end %>

=== file: app/helpers/application_helper.rb ===

# Methods added to this helper will be available to all templates in the application.
module ApplicationHelper
  def checklist(name, collection, value_method, display_method, selected)
    selected ||= []
    
    ERB.new(%{
    <div class="checklist" style="border:1px solid #666; width:20em; height:5em; overflow:auto">
      <% for item in collection %>
        <%= check_box_tag name, item.send(value_method), selected.include?(item.send(value_method)) %> <%=h item.send(display_method) %><br />
      <% end %>
    </div>}).result(binding)
  end
end

That's now working, go ahead and check it out. Reply (and forward, if you did that) are still working. And it already works because our application actually supported multiple recipients in the backend from the beginning. We just needed to provide a user interface to that. And we have.

Back to Top

Reply All

Ah yes, the dreaded Reply All feature. It can be annoying, but it's often considered a mainstay of the email experience, which we're effectively replicating. Reply All is rather similar to Reply, so I'll just show you the code. For those of you who didn't code forwarding yourself earlier, I've also done that here.

=== file: config/routes.rb ===
    
ActionController::Routing::Routes.draw do |map|
  map.resources :users, :sent, :mailbox
  map.resources :messages, :member => { :reply => :get, :forward => :get, :reply_all => :get }
  map.resource :session
  
  # Home route leads to inbox
  map.inbox '', :controller => "mailbox", :action => "index"
  
  # Install the default routes as the lowest priority.
  map.connect ':controller/:action/:id'
  map.connect ':controller/:action/:id.:format'
end

=== file: app/controllers/messages_controller.rb ===

class MessagesController < ApplicationController
  def show
    @message = current_user.received_messages.find(params[:id])
  end
  
  def reply
    @original = current_user.received_messages.find(params[:id])
    
    subject = @original.subject.sub(/^(Re: )?/, "Re: ")
    body = @original.body.gsub(/^/, "> ")
    @message = current_user.sent_messages.build(:to => [@original.author.id], :subject => subject, :body => body)
    render :template => "sent/new"
  end
  
  def forward
    @original = current_user.received_messages.find(params[:id])
    
    subject = @original.subject.sub(/^(Fwd: )?/, "Fwd: ")
    body = @original.body.gsub(/^/, "> ")
    @message = current_user.sent_messages.build(:subject => subject, :body => body)
    render :template => "sent/new"
  end
  
  def reply_all
    @original = current_user.received_messages.find(params[:id])
    
    subject = @original.subject.sub(/^(Re: )?/, "Re: ")
    body = @original.body.gsub(/^/, "> ")
    recipients = @original.recipients.map(&:id) - [current_user.id] + [@original.author.id] 
    @message = current_user.sent_messages.build(:to => recipients, :subject => subject, :body => body)
    render :template => "sent/new"
  end
end

=== file: app/views/messages/show.html.erb ===

<h2><%=h @message.subject %></h2>

<p><strong>From:</strong> <%=h @message.author.login %></p>
<p><strong>To:</strong> <%=h @message.recipients.map(&:login).to_sentence %></p>
<p><strong>Received:</strong> <%= @message.created_at.to_s(:long) %></p>

<pre><%=h @message.body %></pre>

<p>
  <%= link_to "Reply", reply_message_path(@message) %> |
  <%= link_to "Reply All", reply_all_message_path(@message) %> |
  <%= link_to "Forward", forward_message_path(@message) %>
</p>

Back to Top

Delete

Rather than implementing a full, real delete (like you could by calling #destroy), we'll make our deletion functionality just hide it. For this, we'll create a "deleted" column in our message_copies table. We'll show deleted messages in the Trash folder, and non-deleted messages in the inbox (and later, other folders). You will even be able to restore them if you want. To do this, we'll use a plugin called "scope_out" which we installed earlier. We'll call scope_out in our MessageCopy model and create the needed column in the message_copies table. Don't forget to migrate the database again (I haven't shown that here, but it's still necessary). After that comes the more laborious (somewhat) task of replacing the relevant calls so that they return only not deleted messages. Finally, we'll add an interface for deleting, undeleting, and viewing deleted messages.

=== file: db/005_add_deleted_column.rb ===
    
class AddDeletedColumn < ActiveRecord::Migration
  def self.up
    add_column :message_copies, :deleted, :boolean
  end

  def self.down
    remove_column :message_copies, :deleted
  end
end

=== file: app/models/message_copy.rb ===

class MessageCopy < ActiveRecord::Base
  belongs_to :message
  belongs_to :recipient, :class_name => "User"
  belongs_to :folder
  delegate   :author, :created_at, :subject, :body, :recipients, :to => :message
  scope_out  :deleted
  scope_out  :not_deleted, :conditions => ["deleted IS NULL OR deleted = ?", false]
end

=== file: app/controllers/messages_controller.rb ===

class MessagesController < ApplicationController
  def show
    @message = current_user.received_messages.find(params[:id])
  end
  
  def destroy
    @message = current_user.received_messages.find(params[:id])
    @message.update_attribute("deleted", true)
    redirect_to inbox_path
  end
  
  def undelete
    @message = current_user.received_messages.find(params[:id])
    @message.update_attribute("deleted", false)
    redirect_to inbox_path
  end
  
  def reply
    @original = current_user.received_messages.find(params[:id])
    
    subject = @original.subject.sub(/^(Re: )?/, "Re: ")
    body = @original.body.gsub(/^/, "> ")
    @message = current_user.sent_messages.build(:to => [@original.author.id], :subject => subject, :body => body)
    render :template => "sent/new"
  end
  
  def forward
    @original = current_user.received_messages.find(params[:id])
    
    subject = @original.subject.sub(/^(Fwd: )?/, "Fwd: ")
    body = @original.body.gsub(/^/, "> ")
    @message = current_user.sent_messages.build(:subject => subject, :body => body)
    render :template => "sent/new"
  end
  
  def reply_all
    @original = current_user.received_messages.find(params[:id])
    
    subject = @original.subject.sub(/^(Re: )?/, "Re: ")
    body = @original.body.gsub(/^/, "> ")
    recipients = @original.recipients.map(&:id) - [current_user.id] + [@original.author.id] 
    @message = current_user.sent_messages.build(:to => recipients, :subject => subject, :body => body)
    render :template => "sent/new"
  end
end

=== file: app/controllers/mailbox_controller.rb ===

class MailboxController < ApplicationController
  def index
    redirect_to new_session_path and return unless logged_in?
    @folder = current_user.inbox
    show
    render :action => "show"
  end

  def show
    @folder ||= current_user.folders.find(params[:id])
    @messages = @folder.messages.paginate_not_deleted :all, :per_page => 10, :page => params[:page],
          :include => :message, :order => "messages.created_at DESC"
  end
  
  def trash
    @folder = Struct.new(:name, :user_id).new("Trash", current_user.id)
    @messages = current_user.received_messages.paginate_deleted :all, :per_page => 10, :page => params[:page],
          :include => :message, :order => "messages.created_at DESC"
    render :action => "show"
  end
end

=== file: app/views/layouts/_mailbox_list.html.erb ===

<div id="mailbox_list" style="border:1px solid #aaa; float:right; margin:1em; padding:1em; width:20%">
  <p><%= link_to "Compose", new_sent_path %></p>
  
  <p><strong>Mailboxes</strong></p>
  <ul>
    <li><%= link_to "Inbox", inbox_path %></li>
    <li><%= link_to "Sent", :controller => "sent", :action => "index" %></li>
    <li><%= link_to "Trash", trash_mailbox_path %></li>
  </ul>
</div>

=== file: app/views/messages/show.html.erb ==

<h2><%=h @message.subject %></h2>

<p><strong>From:</strong> <%=h @message.author.login %></p>
<p><strong>To:</strong> <%=h @message.recipients.map(&:login).to_sentence %></p>
<p><strong>Received:</strong> <%= @message.created_at.to_s(:long) %></p>

<pre><%=h @message.body %></pre>

<p>
  <%= link_to "Reply", reply_message_path(@message) %> |
  <%= link_to "Reply All", reply_all_message_path(@message) %> |
  <%= link_to "Forward", forward_message_path(@message) %> |
  
  <% unless @message.deleted %>
    <%= link_to "Delete", message_path(@message), :method => "delete" %>
  <% else %>
    <%= link_to "Undelete", undelete_message_path(@message), :method => "put" %>
  <% end %>
</p>

=== file: config/routes.rb ===

ActionController::Routing::Routes.draw do |map|
  map.resources :users, :sent
  map.resources :mailbox, :collection => { :trash => :get }
  map.resources :messages, :member => { :reply => :get, :forward => :get, :reply_all => :get, :undelete => :put }
  map.resource :session
  
  # Home route leads to inbox
  map.inbox '', :controller => "mailbox", :action => "index"
  
  # Install the default routes as the lowest priority.
  map.connect ':controller/:action/:id'
  map.connect ':controller/:action/:id.:format'
end

Okay, that may look long, but if you actually read it, most of it is code we wrote earlier. Only little segments here and there had to be changed: I included the entire file for completeness.

Back to Top

Work In Progress

More will come, just wait. I thought I'd post this here, even though I haven't done everything I've got planned. (Preview: Folder organization and RSS support is coming soon). I'll post a zip file of the whole thing when it's done.

Update: My apologies for not updating this in some time (nearly a year, I'm ashamed to say). Life got in the way, then I put if off a few times, and before I know it, it was almost 2009. If there's anyone reading this, know that it's a New Year's Resolution of mine to update this. In the meantime, I did take the time to go and export a ZIP file of the project as it exists now.