Ruby on Rails チュートリアル
-
第2版 目次
- 第1章ゼロからデプロイまで
- 第2章デモアプリケーション
- 第3章ほぼ静的なページの作成
- 第4章Rails風味のRuby
- 第5章レイアウトを作成する
- 第6章ユーザーのモデルを作成する
- 第7章ユーザー登録
- 第8章サインイン、サインアウト
- 第9章ユーザーの更新・表示・削除
- 第10章ユーザーのマイクロポスト
- 第11章ユーザーをフォローする
|
||
第2版 目次
|
||
最新版を読む |
Ruby on Rails チュートリアル
プロダクト開発の0→1を学ぼう
下記フォームからメールアドレスを入力していただくと、招待リンクが記載されたメールが届きます。リンクをクリックし、アカウントを有効化した時点から『30分間』解説動画のお試し視聴ができます。
メール内のリンクから視聴を開始できます。
第2版 目次
- 第1章ゼロからデプロイまで
- 第2章デモアプリケーション
- 第3章ほぼ静的なページの作成
- 第4章Rails風味のRuby
- 第5章レイアウトを作成する
- 第6章ユーザーのモデルを作成する
- 第7章ユーザー登録
- 第8章サインイン、サインアウト
- 第9章ユーザーの更新・表示・削除
- 第10章ユーザーのマイクロポスト
- 第11章ユーザーをフォローする
第10章ユーザーのマイクロポスト
第9章ではUsersリソース用のRESTアクションを完成させました。次はいよいよ、ユーザーのマイクロポストリソースをすべて追加しましょう1。第2章では、特定のユーザーに関連付けられたいくつかの簡単なメッセージがありました。この章では、2.3で記述したMicropostデータモデルを作成し、Userモデルとhas_many
およびbelongs_to
メソッドを使って関連付けを行い、さらに、結果を処理し表示するために必要なフォームとその部品を作成します。 第11章では、マイクロポストのフィードを受け取るために、ユーザーをフォローするという概念を導入し、Twitterのミニクローンを完成させます。
Git をバージョン管理に使っている場合は、いつものようにトピックブランチを作成しておきましょう。
$ git checkout -b user-microposts
10.1Micropostモデル
まずはMicropostリソースの最も本質的な部分を表現するMicropostモデルを作成するところから始めましょう。2.3で作成したモデルと同様に、この新しいMicropostモデルもデータ検証とUserモデルの関連付けを含んでいます。以前のモデルとは違って、今回のマイクロポストモデルは完全にテストされ、デフォルトの順序を持ち、また親であるユーザーが破棄された場合には自動的に破棄されるようにします。
10.1.1基本的なモデル
Micropostモデルは、マイクロポストの内容を保存するcontent
属性2と、特定のユーザーとマイクロポストを関連付けるuser_id
属性の2つの属性だけを持ちます。Userモデルの場合と同様に (リスト6.1)、generate model
コマンドを使って生成します。
$ rails generate model Micropost content:string user_id:integer
上のコマンドを実行するとMicropostファクトリーが作成されますが、これは後で手動で作成するので (10.1.4)、以下を実行していったん削除してください。
$ rm -f spec/factories/microposts.rb
リスト6.2でデータベースにusers
テーブルを作るマイグレーションファイルを生成した時と同様に、このコマンドは microposts
テーブルを作成するためのマイグレーションファイルを生成します (リスト10.1)。
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
カラムをさらに追加します。
それではUserモデルの場合 (リスト6.5) と同様に、Micropostモデルに対する最小限のテストを書くところから始めましょう。具体的には リスト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
ブロックのコードが慣用的な意味で正しくないことを指摘しています。このコードは動きますが、Railsの流儀に合っていません。その理由を考えてみてください。解答は10.1.3で確認できます。
10.1.2 最初の検証
Micropostモデルが必要とされている理由のひとつに、マイクロポストを投稿したユーザーを示すユーザーidを持っているということがあります。これを慣用的な意味で正しく行うには、Active Recordの関連付けを行います。実際の関連付けは10.1.3で行います。この作業にはある程度のリファクタリングが必要なので、テストを作成してバグの再発をキャッチするようにします。
リスト10.3に示したテストでは、ユーザーidをnil
にしてから投稿したマイクロポストが無効になるかどうかをチェックすることで、検証を行なっています。(リスト6.8でのUserモデルのテストと比較してみてください)。
spec/models/micropost_spec.rb
require 'spec_helper'
describe Micropost do
let(:user) { FactoryGirl.create(:user) }
before do
# このコードは慣用的な意味で正しくない。
@micropost = Micropost.new(content: "Lorem ipsum", user_id: user.id)
end
subject { @micropost }
it { should respond_to(:content) }
it { should respond_to(:user_id) }
it { should be_valid }
describe "when user_id is not present" do
before { @micropost.user_id = nil }
it { should_not be_valid }
end
end
このコードはマイクロポストが有効であり、かつuser_id
属性が存在していることをテストしています。リスト10.4 に示すような簡単な存在確認検証によって、このテストはパスします。
user_id
に対する検証。app/models/micropost.rb
class Micropost < ActiveRecord::Base
validates :user_id, presence: true
end
10.1.3User/Micropostの関連付け
Webアプリケーション用のデータモデルを構築するにあたって、個々のモデル間での関連付けを十分考えておくことが重要です。今回の場合は、 2.3.3でも示したように、それぞれのマイクロポストは1人のユーザーと関連付けられ、それぞれのユーザーは (潜在的に) 複数のマイクロポストと関連付けられます。この関連付けを図10.2と図10.3に示します。これらの関連付けを実装するための一環として、Micropostモデルに対するテストを作成し、さらにUserモデルにいくつかのテストを追加します。
この節で定義する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
は自動的に正しい値に設定されます。特に、以下のような
let(:user) { FactoryGirl.create(:user) }
before do
# このコードは慣用的な意味で正しくない。
@micropost = Micropost.new(content: "Lorem ipsum", user_id: user.id)
end
リスト10.3のコードを、以下のコードと置き換えることができるようになります。
let(:user) { FactoryGirl.create(:user) }
before { @micropost = user.microposts.build(content: "Lorem ipsum") }
一度正しい関連付けを定義してしまえば、@micropost
変数のuser_id
には、関連するユーザーのidが自動的に設定されます。
表10.1に示したように、ユーザーとマイクロポストの関連付けの結果には、単にマイクロポストのユーザーを返すmicropost.user
メソッドもあり、これもテストが必要です。このメソッドは、it
とits
メソッドを以下のように使うことでテストできます。
it { should respond_to(:user) }
its(:user) { should eq user }
上のコードを反映したMicropostモデルのテストをリスト10.5に示します。
spec/models/micropost_spec.rb
require 'spec_helper'
describe Micropost do
let(:user) { FactoryGirl.create(:user) }
before { @micropost = user.microposts.build(content: "Lorem ipsum") }
subject { @micropost }
it { should respond_to(:content) }
it { should respond_to(:user_id) }
it { should respond_to(:user) }
its(:user) { should eq user }
it { should be_valid }
describe "when user_id is not present" do
before { @micropost.user_id = nil }
it { should_not be_valid }
end
end
Userモデル側の関連付けに対する詳細なテストは10.1.4まで保留することにし、今は単にmicroposts
属性の存在をテストするだけにしておきます (リスト10.6)。
microposts
属性に対するテスト。spec/models/user_spec.rb
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "user@example.com",
password: "foobar", password_confirmation: "foobar")
end
subject { @user }
.
.
.
it { should respond_to(:admin) }
it { should respond_to(:microposts) }
.
.
.
end
関連付けを実装する最終的なコードは、テストコードの長さと比べるとあっけないほど短いものになります。リスト10.5とリスト10.6の両方のテストにパスするためには、belongs_to :user
(リスト10.7)とhas_many :microposts
(リスト10.8) の2行を加えるだけで済みます。
belongs_to
) 関連付け。app/models/micropost.rb
class Micropost < ActiveRecord::Base
belongs_to :user
validates :user_id, presence: true
end
has_many
) 関連付け。app/models/user.rb
class User < ActiveRecord::Base
has_many :microposts
.
.
.
end
この時点で、表10.1の項目と、リスト10.5およびリスト10.6のコードをよく見比べて、関連付けの基本的原則を理解しておいてください。テストにパスすることも確認しておきましょう。
$ bundle exec rspec spec/models
10.1.4マイクロポストを改良する
リスト10.6のhas_many
関連付けのテストでは、microposts
属性の存在をほとんど検証していないので、あまり意味がありませんでした。この章では、順序と依存関係をマイクロポストに追加し、user.microposts
メソッドが実際にマイクロポストの配列を返すことをテストします。
Userモデルのテストのためにいくつかのマイクロポストを作成しておく必要がありますので、この時点でマイクロポストを生成するファクトリーを作成しておきましょう。そのためには、Factory Girlに関連付けを作成する方法を知っておく必要があります。幸い、リスト10.9に示したとおり、これは非常に簡単です。
spec/factories.rb
FactoryGirl.define do
factory :user do
sequence(:name) { |n| "Person #{n}" }
sequence(:email) { |n| "person_#{n}@example.com"}
password "foobar"
password_confirmation "foobar"
factory :admin do
admin true
end
end
factory :micropost do
content "Lorem ipsum"
user
end
end
ここでは、以下のようにマイクロポスト用のファクトリーの定義にuserを含めるだけで、マイクロポストに関連付けられるユーザーのことがFactory Girlに伝わります。
factory :micropost do
content "Lorem ipsum"
user
end
次の節でもお見せしますが、これによって、以下のようにマイクロポスト用のファクトリーを定義できるようになります。
FactoryGirl.create(:micropost, user: @user, created_at: 1.day.ago)
デフォルトのスコープ
データベースからユーザーのマイクロポストを読み出すuser.microposts
メソッドは、デフォルトでは読み出しの順序に対して何も保証しませんが、 ブログやTwitterの慣習に従って、作成時間の逆順、つまり最も新しいマイクロポストを最初に表示するようにしてみましょう。この順序をテストするために、次のようにマイクロポストをいくつか作成しておきます。
FactoryGirl.create(:micropost, user: @user, created_at: 1.day.ago)
FactoryGirl.create(:micropost, user: @user, created_at: 1.hour.ago)
(コラム 8.1で説明した時間指定用ヘルパーメソッドを使うことで) 2番目のポストの作成時刻がより新しく (1.hour.ago
: 1時間前)、最初のポストの作成日時が1.day.ago
(1日前) になります。Factory Girlを使用すると、Active Recordがアクセスを許可しないようなcreated_at
属性も手動で設定できるので大変便利です (created_at
やupdated_at
などは通常のカラムと異なる “マジック”カラムであり、これらの作成タイムスタンプや更新タイムスタンプは自動的に設定されてしまうため、明示的に値を設定しても上書きされてしまうことを思い出してください)。
(SQLiteを含む) ほとんどのデータベースアダプターは、マイクロポストを id順で返すため、リスト10.10のようなテストはほぼ確実に失敗することになります。このコードでは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
expect(@user.microposts.to_a).to eq [newer_micropost, older_micropost]
end
end
end
上記のコードで重要なのは、以下の行です。
expect(@user.microposts.to_a).to eq [newer_micropost, older_micropost]
これは新しいポストが最初に来ることをテストしています。デフォルトでは id順に並ぶため[older_micropost, newer_micropost]
の順序になりテストは失敗するはずです。またこのテストは、表10.1で示すように、 user.microposts
が有効なマイクロポストの配列を返すことをチェックすることにより、基本的なhas_many
関連付け自体の正しさも確認しています。4.3.1でも説明したように、to_a
メソッドは、 @user.microposts
をデフォルトの状態 (これはたまたまActive Recordの "collection proxy" になっています) から正しい配列に変換します。変換された配列は、手作りの配列と比較可能になります。
順序テストがパスするために、 リスト 10.11に示すようにRailsのdefault_scope
にorder
パラメータを渡して使用します (このコードはスコープに関する最初の例でもあります。より一般的なスコープについては第11章で説明します)。
default_scope
でマイクロポストを順序付ける。app/models/micropost.rb
class Micropost < ActiveRecord::Base
belongs_to :user
default_scope -> { order('created_at DESC') }
validates :user_id, presence: true
end
上のコードでの順序は’created_at DESC’
としています。DESC
は SQLでいうところの “descending” であり、新しいものから古い順への降順ということになります。
Rails 4.0からは、あらゆるスコープは、スコープで必要な域値を返す無名関数を受け取ります。これにより、スコープをその場で評価する必要がほぼなくなり、後に読み込まれたときに必要に応じて評価するようになります (いわゆる遅延評価 (lazy evaluation) です)。上のコードの場合、以下がその関数です。
-> { order('created_at DESC') }
この種のオブジェクトの構文は、Proc (手続き: procedure) とかラムダ (lambda)と呼ばれ、->
という矢印で表されます。この構文はブロック (4.3.2) を引数に取り、後にcall
メソッドによって呼び出されるときにブロックを実際に評価します。この構文をコンソールで確かめてみましょう。
>> -> { puts "foo" }
=> #<Proc:0x007fab938d0108@(irb):1 (lambda)>
>> -> { puts "foo" }.call
foo
=> nil
(ProcやラムダはRubyのトピックとしてはやや高度な部類に含まれるので、今すぐわからなくても心配する必要はありません。)
Dependent: destroy
順序についてはひとまずここで区切ることにし、今度はマイクロポストに第二の要素を追加してみましょう。9.4で書いたように、サイト管理者はユーザーを破棄する権限を持ちます。ユーザーが破棄された場合、ユーザーのマイクロポストも同様に破棄されるべきです。最初のマイクロポストのユーザーを破棄した後、関連するマイクロポストもデータベースからなくなったことを確認することで、ユーザーの破棄をテストすることができます。
適切にマイクロポストの破棄をテストするために、最初にローカル変数で指定されたユーザーのポストを取得し、次にユーザーを破棄します。シンプルな実装は以下のようになります。
microposts = @user.microposts.to_a
@user.destroy
expect(microposts).not_to be_empty
microposts.each do |micropost|
# マイクロポストがデータベースからなくなったことを確認
end
上のコードで、to_a
メソッドが呼び出されていることでマイクロポストのコピーが作成されていることに注目してください (参照のコピーではなく、オブジェクト自体がコピーされています)。さらに、以下の行にも注目してください。
expect(microposts).not_to be_empty
上の行は一種のセフティチェックの役割も果たしており、うっかりto_a
メソッドを付け忘れたときのエラーをすべてキャッチしてくれます。ここで重要なのは、to_a
メソッドがなかったら、ユーザーを削除したときにmicroposts
変数に含まれているポストまで削除されてしまうということです。
microposts.each do |micropost|
# マイクロポストがデータベースからなくなったことを確認
end
つまり、microposts
が空になってしまうため、上のテストに何を書いても動作しなくなってしまうということです。
データベースにマイクロポストがないという予想は、以下のように書くことができます。
microposts.each do |micropost|
expect(Micropost.where(id: micropost.id)).to be_empty
end
上のコードでは、Micropost.find
ではなくMicropost.where
を使用しています。whereメソッドは、レコードがない場合に空のオブジェクトを返すので多少テストが書きやすくなるためです (findはレコードがない場合に例外を発生します)。(気になる方への補足: findを使用する場合は以下のようになります。
expect do
Micropost.find(micropost)
end.to raise_error(ActiveRecord::RecordNotFound)
これでfindの場合のテストを実施できます。)
これまでのコードを集約したものをリスト10.12に示します。
spec/models/user_spec.rb
require 'spec_helper'
describe User do
.
.
.
describe "micropost associations" do
before { @user.save }
let!(:older_micropost) do
FactoryGirl.create(:micropost, user: @user, created_at: 1.day.ago)
end
let!(:newer_micropost) do
FactoryGirl.create(:micropost, user: @user, created_at: 1.hour.ago)
end
.
.
.
it "should destroy associated microposts" do
microposts = @user.microposts.to_a
@user.destroy
expect(microposts).not_to be_empty
microposts.each do |micropost|
expect(Micropost.where(id: micropost.id)).to be_empty
end
end
end
end
リスト10.12のテストをパスするためのアプリケーションコードは1行に満たないほどの量しかなく、事実それはリスト10.13に示す has_many
関連付けメソッドのオプションにすぎません。
app/models/user.rb
class User < ActiveRecord::Base
has_many :microposts, dependent: :destroy
.
.
.
end
上のコードの中にある以下のdependent: :destroy
オプションは、
has_many :microposts, dependent: :destroy
ユーザー自体が破棄されたときに、そのユーザーに依存するマイクロポスト (つまり特定のユーザーのもの) も破棄されることを指定しています。これは、管理者がシステムからユーザーを削除したときに、依存するユーザーが存在しないマイクロポストがデータベースに取り残されてしまうことを防ぎます。
これで、ユーザー/マイクロポスト関連付けの最終形が完成しました。すべてのテストがパスするはずです。
$ bundle exec rspec spec/
10.1.5コンテンツの検証
Micropostモデルを離れる前に、(2.3.2の例に従い) マイクロポストのcontent
の検証を追加しましょう。user_id
属性と同様、content
属性も存在する必要があり、さらにマイクロポストが140文字より長くならないよう制限を加えます。このテストは6.2でのユーザーモデルの検証テストの例に従って、リスト10.14のように書きます。
spec/models/micropost_spec.rb
require 'spec_helper'
describe Micropost do
let(:user) { FactoryGirl.create(:user) }
before { @micropost = user.microposts.build(content: "Lorem ipsum") }
.
.
.
describe "when user_id is not present" do
before { @micropost.user_id = nil }
it { should_not be_valid }
end
describe "with blank content" do
before { @micropost.content = " " }
it { should_not be_valid }
end
describe "with content that is too long" do
before { @micropost.content = "a" * 141 }
it { should_not be_valid }
end
end
6.2と同様、リスト10.14ではマイクロポストの長さの検証コードをテストするために、文字列の乗算を使用しています。
$ rails console
>> "a" * 10
=> "aaaaaaaaaa"
>> "a" * 141
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
実際のアプリケーションコードはわずか1行です。
validates :content, presence: true, length: { maximum: 140 }
上のコードを反映したMicropostモデルをリスト10.15に示します。
app/models/micropost.rb
class Micropost < ActiveRecord::Base
belongs_to :user
default_scope -> { order('created_at DESC') }
validates :content, presence: true, length: { maximum: 140 }
validates :user_id, presence: true
end
10.2マイクロポストを表示する
Web経由でマイクロポストを作成する方法は現時点ではありませんが (10.3.2から作り始めます)、マイクロポストを表示することと、テストすることならできます。ここでは、Twitterのような独立したマイクロポストindex
ページでユーザーのマイクロポストを表示するよりも、図10.4のモックアップに示したように、直接ユーザーのshow
ページで表示させることにしましょう。ユーザープロファイルにマイクロポストを表示させるため、最初に極めてシンプルなERbテンプレートを作成します。次に、9.3.2のサンプルデータ生成タスクにマイクロポストのサンプルを追加して、画面にサンプルデータが表示されるようにしてみます。
8.2.1でサインイン機能について説明したときと同様に、10.2.1でも最初に実装をお目にかけ、次にそれらの要素を順に解説してきます (スタックに複数の要素を一括でプッシュした後、順にポップするような要領です)。しばらくの間盛り上がりに欠けるかもしれませんが、10.2.2で一気に取り返しますので、それまでご辛抱願います。
10.2.1ユーザー表示ページの拡張
ユーザーのマイクロポスト表示に対するテスト、すなわちユーザーに対するrequest specを作成するところから始めましょう。ユーザーに関連付けられているマイクロポストのファクトリーを作成し、それから表示ページが各ポストの内容を含んでいるか検証する戦略で進めます。また、図 10.4で示されているように、マイクロポストの総数が表示されているかどうかも検証します。
ポストはlet
メソッドで作成できますが、ユーザー表示ページでポストがすぐに表示されるように、リスト10.10と同様に即座に関連付けを作成できるようにしたいと思います。そこで、今回もlet!
を使います。
let(:user) { FactoryGirl.create(:user) }
let!(:m1) { FactoryGirl.create(:micropost, user: user, content: "Foo") }
let!(:m2) { FactoryGirl.create(:micropost, user: user, content: "Bar") }
before { visit user_path(user) }
上のようにマイクロポストを定義したことで、リスト10.16のコードを使って、プロファイルページの外観をテストできるようになります。
show
ページでマイクロポストが表示されていることをテストする。spec/requests/user_pages_spec.rb
require 'spec_helper'
describe "User pages" do
.
.
.
describe "profile page" do
let(:user) { FactoryGirl.create(:user) }
let!(:m1) { FactoryGirl.create(:micropost, user: user, content: "Foo") }
let!(:m2) { FactoryGirl.create(:micropost, user: user, content: "Bar") }
before { visit user_path(user) }
it { should have_content(user.name) }
it { should have_title(user.name) }
describe "microposts" do
it { should have_content(m1.content) }
it { should have_content(m2.content) }
it { should have_content(user.microposts.count) }
end
end
.
.
.
end
count
メソッドは、関連付けを経由して使用していることに注目してください。
user.microposts.count
count
関連付けメソッドは賢くできていて、直接データベースでカウントを行います。特に、データベース上のマイクロポストを全部読みだしてから結果の配列に対してlength
を呼ぶような無駄なことはしていません。そんなことをしたら、マイクロポストの数が増加するにつれて効率が低下してしまいます。代わりに、特定user_id
に対するマイクロポストの数をデータベースに問い合わせます。それでもcountメソッドがアプリケーションのボトルネックになるようなことがあれば、さらに高速なcounter cacheを使うこともできます。
リスト10.16のテストはリスト10.18まではパスしませんが、リスト10.17に示したように、まずはユーザープロファイルページにマイクロポストの一覧を挿入するところからアプリケーションコードの実装を開始することにしましょう。
show
画面にマイクロポストを追加する。app/views/users/show.html.erb
<% provide(:title, @user.name) %>
<div class="row">
.
.
.
<aside>
.
.
.
</aside>
<div class="span8">
<% if @user.microposts.any? %>
<h3>Microposts (<%= @user.microposts.count %>)</h3>
<ol class="microposts">
<%= render @microposts %>
</ol>
<%= will_paginate @microposts %>
<% end %>
</div>
</div>
すぐにもマイクロポスト一覧の実装に取りかかりますが、その前に注意すべき点がいくつかあります。リスト10.17では、if @user.microposts.any?
を使用することによって (以前リスト7.24でも使用した) ユーザーのマイクロポストが1つもない場合に空のリストが表示されないようにしています。
リスト10.17では、以下のようにマイクロポストに対するページネーションのコードを加えていたことに注意してください。
<%= will_paginate @microposts %>
リスト9.33のユーザーインデックスページのコードと比較すると、以前は以下のように単純なコードでした。
<%= will_paginate %>
上のコードは引数なしで動作していました。これはwill_paginate
が、Usersコントローラのコンテキストにおいて、@users
インスタンス変数が存在していることを前提としているためです。このインスタンス変数は、9.3.3でも述べたようにActiveRecord::Relation
クラスのインスタンスです。今回の場合は、ユーザーコントローラのコンテキストにおいて、マイクロポストをページネーションしたいため、明示的に@microposts
変数を will_paginate
に渡す必要があります。もちろん、そのような変数をユーザーshow
アクションで定義しなければなりません (リスト10.19)。
最後に、以下のようにマイクロポストの現在の数のカウントを追加します。
<h3>Microposts (<%= @user.microposts.count %>)</h3>
前述のように、@user.microposts.count
は、ユーザー/マイクロポスト関連付けを経由して、あるユーザーに属するマイクロポストをカウントすることを除けば、User.count
に似ています。
やっとマイクロポスト一覧のコードそのものにたどり着きました。
<ol class="microposts">
<%= render @microposts %>
</ol>
この順序リストタグol
を含むコードがマイクロポストの一覧を生成します。ただしご覧のとおり、実装の厄介な部分をマイクロポストパーシャルに任せています。9.3.4で見た以下のコードは、
<%= render @users %>
_user.html.erb
パーシャルを使って自動的に@users
変数内のそれぞれのユーザーを出力します。同様に以下のコードは、
<%= render @microposts %>
まったく同じことをマイクロポストで行います。つまり、リスト10.18に示すように、_micropost.html.erb
パーシャルを (micropost
用のビューのディレクトリの中に) 定義しなければならないことを意味します。
app/views/microposts/_micropost.html.erb
<li>
<span class="content"><%= micropost.content %></span>
<span class="timestamp">
Posted <%= time_ago_in_words(micropost.created_at) %> ago.
</span>
</li>
詳細は10.2.2で述べますが、上のコードでは素晴らしいtime_ago_in_words
ヘルパーメソッドを使用しています。
これまでのところ、関連するERbテンプレートをすべて定義したにもかかわらず、リスト10.16のテストは、たった1つ、@microposts
変数がないために失敗していたはずです。リスト10.19のように変更することで、テストにパスさせることができます。
@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人のユーザー3だけを選択しましょう。そのためには、User.all
メソッドに:limit
オプションを渡します4。
users = User.all(limit: 6)
その後、各ユーザーに50のマイクロポスト (ページネーションが切り替わる30を超える数) を作成し、Faker gemの便利なLorem.sentenceメソッドを使って各マイクロポストのサンプルコンテンツを生成します (Faker::Lorem.sentence
は、いわゆるlorem ipsumダミーテキストを返します。第6章で述べたように、lorem ipsumには面白い裏話があります)。 変更の結果は、リスト10.20のようなサンプルデータ生成になります。
lib/tasks/sample_data.rake
namespace :db do
desc "Fill database with sample data"
task populate: :environment do
.
.
.
users = User.all(limit: 6)
50.times do
content = Faker::Lorem.sentence(5)
users.each { |user| user.microposts.create!(content: content) }
end
end
end
もちろん、新しいサンプルデータを生成するためにはRakeタスクのdb:populate
を実行する必要があります。
$ bundle exec rake db:reset
$ bundle exec rake db:populate
$ bundle exec rake db:test:prepare
それぞれのマイクロポストの情報を表示することによって、10.2.1の地道な作業がやっと報われはじめました5。実行結果を図10.6に示します。
図10.6のページにはマイクロポスト固有のスタイルが与えられていないので、リスト10.21を追加して、結果のページを見てみましょう6。図10.7が1番目の (ログインした) ユーザーのプロファイルページで、図10.8が2番目のユーザーのプロファイルページです。最後に図10.9は、1番目のユーザーのためのマイクロポストの2番目のページと、下部に改ページのリンクを表示しています。各マイクロポストの表示には、3つのどの場合にも、それが作成されてからの時間 ("1分前に投稿" など) が表示されていることに注目してください。これはリスト10.18の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番目の例になります7。この節では、ステータスフィード (第11章で完成させます) の最初のヒントをお見せします。最後に、ユーザーがマイクロポストをWeb経由で破棄できるようにします。
従来のRails開発の慣習と異なる箇所が1つあります。Micropostsリソースへのインターフェイスは、主にユーザーと静的ページのコントローラを経由して実行されるので、Micropostsコントローラにはnew
やedit
のようなアクションは不要ということになります。create
とdestroy
があれば十分です。従って、リスト10.22に示すように、Micropostsリソースへのルーティングは一層シンプルになります。その結果、リスト10.22のコードは、フルセットのルーティング (表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リクエスト | URL | アクション | 用途 |
---|---|---|---|
POST | /microposts | create | 新しいマイクロポストを作る |
DELETE | /microposts/1 | destroy | id1 のマイクロポストを削除する |
10.3.1アクセス制御
Micropostsリソースの開発の手始めは、Micropostsコントローラ内のアクセス制御から始めることにしましょう。ここでのアクセス制御のポイントは単純です。create
アクションとdestory
アクションは、いずれもユーザーがサインインしていなければ実行できないものとします。これをテストするためのRSpecコードをリスト10.23に示します (マイクロポストをポストしたユーザーだけが削除できるようにする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 { expect(response).to redirect_to(signin_path) }
end
describe "submitting to the destroy action" do
before { delete micropost_path(FactoryGirl.create(:micropost)) }
specify { expect(response).to redirect_to(signin_path) }
end
end
.
.
.
end
end
end
未制作のマイクロポストのWebインターフェースと違い、リスト10.23のコードは、リスト9.13のときの手法と同様に、それぞれのマイクロポストアクションのレベルで動作します。サインインしていないユーザーは、POSTリクエストを/microposts (post microposts_path
、create
アクションが呼び出される) に送信した場合、または DELETEリクエストを/microposts/1 (delete micropost_path(micropost)
、destroy
アクションが呼び出される) に送信した場合にリダイレクトされます。
リスト10.23のテストにパスするためのアプリケーションコードを作成する前に、少しリファクタリングを行いましょう。 9.2.1では、signed_in_user
メソッドと呼ばれるbefore_actionを使用して、サインインを必須にしたことを思い出してください (リスト9.12)。あのときは、このメソッドをUsersコントローラでしか使用しませんでしたが、今回はMicropostsコントローラでもこのメソッドが必要となることがわかったので、リスト10.24に示すように、セッションヘルパーに移動します8。
signed_in_user
メソッドをセッションヘルパーに移動する。app/helpers/sessions_helper.rb
module SessionsHelper
.
.
.
def current_user?(user)
user == current_user
end
def signed_in_user
unless signed_in?
store_location
redirect_to signin_url, notice: "Please sign in."
end
end
.
.
.
end
コードが重複しないよう、signed_in_user
をUsersコントローラからも削除しておきましょう。
リスト10.24のコードにより、signed_in_user
メソッドがMicropostsコントローラで利用可能になりました。これにより、リスト10.25で示すようにcreate
アクションとdestroy
アクションに対してbefore_actionを使用してアクセス制限をかけることが可能になりました (注意: Micropostsコントローラファイルをコマンドラインで生成していなかったので、このコントローラを手動で作成する必要があります)。
app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
before_action :signed_in_user
def create
end
def destroy
end
end
before_actionはデフォルトで両方のアクションに適用されるため、制限を適用するアクションを明示していないことに注意してください。もし仮にindex
アクションを追加し、サインインしていないユーザーでもアクセス可能にしたい場合は、以下のようにindexアクション以外のアクションを明示的に指定する必要があります。
class MicropostsController < ApplicationController
before_action :signed_in_user, only: [:create, :destroy]
def index
end
def create
end
def destroy
end
end
これでテストにパスするはずです。
$ bundle exec rspec spec/requests/authentication_pages_spec.rb
10.3.2マイクロポストを作成する
第7章では、HTTP POSTリクエストをUsersコントローラのcreate
アクションに発行するHTMLフォームを作成することで、ユーザーのサインアップを実装しました。マイクロポスト作成の実装もこれと似ています。主な違いは、別の micropost/new ページを使う代わりに、(Twitterに習って) ホームページ自体 (つまりルートパス) にフォームを置くという点です (図10.10のモックアップ参照)。
最後にホームページを実装したときは (図5.6)、[Sign up now!] ボタンが中央にありました。マイクロポスト作成フォームは、サインインしている特定のユーザーのコンテキストでのみ機能するので、この節の一つの目標は、ユーザーのサインインの状態に応じて、ホームページの表示を変更することです。実装はリスト10.28で行いますが、先にテストを作成しましょう。Usersリソースの場合と同様に、結合テストを使用します。
$ rails generate integration_test micropost_pages
従って、マイクロポスト作成のテストは、リスト7.16のユーザー作成用テストと似ています。このテストをリスト10.26に示します。
spec/requests/micropost_pages_spec.rb
require 'spec_helper'
describe "Micropost pages" do
subject { page }
let(:user) { FactoryGirl.create(:user) }
before { sign_in user }
describe "micropost creation" do
before { visit root_path }
describe "with invalid information" do
it "should not create a micropost" do
expect { click_button "Post" }.not_to change(Micropost, :count)
end
describe "error messages" do
before { click_button "Post" }
it { should have_content('error') }
end
end
describe "with valid information" do
before { fill_in 'micropost_content', with: "Lorem ipsum" }
it "should create a micropost" do
expect { click_button "Post" }.to change(Micropost, :count).by(1)
end
end
end
end
次に、マイクロポストのcreate
アクションを作成しましょう。このアクションも、リスト7.26のユーザー用アクションと似ています。違いは、新しいマイクロポストをbuild
するためにuser/micropost関連付けを使用している点です (リスト10.27)。micropost_params
でStrong Parametersを使用していることにより、マイクロポストのコンテンツだけがWeb経由で編集可能になっていることに注目してください。
create
アクション。app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
before_action :signed_in_user
def create
@micropost = current_user.microposts.build(micropost_params)
if @micropost.save
flash[:success] = "Micropost created!"
redirect_to root_url
else
render 'static_pages/home'
end
end
def destroy
end
private
def micropost_params
params.require(:micropost).permit(:content)
end
end
マイクロポスト作成フォームを構築するために、サイト訪問者がサインインしているかどうかに応じて異なるHTMLを提供するコードを使用します (リスト10.28)。
<% if signed_in? %>
<div class="row">
<aside class="span4">
<section>
<%= render 'shared/user_info' %>
</section>
<section>
<%= render 'shared/micropost_form' %>
</section>
</aside>
</div>
<% else %>
<div class="center hero-unit">
<h1>Welcome to the Sample App</h1>
<h2>
This is the home page for the
<a href="http://railstutorial.jp/">Ruby on Rails Tutorial</a>
sample application.
</h2>
<%= link_to "Sign up now!", signup_path,
class: "btn btn-large btn-primary" %>
</div>
<%= link_to image_tag("rails.png", alt: "Rails"), 'http://rubyonrails.org/' %>
<% end %>
if
-else
分岐を使用してコードを書き分けている点が少し汚いですが、このコードのクリーンアップは演習に回すことにします (10.5)。なお、リスト10.28で使用するパーシャルの実装は必須なので、演習にはしません。新しいホームページのサイドバーはリスト 10.29で、マイクロポストフォームのパーシャルは リスト 10.30で実装します。
app/views/shared/_user_info.html.erb
<a href="<%= user_path(current_user) %>">
<%= gravatar_for current_user, size: 52 %>
</a>
<h1>
<%= current_user.name %>
</h1>
<span>
<%= link_to "view my profile", current_user %>
</span>
<span>
<%= pluralize(current_user.microposts.count, "micropost") %>
</span>
リスト9.24と同様、リスト10.29のコードでもリスト7.30で定義したgravatar_for
ヘルパーを使用します。
プロファイルサイドバー (リスト10.17) のときと同様、リスト10.29のユーザー情報にも、そのユーザーが投稿したマイクロポストの総数が表示されていることに注目してください。ただし少し表示に違いがあります。プロファイルサイドバーでは、 “Microposts” をラベルとし、“Microposts (1)” と表示することは問題ありません。しかし今回のように “1 microposts” と表示してしまうと英語の文法上誤りになってしまうので、pluralize
メソッドを使用して “1 micropost” や “2 microposts” と表示するように調整します。
次はマイクロポスト作成フォームを定義します (リスト10.30)。これはユーザー登録フォームに似ています (リスト7.17)。
app/views/shared/_micropost_form.html.erb
<%= form_for(@micropost) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<div class="field">
<%= f.text_area :content, placeholder: "Compose new micropost..." %>
</div>
<%= f.submit "Post", class: "btn btn-large btn-primary" %>
<% end %>
リスト10.30のフォームが動くようにするためには、2箇所の変更が必要です。1つは、(以前と同様) 関連付けを使用して以下のように@micropost
を定義することです。
@micropost = current_user.microposts.build
変更の結果をリスト10.31に示します。
home
アクションにマイクロポストのインスタンス変数を追加する。app/controllers/static_pages_controller.rb
class StaticPagesController < ApplicationController
def home
@micropost = current_user.microposts.build if signed_in?
end
.
.
.
end
リスト10.31のコードは、ユーザーへのサインイン要求を実装し忘れた場合に、テストが失敗して知らせてくれるので助かります。
リスト10.30を動かすための第2の変更は、以下のようにエラーメッセージパーシャルを再定義することです。
<%= render 'shared/error_messages', object: f.object %>
リスト7.23ではエラーメッセージパーシャルが@user
変数を直接参照していたことを思い出してください。今回は代わりに@micropost
変数を使う必要があります。どのような種類のオブジェクトを渡されてもエラーメッセージパーシャルが動くようにする必要があります。幸い、フォーム変数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.32に示すように、この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.26のテストはパスするはずです。
$ bundle exec rspec spec/requests/micropost_pages_spec.rb
残念ながら、サインアップと編集フォームが古いバージョンのメッセージパーシャルを利用しているため、Userのrequest specが壊れてしまいました。これを修正するために、リスト10.33およびリスト10.34で示すように、これらのフォームをより汎用的なバージョンに更新します。(注: もし9.6の演習で、リスト9.49およびリスト9.50のコードを「必要に応じて流用 (Mutatis mutandis)」して実装していた場合、異なるコードを使用する必要があります。)
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.35に示します。
spec/models/user_spec.rb
require 'spec_helper'
describe User do
.
.
.
it { should respond_to(:microposts) }
it { should respond_to(:feed) }
.
.
.
describe "micropost associations" do
before { @user.save }
let!(:older_micropost) do
FactoryGirl.create(:micropost, user: @user, created_at: 1.day.ago)
end
let!(:newer_micropost) do
FactoryGirl.create(:micropost, user: @user, created_at: 1.hour.ago)
end
.
.
.
describe "status" do
let(:unfollowed_post) do
FactoryGirl.create(:micropost, user: FactoryGirl.create(:user))
end
its(:feed) { should include(newer_micropost) }
its(:feed) { should include(older_micropost) }
its(:feed) { should_not include(unfollowed_post) }
end
end
end
このテストでは、与えられた要素が配列に含まれているかどうかをチェックするinclude?
メソッドを使用しています9。
$ rails console
>> a = [1, "foo", :bar]
>> a.include?("foo")
=> true
>> a.include?(:bar)
=> true
>> a.include?("baz")
=> false
上の例は、RSpecの論理値変換機能の柔軟性が極めて高いことを示しています。include
メソッドは本来、モジュールをインクルードするために使用するRubyキーワードであるにもかかわらず (リスト8.14など)、このRSpecのコンテキストでは配列が要素を含んでいるかどうかをテストしたいという開発者の意図を正確に推測してくれます。
リスト10.36で示すように、user_id
が現在のユーザーidと等しいマイクロポストを見つけることによって、適切なマイクロポストのfeed
メソッドを実装することができます。これはMicropost
モデルでwhere
メソッドを使用することで実現できます10。
app/models/user.rb
class User < ActiveRecord::Base
.
.
.
def feed
# このコードは準備段階です。
# 完全な実装は第11章「ユーザーをフォローする」を参照してください。
Micropost.where("user_id = ?", id)
end
.
.
.
end
以下のコードで使用されている疑問符は、セキュリティ上重要な役割を果たしています。
Micropost.where("user_id = ?", id)
上の疑問符があることで、SQLクエリにインクルードされる前にid
が適切にエスケープされることを保証してくれるため、SQLインジェクションと呼ばれる深刻なセキュリティホールを避けることができます。この場合のid
属性は単なる整数であるため危険はありませんが、SQL文にインクルードされる変数を常にエスケープする習慣はぜひ身につけてください。
注意深い読者は、リスト10.36のコードは本質的に以下のコードと同等であることに気付くかもしれません。
def feed
microposts
end
上のコードを使用せずにリスト10.36のコードを利用したのは、第11章で必要となるフルステータスフィードで応用が効くためです。
ステータスフィードの表示をテストするために、まずいくつかのマイクロポストを作成し、それぞれのページに表示されるリスト要素 (li
)を検証します(リスト10.37)。
spec/requests/static_pages_spec.rb
require 'spec_helper'
describe "Static pages" do
subject { page }
describe "Home page" do
.
.
.
describe "for signed-in users" do
let(:user) { FactoryGirl.create(:user) }
before do
FactoryGirl.create(:micropost, user: user, content: "Lorem ipsum")
FactoryGirl.create(:micropost, user: user, content: "Dolor sit amet")
sign_in user
visit root_path
end
it "should render the user's feed" do
user.feed.each do |item|
expect(page).to have_selector("li##{item.id}", text: item.content)
end
end
end
end
.
.
.
end
リスト10.37のコードでは、以下のように各フィード項目が固有のCSS idを持つことを前提にしています。
expect(page).to have_selector("li##{item.id}", text: item.content)
それにより、上のコードが各アイテムに対してマッチするようにするのが目的です (li##{item.id}
の最初の#
は CSS idを示す Capybara独自の文法で、2番目の#
は Rubyの式展開#{}
の先頭部分であることに注意してください)。
サンプルアプリケーションでフィードを使うために、カレントユーザーの (ページネーション) フィードに@feed_items
インスタンス変数を追加し (リスト10.38)、次にフィードパーシャル (リスト10.39) をHomeページに追加します (リスト10.41)。(ページネーションのテストは演習に回すことにします。10.5参照)。
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.40に示します。
app/views/shared/_feed_item.html.erb
<li id="<%= feed_item.id %>">
<%= link_to gravatar_for(feed_item.user), feed_item.user %>
<span class="user">
<%= link_to feed_item.user.name, feed_item.user %>
</span>
<span class="content"><%= feed_item.content %></span>
<span class="timestamp">
Posted <%= time_ago_in_words(feed_item.created_at) %> ago.
</span>
</li>
リスト10.40は、以下のコードを使用して各フィードにCSS idも追加します。
<li id="<%= feed_item.id %>">
これはリスト10.37のテストで要求されていたものです。
後は、いつものようにフィードパーシャルを表示すればHomeページにフィードを追加できます (リスト10.41)。この結果はホームページのフィードとして表示されます (図10.14)。
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.42のように空の配列を渡しておくことです11。
create
アクションに (空の) @feed_items
インスタンス変数を追加する。app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
.
.
.
def create
@micropost = current_user.microposts.build(micropost_params)
if @micropost.save
flash[:success] = "Micropost created!"
redirect_to root_url
else
@feed_items = []
render 'static_pages/home'
end
end
.
.
.
end
この時点で、(プロト)フィードとそのテストはすべて動くはずです。
$ bundle exec rspec spec/
10.3.4マイクロポストを削除する
最後の機能として、マイクロポストリソースにポストを削除する機能を追加します。これはユーザー削除と同様に(9.4.2)、"delete" リンクで実現します (図10.16)。ユーザーの削除は管理者ユーザーのみが行えるように制限されていたのに対し、今回の場合はカレントユーザーが作成したマイクロポストに対してのみ削除リンクが動作するようにします。
最初のステップとして、リスト10.40のマイクロポストパーシャルに削除リンクを追加します。同様に、リスト10.40のフィードアイテムパーシャルにリンクを追加します。追加の結果をリスト10.43およびリスト10.44に示します (これら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.45)。
destroy
アクションをテストする。spec/requests/micropost_pages_spec.rb
require 'spec_helper'
describe "Micropost pages" do
.
.
.
describe "micropost destruction" do
before { FactoryGirl.create(:micropost, user: user) }
describe "as correct user" do
before { visit root_path }
it "should delete a micropost" do
expect { click_link "delete" }.to change(Micropost, :count).by(-1)
end
end
end
end
リスト9.46のユーザー用アプリケーションコードと似ていますが、最も大きく異なるのは、マイクロポストの場合は、カレントユーザーが実際に指定のidのマイクロポストを持っているのかをチェックするのにadmin_user
before_actionではなく、 correct_user
before_actionを使用している点です。コードをリスト10.46に示します。2番目に新しいポストを削除した場合の結果を図10.17に示します。
destroy
アクション。app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
before_action :signed_in_user, only: [:create, :destroy]
before_action :correct_user, only: :destroy
.
.
.
def destroy
@micropost.destroy
redirect_to root_url
end
private
def micropost_params
params.require(:micropost).permit(:content)
end
def correct_user
@micropost = current_user.microposts.find_by(id: params[:id])
redirect_to root_url if @micropost.nil?
end
end
correct_user
before_actionでは、マイクロポストを以下のように関連付けを経由して見つけていることに注目してください。
current_user.microposts.find_by(id: params[:id])
これによって、カレントユーザーに所属するマイクロポストだけが自動的に見つかることが保証されます。この場合、 find
ではなくfind_by
を使用します。これは、前者ではマイクロポストがない場合に例外が発生しますが、後者はnil
を返すためです。ところで、Rubyの例外処理に慣れている方なら、correct_user
のフィルタを以下のように書くこともできます。
def correct_user
@micropost = current_user.microposts.find(params[:id])
rescue
redirect_to root_url
end
Micropost
モデルを以下のように直接使用してcorrect_user
フィルタを実装することもできます。
@micropost = Micropost.find_by(id: params[:id])
redirect_to root_url unless current_user?(@micropost.user)
上のコードはリスト10.46のコードと同等だと思うかもしれませんが、Wolfram Arnoldがブログ記事「RailsにおけるAccess Control 101とシティバンクでのハッキング事件 (英語)」で解説しているように、対象を探しだすときには常に関連付けを経由することをセキュリティの観点から強くお勧めいたします。
この節のコードで、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.43およびリスト10.44から削除リンクの重複を削除してください。
- 図10.18に示すように、非常に長い単語を入力するとレイアウトが崩れてしまいます。リスト10.47で定義した
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.47の最後の部分でも使用しています。
app/helpers/microposts_helper.rb
module MicropostsHelper
def wrap(content)
sanitize(raw(content.split.map{ |s| wrap_long_string(s) }.join(' ')))
end
private
def wrap_long_string(text, max_width = 30)
zero_width_space = "​"
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
型を使用してください。↑- (ここではカスタムGravatarを持つ5人のユーザーと、デフォルトGravatarを持つ1人のユーザー) ↑
- もしこのメソッドが生成するSQLに興味があるのであれば、
log/development.log
をtailしてみてください (コマンドラインでファイルにtailコマンドを実行するという意味)。 ↑ - Faker gemのlorem ipsumサンプルテキストはランダムに生成される仕様になっているため、サンプルマイクロポストの内容はこの図と違っているはずです。↑
- 便宜上、リスト10.21はこの章で必要なCSSをすべて含んでいます。↑
- 他の2つのリソースとは、7.2のUsersリソースと8.1のSessionsリソースです。↑
- 8.2.1で、ヘルパーメソッドはデフォルトではビューでのみ利用可能であると説明しましたが、
include SessionsHelper
をApplicationコントローラに追加してコントローラでも利用できるようにしました (リスト8.14)。↑ - 1.1.1で、本書を読み終わったら次に純粋なRubyの本を読む事を薦めましたが、これは、
include?
などのメソッドを学んでいただきたいからというのが理由の一つです。↑ where
やlikeの詳細については、Railsガイドの「Active Record クエリインターフェイス」を参照してください。↑- 残念ですが、この場合はページ分割されたフィードを返してもうまく動きません。動かない理由を確認したい方は、実際に実装してページネーションリンクをクリックしてみてください。↑
Railsチュートリアルは YassLab 社によって運営されています。
コンテンツを継続的に提供するため、書籍・動画・質問対応サービスなどもご検討していただけると嬉しいです。
研修支援や教材連携にも対応しています。note マガジンや YouTube チャンネルも始めたので、よければぜひ遊びに来てください!