Vagrant+ChefでMySQL+Ryby2環境構築

1. 前提

VirtualBoxのインストール
 https://www.virtualbox.org/wiki/Downloads
Vagrantのインストール
 http://www.vagrantup.com/downloads.html
Vagrantのインストール確認

$ vagrant -v
Vagrant 1.4.3

RubyバージョンとGem確認

$ ruby -v
ruby 2.0.0p247 (2013-06-27 revision 41674) [universal.x86_64-darwin13]
$ gem list | grep chef
chef (11.8.2)
$ gem list | grep knife
knife-solo (0.4.1)
$ gem list | grep berks
berkshelf (2.0.13)
2. セットアップ
$ knife solo init sandbox-chef-cookbooks 
$ cd sandbox-chef-cookbooks 
$ berks init
3. site-cookbooksの作成

・iptablesの設定をするcookbookeを作成する

$ knife cookbook create site_simple_iptables -o site-cookbooks/

・設定

$ vim site-cookbooks/site_simple_iptables/recipes/default.rb

-- default.rb
# Reject packets other than those explicitly allowed
simple_iptables_policy "INPUT" do
  policy "DROP"
end

# The following rules define a "system" chain; chains
# are used as a convenient way of grouping rules together,
# for logical organization.

# Allow all traffic on the loopback device
simple_iptables_rule "system" do
  rule "--in-interface lo"
  jump "ACCEPT"
end

# Allow any established connections to continue, even
# if they would be in violation of other rules.
simple_iptables_rule "system" do
  rule "-m conntrack --ctstate ESTABLISHED,RELATED"
  jump "ACCEPT"
end

# Allow SSH
simple_iptables_rule "system" do
  rule "--proto tcp --dport 22"
  jump "ACCEPT"
end

# Allow HTTP, HTTPS
simple_iptables_rule "http" do
  rule [ "--proto tcp --dport 80",
         "--proto tcp --dport 443" ]
  jump "ACCEPT"
end

# Allow MySQL
simple_iptables_rule "mysql" do
  rule "--proto tcp --dport 3306"
  jump "ACCEPT"
end

# Allow Rails
simple_iptables_rule "mysql" do
  rule "--proto tcp --dport 3000"
  jump "ACCEPT"
end
4. Berksfile

サードパーティのcookbook(iptable + Ruby + MySQL)を使う設定を追加する

$ vim Berksfile

site :opscode
cookbook 'simple_iptables', git:"git://github.com/dcrosta/cookbook-simple-iptables.git"
cookbook 'site_simple_iptables', path: './site-cookbooks/site_simple_iptables'
cookbook 'ruby_build'
cookbook 'rbenv', github: "fnichol/chef-rbenv"
cookbook 'mysql'

・iptable設定のcookboosとサードパーティのcookbookをインストール

$ berks install --path cookbooks
5. Vagrantfile

mysqlパスワード、rubyバージョンの設定

    chef.json = {
        :mysql => {
            :server_root_password => 'rootpass',
            :server_debian_password => 'debpass',
            :server_repl_password => 'replpass'
        },
        :rbenv => {
            :user_installs => [{
                                   :user => "vagrant",
                                   :rubies => ["2.0.0-p353"],
                                   :global => "2.0.0-p353",
                                   :gems => {
                                       "2.0.0-p353" => [
                                           {:name => "bundler"}
                                       ]
                                   }
                               }]
        }
    }

vagrantの起動

$ vagrant up

OmniAuth + DeviceでTwitter, Facebook連携

0. やりたいこと

Railsアプリでコンテンツ登録時に
Twitterのタイムラインに拡散する
FacebookFacebookページに拡散する

1. 前提

・deviceでユーザー登録機能が実装されている
 Rails 4.0.0.beta1でdevise - t-taira blog
Twitter Developersに登録している
 Twitter Developers
Facebook Developersに登録している
 Home - Facebook Developers

2. 設定関連

・GemFile

# twitter
gem 'twitter'
gem 'omniauth-twitter'

# facebook
gem 'fb_graph'
gem 'omniauth-facebook'

gem 'activerecord-session_store'

・config/routes

  devise_for :users, :controllers => { :omniauth_callbacks => "users/omniauth_callbacks" } do
    delete '/users/disconnect/:provider',
      :to => 'users#disconnect_omniauth_provider',
      :as => 'disconnect_omniauth_provider'
  end
  resources :users

・config/initializers/devise.rb

# ココ追加
config.omniauth :twitter, 'CONSUMER_KEY', 'CONSUMER_SECRET', :display => 'popup'
config.omniauth :facebook, 'APP_ID', 'APP_SECRET', :scope => 'manage_pages', :display => 'popup'
  
DeviseController.class_eval do
  before_action :resource_params #, only: [:show, :edit, :update, :destroy]

  def resource_params
    unless params[resource_name].blank?
      params.require(resource_name).permit(
	    # ココ追加
        :twitter_id, :twitter_token, :twitter_secret,
        :facebook_id, :facebook_token, :facebook_secret,

    ・・・
      )
    end
  end
end

3. Model

・app/models/user

class User < ActiveRecord::Base
  devise :database_authenticatable, :registerable,
    :recoverable, :rememberable, :trackable, :validatable, :confirmable,
    :omniauthable, :omniauth_providers => [:facebook, :twitter] # ココ追加

  # 連携済みかどうか
  def connected?(provider)
    return eval("self.#{provider}_id.nil?")
  end

  # 以下、コールバックで呼ばれるメソッド追加
  def self.find_for_facebook_oauth(access_token, signed_in_resource = nil)
    data = access_token.extra.raw_info
    User.where(:facebook_id => data.id).first
  end

  def self.find_for_twitter_oauth(access_token, signed_in_resource = nil)
    data = access_token
    User.where(:twitter_id => data.uid).first
  end

  def connect_with provider, authdata
    logger.info("AUTH: #{authdata.inspect}")

    case provider
    when :twitter
      self.twitter_id = authdata['uid']
      self.twitter_token = authdata['credentials']['token']
      self.twitter_secret = authdata['credentials']['secret']
    when :facebook
      self.facebook_id = authdata['uid']
      self.facebook_token = authdata['credentials']['token']
    end
    save
  end
end	
4. Controller

・app/controllers/users/omniauth_callbacks_controller.rb

class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def facebook
    authorize :facebook do
      session["devise.facebook_data"] = request.env["omniauth.auth"]
    end
  end

  def twitter
    authorize :twitter do
      # http://stackoverflow.com/questions/7117200/devise-for-twitter-cookie-overflow-error
      session["devise.twitter_data"] = request.env["omniauth.auth"].except('extra')
    end
  end

  private
  def authorize provider
    @user = User.send "find_for_#{provider}_oauth", request.env["omniauth.auth"], current_user

    provider_name = provider.to_s.titleize

    # if current_user exists:
    # if @user is nil, connecting the account
    # if @user is not nil and not equal to current_user, show an error
    # else, see below:
    if current_user
      if @user.nil?
        current_user.connect_with provider, request.env['omniauth.auth']

        flash[:notice] = I18n.t "devise.omniauth_callbacks.success", :kind => provider_name
      elsif @user != current_user
        # TODO: I18n
        flash[:error] = "That #{provider_name} account is already linked to a different user."
      else
        flash[:notice] = "Your account is already linked with the #{provider_name} account."
      end
      redirect_to edit_user_registration_path
    else
      if @user
        flash[:notice] = I18n.t "devise.omniauth_callbacks.success", :kind => provider.to_s.titleize
        sign_in_and_redirect @user, :event => :authentication
      else
        yield if block_given?
        redirect_to new_user_registration_url
      end
    end
  end

end

・app/controllers/users_controller.rb

class UsersController < ApplicationController

 # 連携を解除するメソッド
 def disconnect_omniauth_provider
    provider = params[:provider]
    if Devise.omniauth_providers.include? provider.to_sym
      current_user.update_attributes(
        {
          "#{provider}_id" =>  nil,
          "#{provider}_token" =>  nil,
          "#{provider}_secret" =>  nil,          
        }
      )
      current_user.save

      flash[:success] = "Your account has been disconnected from #{provider.titleize}."
    end
    redirect_to edit_user_registration_path
  end
  
  ・・・
end  
5. View

・app/views/layouts/application.html.erb

<div class="well sidebar-nav">
  <p class="nav-header">SNS連携</p>

  <ul class="nav nav-tabs nav-stacked">
    <% Devise.omniauth_providers.each do |provider| %>

      <% if current_user.connected?(provider) %>
        <li>
          <%= link_to image_tag("authbuttons/#{provider.to_s}_32.png") + " 認証する",
            user_omniauth_authorize_path(provider) %>
        </li>
      <% else %>
        <li>
          <%= link_to image_tag("authbuttons/#{provider.to_s}_32.png") + "  解除する",
            disconnect_omniauth_provider_path(provider), :method => :delete %>
        </li>
      <% end %>
    <% end %>
  </ul>
</div>
6. 連携

Twitter

Twitter.configure do |config|
  config.consumer_key       = CONSUMER_KEY
  config.consumer_secret    = CONSUMER_SECRET
  # oauth_token, oauth_token_secretは連携時に取得した値を利用
  config.oauth_token        = current_user.twitter_token 
  config.oauth_token_secret = current_user.twitter_secret
end

twitter_client = Twitter::Client.new
# ココはRailsアプリで登録したコンテンツにする
twitter_client.update("hogehoge")

Facebook

# facebook_tokenは連携時に取得した値を利用
me = FbGraph::User.me(current_user.facebook_token)
# me.accounts[0]で最新のファンページを取得しているが...
# 本当はファンページが複数ある場合は、指定したい
page_access_token = me.accounts[0].access_token

me = FbGraph::User.me(page_access_token)
me.feed!(
 # # ココはRailsアプリで登録したコンテンツにする
  :message => 'hellow world!',
  :link => 'https://www.google.co.jp/')

Source: OmniAuth: Overview · plataformatec/devise Wiki · GitHub

Railsで「もっと見る」の実装

1. Gemfile

amatsuda/kaminari · GitHub

gem 'kaminari'
2. Model

・ app/models/item.rb

class Item < ActiveRecord::Base
  paginates_per 50
  default_scope :order => 'created_at DESC'
  
end  
3. View

・ app/views/items/_items.html.erb

<% @items.each do |item| %>
  <div><%= item.name %></div>
<% end %>

・ app/views/items/index.html.erb

<div id="items">
  <%= render 'items' %>
</div>

<%= link_to_next_page @items, 'more', remote: true, id: 'more_link' %>

・ app/views/items/index.js.erb

$('#items').append("<%= escape_javascript(render 'items', object: @items) %>")
$("#more_link").replaceWith("<%= escape_javascript(
  link_to_next_page(@items, 'more', remote: true, id: 'more_link')
) %>");
4. Controller

・ app/controller/items_controller.rb

class ItemsController < ApplicationController
  def index
    @items = Item.page params[:page]
  end
end

Source: Rails 3 - "More" ajax pagination with Kaminari - Stack Overflow

2013 PHPのはまりどころ・オブ・ザ・イヤー

cakePHPにて、SQLでレコード取得
$sql = 'select * from table'

// これはキャッシュされる
$Model->query($sql);

// キャッシュしたくないときは
$Model->query($sql, false);
・先月の取得
// 3/29にこうすると
date('Y/m', strtotime(' -1 month'));
=> '2013/03'

// 起算日を指定する
date('Y/m', strtotime(date('Y-m-1').' -1 month'));
=> '2013/02'

Source: PHPで、先月、翌月などを扱うときの注意 - bushimichiの日記

Rails 4.0.0.beta1でcapistrano

とりあえず、gemが対応されるまでこれで凌ごうかと。

1. Gemfile
gem 'capistrano', group: :development
2. cap deployでエラー... orz
cp: cannot stat `/var/www/sandbox/shared/assets/manifest.yml': No such file or directory
3. 原因

Rails 4 support · Issue #362 · capistrano/capistrano · GitHub

Rails 4 store manifest in JSON file like manifest-effbd5715fcc7c1600baa48c1bc283ee.json, not in manifest.yml. So right now we can’t deploy Rails 4 application.
4. config/deploy.rb に以下を追加
namespace :deploy do
  namespace :assets do
    task :precompile, :roles => assets_role, :except => { :no_release => true } do
      run <<-CMD.compact
        cd -- #{latest_release.shellescape} &&
        #{rake} RAILS_ENV=#{rails_env.to_s.shellescape} #{asset_env} assets:precompile
      CMD
    end
  end
end

Source: Rails 4 support · Issue #362 · capistrano/capistrano · GitHub

Rails 4.0.0.beta1でdevise

とりあえず、gemが対応されるまでこれで凌ごうかと。

1. Gemfile
gem 'devise', github: 'plataformatec/devise', branch: 'rails4'
2. エラー... orz
`attr_accessible': `attr_accessible` is extracted out of Rails into a gem.
 Please use new recommended protection model for params(strong_parameters) or
 add `protected_attributes` to your Gemfile to use old one. (RuntimeError)
    from /home/leapfrog/projects/kathloc/app/models/user.rb:8:in `<class:User>'
3. config/initializers/devise.rb に以下を追加
DeviseController.class_eval do
  before_action :resource_params #, only: [:show, :edit, :update, :destroy]

  def resource_params
    unless params[resource_name].blank?
      params.require(resource_name).permit(:email, :password, 
        :password_confirmation, :remember_me, :reset_password_token,
        :current_password)
    end
  end
end

Source: Rails 4 Authentication - Stack Overflow