傑克達與瑞兒絲的大小事

快速筆記整理網站開發上碰到的各種技巧和問題,免得年紀大忘光,至少還有一個地方可以查找

Category Archives: Rails 補充資料

擴展 Paperclip 加入 hash 目錄參數,自訂 :path 和 :url

Paperclip 是 Rails 的一個上傳圖片插件,它可以很方便的幫助我們實現上傳圖片和裁剪圖片的功能。在安裝完 Paperclip 後,Paperclip 會有一組預設指定圖片顯示和存放的路徑,如下:

:url => "/uploadfiles/:class/:attachment/:id/:basename/:style.:extension",
:path => ":rails_root/public/uploadfiles/:class/:attachment/:id/:basename/:style.:extension"

但是因應專案的需求不同,你可能會要重新調整這組存放和顯示的路徑設定,例如,針對上傳的圖片會依照使用者放到對應的目錄下,所以我們需要在路徑加上使用者 ID,如下:

:url => "/uploadfiles/:user_id/:class/:attachment/:id/:basename/:style.:extension",
:path => ":rails_root/public/uploadfiles/:user_id/:class/:attachment/:id/:basename/:style.:extension"

這時該怎麼做呢?

實作方式:

1. 建立 config/initializers/paperclip_extensions.rb,加入以下程式碼

Paperclip.interpolates :user_id do |attachment, style|
return current_user.id
end

2. 修改 model 中 Paperclip 定義的 :path 和 :url 路徑

:url => "/uploadfiles/:user_id/:class/:attachment/:id/:basename/:style.:extension",
:path => ":rails_root/public/uploadfiles/:user_id/:class/:attachment/:id/:basename/:style.:extension"

參考文章:

https://github.com/thoughtbot/paperclip

http://stackoverflow.com/questions/4041373/paperclip-custom-path-and-url

http://www.oschina.net/question/54100_24721

ActiveRecord 新增操作的 create 和 create! 二者差別

雖然這是基本觀念,不過有時還是會分不清這二者的差別和使用時機,還是筆記一下吧!

以下的資料來源均摘錄於 ihower – Ruby on Rails 實戰聖經 ActiveRecord 該篇的內容說明

二者差異:

create 和 create! 就等於 new 完就 save 和 save!,有無驚嘆號的差別在於 validate 資料驗證不正確的動作
無驚嘆號版本會回傳布林值(true或false)
有驚嘆號版本則是驗證錯誤會丟出例外

使用時機:

何時使用驚嘆號版本呢?
save 和 create 通常用在會處理回傳布林值(true/false)的情況下 (例如在 controller 裡面根據成功失敗決定 render 或 redirect),否則預期應該會儲存成功的情況下,請用 save! 或 create! 來處理,這樣一旦碰到儲存失敗的情形,才好追蹤 bug。

參考文章:

http://ihower.tw/rails3/activerecord.html

Rails 專案遇到錯誤時,如何執行發信通知功能 (進階)

接續剛剛提到的專案遇到錯誤執行發信通知功能,如果有一些額外的資訊需要表達在信件中時,該怎麼做呢?

貼心的 exception_notification_rails3 另外有提供了一個 data 的 section 可以讓使用者自訂訊息內容

使用方法:

1. 開啟剛剛在 config/environments/production.rb 的設定,加上粗體表示的那行文字

config.middleware.use ::ExceptionNotifier,
:email_prefix => "[ERROR] ",
:sender_address => %{"Notifier" <notifier@yourdomain.com> },
:exception_recipients => %w{exceptions@yourdomain.com},
:sections => %w(data request session environment backtrace)

2. 接下來要自訂訊息內容,開啟 app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
before_filter :authenticate
before_filter :set_exception_data
...
def set_exception_data
# used for exception notification
user = User.current
return true if user.nil?
user_info = "名字: #{user.display_name}, 電子郵件: #{user.mail}, 部門: #{user.department}"
env['exception_notifier.exception_data'] = { :data=>{ :user => user_info , :deploy => Rails.env } }

true
end

3. 設定完成,之後便會在錯誤信件中看到定義的內容,結果如下:

-------------------------------
Data:
-------------------------------
* user: 名字: 小王, 電子郵件: smallwang@yourdomain.com, 部門: 資訊部
* deploy: production

參考文章:

http://qa.taobao.com/?p=13030

Rails 專案遇到錯誤時,如何執行發信通知功能

最近在專案上遇到一個需求,也就是當 Rails 專案在線上環境遇到錯誤時,要能發信通知開發者,方便快速反應修正問題。透過咕歌大神搜尋後,找到幾個解決方案,其中利用 exception_notification_rails3 gem 看來是最佳的解法,趕快筆記一下

使用方法:

1. 修改 Gemfile,加入以下指令

$ gem 'exception_notification_rails3', '~> 1.2.0', :require => 'exception_notifier'

2. 設定完成後,請執行 bundle 安裝 gem

$ bundle

3. 因為只針對線上 production 的環境才需要執行發信操作,所以開啟 config/environments/production.rb 加入以下設定

config.middleware.use ::ExceptionNotifier,
:email_prefix => "[ERROR] ",
:sender_address => %{"Notifier" <notifier@yourdomain.com> },
:exception_recipients => %w{exceptions@yourdomain.com}

email_prefix: 信件主旨的前綴詞,可自行定義
sender_address: 信件的寄件者
exception_recipients: 信件的收件者,如果有多個收件者,記得用空白隔開,如: %w{abc@yourdomain.com xyz@yourdomain.com}

附註:

如果你希望在 development 和 production 二個環境下都要能收到錯誤通知,可以直接在 config/applicaiton.rb 加入設定

參考文章:

https://github.com/rails/exception_notification

http://ryanwilliams.org/2010/Nov/04/exception-notification-in-rails-3

 

如何避免 Nokogiri 生成 html 時加上 doc 標籤

最近開發碰到一個需求很特殊,在使用 Nokogiri 解析 html 內容時,最後輸出 html 時,本文內容裡不要有 DTD 標籤描述,剛上網查到一種解法,照慣例先筆記

實作方式:

原本透過 Nokogiri 在產出 html 時,只需使用 to_html 方法,但 Nokogiri 會自動將 DTD 標籤補上,如下:

require 'nokogiri'

doc = Nokogiri::HTML('<p>foobar</p>')
puts doc.to_html
# >> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
# >> <html><body><p>foobar</p></body></html>

如果不想在產出 html 時加上 DTD 標籤,可以利用以下的方式:

doc = Nokogiri::HTML.fragment('<p>foobar</p>')
puts doc.to_html
# >> <p>foobar</p>

改為使用 HTML.fragment 知會 Nokogiri 只使用片段內容來解析,那麼 to_html 產出的內容就不會包含 DTD 標籤了

參考文章:

http://stackoverflow.com/questions/4723344/how-to-prevent-nokogiri-from-adding-doc-tags

http://nokogiri.org/

在 nginx 上執行 ruby on rails

最近想要在本地端設定一組 local 的 domain name 來測試一些劇情,所以需要 nginx 的配合,但又不想透過 unicorn 做為 web server ,因為不方便開發時 debug, 之前一直在思考如何在 nginx 上執行 ROR,以往的教學範例都是直接透過 rails -s 直接執行 web server,後來查到一篇文章教學,原來這麼簡單,趕快筆記一下

實作方式:

建立一組 nginx vhost 設定,內容如下:

upstream myapp {
server localhost:3000;
}
server {
listen 80;
server_name myapp.example.local;

access_log /var/www/myapp.example.local/log/access.log;
error_log /var/www/myapp.example.local/log/error.log;
root /var/www/myapp.example.local;
index index.html;

location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
try_files /system/maintenance.html $uri $uri/index.html $uri.html @ruby;
}

location @ruby {
proxy_pass http://myapp;
}
}

只要設定 uptream  指向到本地端的 rails server 就好了,最後在瀏覽器上輸入剛剛自訂好的 domain name (myapp.example.local) ,應該就能正確看到畫面結果,這樣開發時也比較容易 debug 了

p.s:

  1. 完成新增 vhost 設定後,記得要重啟 nginx
  2. 別忘了在本地端的 /etc/hosts 下新增一組 domain name 設定

參考文章:

http://kevin.vanzonneveld.net/techblog/article/ruby_with_nginx_on_ubuntu_lucid/

如何在 Rails 實作 Multitenant Application 架構

最近在專案開發中必須使用到 Multitenant Application 的概念來實作

由於第一次接觸,還是快速筆記免得之後又忘記

什麼是 Multitenant Application

在使用 Multitenant Application 前,還是必須先搞清楚什麼是 Multitenant Application 才能正確理解各項操作的意義
在這邊引用了 阿舍的隨手記記、隨手寫寫… – 什麼是 Multitenant Application (Multitenancy) 這篇文章的解釋

「如果你有一個應用系統(程式)用來服務不同的客戶,而這些客戶用的都是同一台主機,同一個資料庫,同一個應用系統(程式),只有透過程式設計的方式來讓不同的客戶存取不同的資料,那麼就可以說這是一個 Multitenant Application 」

另外這篇 Multi-Tenant Data Architecture 分享 (多租戶的資料設計架構) part1 文章裡有更明確的架構說明

在這次專案裡,只實作最簡單的 “Shared Database, Shared Schemas" 架構,共用一個資料庫,同一個主機

Rails 如何實作

在 Gemfile 載入 multitenant,相關的 github 說明可看這裡

gem 'multitenant'

完成設定後,執行安裝

$ bundle install

安裝成功後,請在 /config/environments/development.rb|production.rb|test.rb 三個環境設定下加入 config.domain = “#{你的網域名稱}",如:

config.domain = 'localhost'

由於需要服務不同的客戶,所以會將每個客戶的帳號做為 subdomain(子網域名稱) 建立對應的 URL 路徑
最後應用程式便可透過 subdomain 取得客戶身份,要做到這件事,便需要從 routing 這邊下手
修改 routes.rb,並使用 :constraints (特殊限定) 設定檢查

定義 constraints 所需要使用的檢查條件類別,建立於 lib 目錄下,命名為 sub_domain.rb

# /lib/sub_domain.rb
class SubDomain
def self.matches?( requrest )
# 取得 environments 環境參數定義的 domain 設定值
# 用來比對目前執行的 host 的 domain 是否為可被允許執行的網站名單
domain = TestProject::Application.config.domain
case requrest.host
# 以下幾個網站都是預設被使用的 domain
when "www.#{domain}", "#{domain}", nil
false
else
true
end
end
end

註: 請確定 /config/application.rb 設定中有自動載入 lib 目錄檔案,設定方式可於 /config/application.rb 加入 config.autoload_paths += %W(#{config.root}/lib)


在 routes.rb 建立限定檢查,檢查目前的 request host 的 subdomain name,是否為不被允許的 subdomain name,這邊定義的是 www 或是 空白,則一律導回至 www 首頁

# routes.rb
# 檢查目前傳入的 request host 是否為不被允許的 subdomain name
# 允許的話則可繼續使用服務,否則統一導向 www 首頁
constraints( SubDomain ) do
root :to => 'index#index'
end
# 導回 www 首頁
root :to => 'index#destroy'

IndexController 請定義 index 和 destroy 二個 action 處理導向狀況

class IndexController < ApplicationController
def index
end

# 負責處理請求網址的客戶不在的情況
# 一律導向至產品網站首頁
#----------------------------------------------------
def destroy
redirect_to "http://www.testproject.com.tw"
end
end

當完成 Multitenant 路徑設定後,接下來是要取得 User 的資料,判斷目前傳入的路徑是否有對應的 User 資料存在

先建立 migration 針對 User model 加入一些必要的欄位用來保存註冊的網域資訊,

$ rails g migration add_domain_fields_to_users.rb

內容如下:

add_column :users, :subdomain_name, :string, :null => false, :default => ""
add_column :users, :cache_domain, :string, :null => false, :default => ""
add_index :users, :subdomain_name, :unique => true

執行資料庫欄位建立

$ rake db:migrate

完成 subdomain 資料欄位新增後,即可將會員註冊的 subdomain 資料保存至資料庫,留待接下來的驗證使用
接著修改 application_controller.rb,加入驗證邏輯,如下:

# application_controller.rb

class ApplicationController < ActionController::Base
protect_from_forgery

before_filter :check_user_domain_exists!

protected
# 透過 subdomain 取得對應的 user 資料
#----------------------------------------------------
def check_user_domain_exists!
# 將目前的 request host 拿來查詢是否有其會員註冊使用,取得該會員
user ||= User.select('id').find_by_cache_domain( request.host )

# 如果會員為空,則一律導向至 www 網站首頁
if( user.blank? )
redirect_to "http://www.testproject.com.tw"
end
end
end

到這為止,基本的 Multitenant 驗證檢查都已經完成,接下來就可以透過 multitenant gem 發功
舉個簡單的例子,一位會員可以擁有很多本書,1對多的關係,所以我們會有 User 和 Book 二個 model,在 db 的結構裡,book table 會記錄 user_id,以前的做法就會是利用 user_id 關聯找出所有的 book 資料,multitenant 也是做類似的事,只是它幫我們包裝的更好,更容易使用

Book Model 結構設定如下:

# models/book.rb
class Book < ActiveRecord::Base
# 設定 Book 和 User 之間的關聯
belongs_to :user
# 設定 multitenant 關聯
belongs_to_multitenant :user
end

之後我們要取得目前使用者擁有的所有書本,使用方式是將目前登入使用者 (current_user) 做為參數傳入 Multitenant.with_tenant 函式中,表示縮限在這個 User 範圍下處理相關操作,包含新增、修改、刪除

Multitenant.with_tenant current_user do
# queries within this block are automatically
# scoped to the current tenant
Book.all

# new objects created within this block are automatically
# assigned to the current tenant
Book.create :name => 'Harry Potter and the Deathly Hallows (Book 7) '
end

後記

以上是本次專案執行的 multitenant 設定流程,因應各種不同的狀況,可能會有不同的處理方式,要留意!!
如果有任何意見指教,也歡迎指正小弟

參考文章:

說明:
http://www.arthurtoday.com/2010/02/multi-tenant-application-multitenancy.html
http://kevingo75.blogspot.com/2011/05/multi-tenant-data-architecture-part1.html

技術:
https://github.com/wireframe/multitenant
http://ihower.tw/rails3/routing.html
http://intridea.com/posts/use-lambdas-for-rails3-route-constraints

解決 simple_form 整合 bootstrap 的 check box 文字無法正確對齊

在專案中使用了整合 bootstrap 樣式的 simple_form ,在處理 devise 會員登入  rember me 的樣式時,發現 label 文字無法正確向左對齊,我希望最後表現出來的結果是像這樣

但實際使用時,在登入頁面執行

<%= f.input :rember_me, :as => :boolean, :label => '記住我' %>

生成的頁面會變成

會發現文字並沒有正確地和 check box 對齊

解決方法:

開啟 config/initializers/simple_form.rb ,加上

config.wrappers :checkbox, :tag => 'div', :class => 'control-group', :error_class => 'error' do |b|

b.use :tag => 'div', :class => 'controls' do |ba|

ba.use :label_input

end

end

最後將登入頁面的 check_box 執行方式改為

<%= f.input, :remember_me, :as => :boolean, :wrapper => :checkbox, :label => '記住我' %>

再重新整理頁面之後,就會看到文字對齊於 check_box

註: 記得修改 initializers 目錄的檔案,要重新啟動 rails server

參考文章:

https://github.com/plataformatec/simple_form/issues/424#issuecomment-4001247

利用 RVM 管理多個不同的 Rails 版本專案

RVM 是什麼:

RVM( Ruby Version Manger),簡單來說就是 Ruby 的版本管理工具,可以讓你同時安裝多個不同版本的Ruby,RVM 裡的每個版本的 Ruby gem 也都可以分開安裝。

RVM 提供了一個 gemset 的功能,可以讓你在同一個 Ruby 環境下,建立二個不同的版本的 Rails 版本,例如想在 Ruby 1.9.3 的環境下建立 Rails 3.1.1 和 Rails 3.2.3 二個不同的 Rails 版本進行開發或測試

該怎麼建立 gemset :

1. 先建立 3.1.1 的 gemset

$ rvm gemset create 3.1.1

成功後會看到類似的訊息 ‘3.1.1’ gemset created (/Users/bjack/.rvm/gems/ruby-1.9.3-p0@3.1.1).

2. 切換至 3.1.1 的 gemset

$ rvm gemset use 3.1.1

成功後顯示 Using ruby-1.9.3-p0 with gemset 3.1.1

3. 安裝 Rails 3.1.1 版本

$ gem install rails -v 3.1.1

同理,安裝 Rails 3.2.3 的方式如下:

$ rvm gemset create 3.2.3
$ rem gemset use 3.2.3
$ gem insall rail -v 3.2.3

清除/刪除建立的 gemset :

清空建立的 gemset,但這個指令只會清空 gemset 資料,gemset 名字還會存在

$ rvm gemset empty 3.1.1

真正刪除 gemset 資料,資料被清空,gemset 名字被移除

$ rvm gemset delete 3.1.1

補充:

查詢目前已經建立的 gemset :

$ rvm gemset list

同時建立多個 gemset :

$ rvm gemset create 3.1.1 3.2.3

查詢已經安裝的 gem :

$ gem list

查詢目前安裝的 rails 版號 :

$ rails --version

參考文章:

RVM – Named Gem Sets :

http://beginrescueend.com/gemsets/basics/

高見龍 – RVM and Gemsets :

http://blog.eddie.com.tw/2011/04/08/rvm-and-gemsets/