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 同一個認證連結被點擊第二次會發生什麼事? 當 user 點擊第二次認證信連結時,預設是會顯示「 Email was already confirmed, please try signing in. 」字樣。可以透過自訂 controller 去修改預設行為 怎麼修改認證信內容? 改 template 改 template 路徑 什麼時候會寄出信? 在建立 user 時,會在呼叫 user.save 後寄信給 user。 ``` user = User.create() user.save ``` 在編輯 user 時,若 email 有修改,會在呼叫 user.save 後寄信給 user。 ``` user = User.find(params[:id]) user.email = 'QQ@QQ' user.save ``` 此時寫入的 email 會被保存到 unconfirmed_email,而原先的 email 欄位在 user 完成認證之前不會改變。 如何測試?加入了 confirmable 之後可能會導致 test fail ,因為 devise 嘗試 send mail 但是 test 環境下沒有設定 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/development.rb ``` config.action_mailer.delivery_method = :letter_opener config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } ``` 在config/environments/test.rb ``` config.action_mailer.delivery_method = :test config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } ``` 在 rspec ``` user = User.create() user.save mail = ActionMailer::Base.deliveries.last mail.from mail.to mail.subject mail.body.to_s ``` 略過 confirm 的方法 ``` user = User.create() user.skip_confirmation! user.save ``` 用 code 完成認證的方法 ``` user = User.find(params[:id]) user.confirm ``` ## 自定義寄信路徑 ``` 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/21

mac - 關閉 chrome 的兩指滑動換頁功能

markdown 在 mac 上的 chrome 有一個功能是使用兩指向右滑動可以回到上一頁,可是幹,我只是想看左邊的東西欸,所以我找到怎麼把這個功能關閉的方法。 在 bash 下輸入以下指令: ``` defaults write com.google.Chrome AppleEnableMouseSwipeNavigateWithScrolls -bool false defaults write com.google.Chrome AppleEnableSwipeNavigateWithScrolls -bool false ``` 然後重開 chrome

2017/11/8

rails - encode & decode

markdown HTML encode ``` require 'cgi' CGI.escapeHTML('<') # "&lt;" ``` HTML decode ``` require 'cgi' CGI.unescapeHTML('&lt;') # "<" ```