About this entry




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

  1. Generating Rails Docs
  2. Annotating Models
  3. Generating Administrative Levels and Administrative Units
  4. Validating and Associating
  5. Updating the Controllers and Views

 

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

Technorati tags: , , ,

Liked it? !

Posted on February 19th | 4 comments | Filed Under: Ruby on Rails