Jex’s Note

Rails Seeds 建立測試資料

介紹

Fabrication

一個 model 對應一個 fabrication

首先先建立模型

Fabricator(:person) do
  age { rand(20..45) }
end

一定要用 { } 包起來,因為有遇過一些奇怪的 bug, 而 seed 不成功

Insert 資料

Fabricate(:person)
Fabricate(:person, email: 'xxx@ff.com')     # 指定欄位

Faker 使用上非常簡單, 直接看 Example

建立 4 個 users, 並且每名 user 指定一個 role, 隨機 user 建立共 50 篇文章, 隨機 user 對隨機挑的 50 篇文章回覆共 200 筆留言

user = User.create({email: "user@gmail.com", password: "00000000", confirmed_at: Time.now})

Fabricator(:post) do
  title { Faker::Lorem.sentence(3, true, 4) }
  content { Faker::Lorem.paragraph(4, true, 7) }
end

50.times { Fabricate(:post, user_id: user.id) }

Fabricator 的 Table name 要用單數

Seed file with paperclip

Fabricator(:product) do
  photo { File.new("app/assets/images/logo.png") }

  或傳入一個 url
  photo { open("http://lorempixel.com/300/300/") }
end

建議可以區分 Seed 的環境, 避免 Seed 到 Production

case Rails.env
when "development"
   ...
when "production"
   ...
end

Locale

Faker 在不同機器可能會讀取到不同的 locale, 例如我在開發機是 en 的, 但到 production 卻變成 zh-TW, 而 Faker::Internet.free_email 卻變得不 work 了

但可以在 config 裡設定他預設的 locale

config/environments/development.rb :

Faker::Config.locale = 'en'

Error

ActiveRecord::RecordNotSaved: You cannot call create unless the parent is saved

有可能會發生在當一筆資料沒有建立成功(可能是 validation 沒有通過),造成資料沒有建立,但又直接建立關聯資料造成的錯誤:

c = Category.create(...)
c.products.create(...)

建議加上 ! 讓錯誤當下直接擲出 Exception 中斷程序

c = Category.create!(...)
c.products.create!(...)

Rails 測試

介紹

  • Rspec-rails, 是一套 Rails 的 BDD Testing framework, 非常強大
  • Factory Girl, 產生測試資料
  • Capybara, 用來搭配 Rspec 的測試工具, 可以模擬使用者操作瀏覽器
  • (??) watchr (存檔馬上測) / rcov(測試涵蓋度) / shoulda (matcher)

Gemfile

group :development, :test do
  gem "rspec"           # 可以不需要,如果要用原來的 rspec 才需引入
  gem 'rspec-rails'     # rspec 與 rails 整合的套件
  gem "factory_girl_rails"
end

安裝 rspec : rails generate rspec:install

Rspec

關於 BDD

BDD 是基於 TDD 發展出來的,不同 TDD 的地方在於 BDD 寫出系統行為的規格,好處是可以盡量避免細節的遺漏、更容易理解及維護。

觀念

  • 台灣公司普遍不重視測試,甚至大多數人沒寫過,網站也活得好好的,但不代表可以不寫
  • 測試是一種非常划算的投資,先花時間,但後面會省更多時間
  • 最容易出錯或重要的部分優先

指令

  • 執行所有測試 : rspec
  • 只跑 controllers 的測試 : rspec spec/controllers

.rspec 指令參數

當安裝完 rspec 時, `.rspec` 參數檔會自動被建立在網站根目錄
  • --color : 在執行測試時的訊息會有顏色方便閱讀
  • --require spec_helper : 載入 spec/spec_helper.rb 這個檔案
  • --warning : 為了避免干擾,移除它可以避免 rspec 與其他 gem 不相容的警告 warning: loading in progress, circular require considered harmful
  • -fs : 輸出 specdoc 文件
  • -fh : 輸出 html 文件

目錄結構

  • spec
    • rails_helper.rb (自動建立的設定檔)
    • spec_helper.rb (自動建立的設定檔)
    • models (手動建立的,用來測試 models)
    • views (手動建立的,用來測試 views)
    • controllers (手動建立的,用來測試 controllers)
      • posts_spec.rb (手動建立的,用來測試 Post controller)

語法

測試檔大致上的架構會是這樣

require 'rails_helper'              # 一定要先加上,用來讀取 rails rspec 的設定檔

# 用 describe 來包 it
describe '描述大方向要做的事情' do
  it '要做的事情的細項' do
    # rspec 的語法
  end
end

describe (= RSpec.describe) / it

describe '描述大方向要做的事情' do
describe PostController, type: :controller do

# describe 也可以 包 describe
describe 'posts/edit.html.erb' do
  # 一個 it 最好只有一種測試目的
  it "#index"               # 用 # 代表 instance method
  it ".index"               # 用 . 代表 class method
  it "render partial"       # 當錯誤發生時,會顯示 posts/edit.html.erb render partial ,所以 describe / it 命名好可以很快知道哪裡沒通過測試

  # 也可以再包一個 describe
  describe ' ... '
end

before / after

before(:all) : runs the block one time before all of the examples are run; sets the instance variables one time before all of the "it" blocks are run.
before(:each) : runs the block one time before each of your specs in the file; resets the instance variables in the before block every time an "it" block is run.

expect / to

expect(1+1).to eq(2)                                                        # 相等
expect{ post :create, post: @post_params }.to change{Post.all.size}.by(1)   # Model 新增成功 ; 判斷 Update 是否成功就直接用 eq 比值
expect{ post :destroy, id: 1 }.to change{Post.all.count}.by(-1)             # Model 刪除成功
expect(columns).not_to include('nickname')                                  # hash 是否有 'nickname'
expect(response).not_to have_http_status(302)                               # http = 200
expect(response).to render_template(:new)                                   # render 的 view
expect(response).to redirect_to(posts_path)                                 # redirect 的頁面
expect(Post.create!).to eq(Post.last)                                       # 驗證是否成功新增一筆資料
expect { ... }.to rqise_error(NotPaidError)                                 # 例外

# should 是舊語法,盡量改用 expect
tweet.status.length.should be <= 140

mock 模仿真的物件

stub 回傳設定好的值

# 用意是讓 Post 這個 model 執行 save 時都一律回傳 false, 以便測試到失敗的例子
allow_any_instance_of(Post).to receive(:save).and_return(:false)

@user = stub("user")
@user.stub(:name).and_return("Apple")                       # @user.name = "Apple"
@client = stub("client")
@client.stub_chain(:foo, :bar, :baz).and_return("blah")     # @client.foo.bar.baz = "blah"
User.stub(:find).and_return("Apple")
receive(:ping).once
receive(:ping).twice
receive(:ping).exactly(3).times
receive(:ping).at_most(3).times
receive(:ping).at_least(3).times
receive(:transfer).and_raise(TransferError)

get / post

get :index
get :edit, id: @post[:id]                           # 傳入 get 參數
post :create, post: @post_params                    # 傳入 post 的參數
post :update, post: @post_params, id: @post[:id]    # 傳入 post 及 get 的參數

pending

describe Post do
  it "has a name"     #沒有do / end
  xit "has a name"
  pending "add some examples to (or delete) #{__FILE__}"
end

Matcher

be_treu / be_afalse / be_nil / be_empty / be_blank / be_admin (target.admin?)
be_a_kind_of(Array) = be_an_instance_of(Array)
have_key(:foo) / include(4) / have(3).items (target.items.length = 3)

實際撰寫的邏輯

controller

# 一般情況 : action 是否正常
it "#index" do
    get :index
    expect(response).to have_http_status(200)
    expect(response).to render_template(:index)
end

# 將需要成功/失敗的先用 stub 處理
it "create / update 失敗" do
    先用 stub 讓 .save 失敗再做其他判斷
end

view

# 一般情況的 render
it "can render" do
    @post = Post.create(:title => "Big Title", :content => "content")
    @posts = Array.new(2, @post)
    render
    expect(rendered).to include("Big Title")
end

# render partial
it "render partial" do
    @post = Post.create(:title => "Big Title", :content => "content")
    render
    expect(response).to render_template(partial: "_form")
end

# 忽略 view helper
it "renders content when @post has content" do
    allow(view).to receive(:render_content).and_return("Stub Content")          # view 有個 render_content 的 helper, 讓它強制回傳 "Stub Content"
    render
    expect(rendered).to include("Stub Content")
end

model

# 一般情況 create
it "is accessable" do
    post = Post.create!(title: "title")
    expect(post).to eq(Post.last)
end

# validate 不通過
it "validates title" do
    expect(Post.new).not_to be_valid
end

# 關聯 many / belongs_to  # (有 gem 可以直接做這件事)
it "has_many comments" do
    post = Post.create(:title => "title", :content => "content")
    comment = Comment.create(:content => "content", :post_id => post.id)
    expect(post.comments).to include(comment)
end

routing

# 一般 route
it "#index" do
    expect(get: '/posts/1').to route_to(
        controller: 'posts',
        action: 'show',
        id: 1
    )
    可以簡化為 :
    expect(get: '/posts').to route_to("posts#index")
end

整合測試 (MVC一連串的行為測試,不再單獨測功能)

先建立 spec/requests/posts_spec.rb

# 實際模擬
describe "posts", type: :request do
    before(:all) do
        @post = Post.create(title: 'post from request spec')
    end

    it "#index" do
        get "/posts"
        expect(response).to have_http_status(200)
        expect(response).to render_template(:index)
        expect(response.body).to include('post from request spec')
    end

    it "#create" do
        params = {title: 'create title', content: 'create content'}
        post "/posts", post: params
        expect(response).to have_http_status(302)       # 會轉址
        expect(Post.last.title).to eq(params[:title])
    end
end

Factory Girl

介紹

在開始或測試一定會需要測試資料, 例如模擬真實上線時頁面的樣子,

或需要搭配 Rspec 測試 models 的 validation,

需要大量又隨機的資料自己手動新增也太辛苦, 可以藉由 Factory Girl 幫你產生

Getting started 未完成, 請勿參考…努力中:(

Factory Girl 建議是一個 model 對應一個 Factory Girl 檔案, 例如 models/user.rb 對應到 sepc/factiries/user.rb

spec/factories/user.rb

FactoryGirl.define do
  factory :user do
    name "foo"
    email "bar@foo.com"
    password "12341234"
    birthday "1982-03-30"
  end
end

未完成, 請勿參考…努力中:(

db/seeds.rb

require 'factory_girl_rails'

puts 'Creating Roles.'
%w(guest user admin super-admin super-duper-admin).each do |role|
  Role.find_or_create_by(name: role)
end

spec/factories/roles.rb:

FactoryGirl.define do
  factory :role do
    name { "role_#{rand(9999)}" }

    factory :guest_role, parent: :role do
      name 'guest'
    end

    factory :user_role, parent: :role do
      name 'user'
    end

    factory :admin_role, parent: :role do
      name 'admin'
    end
  end
end

spec/factories/users.rb:

FactoryGirl.define do
  factory :user do
    email 'user@example.com'

    factory :admin_user, parent: :user do
      email 'admin@example.com'
      after(:create) {|user| user.add_role(:admin)}
    end
  end
end

ref :

Rake

介紹

Rake 是像 C 語言的 Make 工具, 但是用 Ruby 寫的, 主要是用來執行預先寫好的腳本語言

依不同環境跑 tasks

lib/tasks/tasks.rake :

namespace :tasks do
  namespace :create do
    desc "Create all survey templates"
    task :all => [:task1, :task2]

    desc "desc1"
    task :task1 => :environment do
        Rails.logger.info(1)
    end

    desc "desc2"
    task :task2 => :environment do

    end
  end
end

lib/tasks/dev.rake :

namespace :dev do
  desc "Rebuild system"
  task :build => ["tmp:clear", "log:clear", "db:drop", "db:create", "db:migrate"]
  task :rebuild => [ "dev:build", "db:seed" ]
end
  • rake dev:build : 重建一個乾淨的環境
  • rake dev:rebuild : 跑完 dev:build 再跑 db:seed

ref: 參考這裡

Rails 小觀念

nil vs empty vs balnk

首先 blank 是 rails 才有的, ruby 本身是沒有的

nil 可以用在任何物件上, 即使物件為 nil, 當 Object 為 nil 的話為 true

empty 可以用在 strings, arrays and hashes, True 的話有以下三種情況

  • String length == 0
  • Array length == 0
  • Hash length == 0

如果在某個為 nil 的物件上問 .empty? 會擲出 NoMethodError

.blank? 可以解決這個問題, 它不會引發 NoMethodError, 用法跟 empty 幾乎一樣

nil.blank? = true       # empty? 會引發錯誤
false.blank? = true     # empty? 會引發錯誤
[].blank? = true
[].empty? = true
{}.blank? = true
{}.empty? = true
"".blank? = true
"".empty? = true
5.blank? = false       # empty? 會引發錯誤
0.blank? = false       # empty? 會引發錯誤

有一個 space 的情況

" ".blank? = true
" ".empty? = false

Array 是空的情況

[ nil, '' ].blank? == false
[ nil, '' ].all? &:blank? == true

Rails - 其他套件

Gem commands

  • gem -v : RubyGems 版本
  • gem update --system : 升級 RubyGems
  • gem install gem_name : 安裝某個套件, 加上 --no-ri --no-rdoc 不產生文件
  • gem list : 列出安裝的套件
  • gem update gem_name : 更新最新版本
  • gem update : 更新所有 Gem
  • gem install -v x.x.x gemname : 安裝特定版本
  • gem uninstall gem_name : 反安裝

Gemfile 版本符號代表意義

gem 'rails', '4.2.6'
  • 不指定 : 使用最新版
  • 4.2.6 : 明確指定使用 4.2.6
  • ~> 1 : 1.0 ~ 1.9 (小於 2.0)
  • ~> 2.1 : 2.1 ~ 2.9 (小於 3.0)
  • ~> 2.0.3 : 2.0.3 ~ 2.0.9 (小於 2.1)
  • ~> 2.2.beta : will match 2.2.beta.12
  • >= 3.2 : 3.2 以上的版本

引入時指定環境

都不寫代表是全部環境都會引入

gem 'bcrypt'

只引入 development, test

gem 'awesome_print', gorup: :development, :test

或
group :development, :test do
  gem "rspec"
  gem 'rspec-rails'
end

好用的 Gem

必備的 Gem

gem 'autoprefixer-rails'                    # 處理有 prefix 的 css 語法
gem 'bcrypt'                                # 加密,devise 需要
gem 'devise', '~> 3.5.2'                    # 註冊/登入 etc. 整套功能
gem 'mysql2', '~> 0.3.20'                   # MySQL
gem "paperclip", "~> 4.3"                   # 上傳
gem 'will_paginate'                         # 分頁
gem 'migration_comments'                    # 讓 migration 可以在欄位加上註解
gem 'awesome_print'                         # 讓 Rails console 顯示的資料更好閱讀
group :development, :test do
  gem "rspec"                               # Rspec
  gem 'rspec-rails'                         # Rspec
  gem 'factory_girl_rails'                  # Seed 會
  gem 'capybara'                            # 可以摸擬使用者點擊瀏覽器的行為
  gem 'faker'                               # Seed 假資料
  gem 'fabrication'                         # 不知道怎麼形容它,它像是個 Model 的模具,需要時再倒原料進去
end
group :development do
    gem 'quiet_assets'                      # 讓 log 更乾淨,過濾一些 Assets 的 log
    gem 'guard-livereload'                  # 偵測檔案變動,reload 你的瀏覽器
    gem 'better_errors'                     # 當 Error 發生時,讓頁面上顯示的錯誤更好讀
end

看情況用的 Gem

gem 'papercrop', '~> 0.3.0'                             # Crop 圖片,相依於 paperclip
gem 'rolify', '~> 4.1.1'                                # 如果需要細分權限的話
gem 'whenever', require: false                          # cronjob
gem "recaptcha", require: "recaptcha/rails"             # Google recaptcha
gem 'aws-sdk', '~> 2.2.37'                              # AWS SDK
gem 'omniauth-facebook', '~> 3.0.0'                     # FB 登入
gem "paranoia", "~> 2.1.5"                              # 軟刪除
gem 'pg'                                                # 使用 PostgreSQL 當 DB
gem 'rubocop', require: false                           # 可以利用它檢查/更正你不符合 ruby coding style 的程式碼
gem 'rails_12factor', group: :production                # Heroku : 將 rails log 導到標準輸出(STDOUT),否則會無法 Debug

bootstrap

Gemfile

gem 'bootstrap-sass', '~> 3.3.5'
gem 'sass-rails', '>= 3.2'

application.scss (注意副檔名是 .scss)

// "bootstrap-sprockets" must be imported before "bootstrap" and "bootstrap/variables"
@import "bootstrap-sprockets";
@import "bootstrap";

application.js

//= require jquery
//= require bootstrap-sprockets

awesome_print

當用 rails console 撈資料時, 欄位多會造成很難閱讀, 可以藉由 awesome_print 使輸出時好看一點

Gemfile

gem 'awesome_print'

Usage

$ rails console
> ap User.find(1)
  User Load (0.3ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1  [["id", 1]]
#<User:0x007f9755c5be78> {
                        :id => 1,
                     :email => "root@gmail.com",
        :encrypted_password => "$2a$10$/O5sGBe8KonKehd1GIFdaeh.RyDdApKmr60gr5t93Zql1S..WqiuS",
      :reset_password_token => nil,
    :reset_password_sent_at => nil,
       :remember_created_at => nil,
             :sign_in_count => 2,
        :current_sign_in_at => Sat, 04 Jul 2015 09:17:54 UTC +00:00,
           :last_sign_in_at => Sat, 04 Jul 2015 09:14:21 UTC +00:00,
        :current_sign_in_ip => "127.0.0.1",
           :last_sign_in_ip => "127.0.0.1",
                :created_at => Sat, 04 Jul 2015 09:12:24 UTC +00:00,
                :updated_at => Sat, 04 Jul 2015 09:17:54 UTC +00:00,
        :confirmation_token => nil,
              :confirmed_at => Sat, 04 Jul 2015 09:12:23 UTC +00:00,
      :confirmation_sent_at => nil
}
 => nil

Cron jobs

有時候主機需要一個背景需要可以一直跑的程式, 例如幫你檢查 DB 某個欄位再做對應的事

whenever

Install

gem 'whenever', :require => false

bundle install
wheneverize .

會產生 config/schedule.rb, 在這裡定義你的 cron job

注意! 無法直接在 schedule 裡寫 Rails 語法, 維持這裡的乾淨, 定義 cron job 要做的事情就好, 把任務定義在 task

Example

每一分鐘寫入一次檔案或 task

every 1.minute do
  command "/bin/echo '1' >> /tmp/test"
  rake "schedule:find"
end

every 30 minutes between 6 to 9.

every '*/30 6-9 * * *' do
  runner "Model.method"
end

使用步驟

預設的定義檔路徑是 config/schedule.rb, 所以要到 rails app 下執行

1) 查看轉換為 cron syntax 的語法, 但還不會寫入

whenever
whenever --set environment=development              # 執行 rake 必須加上

whenever 執行 rake 指令預設是 production,

2) 寫入 cron

whenever -w

或 update

whenever -i

如果一次 update development 及 production 環境的 schedule, 但只會有一種被寫進 cronjob

註) 啟動後 cron job 就會開始跑了, 因為它是使用系統的 cron job, 所以即使 rails 沒啟動它仍然會在背景跑

如果在啟動後輸入 crontab -e 就會看到剛剛寫入的以下內容

# Begin Whenever generated tasks for: /Users/jex/Desktop/projects/rails/translate/config/schedule.rb
* * * * * /bin/bash -l -c '/bin/echo '\''1'\'' >> /tmp/test'

# End Whenever generated tasks for: /Users/jex/Desktop/projects/rails/translate/config/schedule.rb

Rails 的 production 與 dev 的 cronjob 可同時存在

清除 cron job

whenever -c

在同一個 Rails app 不管執行的是 production, development 都會被清掉

example

lib/tasks/schedule.rake

namespace :schedule do
  desc "TODO.."
  task :find do
    Rails.logger.info(1)
  end
end

缺點

它是在執行的時候從頭啟動 rails 再執行你的 function,會有這麼一點效能浪費,我覺得是個小問題

sitemap_generator

是否該使用 sitemap 可以根據 google 的手冊來決定

1) 根據這個套件的 README 就可以順利安裝, 記得將 sitemap.rb 放在根目錄, 因為產生的 sitemap.xml.gz 會被放在 public/

2) 記得在 robots.txt 加上

Sitemap: https://www.example.com/sitemap.xml.gz

3) 到 google 的 search console 加入你的網站 (會要求你用一些方法確認是你的網站, 加上 <meta> 標簽的方法較簡單), 加入成功後左邊 menu 選擇 Sitemap 並且提交

FTP Server

使用 vsftpd

安裝

sudo apt-get update
sudo apt-get install vsftpd

設定 /etc/vsftpd.conf

write_enable=YES
local_umask=022

chroot_local_user=YES
allow_writeable_chroot=YES

開放 root 帳號登入

/etc/ftpusers : 把 root 註解

啟動

sudo service vsftpd restart

記得開放 21 port

現在 user 都可以登入到自己家目錄了

指令

連線

ftp example.com
sftp example.com

操作

ls     查看 Server 端的目錄或檔案
pwd    查看 Server 端目前所在的目錄
cd     變更 Server 端目前的目錄
cdup   變更 Server 端目前的目錄到上一目錄
lls    查看 Local 端的目錄或檔案
lcd    變更 Local 端目前的目錄
asc    設定傳輸模式為文字檔方式
bin    設定傳輸模式為二進位檔方式
get    將 Server 端的檔案拷貝至 Local 端現在目錄下
mget   拷貝多個 Server 端的檔案至 Local 端現在目錄下
put    將 Local 端的檔案拷貝至 Server 端現在目錄下
mput   拷貝多個 Local 端的檔案至 Server 端現在目錄下
delete 刪除 Server 端的檔案
mkdir  在 Server 端建立目錄
rmdir  刪除 Server 端的目錄
!      shell 指令

!ls    查看 Local 端的目錄或檔案

prompt 變換交談模式 (on/off),

?,help 指令使用說明

bye    結束 FTP

其他

!               cr              macdef          proxy           send
$               delete          mdelete         sendport        status
account         debug           mdir            put             struct
append          dir             mget            pwd             sunique
ascii           disconnect      mkdir           quit            tenex
bell            form            mls             quote           trace
binary          get             mode            recv            type
bye             glob            mput            remotehelp      user
case            hash            nmap            rename          verbose
cd              help            ntrans          reset           ?
cdup            lcd             open            rmdir
close           ls              prompt          runique

ref

Rails - 表單 Form

介紹

Rails 原生表單寫起來會比較囉嗦了點, code 看起來就沒那麼乾淨, 以下除了會紀錄一些原生表單的筆記, 也試用了其他可以讓表單寫起來更乾淨的套件 :

  • simple_form 讓表單寫起來更乾淨, 也支援 bootstrap 樣式, 推薦這套, 它也支援 devise
  • bootstrap_form 也是讓表單更簡單, 但預設使用的是 bootstrap 樣式

Rails 原生表單

form

# method 預設是 post
<%= form_for @post, url: posts_path(@post) do |f| %>
    <%= f.label :title %>
    <%= f.text_field :title %>
    <%= f.label :content %>
    <%= f.text_field :content %>
    <%= f.button :submit, disable_with: 'Submiting' %>
<% end %>

form_for 與 form_tag 差別

使用 form_for 的話一定要在 controller 的先 new 好 (@post = Post.new)

所以要 create 必須先 new 好

view :

form_for @post, do |p|
    p.text_field :title
end

title 必須與 db 一致, 因為它會去抓 model 的欄位

使用 form_tag 的話欄位名稱就可以自己取, 只不過要自己手動抓欄位名稱 (aaa = params[:title])

顯示 validation 錯誤訊息

顯示在欄位後面

<%= f.label :title %> : <%= f.text_field :title, :placeholder => 'At least 5 characters' %><%= @post.errors.full_messages_for(:title).first %>

全部錯誤訊息

<% @post.errors.full_messages.each do |msg| %>
  <li><%= msg %></li>
<% end %>

全部錯誤訊息的第一個

<%= @post.errors.full_messages.first if @post.errors.any? %>

顯示某個欄位的錯誤

<%= @post.errors.full_messages_for(:title).first %>

當 form 的錯誤訊息發生, 造成跑版

因為 label 及 text_field 會被 <div class="field_with_errors"> 包起來, 所以造成跑版

在 config/application.rb 加上就會顯示原始的 html 了

config.action_view.field_error_proc = Proc.new { |html_tag, instance|
  html_tag
}

ActionController::InvalidAuthenticityToken

如果是自己寫 HTML 的 form 送出表單造成沒有一起把 token 送出去, 加入以下這行到 form 即可解決

<%= tag(:input, :type => "hidden", :name => request_forgery_protection_token.to_s, :value => form_authenticity_token) %>

simple_form

Install

gem 'simple_form'

執行

rails generate simple_form:install

基本用法

<%= simple_form_for @user, defaults: { input_html: { class: 'default_class' } } do |f| %>
  <%= f.input :username, input_html: { class: 'special' }, wrapper_html: { class: 'username' }  %>          # wrapper_html 會在 label 及 input 外層包一個 div
  <%= f.input :password, input_html: { maxlength: 20 }, label_html: { class: 'my_class' } %>
  <%= f.input :role, as: :radio_buttons, collection: { t('.role_client') => 'client', t('.role_translator') => 'translator' }, checked: 'client' %>
  <%= f.input :remember_me, input_html: { value: '1' } %>
  <%= f.button :submit %>
<% end %>

f.input 包含了 label, input

bootstrap

執行

$ rails generate simple_form:install --bootstrap

   identical  config/initializers/simple_form.rb
      create  config/initializers/simple_form_bootstrap.rb
       exist  config/locales
   identical  config/locales/simple_form.en.yml
   identical  lib/templates/erb/scaffold/_form.html.erb

locales 只會產生出 simple_form EN 的, 自己再 copy 改成 zh-TW 版本的

基本用法

<div class="row">
  <div class="col-md-6">
    <div class="panel panel-primary">
      <div class="panel-heading">Simple Form: Basic Form</div>
      <div class="panel-body">
        <%= simple_form_for @post, url: posts_path(@post), html: { class: 'form-horizontal' } do |f| %>
          <%= f.error_notification %>

          <%= f.input :title, input_html: {class: 'form-control'} %>

          <%= f.input :content, input_html: {class: 'form-control'} %>

          <%= f.button :submit, disable_with: 'Submiting', input_html: {class: 'btn btn-success'} %>
        <% end %>
      </div>
    </div>
  </div>
</div>

連錯誤訊息都會自動顯示在欄位下, 非常方便

submit button 不能用 f.submit, 應該用 :

`<%= f.button :submit, t('form.submit'), class: 'btn btn-success' %>`

default value, fail validation 會填原本送出的值

<%= f.input :contact_email, required: true, input_html: {value: (params[:user].nil?) ? f.object.email : params[:user]['contact_email']} %>

i18n

copy config/locales/simple_form.en.yml to config/locales/simple_form.zh-TW.yml, 替換第一行 en -> zh-TW

collection + i18n:

Radio

<%= f.input :sex, as: :radio_buttons, collection: [:male, :female] %>
<%= f.input :sex, as: :radio_buttons, collection: User.genders.keys.map { |x| x.to_sym } %>

collection 不能直接用 User.genders.keys, 因為它只是 array, 值一定要 symbol i18n 才會 work

simple_form.zh-TW.yml :

simple_form:
    options:
      user:
        gender:
          male: '男'
          female: '女'
    labels:
      user:
        gender: "性別"
    hints:
      user:
        gender: "請選擇性別"

Radio + ActiveRecord enum

<%= f.input :gender, as: :radio_buttons, collection: User.genders.keys %>

devise + simple_form + bootstrap - Sign_up

<div class="row">
  <div class="col-md-4 col-md-offset-4">
    <h2><%= t('.sign_up', :default => "Sign up") %></h2>

    <%= simple_form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
      <%= f.error_notification %>
      <%= f.input :email, required: true, autofocus: true %>
      <%= f.input :password, required: true, hint: (t('.characters_minimum', num: 8) if @minimum_password_length) %>
      <%= f.input :password_confirmation, required: true %>
      <%= f.button :submit, t('form.submit'), class: 'btn btn-success' %>
    <% end %>

    <%= render "devise/shared/links" %>
  </div>
</div>

bootstrap_form

gem 'bootstrap_form'

application.css 加上

/*
 *= require rails_bootstrap_forms
 */

基本用法

<%= bootstrap_form_for(@user) do |f| %>
  <%= f.email_field :email %>
  <%= f.password_field :password %>
  <%= f.check_box :remember_me %>
  <%= f.submit "Log In" %>
<% end %>

它會產生

<form accept-charset="UTF-8" action="/users" class="new_user" id="new_user" method="post">
  <div class="form-group">
    <label for="user_email">Email</label>
    <input class="form-control" id="user_email" name="user[email]" type="email">
  </div>
  ...略...
</form>

Rails - Devise / Cancancan / Rolify

介紹

  • devise : 提供註冊, 登入, 登出一整套 solution
  • cancancan : 授權功能, 判斷 user 是否可以做什麼, 不可以做什麼
  • rolify : 身份功能, 賦與 user 身份 ex: 一般 user 或 admin 等等..

cancancan 設計上跟 model 綁太緊我覺得實作上會有些綁手綁腳,而 rolify 適合用在 role 分很多的情況下使用,如果希望專案引入的東西單純一點,我建議只需要 devise 就夠了

Devise

安裝

1) Gemfile :

gem 'bcrypt'
gem 'devise', '~> 3.5.6'
  • devise 預設使用 bcrypt 做加密

2) Init :

rails generate devise:install
rails generate devise User
rails generate devise:views
rake db:migrate

3) 補上 zh-TW 旳 rails-i18n 及 devise i18n :

rails i18n (validate 等等會用到的 i18n) 及 devise i18n (devise 專屬的 i18n, 主要是一些流程的訊息) 都有套件可以直接引入,但我不這麼做,因為如果有訊息需要客製會不好改,所以我建議還是手動新增 i18n

  • 預設安裝完 devise 只會產生 config/locales/devise.en.yml, 這樣訊息只會有英文版
  • 新增 devise.zh-TW.yml, 內容從devise-i18n 這裡 copy
  • 新增 rails-i18n.zh-TW.yml, 內容從rails-i18n 這裡 copy

4) 設定預設為 zh-TW, config/application.rb :

config.i18n.default_locale = :'zh-TW'

5) 修改 config/initializers/devise.rb

config.secret_key = 'rake secret 產生的 key 貼到這裡'

# 寄件人 (email header 的 From 及 Reply-To)
config.mailer_sender = 'dev@mail.com'

# 用 custom devise view (views/users 下)
config.scoped_views = true

6) [必要] 設定 mailer 的 host, 在 config/environments/development.rb 加在 block 裡面 :

config.action_mailer.default_url_options = { host: 'localhost:3000' }
config.action_mailer.delivery_method     = :sendmail

7) Confirm email

首先到 app/models/user.rb 加上 :confirmable

devise :database_authenticatable, :registerable,
           :recoverable, :rememberable, :trackable, :validatable, :confirmable

再增加 confirm 欄位到 users

rails g migration add_confirmable_to_devise

migrate/20150703173329_add_confirmable_to_devise.rb (不要使用 change) :

class AddConfirmableToDevise < ActiveRecord::Migration
  # Note: You can't use change, as User.update_all will fail in the down migration
  def up
    add_column :users, :confirmation_token, :string
    add_column :users, :confirmed_at, :datetime
    add_column :users, :confirmation_sent_at, :datetime
    # add_column :users, :unconfirmed_email, :string # Only if using reconfirmable
    add_index :users, :confirmation_token, unique: true
    # User.reset_column_information # Need for some types of updates, but not for update_all.
    # To avoid a short time window between running the migration and updating all existing
    # users as confirmed, do the following

    # mysql 才能執行
    # execute("UPDATE users SET confirmed_at = NOW()")

    # All existing user accounts should be able to log in after this.
    # Remind: Rails using SQLite as default. And SQLite has no such function :NOW.
    # Use :date('now') instead of :NOW when using SQLite.
    # => execute("UPDATE users SET confirmed_at = date('now')")
    # Or => User.all.update_all confirmed_at: Time.now
  end

  def down
    remove_columns :users, :confirmation_token, :confirmed_at, :confirmation_sent_at
    # remove_columns :users, :unconfirmed_email # Only if using reconfirmable
  end
end

8) config/initializers/devise.rb :

config.reconfirmable = false

9) 顯示按鈕及訊息 app/views/layouts/application.html.erb :

<% if notice %>
  <p class="alert alert-success"><%= notice %></p>
<% end %>
<% if alert %>
  <p class="alert alert-danger"><%= alert %></p>
<% end %>

<p class="navbar-text pull-right">
    <% if user_signed_in? %>
      Logged in as <strong><%= current_user.email %></strong>.
      <%= link_to 'Edit profile', edit_user_registration_path, :class => 'navbar-link' %> |
      <%= link_to "Logout", destroy_user_session_path, method: :delete, :class => 'navbar-link'  %>
    <% else %>
      <%= link_to "Sign up", new_user_registration_path, :class => 'navbar-link'  %> |
      <%= link_to "Login", new_user_session_path, :class => 'navbar-link'  %>
    <% end %>
</p>

assets 要引入 jquery_ujs, logout 才會 work, 因為要靠它送出 Http method 為 delete 的 request

10) 完成!

如果 sign up 後沒有收到信

看 rails log 有沒有組出 email 的內容,

有的話可以直接 copy email 內容的 confirm link, 先手動 confirm email

http://localhost:3000/users/confirmation?confirmation_token=hhdRjPszSyPS5w8iczwH

不使用第三方服務,用本機寄信很可能被 gmail 擋掉, 可以先以 sendmail 下指令看 /var/log/mail.log 的錯誤訊息

判斷是否登入

application_controller.rb :

before_action :authenticate_user!

現在應該就可以看到效果了, 首頁會顯示 notice 及 alert

判斷 login status

在 User 新增 status 欄位用來判斷帳號的登入狀態,透過 devise 登入時我們僅需要多加兩個 method 在 user.rb, devise 會自動使用它們

models/user.rb :

enum status: {
  freeze: 0,            # 無法登入
  active: 1,            # 可以正常登入
}

def active_for_authentication?
  super && self.active?                 # status 是 active 才通過,否則觸發下面的錯誤訊息
end

# 錯誤訊息
def inactive_message
  if !self.active?
    :locked                             # 會顯示 i18n (`zh-TW.devise.failure.locked`) 的錯誤訊息
  else
    super # Use whatever other message
  end
end
  • 凍結的帳號,即使已經登入了,操作其他頁面時仍然會被強制登出
  • 如果你有整合 facebook 登入,用戶使用 Facebook 登入時,它也會檢查 status 是否為 active,行為如同使用 devise 登入

判斷原密碼是否正確

User.find(2).valid_password?('00000000')
=> true

驗證碼

class User::SessionsController < ::Devise::SessionsController
  before_action :check_otp, :only => [:create]

  private
  def check_otp
    return true unless user
    return true unless user.profile_otp
    if xxx
      flash[:warning] = "錯誤,請檢查OTP驗證碼"
    end
  end
end

設定 session 存活時間

一般來說, session 存活的時間是看 browser 的 cookie 存活時間, 但也可以透過 devise 去限制 session 存活時間

models/user.rb :

devise :timeoutable

config/initializers/devise.rb :

config.timeout_in = 30.minutes

開發環境可一鍵登入

讓連結 GET users/sign_in?user_id=1 可以直接登入

view :

<%= link_to '一鍵登入', new_user_session_path + "?user_id=#{user.id}", class: 'btn btn-info btn-xs' %>

controllers/users/sessions_controller.rb :

class Users::SessionsController < ::Devise::SessionsController
  before_action :fast_pass, only: [:new]

  private

  def fast_pass
    # 限制測試環境才可以
    if Rails.env.development? && params[:user_id]
      sign_in(:user, User.find(params[:user_id]))
      flash[:notice] = '登入成功'
      redirect_to root_path
      return
    end
  end
end

config/routes.rb

# 將 devise_for 改成 :
devise_for :users, :controllers => {:sessions => "users/sessions"}

註冊加入自訂欄位

情景 : 判斷 user sign_up 選擇的 role 再做 assign

views/users/registrations/new.html.erb
<%= f.input :role, collection: ['user','translator'], prompt: "Select your age", selected: 21 %>
models/user.rb
attr_accessor :role
validates :role, presence: true
after_create :assign_role

def assign_role
  if self.role == 'translator'
    self.add_role(:translator)
  else
    self.add_role(:user)
  end
end
controllers/application_controller.rb
before_filter :configure_permitted_parameters, if: :devise_controller?

protected

def configure_permitted_parameters
  devise_parameter_sanitizer.for(:sign_up) << :role
end

其他寫法

devise_parameter_sanitizer.for(:sign_up) {|u|u.permit(:name,:nick_name,:mobile,:email,:password,:password_confirmation)}
devise_parameter_sanitizer.for(:account_update) << :full_name

可參考 strong parameter

改變註冊完成去的頁面

routes :

devise_for :users, :controllers => { registrations: "users/registrations" }

controllers/users/registrations_controller.rb

如果沒有開啟 email confirmable 的話, 使用這個 method

class Users::RegistrationsController < Devise::RegistrationsController
  protected

  def after_sign_up_path_for(resource)
    root_path
  end
end

有開啟 email confirmable 使用它

class Users::RegistrationsController < Devise::RegistrationsController
  protected

  # 剛註冊完 "確認信已寄到您的 Email 信箱。請點擊信內連結以啓動您的帳號"
  def after_inactive_sign_up_path_for(resource)
    root_path
  end
end

Edit 時不能改變 Email 及不用再輸入密碼

routes :

devise_for :users, :controllers => { registrations: "users/registrations" }

controllers/users/registrations_controller.rb

class Users::RegistrationsController < Devise::RegistrationsController

  def update
    # 排除 email 不給改
    account_update_params = devise_parameter_sanitizer.sanitize(:account_update).except(:email)
    @user = User.find(current_user.id)

    # 判斷此項修改是否需要密碼
    if needs_password?
      successfully_updated = @user.update_with_password(account_update_params)
    else
      # 不需密碼
      account_update_params.delete('password')
      account_update_params.delete('password_confirmation')
      account_update_params.delete('current_password')
      successfully_updated = @user.update_attributes(account_update_params)
    end

    if successfully_updated
      set_flash_message :notice, :updated
      sign_in @user, :bypass => true
      redirect_to edit_user_registration_path
    else
      render 'edit'
    end
  end

  protected

  def needs_password?
    params[:user][:password].present?
  end
end

如果只需要修改密碼

def update
  @user = User.find(current_user.id)
  if @user.update_with_password(account_update_params)
    set_flash_message :notice, :updated
    sign_in @user, :bypass => true
    redirect_to edit_user_registration_path
  else
    render 'edit'
  end
end

def account_update_params
  params[:user].permit(:password, :password_confirmation, :current_password)
end

關閉預設有的 Unhappy? Cancel my account 功能

routes :

devise_for :users, skip: :registrations
devise_scope :user do
  resource :registration,
    only: [:new, :create, :edit, :update],
    path: 'users',
    path_names: { new: 'sign_up' },
    controller: 'devise/registrations',                     # 如果有改原生的, 路徑要指向正確 ex: users/registrations  -> users/registrations_controller.rb
    as: :user_registration do
      get :cancel
    end
end

你會發現原本刪除帳號的 route 己經不見了 :

edit_user_registration DELETE /users(.:format)                  users/registrations#destroy

主要是剛剛 route 的 only 那段把 :delete 拿掉,如果不想用 devise 提供的修改 user 功能,也可以再把 :edit :update 拿掉

不使用 devise 的修改資料功能

先拿掉 devise 修改基本資料功能

devise_for :users, skip: :registrations
devise_scope :user do
  resource :registration,
    only: [:new, :create],          # 拿掉 :edit, :update
    path: 'users',
    path_names: { new: 'sign_up' },
    controller: 'devise/registrations',                     # 如果有改原生的, 路徑要指向正確 ex: users/registrations  -> users/registrations_controller.rb
    as: :user_registration do
      get :cancel
    end
end

resources :users, only: [:edit, :update]

雖然可以直接複寫 devise 的 method,但如果要客製自已功能的話,建議還是自已建新的 controller, view

controllers/users_controller.rb :

class UsersController < ApplicationController

  def edit
  end

  def update
    # 修改基本資料
    current_user.update(params[:user].permit(:name, :phone, :address))

    # 修改密碼
    if current_user.update(params.require(:user).permit(:password, :password_confirmation))
      return redirect_to root_path          # 修改密碼成功後 session 會被清掉,所以返回首頁
    end

    redirect_to edit_user_path(current_user)
  end

end

views/users/edit.html.erb

<h2>修改會員資料</h2>

<%= form_for current_user, url: user_path(current_user), html: { method: :put } do |f| %>

<div>  姓名:<%= f.text_field :name %></div>
<div>  電話:<%= f.text_field :phone %></div>
<div>  地址:<%= f.text_field :address %></div>

<hr>
<div>  密碼:<%= f.password_field :password %></div>
<div>  確認密碼:<%= f.password_field :password_confirmation %></div>
<%= f.submit 'OK' %>
<% end %>

ref : Allow users to edit their password

After email confirmation

routes :

devise_for :users, controllers: { confirmations: "users/confirmations" }

controllers/users/confirmations_controller.rb

class Users::ConfirmationsController < ::Devise::ConfirmationsController

    protected
    def after_confirmation_path_for(resource_name, resource)
      resource.status = 11
      resource.save
      root_path
    end

end

覆寫 devise sign_in 後去的頁面

controllers/application_controller.rb

def after_sign_in_path_for(resource)
  sign_in_url = new_user_session_url
  if request.referer == sign_in_url
    super
  else
    stored_location_for(resource) || request.referer || root_path
  end
end

加上 reCAPTCHA

1) 到 Google reCAPTCHA 申請

2) Install

gem "recaptcha", :require => "recaptcha/rails"

3) config/initializers/recaptcha.rb

Recaptcha.configure do |config|
  config.public_key  = '6Lc6BAAAAAAAAChqRbQZcn_yyyyyyyyyyyyyyyyy'           # Site key
  config.private_key = '6Lc6BAAAAAAAAKN3DRm6VA_xxxxxxxxxxxxxxxxx'           # Secret key
end

# [Optional] Skip Test Env
Recaptcha.configuration.skip_verify_env.delete("test")

4) 使用

<%= form_for @foo do |f| %>
  <%= recaptcha_tags %>
<% end %>

打開頁面就能看到了, 不需要特別引入 reCAPTCHA 的 <script>, 因為 gem 已經幫你引入了

5) 建立 RegistrationsController

users/registrations_controller.rb

class RegistrationsController < Devise::RegistrationsController
    def create
      if verify_recaptcha
        super
      else
        build_resource(sign_up_params)
        clean_up_passwords(resource)
        flash.now[:alert] = 'reCAPTCHA 未通過, 請再試一次'
        flash.delete :recaptcha_error
        render :new
      end
    end
end

routes :

devise_for :users, controllers: {registrations: "users/registrations"}

6) 完成! 如果不通過就會以 alert 的方式顯示

oauth-facebook

最好使用以下的步驟,不要單獨另外自已整合 oauth-facebook,會踩很多雷

0) 先將 Facebook 的 App 申請好

1) 安裝

gem 'omniauth-facebook', '~> 3.0.0'

2) Migrate

rails g migration add_oamniauth_to_users

add_column :users, :oauth_provider, :string
add_column :users, :oauth_uid, :string
add_column :users, :oauth_token, :string
add_column :users, :oauth_expires_at, :datetime

3) config/initializers/devise.rb

config.omniauth :facebook, "APP_ID", "APP_SECRET"

4) app/models/user.rb

# 在原本的後面加上這兩項
devise :omniauthable, :omniauth_providers => [:facebook]

devise 會自動加上這兩個 routes ( user_omniauth_authorize_path(provider), user_omniauth_callback_path(provider) )

5) 重新啟動 rails server

6) 加上 Login Link

<%= link_to "Sign in with Facebook", user_omniauth_authorize_path(:facebook) %>

Logout 用原本 devise 的登出就行了

7) 加上 callback route

devise_for :users, :controllers => { :omniauth_callbacks => "users/omniauth_callbacks" }

8) 建立 app/controllers/users/omniauth_callbacks_controller.rb

class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def facebook
    # You need to implement the method below in your model (e.g. app/models/user.rb)
    @user = User.from_omniauth(request.env["omniauth.auth"])

    if @user.persisted?
      sign_in_and_redirect @user, :event => :authentication #this will throw if @user is not activated
      set_flash_message(:notice, :success, :kind => "Facebook") if is_navigational_format?
    else
      session["devise.facebook_data"] = request.env["omniauth.auth"]
      redirect_to new_user_registration_url
    end
  end

  def failure
    redirect_to root_path
  end
end

9) models/user.rb

def self.from_omniauth(auth)
  where(oauth_provider: auth.provider, oauth_uid: auth.uid).first_or_create do |user|
    user.email = auth.info.email
    user.password = Devise.friendly_token[0,20]         # 密碼必填
    user.confirmed_at = Time.now                        # 直接通過 confirmation
    user.oauth_token = auth.credentials.token
    user.oauth_expires_at = Time.at(auth.credentials.expires_at)
    # user.name = auth.info.name   # assuming the user model has a name
    # user.image = auth.info.image # assuming the user model has an image
  end
end

def self.new_with_session(params, session)
  super.tap do |user|
    if data = session["devise.facebook_data"] && session["devise.facebook_data"]["extra"]["raw_info"]
      user.email = data["email"] if user.email.blank?
    end
  end
end

10) 完成!

Devise view 取得適當的錯誤訊息

以重寄驗證信來說,如果 User 已經 confirm 過再寄驗證信,預設會得到不那麼好看的錯誤訊息

<%= devise_error_messages! %>

顯示 :

有 1 個錯誤導致 用戶 不能被儲存:
電子郵箱 已經驗證,請直接登入。

但我們僅需要直接跟使用者說原因就好

<%= resource.errors.full_messages_for(:email).first %>
<%= resource.errors.messages[:email].first if resource.errors.messages.include?(:email) %>

未登入情況下觸發 ajax 得到 401

如果在未登入的情況下做一些 ajax 的操作,ajax 的 request 可能會因為 authenticate_user 得到 401 且內容為 “您需要先登入或註冊後才能繼續。”, 為了讓這個訊息可以正確被顯示出來要加上以下的 js

$(document).ajaxError(function (e, xhr, settings) {
    if (xhr.status == 401) {
        alert(xhr.responseText);
    }
});

讓 devise 輸出 json api

要在 application_controller.rb 加上 :

respond_to :html, :json

在 devise 裡面的 controller e.g. registrations_controller.rb 是沒有用的

Rolify

安裝

Gemfile :

gem 'rolify'

Init :

rails generate rolify Role User
rake db:migrate

Method

Add role

u1 = User.create({email: "example@gmail.com", password: "00000000"})
u1.add_role :root

Remove role

# rolify 會很聰明的去找 users_roles TABLE 目前這個 role 是不是最後一個 role 正在被使用, 如果是的話, 它不但會刪 users_roles 記錄, 也會去 roles 刪那筆 role
u.remove_role(:admin)

Has role?

u1.has_role? :admin
 => true

Find all users with specific role

User.with_role(:admin)

Find all users with A role or B role

User.with_any_role(:user, :admin)

Find all users with A role and B role

User.with_all_roles(:user, :admin)

Cancancan

安裝

Gemfile

gem 'cancancan'

Init :

rails generate cancan:ability
rake db:migrate

Example

1) app/models/ability.rb :

class Ability
  include CanCan::Ability

  def initialize(user)

    if user.blank?

      cannot :manage, :all

    elsif user.has_role?(:root) || user.has_role?(:admin)

      can :manage, :all

    elsif user.has_role?(:translator)

      can :index, Post

    elsif user.has_role?(:user)

      cannot :index, Post
    end

  end
end

這個 user 其實就是 devise 提供你在 controller 使用的 current_user

2) app/controllers/application_controller.rb

rescue_from CanCan::AccessDenied do |exception|
  redirect_to root_url, :alert => exception.message
end

3) app/controllers/posts_controller.rb

load_and_authorize_resource

4) Check ability in view

<% if can? :index, Post %>
  <%= link_to 'Posts', posts_path, class: 'btn btn-success' %>
<% end %>

4) 測試

  • 如果使用 user 的帳號不會看到 Post button, 即使打網址到 /posts, 也被導到首頁, 並顯示 You are not authorized to access this page.
  • 如果使用 translator, admin, admin 可正常看到 Post button, 也能正常到 /posts

Action

  • :manage : 這個 controller 內所有的 action
  • :read : :index:show
  • :update : :edit:update
  • :destroy : :destroy
  • :create : :new:create
  • :index : :index, 也可特別指定 action name
  • :qq : 也可指定非 restful 的 action, 只要是 controller 下的 action, 都能限制
  • :all : 所有 object (resource), 也就是 model name

也能這樣寫 :

can :read, [ Post, Comment ]
can [ :create, :update ], [ Post, Comment ]

Alias action

alias_action :update, :destroy, :to => :modify
can :modify, Comment

or method

protected

def basic_read_only
  can :read,    Post
  can :list,    Post
  can :search,  Post
end

Ability

只能編輯自己的文章

can :update, Post do |post|
  (post.user_id == user.id)
end

# 也可以這樣寫
can :update, Post, user_id: user.id

View 注意傳入的物件

是否能建立 Post

<% if can? :create, Post %>
  <%= link_to 'New Post', new_post_path, class: 'btn btn-success' %>
<% end %>

傳入的是 Post

只能編輯自己的 Post

<% @posts.each do |post| %>
    <% if can? :update, post %>
      <%= link_to 'Edit', edit_post_path(post), class: 'btn btn-warning btn-xs' %>
    <% end %>
<% end %>

注意! each 裡傳入的是 post 而不是 Post, 否則 view 會與 ability.rb 的 can :update, Post, user_id: user.id 不一致

捕捉 Access Deny Error

rescue_from CanCan::AccessDenied do |exception|
  flash[:warning] = exception.message
  redirect_to root_path
end

raise CanCan::AccessDenied.new("You are not authorized to perform this action!", :custom_action, Project)

測試將 post_controller.rb 的 load_and_authorize_resource 拿掉

view 仍然會依照 ability.rb 的設定顯示或不顯示,

如果權限不夠, 直接去 /posts 也是能正常瀏覽,

但如果將 load_and_authorize_resource 加回來,

就會導致 Access Deny 了, 符合預期結果,

ability.rb 設定完一定要在 post_controller.rb 加上 load_and_authorize_resource 才能真正起到權限管理作用

Authorization for Namespaced Controllers

controllers/dashboard/dashboard_controller.rb

class Dashboard::DashboardController < ApplicationController

controllers/dashboard/log_controller.rb

class Dashboard::LogController < Dashboard::DashboardController

application_controller.rb

private

def current_ability
  # I am sure there is a slicker way to capture the controller namespace
  controller_name_segments = params[:controller].split('/')
  controller_name_segments.pop
  controller_namespace = controller_name_segments.join('/').camelize
  Ability.new(current_user, controller_namespace)
end

ability.rb

class Ability
  include CanCan::Ability

  def initialize(user, controller_namespace)
    case controller_namespace
    when 'Dashboard'
      can :manage, Log
    end
  end
end

uninitialized constant Welcome 錯誤

load_and_authorize_resource 會自動使用 RESTful style 驗證 Controller 及依 Controller Name 去找對應的 Model

會發生這原因是沒有 Welcome model

很多情況我們的 Controller 可能不會有對應的 model,

所以我們可以用以下兩種方式達到 authorize 這個 controller

1) Call authorize

這個 controller 不要寫 authorize_resource

直接在 action 寫

def roll_logs authorize! :roll, :logs end

在 Ability(models/ability.rb) 加上

can :roll, :logs if user.admin?

authorize 可以通過兩個 symbol 給予這個 authorize 的行為命名而不需要跟 controller 或 model name 一樣

2) 讓 Ability 不要去找它的 model

class ToolsController < ApplicationsController
  authorize_resource class: false
  def show
  end
end

它會自動地去 call authorize!(:show, :tool)

在 Ability 加上 :

can :show, :tool if user.admin?

其他

預設 resource 與 Controller 同名

load_and_authorize_resource = load_resource + authorize_resource

load_resource 會自動在 Action 裡執行對應的 instance

new :
    @post = Post.new`

show :
    @post = Post.find(params[:id])

authorize_resource 的 resource 與 models/ability.rb 設定的相同, ex: can :manage, Post

如果是 PostController 預設的 resource 就是 Post, instance 也就是 @post, 也可以另外指定

authorize_resource :message

can :manage, Message

使用 Octopress 寫 Blog

介紹

一個寫 blog 的 framework, 使用 markdown 語法快速的記下筆記, 再利用 generate 指令幫你生成 html, 你可以 deploy 到 github, 直接使用 github 當作你的 host, 就不需要自己再去找主機架設了, 並且使用 git 做版控, 未來要搬家到其他支援 markdown 語法的服務都非常方便, 也不怕文章遺失問題, 唯一缺點就是有點使用門檻

目前最新版本是 3.0

運作原理

source 存放著主程式,當 generate 時會產生在 /public 下,如果執行 preview 其實是看到這資料夾底下的檔案

當執行 deploy 時,會將原本的 _deploy 刪除,並將 /public 資料 copy 為 _deploy 並且 push 到 master

所以 github 上的 master 就是你的 _deploy 資料夾,在這資料夾外面,包含根目錄等等都是 source

而你輸入 example.github.io 就會連到 example.github.iomaster branch

Octopress 3.0

Install

gem install octopress

New a blog

octopress new en.jex.tw

[Config] Modify _config.yml

  • title
  • email
  • description
  • url
  • github_username

[Deploy] Deploy github page as your host.

I already had a website on github, so I use another repository as this demonstration.

1) Set host where will deploy to

octopress deploy init git git@github.com:jex-lin/en

Octopress 3.0 supports two ways to let you deploy to host, which are github and s3.

2) Since I use github page so I need to change git_branch from master to gh-pages

It will generate a file _deploy.yml for deploy config.

3) Git init and commit. And Set remote branch

git init
git add .
git commit -m "First commit"
git remote add origin git@github.com:jex-lin/en.git

4) Build & deploy

jekyll build             # It will create `_site`.
octopress deploy         # It will copy `_site` to `.deploy` and push `.deploy` to remote branch `gh-pages`

5) Done! You will see that jex-lin.github.io/en works!

[Source] Push source to source remote for the first time

Remember to add .deploy to .gitignore before you push.

Your current branch may be master, rename to source and push to github

git branch -m source
git push origin source

[Deploy] Custom domain

1) Add a DNS record: (CNAME) en -> jex-lin.github.io

2) Create CNAME : echo "en.jex.tw" > CNAME

3) Build and deploy. You will see that en.jex.tw works.

[Config] Change url path (Recommended)

The default url path is like this /jekyll/update/2017/01/08/welcome-to-jekyll.html.

The categories defined in your post will be created as a folder when you build.

categories: jekyll update

So your url path will seem very long. Let’s change it shorter.

_config.yml :

permalink: /:title/

or octopress 2.0 default :

permalink: /blog/:year/:month/:day/:title/

[Command] Create a new post

You must be at the root path, because it will create _post and post file on your current path.

octopress new post "writing"

[Command] Preview

jekyll serve

localhost:4000

Octopress 2.0

Install

git clone git://github.com/imathis/octopress.git octopress
bundle install
rake install        安裝預設主題

bundle 指令 - 要先有 rvm 安裝 ruby 及 gem,再使用 gem 安裝 bundler 才能使用,安裝 gem 可參考此篇前半部

[Deploy] 佈署到Github上

[1] 到Github點選New Repository, Repository name 輸入 jex-lin.github.com

[2] 輸入指令

rake setup_github_pages    ( 輸入 git@github.com:jex-lin/jex-lin.github.com.git
rake generate
rake deploy                ( push到Github

[3] 檢查是否成功, 到 github 提供的網域 (ex: https://jex-lin.github.io/)

[Deploy] 新增文章及 push 到 github

因為 deploy 只會 push generate 出來的前端靜態檔, 但文章檔案還是在本機的, 所以要自動手動 push

git add .
git commit -m "First commit"
git push origin source

[Deploy] 在其他電腦部署己存在的 octopress

git clone -b source git@github.com:jex-lin/jex-lin.github.com.git octopress
cd octopress
bundle install          (才能使用 rake new_post['xxx'] 指令)
git clone git@github.com:jex-lin/jex-lin.github.com.git _deploy
rake generate
rake deploy

[Deploy] 同步不同電腦的 octopress

git pull                    ( 只會同步 _deploy
git pull origin source      ( 同步 source/_post

[Deploy] 將 Domain 指向 github page

新增一筆新的 DNS record : CNAME, subdomain: blog 指到 jex-lin.github.io

新增 CNAME 檔案在 source 目錄下

cd source
echo "blog.jex.tw" > CNAME

[Command] 建立文章

rake new_post['article_title']

會產生 .markdown 的檔案, BLOG內容直接打在虛線下面

[Command] 轉成網頁檔

rake generate

[Command] 預覽發佈

rake preview

輸入完指令打開瀏覽器網址列輸入 http://127.0.0.1:4000/

[Command] 發佈到網站

rake deploy

[Command] generate + deploy

rake gen_deploy

[Theme] 安裝主題

git submodule add git://github.com/octopress-themes/classic-light.git .themes/light
rake install[light]
rake generate
rake deploy

[Theme] 移除主題

1) 刪除 .gitmodules

[submodule ".themes/dark"]
  path = .themes/dark
  url = https://github.com/octopress-themes/classic-dark.git

2) 刪除 .git/config

[submodule ".themes/dark"]
  url = https://github.com/octopress-themes/classic-dark.git

刪除 theme files

git rm  --cached .themes/dark

[Layout] Remove sidebar

_config.yml:

  1. 註解 default_asides (# default_asides: [custom/asides/about.html, ...)
  2. 加上 sidebar: collapse

[Layout] 補充 : layout 對應的檔案路徑

  • navigation (Blog / Archieve) 那條的連結 : _includes/custom/navigation.html
  • 右邊關於我 (A little something about me.) : _includes/custom/asides/about.html,但會被 load 主要是在 _config.yml 中設定的 default_asides: [custom/asides/about.html, ..

[Layout] 在 navigation 新增 about me

1) octopress/source/_includes/custom/navigation.html :

<li><a href="/about">About me</a></li>

2) rake new_page["about"]

會產生 source/about/index.markdown

[Layout] 在側欄增加大頭像

1) 將圖片放到 octopress/source/images/about_me/sidebar.jpg

2) octopress/source/_includes/custom/asides/about.html :

<section>
    <h1>Jex</h1>
    <img alt="me" src="/images/about_me/sidebar.jpg">
    <p>Backend Engineer</p>
</section>

_config.yml :

default_asides: [custom/asides/about.html, ...(略)...]

[Config] 修改網站標題及描述

修改 _config.yml, 記得要 generatedeploy 才會生效

[Config] GA

_config.yml :

google_analytics_tracking_id: UA-23414877-5

[Troubleshooting] 如果 deploy 後接到 github 的錯誤信關於 theme 有問題

The page build failed with the following error:

The submodule `.themes/justin-kelly-theme` was not properly initialized with a `.gitmodules` file. For more information, see https://help.github.com/articles/page-build-failed-missing-submodule.

For information on troubleshooting Jekyll see:

  https://help.github.com/articles/troubleshooting-jekyll-builds

If you have any questions you can contact us by replying to this email.

有可能是你的 master (_deploy 的個資料夾)的 .themes 跟你的 source 的 .themes 不一樣,建議刪除 _deploy/.themes 再 copy .themes 過去

[Troubleshooting] 如果你安裝 3.0 後,使用 octopress command 發生錯誤

/Users/jex/.rvm/rubies/ruby-2.3.0/lib/ruby/2.3.0/rubygems/specification.rb:2158:in `method_missing': undefined method `this' for #<Gem::Specification:0x3ff51a403e78 mercenary-0.3.6> (NoMethodError)

表示你需要升級你的 gem 了 :

gem install rubygems-update
update_rubygems
gem update --system

How to create multiple github pages?

First of all, let’s learn about how it works.

By default, http://jex-lin.github.io will point to master branch of jex-lin.github.io.

jex-lin.github.io/{repository_name} will point to gh-pages branch of this repository if it’s available.

Get started

  1. Create a new repository and name en as a example.
  2. Create file index.html in master and push to github, but website won’t be activated in master. So you need to rename from master to gh-pages.
  3. Then you will see that http://jex-lin.github.io/en/ is available in Settings page. ( If you had pointed to, it would be like this: http://blog.jex.tw/en/)

How to point en.jex.tw to http://jex-lin.github.io/en/ ?

  1. Add a record to your DNS. CNAME en -> jex-lin.github.io
  2. Create CNAME file and put en.jex.tw into it.
  3. Then you will see that en.jex.tw works.

Curl 參數

-i -I 顯示 response 的 detail

只顯示 header

curl -I http://wwww.example.com

顯示 header + body

curl -i http://wwww.example.com

-H 指定 Header

curl -H "Authorization:Bearer NDQzYmM1ZD" http://www.example.com

-D 將 response 的 header 輸出成檔案

curl www.google.com -D /tmp/google-cookie.txt

v 顯示 request/reqsponse 的 request detail

curl -v http://wwww.example.com

-L support 302 redirect

$ curl -L www.google.com

-x 指定 proxy

curl -x proxy.example2.com:8080 http://www.example.com

-b 使用指定的 cookie

curl -b cookie.txt http://www.example.com

-e Referrer (若該網站限制需先訪問網站首頁才能訪問下載頁)

curl -e "www.example.com" http://download.example.com

支援 Regex

curl -O http://www.example.com/img0[1-9].jpg

-o 指定下載的檔案名稱

curl -o test.mp4 http://www.example.com/static/test-3841.mp4

-u 指定帳號密碼

curl -u name:passwd ftp://ftp.example.com:13688/path

相當於 $ curl ftp://name:passwd@ftp.example.com:13688/path

-d POST

curl -d "user=robin&password=1234" http://example.com/update_user

-T PUT

curl -X PUT -d "payload" http://localhost
curl -T test.mp4 http://example.com/update_video

-d @{file_path} body referred to file

curl -X PUT -d @/tmp/test.log http://example.com/update_user
curl -X POST -d @/tmp/test.log http://example.com/update_user