Ruby on Rails チュートリアル
-
第2版 目次
- 第1章ゼロからデプロイまで
- 第2章デモアプリケーション
- 第3章ほぼ静的なページの作成
- 第4章Rails風味のRuby
- 第5章レイアウトを作成する
- 第6章ユーザーのモデルを作成する
- 第7章ユーザー登録
- 第8章サインイン、サインアウト
- 第9章ユーザーの更新・表示・削除
- 第10章ユーザーのマイクロポスト
- 第11章ユーザーをフォローする
- 第12章Rails 4.0へのアップグレード
|
||
第2版 目次
|
Ruby on Rails チュートリアル
プロダクト開発の0→1を学ぼう
下記フォームからメールアドレスを入力していただくと、招待リンクが記載されたメールが届きます。リンクをクリックし、アカウントを有効化した時点から『30分間』解説動画のお試し視聴ができます。
メール内のリンクから視聴を開始できます。
第2版 目次
- 第1章ゼロからデプロイまで
- 第2章デモアプリケーション
- 第3章ほぼ静的なページの作成
- 第4章Rails風味のRuby
- 第5章レイアウトを作成する
- 第6章ユーザーのモデルを作成する
- 第7章ユーザー登録
- 第8章サインイン、サインアウト
- 第9章ユーザーの更新・表示・削除
- 第10章ユーザーのマイクロポスト
- 第11章ユーザーをフォローする
- 第12章Rails 4.0へのアップグレード
第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モデルはマイクロポストの内容を保存するcontent
2属性と特定のユーザとマイクロポストを関連付けるuser_id
属性の2つの属性だけを持ちます。ユーザーモデルの場合と同様に(リスト6.1)、generate model
コマンドを使って生成します。
$ rails generate model Micropost content:string user_id:integer
リスト6.2でデータベースにusers
テーブルを作るマイグレーションファイルを生成した時と同様に、このコマンドは microposts
テーブルを作成するためのマイグレーションファイルを生成します (リスト10.1)。
user_id
とcreated_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_id
とcreated_at
カラムにインデックスが付与されていることに注目してください (コラム 6.2)。user idに関連付けられたすべてのマイクロポストを作成時刻の逆順で取り出すためです。
add_index :microposts, [:user_id, :created_at]
user_id
とcreated_at
両方のカラムを1つの配列に含めることで、Active Recordで両方のキーを同時に使用する複合キーインデックスを作成できます。また6.1.1でも述べたように、t.timestamps
の行によってcreated_at
カラムとupdated_at
カラムが作成されていることにも注目してください。この後、10.1.4および10.2.1でcreated_at
カラムをさらに追加します。
それではユーザーモデルの場合 (リスト6.8) と同様に、マイクロポストデルに対する最小限のテストを書くところから始めましょう。具体的には リスト10.2 に示すように、micropostオブジェクトが content
属性とuser_id
属性を持っていることを確認するテストを作成します。
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のようになります。
テストにパスすることを確認してみましょう。
$ 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モデルのテストと比較してみてください)。
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 に示すような簡単な存在検証によって、このテストはパスします。
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.2と9.4.1.1で述べたように、これは、コマンドラインのクライアントを使って悪意のあるリクエストを発行することで、誰でもマイクロポストオブジェクトのあらゆる属性を改変できることを意味します。たとえば、悪意のあるユーザがマイクロポストのuser_id
属性を改変し、別のユーザにマイクロポストを関連づける事も可能です。つまり、user_id
はattr_accessible
リストから削除されるべきであり、またそうすることにより上記のコードはテストに失敗します。この問題は10.1.3で修正します。
10.1.3User/Micropostの関連付け
Webアプリケーション用のデータモデルを構築するにあたって、個々のモデル間での関連づけを十分考えておくことが重要です。今回の場合は、 2.3.3でも示したように、それぞれのマイクロポストは1人のユーザと関連付けられ、それぞれのユーザは (潜在的に) 複数のマイクロポストと関連付けられます。この関連付けを図10.2と図 0.3に示します。それらの関連を実装するにあたって、リスト10.2のようにではなく、リスト10.7のようにattr_accessible
を利用してMicropostモデルのテストを作成します。
このセクションで定義した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.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に示すように失敗するテストを記述します。
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で示すように自分のアプリケーションが正しく設定されているかどうかを確認してください。
config/application.rb
.
.
.
module SampleApp
class Application < Rails::Application
.
.
.
config.active_record.whitelist_attributes = true
.
.
.
end
end
Micropostモデルの場合は、 Web経由で編集されてもよい属性はcontent
属性のみであるため、リスト10.7で示すように:user_id
をアクセス可能リストから取り除く必要があります。
content
属性を (そしてcontent
属性のみを) アクセス可能にする。app/models/micropost.rb
class Micropost < ActiveRecord::Base
attr_accessible :content
validates :user_id, presence: true
end
表10.1に示したように、ユーザーとマイクロポストの関連付けの結果には、単にマイクロポストのユーザを返すmicropost.user
メソッドもあり、これもテストが必要です。このメソッドは、it
とits
メソッドを以下のように使うことでテストできます。
it { should respond_to(:user) }
its(:user) { should == user }
上のコードを反映したMicropostモデルのテストをリスト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)。
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行を加えるだけで済みます。
belongs_to
) 関連付け。app/models/micropost.rb
class Micropost < ActiveRecord::Base
attr_accessible :content
belongs_to :user
validates :user_id, presence: true
end
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.9のhas_many
関連付けのテストでは、microposts
属性の存在をほとんど検証していないので、あまり意味がありませんでした。この章では、順序と依存関係をマイクロポストに追加し、user.microposts
メソッドが実際にマイクロポストの配列を返すことをテストします。
Userモデルのテストのためにいくつかのマイクロポストを作成しておく必要がありますので、この時点でマイクロポストを生成するファクトリーを作成しておきましょう。そのためには、Factory Girlに関連付けを作成する方法を知っておく必要があります。幸い、リスト10.12に示したとおり、これは非常に簡単です。
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_at
やupdated_at
などは通常のカラムと異なる “マジック”カラムであり、これらの作成タイムスタンプや更新タイムスタンプは自動的に設定されてしまうため、明示的に値を設定しても上書きされてしまうことを思い出してください)。
(SQLiteを含む) ほとんどのデータベースアダプターは、マイクロポストを id順で返すため、リスト10.13のようなテストはほぼ確実に失敗することになります。このコードではlet
メソッドの代わりにlet!
(“let バン”と読みます) メソッドを使っています。これは、let
変数は「lazy」、つまり実際に参照されるまで初期化されない性質 (遅延) を持っており、そのことで不都合が生じるためです。ここでは、マイクロポストを遅延することなく即座に作成する必要があります。そのことにより、マイクロポストのタイムスタンプが常に正しい順序で作成されるように、かつ@user.microposts
が空の状態が生じることのないようにしたいからです。let!
を使用すれば、対応する変数を強制的に即座に作成できます。
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章で説明します)。
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
まで逆転されてしまっています。これは、a
とb
が同じ配列を指しているためです (同じことは、文字列とハッシュなど、他の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に示します。
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
関連付けメソッドのオプションにすぎません。
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のように書きます。
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に示します。
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のサンプルデータ生成タスクにマイクロポストのサンプルを追加して、画面にサンプルデータが表示されるようにしてみます。
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のコードを使って、プロファイルページの外観をテストできるようになります。
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に示されているように、まずはユーザープロファイルページにマイクロポストの一覧を挿入するところからアプリケーションコードの実装を始めることにしましょう。
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
用のビューのディレクトリの中に) 定義しなければならないことを意味します。
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のように変更することで、テストにパスさせることができます。
@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つもないのが原因です。いよいよマイクロポストを追加しましょう。
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のようなサンプルデータ生成になります。
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に示します。
図10.6のページにはマイクロポスト固有のスタイルが与えられていないので、リスト10.24を追加して、結果のページを見てみましょう7。図10.7が1番目の (ログインした) ユーザのプロファイルページで、図 10.8が2番目のユーザのプロファイルページです。最後に図10.9は、1番目のユーザーのためのマイクロポストの2番目のページと、下部に改ページのリンクを表示しています。各マイクロポストの表示には、3つのどの場合にも、それが作成されてからの時間 ("1分前に投稿" など) が表示されていることに注目してください。これはリスト10.21のtime_ago_in_words
メソッドによるものです。数分待ってからページを再度読み込むと、このテキストは自動的に新しい時間に基づいて更新されます。
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;
}
}
10.3マイクロポストを操作する
データモデリングとマイクロポスト表示テンプレートの両方が完成したので、次はWeb経由でそれらを作成するためのインターフェイスに取りかかりましょう。HTMLフォームを使用してリソース (今回の場合はMicropostsリソース) を作成する3番目の例になります8。この節では、ステータスフィード(第11章で完成させます) の最初のヒントをお見せします。最後に、ユーザーがマイクロポストをWeb経由で破棄できるようにします。
従来のRails開発の慣習と異なる箇所が1つあります。Micropostsリソースへのインターフェースは、主にユーザーと静的ページのコントローラを経由して実行されるので、Micropostsコントローラにはnew
やedit
のようなアクションは不要ということになります。create
とdestroy
があれば十分です。従って、リスト10.25に示すように、Micropostsリソースへのルーティングは一層シンプルになります。その結果、リスト10.25のコードは、フルセットのルーティング (表2.3) のサブセットであるRESTfulルート (表10.2) になります。もちろん、シンプルになったということは完成度がさらに高まったということの証しであり、退化したわけではありません。第2章でscaffoldに頼りきりだった頃からここに至るまでは長い道のりでしたが、今ではscaffoldが生成するような複雑なコードのほとんどは不要になりました。
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 | /microposts | create | 新しいマイクロポストを作る |
DELETE | /microposts/1 | destroy | id1 のマイクロポストを削除する |
10.3.1アクセス制御
Micropostsリソースの開発の手始めは、Micropostsコントローラ内のアクセス制御から始めることにしましょう。ここでのアクセス制御のポイントは単純です。create
アクションとdestory
アクションは、いずれもユーザがサインインしていなければ実行できないものとします。これをテストするためのRSpecコードをリスト10.26に示します (マイクロポストをポストしたユーザーだけが削除できるようにする3番目の保護は10.3.4で追加します)。
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_path
、create
アクションが呼び出される) に送信した場合、または 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。
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コントローラファイルをコマンドラインで生成していなかったので、このコントローラを手動で作成する必要があります)。
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のモックアップ参照)。
最後にホームページを実装したときは(図5.6)、[Sign up now!] ボタンが中央にありました。マイクロポスト作成フォームは、サインインしている特定のユーザーのコンテキストでのみ機能するので、この節の一つの目標は、ユーザーのサインインの状態に応じて、ホームページの表示を変更することです。実装はリスト10.31で行いますが、先にテストを作成しましょう。Usersリソースの場合と同様に、結合テストを使用します。
$ rails generate integration_test micropost_pages
従って、マイクロポスト作成のテストは、リスト7.16のユーザー作成用テストと似ています。このテストをリスト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)。
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)。
<% 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で実装します。
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)。
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に示します。
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
変数を使う必要があります。どのような種類のオブジェクトが渡されてもエラーメッセージパーシャルが動くようにする必要があります。幸い、フォーム変数f
はf.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.object
はerror_messages
パーシャルの中でobject
という変数名を作成します。リスト10.35に示すように、このobject変数を使用してカスタマイズエラーメッセージを作成できます。
<% 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のコードを実装していた場合、コードは異なるものになるでしょう。)
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>
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が終わるまで待ってください。
10.3.3フィードの原型
10.3.2の最後のコメントでもほのめかしましたが、現在のHomeページはマイクロポストが1つも表示されていません。図10.11のフォームが正しく動作しているかどうかを確認したい場合、正しいエントリーを投稿した後、プロファイルページに移動してポストを表示すればよいのですが、これはかなり面倒な作業です。 図10.13のモックアップで示したような、ユーザー自身のポストを含むマイクロポストフィードがないと不便です (第11章ではフィードを汎用化し、現在のユーザによってフォローされているユーザのマイクロポストも一緒に表示するフィードにする予定です)。
すべてのユーザがフィードを持つので、feed
メソッドはUserモデルに作るのが自然です。最終的にはそのfeedメソッドが、フォローしているユーザのマイクロポストも返すことをテストしますが、今は、feed
メソッドが自分のマイクロポストは含むが他ユーザのマイクポストは含まないことをテストすることにします。その要件をコード化したものをリスト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。
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)。
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参照。)
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
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に示します。
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)。
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 %>
現時点では、新しいマイクロポストの作成は図10.15で示したように期待どおりに動作しています。ただしささいなことではありますが、マイクロポストの投稿が失敗すると、 Homeページは@feed_items
インスタンス変数を期待しているため、現状では壊れてしまいます (このことはテストスイートを実行して確認できます)。最も簡単な解決方法は、リスト10.45のように空の配列を渡しておくことです12。
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)。ユーザーの削除は管理者ユーザのみが行えるように制限されていたのに対し、今回の場合はカレントユーザーが作成したマイクロポストに対してのみ削除リンクが動作するようにします。
最初のステップとして、リスト10.43のマイクロポストパーシャルに削除リンクを追加します。同様に、リスト10.43のフィードアイテムパーシャルにリンクを追加します。追加の結果をリスト10.46およびリスト10.47に示します (これら2つはほとんど同一です。重複コードの削除は演習に回すことにします (10.5))。
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>
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)。
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に示します。
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とシティバンクでのハッキング事件 (英語)」で解説しているように、対象を探しだすときには常に関連付けを経由することをセキュリティの観点から強くお勧めいたします。
この節のコードで、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演習
ここまでに数多くの題材を取り上げてきましたので、今やアプリケーションを拡張する方法は山ほどあります。以下は、その中のごくわずかに過ぎません。
- サイドバーのマイクロポストカウントのテストを追加してください。このとき、表示に単数形と複数形が正しく表示されているかどうかもテストしてください。
- マイクロポストのページネーションのテストを追加してください。
if
-else
文の2つの分岐に対して、それぞれ異なるパーシャルを使用するようにホームページをリファクタリングしてください。- 削除リンクが、現在のユーザーによって作成されていないマイクロポストには表示されないことを確認するためのテストを作成してください。
- パーシャルを使用して、リスト10.46およびリスト10.47から削除リンクの重複を削除してください。
- 図10.18に示すように、非常に長い単語を入力するとレイアウトが崩れてしまいます。リスト10.50で定義した
wrap
ヘルパーを使用して、この問題を修正してください。このとき、出力されたHTMLがRailsによってエスケープされるのを防ぐためにraw
メソッドを使用してください。また、クロスサイトスクリプティング (XSS) を防ぐためにsanitize
メソッドも使用してください。さらに、このコードでは3項演算子 (コラム 10.1) も使用してください。3項演算子は一見奇妙ですが、極めて便利なものです。 - (チャレンジ) 入力できる残り文字数を140 文字からカウントダウンするためのJavaScriptをHomeページに追加してください。
この世には10種類の人々がいます。3項演算子を好きな人、嫌いな人、3項演算子を知らない人です。(もし3番目に該当したとしても、すぐそのカテゴリの人ではなくなりますので心配ご無用です。)
プログラミング経験を重ねるうちに、以下のような制御フローが最もよく使用される、ありふれたものであることはすぐにわかると思います。
if boolean? 何かをする else 別のことをする end
Rubyや他の言語 (C/C++、Perl、PHP、Javaなど) では、上のようなフローをよりコンパクトな3項演算子と呼ばれる表現で置き換えることができます (3つの部分から構成されるためそのように呼ばれます)。
boolean? ? 何かをする : 別のことをする
また、3項演算子で代入文を置き換えることもできます。
if boolean? var = foo else var = bar end
上のコードは以下のように1行で書けます。
var = boolean? ? foo : bar
他に、関数の戻り値で使用することもよくあります。
def foo do_stuff boolean? ? "bar" : "baz" end
Rubyは暗黙的に関数の最後の式の値を返すので、ここではfooメソッドはboolean?の値によって"bar"または"baz"を返します。この手法はリスト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 = "​"
regex = /.{1,#{max_width}}/
(text.length < max_width) ? text :
text.scan(regex).join(zero_width_space)
end
end
- 技術的には、第8章でセッションをリソースとして扱いましたが、セッションはユーザーやマイクロポストと異なり、データベースには保存されません。↑
content
属性はstring
型になりますが、 2.1.2で簡単に述べたように、長文用のテキストフィールドにはtext
型を使用してください。↑- 実は私自身、当初マイクロポストを適切に複製することを忘れており、以前のバージョンのチュートリアルで使用していたテストにはバグもありました。このようなセフティチェックを最初から行なっていれば、エラーを未然に防げたことでしょう。バグを発見して知らせてくれた読者Jacob Turinoに感謝します。↑
- (ここではカスタムGravatarを持つ5人のユーザーと、デフォルトGravatarを持つ1人のユーザー) ↑
- もしこのメソッドが生成するSQLに興味があるのであれば、
log/development.log
をtailしてみてください (コマンドラインでファイルにtailコマンドを実行するという意味)。 ↑ - Faker gemのlorem ipsumサンプルテキストはランダムに生成される仕様になっているため、サンプルマイクロポストの内容はこの図と違っているはずです。↑
- 便宜上、リスト10.24はこの章で必要なCSSをすべて含んでいます。↑
- 他の2つのリソースとは、リスト7.2のユーザーと8.1のセッションです。↑
- 8.2.1で、ヘルパーメソッドはデフォルトではビューでのみ利用可能であると説明しましたが、
include SessionsHelper
をApplicationコントローラに追加してコントローラでも利用できるようににしました (リスト8.14)。↑ - 1.1.1で、本書を読み終わったら次に純粋なRubyの本を読む事を薦めましたが、これは、
include?
のようなメソッドを学んでいただきたいからというのが理由の一つです。↑ where
やlikeの詳細については、Railsガイドの「Active Record クエリインターフェイス」を参照してください。↑- 残念ですが、この場合はページ分割されたフィードを返してもうまく動きません。動かない理由を確認したい方は、実際に実装してページネーションリンクをクリックしてみてください。↑
Railsチュートリアルは YassLab 社によって運営されています。
コンテンツを継続的に提供するため、書籍・動画・質問対応サービスなどもご検討していただけると嬉しいです。
研修支援や教材連携にも対応しています。note マガジンや YouTube チャンネルも始めたので、よければぜひ遊びに来てください!