Ruby on Rails チュートリアル

プロダクト開発の0→1を学ぼう

第2版 目次

第10章ユーザーのマイクロポスト

第9章ではUsersリソース用のRESTアクションを完成させました。次はいよいよ、ユーザーのマイクロポストリソースをすべて追加しましょう1第2章では、特定のユーザーに関連付けられたいくつかの簡単なメッセージがありました。この章では、2.3で記述したMicropostデータモデルを作成し、Userモデルとhas_manyおよびbelongs_toメソッドを使って関連付けを行い、さらに、結果を処理し表示するために必要なフォームとその部品を作成します。 第11章では、マイクロポストのフィードを受け取るために、ユーザーをフォローするという概念を導入し、Twitterのミニクローンを完成させます。

Git をバージョン管理に使っている場合は、いつものようにトピックブランチを作成しておきましょう。

$ git checkout -b user-microposts

10.1Micropostモデル

まずはMicropostリソースの最も本質的な部分を表現するMicropostモデルを作成するところから始めましょう。2.3で作成したモデルと同様に、この新しいMicropostモデルもデータ検証とUserモデルの関連付けを含んでいます。以前のモデルとは違って、今回のマイクロポストモデルは完全にテストされ、デフォルトの順序を持ち、また親であるユーザーが破棄された場合には自動的に破棄されるようにします。

10.1.1基本的なモデル

Micropostモデルは、マイクロポストの内容を保存するcontent属性2と、特定のユーザーとマイクロポストを関連付けるuser_id属性の2つの属性だけを持ちます。Userモデルの場合と同様に (リスト6.1)、generate modelコマンドを使って生成します。

$ rails generate model Micropost content:string user_id:integer

上のコマンドを実行するとMicropostファクトリーが作成されますが、これは後で手動で作成するので (10.1.4)、以下を実行していったん削除してください。

$ rm -f spec/factories/microposts.rb

リスト6.2でデータベースにusersテーブルを作るマイグレーションファイルを生成した時と同様に、このコマンドは micropostsテーブルを作成するためのマイグレーションファイルを生成します (リスト10.1)。

リスト10.1 Micropostマイグレーション(user_idcreated_atにインデックスが与えられていることに注意)
db/migrate/[timestamp]_create_microposts.rb
class CreateMicroposts < ActiveRecord::Migration
  def change
    create_table :microposts do |t|
      t.string :content
      t.integer :user_id


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

ここで、リスト10.1ではuser_idcreated_atカラムにインデックスが付与されていることに注目してください (コラム 6.2)。user idに関連付けられたすべてのマイクロポストを作成時刻の逆順で取り出すためです。

add_index :microposts, [:user_id, :created_at]

user_idcreated_at両方のカラムを1つの配列に含めることで、Active Recordで両方のキーを同時に使用する複合キーインデックスを作成できます。また6.1.1でも述べたように、t.timestampsの行によってcreated_atカラムとupdated_atカラムが作成されていることにも注目してください。この後、10.1.4および10.2.1created_atカラムをさらに追加します。

それではUserモデルの場合 (リスト6.5) と同様に、Micropostモデルに対する最小限のテストを書くところから始めましょう。具体的には リスト10.2 に示すように、micropostオブジェクトが content属性とuser_id属性を持っていることを確認するテストを作成します。

リスト10.2 最初のMicropost spec。
spec/models/micropost_spec.rb
require 'spec_helper'

describe Micropost do

  let(:user) { FactoryGirl.create(:user) }
  before do
    # このコードは慣用的な意味で正しくない。
    @micropost = Micropost.new(content: "Lorem ipsum", user_id: user.id)
  end

  subject { @micropost }

  it { should respond_to(:content) }
  it { should respond_to(:user_id) }
end

Micropost マイグレーションを実行し、テストデータベースを準備することで、これらのテストをパスさせることができます。

$ bundle exec rake db:migrate
$ bundle exec rake db:test:prepare

実行した結果のMicropostモデルの構造は図10.1のようになります。

micropost_model
図10.1Micropostデータモデル

テストにパスすることを確認してみましょう。

$ bundle exec rspec spec/models/micropost_spec.rb

テストにパスしたとしても、コードの中にある以下のコメントに気付いた方もいると思います。

let(:user) { FactoryGirl.create(:user) }
before do
  # このコードは慣用的な意味で正しくない。
  @micropost = Micropost.new(content: "Lorem ipsum", user_id: user.id)
end

上のコメントは、before ブロックのコードが慣用的な意味で正しくないことを指摘しています。このコードは動きますが、Railsの流儀に合っていません。その理由を考えてみてください。解答は10.1.3で確認できます。

10.1.2 最初の検証

Micropostモデルが必要とされている理由のひとつに、マイクロポストを投稿したユーザーを示すユーザーidを持っているということがあります。これを慣用的な意味で正しく行うには、Active Recordの関連付けを行います。実際の関連付けは10.1.3で行います。この作業にはある程度のリファクタリングが必要なので、テストを作成してバグの再発をキャッチするようにします。

リスト10.3に示したテストでは、ユーザーidをnilにしてから投稿したマイクロポストが無効になるかどうかをチェックすることで、検証を行なっています。(リスト6.8でのUserモデルのテストと比較してみてください)。

リスト10.3 新しいマイクロポストに対する検証をテストする。
spec/models/micropost_spec.rb
require 'spec_helper'

describe Micropost do

  let(:user) { FactoryGirl.create(:user) }
  before do
    # このコードは慣用的な意味で正しくない。
    @micropost = Micropost.new(content: "Lorem ipsum", user_id: user.id)
  end

  subject { @micropost }

  it { should respond_to(:content) }
  it { should respond_to(:user_id) }

  it { should be_valid }

  describe "when user_id is not present" do
    before { @micropost.user_id = nil }
    it { should_not be_valid }
  end
end

このコードはマイクロポストが有効であり、かつuser_id属性が存在していることをテストしています。リスト10.4 に示すような簡単な存在確認検証によって、このテストはパスします。

リスト10.4 マイクロポストのuser_idに対する検証。
app/models/micropost.rb
class Micropost < ActiveRecord::Base
  validates :user_id, presence: true
end

10.1.3User/Micropostの関連付け

Webアプリケーション用のデータモデルを構築するにあたって、個々のモデル間での関連付けを十分考えておくことが重要です。今回の場合は、 2.3.3でも示したように、それぞれのマイクロポストは1人のユーザーと関連付けられ、それぞれのユーザーは (潜在的に) 複数のマイクロポストと関連付けられます。この関連付けを図10.2図10.3に示します。これらの関連付けを実装するための一環として、Micropostモデルに対するテストを作成し、さらにUserモデルにいくつかのテストを追加します。

micropost_belongs_to_user
図10.2micropost と user間のbelongs_toリレーションシップ
user_has_many_microposts
図10.3userとmicropost間のhas_manyリレーションシップ

この節で定義するbelongs_to/has_many関連付けを使用することで、表10.1に示すようなメソッドをRailsで使えるようになります。

メソッド用途
micropost.userマイクロポストに関連付けられたユーザーオブジェクトを返す。
user.micropostsユーザーのマイクロポストの配列を返す。
user.microposts.create(arg)マイクロポストを作成する (user_id = user.id)。
user.microposts.create!(arg)マイクロポストを作成する (失敗した場合は例外を発生する)。
user.microposts.build(arg)新しいMicropostオブジェクトを返す (user_id = user.id)。
表10.1user/micropost関連メソッドのまとめ

表10.1では、以下のメソッドではなく

Micropost.create
Micropost.create!
Micropost.new

以下のメソッドになっていることに注意してください。

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

このパターンは、user オブジェクトの関連付けを経由してマイクロポストを作成する標準的な方法です。新規のマイクロポストがこの方法で作成される場合、user_id自動的に正しい値に設定されます。特に、以下のような

let(:user) { FactoryGirl.create(:user) }
before do
  # このコードは慣用的な意味で正しくない。
  @micropost = Micropost.new(content: "Lorem ipsum", user_id: user.id)
end

リスト10.3のコードを、以下のコードと置き換えることができるようになります。

let(:user) { FactoryGirl.create(:user) }
before { @micropost = user.microposts.build(content: "Lorem ipsum") }

一度正しい関連付けを定義してしまえば、@micropost変数のuser_idには、関連するユーザーのidが自動的に設定されます。

表10.1に示したように、ユーザーとマイクロポストの関連付けの結果には、単にマイクロポストのユーザーを返すmicropost.userメソッドもあり、これもテストが必要です。このメソッドは、ititsメソッドを以下のように使うことでテストできます。

it { should respond_to(:user) }
its(:user) { should eq user }

上のコードを反映したMicropostモデルのテストをリスト10.5に示します。

リスト10.5 マイクロポストのユーザーとの関連付けのテスト。
spec/models/micropost_spec.rb
require 'spec_helper'

describe Micropost do

  let(:user) { FactoryGirl.create(:user) }
  before { @micropost = user.microposts.build(content: "Lorem ipsum") }

  subject { @micropost }

  it { should respond_to(:content) }
  it { should respond_to(:user_id) }
  it { should respond_to(:user) }
  its(:user) { should eq user }

  it { should be_valid }

  describe "when user_id is not present" do
    before { @micropost.user_id = nil }
    it { should_not be_valid }
  end
end

Userモデル側の関連付けに対する詳細なテストは10.1.4まで保留することにし、今は単にmicroposts属性の存在をテストするだけにしておきます (リスト10.6)。

リスト10.6 ユーザーのmicroposts属性に対するテスト。
spec/models/user_spec.rb
require 'spec_helper'

describe User do

  before do
    @user = User.new(name: "Example User", email: "user@example.com",
                     password: "foobar", password_confirmation: "foobar")
  end

  subject { @user }
  .
  .
  .
  it { should respond_to(:admin) }
  it { should respond_to(:microposts) }
  .
  .
  .
end

関連付けを実装する最終的なコードは、テストコードの長さと比べるとあっけないほど短いものになります。リスト10.5リスト10.6の両方のテストにパスするためには、belongs_to :user (リスト10.7)とhas_many :microposts (リスト10.8) の2行を加えるだけで済みます。

リスト10.7 マイクロポストがユーザーに所属する (belongs_to) 関連付け。
app/models/micropost.rb
class Micropost < ActiveRecord::Base
  belongs_to :user
  validates :user_id, presence: true
end
リスト10.8 ユーザーがマイクロポストを複数所有する (has_many) 関連付け。
app/models/user.rb
class User < ActiveRecord::Base
  has_many :microposts
  .
  .
  .
end

この時点で、表10.1の項目と、リスト10.5およびリスト10.6のコードをよく見比べて、関連付けの基本的原則を理解しておいてください。テストにパスすることも確認しておきましょう。

$ bundle exec rspec spec/models

10.1.4マイクロポストを改良する

リスト10.6has_many関連付けのテストでは、microposts属性の存在をほとんど検証していないので、あまり意味がありませんでした。この章では、順序依存関係をマイクロポストに追加し、user.micropostsメソッドが実際にマイクロポストの配列を返すことをテストします。

Userモデルのテストのためにいくつかのマイクロポストを作成しておく必要がありますので、この時点でマイクロポストを生成するファクトリーを作成しておきましょう。そのためには、Factory Girlに関連付けを作成する方法を知っておく必要があります。幸い、リスト10.9に示したとおり、これは非常に簡単です。

リスト10.9 マイクロポスト作成用の新しいファクトリーを含む、完全なFactoryファイル。
spec/factories.rb
FactoryGirl.define do
  factory :user do
    sequence(:name)  { |n| "Person #{n}" }
    sequence(:email) { |n| "person_#{n}@example.com"}
    password "foobar"
    password_confirmation "foobar"

    factory :admin do
      admin true
    end
  end

  factory :micropost do
    content "Lorem ipsum"
    user
  end
end

ここでは、以下のようにマイクロポスト用のファクトリーの定義にuserを含めるだけで、マイクロポストに関連付けられるユーザーのことがFactory Girlに伝わります。

factory :micropost do
  content "Lorem ipsum"
  user
end

次の節でもお見せしますが、これによって、以下のようにマイクロポスト用のファクトリーを定義できるようになります。

FactoryGirl.create(:micropost, user: @user, created_at: 1.day.ago)

デフォルトのスコープ

データベースからユーザーのマイクロポストを読み出すuser.micropostsメソッドは、デフォルトでは読み出しの順序に対して何も保証しませんが、 ブログやTwitterの慣習に従って、作成時間の逆順、つまり最も新しいマイクロポストを最初に表示するようにしてみましょう。この順序をテストするために、次のようにマイクロポストをいくつか作成しておきます。

FactoryGirl.create(:micropost, user: @user, created_at: 1.day.ago)
FactoryGirl.create(:micropost, user: @user, created_at: 1.hour.ago)

(コラム 8.1で説明した時間指定用ヘルパーメソッドを使うことで) 2番目のポストの作成時刻がより新しく (1.hour.ago: 1時間前)、最初のポストの作成日時が1.day.ago (1日前) になります。Factory Girlを使用すると、Active Recordがアクセスを許可しないようなcreated_at属性も手動で設定できるので大変便利です (created_atupdated_atなどは通常のカラムと異なる “マジック”カラムであり、これらの作成タイムスタンプや更新タイムスタンプは自動的に設定されてしまうため、明示的に値を設定しても上書きされてしまうことを思い出してください)。

(SQLiteを含む) ほとんどのデータベースアダプターは、マイクロポストを id順で返すため、リスト10.10のようなテストはほぼ確実に失敗することになります。このコードではletメソッドの代わりにlet! (“let バン”と読みます) メソッドを使っています。その理由は、let変数はlazy、つまり参照されたときにはじめて初期化されるためです。ここでは、マイクロポストを遅延することなく即座に作成する必要があります。そのことにより、マイクロポストのタイムスタンプが常に正しい順序で作成されるように、かつ@user.micropostsが空の状態が生じることのないようにしたいからです。let!を使用すれば、対応する変数を強制的に即座に作成できます。

リスト10.10 ユーザーのマイクロポストの順序をテストする。
spec/models/user_spec.rb
require 'spec_helper'

describe User do
  .
  .
  .
  describe "micropost associations" do

    before { @user.save }
    let!(:older_micropost) do
      FactoryGirl.create(:micropost, user: @user, created_at: 1.day.ago)
    end
    let!(:newer_micropost) do
      FactoryGirl.create(:micropost, user: @user, created_at: 1.hour.ago)
    end

    it "should have the right microposts in the right order" do
      expect(@user.microposts.to_a).to eq [newer_micropost, older_micropost]
    end
  end
end

上記のコードで重要なのは、以下の行です。

expect(@user.microposts.to_a).to eq [newer_micropost, older_micropost]

これは新しいポストが最初に来ることをテストしています。デフォルトでは id順に並ぶため[older_micropost, newer_micropost]の順序になりテストは失敗するはずです。またこのテストは、表10.1で示すように、 user.micropostsが有効なマイクロポストの配列を返すことをチェックすることにより、基本的なhas_many関連付け自体の正しさも確認しています。4.3.1でも説明したように、to_aメソッドは、 @user.micropostsをデフォルトの状態 (これはたまたまActive Recordの "collection proxy" になっています) から正しい配列に変換します。変換された配列は、手作りの配列と比較可能になります。

順序テストがパスするために、 リスト 10.11に示すようにRailsのdefault_scopeorderパラメータを渡して使用します (このコードはスコープに関する最初の例でもあります。より一般的なスコープについては第11章で説明します)。

リスト10.11 default_scopeでマイクロポストを順序付ける。
app/models/micropost.rb
class Micropost < ActiveRecord::Base
  belongs_to :user
  default_scope -> { order('created_at DESC') }
  validates :user_id, presence: true
end

上のコードでの順序は’created_at DESC’としています。DESCは SQLでいうところの “descending” であり、新しいものから古い順への降順ということになります。

Rails 4.0からは、あらゆるスコープは、スコープで必要な域値を返す無名関数を受け取ります。これにより、スコープをその場で評価する必要がほぼなくなり、後に読み込まれたときに必要に応じて評価するようになります (いわゆる遅延評価 (lazy evaluation) です)。上のコードの場合、以下がその関数です。

-> { order('created_at DESC') }

この種のオブジェクトの構文は、Proc (手続き: procedure) とかラムダ (lambda)と呼ばれ、->という矢印で表されます。この構文はブロック (4.3.2) を引数に取り、後にcallメソッドによって呼び出されるときにブロックを実際に評価します。この構文をコンソールで確かめてみましょう。

>> -> { puts "foo" }
=> #<Proc:0x007fab938d0108@(irb):1 (lambda)>
>> -> { puts "foo" }.call
foo
=> nil

(ProcやラムダはRubyのトピックとしてはやや高度な部類に含まれるので、今すぐわからなくても心配する必要はありません。)

Dependent: destroy

順序についてはひとまずここで区切ることにし、今度はマイクロポストに第二の要素を追加してみましょう。9.4で書いたように、サイト管理者はユーザーを破棄する権限を持ちます。ユーザーが破棄された場合、ユーザーのマイクロポストも同様に破棄されるべきです。最初のマイクロポストのユーザーを破棄した後、関連するマイクロポストもデータベースからなくなったことを確認することで、ユーザーの破棄をテストすることができます。

適切にマイクロポストの破棄をテストするために、最初にローカル変数で指定されたユーザーのポストを取得し、次にユーザーを破棄します。シンプルな実装は以下のようになります。

microposts = @user.microposts.to_a
@user.destroy
expect(microposts).not_to be_empty
microposts.each do |micropost|
  # マイクロポストがデータベースからなくなったことを確認
end

上のコードで、to_aメソッドが呼び出されていることでマイクロポストのコピーが作成されていることに注目してください (参照のコピーではなく、オブジェクト自体がコピーされています)。さらに、以下の行にも注目してください。

expect(microposts).not_to be_empty

上の行は一種のセフティチェックの役割も果たしており、うっかりto_aメソッドを付け忘れたときのエラーをすべてキャッチしてくれます。ここで重要なのは、to_aメソッドがなかったら、ユーザーを削除したときにmicroposts変数に含まれているポストまで削除されてしまうということです。

microposts.each do |micropost|
  # マイクロポストがデータベースからなくなったことを確認
end

つまり、micropostsが空になってしまうため、上のテストに何を書いても動作しなくなってしまうということです。

データベースにマイクロポストがないという予想は、以下のように書くことができます。

microposts.each do |micropost|
  expect(Micropost.where(id: micropost.id)).to be_empty
end

上のコードでは、Micropost.findではなくMicropost.whereを使用しています。whereメソッドは、レコードがない場合に空のオブジェクトを返すので多少テストが書きやすくなるためです (findはレコードがない場合に例外を発生します)。(気になる方への補足: findを使用する場合は以下のようになります。

expect do
  Micropost.find(micropost)
end.to raise_error(ActiveRecord::RecordNotFound)

これでfindの場合のテストを実施できます。)

これまでのコードを集約したものをリスト10.12に示します。

リスト10.12 ユーザーを破棄するとマイクロポストも破棄されることをテストする。
spec/models/user_spec.rb
require 'spec_helper'

describe User do
  .
  .
  .
  describe "micropost associations" do

    before { @user.save }
    let!(:older_micropost) do
      FactoryGirl.create(:micropost, user: @user, created_at: 1.day.ago)
    end
    let!(:newer_micropost) do
      FactoryGirl.create(:micropost, user: @user, created_at: 1.hour.ago)
    end
    .
    .
    .
    it "should destroy associated microposts" do
      microposts = @user.microposts.to_a
      @user.destroy
      expect(microposts).not_to be_empty
      microposts.each do |micropost|
        expect(Micropost.where(id: micropost.id)).to be_empty
      end
    end
  end
end

リスト10.12のテストをパスするためのアプリケーションコードは1行に満たないほどの量しかなく、事実それはリスト10.13に示す has_many関連付けメソッドのオプションにすぎません。

リスト10.13 ユーザーのマイクロポストがユーザーと一緒に破棄されることを保証するコード。
app/models/user.rb
class User < ActiveRecord::Base
  has_many :microposts, dependent: :destroy
  .
  .
  .
end

上のコードの中にある以下のdependent: :destroyオプションは、

has_many :microposts, dependent: :destroy

ユーザー自体が破棄されたときに、そのユーザーに依存するマイクロポスト (つまり特定のユーザーのもの) も破棄されることを指定しています。これは、管理者がシステムからユーザーを削除したときに、依存するユーザーが存在しないマイクロポストがデータベースに取り残されてしまうことを防ぎます。

これで、ユーザー/マイクロポスト関連付けの最終形が完成しました。すべてのテストがパスするはずです。

$ bundle exec rspec spec/

10.1.5コンテンツの検証

Micropostモデルを離れる前に、(2.3.2の例に従い) マイクロポストのcontentの検証を追加しましょう。user_id属性と同様、content属性も存在する必要があり、さらにマイクロポストが140文字より長くならないよう制限を加えます。このテストは6.2でのユーザーモデルの検証テストの例に従って、リスト10.14のように書きます。

リスト10.14 Micropostモデルの検証のテスト。
spec/models/micropost_spec.rb
require 'spec_helper'

describe Micropost do

  let(:user) { FactoryGirl.create(:user) }
  before { @micropost = user.microposts.build(content: "Lorem ipsum") }
  .
  .
  .
  describe "when user_id is not present" do
    before { @micropost.user_id = nil }
    it { should_not be_valid }
  end

  describe "with blank content" do
    before { @micropost.content = " " }
    it { should_not be_valid }
  end

  describe "with content that is too long" do
    before { @micropost.content = "a" * 141 }
    it { should_not be_valid }
  end
end

6.2と同様、リスト10.14ではマイクロポストの長さの検証コードをテストするために、文字列の乗算を使用しています。

$ rails console
>> "a" * 10
=> "aaaaaaaaaa"
>> "a" * 141
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

実際のアプリケーションコードはわずか1行です。

validates :content, presence: true, length: { maximum: 140 }

上のコードを反映したMicropostモデルをリスト10.15に示します。

リスト10.15 Micropostモデルの検証。
app/models/micropost.rb
class Micropost < ActiveRecord::Base
  belongs_to :user
  default_scope -> { order('created_at DESC') }
  validates :content, presence: true, length: { maximum: 140 }
  validates :user_id, presence: true
end

10.2マイクロポストを表示する

Web経由でマイクロポストを作成する方法は現時点ではありませんが (10.3.2から作り始めます)、マイクロポストを表示することと、テストすることならできます。ここでは、Twitterのような独立したマイクロポストindexページでユーザーのマイクロポストを表示するよりも、図10.4のモックアップに示したように、直接ユーザーのshowページで表示させることにしましょう。ユーザープロファイルにマイクロポストを表示させるため、最初に極めてシンプルなERbテンプレートを作成します。次に、9.3.2のサンプルデータ生成タスクにマイクロポストのサンプルを追加して、画面にサンプルデータが表示されるようにしてみます。

user_microposts_mockup_bootstrap
図10.4マイクロポストが表示されたプロファイルページのモックアップ。(拡大)

8.2.1でサインイン機能について説明したときと同様に、10.2.1でも最初に実装をお目にかけ、次にそれらの要素を順に解説してきます (スタックに複数の要素を一括でプッシュした後、順にポップするような要領です)。しばらくの間盛り上がりに欠けるかもしれませんが、10.2.2で一気に取り返しますので、それまでご辛抱願います。

10.2.1ユーザー表示ページの拡張

ユーザーのマイクロポスト表示に対するテスト、すなわちユーザーに対するrequest specを作成するところから始めましょう。ユーザーに関連付けられているマイクロポストのファクトリーを作成し、それから表示ページが各ポストの内容を含んでいるか検証する戦略で進めます。また、図 10.4で示されているように、マイクロポストの総数が表示されているかどうかも検証します。

ポストはletメソッドで作成できますが、ユーザー表示ページでポストがすぐに表示されるように、リスト10.10と同様に即座に関連付けを作成できるようにしたいと思います。そこで、今回もlet!を使います。

let(:user) { FactoryGirl.create(:user) }
let!(:m1) { FactoryGirl.create(:micropost, user: user, content: "Foo") }
let!(:m2) { FactoryGirl.create(:micropost, user: user, content: "Bar") }

before { visit user_path(user) }

上のようにマイクロポストを定義したことで、リスト10.16のコードを使って、プロファイルページの外観をテストできるようになります。

リスト10.16 ユーザーのshowページでマイクロポストが表示されていることをテストする。
spec/requests/user_pages_spec.rb
require 'spec_helper'

describe "User pages" do
  .
  .
  .
  describe "profile page" do
    let(:user) { FactoryGirl.create(:user) }
    let!(:m1) { FactoryGirl.create(:micropost, user: user, content: "Foo") }
    let!(:m2) { FactoryGirl.create(:micropost, user: user, content: "Bar") }

    before { visit user_path(user) }

    it { should have_content(user.name) }
    it { should have_title(user.name) }

    describe "microposts" do
      it { should have_content(m1.content) }
      it { should have_content(m2.content) }
      it { should have_content(user.microposts.count) }
    end
  end
  .
  .
  .
end

countメソッドは、関連付けを経由して使用していることに注目してください。

user.microposts.count

count関連付けメソッドは賢くできていて、直接データベースでカウントを行います。特に、データベース上のマイクロポストを全部読みだしてから結果の配列に対してlengthを呼ぶような無駄なことはしていません。そんなことをしたら、マイクロポストの数が増加するにつれて効率が低下してしまいます。代わりに、特定user_idに対するマイクロポストの数をデータベースに問い合わせます。それでもcountメソッドがアプリケーションのボトルネックになるようなことがあれば、さらに高速なcounter cacheを使うこともできます。

リスト10.16のテストはリスト10.18まではパスしませんが、リスト10.17に示したように、まずはユーザープロファイルページにマイクロポストの一覧を挿入するところからアプリケーションコードの実装を開始することにしましょう。

リスト10.17 ユーザーのshow画面にマイクロポストを追加する。
app/views/users/show.html.erb
<% provide(:title, @user.name) %>
<div class="row">
  .
  .
  .
  <aside>
    .
    .
    .
  </aside>
  <div class="span8">
    <% if @user.microposts.any? %>
      <h3>Microposts (<%= @user.microposts.count %>)</h3>
      <ol class="microposts">
        <%= render @microposts %>
      </ol>
      <%= will_paginate @microposts %>
    <% end %>
  </div>
</div>

すぐにもマイクロポスト一覧の実装に取りかかりますが、その前に注意すべき点がいくつかあります。リスト10.17では、if @user.microposts.any?を使用することによって (以前リスト7.24でも使用した) ユーザーのマイクロポストが1つもない場合に空のリストが表示されないようにしています。

リスト10.17では、以下のようにマイクロポストに対するページネーションのコードを加えていたことに注意してください。

<%= will_paginate @microposts %>

リスト9.33のユーザーインデックスページのコードと比較すると、以前は以下のように単純なコードでした。

<%= will_paginate %>

上のコードは引数なしで動作していました。これはwill_paginateが、Usersコントローラのコンテキストにおいて、@usersインスタンス変数が存在していることを前提としているためです。このインスタンス変数は、9.3.3でも述べたようにActiveRecord::Relationクラスのインスタンスです。今回の場合は、ユーザーコントローラのコンテキストにおいて、マイクロポストをページネーションしたいため、明示的に@microposts変数を will_paginateに渡す必要があります。もちろん、そのような変数をユーザーshowアクションで定義しなければなりません (リスト10.19)。

最後に、以下のようにマイクロポストの現在の数のカウントを追加します。

<h3>Microposts (<%= @user.microposts.count %>)</h3>

前述のように、@user.microposts.countは、ユーザー/マイクロポスト関連付けを経由して、あるユーザーに属するマイクロポストをカウントすることを除けば、User.countに似ています。

やっとマイクロポスト一覧のコードそのものにたどり着きました。

<ol class="microposts">
  <%= render @microposts %>
</ol>

この順序リストタグolを含むコードがマイクロポストの一覧を生成します。ただしご覧のとおり、実装の厄介な部分をマイクロポストパーシャルに任せています。9.3.4で見た以下のコードは、

<%= render @users %>

_user.html.erbパーシャルを使って自動的に@users変数内のそれぞれのユーザーを出力します。同様に以下のコードは、

<%= render @microposts %>

まったく同じことをマイクロポストで行います。つまり、リスト10.18に示すように、_micropost.html.erbパーシャルを (micropost用のビューのディレクトリの中に) 定義しなければならないことを意味します。

リスト10.18 1つのマイクロポストを表示するパーシャル。
app/views/microposts/_micropost.html.erb
<li>
  <span class="content"><%= micropost.content %></span>
  <span class="timestamp">
    Posted <%= time_ago_in_words(micropost.created_at) %> ago.
  </span>
</li>

詳細は10.2.2で述べますが、上のコードでは素晴らしいtime_ago_in_wordsヘルパーメソッドを使用しています。

これまでのところ、関連するERbテンプレートをすべて定義したにもかかわらず、リスト10.16のテストは、たった1つ、@microposts変数がないために失敗していたはずです。リスト10.19のように変更することで、テストにパスさせることができます。

リスト10.19 @micropostsインスタンス変数をユーザーshowアクションに追加する。
app/controllers/users_controller.rb
class UsersController < ApplicationController
  .
  .
  .
  def show
    @user = User.find(params[:id])
    @microposts = @user.microposts.paginate(page: params[:page])
  end
  .
  .
  .
end

paginateメソッドの素晴らしさに注目してください。マイクロポストの関連付けを経由してmicropostテーブルに到達し、必要なマイクロポストのページを引き出してくれます。

ここで、今作成した新しいユーザープロファイルページ (図10.5) をブラウザで見てみましょう。...何というさみしいページ、がっかりですね。マイクロポストが1つもないのでは無理もありません。ではここでマイクロポストを追加しましょう。

user_profile_no_microposts_bootstrap
図10.5マイクロポスト用のコードのあるユーザープロファイルページ (ただしマイクロポストがない)。(拡大)

10.2.2マイクロポストのサンプル

10.2.1のユーザーマイクロポストのテンプレート作成作業の成果は、何とも拍子抜けでした。9.3.2のサンプル生成にマイクロポストを追加し、この情けない状況を修正しましょう。すべてのユーザーにサンプルマイクロポストを追加しようとすると時間がかかりすぎるので、最初の6人のユーザー3だけを選択しましょう。そのためには、User.allメソッドに:limitオプションを渡します4

users = User.all(limit: 6)

その後、各ユーザーに50のマイクロポスト (ページネーションが切り替わる30を超える数) を作成し、Faker gemの便利なLorem.sentenceメソッドを使って各マイクロポストのサンプルコンテンツを生成します (Faker::Lorem.sentenceは、いわゆるlorem ipsumダミーテキストを返します。第6章で述べたように、lorem ipsumには面白い裏話があります)。 変更の結果は、リスト10.20のようなサンプルデータ生成になります。

リスト10.20 サンプルデータにマイクロポスト用のコードを追加する。
lib/tasks/sample_data.rake
namespace :db do
  desc "Fill database with sample data"
  task populate: :environment do
    .
    .
    .
    users = User.all(limit: 6)
    50.times do
      content = Faker::Lorem.sentence(5)
      users.each { |user| user.microposts.create!(content: content) }
    end
  end
end

もちろん、新しいサンプルデータを生成するためにはRakeタスクのdb:populateを実行する必要があります。

$ bundle exec rake db:reset
$ bundle exec rake db:populate
$ bundle exec rake db:test:prepare

それぞれのマイクロポストの情報を表示することによって、10.2.1の地道な作業がやっと報われはじめました5。実行結果を図10.6に示します。

user_profile_microposts_no_styling_bootstrap
図10.6ユーザープロファイル /users/1 とスタイルのないマイクロポスト。(拡大)

図10.6のページにはマイクロポスト固有のスタイルが与えられていないので、リスト10.21を追加して、結果のページを見てみましょう6図10.7が1番目の (ログインした) ユーザーのプロファイルページで、図10.8が2番目のユーザーのプロファイルページです。最後に図10.9は、1番目のユーザーのためのマイクロポストの2番目のページと、下部に改ページのリンクを表示しています。各マイクロポストの表示には、3つのどの場合にも、それが作成されてからの時間 ("1分​​前に投稿" など) が表示されていることに注目してください。これはリスト10.18time_ago_in_wordsメソッドによるものです。数分待ってからページを再度読み込むと、このテキストは自動的に新しい時間に基づいて更新されます。

リスト10.21 マイクロポスト用のCSS (この章全般にわたって使用するCSSも含む)
app/assets/stylesheets/custom.css.scss
.
.
.
/* microposts */

.microposts {
  list-style: none;
  margin: 10px 0 0 0;

  li {
    padding: 10px 0;
    border-top: 1px solid #e8e8e8;
  }
}
.content {
  display: block;
}
.timestamp {
  color: $grayLight;
}
.gravatar {
  float: left;
  margin-right: 10px;
}
aside {
  textarea {
    height: 100px;
    margin-bottom: 5px;
  }
}
user_profile_with_microposts_bootstrap
図10.7ユーザープロファイル (/users/1) とマイクロポスト。(拡大)
other_profile_with_microposts_bootstrap
図10.8別ユーザーのプロファイルとマイクロポスト (/users/5)。(拡大)
user_profile_microposts_page_2_rails_3_bootstrap
図10.9マイクロポストのページネーション用リンク (/users/1?page=2). (拡大)

10.3マイクロポストを操作する

データモデリングとマイクロポスト表示テンプレートの両方が完成したので、次はWeb経由でそれらを作成するためのインターフェイスに取りかかりましょう。HTMLフォームを使用してリソース (今回の場合はMicropostsリソース) を作成する3番目の例になります7。この節では、ステータスフィード (第11章で完成させます) の最初のヒントをお見せします。最後に、ユーザーがマイクロポストをWeb経由で破棄できるようにします。

従来のRails開発の慣習と異なる箇所が1つあります。Micropostsリソースへのインターフェイスは、主にユーザーと静的ページのコントローラを経由して実行されるので、Micropostsコントローラにはneweditのようなアクションは不要ということになります。createdestroyがあれば十分です。従って、リスト10.22に示すように、Micropostsリソースへのルーティングは一層シンプルになります。その結果、リスト10.22のコードは、フルセットのルーティング (表2.3) のサブセットであるRESTfulルート (表10.2) になります。もちろん、シンプルになったということは完成度がさらに高まったということの証しであり、退化したわけではありません。第2章でscaffoldに頼りきりだった頃からここに至るまでは長い道のりでしたが、今ではscaffoldが生成するような複雑なコードはほとんど不要になりました。

リスト10.22 Micropostsリソース用のルーティング。
config/routes.rb
SampleApp::Application.routes.draw do
  resources :users
  resources :sessions,   only: [:new, :create, :destroy]
  resources :microposts, only: [:create, :destroy]
  .
  .
  .
end
HTTPリクエストURLアクション用途
POST/micropostscreate新しいマイクロポストを作る
DELETE/microposts/1destroyid1のマイクロポストを削除する
表10.2Micropostsリソースが提供するリスト10.22のRESTfulルート。

10.3.1アクセス制御

Micropostsリソースの開発の手始めは、Micropostsコントローラ内のアクセス制御から始めることにしましょう。ここでのアクセス制御のポイントは単純です。createアクションとdestoryアクションは、いずれもユーザーがサインインしていなければ実行できないものとします。これをテストするためのRSpecコードをリスト10.23に示します (マイクロポストをポストしたユーザーだけが削除できるようにする3番目の保護は10.3.4で追加します)。

リスト10.23 マイクロポスト用のアクセス制御テスト。
spec/requests/authentication_pages_spec.rb
require 'spec_helper'

describe "Authentication" do
  .
  .
  .
  describe "authorization" do

    describe "for non-signed-in users" do
      let(:user) { FactoryGirl.create(:user) }
      .
      .
      .
      describe "in the Microposts controller" do

        describe "submitting to the create action" do
          before { post microposts_path }
          specify { expect(response).to redirect_to(signin_path) }
        end

        describe "submitting to the destroy action" do
          before { delete micropost_path(FactoryGirl.create(:micropost)) }
          specify { expect(response).to redirect_to(signin_path) }
        end
      end
      .
      .
      .
    end
  end
end

未制作のマイクロポストのWebインターフェースと違い、リスト10.23のコードは、リスト9.13のときの手法と同様に、それぞれのマイクロポストアクションのレベルで動作します。サインインしていないユーザーは、POSTリクエストを/microposts (post microposts_pathcreateアクションが呼び出される) に送信した場合、または DELETEリクエストを/microposts/1 (delete micropost_path(micropost)destroyアクションが呼び出される) に送信した場合にリダイレクトされます。

リスト10.23のテストにパスするためのアプリケーションコードを作成する前に、少しリファクタリングを行いましょう。 9.2.1では、signed_in_userメソッドと呼ばれるbefore_actionを使用して、サインインを必須にしたことを思い出してください (リスト9.12)。あのときは、このメソッドをUsersコントローラでしか使用しませんでしたが、今回はMicropostsコントローラでもこのメソッドが必要となることがわかったので、リスト10.24に示すように、セッションヘルパーに移動します8

リスト10.24 signed_in_userメソッドをセッションヘルパーに移動する。
app/helpers/sessions_helper.rb
module SessionsHelper
  .
  .
  .
  def current_user?(user)
    user == current_user
  end

  def signed_in_user
    unless signed_in?
      store_location
      redirect_to signin_url, notice: "Please sign in."
    end
  end
  .
  .
  .
end

コードが重複しないよう、signed_in_userをUsersコントローラからも削除しておきましょう。

リスト10.24のコードにより、signed_in_user メソッドがMicropostsコントローラで利用可能になりました。これにより、リスト10.25で示すようにcreateアクションとdestroyアクションに対してbefore_actionを使用してアクセス制限をかけることが可能になりました (注意: Micropostsコントローラファイルをコマンドラインで生成していなかったので、このコントローラを手動で作成する必要があります)。

リスト10.25 Micropostsコントローラのアクションに認証を追加する。
app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
  before_action :signed_in_user

  def create
  end

  def destroy
  end
end

before_actionはデフォルトで両方のアクションに適用されるため、制限を適用するアクションを明示していないことに注意してください。もし仮にindexアクションを追加し、サインインしていないユーザーでもアクセス可能にしたい場合は、以下のようにindexアクション以外のアクションを明示的に指定する必要があります。

class MicropostsController < ApplicationController
  before_action :signed_in_user, only: [:create, :destroy]

  def index
  end

  def create
  end

  def destroy
  end
end

これでテストにパスするはずです。

$ bundle exec rspec spec/requests/authentication_pages_spec.rb

10.3.2マイクロポストを作成する

第7章では、HTTP POSTリクエストをUsersコントローラのcreateアクションに発行するHTMLフォームを作成することで、ユーザーのサインアップを実装しました。マイクロポスト作成の実装もこれと似ています。主な違いは、別の micropost/new ページを使う代わりに、(Twitterに習って) ホームページ自体 (つまりルートパス) にフォームを置くという点です (図10.10のモックアップ参照)。

home_page_with_micropost_form_mockup_bootstrap
図10.10マイクロポスト作成フォームのあるホームページのモックアップ。(拡大)

最後にホームページを実装したときは (図5.6)、[Sign up now!] ボタンが中央にありました。マイクロポスト作成フォームは、サインインしている特定のユーザーのコンテキストでのみ機能するので、この節の一つの目標は、ユーザーのサインインの状態に応じて、ホームページの表示を変更することです。実装はリスト10.28で行いますが、先にテストを作成しましょう。Usersリソースの場合と同様に、結合テストを使用します。

$ rails generate integration_test micropost_pages

従って、マイクロポスト作成のテストは、リスト7.16のユーザー作成用テストと似ています。このテストをリスト10.26に示します。

リスト10.26 マイクロポスト作成のテスト。
spec/requests/micropost_pages_spec.rb
require 'spec_helper'

describe "Micropost pages" do

  subject { page }

  let(:user) { FactoryGirl.create(:user) }
  before { sign_in user }

  describe "micropost creation" do
    before { visit root_path }

    describe "with invalid information" do

      it "should not create a micropost" do
        expect { click_button "Post" }.not_to change(Micropost, :count)
      end

      describe "error messages" do
        before { click_button "Post" }
        it { should have_content('error') }
      end
    end

    describe "with valid information" do

      before { fill_in 'micropost_content', with: "Lorem ipsum" }
      it "should create a micropost" do
        expect { click_button "Post" }.to change(Micropost, :count).by(1)
      end
    end
  end
end

次に、マイクロポストのcreateアクションを作成しましょう。このアクションも、リスト7.26のユーザー用アクションと似ています。違いは、新しいマイクロポストをbuildするためにuser/micropost関連付けを使用している点です (リスト10.27)。micropost_paramsでStrong Parametersを使用していることにより、マイクロポストのコンテンツだけがWeb経由で編集可能になっていることに注目してください。

リスト10.27 Micropostsコントローラのcreateアクション。
app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
  before_action :signed_in_user

  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を提供するコードを使用します (リスト10.28)。

リスト10.28 Homeページ (/) にマイクロポスト作成を追加する。
app/views/static_pages/home.html.erb
<% if signed_in? %>
  <div class="row">
    <aside class="span4">
      <section>
        <%= render 'shared/user_info' %>
      </section>
      <section>
        <%= render 'shared/micropost_form' %>
      </section>
    </aside>
  </div>
<% else %>
  <div class="center hero-unit">
    <h1>Welcome to the Sample App</h1>

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

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

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

if-else分岐を使用してコードを書き分けている点が少し汚いですが、このコードのクリーンアップは演習に回すことにします (10.5)。なお、リスト10.28で使用するパーシャルの実装は必須なので、演習にはしません。新しいホームページのサイドバーはリスト 10.29で、マイクロポストフォームのパーシャルは リスト 10.30で実装します。

リスト10.29 ユーザー情報サイドバーのパーシャル。
app/views/shared/_user_info.html.erb
<a href="<%= user_path(current_user) %>">
  <%= gravatar_for current_user, size: 52 %>
</a>
<h1>
  <%= current_user.name %>
</h1>
<span>
  <%= link_to "view my profile", current_user %>
</span>
<span>
  <%= pluralize(current_user.microposts.count, "micropost") %>
</span>

リスト9.24と同様、リスト10.29のコードでもリスト7.30で定義したgravatar_forヘルパーを使用します。

プロファイルサイドバー (リスト10.17) のときと同様、リスト10.29のユーザー情報にも、そのユーザーが投稿したマイクロポストの総数が表示されていることに注目してください。ただし少し表示に違いがあります。プロファイルサイドバーでは、 “Microposts” をラベルとし、“Microposts (1)” と表示することは問題ありません。しかし今回のように “1 microposts” と表示してしまうと英語の文法上誤りになってしまうので、pluralizeメソッドを使用して “1 micropost” や “2 microposts” と表示するように調整します。

次はマイクロポスト作成フォームを定義します (リスト10.30)。これはユーザー登録フォームに似ています (リスト7.17)。

リスト10.30 マイクロポスト作成フォームのパーシャル。
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-large btn-primary" %>
<% end %>

リスト10.30のフォームが動くようにするためには、2箇所の変更が必要です。1つは、(以前と同様) 関連付けを使用して以下のように@micropostを定義することです。

@micropost = current_user.microposts.build

変更の結果をリスト10.31に示します。

リスト10.31 homeアクションにマイクロポストのインスタンス変数を追加する。
app/controllers/static_pages_controller.rb
class StaticPagesController < ApplicationController

  def home
    @micropost = current_user.microposts.build if signed_in?
  end
  .
  .
  .
end

リスト10.31のコードは、ユーザーへのサインイン要求を実装し忘れた場合に、テストが失敗して知らせてくれるので助かります。

リスト10.30を動かすための第2の変更は、以下のようにエラーメッセージパーシャルを再定義することです。

<%= render 'shared/error_messages', object: f.object %>

リスト7.23ではエラーメッセージパーシャルが@user変数を直接参照していたことを思い出してください。今回は代わりに@micropost変数を使う必要があります。どのような種類のオブジェクトを渡されてもエラーメッセージパーシャルが動くようにする必要があります。幸い、フォーム変数ff.objectとすることによって、関連付けられたオブジェクトにアクセスすることができます。従って、以下のコードの場合

form_for(@user) do |f|

f.object@userとなり、以下のコードの場合は

form_for(@micropost) do |f|

f.object@micropostとなります。

パーシャルにオブジェクトを渡すために、値がオブジェクトで、キーがパーシャルでの変数名と同じハッシュを利用します。これで、以下のコードが完成します。

<%= render 'shared/error_messages', object: f.object %>

言い換えると、object: f.objecterror_messagesパーシャルの中でobjectという変数名を作成します。リスト10.32に示すように、このobject変数を使用してカスタムエラーメッセージを作成できます。

リスト10.32 他のオブジェクトでも動作するようにリスト7.24のエラーメッセージパーシャルを更新する。
app/views/shared/_error_messages.html.erb
<% if object.errors.any? %>
  <div id="error_explanation">
    <div class="alert alert-error">
      The form contains <%= pluralize(object.errors.count, "error") %>.
    </div>
    <ul>
    <% object.errors.full_messages.each do |msg| %>
      <li>* <%= msg %></li>
    <% end %>
    </ul>
  </div>
<% end %>

この時点で、リスト10.26のテストはパスするはずです。

$ bundle exec rspec spec/requests/micropost_pages_spec.rb

残念ながら、サインアップと編集フォームが古いバージョンのメッセージパーシャルを利用しているため、Userのrequest specが壊れてしまいました。これを修正するために、リスト10.33およびリスト10.34で示すように、これらのフォームをより汎用的なバージョンに更新します。(: もし9.6の演習で、リスト9.49およびリスト9.50のコードを「必要に応じて流用 (Mutatis mutandis)」して実装していた場合、異なるコードを使用する必要があります。)

リスト10.33 ユーザーサインアップエラーの表示を更新する。
app/views/users/new.html.erb
<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>

<div class="row">
  <div class="span6 offset3">
    <%= form_for(@user) do |f| %>
      <%= render 'shared/error_messages', object: f.object %>
      .
      .
      .
    <% end %>
  </div>
</div>
リスト10.34 ユーザー編集のエラーを更新する。
app/views/users/edit.html.erb
<% provide(:title, "Edit user") %>
<h1>Update your profile</h1>

<div class="row">
  <div class="span6 offset3">
    <%= form_for(@user) do |f| %>
      <%= render 'shared/error_messages', object: f.object %>
      .
      .
      .
    <% end %>

    <%= gravatar_for(@user) %>
    <a href="http://gravatar.com/emails">change</a>
  </div>
</div>

この時点で、すべてのテストがパスするはずです。

$ bundle exec rspec spec/

さらに、この章で作成したすべてのHTMLが適切に表示されるようになったはずです。最終的なフォームを図10.11に、投稿エラーが表示されたフォームを図10.12に示します。この時点で、新しい投稿を自分自身で作成し、すべてが正しく動いていることを確認することもできます。ただし、できれば10.3.3が終わってからにしてください。

home_with_form_bootstrap
図10.11新しいマイクロポストフォームのあるHomeページ (/)。(拡大)
home_form_errors_bootstrap
図10.12エラーが表示されたHomeページ。(拡大)

10.3.3フィードの原型

10.3.2の最後のコメントでも言及しましたが、現在のHomeページにはマイクロポストが1つも表示されていません。図10.11のフォームが正しく動作しているかどうかを確認したい場合、正しいエントリーを投稿した後、プロファイルページに移動してポストを表示すればよいのですが、これはかなり面倒な作業です。 図10.13のモックアップで示したような、ユーザー自身のポストを含むマイクロポストフィードがないと不便です (第11章ではフィードを汎用化し、現在のユーザーによってフォローされているユーザーのマイクロポストも一緒に表示するフィードにする予定です)。

proto_feed_mockup_bootstrap
図10.13 (プロト)フィードのあるホームページのモックアップ。(拡大)

すべてのユーザーがフィードを持つので、feedメソッドはUserモデルに作るのが自然です。最終的にはそのfeedメソッドが、フォローしているユーザーのマイクロポストも返すことをテストしますが、今は、feedメソッドが自分のマイクロポストは含むが他ユーザーのマイクロポストは含まないことをテストすることにします。その要件をコード化したものをリスト10.35に示します。

リスト10.35 (プロト) ステータスフィードのテスト。
spec/models/user_spec.rb
require 'spec_helper'

describe User do
  .
  .
  .
  it { should respond_to(:microposts) }
  it { should respond_to(:feed) }
  .
  .
  .
  describe "micropost associations" do

    before { @user.save }
    let!(:older_micropost) do
      FactoryGirl.create(:micropost, user: @user, created_at: 1.day.ago)
    end
    let!(:newer_micropost) do
      FactoryGirl.create(:micropost, user: @user, created_at: 1.hour.ago)
    end
    .
    .
    .
    describe "status" do
      let(:unfollowed_post) do
        FactoryGirl.create(:micropost, user: FactoryGirl.create(:user))
      end

      its(:feed) { should include(newer_micropost) }
      its(:feed) { should include(older_micropost) }
      its(:feed) { should_not include(unfollowed_post) }
    end
  end
end

このテストでは、与えられた要素が配列に含まれているかどうかをチェックするinclude?メソッドを使用しています9

$ rails console
>> a = [1, "foo", :bar]
>> a.include?("foo")
=> true
>> a.include?(:bar)
=> true
>> a.include?("baz")
=> false

上の例は、RSpecの論理値変換機能の柔軟性が極めて高いことを示しています。includeメソッドは本来、モジュールをインクルードするために使用するRubyキーワードであるにもかかわらず (リスト8.14など)、このRSpecのコンテキストでは配列が要素を含んでいるかどうかをテストしたいという開発者の意図を正確に推測してくれます。

リスト10.36で示すように、user_idが現在のユーザーidと等しいマイクロポストを見つけることによって、適切なマイクロポストのfeedメソッドを実装することができます。これはMicropostモデルでwhereメソッドを使用することで実現できます10

リスト10.36 マイクロポストステータスフィードの実装を準備する。
app/models/user.rb
class User < ActiveRecord::Base
  .
  .
  .
  def feed
    # このコードは準備段階です。
    # 完全な実装は第11章「ユーザーをフォローする」を参照してください。
    Micropost.where("user_id = ?", id)
  end
  .
  .
  .
end

以下のコードで使用されている疑問符は、セキュリティ上重要な役割を果たしています。

Micropost.where("user_id = ?", id)

上の疑問符があることで、SQLクエリにインクルードされる前にidが適切にエスケープされることを保証してくれるため、SQLインジェクションと呼ばれる深刻なセキュリティホールを避けることができます。この場合のid属性は単なる整数であるため危険はありませんが、SQL文にインクルードされる変数を常にエスケープする習慣はぜひ身につけてください。

注意深い読者は、リスト10.36のコードは本質的に以下のコードと同等であることに気付くかもしれません。

def feed
  microposts
end

上のコードを使用せずにリスト10.36のコードを利用したのは、第11章で必要となるフルステータスフィードで応用が効くためです。

ステータスフィードの表示をテストするために、まずいくつかのマイクロポストを作成し、それぞれのページに表示されるリスト要素 (li)を検証します(リスト10.37)。

リスト10.37 ホームページのフィード表示をテストする。
spec/requests/static_pages_spec.rb
require 'spec_helper'

describe "Static pages" do

  subject { page }

  describe "Home page" do
    .
    .
    .
    describe "for signed-in users" do
      let(:user) { FactoryGirl.create(:user) }
      before do
        FactoryGirl.create(:micropost, user: user, content: "Lorem ipsum")
        FactoryGirl.create(:micropost, user: user, content: "Dolor sit amet")
        sign_in user
        visit root_path
      end

      it "should render the user's feed" do
        user.feed.each do |item|
          expect(page).to have_selector("li##{item.id}", text: item.content)
        end
      end
    end
  end
  .
  .
  .
end

リスト10.37のコードでは、以下のように各フィード項目が固有のCSS idを持つことを前提にしています。

expect(page).to have_selector("li##{item.id}", text: item.content)

それにより、上のコードが各アイテムに対してマッチするようにするのが目的です (li##{item.id}の最初の# は CSS idを示す Capybara独自の文法で、2番目の#は Rubyの式展開#{}の先頭部分であることに注意してください)。

サンプルアプリケーションでフィードを使うために、カレントユーザーの (ページネーション) フィードに@feed_itemsインスタンス変数を追加し (リスト10.38)、次にフィードパーシャル (リスト10.39) をHomeページに追加します (リスト10.41)。(ページネーションのテストは演習に回すことにします。10.5参照)。

リスト10.38 homeアクションにフィードのインスタンス変数を追加する。
app/controllers/static_pages_controller.rb
class StaticPagesController < ApplicationController

  def home
    if signed_in?
      @micropost  = current_user.microposts.build
      @feed_items = current_user.feed.paginate(page: params[:page])
    end
  end
  .
  .
  .
end
リスト10.39 ステータスフィードのパーシャル。
app/views/shared/_feed.html.erb
<% if @feed_items.any? %>
  <ol class="microposts">
    <%= render partial: 'shared/feed_item', collection: @feed_items %>
  </ol>
  <%= will_paginate @feed_items %>
<% end %>

ステータスフィードのパーシャルは以下のコードを使うという点で、フィードアイテムのパーシャルに表示されるフィードアイテムと異なります。

<%= render partial: 'shared/feed_item', collection: @feed_items %>

ここでは、フィードアイテムとして:collectionパラメーターを渡しているので、renderはコレクションの各アイテムを表示するために与えられたパーシャル (この場合は’feed_item’)を使用してくれます。(以前の表示では、render ’shared/micropost’のように:partialパラメーターを省略していましたが、:collectionパラメーターがある場合はこの記法では正常に動作しません。) フィードアイテムのパーシャルをリスト10.40に示します。

リスト10.40 単一のフィードアイテム用のパーシャル
app/views/shared/_feed_item.html.erb
<li id="<%= feed_item.id %>">
  <%= link_to gravatar_for(feed_item.user), feed_item.user %>
  <span class="user">
    <%= link_to feed_item.user.name, feed_item.user %>
  </span>
  <span class="content"><%= feed_item.content %></span>
  <span class="timestamp">
    Posted <%= time_ago_in_words(feed_item.created_at) %> ago.
  </span>
</li>

リスト10.40は、以下のコードを使用して各フィードにCSS idも追加します。

<li id="<%= feed_item.id %>">

これはリスト10.37のテストで要求されていたものです。

後は、いつものようにフィードパーシャルを表示すればHomeページにフィードを追加できます (リスト10.41)。この結果はホームページのフィードとして表示されます (図10.14)。

リスト10.41 Homeページにステータスフィードを追加する。
app/views/static_pages/home.html.erb
<% if signed_in? %>
  <div class="row">
    .
    .
    .
    <div class="span8">
      <h3>Micropost Feed</h3>
      <%= render 'shared/feed' %>
    </div>
  </div>
<% else %>
  .
  .
  .
<% end %>
home_with_proto_feed_bootstrap
図10.14 (プロト) フィードのあるホームページ(/)。(拡大)

現時点では、新しいマイクロポストの作成は図10.15で示したように期待どおりに動作しています。ただしささいなことではありますが、マイクロポストの投稿が失敗すると、 Homeページは@feed_itemsインスタンス変数を期待しているため、現状では壊れてしまいます (このことはテストスイートを実行して確認できます)。最も簡単な解決方法は、リスト10.42のように空の配列を渡しておくことです11

micropost_created_bootstrap
図10.15新しいマイクロポストを作成後のHomeページ。(拡大)
リスト10.42 createアクションに (空の) @feed_itemsインスタンス変数を追加する。
app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
  .
  .
  .
  def create
    @micropost = current_user.microposts.build(micropost_params)
    if @micropost.save
      flash[:success] = "Micropost created!"
      redirect_to root_url
    else
      @feed_items = []
      render 'static_pages/home'
    end
  end
  .
  .
  .
end

この時点で、(プロト)フィードとそのテストはすべて動くはずです。

$ bundle exec rspec spec/

10.3.4マイクロポストを削除する

最後の機能として、マイクロポストリソースにポストを削除する機能を追加します。これはユーザー削除と同様に(9.4.2)、"delete" リンクで実現します (図10.16)。ユーザーの削除は管理者ユーザーのみが行えるように制限されていたのに対し、今回の場合はカレントユーザーが作成したマイクロポストに対してのみ削除リンクが動作するようにします。

micropost_delete_links_mockup_bootstrap
図10.16マイクロポストの削除リンクと (プロト) フィードのモックアップ。(拡大)

最初のステップとして、リスト10.40のマイクロポストパーシャルに削除リンクを追加します。同様に、リスト10.40のフィードアイテムパーシャルにリンクを追加します。追加の結果をリスト10.43およびリスト10.44に示します (これら2つはほとんど同一です。重複コードの削除は演習に回すことにします (10.5))。

リスト10.43 マイクロポストパーシャルに削除リンクを追加する。
app/views/microposts/_micropost.html.erb
<li>
  <span class="content"><%= micropost.content %></span>
  <span class="timestamp">
    Posted <%= time_ago_in_words(micropost.created_at) %> ago.
  </span>
  <% if current_user?(micropost.user) %>
    <%= link_to "delete", micropost, method: :delete,
                                     data: { confirm: "You sure?" },
                                     title: micropost.content %>
  <% end %>
</li>
リスト10.44 フィードアイテムパーシャルに削除リンクを追加する。
app/views/shared/_feed_item.html.erb
<li id="<%= feed_item.id %>">
  <%= link_to gravatar_for(feed_item.user), feed_item.user %>
    <span class="user">
      <%= link_to feed_item.user.name, feed_item.user %>
    </span>
    <span class="content"><%= feed_item.content %></span>
    <span class="timestamp">
      Posted <%= time_ago_in_words(feed_item.created_at) %> ago.
    </span>
  <% if current_user?(feed_item.user) %>
    <%= link_to "delete", feed_item, method: :delete,
                                     data: { confirm: "You sure?" },
                                     title: feed_item.content %>
  <% end %>
</li>

マイクロポスト削除をテストするには、Capybaraを使用して "delete" リンクをクリックし、マイクロポストのカウントが1減っていることを確認します (リスト10.45)。

リスト10.45 Micropostsコントローラのdestroyアクションをテストする。
spec/requests/micropost_pages_spec.rb
require 'spec_helper'

describe "Micropost pages" do
  .
  .
  .
  describe "micropost destruction" do
    before { FactoryGirl.create(:micropost, user: user) }

    describe "as correct user" do
      before { visit root_path }

      it "should delete a micropost" do
        expect { click_link "delete" }.to change(Micropost, :count).by(-1)
      end
    end
  end
end

リスト9.46のユーザー用アプリケーションコードと似ていますが、最も大きく異なるのは、マイクロポストの場合は、カレントユーザーが実際に指定のidのマイクロポストを持っているのかをチェックするのにadmin_user before_actionではなく、 correct_user before_actionを使用している点です。コードをリスト10.46に示します。2番目に新しいポストを削除した場合の結果を図10.17に示します。

リスト10.46 マイクロポストコントローラのdestroyアクション。
app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
  before_action :signed_in_user, only: [:create, :destroy]
  before_action :correct_user,   only: :destroy
  .
  .
  .
  def destroy
    @micropost.destroy
    redirect_to 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 before_actionでは、マイクロポストを以下のように関連付けを経由して見つけていることに注目してください。

current_user.microposts.find_by(id: params[:id])

これによって、カレントユーザーに所属するマイクロポストだけが自動的に見つかることが保証されます。この場合、 findではなくfind_byを使用します。これは、前者ではマイクロポストがない場合に例外が発生しますが、後者はnilを返すためです。ところで、Rubyの例外処理に慣れている方なら、correct_userのフィルタを以下のように書くこともできます。

def correct_user
  @micropost = current_user.microposts.find(params[:id])
rescue
  redirect_to root_url
end

Micropostモデルを以下のように直接使用してcorrect_userフィルタを実装することもできます。

@micropost = Micropost.find_by(id: params[:id])
redirect_to root_url unless current_user?(@micropost.user)

上のコードはリスト10.46のコードと同等だと思うかもしれませんが、Wolfram Arnoldがブログ記事「RailsにおけるAccess Control 101とシティバンクでのハッキング事件 (英語)」で解説しているように、対象を探しだすときには常に関連付けを経由することをセキュリティの観点から強くお勧めいたします。

home_post_delete_bootstrap
図10.172番目に新しいマイクロポストを削除した後のユーザーHomeページ。(拡大)

この節のコードで、Micropostモデルとインターフェイスが完成しました。すべてのテストがパスするはずです。

$ bundle exec rspec spec/

10.4最後に

Micropostsリソースの追加によって、サンプルアプリケーションはほぼ完成に近づきました。残すところは、ユーザーをお互いにフォローするソーシャルレイヤーのみとなりました。第11章では、そのようなユーザー同士の関係 (リレーションシップ) をモデリングする方法を学び、それがステータスフィードにどのように関連するかを学びます。

Gitバージョン管理を使用している方は、次に進む前に変更をマージしてコミットすることを忘れないでください。

$ git add .
$ git commit -m "Add user microposts"
$ git checkout master
$ git merge user-microposts
$ git push

この時点でHerokuにアプリをプッシュしてもよいでしょう。micropostsテーブルを追加するためにデータモデルを変更したので、本番データベースをマイグレートする必要があります。

$ git push heroku
$ heroku pg:reset DATABASE
$ heroku run rake db:migrate
$ heroku run rake db:populate

10.5演習

ここまでに数多くの題材を取り上げてきましたので、今やアプリケーションを拡張する方法は山ほどあります。以下は、その中のごくわずかに過ぎません。

  1. サイドバーのマイクロポストカウントのテストを追加してください。このとき、表示に単数形と複数形が正しく表示されているかどうかもテストしてください。
  2. マイクロポストのページネーションのテストを追加してください。
  3. if-else文の2つの分岐に対して、それぞれ異なるパーシャルを使用するようにホームページをリファクタリングしてください。
  4. 削除リンクが、現在のユーザーによって作成されていないマイクロポストには表示されないことを確認するためのテストを作成してください。
  5. パーシャルを使用して、リスト10.43およびリスト10.44から削除リンクの重複を削除してください。
  6. 図10.18に示すように、非常に長い単語を入力するとレイアウトが崩れてしまいます。リスト10.47で定義したwrapヘルパーを使用して、この問題を修正してください。このとき、出力されたHTMLがRailsによってエスケープされるのを防ぐためにrawメソッドを使用してください。また、クロスサイトスクリプティング (XSS) を防ぐためにsanitizeメソッドも使用してください。さらに、このコードでは3項演算子 (コラム  10.1) も使用してください。3項演算子は一見奇妙ですが、極めて便利なものです。
  7. (チャレンジ) 入力できる残り文字数を140 文字からカウントダウンするためのJavaScriptをHomeページに追加してください。
long_word_micropost_bootstrap
図10.18非常に長い単語によって崩れたレイアウト。(拡大)
リスト10.47 長い単語をラップさせるヘルパー。
app/helpers/microposts_helper.rb
module MicropostsHelper

  def wrap(content)
    sanitize(raw(content.split.map{ |s| wrap_long_string(s) }.join(' ')))
  end

  private

    def wrap_long_string(text, max_width = 30)
      zero_width_space = "&#8203;"
      regex = /.{1,#{max_width}}/
      (text.length < max_width) ? text :
                                  text.scan(regex).join(zero_width_space)
    end
end
  1. 技術的には、第8章でセッションをリソースとして扱いましたが、セッションはユーザーやマイクロポストと異なり、データベースには保存されません。
  2. content属性はstring型になりますが、 2.1.2で簡単に述べたように、長文用のテキストフィールドにはtext型を使用してください。
  3. (ここではカスタムGravatarを持つ5人のユーザーと、デフォルトGravatarを持つ1人のユーザー) 
  4. もしこのメソッドが生成するSQLに興味があるのであれば、log/development.logをtailしてみてください (コマンドラインでファイルにtailコマンドを実行するという意味)。 
  5. Faker gemのlorem ipsumサンプルテキストはランダムに生成される仕様になっているため、サンプルマイクロポストの内容はこの図と違っているはずです。
  6. 便宜上、リスト10.21はこの章で必要なCSSをすべて含んでいます。
  7. 他の2つのリソースとは、7.2のUsersリソースと8.1のSessionsリソースです。
  8. 8.2.1で、ヘルパーメソッドはデフォルトではビューでのみ利用可能であると説明しましたが、include SessionsHelperをApplicationコントローラに追加してコントローラでも利用できるようにしました (リスト8.14)。
  9. 1.1.1で、本書を読み終わったら次に純粋なRubyの本を読む事を薦めましたが、これは、include?などのメソッドを学んでいただきたいからというのが理由の一つです。
  10. whereやlikeの詳細については、Railsガイドの「Active Record クエリインターフェイス」を参照してください。
  11. 残念ですが、この場合はページ分割されたフィードを返してもうまく動きません。動かない理由を確認したい方は、実際に実装してページネーションリンクをクリックしてみてください。
第10章 ユーザーのマイクロポスト Rails 4.0 (第2版)