Ruby on Rails チュートリアル

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

第2版 目次

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

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

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

$ git checkout -b user-microposts

10.1Micropostモデル

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

10.1.1基本的なモデル

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

$ rails generate model Micropost content:string user_id:integer

リスト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カラムをさらに追加します。

それではユーザーモデルの場合 (リスト6.8) と同様に、マイクロポストデルに対する最小限のテストを書くところから始めましょう。具体的には リスト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 ブロックのコードが間違っていることを指摘しています。その理由を考えてみてください。10.1.3で解答を確認できます。

10.1.2Accessible属性と最初の検証

上の解答を見る前に、beforeブロックのコードが誤っている理由を確認するために、まずMicropostモデルに対する検証 (validation) テストを作成してみましょう (リスト10.3) (リスト6.11でのUserモデルのテストと比較してみてください)。

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

describe Micropost do

  let(:user) { FactoryGirl.create(:user) }
  before do
    # This code is wrong!
    @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
  attr_accessible :content, :user_id  
  validates :user_id, presence: true
end

これで、以下のコードが誤っている理由を見つけるための準備が整いました。

@micropost = Micropost.new(content: "Lorem ipsum", user_id: user.id)

問題となっているのは、デフォルト (Rails 3.2.3の場合) でMicropostモデルのすべての属性がアクセス可能になっていることです。6.1.2.29.4.1.1で述べたように、これは、コマンドラインのクライアントを使って悪意のあるリクエストを発行することで、誰でもマイクロポストオブジェクトのあらゆる属性を改変できることを意味します。たとえば、悪意のあるユーザがマイクロポストのuser_id属性を改変し、別のユーザにマイクロポストを関連づける事も可能です。つまり、user_idattr_accessibleリストから削除されるべきであり、またそうすることにより上記のコードはテストに失敗します。この問題は10.1.3で修正します。

10.1.3User/Micropostの関連付け

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

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自動的に正しい値に設定され、 10.1.2で述べたような問題を解決してくれます。特に、以下のような

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が自動的に設定されます。

マイクロポストをユーザと関連付けて構築できても、user_idにアクセスできてしまうというセキュリティ上の問題は解決されません。これはセキュリティ上の重要な懸念事項であるため、リスト10.5に示すように失敗するテストを記述します。

リスト10.5 user_idがアクセス不能であることを確認するテスト。
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 }
  .
  .
  .
  describe "accessible attributes" do
    it "should not allow access to user_id" do
      expect do
        Micropost.new(user_id: user.id)
      end.to raise_error(ActiveModel::MassAssignmentSecurity::Error)
    end    
  end
end

このテストは、空ではないuser_idを使用してMicropost.newを呼ぶと、mass assignment security error例外が発生することを確認しています。この挙動はRails 3.2.3ではデフォルトですが、以前のバージョンではオフにされているため、リスト10.6で示すように自分のアプリケーションが正しく設定されているかどうかを確認してください。

リスト10.6 Railsがinvalid mass assignmentエラーを発生するようにする設定。
config/application.rb
.
.
.
module SampleApp
  class Application < Rails::Application
    .
    .
    .
    config.active_record.whitelist_attributes = true
    .
    .
    .
  end
end

Micropostモデルの場合は、 Web経由で編集されてもよい属性はcontent属性のみであるため、リスト10.7で示すように:user_idをアクセス可能リストから取り除く必要があります。

リスト10.7 content属性を (そしてcontent属性のみを) アクセス可能にする。
app/models/micropost.rb
class Micropost < ActiveRecord::Base
  attr_accessible :content

  validates :user_id, presence: true
end

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

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

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

リスト10.8 マイクロポストのユーザとの関連付けのテスト。
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 == user }

  it { should be_valid }

  describe "accessible attributes" do
    it "should not allow access to user_id" do
      expect do
        Micropost.new(user_id: user.id)
      end.to raise_error(ActiveModel::MassAssignmentSecurity::Error)
    end    
  end

  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.9)。

リスト10.9 ユーザの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(:authenticate) }
  it { should respond_to(:microposts) }
  .
  .
  .
end

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

リスト10.10 マイクロポストがユーザに所属する (belongs_to) 関連付け。
app/models/micropost.rb
class Micropost < ActiveRecord::Base
  attr_accessible :content
  belongs_to :user

  validates :user_id, presence: true  
end
リスト10.11 ユーザがマイクロポストを複数所有する (has_many) 関連付け。
app/models/user.rb
class User < ActiveRecord::Base
  attr_accessible :name, :email, :password, :password_confirmation
  has_secure_password
  has_many :microposts
  .
  .
  .
end

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

$ bundle exec rspec spec/models

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

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

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

リスト10.12 マイクロポスト作成用の新しいファクトリーを含む、完全な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

ここでは、以下のようにマイクロポスト用のファクトリーの定義にユーザを含めるだけで、マイクロポストに関連付けられるユーザーのことが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時間前)、最初のポストの作成日時が1日前になります。Factory Girlを使用すると、attr_accessibleをバイパスすることによってマスアサインメントを使ってユーザーを設定することもできますし、Active Recordがアクセスを許可しないようなcreated_at属性も手動で設定できるので大変便利です (created_atupdated_atなどは通常のカラムと異なる “マジック”カラムであり、これらの作成タイムスタンプや更新タイムスタンプは自動的に設定されてしまうため、明示的に値を設定しても上書きされてしまうことを思い出してください)。

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

リスト10.13 ユーザのマイクロポストの順序をテストする。
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
      @user.microposts.should == [newer_micropost, older_micropost]
    end
  end
end

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

@user.microposts.should == [newer_micropost, older_micropost]

これは新しいポストが最初に来ることをテストしています。デフォルトでは id順に並ぶため[older_micropost, newer_micropost]の順序になりテストは失敗するはずです。また、このテストは表 10.1で示すように、 user.micropostsがマイクロポストの配列を返すことをチェックすることにより、基本的なhas_many関連付け自体の正しさも確認しています。

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

リスト10.14 default_scopeでマイクロポストを順序付ける。
app/models/micropost.rb
class Micropost < ActiveRecord::Base
  .
  .
  .
  default_scope order: 'microposts.created_at DESC'
end

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

Dependent: destroy

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

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

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

残念ですが、上のコードはRubyの配列の妙により動きません。Rubyにおける配列の代入は「参照のコピー」であり、配列全体そのもののコピーではないため、オリジナルの配列に対して何らかの変更を行うと、そのコピーにも同じ変更が行われてしまいます。たとえば以下のように、配列を作成し、2番目の変数をその配列に代入してから、reverse!メソッドを使用して最初の配列を逆順にするとします。

$ rails console
>> a = [1, 2, 3]
=> [1, 2, 3]
>> b = a
=> [1, 2, 3]
>> a.reverse!
=> [3, 2, 1]
>> a
=> [3, 2, 1]
>> b
=> [3, 2, 1]

驚くかもしれませんが、上のコードでは、aが逆転しただけではなく、bまで逆転されてしまっています。これは、abが同じ配列を指しているためです (同じことは、文字列とハッシュなど、他のRubyのデータ構造でも発生します)。

ユーザのマイクロポストの場合には、こうなります。

$ rails console --sandbox
>> @user = User.first
>> microposts = @user.microposts
>> @user.destroy
>> microposts
=> []

(この時点では、関連付けられたマイクロポストの破棄を実装していないので、上のコードは動作しません。原理を説明するためだけに書いています。)ここでは、ユーザオブジェクトを破棄しても、microposts変数は空の配列[]として残されていることがわかります。

この「参照がコピーされる」動作は、Rubyのオブジェクトの複製を行うときに多大な注意を払わないといけないことを意味します。配列などの比較的単純なオブジェクトを複製するには、dupメソッドを使用することができます。

$ rails console
>> a = [1, 2, 3]
=> [1, 2, 3]
>> b = a.dup
=> [1, 2, 3]
>> a.reverse!
=> [3, 2, 1]
>> a
=> [3, 2, 1]
>> b
=> [1, 2, 3]

(このような比較的単純なオブジェクトの複製作業は “shallow copy” として知られています。より複雑なオブジェクトを複製する "deep copy" を実装することははるかに難しい問題であり、実際に一般的な解決策はありませんが、検索エンジンで "ruby deep copy" を検索すると、ネストした配列のような、より複雑な構造をコピーする必要がある場合の解決策が見つかると思います。)ユーザーのマイクロポストにdupメソッドを適用すると、次のようなコードになります。

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

上のコードには、以下の行を追加しました。

microposts.should_not be_empty

上の行は、dupが誤って消されてしまった場合にエラーを検出できるように、セーフティチェックとして追加しました3。完全な実装をリスト10.15に示します。

リスト10.15 ユーザーを破棄するとマイクロポストも破棄されることをテストする。
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.dup
      @user.destroy
      microposts.should_not be_empty
      microposts.each do |micropost|
        Micropost.find_by_id(micropost.id).should be_nil
      end
    end
  end
  .
  .
  .
end

上のコードでは、レコードが見つからない場合はnilを返す Micropost.find_by_idを使用しました。一方、 Micropost.findは例外が発生するため、テストするのが多少難しくなってしまいます。(気になっているかもしれないので書いておくと、findを使用する場合は以下のようになります。

lambda do 
  Micropost.find(micropost.id)
end.should raise_error(ActiveRecord::RecordNotFound)

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

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

リスト10.16 ユーザのマイクロポストがユーザと一緒に破棄されることを保証するコード。
app/models/user.rb
class User < ActiveRecord::Base
  attr_accessible :name, :email, :password, :password_confirmation
  has_secure_password
  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.17のように書きます。

リスト10.17 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.17ではマイクロポストの長さの検証コードをテストするために、文字列の乗算を使用しています。

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

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

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

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

リスト10.18 Micropostモデルの検証。
app/models/micropost.rb
class Micropost < ActiveRecord::Base
  attr_accessible :content

  belongs_to :user

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

  default_scope order: 'microposts.created_at DESC'
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.13と同様に即座に関連付けを作成できるようにしたいと思います。そこで、今回も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.19のコードを使って、プロファイルページの外観をテストできるようになります。

リスト10.19 ユーザの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_selector('h1',    text: user.name) }
    it { should have_selector('title', text: 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.19のテストはリスト10.21まではパスしませんが、リスト10.20に示されているように、まずはユーザープロファイルページにマイクロポストの一覧を挿入するところからアプリケーションコードの実装を始めることにしましょう。

リスト10.20 ユーザーの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.20では、if @user.microposts.any?を使用することによって (以前リスト7.23でも使用しました) ユーザのマイクロポストが1つもない場合に空のリストが表示されないようにしています。

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

<%= will_paginate @microposts %>

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

<%= will_paginate %>

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

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

<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.21に示すように、_micropost.html.erbパーシャルを (micropost用のビューのディレクトリの中に) 定義しなければならないことを意味します。

リスト10.21 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.19のテストは、たった1つ、@microposts変数がないために失敗していたはずです。リスト10.22のように変更することで、テストにパスさせることができます。

リスト10.22 @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人のユーザー4だけを選択しましょう。そのためには、User.allメソッドにlimitオプションを渡します5

users = User.all(limit: 6)

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

リスト10.23 サンプルデータにマイクロポスト用のコードを追加する。
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の地道な作業が、それぞれのマイクロポストの情報を表示することによって報われはじめました6。実行結果を図10.6に示します。

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

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

リスト10.24 マイクロポスト用の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番目の例になります8。この節では、ステータスフィード(第11章で完成させます) の最初のヒントをお見せします。最後に、ユーザーがマイクロポストをWeb経由で破棄できるようにします。

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

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

10.3.1アクセス制御

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

リスト10.26 マイクロポスト用のアクセス制御テスト。
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 { response.should redirect_to(signin_path) }
        end

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

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

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

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

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

  def create
  end

  def destroy
  end
end

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

class MicropostsController < ApplicationController
  before_filter :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.31で行いますが、先にテストを作成しましょう。Usersリソースの場合と同様に、結合テストを使用します。

$ rails generate integration_test micropost_pages

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

リスト10.29 マイクロポスト作成のテスト。
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.25のユーザー用アクションと似ています。違いは、新しいマイクロポストをbuildするためにuser/micropost関連付けを使用している点です (リスト10.30)。

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

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

  def destroy
  end
end

マイクロポスト作成フォームを構築するために、サイト訪問者がサインインしているかどうかに応じて異なるHTMLを提供するコードを使用します (リスト10.31)。

リスト10.31 ホームページ (/) にマイクロポスト作成を追加する。
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.31パーシャルの実装は必須なので、演習にはしません。新しいHomeページのサイドバーはリスト10.32で、 マイクロポストフォームのパーシャルはリスト10.33で実装します。

リスト10.32 ユーザー情報サイドバーのパーシャル。
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.25と同様、リスト10.32のコードでもリスト7.29で定義したgravatar_forヘルパーを使用します。

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

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

リスト10.33 マイクロポスト作成フォームのパーシャル。
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.33のフォームが動くようにするためには、2箇所の変更が必要です。1つは、(以前と同様) 関連付けを使用して以下のように@micropostを定義することです。

@micropost = current_user.microposts.build

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

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

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

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

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

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

リスト7.22ではエラーメッセージパーシャルが@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.35に示すように、このobject変数を使用してカスタマイズエラーメッセージを作成できます。

リスト10.35 他のオブジェクトでも動作するようにリスト7.23のエラーメッセージパーシャルを更新する。
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.29のテストはパスするはずです。

$ bundle exec rspec spec/requests/micropost_pages_spec.rb

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

リスト10.36 ユーザーサインアップエラーの表示を更新する。
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.37 ユーザー編集のエラーを更新する。
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_form_errors_bootstrap
図10.12エラーのあるホームページ。(拡大)

10.3.3フィードの原型

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

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

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

リスト10.38 (プロト) ステータスフィードのテスト。
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?メソッドを使用しています10

$ 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.39で示すように、user_idが現在のユーザーidと等しいマイクロポストを見つけることによって、適切なマイクロポストのfeedメソッドを実装することができます。これはMicropostモデルでwhereメソッドを使用することで実現できます11

リスト10.39 マイクロポストステータスフィードの実装を準備する。
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.39のコードは本質的に以下のコードと等しいことに気付くかもしれません。

def feed
  microposts
end

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

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

リスト10.40 ホームページのフィード表示をテストする。
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|
          page.should have_selector("li##{item.id}", text: item.content)
        end
      end
    end
  end
  .
  .
  .
end

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

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

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

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

リスト10.41 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.42 ステータスフィードのパーシャル。
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.43に示します。

リスト10.43 単一のフィードアイテム用のパーシャル
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.43は、以下のコードを使用して各フィードにCSS idも追加します。

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

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

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

リスト10.44 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.45のように空の配列を渡しておくことです12

micropost_created_bootstrap
図10.15新しいマイクロポストを作成後のHomeページ。(拡大)
リスト10.45 createアクションに (空の) @feed_itemsインスタンス変数を追加する。
app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
  .
  .
  .
  def create
    @micropost = current_user.microposts.build(params[:micropost])
    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.43のマイクロポストパーシャルに削除リンクを追加します。同様に、リスト10.43のフィードアイテムパーシャルにリンクを追加します。追加の結果をリスト10.46およびリスト10.47に示します (これら2つはほとんど同一です。重複コードの削除は演習に回すことにします (10.5))。

リスト10.46 マイクロポストパーシャルに削除リンクを追加する。
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.47 フィードアイテムパーシャルに削除リンクを追加する。
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.48)。

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

リスト10.49 マイクロポストコントローラのdestroyアクション。
app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
  before_filter :signed_in_user, only: [:create, :destroy]
  before_filter :correct_user,   only: :destroy
  .
  .
  .
  def destroy
    @micropost.destroy
    redirect_to root_url
  end

  private

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

correct_user before_filterでは、マイクロポストを以下のように関連付けを経由して見つけていることに注目してください。

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

これによって、カレントユーザーに所属するマイクロポストだけが自動的に見つかることが保証されます。この場合、 findではなくfind_by_idを使用します。これは、前者ではマイクロポストがない場合に例外が発生しますが、後者は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.49のコードと同等だと思うかもしれませんが、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.46およびリスト10.47から削除リンクの重複を削除してください。
  6. 図10.18に示すように、非常に長い単語を入力するとレイアウトが崩れてしまいます。リスト10.50で定義したwrapヘルパーを使用して、この問題を修正してください。このとき、出力されたHTMLがRailsによってエスケープされるのを防ぐためにrawメソッドを使用してください。また、クロスサイトスクリプティング (XSS) を防ぐためにsanitizeメソッドも使用してください。さらに、このコードでは3項演算子 (コラム  10.1) も使用してください。3項演算子は一見奇妙ですが、極めて便利なものです。
  7. (チャレンジ) 入力できる残り文字数を140 文字からカウントダウンするためのJavaScriptをHomeページに追加してください。
long_word_micropost_bootstrap
図10.18非常に長い単語によって崩れたレイアウト。(拡大)
リスト10.50 長い単語をラップさせるヘルパー。
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. 実は私自身、当初マイクロポストを適切に複製することを忘れており、以前のバージョンのチュートリアルで使用していたテストにはバグもありました。このようなセフティチェックを最初から行なっていれば、エラーを未然に防げたことでしょう。バグを発見して知らせてくれた読者Jacob Turinoに感謝します。
  4. (ここではカスタムGravatarを持つ5人のユーザーと、デフォルトGravatarを持つ1人のユーザー) 
  5. もしこのメソッドが生成するSQLに興味があるのであれば、log/development.logをtailしてみてください (コマンドラインでファイルにtailコマンドを実行するという意味)。 
  6. Faker gemのlorem ipsumサンプルテキストはランダムに生成される仕様になっているため、サンプルマイクロポストの内容はこの図と違っているはずです。
  7. 便宜上、リスト10.24はこの章で必要なCSSをすべて含んでいます。
  8. 他の2つのリソースとは、リスト7.2のユーザーと8.1のセッションです。
  9. 8.2.1で、ヘルパーメソッドはデフォルトではビューでのみ利用可能であると説明しましたが、include SessionsHelperをApplicationコントローラに追加してコントローラでも利用できるようににしました (リスト8.14)。
  10. 1.1.1で、本書を読み終わったら次に純粋なRubyの本を読む事を薦めましたが、これは、include?のようなメソッドを学んでいただきたいからというのが理由の一つです。
  11. whereやlikeの詳細については、Railsガイドの「Active Record クエリインターフェイス」を参照してください。
  12. 残念ですが、この場合はページ分割されたフィードを返してもうまく動きません。動かない理由を確認したい方は、実際に実装してページネーションリンクをクリックしてみてください。
前の章
第10章 ユーザーのマイクロポスト
次の章