TweetFollow Us on Twitter

Introduction to Ruby on Rails

Volume Number: 22 (2006)
Issue Number: 9
Column Tag: Ruby

Introduction to Ruby on Rails

by Rich Warren

Ruby on Rails (hereafter just called Rails) appears to be one of the newest hot-fad technologies for web design. It's a relatively new piece of technology, only 18 months old. And they've just released their 1.0 release. But, what is Rails?

On their web page (http://www.rubyonrails.org), they describe Rails as "a full-stack, open-source web framework in Ruby for writing real-world applications with joy and less code than most frameworks spend doing XML sit-ups" Sure, but what does that mean to the average web-app-programmer-on-the-street?

First, Rails is a web application framework. It is a collection of pre-packaged classes that work together to provide the skeleton for the entire web application. Rails automatically handles many of the tedious, repetitive tasks. For example, the ActiveRecord class automatically wraps most common database interactions. Just create the database's table, generate the corresponding model, and you will have objects to create, read, update and delete records (aka CRUD)--all without writing any code. Generate the corresponding controller, and you get ready-baked web pages for these actions (This does require a single line of code - just wait, we'll try it together). This means you spend less time churning out boilerplate, and more time implementing and improving features that make your clients happy. Less work for you, more joy for everyone.

Rails is specifically designed to implement web applications using a Model-View-Controller (MVC) architecture. This is the same basic MVC architecture used in many Cocoa applications. Basically you separate the application into independent components. The model component (represented by Rails's ActiveRecord class) requests data from and saves data to the data store. The model also validates the data and performs any necessary pre-or post-processing.

The View (represented by Rails's ActionView) displays the data. It produces the actual HTML seen in the user's browser. View files are simply *.rhtml files-basic HTML files, with additional Ruby scripting codes thrown in.

Finally the Controller (ActionController) handles incoming requests from the user's browser. It then requests the needed data from the model and funnels it to the proper view. The controller should also handle any business logic.

By keeping the components modular, it is easier to make changes to any one part of the web application, without touching the other two. You can also swap out one component for another. For example, you can easily create several different views of the same underlying data. The controller selects which view to display, based on the user's request.

Finally, Rails expects files to be stored in specific locations, and expects you to follow a set of naming conventions. For example, the table in the database always has a plural name (e.g. "messages"). The corresponding model class will be the capitalized, singular version "Message", and will be stored in app/models/message.rb. The controller (MessageController) will be stored in app/controllers/message_controller.rb. And the views (index.rhtml, new.rhtml, edit.rhtml, list.rhtml, etc.) can all be found in app/views/message/.

By following these simple guidelines, Rails can automatically find all the components in your application. This means, you do not have to create and maintain complex configuration files. Of course, you can explicitly define different relationships (for example, if you need to use a legacy database). But the whole idea behind Rails is simplicity. Or, as they say on their website DRY: Don't Repeat Yourself.

Just A Taste of Ruby

Rails is built using Ruby, a highly dynamic, completely object-oriented language that came out of Japan. Everything is a class. Even, integers (1, 9, -27) are Fixnum objects. You can call their methods (for example -27.abs or 9.size). Additionally, all classes are open. You can add your own methods. You can add methods dynamically at runtime (what some Ruby-pros refer to as Metaprogramming). You can even create a list of every object used in your application. Wrap each one in a proxy-class that overrides all of the object's public methods so it now prints "Whose your daddy?" to the standard output whenever called. I'm not sure why you'd want to...but you can.

You can redefine your ruby world until 2 + 2 = 5. You can make pigs equal to dogs. It's powerful, but a bit scary. Just remember what Spiderman said, "With great power comes great responsibility." Spiderman, right? Batman? Mary Lou Retton? Someone in tights, I'm pretty sure. The main point is, you can do these things,but you don't have to. No one's pointing a gun to your head. No one's forcing you to make crazy changes to Ruby's core classes. So just calm down. Take a deep breath. Everything's going to be OK.

As you will soon see, Ruby lends itself to a very idiomatic style of programming. You won't see a "for" loop anywhere in this tutorial. Ruby has its own, elegant way of iterating over groups. As a result, the programs may look very familiar, if you have a strong LISP background (show of hands, anyone? Hmm. That's what I thought). For the rest of us, it can seem somewhat bizarre at first. Never fear, it is quite easy to learn, and once you get the hang of it, very easy to use.

A full course in Ruby is well beyond the scope of this article. I will recommend some books and websites at the end. The following section; however, gives just a flavor of Ruby,specifically Ruby as used in Rails. It is not complete. Rather, I make the bold assumption that you have experience programming in another object-oriented language (java, Objective C, C++, and so on ), and I will only bring up those issues likely to trip you up. Or, at least, these are the things that tripped me up when I first started.

So, lets start with the source code. Ruby uses simple return characters to indicate the end of a command. You can separate commands with semicolons, if multiple commands are found on the same line. Additionally, you can freely break a command onto multiple lines, as long as the interpreter can tell from context that the command is continuing.

For example:

   print "This" + " is" + 
      " ok."

and

   print "This" + " is" + " also"  \
      + " ok"

In the second example, the backslash indicates that the command continues on the next line.

Rails also uses a standardized naming scheme. Classes begin with a capital letter, and each word in the name is capitalized with no spaces LikeThis. Variables and methods use lowercase letters, with underscores to separate words like_this. An object's instance variables begin with an at-sign @like_this. Instance variables are always private (you cannot access them directly outside the object). However, Ruby provides macros for automatically building accessor methods.

Object's method calls are similar to java: object.like_this(). There is one catch, however. As long as the arguments are clear, the parenthesis is optional. Therefore the following examples all call the like_this method of @my_object, passing in three arguments

@my_object.like_this(:one, :two, :three)
@my_object.like_this :one, :two, :three

Also note, in the above example, :one, :two and :three are symbols. Rails makes heavy use of symbols throughout. If you like, you can think of them as a specialized string. They're often used as keys in hashes.

And speaking of hashes, Rails uses two main collections for containing groups of objects: arrays and hashes. Arrays should seem familiar enough. Just remember, everything is an object (including arrays). Arrays have methods, and you can call these methods to do useful things. Hashes (sometimes called dictionaries) are key/value pairs. You create a hash using curly braces. This lets you look up the value later, using the key. Keys and values are separated as follows "key => value", and the key-value pairs are separated by commas. You access the value using square brackets, much like you would an array. But instead of holding the index, the brackets hold the key. It probably makes more sense when you look at it. The following example shows some basic hash manipulations.

   @hash = {:first_name => "John", :last_name => "Doe", :age => 23}
   @hash[:first_name]   produces "John"
   @hash[:last_name]   produces "Doe"
   @hash[:age]   produces 23

One last trick with hashes. Many of the methods in Rails accept hashes as arguments, often as a way of expressing options. If the hash is the last argument, the curly brackets are optional. So, all the following calls are equivalent.

   link_to("Send Email", {:action => "mail",  :id => @personnel})
   link_to("Send Email", :action => "mail",  :id => @personnel)
   link_to "Send Email", :action => "mail",  :id => @personnel

Ruby already allows you to format commands many different ways. Rails takes this one step forward. By aliasing methods, overloading operators and providing accessor methods, Rails often allows you to access one piece of information in a wide variety of ways. Don't let this throw you. For example, consider the following three commands.

@user.name
@user.attributes["name"]
@user[:name]
@user["name"]

Assuming @user is an instance variable containing an ActiveRecord object, and that the corresponding users table in the database has a "name" column, those commands should return the same value. There are some subtle differences, however. For example, @user.name calls the object's accessor method for the "name" attribute, while @user["name"] calls the object's read_attribute method with the "name" argument.

In general it's a good idea to use the accessors. You can tell, because the code is the shortest (always a good rule of thumb in Rails). I will follow this standard in the rest of this article. However, if you spot Rails code running around in the wild, you may see the other variations from time to time.

String literals come in two basic flavors: 'this' and "this". The first uses the literal directly. The second will process the string first. It is most often used as follows: "Welcome #{h(@name)}". #{...} is a block of Ruby code. The return value of the block will be converted into a string, then placed into the string literal.

OK, one last trick. Ruby uses a programming idiom called blocks. Blocks are sections of code either contained in curly brackets, or, for larger sections, marked by the 'do' and 'end' keywords. Blocks are typically used by methods similar to the way callback functions work. As the method executes, it calls the block one or more times. The method can even pass arguments to the block. Arguments are defined inside the beginning of the block, surrounded by bars |like_this|. Let's look at two examples.

   a = [1, 2, 3, 4]
   a.each {|value| print value}   produces 1234
   a = [[1,2],[3,4],[5,6]]
   a.each do |v1, v2|
print v1
print ","
print v2
print "--"
   end   produces 1,2--3,4--5,6--

In both cases, each method iterates over the array, passing one item at a time to the block. In the first example, the numbers are placed into the local variable "value" and are printed out. In the second case, the inner two-item arrays are passed out. The block automatically assigns the values from the inner array to the block variables v1 and v2. Again, everything is printed out,this time in a slightly more formatted way.

All right, enough of the preliminaries. Let's set up our system, and start building web applications.

Installing MySQL

Before we get started let me remind you, MySQL (like any server) potentially exposes your machine to attacks. I will lead you through the basic security, but remember I'm a programmer, not a system administrator. If you plan to leave MySQL running (and especially if you plan to actually use your machine as a server), please spend some quality time with the manuals. There's lots of information on securing MySQL both on the web and at your local bookstore.

Ok, first step: Download MySQL 5.0 from the MySQL website. Currently, you can go directly to http://dev.mysql.com/downloads/mysql/5.0.html Scroll way down the page (you probably want to search for "Mac OS X"). Eventually you will find several installer packages. Pick one,I downloaded the standard version.

The disk image I got contained four files. The first, mysql-standard-5.0.15-osx10.3-powerpc.pkg, installs the database. Install this. The second, MySQLStartupItem.pkg, will create a startup item for MySQL, causing it to launch automatically when your computer starts up. My system is old and slow enough as it is; I prefer to only start MySQL when I need it, then immediately stop it afterwards.

There is also a MySQL.prefPane. You can double click this to install a MySQL panel in your System Preferences. You might want this. It provides a convenient place to start and stop MySQL. It also sets MySQL to automatically launch on startup. However, I feel the control is rather sluggish, and it occasionally had trouble stopping MySQL on my system. So, for the purpose of this tutorial, we will run everything from the command line.

Finally, there's ReadMe.txt. Like the name says, read it., Particularly if you have trouble installing MySQL on your system. It also has useful information on using MySQL.

Dig around the MySQL site and you should find a few useful tools,in particular MySQL Administrator and MySQL Query Browser. I've used MySQL Administrator to create many databases, but it also has a few problems. Nothing you can't work around, but for now we'll stick to ye old trusty terminal.

Configuring MySQL

Let's launch MySQL. Open up Terminal and type the following:

sudo /usr/local/mysql/bin/mysqld_safe --user=mysql
ctrl -z

Type in your password. This will launch MySQL in the background. You should see the following:

Starting mysqld daemon with databases from /usr/local/mysql/data

You may have to hit return to get a new command line. If you are going to launch MySQL frequently, you might want to add the /usr/local/mysql/bin directory to your path, or alias this command.

We've got MySQL running, now lets hop inside and poke around.

   /usr/local/mysql/bin mysql -u root

Now you can control the database from the command line. First things first, let's look at the current users.

   use mysql
   select host, user from user

You should see both the root user and an anonymous user. First, let's delete the anonymous users. Of course, the response time may be different.

   delete from mysql.user where user = '';
   => Query OK, 2 rows affected (0.06 sec)

Then add a password to the root account.

   set password = password('password');
   => Query OK, 0 rows affected (0.07 sec)

This sets the password when connecting locally. If you need to remotely administer the database, you might want to set a password for remote connections. Since I'll always administer the database locally, I'll go one step further and remove the second root user. When we viewed the user table earlier, it showed two hosts. One was the local host. Use the other host name in the command below.

   delete from mysql.user where host='host_name' and user='root';
=> Query OK, 1 row affected (0.00 sec)

Now make sure you only have one user, using the local host, with an encrypted password.

select host, user, password from user;
+ -- -- -- --   + -- -- --+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --    +
| host          | user    | password                                                   |
+ -- -- -- --   + -- -- --+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --    +
| localhost     | root    |*B5B800A3935788309115348A78F7489B17AB90D                    |
+ -- -- -- --   + -- -- --+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --    +
1 row in set (0.00 sec)

Now test it by logging out and logging back in. This time you'll need to use the -p flag.

   quit
   /usr/local/mysql/bin/mysql -u root -p

MySQL will now prompt you for your password. The root user is now working and reasonably secure. We will use the root user to create our database and tables. However, we will want a special user for the web application to use.

First create the database.

   create database HoneyDo
   create database HoneyDo_test

Then create our user named hd_app. We will limit this user's rights to only allow basic database operations. While they can insert and delete data from the tables, they will not be able to change the tables themselves.

   grant select, insert, update, delete on HoneyDo.* to hd_app identified by 'user_password';
   grant select, insert, update, delete, index, alter, create, drop on HoneyDo_test.* to hd_app;

Note, we give the user more privileges on the test database. This is required for the testing suite. OK, we're done with the database for now. Go ahead and log out of mysql by typing '\q' and pressing enter.

Installing Rails

Tiger comes with Ruby already installed. You will need to install the XCode developer tools from your Tiger DVD.

First, set GCC to version 3.3. You can reset it to 4.0 once we are done.

sudo gcc_select 3.3

If you've upgraded XCode, there's an extra step. For some reason, XCode 2.2 moves the ruby header files to a different directory. The simplest solution is to just create simlinks for all the headers.

sudo ln -s /usr/lib/ruby/1.8/universal-darwin8.0/*.h /usr/lib/ruby/1.8/powerpc-darwin8.0/

For the RubyGems installation, you are going to need to download the package from the RubyGems website < http://rubyforge.org/projects/rubygems/ > - release 0.9.0 as of this writing. Download the file, extract it, and install it in the shell:

cd ~/Desktop/rubygems-0.9.0  
sudo ruby setup.rb

Once successfully installed, we can install the Rails Framework itself. Type the following command:

sudo gem install rails

You will be asked to install many files and resolve dependencies. Say yes to all of them. What this does is use the RubyGems package manager to install the Rails framework.

Unfortunately, like the symlink fix above, there are a few other things that need to be taken care of with Ruby installation on Tiger. The RubyGems contains a fix for this that replaces Tiger's rbconfig.rb file with a working one. To make the update run the following commands:

sudo gem install fixrbconfig  
sudo fixrbconfig

You'll be asked to confirm this action. (Please say 'yes'!). The final part will be to install the Ruby SQLite interface. Once again, you can use the gem package manager:

sudo gem install sqlite3

You will be given several options to choose the first option, and you're on your way.

Generating The Application

All Right! Now we're ready to create our web application. On my computer, I keep all my Rails projects in a ~/rails_dev/ directory. To create our Honey Do List project, I would just type:

   mkdir ~/rails_dev
   cd ~/rails_dev
   rails HoneyDo
   cd HoneyDo

Unless otherwise stated, the rest of the commands will be run from the ~/rails_dev/HoneyDo directory. If you have any trouble with a command, make sure you're in the right directory.

   cd ~/rails_dev/HoneyDo

Getting To Know HoneyDo

Rails is a highly organized environment. It can avoid complex configuration files by making sure there's a place for everything, and keeping everything in it's place. It might seem draconic, but it works. Having edited poorly organized websites in the past (both my own and others) I quickly began to appreciate having this structure imposed upon me.

First let's look at the app folder. This folder will contain most of the source code for the project. It is further divided into controllers, models, views and helpers, not surprising for an MVC-based framework. The Views folder is further divided, one folder per controller, plus a Layouts folder for templates.

Next look at the config folder. While Rails tries to avoid configuration files when possible, it is not always possible. We will use both config/database.yml and config/routes.rb in this tutorial.

Both the lib and vendor folders can be used to hold shared code. Lib should hold your libraries, while vendor should hold third-party code.

The script folder holds Rails' utility scripts. This includes the generator scripts for automatically building models and controllers, as well as a script to launch the WEBrick server.

The test folder holds your testing suite. If you are like me, you will spend more time writing code in the test folder than in the app folder. Briefly, the fixtures subfolder contains any text data you create. The functional and unit folders contain test suites for the controllers and models respectively. Finally the mock folder contains any mock objects (for example, a mock mail server). This allows you to run tests without sending messages to the real object.

Finally there is the public folder. Static web pages go here. There are also built-in folders to hold style sheets, javascript and images. Rails will always look for a web page first in the public folder before running any application code. This is used by the cache system, if you cache a whole page, it will be converted into a static web page, and placed in the public folder. If you want to manually clear the cache, you can simply delete these pages.

There's more hidden away in the nooks and crannies. Take some time to walk through the folders and get a feel for the layout.

Setting Up Tables

First step, we need to edit the database.yml file located in the config folder. The final version should look like this:

# MySQL (default setup).  Versions 4.1 and 5.0 are recommended.
#
# Get the fast C bindings:
#   gem install mysql
#   (on OS X: gem install mysql -- --include=/usr/local/lib)
# And be sure to use new-style password hashing:
#   http://dev.mysql.com/doc/refman/5.0/en/old-client.html
development:
  adapter: mysql
  database: HoneyDo
  username: hd_app
  password: usr_password
  socket: /tmp/mysql.sock
  # Connect on a TCP socket.  If omitted, the adapter will connect on the
  # domain socket given by socket instead.
  #host: localhost
  #port: 3306
  
# Warning: The database defined as 'test' will be erased and
# re-generated from your development database when you run 'rake'.
# Do not set this db to the same as development or production.
test:
  adapter: mysql
  database: HoneyDo_test
  username: hd_app
  password: usr_password
  socket: /tmp/mysql.sock
production:
  adapter: mysql
  database: HoneyDo
  username: hd_app
  password: usr_password
  socket: /tmp/mysql.sock

You can safely delete the rest.

For the purpose of this tutorial, we are using the same database for both development and production. This will allow us to run our application in either debug or production mode, without building a second database. Testing, however, needs its own database, since its contents will be destroyed when running the test suite.

Now create a file containing the test sql.

drop table if exists items;
create table items(
   id    int    not null auto_increment,
   title   varchar(100)   not null,
   description   text    not null,
   
   primary key (id)
   
);

We can create this table by executing the following command:

   /usr/local/mysql/bin/mysql HoneyDo -u root -p < test.sql
   /usr/local/mysql/bin/mysql HoneyDo_test -u root -p < test.sql

Now generate the model and controller for this table.

ruby script/generate model Item
ruby script/generate controller Item

Ok, let's add one line of actual code, and we're ready to go. Edit app/controllers/item_controller.rb by adding the line scaffold :item as shown below.

class ItemController < ApplicationController
   scaffold :item
end

Now make sure everything is working OK. We'll run the tests automatically created by the generated script. We'll use rake to perform the tests.

   rake 

By default, rake will run both the functional and unit tests. You should see a similar result line for each test.

   1 tests, 1 assertions, 0 failures, 0 errors

If you get any errors, something is wrong. Go over the instructions again and make sure you did not miss anything.

Testing is beyond the scope of this article. In an ideal world, you would create a thorough set of tests as you develop the application. Many developers write the tests before the code, using it as their specification for the actual code. Testing early and often will save you a lot of heartache later. Trust me.

If everything looks good, let's take this puppy out for a test drive. Open a new terminal window, and from the HoneyDo directory launch Rail's built-in server. You could run the server in the background, but it produces a lot of output. Additionally, anything you print from the application (e.g. using print or puts) will appear here. I often find it useful to watch the output while debugging. So I keep it running in its own window.

   ruby script/server

Now, point your browser to the following url http://localhost:3000. You should see the "Congratulations, you've put Ruby on Rails!" page. That means the server's working. Now try http://localhost:3000/item. Aha! Now you're seeing your default item list (currently empty). Click on the New item link, and it brings up an automatically generated form. Go ahead, add a few items to your database. Play around with the interface: look at the list, show specific items, edit them, and delete them. All the basic functions are already at your fingertips.

All this comes from the magic of scaffold. This command causes Rails to check the items table in the database, and dynamically creates pages based on its columns. Scaffold may not produce the most attractive pages. We will rewrite most of the default scaffold pages as we develop our application. But it does allow us to quickly make something functional. This is a key insight into Rails development, we quickly produce a basic application, then iteratively add improvements.

So, if these pages are dynamic, what happens if we add a new column to our table? Let's try. Log into the database as root:

   /usr/local/mysql/bin/mysql HoneyDo -u root -p

Then type the following sql command and exit.

   alter table items add date date after description;

Now go back to your browser and add a new item. Notice the nice, new combo boxes for selecting the date? Some days life is easy.

Building the Basic App

OK, time to put the toys away and build a real application. First, create a file named final.sql containing the following commands:

drop table if exists items;
create table items(
   id   int   not null auto_increment,
   title   varchar(100)    not null,
   description   text    not null,
   priority   int   not null,
   date   date   not null,
   user_id   int   not null,
   sender_id   int   not null,
   
   constraint fk_items_user foreign key (user_id) references users(id),
   constraint fk_items_sender foreign key (user_id) references users(id),
   
   
   primary key (id)
   
   );
drop table if exists users;   
create table users (
   id   int    not null auto_increment,
   login    varchar(80)   default NULL,
   password    varchar(40)   default NULL,
   primary key  (id)
);

Now load this schema into both our development and our test databases:

   mysql -u root -p HoneyDo < final.sql
   mysql -u root -p HoneyDo_test < final.sql

First we need a login system. Fortunately, there's a basic login and authentication generator available as a ruby gem. Let's install the generator:

   sudo gem install login_generator

Now build the login and authentication system. This creates the framework to create new users, and to allow users to log in. Note: this only provides a basic login framework. It does not let you assign different privileges to different users. However, it is very easy to expand this system to include access control lists. You can find examples at www.rubyonrails.com But for now, we'll stick with the basics.

   ruby script/generate login Security
   Now modify app/controllers/application.rb as follows:
require_dependency 'login_system' 
class ApplicationController < ActionController::Base
    include LoginSystem
    model :user
end

We only want valid users to access our item controller. So modify app/controlles/item_controller.rb

class ItemController < ApplicationController
   before_filter :login_required
   scaffold :item
end

If someone is not logged in, they should only have access to the login and signup actions. Let's lock down everything else. In the app/controllers/security_controller.rb add the following line:

class SecurityController < ApplicationController
  before_filter :login_required, :except => [:login, :signup]
  layout  'scaffold'

One more thing, open up the app/models/user.rb file. You need to change the salt setting from the default "change-me" value. If you are feeling particularly paranoid, you could create a random hex-only salt value for each user, which could then be appended to the password value and saved in the database.--But, I leave that as an exercise for the user.

OK, crank up the rails server and try it out. Going to any of the localhost:3000/item pages should automatically redirect you to the login screen. To create a new user, go to localhost:3000/security/signup. Once you sign up, try going to the item pages again.

If you were observant, you probably noticed that after signing in, you were sent to a pretty worthless welcome page. We could edit the welcome page. But instead, let's just send everyone directly to the item list. While we're at it, we'll also set the security index page to the list. In app/controllers/security_controller.rb change the empty welcome function, and add the following index function.

def welcome
  redirect_to (:controller => 'item', :action => 'list')
end
  
def index
  redirect_to (:controller => 'item', :action => 'list')
end

And while we're rerouting things, let's take a look at the basic routing definitions. These are found in config/routes.rb. If you open that file, you will see several lines starting with map.connect.... These determine how URLs are routed through your application. It shows that the following URL localhost:3000/item/show/1 would be routed to the show action of the ItemController, passing in a parameter id=1. The last two elements are optional. For example, localhost:3000/item/list fires ItemController's list action with no id. And localhost:3000/item fires its index action (which, in our case, defaults back to the list action).

While we're here, let's make sure that localhost:3000 requests go somewhere useful. Find the line that reads:

   # map.connect '', :controller => "welcome"

and change it to:

   map.connect '', :controller => 'item', :action => 'list'

Save and close this file. Now, delete the public/index.html file. From now on pointing your browser to localhost:3000 will send you directly to your ToDo list, or at least ask you to log in.

One last note about security. Once you log in, you will remain logged in until your session expires. There are two ways you can force your session to expire (which can be very useful when manually testing). First, close down your browser. You have to actually quit the application, not just shut the window. Second, navigate to localhost:3000/security/logout. We'll add a convenient link to the logout screen in the next section.

Iterative Improvements

So what have we done so far? We can sign-up and login to our site. We have a basic ToDo list. We can add items and delete view and edit the items in our list. But right now, all users are viewing copies of the same list. We want to sort our ToDo list by item priority, maybe make it look a bit nicer. We also need to improve the list creation/editing interface.

First let's add a template for all web pages. For the purpose of this article, we'll make it stupidly simple. And we'll use the same template for all pages.

Create a new file named template.rhtml in the app/views/layouts folder. Edit the file as follows:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
        "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
   <%= stylesheet_link_tag 'scaffold' %>
   <title><%= controller.action_name%></title>
</head>
<body>
   <%= "<p style='color: green'>#{h(@flash[:notice])}</p>" if @flash[:notice] %>
   <%= @content_for_layout %>
   <hr/>
   <div align="center">
      <%= Time.now %>
      | <%= link_to_unless_current 'Add New Item', :controller =>
         'Item', :action => 'new' %>
      | <%= link_to_unless_current 'Show ToDo List', :controller =>
         'Item', :action => 'list' %>
      | <%= link_to 'logout', :controller => 'Security',
         :action => 'logout'%>
   </div>
</body>
</html>

RHTML files are simply HTML files that can include ruby scripts. These will look quite similar to JSP scripts. Anything between "<%" and "%>" will be executed as a ruby script. "<%=" and "%>" scripts will be executed and will have the results placed into the HTML document.

Rails also includes, a number of helper functions. Here link_to() and link_to_unless_current() are helper functions. They automatically create URLs that follow the current routing rules for the given actions. Additionally, link_to_unless_current() only creates a link when you are not viewing the linked-to page.

The flash hash is used to pass messages between objects. Here, it looks for any :notice objects (used by both scaffold and security). You can see how the messages are sent by looking at the security model (app/model/security.rb).

Play around with the application a bit. See how you like the new look. If you feel adventurous, change the HTML around some. Personalize it.

Now we need to tell Rails to use this template. Add the following line to both app/controllers/item_controller.rb and app/controllers/security_controller.rb

class ItemController < ApplicationController
   before_filter :login_required
   scaffold :item
   layout 'template'

Now we need to tell Rails to use this template. Add the following line to both app/controllers/item_controller.rb and app/controllers/security_controller.rb

class ItemController < ApplicationController
   before_filter :login_required
   scaffold :item
   layout 'template'

Now we need to connect the item's foreign keys. In each item, both the user_id and the sender_id refer to valid users. In both cases, this is a one-to-may relationship. Each user can have any number of ToDo items assigned to him. Likewise, each user can send out any number of ToDo items. But each item can only have one user and one sender.

First, we will tell Rails that the items belong to both the users and the senders. To do this, modify the item model (app/models/item.rb). Add the following lines:

class Item < ActiveRecord::Base
   belongs_to :user
   belongs_to :sender, :class_name => 'User', :foreign_key => 'sender_id'

The first belongs_to() command is the typical form. However, since both foreign keys refer to the same model, we need to use a more verbose form for the second command. Here we are saying that the model's sender attribute is actually a User object, using the sender_id key from the database.

Not surprisingly, we need to call the has_many method in the user model (app/models/user.rb).

require 'digest/sha1'
# this model expects a certain database layout and it's based on the name/login pattern. 
class User < ActiveRecord::Base
  # Please change the salt to something else, 
  # Every application should use a different one 
  @@salt = 'change-me'
  cattr_accessor :salt
   has_many :todo_items, :class_name => 'Item', :foreign_key => 'user_id',
      :order => 'priority DESC, date'
   has_many :sent_items, :class_name => 'Item', :foreign_key => 'sender_id',
      :order => 'date'
   

Here both attributes are defined using the verbose form. Additionally, I've added the order option, which takes an SQL fragment. This will allow Rails to automatically order these items for us.

The last few changes should have had no visible effect on the application. Now it's time to improve the appearance of the ToDo list. First, let's override the scaffolding method in the item controller (app/controllers/item_controller.rb). Add the following function to the class definition:

   def list
      @user = @request.session[:user]
      @item_list = @user.todo_items
      @name = @user.login.capitalize
      @pages, @items = paginate(:item, :order_by => 'priority DESC, date', 
         :conditions => ['user_id = ?', @user.id])
   end

This creates three variables that we will use in the page itself. @name is the capitalized version of the user's login name. @pages will be used to create the pagination links, and @items contains an array of ToDo items. The paginate command will, by default, only allow ten items per page. If we did not want the pagination, we could have just used @user.todo_items, which would automatically return a properly-ordered list of all items for the given user.

Now let's create a helper function for the item's views. Open app/helpers/item_helper.rb and edit it as follows:

module ItemHelper
   def priority_name(index)
      case index
      
      when 0
         "<font color='green'>Lowest</font>"
      when 1
         "<font color='green'>Low</font>"
      when 2
         "<font color='yellow'>Medium</font>"
      when 3
         "<font color='red'>High</font>"
      when 4
         "<font color='red'>Urgent!</font>"
      else
         "<font color='red'>Undefined</font>"
      end
   end
end

Finally create two RHTML files in the app/views/item folder: list.rhtml and _row.rhtml.

list.rhtml:
<h1><%= h(@name) %>'s HoneyDo List:</h1>
<table border="1" cellspacing="0px" cellpadding="5px" align="center">   
   <tr bgcolor="cc9966">
      <th>Item</th>
      <th>Priority</th>
      <th>Date</th>
      <th>Sender</th>
      <th>Description</th>
      <th></th>
   </tr>
   <%= render :partial => 'row', :collection => @items %>
</table>
<p><%= @item_list.length %> Active Items 
<%= '<pages: ' if @pages.length > 1 %>
<%= pagination_links @pages %>
<%= ' >' if @pages.length > 1%></p>
_row.rhtml:
<% if (row_counter % 2) == 0 %>
   <tr bgcolor="#ccffff">
<% else %>
   <tr bgcolor="#ffff99">
<% end %>
   <td><%=h(row.title)%></td>
   <td><%=priority_name(row.priority)%></td>
   <td><%=h(row.date)%></td>
   <td><%=h(row.sender.login)%></td>
   <td><%=h(truncate(row.description, 30))%></td>
   <td>
      <%= link_to('Show', :action => 'show', :id => row.id) %> |
      <%= link_to('Edit', :action => 'edit', :id => row.id) %> |
      <%= link_to('Delete', {:action => 'destroy', :id => row.id}, 
      {:confirm => "Are you sure you want to delete #{row.title}"}) %>
   </td>
</tr>

The underscore at the front of _row.rhtml indicates that it is a partial. It cannot be displayed by itself. Rather, it is used by the list.rhtml file. The render(:partial .... :collection ...) method uses the _row.rhtml file to render each item in the @items list.

Note that before we display any text stored in the database we first escape it using the h() function. This is a standard security measure. I wish we lived in a world where we could trust our users. Unfortunately, we're not quite there yet.

The h() helper function prevents people from including malicious HTML code in their ToDo items. The h() method escapes all special HTML characters, replacing greater than and less than characters with > and <. If you want to let your users use some (relatively safe) HTML code, you can use the sanitize() function instead. Rails also includes, support for a variety of text formatters. The results of the priority_name() helper function does not need to be escaped, since we entered its HTML code.

We should similarly override item's show() method. Besides wanting to improve the layout, it will currently display the information without escaping it. However, I will leave that as an exercise for the reader.

We still need a better interface for adding and editing items. First add the following four methods to the item's controller (app/controllers/item_controller.rb).

   def new
      @item = Item.new
      @users = User.find(:all, :order => 'login')
   end
   
   def edit
      @item = Item.find(@params['id'])
      @users = User.find(:all, :order => 'login')
   end
   
   def create
      user = @request.session[:user]
      user_id = @params['item'].delete('user_id')
      
   @item = Item.new(@params['item'])
   @item.sender = user
   @item.user = User.find(user_id)
   @item.date = Time.now
   if @item.save
   flash[:notice] = 
      "#{@item.title} successfully added to #{@item.user.login}'s ToDo list!"
      redirect_to :action => 'list'
   else
      @users = User.find(:all, :order => 'login')
      render_action 'new'
   end
   end
   def save
      user = @request.session[:user]
      item_hash = @params['item']
      user_id = item_hash.delete('user_id')
      @item = Item.find(item_hash['id'])
      # don't update the sender!
   @item.user = User.find(user_id)
   # don't update the time!
      
      
      if @item.update_attributes(item_hash)
      flash[:notice] = "#{@item.title} successfully updated!"
   redirect_to :action => 'list'
   else
      @users = User.find(:all, :order => 'login')
      render_action 'edit'
   end
   end

And create both the new and edit templates (app/views/item/new.rhtml and app/views/item/edit.rhtml respectively)

new.rhtml:
<%= error_messages_for(:item, :id => 'ErrorExplanation') %>
<%= form_tag :action => 'create' %>
   <table cellpadding="5px">
      <tr>
         <td><b>Title:</b></td>
         <td><%=text_field :item, :title %></td>
      <tr>
   
      <tr>
         <td><b>Priority:</b></td>
         <td>
            <%=select :item, :priority, 
            [['Lowest', 0], ['Low', 1], 
            ['Medium', 2], ['High', 3], 
            ['Urgent!', 4]]%>
         </td>
      </tr>
      
      <tr>
         <td><b>Send To User:</b></td>
         <td>
            <%= collection_select(:item, :user_id, 
               @users, :id, :login)%>
         </td>
      </tr>
      
      </td>
         <td colspan="2">
            <p><b>Description:</b><br/>
            <%=text_area :item, :description, :cols => '60',
               :rows => '20' %></p>
            <%= submit_tag "Create ToDo Item"%>
         </td>
      </tr>
   </table>
<%= end_form_tag %>
edit.rhtml:
<%= error_messages_for(:item, :id => 'ErrorExplanation') %>
<%= form_tag :action => 'save' %>
   <%=hidden_field :item, :id %>
   <table cellpadding="5px">
      <tr>
         <td><b>Title:</b></td>
         <td><%=text_field :item, :title %></td>
      <tr>
   
      <tr>
         <td><b>Priority:</b></td>
         <td>
            <%=select :item, :priority, [['Lowest', 0],['Low', 1],
               ['Medium', 2], ['High', 3], ['Urgent!', 4]]%>
         </td>
      </tr>
      
      <tr>
         <td><b>Send To User:</b></td>
         <td>
            <%= collection_select(:item, :user_id, 
               @users, :id, :login)%>
         </td>
      </tr>
      
      </td>
         <td colspan="2">
            <p><b>Description:</b><br/>
            <%=text_area :item, :description, :cols => '60',
               :rows => '20' %></p>
            <%= submit_tag "Save Changes"%>
         </td>
      </tr>
   </table>
<%= end_form_tag %>

Notice, these templates are all quite similar. There are a few subtle differences, but you could probably move much of the code into a common partial template. Again, I'll leave that as homework. The create() and save() functions also are quite similar, but they're small enough and different enough to ignore.

All right, take a deep breath and look at what we've done. Play around with the interface. Create multiple users. Notice how easily you can assign ToDo tasks to other users. Try editing tasks, and changing the user (thus sending it off to be someone else's responsibility). Create a lot of tasks for yourself. Notice how the pagination automatically kicks in after you create the eleventh task.

One more step, and we're done. Let's add validation. Open the item model (app/modles/item.rb). Edit it as follows:

class Item < ActiveRecord::Base
   belongs_to :user
   belongs_to :sender, :class_name => 'User', :foreign_key => 'sender_id'
   
   def validate
      unless (0...5).include?(priority)
         errors.add(:priority, 'Is invalid. Must be from 0 to 4')
      end
   end
   
   validates_format_of :title, :with => /\S/, 
      :message => 'Title cannot be blank!'
   validates_presence_of :date, :message => 'Date missing!'
   validates_presence_of :user, :message => 'User missing!'
   validates_presence_of :sender, :message => 'Sender missing!'
   # Description is optional--not validated
end

First, we have added a custom validate() function. This function simply checks the priority and makes sure it is greater than or equal to 0 and less than 5. Then we use several built-in validation functions. validates_format_of() tests the title against a regular expression. Here, we're simply making sure the title has at least one non-whitespace character. Next, validates_presence_of() makes sure both the date and user attributes have non-nil values. (see figure 1.)

I haven't talked about testing in a while. Unfortunately, there's a reason for this. When we generated the login system, the generator created a decent suite of tests for us. However, as we've moved to the 1.0 release of Rails, some things have changed. One important change is to the testing system. There have been several performance-related improvements, which can (unfortunately) break older testing suites, like those automatically created by the generator.

For simplicity's sake, I'm going to use the old-style testing. To do this, we need to add the following lines to the beginning of all test case classes (or modify test/test_helper.rb).

self.use_transactional_fixtures = false
self.use_instantiated_fixtures = true

As of writing this, most of the documentation at www.rubyonrails.org still describes the older style tests. Mike Clark has a thorough description of these changes on his blog at http://www.clarkware.com/cgi/blosxom/2005/10/



Figure 1.

Again, writing tests is beyond the scope of this article. I have, however, produced a set of reasonably thorough tests for this project. These can be found in the source code in the following seven files: test/test_helper.rb, test/unit/item_test.rb, test/unit/user_test.rb, test/functional/item_controller_test.rb, test/functional/security_controller_test.rb, test/fixtures/items.yml, and test/fixtures/users.yml (whew!). The tests are organized into two test suites. Unit tests are used for checking the models. Functional tests are for checking the controller and (to a lesser extent) the view.

Note: The item controller tests do some checks on the HTML. If you make changes to the output format, you may need to change these tests. For simplicity, they are all grouped into the assert_html_check() function in test/test_helper.rb. You can run all the tests by simply typing rake. The following two commands will let you run the unit tests and the functional tests separately: rake test_units, rake test_functional.

Problems with Rails

Don't get me wrong. I really enjoy working with Rails. However, all is not sunlight and roses. One of the biggest problems is that Rails is still an emerging technology. While on the one hand it's under active development, it is also a moving target. More importantly, you may have to search for a host who supports Rails (though www.rubyonrails.org has an ever-growing list). Rails is only about 18 months old. If it manages to live up to even half its current buzz, this problem will go away.

The second problem is more surprising. Rails stores session information either in files (by default) or in a database table. While Rails automates away so many fiddling issues in web development, it doesn't do anything about this session data. If left unchecked, these files (or tables) will grow until they crash your server. This is a bad thing.

This problem can easily be solved, but it requires you to schedule chron jobs on the hosting computer. This is probably not a problem on any host who actively supports Rails, but it seems like something the framework should manage.

Ideas For The Future

We've got a functional web application, but there's definitely room for improvement, and one rather dangerous bug still lurking around.

Let's start with the bug. We're still using scaffolding when you display a single item. First, we should be able to improve the appearance of these pages. More importantly, scaffolding does not escape the text before displaying it. If a user entered dangerous html code in the item's description, scaffolding will innocently display it, possibly with disastrous results.

Bottom line, scaffolding is useful for rapid development, but I'd remove it before taking any system live.

There's a second, lesser bug. Right now, any valid user can view any item (even items that belong to other users), as long as they know (or can guess) the item's id. You can try this at home by directing your browser to the following URL :

http://localhost:3000/item/show/id_to_view

Again, if we implement the show() action, we should verify that the logged-in user and the item's user match.

Ok, enough of that. What cool features can we add?

One obvious improvement is adding permissions. A simple division would be to split users into "administrators" and "users", where administrators could access, edit, and delete other user accounts. While we're at it, there's currently no way for users to change their password.

Second, currently when you create a new item, you can send that item to any valid user (and all the users are displayed in a single, ungainly list). This is fine when you have a half-dozen users, but just won't work if you have thousands. One solution is to create an invite-only system, where each user can invite other users, and only invited users can send them ToDo items. For a more business-oriented application, you could organize the users into a hierarchy, where only your immediate supervisor can send you ToDo items. Rails' ActiveRecord can easily handle all these. Trees are a little more complex than the simple has-one or has-many relationships, but Rails takes most of the pain out of it. We don't have any way to view the items a user has sent out. That would be nice. Items themselves could be improved. We could add due dates. We could add a status and completed flags. We could group them into categories. All of this could be implemented easily with the techniques already presented in this article.

What about automatically sending email messages to the users? Many sites send an e-mail message that you must respond to when you first sign up. We could do something like that. Ruby also supports both reading and generating RSS feeds. We could display new ToDo items to a user's personalized RSS feed (though that would let anyone who could guess the URL read their ToDo items).

Last, but not least, there's Ajax. I'm sure you've heard about Ajax.It has gotten even more buzz than Rails. In a nutshell, Ajax uses fancy javascript tricks to produce web pages that respond more quickly to user actions. This means web pages act more like traditional desktop applications. A ToDo list has prime opportunities for Ajax goodness. However, as anyone who works with client-side web technologies knows, it's only good if the browser supports it. This can make writing and testing raw Ajax functions a real pain.

Fortunately, Rails comes complete with a full set of Ajax helper functions. These functions create tested Ajax code that plays nice with all the Rails components. With these, implementing an Ajax interface is almost as easy as implementing a mundane, HTML interface.

Resources

First I'd recommend checking out the Ruby on Rails website (http://www.rubyonrails.org). It's your one-stop portal into a wealth of information about both Ruby and Rails. However, I'd also like to highlight the following resources:

Books:

  • Agile Web Development with Rails

  • Programming Ruby: The Pragmatic Programmers' Guide, Second Edition

  • Ruby In A Nutshell

Learning Ruby

Other Rails Tutorials:

Podcasts:

Feeds

  • Riding Rails: feed://weblog.rubyonrails.com/xml/atom10/feed.xml

  • Ruby Code and Style: feed://www.artima.com/rubycs/feeds/rubycs.rss

    Others:


    Rich Warren lives in Honolulu, Hawaii with his wife Mika and daughter Haruko. He is a freelance writer, programmer and part-time Graduate student at the University of Hawaii in Manoa. When not playing on the beach with his daughter, he can be found writing, studying, doing research or building web applications--all on his PowerBook. You can reach Rich at rwmlist@gmail.com.

  •  

    Community Search:
    MacTech Search:

    Software Updates via MacUpdate

    Latest Forum Discussions

    See All

    Challenge those pesky wyverns to a dance...
    After recently having you do battle against your foes by wildly flailing Hello Kitty and friends at them, GungHo Online has whipped out another surprising collaboration for Puzzle & Dragons. It is now time to beat your opponents by cha-cha... | Read more »
    Pack a magnifying glass and practice you...
    Somehow it has already been a year since Torchlight: Infinite launched, and XD Games is celebrating by blending in what sounds like a truly fantastic new update. Fans of Cthulhu rejoice, as Whispering Mist brings some horror elements, and tests... | Read more »
    Summon your guild and prepare for war in...
    Netmarble is making some pretty big moves with their latest update for Seven Knights Idle Adventure, with a bunch of interesting additions. Two new heroes enter the battle, there are events and bosses abound, and perhaps most interesting, a huge... | Read more »
    Make the passage of time your plaything...
    While some of us are still waiting for a chance to get our hands on Ash Prime - yes, don’t remind me I could currently buy him this month I’m barely hanging on - Digital Extremes has announced its next anticipated Prime Form for Warframe. Starting... | Read more »
    If you can find it and fit through the d...
    The holy trinity of amazing company names have come together, to release their equally amazing and adorable mobile game, Hamster Inn. Published by HyperBeard Games, and co-developed by Mum Not Proud and Little Sasquatch Studios, it's time to... | Read more »
    Amikin Survival opens for pre-orders on...
    Join me on the wonderful trip down the inspiration rabbit hole; much as Palworld seemingly “borrowed” many aspects from the hit Pokemon franchise, it is time for the heavily armed animal survival to also spawn some illegitimate children as Helio... | Read more »
    PUBG Mobile teams up with global phenome...
    Since launching in 2019, SpyxFamily has exploded to damn near catastrophic popularity, so it was only a matter of time before a mobile game snapped up a collaboration. Enter PUBG Mobile. Until May 12th, players will be able to collect a host of... | Read more »
    Embark into the frozen tundra of certain...
    Chucklefish, developers of hit action-adventure sandbox game Starbound and owner of one of the cutest logos in gaming, has released their roguelike deck-builder Wildfrost. Created alongside developers Gaziter and Deadpan Games, Wildfrost will... | Read more »
    MoreFun Studios has announced Season 4,...
    Tension has escalated in the ever-volatile world of Arena Breakout, as your old pal Randall Fisher and bosses Fred and Perrero continue to lob insults and explosives at each other, bringing us to a new phase of warfare. Season 4, Into The Fog of... | Read more »
    Top Mobile Game Discounts
    Every day, we pick out a curated list of the best mobile discounts on the App Store and post them here. This list won't be comprehensive, but it every game on it is recommended. Feel free to check out the coverage we did on them in the links below... | Read more »

    Price Scanner via MacPrices.net

    Free iPhone 15 plus Unlimited service for $60...
    Boost Infinite, part of MVNO Boost Mobile using AT&T and T-Mobile’s networks, is offering a free 128GB iPhone 15 for $60 per month including their Unlimited service plan (30GB of premium data).... Read more
    $300 off any new iPhone with service at Red P...
    Red Pocket Mobile has new Apple iPhones on sale for $300 off MSRP when you switch and open up a new line of service. Red Pocket Mobile is a nationwide MVNO using all the major wireless carrier... Read more
    Clearance 13-inch M1 MacBook Airs available a...
    Apple has clearance 13″ M1 MacBook Airs, Certified Refurbished, available for $759 for 8-Core CPU/7-Core GPU/256GB models and $929 for 8-Core CPU/8-Core GPU/512GB models. Apple’s one-year warranty is... Read more
    Updated Apple MacBook Price Trackers
    Our Apple award-winning MacBook Price Trackers are continually updated with the latest information on prices, bundles, and availability for 16″ and 14″ MacBook Pros along with 13″ and 15″ MacBook... Read more
    Every model of Apple’s 13-inch M3 MacBook Air...
    Best Buy has Apple 13″ MacBook Airs with M3 CPUs in stock and on sale today for $100 off MSRP. Prices start at $999. Their prices are the lowest currently available for new 13″ M3 MacBook Airs among... Read more
    Sunday Sale: Apple iPad Magic Keyboards for 1...
    Walmart has Apple Magic Keyboards for 12.9″ iPad Pros, in Black, on sale for $150 off MSRP on their online store. Sale price for online orders only, in-store price may vary. Order online and choose... Read more
    Apple Watch Ultra 2 now available at Apple fo...
    Apple has, for the first time, begun offering Certified Refurbished Apple Watch Ultra 2 models in their online store for $679, or $120 off MSRP. Each Watch includes Apple’s standard one-year warranty... Read more
    AT&T has the iPhone 14 on sale for only $...
    AT&T has the 128GB Apple iPhone 14 available for only $5.99 per month for new and existing customers when you activate unlimited service and use AT&T’s 36 month installment plan. The fine... Read more
    Amazon is offering a $100 discount on every M...
    Amazon is offering a $100 instant discount on each configuration of Apple’s new 13″ M3 MacBook Air, in Midnight, this weekend. These are the lowest prices currently available for new 13″ M3 MacBook... Read more
    You can save $300-$480 on a 14-inch M3 Pro/Ma...
    Apple has 14″ M3 Pro and M3 Max MacBook Pros in stock today and available, Certified Refurbished, starting at $1699 and ranging up to $480 off MSRP. Each model features a new outer case, shipping is... Read more

    Jobs Board

    IT Systems Engineer ( *Apple* Platforms) - S...
    IT Systems Engineer ( Apple Platforms) at SpaceX Hawthorne, CA SpaceX was founded under the belief that a future where humanity is out exploring the stars is Read more
    *Apple* Systems Administrator - JAMF - Activ...
    …**Public Trust/Other Required:** None **Job Family:** Systems Administration **Skills:** Apple Platforms,Computer Servers,Jamf Pro **Experience:** 3 + years of Read more
    IT Systems Engineer ( *Apple* Platforms) - S...
    IT Systems Engineer ( Apple Platforms) at SpaceX Hawthorne, CA SpaceX was founded under the belief that a future where humanity is out exploring the stars is Read more
    Nurse Anesthetist - *Apple* Hill Surgery Ce...
    Nurse Anesthetist - Apple Hill Surgery Center Location: WellSpan Medical Group, York, PA Schedule: Full Time Sign-On Bonus Eligible Remote/Hybrid Regular Apply Now Read more
    Housekeeper, *Apple* Valley Village - Cassi...
    Apple Valley Village Health Care Center, a senior care campus, is hiring a Part-Time Housekeeper to join our team! We will train you for this position! In this role, Read more
    All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.