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.)
The installation of Ruby and Ruby on Rails was described in Part I of the demonstation.
Disclaimer: I'm still learning the Rails framework (especially the 2.0 stuff) so there may be some oversights in the material below.
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:
For more information on the other directories, consult the README file in the clinic directory.
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
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:
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 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 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
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 < and & 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 %>
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.')
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.
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.
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.
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.
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) =======================================
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/
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).
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.
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
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
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 %>
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">]
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.
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) ==================================
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.
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
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.
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 %>
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.
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.
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
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.
$ 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
$ 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 ../../..
$ 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>
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
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 %>
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.
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.
The following resources were used to help develop this demonstration. These resources also document many other features of rails not addressed by this demo.