Ruby on Rails チュートリアル

実例を使ってRailsを学ぼう

Michael Hartl (マイケル・ハートル)

第2版 目次

  1. 第1章 ゼロからデプロイまで
    1. 1.1はじめに
      1. 1.1.1読者の皆さまへ
      2. 1.1.2 Railsとスケールについて
      3. 1.1.3この本における取り決め
    2. 1.2 さっそく動作させる
      1. 1.2.1開発環境
        1. IDE
        2. テキストエディタとコマンドライン
        3. ブラウザ
        4. 使用するツールについて
      2. 1.2.2Ruby、RubyGems、Rails、Git
        1. Railsインストーラ (Windows)
        2. Gitのインストール
        3. Rubyのインストール
        4. RubyGemsのインストール
        5. Railsのインストール
      3. 1.2.3最初のアプリケーション
      4. 1.2.4 Bundler
      5. 1.2.5 rails server
      6. 1.2.6Model-view-controller (MVC)
    3. 1.3Gitによるバージョン管理
      1. 1.3.1インストールとセットアップ
        1. 初めてのシステムセットアップ
        2. 初めてのリポジトリセットアップ
      2. 1.3.2追加とコミット
      3. 1.3.3Gitのメリット
      4. 1.3.4 GitHub
      5. 1.3.5ブランチ (branch)、変更 (edit)、 コミット (commit)、マージ (merge)
        1. ブランチ (branch)
        2. 変更 (edit)
        3. コミット (commit)
        4. マージ (merge)
        5. プッシュ (push)
    4. 1.4デプロイする
      1. 1.4.1 Herokuのセットアップ
      2. 1.4.2 Herokuにデプロイする (1)
      3. 1.4.3 Herokuにデプロイする (2)
      4. 1.4.4 Heroku コマンド
    5. 1.5 最後に
  2. 第2章デモアプリケーション
    1. 2.1 アプリの計画
      1. 2.1.1ユーザーのモデル設計
      2. 2.1.2マイクロポストのモデル設計
    2. 2.2Users リソース
      1. 2.2.1ユーザーページを探検する
      2. 2.2.2 MVCの挙動
      3. 2.2.3Users リソースの欠点
    3. 2.3Microposts リソース
      1. 2.3.1マイクロポストのページを探検する
      2. 2.3.2マイクロポストをマイクロにする
      3. 2.3.3ユーザーとマイクロポストをhas_manyで関連づける
      4. 2.3.4継承の階層
      5. 2.3.5デモアプリケーションのデプロイ
    4. 2.4最後に
  3. 第3章ほぼ静的なページの作成
    1. 3.1静的ページ
    2. 3.2最初のテスト
      1. 3.2.1テスト駆動開発
      2. 3.2.2ページの追加
        1. 赤 (Red)
        2. 緑 (Green)
        3. リファクタリング
    3. 3.3少しだけ動的なページ
      1. 3.3.1タイトル変更をテストする
      2. 3.3.2タイトルのテストをパスさせる
      3. 3.3.3埋め込みRuby
      4. 3.3.4レイアウトを使って重複を解消する
    4. 3.4最後に
    5. 3.5演習
    6. 3.6高度なセットアップ
      1. 3.6.1bundle execを追放する
        1. RVM Bundler の統合
        2. binstubsオプション
      2. 3.6.2Guardによるテストの自動化
      3. 3.6.3Spork を使ったテストの高速化
        1. GuardにSporkを導入する
      4. 3.6.4Sublime Text上でテストする
  4. 第4章 Rails風味のRuby
    1. 4.1動機
    2. 4.2文字列(string)とメソッド
      1. 4.2.1コメント
      2. 4.2.2文字列
        1. 出力
        2. シングルクォート内の文字列
      3. 4.2.3オブジェクトとメッセージ受け渡し
      4. 4.2.4メソッドの定義
      5. 4.2.5 title ヘルパー、再び
    3. 4.3他のデータ構造
      1. 4.3.1配列と範囲演算子
      2. 4.3.2ブロック
      3. 4.3.3ハッシュとシンボル
      4. 4.3.4 CSS、再び
    4. 4.4 Ruby におけるクラス
      1. 4.4.1コンストラクタ
      2. 4.4.2クラス継承
      3. 4.4.3組込みクラスの変更
      4. 4.4.4コントローラクラス
      5. 4.4.5ユーザークラス
    5. 4.5最後に
    6. 4.6演習
  5. 第5章レイアウトを作成する
    1. 5.1構造を追加する
      1. 5.1.1ナビゲーション
      2. 5.1.2BootstrapとカスタムCSS
      3. 5.1.3パーシャル (partial)
    2. 5.2SassとAsset Pipeline
      1. 5.2.1Asset Pipeline
        1. アセットディレクトリ
        2. マニフェストファイル
        3. プリプロセッサエンジン
        4. 本番環境での効率性
      2. 5.2.2素晴らしい構文を備えたスタイルシート
        1. ネスト
        2. 変数
    3. 5.3レイアウトのリンク
      1. 5.3.1 ルートのテスト
      2. 5.3.2 Railsのルート
      3. 5.3.3名前付きルート
      4. 5.3.4RSpecを洗練させる
    4. 5.4ユーザー登録: 最初のステップ
      1. 5.4.1Usersコントローラ
      2. 5.4.2 ユーザー登録用URL
    5. 5.5最後に
    6. 5.6演習
  6. 第6章ユーザーのモデルを作成する
    1. 6.1 Userモデル
      1. 6.1.1データベースの移行
      2. 6.1.2modelファイル
      3. 6.1.3ユーザーオブジェクトを作成する
      4. 6.1.4ユーザーオブジェクトを検索する
      5. 6.1.5ユーザーオブジェクトを更新する
    2. 6.2ユーザーを検証する
      1. 6.2.1最初のユーザーテスト
      2. 6.2.2プレゼンスを検証する
      3. 6.2.3長さを検証する
      4. 6.2.4フォーマットを検証する
      5. 6.2.5一意性を検証する
        1. 一意性の警告
    3. 6.3セキュアなパスワードを追加する
      1. 6.3.1暗号化されたパスワード
      2. 6.3.2パスワードと確認
      3. 6.3.3ユーザー認証
      4. 6.3.4ユーザーがセキュアなパスワードを持っている
      5. 6.3.5ユーザーを作成する
    4. 6.4最後に
    5. 6.5演習
  7. 第7章ユーザー登録
    1. 7.1ユーザーを表示する
      1. 7.1.1デバッグとRails環境
      2. 7.1.2ユーザーリソース
      3. 7.1.3ファクトリーを使用してユーザー表示ページをテストする
      4. 7.1.4gravatar画像とサイドバー
    2. 7.2ユーザー登録フォーム
      1. 7.2.1ユーザー登録のためのテスト
      2. 7.2.2form_forを使用する
      3. 7.2.3フォームHTML
    3. 7.3ユーザー登録失敗
      1. 7.3.1正しいフォーム
      2. 7.3.2 Strong Parameters
      3. 7.3.3ユーザー登録のエラーメッセージ
    4. 7.4ユーザー登録成功
      1. 7.4.1登録フォームの完成
      2. 7.4.2flash
      3. 7.4.3実際のユーザー登録
      4. 7.4.4 SSLを導入して本番環境をデプロイする
    5. 7.5最後に
    6. 7.6演習
  8. 第8章サインイン、サインアウト
    1. 8.1セッション、サインインの失敗
      1. 8.1.1Sessionコントローラ
      2. 8.1.2サインインをテストする
      3. 8.1.3サインインのフォーム
      4. 8.1.4確認フォームを送信する
      5. 8.1.5フラッシュメッセージを表示する
    2. 8.2サインイン成功
      1. 8.2.1[このアカウント設定を保存する]
      2. 8.2.2正しいsign_inメソッド
      3. 8.2.3現在のユーザー
      4. 8.2.4レイアウトリンクを変更する
      5. 8.2.5ユーザー登録と同時にサインインする
      6. 8.2.6サインアウトする
    3. 8.3Cucumberの紹介 (オプション)
      1. 8.3.1インストールと設定
      2. 8.3.2フィーチャーとステップ
      3. 8.3.3対比: RSpecのカスタムマッチャー
    4. 8.4最後に
    5. 8.5演習
  9. 第9章 ユーザーの更新・表示・削除
    1. 9.1ユーザーを更新する
      1. 9.1.1編集フォーム
      2. 9.1.2編集の失敗
      3. 9.1.3編集の成功
    2. 9.2認可
      1. 9.2.1ユーザーのサインインを要求する
      2. 9.2.2正しいユーザーを要求する
      3. 9.2.3フレンドリーフォワーディング
    3. 9.3すべてのユーザーを表示する
      1. 9.3.1ユーザーインデックス
      2. 9.3.2サンプルのユーザー
      3. 9.3.3ページネーション
      4. 9.3.4パーシャルのリファクタリング
    4. 9.4ユーザーを削除する
      1. 9.4.1管理ユーザー
        1. Strong Parameters、再び
      2. 9.4.2 destroyアクション
    5. 9.5最後に
    6. 9.6演習
  10. 第10章ユーザーのマイクロポスト
    1. 10.1Micropostモデル
      1. 10.1.1基本的なモデル
      2. 10.1.2最初の検証
      3. 10.1.3User/Micropostの関連付け
      4. 10.1.4マイクロポストを改良する
        1. デフォルトのスコープ
        2. Dependent: destroy
      5. 10.1.5コンテンツの検証
    2. 10.2マイクロポストを表示する
      1. 10.2.1ユーザー表示ページの拡張
      2. 10.2.2マイクロポストのサンプル
    3. 10.3マイクロポストを操作する
      1. 10.3.1アクセス制御
      2. 10.3.2マイクロポストを作成する
      3. 10.3.3フィードの原型
      4. 10.3.4マイクロポストを削除する
    4. 10.4最後に
    5. 10.5演習
  11. 第11章ユーザーをフォローする
    1. 11.1Relationshipモデル
      1. 11.1.1データモデルの問題 (および解決策)
      2. 11.1.2User/relationshipの関連付け
      3. 11.1.3検証
      4. 11.1.4フォローしているユーザー
      5. 11.1.5フォロワー
    2. 11.2フォローしているユーザー用のWebインターフェイス
      1. 11.2.1フォローしているユーザーのサンプルデータ
      2. 11.2.2統計とフォロー用フォーム
      3. 11.2.3「フォローしているユーザー」ページと「フォロワー」ページ
      4. 11.2.4[フォローする] ボタン (標準的な方法)
      5. 11.2.5[フォローする] ボタン (Ajax)
    3. 11.3ステータスフィード
      1. 11.3.1動機と計画
      2. 11.3.2フィードを初めて実装する
      3. 11.3.3サブセレクト
      4. 11.3.4新しいステータスフィード
    4. 11.4最後に
      1. 11.4.1サンプルアプリケーションの機能を拡張する
        1. 返信機能
        2. メッセージ機能
        3. フォロワーの通知
        4. パスワードリマインダー
        5. ユーザー登録の確認
        6. RSSフィード
        7. REST API
        8. 検索機能
      2. 11.4.2読み物ガイド
    5. 11.5演習

前書き

私が前にいた会社 (CD Baby) は、かなり早い段階で Ruby on Rails に一度乗り換えたのですが、残念ながらまた PHP に戻ってしまいました (詳細は私の名前を Google で検索してみてください)。そんな私ですが、Michael Hartl 氏の本を強く勧められたので、その本を使ってもう一度試してみた結果、今度は無事に Rails に乗り換えることができました。それがこの Ruby on Rails チュートリアルという本です。

私は多くの Rails 関連の本を参考にしてきましたが、真の決定版と呼べるものは本書をおいて他にありません。本書では、あらゆる手順が Rails 流で行われています。最初のうちは慣れるまでに時間がかかりましたが、この本を終えた今、ついにこれこそが自然な方式だと感じられるまでになりました。また、本書は Rails 関連の本の中で唯一、多くのプロが推奨するテスト駆動開発 (TDD: Test Driven Development) を、全編を通して実践しています。実例を使ってここまで分かりやすく解説された本は、本書が初めてでしょう。極めつけは、Git や GitHub、Heroku の実例に含めている点です。このような、実際の開発現場で使わているツールもチュートリアルに含まれているため、読者は、まるで実際のプロジェクトの開発プロセスを体験しているかのような感覚が得られるはずです。それでいて、それぞれの実例が独立したセクションになっているのではなく、そのどれもがチュートリアルの内容と見事に一体化しています。

本書は、筋道だった一本道の物語のようになっています。私自身、章の終わりにある練習問題もやりながら、この Rails チュートリアルを3日間かけて一気に読破しました*1。最初から最後まで、途中を飛ばさずに読んでください。それが、最も有益な本書の読み方です。

それでは、楽しんでお読み下さい!

デレックシバーズ (Derek Sivers) (sivers.org) CD Baby 創始者

  1. 3日間で読破するのは異常です! 実際には3日以上かけて読むのが一般的です。

謝辞

Ruby on Rails チュートリアルは、私の以前の著書「RailsSpace」と、その時の共著者の Aurelius Prochazka から多くのことを参考にさせてもらっています。Aure には、RailsSpace での協力と本書への支援も含め、感謝したいと思います。また、RailsSpaceRails チュートリアルの両方の編集を担当して頂いた Debra Williams Cauley 氏にも謝意を表したく思います。彼女が野球の試合に連れて行ってくれる限り、私は本を書き続けるでしょう。

私にインスピレーションと知識を与えてくれた Rubyist の方々にも感謝したいと思います: David Heinemeier Hansson、 Yehuda Katz、 Carl Lerche、 Jeremy Kemper、 Xavier Noria、 Ryan Bates、 Geoffrey Grosenbach、 Peter Cooper、 Matt Aimonetti、 Gregg Pollack、 Wayne E. Seguin、 Amy Hoy、 Dave Chelimsky、 Pat Maddox、 Tom Preston-Werner、 Chris Wanstrath、 Chad Fowler、 Josh Susser、 Obie Fernandez、 Ian McFarland、 Steven Bristol、 Pratik Naik、 Sarah Mei、 Sarah Allen、 Wolfram Arnold、 Alex Chaffee、 Giles Bowkett、 Evan Dorn、 Long Nguyen、 James Lindenbaum、 Adam Wiggins、 Tikhon Bernstam、 Ron Evans、 Wyatt Greene、 Miles Forrest、 Pivotal Labs の方々、 Heroku の方々、 thoughtbot の方々、 そして GitHub の方々、ありがとうございました。最後に、ここに書ききれないほど多くの読者からバグ報告や提案を頂きました。彼ら/彼女らのおかげで、本書を可能な限り良い本に仕上げることが出来ました。

著者

マイケルハートル (Michael Hartl) は、「Ruby on Rails チュートリアル」という、Ruby on Rails を使って初めて Web アプリケーションを開発する際に最もよく参考にされる本の著者です。以前は、(今ではすっかり古くなってしまいましたが)「RailsSpace」という本の執筆および開発に携わったり、また、 一時人気を博した Ruby on Rails ベースのソーシャルネットワーキングプラットフォーム「Insoshi」の開発にも携わっていました。なお、2011年には、Rails コミュニティへの高い貢献が認められて、Ruby Hero Award を受賞しました。ハーバード大学卒業後、カリフォルニア工科大学物理学博士号を取得し、起業プログラム Y Combinator の卒業生でもあります。

著作権とライセンス

Ruby on Rails チュートリアル: 実例を使って Rails を学ぼう. Copyright © 2013 by Michael Hartl.

Ruby on Rails チュートリアル内の全てのソースコードは、MIT ライセンスおよび Beerware ライセンスの元で提供されています。(原文: All source code in the Ruby on Rails Tutorial is available jointly under the MIT License and the Beerware License.)

(訳注: "All source code" とはRailsチュートリアルの題材であるSNSのソースコードを指します。Railsチュートリアルという教材は上記ライセンスの元で提供されていないのでご注意ください)

(The MIT License)

Copyright (c) 2013 Michael Hartl

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR
A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING OUT OF
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
/*
 * ----------------------------------------------------------------------------
 * "THE BEER-WARE LICENSE" (Revision 42):
 * Michael Hartl wrote this code. As long as you retain this notice you
 * can do whatever you want with this stuff. If we meet some day,
 * and you think this stuff is worth it, you can buy me a beer in return.
 * ----------------------------------------------------------------------------
 */

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

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

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

$ git checkout -b user-microposts

10.1Micropostモデル

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

10.1.1基本的なモデル

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

$ rails generate model Micropost content:string user_id:integer

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

$ rm -f spec/factories/microposts.rb

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

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


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

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

add_index :microposts, [:user_id, :created_at]

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

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

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

describe Micropost do

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

  subject { @micropost }

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

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

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

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

micropost_model
図10.1Micropostデータモデル

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

$ bundle exec rspec spec/models/micropost_spec.rb

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

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

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

10.1.2 最初の検証

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

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

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

describe Micropost do

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

  subject { @micropost }

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

  it { should be_valid }

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

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

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

10.1.3User/Micropostの関連付け

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

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

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

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

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

Micropost.create
Micropost.create!
Micropost.new

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

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

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

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

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

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

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

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

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

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

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

describe Micropost do

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

  subject { @micropost }

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

  it { should be_valid }

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

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

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

describe User do

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

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

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

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

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

$ bundle exec rspec spec/models

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

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

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

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

    factory :admin do
      admin true
    end
  end

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

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

factory :micropost do
  content "Lorem ipsum"
  user
end

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

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

デフォルトのスコープ

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

-> { order('created_at DESC') }

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

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

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

Dependent: destroy

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

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

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

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

expect(microposts).not_to be_empty

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

has_many :microposts, dependent: :destroy

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

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

$ bundle exec rspec spec/

10.1.5コンテンツの検証

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

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

describe Micropost do

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

before { visit user_path(user) }

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

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

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

    before { visit user_path(user) }

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

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

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

user.microposts.count

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

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

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

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

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

<%= will_paginate @microposts %>

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

<%= will_paginate %>

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

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

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

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

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

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

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

<%= render @users %>

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

<%= render @microposts %>

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

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

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

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

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

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

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

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

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

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

users = User.all(limit: 6)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

10.3.1アクセス制御

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

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

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

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

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

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

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

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

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

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

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

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

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

  def create
  end

  def destroy
  end
end

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

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

  def index
  end

  def create
  end

  def destroy
  end
end

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

$ bundle exec rspec spec/requests/authentication_pages_spec.rb

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

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

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

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

$ rails generate integration_test micropost_pages

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

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

describe "Micropost pages" do

  subject { page }

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

  describe "micropost creation" do
    before { visit root_path }

    describe "with invalid information" do

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

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

    describe "with valid information" do

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

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

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

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

  def destroy
  end

  private

    def micropost_params
      params.require(:micropost).permit(:content)
    end
end

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

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

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

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

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

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

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

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

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

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

リスト10.30 マイクロポスト作成フォームのパーシャル。
app/views/shared/_micropost_form.html.erb
<%= form_for(@micropost) do |f| %>
  <%= render 'shared/error_messages', object: f.object %>
  <div class="field">
    <%= f.text_area :content, placeholder: "Compose new micropost..." %>
  </div>
  <%= f.submit "Post", class: "btn btn-large btn-primary" %>
<% end %>

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

@micropost = current_user.microposts.build

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

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

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

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

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

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

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

form_for(@user) do |f|

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

form_for(@micropost) do |f|

f.object@micropostとなります。

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

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

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

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

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

$ bundle exec rspec spec/requests/micropost_pages_spec.rb

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

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

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

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

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

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

$ bundle exec rspec spec/

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

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

10.3.3フィードの原型

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

def feed
  microposts
end

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

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

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

describe "Static pages" do

  subject { page }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

$ bundle exec rspec spec/

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

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

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

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

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

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

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

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

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

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

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

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

  private

    def micropost_params
      params.require(:micropost).permit(:content)
    end

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

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

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

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

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

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

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

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

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

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

$ bundle exec rspec spec/

10.4最後に

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

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

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

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

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

10.5演習

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

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

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

  private

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