![]() |
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