Ruby on Rails is a web
application framework that uses Ruby to help build web applications.
It was created by David Heinemeier Hansson in 2004 when he extracted
the core from an existing web application (called Basecamp) and
generalized it so that it could be used to build other applications.
It is still actively maintained and is currently at version 3.0.5,
which was release near the end of February, 2011. (Version 3.0.6
was released on April 6th. This version is now available in
~donald/pub/rails
. The documentation below, however,
still refers to version 3.0.5 in some places.)
Some of the more notable aspects of this framework include:
A typical interaction in the context of a Rails web application is outlined below and illustrated in the figure on the right.
We don't have time to discuss or perform an installation. Refer to the Ruby on Rails Website or Section 1.2 of the Ruby on Rails Tutorial. A private copy of version 3.0.5 of Rails is already installed on the Computer Science machines at MUN and should be accessible with the command (notice the preceding dot):
$ . /users/cs/grad/donald/pub/rails/setpath
This places the directory /users/cs/grad/donald/pub/rails/bin
at the front of your PATH
environment variable so that all
the commands (especially the rails
command) can be executed
without having to type their full path name. To test whether everything
is setup right, try the following commands:
$ which rails /users/cs/grad/donald/pub/rails/bin/rails $ rails -v Rails 3.0.5 $ rails Usage: rails new APP_PATH [options] Options: -r, [--ruby=PATH] # Path to the Ruby binary of your choice # Default: /users/cs/grad/donald/pub/rails/bin/ruby -d, [--database=DATABASE] # Preconfigure for selected database # (options: mysql/oracle/postgresql/sqlite3/frontbase/ibm_db) # Default: sqlite3 -b, [--builder=BUILDER] # Path to an application builder (can be a filesystem path or URL) -m, [--template=TEMPLATE] # Path to an application template (can be a filesystem path or URL) [--dev] # Setup the application with Gemfile pointing to your Rails checkout [--edge] # Setup the application with Gemfile pointing to Rails repository [--skip-gemfile] # Don't create a Gemfile -O, [--skip-active-record] # Skip Active Record files -T, [--skip-test-unit] # Skip Test::Unit files -J, [--skip-prototype] # Skip Prototype files -G, [--skip-git] # Skip Git ignores and keeps Runtime options: -f, [--force] # Overwrite files that already exist -p, [--pretend] # Run but do not make any changes -q, [--quiet] # Supress status output -s, [--skip] # Skip files that already exist Rails options: -v, [--version] # Show Rails version number and quit -h, [--help] # Show this help message and quit Description: The 'rails new' command creates a new Rails application with a default directory structure and configuration at the path you specify. Example: rails new ~/Code/Ruby/weblog This generates a skeletal Rails installation in ~/Code/Ruby/weblog. See the README in the newly created application to get going.
NOTE: Conventional IDEs are typically shunned by the Rails developer community in favour of a terminal command line and a good text editor. There are several guides online to help you understand how to use the command line
For this introduction, we'll generate a very, very simple Electronic
Medical Record (EMR) application. We can use the new
subcommand of the rails
command to generate the
foundation for the application.
$ rails new emr create create README create Rakefile create config.ru create .gitignore create Gemfile create app create app/controllers/application_controller.rb create app/helpers/application_helper.rb create app/mailers create app/models create app/views/layouts/application.html.erb create config create config/routes.rb create config/application.rb create config/environment.rb create config/environments create config/environments/development.rb create config/environments/production.rb create config/environments/test.rb create config/initializers create config/initializers/backtrace_silencers.rb create config/initializers/inflections.rb create config/initializers/mime_types.rb create config/initializers/secret_token.rb create config/initializers/session_store.rb create config/locales create config/locales/en.yml create config/boot.rb create config/database.yml create db create db/seeds.rb create doc create doc/README_FOR_APP create lib create lib/tasks create lib/tasks/.gitkeep create log create log/server.log create log/production.log create log/development.log create log/test.log create public create public/404.html create public/422.html create public/500.html create public/favicon.ico create public/index.html create public/robots.txt create public/images create public/images/rails.png create public/stylesheets create public/stylesheets/.gitkeep create public/javascripts create public/javascripts/application.js create public/javascripts/controls.js create public/javascripts/dragdrop.js create public/javascripts/effects.js create public/javascripts/prototype.js create public/javascripts/rails.js create script create script/rails create test create test/fixtures create test/functional create test/integration create test/performance/browsing_test.rb create test/test_helper.rb create test/unit create tmp create tmp/sessions create tmp/sockets create tmp/cache create tmp/pids create vendor/plugins create vendor/plugins/.gitkeep
This command creates an emr
directory and creates
several more files and subdirectories. We can change to the
emr
directory and see them:
$ cd emr $ ls -F Gemfile Rakefile config/ db/ lib/ public/ test/ vendor/ README app/ config.ru doc/ log/ script/ tmp/
The README
file contains many helpful details
regarding the files and subdirectories generated as well as some
hints on what to do next and how to debug your application.
The most important subdirectories we'll be dealing with is the
app
subdirectory which contains subsubdirectories
that contain the files for the models, views and controllers
and the db
subdirectory which contains the database
as well as files related to the creation and modification of the
database schema.
Although we haven't actually created any real application code yet,
we can still start a webserver so that we can view a welcome page.
This can be done with the server
subcommand of the rails
command:
$ rails server [2011-04-01 17:35:56] INFO WEBrick 1.3.1 [2011-04-01 17:35:56] INFO ruby 1.9.2 (2011-02-18) [i686-linux] [2011-04-01 17:35:56] INFO WEBrick::HTTPServer#start: pid=7428 port=3000
Many log messages will be displayed in this window as you access your application from a web browser. Viewing these log messages can be helpful when diagnosing problems with the web application.
If you get a message about the port already in use, a server
is already running on that machine using that port (probably
a server you started earlier). Either kill that server or
use a different port, for example: rails server -p 3001
.
When you are finished, remember to kill the server by pressing
Ctrl-C
in this terminal window. Or you can find
the process ID using the ps
and kill it off manually.
When the server has been started we can access it through
our web browser by visiting the URL: http://localhost:3000
.
We'll open a new command line window (leaving the window
that we used to start the server) and set up our path using the
. /users/cs/grad/donald/pub/rails/setpath
command again.
Central to any EMR is the patients under a physicians care. We'll use
the generate
subcommand of the rails
command
to generate the scaffolding of a model-view-controller that will be
used to interact with the Patients in the database. After changing
back into the emr
directory, we issue the command:
$ rails generate scaffold Patient name:string address:text dob:date mcp_number:string invoke active_record create db/migrate/20110329194309_create_patients.rb create app/models/patient.rb invoke test_unit create test/unit/patient_test.rb create test/fixtures/patients.yml route resources :patients invoke scaffold_controller create app/controllers/patients_controller.rb invoke erb create app/views/patients create app/views/patients/index.html.erb create app/views/patients/edit.html.erb create app/views/patients/show.html.erb create app/views/patients/new.html.erb create app/views/patients/_form.html.erb invoke test_unit create test/functional/patients_controller_test.rb invoke helper create app/helpers/patients_helper.rb invoke test_unit create test/unit/helpers/patients_helper_test.rb invoke stylesheets create public/stylesheets/scaffold.css
The contents of the controller and the corresponding views are shown below, in abbreviated form. Note the following observations:
.html.erb
view template files.
.html.erb
files are similar to HTML, but
typically contain small amounts of embedded ruby in them
(hence the .erb
suffix). The ruby is written
between <%
... %>
delimiters
or <%=
... %>
delimiters (the
latter pair of delimiters will substitute the result of the ruby
expression directly into the HTML template.)
index
action of the controller, the
@patients
array contains a list of all the patients
in the database. In the index.html.erb
file, we
iterate over this array to display the details associated with
each patient in an HTML table.
# app/controllers/patients_controller.rb
class PatientsController < ApplicationController
# Get a list of all patients
def index
@patients = Patient.all
...
end
# Show details of one patient
def show
@patient = Patient.find(params[:id])
...
end
# Generate a form to create a new patient
def new
@patient = Patient.new(params[:patient])
...
end
# Generate a form to update a patient
def edit
@patient = Patient.find(params[:id])
...
end
# Create a new patient in the database
def create
@patient = Patient.new(params[:patient])
...
if @patient.save
...
end
# Update an existing patient in the database.
def update
@patient = Patient.find(params[:id])
...
end
# Remove a patient from the database.
def destroy
@patient = Patient.find(params[:id])
@patient.destroy
...
end
end
<!-- app/views/patient/index.html.erb -->
<h1>Listing patients</h1>
<table>
<tr>
<th>Name</th>
...
</tr>
<% @patients.each do |patient| %>
<tr>
<td><%= patient.name %></td>
...
</tr>
<% end %>
</table>
<br />
<%= link_to 'New Patient', new_patient_path %>
<!-- app/views/patient/show.html.erb -->
<p id="notice"><%= notice %></p>
<p>
<b>Name:</b>
<%= @patient.name %>
...
</p>
<%= link_to 'Edit', edit_patient_path(@patient) %> |
<%= link_to 'Back', patients_path %>
<!-- app/views/patient/new.html.erb -->
<h1>New patient</h1>
<%= render 'form' %>
<%= link_to 'Back', patients_path %>
<!-- app/views/patient/edit.html.erb -->
<h1>Editing patient</h1>
<%= render 'form' %>
<%= link_to 'Show', @patient %> |
<%= link_to 'Back', patients_path %>
Both the new.html.erb
and edit.html.erb
views use a common chuck of HTML representing the form to be displayed
to the physician. This common code is called a partial in
rails and is located in the file _form.html.erb
in the
app/patient/views
subdirectory. It is rendered with
the render
command in the new.html.erb
and edit.html.erb
views. (Note that we add
a :start_year => 1880
hash parameter to the
:dob
attribute to allow older patients to be represented
in the database.)
<!-- app/views/patient/_form.html.erb -->
<%= form_for(@patient) do |f| %>
<% if @patient.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(@patient.errors.count, "error") %> prohibited this patient from being saved:</h2>
<ul>
<% @patient.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :name %><br />
<%= f.text_field :name %>
</div>
<div class="field">
<%= f.label :address %><br />
<%= f.text_area :address, :rows => 4, :cols => 20 %>
</div>
<div class="field">
<%= f.label :dob %><br />
<%= f.date_select :dob, :start_year => 1900, :end_year => 2011 %>
</div>
<div class="field">
<%= f.label :mcp_number %><br />
<%= f.text_field :mcp_number %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
The code for the Patient
model is pretty simple:
# app/models/patient.rb
class Patient < ActiveRecord::Base
end
Much of the functionality for this class is either inherited or created dynamically during runtime as the database is queried by the controller. We'll be adding validation and associations to this model shortly.
Another file generated is a database migration file. A migration file is simply a ruby program that describes how the database schema (or the data itself) is to be modified as the database evolves over time. The migration generated for us below creates a table named patients with the specified columns in the database.
# db/migrate/*_create_patients.rb
class CreatePatients < ActiveRecord::Migration
def self.up
create_table :patients do |t|
t.string :name
t.text :address
t.date :dob
t.string :mcp_number
t.timestamps
end
end
def self.down
drop_table :patients
end
end
To actually create the schema in the database, we need to apply the
migration by running a task to migrate the database to its new state.
In rails tasks are run using a utility program called rake
,
with the name of the task provided as a command line argument. (A list
of all tasks can be generated with $ rake --tasks
.) To
migrate a database, we specify the task named the db:migrate
.
$ rake db:migrate (in .../emr) == CreatePatients: migrating ================================================= -- create_table(:patients) -> 0.0060s == CreatePatients: migrated (0.0061s) ========================================
By default, the sqlite3 database is used, however other databases are supported as well including MySQL, DB2 and Oracle, to name a few.
With everything now in places, we can visit the URL
http://localhost:3000/patients/
and create, edit, delete,
show or list all the patients in the database. We can change the
routing so that the URL http://localhost:3000/
will automatically visit the list of patients:
# config/routes.rb
Emr::Application.routes.draw do
...
# You can have the root of your site routed with "root"
# just remember to delete public/index.html.
# root :to => "welcome#index"
root :to => "patients#index"
...
end
For this to work, we must also remove the index.html
file from the public
directory:
$ rm public/index.html
As you are adding patients, note the sequence of events:
new
action of the PatientsController
is called.
@patient
member variable (no database access is done).
new.html.erb
form is then generated. Because
this form uses the _form.html.erb
partial, this
partial will also be rendered.
create
action of the PatientsController
is called.
@patient
)
initialized with the data supplied by the physician in the form.
This form data is available in the params
hash.
A similar sequence of events happens when you modify a patient,
except that the edit
/update
actions
are called instead of the new
/create
actions.
Also, during the call to edit
the model is
consulted (@patient = Patient.find(params[:id])
)
to find the patient in the database whose id matches that
selected by the physician in the list. The _form.html.erb
partial employed by the edit.html.erb
view uses
this object to pre-populates form with the patient's details.
Unfortunately, our application has problems. For example, we can create patients without actually filling in any of the fields. We can create invalid/duplicate MCP numbers. Fortunately, models provides an easy way to validate data before actually storing it in the database.
# app/models/patients.rb
class Patient < ActiveRecord::Base
validates :name, :address, :dob, :mcp_number, :presence => true
validates :mcp_number, :uniqueness => true,
:format => {
:with => %r{\A\d{12}\z},
:message => 'must have exactly 12 digits'
}
end
Now, whenever we leave fields blank or try to enter invalid data, we are returned to the form which highlights the problems that were encountered. (Note that the dates are not validated, so an invalid date like April 31 will be stored as May 1. Oddly, Rails 3 does not appear to provide a way to validate dates out of the box.)
To make the application feel a little more "real-world", we can populate it with some
random data. This is known as seeding the database and can be done
by modifying the db/seeds.rb
file. For example, the script
below will add 100 random patients to the database with address, birth dates and
MCP numbers.
# db/seeds.rb
Patient.delete_all
FIRST_NAMES = %w(Alice Bob Charlie Dave Eva Mallory Peggy Victor Trent Walter Arthur)
LAST_NAMES = %w(Smith Jones Brown Williams Miller Wilson Moore Davis)
STREET_NAMES = %w(Pine Elm Oak Main)
STREET_TYPES = %w(Blvd. Cresent Road Street Avenue)
STDOUT.sync = true # Prevent output from being buffered.
print "Seeding database"
100.times do
patient = Patient.create(:name => "%s %s" % [FIRST_NAMES.sample, LAST_NAMES.sample],
:address => "%d %s %s" %
[rand(1000)+1, STREET_NAMES.sample, STREET_TYPES.sample],
:dob => Date.today - rand(40000),
:mcp_number => (rand(900000000000) + 100000000000).to_s)
print "."
end
puts "done"
We can then run this script as a rake task to seed the database:
$ rake db:seed (in .../emr) Seeding database....................................................................................................done
Each patient has a collection of progress notes associated with it. The views for these progress notes will be handled by the patient views, so we'll just generate a model and a controller for the progress notes.
First we create the model. Observe that each note references a patient (in SQL context, this is the foreign key) and as content field that contains text.
$ rails generate model ProgressNote patient:references contents:text create app/controllers/progress_notes_controller.rb invoke erb create app/views/progress_notes invoke test_unit create test/functional/progress_notes_controller_test.rb invoke helper create app/helpers/progress_notes_helper.rb invoke test_unit create test/unit/helpers/progress_notes_helper_test.rb
We re-migrate to introduce the progress note table into the database.
$ rake db:migrate (in .../emr) == CreateProgressNotes: migrating ============================================ -- create_table(:progress_notes) -> 0.0040s == CreateProgressNotes: migrated (0.0041s) ===================================
The ProgressNote
model generated above is as follows.
The belongs_to :patient
creates the association between
the note and the patient to whom it belongs.
# app/models/progress_note.rb
class ProgressNote < ActiveRecord::Base
belongs_to :patient
end
We must also create an association in the opposition direction, informing
the Patient
model that it may have zero or more progress notes.
# app/models/patients.rb
class Patient < ActiveRecord::Base
validates :name, :address, :dob, :mcp_number, :presence => true
validates :mcp_number, :uniqueness => true,
:format => {
:with => %r{\A\d{12}\z},
:message => 'must have exactly 12 digits'
}
has_many :progress_notes
end
A progress note is known as a nested resource. We inform the router of this relationship, as follows:
# config/routes.rb
Emr::Application.routes.draw do
resources :patients
resources :patients do
resources :progress_notes
end
...
end
Now, if patient
is a Patient
object we can
access its associated progress notes by using the syntax
patient.progress_notes
. We can create a new progress note
for a patient using the syntax
patient.progress_notes.create(...)
. We'll see examples
of these later on.
Next, we create the controller class for the ProgressNotes:
$ rails generate controller ProgressNotes
The generated class is very simple with no methods. We'll manaully
add a create
method. This method will be called when
the physician clicks on a Create Progress Note button in
a view that shows a patient that we'll see later. The create
action queries the database for the patient given the patient id,
then creates a progress note for that patient initialized with
the contents of the progress note's text field. After the note
has been created, we redirect to the view that shows the patient
and all of his or here progress notes.
# app/controllers/progress_notes_controller.rb
class ProgressNotesController < ApplicationController
def create
@patient = Patient.find(params[:patient_id])
@patient.progress_notes.create(params[:progress_note])
redirect_to patient_path(@patient)
end
end
In order to show the progress notes in the patient view, we modify
the app/views/patients/show.html.erb
template. If there
are no progress notes, we'll display some text indicating this. Otherwise,
we'll iterate over all the patients progress notes displaying the
date the note was created as well as the contents of the note.
The block of code following the conditional display of the progress
note is a form that allows the physician to create a new progress note.
Because a progress note is a nested resource, the form_for
syntax in this case takes a list of objects that illustrate this
nesting. When the user clicks the submit button created by the form,
the create
method of the ProgressNoteController
will be invoked.
<!-- app/views/patients/show.html.erb -->
<p id="notice"><%= notice %></p>
<p>
<b>Name:</b>
<%= @patient.name %>
</p>
<p>
<b>Address:</b>
<%= @patient.address %>
</p>
<p>
<b>Dob:</b>
<%= @patient.dob %>
</p>
<p>
<b>Mcp number:</b>
<%= @patient.mcp_number %>
</p>
<% if @patient.progress_notes.empty? %>
<strong>No Progress Notes Available</strong>
<% else %>
<h2>Notes</h2>
<% @patient.progress_notes.each do |note| %>
<p>
<b>Date:</b>
<%= note.created_at.localtime %><br/>
<b>Note:</b>
<%= note.contents %>
</p>
<% end %>
<% end %>
<h2>Add a progress note:</h2>
<%= form_for([@patient, @patient.progress_notes.build]) do |f| %>
<div class="field">
<%= f.label :contents %><br />
<%= f.text_area :contents %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
<%= link_to 'Edit', edit_patient_path(@patient) %> |
<%= link_to 'Back', patients_path %>
Finally, we can modify the view that shows a listing
of all the patients (app/views/patients/index.html.erb
).
The modification will add an extra column that shows how many progress
notes the physician created for each patient.
<!-- app/view/patients/index.html.erb -->
<h1>Listing patients</h1>
<table>
<tr>
<th>Name</th>
<th>Address</th>
<th>Dob</th>
<th>Mcp number</th>
<th>Notes</th>
<th></th>
<th></th>
<th></th>
</tr>
<% @patients.each do |patient| %>
<tr>
<td><%= patient.name %></td>
<td><%= patient.address %></td>
<td><%= patient.dob %></td>
<td><%= patient.mcp_number %></td>
<td align="center"><%= patient.progress_notes.size %></td>
<td><%= link_to 'Show', patient %></td>
<td><%= link_to 'Edit', edit_patient_path(patient) %></td>
<td><%= link_to 'Destroy', patient, :confirm => 'Are you sure?', :method => :delete %></td>
</tr>
<% end %>
</table>
<br />
<%= link_to 'New Patient', new_patient_path %>
The db/seeds.rb
can be modified to create random progress
notes for each of the patients. This creates progress notes by grabbing
random words from the /usr/share/dict/words
file and
adding them to the progress note associated with a patient. Note again,
the use of the patient.progress_notes.create(...)
. syntax.
# db/seeds.rb
Patient.delete_all
FIRST_NAMES = %w(Alice Bob Charlie Dave Eva Mallory Peggy Victor Trent Walter Arthur)
LAST_NAMES = %w(Smith Jones Brown Williams Miller Wilson Moore Davis)
STREET_NAMES = %w(Pine Elm Oak Main)
STREET_TYPES = %w(Blvd. Cresent Road Street Avenue)
WORDS = IO.read('/usr/share/dict/words').split("\n")
STDOUT.sync = true # Prevent output from being buffered.
print "Seeding database"
100.times do
patient = Patient.create(:name => "%s %s" % [FIRST_NAMES.sample, LAST_NAMES.sample],
:address => "%d %s %s" %
[rand(1000)+1, STREET_NAMES.sample, STREET_TYPES.sample],
:dob => Date.today - rand(40000),
:mcp_number => (rand(900000000000) + 100000000000).to_s)
rand(10).times do
# Generate gibberish for the progress notes.
contents = (1..(rand(30) + 10)).map { WORDS.sample }.join(" ")
patient.progress_notes.create(:contents => contents)
end
print "."
end
puts "done"