解放 RoR:讓 ActiveScaffold 來管理數據輸入頁面
簡介:節省點時間,少一些頭痛,使用 Ruby on Rails ActiveScaffold 插件可以創建更容易維護的 頁面。ActiveScaffold 可以處理用戶接口所需的所有 CRUD(創建、讀取、更新和刪除)操作,這樣可以 為您節省更多時間來重點關注更有挑戰(也更有趣的)問題。
為復雜應用程序編寫基於 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
User、Organization 和 Project 表都代表域中的傳統實體,而 ProjectsUsers 表則會在 Project 和 User 實體之間增加一個多對多的關系。在本例中,它會添加一個 role_type 屬性,它代表用戶在項 目中所扮演的角色。用戶可能是工人、經理和/或管理員。
在模型上創建任何用戶界面所需要的關 鍵信息都是要理解模型之間的關系。通過在模型中聲明 has_many 和 belongs_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
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
這個默認 顯示可以很好地用於快速構建原型或開發。然而,與所有默認情況類似,稍加定制就可以使它更加適合於 您的特定需求。
定制視圖
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_many 和 has_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 操作:create、list、 search、 show、 update 或 delete。
清單 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} 是 create、read、 update 或 destroy 之一。
清單 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 系統中的一種,它可以極大地簡化數據輸入頁面的創建和維護(有關這些問題的信息,請參看下面 參考資料 中的內容)。
本文配套源碼