By NovaWave Solutions, aka manitoba98
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.
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):
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:
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.
end
Finally, we'll move on to our User model. We'll define associations for both sent and received messages, as well as folders.
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.
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
folders.find_by_name("Inbox")
end
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.
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.
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 === Rails Messenger Rails Messenger Welcome, . You are not logged in. or === file: app/views/layouts/_mailbox_list.html.erb === Mailboxes === file: app/views/sent/new.html.erb === Compose To: Subject: Body:
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 ===
Sent Messages
To
Subject
Sent
ago
=== file: app/views/sent/show.html.erb ===
Sent:
To: