Rails 開発環境を構築するために Chef を使って PostgreSQL をインストールするメモ

Rails アプリの開発環境を Vagrant + Chef で構築していて、 PostgreSQL のインストールでつまづいたのでメモ。

まず Berkshelf で postgresql のクックブックをダウンロード。

echo cookbook "postgresql" >> Berksfile
berks vendor cookbooks

postgresql クックブックはそのまま使うと、どうしてもエンコーディングUTF-8 を指定してデータベースを作成できなかった。 対策として、postgresql をインポートするクックブックを新規作成する。

knife cookbook create postgresql_server_utf8

デフォルトのレシピを記述。

ENV["LANGUAGE"] = ENV["LANG"] = ENV["LC_ALL"] = "en_US.UTF-8"
include_recipe "postgresql::server"

環境変数をセットしているのがポイント。

あとは Vagrantfile に

VAGRANTFILE_API_VERSION = "2"

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  config.vm.box = "hashicorp/precise64"

  config.vm.provision "chef_solo" do |chef|
    chef.cookbooks_path = ["./chef/cookbooks", "./chef/site-cookbooks"]

    chef.add_recipe "postgresql_server_utf8"
    chef.json = {
      "postgresql" => {
        "password" => {
          "postgres" => "postgres"
        },
        "initdb_locale" => "en_US.UTF-8",
        "config" => {
          "lc_messages" => "en_US.UTF-8",
          "lc_monetary" => "en_US.UTF-8",
          "lc_numeric" => "en_US.UTF-8",
          "lc_time" => "en_US.UTF-8"
        }
      }
    }
  end
end

を書いて、vagrant up/provision を実行すれば、 PostgreSQL をインストールできた。 エンコーディングUTF-8 を指定してデータベースを作成できるようにもなった。

Devise でのパスワード変更を自前で実装する

パスワード変更機能を、Devise が提供するコントローラーを使わずに、自前で実装したいときのためのメモ。

現在のパスワード・新しいパスワード・新しいパスワード(確認用) を入力して変更する場合、 update_with_password を使う。

current_user.update_with_password(
  password: "new_password",
  password_confirmation: "new_password",
  current_password: "old_password"
)

新しいパスワードで強制的に変更する場合、update を使う。 管理者がユーザーのパスワードを再発行するときなんかに使ったりする。

current_user.update(
  password: "new_password",
  password_confirmation: "new_password"
)

注意点としては、現在ログインしているユーザーのパスワードを変更すると、 直後ログアウトした状態になってしまうので、 再ログインする処理が必要。

class SettingsController < ApplicationController
  # ... 省略 ...

  def update
    respond_to do |format|
      if current_user.update_with_password(user_params)
        # パスワードを変更するとログアウトしてしまうので、再ログインが必要
       sign_in(current_user, bypass: true)
        format.html { redirect_to edit_setting_path }
      else
        format.html { render :edit }
      end
    end
  end
end

Draper と kaminari を一緒に使う

デフォルトの状態だと、kaminari が生やしたメソッドを Draper が delegate してくれない。

Draper::CollectionDecorator.delegate :current_page, :total_pages, :limit_value, :total_count

を config/initializers/draper.rb あたりに書いておく必要があった。

kaminari で Bootstrap のテーマを使う

$ bin/rails generate kaminari:views bootstrap

で Boostrap 用の kaminari のビューをダウンロードできる。 ただ、Bootstrap 2.0 のときのテーマなので、最新の 3.2 で使うには修正が必要。

app/views/kaminari/_pagenator.html.erb を次のように修正すれば使えた。

<%= paginator.render do -%>
  <ul class="pagination">
    <%= first_page_tag unless current_page.first? %>
    <%= prev_page_tag unless current_page.first? %>
    <% each_page do |page| -%>
      <% if page.left_outer? || page.right_outer? || page.inside_window? -%>
        <%= page_tag page %>
      <% elsif !page.was_truncated? -%>
        <%= gap_tag %>
      <% end -%>
    <% end -%>
    <%= next_page_tag unless current_page.last? %>
    <%= last_page_tag unless current_page.last? %>
  </ul>
<% end -%>

amatsuda/kaminari_themes には Bootstrap 3 対応のプルリクエストがいくつもあるけど、どれもマージされていない。まぁ、これくらいの修正で使えるからいいけど。

Devise でメールアドレスではなくユーザー名とパスワードでサインインできるようにする方法メモ

Devise ではメールアドレスとパスワードを使ってサインインするのがデフォルト。

メールアドレスではなくユーザー名、それとパスワードでサインインしたい場合はどうすればいいか? Devise が呼び出すモデルとコントローラーのメソッドを上書きするしかない。

やったことをメモしておく。

ユーザー名を保存する列を追加

すでに users テーブルを作っていたので、users テーブルに列を追加する。

$ rails generate migration AddUsernameToUsers

マイグレーションファイルの中身は次の通り。

class AddUsernameToUsers < ActiveRecord::Migration
  def change
    add_column :users, :username, :string, null: false, default: ""

    add_index :users, :username, unique: true
  end
end

あとはマイグレーションを実行。

認証のキーにユーザー名を使えるようにモデルを修正

ユーザー名を使ってサインインするのは User だけなので、 User クラスで認証に使うキーを指定する。

class User < ActiveRecord::Base
  # 認証で使うキーを指定
  devise :database_authenticatable, :trackable, :validatable,
    authentication_keys: [:username]

  # ユーザー名で検索
  def self.find_first_by_auth_conditions(warden_conditions)
    conditions = warden_conditions.dup
    if username = conditions.delete(:username)
      where(conditions).where(username: username).first
    else
      where(conditions).first
    end
  end

  # 登録時に email を不要にする
  def email_required?
    false
  end
  def email_changed?
    false
  end
end

ユーザー名を認証時のパラメータとして使えるようにコントローラーを修正

ユーザー名が Strong Parameters ではじかれないように対策が必要。

class ApplicationController < ActionController::Base
  before_filter :configure_permitted_parameters, if: :devise_controller?

  # ...省略...

  protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.for(:sign_up) { |u| u.permit(:subdomain, :username, :email, :password, :password_confirmation) }
    devise_parameter_sanitizer.for(:sign_in) { |u| u.permit(:subdomain, :username, :email, :password, :remember_me) }
  end
end

ユーザー名を入力できるようにカスタムビューを作成

ユーザー名とパスワードでサインインするのは User だけなので、 異なるビューを使えるように initializers/devise.rb で設定する。

Devise.setup do |config|
  # ...省略...

  config.scoped_views = true
end

ビューを生成。

$ rails generate devise:view users

app/views/users/sessions/new.html.erb を修正し、ユーザー名とパスワードを入力できるようにする。

<%= form_for(resource,
             as: resource_name,
             url: session_path(resource_name),
             html: { class: "form-signin", role: "form" }) do |f| %>
  <h2 class="form-signin-heading">
    サインイン
  </h2>

  <%= f.text_field :username,
    class: "form-control",
    placeholder: "ユーザー名" %>

  <%= f.password_field :password,
    autocomplete: "off",
    class: "form-control",
    placeholder: "パスワード" %>

  <%= f.submit "サインイン",
    data: { disable_with: "サインイン中..." },
    class: "btn btn-lg btn-primary btn-block" %>
<% end %>

なお、今回作っているアプリは「ユーザー登録を行うのは管理者ユーザーのみ」という仕様だったので、 ユーザー名とパスワードでのサインアップは省いた。

Rails のベストプラクティスに従っているかチェックするために rails_best_practices を導入

Rails の使い方が間違っていないかどうかを、人が目視でチェックするのは、 無駄ではないけど時間がもったいない。

もしツールでチェックできるとしたら、ツールでやった方がいい。 設計とか、他にもっとレビューするべきところはあるのだから。

そこで rails_best_practices という gem の出番。

この gem は、Rails Best Practices というサイトで公開されているベストプラクティスに、 Rails アプリが従っているかをチェックしてくれる。

gem install rails_best_practices

でインストールしたら、Rails プロジェクトのルートディレクトリで

rails_best_practices -f html .

を実行。すると rails_best_practices_output.html が出力される。 こんな感じのやつ。

f:id:griefworker:20140716205020p:plain

あとは警告を1つずつ潰していけばいい。

Grape で API を複数ファイルに分けて定義する

Grape では 1 ファイルに API をずらっと書けるけど、 規模が大きくなってくると見通しが悪くなるので、複数ファイルに分割したくなる。

Grape では API が他の API をマウントできるので、 その機能を使ってリソースごとにファイルを分割できる。

以下、サンプル。ヘルパーも別ファイルに切り出してみた。

posts.rb
# coding: utf-8
module API
  class Posts < Grape::API
    resource :posts do
      desc "投稿をすべて取得"
      get do
        # posts はヘルパーメソッド
        posts
      end

      desc "投稿を1件取得"
      get ":id" do
        posts.find(params[:id])
      end
    end
  end
end
helpers.rb
# coding: utf-8
module API
  module APIHelpers
    def posts
      Post.order(:id)
    end
  end
end
api.rb
# coding: utf-8
require_relative "helpers"
require_relative "posts"

module API
  class API < Grape::API
    format :json
    default_format :json
    prefix "api"
    version "v1", using: :path

    helpers APIHelpers
    mount Posts
  end
end