About this entry




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

  1. Getting Column Statistics through ActiveRecord
  2. Adding First Level Administrative Units
  3. 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

Technorati tags: , , ,

Liked it? !

Posted on February 21st | 0 comments | Filed Under: Ruby on Rails