Skip to main content

Turbocharge Ruby on Rails with ActiveScaffold

Take the drudgery out of RoR: Let ActiveScaffold manage your data-entry pages

Mike Perham (developerworks@perham.net), Senior Software Engineer, IBM
author photo
Mike Perham is a Senior Software Engineer at IBM working on the WebSphere Business Services Fabric. He's a member of the Apache project and has been developing open source software since 1995. He loves racing motorcycles and learning new technologies, especially anything that makes building Web-based applications easier.

Summary:  Save time and headaches, and create a more easily maintainable set of pages, with the Ruby on Rails ActiveScaffold plugin. ActiveScaffold handles all your CRUD (create, read, update, delete) user interface needs, leaving you more time to focus on more challenging (and interesting!) problems.

Date:  08 Jun 2007
Level:  Intermediate
Activity:  10844 views

Writing a Web-based data entry UI for a complex application is never fun and is often downright tedious. One key attribute for a good user interface is consistency, but it still takes a knowledgeable and diligent development team to create the Web pages that follow through on that design. Like all other Web application frameworks, Ruby on Rails has this same problem. However the dynamic nature of the Ruby language provides for a solution: ActiveScaffold. ActiveScaffold is a plugin for Ruby on Rails (also known as Rails) that provides dynamic model-based view generation. Instead of having to create pages by hand that display your models, ActiveScaffold will introspect your ActiveRecord models and dynamically generate a CRUD (create, read, update, delete) user interface for managing those objects.

This article is based on the latest currently available versions (as of this writing) of ActiveScaffold, Ruby, and Rails (see Resources for links and version numbers). Also, this article assumes that you are familiar with Ruby on Rails and are using Linux® or Mac OS X. Windows® users should modify any commands given to suit their own environment (for example, adding 'ruby' to the start of script commands).

Installing ActiveScaffold

As ActiveScaffold is a Rails plugin, you can install it from a remote Web or Subversion server. The command below will check out the ActiveScaffold plugin from the ActiveScaffold Subversion server.


Listing 1. Installing the ActiveScaffold plugin
                
script/plugin install http://activescaffold.googlecode.com/svn/tags/active_scaffold

Note that this will check out the current release (in other words, the latest release) of ActiveScaffold. This article was written to work with the 1.0 release, but may work with future releases: the ActiveScaffold people have been pretty good about watching for compatibility issues so far.


The model

Most modern Web application frameworks are based on the MVC (model, view, controller) pattern, and Rails is no different. The model represents the data stored in the database with each table having a corresponding ActiveRecord model class in Ruby. In this article, we create a simple project-tracking application where an organization has many users and many projects. The code below shows the ActiveRecord migrations for the application and the corresponding model classes. Note that the model classes are much simpler than the equivalent in the Java world. This is a prime example of Rails' DRY (Don't Repeat Yourself) principle. Because the migration already contains the columns, why should you list them again in the model class?


Listing 2. The migrations
                
class AddOrganizations < ActiveRecord::Migration
  def self.up
    create_table :organizations do |t|
      t.column :name, :string, :limit => 50, :null => false
    end
  end

  def self.down
    drop_table :organizations
  end
end

class AddUsers < ActiveRecord::Migration
  def self.up
    create_table :users do |t|
      t.column :first_name, :string, :limit => 50, :null => false
      t.column :last_name, :string, :limit => 50, :null => false
      t.column :email, :string, :limit => 100, :null => false
      t.column :password_hash, :string, :limit => 64, :null => false
      t.column :organization_id, :integer, :null => false
    end
    add_index :users, :email, :unique => true
  end

  def self.down
    drop_table :users
  end
end

class AddProjects < ActiveRecord::Migration
  def self.up
    create_table :projects do |t|
      t.column :name, :string, :limit => 50, :null => false
      t.column :organization_id, :integer, :null => false
    end
  end

  def self.down
    drop_table :projects
  end
end

class AddProjectsUsers < ActiveRecord::Migration
  def self.up
    create_table :projects_users do |t|
      t.column :project_id, :integer, :null => false
      t.column :user_id, :integer, :null => false
      t.column :role_type, :integer, :null => false
    end
  end

  def self.down
    drop_table :projects_users
  end
end


Listing 3. The models
                
class User < ActiveRecord::Base
   belongs_to :organization
end
class Organization < ActiveRecord::Base
  has_many :projects
  has_many :users
end
class Project < ActiveRecord::Base
  belongs_to :organization
  has_many :projects_users
  has_many :administrators, :through => :projects_users, :source => :user,
           :conditions => "projects_users.role_type = 3"
  has_many :managers, :through => :projects_users, :source => :user,
           :conditions => "projects_users.role_type = 2"
  has_many :workers, :through => :projects_users, :source => :user,
           :conditions => "projects_users.role_type = 1"
end
class ProjectsUser < ActiveRecord::Base
  belongs_to :project
  belongs_to :user
end

Plugins vs. generators

Ruby on Rails supports two types of "development assistants." Generators are static—they run once to generate some code. Plugins are dynamic—they run as part of the application runtime. For example, the standard Rails scaffold generator is run once to create a static HTML template based on the current fields in the model. If you want to add a column, you must regenerate the view (and lose any changes you made) or add the field to the view by hand. This makes model changes harder than necessary.

A plugin will generate those views at runtime, and so model changes become painless. There's no real downside to plugins vs. generators, but they can be a little more complex to use than generators.

The User, Organization, and Project tables represent traditional entities in our domain, while the ProjectsUsers table adds a many-to-many relationship between the Project and User entities. In this case, it adds a role_type property, which represents the role a user plays on a project. A User may be a worker, manager, and/or administrator.

The key information required to create any user interface on top of a model is to understand the relationships between the models. By declaring has_many and belongs_to in the models, you are defining a specific type of relationship between them. Once ActiveScaffold knows these relationships, it can provide a user interface to manipulate these objects in a manner that makes sense to the user. In this case, ActiveScaffold can determine that a Project is owned by an Organization and so adjust the user interface accordingly. If you change this relationship, the user interface will change accordingly without the developer having to make any UI changes.

On a side note: the migrations in Listing 2 unfortunately don't use foreign keys due to an unfortunate limitation in Rails' migration framework. I do strongly recommend their use in order to ensure data consistency. Redhill Consulting provides a nice foreign_key_migrations plugin, which adds support for foreign keys within Rails' database migration framework; see Resources for a link to more info.


Rails scaffolding

Now that the model is fleshed out, you can put a Web interface on top of it. Rails ships with a "scaffold" generator that will generate a basic set of CRUD pages for a given model. The following command will create the standard Ruby scaffolding for the model: a controller with a set of CRUD methods and the corresponding set of HTML views for the model.


Listing 4. Generating the standard Rails scaffolding
                
script/generate scaffold user
script/generate scaffold organization

The scaffold generator has several significant limitations:

  • No relationship support: creating a model instance means you are just editing the basic properties on that instance. If the model requires a relationship to be defined (for example, the Project requires the owning Organization to be selected), you will need to modify the page by hand to add that field to the form.

  • No round tripping: there's no "round trip" support for iterative model changes, because the generated code is static. Once the code is modified, you cannot regenerate the scaffold without losing those changes.

  • No styling: the generated pages are basic black and white with minimal CSS support. There's no support for skinning via CSS without styling the basic HTML tags.

The first two items together mean that scaffolding really is more of a toy than a useful tool. Figure 1 shows the default scaffolding provided with Rails.


Figure 1. The standard Rails scaffolding
Figure 1. The standard Rails scaffolding

Rails also includes dynamic scaffolding, which essentially provides the same code support without the need for the pre-generated controller code. This doesn't gain you much—most of the code is in the HTML views and view code is still required. Dynamic scaffolding is enabled by adding the scaffold method to your controller class.


Listing 5. Adding Rails's standard scaffolding
                
class UsersController < ApplicationController
  scaffold :user
end

Be careful!
The standard Rails scaffold code will cause problems if you try to use it with ActiveScaffold. Please make sure you clean out any scaffold controller and view code before switching to ActiveScaffold.


ActiveScaffold default display

ActiveScaffold provides a much more useful UI for the model. The three issues with scaffolding noted above are all solved. First, modify the controller to use the ActiveScaffold scaffolding:


Listing 6. Adding the ActiveScaffold scaffolding
                
class UsersController < ApplicationController
  active_scaffold :user
  layout "activescaffold"
end

Then add the standard layout for all ActiveScaffold pages (put this code in app/views/layouts/activescaffold.rhtml):


Listing 7. The standard ActiveScaffold layout
                
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
       "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
   <title>My Application</title>
   <%= javascript_include_tag :defaults %>
   <%= active_scaffold_includes %>
</head>
<body>
   <%= yield %>
</body>
</html>

Now the user listing looks much nicer:


Figure 2. The standard ActiveScaffold scaffolding
Figure 2. The standard ActiveScaffold scaffolding

The default display is good for rapid prototyping or development usage. However, like all defaults, a little customization can make it much better for your specific needs.


View customization

ActiveScaffold has several hooks that allow you to customize how a model is displayed. The active_scaffold method can be passed an optional configuration block used to configure that scaffold.

Global configuration

ActiveScaffold's global configuration allows customization across all controllers:


Listing 8. Global configuration
                
class ApplicationController < ActionController::Base
  AjaxScaffold.set_defaults do |conf|
    conf.list.results_per_page = 20
  end
end

This example configures all ActiveScaffold scaffolds in your system to display 20 results per page when listing records.

Local configuration

Each controller scaffold can have its own individual ActiveScaffold configuration.


Listing 9. Local configuration for a specific controller
                
class UsersController < ApplicationController
  active_scaffold :user do |conf|
    conf.modules.exclude :update
    conf.list.label = 'People'
    conf.list.sorting = [{:last_name => :ASC}, {:first_name => :ASC}]
    conf.list.columns.exclude :password_hash
  end
end

This example removes the ability to update instances of your model, changes the list heading, and customizes the default user list sorting. sorting allows you to control how records are returned from the database and expects an array of {column => direction} hashes. I also configured ActiveScaffold not to display certain columns that the user does not need to see; in this example, the password_hash column would be pointless to display in the UI, so it has been excluded.

Displaying ActiveRecord objects

The to_label method allows you to customize how a model instance is displayed in the page. By default, ActiveScaffold will look for a set of methods on your model:

  • to_label
  • name
  • label
  • title
  • to_s

The last method is provided by ActiveRecord and displays as "#:<Address:0xFFFFFF:>", which isn't exactly user-friendly. Here's a better method:


Listing 10. Customizing display of your models
                
class User < ActiveRecord::Base
  belongs_to :organization

  def to_label
    first_name << ' ' << last_name
  end
end

For example, the to_label of a user might be John Doe.

Customizing property display

ActiveScaffold allows the developer full control of how a model property is actually displayed. By default, ActiveScaffold just calls to_s on simple property values to serialize them to HTML. To customize this, just add a column display helper method to the corresponding helper class in app/helpers/<model>_helper.rb.


Listing 11. Customizing property display
                
def birthdate_column(record)
  record.birthdate.strftime("%d %B %Y")
end

In the helper method above, you have full access to the record. In this case, this helper isn't terribly intelligent as it does not account for the user's requested locale, which should be used to determine the proper date format.

For has_many and has_and_belongs_to_many associations, ActiveScaffold will display the first three entries by rendering them using the to_label logic mentioned above. The three entries are linked so that when clicked, the entire association will be listed. This prevents the user interface being overwhelmed by large association sets.

Form display

ActiveScaffold will create a form for your model based on Rails' ActiveRecord and ActiveView libraries. varchar columns will become text inputs, booleans map onto HTML checkboxes, etc.

One caveat is that virtual properties (properties defined as attributes in the model but not actually stored in the database) can have different HTML rendering than regular model properties. Any normal model properties that contain "password" in their name will render in HTML as a password input. This is not true of virtual properties, though, and this is easy to discover when using virtual properties for password form input. In this case, we are using the virtual properties to capture the form input and mapping those values onto the password_hash column on save so that the user's plain text input is stored safely in the database as a SHA256 hash.


Listing 12. Creating virtual properties on the user model
                
require 'digest/sha2'

class User < ActiveRecord::Base
   attr_accessor :password, :password_confirmation
   validates_presence_of :password, :password_confirmation

   def validate
     errors.add('password', 'and confirmation do not match') \
            unless password_confirmation == password
   end

   def before_save
     self.password_hash = Digest::SHA256.hexdigest(password) if password
   end
end

We add two form_column helpers to render them correctly as password inputs. ActiveScaffold expects the inputs to be POSTed with the name given in the field_name parameter.


Listing 13. Customizing the form display of the virtual properties
                
def password_form_column(record, field_name)
  password_field_tag field_name, record.password
end
def password_confirmation_form_column(record, field_name)
  password_field_tag field_name, record.password_confirmation
end
      


Relationships

So far we've considered only basic model operations, like displaying or editing simple column values. The most complex part of ActiveScaffold is determining the relationships between your models and how they affect your application's user interface. This is nearly impossible to get correct out of the box; this section shows how you can configure ActiveScaffold to work correctly with your models.

List display

To navigate between models, ActiveScaffold displays relationship links in the list view. For instance, when viewing a list of organizations, you would see a Users link to display a page with the Users for the given Organization. To customize this link, you need define a helper method for that column:


Listing 14. Customizing association display
                
def users_column(record)
  name = "user"
  name = "users" if record.users.size > 1
  "<a href="/user/list?user_id=#{record.id}">#{record.users.size} #{name}</a>"
end

Form display

ActiveScaffold provides navigation between models based on their defined relationship. Take, for example, a belongs_to relationship. In the example above, a User belongs_to an Organization. This means that a User must have an Organization associated with it when it is created (if the Organization was optional, you would use a nullable has_one relationship instead). ActiveScaffold understands this and displays a list of Organizations from the database in a <select> so the user can pick the Organization associated with the User being created.

This works well for small datasets where you have 10 to 20 Organizations but does not scale to large numbers of Organizations. You can override the rendering of the column using a form column renderer. This is a simple example where you know the possible values at development time:


Listing 15. Using a static list of choices
                
def organization_form_column(record, field_name)
  # simple example that just hard codes two possible values
  select_tag field_name, options_for_select('IBM' => '1', 'Lenovo' => '2')
end


Searching for records

ActiveScaffold provides some useful search capabilities to find records within large tables. By default, your scaffold will have a "Search" link above the list table that opens a text box where the user can enter search terms. ActiveScaffold will create SQL that searches all varchar columns for the model so that entering a term like "ham" will find my user record based on my last name. Like other areas, there are a number of configuration options.

Live search

The default search goes to work when the user hits Return. ActiveScaffold can search in real-time by enabling "live search." This will make an Ajax call every second based on the user's current input. Keep in mind, live search can be database intensive. As explained below, be sure you have configured the search columns and properly indexed your tables before using this feature.


Listing 16. Swapping live search for the default search
                
ActiveScaffold.set_defaults do |conf|
  conf.actions.exclude :search
  conf.actions.add :live_search
end

Usability tweaks


Listing 17. Tweaking the search configuration for a scaffold
                
active_scaffold :user do |conf|
  conf.live_search.columns = [:last_name, :first_name]
  conf.live_search.full_text_search = false
end

This code tells ActiveScaffold to limit searching on this scaffold to the user's first and last name and disables full text search. The latter option is a scalability tweak useful for large tables. If the user searches for "ham," by default ActiveScaffold generates SQL along the lines of lower(column_name) LIKE "%ham%", which cannot be indexed. By disabling full text search, you are telling it to use "starts with" semantics instead: lower(column_name) LIKE "ham%". This of course limits the flexibility of your search, but it scales much better.


Custom actions

ActiveScaffold allows you to define your own controller actions in addition to the standard CRUD actions. Exporting data as PDF, Excel, CSV, or XML is a frequent requirement for database applications. Adding this capability is quite simple. First we add an "action link" to the controller with a corresponding action method:


Listing 18. Defining a custom action
                
class UsersController < ApplicationController
  active_scaffold :user do |conf|
    conf.action_links.add 'export_csv', :label => 'Export to Excel', :page => true
  end

  def export_csv
    # find_page is how the List module gets its data. see Actions::List#do_list.
    records = find_page().items
    return if records.size == 0

    # Note this code is very generic.  We could move this method and the
    # action_link configuration into the ApplicationController and reuse it
    # for all our models.
    data = ""
    cls = records[0].class
    data << cls.csv_header << "\r\n"
    records.each do |inst|
      data << inst.to_csv << "\r\n"
    end
    send_data data, :type => 'text/csv', :filename => cls.name.pluralize + '.csv'
  end
end

You can keep the code object oriented by encapsulating the actual model knowledge within the model itself.


Listing 19. The corresponding model methods for the custom action
                
class User < ActiveRecord::Base
  ...

  # The header line lists the attribute names.  ID is quoted to work
  # around an issue with Excel and CSV files that start with "ID".
  def self.csv_header
    ""ID",Last Name,First Name,Email,Birthdate"
  end

  # Emit our attribute values as a line of CSVs
  def to_csv
    id.to_s << "," << last_name << "," << first_name << "," << email << 
                     "," << birthdate.to_s
  end
end


Localization

One key feature for software to be used worldwide is the ability to work in the native language of the user. Ruby and Rails do not provide a standard API to handle locales, so the integration is more difficult than it would be, for instance, in Java. The ActiveScaffold team decided to defer all localization to the application through a simple lookup hook, the Object::as_ method, which can hook into your favorite Ruby localization plugin. In this case, the code shows how to pass through the method parameters to the _ method (yes, the method is actually named "_") provided by the Globalize plugin (see Resources for a link).


Listing 20. Localizing ActiveScaffold
                
# Put this at the bottom of your app/controllers/application.rb file
class Object
  def as_(string, *args)
    # Use Globalize's _ method to provide the actual lookup of the string.
    _(string, `	*args)
  end
end

Globalize can now provide localized translations for all strings passed into that method.


Styling ActiveScaffold

ActiveScaffold emits a rich set of CSS styles that allow the standard UI to be tweaked to provide a custom look and feel. You can override the default styles to match your site color scheme, fonts, and so on by creating an override CSS file and including it after the standard CSS includes. In this case, we include a file called public/stylesheets/as_overrides.css:


Listing 21. Overriding the default ActiveScaffold styles
                
<head>
   <title>My Application</title>
   <%= javascript_include_tag :defaults %>
   <%= active_scaffold_includes %>
   <%= stylesheet_include_tag "as_overrides" %>
</head>

The standard ActiveScaffold stylesheet can be found in vendor/plugins/active_scaffold/frontends/default/stylesheets/stylesheet.css.


Security

ActiveScaffold provides an authorization API to ensure data security. The first level is coarse-grained security on the controller, which is not record specific. On your controller, you can define #{action}_authorized? methods, where #{action} is an ActiveScaffold action: create, list, search, show, update, or delete.


Listing 22. Controller-based security
                
class ProjectsController < ApplicationController
  active_scaffold :project do |conf|
    # Needed to inject the current_user method into the model
    config.security.current_user_method = :current_user
  end

  protected

  # only authenticated admin users are authorized to create projects
  def create_authorized?
    user = current_user
    !user.nil? && user.is_admin?
  end

  def current_user
    @session[:user_id] ? User.find(@session[:user_id]) : nil
  end
end

The second level allows you to create more complex data-specific logic. For instance, because Projects belongs_to Organizations, it would be reasonable to limit the editing of a project to only the administrators of the organization that owns it. To do this, we add methods to our model such as authorized_for_#{crud_action}, where #{crud_action} is one of create, read, update, or destroy.


Listing 23. Model-based security
                
class Project < ActiveRecord::Base
  belongs_to :organization

  # Since projects are owned by an organization, allow only administrators
  # of that organization to edit the project
  def authorized_for_update?
    organization.is_admin? current_user
  end
end

Note the current_user method is available because ActiveScaffold injects it into the model based on your current_user_method configuration on the corresponding controller.


Conclusion

Dynamic languages like Ruby enable a set of functionality impossible with more static languages like the Java™ language and PHP. ActiveScaffold is one of a number of model-based "smart" UI systems that can dramatically simplify the creation and maintenance of data entry pages (see Resources, below, for a list of some of these).

Have a comment or question on this article? Post a comment on my blog and let me know what you think!



Download

DescriptionNameSizeDownload method
Source code for Rails projectIntroAS-sample.zip101KB HTTP

Information about download methods


Resources

Learn

Get products and technologies

  • Order the SEK for Linux, a two-DVD set containing the latest IBM trial software for Linux from DB2®, Lotus®, Rational®, Tivoli®, and WebSphere®.

  • With IBM trial software, available for download directly from developerWorks, build your next development project on Linux.

Discuss

About the author

author photo

Mike Perham is a Senior Software Engineer at IBM working on the WebSphere Business Services Fabric. He's a member of the Apache project and has been developing open source software since 1995. He loves racing motorcycles and learning new technologies, especially anything that makes building Web-based applications easier.

Comments (Undergoing maintenance)



Trademarks  |  My developerWorks terms and conditions

Help: Update or add to My dW interests

What's this?

This little timesaver lets you update your My developerWorks profile with just one click! The general subject of this content (AIX and UNIX, Information Management, Lotus, Rational, Tivoli, WebSphere, Java, Linux, Open source, SOA and Web services, Web development, or XML) will be added to the interests section of your profile, if it's not there already. You only need to be logged in to My developerWorks.

And what's the point of adding your interests to your profile? That's how you find other users with the same interests as yours, and see what they're reading and contributing to the community. Your interests also help us recommend relevant developerWorks content to you.

View your My developerWorks profile

Return from help

Help: Remove from My dW interests

What's this?

Removing this interest does not alter your profile, but rather removes this piece of content from a list of all content for which you've indicated interest. In a future enhancement to My developerWorks, you'll be able to see a record of that content.

View your My developerWorks profile

Return from help

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Linux, Open source, Web development
ArticleID=229526
ArticleTitle=Turbocharge Ruby on Rails with ActiveScaffold
publish-date=06082007
author1-email=developerworks@perham.net
author1-email-cc=

My developerWorks community

Tags

Help
Use the search field to find all types of content in My developerWorks with that tag.

Use the slider bar to see more or fewer tags.

Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere).

My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Use the search field to find all types of content in My developerWorks with that tag. Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere). My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Rate a product. Write a review.

Special offers