How to build a Rails application - Part 1
Ruby on Rails is cool. If you like coding, and like coding web applications, I recommend you give it a look. If you take a look and go “Hey, that is pretty cool”, there’s a good chance the next thing you’ll want to do is get stuck in. There are piles of tutorials out there on the web to help get you started, so what did I decide to do? Write my own! Well actually, this particular tutorial skips past the installation and configuration bits, and launches straight into some application building.
In my example, I’m building a forum. I recently came across Beast, a forum in written in less than 500 lines of code on Rails. I said to myself “I can do that! I can probably do it better too!” The results of my attempt will be documented in a series of tutorial segments, which I hope will both encourage me to see if to fruitation and encourage others to get interested in Ruby on Rails.
First off, I’m going to make a few assumptions. I’m assuming you have root access to a box running Ubuntu linux, as that’s what I’m using, and that you’ve installed the following (and more importantly, got it working):
- Ruby
- Rails
- The lightttpd webserver
- The DBMS of your choice (I use Postgresql)
Right! Let’s get cracking! First off, the Rails framework is database-driven. It’s not what you want to be building your application in if all you’re doing is coding a hit counter. We are building a forum, which will have users and topics and posts and all sorts, so that means we’re going to need databases to manage all that data. So, let’s create a blank database.
dave@shodan $ createdb -E UTF8 foris_dev
That’s created a blank database called foris_dev (“foris” is what I’m deciding to call my application at the moment), in Unicode format. You might not need to imply the encoding, and you might not even need it to be Unicode, but hey, let’s just go crazy for a moment. Now that we have a database, let’s create our Rails project.
dave@shodan $ cd ~/www
dave@shodan $ rails r-l.org.uk
Holy smokes, there it goes. You’ll see some text come right back at you; that’s Rails doing it’s thing and creating a blank framework for you to use. As long as none of those lines contain the word “ERROR”, you’re good to carry on. The next bit is a big step, but it’s an important one. We need to tell the webserver on our box how to handle Rails applications. I’ve chosen to use lighttpd, which is a bit like the FireFox to Apache’s Mozilla in the webserver world. It’s fast and “lite” and easy to configure.
dave@shodan $ sudo vim /etc/lighttpd/lighttpd.conf
$HTTP["host"] == "r-l.org.uk" {
server.document-root = "/var/www/r-l.org.uk/docs"
fastcgi.server = (
".fcgi" => ( "railsapp" => (
"min-procs" => 1,
"max-procs" => 2,
"socket" => "/tmp/r-lapp.fastcgi",
"bin-path" => "/var/www/r-l.org.uk/docs/dispatch.fcgi",
"bin-environment" => ( "RAILS_ENV" => "development" )
))
)
server.error-handler-404 = "/dispatch.fcgi"
}
Reload lighttpd…
dave@shodan $ sudo /etc/init.d/lighttpd reload
Point a browser at your webserver and hey presto, Rails is all over your screen. So let’s get rid of the placeholder and put something of our own up there. First, we need to tie our Rails app to the database we created earlier. This is done by providing one of the premade config files with a bit of info.
dave@shodan $ cd r-l.org.uk
dave@shodan $ vim config/database.yml
development:
adapter: postgresql
database: foris_dev
username: www-data
password: <password>
host: localhost
Let’s assume for a moment that our database connection works. What the hell are we going to do with it?! How about we do some of that boring database design. Put your thinking shoes on now.
So we’re making a forum. Most forums make you log in, so a table of users is probably a good idea. We want a selection of different boards to group our threads, which are going to be made up of posts. So that’s four tables. Throw in a handful of fields like timestamps and moderator status, and shove it all into an SQL file, and it’s going to look something like this:
dave@shodan $ vim db/create.sql
drop table users cascade;
drop table boards cascade;
drop table threads cascade;
drop table posts cascade;
create table users (
id serial primary key,
username varchar(32) not null unique,
hashed_password varchar(40) not null,
email varchar(128) ,
moderator boolean not null default false
);
create table boards (
id serial primary key,
title varchar(256) not null unique,
hidden boolean not null default false
);
create table threads (
id serial primary key,
board_id int not null references boards(id),
title varchar(256) not null,
sticky boolean not null default false
);
create table posts (
id serial primary key,
thread_id int not null references threads(id) on delete cascade,
user_id int not null references users(id) on delete cascade,
text text not null,
datetime timestamp not null default now()
);
Okay, that’s looks like enough to work with for now. Let’s execute our script. I choose to run mine as the webserver user, www-data.
dave@shodan $ sudo -u www-data psql foris_dev < db/create.sql
You might have noticed I added some DROP commands to the top of the script. This will generate errors in Postgresql if the tables don’t exist already to drop, but we can safely ignore those. Their purpose is to reset the database if we want to make changes during our development. So assuming that all went smoothly, now that we have the structures in place to hold our data, let’s do some real Rails. The first thing we’re going to want to initialise in our application is the set of users. So, let’s create a User model to represent a user.
dave@shodan $ ruby script/generate model User
Awesome. We’re not really doing any coding yet, just defining our data within the Rails environment. Our model isn’t going to do anything on it’s own, so let’s create a controller to look after it. We’re going to call it Login because although it will handle the CRUD functions of our user model, it will mostly be used for logging in and out.
dave@shodan $ ruby script/generate controller Login add_user delete_user list_users login logout
Still no code yet… But we’re nearly there. Our board/thread/post data is going to be more traditionally CRUDdy, so let’s use Rails’ scaffolding capability to whip us up some default actions.
dave@shodan $ ruby script/generate scaffold Board
Easy as pie. On to the next one…
dave@shodan $ ruby script/generate scaffold Thread
ERROR: The name 'Thread' is reserved by Ruby on Rails.
Holy moly! Our first error. Looks like “thread” is a reserved word. I guess there’s no way around it, we’ll have to choose a different name for our thread model.
dave@shodan $ ruby script/generate scaffold Conversation
ERROR: Before updating scaffolding from new DB schema, try creating a table for your model
Looks like our database connection is working, as it’s noticed we haven’t yet created a table to associate the model to. Fine, have it your way.
dave@shodan $ vim db/create.sql
:%s/thread/conversation/g
dave@shodan $ sudo -u www-data psql foris_dev < db/create.sql
dave@shodan $ psql foris_dev
DROP TABLE threads;
Okay, let’s try again.
dave@shodan $ ruby script/generate scaffold Conversation
dave@shodan $ ruby script/generate scaffold Post
No errors this time. We now have one database, four tables, and four models (three created using a scaffold, one semi-manually). Next thing to do is tell Rails how to handle a request for your application’s URL. What’s the default route? We probably want to display the list of boards.
dave@shodan $ vim config/routes.rb
map.connect '', :controller => "boards"
dave@shodan $ rm public/index.html
There are enough notes in the routes.rb file already to explain exactly what that line does. If we refresh our web browser, we might just notice a change. There are a couple of problems that might arise at this stage though, depending on the way you’ve set up your user permissions. You need to make sure whatever user your webserver is running as has permission to create and adjust files in the log/ and tmp/session folders. Make sure that’s the case, and OHMYGOD there’s some text on my screen. We haven’t even started coding yet! Well that’s templates for you.
There’s one final step we need to perform to ensure our models map exactly onto our existing tables. We need to establish relationships between them. This is important because it makes things dead easy later on. A quick think about how our four tables are related results with the following normalised tree:
Boards 1..* Conversations 1..* Posts *..1 Users
Let’s adjust our models and define those relationships.
dave@shodan $ vim app/models/board.rb
has_many :conversations
dave@shodan $ vim app/models/conversation.rb
belongs_to :board
has_many :posts
dave@shodan $ vim app/models/post.rb
belongs_to :conversation
belongs_to :user
dave@shodan $ vim app/models/user.rb
has_many :posts
Fabulous. Now, there are some temping looking links on our screen, which will point us towards the various CRUD actions for the board model. You could create/edit/delete a few to see what Rails has put in place for us, but don’t work too hard because we’re going to overwrite them with some SQL. We’ll add some lines to our creation script to produce some basic data to work with.
dave@shodan $ vim db/create.sql
insert into users (username, hashed_password, email, moderator) values ('dave', '<HASH>', 'dave@email.com', true);
insert into users (username, hashed_password, email) values ('test', '<HASH>', 'test@email.com');
insert into boards (title) values ('Foris Development');
insert into boards (title) values ('Off Topic');
insert into boards (title, hidden) values ('Moderator Talk', true);
insert into conversations (board_id, title) values (1, 'Topic 1');
insert into conversations (board_id, title) values (1, 'Topic 2');
insert into conversations (board_id, title) values (3, 'Who is the greatest mod?');
insert into conversations (board_id, title, sticky) values (1, 'FAQ', true);
insert into posts (conversation_id, user_id, text) values (1, 1, 'Hello this is our very first topic!');
insert into posts (conversation_id, user_id, text) values (1, 2, 'Looks Great!');
insert into posts (conversation_id, user_id, text) values (2, 1, 'This is our second topic!');
insert into posts (conversation_id, user_id, text) values (3, 1, 'I reckon Dave is.');
insert into posts (conversation_id, user_id, text) values (4, 1, 'No Flaming!');
dave@shodan $ sudo -u www-data psql foris_dev < db/create.sql
Refresh your browser, and you’ll see your updated list of boards. Hope you’re still with us, as finally we’re going to do some coding. In Ruby no less! The screen you’re looking at is called a view. You’ll have a view for any action in your controller that produces a visible result. Let’s make the show view of our board model a bit more interesting. When we open a board, we expect to see a list of threads. How do we address the list of threads associated with a particular board? This is where the relationships we created earlier make things easy!
dave@shodan $ vim app/views/boards/show.rhtml
<p>Conversations (<%= @board.conversations.size %>)</p>
<ul>
<% for conversation in @board.conversations %>
<li><%= link_to conversation.title,
:controller => "conversations",
:action => "show",
:id => conversation.id %></li>
<% end %>
</ul>
Navigate to a board, and behold! A list of threads! The above code gathered all the relevant threads from our database when we addressed them like a property of a class. Each conversation is a link which points to the show action of the conversation controller. We’ve linked conversations to boards, let’s do the same for posts to conversations.
dave@shodan $ vim app/views/boards/conversation.rhtml
<p>Posts (<%= @conversation.posts.size %>)</p>
<ul>
<% for post in @conversation.posts %>
<li><%= post.user.username %>: <%= post.text %></li>
<% end %>
</ul>
Oops, we have a conversation! How did that get there? We’ve only written a handful of lines of ruby.
That’s all for now. Next time, we’ll adjust the forms which Rails have given us to input data into our application without having to delve into our database, and we’ll tidy up the presentation. Ciao!