Railsmagazine60x60 Active Scaffold

by Payal Gupta

Issue: Winter Jam

published in December 2009

Payal gupta

Payal Gupta is a web developer working at Vinsol, a leading Rails consultancy based in India.

She has over two years of industry experience. Outside of work, her interests include reading and cooking.

Introduction

ActiveScaffold is a rails plugin that generates an ajaxified management interface based on the database tables. It is an incredibly powerful, configuration-driven framework that automatically provides all CRUD operations. It is easily and highly configurable. Let us have a walkthrough of Active Scaffold with details on configurability and customization options it provides using a sample application. The demo application which we are going to develop is a management site for a sports club.

Configuration

Here our example is about building an administration panel for a sports club “mysportsclub”. It consists of sports, players, coaches and equipments. There are various kinds of sports in the club. Each player can have membership for any number of sports. Each sport will have one trained coach for guidance and various equipments which will be issued to the players. Let’s begin with the implementation of mysportsclub.

Let’s setup our Rails application mysportsclub. Run the following commands: 

rails mysportsclub

Install the latest version of the plugin which is compatible with latest stable Rails (2.3.x): 

script/plugin install git://github.com/activescaffold/active_scaffold.git 

Note: The latest Active Scaffold version works only with Rails version higher than 2.2. Also, install render_component plugin since it is now removed from Rails 2.3 but used by Active Scaffold. 

Layout

This is the standard active scaffold layout admin.html.erb:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"

"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html>

<head>

  <title>My Sports Club</title>

  <%= javascript_include_tag :defaults %>

  <%= active_scaffold_includes %>

</head>

<body>

  <%= yield %>

</body>

</html> 

Model/Migrations 

script\generate model sport name:string description:text coach_id:integer

script\generate model player name:string date:birth_date

script\generate model equipment name:string sport_id:integer

script\generate model coach name:string

script\generate model players_sport player_id:integer sport_id:integer

rake db:migrate

Add the following code to the respective models. This code defines the associations for the various models: 

sports.rb

class Sport < ActiveRecord::Base

  belongs_to :coach

  has_many :equipments, :dependent => :destroy

  has_many :players_sports, :dependent => :destroy

  has_many :players, :through => :players_sports

  validates_presence_of :name

  validates_uniqueness_of :name

end 

coach.rb

class Coach < ActiveRecord::Base

  has_one :sport

  validates_presence_of :name

end

player.rb

class Player < ActiveRecord::Base

  has_many :players_sports, :dependent => :destroy

  has_many :sports, :through => :players_sports

  validates_presence_of :name

end

equipment.rb

class Equipment < ActiveRecord::Base

  belongs_to :sport

  validates_presence_of :name

end 

players_sport.rb

class PlayersSport < ActiveRecord::Base

  belongs_to :player

  belongs_to :sport

end 

Note:

Don’t forget to add the plural form of “equipment” as “equipments” in the config/intializers/inflections.rb.

ActiveSupport::Inflector.inflections do |inflect|

  inflect.plural "equipment", "equipments"

end

Active Scaffold will throw an exception if we do not define the above inflection rule. The controller generated for the ‘Equipment’ model will be ‘EquipmentsController’. But since active scaffold generates the controller name automatically using its inbuilt function: active_scaffold_controller_for(class_name_of_the_model). It will not be able to guess the correct controller name and would throw an exception while we try to access the nested equipments:

Controllers

script\generate controller sports

script\generate controller coaches

script\generate controller equipments

script\generate controller players

Add the following code to your controllers:

sports_contrpller.rb

class SportsController < ApplicationController

  active_scaffold :sport do |config|

    config.columns = [:name, :description, :coach, :equipments]

  end

end

coaches_controller.rb

class CoachesController < ApplicationController

  active_scaffold :coach do |config|

    config.columns = [:name, :sport]

  end

end

players_controller.rb

class PlayersController < ApplicationController

  active_scaffold :player do |config|

    config.columns = [:name]

  end

end

equipments_controller.rb

class EquipmentsController < ApplicationController

  active_scaffold :equipment do |config|

    config.columns = [:name, :sport]

  end

end

players_sports_controller.rb

class PlayersSportsController < ApplicationController

  active_scaffold :players_sport do |config|

    config.columns = [:sport, :player]

  end

end

routes.rb

map.root :controller => 'sports', :action => 'index'

map.resources :sports, :active_scaffold => true

map.resources :players, :active_scaffold => true

map.resources :players_sports, :active_scaffold => true

map.resources :coaches, :active_scaffold => true

map.resources :equipments, :active_scaffold => true

The above code will generate a complete interface for our club.

Following code for your layout will provide a navigation bar in your view.

admin.html.erb

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.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"/>

  <%= stylesheet_link_tag 'admin.css' %>

  <%= javascript_include_tag :defaults %>

  <%= active_scaffold_includes %>

  <title><%= "Admin" %></title>

</head>

<body>

<div>

  <div>

    <h2>Agent Website Admin Panel</h2>

  <div>

  <ul class="links">

    <li><%= link_to 'Sports', sports_path %></li>

    <li><%= link_to 'Players', players_path %></li>

    <li><%= link_to 'Coaches', coaches_path %></li>

    <li><%= link_to 'Equipments', equipments_path %></li>

    <li><%= link_to 'PlayersSports', playerssports_path %></li>

  </ul>

  <div class="clear">

  </div>

</div>

</div>

  <div id="content">

    <div id="main">

      <%= yield %>

    </div>

  </div>

</div>

</body>

</html>

Customization

There are various customization options available. As per our requirement we can customize the columns, links, fields, actions etc. Here are various configuration options illustrated with examples:

1. By default Active Scaffold displays all the fields present in the database. But, the using method config.columns allows adding only the desired fields to the view. Not only we display the accessors for the model but also custom fields can be added to these views.

Here is the default configuration display:

Here is the custom configuration display using config.columns:

equipments_controller.rb

class EquipmentsController < ApplicationController

  active_scaffold :equipment do |config|

    config.columns = [:name, :sport]

  end

end

2. Override the default birth date control for players in edit form.

Add birth_date for the player in the controller will generate the below control in the edit view:

Here as we can see the default value for the date field is current date. But as per the requirement for birth date the date field should have past dates and a wider range of year. This can be done by overriding the field. Here we need to override the field of a form hence it is called form override.

players_helper.rb

module PlayersHelper

  def birth_date_form_column(record, input_name)

    date_select(record, :birth_date, :end_year => Date.today.year, :start_year => 1950, :name => input_name)

  end

end

3. Display the age of each player: We can define helpers for columns in the list and show views. This is called field override.

Add the age column as in the above code and it will be calculated using the instance method calculate_age in the players model.

players_helper.rb

module PlayersHelper

  def birth_date_form_column(record, input_name)

    date_select(record, :birth_date, :end_year => Date.today.year, :start_year => 1950, :name => input_name)

  end

  def age_column(record)

    record.calculate_age

  end

end

players_controller.rb

class PlayersController < ApplicationController

  active_scaffold :player do |config|

    config.columns = [:name, :age]

    config.update.columns.exclude [:age]

  end

end

player.rb

def calculate_age

  Date.today.year - birth_date.to_date.year if birth_date

end

4. Sort the players according to their age:

players_controller.rb

config.columns[:age].sort_by :method => "calculate_age"

5. Modify the label for birth_date to Birthday:

The label method adds custom label to columns as shown.

players_controller.rb

config.columns[:birth_date].label = "Birthday"

Note:

In Active Scaffold if we override a column or field for one model it is reflected for all the other models. For example:

We are modifying the sport column for the coach model and displaying the sport as label instead of the default link to the nested sub-form. We do this via a helper method in coaches_helper.rb.

coaches_helper.rb

module CoachesHelper

  def sport_column

    label record, record.sport.name

  end

end

Adding this code changes the sport column at all places in the application. Just as shown in the list view for equipments. Here sport is now a label.

To resolve this issue there are two approaches:

  • Remove this line from application_controller.rb
    helper :all
  • Add a helper in the application_helper.rb that decides the column definition according to the class_name as explained below:

application_helper.rb

module ApplicationHelper

  def sport_column(record)

    if record.class.name == "Coach"

      label record, record.sport.name

    else

      record.sport.name

    end

  end

end

6. Adding an action link: We can add custom links that link to custom actions in the controller. These actions can be record-specific or class-specific i.e. can be common for all the records or specific to each record. Here is an illustration explaining the implementation:

Each player will have membership fees. The membership fees can be updated by the administrator. We add a collection get_players_list that displays all the players as a list.

Migration

class AddMembershipFeeToPlayer < ActiveRecord::Migration

  def self.up

    add_column :players, :membership_fee, :integer, :default => 0

  end

  def self.down

    remove_column :players, :membership_fee

  end

end

players_controller.rb

config.action_links.add "Bulk Update Membership Fee", {:action => 'get_players_list'}

 

def get_players_list

  render(:layout => false)

end

def bulk_update_fee

  membership_fee = params[:membership_fee].to_i

  if membership_fee > 0

    if params["player"]

      params["player"].keys.each do |id|

        if params["player"][id]["selected_for_updating_fee"] == "1"

          @player = Player.find(id.to_i)

          @player.new_sport_ids = @player.sport_ids

          @player.update_attribute :membership_fee, membership_fee

        end

      end

    end

    respond_to do |format|

      format.js do

        render :update do |page|

          page.replace_html 'notice', "<div class='notice'>Selected players updated with membership of $#{membersip_fee}.</div>"

          page.reload

        end

      end

    end

  else

    respond_to do |format|

      format.js do

        render :update do |page|

          page.replace_html 'notice', "<div class='error'>Please add a valid fee amount.</div>"

        end

      end

    end

  end

end

7. Show a select box for membership fee with custom description

players_controller.rb

config.columns[:membership_fee].description = "Membership Fee in dollars($)"

config.columns[:membership_fee].form_ui = :select

config.columns[:membership_fee].options = [10, 50, 150, 400]

Output:

8. Customize model instance on the show/edit page:

While displaying the model instances Active Scaffold looks for the following methods in the given order.

  • to_label
  • name
  • label
  • title
  • to_s

We can customize it by defining to_label method explicitly as follows:

The instance is displayed in the PlayersSports edit view:

Instead we can display the instance by the following rule ‘Baichung Bhutia - Badminton’ to make it more meaningful.

players_sport.rb

def to_label

  "#{player.name} - #{sport.name}"

end

9. We can also globally configure all the controllers together by defining the methods in application controller.

application_controller.rb

ActiveScaffold.set_defaults do |config|

  # modify the method here

end

10. Display list of sports for selection on the player instance form:

players_helper.rb

def sports_form_column(record, input_name)

  collection_multiple_select( 'record', 'sport_ids', Sport.find(:all), :id, :name)

end

player.rb

class Player < ActiveRecord::Base

  has_many :players_sports, :dependent => :destroy

  has_many :sports, :through => :players_sports

  validates_presence_of :name

  attr_accessor :selected_for_updating_fee, :new_sport_ids

  after_save :update_sports

  def calculate_age

    Date.today.year-birth_date.to_date.year if birth_date

  end

  def sport_ids

    self.sports.map(&:id)

  end

  def update_sports

    self.new_sport_ids = (self.new_sport_ids || []).reject(&:blank?)

    old_ids = self.sport_ids

 

    self.transaction do

      PlayersSport.destroy_all({:sport_id => old_ids - self.new_sport_ids, :player_id => self.id })

      (self.new_sport_ids - old_ids).each do |op_sys_id|

        self.players_sports.create!({:sport_id => op_sys_id})

      end

    end

    self.reload

  end

end

players_controller.rb

def before_update_save(record)

  record.new_sport_ids = params[:record][:sport_ids] unless params[:record][:sport_ids].blank?

end

def before_create_save(record)

  record.new_sport_ids = params[:record][:sport_ids] unless params[:record][:sport_ids].blank?

end