使用 ActiveScaffold 增强 Ruby on Rails 的功能

解放 RoR:让 ActiveScaffold 来管理数据输入页面

节省点时间,少一些头痛,使用 Ruby on Rails ActiveScaffold 插件可以创建更容易维护的页面。ActiveScaffold 可以处理用户接口所需的所有 CRUD(创建、读取、更新和删除)操作,这样可以为您节省更多时间来重点关注更有挑战(也更有趣的)问题。

Mike Perham (developerworks@perham.net), 高级软件工程师, IBM

author photoMike Perham 是 IBM 的一名高级软件工程师,目前正在从事 WebSphere Business Services Fabric 方面的工作。他是 Apache 项目的成员之一,自 1995 年开始从事开源软件开发。他喜欢摩托车比赛,学习新技术,尤其是能够简化基于 Web 的应用程序的构建的一些技术。



2007 年 7 月 09 日

为复杂应用程序编写基于 Web 的数据输入 UI 永远都不是件快乐的事,通常都是非常单调乏味的。良好用户界面的一个关键属性是一致性,但是这需要一个博学勤勉的开发团队才能设计符合这种设计标准的 Web 页面。与其他 Web 应用程序框架类似,Ruby on Rails 也有相同的问题。不过,Ruby 语言的动态特性提供了一个解决方案:ActiveScaffold。ActiveScaffold 是 Ruby on Rails (也称为 Rails)的一个插件,它可以动态地生成基于模型的视图。ActiveScaffold 不需要手工创建页面来显示模型,而是可以从内部审视 ActiveRecord 模型,并动态地生成一个 CRUD(创建、读取、更新、删除)用户界面来管理这些对象。

本文是基于 ActiveScaffold、Ruby 和 Rails 的当前(撰写本文之时)可用的最新版本来撰写的(链接和版本号请参看 参考资料)。另外,本文假设您非常熟悉 Ruby on Rails,并且正在使用 Linux® 或 Mac OS X 系统。Windows® 用户应该修改本文中给出的命令来适合自己的环境(例如,将 ‘ruby’ 添加到脚本命令最前面)。

安装 ActiveScaffold

由于 ActiveScaffold 是一个 Rails 插件,可以从一个远程 Web 或者 Subversion 服务器上安装。下面的命令将从 ActiveScaffold Subversion 服务器中获取 ActiveScaffold。

清单 1. 安装 ActiveScaffold 插件
script/plugin install http://activescaffold.googlecode.com/svn/tags/active_scaffold

注意这将获取 ActiveScaffold 的当前发行版(即最新发行版)。撰写本文时使用的是 1.0 发行版,但是也可以使用将来的发行版:ActiveScaffold 开发人员迄今为止一直很好地关注着兼容性问题。


模型

最现代的 Web 应用程序框架都基于 MVC(模型、视图、控制器)模式,Rails 也不例外。模型表示数据库中存储的数据,每个表在 Ruby 中都有一个对应的 ActiveRecord 模型类。在本文中,我们创建了一个简单的项目跟踪应用程序,其中,组织拥有很多用户和很多项目。下面的代码显示了 ActiveRecord 向应用程序和对应模型类上迁移的过程。注意模型类要比 Java 中相同的类简单很多。这是 Rails 的 DRY(不要重复自己)原则的基本例子。由于迁移早已包含了列,为什么还要在模型类中再次将它们列出来呢?

清单 2. 迁移
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
清单 3. 模型
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

Ruby on Rails 可以支持两种 “开发助手”。生成器 是静态的 — 它们只会在生成代码时运行一次。插件 是动态的 — 它们可以作为应用程序运行时的一部分运行。例如,标准的 Rails scaffold 生成器就是运行一次来基于模型中的当前字段创建一个静态 HTML 模板。如果希望添加一列,就必须重新生成视图(丢失所做的更改),或者将字段手工添加到视图中。这将给模型更改增加不必要的复杂性。

插件会在运行时生成这些视图,因此更改模型就不困难了。与生成器相比,插件并没有什么实际的劣势,但是可能会比使用生成器稍微复杂一点。

User、Organization 和 Project 表都代表域中的传统实体,而 ProjectsUsers 表则会在 Project 和 User 实体之间增加一个多对多的关系。在本例中,它会添加一个 role_type 属性,它代表用户在项目中所扮演的角色。用户可能是工人、经理和/或管理员。

在模型上创建任何用户界面所需要的关键信息都是要理解模型之间的关系。通过在模型中声明 has_manybelongs_to,就在它们之间定义了一种特定类型的关系。一旦 ActiveScaffold 知道了这些关系,就可以提供一个用户界面以一种用户可以理解的方式对这些对象进行操作。在这种情况下,ActiveScaffold 就可以确定某个 Project 是由某个 Organization 所有的,因此可以相应地调整用户界面。如果您更改了这种关系,则用户界面就可以相应地变化,无需开发人员更改 UI。

边注:由于 Rails 迁移框架中存在某种限制,使清单 2 中的迁移无法使用外键。为了确保数据一致性,强烈推荐使用这些外键。Redhill Consulting 提供了一个很好的 foreign_key_migrations 插件,它增加了在 Rail 数据库迁移框架中对外键的支持;有关更多信息,请参看 参考资料 中的链接。


Rails scaffold

现在我们已经充实了模型,接下来可以在上面放一个 Web 界面。Rail 提供了一个 “scaffold” 生成器,它可以为某个给定模型生成一组基本的 CRUD 页面。下面的命令用来为这个模型创建标准的 Ruby scaffold:即一个具有一组 CRUD 方法和一组对应的模型 HTML 视图的控件。

清单 4. 生成标准的 Rails scaffold
script/generate scaffold user
script/generate scaffold organization

scaffold 生成器有几个重要的限制:

  • 没有关系支持:创建模型实例就意味着只能编辑实例的基本属性。如果模型需要定义一个关系(例如,Project 需要选中所有的 Organization),就需要手工修改页面将这个域添加到窗体上。
  • 不能往返:对于模型反复进行的更改不支持 “往返” 操作,这是因为所生成的代码是静态的。一旦代码被修改之后,就不能在不丢失所做更改的情况下重新生成 scaffold 了。
  • 缺少样式支持:所生成的页面都是最基本的黑色和白色,只有最少量的 CSS 支持。不对基本的 HTML 标记使用样式就不支持通过 CSS 使用皮肤功能。

同时具有前两个限制说明 scaffold 实际上更像一个玩具,而不像一个有用的工具。图 1 给出了 Rails 提供的默认 scaffold。

图 1. 标准的 Rails scaffold
图 1. 标准的 Rails scaffold

Rails 还包括了 dynamic scaffold,它实际上提供了相同的代码支持,而不需要提前生成控件代码。这并没有给您带来太多好处 — 因为大部分代码都位于 HTML 视图中,而且仍然需要视图代码。通过将 scaffold 方法添加到控件类中可以启用动态 scaffold。

清单 5. 添加 Rails 的标准 scaffold
class UsersController < ApplicationController
  scaffold :user
end

小心!
如果标准的 Rails scaffold 代码与 ActiveScaffold 一起使用,就可能会出现问题。在切换到 ActiveScaffold 之前,请确保您已经清除了所有的 scaffold 控件和视图代码。


ActiveScaffold 默认显示

ActiveScaffold 为模型提供了一个更加有用的 UI。scaffold 的上述 3 个问题都已解决。首先,我们需要修改控件来使用 ActiveScaffold scaffold:

清单 6. 添加 ActiveScaffold scaffold
class UsersController < ApplicationController
  active_scaffold :user
  layout "activescaffold"
end

然后为所有 ActiveScaffold 页面添加标准布局(将下面的代码放入 app/views/layouts/activescaffold.rhtml 中):

清单 7. ActiveScaffold 标准布局
<!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>

现在用户清单看起来就好多了:

图 2. 标准的 ActiveScaffold scaffold
图 2. 标准的 ActiveScaffold scaffold

这个默认显示可以很好地用于快速构建原型或开发。然而,与所有默认情况类似,稍加定制就可以使它更加适合于您的特定需求。


定制视图

ActiveScaffold 有几个钩子函数可以让您定制如何显示模型。可以给 active_scaffold 方法传递一个用来配置 scaffold 的可选配置块。

全局配置

ActiveScaffold 的全局配置允许对所有控件进行定制:

清单 8. 全局配置
class ApplicationController < ActionController::Base
  AjaxScaffold.set_defaults do |conf|
    conf.list.results_per_page = 20
  end
end

这个例子配置系统中所有的 ActiveScaffold scaffold 以便在显示记录时每页显示 20 个结果。

本地配置

每个控件 scaffold 都可以使用自己特有的 ActiveScaffold 配置。

清单 9. 特定控件的本地配置
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

这个例子就不能更新模型实例、更改列表标题和定制默认用户列表排序了。sorting 让您可以控制如何从数据库中返回记录,并期望得到一个 {column => direction} 散列数组。还配置 ActiveScaffold 不显示用户不需要查看的特定列;在本例中,password_hash 列不需要在 UI 中显示,因此就将其排除了。

显示 ActiveRecord 对象

to_label 方法让您可以定制模型实例在页面中的显示方式。默认情况下,ActiveScaffold 会查找模型中的一组方法:

  • to_label
  • name
  • label
  • title
  • to_s

最后一个方法是由 ActiveRecord 提供的,会显示成 “#:<Address:0xFFFFFF:>” 的形式,这对于用户来说不够友好。下面是一个更好的方法:

清单 10. 定制模型的显示
class User < ActiveRecord::Base
  belongs_to :organization

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

例如,用户的 to_label 可能是 John Doe

定制属性显示

ActiveScaffold 允许开发人员完全控制模型属性的实际显示方式。默认情况下,ActiveScaffold 只会对一些简单的属性值调用 to_s,从而确定它们到 HTML 的顺序。要对此进行定制,只需要在 app/helpers/<model>_helper.rb 中将一个列显示帮助方法添加到相应的帮助类中即可。

清单 11. 定制属性显示
def birthdate_column(record)
  record.birthdate.strftime("%d %B %Y")
end

在上面的帮助方法中,您拥有记录的全部访问权限。在本例中,这个帮助并不是很智能,因为它并不能说明用户所请求的现场,确定日期格式就需要用到该现场。

对于 has_manyhas_and_belongs_to_many 关联来说,ActiveScaffold 会通过使用上面提到的 to_label 逻辑来渲染它们,从而显示前 3 个条目。这 3 个条目会链接在一起,这样在点击时,整个关联就可以显示出来了。这可以防止用户界面被大型关联集所覆盖的情况。

窗体显示

ActiveScaffold 也可以基于 Rails 的 ActiveRecord 和 ActiveView 库为模型创建一个窗体。varchar 列会变成文本输入,boolean 型变量会映射成 HTML 的复选框等等。

有一点需要注意:虚拟属性(在模型中作为属性定义,但却不真正保存在数据库中的属性)的 HTML 渲染方式可能与普通的模型属性不同。任何名字中包含 “password” 的普通模型属性在 HTML 都会渲染为一个密码输入。不过对于虚拟属性来说却并非如此,在使用虚拟属性作为密码窗体输入时,很容易发现这一点。在这种情况下,我们将使用虚拟属性来捕获窗体输入,并在保存时将这些值映射到 password_hash 列中,这样用户的纯文本输入就可以作为一个 SHA256 散列安全地保存到数据库中。

清单 12. 在用户模型中创建虚拟属性
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

我们添加了两个 form_column 帮助方法将它们作为密码输入正确地进行渲染。ActiveScaffold 期望使用 field_name 参数中给定的名称对输入进行 POST 处理。

清单 13. 定制虚拟属性的窗体显示
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

关系

到现在为止,我们只考虑了基本的模型操作,例如显示或编辑简单的列值。ActiveScaffold 中最复杂的部分是确定模型之间的关系以及它们如何影响应用程序的用户界面。要正确实现操作就几乎无法避开这一部分;本节将介绍如何配置 ActiveScaffold 来正确使用模型。

列表显示

为了在模型之间进行导航,ActiveScaffold 会在一个列表视图中显示关系链接。举例来说,在查看一个组织列表时,会看到一个 Users 链接来显示一个页面,其中每个 Users 都对应一个给定的 Organization。要定制此链接,需要为该列定义一个帮助方法:

清单 14. 定制关联的显示
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 还提供了基于所定义的关系在模型之间进行导航的功能。以 belongs_to 关系为例。在上面的例子中,User belongs_to 一个组织。这就意味着一个 User 在创建时必须具有一个相关的 Organization(如果 Organization 是可选的,那么您就应该使用一个可以为空值的 has_one 关系)。 ActiveScaffold 可以理解这种关系,并可以使用 “select” 语句从数据库中显示一个 Organizations 列表,这样用户可以选出与正在创建的 User 关联在一起的 Organization。

这对于只有 10 到 20 个 Organization 的小型数据集而言可以很好地工作,但是却不能扩展到具有大量的 Organization 的情况。您可以使用窗体列渲染程序来重写对列的渲染。下面给出了一个简单的例子,其中,您可以在开发时获悉可能的值:

清单 15. 使用静态选择列表
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 提供了一些有用的搜索功能来查找大型表中的记录。默认情况下,scaffold 在上面的目录表中有一个 “Search” 链接,使用该链接可以打开一个文本框,用户可以在这个文本框中输入搜索条件。ActiveScaffold 会创建一条 SQL 语句为模型搜索所有的 varchar 列,这样输入诸如 “ham” 之类的条件就可以找到基于姓氏的用户记录了。与其他地方类似,这里也有几个配置选项。

实时搜索

当用户按下 Return 时,就会执行默认搜索。ActiveScaffold 可以通过启用 “实时搜索” 选项来进行实时搜索。这会基于用户当前输入每秒生成一个 Ajax 调用。记住,实时搜索可能是数据库密集型的。正如下面解释的一样,在使用这个特性之前,要确保您已经配置了要搜索的列,并且已经正确地创建了表索引。

清单 16. 在实时搜索和默认搜索之间进行切换
ActiveScaffold.set_defaults do |conf|
  conf.actions.exclude :search
  conf.actions.add :live_search
end

调节可用性

清单 17. 调节 scaffold 的搜索配置
active_scaffold :user do |conf|
  conf.live_search.columns = [:last_name, :first_name]
  conf.live_search.full_text_search = false
end

这段代码告诉 ActiveScaffold 要限制对这个 scaffold 的搜索 —— 只允许使用用户的姓和名进行搜索,禁用全文搜索。后一个选项是用于大型表的伸缩性调节选项。如果用户搜索 “ham”,默认情况下,ActiveScaffold 会生成一条 SQL 语句,其中包含以下内容:lower(column_name) LIKE "%ham%",无法为它编制索引。通过禁用全文搜索,告诉它使用 “以...开始” 的语义:lower(column_name) LIKE "ham%"。这固然限制了搜索的灵活性,但得到了更好的伸缩性。


定制操作

除了标准的 CRUD 操作之外,ActiveScaffold 还可以让您定义自己的控件操作。数据库应用程序经常需要将数据导出为 PDF、Excel、CSV 或 XML。添加此功能非常容易,首先我们可以为具有对应操作方法的控件增加一个 “操作链接”:

清单 18. 定义定制操作
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

您可以通过将实际的模型知识封装到模型中来保证代码是面向对象的。

清单 19. 定制操作的对应模型方法
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

本地化

软件要想在全球得到广泛使用,一个关键特性是它需要能够以用户的本地语言进行操作。Ruby 和 Rails 并没有提供标准的 API 来处理 locale,因此它的集成比其他方法的集成更加困难(比如 Java 集成)。ActiveScaffold 小组决定将所有的本地化工作推迟给应用程序执行,这是通过一个简单的查找钩子 Object::as_ 方法实现的,它会嵌入到您喜欢的 Ruby 本地化插件中。在本例中,代码显示了如何将方法参数传递给 Globalize 插件(请参看 参考资料 中的链接)所提供的 _ 方法(是的,这个方法的名字就是 “_”)。

清单 20. 对 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 现在可以为传入这个方法的所有字符串提供本地化翻译。


定制 ActiveScaffold 的样式

ActiveScaffold 提供了一组非常丰富的 CSS 样式,可以对标准的 UI 进行调整以便提供定制的外观。您可以通过创建并重写 CSS 文件并将其包含到标准的 CSS 包含文件之后,从而重写默认样式来匹配站点的颜色方案、字体等设置。在本例中,我们包含了一个名为 public/stylesheets/as_overrides.css 的文件:

清单 21. 重写默认的 ActiveScaffold 样式
<head>
   <title>My Application</title>
   <%= javascript_include_tag :defaults %>
   <%= active_scaffold_includes %>
   <%= stylesheet_include_tag "as_overrides" %>
</head>

标准的 ActiveScaffold 样式表位于 vendor/plugins/active_scaffold/frontends/default/stylesheets/stylesheet.css 中。


安全性

ActiveScaffold 提供了一个身份验证 API 来确保数据安全性。第一级是对控件进行的粗粒度的安全性控制,这并不是记录特有的。在控件上,可以定义 #{action}_authorized? 方法,其中 #{action} 是一个 ActiveScaffold 操作:createlistsearchshowupdatedelete

清单 22. 基于控件的安全性
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

第二级安全性让您可以创建更加复杂的数据特有的逻辑。举例来说,由于 Projects belongs_to Organizations,因此对项目的编辑进行限制,使得只有拥有项目的组织的管理员才能执行编辑操作,这是合理的。为此,将方法添加到模型(比如 authorized_for_#{crud_action})中,其中 #{crud_action}createreadupdatedestroy 之一。

清单 23. 基于模型的安全性
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

注意 current_user 方法是可用的,因为 ActiveScaffold 根据对应控件的 current_user_method 配置将其插入了模型中。


结束语

诸如 Ruby 之类的动态语言中启用了诸如 Java™ 语言和 PHP 之类的静态语言中所没有的一些功能。ActiveScaffold 是众多基于模型的 “智能” UI 系统中的一种,它可以极大地简化数据输入页面的创建和维护(有关这些问题的信息,请参看下面 参考资料 中的内容)。

对于本文有什么评论或问题吗? 请在我的 blog 上发表评论,并让我知道您的想法!


下载

描述名字大小
Rails 项目的源代码IntroAS-sample.zip101KB

参考资料

学习

获得产品和技术

  • 利用可直接从 developerWorks 下载的 IBM 试用软件 在 Linux 上构建您的下一个开发项目。

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Linux, Open source, Web development
ArticleID=239416
ArticleTitle=使用 ActiveScaffold 增强 Ruby on Rails 的功能
publish-date=07092007