About this entry
You’re currently reading the article “Active Record Associations and Validations with Resouce Mapping in Ruby on Rails.”
- Published:
- February 19th 09:19 AM
- Updated:
- August 17th 07:48 PM
- Sections:
- Ruby on Rails
Active Record Associations and Validations with Resouce Mapping in Ruby on Rails
The following article describes how model validation and association within Rails following resource mapping. I also briefly describe how to automatically annotate models and generate rails documentation.
Liked it? !
The following posts are based on a Ruby on Rails workshop I am teaching in Thimphu, Bhutan. It is not a complete tutorial since it does not include explanations. It may in the future but I do not know when I will have time to add them, so I am publishing it now as is to make it available to others that may find it useful. The sketches describe how to progressively build an Image Management System from the ground-up.
Before reading this article it is recommended that you read this first: RESTful Controllers.
Index
- Generating Rails Docs
- Annotating Models
- Generating Administrative Levels and Administrative Units
- Validating and Associating
- Updating the Controllers and Views
- Countries' show
- AdministrativeLevels' Actions
- The Whole AdministrativeLevels Controller
Generating Rails Docs
You make available the rails documentation by creating a new project, freezing rails, and rdocumenting all libraries (make sure you do this at the workspace folder and NOT at the IMS folder):
rails dummy_app
cd dummy_app
rake rails:freeze:gems
echo >vendor/rails/activesupport/README
rake doc:rails
Annotating Models
On the previous article, we created a table called countries. To make the attributes available from the Ruby code, we can install the rails task annotate_models by typing into the command-line:
ruby script/plugin install http://svn.pragprog.com/Public/plugins/annotate_models
You run it by typing into the command-line:
rake annotate_models
Now you will see the countries' field information as comments in app/models/country.rb:
# == Schema Information
# Schema version: 1
#
# Table name: countries
#
# id :integer(11) not null, primary key
# title :string(255)
#
class Country < ActiveRecord::Base
end
Notice that an auto-number id was greated automatically.
Generating Administrative Levels and Administrative Units
Now we will have a table administrative_levels to store for each country the name of each level of its administrative divisions (for instance, the highest level division of Bhutan is a "District", for United States a "State", for China a "Province", for Guatemala a "Department", etc.). We create it similarly to before:
ruby script/generate scaffold_resource administrative_level country_id:integer level:integer title:string
Ensure that the table uses MyISAM and limit to the title by updating app/db/migrate/002_create_administrative_levels to:
class CreateAdministrativeLevels < ActiveRecord::Migration
def self.up
create_table :administrative_levels, :options => 'engine=MyISAM' do |t|
t.column :country_id, :integer
t.column :level, :integer
t.column :title, :string, :limit => 100
end
end
def self.down
drop_table :administrative_levels
end
end
We will also have a table administrative_units to store the actual unit names of the countries at each level:
ruby script/generate scaffold_resource administrative_unit administrative_level_id:integer parent_administrative_unit_id:integer title:string
Again ensure that the table uses MyISAM and limit to the title by updating app/db/migrate/003_create_administrative_units to:
class CreateAdministrativeUnits < ActiveRecord::Migration
def self.up
create_table :administrative_units, :options => 'engine=MyISAM' do |t|
t.column :administrative_level_id, :integer
t.column :parent_administrative_unit_id, :integer
t.column :title, :string, :limit => 100
end
end
def self.down
drop_table :administrative_units
end
end
Run migration:
rake db:migrate
And update models with attribute information:
rake annotate_models
Validating and Associating
Add validations and associations to app/models/country.rb:
# == Schema Information
# Schema version: 3
#
# Table name: countries
#
# id :integer(11) not null, primary key
# title :string(255)
#
class Country < ActiveRecord::Base
validates_presence_of :title
validates_uniqueness_of :title
has_many :administrativeLevels
end
app/models/administrative_level.rb:
# == Schema Information
# Schema version: 3
#
# Table name: administrative_levels
#
# id :integer(11) not null, primary key
# country_id :integer(11)
# level :integer(11)
# title :string(100)
#
class AdministrativeLevel < ActiveRecord::Base
validates_presence_of :country_id, :level, :title
validates_numericality_of :level, :only_integer => true
belongs_to :country
has_many :administrativeUnits
end
and app/models/administrative_unit.rb:
# == Schema Information
# Schema version: 3
#
# Table name: administrative_units
#
# id :integer(11) not null, primary key
# administrative_level_id :integer(11)
# parent_administrative_unit_id :integer(11)
# title :string(100)
#
class AdministrativeUnit < ActiveRecord::Base
validates_presence_of :administrative_level_id, :title
belongs_to :administrativeLevel
belongs_to :parentUnit, :class_name => 'AdministrativeUnit', :foreign_key => 'parent_administrative_unit_id'
end
Updating the Controllers and Views
Countries' show
Since the administrative levels depend on the countries, we have to tie it with the countries' views. First we want to display the administrative levels when displaying a countries' information. This is done by the show action in countries controller. To pass administrative level information to the view, update it to:
# GET /countries/1
# GET /countries/1.xml
def show
@country = Country.find(params[:id])
@administrative_levels = @country.administrativeLevels
respond_to do |format|
format.html # show.rhtml
format.xml { render :xml => @country.to_xml }
end
end
To display the administrative levels and display a link to create new administrative levels, add update app/views/countries/show.rhtml to:
<p>
<b>Title:</b>
<%=h @country.title %>
</p>
<p><b>Administrative Levels</b> (<%= link_to 'New administrative level', new_administrative_level_path(:country_id => @country) %>)</p>
<ul>
<% for level in @administrative_levels %>
<li><%= link_to h(level.title), administrative_level_path(level) %> (<%= level.level %>)</li>
<% end %>
</ul>
<%= link_to 'Edit', edit_country_path(@country) %> |
<%= link_to 'Destroy', country_path(@country), :confirm => 'Are you sure?', :method => :delete %> |
<%= link_to 'Back', countries_path %>
Notice that the country_id is sent with the new_administration_level_path.
AdministrativeLevels' new
Update the new action in the administrative_levels controller to:
# GET /administrative_levels/new?country_id=1
def new
begin
@country = Country.find(params[:country_id])
rescue ActiveRecord::RecordNotFound
logger.error("Attempt to access invalid country #{params[:country_id]}")
redirect_to countries_path
else
@administrative_level = AdministrativeLevel.new(:country => @country)
end
end
The view should be changed to display the country name to the end-user and pass the country_id in the form post (as a hidden field) for the attribute to be there when writing it to the database in the create action within the administrative_levels controller. Update app/views/administrative_levels/new.rhtml
<h1>New administrative level</h1>
<%= error_messages_for :administrative_level %>
<% form_for(:administrative_level, :url => administrative_levels_path) do |f| %>
<p>
<b>Country</b><br />
<%= f.hidden_field :country_id %>
<%= link_to h(@country.title), country_path(@country) %>
</p>
<p>
<b>Level</b><br />
<%= f.text_field :level %>
</p>
<p>
<b>Title</b><br />
<%= f.text_field :title %>
</p>
<p>
<%= submit_tag "Create" %>
</p>
<% end %>
<%= link_to 'Back', administrative_levels_path(:country_id => @country) %>
Notice that the back link is sending the country_id to administrative_levels_path, which will be rendered by the index action in the administrative_levels controller.
AdministrativeLevels' index
To receive the country_id and pass the country's administrative levels to the view, update the index action within the administrative_levels controller to:
# GET /administrative_levels?country_id=1
# GET /administrative_levels.xml?country_id=1
def index
begin
@country = Country.find(params[:country_id])
rescue ActiveRecord::RecordNotFound
logger.error("Attempt to access invalid country #{params[:country_id]}")
redirect_to countries_path
else
@administrative_levels = @country.administrativeLevels
respond_to do |format|
format.html # index.rhtml
format.xml { render :xml => @administrative_levels.to_xml }
end
end
end
Now update the app/views/administrative_levels/index.rhtml for it to be rendered properly:
<h1>Listing administrative levels in <%= link_to h(@country.title), country_path(@country) %></h1>
<table border="1">
<tr>
<th>Level</th>
<th>Title</th>
</tr>
<% for administrative_level in @administrative_levels %>
<tr>
<td><%=h administrative_level.level %></td>
<td><%= link_to h(administrative_level.title), administrative_level_path(administrative_level) %></td>
</tr>
<% end %>
</table>
<br />
<%= link_to 'New administrative level', new_administrative_level_path(:country_id => @country) %> |
<%= link_to 'Back', country_path(@country) %>
Notice that the country_id is sent, as before, to the new_administrative_level_path. We have added a back link to the index view that points to the country for which the administrative levels are displayed. The name of the administrative levels are made links (just as we did for the countries) to the show action. The show action does not need the country_id, since it is part of the administrative_level.
AdministrativeLevels' show
The show action in the administrative_levels controller gets the country from the administrative_level. Update it to:
# GET /administrative_levels/1
# GET /administrative_levels/1.xml
def show
@administrative_level = AdministrativeLevel.find(params[:id])
@country = @administrative_level.country
respond_to do |format|
format.html # show.rhtml
format.xml { render :xml => @administrative_level.to_xml }
end
end
The view should display the name of the country instead of its id. Update app/views/administrative_levels/show.rhtml to:
<p>
<b>Country:</b>
<%=link_to h(@country.title), country_path(@country) %>
</p>
<p>
<b>Level:</b>
<%=h @administrative_level.level %>
</p>
<p>
<b>Title:</b>
<%=h @administrative_level.title %>
</p>
<%= link_to 'Edit', edit_administrative_level_path(@administrative_level) %> |
<%= link_to 'Destroy', administrative_level_path(@administrative_level), :confirm => 'Are you sure?', :method => :delete %> |
<%= link_to 'Back', administrative_levels_path(:country_id => @country) %>
As before the back link passes the country_id to the administrative_levels_path.
AdministrativeLevels' edit
The edit action in the administrative_levels controller should display the country names in a drop down list (instead of a text field for the country_id). To pass all countries to the view, update it to:
# GET /administrative_levels/1;edit
def edit
@countries = Country.find(:all)
@administrative_level = AdministrativeLevel.find(params[:id])
end
Update app/views/administrative_levels/edit.rhtml to:
<h1>Editing administrative level</h1>
<%= error_messages_for :administrative_level %>
<% form_for(:administrative_level, :url => administrative_level_path(@administrative_level), :html => { :method => :put }) do |f| %>
<p>
<b>Country</b><br />
<%= select('administrative_level', 'country_id', @countries.collect {|c| [ h(c.title), c.id ] }, { :include_blank => false }) %> (<%= link_to 'New country', new_country_path %>)
</p>
<p>
<b>Level</b><br />
<%= f.text_field :level %>
</p>
<p>
<b>Title</b><br />
<%= f.text_field :title %>
</p>
<p>
<%= submit_tag "Update" %>
</p>
<% end %>
<%= link_to 'Show', administrative_level_path(@administrative_level) %> |
<%= link_to 'Back', administrative_levels_path(:country_id => @administrative_level.country) %>
AdministrativeLevels' destroy
Finally, the destroy action in the administrative_levels controller should save the country before deleting the administrative level to redirect properly to the administrative_levels' index. Update it to:
# DELETE /administrative_levels/1
# DELETE /administrative_levels/1.xml
def destroy
@administrative_level = AdministrativeLevel.find(params[:id])
country = @administrative_level.country
@administrative_level.destroy
respond_to do |format|
format.html { redirect_to administrative_levels_url(:country_id => country) }
format.xml { head :ok }
end
end
The Whole AdministrativeLevels Controller
Taking into account all changes, the full administrative_levels controller should look like this:
class AdministrativeLevelsController < ApplicationController
# GET /administrative_levels?country_id=1
# GET /administrative_levels.xml?country_id=1
def index
begin
@country = Country.find(params[:country_id])
rescue ActiveRecord::RecordNotFound
logger.error("Attempt to access invalid country #{params[:country_id]}")
redirect_to countries_path
else
@administrative_levels = @country.administrativeLevels
respond_to do |format|
format.html # index.rhtml
format.xml { render :xml => @administrative_levels.to_xml }
end
end
end
# GET /administrative_levels/1
# GET /administrative_levels/1.xml
def show
@administrative_level = AdministrativeLevel.find(params[:id])
@country = @administrative_level.country
respond_to do |format|
format.html # show.rhtml
format.xml { render :xml => @administrative_level.to_xml }
end
end
# GET /administrative_levels/new?country_id=1
def new
begin
@country = Country.find(params[:country_id])
rescue ActiveRecord::RecordNotFound
logger.error("Attempt to access invalid country #{params[:country_id]}")
redirect_to countries_path
else
@administrative_level = AdministrativeLevel.new(:country => @country)
end
end
# GET /administrative_levels/1;edit
def edit
@countries = Country.find(:all)
@administrative_level = AdministrativeLevel.find(params[:id])
end
# POST /administrative_levels
# POST /administrative_levels.xml
def create
@administrative_level = AdministrativeLevel.new(params[:administrative_level])
respond_to do |format|
if @administrative_level.save
flash[:notice] = 'AdministrativeLevel was successfully created.'
format.html { redirect_to administrative_level_url(@administrative_level) }
format.xml { head :created, :location => administrative_level_url(@administrative_level) }
else
format.html { render :action => "new" }
format.xml { render :xml => @administrative_level.errors.to_xml }
end
end
end
# PUT /administrative_levels/1
# PUT /administrative_levels/1.xml
def update
@administrative_level = AdministrativeLevel.find(params[:id])
respond_to do |format|
if @administrative_level.update_attributes(params[:administrative_level])
flash[:notice] = 'AdministrativeLevel was successfully updated.'
format.html { redirect_to administrative_level_url(@administrative_level) }
format.xml { head :ok }
else
format.html { render :action => "edit" }
format.xml { render :xml => @administrative_level.errors.to_xml }
end
end
end
# DELETE /administrative_levels/1
# DELETE /administrative_levels/1.xml
def destroy
@administrative_level = AdministrativeLevel.find(params[:id])
country = @administrative_level.country
@administrative_level.destroy
respond_to do |format|
format.html { redirect_to administrative_levels_url(:country_id => country) }
format.xml { head :ok }
end
end
end
Next recommended article to read: Introduction to Active Support

4 comments
Jump to comment form | comments rss [?]