Ruby and Ruby on Rails Demonstration (Part II)

This page provides a brief demonstration of the Ruby on Rails web application framework presented at the St. John's Linux Users' Group on March 25, 2008. The demo used Ruby v1.8.6 and Rails v2.0.2. It is not intended to be a comprehensive overview of all features of this framework. This presentation was broken into two parts. This second presentation dealt primarily with the Ruby on Rails web application framework. Part I discussed the Ruby programming language using specific examples.

(A previous presentation on Ruby on Rails was prepared a few years ago, but is a bit dated now.)

Installation of Ruby and Ruby on Rails

The installation of Ruby and Ruby on Rails was described in Part I of the demonstation.

The Ruby on Rails Web application Framework (example)

Disclaimer: I'm still learning the Rails framework (especially the 2.0 stuff) so there may be some oversights in the material below.

Create a rails application

To create a rails project, use the rails with the name of the project as an argument. By default, this will create a application that uses the sqlite3 database adapter. To use the mysql adapter, use rails -d mysql project.

$ rails -v
Rails 2.0.2
$ rails clinic
      create  
      create  app/controllers
      create  app/helpers
      create  app/models
      create  app/views/layouts
      create  config/environments
      create  config/initializers
      create  db
      create  doc
      create  lib
      create  lib/tasks
      create  log
      create  public/images
      create  public/javascripts
      create  public/stylesheets
      create  script/performance
      create  script/process
      create  test/fixtures
      create  test/functional
      create  test/integration
      create  test/mocks/development
      create  test/mocks/test
      create  test/unit
      create  vendor
      create  vendor/plugins
      create  tmp/sessions
      create  tmp/sockets
      create  tmp/cache
      create  tmp/pids
      create  Rakefile
      create  README
      create  app/controllers/application.rb
      create  app/helpers/application_helper.rb
      create  test/test_helper.rb
      create  config/database.yml
      create  config/routes.rb
      create  public/.htaccess
      create  config/initializers/inflections.rb
      create  config/initializers/mime_types.rb
      create  config/boot.rb
      create  config/environment.rb
      create  config/environments/production.rb
      create  config/environments/development.rb
      create  config/environments/test.rb
      create  script/about
      create  script/console
      create  script/destroy
      create  script/generate
      create  script/performance/benchmarker
      create  script/performance/profiler
      create  script/performance/request
      create  script/process/reaper
      create  script/process/spawner
      create  script/process/inspector
      create  script/runner
      create  script/server
      create  script/plugin
      create  public/dispatch.rb
      create  public/dispatch.cgi
      create  public/dispatch.fcgi
      create  public/404.html
      create  public/422.html
      create  public/500.html
      create  public/index.html
      create  public/favicon.ico
      create  public/robots.txt
      create  public/images/rails.png
      create  public/javascripts/prototype.js
      create  public/javascripts/effects.js
      create  public/javascripts/dragdrop.js
      create  public/javascripts/controls.js
      create  public/javascripts/application.js
      create  doc/README_FOR_APP
      create  log/server.log
      create  log/production.log
      create  log/development.log
      create  log/test.log
$ cd clinic

A lot of directories and files are created. Some of the most important ones that we will be using during this demo are:

app/{models,views,controllers}
This is were the source code for the application resides. Rails uses a Model/View/Controller (MVC) architecture: Web requests from a web browser are received by the controller which queries the database via the model. The controller then (sometimes implicitly) "invokes" a view which renders a page which is then sent back to the browser. This architecture allows for a very clean separation between program logic and presentation. There are subdirectories in the app directory for the models, views and controllers.
lib/tasks
This directory contains custom ruby rake tasks which can be used for application maintence. There are numerous built-in rake tasks. They can be displayed with the command "rake tasks". A more verbose description of the tasks can be listed with "rake --describe".
script
This directory contains scripts that are used to invoke various services associated with the application and to generate application code itself. We will be using the server, generate and console
config
Contains files use to configure the application and database.
db
Contains the database and migrations for the application.
log
Contains log files that may contain useful debugging information if you application misbehaves.

For more information on the other directories, consult the README file in the clinic directory.

Configuring the database

The config/database.yml file contains the necessary information to connect to the database. This is required for databases which have usernames/passwords (e.g., mysql). Because our application will use sqlite3, there is no configuration necessary. Note that Rails has three separate environments: testing, development and production. By default, the development environment is used.

$ cat config/database.yml
# SQLite version 3.x
#   gem install sqlite3-ruby (not necessary on OS X Leopard)
development:
  adapter: sqlite3
  database: db/development.sqlite3
  timeout: 5000

# 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: sqlite3
  database: db/test.sqlite3
  timeout: 5000

production:
  adapter: sqlite3
  database: db/production.sqlite3
  timeout: 5000
Generate code for a Patient Resource

We'll generate code to create a simple Patient resource. This can be done with the ./script/generate command. To get help on this command, run it without arguments:

$ ./script/generate
Usage: ./script/generate generator [options] [args]

Rails Info:
    -v, --version                    Show the Rails version number and quit.
    -h, --help                       Show this help message and quit.

General Options:
    -p, --pretend                    Run but do not make any changes.
    -f, --force                      Overwrite files that already exist.
    -s, --skip                       Skip files that already exist.
    -q, --quiet                      Suppress normal output.
    -t, --backtrace                  Debugging: show backtrace on errors.
    -c, --svn                        Modify files with subversion. (Note: svn must be in path)


Installed Generators
  Builtin: controller, integration_test, mailer, migration, model, observer, plugin, 
             resource, scaffold, session_migration

More are available at http://rubyonrails.org/show/Generators
  1. Download, for example, login_generator.zip
  2. Unzip to directory $HOME/.rails/generators/login
     to use the generator with all your Rails apps
     or to .../clinic/lib/generators/login
     to use with this app only.
  3. Run generate with no arguments for usage information
       .../clinic/script/generate login

Generator gems are also available:
  1. gem search -r generator
  2. gem install login_generator
  3. .../clinic/script/generate login

We'll use the scaffold generator to generate the resource. This will create a lot of the infrastructure that we need to get up and running as quickly as possible. Again, we can get help by just supplying the scaffold argument alone.

$ ./script/generate scaffold
Usage: ./script/generate scaffold ModelName [field:type, field:type]

Options:
        --skip-timestamps            Don't add timestamps to the migration file for this model
        --skip-migration             Don't generate a migration file for this model

Rails Info:
    -v, --version                    Show the Rails version number and quit.
    -h, --help                       Show this help message and quit.

General Options:
    -p, --pretend                    Run but do not make any changes.
    -f, --force                      Overwrite files that already exist.
    -s, --skip                       Skip files that already exist.
    -q, --quiet                      Suppress normal output.
    -t, --backtrace                  Debugging: show backtrace on errors.
    -c, --svn                        Modify files with subversion. (Note: svn must be in path)

Description:
    Scaffolds an entire resource, from model and migration to controller and
    views, along with a full test suite. The resource is ready to use as a
    starting point for your restful, resource-oriented application.

    Pass the name of the model, either CamelCased or under_scored, as the first
    argument, and an optional list of attribute pairs.

    Attribute pairs are column_name:sql_type arguments specifying the
    model's attributes. Timestamps are added by default, so you don't have to
    specify them by hand as 'created_at:datetime updated_at:datetime'.

    You don't have to think up every attribute up front, but it helps to
    sketch out a few so you can start working with the resource immediately.

    For example, `scaffold post title:string body:text published:boolean`
    gives you a model with those three attributes, a controller that handles
    the create/show/update/destroy, forms to create and edit your posts, and
    an index that lists them all, as well as a map.resources :posts
    declaration in config/routes.rb.

Examples:
    `./script/generate scaffold post` # no attributes, view will be anemic
    `./script/generate scaffold post title:string body:text published:boolean`
    `./script/generate scaffold purchase order_id:integer amount:decimal`

We specify the name of the name of the resource and the attributes/fields of the resource. Other fields can be added later but some of files generated below will have to be manually modified to take the new fields into consideration. Note the we specify the singular form of the resource name.

$ ./script/generate scaffold Patient last_name:string given_names:string dob:date mcp_number:string
      exists  app/models/
      exists  app/controllers/
      exists  app/helpers/
      create  app/views/patients
      exists  app/views/layouts/
      exists  test/functional/
      exists  test/unit/
      create  app/views/patients/index.html.erb
      create  app/views/patients/show.html.erb
      create  app/views/patients/new.html.erb
      create  app/views/patients/edit.html.erb
      create  app/views/layouts/patients.html.erb
      create  public/stylesheets/scaffold.css
  dependency  model
      exists    app/models/
      exists    test/unit/
      exists    test/fixtures/
      create    app/models/patient.rb
      create    test/unit/patient_test.rb
      create    test/fixtures/patients.yml
      create    db/migrate
      create    db/migrate/001_create_patients.rb
      create  app/controllers/patients_controller.rb
      create  test/functional/patients_controller_test.rb
      create  app/helpers/patients_helper.rb
       route  map.resources :patients

Some of the more interesting files that are generated by this command are described below:

The CreatePatients migration

Ruby allows you to incrementally build the database schema using migrations. The migration below will be used to create the patients table in the database. Note that we can incrementally build up migrations (self.up) or undo migrations (self.down). Also, the patients.id field is implicit in the migration.

$ cat db/migrate/001_create_patients.rb
class CreatePatients < ActiveRecord::Migration
  def self.up
    create_table :patients do |t|
      t.string :last_name
      t.string :given_names
      t.date :dob
      t.string :mcp_number

      t.timestamps
    end
  end

  def self.down
    drop_table :patients
  end
end
The Patient Model (ActiveRecord)

The Patient model is derived from rails' base ActiveRecord class. This class provides with a means to access patients in the database using the Object Relational Mapper (ORM). Though the class definition looks trivial, there is an enourmous amount of implied functionality here, as we'll see later.

$ cat app/models/patient.rb
class Patient < ActiveRecord::Base
end
The Patients Controller

The PatientsController contains all the methods necessary to take web requests, read/modify the necessary objects in the database and causes the appropriate view to be rendered. The methods (called actions) mirror the the canonical methods described by Representational State Transfer (REST) software architecture. As a result, this interface is sometimes described as a RESTful interface. It can even convert resources to xml via the respond_to block.

Note the use of the Patient class to query/update the database and the params method which returns a hash object that contains various request parameters (for example, params[:id] is the database id of the patient selected by the user).

Also note that some methods set the @patient or @patients member variable. These variables will be accessible to the corresponding view.

$ cat app/controllers/patients_controller.rb
class PatientsController < ApplicationController
  # GET /patients
  # GET /patients.xml
  def index
    @patients = Patient.find(:all)

    respond_to do |format|
      format.html # index.html.erb
      format.xml  { render :xml => @patients }
    end
  end

  # GET /patients/1
  # GET /patients/1.xml
  def show
    @patient = Patient.find(params[:id])

    respond_to do |format|
      format.html # show.html.erb
      format.xml  { render :xml => @patient }
    end
  end

  # GET /patients/new
  # GET /patients/new.xml
  def new
    @patient = Patient.new

    respond_to do |format|
      format.html # new.html.erb
      format.xml  { render :xml => @patient }
    end
  end

  # GET /patients/1/edit
  def edit
    @patient = Patient.find(params[:id])
  end

  # POST /patients
  # POST /patients.xml
  def create
    @patient = Patient.new(params[:patient])

    respond_to do |format|
      if @patient.save
        flash[:notice] = 'Patient was successfully created.'
        format.html { redirect_to(@patient) }
        format.xml  { render :xml => @patient, :status => :created, :location => @patient }
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @patient.errors, :status => :unprocessable_entity }
      end
    end
  end

  # PUT /patients/1
  # PUT /patients/1.xml
  def update
    @patient = Patient.find(params[:id])

    respond_to do |format|
      if @patient.update_attributes(params[:patient])
        flash[:notice] = 'Patient was successfully updated.'
        format.html { redirect_to(@patient) }
        format.xml  { head :ok }
      else
        format.html { render :action => "edit" }
        format.xml  { render :xml => @patient.errors, :status => :unprocessable_entity }
      end
    end
  end

  # DELETE /patients/1
  # DELETE /patients/1.xml
  def destroy
    @patient = Patient.find(params[:id])
    @patient.destroy

    respond_to do |format|
      format.html { redirect_to(patients_url) }
      format.xml  { head :ok }
    end
  end
end
The Patient Views

Several views are also created by the scaffold generator. These views have ruby code embedded within them (hence the .erb suffix). Each view created by the scaffold generator corresponds to a method in the controller (e.g., index.html.erb corresponds to the index method). Note how the views have access to the @patient or @patients member variable set by the corresponding action in the controller.

The ruby code is delimited by either <% ... %> or <%= ... %>. The former delimiters are used to delimit ruby code that has no output, whereas the latter delimiters are used to delimit ruby code whose output is to be included as part of the content of the web page returned to the web browser.

The h ruby method simply escapes any special HTML characters that occur in the string generated by the ruby code, so, for example, strings containing < and & would be converted to &lt; and &amp; respectively.

$ cat app/views/patients/index.html.erb 
<h1>Listing patients</h1>

<table>
  <tr>
    <th>Last name</th>
    <th>Given names</th>
    <th>Dob</th>
    <th>Mcp number</th>
  </tr>

<% for patient in @patients %>
  <tr>
    <td><%=h patient.last_name %></td>
    <td><%=h patient.given_names %></td>
    <td><%=h patient.dob %></td>
    <td><%=h patient.mcp_number %></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 %>
$ cat app/views/patients/show.html.erb
<p>
  <b>Last name:</b>
  <%=h @patient.last_name %>
</p>

<p>
  <b>Given names:</b>
  <%=h @patient.given_names %>
</p>

<p>
  <b>Dob:</b>
  <%=h @patient.dob %>
</p>

<p>
  <b>Mcp number:</b>
  <%=h @patient.mcp_number %>
</p>


<%= link_to 'Edit', edit_patient_path(@patient) %> |
<%= link_to 'Back', patients_path %>
$ cat app/views/patients/new.html.erb 
<h1>New patient</h1>

<%= error_messages_for :patient %>

<% form_for(@patient) do |f| %>
  <p>
    <b>Last name</b><br />
    <%= f.text_field :last_name %>
  </p>

  <p>
    <b>Given names</b><br />
    <%= f.text_field :given_names %>
  </p>

  <p>
    <b>Dob</b><br />
    <%= f.date_select :dob %>
  </p>

  <p>
    <b>Mcp number</b><br />
    <%= f.text_field :mcp_number %>
  </p>

  <p>
    <%= f.submit "Create" %>
  </p>
<% end %>

<%= link_to 'Back', patients_path %>
$ cat app/views/patients/edit.html.erb 
<h1>Editing patient</h1>

<%= error_messages_for :patient %>

<% form_for(@patient) do |f| %>
  <p>
    <b>Last name</b><br />
    <%= f.text_field :last_name %>
  </p>

  <p>
    <b>Given names</b><br />
    <%= f.text_field :given_names %>
  </p>

  <p>
    <b>Dob</b><br />
    <%= f.date_select :dob %>
  </p>

  <p>
    <b>Mcp number</b><br />
    <%= f.text_field :mcp_number %>
  </p>

  <p>
    <%= f.submit "Update" %>
  </p>
<% end %>

<%= link_to 'Show', @patient %> |
<%= link_to 'Back', patients_path %>

Note that most of the actions in the controller have a corresponding view. Because of this close relationship between the controller and the views, the combination of the two is often called the ActionPack of rails.

Also note that the new.html.erb and edit.html.erb forms, errors related to invalid user input into the forms will be displayed with the code: <%= error_messages_for :patient %>

Main layout page

The main HTML page in which all the other views are included is called a layout view. It contains the standard HTML boilerplate markup. The yield statement is where the HTML for each of the the views is included. The layout can be used to incorporate common header and footer information which is associated with all views of the Patient model.

$ cat app/views/layouts/patients.html.erb
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
       "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
  <title>Patients: <%= controller.action_name %></title>
  <%= stylesheet_link_tag 'scaffold' %>
</head>
<body>

<p style="color: green"><%= flash[:notice] %></p>

<%= yield  %>

</body>
</html>

The flash method above returns a hash that can be used to temporarily store information between successive web requests (remember than HTTP is a stateless protocol). For example, a key in the flash can be set by the the controller to notify the user that his or her attempt to create a new patient was successful: (flash[:notice] = 'Patient was successfully created.')

Routing and named routes

Note the use of named routes in the controllers and views (e.g., edit_patient_path, new_patient_path, patients_path, etc.) above. These named routes are replaced by their corresponding URLs when the page is rendered. These URLs, in turn, map to corresponding controllers and actions when the rails application receives the request. The list of named routes and their corresponding URLS and controller/actions can be displayed with the rake routes command.

$ rake routes | grep -v format
(in .../clinic)
              patients GET    /patients                        {:controller=>"patients", :action=>"index"}
                       POST   /patients                        {:controller=>"patients", :action=>"create"}
           new_patient GET    /patients/new                    {:controller=>"patients", :action=>"new"}
          edit_patient GET    /patients/:id/edit               {:controller=>"patients", :action=>"edit"}
               patient GET    /patients/:id                    {:controller=>"patients", :action=>"show"}
                       PUT    /patients/:id                    {:controller=>"patients", :action=>"update"}
                       DELETE /patients/:id                    {:controller=>"patients", :action=>"destroy"}
                              /:controller/:action/:id         

Appending the suffix _url or _path to the named routes listed in the first column of the output above will result in the generation of a URL with or without the protocol://hostname:port (e.g.http://localhost:3000) prefix, respectively. These routes are generated by the line:

map.resources :patients

in the config/routes.rb file. This line was added to the file automatically by the script/generate scaffold patient ... command, above. The combination of the HTTP verb and the URL uniquely determine the action to be taken. This results in more consistent URLS when accessing resources in a REST-based architecture.

Named route shortcuts

Some named routes have shortcuts to reduce repeating yourself when typing the route. For example, to create a hyperlink in an .erb file to show a particular patient, we can just write:

<%= link_to 'Show', @patient %>
instead of:
<%= link_to 'Show', patient_path(@patient.id) %>

Assuming that @patient refers to patient with id 1, then both forms will be replaced during rendering with:

<a href="/patients/1">Show</a>

Referring to the output of the rake routes command above, this will cause the show action in the controller to be called with the appropriate patient id. (Note that the GET HTTP verb is implicit in this case.

redirect_to and render

Note that there are no corresponding views for the create, update or delete actions — these methods reuse the other action/views. For example, if a patient is successfully created, the create method invokes a redirect, which causes another request to be made to the web server to show the patient.

format.html { redirect_to(@patient) }

Note that @patient, in this context, is the same as patient_path(@patient), which, referring to the routes listed above, causes the show action/view to be called/rendered. Without the { redirect_to(@patient) }, the controller would have, by default, attempted to render the view in the create.html.erb template, which does not exist. redirect_to provides one way of overriding the default rendering mechanism. The controller does similar redirects for the update and delete actions.

Another way to override the default rendering is to use the render method. This causes rendering without an action being called. For example, in the create method defined in the Patient controller, if the creation of the patient is not successful, the new.html.erb view is simply re-rendered, but the new action is not actually called (the code is a little misleading, unfortunately.)

format.html { render :action => "new" }

Note that if this had been replaced with:

format.html { redirect_to(new_patient_path) }

the new action in the controller would be called, meaning that a new Patient object would be created. Then, when rendering the new.html.erb template again, the values which the user had filled in (whether they were valid or not) would be lost and the user would have to enter the data all over again. Not only that, but because a new patient was created, all the the validation information associated with the previous (invalid) patient record would be lost, meaning that the user would have no idea what was wrong with the initial attempt to create a patient.

By bypassing the new action, and going directly to the new.html.erb template, the Patient object created by create would still have the user specified field values intact. These values, as well as the associated validation errors, would be displayed when the new.html.erb template is re-rendered.

Difference between new/create and edit/update

Note the difference between the new/create actions in the controller:

The new action and its corresponding new.html.erb view template cause a form to be sent back to the client web browser for the user to fill in — no updates are made to the database. When the form is submitted by the user, the controller's create action method is called (implicitly, by the form_for method in the new.html.erb template) to actually store the patient in the database if the fields have passed validation.

The difference is similar between and edit and update — the edit action/view sends back a form to let the user modify a patient's attributes and the update action actually stores those changes in the database.

Create the database schema

Next we create the database schema. This can be done using the db:migrate rake task.

$ rake db:migrate
(in .../clinic)
== 1 CreatePatients: migrating ================================================
-- create_table(:patients)
   -> 0.0486s
== 1 CreatePatients: migrated (0.0489s) =======================================
Start the web server

Rails comes with a ruby-based webserver called WEBrick. Other servers can be used, if desired. We change into the top level directory created by the rails command and start the web server:

$ ./script/server
=> Booting WEBrick...
=> Rails application started on http://0.0.0.0:3000
=> Ctrl-C to shutdown server; call with --help for options
[2008-02-11 10:12:37] INFO  WEBrick 1.3.1
[2008-02-11 10:12:37] INFO  ruby 1.8.6 (2007-09-24) [i686-linux]
[2008-02-11 10:12:37] INFO  WEBrick::HTTPServer#start: pid=23400 port=3000

We can now visit the page at the URL http://localhost:3000/

Visit the web application at http://localhost:3000/patients

At this point, with the web server started and the database schema created, we can visit the main application webpage and create, list, show, edit and delete patients. There are some obvious bugs (for example, patients without names or with invalid/identical MCP numbers can be created).

Validating the input

We can quickly correct some of these problems by modifying the Patient model as follows:

$ cat app/models/patient.rb
class Patient < ActiveRecord::Base
  validates_presence_of   :last_name, :given_names
  validates_uniqueness_of :mcp_number
  validates_format_of     :mcp_number, :with => /\A\d{12}\z/
end

This ensures that when patients are created or modified, that the user specifies both names and that the MCP number is unique and consists of exactly 12 digits.

Observe that rails regards validation as a model concern and not a view concern. As a result it is unnecessary to modify any of the views or the controller.

Changing the patient sort order

By default, patients are retrieved and displayed in the same order that they were stored in the database. We can changing this by requesting that the patients be sorted by their last name when using the find method in the index method of the PatientsController.

class PatientsController < ApplicationController
  # GET /patients
  # GET /patients.xml
  def index
    @patients = Patient.find(:all, :order => "last_name")

    respond_to do |format|
      format.html # index.html.erb
      format.xml  { render :xml => @patients }
    end
  end
...
end
Changing the navigation

Whenever we create a new patient or edit an existing one, we are taken back to the show.html page by the create and update methods of the PatientsController (this is done by the redirect_to(@patient) in each of these methods.) Instead, we can return to the patient list after a patient is created or modified by changing the argument of the redirect_to method in create and update.

class PatientsController < ApplicationController
  ...
  # POST /patients
  # POST /patients.xml
  def create
    @patient = Patient.new(params[:patient])

    respond_to do |format|
      if @patient.save
        flash[:notice] = 'Patient was successfully created.'
        format.html { redirect_to( @patient  patients_path) }
        format.xml  { render :xml => @patient, :status => :created, :location => @patient }
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @patient.errors, :status => :unprocessable_entity }
      end
    end
  end

  # PUT /patients/1
  # PUT /patients/1.xml
  def update
    @patient = Patient.find(params[:id])

    respond_to do |format|
      if @patient.update_attributes(params[:patient])
        flash[:notice] = 'Patient was successfully updated.'
        format.html { redirect_to( @patient  patients_path) }
        format.xml  { head :ok }
      else
        format.html { render :action => "edit" }
        format.xml  { render :xml => @patient.errors, :status => :unprocessable_entity }
      end
    end
  end
  ...
end
Highlighting a created/updated patient in the index

Whenever we create or update the patient, we want to display the patient using a different background than the rest of the the patients in the list so that which one we added/modified. This can be done by modifying the method in create and update methods to pass the database id of the patient as a parameter to the index method:

class PatientsController < ApplicationController
  ...
  # POST /patients
  # POST /patients.xml
  def create
    @patient = Patient.new(params[:patient])

    respond_to do |format|
      if @patient.save
        flash[:notice] = 'Patient was successfully created.'
        format.html { redirect_to(patients_path(:pat_id => @patient)) }
        format.xml  { render :xml => @patient, :status => :created, :location => @patient }
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @patient.errors, :status => :unprocessable_entity }
      end
    end
  end

  # PUT /patients/1
  # PUT /patients/1.xml
  def update
    @patient = Patient.find(params[:id])

    respond_to do |format|
      if @patient.update_attributes(params[:patient])
        flash[:notice] = 'Patient was successfully updated.'
        format.html { redirect_to(patients_path(:pat_id => @patient)) }
        format.xml  { head :ok }
      else
        format.html { render :action => "edit" }
        format.xml  { render :xml => @patient.errors, :status => :unprocessable_entity }
      end
    end
  end
  ...
end

Next, we extract the id of the created/updated patient in the index method using params. Note that we store the id in the @pat_id member variable so that we can use it in the corresponding view. Also, because the parameter values are strings, we convert the id to an integer.

class PatientsController < ApplicationController
  # GET /patients
  # GET /patients.xml
  def index
    @patients = Patient.find(:all, :order => "last_name")
    @pat_id = params[:pat_id].to_i

    respond_to do |format|
      format.html # index.html.erb
      format.xml  { render :xml => @patients }
    end
  end
  ...
end

Finally, we modify the index.html.erb patient view to change the background of the table row that contains the id of the patient that was created/updated.

<h1>Listing patients</h1>

<table>
  <tr>
    <th>Last name</th>
    <th>Given names</th>
    <th>Dob</th>
    <th>Mcp number</th>
  </tr>

<% for patient in @patients %>
  <tr<%= patient.id == @pat_id ? ' style="background: yellow"' : "" %>>
    <td><%=h patient.last_name %></td>
    <td><%=h patient.given_names %></td>
    <td><%=h patient.dob %></td>
    <td><%=h patient.mcp_number %></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 %>
Using script/console and ORM

After clearing out the database, we can use the console and the Patient ActiveRecord class, to add patients to the database and get practice with the object relational mapper (ORM):

$ rake db:reset
(in .../clinic)
"db/development.sqlite3 already exists"
-- create_table("patients", {:force=>true})
   -> 0.0630s
-- initialize_schema_information()
   -> 0.1556s
-- columns("schema_info")
   -> 0.0015s
$ ./script/console
>> pat = Patient.new
=> #<Patient id: nil, last_name: nil, given_names: nil, dob: nil, mcp_number: nil,
     created_at: nil, updated_at: nil>

>> pat.given_names = "Homer J."
=> "Homer J."

>> pat.last_name = "Simpson"
=> "Simpson"

>> pat.mcp_number = "123456789012"
=> "123456789012"

>> pat.save
=> true

>> p pat
#<Patient id: 1, last_name: "Simpson", given_names: "Homer J.", mcp_number: "123456789012", dob: nil,
  created_at: "2008-03-05 17:35:53", updated_at: "2008-03-05 17:35:53">
=> nil

>> pat = Patient.new(:given_names => "Bart", :last_name => "Simpson", :mcp_number  => "123234345456")
=> #<Patient id: nil, last_name: "Simpson", given_names: "Bart", mcp_number: "123234345456", dob: nil,
     created_at: nil, updated_at: nil>

>> pat.save
=> true

>> pat = Patient.new(:given_names => "Bart", :last_name => "Simpson", :mcp_number  => "123")
=> #<Patient id: nil, last_name: "Simpson", given_names: "Bart", mcp_number: "123", dob: nil,
     created_at: nil, updated_at: nil>

>> pat.save
=> false

>> pat.save!
ActiveRecord::RecordInvalid: Validation failed: Mcp number is invalid
        from ...bin/local/lib/ruby/gems/1.8/gems/activerecord-2.0.2/
	  lib/active_record/validations.rb:946:in `save_without_transactions!'
        from ...bin/local/lib/ruby/gems/1.8/gems/activerecord-2.0.2/
	  lib/active_record/transactions.rb:112:in `save!'
        from ...bin/local/lib/ruby/gems/1.8/gems/activerecord-2.0.2/
	  lib/active_record/connection_adapters/abstract/database_statements.rb:66:in `transaction'
        from ...bin/local/lib/ruby/gems/1.8/gems/activerecord-2.0.2/
	  lib/active_record/transactions.rb:80:in `transaction'
        from ...bin/local/lib/ruby/gems/1.8/gems/activerecord-2.0.2/
	  lib/active_record/transactions.rb:100:in `transaction'
        from ...bin/local/lib/ruby/gems/1.8/gems/activerecord-2.0.2/
	  lib/active_record/transactions.rb:112:in `save!'
        from ...bin/local/lib/ruby/gems/1.8/gems/activerecord-2.0.2/
	  lib/active_record/transactions.rb:120:in `rollback_active_record_state!'
        from ...bin/local/lib/ruby/gems/1.8/gems/activerecord-2.0.2/
	  lib/active_record/transactions.rb:112:in `save!'
        from (irb):10

>> Patient.create(:given_names => "Marge",
?>                :last_name   => "Simpson",
?>                :mcp_number  => "210987654321")
=> #<Patient id: 3, last_name: "Simpson", given_names: "Marge", mcp_number: "210987654321", dob: nil,
     created_at: "2008-03-05 17:41:54", updated_at: "2008-03-05 17:41:54">

ActiveRecord also supports dynamic finder methods that can be used to query the database. Once patient records are retrieved from the database, then can be modified as if they were objects and resaved to the database.

>> Patient.find(1)
=> #<Patient id: 1, last_name: "Simpson", given_names: "Homer J.", mcp_number: "123456789012", dob: nil,
     created_at: "2008-03-05 17:35:53", updated_at: "2008-03-05 17:35:53">

>> Patient.find_by_last_name("Simpson")
=> #<Patient id: 1, last_name: "Simpson", given_names: "Homer J.", mcp_number: "123456789012", dob: nil,
     created_at: "2008-03-05 17:35:53", updated_at: "2008-03-05 17:35:53">

>> Patient.find_all_by_last_name("Simpson")
=> [#<Patient id: 1, last_name: "Simpson", given_names: "Homer J.", mcp_number: "123456789012", dob: nil,
      created_at: "2008-03-05 17:35:53", updated_at: "2008-03-05 17:35:53">, #<Patient id: 2, last_name: "Simpson",
      given_names: "Bart", mcp_number: "123234345456", dob: nil, created_at: "2008-03-05 17:40:34",
      updated_at: "2008-03-05 17:40:34">, #<Patient id: 3, last_name: "Simpson", given_names: "Marge",
      mcp_number: "210987654321", dob: nil, created_at: "2008-03-05 17:41:54", updated_at: "2008-03-05 17:41:54">]

>> pat = Patient.find_by_given_names("Homer J.")
=> #<Patient id: 1, last_name: "Simpson", given_names: "Homer J.", mcp_number: "123456789012", dob: nil,
     created_at: "2008-03-05 17:35:53", updated_at: "2008-03-05 17:35:53">

>> pat.given_names = "Homer Jay"
=> "Homer Jay"

>> pat.save
=> true

>> Patient.find(:all)
=> [#<Patient id: 1, last_name: "Simpson", given_names: "Homer Jay", mcp_number: "123456789012", dob: nil,
      created_at: "2008-03-05 17:35:53", updated_at: "2008-03-05 17:47:51">, #<Patient id: 2, last_name: "Simpson",
      given_names: "Bart", mcp_number: "123234345456", dob: nil, created_at: "2008-03-05 17:40:34",
      updated_at: "2008-03-05 17:40:34">, #<Patient id: 3, last_name: "Simpson", given_names: "Marge",
      mcp_number: "210987654321", dob: nil, created_at: "2008-03-05 17:41:54", updated_at: "2008-03-05 17:41:54">]

Populating the database with random data using a rake task

We can create a simple custom rake task to populate the database with several random patients. Make sure that the name of your rake tasks ends in .rake and that you run the rake task from the main clinic directory (and not one of its subdirectories).

$ cat lib/tasks/pop.rake
namespace :clinic do
  NUM_PAT      	= 20
  MAX_AGE	= 120
  DAYS_IN_YEAR	= 365.25

  class NameGenerator
    def initialize(file)
      @names = IO.readlines(file).map { |line|
        line.split.first.capitalize
      }
    end
  
    def random
      @names.rand
    end
  end

  DATA_DIR = File.join("db", "data")
  LAST   = NameGenerator.new(File.join(DATA_DIR, "dist.all.last"))
  FEMALE = NameGenerator.new(File.join(DATA_DIR, "dist.female.first"))
  MALE   = NameGenerator.new(File.join(DATA_DIR, "dist.male.first"))

  def patpop
    NUM_PAT.times { 
      print "."; STDOUT.flush
      given = (rand > 0.5 ? FEMALE : MALE)

      Patient.create(
        :given_names => given.random,
	:last_name   => LAST.random,
	:dob         => Date.today - rand(MAX_AGE * DAYS_IN_YEAR),
	:mcp_number  => "%012d" % rand(1000000000000)
      )
    }
  end


  desc "Populate the clinic with patients."
  task :populate  do
    puts "Reinitializing database..."
    Rake::Task["db:drop"].invoke
    Rake::Task["db:migrate"].invoke
    puts "done"

    puts "Populating patients table..."
    patpop
    puts "\ndone"
  end
end

Note that this rake task reads the names from the three files:

from a directory called data in the db directory. These files are from the 1990 US Census.

Output

$ rake --describe clinic
(in ..../clinic)
rake clinic:populate
    Populate the clinic with patients.

$ rake clinic:populate
(in .../clinic)
Reinitializing database...
== 1 CreatePatients: migrating ================================================
-- create_table(:patients)
   -> 0.0181s
== 1 CreatePatients: migrated (0.0183s) =======================================

done
Populating patient table...
....................
done

Note that if the Patient.create method wasn't able to store the patient in the database (if, for example, the MCP number didn't have twelve digits), it will silently fail. To correct this, instead of using Patient.create, a patient can be created in memory using new, then stored to the database using the save or save! methods. The former generates a true/false value for success/failure, while the latter generates an exception if the save failed.

pat = Patient.new
pat.last_name = ...
...
pat.save
...
pat = Patient.new(:given_names => ...)
pat.save!

Rails supports a sophisticated testing framework which can also be used to populate the testing database with test fixtures. These can be used to subsequently test various features of the application.

Generate code for a ProgressNote Resource

To demonstrate how Rails handles association between resources, we'll create a ProgressNote resource which will contain a note associated with the patient's evaluation. Several progress notes will be associated with each Patient resource.

$ ./script/generate scaffold ProgressNote patient:references contents:text
      exists  app/models/
      exists  app/controllers/
      exists  app/helpers/
      create  app/views/progress_notes
      exists  app/views/layouts/
      exists  test/functional/
      exists  test/unit/
      create  app/views/progress_notes/index.html.erb
      create  app/views/progress_notes/show.html.erb
      create  app/views/progress_notes/new.html.erb
      create  app/views/progress_notes/edit.html.erb
      create  app/views/layouts/progress_notes.html.erb
   identical  public/stylesheets/scaffold.css
  dependency  model
      exists    app/models/
      exists    test/unit/
      exists    test/fixtures/
      create    app/models/progress_note.rb
      create    test/unit/progress_note_test.rb
      create    test/fixtures/progress_notes.yml
      exists    db/migrate
      create    db/migrate/002_create_progress_notes.rb
      create  app/controllers/progress_notes_controller.rb
      create  test/functional/progress_notes_controller_test.rb
      create  app/helpers/progress_notes_helper.rb
       route  map.resources :progress_notes

Many of the generated files are similar as before. The migration is displayed below:

$ cat db/migrate/002_create_progress_notes.rb
class CreateProgressNotes < ActiveRecord::Migration
  def self.up
    create_table :progress_notes do |t|
      t.references :patient
      t.text :contents

      t.timestamps
    end
  end

  def self.down
    drop_table :progress_notes
  end
end

With this new migration, we must remember to re-migrate the database.

$ rake db:migrate
(in .../clinic)
== 2 CreateProgressNotes: migrating ===========================================
-- create_table(:progress_notes)
   -> 0.0445s
== 2 CreateProgressNotes: migrated (0.0448s) ==================================
Create associations between the Patient and ProgressNote models

Next we modify the Patient and ProgressNote models to let them know that they are associated with one another. The modifications required are fairly self explanatory:

$ cat app/models/progress_note.rb
class ProgressNote < ActiveRecord::Base
  belongs_to :patient
end
$ cat app/models/patient.rb
class Patient < ActiveRecord::Base
  has_many :progress_notes

  validates_presence_of   :last_name, :given_names
  validates_uniqueness_of :mcp_number
  validates_format_of     :mcp_number, :with => /\A\d{12}\z/
end

Rails supports has_one relationships too.

Modifying the populate task

The populate task that we wrote earlier can be updated to add progress notes for each patient. Note that we use a random sentence generator which creates sentences by taking random words from /usr/share/dict/words and puts them together.

$ cat lib/tasks/pop.rake
namespace :clinic do
  NUM_PAT      	= 10
  MAX_AGE	= 120
  DAYS_IN_YEAR	= 365.25

  class NameGenerator
    def initialize(file)
      @names = IO.readlines(file).map { |line|
        line.split.first.capitalize
      }
    end
  
    def random
      @names.rand
    end
  end

  class SentenceGenerator
    def initialize
      @words = IO.readlines('/usr/share/dict/words').map { |word|
        word.chomp
      }
    end
  
    def random
      sentence = (1..rand(5)+5).map { @words.rand }
      sentence = sentence.join(" ").capitalize
      sentence << "."
    end
  end

  DATA_DIR = File.join("db", "data")
  LAST   = NameGenerator.new(File.join(DATA_DIR, "dist.all.last"))
  FEMALE = NameGenerator.new(File.join(DATA_DIR, "dist.female.first"))
  MALE   = NameGenerator.new(File.join(DATA_DIR, "dist.male.first"))

  def progpop
    sentence = SentenceGenerator.new
    Patient.find(:all).each { |pat|
      (rand(10)+2).times {
        print "."; STDOUT.flush
        note_text = (1..rand(5)+5).map { sentence.random }
	note_date = pat.dob + rand(100 * (Date.today - pat.dob))/100.0
	note_date_str = note_date.strftime("%F %T")
        pat.progress_notes.create(:contents   => note_text.join("  "),
				  :created_at => note_date_str)
      }
    }
  end

  def patpop
    NUM_PAT.times { 
      print "."; STDOUT.flush
      given = (rand > 0.5 ? FEMALE : MALE)

      Patient.create(
        :given_names => given.random,
	:last_name   => LAST.random,
	:dob         => Date.today - rand(MAX_AGE * DAYS_IN_YEAR),
	:mcp_number  => "%012d" % rand(1000000000000)
      )
    }
  end

  desc "Populate the clinic with patients and progress notes."
  task :populate  do
    puts "Reinitializing database..."
    Rake::Task["db:drop"].invoke
    Rake::Task["db:migrate"].invoke
    puts "done"

    puts "Populating patients table..."
    patpop
    puts "\nPopulating progress_notes table..."
    progpop
    puts "\ndone"
  end
end

The progpop method iterates over each patient using Patient.find(:all).each. For each patient, it creates a random number of progress notes, associates them with the patient and stores the note in the database by accessing each patient's progress_notes data member and calling the create method. (The foreign key in the Patient record is set automatically.)

pat.progress_notes.create(:contents   => note_text.join("  "),
                          :created_at => note_date_str)

The progress_notes data member is present by virtue of the has_many :progress_notes declaration that we added to the Patient ActiveRecord.

We can now populate the database with random patients and progress notes:

$ rake --describe clinic
(in .../clinic)
rake clinic:populate
    Populate the clinic with patients and progress notes.

$ rake clinic:populate
(in .../clinic)
Reinitializing database...
== 1 CreatePatients: migrating ================================================
-- create_table(:patients)
   -> 0.0440s
== 1 CreatePatients: migrated (0.0445s) =======================================

== 2 CreateProgressNotes: migrating ===========================================
-- create_table(:progress_notes)
   -> 0.0533s
== 2 CreateProgressNotes: migrated (0.0539s) ==================================

done
Populating patients table...
..........
Populating progress_notes table...
.................................
done
More ./script/console and ORM

We can use ./script/console to demonstrate how to extract patients and their progress notes from the populated database using the ORM features of ActiveRecord. We can update their progress notes as well.

$ ./script/console
Loading development environment (Rails 2.0.2)
# Retrieve all the progress notes of the patient whose database id is 4
>> Patient.find(4).progress_notes
=> [#<ProgressNote id: 17, patient_id: 4, contents: "Solecizer monodactylous noninfantry lauric nessleri...",
    created_at: "1972-10-21 01:40:47", updated_at: "2008-03-20 15:19:02">,
    #<ProgressNote id: 18, patient_id: 4, contents: "Priapus anticapital timbertuned pungence sweatful c...",
    created_at: "1977-06-19 02:52:48", updated_at: "2008-03-20 15:19:02">,
    #<ProgressNote id: 19, patient_id: 4, contents: "Continual ureterosalpingostomy horsy caulote unfram...",
    created_at: "1995-03-29 21:07:11", updated_at: "2008-03-20 15:19:02">,
    #<ProgressNote id: 20, patient_id: 4, contents: "Ununiversity epithet flywheel unstoppable evolution...",
    created_at: "1989-07-17 13:26:24", updated_at: "2008-03-20 15:19:02">,
    #<ProgressNote id: 21, patient_id: 4, contents: "Upgang staphylomatic niacin cytogenetical antichoro...",
    created_at: "1974-03-03 05:45:36", updated_at: "2008-03-20 15:19:02">]

# Retrieve the contents of the first progress note associated with the patient whose database id is 3
>> Patient.find(3).progress_notes.first.contents
=> "Socioeducational peptonize barbituric predispose spiceable tequila.
Mainour buttermaker unrolled oversubscriber trinket!  Eloquential envelop
jiggle deviationism gelatiniferous congress adjutory jetsam.  Beneaped
electrosynthetically outscour aneuric enterolith oestrelata sharecrop
imitatee bartonella?  Castrensial nonreservation yemenic albuminimetry
undeceitful acrochordidae unslockened."

# Retrieve the patient whose MCP Number is 630816084212...
>> pat = Patient.find_by_mcp_number("630816084212")
=> #<Patient id: 5, last_name: "Layton", given_names: "Alex", dob: "1986-02-11",
    mcp_number: "630816084212", created_at: "2008-03-20 15:19:00", updated_at: "2008-03-20 15:19:00">

# ... then retrieve all the patient's progress notes (sorted by creation date) ...
>> notes = pat.progress_notes.find(:all, :order => "created_at")
=> [#<ProgressNote id: 23, patient_id: 5, contents: "Underturf sintsink laurite inculcatory overaccentua...",
      created_at: "1987-10-17 03:35:59", updated_at: "2008-03-20 15:19:02">,
    #<ProgressNote id: 25, patient_id: 5, contents: "Splenopancreatic munitions euphony assistantship bo...",
      created_at: "1988-10-27 02:38:23", updated_at: "2008-03-20 15:19:03">,
    #<ProgressNote id: 24, patient_id: 5, contents: "Soleprint subgenus lymphosarcomatous phineas monepi...",
      created_at: "1991-08-08 11:02:23", updated_at: "2008-03-20 15:19:02">,
    #<ProgressNote id: 22, patient_id: 5, contents: "Superfleet reascend portentously counterfort holost...",
      created_at: "1995-01-18 16:19:12", updated_at: "2008-03-20 15:19:02">,
    #<ProgressNote id: 26, patient_id: 5, contents: "Nonrotatable gelidity radicicola tumboa divvers.  T...",
      created_at: "1996-12-13 02:24:00", updated_at: "2008-03-20 15:19:03">,
    #<ProgressNote id: 28, patient_id: 5, contents: "Barristership revertive underwood pawnbrokeress ome...",
      created_at: "2000-11-13 08:09:35", updated_at: "2008-03-20 15:19:03">,
    #<ProgressNote id: 27, patient_id: 5, contents: "Unshyly negroish lakie engarment farfetched carbona...",
      created_at: "2001-12-17 14:52:48", updated_at: "2008-03-20 15:19:03">]

# ... finally, display the creation date and contents of each progress note.
>> notes.each { |note|
?>       puts "\t#{note.created_at}\n#{note.contents}"
>> }
        Sat Oct 17 03:35:59 -0230 1987
Underturf sintsink laurite inculcatory overaccentuate cymograph?  Fraud
pyelogram palaeophilist thecata bisnaga.  Unagitatedness foresummer
calycozoan daggerbush pentacular lophosteon guarri gastropancreatic
confusion.  Dolous neuropsychiatry buttresslike ambilevous preintone
erythropoiesis dschubba stereophotomicrography.  Millilux luvaridae koff
philosophobia samsoness acoine irideremia repandousness.  Fugue unequable
bronchocephalitis allocable overeyebrowed mina?  Disalicylide ledgeless
casamarca alexander ultralegality wangara.
        Thu Oct 27 02:38:23 -0130 1988
Splenopancreatic munitions euphony assistantship bosser limn unprotectable
lambie appreciativeness.  Flimp antecedaneous hebraic hitlerite
iridic outstrain guimbard yaxche.  Eisegetical antislickens elongative
polystictus occultate rosular outsell dogfoot.  Holconoti trainload
naturalistic fanatic reinstall whatna braehead vittate spavindy.
Cascabel gossiping koa semiglazed gamaliel unwoundable spinulosely.
Medisection predescend plainback hemoclasia jobber squat wellcurb
sodioaurous.
        Thu Aug 08 11:02:23 -0230 1991
Soleprint subgenus lymphosarcomatous phineas monepic moonblink ravenlike
antiheterolysin.  Turbined newsboat zoolatria bryonia phasianus.  Diasyrm
martyrologistic teach brabant hungriness isoetes falsework.  Tardily
bearward septangularness equivalved cumulate sundayfied boswellize.
Rectotome carbeen antiquarianism coenamorment crunchiness!  Monothalamian
scarification vitasti concelebration imamic rhodic tionontati.
        Wed Jan 18 16:19:12 -0330 1995
Superfleet reascend portentously counterfort holostomatous.  Infrarimal
archbishopry duplicidentate artlet kindheartedly zoogeographical
larvikite subminimal.  Nosehole teco ungraded odylize unforgeable barwal
deindustrialization.  Gahnite myxospongida apophorometer washtail hollock
anil gonystylus opsonogen.  Recontact mucoraceous zealotry undesigningly
rapturize.  Plasmodia bassetite outspent reinterference braininess?
Semilunar grounded autosexing empleomania tatinek.
        Fri Dec 13 02:24:00 -0330 1996
Nonrotatable gelidity radicicola tumboa divvers.  Transversovertical
downcurved stoppably doxography ebeneous laniflorous trundle
postmediastinal cerebrum.  Oxhoft adeline coassistance semistill
acroscopic confinement market.  Echiurus analcite rearrest guarded
araucaria.  Guest inviolacy triturium wondercraft getpenny pseudotabes
iterable kahikatea!  Terramara plasterboard numerosity piffle
metakinetic pitayita emmetropy woundwort!  Neuroskeletal pinguecula
tartufery sphaeraphides unexpropriated pichiciago vibrioid juvenal.
Ancylostomiasis aquintocubital melancholious meningitic ecdysiast
figuration fulgurantly unpolish.
        Mon Nov 13 08:09:35 -0330 2000
Barristership revertive underwood pawnbrokeress omentosplenopexy
preobservance ruptile desmoscolecidae wined.  Ratchety pikestaff flung
evolutoid toyless epicedian conscientiously.  Adonis prototheme pisolitic
lithochromatic nonsporeforming malapterurus xeroma.  Predevelopment
harmony unfestively undepravedness aeschynanthus.  Contortioned amchoor
synoecete unconversably unserene!  Vaticide proximateness starchworks
beaverize misasperse pore banky ramshackly.  Mebsuta achaemenidae
pterodactylous gluttonize spiritedly chlorophyllase pappox.
        Mon Dec 17 14:52:48 -0330 2001
Unshyly negroish lakie engarment farfetched carbonado.  Curstful
deputation serific penologic inadhesive antevert.  Overbred lemel
titeration vergeboard invent chemicomineralogical emandibulate
predarkness.  Trichina maltase unlapped zealful adenodermia mandament
menthane.  Collegiately nonpumpable substructural proudish sepiolite
irrecusably iroquois.  Cyclonology potboy clitelline sprayboard tapetal
projecting!
 ...

# Find the first progress note of the first patient whose last name is Aurora...
>> pat = Patient.find_by_last_name("Aurora")
=> #<Patient id: 6, last_name: "Aurora", given_names: "Porsha", dob: "1924-01-01",
    mcp_number: "533862187803", created_at: "2008-03-20 15:19:00", updated_at: "2008-03-20 15:19:00">

# ... modify this patients first progress note to read "This is a progress note" ...
>> pat.progress_notes[0].contents = "This is a progress note."
=> "This is a progress note."

# ... save the change in the database (generating an exception if there was an error).
>> pat.progress_notes[0].save!
=> true

Obviously, the last two find_by queries will fail if there are no patients with the corresponding MCP number or last name. Use Patient.find(:all) to generate an array of all patients.

Displaying the number of Progress Notes associated with a Patient.

We can modify the patient index.html file to display the number of notes associated with each patient.

$ cat app/views/patient/index.html.erb
<h1>Listing patients</h1>

<table>
  <tr>
    <th>Last name</th>
    <th>Given names</th>
    <th>Dob</th>
    <th>Mcp number</th>
    <th>Notes</th>
  </tr>

<% for patient in @patients %>
  <tr<%= patient.id == @pat_id ? ' style="background: yellow"' : "" %>>
    <td><%=h patient.last_name %></td>
    <td><%=h patient.given_names %></td>
    <td><%=h patient.dob %></td>
    <td><%=h patient.mcp_number %></td>
    <td align="center"><%=h 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 %>
Deleting Progress Notes associated with a deleted Patient

In order to ensure that the progress notes are deleted when the patient is deleted, we need to update the Patient model:

$ cat app/models/patient.rb
class Patient < ActiveRecord::Base
  has_many :progress_notes, :dependent => :destroy

  validates_presence_of   :last_name, :given_names
  validates_uniqueness_of :mcp_number
  validates_format_of     :mcp_number, :with => /\A\d{12}\z/
end

Now, whenever we delete a patient, all of his or her progress notes will also be deleted.

Displaying all the Progress Notes associated with a Patient.

Next, we'll modify the patient's show.html.erb view to display all the progress notes associated with the patient.

$ cat app/views/patient/show.html.erb
<p>
  <b>Last name:</b>
  <%=h @patient.last_name %>
</p>

<p>
  <b>Given names:</b>
  <%=h @patient.given_names %>
</p>

<p>
  <b>Dob:</b>
  <%=h @patient.dob %>
</p>

<p>
  <b>Mcp number:</b>
  <%=h @patient.mcp_number %>
</p>

<%= link_to 'Edit', edit_patient_path(@patient) %> |
<%= link_to 'Back', patients_path %>

<hr/>

<h2> Progress Notes </h2>
<% if @patient.progress_notes.empty? %>
<p>
  <em> No progress notes </em>
</p>
<% else %>
<dl>
<%= render :partial => 'progress_note',
	   :collection => @patient.progress_notes.sort_by { |note|
	     note.created_at
	   }.reverse %>
</dl>
<% end %>

We tell the render method to include a fragment of a web page called a partial to actually generate the list of progress notes. The list of progress notes is given as the value to the :collection key argument to render. This creates an implicit loop over the collection which includes the partial web page for each element in the collection. Note that the name of the file containing the partial must contain an underscore prefix (which is not used in render call itself) and that inside the partial, the loop index variable is the same as the name of the file without the underscore prefix and without the file name extensions. For example, for the partial file _progress_note.html.erb, the corresponding loop index variable is called progress_note.

$ cat app/views/patients/_progress_note.html.erb
<dt>
  <strong>
    <%=h progress_note.created_at %>
  </strong>
</dt>
<dd>
  <%=h progress_note.contents %>
</dd>

The views are still very incomplete at this point and errors will be generated as you create/edit new items, but this should give you a rough idea of how they can be modified.

Creating new Progress Notes for a Patient.

To reflect the fact that progress notes is a nested resource of the patient resource, we modify the routes as follows:

$ cat config/routes.rb
ActionController::Routing::Routes.draw do |map|
  map.resources :progress_notes

  map.resources :patients 

  map.resources :patients do |patient|
    patient.resources :progress_notes
  end
  ...
end

Note that this changes the routing in that progress_notes are now nested within a patient. In other words, before creating/accessing a progress note, a patient id must be supplied.

Next, we modify the show.html.erb view to display a simple text area into which new progress notes can be added:

$ cat app/views/patients/show.html.erb
<p>
  <b>Last name:</b>
  <%=h @patient.last_name %>
</p>

<p>
  <b>Given names:</b>
  <%=h @patient.given_names %>
</p>

<p>
  <b>Dob:</b>
  <%=h @patient.dob %>
</p>

<p>
  <b>Mcp number:</b>
  <%=h @patient.mcp_number %>
</p>

<%= link_to 'Edit', edit_patient_path(@patient) %> |
<%= link_to 'Back', patients_path %>

<hr/>

<h2> Progress Notes </h2>

<h3> New Note </h3>
<% form_for([@patient, ProgressNote.new]) do |f| %>
  <p>
    <%= f.text_area :contents, :rows => 24, :cols => 80 %>
  </p>
  <p>
    <%= f.submit "Create" %>
  </p>
<% end %>
<% if @patient.progress_notes.empty? %>
<p>
  <em>
    No progress notes 
  </em>
</p>
<% else %>
<dl>
<%= render :partial => 'progress_note',
	   :collection => @patient.progress_notes.sort_by { |note|
	     note.created_at
	   }.reverse %>
</dl>
<% end %>

The form is generated by the form_for form helper which, in its simplest form takes the object being created as an argument. Because a progress note belongs to a patient (i.e., it is a nested resource), the patient object must be passed in as well. This is done by putting the patient and a (blank) progress note in a list and passing the list as the first argument to the form_for method. We could create a form with pre-initialized values in the fields by setting the various fields in the blank progress note, e.g.:

form_for([@patient, ProgressNote.new(:contents => "Subjective:\n")])

Finally, we update the create method in the ProgressNotesController to retrieve the patient from the database and build its progress note based upon what was entered in the form (the build method will build a progress note in memory and associated it with @patient.) The subsequent call to @progress_note.save actually attempts to save the progress note in the database. The only other change to in this method is to redirect the navigation when the note is successfully (or unsuccessfully) created to the patient show view.

$ cat app/controllers/progress_notes_controller.rb
class ProgressNotesController < ApplicationController
  ...
  # POST /progress_notes
  # POST /progress_notes.xml
  def create
    @patient = Patient.find(params[:patient_id])
    @progress_note = ProgressNote.new@patient.progress_notes.build(params[:progress_note])

    respond_to do |format|
      if @progress_note.save
        flash[:notice] = 'ProgressNote was successfully created.'
        format.html { redirect_to(@progress_note@patient) }
        format.xml  { render :xml => @progress_note, :status => :created, :location => @progress_note }
      else
        format.html { render :action => "new":template => "patients/show", :layout => 'patients' }
        format.xml  { render :xml => @progress_note.errors, :status => :unprocessable_entity }
      end
    end
  end
  ...
end
In-place editing with AJAX

Rails supports various AJAX features which can help may web pages more interactive and less clumsy to navigate. Some of these effects are part of the prototype and script.aculo.us libraries which are included with Rails. Others are available via plugins. We'll install the in-place editing plugin which let's you modify fields directly on a web page without having the navigate to a separate "Edit" form.

  1. Download the in-place editing plugin.
    $ ruby script/plugin install in_place_editing
    + ./README
    + ./Rakefile
    + ./init.rb
    + ./lib/in_place_editing.rb
    + ./lib/in_place_macros_helper.rb
    + ./test/in_place_editing_test.rb
    
  2. Download and apply the patch:
    $ cd vendor/plugins/in_place_editing/
    $ wget http://dev.rubyonrails.org/attachment/ticket/10055/in_place_editing_should_work_with_csrf_and_rjs.patch?format=raw
    --15:17:13--  http://dev.rubyonrails.org/attachment/ticket/10055/in_place_editing_should_work_with_csrf_and_rjs.patch?format=raw
               => `in_place_editing_should_work_with_csrf_and_rjs.patch?format=raw'
    Resolving dev.rubyonrails.org... 8.7.217.32
    Connecting to dev.rubyonrails.org|8.7.217.32|:80... connected.
    HTTP request sent, awaiting response... 200 OK
    Length: 3,757 (3.7K) [text/x-diff]
    
    100%[============================================================================================>] 3,757         11.25K/s             
    
    15:17:14 (11.24 KB/s) - `in_place_editing_should_work_with_csrf_and_rjs.patch?format=raw' saved [3757/3757]
    $ patch -p0 < in_place_editing_should_work_with_csrf_and_rjs.patch?format=raw
    patching file test/in_place_editing_test.rb
    patching file lib/in_place_macros_helper.rb
    $ cd ../../..
    
  3. Include the prototype and script.aculo.us javascript modules in the patient layout.
    $ cat app/views/layouts/patients.html.erb
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
           "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    
    <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
    <head>
      <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
      <title>Patients: <%= controller.action_name %></title>
      <%= stylesheet_link_tag 'scaffold' %>
      <%= javascript_include_tag :defaults %>
    </head>
    <body>
    
    <p style="color: green"><%= flash[:notice] %></p>
    
    <%= yield  %>
    
    </body>
    </html>
    
  4. Let the controller know which fields are going to be in place editted:

    $ cat app/controllers/progress_notes_controller.rb
    class PatientsController < ApplicationController
      in_place_edit_for :patient, :last_name
      ...
    end
    
  5. Modify the index.html.erb view to use in place editing on the fields.

    $ cat app/views/patients/index.html.erb
    <h1>Listing patients</h1>
    
    <table>
      <tr>
        <th>Last name</th>
        <th>Given names</th>
        <th>Dob</th>
        <th>Mcp number</th>
        <th>Notes</th>
      </tr>
    
    <% for patient in @patients %>
      <tr<%= patient.id == @pat_id ? ' style="background: yellow"' : "" %>>
        <td><%=h patient.last_name %></td> 
        <% @patient = patient %>
        <td><%= in_place_editor_field :patient, 'last_name' %></td>
        <td><%=h patient.given_names %></td>
        <td><%=h patient.dob %></td>
        <td><%=h patient.mcp_number %></td>
        <td align="center"><%=h 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 %>
    
  6. Restart the server

    $ kill -INT <pid of server>
    [2008-03-25 15:25:05] INFO  going to shutdown ...
    [2008-03-25 15:25:05] INFO  WEBrick::HTTPServer#start done.
    $ ./script/server
    => Booting WEBrick...
    => Rails application started on http://0.0.0.0:3000
    => Ctrl-C to shutdown server; call with --help for options
    [2008-03-25 15:25:44] INFO  WEBrick 1.3.1
    [2008-03-25 15:25:44] INFO  ruby 1.8.6 (2007-09-24) [i686-linux]
    [2008-03-25 15:25:44] INFO  WEBrick::HTTPServer#start: pid=20002 port=3000
    

There are some problems with the plugin. For example, validations are no longer done when you submit an entry. Also, if you submit an empty field, the field is no longer editable, since there is no text to click on.

Where's all the SQL??

In most cases, Rails does a good job of protecting us from having to use SQL. If you are curious (and maybe a bit masochistic) you can find the SQL that Rails was generating and executing on our behalf in the log/development.log file:

$ cat log/development.log

If you really need to use SQL in some queries, Rails allows that too, if necessary.

Resources

The following resources were used to help develop this demonstration. These resources also document many other features of rails not addressed by this demo.

The Ruby on Rails Documentation home page
Contains links to API documentation, manuals, books etc.
Rails Framework Documentation
Rails API documentation
Rolling with Rails 2.0 - The First Full Tutorial
Introduction to some of the 2.0 features of Rails.
Ruby on Rails - Terms and Concepts.
Explanation of various terms that keep popping up in the Rails world
RESTful Rails. Part I
Introductory article to Ruby and REST
RESTful Rails. Part II
Second part of Ruby and REST article
Ruby on Rails/REST resources
Links to REST resources, including:
RESTful Rails Development [PDF]
Detailed description of RESTful Rails Development
Softies on Rails
Contains a good series on RESTful resources

Donald Craig (donald@mun.ca)
Last modified: April 21, 2008 17:07:20 NDT
Valid XHTML 1.0 Strict Valid CSS!