Learn Ruby On Rails in 4 Days
Learn Ruby On Rails in 4 Days
on Rails
While this is impressive, ‘real’ web developers know that this is smoke and mirrors. ‘Real’ applications aren’t as
simple as that. What’s actually going on beneath the surface? How hard is it to go on and build ‘real’
applications?
This is where life gets a little tricky. Rails is well documented on-line – in fact, possibly too well documented for
beginners, with over 30,000 words of on-line documentation in the format of a reference manual. What’s
missing is a roadmap (railmap?) pointing to the key pages that you need to know to get up and running in Rails
development.
This document sets out to fill that gap. It assumes you’ve got Ruby and Rails up on a PC (if you haven’t got this
far, go back and follow Curt’s article). This takes you to the end of ‘Day 1 on Rails’.
‘Day 2 on Rails’ starts getting behind the smoke and mirrors. It takes you through the ‘scaffold’ code. New
features are highlighted in bold, explained in the text, and followed by a reference to either Rails or Ruby
documentation where you can learn more.
‘Day 3 on Rails’ takes the scaffold and starts to build something recognisable as a ‘real’ application. All the time,
you are building up your tool box of Rails goodies. Most important of all, you should also be feeling comfortable
with the on-line documentation so you can continue your explorations by yourself.
‘Day 4 on Rails’ adds in another table and deals with some of the complexities of maintaining relational integrity.
At the end, you’ll have a working application, enough tools to get you started, and the knowledge of where to
look for more help.
Ten times faster? after four days on Rails, judge for yourself!
Acknowledgements: many thanks to the helpful people on the the irc channel2 and the mailing list3. The on-
line archives record their invaluable assistance as I clawed my way up the Rails and Ruby leaning curves.
Version: 2.3 using version 0.12.1 of Rails – see http://rails.homelinux.org for latest version and to download a
copy of the ToDo code. Document written and pdf file generated with OpenOffice.org 'Writer'.
Copyright: this work is copyright ©2005 John McCreesh [email protected] and is licensed under
the Creative Commons Attribution-NonCommercial-ShareAlike License. To view a copy of this license, visit
http://creativecommons.org/licenses/by-nc-sa/2.0/ or send a letter to Creative Commons, 559 Nathan Abbott
Way, Stanford, California 94305, USA.
Page 1
Day 1 on Rails
The ‘To Do List’ application
This document follows the building of a simple ‘To Do List’ application – the sort of thing you have on your
PDA, with a list of items, grouped into categories, with optional notes (for a sneak preview of what it will look
like, see Illustration 5: The ‘To Do List’ Screen on page 23).
Running rails ToDo creates a new directory ToDo\ and populates it with a series of files and subdirectories, the
most important of which are as follows:
app
contains the core of the application, split between model, view, controller, and
‘helper’ subdirectories
config
contains the database.yml file which provides details of the database to used with
the application
log
application specific logs. Note: development.log keeps a trace of every action Rails
performs – very useful for error tracking, but does need regular purging!
public
the directory available for Apache, which includes images, javascripts, and
stylesheets subdirectories
Switching to fastcgi
Unless you are patient (or have a powerful PC) you should enable fastcgi for this application
Page 3
public\.htaccess
# For better performance replace the dispatcher with the fastcgi one
RewriteRule ^(.*)$ dispatch.fcgi [QSA,L]
Versions of Rails
By the time you read this document, Rails will probably have moved on several versions. If you intend to work
through this document, check the versions installed on your PC:
W:\ToDo>gem list --local
If they are different from the versions listed below, then I would strongly advise you to download the versions
used in ‘Four Days’, e.g.:
W:\ToDo>gem install rails --version 0.12.1
This won’t break anything; Ruby’s gems library is designed to handle multiple versions. You can then force Rails
to use the ‘Four Days’ versions with the ‘To Do List’ application by specifying:
config\environment.rb (excerpt)
# Require Rails libraries.
require 'rubygems'
require_gem 'activesupport', '= 1.0.4'
require_gem 'activerecord', '= 1.10.1'
require_gem 'actionpack', '= 1.8.1'
require_gem 'actionmailer', '= 0.9.1'
require_gem 'actionwebservice', '= 0.7.1'
require_gem 'rails', '= 0.12.1'
The reason using the same versions is quite simple. ‘Four Days’ uses a lot of code generated automatically by
Rails. As Rails develops, so does this code – unfortunately, this document doesn’t (until I get round to producing
a new version!). So, make life easy for yourself, and keep to the same versions as used in ‘Four Days’. Once
you’ve finished working through ‘Four Days’, by all means go onto the latest and greatest Rails versions and see
what improvements the Rails developers have come up with.
config\database.yml (excerpt)
development:
adapter: mysql
database: todos
host: localhost
username: foo
password: bar
MySQL definition
Categories table
CREATE TABLE `categories` (
`id` smallint(5) unsigned NOT NULL auto_increment,
Page 4
`category` varchar(20) NOT NULL default '',
`created_on` timestamp(14) NOT NULL,
`updated_on` timestamp(14) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `category_key` (`category`)
) TYPE=MyISAM COMMENT='List of categories';
• underscores in field names will be changed to spaces by Rails for ‘human friendly’ names
• beware mixed case in field names – some parts of the Rails code have case sensitivities
• every table should have a primary key called ‘id’ - in MySQL it’s easiest to have this as numeric
auto_increment
• links to other tables should follow the same ‘_id’ naming convention
• Rails will automatically maintain fields called created_at/created_on or updated_at/updated_on, so it’s
a good idea to add them in
Documentation: ActiveRecord::Timestamp
• Useful tip: if you are building a multi-user system (not relevant here), Rails will also do optimistic locking if
you add a field called lock_version (integer default 0). All you need to remember is to include
lock_version as a hidden field on your update forms.
Documentation: ActiveRecord::Locking
Data Model
Generate an empty file:
W:\ToDo>
which creates an empty category.rb, and two test files category_controller_test.rb and
categories.yml. We’ll make some entries in the data model in a minute – leave it empty just now.
Scaffold
The controller is at the heart of a Rails application.
Running the generate controller script
W:\ToDo>ruby script/generate controller category
exists app/controllers/
exists app/helpers/
create app/views/category
exists test/functional/
create app/controllers/category_controller.rb
create test/functional/category_controller_test.rb
create app/helpers/category_helper.rb
W:\ToDo>
If you haven’t already seen the model / scaffold trick in operation in a beginner’s tutorial like Rolling with Ruby on
Rails, try it now and amazed yourself how a whole web app can be written in one line of code:
Page 5
app\controllers\category_controller.rb
class CategoryController < ApplicationController
scaffold :category
end
Documentation: ActionController::Scaffolding
To find out how clever it is not, try adding the same new category twice. Rails will collapse with a messy error
message ‘ActiveRecord::StatementInvalid in Category#create’. You can fix this by adding validation into the
Model.
Page 6
To try this out, now try to insert a duplicate record again. This time, Rails handles the error rather than crashing
- see below. The style is a bit in your face – it's not the most subtle of user interfaces. However, what do you
expect for free?
Page 7
Day 2 on Rails
To progress beyond this point, we need to see what’s happening behind the scenes. During day 2, we will work
systematically through the scaffold code generated by Rails, deciphering what it all means. With the scaffold
action, Rails generates all the code it needs dynamically. By running scaffold as a script, we can get all the code
written to disk where we can investigate it and then start tailoring it to our requirements.
Running the generate scaffold script
W:\ToDo>ruby script/generate scaffold category
dependency model
exists app/models/
exists test/unit/
exists test/fixtures/
skip app/models/category.rb
skip test/unit/category_test.rb
skip test/fixtures/categories.yml
exists app/controllers/
exists app/helpers/
create app/views/categories
exists test/functional/
create app/controllers/categories_controller.rb
create test/functional/categories_controller_test.rb
create app/helpers/categories_helper.rb
create app/views/layouts/categories.rhtml
create public/stylesheets/scaffold.css
create app/views/categories/list.rhtml
create app/views/categories/show.rhtml
create app/views/categories/new.rhtml
create app/views/categories/edit.rhtml
create app/views/categories/_form.rhtml
W:\ToDo>
This script generates a range of files needed to create a complete application, including a controller, views,
layouts, and even a style sheet.
Note the slightly bizarre naming convention – we've moved from the singular to the plural, so to use the new
code you need to point your browser at http://todo/categories. In fact, to avoid confusion, it’s best to
delete app\controllers\category_controller.rb etc in case you run it accidentally.
def list
@category_pages, @categories = paginate :category, :per_page => 10
end
def show
@category = Category.find(@params[:id])
end
def new
Page 9
@category = Category.new
end
def create
@category = Category.new(@params[:category])
if @category.save
flash['notice'] = 'Category was successfully created.'
redirect_to :action => 'list'
else
render_action 'new'
end
end
def edit
@category = Category.find(@params[:id])
end
def update
@category = Category.find(@params[:id])
if @category.update_attributes(@params[:category])
flash['notice'] = 'Category was successfully updated.'
redirect_to :action => 'show', :id => @category
else
render_action 'edit'
end
end
def destroy
Category.find(@params[:id]).destroy
redirect_to :action => 'list'
end
end
When the user of a Rails application selects an action – e.g. ‘Show’ - the controller will execute any code in the
appropriate section – ‘def show’ - and then by default will render a template of the same name - ‘show.rthml’.
This default behaviour can be overwritten:
• render_template allows you to render a different template – e.g. the index action will run the code for
‘list’ - ‘def list’, and will then render list.rhtml rather than index.rhtml (which doesn’t exist)
• redirect_to goes one stage further, and uses an external ‘302 moved’ HTTP response to loop back into the
controller – e.g. the destroy action doesn’t need to render a template. After performing its main purpose
(destroying a category), it simply takes the user to the list action.
Documentation: ActionController::Base
The controller uses ActiveRecord methods such as find, find_all, new, save, update_attributes, and
destroy to move data to and from the database tables. Note that you do not have to write any SQL statements,
but if you want to see what SQL Rails is using, it’s all written to the development.log file.
Documentation: ActiveRecord::Base
Notice how one logical activity from the user’s perspective may require two passes through the controller: for
example, updating a record in the table. When the user selects ‘Edit’, the controller extracts the record they
want to edit from the model, and then renders the edit.view. When the user has finished editing, the edit view
invokes the update action, which updates the model and then invokes the show action.
The View
Views are where the user interface are defined. Rails can render the final HTML page presented to the user from
three components:
Page 10
Layout Template Partial
in app\views\layouts\ in app\views\<controller>\ in app\views\<controller>\
default: application.rhtml default: <action>.rhtml default _<partial>.rhtml
or <controller>.rhtml
• A Layout provides common code used by all actions, typically the start and end of the HTML sent to the
browser.
• A Template provides code specific to an action, e.g. ‘List’ code, ‘Edit’ code, etc.
• A Partial provides common code - ‘subroutines’ - which can be used in used in multiple actions – e.g. code
used to lay out tables for a form.
Layout
Rails Naming conventions: if there is a template in app\views\layouts\ with the same name as the current
controller then it will be automatically set as that controller’s layout unless explicitly told otherwise.
A layout with the name application.rhtml or application.rxml will be set as the default controller if there
is no layout with the same name as the current controller, and there is no layout explicitly assigned.
</body>
</html>
This is mostly HTML, plus a few bits of Ruby code embedded within <% %> tags. This layout will be called by
the rendering process regardless of the action being run. It contains the standard HTML tags – the
<html><head>...</head><body>...</body></html> that will appear on every page.
The Ruby bits in bold are translated into HTML during the Rails rendering process as follows:
• action_name is an ActionController method which returns the name of the action the controller is
processing (e.g. ‘List’) - this puts an appropriate title on the page, depending on the action being run.
Documentation: ActionController::Base
• stylesheet_link_tag is a Rails helper - a lazy way of generating code. There are a lot of these ‘helpers’
within Rails. This one simply generates the following HTML: <link
href="/stylesheets/scaffold.css" media="screen" rel="Stylesheet" type="text/css" />
Documentation: ActionView::Helpers::AssetTagHelper
• content_for_layout is the key to what happens next. It allows a single standard layout to have dynamic
content inserted at rendering time based on the action being performed (e.g. ‘edit’, ‘new’, ‘list’). This dynamic
content comes from a Template with the same name – see below.
Documentation: ActionController::Layout::ClassMethods.
Template
Rails naming convention: templates are held in app\views\categories\‘action’.rhtml.
Page 11
The new.rhtml created by the scaffold script is given below:
app\views\categories\new.rhtml
<h1>New category</h1>
• link_to simply creates a link – the most fundamental part of HTML... <a
href="/categories/list">Back</a>
Documentation: ActionView::Helpers::UrlHelper
Partial
Rails naming convention: a partial ‘foo’ will go in a file app\views\‘action’\_foo.rhtml (note the initial
underscore).
The scaffold uses the same code to process both the ‘edit’ and ‘new’ actions, so it puts the code into a partial,
invoked by the render_partial method.
app\views\categories\_form.rhtml
<%= error_messages_for 'category' %>
<!--[form:category]-->
<p><label for="category_category">Category</label><br/>
<%= text_field 'category', 'category' %></p>
• error_messages_for returns a string with marked-up text for any error messages produced by a previous
attempt to submit the form. If one or more errors is detected, the HTML looks like this:
<div class="errorExplanation" id="errorExplanation">
<h2>n errors prohibited this xxx from being saved</h2>
<p>There were problems with the following fields:</p>
<ul>
<li>field_1 error_message_1</li>
<li>... ...</li>
<li>field_n error_message_n</li>
</ul>
</div>
We saw this in action on Day 1 - Illustration 2: Capturing data errors on page 7. Note: the css tags match
Page 12
corresponding statements in the stylesheet created by the generate scaffold script.
Documentation: ActionView::Helpers::ActiveRecordHelper
Note a little bug in Rails – it knows not to create input fields for the reserved field names created_on and
updated_on, but it still generates labels for them.
<h1>New category</h1>
<!--[form:category]-->
<p><label for="category_category">Category</label><br/>
<input id="category_category" name="category[category]" size="30" type="text" value=""
/></p>
<a href="/categories/list">Back</a>
</body>
</html>
paginate populates the @categories instance variable with sorted records from the Categories table, :per_page
records at a time, and contains all the logic for next page / previous page etc. navigation. @category_pages is a
Paginator instance. How these are used in the template is explained at the end of the following section.
Documentation: ActionController::Pagination
Page 13
The template is as follows:
app\views\categories\list.rhtml
<h1>Listing categories</h1>
<table>
<tr>
<% for column in Category.content_columns %>
<th><%= column.human_name %></th>
<% end %>
</tr>
<br />
• content_columns returns an array of column objects excluding any ‘special’ columns (the primary id, all
columns ending in ‘_id’ or ‘_count’, and columns used for single table inheritance)
Documentation: ActionController::Base
• human_name is a synonym for human_attribute_name, which transforms attribute key names into a more
human format, such as ‘First name’ instead of ‘first_name’
Documentation: ActiveRecord::Base
• h automatically ‘escapes’ HTML code. One of the problems with allowing users to input data which is then
displayed on the screen is that they could accidentally (or maliciously) type in code which could break the
system when it was displayed4. To guard against this, it is good practice to ‘HTML escape’ any data which has
been provided by users. This means that e.g. </table> is rendered as </table> which is harmless.
Rails makes this really simple – just add an ‘h’ as shown
• confirm is a useful optional parameter for the link_to helper – it generates a Javascript pop-up box which
forces the user to confirm the Destroy before actioning the link:
4 For example, think what would happen if a user typed in “</table>” as a Category.
Page 14
Documentation: ActionView::Helpers::UrlHelper
The paging logic takes a bit of unravelling.. Ruby can use if as a modifier: expression if boolean-
expression evaluates expression only if boolean-expression is true. @category_pages.current returns
a Page object representing the paginator’s current page
ActionController::Pagination::Paginator
and @category_pages.current.previous returns a new Page object representing the page just before this
page, or nil if this is the first page
ActionController::Pagination::Paginator::Page
So, if there is a previous page to navigate to, then this construct will display a link; if there isn’t, the link is
suppressed.
The rendered code for page n will look like:
<a href="/categories/list?page=[n-1]">Previous page</a>
<a href="/categories/list?page=[n+1]">Next page</a>
The Controller
In a ‘List’ view, I would expect the records to be displayed in alphabetical order. This requires a minor change to
the controller:
app\controllers\categories_controller.rb (excerpt)
def list
@category_pages, @categories = paginate :category,
:per_page => 10, :order_by => 'category'
end
Documentation: ActionController::Pagination
In this application, the show screen is unnecessary – all the fields fit comfortably on a single row on the screen.
So, def show can disappear, and let’s go straight back to the list screen after an ‘Edit’:
app\controllers\categories_controller.rb (excerpt)
def update
@category = Category.find(@params[:id])
if @category.update_attributes(@params[:category])
flash['notice'] = 'Category was successfully updated.'
redirect_to :action => 'list'
else
render_action 'edit'
end
end
The flash message will be picked up and displayed on the next screen to be displayed – in this case, the list
screen. By default, the scaffold script doesn’t display flash messages - we’ll change this in a minute – see below.
The View
Displaying Flash Messages
Rails provides a technique for passing ‘flash’ messages back to the user – e.g. an ‘Update Successful’ message
which displays on the next screen and then disappears. These can be picked up easily with a small change to the
Layout (adding it to the Layout means it will appear on any screen):
Page 15
app\views\layouts\categories.rhtml<html>
<head>
<title>Categories: <%= controller.action_name %></title>
<%= stylesheet_link_tag 'scaffold' %>
</head>
<body>
<h1><%=@heading %></h1>
<% if @flash["notice"] %>
<span class="notice">
<%=h @flash["notice"] %>
</span>
<% end %>
<%= @content_for_layout %>
</body>
</html>
Documentation: ActionController::Flash
A simple addition to the stylesheet makes the flash message more conspicuous:
public\stylesheets\scaffold.css (excerpt)
.notice {
color: red;
}
I’ve made this change and some formatting changes to come up with my finished template:
app\views\categories\list.rhtml
<% @heading = "Categories" %>
<table>
<tr>
<th>Category</th>
<th>Created</th>
<th>Updated</th>
</tr>
<% for category in @categories %>
<tr>
<td><%=h category["category"] %></td>
<td><%= category["created_on"].strftime("%I:%M %p %d-%b-%y") %></td>
<td><%= category["updated_on"].strftime("%I:%M %p %d-%b-%y") %></td>
<td><%= link_to 'Edit', :action => 'edit', :id => category %></td>
<td><%= link_to 'Delete', {:action => 'destroy', :id => category},
:confirm => "Are you sure you want to delete this category?" %></td>
</tr>
<% end %>
</table>
<br />
<%= link_to 'New category', :action => 'new' %>
<% if @category_pages.page_count>1 %>
<hr />
Page: <%=pagination_links @category_pages %>
<hr />
<% end %>
• I don’t like the default date format, so I use a Ruby method strftime() to format the date and time fields
the way I want them.
Ruby Documentation: class Time
Page 16
• pagination_links creates a basic HTML link bar for the given paginator
ActionView::Helpers::PaginationHelper
and a few minor changes to the two templates (note in particular the use of @heading)::
app\views\categories\Edit.rhtml
<% @heading = "Edit Category" %>
<%= start_form_tag :action => 'update', :id => @category %>
<%= render_partial "form" %>
<hr />
<%= submit_tag "Save" %>
<%= end_form_tag %>
<%= link_to 'Back', :action => 'list' %>
app\views\categories\New.rhtml
<% @heading = "New Category" %>
<%= start_form_tag :action => 'create' %>
<%= render_partial "form" %>
<hr />
<%= submit_tag "Save" %>
<%= end_form_tag %>
<%= link_to 'Back', :action => 'list' %>
That takes us to the end of Day 2. We have a working system for maintaining our Categories table, and have
started to take control of the scaffold code which Rails has generated.
Page 17
Day 3 on Rails
Now it’s time to start on the heart of the application. The Items table contains the list of ‘To Dos’. Every Item
may belong to one of the Categories we created on Day 2. An Item optionally may have one Note, held in a
separate table, which we will look at tomorrow. Each table has a primary key ‘id’, which is also used to record
links between the tables.
Items table
CREATE TABLE items (
id smallint(5) unsigned NOT NULL auto_increment,
done tinyint(1) unsigned NOT NULL default '0',
priority tinyint(1) unsigned NOT NULL default '3',
description varchar(40) NOT NULL default '',
due_date date default NULL,
category_id smallint(5) unsigned NOT NULL default '0',
note_id smallint(5) unsigned default NULL,
private tinyint(3) unsigned NOT NULL default '0',
created_on timestamp(14) NOT NULL,
updated_on timestamp(14) NOT NULL,
PRIMARY KEY (id)
) TYPE=MyISAM COMMENT='List of items to be done';
The Model
As before, Rails can generate an empty model file:
W:\ToDo>ruby script/generate model item
exists app/models/
exists test/unit/
exists test/fixtures/
create app/models/item.rb
create test/unit/item_test.rb
create test/fixtures/items.yml
W:\ToDo>
Page 19
which we can populate:
app\models\item.rb
class Item < ActiveRecord::Base
belongs_to :category
validates_associated :category
validates_format_of :done_before_type_cast, :with => /[01]/, :message=>"must be 0 or
1"
validates_inclusion_of :priority, :in=>1..5, :message=>"must be between 1 (high) and
5 (low)"
validates_presence_of :description
validates_length_of :description, :maximum=>40
validates_format_of :private_before_type_cast, :with => /[01]/, :message=>"must be 0
or 1"
end
The Model
Generate the empty model file, but it contains nothing new:
app\models\note.rb
class Note < ActiveRecord::Base
validates_presence_of :more_notes
end
Page 20
but we need to remember to add this link into the Items model:
app\models\item.rb (excerpt)
class Item < ActiveRecord::Base
belongs_to :note
This reads: before you delete an Item record, find the record in Notes whose id equals the value of Note_id in
the Item record you are about to delete, and delete it first. Unless there isn’t one :-)
Similarly, if a record is deleted from the Notes table, then any reference to it in the Items table needs to be
erased:
app\models\note.rb (excerpt)
def before_destroy
Item.find_by_note_id(id).update_attribute('note_id',NIL)
end
end
Documentation: ActiveRecord::Callbacks
More Scaffolding
Let’s generate some more scaffold code. We’ll do this for both the Items table and the Notes table. We aren’t
ready to work on Notes as yet, but having the scaffold in place means we can refer to Notes in today’s coding
without generating lots of errors. Just like building a house – scaffolding allows you to build one wall at a time
without everything crashing around your ears.
Note: as we tailored the stylesheet yesterday, reply “n” to the “overwrite public/stylesheets/scaffold.css? [Ynaq]”
prompt.
More on Views
Creating a Layout for the Application
By now, it is becoming obvious that all my templates will have the same first few lines of code, so it makes sense
to move this common code into an application-wide layout. Delete all the app\views\layouts\*.rhtml files,
Page 21
and replace with a common application.rhtml.
app\views\layouts\application.rhtml
<html>
<head>
<title><%= @heading %></title>
<%= stylesheet_link_tag 'todo' %>
<script language="JavaScript">
<!-- Begin
function setFocus() {
if (document.forms.length > 0) {
var field = document.forms[0];
for (i = 0; i < field.length; i++) {
if ((field.elements[i].type == "text") || (field.elements[i].type == "textarea")
|| (field.elements[i].type.toString().charAt(0) == "s")) {
document.forms[0].elements[i].focus();
break;
}
}
}
}
// End -->
</script>
</head>
<body OnLoad="setFocus()">
<h1><%=@heading %></h1>
<% if @flash["notice"] %>
<span class="notice">
<%=h @flash["notice"] %>
</span>
<% end %>
<%= @content_for_layout %>
</body>
</html>
The @heading set in the Template is now used for the <title> as well as <h1>. I’ve renamed the
public/stylesheets/scaffold.css to todo.css for tidiness, and also generally played with colours, table
borders, to give a prettier layout. I’ve also added in a little Javascript to automatically position the cursor in the
first input field in the browser ready for the user to start typing.
8 It’s amazing what a few lines in a stylesheet can do to change the appearance of a screen, plus of course a collection of
icons...
Page 22
Illustration 5: The ‘To Do List’ Screen
Page 23
Clicking ‘OK’ will invokes the purge_completed method. This new purge_completed method needs to be
defined in the controller:
app\controllers\items_controller.rb (excerpt)
def purge_completed
Item.destroy_all "done = 1"
redirect_to :action => 'list'
end
Item.destroy_all deletes all the records in the Items table where the value of the field done is 1, and then
reruns the list action.
Documentation: ActiveRecord::Base
def list_by_priority
@item_pages, @items = paginate :item,
:per_page => 10, :order_by => 'priority,due_date'
render_action 'list'
end
We’ve specified a sort order for the default list method, and created a new list_by_priority method9. Note
also that we need to explicitly render_action 'list', as by default Rails would try to render a template called
list_by_priority (which doesn’t exist :-)
Adding a Helper
The headings for the Note and Private columns are images, but are not clickable. I decided to write a little
method show_image(name) to just show the image:
app\helpers\application_helper.rb
module ApplicationHelper
def self.append_features(controller)
controller.ancestors.include?(ActionController::Base) ?
controller.add_template_helper(self) : super
end
def show_image(src)
img_options = { "src" => src.include?("/") ? src : "/images/#{src}" }
img_options["src"] = img_options["src"] + ".png" unless
img_options["src"].include?(".")
img_options["border"] = "0"
tag("img", img_options)
end
end
9 list_by_description and list_by_category are similar and are left as an easy exercise for the reader.
However, if you get stuck with list_by_category, see Still to be done on page 39
Page 24
it is available for all the templates in the application.
Documentation: ActionView::Helpers
Rails also passes a sequential number list_stripes_counter to the Partial. This is the key to formatting
alternate rows in the table with either a light grey background or a dark grey background. One way is simply to
test whether the counter is odd or even: if odd, use light gray; if even, use dark gray.
A little bit of Ruby is used to test if the counter is odd or even and render either class=“dk_gray” or
class=“lt_gray”:
list_stripes_counter.modulo(2).nonzero? ? "dk_gray" : "lt_gray"
the code as far as the first question mark asks: is the remainder when you divide list_stripes_counter by 2 nonzero?
Ruby Documentation: class Numeric
The remainder of the line is actually a cryptic if then else expression which sacrifices readability for brevity: if the
Page 25
expression before the question mark is true, return the value before the colon; else return the value after the colon.
Ruby Documentation: Expressions
The two tags dk_gray and lt_gray are then defined in the stylesheet:
public\stylesheets\ToDo.css (excerpt)
.lt_gray { background-color: #e7e7e7; }
.dk_gray { background-color: #d6d7d6; }
Note: the same if then else construct is used to display the ‘tick’ icon if list_stripes["done"]equals 1,
otherwise display an HTML blank space character:
OK. if you’ve followed this so far, you should have a ‘To Do List’ screen looking something like Illustration 5
The ‘To Do List’ Screen on page 23.
Page 26
<table>
<%= render_partial "form" %>
</table>
<hr />
<%= submit_tag "Save" %>
<%= submit_tag "Cancel", {:type => 'button', :onClick=>"parent.location='" + url_for(
:action => 'list' ) + "'" } %>
<%= end_form_tag %>
and the real work is done in the partial, where it can be shared with the ‘Edit’ action:
app\views\items\_form.rhtml
<tr>
<td><b>Description: </b></td>
<td><%= text_field "item", "description", "size" => 40, "maxlength" => 40
%></td>
</tr>
<tr>
<td><b>Date due: </b></td>
<td><%= date_select "item", "due_date", :use_month_numbers => true %></td>
</tr>
<tr>
<td><b>Category: </b></td>
<td><select id="item_category_id" name="item[category_id]">
<%= options_from_collection_for_select @categories, "id", "category",
@item.category_id %>
</select>
</td>
</tr>
<tr>
<td><b>Priority: </b></td>
<% @item.priority = 3 %>
<td><%= select "item","priority",[1,2,3,4,5] %></td>
</tr>
<tr>
<td><b>Private? </b></td>
<td><%= check_box "item","private" %></td>
</tr>
<tr>
<td><b>Complete? </b></td>
<td><%= check_box "item", "done" %></td>
</tr>
Page 27
redirect_to :action => 'new'
end
end
Ruby Documentation: Exceptions, Catch, and Throw
options_from_collection_for_select reads all the records in categories and renders them as <option
value=”[value of id]”>[value of category]</option>. The record that matches @item_category_id
will be tagged as ‘selected’. As is this wasn’t enough, the code even html_escapes the data for you. Neat.
Documentation: ActionView::Helpers::FormOptionsHelper
Note that data driven drop down boxes have to get their data from somewhere – which means an addition to the
controller:
app\controllers\items_controller.rb (excerpt)
def new
@categories = Category.find_all
@item = Item.new
end
def edit
@categories = Category.find_all
@item = Item.find(@params[:id])
end
Creating a Checkbox
Another regular requirement; another helper in Rails:
check_box "item","private"
Documentation: ActionView::Helpers::FormHelper
Finishing Touches
Tailoring the Stylesheet
At this point, the ‘To Do List’ screen should work, and so should the ‘New To Do’ button. To produce the
screens shown here, I also made the following changes to the stylesheet:
public\stylesheets\ToDo.css
body { background-color: #c6c3c6; color: #333; }
.notice {
color: red;
background-color: white;
}
Page 28
h1 {
font-family: verdana, arial, helvetica, sans-serif;
font-size: 14pt;
font-weight: bold;
}
table {
background-color:#e7e7e7;
border: outset 1px;
border-collapse: separate;
border-spacing: 1px;
}
Which takes us to the end of Day 3 – and the application now looks nothing like a Rails scaffold, but under the
surface, we’re still using a whole range of Rails tools to make development easy.
10 But unlike my college text book authors, I do reveal the answers on Day 4 :-) - see app\views\items\edit.rhtml on page 31
Page 29
Day 4 on Rails
The ‘Notes’ screens
Linking ‘Notes’ to the ‘Edit To Do’
Although the Notes scaffold code gives the full CRUD facilities, we don’t want the user to invoke any of this
directly. Instead, if an Item has no associated Note, we want to be able to create one by clicking on a Notes icon
on the Edit To Do screen:
If a Note already exists, we want to edit or delete it by clicking on the appropriate icon on the Edit To Do
screen:
First of all, let’s look at the code for the ‘Edit To Do’ screen. Note how the Notes buttons change according to
whether a Note already exists, and how control is transferred to the Notes controller:
app\views\items\edit.rhtml
<% @heading = "Edit To Do" %>
<%= error_messages_for 'item' %>
<%= start_form_tag :action => 'update', :id => @item %>
<table>
<%= render_partial "form" %>
Page 31
<tr>
<td><b>Notes: </b></td>
<% if @item.note_id.nil? %>
<td>None</td>
<td><%= link_to_image "note", :controller => "notes", :action => "new", :id =>
@item.id %></td>
<% else %>
<td><%=h @item.note.more_notes %></td>
<td><%= link_to_image "edit_button", :controller => "notes", :action => "edit",
:id => @item.note_id %></td>
<td><%= link_to_image "delete_button", {:controller => "notes", :action =>
"destroy", :id => @item.note_id }, :confirm => "Are you sure you want to delete this
note?" %></td>
<% end %>
</tr>
</table>
<hr />
<%= submit_tag "Save" %>
<%= submit_tag "Cancel", {:type => 'button', :onClick=>"parent.location='" + url_for(
:action => 'list' ) + "'" } %>
<%= end_form_tag %>
Once the update or destroy of the Notes table is complete, we want to return to the ‘To Do List’ screen:
app\controllers\notes_controller.rb (excerpt)
def update
@note = Note.find(@params[:id])
if @note.update_attributes(@params[:note])
flash['notice'] = 'Note was successfully updated.'
redirect_to :controller => 'items', :action => 'list'
else
render_action 'edit'
end
end
def destroy
Note.find(@params[:id]).destroy
redirect_to :controller => 'items', :action => 'list'
end
Remember that the referential integrity rules we have already created will ensure that when a Note is deleted, any
references to it in Items will be removed too (see Using a Model to maintain Referential Integrity on page 21).
Page 32
• store the new note in the Notes table
• find the id of the newly created record in the Notes table
• record this id back in the notes_id field of the associated record in the Items table
Session variables provide a useful way of persisting data between screens – we can use them here to store the Id
of the record in the Notes table.
Documentation: ActionController::Base
The new method in the Notes controller stores this away in a session variable:
app\controllers\notes_controller.rb (excerpt)
def new
@session[:item_id] = @params[:id]
@note = Note.new
end
The create method retrieves the session variable again and uses it to find the record in the Items table. It then
updates the note_id in the Item table with the id of the record it has just created in the Note table, and returns
to the Items controller again:
app\controllers\notes_controller.rb (excerpt)
def create
@note = Note.new(@params[:note])
if @note.save
flash['notice'] = 'Note was successfully created.'
@item = Item.find(@session[:item_id])
@item.update_attribute(:note_id, @note.id)
redirect_to :controller => 'items', :action => 'list'
else
render_action 'new'
end
end
Page 33
<th>Created</th>
<th>Updated</th>
</tr>
<% for category in @categories %>
<tr>
<td><%=h category["category"] %></td>
<td><%= category["created_on"].strftime("%I:%M %p %d-%b-%y") %></td>
<td><%= category["updated_on"].strftime("%I:%M %p %d-%b-%y") %></td>
<td><%= link_to_image 'edit', { :action => 'edit', :id => category.id } %></td>
<td><%= link_to_image 'delete', { :action => 'destroy', :id => category.id },
:confirm => 'Are you sure you want to delete this category?' %></td>
</tr>
<% end %>
</table>
<hr />
<input type="submit" value="New Category..." />
<input type="button" value="To Dos" onClick="parent.location='<%= url_for(
:controller => 'items', :action => 'list' ) %>'">
</form>
app\views\categories\new.rhtml
<% @heading = "Add new Category" %>
<%= error_messages_for 'category' %>
<%= start_form_tag :action => 'create' %>
<%= render_partial "form" %>
<hr />
<input type="submit" value="Save" />
<input type="button" value="Cancel" onClick="parent.location='<%= url_for( :action
=> 'list' ) %>'">
<%= end_form_tag %>
app\views\categories\edit.rhtml
<% @heading = "Rename Category" %>
<%= error_messages_for 'category' %>
<%= start_form_tag :action => 'update', :id => @category %>
<%= render_partial "form" %>
<hr />
<input type="submit" value="Update" />
<input type="button" value="Cancel" onClick="parent.location='<%= url_for( :action
=> 'list' ) %>'">
<%= end_form_tag %>
Page 34
New
ToDo
New
Category
List
ToDos New
Note
List
Categories
Edit
Edit Note
ToDo
Edit
Category
and finally
I hope you found this document useful – I’m always happy to receive feedback, good or bad, to
[email protected].
Page 35
Appendix – afterthoughts
After writing ‘Four Days’, I got a huge amount of feedback which greatly helped improve the quality of the
document. One question did crop up repeatedly - “how do you update more than one record from the same
screen” - so here’s an appendix covering this most Frequently Asked Question. It isn’t the easiest Rails concept
to grasp, and it’s an area I would expect to see more “Helpers” appearing in the future.
Multiple Updates
In the screenshot below, the user can tick/untick multiple “To Dos” using the checkboxes in the extreme left
hand column, and then press “Save” to store the results in the database.
Let’s work backwards from the HTML we are trying to generate. This is what it looks like for a record with id =
6:
Page 37
<%=hidden_field_tag("item["+list_stripes.id.to_s+"][done]","0") %>
</td>
The parameters for check_box_tag are name, value = "1", checked = false, options = {};
for hidden_field_tag name, value = nil, options = {}
Documentation: ActionView::Helpers::FormTagHelper
Controller
What gets returned to the controller when you press the ‘Save’ button is the following hash:
params: {
:controller=>"items",
:item=> {
"6"=>{"done"=>"0"},
... etc...
"5"=>{"done"=>"1"}
},
:action=>"updater"
}
We’re interested in the :item bit. For example, the bold line means “the record with id = 6 has the value of the
done field set to 0”. From here, it’s a fairly easy job to update the Items table:
app\controller\items_controller (excerpt)
def updater
@params[:item].each { |item_id, attr|
item = Item.find(item_id)
item.update_attribute(:done,attr[:done])
}
redirect_to :action => 'list'
end
each puts “6” into the variable item_id, and “done” => “0” into attr.
Ruby Documentation: class Array
This code works, but if you watch what is happening in development.log, you’ll see that Rails is retrieving and
updating every record, whether it’s changed or not. Not only is this creating unnecessary database updates, but it
also means that updated_on also gets changed, which isn’t really what we want. Much better to only update if
‘done’ has changed, but this means some coding :-(
app\controller\items_controller (excerpt)
def updater
@params[:item].each { |item_id, contents|
item = Item.find(item_id)
if item.done != contents[:done].to_i
Page 38
item.update_attribute(:done,contents[:done])
end
}
redirect_to :action => 'list'
end
Note that we need to convert the string done to an integer using to_i so we can compare like with like. This is
the kind of gotcha you can easily miss – it’s worth checking development.log from time to time to make sure
Rails is doing what you expect.
Still to be done
On page 24 I left list_by_category as an easy exercise for the reader. It proved to be less easy than it looked
– in fact, I’m still looking for an elegant ‘Rails’ way to sort by a field in a lookup table. I ended up with this rather
horrible code:
app\controller\items_controller (excerpt)
def list_by_category
@item_pages = Paginator.new self, Item.count, 10, @params['page']
@items = Item.find_by_sql 'SELECT i.*, c.category FROM categories c, items i ' +
'WHERE ( c.id = i.category_id ) '+
'ORDER BY c.category ' +
'LIMIT 10 ' +
"OFFSET #{@item_pages.current.to_sql[1]}"
render_action 'list'
end
If anyone has a better solution, please let me know. I leave this code as a reassuring example that if all else fails,
Rails will not leave you stuck but will allow you to resort to ‘old-fashioned’ coding!
Page 39
Index of Rails and Ruby Terms used in this Document
A new..................................................................................... 10
action_name......................................................................11 O
B options_from_collection_for_select.............................28
before_type_cast.............................................................. 20 P
belongs_to......................................................................... 20 paginate.............................................................................. 13
C pagination_links............................................................... 17
check_box......................................................................... 28 Partial................................................................................. 11
check_box_tag..................................................................38 previous............................................................................. 15
confirm........................................................................ 14, 23 R
content_columns..............................................................14 redirect_to......................................................................... 10
content_for_layout.......................................................... 11 Referential Integrity......................................................... 21
created_at.............................................................................5 render_collection_of_partials........................................ 25
created_on.....................................................................5, 13 render_partial............................................................. 12, 25
current................................................................................15 render_template................................................................10
D rescue................................................................................. 27
date_select......................................................................... 27 S
destroy................................................................................10 save..................................................................................... 10
destroy_all......................................................................... 24 select...................................................................................28
development.log.................................................... 3, 10, 39 session variable................................................................. 33
E start_form_tag.................................................................. 12
end_form_tag................................................................... 12 strftime...............................................................................16
error_messages_for......................................................... 12 stylesheet_link_tag........................................................... 11
F submit_tag.........................................................................12
find..................................................................................... 10 T
find_all............................................................................... 10 Template............................................................................11
Flash................................................................................... 15 text_field............................................................................13
H U
h.......................................................................................... 14 update_attribute............................................................... 38
helper..................................................................................11 update_attributes..............................................................10
hidden_field_tag...............................................................38 updated_at........................................................................... 5
HTML escape................................................................... 14 updated_on................................................................... 5, 13
human_attribute_name................................................... 14 url_for................................................................................ 25
human_name.................................................................... 14 V
I validates_associated......................................................... 20
id........................................................................................... 5 validates_format_of.........................................................20
L validates_inclusion_of..................................................... 20
Layout................................................................................ 11 validates_length_of......................................................6, 20
link_to................................................................................ 12 validates_presence_of..................................................... 20
link_to_image................................................................... 23 validates_uniqueness_of................................................... 6
lock_version........................................................................ 5 .
N .each....................................................................................38
Page 41