Ruby on Rails チュートリアル第13章をやってみて

#ユーザーのマイクロポスト
■第13章
Twitterのミニクローンを完成させる。

##13.1 Micropostモデル
Micropostリソースの最も本質的な部分を表現するMicropostモデルを作成する。

###13.1.1　基本的なモデル
まずはMicropostモデルを生成。

```
rails generate model Micropost content:text user:references
```

`ApplicationRecord`を継承したモデルが作られる。そして、`belongs_to`のコードも自動的に追加されている。
理由は、`user:references`という引数も含めていたため。

自動生成されたMicropostモデル。

```rb:micropost.rb
class Micropost < ApplicationRecord
   belongs_to :user
end
```

`User`モデルとの最大の違いはreferences型を利用している点。
これを利用すると、自動的にインデックスと外部キー参照つきの`user_id`カラムが追加され、UserとMicropostを関連付けする下準備をしてくれる。

```rb:[timestamp]_create_microposts.rb
class CreateMicroposts < ActiveRecord::Migration[5.1]
  def change
    create_table :microposts do |t|
      t.text :content
      t.references :user, foreign_key: true

      t.timestamps
    end
    add_index :microposts, [:user_id, :created_at]
  end
end
```

`add_index`によって、作成時刻の逆順で取り出しやすくなる。

###13.1.2 Micropostのバリデーション
バリデーションを追加していく。まずは`Micropost`モデル単体を動くようにしてみます。

`setup`では、fixtureのサンプルユーザーと紐づけた新しいマイクロポストを作成している。

次に、マイクロポストが有効であるかどうかをチェックする。

最後に、`user_id`の存在性のバリデーションに対するテストも追加する。

有効性に対するテスト。

```rb:micropost_test.rb
require 'test_helper'

class MicropostTest < ActiveSupport::TestCase

  def setup
    @user = users(:michael)
    # このコードは慣習的に正しくない
    @micropost = Micropost.new(content: "Lorem ipsum", user_id: @user.id)
  end

  test "should be valid" do
    assert @micropost.valid?
  end

  test "user id should be present" do
    @micropost.user_id = nil
    assert_not @micropost.valid?
  end
end
```

`setup`の中は実は慣習的には正しくないが、後で修正する。

1つ目のテストでは、正常な状態かどうかをテストしている。2つ目のテストでは、`user_id`が存在しているかどうかをテストしている。このテストをパスせさるために、存在性のバリデーションを追加してみる。

```rb:micropost.rb
class Micropost < ApplicationRecord
  belongs_to :user
  validates :user_id, presence: true
end
```

次に、`content`属性に対するバリデーションを追加する。140文字より長くないように制限を加える。

テストを作成。

```rb:micropost_test.rb
  test "content should be present" do
    @micropost.content = "   "
    assert_not @micropost.valid?
  end

  test "content should be at most 140 characters" do
    @micropost.content = "a" * 141
    assert_not @micropost.valid?
  end
```

そしてバリデーションを追加。

```rb:micropost.rb
class Micropost < ApplicationRecord
  belongs_to :user
  validates :user_id, presence: true
  validates :content, presence: true, length: { maximum: 140 }
end
```

テストはなぜか手こずりましたが、無事GREENに。

###13.1.3 User/Micropostの関連付け
1つのツイートは1人のユーザーのみ持っていて、1人のユーザーは複数のツイートを持ってる。

```
user.microposts.create
user.microposts.create!
user.microposts.build
```

これらのメソッドは使うと、紐付いているユーザーを通してマイクロポストを作成することができる。

さっき書いた

```rb
@user = users(:michael)
# このコードは慣習的に正しくない
@micropost = Micropost.new(content: "Lorem ipsum", user_id: @user.id)
```

をこんな風に書くことができる。

```rb
@user = users(:michael)
@micropost = @user.microposts.build(content: "Lorem ipsum")
```

`@user.microposts.build`のようなコードを使うためには、 UserモデルとMicropostモデルをそれぞれ更新して、関連付ける必要がある。
Userモデルは`has_many :microposts`と追加する。

```rb:micropost.rb
class Micropost < ApplicationRecord
  belongs_to :user
  validates :user_id, presence: true
  validates :content, presence: true, length: { maximum: 140 }
end
```

```rb:user.rb
class User < ApplicationRecord
  has_many :microposts
  .
  .
  .
end
```

関連付けできたら、`setup`メソッドを修正する。

```rb
  def setup
    @user = users(:michael)
    @micropost = @user.microposts.build(content: "Lorem ipsum")
  end
```

テストも無事GREEN。

###13.1.4 マイクロポストを改良する
UserとMicropostの関連付けを改良していく。

最も新しいマイクロポストを最初に表示できるようにする。これを実装するためには、default scopeというテクニックを使う。

マイクロポストの順序付けをテスト。

```rb:micropost_test.rb
require 'test_helper'

class MicropostTest < ActiveSupport::TestCase
  .
  .
  .
  test "order should be most recent first" do
    assert_equal microposts(:most_recent), Micropost.first
  end
end
```
fixtureファイルも作成しておく。

```html:microposts.yml
orange:
  content: "I just ate an orange!"
  created_at: <%= 10.minutes.ago %>

tau_manifesto:
  content: "Check out the @tauday site by @mhartl: http://tauday.com"
  created_at: <%= 3.years.ago %>

cat_video:
  content: "Sad cats are sad: http://youtu.be/PKffm2uI4dk"
  created_at: <%= 2.hours.ago %>

most_recent:
  content: "Writing a short test"
  created_at: <%= Time.zone.now %>
```

`created_at`カラムは基本的に手動で更新できないが、fixtureファイルの中では更新可能。

試しにテストしてみるとREDに。

古いバージョンのRailsでは、欲しい振る舞いにするためには生のSQLを書くしか選択肢がなかったが、Rails 4.0からは次のようにRubyの文法でも書けるようになったらしい。

デフォルトスコープを変更する書き方が以下のコード。これによって新規作成順に並ぶ。

```rb:micropost.rb
class Micropost < ApplicationRecord
  belongs_to :user
  default_scope -> { order(created_at: :desc) }
  validates :user_id, presence: true
  validates :content, presence: true, length: { maximum: 140 }
end
```

これでテストをやると無事通る。

ユーザーが削除されたらマイクロポストも削除されるようにする方法は、実はかなり簡単。一行追加するだけ。

```rb:user.rb
class User < ApplicationRecord
  has_many :microposts, dependent: :destroy
  .
  .
  .
end
```

`dependent: :destroy`を使うと。持ち主のいないMicropostがデータベース上に残り続けるという問題を解決することができる。

そのテストを追加する。

```rb:user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com",
                     password: "foobar", password_confirmation: "foobar")
  end
  .
  .
  .
  test "associated microposts should be destroyed" do
    @user.save
    @user.microposts.create!(content: "Lorem ipsum")
    assert_difference 'Micropost.count', -1 do
      @user.destroy
    end
  end
end
```

テストもGREENに。

##13.2 マイクロポストを表示する
マイクロポストの表示とテスト部分を作る。

twitterのような独立したマイクロポストの`index`ページは作らずに、ユーザーの`show`ページで直接マイクロポストを表示してみる。

###13.2.1 マイクロポストの描画
一度データベースをリセットし、サンプルデータを再生成しておく。

```
$ rails db:migrate:reset
$ rails db:seed
```

Micropostsコントローラを作っておく。

```
$ rails generate controller Microposts
```

パーシャルを作る。

```html:_micropost.html.erb
<li id="micropost-<%= micropost.id %>">
  <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
  <span class="user"><%= link_to micropost.user.name, micropost.user %></span>
  <span class="content"><%= micropost.content %></span>
  <span class="timestamp">
    Posted <%= time_ago_in_words(micropost.created_at) %> ago.
  </span>
</li>
```

`time_ago_in_words`というヘルパーメソッドで「○分前に投稿」みたいな文字列を出力できる。

ページネーション用に`show`で`@microposts`を作成する。

```rb:users_controller.rb
class UsersController < ApplicationController
  .
  .
  .
  def show
    @user = User.find(params[:id])
    @microposts = @user.microposts.paginate(page: params[:page])
  end
  .
  .
  .
end
```

プロフィール画面にマイクロポストを表示させる。

```html:show.html.erb
<% provide(:title, @user.name) %>
<div class="row">
  <aside class="col-md-4">
    <section class="user_info">
      <h1>
        <%= gravatar_for @user %>
        <%= @user.name %>
      </h1>
    </section>
  </aside>
  <div class="col-md-8">
    <% if @user.microposts.any? %>
      <h3>Microposts (<%= @user.microposts.count %>)</h3>
      <ol class="microposts">
        <%= render @microposts %>
      </ol>
      <%= will_paginate @microposts %>
    <% end %>
  </div>
</div>
```

###13.2.2 マイクロポストのサンプル
データベースに6人分のマイクロポストを作る。

```rb:seeds.rb
.
.
.
users = User.order(:created_at).take(6)
50.times do
  content = Faker::Lorem.sentence(5)
  users.each { |user| user.microposts.create!(content: content) }
end
```

ここでいつものようにサンプルデータを生成。

```
$ rails db:migrate:reset
$ rails db:seed
```

あとはCSSの見た目を整えて終わり。

###13.2.3 プロフィール画面のマイクロポストをテストする
プロフィール画面で表示されるマイクロポストに対して、統合テストを書いていく。統合テストを作成する。

```
$ rails generate integration_test users_profile
```

fixtureを書いていく。

```microposts.yml
orange:
  content: "I just ate an orange!"
  created_at: <%= 10.minutes.ago %>
  user: michael

tau_manifesto:
  content: "Check out the @tauday site by @mhartl: http://tauday.com"
  created_at: <%= 3.years.ago %>
  user: michael

cat_video:
  content: "Sad cats are sad: http://youtu.be/PKffm2uI4dk"
  created_at: <%= 2.hours.ago %>
  user: michael

most_recent:
  content: "Writing a short test"
  created_at: <%= Time.zone.now %>
  user: michael

<% 30.times do |n| %>
micropost_<%= n %>:
  content: <%= Faker::Lorem.sentence(5) %>
  created_at: <%= 42.days.ago %>
  user: michael
<% end %>
```

プロフィール画面にアクセスした後に、ページタイトルとユーザー名、Gravatar、マイクロポストの投稿数、そしてページ分割されたマイクロポスト、といった順でテストしていく。

`full_title`ヘルパーが利用できている。

```rb:users_profile_test.rb
require 'test_helper'

class UsersProfileTest < ActionDispatch::IntegrationTest
  include ApplicationHelper

  def setup
    @user = users(:michael)
  end

  test "profile display" do
    get user_path(@user)
    assert_template 'users/show'
    assert_select 'title', full_title(@user.name)
    assert_select 'h1', text: @user.name
    assert_select 'h1>img.gravatar'
    assert_match @user.microposts.count.to_s, response.body
    assert_select 'div.pagination'
    @user.microposts.paginate(page: 1).each do |micropost|
      assert_match micropost.content, response.body
    end
  end
end
```

```
assert_select 'h1>img.gravatar'
```

このように書くことで、`h1`タグの内側にある、`gravatar`クラス付きの`img`タグがあるかどうかをチェックできる。

テストも通りました。

###13.3 マイクロポストを操作する
データモデリングとマイクロポスト表示テンプレートの両方が完成。次はWeb経由でそれらを作成するためのインターフェイスに取り掛かる。

最後に、ユーザーがマイクロポストをWeb経由で破棄できるようにする。

マイクロポストリソースのルーティング。

```rb:routes.rb
Rails.application.routes.draw do
  root   'static_pages#home'
  get    '/help',    to: 'static_pages#help'
  get    '/about',   to: 'static_pages#about'
  get    '/contact', to: 'static_pages#contact'
  get    '/signup',  to: 'users#new'
  get    '/login',   to: 'sessions#new'
  post   '/login',   to: 'sessions#create'
  delete '/logout',  to: 'sessions#destroy'
  resources :users
  resources :account_activations, only: [:edit]
  resources :password_resets,     only: [:new, :create, :edit, :update]
  resources :microposts,          only: [:create, :destroy]
end
```

###13.3.1 マイクロポストのアクセス制御
関連付けられたユーザーを通してマイクロポストにアクセスするので、`create`アクションや`destroy`アクションをするユーザーはログイン済みでなければならない。

まずはテストを書いていく。

```rb:microposts_controller_test.rb
require 'test_helper'

class MicropostsControllerTest < ActionDispatch::IntegrationTest

  def setup
    @micropost = microposts(:orange)
  end

  test "should redirect create when not logged in" do
    assert_no_difference 'Micropost.count' do
      post microposts_path, params: { micropost: { content: "Lorem ipsum" } }
    end
    assert_redirected_to login_url
  end

  test "should redirect destroy when not logged in" do
    assert_no_difference 'Micropost.count' do
      delete micropost_path(@micropost)
    end
    assert_redirected_to login_url
  end
end
```

このテストをパスするために、少しアプリケーション側のコードをリファクタリングする必要がある。

`logged_in_user`メソッドがMicropostsコントローラでも必要。各コントローラが継承するApplicationコントローラに、このメソッドを移していく。

```rb:application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  include SessionsHelper

  private

    # ユーザーのログインを確認する
    def logged_in_user
      unless logged_in?
        store_location
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end
end
```

重複しないように、Usersコントローラからも`logged_in_user`を削除しておく。これで`logged_in_user`メソッドを呼び出せるようになった。

Micropostsコントローラの各アクションに認可を追加。

```rb:microposts_controller.rb
class MicropostsController < ApplicationController
  before_action :logged_in_user, only: [:create, :destroy]

  def create
  end

  def destroy
  end
end
```


テストも無事通りました。


###13.3.2 マイクロポストを作成する
マイクロポスト作成画面を作っていく。ユーザーのログイン状況に応じて、ホーム画面の表示を変更することを目標にする。

マイクロポストの`create`アクションを作り始める。

```rb:microposts_controller.rb
class MicropostsController < ApplicationController
  before_action :logged_in_user, only: [:create, :destroy]

  def create
    @micropost = current_user.microposts.build(micropost_params)
    if @micropost.save
      flash[:success] = "Micropost created!"
      redirect_to root_url
    else
      render 'static_pages/home'
    end
  end

  def destroy
  end

  private

    def micropost_params
      params.require(:micropost).permit(:content)
    end
end
```

サイト訪問者がログインしているかどうかに応じて異なるHTMLを提供するコードを使う。

```html:home.html.erb
<% if logged_in? %>
  <div class="row">
    <aside class="col-md-4">
      <section class="user_info">
        <%= render 'shared/user_info' %>
      </section>
      <section class="micropost_form">
        <%= render 'shared/micropost_form' %>
      </section>
    </aside>
  </div>
<% else %>
  <div class="center jumbotron">
    <h1>Welcome to the Sample App</h1>

    <h2>
      This is the home page for the
      <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a>
      sample application.
    </h2>

    <%= link_to "Sign up now!", signup_path, class: "btn btn-lg btn-primary" %>
  </div>

  <%= link_to image_tag("rails.png", alt: "Rails logo"),
              'http://rubyonrails.org/' %>
<% end %>
```

いくつかパーシャルを作る必要がある。まずはHomeの新しいサイドバーから作成。

```rb:_user_info.html.erb
<%= link_to gravatar_for(current_user, size: 50), current_user %>
<h1><%= current_user.name %></h1>
<span><%= link_to "view my profile", current_user %></span>
<span><%= pluralize(current_user.microposts.count, "micropost") %></span>
```

次はマイクロポスト作成フォーム。

```html:_micropost_form.html.erb
<%= form_for(@micropost) do |f| %>
  <%= render 'shared/error_messages', object: f.object %>
  <div class="field">
    <%= f.text_area :content, placeholder: "Compose new micropost..." %>
  </div>
  <%= f.submit "Post", class: "btn btn-primary" %>
<% end %>
```

`home`アクションにマイクロポストのインスタンス変数を追加。

```rb:static_pages_controller.rb
def home
    @micropost = current_user.microposts.build if logged_in?
  end
```
`current_user`メソッドはユーザーがログインしているときしか使えないため、`@micropost`変数もログインしているときのみ定義されるようになる。

Userオブジェクト以外でも動作するようにerror_messagesパーシャルを更新する。

```html:_error_messages.html.erb
<% if object.errors.any? %>
  <div id="error_explanation">
    <div class="alert alert-danger">
      The form contains <%= pluralize(object.errors.count, "error") %>.
    </div>
    <ul>
    <% object.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
    </ul>
  </div>
<% end %>
```

このままだとテストは失敗する。`error_messages`パーシャルが他の場所でも使われているため。

修正したら、無事テストはGREENに。

###13.3.3 フィードの原型
フィードの原型を作る。

`feed`メソッドの作成。


```rb:user.rb
  # 試作feedの定義
  # 完全な実装は次章の「ユーザーをフォローする」を参照
  def feed
    Micropost.where("user_id = ?", id)
  end
```

?をつけるのはSQLインジェクションを避けるため。

`home`アクションにフィードのインスタンス変数を追加する。

```rb:static_pages_controller.rb
  def home
    if logged_in?
      @micropost  = current_user.microposts.build
      @feed_items = current_user.feed.paginate(page: params[:page])
    end
  end
```

ステータスフィードのパーシャルも作成。

```html:_feed.html.erb
<% if @feed_items.any? %>
  <ol class="microposts">
    <%= render @feed_items %>
  </ol>
  <%= will_paginate @feed_items %>
<% end %>
```

そして`Home`ページにステータスフィードを追加する。

```html:
<% if logged_in? %>
  <div class="row">
    <aside class="col-md-4">
      <section class="user_info">
        <%= render 'shared/user_info' %>
      </section>
      <section class="micropost_form">
        <%= render 'shared/micropost_form' %>
      </section>
    </aside>
    <div class="col-md-8">
      <h3>Micropost Feed</h3>
      <%= render 'shared/feed' %>
    </div>
  </div>
```

###13.3.4 マイクロポストを削除する
最後の機能として、マイクロポストを削除する機能を追加する。そのためのリンクを追加する。

マイクロポストのパーシャルに削除リンクを追加する。

```html:_micropost.html.erb
<li id="micropost-<%= micropost.id %>">
  <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
  <span class="user"><%= link_to micropost.user.name, micropost.user %></span>
  <span class="content"><%= micropost.content %></span>
  <span class="timestamp">
    Posted <%= time_ago_in_words(micropost.created_at) %> ago.
    <% if current_user?(micropost.user) %>
      <%= link_to "delete", micropost, method: :delete,
                                       data: { confirm: "You sure?" } %>
    <% end %>
  </span>
</li>
```

次に、Micropostsコントローラの`destroy`アクションを定義し、現在のユーザーが削除対象のマイクロポストを保有しているかどうかを確認する。

```rb:microposts_controller.rb
class MicropostsController < ApplicationController
  before_action :logged_in_user, only: [:create, :destroy]
  before_action :correct_user,   only: :destroy
  .
  .
  .
  def destroy
    @micropost.destroy
    flash[:success] = "Micropost deleted"
    redirect_to request.referrer || root_url
  end

  private

    def micropost_params
      params.require(:micropost).permit(:content)
    end

    def correct_user
      @micropost = current_user.microposts.find_by(id: params[:id])
      redirect_to root_url if @micropost.nil?
    end
end
```

`request.referrer`メソッドは、DELETEリクエストが発行されたページに戻すことができるので非常に便利らしい。

###13.3.5 フィード画面のマイクロポストをテストする
fixtureを作成していく。別々のユーザーに紐付けられたマイクロポストを追加していく。

```microposts.yml
ants:
  content: "Oh, is that what you want? Because that's how you get ants!"
  created_at: <%= 2.years.ago %>
  user: archer

zone:
  content: "Danger zone!"
  created_at: <%= 3.days.ago %>
  user: archer

tone:
  content: "I'm sorry. Your words made sense, but your sarcastic tone did not."
  created_at: <%= 10.minutes.ago %>
  user: lana

van:
  content: "Dude, this van's, like, rolling probable cause."
  created_at: <%= 4.hours.ago %>
  user: lana
```

自分以外のユーザーのマイクロポストは削除をしようとすると、適切にリダイレクトされることをテストで確認する。

```rb:microposts_controller_test.rb
require 'test_helper'

class MicropostsControllerTest < ActionDispatch::IntegrationTest

  def setup
    @micropost = microposts(:orange)
  end

  test "should redirect create when not logged in" do
    assert_no_difference 'Micropost.count' do
      post microposts_path, params: { micropost: { content: "Lorem ipsum" } }
    end
    assert_redirected_to login_url
  end

  test "should redirect destroy when not logged in" do
    assert_no_difference 'Micropost.count' do
      delete micropost_path(@micropost)
    end
    assert_redirected_to login_url
  end

  test "should redirect destroy for wrong micropost" do
    log_in_as(users(:michael))
    micropost = microposts(:ants)
    assert_no_difference 'Micropost.count' do
      delete micropost_path(micropost)
    end
    assert_redirected_to root_url
  end
end
```

ここで統合テストを作成。

```
$ rails generate integration_test microposts_interface
```

テストの中身を記述して、無事GREENに。

##感想
この章はとにかく長かったです。データモデルの関連付けのあたりが業務でも大事になってくるのかなーと感じました。

あと、11章で手こずったHerokuへのアドオンがうまくできたので何よりです。。