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).
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.
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
|
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.
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
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
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.
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.
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.
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.
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.
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
|
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.
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
|
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
|
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.
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
|
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.
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
|
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.
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.
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.
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!
| Description | Name | Size | Download method |
|---|---|---|---|
| Source code for Rails project | IntroAS-sample.zip | 101KB | HTTP |
Information about download methods
Learn
-
ActiveScaffold is just one of many
"smart" UI systems. Others include:
- The Django Project, a Rails-like system for the Python language that contains similar functionality that predates ActiveScaffold.
- Streamlined, another smart scaffolding system for Rails that provides functionality similar to ActiveScaffold.
- Hobo, a Rails plugin designed to make Rails application development even quicker.
- This article is based on
ActiveScaffold 1.0. This version
requires Rails 1.2 and this article
was tested with Ruby 1.8.6
and Rails 1.2.3.
- Learn more about
Using
foreign keys in Rails migrations.
-
Globalize is a Rails
plugin that provides internationalization and localization capabilities to Rails
applications.
- In the
developerWorks Linux zone,
find more resources for Linux developers, including
Linux tutorials,
as well as
our readers' favorite Linux articles and tutorials
over the last month.
- Stay current with
developerWorks technical events and Webcasts.
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
- Get involved in the
developerWorks community
through our developer blogs, forums, podcasts, and community topics in our new
developerWorks spaces.

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.



