【Rails】ユーザーのマイクロポスト【Rails Tutorial 13章まとめ】

##Micropostモデル
ユーザーに紐づいた短いメッセージの投稿機能を実装する。

###user:references
user:referencesをつけて、text型のcontentカラムを持つMicropostモデルを作成する。

```console
$ rails generate model Micropost content:text user:references
```
これによりMicropostモデルは自動でbelong_toによってユーザーに関連づけられ、user_idカラムも追加される。

ユーザーごとの投稿を作成時刻の逆順(新しい順)で取り出しやすくするために、user_idカラムとcreated_atカラムにインデックスを追加する。

```db/migrate/[timestamp]_create_microposts.rb
class CreateMicroposts < ActiveRecord::Migration[5.0]
  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
```
マイグレーションを実行しておく。

###Micropostのバリデーション
Micropostモデルの各カラムにバリデーションを設定していく。
先にテストを書く。
一つ目はMicropostモデルが正常かどうか、二つ目はuser_idの存在性のテストである。

```test/models/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
```
バリデーションを設定してテストが通るようにしておく。

```app/models/micropost.rb
class Micropost < ActiveRecord::Base
  belongs_to :user
  validates :user_id, presence: true
end
```

次に、content属性に関するバリデーションを設定する。
content属性は１４０文字まで、空白は無効とする。
先にテストを書く。

```test/models/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
```
バリデーションを追加する。

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

##User/Micropostの関連付け
###belong_toとhas_many
一つのマイクロポストは、一人のユーザーに関連づけられる一対一の関係にある（belong_to）。
また、一人のユーザーは複数のマイクロポストを持つ一対多の関係にある(has_many)。

そこで、Userモデルにhas_many :micropostsを追加する。

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

この関連付けによって、Userオブジェクトからマイクロポストを作成したり取得できるようになる。
具体的には、Userオブジェクトにmicropostsというメソッドを使用する。

```rb
user.microposts.create
user.microposts.create!
user.microposts.build
```
これで先ほどのテストを書き直すと以下のようになる。

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

##マイクロポストの改良
###デフォルトのスコープ
マイクロポストを作成した日時が新しい順に表示できるようにする。
テスト駆動開発で進めるため、まずfixtureファイルにテスト用のマイクロポストを作る。

```test/fixtures/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 %>
```
fixtureファイルの中では、埋め込みRubyを使って投稿時間を設定できる。
この中で最も新しい投稿であるmonst_recentが最初に取得されるようなテストを書く。

```test/models/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
```

マイクロポストの取得順はdefault_scopeメソッドを使って変更できる。
デフォルトでは昇順（asc）になっているので、これを降順(desc)にする。

```app/models/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
```

これでテストがGREENになる。

###ユーザーと同時に投稿を削除する
ユーザーが削除された時、そのマイクロポストも削除されるようにする。
そのために、has_manyメソッドにdependent: :destroyオプションを追加する。

```app/models/user.rb
  has_many :microposts, dependent: :destroy
```

テストを書く。

```test/models/user_test.rb
  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
```

削除するユーザーに関連付くマイクロポストを一つ作成し、ユーザーの削除とともにそれが破棄され、マイクロポストの数が１減ることをassert_differenceで確認する。

##マイクロポストの表示
###ユーザープロフィールページ
マイクロポストをユーザーのプロフィールページに表示する。
まずMicropostsコントローラを作成する。

```console
$ rails generate controller Microposts
```
マイクロポストの表示部分はパーシャルを使う。
ユーザー一覧ページでは次のようなコードでパーシャルを呼び出していた。

```erb
<ul class="users">
  <%= render @users %>
</ul>
```
こうすると、_user.html.erbパーシャルが呼び出されるとともに、＠users変数がパーシャルで使えるようになる。
同じことをマイクロポストの表示でも行うことにして、まずパーシャルを作成する。

```erb:app/views/microposts/_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メソッドを使うと、「3分前に投稿」といった文字列を表示できる。
また、埋め込みRubyを使ってid="micropost-<%= micropost.id %>とすることで、各マイクロポストにそのidごとにcssのidを与えている。

マイクロポストの表示にもページネーション機能を使うが、Usersコントローラ内でマイクロポストを使う場合は、以下のようにwill_paginateに@microposts変数を引数として与えねばならない。

```erb
<%= will_paginate @microposts %>
```
Usersコントローラ内で@users変数を使う場合は、この引数を省略できる。

@micropostsインスタンス変数を、Usersコントローラのshowアクション内に定義する。

```app/controllers/users_controller.rb
  def show
    @user = User.find(params[:id])
    @microposts = @user.microposts.paginate(page: params[:page])
  end
```

以上をまとめると、ユーザープロフィールページのビューは次のようになる。

```erb:app/views/users/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>
```

###マイクロポストのサンプル
６人のユーザーに、それぞれ５０個のマイクロポストを作成する。

```db/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
```
order(:created_at).take(6)とすることで、最初の６人を取得できる。
content属性には、fakerジェムのLorem.sentenceメソッド使うことでランダムな文章を入れている。

データベースをリセットしてseedファイルを実行しておく。

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

###プロフィールページのマイクロポストのテスト
プロフィールページ用の統合テストを作成する。

```console
$ rails g integration_test users_profile
```
ユーザーに紐付いたテスト用のマイクロポストをfixtureファイルに作成する。
そのためにはuser: michaelを付ける。
また、fakerと埋め込みRubyを使ってテスト用マイクロポストを大量生成する。

```test/fixtures/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 %>
```

テストを書く。

```test/integration/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
```

to_sメソッドは、文字列以外を文字列に変換する。
また、response.bodyはそのページのhtmlの全文を返す。
assert_matchにより、第一引数の文字列がページのどこかに含まれていることを確認する。

##Micropostsリソース
###ルーティング
Micropostsリソースの表示などはUsersコントローラの中で行うので、Micropostsコントローラに必要なのはcreateとdestroyアクションだけである。
よって、ルーティングは以下のようになる。

```config/routes.rb
Rails.application.routes.draw do
  .
  .
  .
  resources :microposts, only: [:create, :destroy]
end
```
名前付きルートなどの関係は以下のよう。
<img width="601" alt="スクリーンショット 2019-12-03 17.32.47.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/529443/673c7e47-ebf8-66ab-9597-3b388392e396.jpeg">

ややこしいことに、createアクションはmicroposts_pathだが、destroyアクションはmicropost_pathである。

###Micropostsコントローラのアクセス制御
マイクロポストは関連付けられたユーザーを通して投稿されるので、ログイン済みでなければならない。
非ログイン時にcreateアクションやdestroyアクションにアクセスした場合、ログイン画面にリダレクトされるようテストを書く。
また、assert_no_differenceでマイクロポストが増減していないことも確認しておく。

```test/controllers/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
```

ここで、非ログインならばリダイレクトするbeforeフィルターであるlogged_in_userが必要になるが、これはUsersコントローラ内にしかない。
そこで、これを各コントローラが継承するApplicationコントローラに移しておく。

```app/controllers/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コントローラのほうからは消しておく。

これをMicropostsコントローラのcreateとdestroyアクションに設定する。

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

  def create
  end

  def destroy
  end
end
```

##マイクロポストの作成
###createアクション
マイクロポストをアプリケーション上で作成するために、createアクションを書いていく。

```app/controllers/microposts_controller.rb
  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

  private

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

Strong Parametersを使って属性に値を渡すことも含め、Usersコントローラのcreateアクションとほとんど同じである。

###homeビュー
マイクロポストの投稿ページはStaticPagesのhomeビューとする。
ログインしている場合は投稿フォームを表示し、非ログイン時はサインアップページへのリンクを表示する。

```app/views/static_pages/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ページに表示するユーザー情報のパーシャルを作成する。

```app/views/shared/_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>
```
マイクロポスト投稿フォームのパーシャルを作成する。

```app/views/shared/_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 %>
```
ここで、エラーメッセージ部分のパーシャル呼び出しは次のようになっている。

```erb
<%= render 'shared/error_messages', object: f.object %>
```
ユーザー新規登録・ログイン用に作っていたエラーメッセージのパーシャルは@user変数を直接参照していたので、@micropostを使えるように修正する必要がある。
このパーシャル呼び出しにはobject: f.objectというハッシュが渡されている。
これによってエラーメッセージのパーシャル内でobjectという変数が使えるようになり、これで＠user変数を置き換えることで、@micropost変数に対応することができる。

```app/views/shared/_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 %>
```
エラーメッセージのパーシャルを呼び出していた他のビュー（ユーザー登録、ユーザー編集、パスワード再設定）でもこのobject: f.objectを設定しておく。

次に、StaticPagesコントローラのhomeアクションで、現在のユーザーに関連付いた新しいMicropostオブジェクトを作成する。

```app/controllers/static_pages_controller.rb
  def home
    @micropost = current_user.microposts.build if logged_in?
  end
```
newではなくbuildを使う点に注意する。
マイクロポスト投稿はログイン時のみの機能なので、if logged_in?を付けてログイン時のみ変数が定義されるようになっている。

##フィード
投稿がすぐに見れるように、homeビューにマイクロポストのフィードを実装する。
このフィードでは将来的にフォローしたユーザーの投稿も見れるようにする。

全てのユーザーがフィードを持つので、Userモデルにfeedメソッドを定義する。

```app/models/user.rb
class User < ApplicationRecord
  .
  .
  .
  # 試作feedの定義
  def feed
    Micropost.where("user_id = ?", id)
  end

  private
    .
    .
    .
end
```

whereの検索条件を"user_id = ?", idとしているのは、セキュリティ上の問題を解決するためである。

このfeedメソッドを使って現在のユーザーのマイクロポストを取得し、インスタンス変数@feed_itemsに入れる。

```app/controllers/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
```
ステータスフィードのパーシャルは次のようになる。

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

ここで、<%= render @feed_items %>というパーシャル呼び出しが使われている。
@feed_itemsに入っている要素はMicropostクラスを持っているために、Railsは対応する名前のパーシャル（_micropost.html.erb）を、同じディレクトリから探してくれる。

これを使って、homeビューにステータスフィードを追加する。

```app/views/static_pages/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 class="col-md-8">
      <h3>Micropost Feed</h3>
      <%= render 'shared/feed' %>
    </div>
  </div>
<% else %>
```
ここで、マイクロポストの投稿が失敗すると、Homeページは@feed_itemsインスタンス変数を期待しているためエラーになる。

ここの意味がよく分からなかったのだが、次のサイトが参考になった。
「Railsチュートリアル13章　@feed_itemsがnilになる？」
https://teratail.com/questions/194996

つまりこういうことになる。
①ステータスフィードのパーシャルは<% if @feed_items.any? %>というif文を使い、@feed_itemsがnilでない時のみマイクロポストのフィードを表示する。
②再レンダリングされるhomeページで使えるインスタンス変数は、Micropostsコントローラのcreateアクション内で定義されたものだけである
③これはリダイレクトではないのでStaticPagesコントローラのhomeアクションは実行されないためである。すなわち、homeアクション内の@feed_itemsが定義されず、nilとなる。
④createアクションで@feed_itemsを定義してやることで、homeビューで@feed_itemsが使えるようになる。


そこで、マイクロポストの投稿が失敗した際の処理として、@feed_itemsを定義してやる。

```app/controllers/sessions_controller.rb
  def create
    @micropost = current_user.microposts.build(micropost_params)
    if @micropost.save
      flash[:success] = "Micropost created!"
      redirect_to root_url
    else
      @feed_items = current_user.feed.paginate(page: params[:page])
      render 'static_pages/home'
    end
  end
```

##マイクロポストの削除
マイクロポストの削除機能を実装する。
ユーザーの削除は管理者のみが行えたが、マイクロポストは投稿したユーザーのみが削除できるようにする。
###削除リンクと認可フィルター
マイクロポストのパーシャルに削除用リンクを追加する。

```app/views/microposts/_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>
```

ここで、ユーザーとマイクロポストの関連付けを利用して、現在のユーザーがそのマイクロポストを投稿したユーザーの場合のみ削除リンクを表示するようにしている。

```erb
<% if current_user?(micropost.user) %>
```

次に、マイクロポストを削除するdestroyアクションを作成する。

```app/controllers/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
```

correct_userメソッドは、削除するマイクロポストに関連付けられたユーザーが現在のユーザーと一致するかを確認するフィルターである。
同時に、このメソッドは削除するマイクロポストの取得も行なっている。
つまり、現在のユーザーのマイクロポストの中に、削除するマイクロポストがあるかどうかを確認し、あれば取得して、destroyアクションに繋ぐ。
無ければリダイレクトする。

destroyアクション内の、マイクロポスト削除後のリダイレクトは以下のようになっている。

```rb
request.referrer || root_url
```
request.referrerは、一つ前のURLを返す。
マイクロポストをhomeページから削除すればhomeページに、プロフィールページから削除すればプロフィールページに戻る。
もし戻り先が見つからなかったとしても、or演算子でルートURLを指定して、そちらに移動するようにしている。

##フィード画面のマイクロポストのテスト
###認可のテスト
他人のマイクロポストを削除できないことのテストを書く。
まず、fixtureファイルに別のユーザーと紐づけられたマイクロポストを追加する。

```test/fixtures/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
```

テストは以下のようになる。

```test/controllers/microposts_controller_test.rb
  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
```

###統合テスト
マイクロポスト機能の統合テストを作成する。

```console
$ rails generate integration_test microposts_interface
```

```test/integration/microposts_interface_test.rb
require 'test_helper'

class MicropostsInterfaceTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end

  test "micropost interface" do
    log_in_as(@user)
    get root_path
    assert_select 'div.pagination'
    # 無効な送信
    assert_no_difference 'Micropost.count' do
      post microposts_path, params: { micropost: { content: "" } }
    end
    assert_select 'div#error_explanation'
    # 有効な送信
    content = "This micropost really ties the room together"
    assert_difference 'Micropost.count', 1 do
      post microposts_path, params: { micropost: { content: content } }
    end
    assert_redirected_to root_url
    follow_redirect!
    assert_match content, response.body
    # 投稿を削除する
    assert_select 'a', text: 'delete'
    first_micropost = @user.microposts.paginate(page: 1).first
    assert_difference 'Micropost.count', -1 do
      delete micropost_path(first_micropost)
    end
    # 違うユーザーのプロフィールにアクセス (削除リンクがないことを確認)
    get user_path(users(:archer))
    assert_select 'a', text: 'delete', count: 0
  end
end
```