About this entry
You’re currently reading the article “Introduction to Active Support in Ruby on Rails.”
- Published:
- February 21st 09:23 AM
- Updated:
- August 17th 07:48 PM
- Sections:
- Ruby on Rails
Introduction to Active Support in Ruby on Rails
The following article goes further into the ActiveRecord usage and introduces ActiveSupport.
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: Active Record Associations and Validations with Resouce Mapping.
Index
- Getting Column Statistics through ActiveRecord
- Adding First Level Administrative Units
- Countries' new
- AdministrativeUnits
- Adding Lower Level Administrative Units
Getting Column Statistics through ActiveRecord
Having the user enter the level number for the administrative_levels is not safe. The level should be generated automatically. Update de 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
level = AdministrativeLevel.maximum(:level, :conditions => {:country_id => @country})
if level.nil?
@highest_level = 1
else
@highest_level = level + 1
end
@administrative_level = AdministrativeLevel.new(:country => @country, :level => @highest_level)
end
end
app/views/administrative_levels/new.rhtml needs to be updated to only display the level and not allow editing:
<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 />
<%= @highest_level %>
<%= f.hidden_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) %>
The edit action should not allow for changing the level either. Update the edit action in the administrative_levels controller to:
# GET /administrative_levels/1;edit
def edit
@administrative_level = AdministrativeLevel.find(params[:id])
@country = @administrative_level.country
@level = @administrative_level.level
end
and 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 />
<%= f.hidden_field :country_id %>
<%= link_to @country.title, country_path(@country) %>
</p>
<p>
<b>Level</b><br />
<%= @level %>
<%= f.hidden_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) %>
Notice that in the cases of both new and edit besides displaying the level in the view, we are also including it as a hidden field so that when the form is posted to create and update respectively, the level is sent and stored properly in the database. Finally we need to update the destroy action in the administrative_levels controller so that the higher levels are updated when a lower level is deleted:
# DELETE /administrative_levels/1
# DELETE /administrative_levels/1.xml
def destroy
@administrative_level = AdministrativeLevel.find(params[:id])
country = @administrative_level.country
level = @administrative_level.level
@administrative_level.destroy
AdministrativeLevel.update_all('level = level - 1', ['country_id = ? AND level > ?', country, level])
respond_to do |format|
format.html { redirect_to administrative_levels_url(:country_id => country) }
format.xml { head :ok }
end
end
Adding First Level Administrative Units
Countries' new
Now it is time to develop to get back to the administrative units, whose resource scaffold we created on the last article. First, lets display the top level units when displaying the country information. Update the show action in the countries controller to:
# GET /countries/1
# GET /countries/1.xml
def show
@country = Country.find(params[:id])
@administrative_levels = @country.administrativeLevels
@first_level = @administrative_levels.first
if @first_level.nil?
@first_level_units = nil
else
@first_level_units = @first_level.administrativeUnits
end
respond_to do |format|
format.html # show.rhtml
format.xml { render :xml => @country.to_xml }
end
end
To ensure that the first method actually returns the first level we have to make the administrative levels for a country query ordered with respect to the level. Update app/models/country.rb to:
class Country < ActiveRecord::Base
validates_presence_of :title
validates_uniqueness_of :title
has_many :administrativeLevels, :order => 'level'
end
Update the 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>
<% if !@first_level.nil? %>
<p><b><%= @first_level.title.pluralize %></b> (<%= link_to "New #{@first_level.title}", new_administrative_unit_path(:administrative_level_id => @first_level) %>)</p>
<ul>
<% for unit in @first_level_units %>
<li><%= link_to h(unit.title), administrative_unit_path(unit) %></li>
<% end %>
</ul>
<% end %>
<%= 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 we are displaying the administrative level title in the plural (using the ActiveSupport method added to the String class) for the list of the top level administrative units. We are sending the administrative_level_id to the new_administrative_unit_path since we want the link to create a new top level unit with no parent, so the level is enough information for the creation.
AdministrativeUnits' new
When we are creating a unit that has a parent, we would also need to send the parent unit id to the new action in the administrative_units. It will end up like this:
# GET /administrative_units/new?administrative_level_id=1[&parent_administrative_unit_id=2]
def new
begin
@administrative_level = AdministrativeLevel.find(params[:administrative_level_id])
rescue ActiveRecord::RecordNotFound
logger.error("Attempt to access invalid administrative level #{params[:administrative_level_id]}")
redirect_to countries_path
else
parent_unit_id = params[:parent_administrative_unit_id]
if !parent_unit_id.blank?
@parent_unit = AdministrativeUnit.find(parent_unit_id)
@parent_title = @parent_unit.title
else
@parent_unit = nil
@parent_title = @administrative_level.country.title
end
@administrative_unit = AdministrativeUnit.new(:administrativeLevel => @administrative_level, :parentUnit => @parent_unit)
end
end
In the view, we won't display the parent unit nor the level, but still pass them as hidden fields. The back should point to the countries. Update app/views/administrative_units/new.rhtml to:
<h1>New <%= "#{@administrative_level.title} in #{@parent_title}" %></h1>
<%= error_messages_for :administrative_unit %>
<% form_for(:administrative_unit, :url => administrative_units_path) do |f| %>
<%= f.hidden_field :administrative_level_id %>
<%= f.hidden_field :parent_administrative_unit_id %>
<p>
<b>Title</b><br />
<%= f.text_field :title %>
</p>
<p>
<%= submit_tag "Create" %>
</p>
<% end
options = {:administrative_level_id => @administrative_level}
if !@parent_unit.nil?
options[:parent_administrative_unit_id] = @parent_unit
end %>
<%= link_to 'Back', administrative_units_path(options) %>
Notice that the options send to the back link path are a hash that is built in dependence upon there being a parent unit or not.
AdministrativeUnits' show
As for the show action in the administrative_units controller:
# GET /administrative_units/1
# GET /administrative_units/1.xml
def show
@administrative_unit = AdministrativeUnit.find(params[:id])
@administrative_level = @administrative_unit.administrativeLevel
@parent_unit = @administrative_unit.parentUnit
respond_to do |format|
format.html # show.rhtml
format.xml { render :xml => @administrative_unit.to_xml }
end
end
Update app/views/administrative_units/show.rhtml to:
<p>
<b><%=h @administrative_level.title %> Title:</b>
<%=h @administrative_unit.title %>
</p>
<% options = {:administrative_level_id => @administrative_level}
if !@parent_unit.nil?
options[:parent_administrative_unit_id] = @parent_unit %>
<p>
<b>Parent <%=h @parent_unit.administrativeLevel.title %>:</b>
<%= link_to h(@parent_unit.title), administrative_unit_path(@parent_unit) %>
</p>
<% end %>
<%= link_to 'Edit', edit_administrative_unit_path(@administrative_unit) %> |
<%= link_to 'Destroy', administrative_unit_path(@administrative_unit), :confirm => 'Are you sure?', :method => :delete %> |
<%= link_to 'Back', administrative_units_path(options) %>
AdministrativeUnits' index
The index action in the administrative units controller receives the administrative level id and the parent unit id if there is one. There would not be a parent unit if it is a top level unit; in that case, display all units within that level. If there is a parent unit, display all children units. Update the action index in the administrative_units controller to:
# GET /administrative_units?administrative_level_id=1[&parent_administrative_unit_id=2]
# GET /administrative_units?administrative_level_id=1[&parent_administrative_unit_id=2].xml
def index
begin
@administrative_level = AdministrativeLevel.find(params[:administrative_level_id])
rescue ActiveRecord::RecordNotFound
logger.error("Attempt to access invalid administrative level #{params[:administrative_level_id]}")
redirect_to countries_path
else
parent_administrative_unit_id = params[:parent_administrative_unit_id]
if !parent_administrative_unit_id.blank?
@parent_unit = AdministrativeUnit.find(parent_administrative_unit_id)
@administrative_units = @parent_unit.administrativeUnits
@parent_title = @parent_unit.title
else
@parent_unit = nil
@administrative_units = @administrative_level.childrenUnits
@parent_title = @administrative_level.country.title
end
respond_to do |format|
format.html # index.rhtml
format.xml { render :xml => @administrative_units.to_xml }
end
end
end
Update app/views/administrative_units/index.rhtml to:
<h1>Listing <%=h "#{@administrative_level.title.pluralize} in #{@parent_title}" %></h1>
<table border="1">
<tr>
<th>Title</th>
</tr>
<% for administrative_unit in @administrative_units %>
<tr>
<td><%= link_to h(administrative_unit.title), administrative_unit_path(administrative_unit) %></td>
</tr>
<% end %>
</table>
<br />
<% options = {:administrative_level_id => @administrative_level}
if !@parent_unit.nil?
options[:parent_administrative_unit_id] = @parent_unit
end %>
<%= link_to "New #{@administrative_level.title}", new_administrative_unit_path(options) %> |
<% if @parent_unit.nil? %>
<%= link_to 'Back', country_path(@administrative_level.country) %>
<% else %>
<%= link_to 'Back', administrative_unit_path(@parent_unit) %>
<% end %>
Notice that if there is a parent unit the back link will show the parent unit else it will show the country associated to the administrative level.
AdministrativeUnits' edit
The edit action in the administrative units controller, just like the new action, should not allow the user the change the administrative level and parent unit for data consistency. Update the edit action to:
# GET /administrative_units/1;edit
def edit
@administrative_unit = AdministrativeUnit.find(params[:id])
@administrative_level = @administrative_unit.administrativeLevel
@parent_unit = @administrative_unit.parentUnit
end
Update the app/views/administrative_units/edit.rhtml to:
<h1>Editing <%=h @administrative_level.title %></h1>
<%= error_messages_for :administrative_unit %>
<% options = {:administrative_level_id => @administrative_level}
form_for(:administrative_unit, :url => administrative_unit_path(@administrative_unit), :html => { :method => :put }) do |f| %>
<%= f.hidden_field :administrative_level_id %>
<% if !@parent_unit.nil?
options[:parent_administrative_unit_id] = @parent_unit %>
<p>
<b>Parent <%=h @parent_unit.administrativeLevel.title %>:</b>
<%= link_to h(@parent_unit.title), administrative_unit_path(@parent_unit) %>
</p>
<% end %>
<%= f.hidden_field :parent_administrative_unit_id %>
<p>
<b>Title</b><br />
<%= f.text_field :title %>
</p>
<p>
<%= submit_tag "Update" %>
</p>
<% end %>
<%= link_to 'Show', administrative_unit_path(@administrative_unit) %> |
<%= link_to 'Back', administrative_units_path(options) %>
Adding Lower Level Administrative Units
First we need a way to get the next level of each unit to display the children's header. Update app/models/administrative_level.rb to:
class AdministrativeLevel < ActiveRecord::Base
validates_presence_of :country_id, :level, :title
validates_numericality_of :level, :only_integer => true
belongs_to :country
has_many :administrativeUnits
def next_level
AdministrativeLevel.find(:first, :conditions => { :country_id => country_id, :level => level + 1 })
end
end
We use this method to get the information in the show action in the administrative_units controller to the view:
# GET /administrative_units/1
# GET /administrative_units/1.xml
def show
@administrative_unit = AdministrativeUnit.find(params[:id])
@administrative_level = @administrative_unit.administrativeLevel
@parent_unit = @administrative_unit.parentUnit
@next_level = @administrative_level.next_level
respond_to do |format|
format.html # show.rhtml
format.xml { render :xml => @administrative_unit.to_xml }
end
end
Update the app/views/administrative_units/show.rhtml to:
<p>
<b><%=h @administrative_level.title %> Title:</b>
<%=h @administrative_unit.title %>
</p>
<p>
<% options = {:administrative_level_id => @administrative_level}
if !@parent_unit.nil?
options[:parent_administrative_unit_id] = @parent_unit %>
<b>Parent <%=h @parent_unit.administrativeLevel.title %>:</b>
<%= link_to h(@parent_unit.title), administrative_unit_path(@parent_unit) %>
<% else
country = @administrative_level.country %>
<b>Parent Country:</b>
<%= link_to h(country.title), country_path(country) %>
<% end %>
</p>
<% if !@next_level.nil? %>
<p><b><%=h @next_level.title.pluralize %></b> (<%= link_to "New #{@next_level.title}", new_administrative_unit_path(:administrative_level_id => @next_level, :parent_administrative_unit_id => @administrative_unit) %>)</p>
<ul>
<% for unit in @administrative_unit.childrenUnits %>
<li><%= link_to h(unit.title), administrative_unit_path(unit) %></li>
<% end %>
</ul>
<% end %>
<%= link_to 'Edit', edit_administrative_unit_path(@administrative_unit) %> |
<%= link_to 'Destroy', administrative_unit_path(@administrative_unit), :confirm => 'Are you sure?', :method => :delete %> |
<%= link_to 'Back', administrative_units_path(options) %>
Next recommended article to read: Introduction to AJAX

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