Ruby on Rails チュートリアル
-
第3版 目次
- 第1章 ゼロからデプロイまで
- 第2章 Toyアプリケーション
- 第3章 ほぼ静的なページの作成
- 第4章 Rails風味のRuby
- 第5章 レイアウトを作成する
- 第6章 ユーザーのモデルを作成する
- 第7章 ユーザー登録
- 第8章 ログイン、ログアウト
- 第9章 ユーザーの更新・表示・削除
- 第10章 アカウント有効化とパスワード再設定
- 第11章 ユーザーのマイクロポスト
- 第12章 ユーザーをフォローする
|
||
第3版 目次
|
||
最新版を読む |
Ruby on Rails チュートリアル
プロダクト開発の0→1を学ぼう
下記フォームからメールアドレスを入力していただくと、招待リンクが記載されたメールが届きます。リンクをクリックし、アカウントを有効化した時点から『30分間』解説動画のお試し視聴ができます。
メール内のリンクから視聴を開始できます。
第3版 目次
- 第1章 ゼロからデプロイまで
- 第2章 Toyアプリケーション
- 第3章 ほぼ静的なページの作成
- 第4章 Rails風味のRuby
- 第5章 レイアウトを作成する
- 第6章 ユーザーのモデルを作成する
- 第7章 ユーザー登録
- 第8章 ログイン、ログアウト
- 第9章 ユーザーの更新・表示・削除
- 第10章 アカウント有効化とパスワード再設定
- 第11章 ユーザーのマイクロポスト
- 第12章 ユーザーをフォローする
第11章ユーザーのマイクロポスト
サンプルアプリケーションのコア部分を開発するために、これまでにユーザー、セッション、アカウント有効化、パスワードリセットという4つのリソースについて見てきました。そして、これらのうち「ユーザー」というリソースだけが、Active Recordによってデータベース上のテーブルと紐付いています。全ての準備が整った今、ユーザーが短いメッセージを投稿できるようにするためのリソース「マイクロポスト」を追加していきます1。第2章で簡易的なマイクロポスト投稿フォームに触れましたが、この章では、2.3で記述したMicropostデータモデルを作成し、Userモデルとhas_many
およびbelongs_to
メソッドを使って関連付けを行い、さらに、結果を処理し表示するために必要なフォームとその部品を作成します (11.4で画像のアップロードも実装します)。第12章では、マイクロポストのフィードを受け取るために、ユーザーをフォローするという概念を導入し、Twitterのミニクローンを完成させます。
11.1 Micropostモデル
まずはMicropostリソースの最も本質的な部分を表現するMicropostモデルを作成するところから始めましょう。2.3で作成したモデルと同様に、この新しいMicropostモデルもデータ検証とUserモデルの関連付けを含んでいます。以前のモデルとは違って、今回のマイクロポストモデルは完全にテストされ、デフォルトの順序を持ち、また親であるユーザーが破棄された場合には自動的に破棄されるようにします。
Git をバージョン管理に使っている場合は、いつものようにトピックブランチを作成しておきましょう。
$ git checkout master
$ git checkout -b user-microposts
11.1.1 基本的なモデル
Micropostモデルは、マイクロポストの内容を保存するcontent
属性と、特定のユーザーとマイクロポストを関連付けるuser_id
属性の2つの属性だけを持ちます。実行した結果のMicropostモデルの構造は図11.1のようになります。
図11.1のモデルでは、マイクロポストの投稿にString
型ではなくtext
型を使っている点に注目してください。これは、ある程度の量のテキストを格納するときに使われる型です。String
型でも255文字までは格納できるため、この型でも11.1.2で実装する140文字制限を満たせるのですが、Text
型の方が表現豊かなマイクロポストを実現できます。たとえば、11.3.2では投稿フォームにString用のテキストフィールドではなくてText用のテキストエリアを使うため、より自然な投稿フォームが実現できます。また、Text
型の方が将来における柔軟性に富んでいて、たとえばいつか国際化をするときに、言語に応じて投稿の長さを調節することもできます。さらに、Text
型を使っていても本番環境でパフォーマンスの差は出ません2。これらの理由から、デメリットよりもメリットの方が多いので、今回はText型を採用しています。
では、リスト6.1でUserモデルを生成したときと同様に、Railsのgenerate model
コマンドを使ってMicropostモデルを生成してみます。
$ rails generate model Micropost content:text user:references
リスト6.2でデータベースにusers
テーブルを作るマイグレーションファイルを生成した時と同様に、このgenerate
コマンドはmicroposts
テーブルを作成するためのマイグレーションファイルを生成します (リスト11.1)。Userモデルとの最大の違いはreferences
型を利用している点です。これを利用すると、自動的にインデックスと外部参照キー付きのuser_id
カラムが追加され3、UserとMicropostを関連付けする下準備をしてくれます。Userモデルのときと同じで、Micropostモデルのマイグレーションファイルでもt.timestamps
という行 (マジックカラム) が自動的に生成されています。これにより、 6.1.1で説明したようにcreated_at
とupdated_at
というカラムが追加されます (図 11.1)。なお、created_at
カラムは、11.1.4や11.2.1の実装を進めていく上で必要なカラムです。
class CreateMicroposts < ActiveRecord::Migration
def change
create_table :microposts do |t|
t.text :content
t.references :user, index: true, foreign_key: true
t.timestamps null: false
end
add_index :microposts, [:user_id, :created_at]
end
end
ここで、リスト11.1ではuser_id
とcreated_at
カラムにインデックスが付与されていることに注目してください (コラム6.2)。これは、user_idに関連付けられたすべてのマイクロポストを作成時刻の逆順で取り出すためです。
add_index :microposts, [:user_id, :created_at]
user_id
とcreated_at
両方のカラムを1つの配列に含めることで、Active Recordで両方のキーを同時に使用する複合キーインデックスを作成できます。
それでは、リスト11.1をマイグレーション使って、いつものようにデータベースを更新してみましょう。
$ bundle exec rake db:migrate
11.1.2 Micropostのバリデーション
基本的なモデルを作成したので、次に要求される制限を実現するためのバリデーションを追加しましょう。Micropostモデルを作成したときに、マイクロポストは投稿したユーザーのid (user_id) を持たせるようにしました。これを使って、慣習的に正しくActive Recordの関連付けを実装していきます (11.1.3) が、まずはMicropost
モデル単体を (テスト駆動開発で) 動くようにしてみます。
Micropostの初期テストはUserモデルの初期テスト (リスト6.7) と似ています。まずはsetup
のステップで、fixtureのサンプルユーザーと紐付けた新しいマイクロポストを作成しています。次に、作成したマイクロポストが有効かどうかをチェックしてます。最後に、あらゆるマイクロポストはユーザーのidを持っているべきなので、user_id
の存在性のバリデーションに対するテストも追加します。これらの要素を1つにまとめると、リスト11.2のようなテストコードになります。
require 'test_helper'
class MicropostTest < ActiveSupport::TestCase
def setup
@user = users(:michael)
# 次の行は慣習的に間違っている
@micropost = Micropost.new(content: "Lorem ipsum", user_id: @user.id)
end
test "should be valid" do
assert @micropost.valid?
end
test "user id should be present" do
@micropost.user_id = nil
assert_not @micropost.valid?
end
end
setup
メソッドの中でコメントしているとおり、マイクロポストを作成するコードは動きますが、慣習的には正しくありません (11.1.3で修正します)。
また、有効性に対するテストは成功しますが、存在性に対するテストは失敗するはずです。これは、Micropostモデルのバリデーションがまだ何もないことが原因です。
$ bundle exec rake test:models
これを修正するためには、ユーザーidに対するバリデーションを追加する必要があります (リスト11.4)。なお、リスト11.4にはすでにbelongs_to
というコードがありますが、これはリスト11.1のマイグレーションによって自動的に生成されたコードです。この行の意味については、11.1.3で説明します。
user_id
に対する検証 GREEN app/models/micropost.rb
class Micropost < ActiveRecord::Base
belongs_to :user
validates :user_id, presence: true
end
これにより、モデルのテストは成功するようになります。
$ bundle exec rake test:models
次に、マイクロポストのcontent
属性に対するバリデーションを追加しましょう (2.3.2で紹介した例と同じです)。user_id
属性と同様に、content
属性も存在する必要があり、さらにマイクロポストが140文字より長くならないよう制限を加えます。6.2で使ったUserモデルのバリデーションを参考に、まずはこれらの制限を簡潔にテストしてみます。結果はリスト11.6のとおりです。
require 'test_helper'
class MicropostTest < ActiveSupport::TestCase
def setup
@user = users(:michael)
@micropost = Micropost.new(content: "Lorem ipsum", user_id: @user.id)
end
test "should be valid" do
assert @micropost.valid?
end
test "user id should be present" do
@micropost.user_id = nil
assert_not @micropost.valid?
end
test "content should be present" do
@micropost.content = " "
assert_not @micropost.valid?
end
test "content should be at most 140 characters" do
@micropost.content = "a" * 141
assert_not @micropost.valid?
end
end
6.2と同様で、リスト11.6ではマイクロポストの長さをテストするために、文字列の乗算を使用しています。
$ rails console
>> "a" * 10
=> "aaaaaaaaaa"
>> "a" * 141
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
これに対応するアプリケーション側の実装は、Userのname
用バリデーション (リスト6.16) と全く同じです。リスト11.7に結果を示します。
class Micropost < ActiveRecord::Base
belongs_to :user
validates :user_id, presence: true
validates :content, presence: true, length: { maximum: 140 }
end
この時点では、全てのテストが成功するはずです。
$ bundle exec rake test
11.1.3 User/Micropostの関連付け
Webアプリケーション用のデータモデルを構築するにあたって、個々のモデル間での関連付けを十分考えておくことが重要です。今回の場合は、 2.3.3でも示したように、それぞれのマイクロポストは1人のユーザーと関連付けられ、それぞれのユーザーは (潜在的に) 複数のマイクロポストと関連付けられます。この関連付けを図11.2と図11.3に示します。これらの関連付けを実装するための一環として、Micropostモデルに対するテストを作成し、さらにUserモデルにいくつかのテストを追加します。
この節で定義するbelongs_to
/has_many
関連付けを使用することで、表11.1に示すようなメソッドをRailsで使えるようになります。表11.1では、以下のメソッドではなく
Micropost.create
Micropost.create!
Micropost.new
以下のメソッドになっていることに注意してください。
user.microposts.create
user.microposts.create!
user.microposts.build
これらのメソッドは使うと、紐付いているユーザーを通してマイクロポストを作成することができます (慣習的に正しい方法です)。新規のマイクロポストがこの方法で作成される場合、user_id
は自動的に正しい値に設定されます。この方法を使うと、たとえば以下のような
@user = users(:michael)
# 次の行は慣習的に間違っている
@micropost = Micropost.new(content: "Lorem ipsum", user_id: @user.id)
という書き方 (リスト11.2) が、以下のように書き換えられます。
@user = users(:michael)
@micropost = @user.microposts.build(content: "Lorem ipsum")
(new
メソッドと同様に、build
メソッドはオブジェクトを返しますがデータベースには反映されません。) 一度正しい関連付けを定義してしまえば、@micropost
変数のuser_id
には、関連するユーザーのidが自動的に設定されます。
メソッド | 用途 |
micropost.user |
Micropostに紐付いたUserオブジェクトを返す |
user.microposts |
Userのマイクロポストの集合を返す |
user.microposts.create(arg) |
user に紐付いたマイクロポストを作成する
|
user.microposts.create!(arg) |
user に紐付いたマイクロポストを作成する (失敗時に例外を発生) |
user.microposts.build(arg) |
user に紐付いた新しいMicropostオブジェクトを返す
|
user.microposts.find_by(id: 1) |
user に紐付いていて、id が1 であるマイクロポストを検索する
|
@user.microposts.build
のようなコードを使うためには、 UserモデルとMicropostモデルをそれぞれ更新して、関連付ける必要があります。Micropostモデルの方では、belongs_to :user
というコードが必要になるのですが、これは リスト11.1のマイグレーションによって自動的に生成されているはずです (リスト11.9)。一方、Userモデルの方では、has_many :microposts
と追加する必要があります。ここは自動的に生成されないので、手動で追加してください (リスト11.10)。
belongs_to
) 関連付け GREEN app/models/micropost.rb
class Micropost < ActiveRecord::Base
belongs_to :user
validates :user_id, presence: true
validates :content, presence: true, length: { maximum: 140 }
end
has_many
) 関連付け GREEN app/models/user.rb
class User < ActiveRecord::Base
has_many :microposts
.
.
.
end
正しく関連付けができたら、リスト11.2のsetup
メソッドを修正して、慣習的に正しくマイクロポストを作成してみます (リスト11.11)。
require 'test_helper'
class MicropostTest < ActiveSupport::TestCase
def setup
@user = users(:michael)
@micropost = @user.microposts.build(content: "Lorem ipsum")
end
test "should be valid" do
assert @micropost.valid?
end
test "user id should be present" do
@micropost.user_id = nil
assert_not @micropost.valid?
end
.
.
.
end
もちろん、些細なリファクタリングでしかないので、テストは成功したままになっているはずです。
$ bundle exec rake test
11.1.4 マイクロポストを改良する
この項では、UserとMicropostの関連付けを改良していきます。具体的には、ユーザーのマイクロポストを特定の順序で取得できるようにしたり、マイクロポストをユーザーに依存させて、ユーザーが削除されたらマイクロポストも自動的に削除されるようにしていきます。
デフォルトのスコープ
user.microposts
メソッドはデフォルトでは読み出しの順序に対して何も保証しませんが、 ブログやTwitterの慣習に従って、作成時間の逆順、つまり最も新しいマイクロポストを最初に表示するようにしてみましょう4。これを実装するためには、default scopeというテクニックを使います。
この機能のテストは、見せかけの成功に陥りやすい部分で、「アプリケーション側の実装が本当は間違っているのにテストが成功してしまう」という罠があります。正しいテストを書くために、ここではテスト駆動開発で進めていきます。具体的には、まずデータベース上の最初のマイクロポストが、fixture内のマイクロポスト (most_recent
) と同じであるか検証するテストを書いていきましょう (リスト11.13)。
require 'test_helper'
class MicropostTest < ActiveSupport::TestCase
.
.
.
test "order should be most recent first" do
assert_equal microposts(:most_recent), Micropost.first
end
end
リスト11.13では、マイクロポスト用のfixtureファイルからサンプルデータを読み出しているので、次のfixtureファイルも必要になります (リスト11.14)。
orange:
content: "I just ate an orange!"
created_at: <%= 10.minutes.ago %>
tau_manifesto:
content: "Check out the @tauday site by @mhartl: http://tauday.com"
created_at: <%= 3.years.ago %>
cat_video:
content: "Sad cats are sad: http://youtu.be/PKffm2uI4dk"
created_at: <%= 2.hours.ago %>
most_recent:
content: "Writing a short test"
created_at: <%= Time.zone.now %>
ここでは、埋め込みRubyを使ってcreated_at
カラムに明示的に値をセットしている点に注目してください。このマジックカラムはRailsによって自動的に更新されるため、基本的には手動で更新することはできないのですが、fixtureファイルの中ではそれが可能になっています。また、原理的には必要はないかもしれませんが、ほとんどのシステムでは上から順に作成されるので、fixtureファイルでも意図的に順序をいじっています。たとえば、ファイル内の一番下のサンプルデータは最後に生成されるので、最も新しい投稿になるように修正する、といった感じです。ただ、この振る舞いは恐らくシステムに依存していて崩れやすいので、(本来は) この振る舞いに依存したテストは書くべきでは無いでしょう。
リスト11.13とリスト11.14を追加して、このテストを実行すると失敗するはずです。
$ bundle exec rake test TEST=test/models/micropost_test.rb \
> TESTOPTS="--name test_order_should_be_most_recent_first"
次に、Railsのdefault_scope
メソッドを使ってこのテストを成功させます。このメソッドは、データベースから要素を取得したときの、デフォルトの順序を指定するメソッドです。特定の順序にしたい場合は、default_scope
の引数にorder
を与えます。たとえば、created_at
カラムの順にしたい場合は次のようになります。
order(:created_at)
ただし、残念ながらデフォルトの順序が昇順となっているので、このままでは数の小さい値から大きい値にソートされてしまいます (最も古い投稿が最初に表示されてしまいます)。順序を逆にしたい場合は、一段階低いレベルの技術ではありますが、次のように生のSQLを引数に与える必要があります。
order('created_at DESC')
ここで使用したDESC
とは、SQLの“降順 (descending)”を指します。したがって、これで新しい投稿から古い投稿の順に並びます5。古いバージョンのRailsでは、欲しい振る舞いにするためには生のSQLを書くしか選択肢がなかったのですが、Rails 4.0からは次のようにRubyの文法でも書けるようになりました。
order(created_at: :desc)
このコードを使ってMicropostモデルを更新した結果を、リスト11.16に示します。
default_scope
ででマイクロポストを順序付ける GREEN app/models/micropost.rb
class Micropost < ActiveRecord::Base
belongs_to :user
default_scope -> { order(created_at: :desc) }
validates :user_id, presence: true
validates :content, presence: true, length: { maximum: 140 }
end
リスト11.16では新たに、ラムダ式 (Stabby lambda) という文法を使っています。これは、Procやlambda (もしくは無名関数)と呼ばれるオブジェクトを作成する文法です。->
というラムダ式は、ブロック (4.3.2) を引数に取り、Procオブジェクトを返します。このオブジェクトは、call
メソッドが呼ばれたとき、ブロック内の処理を評価します。この構文をコンソールで確かめてみましょう。
>> -> { puts "foo" }
=> #<Proc:0x007fab938d0108@(irb):1 (lambda)>
>> -> { puts "foo" }.call
foo
=> nil
(ProcやRubyのトピックとしてはやや高度な部類に含まれるので、今すぐわからなくても心配する必要はありません。)
リスト11.16のコードを追加することで、テストが成功するようになります。
$ bundle exec rake test
Dependent: destroy
順序についてはひとまずここで区切ることにし、今度はマイクロポストに第二の要素を追加してみましょう。9.4で書いたように、サイト管理者はユーザーを破棄する権限を持ちます。ユーザーが破棄された場合、ユーザーのマイクロポストも同様に破棄されるべきです。
この振る舞いは、has_many
メソッドにオプションを渡してあげることで実装できます (リスト11.18)。
class User < ActiveRecord::Base
has_many :microposts, dependent: :destroy
.
.
.
end
dependent: :destroy
というオプションを使うと、ユーザーが削除されたときに、そのユーザーに紐付いた (そのユーザーが投稿した) マイクロポストも一緒に削除されるようになります。これは、管理者がシステムからユーザーを削除したとき、持ち主の存在しないマイクロポストがデータベースに取り残されてしまう問題を防ぎます。
次に、リスト11.18が正しく動くかどうか、テストを使ってUserモデルを検証してみます。このテストでは、 (idを紐づけるための) ユーザーを作成することと、そのユーザーに紐付いたマイクロポストを作成する必要があります。その後、ユーザーを削除してみて、マイクロポストの数が1つ減っているかどうかを確認します。これらをコードにすると、結果はリスト11.19のようになります。([delete] リンクの統合テスト (リスト9.57) と比較してみてください。)
dependent: :destroy
のテスト GREEN test/models/user_test.rb
require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
@user = User.new(name: "Example User", email: "user@example.com",
password: "foobar", password_confirmation: "foobar")
end
.
.
.
test "associated microposts should be destroyed" do
@user.save
@user.microposts.create!(content: "Lorem ipsum")
assert_difference 'Micropost.count', -1 do
@user.destroy
end
end
end
リスト11.18のコードが正しく動いていれば、テストが成功するようになります。
$ bundle exec rake test
11.2 マイクロポストを表示する
Web経由でマイクロポストを作成する方法は現時点ではありませんが (11.3.2から作り始めます)、マイクロポストを表示することと、テストすることならできます。ここでは、Twitterのような独立したマイクロポストのindex
ページは作らずに、図11.4のモックアップに示したように、ユーザーのshow
ページで直接マイクロポストを表示させることにします。ユーザープロフィールにマイクロポストを表示させるため、最初に極めてシンプルなERbテンプレートを作成します。次に、9.3.2のサンプルデータ生成タスクにマイクロポストのサンプルを追加して、画面にサンプルデータが表示されるようにしてみます。
11.2.1 マイクロポストの描画
この項では、ユーザーのプロフィール画面 (show.html.erb
) でそのユーザーのマイクロポストを表示させ、また、これまでに投稿した総数も表示するようにしていきます。とはいえ、今回必要となるアイデアのほとんどは、9.3で実装したユーザーを表示する部分と似ています。
まずは、Micropostのコントローラとビューを作成するために、コントローラを生成しましょう (今回必要なのはビューだけで、Micropostsコントローラは11.3まで使いません)。
$ rails generate controller Microposts
今回の目的は、ユーザー毎にすべてのマイクロポストを描画できるようにすることです。9.3.5で見た次のコードでは、
<ul class="users">
<%= render @users %>
</ul>
_user.html.erb
パーシャルを使って自動的に@users
変数内のそれぞれのユーザーを出力していました。これを参考に、_micropost.html.erb
パーシャルを使ってマイクロポストのコレクションを表示しようとすると、次のようになります。
<ol class="microposts">
<%= render @microposts %>
</ol>
まずは、順序無しリストの ul
タグではなく、順序付きリストのol
タグを使っている点に注目してください。これは、マイクロポストが特定の順序 (新しい→古い) に依存しているためです。次に、対応するパーシャルをリスト11.21に示します。
<li id="micropost-<%= micropost.id %>">
<%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
<span class="user"><%= link_to micropost.user.name, micropost.user %></span>
<span class="content"><%= micropost.content %></span>
<span class="timestamp">
Posted <%= time_ago_in_words(micropost.created_at) %> ago.
</span>
</li>
ここではtime_ago_in_words
というヘルパーメソッドを使っています。これはメソッド名の表すとおりですが、「3分前に投稿」といった文字列を出力します。具体的な効果について11.2.2で説明します。また、リスト11.21では各マイクロポストに対してCSSのidを割り振っています。
<li id="micropost-<%= micropost.id %>">
これは一般的に良いとされる慣習で、たとえば将来、JavaScriptを使って各マイクロポストを操作したくなったときなどに役立ちます。
次は、一度にすべてのマイクロポストが表示されてしまう潜在的問題に対処します。9.3.3ではページネーションを使いましたが、今回も同じ方法でこの問題を解決します。前回同様、will_paginate
メソッドを使うと次のようになります。
<%= will_paginate @microposts %>
リスト9.41のユーザー一覧画面のコードと比較すると、少し違っています。。以前は次のように単純なコードでした。
<%= will_paginate %>
実は、上のコードは引数なしで動作していました。これはwill_paginate
が、Usersコントローラのコンテキストにおいて、@users
インスタンス変数が存在していることを前提としているためです。このインスタンス変数は、9.3.3でも述べたようにActiveRecord::Relation
クラスのインスタンスです。今回の場合は、ユーザーコントローラのコンテキストにおいて、マイクロポストをページネーションしたいため、明示的に@microposts
変数を will_paginate
に渡す必要があります。もちろん、そのような変数をユーザーshow
アクションで定義しなければなりません (リスト11.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テーブルに到達し、必要なマイクロポストのページを引き出してくれます。
最後の課題はマイクロポストの投稿数を表示することですが、これはcount
メソッドを使うことで解決できます。
user.microposts.count
paginate
と同様に、関連付けをとおしてcount
メソッドを呼び出すことができます。大事なことは、count
メソッドではデータベース上のマイクロポストを全部読みだしてから結果の配列に対してlength
を呼ぶ、といった無駄な処理はしていないという点です。そんなことをしたら、マイクロポストの数が増加するにつれて効率が低下してしまいます。そうではなく、(データベース内での計算は高度に最適化されいるので) データベースに代わりに計算してもらい、特定のuser_id
に紐付いたマイクロポストの数をデータベースに問い合わせています。(それでもcountメソッドがアプリケーションのボトルネックになるようなことがあれば、さらに高速なcounter cacheを使うこともできます。)
これですべての要素が揃ったので、プロフィール画面にマイクロポストを表示させてみましょう (リスト11.23)。(このとき、リスト7.19と同様にif @user.microposts.any?
を使って、ユーザーのマイクロポストが1つもない場合には空のリストを表示させていない点にも注目してください。)
show
ページ (プロフィール画面) に追加する app/views/users/show.html.erb
<% provide(:title, @user.name) %>
<div class="row">
<aside class="col-md-4">
<section class="user_info">
<h1>
<%= gravatar_for @user %>
<%= @user.name %>
</h1>
</section>
</aside>
<div class="col-md-8">
<% if @user.microposts.any? %>
<h3>Microposts (<%= @user.microposts.count %>)</h3>
<ol class="microposts">
<%= render @microposts %>
</ol>
<%= will_paginate @microposts %>
<% end %>
</div>
</div>
ここで、改良した新しいプロフィール画面をブラウザで見てみましょう (図11.5)。...何とも寂しいページで、がっかりですね。マイクロポストが1つもないのでは無理もありません。ではここでマイクロポストを追加しましょう。
11.2.2 マイクロポストのサンプル
11.2.1のユーザーマイクロポストのテンプレート作成作業の成果は、何とも拍子抜けでした。9.3.2のサンプルデータ生成タスクにマイクロポストも追加して、この情けない状況を修正しましょう。
すべてのユーザーにマイクロポストを追加しようとすると時間が掛かり過ぎるので、take
メソッドを使って最初の6人だけに追加します。
User.order(:created_at).take(6)
(このとき、order
メソッドを経由することで、明示的に最初の (IDが小さい順に) 6人を呼び出すようにしています。)
この6人については、1ページの表示限界数 (30) を越えさせるために、それぞれ50個分のマイクロポストを追加するようにしています。また、各投稿内容についてですが、Faker gemにLorem.sentenceという便利なメソッドがあるので、これを使います6。変更した結果はリスト11.24のとおりです。(リスト11.24のループの順序に違和感があるかもしれませんが、これは12.3でステータスフィード (いわゆるタイムライン) を実装するときに役立ちます。というのも、ユーザー毎に50個分のマイクロポストをまとめて作成してしまうと、ステータスフィードに表示される投稿がすべて同じユーザーになってしまい、視覚的な見栄えが悪くなるからです。)
.
.
.
users = User.order(:created_at).take(6)
50.times do
content = Faker::Lorem.sentence(5)
users.each { |user| user.microposts.create!(content: content) }
end
ここで、いつものように開発環境用のデータベースで再度サンプルデータを生成します。
$ bundle exec rake db:migrate:reset
$ bundle exec rake db:seed
生成し終わったら、Railsサーバーを一度落として、起動し直してください。
それぞれのマイクロポストの情報を表示することによって、11.2.1の地道な作業がやっと報われはじめました7。実行結果を図11.6に示します。
図11.6のページにはマイクロポスト固有のスタイルが与えられていないので、リスト11.25を追加して、結果のページを見てみましょう8。
.
.
.
/* microposts */
.microposts {
list-style: none;
padding: 0;
li {
padding: 10px 0;
border-top: 1px solid #e8e8e8;
}
.user {
margin-top: 5em;
padding-top: 0;
}
.content {
display: block;
margin-left: 60px;
img {
display: block;
padding: 5px 0;
}
}
.timestamp {
color: $gray-light;
display: block;
margin-left: 60px;
}
.gravatar {
float: left;
margin-right: 10px;
margin-top: 5px;
}
}
aside {
textarea {
height: 100px;
margin-bottom: 5px;
}
}
span.picture {
margin-top: 10px;
input {
border: 0;
}
}
図11.7では最初のユーザーのプロフィール画面を、図11.8では2番目のユーザーのプロフィール画面を表示しています。最後の図11.9では、最初のユーザーの2番目のページと、下部にあるページネーションのリンクを表示しています。各マイクロポストの表示には、3つのどの場合にも、それが作成されてからの時間 ("1分前に投稿" など) が表示されていることに注目してください。これはリスト11.21のtime_ago_in_words
メソッドによるものです。数分待ってからページを再度読み込むと、このテキストは自動的に新しい時間に基づいて更新されます。
11.2.3 プロフィール画面におけるマイクロポストのテスト
アカウントを有効化したばかりのユーザーはプロフィール画面にリダイレクトされるので、そのプロフィール画面が正しく描画されていることは、単体テストを通して確認済みです (リスト10.31)。この項では、プロフィール画面で表示されるマイクロポストに対して、統合テストを書いていきます。まずは、プロフィール画面用の統合テストを生成してみましょう。
$ rails generate integration_test users_profile
invoke test_unit
create test/integration/users_profile_test.rb
プロフィール画面におけるマイクロポストをテストするためには、ユーザーに紐付いたマイクロポストのテスト用データが必要になります。Railsの慣習に従って、関連付けされたテストデータをfixtureファイルに追加すると、次のようになります。
orange:
content: "I just ate an orange!"
created_at: <%= 10.minutes.ago %>
user: michael
user
にmichael
という値を渡すと、Railsはfixtureファイル内の対応するユーザーを探し出して、(もし見つかれば) マイクロポストに関連付けてくれます。
michael:
name: Michael Example
email: michael@example.com
.
.
.
また、マイクロポストのページネーションをテストするためには、マイクロポスト用のfixtureにいくつかテストデータを追加する必要がありますが、これはリスト9.43でユーザーを追加したときと同様に、埋め込みRubyを使うと簡単です。
<% 30.times do |n| %>
micropost_<%= n %>:
content: <%= Faker::Lorem.sentence(5) %>
created_at: <%= 42.days.ago %>
user: michael
<% end %>
これらのコードを1つにまとめると、マイクロポスト用のfixtureファイルはリスト11.26のようになります。
orange:
content: "I just ate an orange!"
created_at: <%= 10.minutes.ago %>
user: michael
tau_manifesto:
content: "Check out the @tauday site by @mhartl: http://tauday.com"
created_at: <%= 3.years.ago %>
user: michael
cat_video:
content: "Sad cats are sad: http://youtu.be/PKffm2uI4dk"
created_at: <%= 2.hours.ago %>
user: michael
most_recent:
content: "Writing a short test"
created_at: <%= Time.zone.now %>
user: michael
<% 30.times do |n| %>
micropost_<%= n %>:
content: <%= Faker::Lorem.sentence(5) %>
created_at: <%= 42.days.ago %>
user: michael
<% end %>
テストデータの準備は完了したので、これからテストを書いていきますが、今回のテストはやや単純です。今回のテストでは、プロフィール画面にアクセスした後に、ページタイトルとユーザー名、Gravatar、マイクロポストの投稿数、そしてページ分割されたマイクロポスト、といった順でテストしていきます。これらをテストコードにした結果はリスト11.27のとおりです (Applicationヘルパーをインクルードすることで、リスト4.2のfull_title
ヘルパーが利用できている点に注目してください9)。
require 'test_helper'
class UsersProfileTest < ActionDispatch::IntegrationTest
include ApplicationHelper
def setup
@user = users(:michael)
end
test "profile display" do
get user_path(@user)
assert_template 'users/show'
assert_select 'title', full_title(@user.name)
assert_select 'h1', text: @user.name
assert_select 'h1>img.gravatar'
assert_match @user.microposts.count.to_s, response.body
assert_select 'div.pagination'
@user.microposts.paginate(page: 1).each do |micropost|
assert_match micropost.content, response.body
end
end
end
リスト11.27ではマイクロポストの投稿数をチェックするために、第10章の演習(10.5)で紹介したresponse.body
を使っています。名前を見ると誤解されがちですが、response.body
にはそのページの完全なHTMLが含まれています (HTMLのbodyタグだけではありません)。したがって、そのページのどこかしらにマイクロポストの投稿数が存在するのであれば、次のように探し出してマッチできるはずです。
assert_match @user.microposts.count.to_s, response.body
これはassert_select
よりもずっと抽象的なメソッドです。特に、 assert_select
ではどのHTMLタグを探すのか伝える必要がありますが、assert_match
メソッドではその必要がない点が違います。
また、リスト11.27のassert_select
の引数では、ネストした文法を使っている点にも注目してください。
assert_select 'h1>img.gravatar'
このように書くことで、h1
タグ (トップレベルの見出し) の内側にある、gravatar
クラス付きのimg
タグがあるかどうかをチェックできます。
そして、アプリケーション側のコードは実装済みなので、これらのテストは成功するはずです。
$ bundle exec rake test
11.3 マイクロポストを操作する
データモデリングとマイクロポスト表示テンプレートの両方が完成したので、次はWeb経由でそれらを作成するためのインターフェイスに取りかかりましょう。この節では、ステータスフィード (第12章で完成させます) の最初のヒントをお見せします。最後に、ユーザーがマイクロポストをWeb経由で破棄できるようにします。
従来のRails開発の慣習と異なる箇所が1つあります。Micropostsリソースへのインターフェイスは、主にProfileページとHomeページのコントローラを経由して実行されるので、Micropostsコントローラにはnew
やedit
のようなアクションは不要ということになります。create
とdestroy
があれば十分です。したがって、Micropostsのリソースはリスト11.29のようになります。その結果、リスト11.29のコードは、フルセットのルーティング (表2.3) のサブセットであるRESTfulルート (表11.2) になります。もちろん、シンプルになったということは完成度がさらに高まったということの証しであり、退化したわけではありません。第2章でscaffoldに頼りきりだった頃からここに至るまでは長い道のりでしたが、今ではscaffoldが生成するような複雑なコードはほとんど不要になりました。
Rails.application.routes.draw do
root 'static_pages#home'
get 'help' => 'static_pages#help'
get 'about' => 'static_pages#about'
get 'contact' => 'static_pages#contact'
get 'signup' => 'users#new'
get 'login' => 'sessions#new'
post 'login' => 'sessions#create'
delete 'logout' => 'sessions#destroy'
resources :users
resources :account_activations, only: [:edit]
resources :password_resets, only: [:new, :create, :edit, :update]
resources :microposts, only: [:create, :destroy]
end
HTTPリクエスト | URL | アクション | 用途 |
POST | /microposts | create |
新しいマイクロポストを作る |
DELETE | /microposts/1 | destroy |
id=1 のマイクロポストを削除する
|
11.3.1 マイクロポストのアクセス制御
Micropostsリソースの開発では、Micropostsコントローラ内のアクセス制御から始めることにしましょう。関連付けられたユーザーを通してマイクロポストにアクセスするので、create
アクションやdestroy
アクションを利用するユーザーは、ログイン済みでなければなりません。
ログイン済みかどうかを確かめるテストでは、Usersコントローラ用のテストがそのまま役に立ちます (リスト9.17、リスト9.56)。つまり、正しいリクエストを各アクションに向けて発行し、マイクロポストの数が変化していないかどうか、また、リダイレクトされるかどうかを確かめればよいのです (リスト11.30)。
require 'test_helper'
class MicropostsControllerTest < ActionController::TestCase
def setup
@micropost = microposts(:orange)
end
test "should redirect create when not logged in" do
assert_no_difference 'Micropost.count' do
post :create, micropost: { content: "Lorem ipsum" }
end
assert_redirected_to login_url
end
test "should redirect destroy when not logged in" do
assert_no_difference 'Micropost.count' do
delete :destroy, id: @micropost
end
assert_redirected_to login_url
end
end
リスト11.30のテストにパスするためには、少しアプリケーション側のコードをリファクタリングしておく必要があります。というのも、9.2.1では、beforeフィルターのlogged_in_user
メソッドを使って、ログインを要求したことについて思い出してください (リスト9.12)。あのときはUsersコントローラ内にこのメソッドがあったので、beforeフィルターで指定していましたが、 このメソッドはMicropostsコントローラでも必要です。そこで、各コントローラが継承するApplicationコントローラに (4.4.4)、このメソッドを移してしまいましょう。 結果はリスト11.31のようになります。
logged_in_user
メソッドをApplicationコントローラに移す app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
include SessionsHelper
private
# ユーザーのログインを確認する
def logged_in_user
unless logged_in?
store_location
flash[:danger] = "Please log in."
redirect_to login_url
end
end
end
コードが重複しないよう、このときUsersコントローラからもlogged_in_user
を削除しておきましょう。
リスト11.31のコードによって、Micropostsコントローラからもlogged_in_user
メソッドを呼び出せるようになりました。これにより、create
アクションやdestroy
アクションに対するアクセス制限が、beforeフィルターで簡単に実装できるようになります (リスト11.32)。
class MicropostsController < ApplicationController
before_action :logged_in_user, only: [:create, :destroy]
def create
end
def destroy
end
end
これでテストにパスするはずです。
$ bundle exec rake test
11.3.2 マイクロポストを作成する
第7章では、HTTP POSTリクエストをUsersコントローラのcreate
アクションに発行するHTMLフォームを作成することで、ユーザーのサインアップを実装しました。マイクロポスト作成の実装もこれと似ています。主な違いは、別の micropost/new ページを使う代わりに、ホーム画面 (つまりルートパス) にフォームを置くという点です。図11.10のモックアップを見てください。
最後にホーム画面を実装したときは (図5.6)、[Sign up now!] ボタンが中央にありました。マイクロポスト作成フォームは、ログインしている特定のユーザーのコンテキストでのみ機能するので、この節の一つの目標は、ユーザーのログイン状態に応じて、ホーム画面の表示を変更することです。これについては、リスト11.35で実装します。
次に、マイクロポストのcreate
アクションを作り始めましょう。このアクションも、リスト7.23のユーザー用アクションと似ています。違いは、新しいマイクロポストをbuild
するためにuser/micropost関連付けを使用している点です (リスト11.34)。micropost_params
でStrong Parametersを使用していることにより、マイクロポストのcontent
属性だけがWeb経由で変更可能になっていることに注目してください。
create
アクション app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
before_action :logged_in_user, only: [:create, :destroy]
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を提供するコードを使用します (リスト11.35)。
<% if logged_in? %>
<div class="row">
<aside class="col-md-4">
<section class="user_info">
<%= render 'shared/user_info' %>
</section>
<section class="micropost_form">
<%= render 'shared/micropost_form' %>
</section>
</aside>
</div>
<% else %>
<div class="center jumbotron">
<h1>Welcome to the Sample App</h1>
<h2>
This is the home page for the
<a href="http://www.railstutorial.org/">Ruby on Rails Tutorial</a>
sample application.
</h2>
<%= link_to "Sign up now!", signup_path, class: "btn btn-lg btn-primary" %>
</div>
<%= link_to image_tag("rails.png", alt: "Rails logo"),
'http://rubyonrails.org/' %>
<% end %>
if
-else
分岐を使用してコードを書き分けている点が少し汚いですが、このコードのクリーンアップは演習に回すことにします (11.6)。
リスト11.35のコードを動かすためには、いくつかのPartialを作る必要があります。まずはHomeページの新しいサイドバーからです。以下のリスト11.36のようになります。
<%= link_to gravatar_for(current_user, size: 50), current_user %>
<h1><%= current_user.name %></h1>
<span><%= link_to "view my profile", current_user %></span>
<span><%= pluralize(current_user.microposts.count, "micropost") %></span>
プロフィールサイドバー (リスト11.23) のときと同様、リスト11.36のユーザー情報にも、そのユーザーが投稿したマイクロポストの総数が表示されていることに注目してください。ただし少し表示に違いがあります。プロフィールサイドバーでは、 “Microposts” をラベルとし、“Microposts (1)” と表示することは問題ありません。しかし、今回のように “1 microposts” と表示してしまうと英語の文法上誤りになってしまいます。そこで、7.3.3で紹介したpluralize
メソッドを使って “1 micropost” や “2 microposts” と表示するように調整しています。
次はマイクロポスト作成フォームを定義します (リスト11.37)。これはユーザー登録フォームに似ています (リスト7.13)。
<%= form_for(@micropost) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<div class="field">
<%= f.text_area :content, placeholder: "Compose new micropost..." %>
</div>
<%= f.submit "Post", class: "btn btn-primary" %>
<% end %>
リスト11.37のフォームが動くようにするためには、2箇所の変更が必要です。1つは、(以前と同様) 関連付けを使用して次のように@micropost
を定義することです。
@micropost = current_user.microposts.build
変更の結果をリスト11.38に示します。
home
アクションにマイクロポストのインスタンス変数を追加する app/controllers/static_pages_controller.rb
class StaticPagesController < ApplicationController
def home
@micropost = current_user.microposts.build if logged_in?
end
def help
end
def about
end
def contact
end
end
もちろん、current_user
メソッドはユーザーがログインしているときしか使えません。したがって、@micropost
変数もログインしているときしか定義されません。
リスト11.37を動かすためのもう1つの変更は、エラーメッセージのパーシャルを再定義することです。でなければ、リスト11.37の次のコードが動きません。
<%= render 'shared/error_messages', object: f.object %>
リスト7.18ではエラーメッセージパーシャルが@user
変数を直接参照していたことを思い出してください。今回は代わりに@micropost
変数を使う必要があります。これらのケースをまとめると、フォーム変数f
をf.object
とすることによって、関連付けられたオブジェクトにアクセスすることができます。したがって、以下のコードの場合
form_for(@user) do |f|
f.object
は@user
となり、以下のコードの場合は
form_for(@micropost) do |f|
ここでいう f.object
は、@micropost
などになります。
パーシャルにオブジェクトを渡すために、値がオブジェクトで、キーがパーシャルでの変数名と同じハッシュを利用します。これで、リスト11.37の2行目のコードが完成します。言い換えると、object: f.object
はerror_messages
パーシャルの中でobject
という変数名を作成してくれるので、この変数を使ってエラーメッセージを更新すればよいということです (リスト11.39)。
<% if object.errors.any? %>
<div id="error_explanation">
<div class="alert alert-danger">
The form contains <%= pluralize(object.errors.count, "error") %>.
</div>
<ul>
<% object.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
この時点でテストを走らせてみてください。テストが失敗したままになっています。
$ bundle exec rake test
なぜ失敗しているのでしょうか。ヒントはerror_messagesパーシャルの他の出現場所です。このパーシャルは他の場所でも使われていたため、ユーザー登録 (リスト7.18)、パスワード再設定 (リスト10.50)、そしてユーザー編集 (リスト9.2) のそれぞれのビューを更新する必要があったのです。各ビューを更新した結果を、リスト11.41、リスト11.43、リスト11.42に示します。
<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(@user) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control' %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
<%= f.submit "Create my account", class: "btn btn-primary" %>
<% end %>
</div>
</div>
<% provide(:title, "Edit user") %>
<h1>Update your profile</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(@user) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control' %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
<%= f.submit "Save changes", class: "btn btn-primary" %>
<% end %>
<div class="gravatar_edit">
<%= gravatar_for @user %>
<a href="http://gravatar.com/emails">change</a>
</div>
</div>
</div>
<% provide(:title, 'Reset password') %>
<h1>Password reset</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(@user, url: password_reset_path(params[:id])) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<%= hidden_field_tag :email, @user.email %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
<%= f.submit "Update password", class: "btn btn-primary" %>
<% end %>
</div>
</div>
これで、すべてのテストが成功するはずです。
$ bundle exec rake test
さらに、この章で作成したすべてのHTMLが適切に表示されるようになったはずです。最終的なフォームを図11.11に、投稿エラーが表示されたフォームを図11.12に示します。
11.3.3 フィードの原型
マイクロポスト投稿フォームが動くようになりましたが、今の段階では投稿した内容をすぐに見ることができません。というのも、Homeページにまだマイクロポストを表示する部分が実装されていないからです。図11.11のフォームが正しく動作しているかどうかを確認したい場合、正しいエントリーを投稿した後、プロフィールページに移動してポストを表示すればよいのですが、これはかなり面倒な作業です。図11.13のモックアップで示したような、ユーザー自身のポストを含むマイクロポストのフィードがないと不便です (第12章ではフィードを汎用化し、現在のユーザーによってフォローされているユーザーのマイクロポストも一緒に表示するフィードにする予定です)。
すべてのユーザーがフィードを持つので、feed
メソッドはUserモデルで作るのが自然です。フィードの原型では、まずは現在ログインしているユーザーのマイクロポストをすべて取得してきます。(次章で完全なフィードを実装するため) 今回は10.5で紹介したwhere
メソッドでこれを実現します。Micropost
モデルに変更を加えた結果を、リスト11.44に示します10。
class User < ActiveRecord::Base
.
.
.
# 試作feedの定義
# 完全な実装は第12章「ユーザーをフォローする」を参照してください。
def feed
Micropost.where("user_id = ?", id)
end
private
.
.
.
end
以下のコードで使用されている疑問符は、セキュリティ上重要な役割を果たしています。
Micropost.where("user_id = ?", id)
上の疑問符があることで、SQLクエリにインクルードされる前にid
が適切にエスケープされることを保証してくれるため、SQLインジェクションと呼ばれる深刻なセキュリティホールを避けることができます。この場合のid
属性は単なる整数 (すなわちself.id
はユーザーのid) であるため危険はありませんが、SQL文にインクルードされる変数を常にエスケープする習慣はぜひ身につけてください。
注意深い読者は、リスト11.44のコードは本質的に次のコードと同等であることに気付くかもしれません。
def feed
microposts
end
上のコードを使用せずにあえてリスト11.44のコードを利用したのは、第12章で必要となる完全なステータスフィードで応用が効くためです。
サンプルアプリケーションでフィードを使うために、カレントユーザーのページ分割されたフィードに@feed_items
インスタンス変数を追加し (リスト11.45)、次にフィード用のパーシャル (リスト11.46) をHomeページに追加します。Homeページに変更を加えた結果はリスト11.47で示します。このとき、ユーザーがログインしているかどうかを調べる後置if文が変化している点に注目してください。すなわち、リスト11.45では、次のコードが
@micropost = current_user.microposts.build if logged_in?
リスト11.38のときとは異なり、
if logged_in?
@micropost = current_user.microposts.build
@feed_items = current_user.feed.paginate(page: params[:page])
end
といった前置if文に変わっています (訳注: 1行のときは後置if文、2行以上のときは前置if文を使うのがRubyの習慣です)。
home
アクションにフィードのインスタンス変数を追加する app/controllers/static_pages_controller.rb
class StaticPagesController < ApplicationController
def home
if logged_in?
@micropost = current_user.microposts.build
@feed_items = current_user.feed.paginate(page: params[:page])
end
end
def help
end
def about
end
def contact
end
end
<% if @feed_items.any? %>
<ol class="microposts">
<%= render @feed_items %>
</ol>
<%= will_paginate @feed_items %>
<% end %>
ステータスフィードのパーシャルは、Micropostのパーシャル (リスト11.21) とは異なっている点に注目してください。
<%= render @feed_items %>
このとき、@feed_items
の各要素がMicropost
クラスを持っていたため、RailsはMicropostのパーシャルの呼び出すことができました。このように、Railsは対応する名前のパーシャルを、与えられたリソースのディレクトリ内から探しにいくことができます。
app/views/microposts/_micropost.html.erb
後は、いつものようにフィードパーシャルを表示すればHomeページにフィードを追加できます (リスト11.47)。この結果はHomeページのフィードとして表示されます (図11.14)。
<% if logged_in? %>
<div class="row">
<aside class="col-md-4">
<section class="user_info">
<%= render 'shared/user_info' %>
</section>
<section class="micropost_form">
<%= render 'shared/micropost_form' %>
</section>
</aside>
<div class="col-md-8">
<h3>Micropost Feed</h3>
<%= render 'shared/feed' %>
</div>
</div>
<% else %>
.
.
.
<% end %>
現時点では、新しいマイクロポストの作成は図11.15で示したように期待どおりに動作しています。ただしささいなことではありますが、マイクロポストの投稿が失敗すると、 Homeページは@feed_items
インスタンス変数を期待しているため、現状では壊れてしまいます。最も簡単な解決方法は、リスト11.48のように空の配列を渡しておくことです。残念ですが、この場合はページ分割されたフィードを返してもうまく動きません。動かない理由を確認したい方は、実際に実装してページネーションのリンクをクリックしてみてください。
create
アクションに空の@feed_items
インスタンス変数を追加する app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
before_action :logged_in_user, only: [:create, :destroy]
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
def destroy
end
private
def micropost_params
params.require(:micropost).permit(:content)
end
end
11.3.4 マイクロポストを削除する
最後の機能として、マイクロポストリソースにポストを削除する機能を追加します。これはユーザー削除と同様に(9.4.2)、"delete" リンクで実現します (図11.16)。ユーザーの削除は管理者ユーザーのみが行えるように制限されていたのに対し、今回の場合はカレントユーザーが作成したマイクロポストに対してのみ削除リンクが動作するようにします。
最初のステップとして、マイクロポストのパーシャル (リスト11.21) に削除リンクを追加します。変更の結果をリスト11.49に示します。
<li id="<%= micropost.id %>">
<%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
<span class="user"><%= link_to micropost.user.name, micropost.user %></span>
<span class="content"><%= micropost.content %></span>
<span class="timestamp">
Posted <%= time_ago_in_words(micropost.created_at) %> ago.
<% if current_user?(micropost.user) %>
<%= link_to "delete", micropost, method: :delete,
data: { confirm: "You sure?" } %>
<% end %>
</span>
</li>
次に、Micropostsコントローラのdestroy
アクションを定義しましょう。これも、ユーザーにおける実装 (リスト9.54) と大体同じです。大きな違いは、admin_user
フィルターで@user
変数を使うのではなく、関連付けを使ってマイクロポストを見つけるようにしている点です。これにより、あるユーザーが他のユーザーのマイクロポストを削除しようとすると、自動的に失敗するようになります。具体的には、correct_user
フィルター内でfind
メソッドを呼び出すことで、カレントユーザーが削除対象のマイクロポストを保有しているかどうかを確認します。変更した結果はリスト11.50のようになります。
destroy
アクション app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
before_action :logged_in_user, only: [:create, :destroy]
before_action :correct_user, only: :destroy
.
.
.
def destroy
@micropost.destroy
flash[:success] = "Micropost deleted"
redirect_to request.referrer || root_url
end
private
def micropost_params
params.require(:micropost).permit(:content)
end
def correct_user
@micropost = current_user.microposts.find_by(id: params[:id])
redirect_to root_url if @micropost.nil?
end
end
このとき、 リスト11.50のdestroy
メソッドではリダイレクトを使っている点に注目してください。
request.referrer || root_url
ここではrequest.referrer
というメソッドを使っています11。このメソッドはフレンドリーフォワーディングのrequest.url
変数 (9.2.3) と似ていて、一つ前のURLを返します12。このため、マイクロポストがHomeページから削除された場合でもProfileページから削除された場合でも、request.referrer
を使うことでDELETEリクエストが発行されたページに戻すことができるので、非常に便利です。ちなみに、元に戻すURLが見つからなかった場合でも (例えばテストではnil
が返ってくることもあります)、リスト11.50の||
演算子でroot_url
をデフォルトに設定しているため、大丈夫です。(リスト8.50で定義したデフォルトオプションと比較してみてください。)
これらのコードにより、上から2番目のマイクロポストを削除すると、図 11.17のようにうまく動くはずです。
11.3.5 フィード画面におけるマイクロポストのテスト
11.3.4のコードで、Micropostモデルとそのインターフェースが完成しました。残っている箇所は、Micropostsコントローラの認可をチェックする短いテストと、それらをまとめる統合テストを書くことです。
まずはマイクロポスト用のfixtureに、別々のユーザーに紐付けられたマイクロポストを追加していきます (リスト11.51)。(今はこのうちの1つしか使いませんが、あとで他のマイクロポストも利用していきます。)
.
.
.
ants:
content: "Oh, is that what you want? Because that's how you get ants!"
created_at: <%= 2.years.ago %>
user: archer
zone:
content: "Danger zone!"
created_at: <%= 3.days.ago %>
user: archer
tone:
content: "I'm sorry. Your words made sense, but your sarcastic tone did not."
created_at: <%= 10.minutes.ago %>
user: lana
van:
content: "Dude, this van's, like, rolling probable cause."
created_at: <%= 4.hours.ago %>
user: lana
次に、自分以外のユーザーのマイクロポストは削除をしようとすると、適切にリダイレクトされることをテストで確認します (リスト11.52)。
require 'test_helper'
class MicropostsControllerTest < ActionController::TestCase
def setup
@micropost = microposts(:orange)
end
test "should redirect create when not logged in" do
assert_no_difference 'Micropost.count' do
post :create, micropost: { content: "Lorem ipsum" }
end
assert_redirected_to login_url
end
test "should redirect destroy when not logged in" do
assert_no_difference 'Micropost.count' do
delete :destroy, id: @micropost
end
assert_redirected_to login_url
end
test "should redirect destroy for wrong micropost" do
log_in_as(users(:michael))
micropost = microposts(:ants)
assert_no_difference 'Micropost.count' do
delete :destroy, id: micropost
end
assert_redirected_to root_url
end
end
最後に、統合テストを書きます。今回の統合テストでは、ログイン、マイクロポストのページ分割の確認、無効なマイクロポストを投稿、有効なマイクロポストを投稿、マイクロポストの削除、そして他のユーザーのマイクロポストには [delete] リンクが表示されないことを確認、といった順でテストしていきます。いつものように、統合テストを生成するところから始めましょう。
$ rails generate integration_test microposts_interface
invoke test_unit
create test/integration/microposts_interface_test.rb
先ほどの順で書いた統合テストは、リスト11.53のようになります。リスト11.11で書いたコードと、先ほどのステップが結合されている点に注意してください。(リスト11.53では、post_via_redirect
をpost
とfollow_redirect!
に分割している点にも注目してください。これは、画像アップロードのテストを演習 (リスト11.69) でやるための準備です。)
require 'test_helper'
class MicropostsInterfaceTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
test "micropost interface" do
log_in_as(@user)
get root_path
assert_select 'div.pagination'
# 無効な送信
assert_no_difference 'Micropost.count' do
post microposts_path, micropost: { content: "" }
end
assert_select 'div#error_explanation'
# 有効な送信
content = "This micropost really ties the room together"
assert_difference 'Micropost.count', 1 do
post microposts_path, micropost: { content: content }
end
assert_redirected_to root_url
follow_redirect!
assert_match content, response.body
# 投稿を削除する
assert_select 'a', text: 'delete'
first_micropost = @user.microposts.paginate(page: 1).first
assert_difference 'Micropost.count', -1 do
delete micropost_path(first_micropost)
end
# 違うユーザーのプロフィールにアクセスする
get user_path(users(:archer))
assert_select 'a', text: 'delete', count: 0
end
end
既にアプリケーション側のコードは実装してあるので、このテストは成功するはずです。
$ bundle exec rake test
11.4 マイクロポストの画像投稿
ここまででマイクロポストに関する基本的な操作はすべて実装できました。この節では、応用編として画像付きマイクロポストを投稿できるようにしてみます。手順としては、まずは開発環境用のβ版を実装し、その後、いくつかの改善をとおして本番環境用の完成版を実装します。
画像アップロード機能を追加するためには、2つの視覚的な要素が必要です。1つは画像をアップロードするためのフォーム、もう1つは投稿された画像そのものです。[Upload image] ボタンと画像付きマイクロポストのモックアップを図11.18に示します13。
11.4.1 基本的な画像アップロード
投稿した画像を扱ったり、その画像をMicropostモデルと関連付けするために、今回はCarrierWaveという画像アップローダーを使います。まずはcarrierwave gemをGemfile
に追加しましょう (リスト11.55)。またリスト11.55では、あとで必要になるmini_magick gemとfog gemsも含めている点に注目してください。これらのgemは画像をリサイズしたり (11.4.3)、本番環境で画像をアップロードする (11.4.4) ために使います。
Gemfile
にCarrierWaveを追加する
source 'https://rubygems.org'
gem 'rails', '4.2.2'
gem 'bcrypt', '3.1.7'
gem 'faker', '1.4.2'
gem 'carrierwave', '0.10.0'
gem 'mini_magick', '3.8.0'
gem 'fog', '1.36.0'
gem 'will_paginate', '3.0.7'
gem 'bootstrap-will_paginate', '0.0.10'
.
.
.
次に、いつものようにインストールします。
$ bundle install
CarrierWaveを導入すると、Railsのジェネレーターで画像アップローダーが生成できるようになります。早速、次のコマンドを実行してみましょう (画像のことをimageとすると一般的過ぎるので、今回はpicture
と呼ぶことにします) 14。
$ rails generate uploader Picture
CarrierWaveでアップロードされた画像は、Active Recordモデルの属性と関連付けされているべきです。関連付けされる属性には画像のファイル名が格納されるため、String型にしておきます。拡張したマイクロポストのデータモデルを、図11.19に示します。
必要となるpicture
属性をMicropostモデルに追加するために、マイグレーションファイルを生成し、開発環境のデータベースに適用します。
$ rails generate migration add_picture_to_microposts picture:string
$ bundle exec rake db:migrate
CarrierWaveに画像と関連付けたモデルを伝えるためには、mount_uploader
というメソッドを使います。このメソッドは、引数に属性名のシンボルと生成されたアップローダーのクラス名を取ります。
mount_uploader :picture, PictureUploader
(picture_uploader.rb
というファイルでPictureUploader
クラスが定義されています。11.4.2で修正しますが、今はデフォルトのままで大丈夫です。) Micropostモデルにアップローダーを追加した結果をリスト11.56に示します。
class Micropost < ActiveRecord::Base
belongs_to :user
default_scope -> { order(created_at: :desc) }
mount_uploader :picture, PictureUploader
validates :user_id, presence: true
validates :content, presence: true, length: { maximum: 140 }
end
システムによっては、ここで一旦Railsサーバーを再起動させる必要があります。再起動させたらテストスイートを走らせてみてください。成功しているはずです。(ただし、3.7.3で説明したGuardを使っている場合は、再起動させるだけではうまく動かないかもしれません。その場合はターミナルから一旦抜けて、新しいターミナルでGuardを再実行してみてください。)
図 11.18のようにHomeページ上にアップローダーを追加するためには、マイクロポストのフォームにfile_field
タグを含める必要があります (リスト11.57)。
<%= form_for(@micropost, html: { multipart: true }) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<div class="field">
<%= f.text_area :content, placeholder: "Compose new micropost..." %>
</div>
<%= f.submit "Post", class: "btn btn-primary" %>
<span class="picture">
<%= f.file_field :picture %>
</span>
<% end %>
このとき、
html: { multipart: true }
form_for
の引数に上のオプションが追加されていることに注目してください。これはファイルをアップロードする際に必要となるオプションです。
最後に、Webから更新できる許可リストにpicture
属性を追加しましょう。追加すると、micropost_params
メソッドはリスト11.58のようになります。
picture
を許可された属性のリストに追加する app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
before_action :logged_in_user, only: [:create, :destroy]
before_action :correct_user, only: :destroy
.
.
.
private
def micropost_params
params.require(:micropost).permit(:content, :picture)
end
def correct_user
@micropost = current_user.microposts.find_by(id: params[:id])
redirect_to root_url if @micropost.nil?
end
end
一度画像がアップロードされれば、Micropostパーシャルのimage_tag
ヘルパーでその画像を描画できるようになります (リスト11.59)。また、画像の無い (テキストのみの) マイクロポストでは画像を表示させないようにするために、picture?
という論理値を返すメソッドを使っている点に注目してください。このメソッドは、画像用の属性名に応じて、CarrierWaveが自動的に生成してくれるメソッドです。手動で画像付きの投稿をしてみると、図 11.20のようになります。画像アップロードに対するテストは、演習に回します (11.6)。
<li id="micropost-<%= micropost.id %>">
<%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
<span class="user"><%= link_to micropost.user.name, micropost.user %></span>
<span class="content">
<%= micropost.content %>
<%= image_tag micropost.picture.url if micropost.picture? %>
</span>
<span class="timestamp">
Posted <%= time_ago_in_words(micropost.created_at) %> ago.
<% if current_user?(micropost.user) %>
<%= link_to "delete", micropost, method: :delete,
data: { confirm: "You sure?" } %>
<% end %>
</span>
</li>
11.4.2 画像の検証
11.4.1のアップローダーも悪くはありませんが、いくつかの目立つ欠点があります。例えば、アップロードされた画像に対する制限がないため、もしユーザーが巨大なファイルを上げたり、無効なファイルを上げると問題が発生してしまいます。この欠点を直すために、画像サイズやフォーマットに対するバリデーションを実装し、サーバー用とクライアント (ブラウザ) 用の両方に追加しましょう。
最初のバリデーションでは、有効な画像の種類を制限していきますが、これはCarrierWaveのアップローダーの中に既にヒントがあります。生成されたアップローダーの中にコメントアウトされたコードがありますが、ここのコメントアウトを取り消すことで、画像のファイル名から有効な拡張子 (PNG/GIF/JPEGなど) を検証することができます (リスト11.60)。
class PictureUploader < CarrierWave::Uploader::Base
storage :file
# アップロードファイルの保存先ディレクトリは上書き可能
# 下記はデフォルトの保存先
def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
# アップロード可能な拡張子のリスト
def extension_white_list
%w(jpg jpeg gif png)
end
end
2つ目のバリデーションでは、画像のサイズを制御します。これはMicropostモデルに書き足していきます。先ほどのバリデーションとは異なり、ファイルサイズに対するバリデーションはRailsの既存のオプション (presenceやlengthなど) にはありません。したがって、今回は手動でpicture_size
という独自のバリデーションを定義します。結果はリスト11.61のとおりです。独自のバリデーションを定義するために、今まで使っていたvalidates
メソッドではなく、validate
メソッドを使っている点に注目してください。
class Micropost < ActiveRecord::Base
belongs_to :user
default_scope -> { order(created_at: :desc) }
mount_uploader :picture, PictureUploader
validates :user_id, presence: true
validates :content, presence: true, length: { maximum: 140 }
validate :picture_size
private
# アップロード画像のサイズを検証する
def picture_size
if picture.size > 5.megabytes
errors.add(:picture, "should be less than 5MB")
end
end
end
このvalidateメソッドでは、引数にシンボル (:picture_size
) を取り、そのシンボル名に対応したメソッドを呼び出します。また、呼び出されたpicture_size
メソッドでは、5MBを上限として (文法はコラム8.2を参照)、それを超えた場合はカスタマイズしたエラーメッセージをerrors
コレクションに追加しています (errorsについては6.2.2で紹介しました)。
リスト11.60やリスト11.61で定義した画像のバリデーションをビューに組み込むために、クライアント側に2つの処理を追加しましょう。まずはフォーマットのバリデーションを反映するためには、file_field
タグにaccept
パラメータを付与して使います。
<%= f.file_field :picture, accept: 'image/jpeg,image/gif,image/png' %>
このときacceptパラメータでは、リスト11.60で許可したファイル形式を、MIMEタイプで指定するようにします。
次に、大きすぎるファイルサイズに対して警告を出すために、ちょっとしたJavaScript (正確にはjQuery) を書き加えます。こうすることで、長すぎるアップロード時間を防いだり、サーバーへの負荷を抑えたりすることに繋がります。
$('#micropost_picture').bind('change', function() {
var size_in_megabytes = this.files[0].size/1024/1024;
if (size_in_megabytes > 5) {
alert('Maximum file size is 5MB. Please choose a smaller file.');
}
});
jQueryは本書のトピックではないので詳細な解説はしませんが、上のコードでは (ハッシュマーク#
から分かるように) CSS idのmicropost_picture
を含んだ要素を見つけ出し、この要素を監視しています。そしてこのidを持った要素とは、マイクロポストのフォームリスト11.57を指します (ブラウザ上で画面を右クリックし、インスペクターで要素を調べると確認できます)。つまり、このCSS idを持つ要素が変化したとき、このjQueryの関数が動き出します。そして、もしファイルサイズが大きすぎた場合、alert
メソッドで警告を出すといった仕組みです15。
これらの追加的なチェック機能をまとめると、リスト11.62のようになります。
<%= form_for(@micropost, html: { multipart: true }) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<div class="field">
<%= f.text_area :content, placeholder: "Compose new micropost..." %>
</div>
<%= f.submit "Post", class: "btn btn-primary" %>
<span class="picture">
<%= f.file_field :picture, accept: 'image/jpeg,image/gif,image/png' %>
</span>
<% end %>
<script type="text/javascript">
$('#micropost_picture').bind('change', function() {
var size_in_megabytes = this.files[0].size/1024/1024;
if (size_in_megabytes > 5) {
alert('Maximum file size is 5MB. Please choose a smaller file.');
}
});
</script>
ちなみに、リスト11.62のようなコードでは大きすぎるファイルのアップロードを完全には阻止できない、という点を覚えておいてください。というのも、このコードは送信フォームを使った投稿は制限できても、インスペクター画面でJavaScriptをいじって投稿したり、curl
などを使って直接POSTリクエストを送信する場合には制限できないからです。こういった場合にも対応できるようにするため、リスト11.61で実装したサーバー側のバリデーションも重要なのです。
11.4.3 画像のリサイズ
ファイルサイズに対するバリデーション (11.4.2) はうまくいきましたが、画像サイズ (縦横の長さ) に対する制限はないので、大きすぎる画像サイズがアップロードされると図11.21のようにレイアウトが壊れてしまいます。とはいえ、ユーザーに手元で画像サイズを変更させるのは不便です。なので、画像を表示させる前にサイズを変更する (リサイズする) ようにしてみましょう16。
画像をリサイズするためには、画像を操作するプログラムが必要になります。今回はImageMagickというプログラムを使うので、これを開発環境にインストールしておく必要になります (11.4.4でも説明しますが、本番環境がHerokuであれば、既に本番環境でImageMagickが使えるようになっています)。Cloud IDEでは、次のコマンドでこのプログラムをインストールできます17。
$ sudo apt-get update
$ sudo apt-get install imagemagick --fix-missing
次に、MiniMagickというgemを使って、CarrierWaveからImageMagickを使えるようにします。MiniMagickのドキュメント (英語) を見るとさまざまな方法でリサイズできることがわかりますが、今回はresize_to_limit: [400, 400]
という方法を使います。これは、縦横どちらかが400pxを超えていた場合、適切なサイズに縮小するオプションです (ただし小さい画像であっても拡大はしません)。ちなみにCarrierWaveのMiniMagickの項目を見ると、 小さすぎる画像を引き延ばすこともできるようですが、今回は使いません。したがって、最終的なコードはリスト11.63のようになります。これにより、大きな画像サイズでも適切にリサイズされるようになります (図11.22)。
class PictureUploader < CarrierWave::Uploader::Base
include CarrierWave::MiniMagick
process resize_to_limit: [400, 400]
storage :file
# アップロードファイルの保存先ディレクトリは上書き可能
# 下記はデフォルトの保存先
def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
# アップロード可能な拡張子のリスト
def extension_white_list
%w(jpg jpeg gif png)
end
end
11.4.4 本番環境での画像アップロード
(訳注: この項はスキップできます。もしうまくいかなければスキップしても大丈夫です) 11.4.3で実装した画像アップローダーは、開発環境で動かす分には問題ないのですが、本番環境には適していません。これはリスト11.63のstorage :file
という行によって、ローカルのファイルシステムに画像を保存するようになっているからです18 (訳注: ただしHerokuのファイルシステムは一時的にしか使え無いので、本番にデプロイするたびに画像が消えます)。本番環境では、ファイルシステムではなくクラウドストレージサービスに画像を保存するようにしてみましょう19。
本番環境でクラウドストレージに保存するためには、リスト11.64のようにfog gemを使うと簡単です。
class PictureUploader < CarrierWave::Uploader::Base
include CarrierWave::MiniMagick
process resize_to_limit: [400, 400]
if Rails.env.production?
storage :fog
else
storage :file
end
# アップロードファイルの保存先ディレクトリは上書き可能
# 下記はデフォルトの保存先
def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
# アップロード可能な拡張子のリスト
def extension_white_list
%w(jpg jpeg gif png)
end
end
リスト11.64ではproduction?
という論理値を返すメソッドを使っています。このメソッドはコラム7.1でも紹介しましたが、これを使うと環境毎に保存先を切り替えることができます。
if Rails.env.production?
storage :fog
else
storage :file
end
世の中には多くのクラウドストレージサービスがありますが、今回は有名で信頼性も高いアマゾンの「Simple Storage Service (S3)20」を使います。セットアップの手順は次のとおりです。
- Amazon Web Servicesアカウントにサインアップする
- AWS Identity and Access Management (IAM)でユーザーを作成し、AccessキーとSecretキーをメモする
- AWS ConsoleからS3 bucketを作成し (bucketの名前はなんでも大丈夫です)、2.で作成したユーザーに対してRead権限とWrite権限を付与する
より詳細について知りたい場合は、S3のドキュメント (英語) を読んだり、GoogleやStack Overflowで検索してみたりしてください21 。
S3アカウントの作成と設定が終わったら、CarrierWaveの設定ファイルを次のリスト11.65のように修正してください。
(訳注: fogでリージョンを指定する場合は、 :region => ENV['S3_REGION'] といったパラメータを渡し、heroku config:set S3_REGION="リージョン名" といったコマンドを実行することで設定できます。なお、東京のリージョン名は "ap-northeast-1" です。詳細はAmazon Web Serviceのリージョンとエンドポイントを参照してください。)
if Rails.env.production?
CarrierWave.configure do |config|
config.fog_credentials = {
# Amazon S3用の設定
:provider => 'AWS',
:region => ENV['S3_REGION'], # 例: 'ap-northeast-1'
:aws_access_key_id => ENV['S3_ACCESS_KEY'],
:aws_secret_access_key => ENV['S3_SECRET_KEY']
}
config.fog_directory = ENV['S3_BUCKET']
end
end
本番環境のメール設定 (リスト10.56) と同様に、リスト11.65ではHerokuの環境変数 ENV
を使って、機密情報が漏洩しないようにしています。10.3では、SendGridのアドオンがこれらの環境変数を自動的に設定してくれましたが、今回は手動で設定する必要があります。heroku config:set
コマンドを使って、次のようにHeroku上の環境変数を設定してください。
$ heroku config:set S3_ACCESS_KEY="ココに先ほどメモしたAccessキーを入力"
$ heroku config:set S3_SECRET_KEY="同様に、Secretキーを入力"
$ heroku config:set S3_BUCKET="Bucketの名前を入力"
$ heroku config:set S3_REGION="Regionの名前を入力"
設定が無事に終わったら、これまでの変更をコミットしたりデプロイする準備が整いました。ただし、その前に.gitignore
ファイルをリスト11.66のように更新しおきましょう。これにより、画像を保存するディレクトリがGitへの保存対象から除かれるので、アプリケーションと関係の無い画像ファイルなどが無視できるようになります。
.gitignore
ファイルに画像用ディレクトリを追加する
# See https://help.github.com/articles/ignoring-files for more about ignoring
# files.
#
# If you find yourself ignoring temporary files generated by your text editor
# or operating system, you probably want to add a global ignore instead:
# git config --global core.excludesfile '~/.gitignore_global'
# Ignore bundler config.
/.bundle
# Ignore the default SQLite database.
/db/*.sqlite3
/db/*.sqlite3-journal
# Ignore all logfiles and tempfiles.
/log/*.log
/tmp
# Ignore Spring files.
/spring/*.pid
# Ignore uploaded test images.
/public/uploads
それでは、これまでの変更をトピックブランチにコミットし、masterブランチにmergeしていきましょう。
$ bundle exec rake test
$ git add -A
$ git commit -m "Add user microposts"
$ git checkout master
$ git merge user-microposts
$ git push
次に、Herokuへのデプロイ、データベースのリセット、サンプルデータの生成を順に実行していきます。
$ git push heroku
$ heroku pg:reset DATABASE
$ heroku run rake db:migrate
$ heroku run rake db:seed
Herokuには既にImageMagickがインストールされているので、(設定がうまくいっていれば) 画像リサイズや本番での画像アップロードも成功します。次の図11.23のようになっていれば成功です。
11.5 最後に
Micropostsリソースの追加によって、サンプルアプリケーションはほぼ完成に近づきました。残すところは、ユーザーをお互いにフォローするソーシャルな仕組みのみとなります。第12章では、そのようなユーザー同士の関係 (リレーションシップ) をモデリングする方法を学び、それがマイクロポストのフィードにどのように関連するかを学びます。
もし11.4.4をスキップしていたら、ここで今までの変更のコミットとmergeを済ませてください。
$ bundle exec rake test
$ git add -A
$ git commit -m "Add user microposts"
$ git checkout master
$ git merge user-microposts
$ git push
準備ができたら、本番環境へデプロイしてみましょう。
$ git push heroku
$ heroku pg:reset DATABASE
$ heroku run rake db:migrate
$ heroku run rake db:seed
なお、必要なgemはここまでですべてインストールしたので、今後の章では新たなgemは追加しません。参考までに、最終状態のGemfile
をリスト11.67に示します。
Gemfile
(完成)
source 'https://rubygems.org'
gem 'rails', '4.2.2'
gem 'bcrypt', '3.1.7'
gem 'faker', '1.4.2'
gem 'carrierwave', '0.10.0'
gem 'mini_magick', '3.8.0'
gem 'fog', '1.26.0'
gem 'will_paginate', '3.0.7'
gem 'bootstrap-will_paginate', '0.0.10'
gem 'bootstrap-sass', '3.2.0.0'
gem 'sass-rails', '5.0.2'
gem 'uglifier', '2.5.3'
gem 'coffee-rails', '4.1.0'
gem 'jquery-rails', '4.0.3'
gem 'turbolinks', '2.3.0'
gem 'jbuilder', '2.2.3'
gem 'sdoc', '0.4.0', group: :doc
group :development, :test do
gem 'sqlite3', '1.3.9'
gem 'byebug', '3.4.0'
gem 'web-console', '2.0.0.beta3'
gem 'spring', '1.1.3'
end
group :test do
gem 'minitest-reporters', '1.0.5'
gem 'mini_backtrace', '0.1.3'
gem 'guard-minitest', '2.3.1'
end
group :production do
gem 'pg', '0.17.1'
gem 'rails_12factor', '0.0.2'
gem 'puma', '2.11.1'
end
11.5.1 本章のまとめ
- Active Recordモデルの力によって、マイクロポストも (ユーザーと同じで) リソースとして扱える
- Railsは複数のキーインデックスをサポートしている
- Userは複数のMicropostsを持っていて (
has_many
)、Micropostは1人のUserに依存している (belongs_to
) といった関係性をモデル化した -
has_many
やbelongs_to
を利用することで、関連付けを通して多くのメソッドが使えるようになった -
user.microposts.build(...)
というコードは、引数で与えたユーザーに関連付けされたマイクロポストを返す -
default_scope
を使うとデフォルトの順序を変更できる - default_scopeは引数に無名関数 (->) を取る
-
dependent: :destroy
オプションを使うと、関連付けされたオブジェクトが削除されると同時に、自分自身も削除する - paginateメソッドやcountメソッドは、どちらも関連付けを通して実行され、効率的にデータベースに問い合わせしている
- fixtureは、関連付けを使ったオブジェクトの作成もサポートとしている
- パーシャルを呼び出すときに、一緒に変数を渡すことができる
-
where
メソッドを使うと、Active Recordを通して選択 (部分集合を取り出すこと) ができる - 依存しているオブジェクトを作成/削除するときは、常に関連付けを通すようにすることで、よりセキュアな操作が実現できる
- CarrierWaveを使うと画像アップロードや画像リサイズができる
11.6 演習
注: 『演習の解答マニュアル (英語)』にはRuby on Railsチュートリアルのすべての演習の解答が掲載されており、www.railstutorial.orgで原著を購入いただいた方には無料で配布しています (訳注: 解答は英語です)。
なお、演習とチュートリアル本編の食い違いを避ける方法については、演習用のトピックブランチに追加したメモ (3.6) を参考にしてください。
-
if
-else
文の2つの分岐に対して、それぞれ異なるパーシャルを使用するようにHomeページをリファクタリングしてください。 - サイドバーにあるマイクロポストの合計投稿数をテストしてください。このとき、単数形 (micropost) と複数形 (microposts) が正しく表示されているかどうかもテストしてください。(リスト11.68を参考にしてみてください)
-
リスト11.69に示すテンプレートを参考に、11.4で実装した画像アップローダーをテストしてください。テストの準備として、まずはサンプル画像をfixtureディレクトリに追加してください (コマンド例: "
cp app/assets/images/rails.png
test/fixtures/
")。紛らわしいエラーを回避するためには、CarrierWaveの設定を変更し、テスト環境では画像リサイズをしないようにする必要があるので、リスト11.70に示す設定ファイルを使ってください。リスト11.69で追加したテストでは、マイクロポストの投稿フォームやpicture属性をいじって、無効な送信や有効な送信をチェックしています。なお、テスト内にあるfixture_file_upload
というメソッドは、fixtureで定義されたファイルをアップロードする特別なメソッドです22。ヒント:picture
属性が有効かどうかを確かめるときは、10.1.4で紹介したassigns
メソッドを使ってください。このメソッドを使うと、 投稿に成功した後にcreate
アクション内のマイクロポストにアクセスするようになります。
require 'test_helper'
class MicropostInterfaceTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
.
.
.
test "micropost sidebar count" do
log_in_as(@user)
get root_path
assert_match "#{FILL_IN} microposts", response.body
# まだマイクロポストを投稿していないユーザー
other_user = users(:mallory)
log_in_as(other_user)
get root_path
assert_match "0 microposts", response.body
other_user.microposts.create!(content: "A micropost")
get root_path
assert_match FILL_IN, response.body
end
end
require 'test_helper'
class MicropostInterfaceTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
test "micropost interface" do
log_in_as(@user)
get root_path
assert_select 'div.pagination'
assert_select 'input[type=FILL_IN]'
# 無効な送信
post microposts_path, micropost: { content: "" }
assert_select 'div#error_explanation'
# 有効な送信
content = "This micropost really ties the room together"
picture = fixture_file_upload('test/fixtures/rails.png', 'image/png')
assert_difference 'Micropost.count', 1 do
post microposts_path, micropost: { content: content, picture: FILL_IN }
end
assert FILL_IN.picture?
follow_redirect!
assert_match content, response.body
# 投稿を削除する
assert_select 'a', 'delete'
first_micropost = @user.microposts.paginate(page: 1).first
assert_difference 'Micropost.count', -1 do
delete micropost_path(first_micropost)
end
# 違うユーザーのプロフィールにアクセスする
get user_path(users(:archer))
assert_select 'a', { text: 'delete', count: 0 }
end
.
.
.
end
if Rails.env.test?
CarrierWave.configure do |config|
config.enable_processing = false
end
end
- この名前はTwitterのマイクロブログという説明分から着想を得ました。ブログにはポストがあるので、マイクロブログがあればマイクロポストもある、といった具合です。↑
- http://www.postgresql.org/docs/9.1/static/datatype-character.html ↑
- 外部参照キーは、データベースレベルでの制約です。これによって、Micropostsテーブルのuser_idは、Usersテーブルのidカラムを参照するようになります。本チュートリアルでこの詳細が重要になることはありません。また、この外部キーによる制約は、すべてのデータベースで使えるわけではありません (たとえばHerokuのPostgreSQLではサポートされていますが、開発用のSQLiteではサポートされていません)。外部キーの詳細は12.1.2で学びます。↑
- ユーザー一覧を実装するときも (9.5)、似たような問題にぶつかりました。↑
- SQLは大文字小文字を区別しませんが、慣習的にSQLのキーワード (
DESC
など) は大文字で書くことになっています。↑ -
(
Faker::Lorem.sentence
は、いわゆるlorem ipsumダミーテキストを返します。第6章で述べたように、lorem ipsumには面白い裏話があります)。↑ - Faker gemのlorem ipsumサンプルテキストはランダムに生成される仕様になっているため、サンプルマイクロポストの内容はこの図と違っているはずです。↑
- 便宜上、リスト11.25はこの章で必要なCSSをすべて含んでいます。↑
- もし
full_title
ヘルパーを使って他のテストもリファクタリングしたくなったら (例えばリスト3.38など)、test_helper.rb
からApplicationヘルパーをインクルードしてください。↑ -
where
メソッドや他の関連するメソッドの詳細については、RailsガイドのActive Record クエリインターフェイスを読んでください。↑ - これは、HTTPの仕様として定義されているHTTP_REFERERと対応しています。 ちなみに “referer” は誤字ではありません。仕様では確かにこの(間違った)スペルを使っているのです。一方、Railsは “referrer” という正しいスペルで使っています。↑
- 私もRailsがどうやってこのURLを取得しているのか、パッと思い出すことはできませんでした。そこで、Googleで “rails request previous url” と検索し、Stack Overflowのスレッドを見つけ、この答えに至りました。↑
- ビーチの写真は https://www.flickr.com/photos/grungepunk/14026922186 から引用しました。↑
- 最初は
image
という属性名を使っていたのですが、この名前だと一般的すぎて、逆に混乱を招いてしまいました。↑ - この手のトピックを学ぶには「Googleで “javascript maximum file size”といった関連するキーワードで検索し、Stack Overflowが見つかるまで (検索ワードを調整しながら) 繰り返す」、これが一番です。↑
- 他の解決策としてCSSで表示サイズを調整する方法もありますが、これだとファイルサイズが変わりません。結果として、ファイルサイズの大きな画像によって、読み込み時間が長くなるといった問題が発生します。たとえば "小さい" 画像を表示するだけなのに、やたらに読み込み時間が長いウェブサイトに訪れたことはありませんか。これがその原因です。↑
-
Ubuntuの公式ドキュメント (英語) でこれを見つけました。もしCloud IDEやLinuxライクなシステム以外で開発しているのであれば、Google で “imagemagick <あなたのプラットフォーム名>” と検索してください。なお、OS Xであれば
brew install imagemagick
でインストールできます (Homebrewがインストールされていなければインストールしてください)。↑ - 特に、Herokuのファイルストレージは一時的なので、アップロードした画像はデプロイする度に削除される仕様になっています (訳注: とはいえ、アプリケーションの動作を本番環境で確認するだけであれば、Herokuのファイルストレージのままでも問題はありません)。↑
- この節の内容は必須ではありませんので、スキップしても問題ありません。↑
- S3は課金サービスですが、Railsチュートリアルのサンプルアプリケーションをセットアップしたりテストするだけであれば、毎月1円ほどしか課金されません。↑
- http://aws.amazon.com/documentation/s3/ ↑
- Windows上で開発している場合は (Cloud IDEで開発してれば大丈夫です)、次のように
:binary
パラメーターを追加してください。fixture_file_upload(file, type, :binary)
↑
Railsチュートリアルは YassLab 社によって運営されています。
コンテンツを継続的に提供するため、書籍・動画・質問対応サービスなどもご検討していただけると嬉しいです。
研修支援や教材連携にも対応しています。note マガジンや YouTube チャンネルも始めたので、よければぜひ遊びに来てください!