2017/11/29
Ruby - 使用多態關聯
markdown
## 使用 [多態關聯 Polymorphic Associations](http://guides.rubyonrails.org/association_basics.html#polymorphic-associations)
目的:在一個表格可能被多個表格參考時,不使用多個 references 去儲存參考,而是使用一個 reference + 一個 type 欄位去儲存。
建立:
```
class CreatePictures < ActiveRecord::Migration[5.0]
def change
create_table :pictures do |t|
t.string :name
t.references :imageable, polymorphic: true, index: true
t.timestamps
end
end
end
```
```
class Picture < ApplicationRecord
belongs_to :imageable, polymorphic: true
end
class Employee < ApplicationRecord
has_many :pictures, as: :imageable
end
class Product < ApplicationRecord
has_many :pictures, as: :imageable
end
```
使用:
```
Product.last.pictures
Employee.last.pictures
```
Ruby - 使用 Devise confirmable
markdown
## Devise confirmable
當你想要認證註冊者的信箱時可以使用 confirmable
安裝方式請參考:[https://github.com/plataformatec/devise/wiki/How-To:-Add-:confirmable-to-Users](https://github.com/plataformatec/devise/wiki/How-To:-Add-:confirmable-to-Users)
以下說明一些實務上的可能會遇到的細節調整方式。
## 什麼時候會寄出信?
### 建立 user 時
在建立 user 時,會在呼叫 user.save 後寄信給 user。
```
user = User.create
user.save
```
### user email 更新時
在編輯 user 時,若 email 有修改,會在呼叫 user.save 後寄信給 user。
```
user = User.find(params[:id])
user.email = 'QQ@QQ'
user.save
```
此時寫入的 email 會被保存到 unconfirmed_email,而原先的 email 欄位在 user 完成認證之前不會改變。
## 開發時測試寄信的方法
在開發時可能會希望不要真的寄出信件,此時可以使用 letter_opener,他會在需要寄信時,只將信件內容印在 console log 上。
設定方法是在 `config/environments/development.rb` 加入以下程式碼:
```
config.action_mailer.delivery_method = :letter_opener
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
```
## confirmable 自動測試
加入了 confirmable 之後可能會導致 test fail,因為 devise 嘗試 send mail 但是 test 環境下可能無法正確寄信。
#### 避免寄出信件的方法
如果你希望在測試時不要寄信,如果你使用 FactoryBot 這個套件來生成 user,以下是跳過 email 驗證的方法:
```
FactoryBot.define do
factory :user do
after(:build) { |u| u.skip_confirmation! }
end
end
```
在不使用 FactoryBot 的情況下,跳過 email 驗證的方法:
```
user = User.new
user.skip_confirmation!
user.save
```
#### 成功寄出信件的方法
如果你希望寄信,但是你沒有設定 host 值,那麼你會看見這個:
```
ActionView::Template::Error: Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true
```
此時你需要加入以下內容至 `config/environments/test.rb`:
```
config.action_mailer.delivery_method = :test
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
```
### 測試寄件內容
使用 ActionMailer::Base.deliveries.last 可以取得最後一次寄出的信件內容:
```
user = User.new
user.save
mail = ActionMailer::Base.deliveries.last
mail.from
mail.to
mail.subject
mail.body.to_s
```
因此可以對信件內容做測試。
### 模擬使用者完成認證
用 code 完成認證的方法
```
user = User.find(params[:id])
user.confirm
```
## 同一個認證連結被點擊第二次會發生什麼事?
當 user 點擊第二次認證信連結時,預設是會顯示「 Email was already confirmed, please try signing in. 」字樣。可以透過自訂 controller 去修改預設行為
## 怎麼修改認證信內容?
改 template
改 template 路徑
## 自定義寄信路徑
```
class DeviseMailer < Devise::Mailer
helper :application # gives access to all helpers defined within `application_helper`.
include Devise::Controllers::UrlHelpers # Optional. eg. `confirmation_url`
def headers_for(action, opts)
super.merge!({template_path: '/users/mailer'}) # this moves the Devise template path from /views/devise/mailer to /views/users/mailer
end
# def confirmation_instructions(record, token, opts={})
# headers["Custom-header"] = "Bar"
# opts[:from] = 'my_custom_from@domain.com'
# opts[:reply_to] = 'my_custom_from@domain.com'
# super
# end
end
```
## 參考資料:
測試寄信的方法
[http://guides.rubyonrails.org/testing.html#testing-your-mailers](http://guides.rubyonrails.org/testing.html#testing-your-mailers)
阻止修改mail時的認證:[https://coderwall.com/p/7_yh8q/skip-devise-email-confirmation-on-update](https://coderwall.com/p/7_yh8q/skip-devise-email-confirmation-on-update)
2017/11/28
Ruby - 使用 Pundit
markdown
Pundit 是一個用來做身分驗證的工具。對於每一個 Model 來說,目前登入者能不能對這個 Model 進行某種操作,可以被定義在 Policy 上。
Pundit 並不是一個複雜的 gem,但仍然很多人使用,我認為他存在的價值跟 rails 一樣,都是在提出一個收納的概念,教你如何存放你的 code 到正確的位置。
Pundit:我認為所有跟登入者權限相關的東西都應該被儲存在同一個 class(Policy),同一個資料夾下(Policy),我創造了一個架構,使得所有按照我架構的寫法的 code 可以少寫一些字,並且讓專案的 code 看起來更乾淨。
如果你想要檢查登入者(user) 有沒有辦法對資料(record) 進行某種操作 (update?),可以這樣寫:
```
class PostPolicy < ApplicationPolicy
def update?
user.admin? or not record.published?
end
end
```
下面的 code 跟 上面的 code 是等價的
```
class PostPolicy
attr_reader :user, :post
def initialize(user, post)
@user = user
@post = post
end
def update?
user.admin? or not post.published?
end
end
```
當你在 Controller 的 action 裡面寫到 authorize model 時,Pundit 會幫你看有沒有跟 action 同名的 Policy。透過 current_user 跟 model 這兩個值去做檢查。
注意:Devise 剛好會生成一個方法叫做是 current_user。
舉例來說,這個 action:
```
def update
@post = Post.find(params[:id])
authorize @post
if @post.update(post_params)
redirect_to @post
else
render :edit
end
end
```
執行 authorize @post 時,等於去執行以下程式:
```
unless PostPolicy.new(current_user, @post).update?
raise Pundit::NotAuthorizedError, "not allowed to update? this #{@post.inspect}"
end
```
你也可以自己指定要執行的 policy 方法名稱
```
authorize @post :update?
```
如果 policy 不需要 record 變數就可以做,則 record 可以不傳,改傳 record 的 Class
```
# in controller
def admin_list
authorize Post # we don't have a particular post to authorize
# Rest of controller action
end
# in policy
class PostPolicy < ApplicationPolicy
def admin_list?
user.admin?
end
end
```
authorize 會回傳傳入的參數,所以可以在 authorize 的時候一邊指定要儲存到哪個變數。
```
# in controller
def show
@user = authorize User.find(params[:id])
end
```
policy method 可以讓你取得 policy 物件
```
policy(@post)
```
等於
```
PostPolicy.new(current_user, @post)
```
Pundit 不只對 model 可以加 policy,也可以對 symbol 加 policy。
```
# in policy
class DashboardPolicy < Struct.new(:user, :dashboard)
# ...
end
#in controller
authorize :dashboard, :show?
# In views
<% if policy(:dashboard).show? %>
```
Pundit 對於 Scope 也有處理:
```
class PostPolicy < ApplicationPolicy
class Scope
attr_reader :user, :scope
def initialize(user, scope)
@user = user
@scope = scope
end
def resolve
if user.admin?
scope.all
else
scope.where(published: true)
end
end
end
def update?
user.admin? or not post.published?
end
end
```
可以簡寫為
```
class PostPolicy < ApplicationPolicy
class Scope < Scope
def resolve
if user.admin?
scope.all
else
scope.where(published: true)
end
end
end
def update?
user.admin? or not post.published?
end
end
```
當你定義好 scope 之後,可以這樣去使用它:
```
def index
@posts = policy_scope(Post)
end
```
policy_scope 傳入的參數會是一個可以下 query 的物件,然後會執行相當於以下程式:
```
def index
@posts = PostPolicy::Scope.new(current_user, Post).resolve
end
```
如果你不想忘記寫 authorize,那你可以在 ApplicationController 加入下面的程式:
```
after_action :verify_authorized, except: :index
after_action :verify_policy_scoped, only: :index
```
這樣的話就會在 controller action 中沒有呼叫 authorize 或 policy_scope 的時候跳出錯誤。
如果真的不需要驗證,那你可以在 action 中寫入 skip_authorization。
```
def show
record = Record.find_by(attribute: "value")
if record.present?
authorize record
else
skip_authorization
end
end
```
你可以指定 Model 對應的 Policy 名稱
```
class Post
def self.policy_class
PostablePolicy
end
end
```
pundit 也有提供 generator
```
# in bash:
rails g pundit:policy post
```
你可以做一個 ApplicationPolicy ,讓他被繼承到每一個 Policy, 使得所有的 Policy 都會去檢查是否使用者有登入
```
class ApplicationPolicy
def initialize(user, record)
raise Pundit::NotAuthorizedError, "must be logged in" unless user
@user = user
@record = record
end
end
```
然後你就可以在 ApplicationController 用 rescue_from 去接他。
```
class ApplicationController < ActionController::Base
protect_from_forgery
include Pundit
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
private
def user_not_authorized
flash[:alert] = "You are not authorized to perform this action."
redirect_to(request.referrer || root_path)
end
end
```
或者在你的 config/application.rb 去接他
config.action_dispatch.rescue_responses["Pundit::NotAuthorizedError"] = :forbidden
pundit 也可以用來處理 params
```
# in policy
class PostPolicy < ApplicationPolicy
def permitted_attributes
if user.admin? || user.owner_of?(post)
[:title, :body, :tag_list]
else
[:tag_list]
end
end
end
# in controller
def update
@post = Post.find(params[:id])
if @post.update_attributes(permitted_attributes(@post))
redirect_to @post
else
render :edit
end
end
```
你可以針對每個不同的 action 提供不同的 permitted_attributes
```
class PostPolicy < ApplicationPolicy
def permitted_attributes_for_create
[:title, :body]
end
def permitted_attributes_for_edit
[:body]
end
end
```
2017/11/6
Ruby - warning: toplevel constant B referenced by A::B
markdown
##問題
warning: toplevel constant B referenced by A::B
##成因
當 A 是一個 class 且 A::B 還沒有被定義時,ruby 找不到 A::B 時,若 B 有定義,就先使用 B, 而不是拋出 Module#const_missing。
```
class A
end
A::String
#warning: toplevel constant String referenced by A::String
```
但 rails 的 autoload 是透過修改 Module#const_missing 而完成的。也就是說,rails 還來不及 autoload 就已經被 toplevel constant 攔截了。
##解法一
在使用到 A::B 的檔案前面都加 require_dependency 'a/b'
##解法二
在使用到 B 的檔案後面加 require_dependency 'a/b' 確保 rails 不可能會有知道 B 的存在但不知道 A::B 的存在的可能發生。
參考文件
[http://stem.ps/rails/2015/01/25/ruby-gotcha-toplevel-constant-referenced-by.html](http://stem.ps/rails/2015/01/25/ruby-gotcha-toplevel-constant-referenced-by.html)
[https://stackoverflow.com/questions/18515100/warning-toplevel-constant-referenced](https://stackoverflow.com/questions/18515100/warning-toplevel-constant-referenced)
2017/10/19
Ruby - RSpec 的使用方法
markdown
## RSpec 簡介
RSpec 在執行的時候不保證執行順序,每個測試產生的資料不會自動被清除。
新增測試的generator語法:
```
rails generate rspec:model user
```
這樣寫會建立一個檔案在 spec/models/user_spec.rb
執行測試的指令是在 rails 專案目錄下輸入以下指令:
```
# 這裡是 bash
# 執行所有測試
rspec
# 執行某個資料夾下的所有測試
rspec ./spec/models
# 執行某個檔案裡的所有測試
rspec ./spec/models/user_spec.rb
# 執行某個檔案裡的某個測試
rspec ./spec/models/user_spec.rb:8
```
簡單的範例
```
require 'rails_helper'
RSpec.describe "規格說明" do
describe "處於某個狀態下" do
# 設定狀態變數
let(:a) { 1 }
it "should be 1" do
puts "should be 1"
expect(a).to eq(1)
end
end
end
```
let 在每次測試裡,第一次存取變數時就會執行對應的程式
```
require 'rails_helper'
RSpec.describe "規格說明" do
describe "處於某個狀態下" do
let(:a) { puts "let a"; 1 }
it "1" do
puts "1"
puts "a=#{a}"
end
it "2" do
puts "2"
puts "a=#{a}"
puts "a=#{a}"
puts "a=#{a}"
end
end
end
```
輸出
```
1
let a
a=1
.2
let a
a=1
a=1
a=1
.
```
before 和 after 會在所有測試的執行前後做事
```
RSpec.describe "規格說明" do
describe "處於某個狀態下" do
before { puts "before" }
after { puts "after" }
it "1" do
puts "1"
end
it "2" do
puts "2"
end
end
end
```
執行結果
```
before
1
after
.before
2
after
.
```
因為測試對資料庫的操作會互相影響,如果想要確保每個測試都是在資料庫乾淨的狀態下,可以使用 [database_cleaner](https://github.com/DatabaseCleaner/database_cleaner)。
```
require 'database_cleaner'
...
RSpec.configure do |config|
...
config.before(:suite) do
DatabaseCleaner.strategy = :transaction
DatabaseCleaner.clean_with(:truncation)
end
config.around(:each) do |example|
DatabaseCleaner.cleaning do
example.run
end
end
...
end
```
其中的 before、around 可以參考官網說明文件:[https://relishapp.com/rspec/rspec-core/v/2-13/docs/hooks/before-and-after-hooks#before/after-blocks-defined-in-config-are-run-in-order](https://relishapp.com/rspec/rspec-core/v/2-13/docs/hooks/before-and-after-hooks#before/after-blocks-defined-in-config-are-run-in-order)
如果想要控制 rspec 執行每一個 test 的順序,可以這樣寫:
```
# 這裡是 bash
rspec --order defined
rspec --seed 1
```
詳細的介紹可以參考 [https://relishapp.com/rspec/rspec-core/docs/command-line/order](https://relishapp.com/rspec/rspec-core/docs/command-line/order)
如果想要把結果輸出到檔案,可以這樣寫:
```
# 這裡是 bash
rspec --out result.txt
rspec --format documentation --out result.txt
```
上面兩行會使檔案內容不同,詳細的介紹可以參考 [https://relishapp.com/rspec/rspec-core/v/2-4/docs/command-line/format-option](https://relishapp.com/rspec/rspec-core/v/2-4/docs/command-line/format-option)
Ruby - debug 方法
markdown
## 查詢繼承關係
```
File.ancestors
# [File, IO, File::Constants, Enumerable, Object, Kernel, BasicObject]
```
## 從物件找方法
```
# 查 File 的類別方法
File.methods
# 查 File 的實體方法
File.instance_methods
# 查 File 的實體方法
File.new('/').methods
# 取得繼承樹上所有的方法
File.methods(true)
# 只取得屬於 File 的方法
File.methods(false)
```
## 從方法找定義
```
File.method(:read)
# #Method: File(IO).read
IO.method(:read)
# #Method: IO.read
IO.method(:read).source_location
# nil
```
因為 IO.read 的定義是寫在 c 語言,所以就不顯示了。
## 參考文件
為什麼File.method(:read)是nil:[https://ja.stackoverflow.com/questions/5755/ruby-file-read-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%AE%E8%AA%AC%E6%98%8E%E3%82%92api%E3%83%89%E3%82%AD%E3%83%A5%E3%83%A1%E3%83%B3%E3%83%88%E3%81%A7%E8%AA%BF%E3%81%B9%E3%81%9F%E3%81%84](https://ja.stackoverflow.com/questions/5755/ruby-file-read-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%AE%E8%AA%AC%E6%98%8E%E3%82%92api%E3%83%89%E3%82%AD%E3%83%A5%E3%83%A1%E3%83%B3%E3%83%88%E3%81%A7%E8%AA%BF%E3%81%B9%E3%81%9F%E3%81%84)
2017/10/11
安裝 nokogiri 失敗時的解決方法
(參考此篇文章)[https://github.com/sparklemotion/nokogiri/issues/1483]
在 bash 輸入 xcode-select --install 可解決問題。
在我的 macbook 上的錯誤訊息如下:
```
Gem::Ext::BuildError: ERROR: Failed to build gem native extension.
current directory: /Users/etrex/.rvm/gems/ruby-2.4.1@-global/gems/nokogiri-1.8.0/ext/nokogiri
/Users/etrex/.rvm/rubies/ruby-2.4.1/bin/ruby -r ./siteconf20171011-80748-pgpdll.rb extconf.rb
checking if the C compiler accepts ... yes
checking if the C compiler accepts -Wno-error=unused-command-line-argument-hard-error-in-future... no
Building nokogiri using packaged libraries.
Using mini_portile version 2.2.0
checking for iconv.h... yes
checking for gzdopen() in -lz... yes
checking for iconv using --with-opt-* flags... yes
************************************************************************
IMPORTANT NOTICE:
Building Nokogiri with a packaged version of libxml2-2.9.4
with the following patches applied:
- 0001-Fix-comparison-with-root-node-in-xmlXPathCmpNodes.patch
- 0002-Fix-XPointer-paths-beginning-with-range-to.patch
- 0003-Disallow-namespace-nodes-in-XPointer-ranges.patch
Team Nokogiri will keep on doing their best to provide security
updates in a timely manner, but if this is a concern for you and want
to use the system library instead; abort this installation process and
reinstall nokogiri as follows:
gem install nokogiri -- --use-system-libraries
[--with-xml2-config=/path/to/xml2-config]
[--with-xslt-config=/path/to/xslt-config]
If you are using Bundler, tell it to use the option:
bundle config build.nokogiri --use-system-libraries
bundle install
Note, however, that nokogiri is not fully compatible with arbitrary
versions of libxml2 provided by OS/package vendors.
************************************************************************
Extracting libxml2-2.9.4.tar.gz into tmp/x86_64-apple-darwin15.4.0/ports/libxml2/2.9.4... OK
Running git apply with /Users/etrex/.rvm/gems/ruby-2.4.1@-global/gems/nokogiri-1.8.0/patches/libxml2/0001-Fix-comparison-with-root-node-in-xmlXPathCmpNodes.patch... OK
Running git apply with /Users/etrex/.rvm/gems/ruby-2.4.1@-global/gems/nokogiri-1.8.0/patches/libxml2/0002-Fix-XPointer-paths-beginning-with-range-to.patch... OK
Running git apply with /Users/etrex/.rvm/gems/ruby-2.4.1@-global/gems/nokogiri-1.8.0/patches/libxml2/0003-Disallow-namespace-nodes-in-XPointer-ranges.patch... OK
Running 'configure' for libxml2 2.9.4... OK
Running 'compile' for libxml2 2.9.4... ERROR, review
'/Users/etrex/.rvm/gems/ruby-2.4.1@-global/gems/nokogiri-1.8.0/ext/nokogiri/tmp/x86_64-apple-darwin15.4.0/ports/libxml2/2.9.4/compile.log' to see what happened. Last lines are:
========================================================================
unsigned short* in = (unsigned short*) inb;
^~~~~~~~~~~~~~~~~~~~~
encoding.c:815:27: warning: cast from 'unsigned char *' to 'unsigned short *' increases required alignment from 1 to 2 [-Wcast-align]
unsigned short* out = (unsigned short*) outb;
^~~~~~~~~~~~~~~~~~~~~~
4 warnings generated.
CC error.lo
CC parserInternals.lo
CC parser.lo
CC tree.lo
CC hash.lo
CC list.lo
CC xmlIO.lo
xmlIO.c:1450:52: error: use of undeclared identifier 'LZMA_OK'
ret = (__libxml2_xzclose((xzFile) context) == LZMA_OK ) ? 0 : -1;
^
1 error generated.
make[2]: *** [xmlIO.lo] Error 1
make[1]: *** [all-recursive] Error 1
make: *** [all] Error 2
========================================================================
*** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of necessary
libraries and/or headers. Check the mkmf.log file for more details. You may
need configuration options.
```
2017/9/13
在 ruby 可執行 js 的爬蟲
markdown
[watir](https://github.com/watir/watir)
是一個自動測試工具,我打算拿來做爬蟲,因為我的爬蟲需要執行頁面 js 獲得動態 dom 結果,而 watir 剛好可以做到這件事情。
watir 需要搭配瀏覽器使用,我先試著使用 phantomjs 這個瀏覽器。
phantomjs 是一款無頭瀏覽器 [Headless_browser](https://en.wikipedia.org/wiki/Headless_browser),
因為他不需要真正顯示畫面,所以他的效能比較好。
[怎麼在 heroku 上跑 watir (phantomjs)](https://github.com/edelpero/watir-examples/blob/master/watir_on_heroku.md)
但是因為 chrome 也做了無頭版的瀏覽器,所以 phantomjs 的作者後來就不更新了。
[Getting Started with Headless Chrome](https://developers.google.com/web/updates/2017/04/headless-chrome)
[Headless Capybara Feature Specs with Chrome](https://robots.thoughtbot.com/headless-feature-specs-with-chrome)
[資料來源1](https://ruby-china.org/topics/31784)
2017/9/12
2017/3/2
ruby 全形轉半形(含英文數字符號)
markdown
寫法:
```
def to_half(str)
full = " !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
half = " !\"\#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
str.tr(full, half)
end
```
測試:
```
puts to_half("!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~")
```
訂閱:
文章 (Atom)