2016-09-20

初心者がRails+DeviseでTodoアプリを作ってみた【Herokuにデプロイするまで】

2番煎じどころか100番煎じくらいだと思いますが、Ruby on Rails+Deviseで認証機能付きTodoアプリを作ってみました。

今回やることは以下です。

Rubyのバージョンは2.2.4、Railsは5.0.0.1です。

Railsアプリのプロジェクト作成

雛形となるプロジェクトを以下のコマンドで作成(rails newはジェネレータなコマンド)
$ rails new {APP_NAME} --skip-bundle
$ cd {APP_NAME}

Railsにユーザ認証機能を追加してくれるDevise、環境変数を良い感じに操作できるDotEnv、PostgreSQL用のライブラリpgをGemfileに追加してインストール

group :development, :test do
  gem 'dotenv-rails' 
  gem 'sqlite3' # development,testだけで利用されるように変更
end

gem 'devise'
gem 'pg'

パス指定してインストール(システムのgemに影響を与えない)

$ bundle install --path=vendor/bundle

Deviseのインストール&設定

Devise用のファイルをジェネレータで作成
$ rails g devise:install

表示される注記の通りに変更する

config/environments/development.rbは以下を追加。アクティベーションのリンク等、認証系のメールに記載されるURLを設定しています。

config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

ホーム画面のコントローラ(home)+メソッド(index)を作成してルーティングを追加

$ rails g controller home index

config/routes.rbに以下を追記

root to: "home#index"

全ビューに適用されるテンプレートに、flash messageを追加

<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>

Devise用のビューをカスタマイズするために、Deviseのviewファイルをジェネレータで作成

$ rails g devise:views

Devise用のユーザモデルを作成

$ rails g devise User

Deviseの機能をフルで使うので、コメントアウトを解除

class DeviseCreateUsers < ActiveRecord::Migration[5.0]
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at

      ## Trackable
      t.integer  :sign_in_count, default: 0, null: false
      t.datetime :current_sign_in_at
      t.datetime :last_sign_in_at
      t.string   :current_sign_in_ip
      t.string   :last_sign_in_ip

      ## Confirmable
      t.string   :confirmation_token
      t.datetime :confirmed_at
      t.datetime :confirmation_sent_at
      t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
      t.string   :unlock_token # Only if unlock strategy is :email or :both
      t.datetime :locked_at


      t.timestamps null: false
    end

    add_index :users, :email,                unique: true
    add_index :users, :reset_password_token, unique: true
    add_index :users, :confirmation_token,   unique: true
    add_index :users, :unlock_token,         unique: true
  end
end

とりあえず、database.ymlはsqlite3のままでマイグレーション

$ bundle exec rake db:migrate

Userモデルのコメントアウトも解除しておく

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable,
         :confirmable, :lockable, :timeoutable # , :omniauthable
end

sqlite3の確認はDB Browser for SQLiteを使うと良い

ルーティングの確認は以下のコマンドで

$ rails routes

出力こんな感じ。色々とできています。

Prefix Verb   URI Pattern                       Controller#Action
        new_user_session GET    /users/sign_in(.:format)          devise/sessions#new
            user_session POST   /users/sign_in(.:format)          devise/sessions#create
    destroy_user_session DELETE /users/sign_out(.:format)         devise/sessions#destroy
           user_password POST   /users/password(.:format)         devise/passwords#create
       new_user_password GET    /users/password/new(.:format)     devise/passwords#new
      edit_user_password GET    /users/password/edit(.:format)    devise/passwords#edit
                         PATCH  /users/password(.:format)         devise/passwords#update
                         PUT    /users/password(.:format)         devise/passwords#update
cancel_user_registration GET    /users/cancel(.:format)           devise/registrations#cancel
       user_registration POST   /users(.:format)                  devise/registrations#create
   new_user_registration GET    /users/sign_up(.:format)          devise/registrations#new
  edit_user_registration GET    /users/edit(.:format)             devise/registrations#edit
                         PATCH  /users(.:format)                  devise/registrations#update
                         PUT    /users(.:format)                  devise/registrations#update
                         DELETE /users(.:format)                  devise/registrations#destroy
       user_confirmation POST   /users/confirmation(.:format)     devise/confirmations#create
   new_user_confirmation GET    /users/confirmation/new(.:format) devise/confirmations#new
                         GET    /users/confirmation(.:format)     devise/confirmations#show
             user_unlock POST   /users/unlock(.:format)           devise/unlocks#create
         new_user_unlock GET    /users/unlock/new(.:format)       devise/unlocks#new
                         GET    /users/unlock(.:format)           devise/unlocks#show
              home_index GET    /home/index(.:format)             home#index
                    root GET    /                                 home#index

この時点でDevise部分は確認可能なので、一旦Appサーバ立ち上げて確認

$ rails s

http://localhost:3000/users/sign_upでサインアップできることを確認する

<% if user_signed_in? %>
<p>
  <%= link_to 'ログアウト', destroy_user_session_path, method: :delete %> |
  <%= link_to 'ユーザ情報変更', edit_user_registration_path %>
</p>
<p>Your are <%= current_user.email %></p>
<% else %>
<p>
  <%= link_to 'ユーザ登録', new_user_registration_path %> |
  <%= link_to 'ログイン', new_user_session_path %> |
  <%= link_to 'パスワード変更', new_user_password_path %>
</p>
<% end %>

タスク用CRUD画面の作成

まずはモデルを作成
$ rails g model task

マイグレーションファイルはこんな感じで。Userモデルと紐付けて管理するので、user_idをIntegerで定義しています。

class CreateTasks < ActiveRecord::Migration[5.0]
  def change
    create_table :tasks do |t|
      t.string :title, null: false
      t.text :description, null: true
      t.datetime :target_at, null: true
      t.datetime :completed_at, null: true
      t.boolean :completed, null: false, default: false
      t.integer :user_id, null: true

      t.timestamps
    end
  end
end

項目定義したのち、マイグレーション

$ bundle exec rake db:migrate

Userモデルクラス、Taskモデルクラスにアソシエーションの設定をする

class User < ApplicationRecord
  has_many :tasks, dependent: :destroy

  # Include default devise modules. Others available are:
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable,
         :confirmable, :lockable, :timeoutable # , :omniauthable
end

Taskモデル↓

class Task < ApplicationRecord
  belongs_to :user
end

次にタスクCRUD画面のコントローラ+メソッドを作成

$ rails g controller tasks index show new edit

中身はこんな感じで。ユーザに紐づくTaskを取得したり、before_actionでDeviseの認証ヘルパーを噛ませてます。

class TasksController < ApplicationController
  before_action :authenticate_user!

  def index
    @tasks = current_user.tasks
  end

  def show
    @task = target_task params[:id]
  end

  def new
    @task = Task.new
  end

  def create
    @task = current_user.tasks.new task_params
    @task.save!
    redirect_to @task
  end

  def edit
    @task = target_task params[:id]
  end

  def update
    @task = target_task params[:id]
    @task.update(task_params)
    redirect_to @task
  end

  def destroy
    @task = target_task params[:id]
    @task.destroy
    redirect_to tasks_url
  end

  private
  def target_task task_id
    current_user.tasks.where(id: task_id).take
  end

  def task_params
    params.require(:task).permit(:title, :description)
  end
end

ビューはこんな感じで

app/views/tasks/index.html.erb

<h1>Tasks#index</h1>
<p>Find me in app/views/tasks/index.html.erb</p>
<table>
  <tr>
    <th>Id</th><th>Title</th><th>Description</th><th></th><th></th><th></th>
  </tr>
  <% @tasks.each do |task| %>
  <tr>
    <td><%= task.id %></td>
    <td><%= task.title %></td>
    <td><%= task.description %></td>
    <td><%= link_to 'Show', task_path(task) %></td>
    <td><%= link_to 'Edit', edit_task_path(task) %></td>
    <td><%= link_to 'Delete', task_path(task), method: :delete %></td>
  </tr>
  <% end %>
</table>
<p><%= link_to 'ToDoの作成', new_task_path %></p>

app/views/tasks/edit.html.erb

<h1>Tasks#edit</h1>
<p>Find me in app/views/tasks/edit.html.erb</p>
<div style="width: 300px;">
  <%= form_for(@task) do |f| %>
    <div>
      <%= f.label :title %>
      <%= f.text_field :title, size: 50 %>
    </div>
    <div>
      <%= f.label :description %>
      <%= f.text_area :description %>
    </div>
    <%= f.submit 'Submit' %>
  <% end %>
</div>
<p><%= link_to '一覧に戻る', tasks_url %></p>

app/views/tasks/new.html.erb

<h1>Tasks#new</h1>
<p>Find me in app/views/tasks/new.html.erb</p>
<div style="width: 300px;">
  <%= form_for(@task) do |f| %>
  <div>
    <%= f.label :title %>
    <%= f.text_field :title, size: 50 %>
  </div>
  <div>
    <%= f.label :description %>
    <%= f.text_area :description %>
  </div>
    <%= f.submit 'Submit' %>
  <% end %>
</div>
<p><%= link_to '一覧に戻る', tasks_url %></p>

app/views/tasks/show.html.erb

<h1>Tasks#show</h1>
<p>Find me in app/views/tasks/show.html.erb</p>
<table>
  <tr>
    <th>Title</th>
    <td><%= @task.title %></td></tr>
  <tr>
    <th>Description</th>
    <td><%= @task.description %></td>
  </tr>
  <tr>
    <th>User.Email</th>
    <td><%= @task.user.email %></td>
  </tr>
</table>
<p><%= link_to '一覧に戻る', tasks_url %></p>

config/routes.rbに以下を追加

resources :tasks

そうすると以下のようなルーティングが自動的に実装される

     tasks GET    /tasks(.:format)                  tasks#index
           POST   /tasks(.:format)                  tasks#create
  new_task GET    /tasks/new(.:format)              tasks#new
 edit_task GET    /tasks/:id/edit(.:format)         tasks#edit
      task GET    /tasks/:id(.:format)              tasks#show
           PATCH  /tasks/:id(.:format)              tasks#update
           PUT    /tasks/:id(.:format)              tasks#update
           DELETE /tasks/:id(.:format)              tasks#destroy

本番環境用にconfig/database.ymlを設定(Herokuにデプロイする場合は、環境変数DATABASE_URLを見てくれるのでクレデンシャルの設定は環境変数に任せればOK)

...
production:
  adapter: postgresql
  encoding: unicode
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

Herokuのアプリ作成+設定

$ git init
$ echo "vendor" >> .gitignore
$ heroku login
$ heroku apps:create {HEROKU_APP_NAME}
$ heroku addons:create sendgrid:starter
$ heroku addons:create heroku-postgresql:hobby-dev
$ heroku config:set HEROKU_APP_DOMAIN={HEROKU_APP_NAME}.herokuapp.com

config/environments/production.rbに以下を追加

config.action_mailer.default_url_options = { :host => ENV['HEROKU_APP_DOMAIN']}

SendGridのメール設定

以下のコマンドでSendGridのHerokuの環境変数(=Addonで割り当てられたSendGridのクレデンシャル)を抽出する。

$ heroku config | grep SENDGRID

config/environment.rbに以下を追記

ActionMailer::Base.smtp_settings = {
    :address        => 'smtp.sendgrid.net',
    :port           => '587',
    :authentication => :plain,
    :user_name      => ENV['SENDGRID_USERNAME'],
    :password       => ENV['SENDGRID_PASSWORD'],
    :domain         => 'heroku.com',
    :enable_starttls_auto => true
}

ローカル開発でもSENDGRID使う場合は以下のように.envファイルをアプリケーションのルートに設置

SENDGRID_USERNAME={SENDGRID_USERNAME}
SENDGRID_PASSWORD={SENDGRID_PASSWORD}

Herokuにデプロイ

$ git add .
$ git commit -m "first commit"
$ git push heroku master # heroku apps:createの時点でremoteリポジトリに追加されている
$ heroku run rake db:migrate

ハマりの備忘

【事象】以下のsign_outのリンククリックでActionController::InvalidAuthenticityTokenのエラーになる
<%= link_to "ログアウト", destroy_user_session_path, method: :delete %>

【原因】csrfのメタタグが入っていないだけだった(なぜか削除していた)

<%= csrf_meta_tag %>

method: :deleteを設定するとjQueryでAjax通信するが、そのなかでmeta[]要素を取得できずエラーになっていた

このエントリーをはてなブックマークに追加