Software engineering notes

Rails Third-party Gems

will_paginate

提供分頁功能

will_paginate 如何使用

controller : posts/index

@posts = Post.includes(:user)
改成
@posts = Post.includes(:user).paginate(page: params[:page], per_page: 5)

view : posts/index

<% @posts.each do |post| %>
  <%= post.title %>
<% end %>
<%= will_paginate @posts %>

will_paginate not working

如果傳入的是 ActiveRecord_Associations_CollectionProxy 而不是 ActiveRecord_Relation 可能會造成此狀況,在 controller 要額外引入

require 'will_paginate/array'

bootstrap 樣式

view :

<%= will_paginate @posts, :renderer => PaginationLinkRenderer %>

config/initializers/pagination_link_renderer.rb :

require 'will_paginate/view_helpers/link_renderer'
require 'will_paginate/view_helpers/action_view'

class PaginationLinkRenderer < ::WillPaginate::ActionView::LinkRenderer
  protected
  def page_number(page)
    unless page == current_page
      tag(:li, link(page, page, :rel => rel_value(page)))
    else
      tag(:li, link(page, '#', :rel => rel_value(page)), :class => "active disabled")
    end
  end

  def gap
    tag(:li, link('...' , '#') , :class => "disabled")
  end

  def previous_or_next_page(page, text, classname)
    tag(:li, link(text , page || '#'), :class => page ? classname : classname + ' disabled')
  end

  def html_container(html)
    tag(:div , tag(:ul, html , container_attributes) , :class => 'pagination_label col-xs-12 center')
  end
end

小技巧 - 簡化 view 的分頁

在 controller 多指定變數給 @paginate

@users = @paginate = User.all.paginate(:page => params[:page] , :per_page => 15)

不必擔心 @users@paginate 佔兩份記憶體, 它們都指向同一個 object_id

view/layout

<%= raw(will_paginate(@paginate ,:renderer => PaginationLinkRenderer)) if @paginate %>

在 layout 放這段, 讓 renderer 覆寫掉原生的 gem (寫在 initializers 裡, 可參考本篇 bootstrap 那邊寫法), 就不用每一個 view 都要寫 will_paginate

之後只要在需要分頁的 view 直接 <%= @paginate %> 就好

其他

取得總數

@products.total_entries

FB Oauth

如果使用 Devise,請不要用下列的方法,請到這篇搜尋 oauth-facebook

申請 FB APP

  1. Developers

  2. Add a New App 選擇 Website (網站)

  3. 建立 App -> 輸入一個 Name -> 按步驟 -> Create a New App ID

  4. 建立成功, 下面有個欄位輸入 Domain : http://test.example.com:3000/

如果沒有 domain 可以暫時用 ngrok,但注意!FB 登入時要用 http://e2191881.ngrok.io/ 而不是 http://127.0.0.1:3000/

  1. 右上角大頭像的 My Apps 選剛剛建立的 App

  2. 將 App ID 及 App Secret Copy 貼到安裝完 omniauth 的設定檔 config/initializers/omniauth.rb

如果日後要換 domain 一樣在 App Dashboard 頁面, 在 App ID 下面那塊, 選擇 Choose a Platform -> WWW -> 就可以改 domain 了

Install

Gemfile

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

安裝完重啟動 rails

新增 User model

rails g model User provider uid name oauth_token oauth_expires_at:datetime

或直接建立

class CreateUsers < ActiveRecord::Migration
  def change
    create_table :users do |t|
      t.string :provider
      t.string :uid
      t.string :name
      t.string :oauth_token
      t.datetime :oauth_expires_at

      t.timestamps null: false
    end
  end
end

app/models/user.rb

def self.from_omniauth(auth)
  where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
    user.provider = auth.provider
    user.uid = auth.uid
    user.name = auth.info.name
    user.oauth_token = auth.credentials.token
    user.oauth_expires_at = Time.at(auth.credentials.expires_at)
    user.save!
  end
end

寫入後資料如下

:id => 1,
:provider => "facebook",
:uid => "860********7924",
:name => "Test Account",
:oauth_token => "CAAF5japY********************n7hjii",
:oauth_expires_at => Sat, 10 Oct 2015 14:26:09 UTC +00:00,

新增 Sessions Controller

app/controllers/application_controller.rb

private
def current_user
    @current_user ||= User.find(session[:user_id]) if session[:user_id]
end
helper_method :current_user

app/controllers/sessions_controller.rb

class SessionsController < ApplicationController
  def create
    user = User.from_omniauth(env["omniauth.auth"])
    session[:user_id] = user.id
    redirect_to root_url
  end

  def destroy
    session[:user_id] = nil
    redirect_to root_url
  end
end

新增 routes

config/routes

match 'auth/:provider/callback', to: 'sessions#create', via: [:get, :post]
match 'auth/failure', to: redirect('/'), via: [:get, :post]
match 'signout', to: 'sessions#destroy', as: 'signout', via: [:get, :post]

手動建立 Omniauth initializer

config/initializers/omniauth.rb

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :facebook, '41512******2503', '1791f7944e*********7ebaa65989414'
end

登入 / 登出

app/views/welcome/index.html.erb

<div id="user-widget">
  <% if current_user %>
    <%= link_to "Sign out", signout_path, id: "sign_out" %>
  <% else %>
    <%= link_to "Sign in with Facebook", "/auth/facebook", id: "sign_in" %>
  <% end %>
</div>

(選項, 可不加) JS :

$(document).ready(function () {

    $('body').prepend('<div id="fb-root"></div>')

      $.ajax
        url: "#{window.location.protocol}//connect.facebook.net/en_US/all.js"
        dataType: 'script'
        cache: true


    window.fbAsyncInit = ->
      FB.init(appId: 'YOUR-APP-ID', cookie: true)

      $('#sign_in').click (e) ->
        e.preventDefault()
        FB.login (response) ->
          window.location = '/auth/facebook/callback' if response.authResponse

      $('#sign_out').click (e) ->
        FB.getLoginStatus (response) ->
          FB.logout() if response.authResponse
        true
});

ref : https://coderwall.com/p/bsfitw/ruby-on-rails-4-authentication-with-facebook-and-omniauth

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,

  1. 寫入 cron

    whenever -w

或 update

whenever -i

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

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

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

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

# End Whenever generated tasks for: /Users/test/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 並且提交